diff --git a/build/NuSpecs/UmbracoCms.Core.nuspec b/build/NuSpecs/UmbracoCms.Core.nuspec index 4abb26377b..ce837f7189 100644 --- a/build/NuSpecs/UmbracoCms.Core.nuspec +++ b/build/NuSpecs/UmbracoCms.Core.nuspec @@ -44,37 +44,37 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/UmbracoVersion.txt b/build/UmbracoVersion.txt index 0294012380..f8d3c5e1c7 100644 --- a/build/UmbracoVersion.txt +++ b/build/UmbracoVersion.txt @@ -1,3 +1,3 @@ # Usage: on line 2 put the release version, on line 3 put the version comment (example: beta) 7.6.0 -alpha056 \ No newline at end of file +alpha064 \ No newline at end of file diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 7fc240f4f0..24866498b8 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -12,4 +12,4 @@ using System.Resources; [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyFileVersion("7.6.0")] -[assembly: AssemblyInformationalVersion("7.6.0-alpha056")] \ No newline at end of file +[assembly: AssemblyInformationalVersion("7.6.0-alpha064")] \ No newline at end of file diff --git a/src/Umbraco.Core/CodeAnnotations/UmbracoUdiTypeAttribute.cs b/src/Umbraco.Core/CodeAnnotations/UmbracoUdiTypeAttribute.cs new file mode 100644 index 0000000000..0bb9de6c86 --- /dev/null +++ b/src/Umbraco.Core/CodeAnnotations/UmbracoUdiTypeAttribute.cs @@ -0,0 +1,15 @@ +using System; + +namespace Umbraco.Core.CodeAnnotations +{ + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] + internal class UmbracoUdiTypeAttribute : Attribute + { + public string UdiType { get; private set; } + + public UmbracoUdiTypeAttribute(string udiType) + { + UdiType = udiType; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs index aed7f61a06..05189acc3e 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs @@ -159,6 +159,12 @@ namespace Umbraco.Core.Configuration.UmbracoSettings get { return GetOptionalTextElement("defaultDocumentTypeProperty", "Textstring"); } } + [ConfigurationProperty("showDeprecatedPropertyEditors")] + internal InnerTextConfigurationElement ShowDeprecatedPropertyEditors + { + get { return GetOptionalTextElement("showDeprecatedPropertyEditors", false); } + } + [ConfigurationProperty("EnableInheritedDocumentTypes")] internal InnerTextConfigurationElement EnableInheritedDocumentTypes { @@ -306,6 +312,11 @@ namespace Umbraco.Core.Configuration.UmbracoSettings get { return DefaultDocumentTypeProperty; } } + bool IContentSection.ShowDeprecatedPropertyEditors + { + get { return ShowDeprecatedPropertyEditors; } + } + bool IContentSection.EnableInheritedDocumentTypes { get { return EnableInheritedDocumentTypes; } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs index 3d5e4435b6..a388126948 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs @@ -60,6 +60,12 @@ namespace Umbraco.Core.Configuration.UmbracoSettings string DefaultDocumentTypeProperty { get; } + /// + /// The default for this is false but if you would like deprecated property editors displayed + /// in the data type editor you can enable this + /// + bool ShowDeprecatedPropertyEditors { get; } + bool EnableInheritedDocumentTypes { get; } bool EnableInheritedMediaTypes { get; } diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index a741f65e4e..32d96a46b3 100644 --- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs @@ -24,7 +24,7 @@ namespace Umbraco.Core.Configuration /// Gets the version comment (like beta or RC). /// /// The version comment. - public static string CurrentComment { get { return "alpha056"; } } + public static string CurrentComment { get { return "alpha064"; } } // Get the version of the umbraco.dll by looking at a class in that dll // Had to do it like this due to medium trust issues, see: http://haacked.com/archive/2010/11/04/assembly-location-and-medium-trust.aspx diff --git a/src/Umbraco.Core/Constants-ObjectTypes.cs b/src/Umbraco.Core/Constants-ObjectTypes.cs index 7bba427f12..d364c1379c 100644 --- a/src/Umbraco.Core/Constants-ObjectTypes.cs +++ b/src/Umbraco.Core/Constants-ObjectTypes.cs @@ -174,24 +174,24 @@ namespace Umbraco.Core public static readonly Guid FormsFormGuid = new Guid(FormsForm); /// - /// Guid for a Forms Workflow. + /// Guid for a Forms PreValue Source. /// - public const string FormsWorkflow = "42D7BF9B-A362-4FEE-B45A-674D5C064B70"; + public const string FormsPreValue = "42D7BF9B-A362-4FEE-B45A-674D5C064B70"; /// - /// Guid for a Forms Workflow. + /// Guid for a Forms PreValue Source. /// - public static readonly Guid FormsWorkflowGuid = new Guid(FormsWorkflow); + public static readonly Guid FormsPreValueGuid = new Guid(FormsPreValue); /// - /// Guid for a Forms Record. + /// Guid for a Forms DataSource. /// - public const string FormsRecord = "CFED6CE4-9359-443E-9977-9956FEB1D867"; + public const string FormsDataSource = "CFED6CE4-9359-443E-9977-9956FEB1D867"; /// - /// Guid for a Forms Record. + /// Guid for a Forms DataSource. /// - public static readonly Guid FormsRecordGuid = new Guid(FormsRecord); + public static readonly Guid FormsDataSourceGuid = new Guid(FormsDataSource); } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs index 80f118b58e..189d11d3a1 100644 --- a/src/Umbraco.Core/Constants-PropertyEditors.cs +++ b/src/Umbraco.Core/Constants-PropertyEditors.cs @@ -42,10 +42,14 @@ namespace Umbraco.Core [Obsolete("GUIDs are no longer used to reference Property Editors, use the Alias constant instead. This will be removed in future versions")] public const string ContentPicker = "158AA029-24ED-4948-939E-C3DA209E5FBA"; + + [Obsolete("This is an obsoleted content picker, use ContentPicker2Alias instead")] + public const string ContentPickerAlias = "Umbraco.ContentPickerAlias"; + /// /// Alias for the Content Picker datatype. /// - public const string ContentPickerAlias = "Umbraco.ContentPickerAlias"; + public const string ContentPicker2Alias = "Umbraco.ContentPicker2"; /// /// Guid for the Date datatype. @@ -192,11 +196,15 @@ namespace Umbraco.Core [Obsolete("GUIDs are no longer used to reference Property Editors, use the Alias constant instead. This will be removed in future versions")] public const string MediaPicker = "EAD69342-F06D-4253-83AC-28000225583B"; + [Obsolete("This is an obsoleted picker, use MediaPicker2Alias instead")] + public const string MediaPickerAlias = "Umbraco.MediaPicker"; + /// /// Alias for the Media Picker datatype. /// - public const string MediaPickerAlias = "Umbraco.MediaPicker"; + public const string MediaPicker2Alias = "Umbraco.MediaPicker2"; + [Obsolete("This is an obsoleted picker, use MediaPicker2Alias instead")] public const string MultipleMediaPickerAlias = "Umbraco.MultipleMediaPicker"; /// @@ -205,26 +213,32 @@ namespace Umbraco.Core [Obsolete("GUIDs are no longer used to reference Property Editors, use the Alias constant instead. This will be removed in future versions")] public const string MemberPicker = "39F533E4-0551-4505-A64B-E0425C5CE775"; + [Obsolete("This is an obsoleted picker, use MemberPicker2Alias instead")] + public const string MemberPickerAlias = "Umbraco.MemberPicker"; + /// /// Alias for the Member Picker datatype. /// - public const string MemberPickerAlias = "Umbraco.MemberPicker"; + public const string MemberPicker2Alias = "Umbraco.MemberPicker2"; /// /// Alias for the Member Group Picker datatype. /// public const string MemberGroupPickerAlias = "Umbraco.MemberGroupPicker"; - + /// /// Guid for the Multi-Node Tree Picker datatype /// [Obsolete("GUIDs are no longer used to reference Property Editors, use the Alias constant instead. This will be removed in future versions")] public const string MultiNodeTreePicker = "7E062C13-7C41-4AD9-B389-41D88AEEF87C"; + [Obsolete("This is an obsoleted picker, use MultiNodeTreePicker2Alias instead")] + public const string MultiNodeTreePickerAlias = "Umbraco.MultiNodeTreePicker"; + /// /// Alias for the Multi-Node Tree Picker datatype /// - public const string MultiNodeTreePickerAlias = "Umbraco.MultiNodeTreePicker"; + public const string MultiNodeTreePicker2Alias = "Umbraco.MultiNodeTreePicker2"; /// /// Guid for the Multiple Textstring datatype. diff --git a/src/Umbraco.Core/Events/DeleteEventArgs.cs b/src/Umbraco.Core/Events/DeleteEventArgs.cs index 8a0fdaf290..df13363b95 100644 --- a/src/Umbraco.Core/Events/DeleteEventArgs.cs +++ b/src/Umbraco.Core/Events/DeleteEventArgs.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; namespace Umbraco.Core.Events { - public class DeleteEventArgs : CancellableObjectEventArgs>, IEquatable> + public class DeleteEventArgs : CancellableObjectEventArgs>, IEquatable>, IDeletingMediaFilesEventArgs { /// /// Constructor accepting multiple entities that are used in the delete operation diff --git a/src/Umbraco.Core/Events/EventsDispatchMode.cs b/src/Umbraco.Core/Events/EventsDispatchMode.cs deleted file mode 100644 index 23fe013af9..0000000000 --- a/src/Umbraco.Core/Events/EventsDispatchMode.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace Umbraco.Core.Events -{ - public enum EventsDispatchMode - { - // in 7.5 we'd do: - // - // using (var uow = ...) - // { ... } - // Done.RaiseEvent(...); - // - // and so the event would trigger only *after* the transaction has completed, - // so actually PassThrough is more aggressive than what we had in 7.5 and should - // not be used. now in 7.6 we do: - // - // using (var uow = ...) - // { - // ... - // uow.Events.Dispatch(Done, ...); - // } - // - // so the event can be collected, so the default "kinda compatible" more has to be - // the Scope mode, and Passive is for Deploy only - - Unspecified = 0, - PassThrough, // both Doing and Done trigger immediately - Scope, // Doing triggers immediately, Done queued and triggered when & if the scope completes - Passive // Doing never triggers, Done queued and needs to be handled by custom code - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Events/IDeletingMediaFilesEventArgs.cs b/src/Umbraco.Core/Events/IDeletingMediaFilesEventArgs.cs new file mode 100644 index 0000000000..45681042ba --- /dev/null +++ b/src/Umbraco.Core/Events/IDeletingMediaFilesEventArgs.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Events +{ + internal interface IDeletingMediaFilesEventArgs + { + List MediaFilesToDelete { get; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Events/ImportPackageEventArgs.cs b/src/Umbraco.Core/Events/ImportPackageEventArgs.cs index 506481a79e..231e58c07e 100644 --- a/src/Umbraco.Core/Events/ImportPackageEventArgs.cs +++ b/src/Umbraco.Core/Events/ImportPackageEventArgs.cs @@ -23,6 +23,11 @@ namespace Umbraco.Core.Events { get { return _packageMetaData; } } + + public IEnumerable InstallationSummary + { + get { return EventObject; } + } public bool Equals(ImportPackageEventArgs other) { diff --git a/src/Umbraco.Core/Events/RecycleBinEventArgs.cs b/src/Umbraco.Core/Events/RecycleBinEventArgs.cs index c6049cb7a6..c6d7c659b7 100644 --- a/src/Umbraco.Core/Events/RecycleBinEventArgs.cs +++ b/src/Umbraco.Core/Events/RecycleBinEventArgs.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Models; namespace Umbraco.Core.Events { - public class RecycleBinEventArgs : CancellableEventArgs, IEquatable + public class RecycleBinEventArgs : CancellableEventArgs, IEquatable, IDeletingMediaFilesEventArgs { public RecycleBinEventArgs(Guid nodeObjectType, Dictionary> allPropertyData, bool emptiedSuccessfully) : base(false) @@ -97,6 +97,8 @@ namespace Umbraco.Core.Events /// public List Files { get; private set; } + public List MediaFilesToDelete { get { return Files; } } + /// /// Gets the list of all property data associated with a content id /// diff --git a/src/Umbraco.Core/Events/ScopeEventDispatcher.cs b/src/Umbraco.Core/Events/ScopeEventDispatcher.cs index 6eb6ee3b85..93315a9946 100644 --- a/src/Umbraco.Core/Events/ScopeEventDispatcher.cs +++ b/src/Umbraco.Core/Events/ScopeEventDispatcher.cs @@ -1,126 +1,48 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; +using Umbraco.Core.IO; namespace Umbraco.Core.Events -{ - +{ /// /// This event manager is created for each scope and is aware of if it is nested in an outer scope /// /// /// The outer scope is the only scope that can raise events, the inner scope's will defer to the outer scope /// - internal class ScopeEventDispatcher : IEventDispatcher + internal class ScopeEventDispatcher : ScopeEventDispatcherBase { - private readonly EventsDispatchMode _mode; - private List _events; + public ScopeEventDispatcher() + : base(true) + { } - public ScopeEventDispatcher(EventsDispatchMode mode) - { - _mode = mode; - } - - private List Events { get { return _events ?? (_events = new List()); } } - - private bool PassThroughCancelable { get { return _mode == EventsDispatchMode.PassThrough || _mode == EventsDispatchMode.Scope; } } - - private bool PassThrough { get { return _mode == EventsDispatchMode.PassThrough; } } - - private bool RaiseEvents { get { return _mode == EventsDispatchMode.Scope; } } - - public bool DispatchCancelable(EventHandler eventHandler, object sender, CancellableEventArgs args, string eventName = null) - { - if (eventHandler == null) return args.Cancel; - if (PassThroughCancelable == false) return args.Cancel; - eventHandler(sender, args); - return args.Cancel; - } - - public bool DispatchCancelable(EventHandler eventHandler, object sender, TArgs args, string eventName = null) - where TArgs : CancellableEventArgs - { - if (eventHandler == null) return args.Cancel; - if (PassThroughCancelable == false) return args.Cancel; - eventHandler(sender, args); - return args.Cancel; - } - - public bool DispatchCancelable(TypedEventHandler eventHandler, TSender sender, TArgs args, string eventName = null) - where TArgs : CancellableEventArgs - { - if (eventHandler == null) return args.Cancel; - if (PassThroughCancelable == false) return args.Cancel; - eventHandler(sender, args); - return args.Cancel; - } - - public void Dispatch(EventHandler eventHandler, object sender, EventArgs args, string eventName = null) - { - if (eventHandler == null) return; - if (PassThrough) - eventHandler(sender, args); - else - Events.Add(new EventDefinition(eventHandler, sender, args, eventName)); - } - - public void Dispatch(EventHandler eventHandler, object sender, TArgs args, string eventName = null) - { - if (eventHandler == null) return; - if (PassThrough) - eventHandler(sender, args); - else - Events.Add(new EventDefinition(eventHandler, sender, args, eventName)); - } - - public void Dispatch(TypedEventHandler eventHandler, TSender sender, TArgs args, string eventName = null) - { - if (eventHandler == null) return; - if (PassThrough) - eventHandler(sender, args); - else - Events.Add(new EventDefinition(eventHandler, sender, args, eventName)); - } - - public IEnumerable GetEvents(EventDefinitionFilter filter) - { - if (_events == null) - return Enumerable.Empty(); - - switch (filter) - { - case EventDefinitionFilter.All: - return _events; - case EventDefinitionFilter.FirstIn: - var l1 = new OrderedHashSet(); - foreach (var e in _events) - { - l1.Add(e); - } - return l1; - case EventDefinitionFilter.LastIn: - var l2 = new OrderedHashSet(keepOldest:false); - foreach (var e in _events) - { - l2.Add(e); - } - return l2; - default: - throw new ArgumentOutOfRangeException("filter", filter, null); - } - } - - public void ScopeExit(bool completed) + protected override void ScopeExitCompleted() { // fixme - we'd need to de-duplicate events somehow, etc - and the deduplication should be last in wins - if (_events == null) return; + foreach (var e in GetEvents(EventDefinitionFilter.All)) + { + e.RaiseEvent(); - if (RaiseEvents && completed) - foreach (var e in _events) - e.RaiseEvent(); - _events.Clear(); + // fixme - not sure I like doing it here - but then where? how? + var delete = e.Args as IDeletingMediaFilesEventArgs; + if (delete != null && delete.MediaFilesToDelete.Count > 0) + MediaFileSystem.DeleteMediaFiles(delete.MediaFilesToDelete); + } + } + + private MediaFileSystem _mediaFileSystem; + + private MediaFileSystem MediaFileSystem + { + get + { + if (_mediaFileSystem != null) return _mediaFileSystem; + + // fixme - insane! reading config goes cross AppDomain and serializes context? + using (new SafeCallContext()) + { + return _mediaFileSystem = FileSystemProviderManager.Current.MediaFileSystem; + } + } } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/ScopeEventDispatcherBase.cs b/src/Umbraco.Core/Events/ScopeEventDispatcherBase.cs new file mode 100644 index 0000000000..d8462d18b3 --- /dev/null +++ b/src/Umbraco.Core/Events/ScopeEventDispatcherBase.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Umbraco.Core.Events +{ + public abstract class ScopeEventDispatcherBase : IEventDispatcher + { + private List _events; + private readonly bool _raiseCancelable; + + protected ScopeEventDispatcherBase(bool raiseCancelable) + { + _raiseCancelable = raiseCancelable; + } + + private List Events { get { return _events ?? (_events = new List()); } } + + public bool DispatchCancelable(EventHandler eventHandler, object sender, CancellableEventArgs args, string eventName = null) + { + if (eventHandler == null) return args.Cancel; + if (_raiseCancelable == false) return args.Cancel; + eventHandler(sender, args); + return args.Cancel; + } + + public bool DispatchCancelable(EventHandler eventHandler, object sender, TArgs args, string eventName = null) + where TArgs : CancellableEventArgs + { + if (eventHandler == null) return args.Cancel; + if (_raiseCancelable == false) return args.Cancel; + eventHandler(sender, args); + return args.Cancel; + } + + public bool DispatchCancelable(TypedEventHandler eventHandler, TSender sender, TArgs args, string eventName = null) + where TArgs : CancellableEventArgs + { + if (eventHandler == null) return args.Cancel; + if (_raiseCancelable == false) return args.Cancel; + eventHandler(sender, args); + return args.Cancel; + } + + public void Dispatch(EventHandler eventHandler, object sender, EventArgs args, string eventName = null) + { + if (eventHandler == null) return; + Events.Add(new EventDefinition(eventHandler, sender, args, eventName)); + } + + public void Dispatch(EventHandler eventHandler, object sender, TArgs args, string eventName = null) + { + if (eventHandler == null) return; + Events.Add(new EventDefinition(eventHandler, sender, args, eventName)); + } + + public void Dispatch(TypedEventHandler eventHandler, TSender sender, TArgs args, string eventName = null) + { + if (eventHandler == null) return; + Events.Add(new EventDefinition(eventHandler, sender, args, eventName)); + } + + public IEnumerable GetEvents(EventDefinitionFilter filter) + { + if (_events == null) + return Enumerable.Empty(); + + switch (filter) + { + case EventDefinitionFilter.All: + return _events; + case EventDefinitionFilter.FirstIn: + var l1 = new OrderedHashSet(); + foreach (var e in _events) + { + l1.Add(e); + } + return l1; + case EventDefinitionFilter.LastIn: + var l2 = new OrderedHashSet(keepOldest: false); + foreach (var e in _events) + { + l2.Add(e); + } + return l2; + default: + throw new ArgumentOutOfRangeException("filter", filter, null); + } + } + + public void ScopeExit(bool completed) + { + if (_events == null) return; + if (completed) + ScopeExitCompleted(); + _events.Clear(); + } + + protected abstract void ScopeExitCompleted(); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Events/UninstallPackageEventArgs.cs b/src/Umbraco.Core/Events/UninstallPackageEventArgs.cs new file mode 100644 index 0000000000..324867a8f7 --- /dev/null +++ b/src/Umbraco.Core/Events/UninstallPackageEventArgs.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using Umbraco.Core.Packaging.Models; + +namespace Umbraco.Core.Events +{ + internal class UninstallPackageEventArgs : CancellableObjectEventArgs> + { + private readonly MetaData _packageMetaData; + + public UninstallPackageEventArgs(TEntity eventObject, bool canCancel) + : base(new[] { eventObject }, canCancel) + { + } + + public UninstallPackageEventArgs(TEntity eventObject, MetaData packageMetaData) + : base(new[] { eventObject }) + { + _packageMetaData = packageMetaData; + } + + public MetaData PackageMetaData + { + get { return _packageMetaData; } + } + + public IEnumerable UninstallationSummary + { + get { return EventObject; } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/IO/FileSystemWrapper.cs b/src/Umbraco.Core/IO/FileSystemWrapper.cs index 27e08330ed..97b8a2f8f6 100644 --- a/src/Umbraco.Core/IO/FileSystemWrapper.cs +++ b/src/Umbraco.Core/IO/FileSystemWrapper.cs @@ -109,5 +109,24 @@ namespace Umbraco.Core.IO var wrapped2 = Wrapped as IFileSystem2; return wrapped2 == null ? Wrapped.GetSize(path) : wrapped2.GetSize(path); } + + // explicitely implementing - not breaking + bool IFileSystem2.CanAddPhysical + { + get + { + var wrapped2 = Wrapped as IFileSystem2; + return wrapped2 != null && wrapped2.CanAddPhysical; + } + } + + // explicitely implementing - not breaking + void IFileSystem2.AddFile(string path, string physicalPath, bool overrideIfExists, bool copy) + { + var wrapped2 = Wrapped as IFileSystem2; + if (wrapped2 == null) + throw new NotSupportedException(); + wrapped2.AddFile(path, physicalPath, overrideIfExists, copy); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/IO/IFileSystem.cs b/src/Umbraco.Core/IO/IFileSystem.cs index 003b4891f5..e3e0f9e2d2 100644 --- a/src/Umbraco.Core/IO/IFileSystem.cs +++ b/src/Umbraco.Core/IO/IFileSystem.cs @@ -44,6 +44,10 @@ namespace Umbraco.Core.IO { long GetSize(string path); + bool CanAddPhysical { get; } + + void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false); + // TODO: implement these // //void CreateDirectory(string path); diff --git a/src/Umbraco.Core/IO/IOHelper.cs b/src/Umbraco.Core/IO/IOHelper.cs index 286acf0285..2ee0463435 100644 --- a/src/Umbraco.Core/IO/IOHelper.cs +++ b/src/Umbraco.Core/IO/IOHelper.cs @@ -4,11 +4,11 @@ using System.Globalization; using System.Reflection; using System.IO; using System.Configuration; +using System.Linq; using System.Web; using System.Text.RegularExpressions; using System.Web.Hosting; using Umbraco.Core.Configuration; -using Umbraco.Core.Logging; namespace Umbraco.Core.IO { @@ -351,7 +351,54 @@ namespace Umbraco.Core.IO writer.Write(contents); } } - - } + + } + + /// + /// Checks if a given path is a full path including drive letter + /// + /// + /// + // From: http://stackoverflow.com/a/35046453/5018 + internal static bool IsFullPath(this string path) + { + return string.IsNullOrWhiteSpace(path) == false + && path.IndexOfAny(Path.GetInvalidPathChars().ToArray()) == -1 + && Path.IsPathRooted(path) + && Path.GetPathRoot(path).Equals(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal) == false; + } + + /// + /// Get properly formatted relative path from an existing absolute or relative path + /// + /// + /// + internal static string GetRelativePath(this string path) + { + if (path.IsFullPath()) + { + var rootDirectory = GetRootDirectorySafe(); + var relativePath = path.ToLowerInvariant().Replace(rootDirectory.ToLowerInvariant(), string.Empty); + path = relativePath; + } + + return path.EnsurePathIsApplicationRootPrefixed(); + } + + /// + /// Ensures that a path has `~/` as prefix + /// + /// + /// + internal static string EnsurePathIsApplicationRootPrefixed(this string path) + { + if (path.StartsWith("~/")) + return path; + if (path.StartsWith("/") == false && path.StartsWith("\\") == false) + path = string.Format("/{0}", path); + if (path.StartsWith("~") == false) + path = string.Format("~{0}", path); + return path; + } } } diff --git a/src/Umbraco.Core/IO/MediaFileSystem.cs b/src/Umbraco.Core/IO/MediaFileSystem.cs index 6f32ef6da0..d9281a7590 100644 --- a/src/Umbraco.Core/IO/MediaFileSystem.cs +++ b/src/Umbraco.Core/IO/MediaFileSystem.cs @@ -7,6 +7,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Threading; +using System.Threading.Tasks; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; @@ -24,6 +25,7 @@ namespace Umbraco.Core.IO { private readonly IContentSection _contentConfig; private readonly UploadAutoFillProperties _uploadAutoFillProperties; + private readonly ILogger _logger; private readonly object _folderCounterLock = new object(); private long _folderCounter; @@ -42,6 +44,7 @@ namespace Umbraco.Core.IO public MediaFileSystem(IFileSystem wrapped, IContentSection contentConfig, ILogger logger) : base(wrapped) { + _logger = logger; _contentConfig = contentConfig; _uploadAutoFillProperties = new UploadAutoFillProperties(this, logger, contentConfig); } @@ -99,7 +102,7 @@ namespace Umbraco.Core.IO } else { - // new scheme: path is "-/" OR "--" + // new scheme: path is "/" where xuid is a combination of cuid and puid // default media filesystem maps to "~/media/" // assumes that cuid and puid keys can be trusted - and that a single property type // for a single content cannot store two different files with the same name @@ -435,6 +438,64 @@ namespace Umbraco.Core.IO } } + public void DeleteMediaFiles(IEnumerable files) + { + files = files.Distinct(); + + Parallel.ForEach(files, file => + { + try + { + if (file.IsNullOrWhiteSpace()) return; + + if (FileExists(file) == false) return; + DeleteFile(file, true); + + if (UseTheNewMediaPathScheme == false) + { + // old scheme: filepath is "/" OR "-" + // remove the directory if any + var dir = Path.GetDirectoryName(file); + if (string.IsNullOrWhiteSpace(dir) == false) + DeleteDirectory(dir, true); + } + else + { + // new scheme: path is "/" where xuid is a combination of cuid and puid + // remove the directory + var dir = Path.GetDirectoryName(file); + DeleteDirectory(dir, true); + } + + // I don't even understand... + /* + + var relativeFilePath = GetRelativePath(file); // fixme - should be relative already + if (FileExists(relativeFilePath) == false) return; + + var parentDirectory = Path.GetDirectoryName(relativeFilePath); + + // don't want to delete the media folder if not using directories. + if (_contentSection.UploadAllowDirectories && parentDirectory != GetRelativePath("/")) + { + //issue U4-771: if there is a parent directory the recursive parameter should be true + DeleteDirectory(parentDirectory, string.IsNullOrEmpty(parentDirectory) == false); + } + else + { + DeleteFile(file, true); + } + + */ + } + catch (Exception e) + { + _logger.Error("Failed to delete attached file \"" + file + "\".", e); + } + }); + } + + #endregion #region GenerateThumbnails diff --git a/src/Umbraco.Core/IO/PhysicalFileSystem.cs b/src/Umbraco.Core/IO/PhysicalFileSystem.cs index 33e4dc71e3..bf18d02e54 100644 --- a/src/Umbraco.Core/IO/PhysicalFileSystem.cs +++ b/src/Umbraco.Core/IO/PhysicalFileSystem.cs @@ -364,6 +364,29 @@ namespace Umbraco.Core.IO return file.Exists ? file.Length : -1; } + public bool CanAddPhysical { get { return true; } } + + public void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false) + { + var fullPath = GetFullPath(path); + + if (File.Exists(fullPath)) + { + if (overrideIfExists == false) + throw new InvalidOperationException(string.Format("A file at path '{0}' already exists", path)); + File.Delete(fullPath); + } + + var directory = Path.GetDirectoryName(fullPath); + if (directory == null) throw new InvalidOperationException("Could not get directory."); + Directory.CreateDirectory(directory); // ensure it exists + + if (copy) + File.Copy(physicalPath, fullPath); + else + File.Move(physicalPath, fullPath); + } + #region Helper Methods protected virtual void EnsureDirectory(string path) diff --git a/src/Umbraco.Core/IO/ShadowFileSystem.cs b/src/Umbraco.Core/IO/ShadowFileSystem.cs index bce8b8861b..cb2f4e346c 100644 --- a/src/Umbraco.Core/IO/ShadowFileSystem.cs +++ b/src/Umbraco.Core/IO/ShadowFileSystem.cs @@ -34,8 +34,16 @@ namespace Umbraco.Core.IO { try { - using (var stream = _sfs.OpenFile(kvp.Key)) - _fs.AddFile(kvp.Key, stream, true); + var fs2 = _fs as IFileSystem2; + if (fs2 != null && fs2.CanAddPhysical) + { + fs2.AddFile(kvp.Key, _sfs.GetFullPath(kvp.Key)); // overwrite, move + } + else + { + using (var stream = _sfs.OpenFile(kvp.Key)) + _fs.AddFile(kvp.Key, stream, true); + } } catch (Exception e) { @@ -288,6 +296,37 @@ namespace Umbraco.Core.IO return _sfs.GetSize(path); } + public bool CanAddPhysical { get { return true; } } + + public void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false) + { + ShadowNode sf; + var normPath = NormPath(path); + if (Nodes.TryGetValue(normPath, out sf) && sf.IsExist && (sf.IsDir || overrideIfExists == false)) + throw new InvalidOperationException(string.Format("A file at path '{0}' already exists", path)); + + var parts = normPath.Split('/'); + for (var i = 0; i < parts.Length - 1; i++) + { + var dirPath = string.Join("/", parts.Take(i + 1)); + ShadowNode sd; + if (Nodes.TryGetValue(dirPath, out sd)) + { + if (sd.IsFile) throw new InvalidOperationException("Invalid path."); + if (sd.IsDelete) Nodes[dirPath] = new ShadowNode(false, true); + } + else + { + if (_fs.DirectoryExists(dirPath)) continue; + if (_fs.FileExists(dirPath)) throw new InvalidOperationException("Invalid path."); + Nodes[dirPath] = new ShadowNode(false, true); + } + } + + _sfs.AddFile(path, physicalPath, overrideIfExists, copy); + Nodes[normPath] = new ShadowNode(false, false); + } + /// /// Helper function for filtering keys by Regex if a filter is specified. /// diff --git a/src/Umbraco.Core/IO/ShadowWrapper.cs b/src/Umbraco.Core/IO/ShadowWrapper.cs index 7c8bd55830..503791226f 100644 --- a/src/Umbraco.Core/IO/ShadowWrapper.cs +++ b/src/Umbraco.Core/IO/ShadowWrapper.cs @@ -164,5 +164,22 @@ namespace Umbraco.Core.IO var filesystem2 = filesystem as IFileSystem2; return filesystem2 == null ? filesystem.GetSize(path) : filesystem2.GetSize(path); } + + public bool CanAddPhysical + { + get + { + var fileSystem2 = FileSystem as IFileSystem2; + return fileSystem2 != null && fileSystem2.CanAddPhysical; + } + } + + public void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false) + { + var fileSystem2 = FileSystem as IFileSystem2; + if (fileSystem2 == null) + throw new NotSupportedException(); + fileSystem2.AddFile(path, physicalPath, overrideIfExists, copy); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs index 0dc95a8987..fab34e5f17 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs @@ -13,6 +13,7 @@ namespace Umbraco.Core.Models.Identity public override void ConfigureMappings(IConfiguration config, ApplicationContext applicationContext) { config.CreateMap() + .ForMember(user => user.LastLoginDateUtc, expression => expression.MapFrom(user => user.LastLoginDate.ToUniversalTime())) .ForMember(user => user.Email, expression => expression.MapFrom(user => user.Email)) .ForMember(user => user.Id, expression => expression.MapFrom(user => user.Id)) .ForMember(user => user.LockoutEndDateUtc, expression => expression.MapFrom(user => user.IsLockedOut ? DateTime.MaxValue.ToUniversalTime() : (DateTime?)null)) diff --git a/src/Umbraco.Core/Models/Identity/IdentityUser.cs b/src/Umbraco.Core/Models/Identity/IdentityUser.cs index cba4fc514a..c867dcf622 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityUser.cs @@ -24,12 +24,17 @@ namespace Umbraco.Core.Models.Identity /// /// public IdentityUser() - { + { this.Claims = new List(); this.Roles = new List(); this.Logins = new List(); } + /// + /// Last login date + /// + public virtual DateTime? LastLoginDateUtc { get; set; } + /// /// Email /// diff --git a/src/Umbraco.Core/Models/Rdbms/DataTypePreValueDto.cs b/src/Umbraco.Core/Models/Rdbms/DataTypePreValueDto.cs index 652e63df0b..6e8fc29d95 100644 --- a/src/Umbraco.Core/Models/Rdbms/DataTypePreValueDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/DataTypePreValueDto.cs @@ -9,7 +9,7 @@ namespace Umbraco.Core.Models.Rdbms internal class DataTypePreValueDto { [Column("id")] - [PrimaryKeyColumn(IdentitySeed = 6)] + [PrimaryKeyColumn(IdentitySeed = 10)] public int Id { get; set; } [Column("datatypeNodeId")] diff --git a/src/Umbraco.Core/Models/Rdbms/MemberDto.cs b/src/Umbraco.Core/Models/Rdbms/MemberDto.cs index e5f7b3f17c..cbe9f909f8 100644 --- a/src/Umbraco.Core/Models/Rdbms/MemberDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/MemberDto.cs @@ -22,6 +22,7 @@ namespace Umbraco.Core.Models.Rdbms [Column("LoginName")] [Length(1000)] [Constraint(Default = "''")] + [Index(IndexTypes.NonClustered, Name = "IX_cmsMember_LoginName")] public string LoginName { get; set; } [Column("Password")] diff --git a/src/Umbraco.Core/Models/Rdbms/RelationDto.cs b/src/Umbraco.Core/Models/Rdbms/RelationDto.cs index 368904a5cb..d3d741a191 100644 --- a/src/Umbraco.Core/Models/Rdbms/RelationDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/RelationDto.cs @@ -16,6 +16,7 @@ namespace Umbraco.Core.Models.Rdbms [Column("parentId")] [ForeignKey(typeof(NodeDto), Name = "FK_umbracoRelation_umbracoNode")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelation_parentChildType", ForColumns = "parentId,childId,relType")] public int ParentId { get; set; } [Column("childId")] diff --git a/src/Umbraco.Core/Models/Rdbms/RelationTypeDto.cs b/src/Umbraco.Core/Models/Rdbms/RelationTypeDto.cs index 54052f58a3..d13ce33520 100644 --- a/src/Umbraco.Core/Models/Rdbms/RelationTypeDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/RelationTypeDto.cs @@ -29,11 +29,13 @@ namespace Umbraco.Core.Models.Rdbms public Guid ChildObjectType { get; set; } [Column("name")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_name")] public string Name { get; set; } [Column("alias")] [NullSetting(NullSetting = NullSettings.Null)] [Length(100)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_alias")] public string Alias { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/User2NodePermissionDto.cs b/src/Umbraco.Core/Models/Rdbms/User2NodePermissionDto.cs index 1e6662735f..f0d769a21e 100644 --- a/src/Umbraco.Core/Models/Rdbms/User2NodePermissionDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/User2NodePermissionDto.cs @@ -15,6 +15,7 @@ namespace Umbraco.Core.Models.Rdbms [Column("nodeId")] [ForeignKey(typeof(NodeDto))] + [Index(IndexTypes.NonClustered, Name = "IX_umbracoUser2NodePermission_nodeId")] public int NodeId { get; set; } [Column("permission")] diff --git a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs index b7bf07af34..0df7a21e57 100644 --- a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs +++ b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs @@ -26,7 +26,7 @@ namespace Umbraco.Core.Models /// /// Root /// - [UmbracoObjectType(Constants.ObjectTypes.SystemRoot)] + [UmbracoObjectType(Constants.ObjectTypes.SystemRoot)] [FriendlyName("Root")] ROOT, @@ -35,6 +35,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.Document, typeof(IContent))] [FriendlyName("Document")] + [UmbracoUdiType(Constants.UdiEntityType.Document)] Document, /// @@ -42,6 +43,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.Media, typeof(IMedia))] [FriendlyName("Media")] + [UmbracoUdiType(Constants.UdiEntityType.Media)] Media, /// @@ -49,6 +51,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.MemberType, typeof(IMemberType))] [FriendlyName("Member Type")] + [UmbracoUdiType(Constants.UdiEntityType.MemberType)] MemberType, /// @@ -56,6 +59,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.Template, typeof(ITemplate))] [FriendlyName("Template")] + [UmbracoUdiType(Constants.UdiEntityType.Template)] Template, /// @@ -63,6 +67,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.MemberGroup)] [FriendlyName("Member Group")] + [UmbracoUdiType(Constants.UdiEntityType.MemberGroup)] MemberGroup, //TODO: What is a 'Content Item' supposed to be??? @@ -80,6 +85,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.MediaType, typeof(IMediaType))] [FriendlyName("Media Type")] + [UmbracoUdiType(Constants.UdiEntityType.MediaType)] MediaType, /// @@ -87,13 +93,14 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.DocumentType, typeof(IContentType))] [FriendlyName("Document Type")] + [UmbracoUdiType(Constants.UdiEntityType.DocumentType)] DocumentType, /// /// Recycle Bin /// [UmbracoObjectType(Constants.ObjectTypes.ContentRecycleBin)] - [FriendlyName("Recycle Bin")] + [FriendlyName("Recycle Bin")] RecycleBin, /// @@ -101,6 +108,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.Stylesheet)] [FriendlyName("Stylesheet")] + [UmbracoUdiType(Constants.UdiEntityType.Stylesheet)] Stylesheet, /// @@ -108,6 +116,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.Member, typeof(IMember))] [FriendlyName("Member")] + [UmbracoUdiType(Constants.UdiEntityType.Member)] Member, /// @@ -115,6 +124,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.DataType, typeof(IDataTypeDefinition))] [FriendlyName("Data Type")] + [UmbracoUdiType(Constants.UdiEntityType.DataType)] DataType, /// @@ -122,6 +132,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.DocumentTypeContainer)] [FriendlyName("Document Type Container")] + [UmbracoUdiType(Constants.UdiEntityType.DocumentTypeContainer)] DocumentTypeContainer, /// @@ -129,6 +140,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.MediaTypeContainer)] [FriendlyName("Media Type Container")] + [UmbracoUdiType(Constants.UdiEntityType.MediaTypeContainer)] MediaTypeContainer, /// @@ -136,6 +148,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.DataTypeContainer)] [FriendlyName("Data Type Container")] + [UmbracoUdiType(Constants.UdiEntityType.DataTypeContainer)] DataTypeContainer, /// @@ -143,6 +156,7 @@ namespace Umbraco.Core.Models /// [UmbracoObjectType(Constants.ObjectTypes.RelationType)] [FriendlyName("Relation Type")] + [UmbracoUdiType(Constants.UdiEntityType.RelationType)] RelationType, /// @@ -153,17 +167,17 @@ namespace Umbraco.Core.Models FormsForm, /// - /// Forms Workflow + /// Forms PreValue /// - [UmbracoObjectType(Constants.ObjectTypes.FormsWorkflow)] - [FriendlyName("Workflow")] - FormsWorkflow, + [UmbracoObjectType(Constants.ObjectTypes.FormsPreValue)] + [FriendlyName("PreValue")] + FormsPreValue, /// - /// Forms Record + /// Forms DataSource /// - [UmbracoObjectType(Constants.ObjectTypes.FormsRecord)] - [FriendlyName("Record")] - FormsRecord + [UmbracoObjectType(Constants.ObjectTypes.FormsDataSource)] + [FriendlyName("DataSource")] + FormsDataSource } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/UmbracoObjectTypesExtensions.cs b/src/Umbraco.Core/Models/UmbracoObjectTypesExtensions.cs index 34ee29b96f..5f92e6425e 100644 --- a/src/Umbraco.Core/Models/UmbracoObjectTypesExtensions.cs +++ b/src/Umbraco.Core/Models/UmbracoObjectTypesExtensions.cs @@ -12,6 +12,7 @@ namespace Umbraco.Core.Models { //MUST be concurrent to avoid thread collisions! private static readonly ConcurrentDictionary UmbracoObjectTypeCache = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary UmbracoObjectTypeUdiCache = new ConcurrentDictionary(); /// /// Get an UmbracoObjectTypes value from it's name @@ -43,6 +44,21 @@ namespace Umbraco.Core.Models return umbracoObjectType; } + public static string GetUdiType(Guid guid) + { + var umbracoObjectType = Constants.UdiEntityType.Unknown; + + foreach (var name in Enum.GetNames(typeof(UmbracoObjectTypes))) + { + var objType = GetUmbracoObjectType(name); + if (objType.GetGuid() == guid) + { + umbracoObjectType = GetUdiType(objType); + } + } + return umbracoObjectType; + } + /// /// Extension method for the UmbracoObjectTypes enum to return the enum GUID /// @@ -68,6 +84,26 @@ namespace Umbraco.Core.Models }); } + public static string GetUdiType(this UmbracoObjectTypes umbracoObjectType) + { + return UmbracoObjectTypeUdiCache.GetOrAdd(umbracoObjectType, types => + { + var type = typeof(UmbracoObjectTypes); + var memInfo = type.GetMember(umbracoObjectType.ToString()); + var attributes = memInfo[0].GetCustomAttributes(typeof(UmbracoUdiTypeAttribute), + false); + + if (attributes.Length == 0) + return Constants.UdiEntityType.Unknown; + + var attribute = ((UmbracoUdiTypeAttribute)attributes[0]); + if (attribute == null) + return Constants.UdiEntityType.Unknown; + + return attribute.UdiType; + }); + } + /// /// Extension method for the UmbracoObjectTypes enum to return the enum name /// diff --git a/src/Umbraco.Core/ObjectExtensions.cs b/src/Umbraco.Core/ObjectExtensions.cs index fcdc2f52a6..6a1befaad8 100644 --- a/src/Umbraco.Core/ObjectExtensions.cs +++ b/src/Umbraco.Core/ObjectExtensions.cs @@ -647,5 +647,10 @@ namespace Umbraco.Core return "[GetPropertyValueException]"; } } + + internal static Guid AsGuid(this object value) + { + return value is Guid ? (Guid) value : Guid.Empty; + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Packaging/Models/UninstallationSummary.cs b/src/Umbraco.Core/Packaging/Models/UninstallationSummary.cs new file mode 100644 index 0000000000..13bb4bfc77 --- /dev/null +++ b/src/Umbraco.Core/Packaging/Models/UninstallationSummary.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Packaging.Models +{ + [Serializable] + [DataContract(IsReference = true)] + internal class UninstallationSummary + { + public MetaData MetaData { get; set; } + public IEnumerable DataTypesUninstalled { get; set; } + public IEnumerable LanguagesUninstalled { get; set; } + public IEnumerable DictionaryItemsUninstalled { get; set; } + public IEnumerable MacrosUninstalled { get; set; } + public IEnumerable FilesUninstalled { get; set; } + public IEnumerable TemplatesUninstalled { get; set; } + public IEnumerable ContentTypesUninstalled { get; set; } + public IEnumerable StylesheetsUninstalled { get; set; } + public IEnumerable ContentUninstalled { get; set; } + public bool PackageUninstalled { get; set; } + } + + internal static class UninstallationSummaryExtentions + { + public static UninstallationSummary InitEmpty(this UninstallationSummary summary) + { + summary.ContentUninstalled = new List(); + summary.ContentTypesUninstalled = new List(); + summary.DataTypesUninstalled = new List(); + summary.DictionaryItemsUninstalled = new List(); + summary.FilesUninstalled = new List(); + summary.LanguagesUninstalled = new List(); + summary.MacrosUninstalled = new List(); + summary.MetaData = new MetaData(); + summary.TemplatesUninstalled = new List(); + summary.PackageUninstalled = false; + return summary; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Factories/ContentFactory.cs b/src/Umbraco.Core/Persistence/Factories/ContentFactory.cs index 88de5a1d6c..0532eab6b1 100644 --- a/src/Umbraco.Core/Persistence/Factories/ContentFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ContentFactory.cs @@ -27,7 +27,20 @@ namespace Umbraco.Core.Persistence.Factories #region Implementation of IEntityFactory - public static IContent BuildEntity(DocumentDto dto, IContentType contentType) + /// + /// Builds a IContent item from the dto(s) and content type + /// + /// + /// This DTO can contain all of the information to build an IContent item, however in cases where multiple entities are being built, + /// a separate publishedDto entity will be supplied in place of the 's own + /// ResultColumn DocumentPublishedReadOnlyDto + /// + /// + /// + /// When querying for multiple content items the main DTO will not contain the ResultColumn DocumentPublishedReadOnlyDto and a separate publishedDto instance will be supplied + /// + /// + public static IContent BuildEntity(DocumentDto dto, IContentType contentType, DocumentPublishedReadOnlyDto publishedDto = null) { var content = new Content(dto.Text, dto.ContentVersionDto.ContentDto.NodeDto.ParentId, contentType); @@ -52,8 +65,13 @@ namespace Umbraco.Core.Persistence.Factories content.ExpireDate = dto.ExpiresDate.HasValue ? dto.ExpiresDate.Value : (DateTime?)null; content.ReleaseDate = dto.ReleaseDate.HasValue ? dto.ReleaseDate.Value : (DateTime?)null; content.Version = dto.ContentVersionDto.VersionId; + content.PublishedState = dto.Published ? PublishedState.Published : PublishedState.Unpublished; - content.PublishedVersionGuid = dto.DocumentPublishedReadOnlyDto == null ? default(Guid) : dto.DocumentPublishedReadOnlyDto.VersionId; + + //Check if the publishedDto has been supplied, if not the use the dto's own DocumentPublishedReadOnlyDto value + content.PublishedVersionGuid = publishedDto == null + ? (dto.DocumentPublishedReadOnlyDto == null ? default(Guid) : dto.DocumentPublishedReadOnlyDto.VersionId) + : publishedDto.VersionId; //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs index 3c7fc5ed42..7dc57ba7f6 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs @@ -130,21 +130,21 @@ namespace Umbraco.Core.Persistence.Migrations.Initial _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = Constants.System.DefaultMembersListViewDataTypeId, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,-97", SortOrder = 2, UniqueId = new Guid("AA2C52A0-CE87-4E65-A47C-7DF09358585D"), Text = Constants.Conventions.DataTypes.ListViewPrefix + "Members", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1031, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1031", SortOrder = 2, UniqueId = new Guid("f38bd2d7-65d0-48e6-95dc-87ce06ec2d3d"), Text = Constants.Conventions.MediaTypes.Folder, NodeObjectType = new Guid(Constants.ObjectTypes.MediaType), CreateDate = DateTime.Now }); _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1032, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1032", SortOrder = 2, UniqueId = new Guid("cc07b313-0843-4aa8-bbda-871c8da728c8"), Text = Constants.Conventions.MediaTypes.Image, NodeObjectType = new Guid(Constants.ObjectTypes.MediaType), CreateDate = DateTime.Now }); - _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1033, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1033", SortOrder = 2, UniqueId = new Guid("4c52d8ab-54e6-40cd-999c-7a5f24903e4d"), Text = Constants.Conventions.MediaTypes.File, NodeObjectType = new Guid(Constants.ObjectTypes.MediaType), CreateDate = DateTime.Now }); - _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1034, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1034", SortOrder = 2, UniqueId = new Guid("a6857c73-d6e9-480c-b6e6-f15f6ad11125"), Text = "Content Picker", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); - _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1035, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1035", SortOrder = 2, UniqueId = new Guid("93929b9a-93a2-4e2a-b239-d99334440a59"), Text = "Media Picker", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); - _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1036, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1036", SortOrder = 2, UniqueId = new Guid("2b24165f-9782-4aa3-b459-1de4a4d21f60"), Text = "Member Picker", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); + _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1033, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1033", SortOrder = 2, UniqueId = new Guid("4c52d8ab-54e6-40cd-999c-7a5f24903e4d"), Text = Constants.Conventions.MediaTypes.File, NodeObjectType = new Guid(Constants.ObjectTypes.MediaType), CreateDate = DateTime.Now }); _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1040, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1040", SortOrder = 2, UniqueId = new Guid("21e798da-e06e-4eda-a511-ed257f78d4fa"), Text = "Related Links", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1041, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1041", SortOrder = 2, UniqueId = new Guid("b6b73142-b9c1-4bf8-a16d-e1c23320b549"), Text = "Tags", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1043, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1043", SortOrder = 2, UniqueId = new Guid("1df9f033-e6d4-451f-b8d2-e0cbc50a836f"), Text = "Image Cropper", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1044, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1044", SortOrder = 0, UniqueId = new Guid("d59be02f-1df9-4228-aa1e-01917d806cda"), Text = Constants.Conventions.MemberTypes.DefaultAlias, NodeObjectType = new Guid(Constants.ObjectTypes.MemberType), CreateDate = DateTime.Now }); - _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1045, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1045", SortOrder = 2, UniqueId = new Guid("7E3962CC-CE20-4FFC-B661-5897A894BA7E"), Text = "Multiple Media Picker", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); + //New UDI pickers with newer Ids + _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1046, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1046", SortOrder = 2, UniqueId = new Guid("FD1E0DA5-5606-4862-B679-5D0CF3A52A59"), Text = "Content Picker", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); + _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1047, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1047", SortOrder = 2, UniqueId = new Guid("1EA2E01F-EBD8-4CE1-8D71-6B1149E63548"), Text = "Member Picker", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); + _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1048, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1048", SortOrder = 2, UniqueId = new Guid("135D60E0-64D9-49ED-AB08-893C9BA44AE5"), Text = "Media Picker", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); + _database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1049, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1049", SortOrder = 2, UniqueId = new Guid("9DBBCBBB-2327-434A-B355-AF1B84E5010A"), Text = "Multiple Media Picker", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); //TODO: We're not creating these for 7.0 //_database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1039, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1039", SortOrder = 2, UniqueId = new Guid("06f349a9-c949-4b6a-8660-59c10451af42"), Text = "Ultimate Picker", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); //_database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1038, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1038", SortOrder = 2, UniqueId = new Guid("1251c96c-185c-4e9b-93f4-b48205573cbd"), Text = "Simple Editor", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); - //_database.Insert("umbracoNode", "id", false, new NodeDto { NodeId = 1042, Trashed = false, ParentId = -1, UserId = 0, Level = 1, Path = "-1,1042", SortOrder = 2, UniqueId = new Guid("0a452bd5-83f9-4bc3-8403-1286e13fb77e"), Text = "Macro Container", NodeObjectType = new Guid(Constants.ObjectTypes.DataType), CreateDate = DateTime.Now }); } @@ -246,17 +246,19 @@ namespace Umbraco.Core.Persistence.Migrations.Initial _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 12, DataTypeId = -40, PropertyEditorAlias = Constants.PropertyEditors.RadioButtonListAlias, DbType = "Nvarchar" }); _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 13, DataTypeId = -41, PropertyEditorAlias = Constants.PropertyEditors.DateAlias, DbType = "Date" }); _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 14, DataTypeId = -42, PropertyEditorAlias = Constants.PropertyEditors.DropDownListAlias, DbType = "Integer" }); - _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 15, DataTypeId = -43, PropertyEditorAlias = Constants.PropertyEditors.CheckBoxListAlias, DbType = "Nvarchar" }); - _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 16, DataTypeId = 1034, PropertyEditorAlias = Constants.PropertyEditors.ContentPickerAlias, DbType = "Integer" }); - _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 18, DataTypeId = 1036, PropertyEditorAlias = Constants.PropertyEditors.MemberPickerAlias, DbType = "Integer" }); - _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 17, DataTypeId = 1035, PropertyEditorAlias = Constants.PropertyEditors.MultipleMediaPickerAlias, DbType = "Nvarchar" }); + _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 15, DataTypeId = -43, PropertyEditorAlias = Constants.PropertyEditors.CheckBoxListAlias, DbType = "Nvarchar" }); _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 21, DataTypeId = 1040, PropertyEditorAlias = Constants.PropertyEditors.RelatedLinksAlias, DbType = "Ntext" }); _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 22, DataTypeId = 1041, PropertyEditorAlias = Constants.PropertyEditors.TagsAlias, DbType = "Ntext" }); _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 24, DataTypeId = 1043, PropertyEditorAlias = Constants.PropertyEditors.ImageCropperAlias, DbType = "Ntext" }); - _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 25, DataTypeId = 1045, PropertyEditorAlias = Constants.PropertyEditors.MultipleMediaPickerAlias, DbType = "Nvarchar" }); _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = -26, DataTypeId = Constants.System.DefaultContentListViewDataTypeId, PropertyEditorAlias = Constants.PropertyEditors.ListViewAlias, DbType = "Nvarchar" }); _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = -27, DataTypeId = Constants.System.DefaultMediaListViewDataTypeId, PropertyEditorAlias = Constants.PropertyEditors.ListViewAlias, DbType = "Nvarchar" }); - _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = -28, DataTypeId = Constants.System.DefaultMembersListViewDataTypeId, PropertyEditorAlias = Constants.PropertyEditors.ListViewAlias, DbType = "Nvarchar" }); + _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = -28, DataTypeId = Constants.System.DefaultMembersListViewDataTypeId, PropertyEditorAlias = Constants.PropertyEditors.ListViewAlias, DbType = "Nvarchar" }); + + //New UDI pickers with newer Ids + _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 26, DataTypeId = 1046, PropertyEditorAlias = Constants.PropertyEditors.ContentPicker2Alias, DbType = "Nvarchar" }); + _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 27, DataTypeId = 1047, PropertyEditorAlias = Constants.PropertyEditors.MemberPicker2Alias, DbType = "Nvarchar" }); + _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 28, DataTypeId = 1048, PropertyEditorAlias = Constants.PropertyEditors.MediaPicker2Alias, DbType = "Ntext" }); + _database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 29, DataTypeId = 1049, PropertyEditorAlias = Constants.PropertyEditors.MediaPicker2Alias, DbType = "Ntext" }); //TODO: We're not creating these for 7.0 //_database.Insert("cmsDataType", "pk", false, new DataTypeDto { PrimaryKey = 19, DataTypeId = 1038, PropertyEditorAlias = Constants.PropertyEditors.MarkdownEditorAlias, DbType = "Ntext" }); @@ -268,10 +270,7 @@ namespace Umbraco.Core.Persistence.Migrations.Initial { _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = 3, Alias = "", SortOrder = 0, DataTypeNodeId = -87, Value = ",code,undo,redo,cut,copy,mcepasteword,stylepicker,bold,italic,bullist,numlist,outdent,indent,mcelink,unlink,mceinsertanchor,mceimage,umbracomacro,mceinserttable,umbracoembed,mcecharmap,|1|1,2,3,|0|500,400|1049,|true|" }); _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = 4, Alias = "group", SortOrder = 0, DataTypeNodeId = 1041, Value = "default" }); - - //default's for MultipleMediaPickerAlias picker - _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = 5, Alias = "multiPicker", SortOrder = 0, DataTypeNodeId = 1045, Value = "1" }); - + //defaults for the member list _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -1, Alias = "pageSize", SortOrder = 1, DataTypeNodeId = Constants.System.DefaultMembersListViewDataTypeId, Value = "10" }); _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -2, Alias = "orderBy", SortOrder = 2, DataTypeNodeId = Constants.System.DefaultMembersListViewDataTypeId, Value = "username" }); @@ -288,6 +287,9 @@ namespace Umbraco.Core.Persistence.Migrations.Initial _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -7, Alias = "orderDirection", SortOrder = 3, DataTypeNodeId = Constants.System.DefaultMediaListViewDataTypeId, Value = "desc" }); _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -8, Alias = "layouts", SortOrder = 4, DataTypeNodeId = Constants.System.DefaultMediaListViewDataTypeId, Value = "[" + cardLayout + "," + listLayout + "]" }); _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = -9, Alias = "includeProperties", SortOrder = 5, DataTypeNodeId = Constants.System.DefaultMediaListViewDataTypeId, Value = "[{\"alias\":\"updateDate\",\"header\":\"Last edited\",\"isSystem\":1},{\"alias\":\"owner\",\"header\":\"Updated by\",\"isSystem\":1}]" }); + + //default's for MultipleMediaPickerAlias picker + _database.Insert("cmsDataTypePreValues", "id", false, new DataTypePreValueDto { Id = 6, Alias = "multiPicker", SortOrder = 0, DataTypeNodeId = 1049, Value = "1" }); } private void CreateUmbracoRelationTypeData() diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToCmsMemberLoginName.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToCmsMemberLoginName.cs new file mode 100644 index 0000000000..0cb4f297b1 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToCmsMemberLoginName.cs @@ -0,0 +1,35 @@ +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero +{ + [Migration("7.6.0", 0, Constants.System.UmbracoMigrationName)] + public class AddIndexToCmsMemberLoginName : MigrationBase + { + public AddIndexToCmsMemberLoginName(ISqlSyntaxProvider sqlSyntax, ILogger logger) + : base(sqlSyntax, logger) + { } + + public override void Up() + { + var dbIndexes = SqlSyntax.GetDefinedIndexesDefinitions(Context.Database); + + //make sure it doesn't already exist + if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_cmsMember_LoginName")) == false) + { + Create.Index("IX_cmsMember_LoginName").OnTable("cmsMember") + .OnColumn("LoginName") + .Ascending() + .WithOptions() + .NonClustered(); + } + } + + public override void Down() + { + Delete.Index("IX_cmsMember_LoginName").OnTable("cmsMember"); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToUmbracoNodePath.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToUmbracoNodePath.cs index 229311dd9f..e59252390a 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToUmbracoNodePath.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToUmbracoNodePath.cs @@ -14,14 +14,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero public override void Up() { - var dbIndexes = SqlSyntax.GetDefinedIndexes(Context.Database) - .Select(x => new DbIndexDefinition() - { - TableName = x.Item1, - IndexName = x.Item2, - ColumnName = x.Item3, - IsUnique = x.Item4 - }).ToArray(); + var dbIndexes = SqlSyntax.GetDefinedIndexesDefinitions(Context.Database); //make sure it doesn't already exist if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_umbracoNodePath")) == false) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToUser2NodePermission.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToUser2NodePermission.cs new file mode 100644 index 0000000000..b510ef428c --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToUser2NodePermission.cs @@ -0,0 +1,34 @@ +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero +{ + [Migration("7.6.0", 0, Constants.System.UmbracoMigrationName)] + public class AddIndexToUser2NodePermission : MigrationBase + { + public AddIndexToUser2NodePermission(ISqlSyntaxProvider sqlSyntax, ILogger logger) + : base(sqlSyntax, logger) + { } + + public override void Up() + { + var dbIndexes = SqlSyntax.GetDefinedIndexesDefinitions(Context.Database); + + //make sure it doesn't already exist + if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_umbracoUser2NodePermission_nodeId")) == false) + { + Create.Index("IX_umbracoUser2NodePermission_nodeId").OnTable("umbracoUser2NodePermission") + .OnColumn("nodeId") + .Ascending() + .WithOptions() + .NonClustered(); + } + } + + public override void Down() + { + Delete.Index("IX_umbracoUser2NodePermission_nodeId").OnTable("cmsMember"); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexesToUmbracoRelationTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexesToUmbracoRelationTables.cs new file mode 100644 index 0000000000..b8c0d78ef1 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexesToUmbracoRelationTables.cs @@ -0,0 +1,91 @@ +using System; +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero +{ + [Migration("7.6.0", 0, Constants.System.UmbracoMigrationName)] + public class AddIndexesToUmbracoRelationTables : MigrationBase + { + public AddIndexesToUmbracoRelationTables(ISqlSyntaxProvider sqlSyntax, ILogger logger) + : base(sqlSyntax, logger) + { } + + public override void Up() + { + var dbIndexes = SqlSyntax.GetDefinedIndexesDefinitions(Context.Database).ToArray(); + + //make sure it doesn't already exist + if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_umbracoRelation_parentChildType")) == false) + { + //This will remove any corrupt/duplicate data in the relation table before the index is applied + //Ensure this executes in a defered block which will be done inside of the migration transaction + this.Execute.Code(database => + { + //We need to check if this index has corrupted data and then clear that data + var duplicates = database.Fetch("SELECT parentId,childId,relType FROM umbracoRelation GROUP BY parentId,childId,relType HAVING COUNT(*) > 1"); + if (duplicates.Count > 0) + { + //need to fix this there cannot be duplicates so we'll take the latest entries, it's really not going to matter though + foreach (var duplicate in duplicates) + { + var ids = database.Fetch("SELECT id FROM umbracoRelation WHERE parentId=@parentId AND childId=@childId AND relType=@relType ORDER BY datetime DESC", + new { parentId = duplicate.parentId, childId = duplicate.childId, relType = duplicate.relType }); + + if (ids.Count == 1) + { + //this is just a safety check, this should absolutely never happen + throw new InvalidOperationException("Duplicates were detected but could not be discovered"); + } + + //delete the others + ids = ids.Skip(0).ToList(); + + //iterate in groups of 2000 to avoid the max sql parameter limit + foreach (var idGroup in ids.InGroupsOf(2000)) + { + database.Execute("DELETE FROM umbracoRelation WHERE id IN (@ids)", new { ids = idGroup }); + } + } + } + return ""; + }); + + //unique index to prevent duplicates - and for better perf + Create.Index("IX_umbracoRelation_parentChildType").OnTable("umbracoRelation") + .OnColumn("parentId").Ascending() + .OnColumn("childId").Ascending() + .OnColumn("relType").Ascending() + .WithOptions() + .Unique(); + } + + //need indexes on alias and name for relation type since these are queried against + + //make sure it doesn't already exist + if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_umbracoRelationType_alias")) == false) + { + Create.Index("IX_umbracoRelationType_alias").OnTable("umbracoRelationType") + .OnColumn("alias") + .Ascending() + .WithOptions() + .NonClustered(); + } + if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_umbracoRelationType_name")) == false) + { + Create.Index("IX_umbracoRelationType_name").OnTable("umbracoRelationType") + .OnColumn("name") + .Ascending() + .WithOptions() + .NonClustered(); + } + + } + + public override void Down() + { + Delete.Index("IX_umbracoNodePath").OnTable("umbracoNode"); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs b/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs index 2db9fcfdb4..8626714a43 100644 --- a/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs +++ b/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs @@ -105,8 +105,8 @@ namespace Umbraco.Core.Persistence // try to update var rowCount = updateCommand.IsNullOrWhiteSpace() - ? db.Update(poco) - : db.Update(updateCommand, updateArgs); + ? db.Update(poco) + : db.Update(updateCommand, updateArgs); if (rowCount > 0) return RecordPersistenceType.Update; @@ -162,7 +162,7 @@ namespace Umbraco.Core.Persistence [Obsolete("Use the DatabaseSchemaHelper instead")] public static void CreateTable(this Database db) - where T : new() + where T : new() { var creator = new DatabaseSchemaHelper(db, LoggerResolver.Current.Logger, SqlSyntaxContext.SqlSyntaxProvider); creator.CreateTable(); @@ -170,7 +170,7 @@ namespace Umbraco.Core.Persistence [Obsolete("Use the DatabaseSchemaHelper instead")] public static void CreateTable(this Database db, bool overwrite) - where T : new() + where T : new() { var creator = new DatabaseSchemaHelper(db, LoggerResolver.Current.Logger, SqlSyntaxContext.SqlSyntaxProvider); creator.CreateTable(overwrite); @@ -231,6 +231,24 @@ namespace Umbraco.Core.Persistence ISqlSyntaxProvider syntaxProvider, bool useNativeSqlPlatformBulkInsert = true, bool commitTrans = false) + { + db.OpenSharedConnection(); + try + { + return BulkInsertRecordsTry(db, collection, tr, syntaxProvider, useNativeSqlPlatformBulkInsert, commitTrans); + } + finally + { + db.CloseSharedConnection(); + } + } + + public static int BulkInsertRecordsTry(this Database db, + IEnumerable collection, + Transaction tr, + ISqlSyntaxProvider syntaxProvider, + bool useNativeSqlPlatformBulkInsert = true, + bool commitTrans = false) { if (commitTrans && tr == null) throw new ArgumentNullException("tr", "The transaction cannot be null if commitTrans is true."); @@ -241,15 +259,15 @@ namespace Umbraco.Core.Persistence return 0; } - var pd = Database.PocoData.ForType(typeof(T)); - if (pd == null) throw new InvalidOperationException("Could not find PocoData for " + typeof(T)); + var pd = Database.PocoData.ForType(typeof (T)); + if (pd == null) throw new InvalidOperationException("Could not find PocoData for " + typeof (T)); try { int processed = 0; var usedNativeSqlPlatformInserts = useNativeSqlPlatformBulkInsert - && NativeSqlPlatformBulkInsertRecords(db, syntaxProvider, pd, collection, out processed); + && NativeSqlPlatformBulkInsertRecords(db, syntaxProvider, pd, collection, out processed); if (usedNativeSqlPlatformInserts == false) { @@ -283,19 +301,13 @@ namespace Umbraco.Core.Persistence } if (commitTrans) - { - if (tr == null) throw new ArgumentNullException("The transaction cannot be null if commitTrans is true"); tr.Complete(); - } return processed; } catch { if (commitTrans) - { - if (tr == null) throw new ArgumentNullException("The transaction cannot be null if commitTrans is true"); tr.Dispose(); - } throw; } @@ -337,13 +349,16 @@ namespace Umbraco.Core.Persistence IEnumerable collection, out string[] sql) { + if (db == null) throw new ArgumentNullException("db"); + if (db.Connection == null) throw new ArgumentException("db.Connection is null."); + var tableName = db.EscapeTableName(pd.TableInfo.TableName); //get all columns to include and format for sql var cols = string.Join(", ", pd.Columns - .Where(c => IncludeColumn(pd, c)) - .Select(c => tableName + "." + db.EscapeSqlIdentifier(c.Key)).ToArray()); + .Where(c => IncludeColumn(pd, c)) + .Select(c => tableName + "." + db.EscapeSqlIdentifier(c.Key)).ToArray()); var itemArray = collection.ToArray(); @@ -357,9 +372,9 @@ namespace Umbraco.Core.Persistence // 4168 / 262 = 15.908... = there will be 16 trans in total //all items will be included if we have disabled db parameters - var itemsPerTrans = Math.Floor(2000.00 / paramsPerItem); + var itemsPerTrans = Math.Floor(2000.00/paramsPerItem); //there will only be one transaction if we have disabled db parameters - var numTrans = Math.Ceiling(itemArray.Length / itemsPerTrans); + var numTrans = Math.Ceiling(itemArray.Length/itemsPerTrans); var sqlQueries = new List(); var commands = new List(); @@ -367,8 +382,8 @@ namespace Umbraco.Core.Persistence for (var tIndex = 0; tIndex < numTrans; tIndex++) { var itemsForTrans = itemArray - .Skip(tIndex * (int)itemsPerTrans) - .Take((int)itemsPerTrans); + .Skip(tIndex*(int) itemsPerTrans) + .Take((int) itemsPerTrans); var cmd = db.CreateCommand(db.Connection, string.Empty); var pocoValues = new List(); @@ -418,7 +433,6 @@ namespace Umbraco.Core.Persistence /// The number of records inserted private static bool NativeSqlPlatformBulkInsertRecords(Database db, ISqlSyntaxProvider syntaxProvider, Database.PocoData pd, IEnumerable collection, out int processed) { - var dbConnection = db.Connection; //unwrap the profiled connection if there is one @@ -447,7 +461,6 @@ namespace Umbraco.Core.Persistence //could not use the SQL server's specific bulk insert operations processed = 0; return false; - } /// diff --git a/src/Umbraco.Core/Persistence/Repositories/BaseQueryType.cs b/src/Umbraco.Core/Persistence/Repositories/BaseQueryType.cs index 4579fd98fb..05061c47af 100644 --- a/src/Umbraco.Core/Persistence/Repositories/BaseQueryType.cs +++ b/src/Umbraco.Core/Persistence/Repositories/BaseQueryType.cs @@ -2,8 +2,30 @@ namespace Umbraco.Core.Persistence.Repositories { internal enum BaseQueryType { - Full, + /// + /// A query to return all information for a single item + /// + /// + /// In some cases this will be the same as + /// + FullSingle, + + /// + /// A query to return all information for multiple items + /// + /// + /// In some cases this will be the same as + /// + FullMultiple, + + /// + /// A query to return the ids for items + /// Ids, + + /// + /// A query to return the count for items + /// Count } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 044e2a95e8..37e5e80fe9 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Xml; @@ -53,7 +54,7 @@ namespace Umbraco.Core.Persistence.Repositories protected override IContent PerformGet(int id) { - var sql = GetBaseQuery(false) + var sql = GetBaseQuery(BaseQueryType.FullSingle) .Where(GetBaseWhereClause(), new { Id = id }) .Where(x => x.Newest, SqlSyntax) .OrderByDescending(x => x.VersionDate, SqlSyntax); @@ -81,15 +82,15 @@ namespace Umbraco.Core.Persistence.Repositories return s; }; - var sqlBaseFull = GetBaseQuery(BaseQueryType.Full); + var sqlBaseFull = GetBaseQuery(BaseQueryType.FullMultiple); var sqlBaseIds = GetBaseQuery(BaseQueryType.Ids); - return ProcessQuery(translate(sqlBaseFull), translate(sqlBaseIds)); + return ProcessQuery(translate(sqlBaseFull), new PagingSqlQuery(translate(sqlBaseIds))); } protected override IEnumerable PerformGetByQuery(IQuery query) { - var sqlBaseFull = GetBaseQuery(BaseQueryType.Full); + var sqlBaseFull = GetBaseQuery(BaseQueryType.FullMultiple); var sqlBaseIds = GetBaseQuery(BaseQueryType.Ids); Func, Sql> translate = (translator) => @@ -103,13 +104,25 @@ namespace Umbraco.Core.Persistence.Repositories var translatorFull = new SqlTranslator(sqlBaseFull, query); var translatorIds = new SqlTranslator(sqlBaseIds, query); - return ProcessQuery(translate(translatorFull), translate(translatorIds)); + return ProcessQuery(translate(translatorFull), new PagingSqlQuery(translate(translatorIds))); } #endregion #region Overrides of PetaPocoRepositoryBase + /// + /// Returns the base query to return Content + /// + /// + /// + /// + /// Content queries will differ depending on what needs to be returned: + /// * FullSingle: When querying for a single document, this will include the Outer join to fetch the content item's published version info + /// * FullMultiple: When querying for multiple documents, this will exclude the Outer join to fetch the content item's published version info - this info would need to be fetched separately + /// * Ids: This would essentially be the same as FullMultiple however the columns specified will only return the Ids for the documents + /// * Count: A query to return the count for documents + /// protected override Sql GetBaseQuery(BaseQueryType queryType) { var sql = new Sql(); @@ -122,14 +135,14 @@ namespace Umbraco.Core.Persistence.Repositories .InnerJoin(SqlSyntax) .On(SqlSyntax, left => left.NodeId, right => right.NodeId); - if (queryType == BaseQueryType.Full) + if (queryType == BaseQueryType.FullSingle) { //The only reason we apply this left outer join is to be able to pull back the DocumentPublishedReadOnlyDto //information with the entire data set, so basically this will get both the latest document and also it's published - //version if it has one. When performing a count or when just retrieving Ids like in paging, this is unecessary + //version if it has one. When performing a count or when retrieving Ids like in paging, this is unecessary //and causes huge performance overhead for the SQL server, especially when sorting the result. - //To fix this perf overhead we'd need another index on : - // CREATE NON CLUSTERED INDEX ON cmsDocument.node + cmsDocument.published + //We also don't include this outer join when querying for multiple entities since it is much faster to fetch this information + //in a separate query. For a single entity this is ok. var sqlx = string.Format("LEFT OUTER JOIN {0} {1} ON ({1}.{2}={0}.{2} AND {1}.{3}=1)", SqlSyntax.GetQuotedTableName("cmsDocument"), @@ -151,7 +164,7 @@ namespace Umbraco.Core.Persistence.Repositories protected override Sql GetBaseQuery(bool isCount) { - return GetBaseQuery(isCount ? BaseQueryType.Count : BaseQueryType.Full); + return GetBaseQuery(isCount ? BaseQueryType.Count : BaseQueryType.FullSingle); } protected override string GetBaseWhereClause() @@ -222,10 +235,10 @@ namespace Umbraco.Core.Persistence.Repositories while (true) { // get the next group of nodes - var sqlFull = translate(baseId, GetBaseQuery(BaseQueryType.Full)); + var sqlFull = translate(baseId, GetBaseQuery(BaseQueryType.FullMultiple)); var sqlIds = translate(baseId, GetBaseQuery(BaseQueryType.Ids)); - var xmlItems = ProcessQuery(SqlSyntax.SelectTop(sqlFull, groupSize), SqlSyntax.SelectTop(sqlIds, groupSize)) + var xmlItems = ProcessQuery(SqlSyntax.SelectTop(sqlFull, groupSize), new PagingSqlQuery(SqlSyntax.SelectTop(sqlIds, groupSize))) .Select(x => new ContentXmlDto { NodeId = x.Id, Xml = serializer(x).ToString() }) .ToList(); @@ -245,7 +258,7 @@ namespace Umbraco.Core.Persistence.Repositories Logger.Error("Could not rebuild XML for nodeId=" + xmlItem.NodeId, e); } } - baseId = xmlItems.Last().NodeId; + baseId = xmlItems[xmlItems.Count - 1].NodeId; } } @@ -257,15 +270,15 @@ namespace Umbraco.Core.Persistence.Repositories .OrderByDescending(x => x.VersionDate, SqlSyntax); }; - var sqlFull = translate(GetBaseQuery(BaseQueryType.Full)); + var sqlFull = translate(GetBaseQuery(BaseQueryType.FullMultiple)); var sqlIds = translate(GetBaseQuery(BaseQueryType.Ids)); - return ProcessQuery(sqlFull, sqlIds, true); + return ProcessQuery(sqlFull, new PagingSqlQuery(sqlIds), true); } public override IContent GetByVersion(Guid versionId) { - var sql = GetBaseQuery(false); + var sql = GetBaseQuery(BaseQueryType.FullSingle); sql.Where("cmsContentVersion.VersionId = @VersionId", new { VersionId = versionId }); sql.OrderByDescending(x => x.VersionDate, SqlSyntax); @@ -675,12 +688,12 @@ namespace Umbraco.Core.Persistence.Repositories // ORDER BY substring(path, 1, len(path) - charindex(',', reverse(path))), sortOrder // but that's probably an overkill - sorting by level,sortOrder should be enough - var sqlFull = GetBaseQuery(BaseQueryType.Full); + var sqlFull = GetBaseQuery(BaseQueryType.FullMultiple); var translatorFull = new SqlTranslator(sqlFull, query); var sqlIds = GetBaseQuery(BaseQueryType.Ids); var translatorIds = new SqlTranslator(sqlIds, query); - return ProcessQuery(translate(translatorFull), translate(translatorIds), true); + return ProcessQuery(translate(translatorFull), new PagingSqlQuery(translate(translatorIds)), true); } /// @@ -772,13 +785,8 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", public void ClearPublished(IContent content) { - // race cond! - var documentDtos = Database.Fetch("WHERE nodeId=@id AND published=@published", new { id = content.Id, published = true }); - foreach (var documentDto in documentDtos) - { - documentDto.Published = false; - Database.Update(documentDto); - } + var sql = "UPDATE cmsDocument SET published=0 WHERE nodeId=@id AND published=1"; + Database.Execute(sql, new {id = content.Id}); } /// @@ -860,7 +868,7 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, new Tuple("cmsDocument", "nodeId"), - (sqlFull, sqlIds) => ProcessQuery(sqlFull, sqlIds), orderBy, orderDirection, orderBySystemField, + (sqlFull, pagingSqlQuery) => ProcessQuery(sqlFull, pagingSqlQuery), orderBy, orderDirection, orderBySystemField, filterCallback); } @@ -890,23 +898,58 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", return base.GetDatabaseFieldNameForOrderBy(orderBy); } - + /// /// This is the underlying method that processes most queries for this repository /// /// - /// The full SQL with the outer join to return all data required to create an IContent + /// The FullMultiple SQL without the outer join to return all data required to create an IContent excluding it's published state data which this will query separately /// - /// + /// /// The Id SQL without the outer join to just return all document ids - used to process the properties for the content item /// /// /// - private IEnumerable ProcessQuery(Sql sqlFull, Sql sqlIds, bool withCache = false) + private IEnumerable ProcessQuery(Sql sqlFull, PagingSqlQuery pagingSqlQuery, bool withCache = false) { // fetch returns a list so it's ok to iterate it in this method - var dtos = Database.Fetch(sqlFull); + var dtos = Database.Fetch(sqlFull); if (dtos.Count == 0) return Enumerable.Empty(); + + //Go and get all of the published version data separately for this data, this is because when we are querying + //for multiple content items we don't include the outer join to fetch this data in the same query because + //it is insanely slow. Instead we just fetch the published version data separately in one query. + + //we need to parse the original SQL statement and reduce the columns to just cmsDocument.nodeId so that we can use + // the statement to go get the published data for all of the items by using an inner join + var parsedOriginalSql = "SELECT cmsDocument.nodeId " + sqlFull.SQL.Substring(sqlFull.SQL.IndexOf("FROM", StringComparison.Ordinal)); + //now remove everything from an Orderby clause and beyond + if (parsedOriginalSql.InvariantContains("ORDER BY ")) + { + parsedOriginalSql = parsedOriginalSql.Substring(0, parsedOriginalSql.LastIndexOf("ORDER BY ", StringComparison.Ordinal)); + } + + var publishedSql = new Sql(@"SELECT * +FROM cmsDocument AS doc2 +INNER JOIN + (" + parsedOriginalSql + @") as docData +ON doc2.nodeId = docData.nodeId +WHERE doc2.published = 1 +ORDER BY doc2.nodeId +", sqlFull.Arguments); + + //go and get the published version data, we do a Query here and not a Fetch so we are + //not allocating a whole list to memory just to allocate another list in memory since + //we are assigning this data to a keyed collection for fast lookup below + var publishedData = Database.Query(publishedSql); + var publishedDataCollection = new DocumentPublishedReadOnlyDtoCollection(); + foreach (var publishedDto in publishedData) + { + //double check that there's no corrupt db data, there should only be a single published item + if (publishedDataCollection.Contains(publishedDto.NodeId) == false) + publishedDataCollection.Add(publishedDto); + } + var content = new IContent[dtos.Count]; var defs = new List(); @@ -920,6 +963,8 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", for (var i = 0; i < dtos.Count; i++) { var dto = dtos[i]; + DocumentPublishedReadOnlyDto publishedDto; + publishedDataCollection.TryGetValue(dto.NodeId, out publishedDto); // if the cache contains the published version, use it if (withCache) @@ -947,7 +992,7 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", contentTypes[dto.ContentVersionDto.ContentDto.ContentTypeId] = contentType; } - content[i] = ContentFactory.BuildEntity(dto, contentType); + content[i] = ContentFactory.BuildEntity(dto, contentType, publishedDto); // need template if (dto.TemplateId.HasValue && dto.TemplateId.Value > 0) @@ -968,7 +1013,7 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", .ToDictionary(x => x.Id, x => x); // load all properties for all documents from database in 1 query - var propertyData = GetPropertyCollection(sqlIds, defs); + var propertyData = GetPropertyCollection(pagingSqlQuery, defs); // assign var dtoIndex = 0; @@ -1056,7 +1101,7 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", return currentName; } - + /// /// Dispose disposable properties /// @@ -1071,5 +1116,26 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", _contentPreviewRepository.Dispose(); _contentXmlRepository.Dispose(); } + + /// + /// A keyed collection for fast lookup when retrieving a separate list of published version data + /// + private class DocumentPublishedReadOnlyDtoCollection : KeyedCollection + { + protected override int GetKeyForItem(DocumentPublishedReadOnlyDto item) + { + return item.NodeId; + } + + public bool TryGetValue(int key, out DocumentPublishedReadOnlyDto val) + { + if (Dictionary == null) + { + val = null; + return false; + } + return Dictionary.TryGetValue(key, out val); + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IDeleteMediaFilesRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IDeleteMediaFilesRepository.cs index 005c1d62ba..8a5a99b32a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IDeleteMediaFilesRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IDeleteMediaFilesRepository.cs @@ -1,7 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace Umbraco.Core.Persistence.Repositories { + // cannot kill in v7 because it is public, kill in v8 + [Obsolete("Use MediaFileSystem.DeleteMediaFiles instead.", false)] public interface IDeleteMediaFilesRepository { /// @@ -9,6 +12,7 @@ namespace Umbraco.Core.Persistence.Repositories /// /// /// + [Obsolete("Use MediaFileSystem.DeleteMediaFiles instead.", false)] bool DeleteMediaFiles(IEnumerable files); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index 22b9b3c2e4..4b7afe9000 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -68,7 +68,7 @@ namespace Umbraco.Core.Persistence.Repositories sql.Where("umbracoNode.id in (@ids)", new { ids = ids }); } - return ProcessQuery(sql); + return ProcessQuery(sql, new PagingSqlQuery(sql)); } protected override IEnumerable PerformGetByQuery(IQuery query) @@ -78,7 +78,7 @@ namespace Umbraco.Core.Persistence.Repositories var sql = translator.Translate() .OrderBy(x => x.SortOrder, SqlSyntax); - return ProcessQuery(sql); + return ProcessQuery(sql, new PagingSqlQuery(sql)); } #endregion @@ -100,7 +100,7 @@ namespace Umbraco.Core.Persistence.Repositories protected override Sql GetBaseQuery(bool isCount) { - return GetBaseQuery(isCount ? BaseQueryType.Count : BaseQueryType.Full); + return GetBaseQuery(isCount ? BaseQueryType.Count : BaseQueryType.FullSingle); } protected override string GetBaseWhereClause() @@ -143,13 +143,24 @@ namespace Umbraco.Core.Persistence.Repositories var sql = GetBaseQuery(false) .Where(GetBaseWhereClause(), new { Id = id }) .OrderByDescending(x => x.VersionDate, SqlSyntax); - return ProcessQuery(sql, true); + return ProcessQuery(sql, new PagingSqlQuery(sql), true); } - private IEnumerable ProcessQuery(Sql sql, bool withCache = false) + /// + /// This is the underlying method that processes most queries for this repository + /// + /// + /// The full SQL to select all media data + /// + /// + /// The Id SQL to just return all media ids - used to process the properties for the media item + /// + /// + /// + private IEnumerable ProcessQuery(Sql sqlFull, PagingSqlQuery pagingSqlQuery, bool withCache = false) { // fetch returns a list so it's ok to iterate it in this method - var dtos = Database.Fetch(sql); + var dtos = Database.Fetch(sqlFull); var content = new IMedia[dtos.Count]; var defs = new List(); @@ -200,7 +211,7 @@ namespace Umbraco.Core.Persistence.Repositories } // load all properties for all documents from database in 1 query - var propertyData = GetPropertyCollection(sql, defs); + var propertyData = GetPropertyCollection(pagingSqlQuery, defs); // assign var dtoIndex = 0; @@ -257,7 +268,8 @@ namespace Umbraco.Core.Persistence.Repositories query = query .Where(x => x.NodeId > baseId, SqlSyntax) .OrderBy(x => x.NodeId, SqlSyntax); - var xmlItems = ProcessQuery(SqlSyntax.SelectTop(query, groupSize)) + var sql = SqlSyntax.SelectTop(query, groupSize); + var xmlItems = ProcessQuery(sql, new PagingSqlQuery(sql)) .Select(x => new ContentXmlDto { NodeId = x.Id, Xml = serializer(x).ToString() }) .ToList(); @@ -505,7 +517,7 @@ namespace Umbraco.Core.Persistence.Repositories return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, new Tuple("cmsContentVersion", "contentId"), - (sqlFull, sqlIds) => ProcessQuery(sqlFull), orderBy, orderDirection, orderBySystemField, + (sqlFull, pagingSqlQuery) => ProcessQuery(sqlFull, pagingSqlQuery), orderBy, orderDirection, orderBySystemField, filterCallback); } @@ -513,7 +525,7 @@ namespace Umbraco.Core.Persistence.Repositories /// /// Private method to create a media object from a ContentDto /// - /// + /// /// /// /// @@ -525,7 +537,7 @@ namespace Umbraco.Core.Persistence.Repositories var docDef = new DocumentDefinition(dto.NodeId, versionId, media.UpdateDate, media.CreateDate, contentType); - var properties = GetPropertyCollection(docSql, new[] { docDef }); + var properties = GetPropertyCollection(new PagingSqlQuery(docSql), new[] { docDef }); media.Properties = properties[dto.NodeId]; diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs index d824deae5a..95028f99cd 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs @@ -68,7 +68,7 @@ namespace Umbraco.Core.Persistence.Repositories sql.Where("umbracoNode.id in (@ids)", new { ids = ids }); } - return ProcessQuery(sql); + return ProcessQuery(sql, new PagingSqlQuery(sql)); } @@ -90,7 +90,7 @@ namespace Umbraco.Core.Persistence.Repositories baseQuery.Append(new Sql("WHERE umbracoNode.id IN (" + sql.SQL + ")", sql.Arguments)) .OrderBy(x => x.SortOrder); - return ProcessQuery(baseQuery); + return ProcessQuery(baseQuery, new PagingSqlQuery(baseQuery)); } else { @@ -98,7 +98,7 @@ namespace Umbraco.Core.Persistence.Repositories var sql = translator.Translate() .OrderBy(x => x.SortOrder); - return ProcessQuery(sql); + return ProcessQuery(sql, new PagingSqlQuery(sql)); } } @@ -129,7 +129,7 @@ namespace Umbraco.Core.Persistence.Repositories protected override Sql GetBaseQuery(bool isCount) { - return GetBaseQuery(isCount ? BaseQueryType.Count : BaseQueryType.Full); + return GetBaseQuery(isCount ? BaseQueryType.Count : BaseQueryType.FullSingle); } protected override string GetBaseWhereClause() @@ -385,7 +385,7 @@ namespace Umbraco.Core.Persistence.Repositories var sql = GetBaseQuery(false) .Where(GetBaseWhereClause(), new { Id = id }) .OrderByDescending(x => x.VersionDate, SqlSyntax); - return ProcessQuery(sql, true); + return ProcessQuery(sql, new PagingSqlQuery(sql), true); } public void RebuildXmlStructures(Func serializer, int groupSize = 200, IEnumerable contentTypeIds = null) @@ -408,7 +408,8 @@ namespace Umbraco.Core.Persistence.Repositories query = query .Where(x => x.NodeId > baseId) .OrderBy(x => x.NodeId, SqlSyntax); - var xmlItems = ProcessQuery(SqlSyntax.SelectTop(query, groupSize)) + var sql = SqlSyntax.SelectTop(query, groupSize); + var xmlItems = ProcessQuery(sql, new PagingSqlQuery(sql)) .Select(x => new ContentXmlDto { NodeId = x.Id, Xml = serializer(x).ToString() }) .ToList(); @@ -449,7 +450,7 @@ namespace Umbraco.Core.Persistence.Repositories var factory = new MemberFactory(memberType, NodeObjectTypeId, dto.NodeId); var media = factory.BuildEntity(dto); - var properties = GetPropertyCollection(sql, new[] { new DocumentDefinition(dto.NodeId, dto.ContentVersionDto.VersionId, media.UpdateDate, media.CreateDate, memberType) }); + var properties = GetPropertyCollection(new PagingSqlQuery(sql), new[] { new DocumentDefinition(dto.NodeId, dto.ContentVersionDto.VersionId, media.UpdateDate, media.CreateDate, memberType) }); media.Properties = properties[dto.NodeId]; @@ -540,7 +541,7 @@ namespace Umbraco.Core.Persistence.Repositories .OrderByDescending(x => x.VersionDate) .OrderBy(x => x.SortOrder); - return ProcessQuery(sql); + return ProcessQuery(sql, new PagingSqlQuery(sql)); } @@ -601,7 +602,7 @@ namespace Umbraco.Core.Persistence.Repositories return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, new Tuple("cmsMember", "nodeId"), - (sqlFull, sqlIds) => ProcessQuery(sqlFull), orderBy, orderDirection, orderBySystemField, + (sqlFull, sqlIds) => ProcessQuery(sqlFull, sqlIds), orderBy, orderDirection, orderBySystemField, filterCallback); } @@ -641,10 +642,21 @@ namespace Umbraco.Core.Persistence.Repositories return base.GetEntityPropertyNameForOrderBy(orderBy); } - private IEnumerable ProcessQuery(Sql sql, bool withCache = false) + /// + /// This is the underlying method that processes most queries for this repository + /// + /// + /// The full SQL to select all member data + /// + /// + /// The Id SQL to just return all member ids - used to process the properties for the member item + /// + /// + /// + private IEnumerable ProcessQuery(Sql sqlFull, PagingSqlQuery pagingSqlQuery, bool withCache = false) { // fetch returns a list so it's ok to iterate it in this method - var dtos = Database.Fetch(sql); + var dtos = Database.Fetch(sqlFull); var content = new IMember[dtos.Count]; var defs = new List(); @@ -681,7 +693,7 @@ namespace Umbraco.Core.Persistence.Repositories } // load all properties for all documents from database in 1 query - var propertyData = GetPropertyCollection(sql, defs); + var propertyData = GetPropertyCollection(pagingSqlQuery, defs); // assign var dtoIndex = 0; diff --git a/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs b/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs index e5f93d3111..cb5f7c8810 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs @@ -63,6 +63,7 @@ namespace Umbraco.Core.Persistence.Repositories FormatDeleteStatement("cmsContentVersion", "ContentId"), FormatDeleteStatement("cmsContentXml", "nodeId"), FormatDeleteStatement("cmsContent", "nodeId"), + //TODO: Why is this being done? We just delete this exact data in the next line "UPDATE umbracoNode SET parentID = '" + RecycleBinId + "' WHERE trashed = '1' AND nodeObjectType = @NodeObjectType", "DELETE FROM umbracoNode WHERE trashed = '1' AND nodeObjectType = @NodeObjectType" }; @@ -91,14 +92,18 @@ namespace Umbraco.Core.Persistence.Repositories } } + /// + /// A delete statement that will delete anything in the table specified where its PK (keyName) is found in the + /// list of umbracoNode.id that have trashed flag set + /// + /// + /// + /// private string FormatDeleteStatement(string tableName, string keyName) { - //This query works with sql ce and sql server: - //DELETE FROM umbracoUser2NodeNotify WHERE umbracoUser2NodeNotify.nodeId IN - //(SELECT nodeId FROM umbracoUser2NodeNotify as TB1 INNER JOIN umbracoNode as TB2 ON TB1.nodeId = TB2.id WHERE TB2.trashed = '1' AND TB2.nodeObjectType = 'C66BA18E-EAF3-4CFF-8A22-41B16D66A972') return string.Format( - "DELETE FROM {0} WHERE {0}.{1} IN (SELECT TB1.{1} FROM {0} as TB1 INNER JOIN umbracoNode as TB2 ON TB1.{1} = TB2.id WHERE TB2.trashed = '1' AND TB2.nodeObjectType = @NodeObjectType)", + "DELETE FROM {0} WHERE {0}.{1} IN (SELECT id FROM umbracoNode WHERE trashed = '1' AND nodeObjectType = @NodeObjectType)", tableName, keyName); } diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index e17de65786..d802d10c66 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -432,7 +432,7 @@ namespace Umbraco.Core.Persistence.Repositories /// orderBy protected IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, Tuple nodeIdSelect, - Func> processQuery, + Func, IEnumerable> processQuery, string orderBy, Direction orderDirection, bool orderBySystemField, @@ -443,7 +443,7 @@ namespace Umbraco.Core.Persistence.Repositories // Get base query for returning IDs var sqlBaseIds = GetBaseQuery(BaseQueryType.Ids); // Get base query for returning all data - var sqlBaseFull = GetBaseQuery(BaseQueryType.Full); + var sqlBaseFull = GetBaseQuery(BaseQueryType.FullMultiple); if (query == null) query = new Query(); var translatorIds = new SqlTranslator(sqlBaseIds, query); @@ -466,7 +466,7 @@ namespace Umbraco.Core.Persistence.Repositories // the pageResult, then the GetAll will actually return ALL records in the db. if (pagedResult.Items.Any()) { - //Crete the inner paged query that was used above to get the paged result, we'll use that as the inner sub query + //Create the inner paged query that was used above to get the paged result, we'll use that as the inner sub query var args = sqlNodeIdsWithSort.Arguments; string sqlStringCount, sqlStringPage; Database.BuildPageQueries(pageIndex * pageSize, pageSize, sqlNodeIdsWithSort.SQL, ref args, out sqlStringCount, out sqlStringPage); @@ -486,8 +486,8 @@ namespace Umbraco.Core.Persistence.Repositories var fullQuery = GetSortedSqlForPagedResults( GetFilteredSqlForPagedResults(fullQueryWithPagedInnerJoin, defaultFilter), orderDirection, orderBy, orderBySystemField, nodeIdSelect); - - return processQuery(fullQuery, sqlNodeIdsWithSort); + + return processQuery(fullQuery, new PagingSqlQuery(Database, sqlNodeIdsWithSort, pageIndex, pageSize)); } else { @@ -497,18 +497,47 @@ namespace Umbraco.Core.Persistence.Repositories return result; } + /// + /// Gets the property collection for a non-paged query + /// + /// + /// + /// protected IDictionary GetPropertyCollection( - Sql docSql, + Sql sql, + IReadOnlyCollection documentDefs) + { + return GetPropertyCollection(new PagingSqlQuery(sql), documentDefs); + } + + /// + /// Gets the property collection for a query + /// + /// + /// + /// + protected IDictionary GetPropertyCollection( + PagingSqlQuery pagingSqlQuery, IReadOnlyCollection documentDefs) { if (documentDefs.Count == 0) return new Dictionary(); + //initialize to the query passed in + var docSql = pagingSqlQuery.PrePagedSql; + //we need to parse the original SQL statement and reduce the columns to just cmsContent.nodeId, cmsContentVersion.VersionId so that we can use // the statement to go get the property data for all of the items by using an inner join var parsedOriginalSql = "SELECT {0} " + docSql.SQL.Substring(docSql.SQL.IndexOf("FROM", StringComparison.Ordinal)); - //now remove everything from an Orderby clause and beyond - if (parsedOriginalSql.InvariantContains("ORDER BY ")) + + if (pagingSqlQuery.HasPaging) { + //if this is a paged query, build the paged query with the custom column substitution, then re-assign + docSql = pagingSqlQuery.BuildPagedQuery("{0}"); + parsedOriginalSql = docSql.SQL; + } + else if (parsedOriginalSql.InvariantContains("ORDER BY ")) + { + //now remove everything from an Orderby clause and beyond if this is unpaged data parsedOriginalSql = parsedOriginalSql.Substring(0, parsedOriginalSql.LastIndexOf("ORDER BY ", StringComparison.Ordinal)); } @@ -525,7 +554,7 @@ WHERE EXISTS( INNER JOIN cmsPropertyType ON b.datatypeNodeId = cmsPropertyType.dataTypeId INNER JOIN - (" + string.Format(parsedOriginalSql, "DISTINCT cmsContent.contentType") + @") as docData + (" + string.Format(parsedOriginalSql, "cmsContent.contentType") + @") as docData ON cmsPropertyType.contentTypeId = docData.contentType WHERE a.id = b.id)", docSql.Arguments); @@ -646,28 +675,7 @@ ORDER BY contentNodeId, propertytypeid return result; } - - public class DocumentDefinition - { - /// - /// Initializes a new instance of the class. - /// - public DocumentDefinition(int id, Guid version, DateTime versionDate, DateTime createDate, IContentTypeComposition composition) - { - Id = id; - Version = version; - VersionDate = versionDate; - CreateDate = createDate; - Composition = composition; - } - - public int Id { get; set; } - public Guid Version { get; set; } - public DateTime VersionDate { get; set; } - public DateTime CreateDate { get; set; } - public IContentTypeComposition Composition { get; set; } - } - + protected virtual string GetDatabaseFieldNameForOrderBy(string orderBy) { // Translate the passed order by field (which were originally defined for in-memory object sorting @@ -763,5 +771,92 @@ ORDER BY contentNodeId, propertytypeid /// /// protected abstract Sql GetBaseQuery(BaseQueryType queryType); + + internal class DocumentDefinition + { + /// + /// Initializes a new instance of the class. + /// + public DocumentDefinition(int id, Guid version, DateTime versionDate, DateTime createDate, IContentTypeComposition composition) + { + Id = id; + Version = version; + VersionDate = versionDate; + CreateDate = createDate; + Composition = composition; + } + + public int Id { get; set; } + public Guid Version { get; set; } + public DateTime VersionDate { get; set; } + public DateTime CreateDate { get; set; } + public IContentTypeComposition Composition { get; set; } + } + + /// + /// An object representing a query that may contain paging information + /// + internal class PagingSqlQuery + { + public Sql PrePagedSql { get; private set; } + + public PagingSqlQuery(Sql prePagedSql) + { + PrePagedSql = prePagedSql; + } + + public virtual bool HasPaging + { + get { return false; } + } + + public virtual Sql BuildPagedQuery(string selectColumns) + { + throw new InvalidOperationException("This query has no paging information"); + } + } + + /// + /// An object representing a query that contains paging information + /// + /// + internal class PagingSqlQuery : PagingSqlQuery + { + private readonly Database _db; + private readonly long _pageIndex; + private readonly int _pageSize; + + public PagingSqlQuery(Database db, Sql prePagedSql, long pageIndex, int pageSize) : base(prePagedSql) + { + _db = db; + _pageIndex = pageIndex; + _pageSize = pageSize; + } + + public override bool HasPaging + { + get { return _pageSize > 0; } + } + + /// + /// Creates a paged query based on the original query and subtitutes the selectColumns specified + /// + /// + /// + public override Sql BuildPagedQuery(string selectColumns) + { + if (HasPaging == false) throw new InvalidOperationException("This query has no paging information"); + + var resultSql = string.Format("SELECT {0} {1}", selectColumns, PrePagedSql.SQL.Substring(PrePagedSql.SQL.IndexOf("FROM", StringComparison.Ordinal))); + + //this query is meant to be paged so we need to generate the paging syntax + //Create the inner paged query that was used above to get the paged result, we'll use that as the inner sub query + var args = PrePagedSql.Arguments; + string sqlStringCount, sqlStringPage; + _db.BuildPageQueries(_pageIndex * _pageSize, _pageSize, resultSql, ref args, out sqlStringCount, out sqlStringPage); + + return new Sql(sqlStringPage, args); + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs index 1231765f20..f0bafdacf7 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs @@ -1,7 +1,23 @@ -namespace Umbraco.Core.Persistence.SqlSyntax +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; + +namespace Umbraco.Core.Persistence.SqlSyntax { internal static class SqlSyntaxProviderExtensions { + public static IEnumerable GetDefinedIndexesDefinitions(this ISqlSyntaxProvider sql, Database db) + { + return sql.GetDefinedIndexes(db) + .Select(x => new DbIndexDefinition() + { + TableName = x.Item1, + IndexName = x.Item2, + ColumnName = x.Item3, + IsUnique = x.Item4 + }).ToArray(); + } + /// /// Returns the quotes tableName.columnName combo /// diff --git a/src/Umbraco.Core/PropertyEditors/PreValueField.cs b/src/Umbraco.Core/PropertyEditors/PreValueField.cs index 3cf4e960ea..2b66f7a6a8 100644 --- a/src/Umbraco.Core/PropertyEditors/PreValueField.cs +++ b/src/Umbraco.Core/PropertyEditors/PreValueField.cs @@ -15,6 +15,7 @@ namespace Umbraco.Core.PropertyEditors public PreValueField() { Validators = new List(); + Config = new Dictionary(); //check for an attribute and fill the values var att = GetType().GetCustomAttribute(false); @@ -79,5 +80,11 @@ namespace Umbraco.Core.PropertyEditors /// [JsonProperty("validation", ItemConverterType = typeof(ManifestValidatorConverter))] public List Validators { get; private set; } + + /// + /// This allows for custom configuration to be injected into the pre-value editor + /// + [JsonProperty("config")] + public IDictionary Config { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditor.cs index 6cecc9dff5..c2498ecc7a 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyEditor.cs @@ -38,6 +38,7 @@ namespace Umbraco.Core.PropertyEditors IsParameterEditor = _attribute.IsParameterEditor; Icon = _attribute.Icon; Group = _attribute.Group; + IsDeprecated = _attribute.IsDeprecated; } } @@ -90,6 +91,9 @@ namespace Umbraco.Core.PropertyEditors get { return CreateValueEditor(); } } + [JsonIgnore] + public bool IsDeprecated { get; internal set; } + [JsonIgnore] IValueEditor IParameterEditor.ValueEditor { diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditorAttribute.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditorAttribute.cs index d120753185..41e4ccc74e 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyEditorAttribute.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyEditorAttribute.cs @@ -60,6 +60,12 @@ namespace Umbraco.Core.PropertyEditors public string ValueType { get; set; } public bool IsParameterEditor { get; set; } + /// + /// If set to true, this property editor will not show up in the DataType's drop down list + /// if there is not already one of them chosen for a DataType + /// + public bool IsDeprecated { get; set; } + /// /// If this is is true than the editor will be displayed full width without a label /// diff --git a/src/Umbraco.Core/Scoping/ActionTime.cs b/src/Umbraco.Core/Scoping/ActionTime.cs deleted file mode 100644 index 7cb5e3ed35..0000000000 --- a/src/Umbraco.Core/Scoping/ActionTime.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace Umbraco.Core.Scoping -{ - [Flags] - public enum ActionTime - { - None = 0, - BeforeCommit = 1, - BeforeEvents = 2, - BeforeDispose = 4 - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Scoping/IInstanceIdentifiable.cs b/src/Umbraco.Core/Scoping/IInstanceIdentifiable.cs new file mode 100644 index 0000000000..4c88e1c1b5 --- /dev/null +++ b/src/Umbraco.Core/Scoping/IInstanceIdentifiable.cs @@ -0,0 +1,9 @@ +using System; + +namespace Umbraco.Core.Scoping +{ + public interface IInstanceIdentifiable + { + Guid InstanceId { get; } + } +} diff --git a/src/Umbraco.Core/Scoping/IScope.cs b/src/Umbraco.Core/Scoping/IScope.cs index a0d994b2f2..4f178b80bc 100644 --- a/src/Umbraco.Core/Scoping/IScope.cs +++ b/src/Umbraco.Core/Scoping/IScope.cs @@ -8,7 +8,7 @@ namespace Umbraco.Core.Scoping /// /// Represents a scope. /// - public interface IScope : IDisposable + public interface IScope : IDisposable, IInstanceIdentifiable { /// /// Gets the scope database. @@ -38,10 +38,8 @@ namespace Umbraco.Core.Scoping /// /// Completes the scope. /// - void Complete(); - -#if DEBUG_SCOPES - Guid InstanceId { get; } -#endif + /// A value indicating whether the scope has been successfully completed. + /// Can return false if any child scope has not completed. + bool Complete(); } } diff --git a/src/Umbraco.Core/Scoping/IScopeInternal.cs b/src/Umbraco.Core/Scoping/IScopeInternal.cs index 86276fc7f9..c1c28b41fe 100644 --- a/src/Umbraco.Core/Scoping/IScopeInternal.cs +++ b/src/Umbraco.Core/Scoping/IScopeInternal.cs @@ -7,7 +7,7 @@ namespace Umbraco.Core.Scoping internal interface IScopeInternal : IScope { IScopeInternal ParentScope { get; } - EventsDispatchMode DispatchMode { get; } + bool CallContext { get; } IsolationLevel IsolationLevel { get; } UmbracoDatabase DatabaseOrNull { get; } EventMessages MessagesOrNull { get; } diff --git a/src/Umbraco.Core/Scoping/IScopeProvider.cs b/src/Umbraco.Core/Scoping/IScopeProvider.cs index 6833eac699..754fb63aa7 100644 --- a/src/Umbraco.Core/Scoping/IScopeProvider.cs +++ b/src/Umbraco.Core/Scoping/IScopeProvider.cs @@ -1,4 +1,5 @@ -using System.Data; +using System; +using System.Data; using Umbraco.Core.Events; #if DEBUG_SCOPES using System.Collections.Generic; @@ -23,8 +24,9 @@ namespace Umbraco.Core.Scoping IScope CreateScope( IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, - EventsDispatchMode dispatchMode = EventsDispatchMode.Unspecified, - bool? scopeFileSystems = null); + IEventDispatcher eventDispatcher = null, + bool? scopeFileSystems = null, + bool callContext = false); /// /// Creates a detached scope. @@ -37,17 +39,18 @@ namespace Umbraco.Core.Scoping IScope CreateDetachedScope( IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, - EventsDispatchMode dispatchMode = EventsDispatchMode.Unspecified, + IEventDispatcher eventDispatcher = null, bool? scopeFileSystems = null); /// /// Attaches a scope. /// /// The scope to attach. + /// A value indicating whether to force usage of call context. /// /// Only a scope created by can be attached. /// - void AttachScope(IScope scope); + void AttachScope(IScope scope, bool callContext = false); /// /// Detaches a scope. @@ -64,7 +67,9 @@ namespace Umbraco.Core.Scoping ScopeContext Context { get; } #if DEBUG_SCOPES + Dictionary CallContextObjects { get; } IEnumerable ScopeInfos { get; } + ScopeInfo GetScopeInfo(IScope scope); #endif } } diff --git a/src/Umbraco.Core/Scoping/NoScope.cs b/src/Umbraco.Core/Scoping/NoScope.cs index b08f10ad0e..2d8fa245b0 100644 --- a/src/Umbraco.Core/Scoping/NoScope.cs +++ b/src/Umbraco.Core/Scoping/NoScope.cs @@ -24,10 +24,11 @@ namespace Umbraco.Core.Scoping #endif } -#if DEBUG_SCOPES private readonly Guid _instanceId = Guid.NewGuid(); public Guid InstanceId { get { return _instanceId; } } -#endif + + /// + public bool CallContext { get { return false; } } /// public RepositoryCacheMode RepositoryCacheMode @@ -75,7 +76,7 @@ namespace Umbraco.Core.Scoping } /// - public void Complete() + public bool Complete() { throw new NotSupportedException(); } @@ -100,15 +101,13 @@ namespace Umbraco.Core.Scoping if (_database != null) _database.Dispose(); - _scopeProvider.AmbientScope = null; - _scopeProvider.AmbientContext = null; + _scopeProvider.SetAmbient(null); _disposed = true; GC.SuppressFinalize(this); } public IScopeInternal ParentScope { get { return null; } } - public EventsDispatchMode DispatchMode { get {return EventsDispatchMode.Unspecified; } } public IsolationLevel IsolationLevel { get {return IsolationLevel.Unspecified; } } public bool ScopedFileSystems { get { return false; } } public void ChildCompleted(bool? completed) { } diff --git a/src/Umbraco.Core/Scoping/Scope.cs b/src/Umbraco.Core/Scoping/Scope.cs index d6aee52205..ab66be1bd2 100644 --- a/src/Umbraco.Core/Scoping/Scope.cs +++ b/src/Umbraco.Core/Scoping/Scope.cs @@ -16,9 +16,9 @@ namespace Umbraco.Core.Scoping private readonly ScopeProvider _scopeProvider; private readonly IsolationLevel _isolationLevel; private readonly RepositoryCacheMode _repositoryCacheMode; - private readonly EventsDispatchMode _dispatchMode; private readonly bool? _scopeFileSystem; private readonly ScopeContext _scopeContext; + private bool _callContext; private bool _disposed; private bool? _completed; @@ -35,15 +35,17 @@ namespace Umbraco.Core.Scoping Scope parent, ScopeContext scopeContext, bool detachable, IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, - EventsDispatchMode dispatchMode = EventsDispatchMode.Unspecified, - bool? scopeFileSystems = null) + IEventDispatcher eventDispatcher = null, + bool? scopeFileSystems = null, + bool callContext = false) { _scopeProvider = scopeProvider; _scopeContext = scopeContext; _isolationLevel = isolationLevel; _repositoryCacheMode = repositoryCacheMode; - _dispatchMode = dispatchMode; + _eventDispatcher = eventDispatcher; _scopeFileSystem = scopeFileSystems; + _callContext = callContext; Detachable = detachable; #if DEBUG_SCOPES @@ -74,9 +76,9 @@ namespace Umbraco.Core.Scoping if (repositoryCacheMode != RepositoryCacheMode.Unspecified && parent.RepositoryCacheMode != repositoryCacheMode) throw new ArgumentException("Cannot be different from parent.", "repositoryCacheMode"); - // cannot specify a different mode! - if (_dispatchMode != EventsDispatchMode.Unspecified && parent._dispatchMode != dispatchMode) - throw new ArgumentException("Cannot be different from parent.", "dispatchMode"); + // cannot specify a dispatcher! + if (_eventDispatcher != null) + throw new ArgumentException("Cannot be specified on nested scope.", "eventDispatcher"); // cannot specify a different fs scope! if (scopeFileSystems != null && parent._scopeFileSystem != scopeFileSystems) @@ -97,30 +99,31 @@ namespace Umbraco.Core.Scoping ScopeContext scopeContext, IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, - EventsDispatchMode dispatchMode = EventsDispatchMode.Unspecified, - bool? scopeFileSystems = null) - : this(scopeProvider, null, scopeContext, detachable, isolationLevel, repositoryCacheMode, dispatchMode, scopeFileSystems) - { - } + IEventDispatcher eventDispatcher = null, + bool? scopeFileSystems = null, + bool callContext = false) + : this(scopeProvider, null, scopeContext, detachable, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext) + { } // initializes a new scope in a nested scopes chain, with its parent public Scope(ScopeProvider scopeProvider, Scope parent, IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, - EventsDispatchMode dispatchMode = EventsDispatchMode.Unspecified, - bool? scopeFileSystems = null) - : this(scopeProvider, parent, null, false, isolationLevel, repositoryCacheMode, dispatchMode, scopeFileSystems) - { - } + IEventDispatcher eventDispatcher = null, + bool? scopeFileSystems = null, + bool callContext = false) + : this(scopeProvider, parent, null, false, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext) + { } // initializes a new scope, replacing a NoScope instance public Scope(ScopeProvider scopeProvider, NoScope noScope, ScopeContext scopeContext, IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, - EventsDispatchMode dispatchMode = EventsDispatchMode.Unspecified, - bool? scopeFileSystems = null) - : this(scopeProvider, null, scopeContext, false, isolationLevel, repositoryCacheMode, dispatchMode, scopeFileSystems) + IEventDispatcher eventDispatcher = null, + bool? scopeFileSystems = null, + bool callContext = false) + : this(scopeProvider, null, scopeContext, false, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext) { // steal everything from NoScope _database = noScope.DatabaseOrNull; @@ -130,10 +133,20 @@ namespace Umbraco.Core.Scoping throw new Exception("NoScope instance is not free."); } -#if DEBUG_SCOPES private readonly Guid _instanceId = Guid.NewGuid(); public Guid InstanceId { get { return _instanceId; } } -#endif + + // a value indicating whether to force call-context + public bool CallContext + { + get + { + if (_callContext) return true; + if (ParentScope != null) return ParentScope.CallContext; + return false; + } + set { _callContext = value; } + } public bool ScopedFileSystems { @@ -144,16 +157,6 @@ namespace Umbraco.Core.Scoping } } - public EventsDispatchMode DispatchMode - { - get - { - if (_dispatchMode != EventsDispatchMode.Unspecified) return _dispatchMode; - if (ParentScope != null) return ParentScope.DispatchMode; - return EventsDispatchMode.Scope; - } - } - /// public RepositoryCacheMode RepositoryCacheMode { @@ -184,6 +187,8 @@ namespace Umbraco.Core.Scoping // the parent scope (in a nested scopes chain) public IScopeInternal ParentScope { get; set; } + public bool Attached { get; set; } + // the original scope (when attaching a detachable scope) public IScopeInternal OrigScope { get; set; } @@ -290,15 +295,16 @@ namespace Umbraco.Core.Scoping { EnsureNotDisposed(); if (ParentScope != null) return ParentScope.Events; - return _eventDispatcher ?? (_eventDispatcher = new ScopeEventDispatcher(DispatchMode)); + return _eventDispatcher ?? (_eventDispatcher = new ScopeEventDispatcher()); } } /// - public void Complete() + public bool Complete() { if (_completed.HasValue == false) _completed = true; + return _completed.Value; } public void Reset() @@ -324,7 +330,18 @@ namespace Umbraco.Core.Scoping EnsureNotDisposed(); if (this != _scopeProvider.AmbientScope) + { +#if DEBUG_SCOPES + var ambient = _scopeProvider.AmbientScope; + Logging.LogHelper.Debug("Dispose error (" + (ambient == null ? "no" : "other") + " ambient)"); + if (ambient == null) + throw new InvalidOperationException("Not the ambient scope (no ambient scope)."); + var infos = _scopeProvider.GetScopeInfo(ambient); + throw new InvalidOperationException("Not the ambient scope (see current ambient ctor stack trace).\r\n" + infos.CtorStack); +#else throw new InvalidOperationException("Not the ambient scope."); +#endif + } #if DEBUG_SCOPES _scopeProvider.Disposed(this); @@ -348,7 +365,7 @@ namespace Umbraco.Core.Scoping var completed = _completed.HasValue && _completed.Value; // deal with database - bool ex = false; + var databaseException = false; if (_database != null) { try @@ -360,7 +377,7 @@ namespace Umbraco.Core.Scoping } catch { - ex = true; + databaseException = true; throw; } finally @@ -368,7 +385,7 @@ namespace Umbraco.Core.Scoping _database.Dispose(); _database = null; - if (ex) + if (databaseException) RobustExit(false, true); } } @@ -376,9 +393,22 @@ namespace Umbraco.Core.Scoping RobustExit(completed, false); } - private void RobustExit(bool completed, bool kabum) + // this chains some try/finally blocks to + // - complete and dispose the scoped filesystems + // - deal with events if appropriate + // - remove the scope context if it belongs to this scope + // - deal with detachable scopes + // here, + // - completed indicates whether the scope has been completed + // can be true or false, but in both cases the scope is exiting + // in a normal way + // - onException indicates whether completing/aborting the database + // transaction threw an exception, in which case 'completed' has + // to be false + events don't trigger and we just to some cleanup + // to ensure we don't leave a scope around, etc + private void RobustExit(bool completed, bool onException) { - if (kabum) completed = false; + if (onException) completed = false; TryFinally(() => { @@ -392,7 +422,7 @@ namespace Umbraco.Core.Scoping }, () => { // deal with events - if (kabum == false && _eventDispatcher != null) + if (onException == false && _eventDispatcher != null) _eventDispatcher.ScopeExit(completed); }, () => { @@ -405,7 +435,7 @@ namespace Umbraco.Core.Scoping } finally { - _scopeProvider.AmbientContext = null; + _scopeProvider.SetAmbient(null); } } }, () => @@ -413,8 +443,10 @@ namespace Umbraco.Core.Scoping if (Detachable) { // get out of the way, restore original - _scopeProvider.AmbientScope = OrigScope; - _scopeProvider.AmbientContext = OrigContext; + _scopeProvider.SetAmbient(OrigScope, OrigContext); + Attached = false; + OrigScope = null; + OrigContext = null; } }); } diff --git a/src/Umbraco.Core/Scoping/ScopeContext.cs b/src/Umbraco.Core/Scoping/ScopeContext.cs index 7503271b5a..cca0be560d 100644 --- a/src/Umbraco.Core/Scoping/ScopeContext.cs +++ b/src/Umbraco.Core/Scoping/ScopeContext.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; namespace Umbraco.Core.Scoping { - public class ScopeContext + public class ScopeContext : IInstanceIdentifiable { private Dictionary _enlisted; @@ -27,6 +27,9 @@ namespace Umbraco.Core.Scoping throw new AggregateException("Exceptions were thrown by listed actions.", exceptions); } + private readonly Guid _instanceId = Guid.NewGuid(); + public Guid InstanceId { get { return _instanceId; } } + private IDictionary Enlisted { get diff --git a/src/Umbraco.Core/Scoping/ScopeProvider.cs b/src/Umbraco.Core/Scoping/ScopeProvider.cs index 2f00e5b862..c500a633d0 100644 --- a/src/Umbraco.Core/Scoping/ScopeProvider.cs +++ b/src/Umbraco.Core/Scoping/ScopeProvider.cs @@ -1,11 +1,14 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Data; -using System.Linq; using System.Runtime.Remoting.Messaging; using System.Web; using Umbraco.Core.Events; using Umbraco.Core.Persistence; +#if DEBUG_SCOPES +using System.Linq; +#endif namespace Umbraco.Core.Scoping { @@ -24,17 +27,17 @@ namespace Umbraco.Core.Scoping SafeCallContext.Register( () => { - var scope = StaticAmbientScope; - var context = StaticAmbientContext; - StaticAmbientScope = null; - StaticAmbientContext = null; + var scope = GetCallContextObject(ScopeItemKey); + var context = GetCallContextObject(ContextItemKey); + SetCallContextObject(ScopeItemKey, null); + SetCallContextObject(ContextItemKey, null); return Tuple.Create(scope, context); }, o => { // cannot re-attached over leaked scope/context // except of course over NoScope (which leaks) - var ambientScope = StaticAmbientScope; + var ambientScope = GetCallContextObject(ScopeItemKey); if (ambientScope != null) { var ambientNoScope = ambientScope as NoScope; @@ -44,59 +47,210 @@ namespace Umbraco.Core.Scoping // this should rollback any pending transaction ambientNoScope.Dispose(); } - if (StaticAmbientContext != null) throw new Exception("Found leaked context when restoring call context."); + if (GetCallContextObject(ContextItemKey) != null) + throw new Exception("Found leaked context when restoring call context."); - var t = (Tuple)o; - StaticAmbientScope = t.Item1; - StaticAmbientContext = t.Item2; + var t = (Tuple) o; + SetCallContextObject(ScopeItemKey, t.Item1); + SetCallContextObject(ContextItemKey, t.Item2); }); } public IDatabaseFactory2 DatabaseFactory { get; private set; } + #region Context + + // objects that go into the logical call context better be serializable else they'll eventually + // cause issues whenever some cross-AppDomain code executes - could be due to ReSharper running + // tests, any other things (see https://msdn.microsoft.com/en-us/library/dn458353(v=vs.110).aspx), + // but we don't want to make all of our objects serializable since they are *not* meant to be + // used in cross-AppDomain scenario anyways. + // in addition, whatever goes into the logical call context is serialized back and forth any + // time cross-AppDomain code executes, so if we put an "object" there, we'll can *another* + // "object" instance - and so we cannot use a random object as a key. + // so what we do is: we register a guid in the call context, and we keep a table mapping those + // guids to the actual objects. the guid serializes back and forth without causing any issue, + // and we can retrieve the actual objects from the table. + // only issue: how are we supposed to clear the table? we can't, really. objects should take + // care of de-registering themselves from context. + // everything we use does, except the NoScope scope, which just stays there + // + // during tests, NoScope can to into call context... nothing much we can do about it + + private static readonly object StaticCallContextObjectsLock = new object(); + private static readonly Dictionary StaticCallContextObjects + = new Dictionary(); + +#if DEBUG_SCOPES + public Dictionary CallContextObjects + { + get + { + lock (StaticCallContextObjectsLock) + { + // capture in a dictionary + return StaticCallContextObjects.ToDictionary(x => x.Key, x => x.Value); + } + } + } +#endif + + private static T GetCallContextObject(string key) + where T : class + { + var objectKey = CallContext.LogicalGetData(key).AsGuid(); + lock (StaticCallContextObjectsLock) + { + object callContextObject; + if (StaticCallContextObjects.TryGetValue(objectKey, out callContextObject)) + { +#if DEBUG_SCOPES + //Logging.LogHelper.Debug("GotObject " + objectKey.ToString("N").Substring(0, 8)); +#endif + return (T) callContextObject; + } +#if DEBUG_SCOPES + //Logging.LogHelper.Debug("MissedObject " + objectKey.ToString("N").Substring(0, 8)); +#endif + return null; + } + } + + private static void SetCallContextObject(string key, IInstanceIdentifiable value) + { +#if DEBUG_SCOPES + // manage the 'context' that contains the scope (null, "http" or "call") + // only for scopes of course! + if (key == ScopeItemKey) + { + // first, null-register the existing value + var ambientKey = CallContext.LogicalGetData(ScopeItemKey).AsGuid(); + object o = null; + lock (StaticCallContextObjectsLock) + { + if (ambientKey != default(Guid)) + StaticCallContextObjects.TryGetValue(ambientKey, out o); + } + var ambientScope = o as IScope; + if (ambientScope != null) RegisterContext(ambientScope, null); + // then register the new value + var scope = value as IScope; + if (scope != null) RegisterContext(scope, "call"); + } +#endif + if (value == null) + { + var objectKey = CallContext.LogicalGetData(key).AsGuid(); + CallContext.FreeNamedDataSlot(key); + if (objectKey == default (Guid)) return; + lock (StaticCallContextObjectsLock) + { +#if DEBUG_SCOPES + //Logging.LogHelper.Debug("RemoveObject " + objectKey.ToString("N").Substring(0, 8)); +#endif + StaticCallContextObjects.Remove(objectKey); + } + } + else + { + // note - we are *not* detecting an already-existing value + // because our code in this class *always* sets to null before + // setting to a real value + var objectKey = value.InstanceId; + lock (StaticCallContextObjectsLock) + { +#if DEBUG_SCOPES + //Logging.LogHelper.Debug("AddObject " + objectKey.ToString("N").Substring(0, 8)); +#endif + StaticCallContextObjects.Add(objectKey, value); + } + CallContext.LogicalSetData(key, objectKey); + } + } + + internal static Func HttpContextItemsGetter { get; set; } + + private static IDictionary HttpContextItems + { + get + { + return HttpContextItemsGetter == null + ? (HttpContext.Current == null ? null : HttpContext.Current.Items) + : HttpContextItemsGetter(); + } + } + + public static T GetHttpContextObject(string key, bool required = true) + where T : class + { + var httpContextItems = HttpContextItems; + if (httpContextItems != null) + return (T)httpContextItems[key]; + if (required) + throw new Exception("HttpContext.Current is null."); + return null; + } + + private static bool SetHttpContextObject(string key, object value, bool required = true) + { + var httpContextItems = HttpContextItems; + if (httpContextItems == null) + { + if (required) + throw new Exception("HttpContext.Current is null."); + return false; + } +#if DEBUG_SCOPES + // manage the 'context' that contains the scope (null, "http" or "call") + // only for scopes of course! + if (key == ScopeItemKey) + { + // first, null-register the existing value + var ambientScope = (IScope)httpContextItems[ScopeItemKey]; + if (ambientScope != null) RegisterContext(ambientScope, null); + // then register the new value + var scope = value as IScope; + if (scope != null) RegisterContext(scope, "http"); + } +#endif + if (value == null) + httpContextItems.Remove(key); + else + httpContextItems[key] = value; + return true; + } + +#endregion + #region Ambient Context internal const string ContextItemKey = "Umbraco.Core.Scoping.ScopeContext"; - private static ScopeContext CallContextContextValue + internal static ScopeContext AmbientContextInternal { - get { return (ScopeContext)CallContext.LogicalGetData(ContextItemKey); } - set + get { - if (value == null) CallContext.FreeNamedDataSlot(ContextItemKey); - else CallContext.LogicalSetData(ContextItemKey, value); + // try http context, fallback onto call context + var value = GetHttpContextObject(ContextItemKey, false); + return value ?? GetCallContextObject(ContextItemKey); } - } - - private static ScopeContext HttpContextContextValue - { - get { return (ScopeContext)HttpContext.Current.Items[ContextItemKey]; } set { - if (value == null) - HttpContext.Current.Items.Remove(ContextItemKey); - else - HttpContext.Current.Items[ContextItemKey] = value; - } - } + // clear both + SetHttpContextObject(ContextItemKey, null, false); + SetCallContextObject(ContextItemKey, null); + if (value == null) return; - private static ScopeContext StaticAmbientContext - { - get { return HttpContext.Current == null ? CallContextContextValue : HttpContextContextValue; } - set - { - if (HttpContext.Current == null) - CallContextContextValue = value; - else - HttpContextContextValue = value; + // set http/call context + if (SetHttpContextObject(ContextItemKey, value, false) == false) + SetCallContextObject(ContextItemKey, value); } } /// public ScopeContext AmbientContext { - get { return StaticAmbientContext; } - set { StaticAmbientContext = value; } + get { return AmbientContextInternal; } } #endregion @@ -109,86 +263,84 @@ namespace Umbraco.Core.Scoping // only 1 instance which can be disposed and disposed again private static readonly ScopeReference StaticScopeReference = new ScopeReference(new ScopeProvider(null)); - private static IScopeInternal CallContextValue + internal static IScopeInternal AmbientScopeInternal { - get { return (IScopeInternal) CallContext.LogicalGetData(ScopeItemKey); } - set + get { -#if DEBUG_SCOPES - // manage the 'context' that contains the scope (null, "http" or "lcc") - var ambientScope = (IScope) CallContext.LogicalGetData(ScopeItemKey); - if (ambientScope != null) RegisterContext(ambientScope, null); - if (value != null) RegisterContext(value, "lcc"); -#endif - if (value == null) CallContext.FreeNamedDataSlot(ScopeItemKey); - else CallContext.LogicalSetData(ScopeItemKey, value); + // try http context, fallback onto call context + var value = GetHttpContextObject(ScopeItemKey, false); + return value ?? GetCallContextObject(ScopeItemKey); } - } - - private static IScopeInternal HttpContextValue - { - get { return (IScopeInternal) HttpContext.Current.Items[ScopeItemKey]; } set { -#if DEBUG_SCOPES - // manage the 'context' that contains the scope (null, "http" or "lcc") - var ambientScope = (IScope) HttpContext.Current.Items[ScopeItemKey]; - if (ambientScope != null) RegisterContext(ambientScope, null); - if (value != null) RegisterContext(value, "http"); -#endif - if (value == null) - { - HttpContext.Current.Items.Remove(ScopeItemKey); - HttpContext.Current.Items.Remove(ScopeRefItemKey); - } - else - { - HttpContext.Current.Items[ScopeItemKey] = value; - if (HttpContext.Current.Items[ScopeRefItemKey] == null) - HttpContext.Current.Items[ScopeRefItemKey] = StaticScopeReference; - } - } - } + // clear both + SetHttpContextObject(ScopeItemKey, null, false); + SetHttpContextObject(ScopeRefItemKey, null, false); + SetCallContextObject(ScopeItemKey, null); + if (value == null) return; - private static IScopeInternal StaticAmbientScope - { - get { return HttpContext.Current == null ? CallContextValue : HttpContextValue; } - set - { - if (HttpContext.Current == null) - CallContextValue = value; + // set http/call context + if (value.CallContext == false && SetHttpContextObject(ScopeItemKey, value, false)) + SetHttpContextObject(ScopeRefItemKey, StaticScopeReference); else - HttpContextValue = value; + SetCallContextObject(ScopeItemKey, value); } } /// public IScopeInternal AmbientScope { - get { return StaticAmbientScope; } - set { StaticAmbientScope = value; } + get { return AmbientScopeInternal; } + internal set { AmbientScopeInternal = value; } } /// public IScopeInternal GetAmbientOrNoScope() { - return AmbientScope ?? (AmbientScope = new NoScope(this)); + return AmbientScope ?? (AmbientScope = new NoScope(this)); } #endregion + public void SetAmbient(IScopeInternal scope, ScopeContext context = null) + { + // clear all + SetHttpContextObject(ScopeItemKey, null, false); + SetHttpContextObject(ScopeRefItemKey, null, false); + SetCallContextObject(ScopeItemKey, null); + SetHttpContextObject(ContextItemKey, null, false); + SetCallContextObject(ContextItemKey, null); + if (scope == null) + { + if (context != null) + throw new ArgumentException("Must be null if scope is null.", "context"); + return; + } + + if (scope.CallContext == false && SetHttpContextObject(ScopeItemKey, scope, false)) + { + SetHttpContextObject(ScopeRefItemKey, StaticScopeReference); + SetHttpContextObject(ContextItemKey, context); + } + else + { + SetCallContextObject(ScopeItemKey, scope); + SetCallContextObject(ContextItemKey, context); + } + } + /// public IScope CreateDetachedScope( IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, - EventsDispatchMode dispatchMode = EventsDispatchMode.Unspecified, + IEventDispatcher eventDispatcher = null, bool? scopeFileSystems = null) { - return new Scope(this, true, null, isolationLevel, repositoryCacheMode, dispatchMode, scopeFileSystems); + return new Scope(this, true, null, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems); } /// - public void AttachScope(IScope other) + public void AttachScope(IScope other, bool callContext = false) { var otherScope = other as Scope; if (otherScope == null) @@ -197,10 +349,15 @@ namespace Umbraco.Core.Scoping if (otherScope.Detachable == false) throw new ArgumentException("Not a detachable scope."); + if (otherScope.Attached) + throw new InvalidOperationException("Already attached."); + + otherScope.Attached = true; otherScope.OrigScope = AmbientScope; otherScope.OrigContext = AmbientContext; - AmbientScope = otherScope; - AmbientContext = otherScope.Context; + + otherScope.CallContext = callContext; + SetAmbient(otherScope, otherScope.Context); } /// @@ -221,10 +378,10 @@ namespace Umbraco.Core.Scoping if (scope.Detachable == false) throw new InvalidOperationException("Ambient scope is not detachable."); - AmbientScope = scope.OrigScope; - AmbientContext = scope.OrigContext; + SetAmbient(scope.OrigScope, scope.OrigContext); scope.OrigScope = null; scope.OrigContext = null; + scope.Attached = false; return scope; } @@ -232,16 +389,19 @@ namespace Umbraco.Core.Scoping public IScope CreateScope( IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, - EventsDispatchMode dispatchMode = EventsDispatchMode.Unspecified, - bool? scopeFileSystems = null) + IEventDispatcher eventDispatcher = null, + bool? scopeFileSystems = null, + bool callContext = false) { var ambient = AmbientScope; if (ambient == null) { - var context = AmbientContext == null ? new ScopeContext() : null; - var scope = new Scope(this, false, context, isolationLevel, repositoryCacheMode, dispatchMode, scopeFileSystems); - if (AmbientContext == null) AmbientContext = context; // assign only if scope creation did not throw! - return AmbientScope = scope; + var ambientContext = AmbientContext; + var newContext = ambientContext == null ? new ScopeContext() : null; + var scope = new Scope(this, false, newContext, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext); + // assign only if scope creation did not throw! + SetAmbient(scope, newContext ?? ambientContext); + return scope; } // replace noScope with a real one @@ -255,16 +415,20 @@ namespace Umbraco.Core.Scoping var database = noScope.DatabaseOrNull; if (database != null && database.InTransaction) throw new Exception("NoScope is in a transaction."); - var context = AmbientContext == null ? new ScopeContext() : null; - var scope = new Scope(this, noScope, context, isolationLevel, repositoryCacheMode, dispatchMode, scopeFileSystems); - if (AmbientContext == null) AmbientContext = context; // assign only if scope creation did not throw! - return AmbientScope = scope; + var ambientContext = AmbientContext; + var newContext = ambientContext == null ? new ScopeContext() : null; + var scope = new Scope(this, noScope, newContext, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext); + // assign only if scope creation did not throw! + SetAmbient(scope, newContext ?? ambientContext); + return scope; } var ambientScope = ambient as Scope; if (ambientScope == null) throw new Exception("Ambient scope is not a Scope instance."); - return AmbientScope = new Scope(this, ambientScope, isolationLevel, repositoryCacheMode, dispatchMode, scopeFileSystems); + var nested = new Scope(this, ambientScope, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext); + SetAmbient(nested, AmbientContext); + return nested; } /// @@ -309,7 +473,7 @@ namespace Umbraco.Core.Scoping // all scope instances that are currently beeing tracked private static readonly object StaticScopeInfosLock = new object(); - private static readonly List StaticScopeInfos = new List(); + private static readonly Dictionary StaticScopeInfos = new Dictionary(); public IEnumerable ScopeInfos { @@ -317,37 +481,53 @@ namespace Umbraco.Core.Scoping { lock (StaticScopeInfosLock) { - return StaticScopeInfos.ToArray(); // capture in an array + return StaticScopeInfos.Values.ToArray(); // capture in an array } } } + public ScopeInfo GetScopeInfo(IScope scope) + { + lock (StaticScopeInfosLock) + { + ScopeInfo scopeInfo; + return StaticScopeInfos.TryGetValue(scope, out scopeInfo) ? scopeInfo : null; + } + } + //private static void Log(string message, UmbracoDatabase database) //{ // LogHelper.Debug(message + " (" + (database == null ? "" : database.InstanceSid) + ")."); //} + // register a scope and capture its ctor stacktrace public void RegisterScope(IScope scope) { lock (StaticScopeInfosLock) { - if (StaticScopeInfos.Any(x => x.Scope == scope)) throw new Exception("oops: already registered."); - StaticScopeInfos.Add(new ScopeInfo(scope, Environment.StackTrace)); + if (StaticScopeInfos.ContainsKey(scope)) throw new Exception("oops: already registered."); + //Logging.LogHelper.Debug("Register " + scope.InstanceId.ToString("N").Substring(0, 8)); + StaticScopeInfos[scope] = new ScopeInfo(scope, Environment.StackTrace); } } - // 'context' that contains the scope (null, "http" or "lcc") + // register that a scope is in a 'context' + // 'context' that contains the scope (null, "http" or "call") public static void RegisterContext(IScope scope, string context) { lock (StaticScopeInfosLock) { - var info = StaticScopeInfos.FirstOrDefault(x => x.Scope == scope); + ScopeInfo info; + if (StaticScopeInfos.TryGetValue(scope, out info) == false) info = null; if (info == null) { if (context == null) return; throw new Exception("oops: unregistered scope."); } + //Logging.LogHelper.Debug("Register context " + (context ?? "null") + " for " + scope.InstanceId.ToString("N").Substring(0, 8)); if (context == null) info.NullStack = Environment.StackTrace; + //if (context == null) + // Logging.LogHelper.Debug("STACK\r\n" + info.NullStack); info.Context = context; } } @@ -356,11 +536,12 @@ namespace Umbraco.Core.Scoping { lock (StaticScopeInfosLock) { - var info = StaticScopeInfos.FirstOrDefault(x => x.Scope == scope); - if (info != null) + if (StaticScopeInfos.ContainsKey(scope)) { // enable this by default - StaticScopeInfos.Remove(info); + //Console.WriteLine("unregister " + scope.InstanceId.ToString("N").Substring(0, 8)); + StaticScopeInfos.Remove(scope); + //Logging.LogHelper.Debug("Remove " + scope.InstanceId.ToString("N").Substring(0, 8)); // instead, enable this to keep *all* scopes // beware, there can be a lot of scopes! diff --git a/src/Umbraco.Core/Security/BackOfficeSignInManager.cs b/src/Umbraco.Core/Security/BackOfficeSignInManager.cs index bd943309d0..71bd2474cd 100644 --- a/src/Umbraco.Core/Security/BackOfficeSignInManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeSignInManager.cs @@ -176,7 +176,7 @@ namespace Umbraco.Core.Security AllowRefresh = true, IssuedUtc = nowUtc, ExpiresUtc = nowUtc.AddMinutes(GlobalSettings.TimeOutInMinutes) - }, userIdentity, rememberBrowserIdentity); + }, userIdentity, rememberBrowserIdentity); } else { @@ -189,6 +189,10 @@ namespace Umbraco.Core.Security }, userIdentity); } + //track the last login date + user.LastLoginDateUtc = DateTime.UtcNow; + await UserManager.UpdateAsync(user); + _logger.WriteCore(TraceEventType.Information, 0, string.Format( "Login attempt succeeded for username {0} from IP address {1}", diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index 08c6f54dcf..889c7004d7 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -625,6 +625,12 @@ namespace Umbraco.Core.Security var anythingChanged = false; //don't assign anything if nothing has changed as this will trigger //the track changes of the model + if ((user.LastLoginDate != default(DateTime) && identityUser.LastLoginDateUtc.HasValue == false) + || identityUser.LastLoginDateUtc.HasValue && user.LastLoginDate.ToUniversalTime() != identityUser.LastLoginDateUtc.Value) + { + anythingChanged = true; + user.LastLoginDate = identityUser.LastLoginDateUtc.Value.ToLocalTime(); + } if (user.Name != identityUser.Name && identityUser.Name.IsNullOrWhiteSpace() == false) { anythingChanged = true; diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index af06c7e0ba..0723431ada 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -179,7 +179,7 @@ namespace Umbraco.Core.Services uow.Events.Dispatch(Created, this, new NewEventArgs(content, false, contentTypeAlias, parentId)); //Created.RaiseEvent(new NewEventArgs(content, false, contentTypeAlias, parentId), this, uow.Events); - // fixme + // fixme var auditRepo = RepositoryFactory.CreateAuditRepository(uow); auditRepo.AddOrUpdate(new AuditItem(content.Id, string.Format("Content '{0}' was created", name), AuditType.New, content.CreatorId)); uow.Commit(); @@ -1051,9 +1051,9 @@ namespace Umbraco.Core.Services UnPublish(content, userId); } content.WriterId = userId; - content.ChangeTrashedState(true); + content.ChangeTrashedState(true); repository.AddOrUpdate(content); - + //Loop through descendants to update their trash state, but ensuring structure by keeping the ParentId foreach (var descendant in descendants) { @@ -1063,14 +1063,14 @@ namespace Umbraco.Core.Services descendant.ChangeTrashedState(true, descendant.ParentId); repository.AddOrUpdate(descendant); - moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); + moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); } uow.Commit(); uow.Events.Dispatch(Trashed, this, new MoveEventArgs(false, evtMsgs, moveInfo.ToArray()), "Trashed"); } - + Audit(AuditType.Move, "Move Content to Recycle Bin performed by user", userId, content.Id); return OperationStatus.Success(evtMsgs); @@ -1285,9 +1285,6 @@ namespace Umbraco.Core.Services var args = new DeleteEventArgs(content, false, evtMsgs); uow.Events.Dispatch(Deleted, this, args, "Deleted"); // fixme why the event name?! - - //remove any flagged media files - repository.DeleteMediaFiles(args.MediaFilesToDelete); } Audit(AuditType.Delete, "Delete Content performed by user", userId, content.Id); @@ -1335,7 +1332,7 @@ namespace Umbraco.Core.Services /// /// Deletes all content of the specified types. All Descendants of deleted content that is not of these types is moved to Recycle Bin. - /// + /// /// Id of the /// Optional Id of the user issueing the delete operation public void DeleteContentOfTypes(IEnumerable contentTypeIds, int userId = 0) @@ -1344,7 +1341,7 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.GetUnitOfWork()) { var repository = RepositoryFactory.CreateContentRepository(uow); - + //track the 'root' items of the collection of nodes discovered to delete, we need to use //these items to lookup descendants that are not of this doc type so they can be transfered //to the recycle bin @@ -1559,9 +1556,6 @@ namespace Umbraco.Core.Services success = repository.EmptyRecycleBin(); - if (success) - repository.DeleteMediaFiles(files); - uow.Events.Dispatch(EmptiedRecycleBin, this, new RecycleBinEventArgs(nodeObjectType, entities, files, success)); uow.Commit(); } @@ -1610,7 +1604,6 @@ namespace Umbraco.Core.Services // fixme mess! using (var scope = UowProvider.ScopeProvider.CreateScope()) { - using (var uow = UowProvider.GetUnitOfWork()) { if (uow.Events.DispatchCancelable(Copying, this, new CopyEventArgs(content, copy, parentId))) diff --git a/src/Umbraco.Core/Services/ContentTypeService.cs b/src/Umbraco.Core/Services/ContentTypeService.cs index 43ed68b5a2..32d14f6656 100644 --- a/src/Umbraco.Core/Services/ContentTypeService.cs +++ b/src/Umbraco.Core/Services/ContentTypeService.cs @@ -756,6 +756,7 @@ namespace Umbraco.Core.Services using (var scope = UowProvider.ScopeProvider.CreateScope()) // fixme what a mess { scope.Events.Dispatch(SavedContentType, this, new SaveEventArgs(contentType, false)); + scope.Complete(); } Audit(AuditType.Save, "Save ContentType performed by user", userId, contentType.Id); } @@ -800,8 +801,9 @@ namespace Umbraco.Core.Services using (var scope = UowProvider.ScopeProvider.CreateScope()) // fixme what a mess { scope.Events.Dispatch(SavedContentType, this, new SaveEventArgs(asArray, false)); + scope.Complete(); } - Audit(AuditType.Save, "Save ContentTypes performed by user", userId, -1); + Audit(AuditType.Save, "Save ContentTypes performed by user", userId, -1); } /// @@ -1211,6 +1213,7 @@ namespace Umbraco.Core.Services using (var scope = UowProvider.ScopeProvider.CreateScope()) // fixme what a mess { scope.Events.Dispatch(SavedMediaType, this, new SaveEventArgs(mediaType, false)); + scope.Complete(); } Audit(AuditType.Save, "Save MediaType performed by user", userId, mediaType.Id); } @@ -1255,6 +1258,7 @@ namespace Umbraco.Core.Services using (var scope = UowProvider.ScopeProvider.CreateScope()) // fixme what a mess { scope.Events.Dispatch(SavedMediaType, this, new SaveEventArgs(asArray, false)); + scope.Complete(); } Audit(AuditType.Save, "Save MediaTypes performed by user", userId, -1); } diff --git a/src/Umbraco.Core/Services/FileService.cs b/src/Umbraco.Core/Services/FileService.cs index f4f634d974..ec886d788d 100644 --- a/src/Umbraco.Core/Services/FileService.cs +++ b/src/Umbraco.Core/Services/FileService.cs @@ -735,6 +735,26 @@ namespace Umbraco.Core.Services return empty.Union(files.Except(empty)); } + public void CreatePartialViewFolder(string folderPath) + { + var uow = _fileUowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreatePartialViewRepository(uow)) + { + ((PartialViewRepository)repository).AddFolder(folderPath); + uow.Commit(); + } + } + + public void CreatePartialViewMacroFolder(string folderPath) + { + var uow = _fileUowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreatePartialViewMacroRepository(uow)) + { + ((PartialViewMacroRepository)repository).AddFolder(folderPath); + uow.Commit(); + } + } + public void DeletePartialViewFolder(string folderPath) { using (var uow = _fileUowProvider.GetUnitOfWork()) diff --git a/src/Umbraco.Core/Services/IFileService.cs b/src/Umbraco.Core/Services/IFileService.cs index 453d199bfe..e8f1065219 100644 --- a/src/Umbraco.Core/Services/IFileService.cs +++ b/src/Umbraco.Core/Services/IFileService.cs @@ -11,6 +11,8 @@ namespace Umbraco.Core.Services public interface IFileService : IService { IEnumerable GetPartialViewSnippetNames(params string[] filterNames); + void CreatePartialViewFolder(string folderPath); + void CreatePartialViewMacroFolder(string folderPath); void DeletePartialViewFolder(string folderPath); void DeletePartialViewMacroFolder(string folderPath); IPartialView GetPartialView(string path); diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index 9f5ec811c9..08494a3662 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -831,9 +831,6 @@ namespace Umbraco.Core.Services var args = new DeleteEventArgs(media, false, evtMsgs); uow.Events.Dispatch(Deleted, this, args); - - //remove any flagged media files - repository.DeleteMediaFiles(args.MediaFilesToDelete); } Audit(AuditType.Delete, "Delete Media performed by user", userId, media.Id); @@ -958,9 +955,6 @@ namespace Umbraco.Core.Services success = repository.EmptyRecycleBin(); // FIXME shouldn't we commit here?! uow.Events.Dispatch(EmptiedRecycleBin, this, new RecycleBinEventArgs(nodeObjectType, entities, files, success)); - - if (success) - repository.DeleteMediaFiles(files); } } Audit(AuditType.Delete, "Empty Media Recycle Bin performed by user", 0, -21); diff --git a/src/Umbraco.Core/Services/MemberService.cs b/src/Umbraco.Core/Services/MemberService.cs index 7c7afdd2a6..841d79f687 100644 --- a/src/Umbraco.Core/Services/MemberService.cs +++ b/src/Umbraco.Core/Services/MemberService.cs @@ -45,7 +45,7 @@ namespace Umbraco.Core.Services /// /// Gets the default MemberType alias /// - /// By default we'll return the 'writer', but we need to check it exists. If it doesn't we'll + /// By default we'll return the 'writer', but we need to check it exists. If it doesn't we'll /// return the first type that is not an admin, otherwise if there's only one we will return that one. /// Alias of the default MemberType public string GetDefaultMemberType() @@ -86,7 +86,7 @@ namespace Umbraco.Core.Services /// /// This is simply a helper method which essentially just wraps the MembershipProvider's ChangePassword method /// - /// This method exists so that Umbraco developers can use one entry point to create/update + /// This method exists so that Umbraco developers can use one entry point to create/update /// Members if they choose to. /// The Member to save the password for /// The password to encrypt and save @@ -776,7 +776,7 @@ namespace Umbraco.Core.Services /// /// Creates and persists a Member /// - /// Using this method will persist the Member object before its returned + /// Using this method will persist the Member object before its returned /// meaning that it will have an Id available (unlike the CreateMember method) /// Username of the Member to create /// Email of the Member to create @@ -792,7 +792,7 @@ namespace Umbraco.Core.Services /// /// Creates and persists a Member /// - /// Using this method will persist the Member object before its returned + /// Using this method will persist the Member object before its returned /// meaning that it will have an Id available (unlike the CreateMember method) /// Username of the Member to create /// Email of the Member to create @@ -806,7 +806,7 @@ namespace Umbraco.Core.Services /// /// Creates and persists a Member /// - /// Using this method will persist the Member object before its returned + /// Using this method will persist the Member object before its returned /// meaning that it will have an Id available (unlike the CreateMember method) /// Username of the Member to create /// Email of the Member to create @@ -836,7 +836,7 @@ namespace Umbraco.Core.Services /// /// Creates and persists a Member /// - /// Using this method will persist the Member object before its returned + /// Using this method will persist the Member object before its returned /// meaning that it will have an Id available (unlike the CreateMember method) /// Username of the Member to create /// Email of the Member to create @@ -921,7 +921,7 @@ namespace Umbraco.Core.Services /// public IMember GetByUsername(string username) { - //TODO: Somewhere in here, whether at this level or the repository level, we need to add + //TODO: Somewhere in here, whether at this level or the repository level, we need to add // a caching mechanism since this method is used by all the membership providers and could be // called quite a bit when dealing with members. @@ -953,9 +953,6 @@ namespace Umbraco.Core.Services var args = new DeleteEventArgs(member, false); uow.Events.Dispatch(Deleted, this, args); - - //remove any flagged media files - repository.DeleteMediaFiles(args.MediaFilesToDelete); } } @@ -963,7 +960,7 @@ namespace Umbraco.Core.Services /// Saves an /// /// to Save - /// Optional parameter to raise events. + /// Optional parameter to raise events. /// Default is True otherwise set to False to not raise events public void Save(IMember entity, bool raiseEvents = true) { @@ -1004,7 +1001,7 @@ namespace Umbraco.Core.Services /// Saves a list of objects /// /// to save - /// Optional parameter to raise events. + /// Optional parameter to raise events. /// Default is True otherwise set to False to not raise events public void Save(IEnumerable entities, bool raiseEvents = true) { diff --git a/src/Umbraco.Core/Services/MemberTypeService.cs b/src/Umbraco.Core/Services/MemberTypeService.cs index 4634288357..be55a335b3 100644 --- a/src/Umbraco.Core/Services/MemberTypeService.cs +++ b/src/Umbraco.Core/Services/MemberTypeService.cs @@ -17,7 +17,7 @@ namespace Umbraco.Core.Services private readonly IMemberService _memberService; private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(); - + public MemberTypeService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger, IEventMessagesFactory eventMessagesFactory, IMemberService memberService) : base(provider, repositoryFactory, logger, eventMessagesFactory) @@ -110,7 +110,7 @@ namespace Umbraco.Core.Services public void Save(IEnumerable memberTypes, int userId = 0) { var asArray = memberTypes.ToArray(); - + using (new WriteLock(Locker)) { using (var uow = UowProvider.GetUnitOfWork()) @@ -137,7 +137,7 @@ namespace Umbraco.Core.Services uow.Events.Dispatch(Saved, this, new SaveEventArgs(asArray, false)); } } - + } public void Delete(IMemberType memberType, int userId = 0) @@ -167,7 +167,7 @@ namespace Umbraco.Core.Services public void Delete(IEnumerable memberTypes, int userId = 0) { var asArray = memberTypes.ToArray(); - + using (new WriteLock(Locker)) { using (var scope = UowProvider.ScopeProvider.CreateScope()) diff --git a/src/Umbraco.Core/Services/PackagingService.cs b/src/Umbraco.Core/Services/PackagingService.cs index f2530a3319..d5f1ffea4c 100644 --- a/src/Umbraco.Core/Services/PackagingService.cs +++ b/src/Umbraco.Core/Services/PackagingService.cs @@ -1716,6 +1716,24 @@ namespace Umbraco.Core.Services #region Package Building #endregion + /// + /// This method can be used to trigger the 'ImportedPackage' event when a package is installed by something else but this service. + /// + /// + internal static void OnImportedPackage(ImportPackageEventArgs args) + { + ImportedPackage.RaiseEvent(args, null); + } + + /// + /// This method can be used to trigger the 'UninstalledPackage' event when a package is uninstalled by something else but this service. + /// + /// + internal static void OnUninstalledPackage(UninstallPackageEventArgs args) + { + UninstalledPackage.RaiseEvent(args, null); + } + #region Event Handlers /// /// Occurs before Importing Content @@ -1876,10 +1894,15 @@ namespace Umbraco.Core.Services internal static event TypedEventHandler> ImportingPackage; /// - /// Occurs after a apckage is imported + /// Occurs after a package is imported /// internal static event TypedEventHandler> ImportedPackage; + /// + /// Occurs after a package is uninstalled + /// + internal static event TypedEventHandler> UninstalledPackage; + #endregion } } diff --git a/src/Umbraco.Core/Services/RelationService.cs b/src/Umbraco.Core/Services/RelationService.cs index ec42d4bd74..76222810d2 100644 --- a/src/Umbraco.Core/Services/RelationService.cs +++ b/src/Umbraco.Core/Services/RelationService.cs @@ -215,7 +215,7 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.GetUnitOfWork(commit: true)) { var repository = RepositoryFactory.CreateRelationRepository(uow); - var query = new Query().Where(x => x.ChildId == id || x.ParentId == id); + var query = new Query().Where(x => x.ParentId == id || x.ChildId == id); return repository.GetByQuery(query); } } @@ -230,7 +230,7 @@ namespace Umbraco.Core.Services if (relationType == null) return Enumerable.Empty(); var relationRepo = RepositoryFactory.CreateRelationRepository(uow); - var query = new Query().Where(x => (x.ChildId == id || x.ParentId == id) && x.RelationTypeId == relationType.Id); + var query = new Query().Where(x => (x.ParentId == id || x.ChildId == id) && x.RelationTypeId == relationType.Id); return relationRepo.GetByQuery(query); } } diff --git a/src/Umbraco.Core/TypeExtensions.cs b/src/Umbraco.Core/TypeExtensions.cs index 3208c1fbe8..76dc79c219 100644 --- a/src/Umbraco.Core/TypeExtensions.cs +++ b/src/Umbraco.Core/TypeExtensions.cs @@ -425,7 +425,28 @@ namespace Umbraco.Core assemblyName.FullName.StartsWith("App_Code.") ? "App_Code" : assemblyName.Name); } + /// + /// If the given is an array or some other collection + /// comprised of 0 or more instances of a "subtype", get that type + /// + /// the source type + /// + internal static Type GetEnumeratedType(this Type type) + { + if (typeof(IEnumerable).IsAssignableFrom(type) == false) + return null; + // provided by Array + var elType = type.GetElementType(); + if (null != elType) return elType; + + // otherwise provided by collection + var elTypes = type.GetGenericArguments(); + if (elTypes.Length > 0) return elTypes[0]; + + // otherwise is not an 'enumerated' type + return null; + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Udi.cs b/src/Umbraco.Core/Udi.cs index a7298c6f89..9d67c6ccab 100644 --- a/src/Umbraco.Core/Udi.cs +++ b/src/Umbraco.Core/Udi.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; +using System.Reflection; using Umbraco.Core.Deploy; namespace Umbraco.Core @@ -10,9 +12,10 @@ namespace Umbraco.Core /// Represents an entity identifier. /// /// An Udi can be fully qualified or "closed" eg umb://document/{guid} or "open" eg umb://document. + [TypeConverter(typeof(UdiTypeConverter))] public abstract class Udi : IComparable { - private static readonly Dictionary UdiTypes = new Dictionary(); + private static readonly Lazy> UdiTypes; private static readonly ConcurrentDictionary RootUdis = new ConcurrentDictionary(); internal readonly Uri UriValue; // internal for UdiRange @@ -39,32 +42,50 @@ namespace Umbraco.Core static Udi() { - // for tests etc. - UdiTypes[Constants.DeployEntityType.AnyGuid] = UdiType.GuidUdi; - UdiTypes[Constants.DeployEntityType.AnyString] = UdiType.StringUdi; - - // we don't have connectors for these... - UdiTypes[Constants.DeployEntityType.Member] = UdiType.GuidUdi; - UdiTypes[Constants.DeployEntityType.MemberGroup] = UdiType.GuidUdi; - - // fixme - or inject from...? - // there is no way we can get the "registered" service connectors, as registration - // happens in Deploy, not in Core, and the Udi class belongs to Core - therefore, we - // just pick every service connectors - just making sure that not two of them - // would register the same entity type, with different udi types (would not make - // much sense anyways). - var connectors = PluginManager.Current.ResolveTypes(); - foreach (var connector in connectors) + UdiTypes = new Lazy>(() => { - var attrs = connector.GetCustomAttributes(false); - foreach (var attr in attrs) + var result = new Dictionary(); + + // known types: + foreach (var fi in typeof(Constants.UdiEntityType).GetFields(BindingFlags.Public | BindingFlags.Static)) { - UdiType udiType; - if (UdiTypes.TryGetValue(attr.EntityType, out udiType) && udiType != attr.UdiType) - throw new Exception(string.Format("Entity type \"{0}\" is declared by more than one IServiceConnector, with different UdiTypes.", attr.EntityType)); - UdiTypes[attr.EntityType] = attr.UdiType; + // IsLiteral determines if its value is written at + // compile time and not changeable + // IsInitOnly determine if the field can be set + // in the body of the constructor + // for C# a field which is readonly keyword would have both true + // but a const field would have only IsLiteral equal to true + if (fi.IsLiteral && fi.IsInitOnly == false) + { + var udiType = fi.GetCustomAttribute(); + + if (udiType == null) + throw new InvalidOperationException("All Constants listed in UdiEntityType must be attributed with " + typeof(Constants.UdiTypeAttribute)); + result[fi.GetValue(null).ToString()] = udiType.UdiType; + } } - } + + // Scan for unknown UDI types + // there is no way we can get the "registered" service connectors, as registration + // happens in Deploy, not in Core, and the Udi class belongs to Core - therefore, we + // just pick every service connectors - just making sure that not two of them + // would register the same entity type, with different udi types (would not make + // much sense anyways). + var connectors = PluginManager.Current.ResolveTypes(); + foreach (var connector in connectors) + { + var attrs = connector.GetCustomAttributes(false); + foreach (var attr in attrs) + { + UdiType udiType; + if (result.TryGetValue(attr.EntityType, out udiType) && udiType != attr.UdiType) + throw new Exception(string.Format("Entity type \"{0}\" is declared by more than one IServiceConnector, with different UdiTypes.", attr.EntityType)); + result[attr.EntityType] = attr.UdiType; + } + } + + return result; + }); } /// @@ -105,8 +126,8 @@ namespace Umbraco.Core udi = null; Uri uri; - if (!Uri.IsWellFormedUriString(s, UriKind.Absolute) - || !Uri.TryCreate(s, UriKind.Absolute, out uri)) + if (Uri.IsWellFormedUriString(s, UriKind.Absolute) == false + || Uri.TryCreate(s, UriKind.Absolute, out uri) == false) { if (tryParse) return false; throw new FormatException(string.Format("String \"{0}\" is not a valid udi.", s)); @@ -114,7 +135,7 @@ namespace Umbraco.Core var entityType = uri.Host; UdiType udiType; - if (!UdiTypes.TryGetValue(entityType, out udiType)) + if (UdiTypes.Value.TryGetValue(entityType, out udiType) == false) { if (tryParse) return false; throw new FormatException(string.Format("Unknown entity type \"{0}\".", entityType)); @@ -128,7 +149,7 @@ namespace Umbraco.Core return true; } Guid guid; - if (!Guid.TryParse(path, out guid)) + if (Guid.TryParse(path, out guid) == false) { if (tryParse) return false; throw new FormatException(string.Format("String \"{0}\" is not a valid udi.", s)); @@ -150,7 +171,7 @@ namespace Umbraco.Core return RootUdis.GetOrAdd(entityType, x => { UdiType udiType; - if (!UdiTypes.TryGetValue(x, out udiType)) + if (UdiTypes.Value.TryGetValue(x, out udiType) == false) throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", entityType)); return udiType == UdiType.StringUdi ? (Udi)new StringUdi(entityType, string.Empty) @@ -177,7 +198,7 @@ namespace Umbraco.Core public static Udi Create(string entityType, string id) { UdiType udiType; - if (!UdiTypes.TryGetValue(entityType, out udiType)) + if (UdiTypes.Value.TryGetValue(entityType, out udiType) == false) throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", entityType), "entityType"); if (string.IsNullOrWhiteSpace(id)) throw new ArgumentException("Value cannot be null or whitespace.", "id"); @@ -196,7 +217,7 @@ namespace Umbraco.Core public static Udi Create(string entityType, Guid id) { UdiType udiType; - if (!UdiTypes.TryGetValue(entityType, out udiType)) + if (UdiTypes.Value.TryGetValue(entityType, out udiType) == false) throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", entityType), "entityType"); if (udiType != UdiType.GuidUdi) throw new InvalidOperationException(string.Format("Entity type \"{0}\" does not have guid udis.", entityType)); @@ -208,7 +229,7 @@ namespace Umbraco.Core internal static Udi Create(Uri uri) { UdiType udiType; - if (!UdiTypes.TryGetValue(uri.Host, out udiType)) + if (UdiTypes.Value.TryGetValue(uri.Host, out udiType) == false) throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", uri.Host), "uri"); if (udiType == UdiType.GuidUdi) return new GuidUdi(uri); @@ -219,7 +240,7 @@ namespace Umbraco.Core public void EnsureType(params string[] validTypes) { - if (!validTypes.Contains(EntityType)) + if (validTypes.Contains(EntityType) == false) throw new Exception(string.Format("Unexpected entity type \"{0}\".", EntityType)); } @@ -260,7 +281,7 @@ namespace Umbraco.Core public static bool operator !=(Udi udi1, Udi udi2) { - return !(udi1 == udi2); + return (udi1 == udi2) == false; } } diff --git a/src/Umbraco.Core/Constants-DeployEntityType.cs b/src/Umbraco.Core/UdiEntityType.cs similarity index 73% rename from src/Umbraco.Core/Constants-DeployEntityType.cs rename to src/Umbraco.Core/UdiEntityType.cs index 0f91a3c086..f6b9b1e3b0 100644 --- a/src/Umbraco.Core/Constants-DeployEntityType.cs +++ b/src/Umbraco.Core/UdiEntityType.cs @@ -7,52 +7,82 @@ namespace Umbraco.Core public static partial class Constants { - /// /// Defines well-known entity types. /// /// Well-known entity types are those that Deploy already knows about, /// but entity types are strings and so can be extended beyond what is defined here. - public static class DeployEntityType + public static class UdiEntityType { + [UdiType(UdiType.Unknown)] + public const string Unknown = "unknown"; + // guid entity types + [UdiType(UdiType.GuidUdi)] public const string AnyGuid = "any-guid"; // that one is for tests + [UdiType(UdiType.GuidUdi)] public const string Document = "document"; + [UdiType(UdiType.GuidUdi)] public const string Media = "media"; + [UdiType(UdiType.GuidUdi)] public const string Member = "member"; + [UdiType(UdiType.GuidUdi)] public const string DictionaryItem = "dictionary-item"; + [UdiType(UdiType.GuidUdi)] public const string Macro = "macro"; + [UdiType(UdiType.GuidUdi)] public const string Template = "template"; + [UdiType(UdiType.GuidUdi)] public const string DocumentType = "document-type"; + [UdiType(UdiType.GuidUdi)] public const string DocumentTypeContainer = "document-type-container"; + [UdiType(UdiType.GuidUdi)] public const string MediaType = "media-type"; + [UdiType(UdiType.GuidUdi)] public const string MediaTypeContainer = "media-type-container"; + [UdiType(UdiType.GuidUdi)] public const string DataType = "data-type"; + [UdiType(UdiType.GuidUdi)] public const string DataTypeContainer = "data-type-container"; + [UdiType(UdiType.GuidUdi)] public const string MemberType = "member-type"; + [UdiType(UdiType.GuidUdi)] public const string MemberGroup = "member-group"; + [UdiType(UdiType.GuidUdi)] public const string RelationType = "relation-type"; - + // forms + + [UdiType(UdiType.GuidUdi)] public const string FormsForm = "forms-form"; - public const string FormsWorkflow = "forms-workflow"; - public const string FormsRecord = "forms-record"; + [UdiType(UdiType.GuidUdi)] + public const string FormsPreValue = "forms-prevalue"; + [UdiType(UdiType.GuidUdi)] + public const string FormsDataSource = "forms-datasource"; // string entity types + [UdiType(UdiType.StringUdi)] public const string AnyString = "any-string"; // that one is for tests + [UdiType(UdiType.StringUdi)] public const string MediaFile = "media-file"; + [UdiType(UdiType.StringUdi)] public const string TemplateFile = "template-file"; + [UdiType(UdiType.StringUdi)] public const string Script = "script"; + [UdiType(UdiType.StringUdi)] public const string Stylesheet = "stylesheet"; + [UdiType(UdiType.StringUdi)] public const string PartialView = "partial-view"; + [UdiType(UdiType.StringUdi)] public const string PartialViewMacro = "partial-view-macro"; + [UdiType(UdiType.StringUdi)] public const string Xslt = "xslt"; public static string FromUmbracoObjectType(UmbracoObjectTypes umbracoObjectType) @@ -89,10 +119,10 @@ namespace Umbraco.Core return RelationType; case UmbracoObjectTypes.FormsForm: return FormsForm; - case UmbracoObjectTypes.FormsWorkflow: - return FormsWorkflow; - case UmbracoObjectTypes.FormsRecord: - return FormsRecord; + case UmbracoObjectTypes.FormsPreValue: + return FormsPreValue; + case UmbracoObjectTypes.FormsDataSource: + return FormsDataSource; } throw new NotSupportedException(string.Format("UmbracoObjectType \"{0}\" does not have a matching EntityType.", umbracoObjectType)); } @@ -131,16 +161,25 @@ namespace Umbraco.Core return UmbracoObjectTypes.RelationType; case FormsForm: return UmbracoObjectTypes.FormsForm; - case FormsWorkflow: - return UmbracoObjectTypes.FormsWorkflow; - case FormsRecord: - return UmbracoObjectTypes.FormsRecord; + case FormsPreValue: + return UmbracoObjectTypes.FormsPreValue; + case FormsDataSource: + return UmbracoObjectTypes.FormsDataSource; } throw new NotSupportedException( string.Format("EntityType \"{0}\" does not have a matching UmbracoObjectType.", entityType)); } } + [AttributeUsage(AttributeTargets.Field)] + internal class UdiTypeAttribute : Attribute + { + public UdiType UdiType { get; private set; } + public UdiTypeAttribute(UdiType udiType) + { + UdiType = udiType; + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/UdiGetterExtensions.cs b/src/Umbraco.Core/UdiGetterExtensions.cs index 1bd018a754..795d48c259 100644 --- a/src/Umbraco.Core/UdiGetterExtensions.cs +++ b/src/Umbraco.Core/UdiGetterExtensions.cs @@ -17,7 +17,7 @@ namespace Umbraco.Core public static GuidUdi GetUdi(this ITemplate entity) { if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.DeployEntityType.Template, entity.Key).EnsureClosed(); + return new GuidUdi(Constants.UdiEntityType.Template, entity.Key).EnsureClosed(); } /// @@ -28,7 +28,7 @@ namespace Umbraco.Core public static GuidUdi GetUdi(this IContentType entity) { if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.DeployEntityType.DocumentType, entity.Key).EnsureClosed(); + return new GuidUdi(Constants.UdiEntityType.DocumentType, entity.Key).EnsureClosed(); } /// @@ -39,7 +39,7 @@ namespace Umbraco.Core public static GuidUdi GetUdi(this IMediaType entity) { if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.DeployEntityType.MediaType, entity.Key).EnsureClosed(); + return new GuidUdi(Constants.UdiEntityType.MediaType, entity.Key).EnsureClosed(); } /// @@ -50,7 +50,7 @@ namespace Umbraco.Core public static GuidUdi GetUdi(this IMemberType entity) { if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.DeployEntityType.MemberType, entity.Key).EnsureClosed(); + return new GuidUdi(Constants.UdiEntityType.MemberType, entity.Key).EnsureClosed(); } /// @@ -61,7 +61,7 @@ namespace Umbraco.Core public static GuidUdi GetUdi(this IMemberGroup entity) { if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.DeployEntityType.MemberGroup, entity.Key).EnsureClosed(); + return new GuidUdi(Constants.UdiEntityType.MemberGroup, entity.Key).EnsureClosed(); } /// @@ -74,9 +74,9 @@ namespace Umbraco.Core if (entity == null) throw new ArgumentNullException("entity"); string type; - if (entity is IContentType) type = Constants.DeployEntityType.DocumentType; - else if (entity is IMediaType) type = Constants.DeployEntityType.MediaType; - else if (entity is IMemberType) type = Constants.DeployEntityType.MemberType; + if (entity is IContentType) type = Constants.UdiEntityType.DocumentType; + else if (entity is IMediaType) type = Constants.UdiEntityType.MediaType; + else if (entity is IMemberType) type = Constants.UdiEntityType.MemberType; else throw new NotSupportedException(string.Format("Composition type {0} is not supported.", entity.GetType().FullName)); return new GuidUdi(type, entity.Key).EnsureClosed(); } @@ -89,7 +89,7 @@ namespace Umbraco.Core public static GuidUdi GetUdi(this IDataTypeDefinition entity) { if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.DeployEntityType.DataType, entity.Key).EnsureClosed(); + return new GuidUdi(Constants.UdiEntityType.DataType, entity.Key).EnsureClosed(); } /// @@ -103,11 +103,11 @@ namespace Umbraco.Core string entityType; if (entity.ContainedObjectType == Constants.ObjectTypes.DataTypeGuid) - entityType = Constants.DeployEntityType.DataTypeContainer; + entityType = Constants.UdiEntityType.DataTypeContainer; else if (entity.ContainedObjectType == Constants.ObjectTypes.DocumentTypeGuid) - entityType = Constants.DeployEntityType.DocumentTypeContainer; + entityType = Constants.UdiEntityType.DocumentTypeContainer; else if (entity.ContainedObjectType == Constants.ObjectTypes.MediaTypeGuid) - entityType = Constants.DeployEntityType.MediaTypeContainer; + entityType = Constants.UdiEntityType.MediaTypeContainer; else throw new NotSupportedException(string.Format("Contained object type {0} is not supported.", entity.ContainedObjectType)); return new GuidUdi(entityType, entity.Key).EnsureClosed(); @@ -121,7 +121,7 @@ namespace Umbraco.Core public static GuidUdi GetUdi(this IMedia entity) { if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.DeployEntityType.Media, entity.Key).EnsureClosed(); + return new GuidUdi(Constants.UdiEntityType.Media, entity.Key).EnsureClosed(); } /// @@ -132,7 +132,7 @@ namespace Umbraco.Core public static GuidUdi GetUdi(this IContent entity) { if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.DeployEntityType.Document, entity.Key).EnsureClosed(); + return new GuidUdi(Constants.UdiEntityType.Document, entity.Key).EnsureClosed(); } /// @@ -143,7 +143,7 @@ namespace Umbraco.Core public static GuidUdi GetUdi(this IMember entity) { if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.DeployEntityType.Member, entity.Key).EnsureClosed(); + return new GuidUdi(Constants.UdiEntityType.Member, entity.Key).EnsureClosed(); } /// @@ -154,7 +154,7 @@ namespace Umbraco.Core public static StringUdi GetUdi(this Stylesheet entity) { if (entity == null) throw new ArgumentNullException("entity"); - return new StringUdi(Constants.DeployEntityType.Stylesheet, entity.Path.TrimStart('/')).EnsureClosed(); + return new StringUdi(Constants.UdiEntityType.Stylesheet, entity.Path.TrimStart('/')).EnsureClosed(); } /// @@ -165,7 +165,7 @@ namespace Umbraco.Core public static StringUdi GetUdi(this Script entity) { if (entity == null) throw new ArgumentNullException("entity"); - return new StringUdi(Constants.DeployEntityType.Script, entity.Path.TrimStart('/')).EnsureClosed(); + return new StringUdi(Constants.UdiEntityType.Script, entity.Path.TrimStart('/')).EnsureClosed(); } /// @@ -176,7 +176,7 @@ namespace Umbraco.Core public static GuidUdi GetUdi(this IDictionaryItem entity) { if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.DeployEntityType.DictionaryItem, entity.Key).EnsureClosed(); + return new GuidUdi(Constants.UdiEntityType.DictionaryItem, entity.Key).EnsureClosed(); } /// @@ -187,7 +187,7 @@ namespace Umbraco.Core public static GuidUdi GetUdi(this IMacro entity) { if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.DeployEntityType.Macro, entity.Key).EnsureClosed(); + return new GuidUdi(Constants.UdiEntityType.Macro, entity.Key).EnsureClosed(); } /// @@ -198,7 +198,7 @@ namespace Umbraco.Core public static StringUdi GetUdi(this IPartialView entity) { if (entity == null) throw new ArgumentNullException("entity"); - return new StringUdi(Constants.DeployEntityType.PartialView, entity.Path.TrimStart('/')).EnsureClosed(); + return new StringUdi(Constants.UdiEntityType.PartialView, entity.Path.TrimStart('/')).EnsureClosed(); } /// @@ -209,7 +209,7 @@ namespace Umbraco.Core public static StringUdi GetUdi(this IXsltFile entity) { if (entity == null) throw new ArgumentNullException("entity"); - return new StringUdi(Constants.DeployEntityType.Xslt, entity.Path.TrimStart('/')).EnsureClosed(); + return new StringUdi(Constants.UdiEntityType.Xslt, entity.Path.TrimStart('/')).EnsureClosed(); } /// @@ -222,9 +222,9 @@ namespace Umbraco.Core if (entity == null) throw new ArgumentNullException("entity"); string type; - if (entity is IContent) type = Constants.DeployEntityType.Document; - else if (entity is IMedia) type = Constants.DeployEntityType.Media; - else if (entity is IMember) type = Constants.DeployEntityType.Member; + if (entity is IContent) type = Constants.UdiEntityType.Document; + else if (entity is IMedia) type = Constants.UdiEntityType.Media; + else if (entity is IMember) type = Constants.UdiEntityType.Member; else throw new NotSupportedException(string.Format("ContentBase type {0} is not supported.", entity.GetType().FullName)); return new GuidUdi(type, entity.Key).EnsureClosed(); } @@ -237,7 +237,7 @@ namespace Umbraco.Core public static GuidUdi GetUdi(this IRelationType entity) { if (entity == null) throw new ArgumentNullException("entity"); - return new GuidUdi(Constants.DeployEntityType.RelationType, entity.Key).EnsureClosed(); + return new GuidUdi(Constants.UdiEntityType.RelationType, entity.Key).EnsureClosed(); } /// @@ -285,18 +285,30 @@ namespace Umbraco.Core var member = entity as IMember; if (member != null) return member.GetUdi(); - var contentBase = entity as IContentBase; - if (contentBase != null) return contentBase.GetUdi(); + var stylesheet = entity as Stylesheet; + if (stylesheet != null) return stylesheet.GetUdi(); + + var script = entity as Script; + if (script != null) return script.GetUdi(); + + var dictionaryItem = entity as IDictionaryItem; + if (dictionaryItem != null) return dictionaryItem.GetUdi(); var macro = entity as IMacro; if (macro != null) return macro.GetUdi(); + var partialView = entity as IPartialView; + if (partialView != null) return partialView.GetUdi(); + + var xsltFile = entity as IXsltFile; + if (xsltFile != null) return xsltFile.GetUdi(); + + var contentBase = entity as IContentBase; + if (contentBase != null) return contentBase.GetUdi(); + var relationType = entity as IRelationType; if (relationType != null) return relationType.GetUdi(); - var dictionaryItem = entity as IDictionaryItem; - if (dictionaryItem != null) return dictionaryItem.GetUdi(); - throw new NotSupportedException(string.Format("Entity type {0} is not supported.", entity.GetType().FullName)); } } diff --git a/src/Umbraco.Core/UdiTypeConverter.cs b/src/Umbraco.Core/UdiTypeConverter.cs new file mode 100644 index 0000000000..110b899454 --- /dev/null +++ b/src/Umbraco.Core/UdiTypeConverter.cs @@ -0,0 +1,37 @@ +using System; +using System.ComponentModel; +using System.Globalization; + +namespace Umbraco.Core +{ + /// + /// A custom type converter for UDI + /// + /// + /// Primarily this is used so that WebApi can auto-bind a string parameter to a UDI instance + /// + internal class UdiTypeConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + if (sourceType == typeof(string)) + { + return true; + } + return base.CanConvertFrom(context, sourceType); + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + if (value is string) + { + Udi udi; + if (Udi.TryParse((string)value, out udi)) + { + return udi; + } + } + return base.ConvertFrom(context, culture, value); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 94267cf21f..2720bb910b 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -163,6 +163,7 @@ + @@ -289,7 +290,6 @@ - @@ -313,7 +313,10 @@ + + + @@ -361,7 +364,6 @@ - @@ -450,6 +452,7 @@ + @@ -479,7 +482,10 @@ + + + @@ -558,7 +564,7 @@ - + @@ -1435,9 +1441,11 @@ + + diff --git a/src/Umbraco.Tests/IO/IOHelperTest.cs b/src/Umbraco.Tests/IO/IOHelperTest.cs index 559ba1a2f3..e42491454b 100644 --- a/src/Umbraco.Tests/IO/IOHelperTest.cs +++ b/src/Umbraco.Tests/IO/IOHelperTest.cs @@ -56,5 +56,14 @@ namespace Umbraco.Tests.IO Assert.AreEqual(IOHelper.MapPath(SystemDirectories.WebServices, true), IOHelper.MapPath(SystemDirectories.WebServices, false)); Assert.AreEqual(IOHelper.MapPath(SystemDirectories.Xslt, true), IOHelper.MapPath(SystemDirectories.Xslt, false)); } + + [Test] + public void EnsurePathIsApplicationRootPrefixed() + { + //Assert + Assert.AreEqual("~/Views/Template.cshtml", IOHelper.EnsurePathIsApplicationRootPrefixed("Views/Template.cshtml")); + Assert.AreEqual("~/Views/Template.cshtml", IOHelper.EnsurePathIsApplicationRootPrefixed("/Views/Template.cshtml")); + Assert.AreEqual("~/Views/Template.cshtml", IOHelper.EnsurePathIsApplicationRootPrefixed("~/Views/Template.cshtml")); + } } } diff --git a/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs b/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs index 793d23212f..a4a4cd5222 100644 --- a/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs +++ b/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs @@ -365,7 +365,8 @@ namespace Umbraco.Tests.IO ss.Complete(); - Assert.IsTrue(File.Exists(path + "/ShadowSystem/path/to/some/dir/f1.txt")); // *not* cleaning + // yes we are cleaning now + //Assert.IsTrue(File.Exists(path + "/ShadowSystem/path/to/some/dir/f1.txt")); // *not* cleaning Assert.IsTrue(File.Exists(path + "/ShadowTests/path/to/some/dir/f1.txt")); Assert.IsFalse(File.Exists(path + "/ShadowTests/sub/sub/f2.txt")); } @@ -559,7 +560,7 @@ namespace Umbraco.Tests.IO Assert.AreEqual(1, ae.InnerExceptions.Count); e = ae.InnerExceptions[0]; Assert.IsNotNull(e.InnerException); - Assert.IsInstanceOf(e.InnerException); + Assert.IsInstanceOf(e.InnerException); } // still, the rest of the changes has been applied ok diff --git a/src/Umbraco.Tests/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs index b2f8ff9d73..4f1569ac7b 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs @@ -441,7 +441,7 @@ namespace Umbraco.Tests.Persistence.Repositories { // Act - var exists = repository.Exists(1034); //Content picker + var exists = repository.Exists(1046); //Content picker var doesntExist = repository.Exists(-80); // Assert diff --git a/src/Umbraco.Tests/Persistence/SyntaxProvider/SqlCeSyntaxProviderTests.cs b/src/Umbraco.Tests/Persistence/SyntaxProvider/SqlCeSyntaxProviderTests.cs index fafddb8dfd..105b3d0c11 100644 --- a/src/Umbraco.Tests/Persistence/SyntaxProvider/SqlCeSyntaxProviderTests.cs +++ b/src/Umbraco.Tests/Persistence/SyntaxProvider/SqlCeSyntaxProviderTests.cs @@ -121,6 +121,19 @@ WHERE (([umbracoNode].[nodeObjectType] = @0))) x)".Replace(Environment.NewLine, Assert.AreEqual("CREATE UNIQUE NONCLUSTERED INDEX [IX_A] ON [TheTable] ([A])", createExpression.ToString()); } + [Test] + public void CreateIndexBuilder_SqlServer_Unique_CreatesUniqueNonClusteredIndex_Multi_Columnn() + { + var sqlSyntax = new SqlServerSyntaxProvider(); + var createExpression = new CreateIndexExpression(DatabaseProviders.SqlServer, new[] { DatabaseProviders.SqlServer }, sqlSyntax) + { + Index = { Name = "IX_AB" } + }; + var builder = new CreateIndexBuilder(createExpression); + builder.OnTable("TheTable").OnColumn("A").Ascending().OnColumn("B").Ascending().WithOptions().Unique(); + Assert.AreEqual("CREATE UNIQUE NONCLUSTERED INDEX [IX_AB] ON [TheTable] ([A],[B])", createExpression.ToString()); + } + [Test] public void CreateIndexBuilder_SqlServer_Clustered_CreatesClusteredIndex() { diff --git a/src/Umbraco.Tests/Scoping/LeakTests.cs b/src/Umbraco.Tests/Scoping/LeakTests.cs index 1e4fd00feb..55b80062a4 100644 --- a/src/Umbraco.Tests/Scoping/LeakTests.cs +++ b/src/Umbraco.Tests/Scoping/LeakTests.cs @@ -2,6 +2,7 @@ using System.Data; using System.Runtime.Remoting.Messaging; using NUnit.Framework; +using Umbraco.Core; using Umbraco.Core.Persistence; using Umbraco.Core.Scoping; using Umbraco.Tests.TestHelpers; @@ -38,11 +39,14 @@ namespace Umbraco.Tests.Scoping _database.BeginTransaction(); // opens and maintains a connection // the test is leaking a scope with a non-null database - var contextScope = CallContext.LogicalGetData(ScopeProvider.ScopeItemKey); - Assert.IsNotNull(contextScope); - Assert.IsInstanceOf(CallContext.LogicalGetData(ScopeProvider.ScopeItemKey)); - Assert.IsNotNull(((NoScope) contextScope).DatabaseOrNull); - Assert.AreSame(_database, ((NoScope)contextScope).DatabaseOrNull); + var contextGuid = CallContext.LogicalGetData(ScopeProvider.ScopeItemKey).AsGuid(); + Assert.AreNotEqual(Guid.Empty, contextGuid); + + // only if Core.DEBUG_SCOPES are defined + //var contextScope = DatabaseContext.ScopeProvider.CallContextObjects[contextGuid] as NoScope; + //Assert.IsNotNull(contextScope); + //Assert.IsNotNull(contextScope.DatabaseOrNull); + //Assert.AreSame(_database, contextScope.DatabaseOrNull); // save the connection _connection = _database.Connection; diff --git a/src/Umbraco.Tests/Scoping/ScopeEventDispatcherTests.cs b/src/Umbraco.Tests/Scoping/ScopeEventDispatcherTests.cs index 7d5754529e..b68e2a3f9f 100644 --- a/src/Umbraco.Tests/Scoping/ScopeEventDispatcherTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopeEventDispatcherTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using Moq; using NUnit.Framework; @@ -8,7 +9,6 @@ using Umbraco.Core.Scoping; namespace Umbraco.Tests.Scoping { - [TestFixture] public class ScopeEventDispatcherTests { @@ -21,11 +21,15 @@ namespace Umbraco.Tests.Scoping DoThing3 = null; } - [TestCase(EventsDispatchMode.PassThrough, true, true)] - [TestCase(EventsDispatchMode.PassThrough, true, false)] - [TestCase(EventsDispatchMode.PassThrough, false, true)] - [TestCase(EventsDispatchMode.PassThrough, false, false)] - public void PassThroughCancelable(EventsDispatchMode mode, bool cancel, bool complete) + [TestCase(false, true, true)] + [TestCase(false, true, false)] + [TestCase(false, false, true)] + [TestCase(false, false, false)] + [TestCase(true, true, true)] + [TestCase(true, true, false)] + [TestCase(true, false, true)] + [TestCase(true, false, false)] + public void EventsHandling(bool passive, bool cancel, bool complete) { var counter1 = 0; var counter2 = 0; @@ -34,7 +38,7 @@ namespace Umbraco.Tests.Scoping DoThing2 += (sender, args) => { counter2++; }; var scopeProvider = new ScopeProvider(Mock.Of()); - using (var scope = scopeProvider.CreateScope(dispatchMode: mode)) + using (var scope = scopeProvider.CreateScope(eventDispatcher: passive ? new PassiveEventDispatcher() : null)) { var cancelled = scope.Events.DispatchCancelable(DoThing1, this, new SaveEventArgs("test")); if (cancelled == false) @@ -43,22 +47,15 @@ namespace Umbraco.Tests.Scoping scope.Complete(); } - var expected1 = mode == EventsDispatchMode.Passive ? 0 : 1; + var expected1 = passive ? 0 : 1; Assert.AreEqual(expected1, counter1); - var expected2 = -1; - switch (mode) - { - case EventsDispatchMode.PassThrough: - expected2 = cancel ? 0 : 1; - break; - case EventsDispatchMode.Scope: - expected2 = cancel ? 0 : (complete ? 1 : 0); - break; - case EventsDispatchMode.Passive: - expected2 = 0; - break; - } + int expected2; + if (passive) + expected2 = 0; + else + expected2 = cancel ? 0 : (complete ? 1 : 0); + Assert.AreEqual(expected2, counter2); } @@ -70,7 +67,7 @@ namespace Umbraco.Tests.Scoping DoThing3 += OnDoThingFail; var scopeProvider = new ScopeProvider(Mock.Of()); - using (var scope = scopeProvider.CreateScope(dispatchMode: EventsDispatchMode.Passive)) + using (var scope = scopeProvider.CreateScope(eventDispatcher: new PassiveEventDispatcher())) { scope.Events.Dispatch(DoThing1, this, new SaveEventArgs("test")); scope.Events.Dispatch(DoThing2, this, new SaveEventArgs(0)); @@ -94,45 +91,14 @@ namespace Umbraco.Tests.Scoping [TestCase(true)] [TestCase(false)] - public void EventsDispatchmode_PassThrough(bool complete) - { - var counter = 0; - - DoThing1 += (sender, args) => { counter++; }; - DoThing2 += (sender, args) => { counter++; }; - DoThing3 += (sender, args) => { counter++; }; - - var scopeProvider = new ScopeProvider(Mock.Of()); - using (var scope = scopeProvider.CreateScope(dispatchMode: EventsDispatchMode.PassThrough)) - { - scope.Events.Dispatch(DoThing1, this, new SaveEventArgs("test")); - scope.Events.Dispatch(DoThing2, this, new SaveEventArgs(0)); - scope.Events.Dispatch(DoThing3, this, new SaveEventArgs(0)); - - // events have not been queued - Assert.IsEmpty(scope.Events.GetEvents(EventDefinitionFilter.All)); - - // events have been raised - Assert.AreEqual(3, counter); - - if (complete) - scope.Complete(); - } - - // nothing has changed - Assert.AreEqual(3, counter); - } - - [TestCase(true)] - [TestCase(false)] - public void EventsDispatchMode_Passive(bool complete) + public void EventsDispatching_Passive(bool complete) { DoThing1 += OnDoThingFail; DoThing2 += OnDoThingFail; DoThing3 += OnDoThingFail; var scopeProvider = new ScopeProvider(Mock.Of()); - using (var scope = scopeProvider.CreateScope(dispatchMode: EventsDispatchMode.Passive)) + using (var scope = scopeProvider.CreateScope(eventDispatcher: new PassiveEventDispatcher())) { scope.Events.Dispatch(DoThing1, this, new SaveEventArgs("test")); scope.Events.Dispatch(DoThing2, this, new SaveEventArgs(0)); @@ -150,7 +116,7 @@ namespace Umbraco.Tests.Scoping [TestCase(true)] [TestCase(false)] - public void EventsDispatchMode_Scope(bool complete) + public void EventsDispatching_Scope(bool complete) { var counter = 0; IScope ambientScope = null; @@ -170,7 +136,7 @@ namespace Umbraco.Tests.Scoping }; Guid guid; - using (var scope = scopeProvider.CreateScope(dispatchMode: EventsDispatchMode.Scope)) + using (var scope = scopeProvider.CreateScope()) { Assert.IsNotNull(scopeProvider.AmbientContext); guid = scopeProvider.Context.Enlist("value", Guid.NewGuid, (c, o) => { }); @@ -181,6 +147,7 @@ namespace Umbraco.Tests.Scoping // events have been queued Assert.AreEqual(3, scope.Events.GetEvents(EventDefinitionFilter.All).Count()); + Assert.AreEqual(0, counter); if (complete) scope.Complete(); @@ -215,5 +182,17 @@ namespace Umbraco.Tests.Scoping public static event EventHandler> DoThing2; public static event TypedEventHandler> DoThing3; - } + + public class PassiveEventDispatcher : ScopeEventDispatcherBase + { + public PassiveEventDispatcher() + : base(false) + { } + + protected override void ScopeExitCompleted() + { + // do nothing + } + } + } } \ No newline at end of file diff --git a/src/Umbraco.Tests/Scoping/ScopeTests.cs b/src/Umbraco.Tests/Scoping/ScopeTests.cs index 446b7ea9f0..747e5b1f40 100644 --- a/src/Umbraco.Tests/Scoping/ScopeTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopeTests.cs @@ -1,10 +1,12 @@ using System; +using System.Collections; +using System.Runtime.Remoting.Messaging; +using System.Threading; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Persistence; using Umbraco.Core.Scoping; using Umbraco.Tests.TestHelpers; -using Umbraco.Core.Events; namespace Umbraco.Tests.Scoping { @@ -97,6 +99,54 @@ namespace Umbraco.Tests.Scoping Assert.IsNull(scopeProvider.AmbientScope); } + [Test] + public void NestedMigrateScope() + { + var scopeProvider = DatabaseContext.ScopeProvider; + Assert.IsNull(scopeProvider.AmbientScope); + + var httpContextItems = new Hashtable(); + ScopeProvider.HttpContextItemsGetter = () => httpContextItems; + try + { + using (var scope = scopeProvider.CreateScope()) + { + Assert.IsInstanceOf(scope); + Assert.IsNotNull(scopeProvider.AmbientScope); + Assert.AreSame(scope, scopeProvider.AmbientScope); + Assert.AreSame(scope, httpContextItems[ScopeProvider.ScopeItemKey]); + + // only if Core.DEBUG_SCOPES are defined + //Assert.IsEmpty(scopeProvider.CallContextObjects); + + using (var nested = scopeProvider.CreateScope(callContext: true)) + { + Assert.IsInstanceOf(nested); + Assert.IsNotNull(scopeProvider.AmbientScope); + Assert.AreSame(nested, scopeProvider.AmbientScope); + Assert.AreSame(scope, ((Scope) nested).ParentScope); + + // it's moved over to call context + Assert.IsNull(httpContextItems[ScopeProvider.ScopeItemKey]); + var callContextKey = CallContext.LogicalGetData(ScopeProvider.ScopeItemKey).AsGuid(); + Assert.AreNotEqual(Guid.Empty, callContextKey); + + // only if Core.DEBUG_SCOPES are defined + //var ccnested = scopeProvider.CallContextObjects[callContextKey]; + //Assert.AreSame(nested, ccnested); + } + + // it's naturally back in http context + Assert.AreSame(scope, httpContextItems[ScopeProvider.ScopeItemKey]); + } + Assert.IsNull(scopeProvider.AmbientScope); + } + finally + { + ScopeProvider.HttpContextItemsGetter = null; + } + } + [Test] public void NestedCreateScopeContext() { @@ -478,17 +528,82 @@ namespace Umbraco.Tests.Scoping } [Test] - public void CallContextScope() + public void CallContextScope1() { var scopeProvider = DatabaseContext.ScopeProvider; - var scope = scopeProvider.CreateScope(); - Assert.IsNotNull(scopeProvider.AmbientScope); - using (new SafeCallContext()) + using (var scope = scopeProvider.CreateScope()) { - Assert.IsNull(scopeProvider.AmbientScope); + Assert.IsNotNull(scopeProvider.AmbientScope); + Assert.IsNotNull(scopeProvider.AmbientContext); + using (new SafeCallContext()) + { + Assert.IsNull(scopeProvider.AmbientScope); + Assert.IsNull(scopeProvider.AmbientContext); + + using (var newScope = scopeProvider.CreateScope()) + { + Assert.IsNotNull(scopeProvider.AmbientScope); + Assert.IsNull(scopeProvider.AmbientScope.ParentScope); + Assert.IsNotNull(scopeProvider.AmbientContext); + } + + Assert.IsNull(scopeProvider.AmbientScope); + Assert.IsNull(scopeProvider.AmbientContext); + } + Assert.IsNotNull(scopeProvider.AmbientScope); + Assert.AreSame(scope, scopeProvider.AmbientScope); + } + + Assert.IsNull(scopeProvider.AmbientScope); + Assert.IsNull(scopeProvider.AmbientContext); + } + + [Test] + public void CallContextScope2() + { + var scopeProvider = DatabaseContext.ScopeProvider; + Assert.IsNull(scopeProvider.AmbientScope); + + var httpContextItems = new Hashtable(); + ScopeProvider.HttpContextItemsGetter = () => httpContextItems; + try + { + using (var scope = scopeProvider.CreateScope()) + { + Assert.IsNotNull(scopeProvider.AmbientScope); + Assert.IsNotNull(scopeProvider.AmbientContext); + using (new SafeCallContext()) + { + // pretend it's another thread + ScopeProvider.HttpContextItemsGetter = null; + + Assert.IsNull(scopeProvider.AmbientScope); + Assert.IsNull(scopeProvider.AmbientContext); + + using (var newScope = scopeProvider.CreateScope()) + { + Assert.IsNotNull(scopeProvider.AmbientScope); + Assert.IsNull(scopeProvider.AmbientScope.ParentScope); + Assert.IsNotNull(scopeProvider.AmbientContext); + } + + Assert.IsNull(scopeProvider.AmbientScope); + Assert.IsNull(scopeProvider.AmbientContext); + + // back to original thread + ScopeProvider.HttpContextItemsGetter = () => httpContextItems; + } + Assert.IsNotNull(scopeProvider.AmbientScope); + Assert.AreSame(scope, scopeProvider.AmbientScope); + } + + Assert.IsNull(scopeProvider.AmbientScope); + Assert.IsNull(scopeProvider.AmbientContext); + } + finally + { + ScopeProvider.HttpContextItemsGetter = null; } - Assert.IsNotNull(scopeProvider.AmbientScope); - Assert.AreSame(scope, scopeProvider.AmbientScope); } [Test] diff --git a/src/Umbraco.Tests/Scoping/ScopedXmlTests.cs b/src/Umbraco.Tests/Scoping/ScopedXmlTests.cs index 179c924f7b..6267e1dd1c 100644 --- a/src/Umbraco.Tests/Scoping/ScopedXmlTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopedXmlTests.cs @@ -213,12 +213,8 @@ namespace Umbraco.Tests.Scoping var scopeProvider = ApplicationContext.Current.ScopeProvider as IScopeProviderInternal; Assert.IsNotNull(scopeProvider); - if (complete) - // because some event handlers trigger xml refresh with directly uses the DB - Assert.IsNotNull(scopeProvider.AmbientScope); - else - // because nothing happened - Assert.IsNull(scopeProvider.AmbientScope); + // ambient scope may be null, or maybe not, depending on whether the code that + // was called did proper scoped work, or some direct (NoScope) use of the database Assert.IsNull(scopeProvider.AmbientContext); // limited number of clones! diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index e360018104..2b3a5e1323 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -1343,6 +1343,69 @@ namespace Umbraco.Tests.Services Assert.That(contents.Any(), Is.False); } + [Test] + public void Can_Empty_RecycleBin_With_Content_That_Has_All_Related_Data() + { + // Arrange + //need to: + // * add relations + // * add permissions + // * add notifications + // * public access + // * tags + // * domain + // * published & preview data + // * multiple versions + + var contentType = MockedContentTypes.CreateAllTypesContentType("test", "test"); + ServiceContext.ContentTypeService.Save(contentType, 0); + + object obj = + new + { + tags = "Hello,World" + }; + var content1 = MockedContent.CreateBasicContent(contentType); + content1.PropertyValues(obj); + content1.ResetDirtyProperties(false); + ServiceContext.ContentService.Save(content1, 0); + Assert.IsTrue(ServiceContext.ContentService.PublishWithStatus(content1, 0).Success); + var content2 = MockedContent.CreateBasicContent(contentType); + content2.PropertyValues(obj); + content2.ResetDirtyProperties(false); + ServiceContext.ContentService.Save(content2, 0); + Assert.IsTrue(ServiceContext.ContentService.PublishWithStatus(content2, 0).Success); + + ServiceContext.RelationService.Save(new RelationType(Constants.ObjectTypes.DocumentGuid, Constants.ObjectTypes.DocumentGuid, "test")); + Assert.IsNotNull(ServiceContext.RelationService.Relate(content1, content2, "test")); + + ServiceContext.PublicAccessService.Save(new PublicAccessEntry(content1, content2, content2, new List + { + new PublicAccessRule + { + RuleType = "test", + RuleValue = "test" + } + })); + Assert.IsTrue(ServiceContext.PublicAccessService.AddRule(content1, "test2", "test2").Success); + + Assert.IsNotNull(ServiceContext.NotificationService.CreateNotification(ServiceContext.UserService.GetUserById(0), content1, "test")); + + ServiceContext.ContentService.AssignContentPermission(content1, 'A', new[] {0}); + + Assert.IsTrue(ServiceContext.DomainService.Save(new UmbracoDomain("www.test.com", "en-AU") + { + RootContentId = content1.Id + }).Success); + + // Act + ServiceContext.ContentService.EmptyRecycleBin(); + var contents = ServiceContext.ContentService.GetContentInRecycleBin(); + + // Assert + Assert.That(contents.Any(), Is.False); + } + [Test] public void Can_Move_Content() { diff --git a/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs b/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs index 70f48b12fb..2370ad1dfd 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs @@ -72,11 +72,12 @@ namespace Umbraco.Tests.TestHelpers GetDbProviderName(), Logger); - // fixme - bah - this is needed to reset static properties? Stephen to update this note + // ensure we start tests in a clean state ie without any scope in context + // anything that used a true 'Scope' would have removed it, but there could + // be a rogue 'NoScope' there - and we want to make sure it is gone var scopeProvider = new ScopeProvider(null); if (scopeProvider.AmbientScope != null) - scopeProvider.AmbientScope.Dispose(); - scopeProvider.AmbientScope = null; + scopeProvider.AmbientScope.Dispose(); // removes scope from context base.Initialize(); diff --git a/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs b/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs index d517f81db3..893900677e 100644 --- a/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs +++ b/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs @@ -358,9 +358,9 @@ namespace Umbraco.Tests.TestHelpers.Entities contentCollection.Add(new PropertyType(Constants.PropertyEditors.DateAlias, DataTypeDatabaseType.Date) { Alias = "date", Name = "Date", Mandatory = false, SortOrder = 13, DataTypeDefinitionId = -41 }); contentCollection.Add(new PropertyType(Constants.PropertyEditors.DropDownListAlias, DataTypeDatabaseType.Integer) { Alias = "ddl", Name = "Dropdown List", Mandatory = false, SortOrder = 14, DataTypeDefinitionId = -42 }); contentCollection.Add(new PropertyType(Constants.PropertyEditors.CheckBoxListAlias, DataTypeDatabaseType.Nvarchar) { Alias = "chklist", Name = "Checkbox List", Mandatory = false, SortOrder = 15, DataTypeDefinitionId = -43 }); - contentCollection.Add(new PropertyType(Constants.PropertyEditors.ContentPickerAlias, DataTypeDatabaseType.Integer) { Alias = "contentPicker", Name = "Content Picker", Mandatory = false, SortOrder = 16, DataTypeDefinitionId = 1034 }); - contentCollection.Add(new PropertyType(Constants.PropertyEditors.MediaPickerAlias, DataTypeDatabaseType.Integer) { Alias = "mediaPicker", Name = "Media Picker", Mandatory = false, SortOrder = 17, DataTypeDefinitionId = 1035 }); - contentCollection.Add(new PropertyType(Constants.PropertyEditors.MemberPickerAlias, DataTypeDatabaseType.Integer) { Alias = "memberPicker", Name = "Member Picker", Mandatory = false, SortOrder = 18, DataTypeDefinitionId = 1036 }); + contentCollection.Add(new PropertyType(Constants.PropertyEditors.ContentPicker2Alias, DataTypeDatabaseType.Integer) { Alias = "contentPicker", Name = "Content Picker", Mandatory = false, SortOrder = 16, DataTypeDefinitionId = 1046 }); + contentCollection.Add(new PropertyType(Constants.PropertyEditors.MediaPicker2Alias, DataTypeDatabaseType.Integer) { Alias = "mediaPicker", Name = "Media Picker", Mandatory = false, SortOrder = 17, DataTypeDefinitionId = 1048 }); + contentCollection.Add(new PropertyType(Constants.PropertyEditors.MemberPicker2Alias, DataTypeDatabaseType.Integer) { Alias = "memberPicker", Name = "Member Picker", Mandatory = false, SortOrder = 18, DataTypeDefinitionId = 1047 }); contentCollection.Add(new PropertyType(Constants.PropertyEditors.RelatedLinksAlias, DataTypeDatabaseType.Ntext) { Alias = "relatedLinks", Name = "Related Links", Mandatory = false, SortOrder = 21, DataTypeDefinitionId = 1040 }); contentCollection.Add(new PropertyType(Constants.PropertyEditors.TagsAlias, DataTypeDatabaseType.Ntext) { Alias = "tags", Name = "Tags", Mandatory = false, SortOrder = 22, DataTypeDefinitionId = 1041 }); diff --git a/src/Umbraco.Tests/UdiTests.cs b/src/Umbraco.Tests/UdiTests.cs index 36242bab13..9b803d5fa3 100644 --- a/src/Umbraco.Tests/UdiTests.cs +++ b/src/Umbraco.Tests/UdiTests.cs @@ -13,41 +13,41 @@ namespace Umbraco.Tests [Test] public void StringEntityCtorTest() { - var udi = new StringUdi(Constants.DeployEntityType.AnyString, "test-id"); - Assert.AreEqual(Constants.DeployEntityType.AnyString, udi.EntityType); + var udi = new StringUdi(Constants.UdiEntityType.AnyString, "test-id"); + Assert.AreEqual(Constants.UdiEntityType.AnyString, udi.EntityType); Assert.AreEqual("test-id", udi.Id); - Assert.AreEqual("umb://" + Constants.DeployEntityType.AnyString + "/test-id", udi.ToString()); + Assert.AreEqual("umb://" + Constants.UdiEntityType.AnyString + "/test-id", udi.ToString()); } [Test] public void StringEntityParseTest() { - var udi = Udi.Parse("umb://" + Constants.DeployEntityType.AnyString + "/test-id"); - Assert.AreEqual(Constants.DeployEntityType.AnyString, udi.EntityType); + var udi = Udi.Parse("umb://" + Constants.UdiEntityType.AnyString + "/test-id"); + Assert.AreEqual(Constants.UdiEntityType.AnyString, udi.EntityType); Assert.IsInstanceOf(udi); var stringEntityId = udi as StringUdi; Assert.IsNotNull(stringEntityId); Assert.AreEqual("test-id", stringEntityId.Id); - Assert.AreEqual("umb://" + Constants.DeployEntityType.AnyString + "/test-id", udi.ToString()); + Assert.AreEqual("umb://" + Constants.UdiEntityType.AnyString + "/test-id", udi.ToString()); } [Test] public void GuidEntityCtorTest() { var guid = Guid.NewGuid(); - var udi = new GuidUdi(Constants.DeployEntityType.AnyGuid, guid); - Assert.AreEqual(Constants.DeployEntityType.AnyGuid, udi.EntityType); + var udi = new GuidUdi(Constants.UdiEntityType.AnyGuid, guid); + Assert.AreEqual(Constants.UdiEntityType.AnyGuid, udi.EntityType); Assert.AreEqual(guid, udi.Guid); - Assert.AreEqual("umb://" + Constants.DeployEntityType.AnyGuid + "/" + guid.ToString("N"), udi.ToString()); + Assert.AreEqual("umb://" + Constants.UdiEntityType.AnyGuid + "/" + guid.ToString("N"), udi.ToString()); } [Test] public void GuidEntityParseTest() { var guid = Guid.NewGuid(); - var s = "umb://" + Constants.DeployEntityType.AnyGuid + "/" + guid.ToString("N"); + var s = "umb://" + Constants.UdiEntityType.AnyGuid + "/" + guid.ToString("N"); var udi = Udi.Parse(s); - Assert.AreEqual(Constants.DeployEntityType.AnyGuid, udi.EntityType); + Assert.AreEqual(Constants.UdiEntityType.AnyGuid, udi.EntityType); Assert.IsInstanceOf(udi); var gudi = udi as GuidUdi; Assert.IsNotNull(gudi); @@ -82,9 +82,9 @@ namespace Umbraco.Tests var guid1 = Guid.NewGuid(); var entities = new[] { - new GuidUdi(Constants.DeployEntityType.AnyGuid, guid1), - new GuidUdi(Constants.DeployEntityType.AnyGuid, guid1), - new GuidUdi(Constants.DeployEntityType.AnyGuid, guid1), + new GuidUdi(Constants.UdiEntityType.AnyGuid, guid1), + new GuidUdi(Constants.UdiEntityType.AnyGuid, guid1), + new GuidUdi(Constants.UdiEntityType.AnyGuid, guid1), }; Assert.AreEqual(1, entities.Distinct().Count()); } @@ -93,12 +93,12 @@ namespace Umbraco.Tests public void CreateTest() { var guid = Guid.NewGuid(); - var udi = Udi.Create(Constants.DeployEntityType.AnyGuid, guid); - Assert.AreEqual(Constants.DeployEntityType.AnyGuid, udi.EntityType); + var udi = Udi.Create(Constants.UdiEntityType.AnyGuid, guid); + Assert.AreEqual(Constants.UdiEntityType.AnyGuid, udi.EntityType); Assert.AreEqual(guid, ((GuidUdi)udi).Guid); - Assert.Throws(() => Udi.Create(Constants.DeployEntityType.AnyString, guid)); - Assert.Throws(() => Udi.Create(Constants.DeployEntityType.AnyGuid, "foo")); + Assert.Throws(() => Udi.Create(Constants.UdiEntityType.AnyString, guid)); + Assert.Throws(() => Udi.Create(Constants.UdiEntityType.AnyGuid, "foo")); Assert.Throws(() => Udi.Create("barf", "foo")); } @@ -106,13 +106,13 @@ namespace Umbraco.Tests public void RangeTest() { // can parse open string udi - var stringUdiString = "umb://" + Constants.DeployEntityType.AnyString; + var stringUdiString = "umb://" + Constants.UdiEntityType.AnyString; Udi stringUdi; Assert.IsTrue(Udi.TryParse(stringUdiString, out stringUdi)); Assert.AreEqual(string.Empty, ((StringUdi)stringUdi).Id); // can parse open guid udi - var guidUdiString = "umb://" + Constants.DeployEntityType.AnyGuid; + var guidUdiString = "umb://" + Constants.UdiEntityType.AnyGuid; Udi guidUdi; Assert.IsTrue(Udi.TryParse(guidUdiString, out guidUdi)); Assert.AreEqual(Guid.Empty, ((GuidUdi)guidUdi).Guid); @@ -134,12 +134,12 @@ namespace Umbraco.Tests var guid = Guid.NewGuid(); - var udi = new GuidUdi(Constants.DeployEntityType.AnyGuid, guid); + var udi = new GuidUdi(Constants.UdiEntityType.AnyGuid, guid); var json = JsonConvert.SerializeObject(udi, settings); Assert.AreEqual(string.Format("\"umb://any-guid/{0:N}\"", guid), json); var dudi = JsonConvert.DeserializeObject(json, settings); - Assert.AreEqual(Constants.DeployEntityType.AnyGuid, dudi.EntityType); + Assert.AreEqual(Constants.UdiEntityType.AnyGuid, dudi.EntityType); Assert.AreEqual(guid, ((GuidUdi)dudi).Guid); var range = new UdiRange(udi, Constants.DeploySelector.ChildrenOfThis); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbbreadcrumbs.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbbreadcrumbs.directive.js index f2797425c5..61906bbf74 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbbreadcrumbs.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbbreadcrumbs.directive.js @@ -40,27 +40,50 @@ Use this directive to generate a list of breadcrumbs. @param {array} ancestors Array of ancestors @param {string} entityType The content entity type (member, media, content). +@param {callback} Callback when an ancestor is clicked. It will override the default link behaviour. **/ -(function() { - 'use strict'; +(function () { + 'use strict'; - function BreadcrumbsDirective() { + function BreadcrumbsDirective() { - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/editor/umb-breadcrumbs.html', - scope: { - ancestors: "=", - entityType: "@" - } - }; + function link(scope, el, attr, ctrl) { - return directive; + scope.allowOnOpen = false; - } + scope.open = function(ancestor) { + if(scope.onOpen && scope.allowOnOpen) { + scope.onOpen({'ancestor': ancestor}); + } + }; - angular.module('umbraco.directives').directive('umbBreadcrumbs', BreadcrumbsDirective); + function onInit() { + if ("onOpen" in attr) { + scope.allowOnOpen = true; + } + } + + onInit(); + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-breadcrumbs.html', + scope: { + ancestors: "=", + entityType: "@", + onOpen: "&" + }, + link: link + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbBreadcrumbs', BreadcrumbsDirective); })(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js index 75d0144982..7a74716ea1 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js @@ -21,7 +21,8 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat customtreeparams: '@', eventhandler: '=', enablecheckboxes: '@', - enablelistviewsearch: '@' + enablelistviewsearch: '@', + enablelistviewexpand: '@' }, compile: function(element, attrs) { @@ -35,7 +36,7 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat '' + ''; template += '
    ' + - '' + + '' + '
' + '' + ''; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js index f4fe0db4e6..b32942791c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js @@ -27,6 +27,7 @@ angular.module("umbraco.directives") section: '@', eventhandler: '=', currentNode: '=', + enablelistviewexpand: '@', node: '=', tree: '=' }, @@ -38,7 +39,7 @@ angular.module("umbraco.directives") '
' + //NOTE: This ins element is used to display the search icon if the node is a container/listview and the tree is currently in dialog //'' + - ' ' + + ' ' + '' + '' + //NOTE: These are the 'option' elipses @@ -74,11 +75,12 @@ angular.module("umbraco.directives") //toggle visibility of last 'ins' depending on children //visibility still ensure the space is "reserved", so both nodes with and without children are aligned. - if (!node.hasChildren) { - element.find("ins").last().css("visibility", "hidden"); + + if (node.hasChildren || node.metaData.isContainer && scope.enablelistviewexpand === "true") { + element.find("ins").last().css("visibility", "visible"); } else { - element.find("ins").last().css("visibility", "visible"); + element.find("ins").last().css("visibility", "hidden"); } var icon = element.find("i:first"); @@ -192,7 +194,7 @@ angular.module("umbraco.directives") emits treeNodeCollapsing event if already expanded and treeNodeExpanding if collapsed */ scope.load = function (node) { - if (node.expanded) { + if (node.expanded && !node.metaData.isContainer) { deleteAnimations = false; emitEvent("treeNodeCollapsing", { tree: scope.tree, node: node, element: element }); node.expanded = false; @@ -227,7 +229,7 @@ angular.module("umbraco.directives") setupNodeDom(scope.node, scope.tree); - var template = '
'; + var template = '
'; var newElement = angular.element(template); $compile(newElement)(scope); element.append(newElement); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbminilistview.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbminilistview.directive.js new file mode 100644 index 0000000000..281242621a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbminilistview.directive.js @@ -0,0 +1,209 @@ +(function () { + 'use strict'; + + function MiniListViewDirective(contentResource, memberResource, mediaResource) { + + function link(scope, el, attr, ctrl) { + + scope.search = ""; + scope.miniListViews = []; + scope.breadcrumb = []; + + var miniListViewsHistory = []; + var goingForward = true; + var skipAnimation = true; + + function onInit() { + open(scope.node); + } + + function open(node) { + + goingForward = true; + + var miniListView = { + node: node, + loading: true, + pagination: { + pageSize: 10, + pageNumber: 1, + filter: '', + orderDirection: "Ascending", + orderBy: "SortOrder", + orderBySystemField: true + } + }; + + // clear and push mini list view in dom so we only render 1 view + scope.miniListViews = []; + scope.miniListViews.push(miniListView); + + // store in history so we quickly can navigate back + miniListViewsHistory.push(miniListView); + + // get children + getChildrenForMiniListView(miniListView); + + makeBreadcrumb(); + + } + + function getChildrenForMiniListView(miniListView) { + + // start loading animation list view + miniListView.loading = true; + + // setup the correct resource depending on section + var resource = ""; + + if (scope.entityType === "Member") { + resource = memberResource.getPagedResults; + } else if (scope.entityType === "Media") { + resource = mediaResource.getChildren; + } else { + resource = contentResource.getChildren; + } + + resource(miniListView.node.id, miniListView.pagination) + .then(function (data) { + // update children + miniListView.children = data.items; + // update pagination + miniListView.pagination.totalItems = data.totalItems; + miniListView.pagination.totalPages = data.totalPages; + // stop load indicator + miniListView.loading = false; + }); + } + + scope.openNode = function(event, node) { + open(node); + event.stopPropagation(); + }; + + scope.selectNode = function(node) { + if(scope.onSelect) { + scope.onSelect({'node': node}); + } + }; + + /* Pagination */ + scope.goToPage = function(pageNumber, miniListView) { + // set new page number + miniListView.pagination.pageNumber = pageNumber; + // get children + getChildrenForMiniListView(miniListView); + }; + + /* Breadcrumb */ + scope.clickBreadcrumb = function(ancestor) { + + var found = false; + goingForward = false; + + angular.forEach(miniListViewsHistory, function(historyItem, index){ + // We need to make sure we can compare the two id's. + // Some id's are integers and others are strings. + // Members have string ids like "all-members". + if(historyItem.node.id.toString() === ancestor.id.toString()) { + // load the list view from history + scope.miniListViews = []; + scope.miniListViews.push(historyItem); + // clean up history - remove all children after + miniListViewsHistory.splice(index + 1, miniListViewsHistory.length); + found = true; + } + }); + + if(!found) { + // if we can't find the view in the history - close the list view + scope.exitMiniListView(); + } + + // update the breadcrumb + makeBreadcrumb(); + + }; + + scope.showBackButton = function() { + // don't show the back button if the start node is a list view + if(scope.node.metaData && scope.node.metaData.IsContainer || scope.node.isContainer) { + return false; + } else { + return true; + } + }; + + scope.exitMiniListView = function() { + miniListViewsHistory = []; + scope.miniListViews = []; + if(scope.onClose) { + scope.onClose(); + } + }; + + function makeBreadcrumb() { + scope.breadcrumb = []; + angular.forEach(miniListViewsHistory, function(historyItem){ + scope.breadcrumb.push(historyItem.node); + }); + } + + /* Search */ + scope.searchMiniListView = function(search, miniListView) { + // set search value + miniListView.pagination.filter = search; + // reset pagination + miniListView.pagination.pageNumber = 1; + // start loading animation list view + miniListView.loading = true; + searchMiniListView(miniListView); + }; + + var searchMiniListView = _.debounce(function (miniListView) { + scope.$apply(function () { + getChildrenForMiniListView(miniListView); + }); + }, 500); + + /* Animation */ + scope.getMiniListViewAnimation = function() { + + // disable the first "slide-in-animation"" if the start node is a list view + if(scope.node.metaData && scope.node.metaData.IsContainer && skipAnimation || scope.node.isContainer && skipAnimation) { + skipAnimation = false; + return; + } + + if(goingForward) { + return 'umb-mini-list-view--forward'; + } else { + return 'umb-mini-list-view--backwards'; + } + }; + + onInit(); + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-mini-list-view.html', + scope: { + node: "=", + entityType: "@", + startNodeId: "=", + onSelect: "&", + onClose: "&" + }, + link: link + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbMiniListView', MiniListViewDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js index fe753171e1..610f8546a3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js @@ -134,25 +134,40 @@ Use this directive to generate a pagination. } - scope.next = function() { - if (scope.onNext && scope.pageNumber < scope.totalPages) { - scope.pageNumber++; - scope.onNext(scope.pageNumber); - } + scope.next = function () { + if (scope.pageNumber < scope.totalPages) { + scope.pageNumber++; + if (scope.onNext) { + scope.onNext(scope.pageNumber); + } + if (scope.onChange) { + scope.onChange({ "pageNumber": scope.pageNumber }); + } + } }; - scope.prev = function(pageNumber) { - if (scope.onPrev && scope.pageNumber > 1) { - scope.pageNumber--; - scope.onPrev(scope.pageNumber); - } + scope.prev = function (pageNumber) { + if (scope.pageNumber > 1) { + scope.pageNumber--; + if (scope.onPrev) { + scope.onPrev(scope.pageNumber); + } + if (scope.onChange) { + scope.onChange({ "pageNumber": scope.pageNumber }); + } + } }; - scope.goToPage = function(pageNumber) { - if(scope.onGoToPage) { - scope.pageNumber = pageNumber + 1; - scope.onGoToPage(scope.pageNumber); - } + scope.goToPage = function (pageNumber) { + scope.pageNumber = pageNumber + 1; + if (scope.onGoToPage) { + scope.onGoToPage(scope.pageNumber); + } + if (scope.onChange) { + if (scope.onChange) { + scope.onChange({ "pageNumber": scope.pageNumber }); + } + } }; var unbindPageNumberWatcher = scope.$watch('pageNumber', function(newValue, oldValue){ @@ -176,7 +191,8 @@ Use this directive to generate a pagination. totalPages: "=", onNext: "=", onPrev: "=", - onGoToPage: "=" + onGoToPage: "=", + onChange: "&" }, link: link }; diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/codefile.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/codefile.resource.js index e83bb4f14e..44454f21e3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/codefile.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/codefile.resource.js @@ -205,6 +205,38 @@ function codefileResource($q, $http, umbDataFormatter, umbRequestHelper) { "codeFileApiBaseUrl", "GetScaffold?type=" + type + "&id=" + id + "&snippetName=" + snippetName)), "Failed to get scaffold for" + type); + }, + + /** + * @ngdoc method + * @name umbraco.resources.codefileResource#createContainer + * @methodOf umbraco.resources.codefileResource + * + * @description + * Creates a container/folder + * + * ##usage + *
+         * codefileResource.createContainer("partialViews", "folder%2ffolder", "folder")
+         *    .then(function(data) {
+         *        alert('its here!');
+         *    });
+         * 
+ * + * @param {string} File type: (scripts, partialViews, partialViewMacros). + * @param {string} Parent Id: url encoded path + * @param {string} Container name + * @returns {Promise} resourcePromise object. + * + */ + + createContainer: function(type, parentId, name) { + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl( + "codeFileApiBaseUrl", + "PostCreateContainer", + { type: type, parentId: parentId, name: encodeURIComponent(name) })), + 'Failed to create a folder under parent id ' + parentId); } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js index 5e0f5deada..3defb691aa 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js @@ -166,24 +166,17 @@ function entityResource($q, $http, umbRequestHelper) { */ getByIds: function (ids, type) { - var query = ""; - _.each(ids, function(item) { - query += "ids=" + item + "&"; - }); - - // if ids array is empty we need a empty variable in the querystring otherwise the service returns a error - if (ids.length === 0) { - query += "ids=&"; - } - - query += "type=" + type; + var query = "type=" + type; return umbRequestHelper.resourcePromise( - $http.get( + $http.post( umbRequestHelper.getApiUrl( "entityApiBaseUrl", "GetByIds", - query)), + query), + { + ids: ids + }), 'Failed to retrieve entity data for ids ' + ids); }, diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index 6fbf12e815..be55d136da 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -118,6 +118,8 @@ @import "components/umb-avatar.less"; @import "components/umb-progress-bar.less"; @import "components/umb-querybuilder.less"; +@import "components/umb-pagination.less"; +@import "components/umb-mini-list-view.less"; @import "components/buttons/umb-button.less"; @import "components/buttons/umb-button-group.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-mini-list-view.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-mini-list-view.less new file mode 100644 index 0000000000..1804b45260 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-mini-list-view.less @@ -0,0 +1,97 @@ +.umb-mini-list-view__title { + display: flex; + align-items: center; +} + +.umb-mini-list-view__title-text { + font-size: 16px; + font-weight: bold; +} + +.umb-mini-list-view__title-icon { + font-size: 20px; + margin-right: 5px; +} + +.umb-mini-list-view__back { + font-size: 11px; + margin-right: 5px; + color: @gray; + display: flex; + align-items: center; +} + +.umb-mini-list-view__back-icon { + margin-right: 4px; + height: 11px; + line-height: 11px; +} + +.umb-mini-list-view__back-text { + text-decoration: underline; + margin-right: 5px; +} + +.umb-mini-list-view__back:hover { + opacity: 1; + text-decoration: none; + color: @black; +} + +/* Animations */ + +/* Forward */ + +.umb-mini-list-view--forward-enter, +.umb-mini-list-view--forward-leave +{ + transition: 120ms cubic-bezier(0.250, 0.250, 0.750, 0.750) all; + position: relative; + display: block; +} + +.umb-mini-list-view--forward-enter { + left: 100%; +} + +.umb-mini-list-view--forward-enter.umb-mini-list-view--forward-enter-active { + left: 0; + opacity: 1; +} + +.umb-mini-list-view--forward-leave { + left: 0; +} + +.umb-mini-list-view--forward-leave.umb-mini-list-view--forward-leave-active{ + left: -100%; + opacity: 0; +} + +/* Backwards */ + +.umb-mini-list-view--backwards-enter, +.umb-mini-list-view--backwards-leave +{ + transition: 120ms cubic-bezier(0.250, 0.250, 0.750, 0.750) all; + position: relative; + display: block; +} + +.umb-mini-list-view--backwards-enter { + right: 100%; +} + +.umb-mini-list-view--backwards-enter.umb-mini-list-view--backwards-enter-active { + right: 0; + opacity: 1; +} + +.umb-mini-list-view--backwards-leave { + left: 0; +} + +.umb-mini-list-view--backwards-leave.umb-mini-list-view--backwards-leave-active{ + right: -100%; + opacity: 0; +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-pagination.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-pagination.less new file mode 100644 index 0000000000..8d3d563cab --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-pagination.less @@ -0,0 +1,3 @@ +.umb-pagination ul { + box-shadow: none; +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less index bc8e19cf2b..9cf1b3cfc8 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less @@ -10,7 +10,7 @@ flex-wrap: nowrap; justify-content: space-between; - min-width: 640px; + min-width: auto; } .umb-table.umb-table-inactive { @@ -103,11 +103,17 @@ input.umb-table__input { // Table Body Styles +.umb-table-body { + position: relative; +} + .umb-table-body .umb-table-row { color: fade(@gray, 75%); border-top: 1px solid @grayLight; cursor: pointer; font-size: 13px; + position: relative; + min-height: 32px; &:hover { background-color: fade(@grayLighter, 90%); @@ -198,30 +204,33 @@ input.umb-table__input { } } - - - - - // Table Row Styles .umb-table-row { display: flex; flex-flow: row nowrap; align-items: center; - user-select: none; } +.umb-table-body .umb-table-row--empty { + flex: 1 1 auto; + display: flex; + justify-content: center; + padding: 5px 0; + cursor: auto; + user-select: auto; +} +.umb-table-body .umb-table-row--empty:hover { + background-color: transparent; + cursor: auto; +} .umb-table-row.-selected, .umb-table-row.-selected:hover { background-color: fade(@blueDark, 4%); } - - - // Table Cell Styles .umb-table-cell { display: flex; @@ -250,9 +259,44 @@ input.umb-table__input { padding: 15px 0; } +.umb-table-cell--auto-width { + flex: 0 0 auto !important; +} + +.umb-table-cell--faded { + opacity: 0.4; +} + // Increases the space for the name cell .umb-table__name { flex: 1 1 25%; max-width: 25%; } + +.umb-table__loading-overlay { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.7); + z-index: 1; +} + +.umb-table__row-expand { + font-size: 12px; + text-decoration: none; + color: @black; +} + +.umb-table--condensed { + + .umb-table-cell:first-of-type:not(.not-fixed) { + padding-top: 10px; + padding-bottom: 10px; + } + + .umb-table-body__icon { + font-size: 20px; + } + +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/main.less b/src/Umbraco.Web.UI.Client/src/less/main.less index 85ab4e6e56..f977684c25 100644 --- a/src/Umbraco.Web.UI.Client/src/less/main.less +++ b/src/Umbraco.Web.UI.Client/src/less/main.less @@ -156,6 +156,11 @@ h5.-black { margin-left: 0; } +/* CONTROL VALIDATION */ +.umb-control-required { + color: @controlRequiredColor; +} + .controls-row { padding-bottom: 5px; margin-left: 240px; diff --git a/src/Umbraco.Web.UI.Client/src/less/variables.less b/src/Umbraco.Web.UI.Client/src/less/variables.less index 8ebb0f08ef..4774410e7c 100644 --- a/src/Umbraco.Web.UI.Client/src/less/variables.less +++ b/src/Umbraco.Web.UI.Client/src/less/variables.less @@ -36,6 +36,7 @@ .red{color: @red;} .blue{color: @blue;} +.black{color: @black;} //icon colors for tree icons @@ -136,6 +137,7 @@ @inputDisabledBackground: @grayLighter; @formActionsBackground: #f5f5f5; @inputHeight: @baseLineHeight + 10px; // base line-height + 8px vertical padding + 2px top/bottom border +@controlRequiredColor: #ee5f5b; // Tabs @@ -351,4 +353,4 @@ // SORTABLE // -------------------------------------------------- @sortableHelperBg: rgba(4, 156, 219, 0.5); -@sortablePlaceholderBg : @blue; \ No newline at end of file +@sortablePlaceholderBg : @blue; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contentpicker/contentpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contentpicker/contentpicker.html index 8e70ad163c..43eab532d4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contentpicker/contentpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contentpicker/contentpicker.html @@ -1,34 +1,48 @@
-
- - -
+
- - +
+ + +
-
- - -
+ + -
+
+ + +
+ +
+ + + + +
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/memberpicker/memberpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/memberpicker/memberpicker.html index e4a8d36263..00475dddfe 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/memberpicker/memberpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/memberpicker/memberpicker.html @@ -1,34 +1,47 @@
-
- - -
+
- - +
+ + +
-
- - -
+ + + +
+ + +
+
+ + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js index e69b5ee3ce..1caff3ab31 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js @@ -1,6 +1,6 @@ //used for the media picker dialog angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", - function ($scope, entityResource, eventsService, $log, searchService, angularHelper, $timeout, localizationService, treeService) { + function ($scope, $q, entityResource, eventsService, $log, searchService, angularHelper, $timeout, localizationService, treeService, contentResource, mediaResource, memberResource) { var tree = null; var dialogOptions = $scope.model; @@ -22,27 +22,22 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", $scope.init = function(contentType) { if(contentType === "content") { - entityType = "Document"; + $scope.entityType = "Document"; if(!$scope.model.title) { $scope.model.title = localizationService.localize("defaultdialogs_selectContent"); } } else if(contentType === "member") { - entityType = "Member"; + $scope.entityType = "Member"; if(!$scope.model.title) { $scope.model.title = localizationService.localize("defaultdialogs_selectMember"); } } else if(contentType === "media") { - entityType = "Media"; + $scope.entityType = "Media"; if(!$scope.model.title) { $scope.model.title = localizationService.localize("defaultdialogs_selectMedia"); } } } - - // Search is only working for content, media and member section so we will remove it from everything else - if($scope.section === "content" || $scope.section === "media" || $scope.section === "member" ) { - $scope.enableSearh = true; - } //create the custom query string param for this tree $scope.customTreeParams = dialogOptions.startNodeId ? "startNodeId=" + dialogOptions.startNodeId : ""; @@ -54,7 +49,7 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", }); // Allow the entity type to be passed in but defaults to Document for backwards compatibility. - var entityType = dialogOptions.entityType ? dialogOptions.entityType : "Document"; + $scope.entityType = dialogOptions.entityType ? dialogOptions.entityType : "Document"; //min / max values @@ -66,10 +61,24 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", } if (dialogOptions.section === "member") { - entityType = "Member"; + $scope.entityType = "Member"; } else if (dialogOptions.section === "media") { - entityType = "Media"; + $scope.entityType = "Media"; + } + + // Search and listviews is only working for content, media and member section so we will remove it from everything else + if ($scope.section === "content" || $scope.section === "media" || $scope.section === "member") { + $scope.enableSearh = true; + + //if a alternative startnode is used, we need to check if it is a container + if (dialogOptions.startNodeId && dialogOptions.startNodeId !== -1) { + entityResource.getById(dialogOptions.startNodeId, $scope.entityType).then(function (node) { + if (node.metaData.IsContainer) { + openMiniListView(node); + } + }); + } } //Configures filtering @@ -101,54 +110,17 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", } function nodeExpandedHandler(ev, args) { + + // open mini list view for list views + if (args.node.metaData.isContainer) { + openMiniListView(args.node); + } + if (angular.isArray(args.children)) { //iterate children _.each(args.children, function (child) { - //check if any of the items are list views, if so we need to add some custom - // children: A node to activate the search, any nodes that have already been - // selected in the search - if (child.metaData.isContainer) { - child.hasChildren = true; - child.children = [ - { - level: child.level + 1, - hasChildren: false, - parent: function () { - return child; - }, - name: searchText, - metaData: { - listViewNode: child, - }, - cssClass: "icon-search", - cssClasses: ["not-published"] - } - ]; - //add base transition classes to this node - child.cssClasses.push("tree-node-slide-up"); - - var listViewResults = _.filter($scope.searchInfo.selectedSearchResults, function(i) { - return i.parentId == child.id; - }); - _.each(listViewResults, function(item) { - child.children.unshift({ - id: item.id, - name: item.name, - cssClass: "icon umb-tree-icon sprTree " + item.icon, - level: child.level + 1, - metaData: { - isSearchResult: true - }, - hasChildren: false, - parent: function () { - return child; - } - }); - }); - } - //now we need to look in the already selected search results and // toggle the check boxes for those ones that are listed var exists = _.find($scope.searchInfo.selectedSearchResults, function (selected) { @@ -174,18 +146,7 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", args.event.preventDefault(); args.event.stopPropagation(); - if (args.node.metaData.listViewNode) { - //check if list view 'search' node was selected - - $scope.searchInfo.showSearch = true; - $scope.searchInfo.searchFromId = args.node.metaData.listViewNode.id; - $scope.searchInfo.searchFromName = args.node.metaData.listViewNode.name; - - //add transition classes - var listViewNode = args.node.parent(); - listViewNode.cssClasses.push('tree-node-slide-up-hide-active'); - } - else if (args.node.metaData.isSearchResult) { + if (args.node.metaData.isSearchResult) { //check if the item selected was a search result from a list view //unselect @@ -232,7 +193,7 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", multiSelectItem(entity); } else { //otherwise we have to get it from the server - entityResource.getById(id, entityType).then(function (ent) { + entityResource.getById(id, $scope.entityType).then(function (ent) { multiSelectItem(ent); }); } @@ -257,7 +218,7 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", multiSelectItem(entity); } else { //otherwise we have to get it from the server - entityResource.getById(id, entityType).then(function (ent) { + entityResource.getById(id, $scope.entityType).then(function (ent) { multiSelectItem(ent); }); } @@ -274,7 +235,7 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", $scope.model.submit($scope.model); } else { //otherwise we have to get it from the server - entityResource.getById(id, entityType).then(function (ent) { + entityResource.getById(id, $scope.entityType).then(function (ent) { $scope.model.selection.push(ent); $scope.model.submit($scope.model); }); @@ -355,7 +316,7 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", } $scope.multiSubmit = function (result) { - entityResource.getByIds(result, entityType).then(function (ents) { + entityResource.getByIds(result, $scope.entityType).then(function (ents) { $scope.submit(ents); }); }; @@ -504,4 +465,19 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", $scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler); $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); }); + + $scope.selectListViewNode = function(node) { + select(node.name, node.id); + //toggle checked state + node.selected = node.selected === true ? false : true; + }; + + $scope.closeMiniListView = function() { + $scope.miniListView = undefined; + }; + + function openMiniListView(node) { + $scope.miniListView = node; + } + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.html index b009b4d45f..c9785ed9e1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.html @@ -1,35 +1,49 @@
-
- - -
+
- - +
+ + +
-
- - -
+ + -
+
+ + +
+ +
+ + + + + \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-breadcrumbs.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-breadcrumbs.html index 219e90fc4c..a9e42d01d6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-breadcrumbs.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-breadcrumbs.html @@ -1,10 +1,15 @@ + {{ancestor.name}} + + + \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html index 7517795136..b24e0985af 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html @@ -1,13 +1,16 @@
-
- +
+ - +
- + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-list-view.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-list-view.html new file mode 100644 index 0000000000..97884a6d9e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-list-view.html @@ -0,0 +1,101 @@ +
+ +
+ +
+ +

{{ miniListView.node.name }}

+
+ +
+ + + + Back / + + + + + +
+ +
+ + +
+
+ +
+ +
+
+
+ + +
+ + +
+ +
+ + +
+
+
+   + + +
+
+
{{ child.name }}
+
+ + +
+ + +
+ + +
+ +
+ +
+ +
+ +
+ + +
+ +
+ +
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-pagination.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-pagination.html index 9e4a1697a3..e8effcd761 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-pagination.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-pagination.html @@ -1,4 +1,4 @@ -