diff --git a/build/NuSpecs/UmbracoCms.Core.nuspec b/build/NuSpecs/UmbracoCms.Core.nuspec index 2536316906..0f31f77d04 100644 --- a/build/NuSpecs/UmbracoCms.Core.nuspec +++ b/build/NuSpecs/UmbracoCms.Core.nuspec @@ -32,9 +32,9 @@ - + - + @@ -104,4 +104,4 @@ - \ No newline at end of file + diff --git a/build/UmbracoVersion.txt b/build/UmbracoVersion.txt index 1672ad53d0..07f33971ff 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 -alpha048 +alpha055 \ No newline at end of file diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index b092457e25..40455f708c 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-alpha048")] \ No newline at end of file +[assembly: AssemblyInformationalVersion("7.6.0-alpha055")] \ No newline at end of file diff --git a/src/Umbraco.Core/Cache/CacheKeys.cs b/src/Umbraco.Core/Cache/CacheKeys.cs index 0c1a202b66..3b9ee9c63a 100644 --- a/src/Umbraco.Core/Cache/CacheKeys.cs +++ b/src/Umbraco.Core/Cache/CacheKeys.cs @@ -64,7 +64,8 @@ namespace Umbraco.Core.Cache [UmbracoWillObsolete("This cache key is only used for legacy business logic caching, remove in v8")] public const string ContentTypePropertiesCacheKey = "ContentType_PropertyTypes_Content:"; - [UmbracoWillObsolete("This cache key is only used for legacy business logic caching, remove in v8")] + [Obsolete("No longer used and will be removed in v8")] + [EditorBrowsable(EditorBrowsableState.Never)] public const string PropertyTypeCacheKey = "UmbracoPropertyTypeCache"; [Obsolete("This is no longer used and will be removed from the codebase in the future")] diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index cd759cba82..1637763e6e 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 "alpha048"; } } + public static string CurrentComment { get { return "alpha055"; } } // 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-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs index d7f4576137..2e3d652d7e 100644 --- a/src/Umbraco.Core/Constants-Conventions.cs +++ b/src/Umbraco.Core/Constants-Conventions.cs @@ -4,7 +4,8 @@ using Umbraco.Core.Models; namespace Umbraco.Core { - public static partial class Constants + + public static partial class Constants { /// /// Defines the identifiers for property-type alias conventions that are used within the Umbraco core. diff --git a/src/Umbraco.Core/Constants-DatabaseProviders.cs b/src/Umbraco.Core/Constants-DatabaseProviders.cs new file mode 100644 index 0000000000..2a64ade081 --- /dev/null +++ b/src/Umbraco.Core/Constants-DatabaseProviders.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Core +{ + public static partial class Constants + { + public static class DatabaseProviders + { + public const string SqlCe = "System.Data.SqlServerCe.4.0"; + public const string SqlServer = "System.Data.SqlClient"; + public const string MySql = "MySql.Data.MySqlClient"; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Constants-DeployEntityType.cs b/src/Umbraco.Core/Constants-DeployEntityType.cs new file mode 100644 index 0000000000..f622661dff --- /dev/null +++ b/src/Umbraco.Core/Constants-DeployEntityType.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Models; + +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 + { + // guid entity types + + public const string AnyGuid = "any-guid"; // that one is for tests + + public const string Document = "document"; + public const string Media = "media"; + public const string Member = "member"; + + public const string DictionaryItem = "dictionary-item"; + public const string Macro = "macro"; + public const string Template = "template"; + + public const string DocumentType = "document-type"; + public const string DocumentTypeContainer = "document-type-container"; + public const string MediaType = "media-type"; + public const string MediaTypeContainer = "media-type-container"; + public const string DataType = "data-type"; + public const string DataTypeContainer = "data-type-container"; + public const string MemberType = "member-type"; + public const string MemberGroup = "member-group"; + + public const string RelationType = "relation-type"; + + // string entity types + + public const string AnyString = "any-string"; // that one is for tests + + public const string MediaFile = "media-file"; + public const string TemplateFile = "template-file"; + public const string Script = "script"; + public const string Stylesheet = "stylesheet"; + public const string PartialView = "partial-view"; + public const string PartialViewMacro = "partial-view-macro"; + public const string Xslt = "xslt"; + + public static string FromUmbracoObjectType(UmbracoObjectTypes umbracoObjectType) + { + switch (umbracoObjectType) + { + case UmbracoObjectTypes.Document: + return Document; + case UmbracoObjectTypes.Media: + return Media; + case UmbracoObjectTypes.Member: + return Member; + case UmbracoObjectTypes.Template: + return Template; + case UmbracoObjectTypes.DocumentType: + return DocumentType; + case UmbracoObjectTypes.DocumentTypeContainer: + return DocumentTypeContainer; + case UmbracoObjectTypes.MediaType: + return MediaType; + case UmbracoObjectTypes.MediaTypeContainer: + return MediaTypeContainer; + case UmbracoObjectTypes.DataType: + return DataType; + case UmbracoObjectTypes.DataTypeContainer: + return DataTypeContainer; + case UmbracoObjectTypes.MemberType: + return MemberType; + case UmbracoObjectTypes.MemberGroup: + return MemberGroup; + case UmbracoObjectTypes.Stylesheet: + return Stylesheet; + case UmbracoObjectTypes.RelationType: + return RelationType; + } + throw new NotSupportedException(string.Format("UmbracoObjectType \"{0}\" does not have a matching EntityType.", umbracoObjectType)); + } + + public static UmbracoObjectTypes ToUmbracoObjectType(string entityType) + { + switch (entityType) + { + case Document: + return UmbracoObjectTypes.Document; + case Media: + return UmbracoObjectTypes.Media; + case Member: + return UmbracoObjectTypes.Member; + case Template: + return UmbracoObjectTypes.Template; + case DocumentType: + return UmbracoObjectTypes.DocumentType; + case DocumentTypeContainer: + return UmbracoObjectTypes.DocumentTypeContainer; + case MediaType: + return UmbracoObjectTypes.MediaType; + case MediaTypeContainer: + return UmbracoObjectTypes.MediaTypeContainer; + case DataType: + return UmbracoObjectTypes.DataType; + case DataTypeContainer: + return UmbracoObjectTypes.DataTypeContainer; + case MemberType: + return UmbracoObjectTypes.MemberType; + case MemberGroup: + return UmbracoObjectTypes.MemberGroup; + case Stylesheet: + return UmbracoObjectTypes.Stylesheet; + case RelationType: + return UmbracoObjectTypes.RelationType; + } + throw new NotSupportedException( + string.Format("EntityType \"{0}\" does not have a matching UmbracoObjectType.", entityType)); + } + } + + + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Constants-DeploySelector.cs b/src/Umbraco.Core/Constants-DeploySelector.cs new file mode 100644 index 0000000000..cd9c48a6f5 --- /dev/null +++ b/src/Umbraco.Core/Constants-DeploySelector.cs @@ -0,0 +1,17 @@ +namespace Umbraco.Core +{ + public static partial class Constants + { + /// + /// Contains the valid selector values. + /// + public static class DeploySelector + { + public const string This = "this"; + public const string ThisAndChildren = "this-and-children"; + public const string ThisAndDescendants = "this-and-descendants"; + public const string ChildrenOfThis = "children"; + public const string DescendantsOfThis = "descendants"; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Constants-Security.cs b/src/Umbraco.Core/Constants-Security.cs new file mode 100644 index 0000000000..bd2e1c5acf --- /dev/null +++ b/src/Umbraco.Core/Constants-Security.cs @@ -0,0 +1,33 @@ +using System; +using System.ComponentModel; + +namespace Umbraco.Core +{ + public static partial class Constants + { + public static class Security + { + + public const string BackOfficeAuthenticationType = "UmbracoBackOffice"; + public const string BackOfficeExternalAuthenticationType = "UmbracoExternalCookie"; + public const string BackOfficeExternalCookieName = "UMB_EXTLOGIN"; + public const string BackOfficeTokenAuthenticationType = "UmbracoBackOfficeToken"; + public const string BackOfficeTwoFactorAuthenticationType = "UmbracoTwoFactorCookie"; + + /// + /// The prefix used for external identity providers for their authentication type + /// + /// + /// By default we don't want to interfere with front-end external providers and their default setup, for back office the + /// providers need to be setup differently and each auth type for the back office will be prefixed with this value + /// + public const string BackOfficeExternalAuthenticationTypePrefix = "Umbraco."; + + public const string StartContentNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startcontentnode"; + public const string StartMediaNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startmedianode"; + public const string AllowedApplicationsClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/allowedapp"; + public const string SessionIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/sessionid"; + + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Constants-System.cs b/src/Umbraco.Core/Constants-System.cs index d1ac6fbb38..b38417c39b 100644 --- a/src/Umbraco.Core/Constants-System.cs +++ b/src/Umbraco.Core/Constants-System.cs @@ -29,12 +29,6 @@ public const string UmbracoConnectionName = "umbracoDbDSN"; public const string UmbracoMigrationName = "Umbraco"; } - - public static class DatabaseProviders - { - public const string SqlCe = "System.Data.SqlServerCe.4.0"; - public const string SqlServer = "System.Data.SqlClient"; - public const string MySql = "MySql.Data.MySqlClient"; - } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index f596820506..67b5698610 100644 --- a/src/Umbraco.Core/Constants-Web.cs +++ b/src/Umbraco.Core/Constants-Web.cs @@ -29,30 +29,6 @@ namespace Umbraco.Core public const string AuthCookieName = "UMB_UCONTEXT"; } - - public static class Security - { - - public const string BackOfficeAuthenticationType = "UmbracoBackOffice"; - public const string BackOfficeExternalAuthenticationType = "UmbracoExternalCookie"; - public const string BackOfficeExternalCookieName = "UMB_EXTLOGIN"; - public const string BackOfficeTokenAuthenticationType = "UmbracoBackOfficeToken"; - public const string BackOfficeTwoFactorAuthenticationType = "UmbracoTwoFactorCookie"; - - /// - /// The prefix used for external identity providers for their authentication type - /// - /// - /// By default we don't want to interfere with front-end external providers and their default setup, for back office the - /// providers need to be setup differently and each auth type for the back office will be prefixed with this value - /// - public const string BackOfficeExternalAuthenticationTypePrefix = "Umbraco."; - - public const string StartContentNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startcontentnode"; - public const string StartMediaNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startmedianode"; - public const string AllowedApplicationsClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/allowedapp"; - public const string SessionIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/sessionid"; - - } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/CoreBootManager.cs b/src/Umbraco.Core/CoreBootManager.cs index e98cd31dbf..2a6b044664 100644 --- a/src/Umbraco.Core/CoreBootManager.cs +++ b/src/Umbraco.Core/CoreBootManager.cs @@ -415,7 +415,7 @@ namespace Umbraco.Core if (currentTry == 5) { - throw new UmbracoStartupFailedException("Umbraco cannot start. A connection string is configured but the Umbraco cannot connect to the database."); + throw new UmbracoStartupFailedException("Umbraco cannot start. A connection string is configured but Umbraco cannot connect to the database."); } } diff --git a/src/Umbraco.Core/DatabaseContext.cs b/src/Umbraco.Core/DatabaseContext.cs index 483e124e35..837e59ca12 100644 --- a/src/Umbraco.Core/DatabaseContext.cs +++ b/src/Umbraco.Core/DatabaseContext.cs @@ -241,15 +241,10 @@ namespace Umbraco.Core var path = Path.Combine(GlobalSettings.FullpathToRoot, "App_Data", "Umbraco.sdf"); if (File.Exists(path) == false) { - var engine = new SqlCeEngine(connectionString); - engine.CreateDatabase(); - - // SD: Pretty sure this should be in a using clause but i don't want to cause unknown side-effects here - // since it's been like this for quite some time - //using (var engine = new SqlCeEngine(connectionString)) - //{ - // engine.CreateDatabase(); - //} + using (var engine = new SqlCeEngine(connectionString)) + { + engine.CreateDatabase(); + } } Initialize(providerName); diff --git a/src/Umbraco.Core/Deploy/ArtifactBase.cs b/src/Umbraco.Core/Deploy/ArtifactBase.cs new file mode 100644 index 0000000000..b507c54415 --- /dev/null +++ b/src/Umbraco.Core/Deploy/ArtifactBase.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; + +namespace Umbraco.Core.Deploy +{ + /// + /// Provides a base class to all artifacts. + /// + public abstract class ArtifactBase : IArtifact + where TUdi : Udi + { + protected ArtifactBase(TUdi udi, IEnumerable dependencies = null) + { + if (udi == null) + throw new ArgumentNullException("udi"); + Udi = udi; + + Dependencies = dependencies ?? Enumerable.Empty(); + _checksum = new Lazy(GetChecksum); + } + + private readonly Lazy _checksum; + private IEnumerable _dependencies; + + protected abstract string GetChecksum(); + + #region Abstract implementation of IArtifactSignature + + Udi IArtifactSignature.Udi + { + get { return Udi; } + } + + public TUdi Udi { get; set; } + + [JsonIgnore] + public string Checksum + { + get { return _checksum.Value; } + } + + public IEnumerable Dependencies + { + get { return _dependencies; } + set { _dependencies = value.OrderBy(x => x.Udi); } + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/ArtifactDependency.cs b/src/Umbraco.Core/Deploy/ArtifactDependency.cs new file mode 100644 index 0000000000..41e349d636 --- /dev/null +++ b/src/Umbraco.Core/Deploy/ArtifactDependency.cs @@ -0,0 +1,40 @@ +namespace Umbraco.Core.Deploy +{ + /// + /// Represents an artifact dependency. + /// + /// + /// Dependencies have an order property which indicates whether it must be respected when ordering artifacts. + /// Dependencies have a mode which can be Match or Exist depending on whether the checksum should match. + /// + public class ArtifactDependency + { + /// + /// Initializes a new instance of the ArtifactDependency class with an entity identifier and a mode. + /// + /// The entity identifier of the artifact that is a dependency. + /// A value indicating whether the dependency is ordering. + /// The dependency mode. + public ArtifactDependency(Udi udi, bool ordering, ArtifactDependencyMode mode) + { + Udi = udi; + Ordering = ordering; + Mode = mode; + } + + /// + /// Gets the entity id of the artifact that is a dependency. + /// + public Udi Udi { get; private set; } + + /// + /// Gets a value indicating whether the dependency is ordering. + /// + public bool Ordering { get; private set; } + + /// + /// Gets the dependency mode. + /// + public ArtifactDependencyMode Mode { get; private set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/ArtifactDependencyCollection.cs b/src/Umbraco.Core/Deploy/ArtifactDependencyCollection.cs new file mode 100644 index 0000000000..fd036f4628 --- /dev/null +++ b/src/Umbraco.Core/Deploy/ArtifactDependencyCollection.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Umbraco.Core.Deploy +{ + /// + /// Represents a collection of distinct . + /// + /// The collection cannot contain duplicates and modes are properly managed. + public class ArtifactDependencyCollection : ICollection + { + private readonly Dictionary _dependencies + = new Dictionary(); + + public IEnumerator GetEnumerator() + { + return _dependencies.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public void Add(ArtifactDependency item) + { + if (_dependencies.ContainsKey(item.Udi)) + { + var exist = _dependencies[item.Udi]; + if (item.Mode == ArtifactDependencyMode.Exist || item.Mode == exist.Mode) + return; + } + + _dependencies[item.Udi] = item; + } + + public void Clear() + { + _dependencies.Clear(); + } + + public bool Contains(ArtifactDependency item) + { + return _dependencies.ContainsKey(item.Udi) && + (_dependencies[item.Udi].Mode == item.Mode || _dependencies[item.Udi].Mode == ArtifactDependencyMode.Match); + } + + public void CopyTo(ArtifactDependency[] array, int arrayIndex) + { + _dependencies.Values.CopyTo(array, arrayIndex); + } + + public bool Remove(ArtifactDependency item) + { + throw new NotSupportedException(); + } + + public int Count + { + get { return _dependencies.Count; } + } + + public bool IsReadOnly + { + get { return false; } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/ArtifactDependencyMode.cs b/src/Umbraco.Core/Deploy/ArtifactDependencyMode.cs new file mode 100644 index 0000000000..7ee5c2f220 --- /dev/null +++ b/src/Umbraco.Core/Deploy/ArtifactDependencyMode.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Core.Deploy +{ + /// + /// Indicates the mode of the dependency. + /// + public enum ArtifactDependencyMode + { + /// + /// The dependency must match exactly. + /// + Match, + + /// + /// The dependency must exist. + /// + Exist + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/ArtifactDeployState.cs b/src/Umbraco.Core/Deploy/ArtifactDeployState.cs new file mode 100644 index 0000000000..3723e483cb --- /dev/null +++ b/src/Umbraco.Core/Deploy/ArtifactDeployState.cs @@ -0,0 +1,50 @@ +namespace Umbraco.Core.Deploy +{ + /// + /// Represent the state of an artifact being deployed. + /// + public abstract class ArtifactDeployState + { + /// + /// Creates a new instance of the class from an artifact and an entity. + /// + /// The type of the artifact. + /// The type of the entity. + /// The artifact. + /// The entity. + /// The service connector deploying the artifact. + /// The next pass number. + /// A deploying artifact. + public static ArtifactDeployState Create(TArtifact art, TEntity entity, IServiceConnector connector, int nextPass) + where TArtifact : IArtifact + { + return new ArtifactDeployState(art, entity, connector, nextPass); + } + + /// + /// Gets the artifact. + /// + public IArtifact Artifact + { + get { return GetArtifactAsIArtifact(); } + } + + /// + /// Gets the artifact as an . + /// + /// The artifact, as an . + /// This is because classes that inherit from this class cannot override the Artifact property + /// with a property that specializes the return type, and so they need to 'new' the property. + protected abstract IArtifact GetArtifactAsIArtifact(); + + /// + /// Gets or sets the service connector in charge of deploying the artifact. + /// + public IServiceConnector Connector { get; set; } + + /// + /// Gets or sets the next pass number. + /// + public int NextPass { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/ArtifactDeployStateOfTArtifactTEntity.cs b/src/Umbraco.Core/Deploy/ArtifactDeployStateOfTArtifactTEntity.cs new file mode 100644 index 0000000000..b4d2be23cd --- /dev/null +++ b/src/Umbraco.Core/Deploy/ArtifactDeployStateOfTArtifactTEntity.cs @@ -0,0 +1,48 @@ +namespace Umbraco.Core.Deploy +{ + /// + /// Represent the state of an artifact being deployed. + /// + /// The type of the artifact. + /// The type of the entity. + public class ArtifactDeployState : ArtifactDeployState + where TArtifact : IArtifact + { + /// + /// Initializes a new instance of the class. + /// + public ArtifactDeployState() + { } + + /// + /// Initializes a new instance of the class. + /// + /// The artifact. + /// The entity. + /// The service connector deploying the artifact. + /// The next pass number. + public ArtifactDeployState(TArtifact art, TEntity entity, IServiceConnector connector, int nextPass) + { + Artifact = art; + Entity = entity; + Connector = connector; + NextPass = nextPass; + } + + /// + /// Gets or sets the artifact. + /// + public new TArtifact Artifact { get; set; } + + /// + /// Gets or sets the entity. + /// + public TEntity Entity { get; set; } + + /// + protected sealed override IArtifact GetArtifactAsIArtifact() + { + return Artifact; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/ArtifactSignature.cs b/src/Umbraco.Core/Deploy/ArtifactSignature.cs new file mode 100644 index 0000000000..4bea6a5ce8 --- /dev/null +++ b/src/Umbraco.Core/Deploy/ArtifactSignature.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Umbraco.Core.Deploy +{ + public sealed class ArtifactSignature : IArtifactSignature + { + public ArtifactSignature(Udi udi, string checksum, IEnumerable dependencies = null) + { + Udi = udi; + Checksum = checksum; + Dependencies = dependencies ?? Enumerable.Empty(); + } + + public Udi Udi { get; private set; } + + public string Checksum { get; private set; } + + public IEnumerable Dependencies { get; private set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/Difference.cs b/src/Umbraco.Core/Deploy/Difference.cs new file mode 100644 index 0000000000..90329afdd6 --- /dev/null +++ b/src/Umbraco.Core/Deploy/Difference.cs @@ -0,0 +1,28 @@ +namespace Umbraco.Core.Deploy +{ + public class Difference + { + public Difference(string title, string text = null, string category = null) + { + Title = title; + Text = text; + Category = category; + } + + public string Title { get; set; } + public string Text { get; set; } + public string Category { get; set; } + + public override string ToString() + { + var s = Title; + if (!string.IsNullOrWhiteSpace(Category)) s += string.Format("[{0}]", Category); + if (!string.IsNullOrWhiteSpace(Text)) + { + if (s.Length > 0) s += ":"; + s += Text; + } + return s; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/Direction.cs b/src/Umbraco.Core/Deploy/Direction.cs new file mode 100644 index 0000000000..b0e1c1dc0a --- /dev/null +++ b/src/Umbraco.Core/Deploy/Direction.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Core.Deploy +{ + public enum Direction + { + ToArtifact, + FromArtifact + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/IArtifact.cs b/src/Umbraco.Core/Deploy/IArtifact.cs new file mode 100644 index 0000000000..f39b7b433f --- /dev/null +++ b/src/Umbraco.Core/Deploy/IArtifact.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Core.Deploy +{ + /// + /// Represents an artifact ie an object that can be transfered between environments. + /// + public interface IArtifact : IArtifactSignature + { } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/IArtifactSignature.cs b/src/Umbraco.Core/Deploy/IArtifactSignature.cs new file mode 100644 index 0000000000..83b112586b --- /dev/null +++ b/src/Umbraco.Core/Deploy/IArtifactSignature.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Deploy +{ + /// + /// Represents the signature of an artifact. + /// + public interface IArtifactSignature + { + /// + /// Gets the entity unique identifier of this artifact. + /// + /// + /// The project identifier is independent from the state of the artifact, its data + /// values, dependencies, anything. It never changes and fully identifies the artifact. + /// What an entity uses as a unique identifier will influence what we can transfer + /// between environments. Eg content type "Foo" on one environment is not necessarily the + /// same as "Foo" on another environment, if guids are used as unique identifiers. What is + /// used should be documented for each entity, along with the consequences of the choice. + /// + Udi Udi { get; } + + /// + /// Gets the checksum of this artifact. + /// + /// + /// The checksum depends on the artifact's properties, and on the identifiers of all its dependencies, + /// but not on their checksums. So the checksum changes when any of the artifact's properties changes, + /// or when the list of dependencies changes. But not if one of these dependencies change. + /// It is assumed that checksum collisions cannot happen ie that no two different artifact's + /// states will ever produce the same checksum, so that if two artifacts have the same checksum then + /// they are identical. + /// + string Checksum { get; } + + /// + /// Gets the dependencies of this artifact. + /// + IEnumerable Dependencies { get; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/IDeployContext.cs b/src/Umbraco.Core/Deploy/IDeployContext.cs new file mode 100644 index 0000000000..7d4066e015 --- /dev/null +++ b/src/Umbraco.Core/Deploy/IDeployContext.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; + +namespace Umbraco.Core.Deploy +{ + /// + /// Represents a deployment context. + /// + public interface IDeployContext + { + /// + /// Gets the unique identifier of the deployment. + /// + Guid SessionId { get; } + + /// + /// Gets the file source. + /// + /// The file source is used to obtain files from the source environment. + IFileSource FileSource { get; } + + /// + /// Gets the next number in a numerical sequence. + /// + /// The next sequence number. + /// Can be used to uniquely number things during a deployment. + int NextSeq(); + + /// + /// Gets items. + /// + IDictionary Items { get; } + + /// + /// Gets item. + /// + /// The type of the item. + /// The key of the item. + /// The item with the specified key and type, if any, else null. + T Item(string key) where T : class; + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/IFileSource.cs b/src/Umbraco.Core/Deploy/IFileSource.cs new file mode 100644 index 0000000000..ea73d2b571 --- /dev/null +++ b/src/Umbraco.Core/Deploy/IFileSource.cs @@ -0,0 +1,81 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Umbraco.Core.Deploy +{ + /// + /// Represents a file source, ie a mean for a target environment involved in a + /// deployment to obtain the content of files being deployed. + /// + public interface IFileSource + { + /// + /// Gets the content of a file as a stream. + /// + /// A file entity identifier. + /// A stream with read access to the file content. + /// + /// Returns null if no content could be read. + /// The caller should ensure that the stream is properly closed/disposed. + /// + Stream GetFileStream(StringUdi udi); + + /// + /// Gets the content of a file as a stream. + /// + /// A file entity identifier. + /// A cancellation token. + /// A stream with read access to the file content. + /// + /// Returns null if no content could be read. + /// The caller should ensure that the stream is properly closed/disposed. + /// + Task GetFileStreamAsync(StringUdi udi, CancellationToken token); + + /// + /// Gets the content of a file as a string. + /// + /// A file entity identifier. + /// A string containing the file content. + /// Returns null if no content could be read. + string GetFileContent(StringUdi udi); + + /// + /// Gets the content of a file as a string. + /// + /// A file entity identifier. + /// A cancellation token. + /// A string containing the file content. + /// Returns null if no content could be read. + Task GetFileContentAsync(StringUdi udi, CancellationToken token); + + /// + /// Gets the length of a file. + /// + /// A file entity identifier. + /// The length of the file, or -1 if the file does not exist. + long GetFileLength(StringUdi udi); + + /// + /// Gets the length of a file. + /// + /// A file entity identifier. + /// A cancellation token. + /// The length of the file, or -1 if the file does not exist. + Task GetFileLengthAsync(StringUdi udi, CancellationToken token); + + // fixme - doc + void GetFiles(IEnumerable udis, IFileStore fileStore); + + Task GetFilesAsync(IEnumerable udis, IFileStore fileStore, CancellationToken token); + + ///// + ///// Gets the content of a file as a bytes array. + ///// + ///// A file entity identifier. + ///// A byte array containing the file content. + //byte[] GetFileBytes(StringUdi Udi); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/IFileStore.cs b/src/Umbraco.Core/Deploy/IFileStore.cs new file mode 100644 index 0000000000..0cd7425eb9 --- /dev/null +++ b/src/Umbraco.Core/Deploy/IFileStore.cs @@ -0,0 +1,9 @@ +using System.IO; + +namespace Umbraco.Core.Deploy +{ + public interface IFileStore + { + void SaveStream(StringUdi udi, Stream stream); + } +} diff --git a/src/Umbraco.Core/Deploy/IGridCellValueConnector.cs b/src/Umbraco.Core/Deploy/IGridCellValueConnector.cs new file mode 100644 index 0000000000..65fde1ec0b --- /dev/null +++ b/src/Umbraco.Core/Deploy/IGridCellValueConnector.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Deploy +{ + /// + /// Defines methods that can convert a grid cell value to / from an environment-agnostic string. + /// + /// Grid cell values may contain values such as content identifiers, that would be local + /// to one environment, and need to be converted in order to be deployed. + public interface IGridCellValueConnector + { + /// + /// Gets a value indicating whether the connector supports a specified grid editor alias. + /// + /// The grid editor alias. + /// A value indicating whether the connector supports the grid editor alias. + /// Note that can be string.Empty to indicate the "default" connector. + bool IsConnector(string alias); + + /// + /// Gets the value to be deployed from the control value as a string. + /// + /// The control containing the value. + /// The property where the control is located. Do not modify - only used for context + /// The dependencies of the property. + /// The grid cell value to be deployed. + /// Note that + string GetValue(GridValue.GridControl gridControl, Property property, ICollection dependencies); + + /// + /// Allows you to modify the value of a control being deployed. + /// + /// The control being deployed. + /// The property where the is located. Do not modify - only used for context. + /// Follows the pattern of the property value connectors (). The SetValue method is used to modify the value of the . + /// Note that only the value should be modified - not the . + /// The should only be used to assist with context data relevant when setting the value. + void SetValue(GridValue.GridControl gridControl, Property property); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/IImageSourceParser.cs b/src/Umbraco.Core/Deploy/IImageSourceParser.cs new file mode 100644 index 0000000000..d8e8d860ac --- /dev/null +++ b/src/Umbraco.Core/Deploy/IImageSourceParser.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Deploy +{ + /// + /// Provides methods to parse image tag sources in property values. + /// + public interface IImageSourceParser + { + /// + /// Parses an Umbraco property value and produces an artifact property value. + /// + /// The property value. + /// A list of dependencies. + /// The parsed value. + /// Turns src="/media/..." into src="umb://media/..." and adds the corresponding udi to the dependencies. + string ToArtifact(string value, ICollection dependencies); + + /// + /// Parses an artifact property value and produces an Umbraco property value. + /// + /// The artifact property value. + /// The parsed value. + /// Turns umb://media/... into /media/.... + string FromArtifact(string value); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/ILocalLinkParser.cs b/src/Umbraco.Core/Deploy/ILocalLinkParser.cs new file mode 100644 index 0000000000..c5906c2060 --- /dev/null +++ b/src/Umbraco.Core/Deploy/ILocalLinkParser.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Deploy +{ + /// + /// Provides methods to parse local link tags in property values. + /// + public interface ILocalLinkParser + { + /// + /// Parses an Umbraco property value and produces an artifact property value. + /// + /// The property value. + /// A list of dependencies. + /// The parsed value. + /// Turns {{localLink:1234}} into {{localLink:umb://{type}/{id}}} and adds the corresponding udi to the dependencies. + string ToArtifact(string value, ICollection dependencies); + + /// + /// Parses an artifact property value and produces an Umbraco property value. + /// + /// The artifact property value. + /// The parsed value. + /// Turns {{localLink:umb://{type}/{id}}} into {{localLink:1234}}. + string FromArtifact(string value); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/IMacroParser.cs b/src/Umbraco.Core/Deploy/IMacroParser.cs new file mode 100644 index 0000000000..9cde8ef8b6 --- /dev/null +++ b/src/Umbraco.Core/Deploy/IMacroParser.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Deploy +{ + public interface IMacroParser + { + /// + /// Parses an Umbraco property value and produces an artifact property value. + /// + /// Property value. + /// A list of dependencies. + /// Parsed value. + string ToArtifact(string value, ICollection dependencies); + + /// + /// Parses an artifact property value and produces an Umbraco property value. + /// + /// Artifact property value. + /// Parsed value. + string FromArtifact(string value); + + /// + /// Tries to replace the value of the attribute/parameter with a value containing a converted identifier. + /// + /// Value to attempt to convert + /// Alias of the editor used for the parameter + /// Collection to add dependencies to when performing ToArtifact + /// Indicates which action is being performed (to or from artifact) + /// Value with converted identifiers + string ReplaceAttributeValue(string value, string editorAlias, ICollection dependencies, Direction direction); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/IPreValueConnector.cs b/src/Umbraco.Core/Deploy/IPreValueConnector.cs new file mode 100644 index 0000000000..4ef898cc74 --- /dev/null +++ b/src/Umbraco.Core/Deploy/IPreValueConnector.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Deploy +{ + /// + /// Defines methods that can convert a preValue to / from an environment-agnostic string. + /// + /// PreValues may contain values such as content identifiers, that would be local + /// to one environment, and need to be converted in order to be deployed. + public interface IPreValueConnector + { + /// + /// Gets the property editor aliases that the value converter supports by default. + /// + IEnumerable PropertyEditorAliases { get; } + + /// + /// Gets the environment-agnostic preValues corresponding to environment-specific preValues. + /// + /// The environment-specific preValues. + /// The dependencies. + /// + IDictionary ConvertToDeploy(IDictionary preValues, ICollection dependencies); + + /// + /// Gets the environment-specific preValues corresponding to environment-agnostic preValues. + /// + /// The environment-agnostic preValues. + /// + IDictionary ConvertToLocalEnvironment(IDictionary preValues); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/IPrettyArtifact.cs b/src/Umbraco.Core/Deploy/IPrettyArtifact.cs new file mode 100644 index 0000000000..38c0ddee4c --- /dev/null +++ b/src/Umbraco.Core/Deploy/IPrettyArtifact.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Core.Deploy +{ + public interface IPrettyArtifact + { + string Name { get; } + string Alias { get; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Deploy/IServiceConnector.cs b/src/Umbraco.Core/Deploy/IServiceConnector.cs new file mode 100644 index 0000000000..d1c0bf61de --- /dev/null +++ b/src/Umbraco.Core/Deploy/IServiceConnector.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Umbraco.Core; + +namespace Umbraco.Core.Deploy +{ + /// + /// Connects to an Umbraco service. + /// + public interface IServiceConnector + { + /// + /// Gets an artifact. + /// + /// The entity identifier of the artifact. + /// The corresponding artifact, or null. + IArtifact GetArtifact(Udi udi); + + /// + /// Gets an artifact. + /// + /// The entity. + /// The corresponding artifact. + IArtifact GetArtifact(object entity); + + /// + /// Initializes processing for an artifact. + /// + /// The artifact. + /// The deploy context. + /// The mapped artifact. + ArtifactDeployState ProcessInit(IArtifact art, IDeployContext context); + + /// + /// Processes an artifact. + /// + /// The mapped artifact. + /// The deploy context. + /// The processing pass number. + void Process(ArtifactDeployState dart, IDeployContext context, int pass); + + /// + /// Explodes a range into udis. + /// + /// The range. + /// The list of udis where to add the new udis. + /// Also, it's cool to have a method named Explode. Kaboom! + void Explode(UdiRange range, List udis); + + /// + /// Gets a named range for a specified udi and selector. + /// + /// The udi. + /// The selector. + /// The named range for the specified udi and selector. + NamedUdiRange GetRange(Udi udi, string selector); + + /// + /// Gets a named range for specified entity type, identifier and selector. + /// + /// The entity type. + /// The identifier. + /// The selector. + /// The named range for the specified entity type, identifier and selector. + /// + /// This is temporary. At least we thought it would be, in sept. 2016. What day is it now? + /// At the moment our UI has a hard time returning proper udis, mainly because Core's tree do + /// not manage guids but only ints... so we have to provide a way to support it. The string id here + /// can be either a real string (for string udis) or an "integer as a string", using the value "-1" to + /// indicate the "root" i.e. an open udi. + /// + NamedUdiRange GetRange(string entityType, string sid, string selector); + + /// + /// Compares two artifacts. + /// + /// The first artifact. + /// The second artifact. + /// A collection of differences to append to, if not null. + /// A boolean value indicating whether the artifacts are identical. + /// ServiceConnectorBase{TArtifact} provides a very basic default implementation. + bool Compare(IArtifact art1, IArtifact art2, ICollection differences = null); + } + +} diff --git a/src/Umbraco.Core/Deploy/IValueConnector.cs b/src/Umbraco.Core/Deploy/IValueConnector.cs new file mode 100644 index 0000000000..a93e5a05a4 --- /dev/null +++ b/src/Umbraco.Core/Deploy/IValueConnector.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Deploy +{ + /// + /// Defines methods that can convert a property value to / from an environment-agnostic string. + /// + /// Property values may contain values such as content identifiers, that would be local + /// to one environment, and need to be converted in order to be deployed. Connectors also deal + /// with serializing to / from string. + public interface IValueConnector + { + /// + /// Gets the property editor aliases that the value converter supports by default. + /// + IEnumerable PropertyEditorAliases { get; } + + /// + /// Gets the deploy property corresponding to a content property. + /// + /// The content property. + /// The content dependencies. + /// The deploy property value. + string GetValue(Property property, ICollection dependencies); + + /// + /// Sets a content property value using a deploy property. + /// + /// The content item. + /// The property alias. + /// The deploy property value. + void SetValue(IContentBase content, string alias, string value); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/GuidUdi.cs b/src/Umbraco.Core/GuidUdi.cs new file mode 100644 index 0000000000..fabdf46c93 --- /dev/null +++ b/src/Umbraco.Core/GuidUdi.cs @@ -0,0 +1,83 @@ +using System; + +namespace Umbraco.Core +{ + /// + /// Represents a guid-based entity identifier. + /// + public class GuidUdi : Udi + { + /// + /// The guid part of the identifier. + /// + public Guid Guid { get; private set; } + + /// + /// Initializes a new instance of the GuidUdi class with an entity type and a guid. + /// + /// The entity type part of the udi. + /// The guid part of the udi. + public GuidUdi(string entityType, Guid guid) + : base(entityType, "umb://" + entityType + "/" + guid.ToString("N")) + { + Guid = guid; + } + + /// + /// Initializes a new instance of the GuidUdi class with an uri value. + /// + /// The uri value of the udi. + public GuidUdi(Uri uriValue) + : base(uriValue) + { + Guid = Guid.Parse(uriValue.AbsolutePath.TrimStart('/')); + } + + /// + /// Converts the string representation of an entity identifier into the equivalent GuidUdi instance. + /// + /// The string to convert. + /// A GuidUdi instance that contains the value that was parsed. + public new static GuidUdi Parse(string s) + { + var udi = Udi.Parse(s); + if (!(udi is GuidUdi)) + throw new FormatException("String \"" + s + "\" is not a guid entity id."); + return (GuidUdi)udi; + } + + public static bool TryParse(string s, out GuidUdi udi) + { + Udi tmp; + udi = null; + if (!TryParse(s, out tmp)) return false; + udi = tmp as GuidUdi; + return udi != null; + } + + public override bool Equals(object obj) + { + var other = obj as GuidUdi; + if (other == null) return false; + return EntityType == other.EntityType && Guid == other.Guid; + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + + /// + public override bool IsRoot + { + get { return Guid == Guid.Empty; } + } + + /// + public GuidUdi EnsureClosed() + { + base.EnsureNotRoot(); + return this; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/GridValue.cs b/src/Umbraco.Core/Models/GridValue.cs new file mode 100644 index 0000000000..87f6146af6 --- /dev/null +++ b/src/Umbraco.Core/Models/GridValue.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Umbraco.Core.Models +{ + /// + /// A model representing the value saved for the grid + /// + public class GridValue + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("sections")] + public IEnumerable Sections { get; set; } + + public class GridSection + { + [JsonProperty("grid")] + public string Grid { get; set; } + + [JsonProperty("rows")] + public IEnumerable Rows { get; set; } + } + + public class GridRow + { + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("id")] + public Guid Id { get; set; } + + [JsonProperty("areas")] + public IEnumerable Areas { get; set; } + + [JsonProperty("styles")] + public JToken Styles { get; set; } + + [JsonProperty("config")] + public JToken Config { get; set; } + } + + public class GridArea + { + [JsonProperty("grid")] + public string Grid { get; set; } + + [JsonProperty("controls")] + public IEnumerable Controls { get; set; } + + [JsonProperty("styles")] + public JToken Styles { get; set; } + + [JsonProperty("config")] + public JToken Config { get; set; } + } + + public class GridControl + { + [JsonProperty("value")] + public JToken Value { get; set; } + + [JsonProperty("editor")] + public GridEditor Editor { get; set; } + + [JsonProperty("styles")] + public JToken Styles { get; set; } + + [JsonProperty("config")] + public JToken Config { get; set; } + } + + public class GridEditor + { + [JsonProperty("alias")] + public string Alias { get; set; } + + [JsonProperty("view")] + public string View { get; set; } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/IMediaType.cs b/src/Umbraco.Core/Models/IMediaType.cs index 29e4b665ba..e8f7c16190 100644 --- a/src/Umbraco.Core/Models/IMediaType.cs +++ b/src/Umbraco.Core/Models/IMediaType.cs @@ -4,7 +4,7 @@ namespace Umbraco.Core.Models { /// /// Defines a ContentType, which Media is based on - /// public interface IMediaType : IContentTypeComposition { diff --git a/src/Umbraco.Core/NamedUdiRange.cs b/src/Umbraco.Core/NamedUdiRange.cs new file mode 100644 index 0000000000..c87e5db82e --- /dev/null +++ b/src/Umbraco.Core/NamedUdiRange.cs @@ -0,0 +1,34 @@ +namespace Umbraco.Core +{ + /// + /// Represents a complemented with a name. + /// + public class NamedUdiRange : UdiRange + { + /// + /// Initializes a new instance of the class with a and an optional selector. + /// + /// A . + /// An optional selector. + public NamedUdiRange(Udi udi, string selector = Constants.DeploySelector.This) + : base(udi, selector) + { } + + /// + /// Initializes a new instance of the class with a , a name, and an optional selector. + /// + /// A . + /// A name. + /// An optional selector. + public NamedUdiRange(Udi udi, string name, string selector = Constants.DeploySelector.This) + : base(udi, selector) + { + Name = name; + } + + /// + /// Gets or sets the name of the range. + /// + public string Name { get; set; } + } +} \ 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 5dcec8fed0..88de5a1d6c 100644 --- a/src/Umbraco.Core/Persistence/Factories/ContentFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ContentFactory.cs @@ -27,15 +27,15 @@ namespace Umbraco.Core.Persistence.Factories #region Implementation of IEntityFactory - public IContent BuildEntity(DocumentDto dto) + public static IContent BuildEntity(DocumentDto dto, IContentType contentType) { - var content = new Content(dto.Text, dto.ContentVersionDto.ContentDto.NodeDto.ParentId, _contentType); + var content = new Content(dto.Text, dto.ContentVersionDto.ContentDto.NodeDto.ParentId, contentType); try { content.DisableChangeTracking(); - content.Id = _id; + content.Id = dto.NodeId; content.Key = dto.ContentVersionDto.ContentDto.NodeDto.UniqueId; content.Name = dto.Text; content.NodeName = dto.ContentVersionDto.ContentDto.NodeDto.Text; @@ -49,8 +49,8 @@ namespace Umbraco.Core.Persistence.Factories content.Published = dto.Published; content.CreateDate = dto.ContentVersionDto.ContentDto.NodeDto.CreateDate; content.UpdateDate = dto.ContentVersionDto.VersionDate; - content.ExpireDate = dto.ExpiresDate.HasValue ? dto.ExpiresDate.Value : (DateTime?) null; - content.ReleaseDate = dto.ReleaseDate.HasValue ? dto.ReleaseDate.Value : (DateTime?) null; + 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; @@ -64,6 +64,13 @@ namespace Umbraco.Core.Persistence.Factories { content.EnableChangeTracking(); } + + } + + [Obsolete("Use the static BuildEntity instead so we don't have to allocate one of these objects everytime we want to map values")] + public IContent BuildEntity(DocumentDto dto) + { + return BuildEntity(dto, _contentType); } public DocumentDto BuildDto(IContent entity) diff --git a/src/Umbraco.Core/Persistence/Factories/MediaFactory.cs b/src/Umbraco.Core/Persistence/Factories/MediaFactory.cs index 0fcb654cb7..5729bb125e 100644 --- a/src/Umbraco.Core/Persistence/Factories/MediaFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/MediaFactory.cs @@ -27,15 +27,15 @@ namespace Umbraco.Core.Persistence.Factories #region Implementation of IEntityFactory - public IMedia BuildEntity(ContentVersionDto dto) + public static IMedia BuildEntity(ContentVersionDto dto, IMediaType contentType) { - var media = new Models.Media(dto.ContentDto.NodeDto.Text, dto.ContentDto.NodeDto.ParentId, _contentType); + var media = new Models.Media(dto.ContentDto.NodeDto.Text, dto.ContentDto.NodeDto.ParentId, contentType); try { media.DisableChangeTracking(); - media.Id = _id; + media.Id = dto.NodeId; media.Key = dto.ContentDto.NodeDto.UniqueId; media.Path = dto.ContentDto.NodeDto.Path; media.CreatorId = dto.ContentDto.NodeDto.UserId.Value; @@ -55,6 +55,13 @@ namespace Umbraco.Core.Persistence.Factories { media.EnableChangeTracking(); } + + } + + [Obsolete("Use the static BuildEntity instead so we don't have to allocate one of these objects everytime we want to map values")] + public IMedia BuildEntity(ContentVersionDto dto) + { + return BuildEntity(dto, _contentType); } public ContentVersionDto BuildDto(IMedia entity) diff --git a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs index 446bd426ad..f202d8c321 100644 --- a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs @@ -30,11 +30,11 @@ namespace Umbraco.Core.Persistence.Factories _updateDate = updateDate; } - public IEnumerable BuildEntity(PropertyDataDto[] dtos) + public static IEnumerable BuildEntity(IReadOnlyCollection dtos, PropertyType[] compositionTypeProperties, DateTime createDate, DateTime updateDate) { var properties = new List(); - foreach (var propertyType in _compositionTypeProperties) + foreach (var propertyType in compositionTypeProperties) { var propertyDataDto = dtos.LastOrDefault(x => x.PropertyTypeId == propertyType.Id); var property = propertyDataDto == null @@ -47,8 +47,8 @@ namespace Umbraco.Core.Persistence.Factories //on initial construction we don't want to have dirty properties tracked property.DisableChangeTracking(); - property.CreateDate = _createDate; - property.UpdateDate = _updateDate; + property.CreateDate = createDate; + property.UpdateDate = updateDate; // http://issues.umbraco.org/issue/U4-1946 property.ResetDirtyProperties(false); properties.Add(property); @@ -57,12 +57,18 @@ namespace Umbraco.Core.Persistence.Factories { property.EnableChangeTracking(); } - + } return properties; } + [Obsolete("Use the static method instead, there's no reason to allocate one of these classes everytime we want to map values")] + public IEnumerable BuildEntity(PropertyDataDto[] dtos) + { + return BuildEntity(dtos, _compositionTypeProperties, _createDate, _updateDate); + } + public IEnumerable BuildDto(IEnumerable properties) { var propertyDataDtos = new List(); diff --git a/src/Umbraco.Core/Persistence/Querying/ModelToSqlExpressionVisitor.cs b/src/Umbraco.Core/Persistence/Querying/ModelToSqlExpressionVisitor.cs index b265a5b587..7f5e479af6 100644 --- a/src/Umbraco.Core/Persistence/Querying/ModelToSqlExpressionVisitor.cs +++ b/src/Umbraco.Core/Persistence/Querying/ModelToSqlExpressionVisitor.cs @@ -20,6 +20,7 @@ namespace Umbraco.Core.Persistence.Querying _mapper = mapper; } + [Obsolete("Use the overload the specifies a SqlSyntaxProvider")] public ModelToSqlExpressionVisitor() : this(SqlSyntaxContext.SqlSyntaxProvider, MappingResolver.Current.ResolveMapperByType(typeof(T))) { } diff --git a/src/Umbraco.Core/Persistence/Querying/PocoToSqlExpressionVisitor.cs b/src/Umbraco.Core/Persistence/Querying/PocoToSqlExpressionVisitor.cs index e0a4f07e48..4569b95853 100644 --- a/src/Umbraco.Core/Persistence/Querying/PocoToSqlExpressionVisitor.cs +++ b/src/Umbraco.Core/Persistence/Querying/PocoToSqlExpressionVisitor.cs @@ -18,14 +18,13 @@ namespace Umbraco.Core.Persistence.Querying public PocoToSqlExpressionVisitor(ISqlSyntaxProvider syntaxProvider) : base(syntaxProvider) { - + _pd = new Database.PocoData(typeof(T)); } [Obsolete("Use the overload the specifies a SqlSyntaxProvider")] public PocoToSqlExpressionVisitor() - : base(SqlSyntaxContext.SqlSyntaxProvider) - { - _pd = new Database.PocoData(typeof(T)); + : this(SqlSyntaxContext.SqlSyntaxProvider) + { } protected override string VisitMemberAccess(MemberExpression m) diff --git a/src/Umbraco.Core/Persistence/Repositories/BaseQueryType.cs b/src/Umbraco.Core/Persistence/Repositories/BaseQueryType.cs new file mode 100644 index 0000000000..4579fd98fb --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/BaseQueryType.cs @@ -0,0 +1,9 @@ +namespace Umbraco.Core.Persistence.Repositories +{ + internal enum BaseQueryType + { + Full, + Ids, + 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 7f3e657dc2..765843af9f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -55,7 +55,7 @@ namespace Umbraco.Core.Persistence.Repositories { var sql = GetBaseQuery(false) .Where(GetBaseWhereClause(), new { Id = id }) - .Where(x => x.Newest) + .Where(x => x.Newest, SqlSyntax) .OrderByDescending(x => x.VersionDate, SqlSyntax); var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); @@ -70,63 +70,90 @@ namespace Umbraco.Core.Persistence.Repositories protected override IEnumerable PerformGetAll(params int[] ids) { - var sql = GetBaseQuery(false); - if (ids.Any()) + Func translate = s => { - sql.Where("umbracoNode.id in (@ids)", new { ids }); - } + if (ids.Any()) + { + s.Where("umbracoNode.id in (@ids)", new { ids }); + } + //we only want the newest ones with this method + s.Where(x => x.Newest, SqlSyntax); + return s; + }; + + var sqlBaseFull = GetBaseQuery(BaseQueryType.Full); + var sqlBaseIds = GetBaseQuery(BaseQueryType.Ids); - //we only want the newest ones with this method - sql.Where(x => x.Newest); - - return ProcessQuery(sql); + return ProcessQuery(translate(sqlBaseFull), translate(sqlBaseIds)); } protected override IEnumerable PerformGetByQuery(IQuery query) { - var sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate() - .Where(x => x.Newest) - .OrderByDescending(x => x.VersionDate) - .OrderBy(x => x.SortOrder); + var sqlBaseFull = GetBaseQuery(BaseQueryType.Full); + var sqlBaseIds = GetBaseQuery(BaseQueryType.Ids); - return ProcessQuery(sql); + Func, Sql> translate = (translator) => + { + return translator.Translate() + .Where(x => x.Newest, SqlSyntax) + .OrderByDescending(x => x.VersionDate, SqlSyntax) + .OrderBy(x => x.SortOrder, SqlSyntax); + }; + + var translatorFull = new SqlTranslator(sqlBaseFull, query); + var translatorIds = new SqlTranslator(sqlBaseIds, query); + + return ProcessQuery(translate(translatorFull), translate(translatorIds)); } #endregion #region Overrides of PetaPocoRepositoryBase - - protected override Sql GetBaseQuery(bool isCount) + protected override Sql GetBaseQuery(BaseQueryType queryType) { - var sqlx = string.Format("LEFT OUTER JOIN {0} {1} ON ({1}.{2}={0}.{2} AND {1}.{3}=1)", + var sql = new Sql(); + sql.Select(queryType == BaseQueryType.Count ? "COUNT(*)" : (queryType == BaseQueryType.Ids ? "cmsDocument.nodeId" : "*")) + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.VersionId, right => right.VersionId) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId); + + if (queryType == BaseQueryType.Full) + { + //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 + //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 + + var sqlx = string.Format("LEFT OUTER JOIN {0} {1} ON ({1}.{2}={0}.{2} AND {1}.{3}=1)", SqlSyntax.GetQuotedTableName("cmsDocument"), SqlSyntax.GetQuotedTableName("cmsDocument2"), SqlSyntax.GetQuotedColumnName("nodeId"), SqlSyntax.GetQuotedColumnName("published")); - var sql = new Sql(); - sql.Select(isCount ? "COUNT(*)" : "*") - .From() - .InnerJoin() - .On(left => left.VersionId, right => right.VersionId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - // cannot do this because PetaPoco does not know how to alias the table //.LeftOuterJoin() //.On(left => left.NodeId, right => right.NodeId) // so have to rely on writing our own SQL - .Append(sqlx/*, new { @published = true }*/) + sql.Append(sqlx /*, new { @published = true }*/); + } + + sql.Where(x => x.NodeObjectType == NodeObjectTypeId, SqlSyntax); - .Where(x => x.NodeObjectType == NodeObjectTypeId); return sql; } + protected override Sql GetBaseQuery(bool isCount) + { + return GetBaseQuery(isCount ? BaseQueryType.Count : BaseQueryType.Full); + } + protected override string GetBaseWhereClause() { return "umbracoNode.id = @Id"; @@ -173,20 +200,32 @@ namespace Umbraco.Core.Persistence.Repositories // not bring much safety - so this reverts to updating each record individually, // and it may be slower in the end, but should be more resilient. - var baseId = 0; var contentTypeIdsA = contentTypeIds == null ? new int[0] : contentTypeIds.ToArray(); + + Func translate = (bId, sql) => + { + if (contentTypeIdsA.Length > 0) + { + sql.WhereIn(x => x.ContentTypeId, contentTypeIdsA, SqlSyntax); + } + + sql + .Where(x => x.NodeId > bId && x.Trashed == false, SqlSyntax) + .Where(x => x.Published, SqlSyntax) + .OrderBy(x => x.NodeId, SqlSyntax); + + return sql; + }; + + var baseId = 0; + while (true) { // get the next group of nodes - var query = GetBaseQuery(false); - if (contentTypeIdsA.Length > 0) - query = query - .WhereIn(x => x.ContentTypeId, contentTypeIdsA, SqlSyntax); - query = query - .Where(x => x.NodeId > baseId && x.Trashed == false) - .Where(x => x.Published) - .OrderBy(x => x.NodeId, SqlSyntax); - var xmlItems = ProcessQuery(SqlSyntax.SelectTop(query, groupSize)) + var sqlFull = translate(baseId, GetBaseQuery(BaseQueryType.Full)); + var sqlIds = translate(baseId, GetBaseQuery(BaseQueryType.Ids)); + + var xmlItems = ProcessQuery(SqlSyntax.SelectTop(sqlFull, groupSize), SqlSyntax.SelectTop(sqlIds, groupSize)) .Select(x => new ContentXmlDto { NodeId = x.Id, Xml = serializer(x).ToString() }) .ToList(); @@ -212,17 +251,23 @@ namespace Umbraco.Core.Persistence.Repositories public override IEnumerable GetAllVersions(int id) { - var sql = GetBaseQuery(false) - .Where(GetBaseWhereClause(), new { Id = id }) - .OrderByDescending(x => x.VersionDate, SqlSyntax); - return ProcessQuery(sql, true); + Func translate = s => + { + return s.Where(GetBaseWhereClause(), new {Id = id}) + .OrderByDescending(x => x.VersionDate, SqlSyntax); + }; + + var sqlFull = translate(GetBaseQuery(BaseQueryType.Full)); + var sqlIds = translate(GetBaseQuery(BaseQueryType.Ids)); + + return ProcessQuery(sqlFull, sqlIds, true); } public override IContent GetByVersion(Guid versionId) { var sql = GetBaseQuery(false); sql.Where("cmsContentVersion.VersionId = @VersionId", new { VersionId = versionId }); - sql.OrderByDescending(x => x.VersionDate); + sql.OrderByDescending(x => x.VersionDate, SqlSyntax); var dto = Database.Fetch(sql).FirstOrDefault(); @@ -238,10 +283,10 @@ namespace Umbraco.Core.Persistence.Repositories { var sql = new Sql() .Select("*") - .From() - .InnerJoin().On(left => left.VersionId, right => right.VersionId) - .Where(x => x.VersionId == versionId) - .Where(x => x.Newest != true); + .From(SqlSyntax) + .InnerJoin(SqlSyntax).On(SqlSyntax, left => left.VersionId, right => right.VersionId) + .Where(x => x.VersionId == versionId, SqlSyntax) + .Where(x => x.Newest != true, SqlSyntax); var dto = Database.Fetch(sql).FirstOrDefault(); if (dto == null) return; @@ -617,21 +662,27 @@ namespace Umbraco.Core.Persistence.Repositories public IEnumerable GetByPublishedVersion(IQuery query) { + Func, Sql> translate = t => + { + return t.Translate() + .Where(x => x.Published, SqlSyntax) + .OrderBy(x => x.Level, SqlSyntax) + .OrderBy(x => x.SortOrder, SqlSyntax); + }; + // we WANT to return contents in top-down order, ie parents should come before children // ideal would be pure xml "document order" which can be achieved with: // 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 sqlClause = GetBaseQuery(false); - var translator = new SqlTranslator(sqlClause, query); - var sql = translator.Translate() - .Where(x => x.Published) - .OrderBy(x => x.Level, SqlSyntax) - .OrderBy(x => x.SortOrder, SqlSyntax); + var sqlFull = GetBaseQuery(BaseQueryType.Full); + var translatorFull = new SqlTranslator(sqlFull, query); + var sqlIds = GetBaseQuery(BaseQueryType.Ids); + var translatorIds = new SqlTranslator(sqlIds, query); - return ProcessQuery(sql, true); + return ProcessQuery(translate(translatorFull), translate(translatorIds), true); } - + /// /// This builds the Xml document used for the XML cache /// @@ -663,10 +714,14 @@ namespace Umbraco.Core.Persistence.Repositories parent.Attributes.Append(pIdAtt); xmlDoc.AppendChild(parent); - const string sql = @"select umbracoNode.id, umbracoNode.parentID, umbracoNode.sortOrder, cmsContentXml.xml, umbracoNode.level from umbracoNode + //Ensure that only nodes that have published versions are selected + var sql = string.Format(@"select umbracoNode.id, umbracoNode.parentID, umbracoNode.sortOrder, cmsContentXml.{0}, umbracoNode.{1} from umbracoNode inner join cmsContentXml on cmsContentXml.nodeId = umbracoNode.id and umbracoNode.nodeObjectType = @type where umbracoNode.id in (select cmsDocument.nodeId from cmsDocument where cmsDocument.published = 1) -order by umbracoNode.level, umbracoNode.parentID, umbracoNode.sortOrder"; +order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", + SqlSyntax.GetQuotedColumnName("xml"), + SqlSyntax.GetQuotedColumnName("level"), + SqlSyntax.GetQuotedColumnName("level")); XmlElement last = null; @@ -803,9 +858,9 @@ order by umbracoNode.level, umbracoNode.parentID, umbracoNode.sortOrder"; Func> filterCallback = () => new Tuple(filterSql.SQL, filterSql.Arguments); - return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, + return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, new Tuple("cmsDocument", "nodeId"), - sql => ProcessQuery(sql), orderBy, orderDirection, orderBySystemField, + (sqlFull, sqlIds) => ProcessQuery(sqlFull, sqlIds), orderBy, orderDirection, orderBySystemField, filterCallback); } @@ -836,16 +891,32 @@ order by umbracoNode.level, umbracoNode.parentID, umbracoNode.sortOrder"; return base.GetDatabaseFieldNameForOrderBy(orderBy); } - private IEnumerable ProcessQuery(Sql sql, bool withCache = false) + /// + /// 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 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) { // fetch returns a list so it's ok to iterate it in this method - var dtos = Database.Fetch(sql); + var dtos = Database.Fetch(sqlFull); if (dtos.Count == 0) return Enumerable.Empty(); var content = new IContent[dtos.Count]; var defs = new List(); var templateIds = new List(); - + + //track the looked up content types, even though the content types are cached + // they still need to be deep cloned out of the cache and we don't want to add + // the overhead of deep cloning them on every item in this loop + var contentTypes = new Dictionary(); + for (var i = 0; i < dtos.Count; i++) { var dto = dtos[i]; @@ -864,9 +935,19 @@ order by umbracoNode.level, umbracoNode.parentID, umbracoNode.sortOrder"; // else, need to fetch from the database // content type repository is full-cache so OK to get each one independently - var contentType = _contentTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); - var factory = new ContentFactory(contentType, NodeObjectTypeId, dto.NodeId); - content[i] = factory.BuildEntity(dto); + + IContentType contentType; + if (contentTypes.ContainsKey(dto.ContentVersionDto.ContentDto.ContentTypeId)) + { + contentType = contentTypes[dto.ContentVersionDto.ContentDto.ContentTypeId]; + } + else + { + contentType = _contentTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); + contentTypes[dto.ContentVersionDto.ContentDto.ContentTypeId] = contentType; + } + + content[i] = ContentFactory.BuildEntity(dto, contentType); // need template if (dto.TemplateId.HasValue && dto.TemplateId.Value > 0) @@ -887,7 +968,7 @@ order by umbracoNode.level, umbracoNode.parentID, umbracoNode.sortOrder"; .ToDictionary(x => x.Id, x => x); // load all properties for all documents from database in 1 query - var propertyData = GetPropertyCollection(sql, defs); + var propertyData = GetPropertyCollection(sqlIds, defs); // assign var dtoIndex = 0; @@ -907,7 +988,7 @@ order by umbracoNode.level, umbracoNode.parentID, umbracoNode.sortOrder"; //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 - ((Entity) cc).ResetDirtyProperties(false); + cc.ResetDirtyProperties(false); } return content; @@ -924,8 +1005,7 @@ order by umbracoNode.level, umbracoNode.parentID, umbracoNode.sortOrder"; { var contentType = _contentTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); - var factory = new ContentFactory(contentType, NodeObjectTypeId, dto.NodeId); - var content = factory.BuildEntity(dto); + var content = ContentFactory.BuildEntity(dto, contentType); //Check if template id is set on DocumentDto, and get ITemplate if it is. if (dto.TemplateId.HasValue && dto.TemplateId.Value > 0) diff --git a/src/Umbraco.Core/Persistence/Repositories/EntityContainerRepository.cs b/src/Umbraco.Core/Persistence/Repositories/EntityContainerRepository.cs index cc13275798..b76abf4db6 100644 --- a/src/Umbraco.Core/Persistence/Repositories/EntityContainerRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/EntityContainerRepository.cs @@ -62,17 +62,27 @@ namespace Umbraco.Core.Persistence.Repositories protected override IEnumerable PerformGetAll(params int[] ids) { - //we need to batch these in groups of 2000 so we don't exceed the max 2100 limit - return ids.InGroupsOf(2000).SelectMany(@group => + if (ids.Any()) + { + //we need to batch these in groups of 2000 so we don't exceed the max 2100 limit + return ids.InGroupsOf(2000).SelectMany(@group => + { + var sql = GetBaseQuery(false) + .Where("nodeObjectType=@umbracoObjectTypeId", new {umbracoObjectTypeId = NodeObjectTypeId}) + .Where(string.Format("{0} IN (@ids)", SqlSyntax.GetQuotedColumnName("id")), new {ids = @group}); + + sql.OrderBy(x => x.Level, SqlSyntax); + + return Database.Fetch(sql).Select(CreateEntity); + }); + } + else { var sql = GetBaseQuery(false) - .Where("nodeObjectType=@umbracoObjectTypeId", new { umbracoObjectTypeId = NodeObjectTypeId }) - .Where(string.Format("{0} IN (@ids)", SqlSyntax.GetQuotedColumnName("id")), new { ids = @group }); - + .Where("nodeObjectType=@umbracoObjectTypeId", new {umbracoObjectTypeId = NodeObjectTypeId}); sql.OrderBy(x => x.Level, SqlSyntax); - return Database.Fetch(sql).Select(CreateEntity); - }); + } } protected override IEnumerable PerformGetByQuery(IQuery query) diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs index e2555995d2..28e4fbf199 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs @@ -96,5 +96,6 @@ namespace Umbraco.Core.Persistence.Repositories /// An Enumerable list of objects IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter = null); + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs index 64989f9269..9e8890a37b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs @@ -38,15 +38,6 @@ namespace Umbraco.Core.Persistence.Repositories /// An Enumerable list of objects IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, bool orderBySystemField, string filter = ""); - - /// - /// Gets paged media descendants as XML by path - /// - /// Path starts with - /// Page number - /// Page size - /// Total records the query would return without paging - /// A paged enumerable of XML entries of media items - IEnumerable GetPagedXmlEntriesByPath(string path, long pageIndex, int pageSize, out long totalRecords); + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs index a24116f0e2..9528fd3d76 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs @@ -73,6 +73,6 @@ namespace Umbraco.Core.Persistence.Repositories /// /// void AddOrUpdatePreviewXml(IMember content, Func xml); - + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepositoryVersionable.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepositoryVersionable.cs index 3e05d1feaf..b318223ca7 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepositoryVersionable.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepositoryVersionable.cs @@ -67,5 +67,16 @@ namespace Umbraco.Core.Persistence.Repositories /// Id of the object to delete versions from /// Latest version date void DeleteVersions(int id, DateTime versionDate); + + /// + /// Gets paged content descendants as XML by path + /// + /// Path starts with + /// Page number + /// Page size + /// + /// Total records the query would return without paging + /// A paged enumerable of XML entries of content items + IEnumerable GetPagedXmlEntriesByPath(string path, long pageIndex, int pageSize, string[] orderBy, out long totalRecords); } } \ 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 08c2f7e0be..8cfb037c6c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -76,7 +76,7 @@ namespace Umbraco.Core.Persistence.Repositories var sqlClause = GetBaseQuery(false); var translator = new SqlTranslator(sqlClause, query); var sql = translator.Translate() - .OrderBy(x => x.SortOrder); + .OrderBy(x => x.SortOrder, SqlSyntax); return ProcessQuery(sql); } @@ -84,18 +84,23 @@ namespace Umbraco.Core.Persistence.Repositories #endregion #region Overrides of PetaPocoRepositoryBase + + protected override Sql GetBaseQuery(BaseQueryType queryType) + { + var sql = new Sql(); + sql.Select(queryType == BaseQueryType.Count ? "COUNT(*)" : (queryType == BaseQueryType.Ids ? "cmsContentVersion.contentId" : "*")) + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId, SqlSyntax) + .Where(x => x.NodeObjectType == NodeObjectTypeId, SqlSyntax); + return sql; + } protected override Sql GetBaseQuery(bool isCount) { - var sql = new Sql(); - sql.Select(isCount ? "COUNT(*)" : "*") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId); - return sql; + return GetBaseQuery(isCount ? BaseQueryType.Count : BaseQueryType.Full); } protected override string GetBaseWhereClause() @@ -148,6 +153,11 @@ namespace Umbraco.Core.Persistence.Repositories var content = new IMedia[dtos.Count]; var defs = new List(); + //track the looked up content types, even though the content types are cached + // they still need to be deep cloned out of the cache and we don't want to add + // the overhead of deep cloning them on every item in this loop + var contentTypes = new Dictionary(); + for (var i = 0; i < dtos.Count; i++) { var dto = dtos[i]; @@ -165,9 +175,19 @@ namespace Umbraco.Core.Persistence.Repositories // else, need to fetch from the database // content type repository is full-cache so OK to get each one independently - var contentType = _mediaTypeRepository.Get(dto.ContentDto.ContentTypeId); - var factory = new MediaFactory(contentType, NodeObjectTypeId, dto.NodeId); - content[i] = factory.BuildEntity(dto); + + IMediaType contentType; + if (contentTypes.ContainsKey(dto.ContentDto.ContentTypeId)) + { + contentType = contentTypes[dto.ContentDto.ContentTypeId]; + } + else + { + contentType = _mediaTypeRepository.Get(dto.ContentDto.ContentTypeId); + contentTypes[dto.ContentDto.ContentTypeId] = contentType; + } + + content[i] = MediaFactory.BuildEntity(dto, contentType); // need properties defs.Add(new DocumentDefinition( @@ -195,7 +215,7 @@ namespace Umbraco.Core.Persistence.Repositories //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 - ((Entity) cc).ResetDirtyProperties(false); + cc.ResetDirtyProperties(false); } return content; @@ -205,26 +225,16 @@ namespace Umbraco.Core.Persistence.Repositories { var sql = GetBaseQuery(false); sql.Where("cmsContentVersion.VersionId = @VersionId", new { VersionId = versionId }); - sql.OrderByDescending(x => x.VersionDate); + sql.OrderByDescending(x => x.VersionDate, SqlSyntax); var dto = Database.Fetch(sql).FirstOrDefault(); if (dto == null) return null; - var mediaType = _mediaTypeRepository.Get(dto.ContentDto.ContentTypeId); + var content = CreateMediaFromDto(dto, versionId, sql); - var factory = new MediaFactory(mediaType, NodeObjectTypeId, dto.NodeId); - var media = factory.BuildEntity(dto); - - var properties = GetPropertyCollection(sql, new[] { new DocumentDefinition(dto.NodeId, dto.VersionId, media.UpdateDate, media.CreateDate, mediaType) }); - - media.Properties = properties[dto.NodeId]; - - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - ((Entity)media).ResetDirtyProperties(false); - return media; + return content; } public void RebuildXmlStructures(Func serializer, int groupSize = 200, IEnumerable contentTypeIds = null) @@ -245,7 +255,7 @@ namespace Umbraco.Core.Persistence.Repositories query = query .WhereIn(x => x.ContentTypeId, contentTypeIdsA, SqlSyntax); query = query - .Where(x => x.NodeId > baseId) + .Where(x => x.NodeId > baseId, SqlSyntax) .OrderBy(x => x.NodeId, SqlSyntax); var xmlItems = ProcessQuery(SqlSyntax.SelectTop(query, groupSize)) .Select(x => new ContentXmlDto { NodeId = x.Id, Xml = serializer(x).ToString() }) @@ -495,37 +505,13 @@ namespace Umbraco.Core.Persistence.Repositories filterCallback = () => new Tuple(sbWhere.ToString().Trim(), args.ToArray()); } - return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, + return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, new Tuple("cmsContentVersion", "contentId"), - sql => ProcessQuery(sql), orderBy, orderDirection, orderBySystemField, + (sqlFull, sqlIds) => ProcessQuery(sqlFull), orderBy, orderDirection, orderBySystemField, filterCallback); } - /// - /// Gets paged media descendants as XML by path - /// - /// Path starts with - /// Page number - /// Page size - /// Total records the query would return without paging - /// A paged enumerable of XML entries of media items - public IEnumerable GetPagedXmlEntriesByPath(string path, long pageIndex, int pageSize, out long totalRecords) - { - Sql query; - if (path == "-1") - { - query = new Sql().Select("nodeId, xml").From("cmsContentXml").Where("nodeId IN (SELECT id FROM umbracoNode WHERE nodeObjectType = @0)", Guid.Parse(Constants.ObjectTypes.Media)).OrderBy("nodeId"); - } - else - { - query = new Sql().Select("nodeId, xml").From("cmsContentXml").Where("nodeId IN (SELECT id FROM umbracoNode WHERE path LIKE @0)", path.EnsureEndsWith(",%")).OrderBy("nodeId"); - } - var pagedResult = Database.Page(pageIndex+1, pageSize, query); - totalRecords = pagedResult.TotalItems; - return pagedResult.Items.Select(dto => XElement.Parse(dto.Xml)); - } - /// /// Private method to create a media object from a ContentDto /// @@ -536,9 +522,8 @@ namespace Umbraco.Core.Persistence.Repositories private IMedia CreateMediaFromDto(ContentVersionDto dto, Guid versionId, Sql docSql) { var contentType = _mediaTypeRepository.Get(dto.ContentDto.ContentTypeId); - - var factory = new MediaFactory(contentType, NodeObjectTypeId, dto.NodeId); - var media = factory.BuildEntity(dto); + + var media = MediaFactory.BuildEntity(dto, contentType); var docDef = new DocumentDefinition(dto.NodeId, versionId, media.UpdateDate, media.CreateDate, contentType); diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs index dcab898685..8b3bf4a471 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs @@ -107,24 +107,29 @@ namespace Umbraco.Core.Persistence.Repositories #region Overrides of PetaPocoRepositoryBase - protected override Sql GetBaseQuery(bool isCount) + protected override Sql GetBaseQuery(BaseQueryType queryType) { var sql = new Sql(); - sql.Select(isCount ? "COUNT(*)" : "*") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) + sql.Select(queryType == BaseQueryType.Count ? "COUNT(*)" : (queryType == BaseQueryType.Ids ? "cmsMember.nodeId" : "*")) + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId) //We're joining the type so we can do a query against the member type - not sure if this adds much overhead or not? // the execution plan says it doesn't so we'll go with that and in that case, it might be worth joining the content // types by default on the document and media repo's so we can query by content type there too. - .InnerJoin().On(left => left.NodeId, right => right.ContentTypeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(x => x.NodeObjectType == NodeObjectTypeId); + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.ContentTypeId) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId, SqlSyntax); return sql; + } + protected override Sql GetBaseQuery(bool isCount) + { + return GetBaseQuery(isCount ? BaseQueryType.Count : BaseQueryType.Full); } protected override string GetBaseWhereClause() @@ -381,7 +386,7 @@ namespace Umbraco.Core.Persistence.Repositories .Where(GetBaseWhereClause(), new { Id = id }) .OrderByDescending(x => x.VersionDate, SqlSyntax); return ProcessQuery(sql, true); - } + } public void RebuildXmlStructures(Func serializer, int groupSize = 200, IEnumerable contentTypeIds = null) { @@ -617,9 +622,9 @@ namespace Umbraco.Core.Persistence.Repositories filterCallback = () => new Tuple(sbWhere.ToString().Trim(), args.ToArray()); } - return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, + return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, new Tuple("cmsMember", "nodeId"), - sql => ProcessQuery(sql), orderBy, orderDirection, orderBySystemField, + (sqlFull, sqlIds) => ProcessQuery(sqlFull), orderBy, orderDirection, orderBySystemField, filterCallback); } diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index 163e0810ad..16382ae462 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; +using System.Xml.Linq; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; @@ -26,8 +28,6 @@ using Umbraco.Core.Media; namespace Umbraco.Core.Persistence.Repositories { - using SqlSyntax; - internal abstract class VersionableRepositoryBase : PetaPocoRepositoryBase where TEntity : class, IAggregateRoot { @@ -138,6 +138,40 @@ namespace Umbraco.Core.Persistence.Repositories #endregion + /// + /// Gets paged document descendants as XML by path + /// + /// Path starts with + /// Page number + /// Page size + /// + /// Total records the query would return without paging + /// A paged enumerable of XML entries of content items + public virtual IEnumerable GetPagedXmlEntriesByPath(string path, long pageIndex, int pageSize, string[] orderBy, out long totalRecords) + { + var query = new Sql().Select(string.Format("umbracoNode.id, cmsContentXml.{0}", SqlSyntax.GetQuotedColumnName("xml"))) + .From("umbracoNode") + .InnerJoin("cmsContentXml").On("cmsContentXml.nodeId = umbracoNode.id"); + + if (path == "-1") + { + query.Where("umbracoNode.nodeObjectType = @type", new { type = NodeObjectTypeId }); + } + else + { + query.Where(string.Format("umbracoNode.{0} LIKE (@0)", SqlSyntax.GetQuotedColumnName("path")), path.EnsureEndsWith(",%")); + } + + //each order by param needs to be in a bracket! see: https://github.com/toptensoftware/PetaPoco/issues/177 + query.OrderBy(orderBy == null + ? "(umbracoNode.id)" + : string.Join(",", orderBy.Select(x => string.Format("({0})", SqlSyntax.GetQuotedColumnName(x))))); + + var pagedResult = Database.Page(pageIndex + 1, pageSize, query); + totalRecords = pagedResult.TotalItems; + return pagedResult.Items.Select(dto => XElement.Parse(dto.Xml)); + } + public int CountDescendants(int parentId, string contentTypeAlias = null) { var pathMatch = parentId == -1 @@ -349,12 +383,8 @@ namespace Umbraco.Core.Persistence.Repositories ON CustomPropData.CustomPropValContentId = umbracoNode.id ", sortedInt, sortedDecimal, sortedDate, sortedString, nodeIdSelect.Item2, nodeIdSelect.Item1, versionQuery, sortedSql.Arguments.Length, newestQuery); - //insert this just above the first LEFT OUTER JOIN (for cmsDocument) or the last WHERE (everything else) - string newSql; - if (nodeIdSelect.Item1 == "cmsDocument") - newSql = sortedSql.SQL.Insert(sortedSql.SQL.IndexOf("LEFT OUTER JOIN"), outerJoinTempTable); - else - newSql = sortedSql.SQL.Insert(sortedSql.SQL.LastIndexOf("WHERE"), outerJoinTempTable); + //insert this just above the last WHERE + string newSql = sortedSql.SQL.Insert(sortedSql.SQL.LastIndexOf("WHERE"), outerJoinTempTable); var newArgs = sortedSql.Arguments.ToList(); newArgs.Add(orderBy); @@ -371,11 +401,14 @@ namespace Umbraco.Core.Persistence.Repositories } } - //no matter what we always MUST order the result also by umbracoNode.id to ensure that all records being ordered by are unique. - // if we do not do this then we end up with issues where we are ordering by a field that has duplicate values (i.e. the 'text' column - // is empty for many nodes) - // see: http://issues.umbraco.org/issue/U4-8831 - sortedSql.OrderBy("umbracoNode.id"); + if (orderBySystemField && orderBy != "umbracoNode.id") + { + //no matter what we always MUST order the result also by umbracoNode.id to ensure that all records being ordered by are unique. + // if we do not do this then we end up with issues where we are ordering by a field that has duplicate values (i.e. the 'text' column + // is empty for many nodes) + // see: http://issues.umbraco.org/issue/U4-8831 + sortedSql.OrderBy("umbracoNode.id"); + } return sortedSql; @@ -385,7 +418,6 @@ namespace Umbraco.Core.Persistence.Repositories /// A helper method for inheritors to get the paged results by query in a way that minimizes queries /// /// The type of the d. - /// The 'true' entity type (i.e. Content, Member, etc...) /// The query. /// Index of the page. /// Size of the page. @@ -398,34 +430,30 @@ namespace Umbraco.Core.Persistence.Repositories /// Flag to indicate when ordering by system field /// /// orderBy - protected IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, + protected IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, Tuple nodeIdSelect, - Func> processQuery, + Func> processQuery, string orderBy, Direction orderDirection, bool orderBySystemField, Func> defaultFilter = null) - where TContentBase : class, IAggregateRoot, TEntity { if (orderBy == null) throw new ArgumentNullException("orderBy"); - // Get base query - var sqlBase = GetBaseQuery(false); + // Get base query for returning IDs + var sqlBaseIds = GetBaseQuery(BaseQueryType.Ids); + // Get base query for returning all data + var sqlBaseFull = GetBaseQuery(BaseQueryType.Full); if (query == null) query = new Query(); - var translator = new SqlTranslator(sqlBase, query); - var sqlQuery = translator.Translate(); - - // Note we can't do multi-page for several DTOs like we can multi-fetch and are doing in PerformGetByQuery, - // but actually given we are doing a Get on each one (again as in PerformGetByQuery), we only need the node Id. - // So we'll modify the SQL. - var sqlNodeIds = new Sql( - sqlQuery.SQL.Replace("SELECT *", string.Format("SELECT {0}.{1}", nodeIdSelect.Item1, nodeIdSelect.Item2)), - sqlQuery.Arguments); + var translatorIds = new SqlTranslator(sqlBaseIds, query); + var sqlQueryIds = translatorIds.Translate(); + var translatorFull = new SqlTranslator(sqlBaseFull, query); + var sqlQueryFull = translatorFull.Translate(); //get sorted and filtered sql var sqlNodeIdsWithSort = GetSortedSqlForPagedResults( - GetFilteredSqlForPagedResults(sqlNodeIds, defaultFilter), + GetFilteredSqlForPagedResults(sqlQueryIds, defaultFilter), orderDirection, orderBy, orderBySystemField, nodeIdSelect); // Get page of results and total count @@ -441,31 +469,25 @@ namespace Umbraco.Core.Persistence.Repositories //Crete 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); + Database.BuildPageQueries(pageIndex * pageSize, pageSize, sqlNodeIdsWithSort.SQL, ref args, out sqlStringCount, out sqlStringPage); - //if this is for sql server, the sqlPage will start with a SELECT * but we don't want that, we only want to return the nodeId - sqlStringPage = sqlStringPage - .Replace("SELECT *", - //This ensures we only take the field name of the node id select and not the table name - since the resulting select - // will ony work with the field name. - "SELECT " + nodeIdSelect.Item2); - - //We need to make this an inner join on the paged query - var splitQuery = sqlQuery.SQL.Split(new[] { "WHERE " }, StringSplitOptions.None); - var withInnerJoinSql = new Sql(splitQuery[0]) + //We need to make this FULL query an inner join on the paged ID query + var splitQuery = sqlQueryFull.SQL.Split(new[] { "WHERE " }, StringSplitOptions.None); + var fullQueryWithPagedInnerJoin = new Sql(splitQuery[0]) .Append("INNER JOIN (") //join the paged query with the paged query arguments .Append(sqlStringPage, args) .Append(") temp ") .Append(string.Format("ON {0}.{1} = temp.{1}", nodeIdSelect.Item1, nodeIdSelect.Item2)) //add the original where clause back with the original arguments - .Where(splitQuery[1], sqlQuery.Arguments); + .Where(splitQuery[1], sqlQueryIds.Arguments); //get sorted and filtered sql var fullQuery = GetSortedSqlForPagedResults( - GetFilteredSqlForPagedResults(withInnerJoinSql, defaultFilter), + GetFilteredSqlForPagedResults(fullQueryWithPagedInnerJoin, defaultFilter), orderDirection, orderBy, orderBySystemField, nodeIdSelect); - return processQuery(fullQuery); + + return processQuery(fullQuery, sqlNodeIdsWithSort); } else { @@ -477,9 +499,9 @@ namespace Umbraco.Core.Persistence.Repositories protected IDictionary GetPropertyCollection( Sql docSql, - IEnumerable documentDefs) + IReadOnlyCollection documentDefs) { - if (documentDefs.Any() == false) return new Dictionary(); + if (documentDefs.Count == 0) return new Dictionary(); //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 @@ -490,23 +512,12 @@ namespace Umbraco.Core.Persistence.Repositories parsedOriginalSql = parsedOriginalSql.Substring(0, parsedOriginalSql.LastIndexOf("ORDER BY ", StringComparison.Ordinal)); } - var propSql = new Sql(@"SELECT cmsPropertyData.* -FROM cmsPropertyData -INNER JOIN cmsPropertyType -ON cmsPropertyData.propertytypeid = cmsPropertyType.id -INNER JOIN - (" + string.Format(parsedOriginalSql, "cmsContent.nodeId, cmsContentVersion.VersionId") + @") as docData -ON cmsPropertyData.versionId = docData.VersionId AND cmsPropertyData.contentNodeId = docData.nodeId -LEFT OUTER JOIN cmsDataTypePreValues -ON cmsPropertyType.dataTypeId = cmsDataTypePreValues.datatypeNodeId", docSql.Arguments); - - var allPropertyData = Database.Fetch(propSql); - - //This is a lazy access call to get all prevalue data for the data types that make up all of these properties which we use - // below if any property requires tag support - var allPreValues = new Lazy>(() => - { - var preValsSql = new Sql(@"SELECT a.id, a.value, a.sortorder, a.alias, a.datatypeNodeId + //This retrieves all pre-values for all data types that are referenced for all property types + // that exist in the data set. + //Benchmarks show that eagerly loading these so that we can lazily read the property data + // below (with the use of Query intead of Fetch) go about 30% faster, so we'll eagerly load + // this now since we cannot execute another reader inside of reading the property data. + var preValsSql = new Sql(@"SELECT a.id, a.value, a.sortorder, a.alias, a.datatypeNodeId FROM cmsDataTypePreValues a WHERE EXISTS( SELECT DISTINCT b.id as preValIdInner @@ -518,25 +529,85 @@ WHERE EXISTS( ON cmsPropertyType.contentTypeId = docData.contentType WHERE a.id = b.id)", docSql.Arguments); - return Database.Fetch(preValsSql); - }); + var allPreValues = Database.Fetch(preValsSql); + + //It's Important with the sort order here! We require this to be sorted by node id, + // this is required because this data set can be huge depending on the page size. Due + // to it's size we need to be smart about iterating over the property values to build + // the document. Before we used to use Linq to get the property data for a given content node + // and perform a Distinct() call. This kills performance because that would mean if we had 7000 nodes + // and on each iteration we will perform a lookup on potentially 100,000 property rows against the node + // id which turns out to be a crazy amount of iterations. Instead we know it's sorted by this value we'll + // keep an index stored of the rows being read so we never have to re-iterate the entire data set + // on each document iteration. + var propSql = new Sql(@"SELECT cmsPropertyData.* +FROM cmsPropertyData +INNER JOIN cmsPropertyType +ON cmsPropertyData.propertytypeid = cmsPropertyType.id +INNER JOIN + (" + string.Format(parsedOriginalSql, "cmsContent.nodeId, cmsContentVersion.VersionId") + @") as docData +ON cmsPropertyData.versionId = docData.VersionId AND cmsPropertyData.contentNodeId = docData.nodeId +ORDER BY contentNodeId, propertytypeid +", docSql.Arguments); + + //This does NOT fetch all data into memory in a list, this will read + // over the records as a data reader, this is much better for performance and memory, + // but it means that during the reading of this data set, nothing else can be read + // from SQL server otherwise we'll get an exception. + var allPropertyData = Database.Query(propSql); var result = new Dictionary(); - var propertiesWithTagSupport = new Dictionary(); + //used to track the resolved composition property types per content type so we don't have to re-resolve (ToArray) the list every time + var resolvedCompositionProperties = new Dictionary(); - //iterate each definition grouped by it's content type - this will mean less property type iterations while building - // up the property collections - foreach (var compositionGroup in documentDefs.GroupBy(x => x.Composition)) + //keep track of the current property data item being enumerated + var propertyDataSetEnumerator = allPropertyData.GetEnumerator(); + + try { - var compositionProperties = compositionGroup.Key.CompositionPropertyTypes.ToArray(); - - foreach (var def in compositionGroup) + //This must be sorted by node id because this is how we are sorting the query to lookup property types above, + // which allows us to more efficiently iterate over the large data set of property values + foreach (var def in documentDefs.OrderBy(x => x.Id)) { - var propertyDataDtos = allPropertyData.Where(x => x.NodeId == def.Id).Distinct(); + //get the resolved proeprties from our local cache, or resolve them and put them in cache + PropertyType[] compositionProperties; + if (resolvedCompositionProperties.ContainsKey(def.Composition.Id)) + { + compositionProperties = resolvedCompositionProperties[def.Composition.Id]; + } + else + { + compositionProperties = def.Composition.CompositionPropertyTypes.ToArray(); + resolvedCompositionProperties[def.Composition.Id] = compositionProperties; + } - var propertyFactory = new PropertyFactory(compositionProperties, def.Version, def.Id, def.CreateDate, def.VersionDate); - var properties = propertyFactory.BuildEntity(propertyDataDtos.ToArray()).ToArray(); + var propertyDataDtos = new List(); + + //Check if there is a current enumerated item and check if we match and add it + if (propertyDataSetEnumerator.Current != null) + { + if (propertyDataSetEnumerator.Current.NodeId == def.Id) + { + propertyDataDtos.Add(propertyDataSetEnumerator.Current); + } + } + //Move to the next position, see if we match and add it, if not exit + while (propertyDataSetEnumerator.MoveNext()) + { + if (propertyDataSetEnumerator.Current.NodeId == def.Id) + { + propertyDataDtos.Add(propertyDataSetEnumerator.Current); + } + else + { + //the node id has changed so we need to exit the loop, + //the enumerator position will be maintained + break; + } + } + + var properties = PropertyFactory.BuildEntity(propertyDataDtos, compositionProperties, def.CreateDate, def.VersionDate).ToArray(); foreach (var property in properties) { @@ -553,7 +624,7 @@ WHERE EXISTS( propertiesWithTagSupport[property.PropertyType.PropertyEditorAlias] = tagSupport; //this property has tags, so we need to extract them and for that we need the prevals which we've already looked up - var preValData = allPreValues.Value.Where(x => x.DataTypeNodeId == property.PropertyType.DataTypeDefinitionId) + var preValData = allPreValues.Where(x => x.DataTypeNodeId == property.PropertyType.DataTypeDefinitionId) .Distinct() .ToArray(); @@ -574,8 +645,13 @@ WHERE EXISTS( result[def.Id] = new PropertyCollection(properties); } } + finally + { + propertyDataSetEnumerator.Dispose(); + } return result; + } public class DocumentDefinition @@ -687,5 +763,12 @@ WHERE EXISTS( return allsuccess; } + + /// + /// For Paging, repositories must support returning different query for the query type specified + /// + /// + /// + protected abstract Sql GetBaseQuery(BaseQueryType queryType); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Properties/AssemblyInfo.cs b/src/Umbraco.Core/Properties/AssemblyInfo.cs index dbc1ab6c93..a74ebb8fec 100644 --- a/src/Umbraco.Core/Properties/AssemblyInfo.cs +++ b/src/Umbraco.Core/Properties/AssemblyInfo.cs @@ -29,7 +29,7 @@ using System.Security.Permissions; [assembly: InternalsVisibleTo("umbraco.webservices")] [assembly: InternalsVisibleTo("umbraco.datalayer")] [assembly: InternalsVisibleTo("umbraco.MacroEngines")] - +[assembly: InternalsVisibleTo("umbraco.providers")] [assembly: InternalsVisibleTo("umbraco.editorControls")] [assembly: InternalsVisibleTo("Umbraco.Tests")] [assembly: InternalsVisibleTo("Umbraco.Tests.Benchmarks")] @@ -42,7 +42,10 @@ using System.Security.Permissions; [assembly: InternalsVisibleTo("Umbraco.VisualStudio")] [assembly: InternalsVisibleTo("Umbraco.Courier.Core")] [assembly: InternalsVisibleTo("Umbraco.Courier.Persistence")] -[assembly: InternalsVisibleTo("umbraco.providers")] + +[assembly: InternalsVisibleTo("Umbraco.Deploy")] +[assembly: InternalsVisibleTo("Umbraco.Deploy.UI")] +[assembly: InternalsVisibleTo("Umbraco.Deploy.Cloud")] //allow this to be mocked in our unit tests [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] \ No newline at end of file diff --git a/src/Umbraco.Core/Serialization/StreamResultExtensions.cs b/src/Umbraco.Core/Serialization/StreamResultExtensions.cs new file mode 100644 index 0000000000..96490a933c --- /dev/null +++ b/src/Umbraco.Core/Serialization/StreamResultExtensions.cs @@ -0,0 +1,22 @@ +using System.IO; +using System.Text; +using System.Xml.Linq; + +namespace Umbraco.Core.Serialization +{ + public static class StreamResultExtensions + { + public static string ToJsonString(this Stream stream) + { + byte[] bytes = new byte[stream.Length]; + stream.Position = 0; + stream.Read(bytes, 0, (int)stream.Length); + return Encoding.UTF8.GetString(bytes); + } + + public static XDocument ToXDoc(this Stream stream) + { + return XDocument.Load(stream); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Serialization/StreamedResult.cs b/src/Umbraco.Core/Serialization/StreamedResult.cs index 9b73fd06bf..0a50751229 100644 --- a/src/Umbraco.Core/Serialization/StreamedResult.cs +++ b/src/Umbraco.Core/Serialization/StreamedResult.cs @@ -1,6 +1,4 @@ using System.IO; -using System.Text; -using System.Xml.Linq; namespace Umbraco.Core.Serialization { @@ -20,20 +18,4 @@ namespace Umbraco.Core.Serialization #endregion } - - public static class StreamResultExtensions - { - public static string ToJsonString(this Stream stream) - { - byte[] bytes = new byte[stream.Length]; - stream.Position = 0; - stream.Read(bytes, 0, (int)stream.Length); - return Encoding.UTF8.GetString(bytes); - } - - public static XDocument ToXDoc(this Stream stream) - { - return XDocument.Load(stream); - } - } } \ No newline at end of file diff --git a/src/Umbraco.Core/Serialization/UdiJsonConverter.cs b/src/Umbraco.Core/Serialization/UdiJsonConverter.cs new file mode 100644 index 0000000000..ff62535825 --- /dev/null +++ b/src/Umbraco.Core/Serialization/UdiJsonConverter.cs @@ -0,0 +1,27 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Umbraco.Core.Serialization +{ + + public class UdiJsonConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return typeof(Udi).IsAssignableFrom(objectType); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteValue(value.ToString()); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var jo = JToken.ReadFrom(reader); + var val = jo.ToObject(); + return val == null ? null : Udi.Parse(val); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Serialization/UdiRangeJsonConverter.cs b/src/Umbraco.Core/Serialization/UdiRangeJsonConverter.cs new file mode 100644 index 0000000000..099c46f29d --- /dev/null +++ b/src/Umbraco.Core/Serialization/UdiRangeJsonConverter.cs @@ -0,0 +1,26 @@ +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Umbraco.Core.Serialization +{ + public class UdiRangeJsonConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return typeof(UdiRange).IsAssignableFrom(objectType); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteValue(value.ToString()); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var jo = JToken.ReadFrom(reader); + var val = jo.ToObject(); + return val == null ? null : UdiRange.Parse(val); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index e0edaa38c4..32665910ad 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -585,7 +585,7 @@ namespace Umbraco.Core.Services [Obsolete("Use the overload with 'long' parameter types instead")] [EditorBrowsable(EditorBrowsableState.Never)] - public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") + public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = "") { long total; var result = GetPagedDescendants(id, Convert.ToInt64(pageIndex), pageSize, out total, orderBy, orderDirection, true, filter); @@ -604,7 +604,7 @@ namespace Umbraco.Core.Services /// Direction to order by /// Search text filter /// An Enumerable list of objects - public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") + public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = "") { return GetPagedDescendants(id, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, true, filter); } @@ -1764,6 +1764,31 @@ namespace Umbraco.Core.Services return true; } + /// + /// Gets paged content descendants as XML by path + /// + /// Path starts with + /// Page number + /// Page size + /// Total records the query would return without paging + /// A paged enumerable of XML entries of content items + public IEnumerable GetPagedXmlEntries(string path, long pageIndex, int pageSize, out long totalRecords) + { + Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); + Mandate.ParameterCondition(pageSize > 0, "pageSize"); + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentRepository(uow)) + { + var contents = repository.GetPagedXmlEntriesByPath(path, pageIndex, pageSize, + //This order by is VERY important! This allows us to figure out what is implicitly not published, see ContentRepository.BuildXmlCache and + // UmbracoContentIndexer.PerformIndexAll which uses the logic based on this sort order + new[] {"level", "parentID", "sortOrder"}, + out totalRecords); + return contents; + } + } + /// /// This builds the Xml document used for the XML cache /// @@ -2192,6 +2217,11 @@ namespace Umbraco.Core.Services } } + if (string.IsNullOrWhiteSpace(content.Name)) + { + throw new ArgumentException("Cannot save content with empty name."); + } + using (new WriteLock(Locker)) { var uow = UowProvider.GetUnitOfWork(); diff --git a/src/Umbraco.Core/Services/ContentTypeService.cs b/src/Umbraco.Core/Services/ContentTypeService.cs index 31e86cd128..9f625c27b8 100644 --- a/src/Umbraco.Core/Services/ContentTypeService.cs +++ b/src/Umbraco.Core/Services/ContentTypeService.cs @@ -719,8 +719,13 @@ namespace Umbraco.Core.Services /// Optional id of the user saving the ContentType public void Save(IContentType contentType, int userId = 0) { - if (SavingContentType.IsRaisedEventCancelled(new SaveEventArgs(contentType), this)) - return; + if (SavingContentType.IsRaisedEventCancelled(new SaveEventArgs(contentType), this)) + return; + + if (string.IsNullOrWhiteSpace(contentType.Name)) + { + throw new ArgumentException("Cannot save content type with empty name."); + } using (new WriteLock(Locker)) { diff --git a/src/Umbraco.Core/Services/DataTypeService.cs b/src/Umbraco.Core/Services/DataTypeService.cs index 2cdcfc76d5..b3fca10798 100644 --- a/src/Umbraco.Core/Services/DataTypeService.cs +++ b/src/Umbraco.Core/Services/DataTypeService.cs @@ -361,8 +361,13 @@ namespace Umbraco.Core.Services /// Id of the user issueing the save public void Save(IDataTypeDefinition dataTypeDefinition, int userId = 0) { - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(dataTypeDefinition), this)) - return; + if (Saving.IsRaisedEventCancelled(new SaveEventArgs(dataTypeDefinition), this)) + return; + + if (string.IsNullOrWhiteSpace(dataTypeDefinition.Name)) + { + throw new ArgumentException("Cannot save datatype with empty name."); + } var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateDataTypeDefinitionRepository(uow)) diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index ec5db8c4fb..ff515169d9 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Xml; +using System.Xml.Linq; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence.DatabaseModelDefinitions; @@ -95,6 +96,19 @@ namespace Umbraco.Core.Services /// public interface IContentService : IService { + /// + /// Gets all XML entries found in the cmsContentXml table based on the given path + /// + /// Path starts with + /// Page number + /// Page size + /// Total records the query would return without paging + /// A paged enumerable of XML entries of content items + /// + /// If -1 is passed, then this will return all content xml entries, otherwise will return all descendents from the path + /// + IEnumerable GetPagedXmlEntries(string path, long pageIndex, int pageSize, out long totalRecords); + /// /// This builds the Xml document used for the XML cache /// @@ -245,7 +259,7 @@ namespace Umbraco.Core.Services [Obsolete("Use the overload with 'long' parameter types instead")] [EditorBrowsable(EditorBrowsableState.Never)] IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalRecords, - string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = ""); + string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = ""); /// /// Gets a collection of objects by Parent Id @@ -259,7 +273,7 @@ namespace Umbraco.Core.Services /// Search text filter /// An Enumerable list of objects IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = ""); + string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = ""); /// /// Gets a collection of objects by Parent Id diff --git a/src/Umbraco.Core/Services/IMediaService.cs b/src/Umbraco.Core/Services/IMediaService.cs index 510a1e26ca..375981853e 100644 --- a/src/Umbraco.Core/Services/IMediaService.cs +++ b/src/Umbraco.Core/Services/IMediaService.cs @@ -170,7 +170,7 @@ namespace Umbraco.Core.Services [Obsolete("Use the overload with 'long' parameter types instead")] [EditorBrowsable(EditorBrowsableState.Never)] IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalRecords, - string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = ""); + string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = ""); /// /// Gets a collection of objects by Parent Id @@ -184,7 +184,7 @@ namespace Umbraco.Core.Services /// Search text filter /// An Enumerable list of objects IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalRecords, - string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = ""); + string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = ""); /// /// Gets a collection of objects by Parent Id diff --git a/src/Umbraco.Core/Services/IMemberService.cs b/src/Umbraco.Core/Services/IMemberService.cs index a893d89feb..38f53f2d68 100644 --- a/src/Umbraco.Core/Services/IMemberService.cs +++ b/src/Umbraco.Core/Services/IMemberService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Xml.Linq; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence.DatabaseModelDefinitions; @@ -13,6 +14,15 @@ namespace Umbraco.Core.Services /// public interface IMemberService : IMembershipMemberService { + /// + /// Gets all XML entries found in the cmsContentXml table + /// + /// Page number + /// Page size + /// Total records the query would return without paging + /// A paged enumerable of XML entries of content items + IEnumerable GetPagedXmlEntries(long pageIndex, int pageSize, out long totalRecords); + /// /// Rebuilds all xml content in the cmsContentXml table for all documents /// diff --git a/src/Umbraco.Core/Services/MacroService.cs b/src/Umbraco.Core/Services/MacroService.cs index a6de12ed32..76515a71df 100644 --- a/src/Umbraco.Core/Services/MacroService.cs +++ b/src/Umbraco.Core/Services/MacroService.cs @@ -161,19 +161,24 @@ namespace Umbraco.Core.Services /// Optional Id of the user deleting the macro public void Save(IMacro macro, int userId = 0) { - if (Saving.IsRaisedEventCancelled(new SaveEventArgs(macro), this)) - return; - - var uow = UowProvider.GetUnitOfWork(); - using (var repository = RepositoryFactory.CreateMacroRepository(uow)) - { - repository.AddOrUpdate(macro); - uow.Commit(); + if (Saving.IsRaisedEventCancelled(new SaveEventArgs(macro), this)) + return; - Saved.RaiseEvent(new SaveEventArgs(macro, false), this); - } + if (string.IsNullOrWhiteSpace(macro.Name)) + { + throw new ArgumentException("Cannot save macro with empty name."); + } - Audit(AuditType.Save, "Save Macro performed by user", userId, -1); + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMacroRepository(uow)) + { + repository.AddOrUpdate(macro); + uow.Commit(); + + Saved.RaiseEvent(new SaveEventArgs(macro, false), this); + } + + Audit(AuditType.Save, "Save Macro performed by user", userId, -1); } ///// diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index 7c583dfe28..63d3388103 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -451,7 +451,7 @@ namespace Umbraco.Core.Services [Obsolete("Use the overload with 'long' parameter types instead")] [EditorBrowsable(EditorBrowsableState.Never)] - public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") + public IEnumerable GetPagedDescendants(int id, int pageIndex, int pageSize, out int totalChildren, string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = "") { long total; var result = GetPagedDescendants(id, Convert.ToInt64(pageIndex), pageSize, out total, orderBy, orderDirection, true, filter); @@ -470,7 +470,7 @@ namespace Umbraco.Core.Services /// Direction to order by /// Search text filter /// An Enumerable list of objects - public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "Path", Direction orderDirection = Direction.Ascending, string filter = "") + public IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, out long totalChildren, string orderBy = "path", Direction orderDirection = Direction.Ascending, string filter = "") { return GetPagedDescendants(id, pageIndex, pageSize, out totalChildren, orderBy, orderDirection, true, filter); } @@ -837,7 +837,11 @@ namespace Umbraco.Core.Services { return OperationStatus.Cancelled(evtMsgs); } + } + if (string.IsNullOrWhiteSpace(media.Name)) + { + throw new ArgumentException("Cannot save media with empty name."); } var uow = UowProvider.GetUnitOfWork(); @@ -1224,7 +1228,7 @@ namespace Umbraco.Core.Services var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateMediaRepository(uow)) { - var contents = repository.GetPagedXmlEntriesByPath(path, pageIndex, pageSize, out totalRecords); + var contents = repository.GetPagedXmlEntriesByPath(path, pageIndex, pageSize, null, out totalRecords); return contents; } } diff --git a/src/Umbraco.Core/Services/MemberService.cs b/src/Umbraco.Core/Services/MemberService.cs index 094539d66e..70d45926fc 100644 --- a/src/Umbraco.Core/Services/MemberService.cs +++ b/src/Umbraco.Core/Services/MemberService.cs @@ -612,6 +612,26 @@ namespace Umbraco.Core.Services Audit(AuditType.Publish, "MemberService.RebuildXmlStructures completed, the xml has been regenerated in the database", 0, -1); } + /// + /// Gets paged member descendants as XML by path + /// + /// Page number + /// Page size + /// Total records the query would return without paging + /// A paged enumerable of XML entries of member items + public IEnumerable GetPagedXmlEntries(long pageIndex, int pageSize, out long totalRecords) + { + Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); + Mandate.ParameterCondition(pageSize > 0, "pageSize"); + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMemberRepository(uow)) + { + var contents = repository.GetPagedXmlEntriesByPath("-1", pageIndex, pageSize, null, out totalRecords); + return contents; + } + } + #endregion #region IMembershipMemberService Implementation @@ -978,6 +998,11 @@ namespace Umbraco.Core.Services } } + if (string.IsNullOrWhiteSpace(entity.Name)) + { + throw new ArgumentException("Cannot save member with empty name."); + } + var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateMemberRepository(uow)) { diff --git a/src/Umbraco.Core/Services/MemberTypeService.cs b/src/Umbraco.Core/Services/MemberTypeService.cs index c6bc9dd24a..164bbc8243 100644 --- a/src/Umbraco.Core/Services/MemberTypeService.cs +++ b/src/Umbraco.Core/Services/MemberTypeService.cs @@ -78,6 +78,11 @@ namespace Umbraco.Core.Services if (Saving.IsRaisedEventCancelled(new SaveEventArgs(memberType), this)) return; + if (string.IsNullOrWhiteSpace(memberType.Name)) + { + throw new ArgumentException("Cannot save MemberType with empty name."); + } + using (new WriteLock(Locker)) { var uow = UowProvider.GetUnitOfWork(); diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 8e984d1e5d..e36e454987 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -125,6 +125,11 @@ namespace Umbraco.Core.Services { if (userType == null) throw new ArgumentNullException("userType"); + if (string.IsNullOrWhiteSpace(username)) + { + throw new ArgumentException("Cannot create user with empty username."); + } + //TODO: PUT lock here!! var uow = UowProvider.GetUnitOfWork(); @@ -312,6 +317,15 @@ namespace Umbraco.Core.Services return; } + if (string.IsNullOrWhiteSpace(entity.Username)) + { + throw new ArgumentException("Cannot save user with empty username."); + } + if (string.IsNullOrWhiteSpace(entity.Name)) + { + throw new ArgumentException("Cannot save user with empty name."); + } + var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateUserRepository(uow)) { @@ -353,6 +367,14 @@ namespace Umbraco.Core.Services { foreach (var member in entities) { + if (string.IsNullOrWhiteSpace(member.Username)) + { + throw new ArgumentException("Cannot save user with empty username."); + } + if (string.IsNullOrWhiteSpace(member.Name)) + { + throw new ArgumentException("Cannot save user with empty name."); + } repository.AddOrUpdate(member); } //commit the whole lot in one go diff --git a/src/Umbraco.Core/StringUdi.cs b/src/Umbraco.Core/StringUdi.cs new file mode 100644 index 0000000000..2fcb53f263 --- /dev/null +++ b/src/Umbraco.Core/StringUdi.cs @@ -0,0 +1,71 @@ +using System; + +namespace Umbraco.Core +{ + /// + /// Represents a string-based entity identifier. + /// + public class StringUdi : Udi + { + /// + /// The string part of the identifier. + /// + public string Id { get; private set; } + + /// + /// Initializes a new instance of the StringUdi class with an entity type and a string id. + /// + /// The entity type part of the udi. + /// The string id part of the udi. + public StringUdi(string entityType, string id) + : base(entityType, "umb://" + entityType + "/" + id) + { + Id = id; + } + + /// + /// Initializes a new instance of the StringUdi class with a uri value. + /// + /// The uri value of the udi. + public StringUdi(Uri uriValue) + : base(uriValue) + { + Id = uriValue.AbsolutePath.TrimStart('/'); + } + + /// + /// Converts the string representation of an entity identifier into the equivalent StringUdi instance. + /// + /// The string to convert. + /// A StringUdi instance that contains the value that was parsed. + public new static StringUdi Parse(string s) + { + var udi = Udi.Parse(s); + if (!(udi is StringUdi)) + throw new FormatException("String \"" + s + "\" is not a string entity id."); + return (StringUdi)udi; + } + + public static bool TryParse(string s, out StringUdi udi) + { + udi = null; + Udi tmp; + if (!TryParse(s, out tmp) || !(tmp is StringUdi)) return false; + udi = (StringUdi)tmp; + return true; + } + + /// + public override bool IsRoot + { + get { return Id == string.Empty; } + } + + /// + public StringUdi EnsureClosed() + { + base.EnsureNotRoot(); + return this; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Udi.cs b/src/Umbraco.Core/Udi.cs new file mode 100644 index 0000000000..a7298c6f89 --- /dev/null +++ b/src/Umbraco.Core/Udi.cs @@ -0,0 +1,267 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Deploy; + +namespace Umbraco.Core +{ + /// + /// Represents an entity identifier. + /// + /// An Udi can be fully qualified or "closed" eg umb://document/{guid} or "open" eg umb://document. + public abstract class Udi : IComparable + { + private static readonly Dictionary UdiTypes = new Dictionary(); + private static readonly ConcurrentDictionary RootUdis = new ConcurrentDictionary(); + internal readonly Uri UriValue; // internal for UdiRange + + /// + /// Initializes a new instance of the Udi class. + /// + /// The entity type part of the identifier. + /// The string value of the identifier. + protected Udi(string entityType, string stringValue) + { + EntityType = entityType; + UriValue = new Uri(stringValue); + } + + /// + /// Initializes a new instance of the Udi class. + /// + /// The uri value of the identifier. + protected Udi(Uri uriValue) + { + EntityType = uriValue.Host; + UriValue = uriValue; + } + + 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) + { + var attrs = connector.GetCustomAttributes(false); + foreach (var attr in attrs) + { + 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; + } + } + } + + /// + /// Gets the entity type part of the identifier. + /// + public string EntityType { get; private set; } + + public int CompareTo(Udi other) + { + return string.Compare(UriValue.ToString(), other.UriValue.ToString(), StringComparison.InvariantCultureIgnoreCase); + } + + public override string ToString() + { + // UriValue is created in the ctor and is never null + return UriValue.ToString(); + } + + /// + /// Converts the string representation of an entity identifier into the equivalent Udi instance. + /// + /// The string to convert. + /// An Udi instance that contains the value that was parsed. + public static Udi Parse(string s) + { + Udi udi; + ParseInternal(s, false, out udi); + return udi; + } + + public static bool TryParse(string s, out Udi udi) + { + return ParseInternal(s, true, out udi); + } + + private static bool ParseInternal(string s, bool tryParse, out Udi udi) + { + udi = null; + Uri uri; + + if (!Uri.IsWellFormedUriString(s, UriKind.Absolute) + || !Uri.TryCreate(s, UriKind.Absolute, out uri)) + { + if (tryParse) return false; + throw new FormatException(string.Format("String \"{0}\" is not a valid udi.", s)); + } + + var entityType = uri.Host; + UdiType udiType; + if (!UdiTypes.TryGetValue(entityType, out udiType)) + { + if (tryParse) return false; + throw new FormatException(string.Format("Unknown entity type \"{0}\".", entityType)); + } + var path = uri.AbsolutePath.TrimStart('/'); + if (udiType == UdiType.GuidUdi) + { + if (path == string.Empty) + { + udi = GetRootUdi(uri.Host); + return true; + } + Guid guid; + if (!Guid.TryParse(path, out guid)) + { + if (tryParse) return false; + throw new FormatException(string.Format("String \"{0}\" is not a valid udi.", s)); + } + udi = new GuidUdi(uri.Host, guid); + return true; + } + if (udiType == UdiType.StringUdi) + { + udi = path == string.Empty ? GetRootUdi(uri.Host) : new StringUdi(uri.Host, path); + return true; + } + if (tryParse) return false; + throw new InvalidOperationException("Internal error."); + } + + private static Udi GetRootUdi(string entityType) + { + return RootUdis.GetOrAdd(entityType, x => + { + UdiType udiType; + if (!UdiTypes.TryGetValue(x, out udiType)) + throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", entityType)); + return udiType == UdiType.StringUdi + ? (Udi)new StringUdi(entityType, string.Empty) + : new GuidUdi(entityType, Guid.Empty); + }); + } + + /// + /// Creates a root Udi for an entity type. + /// + /// The entity type. + /// The root Udi for the entity type. + public static Udi Create(string entityType) + { + return GetRootUdi(entityType); + } + + /// + /// Creates a string Udi. + /// + /// The entity type. + /// The identifier. + /// The string Udi for the entity type and identifier. + public static Udi Create(string entityType, string id) + { + UdiType udiType; + if (!UdiTypes.TryGetValue(entityType, out udiType)) + 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"); + if (udiType != UdiType.StringUdi) + throw new InvalidOperationException(string.Format("Entity type \"{0}\" does not have string udis.", entityType)); + + return new StringUdi(entityType, id); + } + + /// + /// Creates a Guid Udi. + /// + /// The entity type. + /// The identifier. + /// The Guid Udi for the entity type and identifier. + public static Udi Create(string entityType, Guid id) + { + UdiType udiType; + if (!UdiTypes.TryGetValue(entityType, out udiType)) + 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)); + if (id == default(Guid)) + throw new ArgumentException("Cannot be an empty guid.", "id"); + return new GuidUdi(entityType, id); + } + + internal static Udi Create(Uri uri) + { + UdiType udiType; + if (!UdiTypes.TryGetValue(uri.Host, out udiType)) + throw new ArgumentException(string.Format("Unknown entity type \"{0}\".", uri.Host), "uri"); + if (udiType == UdiType.GuidUdi) + return new GuidUdi(uri); + if (udiType == UdiType.GuidUdi) + return new StringUdi(uri); + throw new ArgumentException(string.Format("Uri \"{0}\" is not a valid udi.", uri)); + } + + public void EnsureType(params string[] validTypes) + { + if (!validTypes.Contains(EntityType)) + throw new Exception(string.Format("Unexpected entity type \"{0}\".", EntityType)); + } + + /// + /// Gets a value indicating whether this Udi is a root Udi. + /// + /// A root Udi points to the "root of all things" for a given entity type, e.g. the content tree root. + public abstract bool IsRoot { get; } + + /// + /// Ensures that this Udi is not a root Udi. + /// + /// This Udi. + /// When this Udi is a Root Udi. + public Udi EnsureNotRoot() + { + if (IsRoot) throw new Exception("Root Udi."); + return this; + } + + public override bool Equals(object obj) + { + var other = obj as Udi; + return other != null && GetType() == other.GetType() && UriValue == other.UriValue; + } + + public override int GetHashCode() + { + return UriValue.GetHashCode(); + } + + public static bool operator ==(Udi udi1, Udi udi2) + { + if (ReferenceEquals(udi1, udi2)) return true; + if ((object)udi1 == null || (object)udi2 == null) return false; + return udi1.Equals(udi2); + } + + public static bool operator !=(Udi udi1, Udi udi2) + { + return !(udi1 == udi2); + } + } + +} diff --git a/src/Umbraco.Core/UdiDefinitionAttribute.cs b/src/Umbraco.Core/UdiDefinitionAttribute.cs new file mode 100644 index 0000000000..cc52bb6039 --- /dev/null +++ b/src/Umbraco.Core/UdiDefinitionAttribute.cs @@ -0,0 +1,20 @@ +using System; + +namespace Umbraco.Core +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] + public sealed class UdiDefinitionAttribute : Attribute + { + public UdiDefinitionAttribute(string entityType, UdiType udiType) + { + if (string.IsNullOrWhiteSpace(entityType)) throw new ArgumentNullException("entityType"); + if (udiType != UdiType.GuidUdi && udiType != UdiType.StringUdi) throw new ArgumentException("Invalid value.", "udiType"); + EntityType = entityType; + UdiType = udiType; + } + + public string EntityType { get; private set; } + + public UdiType UdiType { get; private set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/UdiGetterExtensions.cs b/src/Umbraco.Core/UdiGetterExtensions.cs new file mode 100644 index 0000000000..1bd018a754 --- /dev/null +++ b/src/Umbraco.Core/UdiGetterExtensions.cs @@ -0,0 +1,303 @@ +using System; +using Umbraco.Core.Models; +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core +{ + /// + /// Provides extension methods that return udis for Umbraco entities. + /// + public static class UdiGetterExtensions + { + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this ITemplate entity) + { + if (entity == null) throw new ArgumentNullException("entity"); + return new GuidUdi(Constants.DeployEntityType.Template, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IContentType entity) + { + if (entity == null) throw new ArgumentNullException("entity"); + return new GuidUdi(Constants.DeployEntityType.DocumentType, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IMediaType entity) + { + if (entity == null) throw new ArgumentNullException("entity"); + return new GuidUdi(Constants.DeployEntityType.MediaType, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IMemberType entity) + { + if (entity == null) throw new ArgumentNullException("entity"); + return new GuidUdi(Constants.DeployEntityType.MemberType, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IMemberGroup entity) + { + if (entity == null) throw new ArgumentNullException("entity"); + return new GuidUdi(Constants.DeployEntityType.MemberGroup, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IContentTypeComposition entity) + { + 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; + else throw new NotSupportedException(string.Format("Composition type {0} is not supported.", entity.GetType().FullName)); + return new GuidUdi(type, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IDataTypeDefinition entity) + { + if (entity == null) throw new ArgumentNullException("entity"); + return new GuidUdi(Constants.DeployEntityType.DataType, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this Umbraco.Core.Models.EntityContainer entity) + { + if (entity == null) throw new ArgumentNullException("entity"); + + string entityType; + if (entity.ContainedObjectType == Constants.ObjectTypes.DataTypeGuid) + entityType = Constants.DeployEntityType.DataTypeContainer; + else if (entity.ContainedObjectType == Constants.ObjectTypes.DocumentTypeGuid) + entityType = Constants.DeployEntityType.DocumentTypeContainer; + else if (entity.ContainedObjectType == Constants.ObjectTypes.MediaTypeGuid) + entityType = Constants.DeployEntityType.MediaTypeContainer; + else + throw new NotSupportedException(string.Format("Contained object type {0} is not supported.", entity.ContainedObjectType)); + return new GuidUdi(entityType, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IMedia entity) + { + if (entity == null) throw new ArgumentNullException("entity"); + return new GuidUdi(Constants.DeployEntityType.Media, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IContent entity) + { + if (entity == null) throw new ArgumentNullException("entity"); + return new GuidUdi(Constants.DeployEntityType.Document, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IMember entity) + { + if (entity == null) throw new ArgumentNullException("entity"); + return new GuidUdi(Constants.DeployEntityType.Member, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static StringUdi GetUdi(this Stylesheet entity) + { + if (entity == null) throw new ArgumentNullException("entity"); + return new StringUdi(Constants.DeployEntityType.Stylesheet, entity.Path.TrimStart('/')).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static StringUdi GetUdi(this Script entity) + { + if (entity == null) throw new ArgumentNullException("entity"); + return new StringUdi(Constants.DeployEntityType.Script, entity.Path.TrimStart('/')).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IDictionaryItem entity) + { + if (entity == null) throw new ArgumentNullException("entity"); + return new GuidUdi(Constants.DeployEntityType.DictionaryItem, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IMacro entity) + { + if (entity == null) throw new ArgumentNullException("entity"); + return new GuidUdi(Constants.DeployEntityType.Macro, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static StringUdi GetUdi(this IPartialView entity) + { + if (entity == null) throw new ArgumentNullException("entity"); + return new StringUdi(Constants.DeployEntityType.PartialView, entity.Path.TrimStart('/')).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static StringUdi GetUdi(this IXsltFile entity) + { + if (entity == null) throw new ArgumentNullException("entity"); + return new StringUdi(Constants.DeployEntityType.Xslt, entity.Path.TrimStart('/')).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IContentBase entity) + { + 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; + else throw new NotSupportedException(string.Format("ContentBase type {0} is not supported.", entity.GetType().FullName)); + return new GuidUdi(type, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static GuidUdi GetUdi(this IRelationType entity) + { + if (entity == null) throw new ArgumentNullException("entity"); + return new GuidUdi(Constants.DeployEntityType.RelationType, entity.Key).EnsureClosed(); + } + + /// + /// Gets the entity identifier of the entity. + /// + /// The entity. + /// The entity identifier of the entity. + public static Udi GetUdi(this IEntity entity) + { + if (entity == null) throw new ArgumentNullException("entity"); + + // entity could eg be anything implementing IThing + // so we have to go through casts here + + var template = entity as ITemplate; + if (template != null) return template.GetUdi(); + + var contentType = entity as IContentType; + if (contentType != null) return contentType.GetUdi(); + + var mediaType = entity as IMediaType; + if (mediaType != null) return mediaType.GetUdi(); + + var memberType = entity as IMemberType; + if (memberType != null) return memberType.GetUdi(); + + var memberGroup = entity as IMemberGroup; + if (memberGroup != null) return memberGroup.GetUdi(); + + var contentTypeComposition = entity as IContentTypeComposition; + if (contentTypeComposition != null) return contentTypeComposition.GetUdi(); + + var dataTypeComposition = entity as IDataTypeDefinition; + if (dataTypeComposition != null) return dataTypeComposition.GetUdi(); + + var container = entity as Umbraco.Core.Models.EntityContainer; + if (container != null) return container.GetUdi(); + + var media = entity as IMedia; + if (media != null) return media.GetUdi(); + + var content = entity as IContent; + if (content != null) return content.GetUdi(); + + var member = entity as IMember; + if (member != null) return member.GetUdi(); + + var contentBase = entity as IContentBase; + if (contentBase != null) return contentBase.GetUdi(); + + var macro = entity as IMacro; + if (macro != null) return macro.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)); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/UdiRange.cs b/src/Umbraco.Core/UdiRange.cs new file mode 100644 index 0000000000..a708220066 --- /dev/null +++ b/src/Umbraco.Core/UdiRange.cs @@ -0,0 +1,104 @@ +using System; + +namespace Umbraco.Core +{ + /// + /// Represents a range. + /// + /// + /// A Udi range is composed of a which represents the base of the range, + /// plus a selector that can be "." (the Udi), ".*" (the Udi and its children), ".**" (the udi and + /// its descendants, "*" (the children of the Udi), and "**" (the descendants of the Udi). + /// The Udi here can be a closed entity, or an open entity. + public class UdiRange + { + private readonly Uri _uriValue; + + /// + /// Initializes a new instance of the class with a and an optional selector. + /// + /// A . + /// An optional selector. + public UdiRange(Udi udi, string selector = Constants.DeploySelector.This) + { + Udi = udi; + switch (selector) + { + case Constants.DeploySelector.This: + Selector = selector; + _uriValue = udi.UriValue; + break; + case Constants.DeploySelector.ChildrenOfThis: + case Constants.DeploySelector.DescendantsOfThis: + case Constants.DeploySelector.ThisAndChildren: + case Constants.DeploySelector.ThisAndDescendants: + Selector = selector; + _uriValue = new Uri(Udi + "?" + selector); + break; + default: + throw new ArgumentException(string.Format("Invalid selector \"{0}\".", selector)); + } + } + + /// + /// Gets the for this range. + /// + public Udi Udi { get; private set; } + + /// + /// Gets or sets the selector for this range. + /// + public string Selector { get; private set; } + + /// + /// Gets the entity type of the for this range. + /// + public string EntityType + { + get { return Udi.EntityType; } + } + + public static UdiRange Parse(string s) + { + Uri uri; + + if (!Uri.IsWellFormedUriString(s, UriKind.Absolute) + || !Uri.TryCreate(s, UriKind.Absolute, out uri)) + { + //if (tryParse) return false; + throw new FormatException(string.Format("String \"{0}\" is not a valid udi range.", s)); + } + + var udiUri = uri.Query == string.Empty ? uri : new UriBuilder(uri) { Query = string.Empty }.Uri; + return new UdiRange(Udi.Create(udiUri), uri.Query.TrimStart('?')); + } + + public override string ToString() + { + return _uriValue.ToString(); + } + + public override bool Equals(object obj) + { + var other = obj as UdiRange; + return other != null && GetType() == other.GetType() && _uriValue == other._uriValue; + } + + public override int GetHashCode() + { + return _uriValue.GetHashCode(); + } + + public static bool operator ==(UdiRange range1, UdiRange range2) + { + if (ReferenceEquals(range1, range2)) return true; + if ((object)range1 == null || (object)range2 == null) return false; + return range1.Equals(range2); + } + + public static bool operator !=(UdiRange range1, UdiRange range2) + { + return !(range1 == range2); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/UdiType.cs b/src/Umbraco.Core/UdiType.cs new file mode 100644 index 0000000000..d15987033d --- /dev/null +++ b/src/Umbraco.Core/UdiType.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Core +{ + /// + /// Defines Udi types. + /// + public enum UdiType + { + Unknown, + GuidUdi, + StringUdi + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 950a44502f..3a53f273f4 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -290,15 +290,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -340,6 +367,7 @@ True Files.resx + @@ -410,6 +438,7 @@ + @@ -482,6 +511,7 @@ + @@ -527,6 +557,9 @@ + + + @@ -1326,6 +1359,7 @@ + @@ -1370,6 +1404,11 @@ + + + + + diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs index 84cdef73e1..699e563407 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -172,6 +172,38 @@ namespace Umbraco.Tests.Persistence.Repositories } } + [Test] + public void Can_Get_All_Containers() + { + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + EntityContainer container1, container2, container3; + using (var containerRepository = CreateContainerRepository(unitOfWork, Constants.ObjectTypes.DocumentTypeContainerGuid)) + { + container1 = new EntityContainer(Constants.ObjectTypes.DocumentTypeGuid) { Name = "container1" }; + containerRepository.AddOrUpdate(container1); + container2 = new EntityContainer(Constants.ObjectTypes.DocumentTypeGuid) { Name = "container2" }; + containerRepository.AddOrUpdate(container2); + container3 = new EntityContainer(Constants.ObjectTypes.DocumentTypeGuid) { Name = "container3" }; + containerRepository.AddOrUpdate(container3); + unitOfWork.Commit(); + Assert.That(container1.Id, Is.GreaterThan(0)); + Assert.That(container2.Id, Is.GreaterThan(0)); + Assert.That(container3.Id, Is.GreaterThan(0)); + } + using (var containerRepository = CreateContainerRepository(unitOfWork, Constants.ObjectTypes.DocumentTypeContainerGuid)) + { + var found1 = containerRepository.Get(container1.Id); + Assert.IsNotNull(found1); + var found2 = containerRepository.Get(container2.Id); + Assert.IsNotNull(found2); + var found3 = containerRepository.Get(container3.Id); + Assert.IsNotNull(found3); + var allContainers = containerRepository.GetAll(); + Assert.AreEqual(3, allContainers.Count()); + } + } + [Test] public void Can_Delete_Container() { diff --git a/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs b/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs index c8372e70ac..062ab164b0 100644 --- a/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs +++ b/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs @@ -367,5 +367,17 @@ namespace Umbraco.Tests.PropertyEditors var urlString = mediaPath.GetCropUrl(imageCropperValue: cropperJson, height: 200); Assert.AreEqual(mediaPath + "?anchor=center&mode=crop&height=200", urlString); } + + /// + /// Test to check result when using a background color with padding + /// + [Test] + public void GetCropUrl_BackgroundColorParameter() + { + var cropperJson = "{\"focalPoint\": {\"left\": 0.5,\"top\": 0.5},\"src\": \"" + mediaPath + "\",\"crops\": [{\"alias\": \"home\",\"width\": 270,\"height\": 161}]}"; + + var urlString = mediaPath.GetCropUrl(400, 400, cropperJson, imageCropMode: ImageCropMode.Pad, backgroundColor: "fff"); + Assert.AreEqual(mediaPath + "?mode=pad&width=400&height=400&bgcolor=fff", urlString); + } } } \ No newline at end of file diff --git a/src/Umbraco.Tests/PublishedContent/LegacyExamineBackedMediaTests.cs b/src/Umbraco.Tests/PublishedContent/LegacyExamineBackedMediaTests.cs index 5bf6a4edc5..f48887498e 100644 --- a/src/Umbraco.Tests/PublishedContent/LegacyExamineBackedMediaTests.cs +++ b/src/Umbraco.Tests/PublishedContent/LegacyExamineBackedMediaTests.cs @@ -1,6 +1,8 @@ using System; using System.Linq; +using Lucene.Net.Analysis.Standard; using Lucene.Net.Documents; +using Lucene.Net.Index; using Lucene.Net.Store; using Moq; using NUnit.Framework; @@ -31,12 +33,15 @@ namespace Umbraco.Tests.PublishedContent [Test] public void Ensure_Children_Are_Sorted() { - using (var luceneDir = new RAMDirectory()) + using (var luceneDir = new RandomIdRAMDirectory()) + using (var writer = new IndexWriter(luceneDir, new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_29), IndexWriter.MaxFieldLength.LIMITED)) + using (var indexer = IndexInitializer.GetUmbracoIndexer(writer)) + using (var searcher = IndexInitializer.GetUmbracoSearcher(writer)) { - var indexer = IndexInitializer.GetUmbracoIndexer(luceneDir); + //var indexer = IndexInitializer.GetUmbracoIndexer(luceneDir); indexer.RebuildIndex(); - var searcher = IndexInitializer.GetUmbracoSearcher(luceneDir); + //var searcher = IndexInitializer.GetUmbracoSearcher(luceneDir); var result = searcher.Search(searcher.CreateSearchCriteria().Id(1111).Compile()); Assert.IsNotNull(result); Assert.AreEqual(1, result.TotalItemCount); @@ -60,12 +65,15 @@ namespace Umbraco.Tests.PublishedContent [Test] public void Ensure_Result_Has_All_Values() { - using (var luceneDir = new RAMDirectory()) + using (var luceneDir = new RandomIdRAMDirectory()) + using (var writer = new IndexWriter(luceneDir, new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_29), IndexWriter.MaxFieldLength.LIMITED)) + using (var indexer = IndexInitializer.GetUmbracoIndexer(writer)) + using (var searcher = IndexInitializer.GetUmbracoSearcher(writer)) { - var indexer = IndexInitializer.GetUmbracoIndexer(luceneDir); + //var indexer = IndexInitializer.GetUmbracoIndexer(luceneDir); indexer.RebuildIndex(); - var searcher = IndexInitializer.GetUmbracoSearcher(luceneDir); + //var searcher = IndexInitializer.GetUmbracoSearcher(luceneDir); var result = searcher.Search(searcher.CreateSearchCriteria().Id(1111).Compile()); Assert.IsNotNull(result); Assert.AreEqual(1, result.TotalItemCount); diff --git a/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs index 6ec347cc0f..eadc08d834 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs @@ -27,6 +27,7 @@ using UmbracoExamine; using UmbracoExamine.DataServices; using umbraco.BusinessLogic; using System.Linq; +using Lucene.Net.Index; namespace Umbraco.Tests.PublishedContent { @@ -106,11 +107,14 @@ namespace Umbraco.Tests.PublishedContent [Test] public void Ensure_Children_Sorted_With_Examine() { - using (var luceneDir = new RAMDirectory()) + using (var luceneDir = new RandomIdRAMDirectory()) + using (var writer = new IndexWriter(luceneDir, new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_29), IndexWriter.MaxFieldLength.LIMITED)) + using (var indexer = IndexInitializer.GetUmbracoIndexer(writer)) + using (var searcher = IndexInitializer.GetUmbracoSearcher(writer)) { - var indexer = IndexInitializer.GetUmbracoIndexer(luceneDir); + //var indexer = IndexInitializer.GetUmbracoIndexer(luceneDir); indexer.RebuildIndex(); - var searcher = IndexInitializer.GetUmbracoSearcher(luceneDir); + //var searcher = IndexInitializer.GetUmbracoSearcher(luceneDir); var ctx = GetUmbracoContext("/test", 1234); var cache = new ContextualPublishedMediaCache(new PublishedMediaCache(ctx.Application, searcher, indexer), ctx); @@ -135,11 +139,14 @@ namespace Umbraco.Tests.PublishedContent [Test] public void Do_Not_Find_In_Recycle_Bin() { - using (var luceneDir = new RAMDirectory()) + using (var luceneDir = new RandomIdRAMDirectory()) + using (var writer = new IndexWriter(luceneDir, new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_29), IndexWriter.MaxFieldLength.LIMITED)) + using (var indexer = IndexInitializer.GetUmbracoIndexer(writer)) + using (var searcher = IndexInitializer.GetUmbracoSearcher(writer)) { - var indexer = IndexInitializer.GetUmbracoIndexer(luceneDir); + //var indexer = IndexInitializer.GetUmbracoIndexer(luceneDir); indexer.RebuildIndex(); - var searcher = IndexInitializer.GetUmbracoSearcher(luceneDir); + //var searcher = IndexInitializer.GetUmbracoSearcher(luceneDir); var ctx = GetUmbracoContext("/test", 1234); var cache = new ContextualPublishedMediaCache(new PublishedMediaCache(ctx.Application, searcher, indexer), ctx); @@ -175,11 +182,14 @@ namespace Umbraco.Tests.PublishedContent [Test] public void Children_With_Examine() { - using (var luceneDir = new RAMDirectory()) + using (var luceneDir = new RandomIdRAMDirectory()) + using (var writer = new IndexWriter(luceneDir, new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_29), IndexWriter.MaxFieldLength.LIMITED)) + using (var indexer = IndexInitializer.GetUmbracoIndexer(writer)) + using (var searcher = IndexInitializer.GetUmbracoSearcher(writer)) { - var indexer = IndexInitializer.GetUmbracoIndexer(luceneDir); + //var indexer = IndexInitializer.GetUmbracoIndexer(luceneDir); indexer.RebuildIndex(); - var searcher = IndexInitializer.GetUmbracoSearcher(luceneDir); + //var searcher = IndexInitializer.GetUmbracoSearcher(luceneDir); var ctx = GetUmbracoContext("/test", 1234); var cache = new ContextualPublishedMediaCache(new PublishedMediaCache(ctx.Application, searcher, indexer), ctx); @@ -197,11 +207,14 @@ namespace Umbraco.Tests.PublishedContent [Test] public void Descendants_With_Examine() { - using (var luceneDir = new RAMDirectory()) + using (var luceneDir = new RandomIdRAMDirectory()) + using (var writer = new IndexWriter(luceneDir, new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_29), IndexWriter.MaxFieldLength.LIMITED)) + using (var indexer = IndexInitializer.GetUmbracoIndexer(writer)) + using (var searcher = IndexInitializer.GetUmbracoSearcher(writer)) { - var indexer = IndexInitializer.GetUmbracoIndexer(luceneDir); + //var indexer = IndexInitializer.GetUmbracoIndexer(luceneDir); indexer.RebuildIndex(); - var searcher = IndexInitializer.GetUmbracoSearcher(luceneDir); + //var searcher = IndexInitializer.GetUmbracoSearcher(luceneDir); var ctx = GetUmbracoContext("/test", 1234); var cache = new ContextualPublishedMediaCache(new PublishedMediaCache(ctx.Application, searcher, indexer), ctx); @@ -219,11 +232,14 @@ namespace Umbraco.Tests.PublishedContent [Test] public void DescendantsOrSelf_With_Examine() { - using (var luceneDir = new RAMDirectory()) + using (var luceneDir = new RandomIdRAMDirectory()) + using (var writer = new IndexWriter(luceneDir, new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_29), IndexWriter.MaxFieldLength.LIMITED)) + using (var indexer = IndexInitializer.GetUmbracoIndexer(writer)) + using (var searcher = IndexInitializer.GetUmbracoSearcher(writer)) { - var indexer = IndexInitializer.GetUmbracoIndexer(luceneDir); + //var indexer = IndexInitializer.GetUmbracoIndexer(luceneDir); indexer.RebuildIndex(); - var searcher = IndexInitializer.GetUmbracoSearcher(luceneDir); + //var searcher = IndexInitializer.GetUmbracoSearcher(luceneDir); var ctx = GetUmbracoContext("/test", 1234); var cache = new ContextualPublishedMediaCache(new PublishedMediaCache(ctx.Application, searcher, indexer), ctx); @@ -241,12 +257,15 @@ namespace Umbraco.Tests.PublishedContent [Test] public void Ancestors_With_Examine() { - using (var luceneDir = new RAMDirectory()) + using (var luceneDir = new RandomIdRAMDirectory()) + using (var writer = new IndexWriter(luceneDir, new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_29), IndexWriter.MaxFieldLength.LIMITED)) + using (var indexer = IndexInitializer.GetUmbracoIndexer(writer)) + using (var searcher = IndexInitializer.GetUmbracoSearcher(writer)) { - var indexer = IndexInitializer.GetUmbracoIndexer(luceneDir); + //var indexer = IndexInitializer.GetUmbracoIndexer(luceneDir); indexer.RebuildIndex(); var ctx = GetUmbracoContext("/test", 1234); - var searcher = IndexInitializer.GetUmbracoSearcher(luceneDir); + //var searcher = IndexInitializer.GetUmbracoSearcher(luceneDir); var cache = new ContextualPublishedMediaCache(new PublishedMediaCache(ctx.Application, searcher, indexer), ctx); //we are using the media.xml media to test the examine results implementation, see the media.xml file in the ExamineHelpers namespace @@ -260,12 +279,15 @@ namespace Umbraco.Tests.PublishedContent [Test] public void AncestorsOrSelf_With_Examine() { - using (var luceneDir = new RAMDirectory()) + using (var luceneDir = new RandomIdRAMDirectory()) + using (var writer = new IndexWriter(luceneDir, new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_29), IndexWriter.MaxFieldLength.LIMITED)) + using (var indexer = IndexInitializer.GetUmbracoIndexer(writer)) + using (var searcher = IndexInitializer.GetUmbracoSearcher(writer)) { - var indexer = IndexInitializer.GetUmbracoIndexer(luceneDir); + //var indexer = IndexInitializer.GetUmbracoIndexer(luceneDir); indexer.RebuildIndex(); var ctx = GetUmbracoContext("/test", 1234); - var searcher = IndexInitializer.GetUmbracoSearcher(luceneDir); + //var searcher = IndexInitializer.GetUmbracoSearcher(luceneDir); var cache = new ContextualPublishedMediaCache(new PublishedMediaCache(ctx.Application, searcher, indexer), ctx); //we are using the media.xml media to test the examine results implementation, see the media.xml file in the ExamineHelpers namespace diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 72a58861e2..0078d3e83e 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -668,6 +668,17 @@ namespace Umbraco.Tests.Services Assert.Throws(() => contentService.CreateContent("Test", -1, "umbAliasDoesntExist")); } + [Test] + public void Cannot_Save_Content_With_Empty_Name() + { + // Arrange + var contentService = ServiceContext.ContentService; + var content = new Content(string.Empty, -1, ServiceContext.ContentTypeService.GetContentType("umbTextpage")); + + // Act & Assert + Assert.Throws(() => contentService.Save(content)); + } + [Test] public void Can_Get_Content_By_Id() { diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs index bc9ec8a9ad..78f4a5dad9 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs @@ -304,6 +304,42 @@ namespace Umbraco.Tests.Services Assert.IsNull(deletedContentType); } + [Test] + public void Can_Create_Container() + { + // Arrange + var cts = ServiceContext.ContentTypeService; + + // Act + var container = new EntityContainer(Constants.ObjectTypes.DocumentTypeGuid); + container.Name = "container1"; + cts.SaveContentTypeContainer(container); + + // Assert + var createdContainer = cts.GetContentTypeContainer(container.Id); + Assert.IsNotNull(createdContainer); + } + + [Test] + public void Can_Get_All_Containers() + { + // Arrange + var cts = ServiceContext.ContentTypeService; + + // Act + var container1 = new EntityContainer(Constants.ObjectTypes.DocumentTypeGuid); + container1.Name = "container1"; + cts.SaveContentTypeContainer(container1); + + var container2 = new EntityContainer(Constants.ObjectTypes.DocumentTypeGuid); + container2.Name = "container2"; + cts.SaveContentTypeContainer(container2); + + // Assert + var containers = cts.GetContentTypeContainers(new int[0]); + Assert.AreEqual(2, containers.Count()); + } + [Test] public void Deleting_ContentType_Sends_Correct_Number_Of_DeletedEntities_In_Events() { @@ -717,6 +753,16 @@ namespace Umbraco.Tests.Services Assert.DoesNotThrow(() => service.GetContentType("advancedPage")); } + [Test] + public void Cannot_Save_ContentType_With_Empty_Name() + { + // Arrange + var contentType = MockedContentTypes.CreateSimpleContentType("contentType", string.Empty); + + // Act & Assert + Assert.Throws(() => ServiceContext.ContentTypeService.Save(contentType)); + } + [Test] public void Cannot_Rename_PropertyType_Alias_On_Composition_Which_Would_Cause_Conflict_In_Other_Composition() { diff --git a/src/Umbraco.Tests/Services/DataTypeServiceTests.cs b/src/Umbraco.Tests/Services/DataTypeServiceTests.cs index 073d7af406..ed9fc9a7e7 100644 --- a/src/Umbraco.Tests/Services/DataTypeServiceTests.cs +++ b/src/Umbraco.Tests/Services/DataTypeServiceTests.cs @@ -221,5 +221,18 @@ namespace Umbraco.Tests.Services Assert.AreEqual("preVal1", preVals.PreValuesAsArray.First().Value); Assert.AreEqual("preVal2", preVals.PreValuesAsArray.Last().Value); } + + [Test] + public void Cannot_Save_DataType_With_Empty_Name() + { + // Arrange + var dataTypeService = ServiceContext.DataTypeService; + + // Act + var dataTypeDefinition = new DataTypeDefinition(-1, "Test.TestEditor") { Name = string.Empty, DatabaseType = DataTypeDatabaseType.Ntext }; + + // Act & Assert + Assert.Throws(() => dataTypeService.Save(dataTypeDefinition)); + } } } \ No newline at end of file diff --git a/src/Umbraco.Tests/Services/MacroServiceTests.cs b/src/Umbraco.Tests/Services/MacroServiceTests.cs index a12ba731a4..a4d6e3d5ef 100644 --- a/src/Umbraco.Tests/Services/MacroServiceTests.cs +++ b/src/Umbraco.Tests/Services/MacroServiceTests.cs @@ -251,6 +251,17 @@ namespace Umbraco.Tests.Services } + [Test] + public void Cannot_Save_Macro_With_Empty_Name() + { + // Arrange + var macroService = ServiceContext.MacroService; + var macro = new Macro("test", string.Empty, scriptPath: "~/Views/MacroPartials/Test.cshtml", cacheDuration: 1234); + + // Act & Assert + Assert.Throws(() => macroService.Save(macro)); + } + //[Test] //public void Can_Get_Many_By_Alias() //{ diff --git a/src/Umbraco.Tests/Services/MediaServiceTests.cs b/src/Umbraco.Tests/Services/MediaServiceTests.cs index a53257b26a..8193df911c 100644 --- a/src/Umbraco.Tests/Services/MediaServiceTests.cs +++ b/src/Umbraco.Tests/Services/MediaServiceTests.cs @@ -81,6 +81,19 @@ namespace Umbraco.Tests.Services Assert.That(mediaChild.Trashed, Is.False); } + [Test] + public void Cannot_Save_Media_With_Empty_Name() + { + // Arrange + var mediaService = ServiceContext.MediaService; + var mediaType = MockedContentTypes.CreateVideoMediaType(); + ServiceContext.ContentTypeService.Save(mediaType); + var media = mediaService.CreateMedia(string.Empty, -1, "video"); + + // Act & Assert + Assert.Throws(() => mediaService.Save(media)); + } + [Test] public void Ensure_Content_Xml_Created() { diff --git a/src/Umbraco.Tests/Services/MemberServiceTests.cs b/src/Umbraco.Tests/Services/MemberServiceTests.cs index c9cf4e6d5b..522ecc4a8c 100644 --- a/src/Umbraco.Tests/Services/MemberServiceTests.cs +++ b/src/Umbraco.Tests/Services/MemberServiceTests.cs @@ -185,6 +185,18 @@ namespace Umbraco.Tests.Services Assert.AreEqual(2, membersInRole.Count()); } + [Test] + public void Cannot_Save_Member_With_Empty_Name() + { + IMemberType memberType = MockedContentTypes.CreateSimpleMemberType(); + ServiceContext.MemberTypeService.Save(memberType); + IMember member = MockedMember.CreateSimpleMember(memberType, string.Empty, "test@test.com", "pass", "test"); + + // Act & Assert + Assert.Throws(() => ServiceContext.MemberService.Save(member)); + + } + [TestCase("MyTestRole1", "test1", StringPropertyMatchType.StartsWith, 1)] [TestCase("MyTestRole1", "test", StringPropertyMatchType.StartsWith, 3)] [TestCase("MyTestRole1", "test1", StringPropertyMatchType.Exact, 1)] diff --git a/src/Umbraco.Tests/Services/MemberTypeServiceTests.cs b/src/Umbraco.Tests/Services/MemberTypeServiceTests.cs index c81d8c3f4e..8dd18b8b50 100644 --- a/src/Umbraco.Tests/Services/MemberTypeServiceTests.cs +++ b/src/Umbraco.Tests/Services/MemberTypeServiceTests.cs @@ -173,6 +173,16 @@ namespace Umbraco.Tests.Services } } + [Test] + public void Cannot_Save_MemberType_With_Empty_Name() + { + // Arrange + IMemberType memberType = MockedContentTypes.CreateSimpleMemberType("memberTypeAlias", string.Empty); + + // Act & Assert + Assert.Throws(() => ServiceContext.MemberTypeService.Save(memberType)); + } + //[Test] //public void Can_Save_MemberType_Structure_And_Create_A_Member_Based_On_It() //{ diff --git a/src/Umbraco.Tests/Services/UserServiceTests.cs b/src/Umbraco.Tests/Services/UserServiceTests.cs index c7002ce79d..833ed1771b 100644 --- a/src/Umbraco.Tests/Services/UserServiceTests.cs +++ b/src/Umbraco.Tests/Services/UserServiceTests.cs @@ -490,6 +490,43 @@ namespace Umbraco.Tests.Services Assert.IsTrue(result4.AllowedSections.Contains("test")); } + [Test] + public void Cannot_Create_User_With_Empty_Username() + { + // Arrange + var userService = ServiceContext.UserService; + var userType = userService.GetUserTypeByAlias("admin"); + + // Act & Assert + Assert.Throws(() => userService.CreateUserWithIdentity(string.Empty, "john@umbraco.io", userType)); + } + + [Test] + public void Cannot_Save_User_With_Empty_Username() + { + // Arrange + var userService = ServiceContext.UserService; + var userType = userService.GetUserTypeByAlias("admin"); + var user = userService.CreateUserWithIdentity("John Doe", "john@umbraco.io", userType); + user.Username = string.Empty; + + // Act & Assert + Assert.Throws(() => userService.Save(user)); + } + + [Test] + public void Cannot_Save_User_With_Empty_Name() + { + // Arrange + var userService = ServiceContext.UserService; + var userType = userService.GetUserTypeByAlias("admin"); + var user = userService.CreateUserWithIdentity("John Doe", "john@umbraco.io", userType); + user.Name = string.Empty; + + // Act & Assert + Assert.Throws(() => userService.Save(user)); + } + [Test] public void Get_By_Profile_Username() { diff --git a/src/Umbraco.Tests/UdiTests.cs b/src/Umbraco.Tests/UdiTests.cs new file mode 100644 index 0000000000..36242bab13 --- /dev/null +++ b/src/Umbraco.Tests/UdiTests.cs @@ -0,0 +1,155 @@ +using System; +using System.Linq; +using Newtonsoft.Json; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Serialization; + +namespace Umbraco.Tests +{ + [TestFixture] + public class UdiTests + { + [Test] + public void StringEntityCtorTest() + { + var udi = new StringUdi(Constants.DeployEntityType.AnyString, "test-id"); + Assert.AreEqual(Constants.DeployEntityType.AnyString, udi.EntityType); + Assert.AreEqual("test-id", udi.Id); + Assert.AreEqual("umb://" + Constants.DeployEntityType.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); + 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()); + } + + [Test] + public void GuidEntityCtorTest() + { + var guid = Guid.NewGuid(); + var udi = new GuidUdi(Constants.DeployEntityType.AnyGuid, guid); + Assert.AreEqual(Constants.DeployEntityType.AnyGuid, udi.EntityType); + Assert.AreEqual(guid, udi.Guid); + Assert.AreEqual("umb://" + Constants.DeployEntityType.AnyGuid + "/" + guid.ToString("N"), udi.ToString()); + } + + [Test] + public void GuidEntityParseTest() + { + var guid = Guid.NewGuid(); + var s = "umb://" + Constants.DeployEntityType.AnyGuid + "/" + guid.ToString("N"); + var udi = Udi.Parse(s); + Assert.AreEqual(Constants.DeployEntityType.AnyGuid, udi.EntityType); + Assert.IsInstanceOf(udi); + var gudi = udi as GuidUdi; + Assert.IsNotNull(gudi); + Assert.AreEqual(guid, gudi.Guid); + Assert.AreEqual(s, udi.ToString()); + } + + [Test] + public void EqualityTest() + { + var guid1 = Guid.NewGuid(); + var guid2 = Guid.NewGuid(); + + Assert.IsTrue(new GuidUdi("type", guid1).Equals(new GuidUdi("type", guid1))); + Assert.IsTrue(new GuidUdi("type", guid1) == new GuidUdi("type", guid1)); + + Assert.IsTrue(((Udi)new GuidUdi("type", guid1)).Equals((Udi)new GuidUdi("type", guid1))); + Assert.IsTrue((Udi)new GuidUdi("type", guid1) == (Udi)new GuidUdi("type", guid1)); + + Assert.IsFalse(new GuidUdi("type", guid1).Equals(new GuidUdi("typex", guid1))); + Assert.IsFalse(new GuidUdi("type", guid1) == new GuidUdi("typex", guid1)); + Assert.IsFalse(new GuidUdi("type", guid1).Equals(new GuidUdi("type", guid2))); + Assert.IsFalse(new GuidUdi("type", guid1) == new GuidUdi("type", guid2)); + + Assert.IsTrue(new GuidUdi("type", guid1).ToString() == new StringUdi("type", guid1.ToString("N")).ToString()); + Assert.IsFalse(new GuidUdi("type", guid1) == new StringUdi("type", guid1.ToString("N"))); + } + + [Test] + public void DistinctTest() + { + 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), + }; + Assert.AreEqual(1, entities.Distinct().Count()); + } + + [Test] + public void CreateTest() + { + var guid = Guid.NewGuid(); + var udi = Udi.Create(Constants.DeployEntityType.AnyGuid, guid); + Assert.AreEqual(Constants.DeployEntityType.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("barf", "foo")); + } + + [Test] + public void RangeTest() + { + // can parse open string udi + var stringUdiString = "umb://" + Constants.DeployEntityType.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; + Udi guidUdi; + Assert.IsTrue(Udi.TryParse(guidUdiString, out guidUdi)); + Assert.AreEqual(Guid.Empty, ((GuidUdi)guidUdi).Guid); + + // can create a range + var range = new UdiRange(stringUdi, Constants.DeploySelector.ChildrenOfThis); + + // cannot create invalid ranges + Assert.Throws(() => new UdiRange(guidUdi, "x")); + } + + [Test] + public void SerializationTest() + { + var settings = new JsonSerializerSettings + { + Converters = new JsonConverter[] { new UdiJsonConverter(), new UdiRangeJsonConverter() } + }; + + + var guid = Guid.NewGuid(); + var udi = new GuidUdi(Constants.DeployEntityType.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(guid, ((GuidUdi)dudi).Guid); + + var range = new UdiRange(udi, Constants.DeploySelector.ChildrenOfThis); + json = JsonConvert.SerializeObject(range, settings); + Assert.AreEqual(string.Format("\"umb://any-guid/{0:N}?children\"", guid), json); + + var drange = JsonConvert.DeserializeObject(json, settings); + Assert.AreEqual(udi, drange.Udi); + Assert.AreEqual(string.Format("umb://any-guid/{0:N}", guid), drange.Udi.UriValue.ToString()); + Assert.AreEqual(Constants.DeploySelector.ChildrenOfThis, drange.Selector); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 1f5e324142..9bb433f59b 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -58,8 +58,9 @@ ..\packages\AutoMapper.3.3.1\lib\net40\AutoMapper.Net4.dll - - ..\packages\Examine.0.1.70.0\lib\Examine.dll + + ..\packages\Examine.0.1.80\lib\net45\Examine.dll + True ..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll @@ -170,6 +171,9 @@ + + + diff --git a/src/Umbraco.Tests/UmbracoExamine/EventsTest.cs b/src/Umbraco.Tests/UmbracoExamine/EventsTest.cs index 3e7377f3b6..34c7a1f6ac 100644 --- a/src/Umbraco.Tests/UmbracoExamine/EventsTest.cs +++ b/src/Umbraco.Tests/UmbracoExamine/EventsTest.cs @@ -1,6 +1,8 @@ using System; using System.Linq; using Examine; +using Lucene.Net.Analysis.Standard; +using Lucene.Net.Index; using Lucene.Net.Store; using NUnit.Framework; using Umbraco.Tests.TestHelpers; @@ -15,53 +17,39 @@ namespace Umbraco.Tests.UmbracoExamine [Test] public void Events_Ignoring_Node() { - //change the parent id so that they are all ignored - var existingCriteria = _indexer.IndexerData; - _indexer.IndexerData = new IndexCriteria(existingCriteria.StandardFields, existingCriteria.UserFields, existingCriteria.IncludeNodeTypes, existingCriteria.ExcludeNodeTypes, - 999); //change to 999 + using (var luceneDir = new RandomIdRAMDirectory()) + using (var writer = new IndexWriter(luceneDir, new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_29), IndexWriter.MaxFieldLength.LIMITED)) + using (var indexer = IndexInitializer.GetUmbracoIndexer(writer)) + using (var searcher = IndexInitializer.GetUmbracoSearcher(writer)) + { + //change the parent id so that they are all ignored + var existingCriteria = indexer.IndexerData; + indexer.IndexerData = new IndexCriteria(existingCriteria.StandardFields, existingCriteria.UserFields, existingCriteria.IncludeNodeTypes, existingCriteria.ExcludeNodeTypes, + 999); //change to 999 - var isIgnored = false; + var isIgnored = false; - EventHandler ignoringNode = (s, e) => - { - isIgnored = true; - }; + EventHandler ignoringNode = (s, e) => + { + isIgnored = true; + }; - _indexer.IgnoringNode += ignoringNode; + indexer.IgnoringNode += ignoringNode; - //get a node from the data repo - var node = _contentService.GetPublishedContentByXPath("//*[string-length(@id)>0 and number(@id)>0]") - .Root - .Elements() - .First(); + //get a node from the data repo + var node = _contentService.GetPublishedContentByXPath("//*[string-length(@id)>0 and number(@id)>0]") + .Root + .Elements() + .First(); - _indexer.ReIndexNode(node, IndexTypes.Content); + indexer.ReIndexNode(node, IndexTypes.Content); - Assert.IsTrue(isIgnored); - + Assert.IsTrue(isIgnored); + } } private readonly TestContentService _contentService = new TestContentService(); - private static UmbracoExamineSearcher _searcher; - private static UmbracoContentIndexer _indexer; - private Lucene.Net.Store.Directory _luceneDir; - - public override void Initialize() - { - base.Initialize(); - - _luceneDir = new RAMDirectory(); - _indexer = IndexInitializer.GetUmbracoIndexer(_luceneDir); - _indexer.RebuildIndex(); - _searcher = IndexInitializer.GetUmbracoSearcher(_luceneDir); - } - - public override void TearDown() - { - base.TearDown(); - _luceneDir.Dispose(); - } } } \ No newline at end of file diff --git a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs index 00e94ced63..c7922b9d58 100644 --- a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs +++ b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs @@ -7,6 +7,7 @@ using Examine.LuceneEngine.Config; using Examine.LuceneEngine.Providers; using Lucene.Net.Analysis; using Lucene.Net.Analysis.Standard; +using Lucene.Net.Index; using Lucene.Net.Store; using Moq; using Umbraco.Core; @@ -33,7 +34,7 @@ namespace Umbraco.Tests.UmbracoExamine internal static class IndexInitializer { public static UmbracoContentIndexer GetUmbracoIndexer( - Directory luceneDir, + IndexWriter writer, Analyzer analyzer = null, IDataService dataService = null, IContentService contentService = null, @@ -41,7 +42,8 @@ namespace Umbraco.Tests.UmbracoExamine IDataTypeService dataTypeService = null, IMemberService memberService = null, IUserService userService = null, - IContentTypeService contentTypeService = null) + IContentTypeService contentTypeService = null, + bool supportUnpublishedContent = false) { if (dataService == null) { @@ -177,15 +179,17 @@ namespace Umbraco.Tests.UmbracoExamine var indexCriteria = indexSet.ToIndexCriteria(dataService, UmbracoContentIndexer.IndexFieldPolicies); var i = new UmbracoContentIndexer(indexCriteria, - luceneDir, //custom lucene directory - dataService, - contentService, - mediaService, - dataTypeService, - userService, - contentTypeService, - analyzer, - false); + writer, + dataService, + contentService, + mediaService, + dataTypeService, + userService, + contentTypeService, + false) + { + SupportUnpublishedContent = supportUnpublishedContent + }; //i.IndexSecondsInterval = 1; @@ -193,13 +197,14 @@ namespace Umbraco.Tests.UmbracoExamine return i; } - public static UmbracoExamineSearcher GetUmbracoSearcher(Directory luceneDir, Analyzer analyzer = null) + + public static UmbracoExamineSearcher GetUmbracoSearcher(IndexWriter writer, Analyzer analyzer = null) { if (analyzer == null) { analyzer = new StandardAnalyzer(Version.LUCENE_29); } - return new UmbracoExamineSearcher(luceneDir, analyzer); + return new UmbracoExamineSearcher(writer, analyzer); } public static LuceneSearcher GetLuceneSearcher(Directory luceneDir) diff --git a/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs b/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs index 5d6812f94a..3f324bb2ed 100644 --- a/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs +++ b/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs @@ -1,11 +1,11 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Examine; using Examine.LuceneEngine; using Examine.LuceneEngine.Providers; using Examine.LuceneEngine.SearchCriteria; using Examine.SearchCriteria; +using Lucene.Net.Analysis.Standard; using Lucene.Net.Index; using Lucene.Net.Search; using Lucene.Net.Store; @@ -15,218 +15,230 @@ using UmbracoExamine; namespace Umbraco.Tests.UmbracoExamine { - /// /// Tests the standard indexing capabilities /// [DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerTest)] [TestFixture, RequiresSTA] public class IndexTest : ExamineBaseTest - { + { + /// + /// Check that the node signalled as protected in the content service is not present in the index. + /// + [Test] + public void Index_Protected_Content_Not_Indexed() + { - ///// - /// - /// Check that the node signalled as protected in the content service is not present in the index. - /// - [Test] - public void Index_Protected_Content_Not_Indexed() - { + using (var luceneDir = new RandomIdRAMDirectory()) + using (var writer = new IndexWriter(luceneDir, new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_29), IndexWriter.MaxFieldLength.LIMITED)) + using (var indexer = IndexInitializer.GetUmbracoIndexer(writer)) + using (var searcher = IndexInitializer.GetUmbracoSearcher(writer)) + { + indexer.RebuildIndex(); - var protectedQuery = new BooleanQuery(); - protectedQuery.Add( - new BooleanClause( - new TermQuery(new Term(LuceneIndexer.IndexTypeFieldName, IndexTypes.Content)), - BooleanClause.Occur.MUST)); + var protectedQuery = new BooleanQuery(); + protectedQuery.Add( + new BooleanClause( + new TermQuery(new Term(LuceneIndexer.IndexTypeFieldName, IndexTypes.Content)), + BooleanClause.Occur.MUST)); - protectedQuery.Add( - new BooleanClause( - new TermQuery(new Term(LuceneIndexer.IndexNodeIdFieldName, TestContentService.ProtectedNode.ToString())), - BooleanClause.Occur.MUST)); + protectedQuery.Add( + new BooleanClause( + new TermQuery(new Term(LuceneIndexer.IndexNodeIdFieldName, TestContentService.ProtectedNode.ToString())), + BooleanClause.Occur.MUST)); - var collector = new AllHitsCollector(false, true); - var s = _searcher.GetSearcher(); - s.Search(protectedQuery, collector); + var collector = new AllHitsCollector(false, true); + var s = searcher.GetSearcher(); + s.Search(protectedQuery, collector); - Assert.AreEqual(0, collector.Count, "Protected node should not be indexed"); + Assert.AreEqual(0, collector.Count, "Protected node should not be indexed"); + } - } + } - [Test] - public void Index_Move_Media_From_Non_Indexable_To_Indexable_ParentID() - { - //change parent id to 1116 - var existingCriteria = _indexer.IndexerData; - _indexer.IndexerData = new IndexCriteria(existingCriteria.StandardFields, existingCriteria.UserFields, existingCriteria.IncludeNodeTypes, existingCriteria.ExcludeNodeTypes, - 1116); - - //rebuild so it excludes children unless they are under 1116 - _indexer.RebuildIndex(); + [Test] + public void Index_Move_Media_From_Non_Indexable_To_Indexable_ParentID() + { + using (var luceneDir = new RandomIdRAMDirectory()) + using (var writer = new IndexWriter(luceneDir, new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_29), IndexWriter.MaxFieldLength.LIMITED)) + using (var indexer = IndexInitializer.GetUmbracoIndexer(writer)) + using (var searcher = IndexInitializer.GetUmbracoSearcher(writer)) + { + indexer.RebuildIndex(); - //ensure that node 2112 doesn't exist - var results = _searcher.Search(_searcher.CreateSearchCriteria().Id(2112).Compile()); - Assert.AreEqual(0, results.Count()); + var mediaService = new TestMediaService(); - //get a node from the data repo (this one exists underneath 2222) - var node = _mediaService.GetLatestMediaByXpath("//*[string-length(@id)>0 and number(@id)>0]") - .Root - .Elements() - .Where(x => (int)x.Attribute("id") == 2112) - .First(); + //change parent id to 1116 + var existingCriteria = indexer.IndexerData; + indexer.IndexerData = new IndexCriteria(existingCriteria.StandardFields, existingCriteria.UserFields, existingCriteria.IncludeNodeTypes, existingCriteria.ExcludeNodeTypes, + 1116); - var currPath = (string)node.Attribute("path"); //should be : -1,1111,2222,2112 - Assert.AreEqual("-1,1111,2222,2112", currPath); + //rebuild so it excludes children unless they are under 1116 + indexer.RebuildIndex(); - //now mimic moving 2112 to 1116 - //node.SetAttributeValue("path", currPath.Replace("2222", "1116")); - node.SetAttributeValue("path", "-1,1116,2112"); - node.SetAttributeValue("parentID", "1116"); + //ensure that node 2112 doesn't exist + var results = searcher.Search(searcher.CreateSearchCriteria().Id(2112).Compile()); + Assert.AreEqual(0, results.TotalItemCount); - //now reindex the node, this should first delete it and then WILL add it because of the parent id constraint - _indexer.ReIndexNode(node, IndexTypes.Media); + //get a node from the data repo (this one exists underneath 2222) + var node = mediaService.GetLatestMediaByXpath("//*[string-length(@id)>0 and number(@id)>0]") + .Root + .Elements() + .First(x => (int)x.Attribute("id") == 2112); - //RESET the parent id - existingCriteria = ((IndexCriteria)_indexer.IndexerData); - _indexer.IndexerData = new IndexCriteria(existingCriteria.StandardFields, existingCriteria.UserFields, existingCriteria.IncludeNodeTypes, existingCriteria.ExcludeNodeTypes, - null); + var currPath = (string)node.Attribute("path"); //should be : -1,1111,2222,2112 + Assert.AreEqual("-1,1111,2222,2112", currPath); - //now ensure it's deleted - var newResults = _searcher.Search(_searcher.CreateSearchCriteria().Id(2112).Compile()); - Assert.AreEqual(1, newResults.Count()); - } + //now mimic moving 2112 to 1116 + //node.SetAttributeValue("path", currPath.Replace("2222", "1116")); + node.SetAttributeValue("path", "-1,1116,2112"); + node.SetAttributeValue("parentID", "1116"); - [Test] - [Ignore] - public void Index_Move_Media_To_Non_Indexable_ParentID() - { - //get a node from the data repo (this one exists underneath 2222) - var node = _mediaService.GetLatestMediaByXpath("//*[string-length(@id)>0 and number(@id)>0]") - .Root - .Elements() - .Where(x => (int)x.Attribute("id") == 2112) - .First(); + //now reindex the node, this should first delete it and then WILL add it because of the parent id constraint + indexer.ReIndexNode(node, IndexTypes.Media); - var currPath = (string)node.Attribute("path"); //should be : -1,1111,2222,2112 - Assert.AreEqual("-1,1111,2222,2112", currPath); + //RESET the parent id + existingCriteria = ((IndexCriteria)indexer.IndexerData); + indexer.IndexerData = new IndexCriteria(existingCriteria.StandardFields, existingCriteria.UserFields, existingCriteria.IncludeNodeTypes, existingCriteria.ExcludeNodeTypes, + null); - //ensure it's indexed - _indexer.ReIndexNode(node, IndexTypes.Media); + //now ensure it's deleted + var newResults = searcher.Search(searcher.CreateSearchCriteria().Id(2112).Compile()); + Assert.AreEqual(1, newResults.TotalItemCount); + } - //change the parent node id to be the one it used to exist under - var existingCriteria = _indexer.IndexerData; - _indexer.IndexerData = new IndexCriteria(existingCriteria.StandardFields, existingCriteria.UserFields, existingCriteria.IncludeNodeTypes, existingCriteria.ExcludeNodeTypes, - 2222); + + } - //now mimic moving the node underneath 1116 instead of 2222 - node.SetAttributeValue("path", currPath.Replace("2222", "1116")); - node.SetAttributeValue("parentID", "1116"); + [Test] + public void Index_Move_Media_To_Non_Indexable_ParentID() + { + using (var luceneDir = new RandomIdRAMDirectory()) + using (var writer = new IndexWriter(luceneDir, new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_29), IndexWriter.MaxFieldLength.LIMITED)) + using (var indexer = IndexInitializer.GetUmbracoIndexer(writer)) + using (var searcher = IndexInitializer.GetUmbracoSearcher(writer)) + { + indexer.RebuildIndex(); - //now reindex the node, this should first delete it and then NOT add it because of the parent id constraint - _indexer.ReIndexNode(node, IndexTypes.Media); + var mediaService = new TestMediaService(); - //RESET the parent id - existingCriteria = ((IndexCriteria)_indexer.IndexerData); - _indexer.IndexerData = new IndexCriteria(existingCriteria.StandardFields, existingCriteria.UserFields, existingCriteria.IncludeNodeTypes, existingCriteria.ExcludeNodeTypes, - null); + //get a node from the data repo (this one exists underneath 2222) + var node = mediaService.GetLatestMediaByXpath("//*[string-length(@id)>0 and number(@id)>0]") + .Root + .Elements() + .First(x => (int)x.Attribute("id") == 2112); - //now ensure it's deleted - var results = _searcher.Search(_searcher.CreateSearchCriteria().Id(2112).Compile()); - Assert.AreEqual(0, results.Count()); + var currPath = (string)node.Attribute("path"); //should be : -1,1111,2222,2112 + Assert.AreEqual("-1,1111,2222,2112", currPath); - } + //ensure it's indexed + indexer.ReIndexNode(node, IndexTypes.Media); + + //change the parent node id to be the one it used to exist under + var existingCriteria = indexer.IndexerData; + indexer.IndexerData = new IndexCriteria(existingCriteria.StandardFields, existingCriteria.UserFields, existingCriteria.IncludeNodeTypes, existingCriteria.ExcludeNodeTypes, + 2222); + + //now mimic moving the node underneath 1116 instead of 2222 + node.SetAttributeValue("path", currPath.Replace("2222", "1116")); + node.SetAttributeValue("parentID", "1116"); + + //now reindex the node, this should first delete it and then NOT add it because of the parent id constraint + indexer.ReIndexNode(node, IndexTypes.Media); + + //RESET the parent id + existingCriteria = ((IndexCriteria)indexer.IndexerData); + indexer.IndexerData = new IndexCriteria(existingCriteria.StandardFields, existingCriteria.UserFields, existingCriteria.IncludeNodeTypes, existingCriteria.ExcludeNodeTypes, + null); + + //now ensure it's deleted + var results = searcher.Search(searcher.CreateSearchCriteria().Id(2112).Compile()); + Assert.AreEqual(0, results.TotalItemCount); + } + } - /// - /// This will ensure that all 'Content' (not media) is cleared from the index using the Lucene API directly. - /// We then call the Examine method to re-index Content and do some comparisons to ensure that it worked correctly. - /// - [Test] - public void Index_Reindex_Content() - { - var s = (IndexSearcher)_searcher.GetSearcher(); + /// + /// This will ensure that all 'Content' (not media) is cleared from the index using the Lucene API directly. + /// We then call the Examine method to re-index Content and do some comparisons to ensure that it worked correctly. + /// + [Test] + public void Index_Reindex_Content() + { + using (var luceneDir = new RandomIdRAMDirectory()) + using (var writer = new IndexWriter(luceneDir, new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_29), IndexWriter.MaxFieldLength.LIMITED)) + using (var indexer = IndexInitializer.GetUmbracoIndexer(writer, supportUnpublishedContent: true)) + using (var searcher = IndexInitializer.GetUmbracoSearcher(writer)) + { + indexer.RebuildIndex(); - //first delete all 'Content' (not media). This is done by directly manipulating the index with the Lucene API, not examine! - - var contentTerm = new Term(LuceneIndexer.IndexTypeFieldName, IndexTypes.Content); - var writer = _indexer.GetIndexWriter(); - writer.DeleteDocuments(contentTerm); - writer.Commit(); - + var s = (IndexSearcher)searcher.GetSearcher(); - //make sure the content is gone. This is done with lucene APIs, not examine! - var collector = new AllHitsCollector(false, true); - var query = new TermQuery(contentTerm); - s = (IndexSearcher)_searcher.GetSearcher(); //make sure the searcher is up do date. - s.Search(query, collector); - Assert.AreEqual(0, collector.Count); + //first delete all 'Content' (not media). This is done by directly manipulating the index with the Lucene API, not examine! - //call our indexing methods - _indexer.IndexAll(IndexTypes.Content); + var contentTerm = new Term(LuceneIndexer.IndexTypeFieldName, IndexTypes.Content); + writer.DeleteDocuments(contentTerm); + writer.Commit(); - collector = new AllHitsCollector(false, true); - s = (IndexSearcher)_searcher.GetSearcher(); //make sure the searcher is up do date. - s.Search(query, collector); - //var ids = new List(); - //for (var i = 0; i < collector.Count;i++) - //{ - // ids.Add(s.Doc(collector.GetDocId(i)).GetValues("__NodeId")[0]); - //} - Assert.AreEqual(20, collector.Count); - } + //make sure the content is gone. This is done with lucene APIs, not examine! + var collector = new AllHitsCollector(false, true); + var query = new TermQuery(contentTerm); + s = (IndexSearcher)searcher.GetSearcher(); //make sure the searcher is up do date. + s.Search(query, collector); + Assert.AreEqual(0, collector.Count); - /// - /// This will delete an item from the index and ensure that all children of the node are deleted too! - /// - [Test] - [Ignore] - public void Index_Delete_Index_Item_Ensure_Heirarchy_Removed() - { + //call our indexing methods + indexer.IndexAll(IndexTypes.Content); - //now delete a node that has children + collector = new AllHitsCollector(false, true); + s = (IndexSearcher)searcher.GetSearcher(); //make sure the searcher is up do date. + s.Search(query, collector); + //var ids = new List(); + //for (var i = 0; i < collector.Count;i++) + //{ + // ids.Add(s.Doc(collector.GetDocId(i)).GetValues("__NodeId")[0]); + //} + Assert.AreEqual(21, collector.Count); + } + } - _indexer.DeleteFromIndex(1140.ToString()); - //this node had children: 1141 & 1142, let's ensure they are also removed + /// + /// This will delete an item from the index and ensure that all children of the node are deleted too! + /// + [Test] + public void Index_Delete_Index_Item_Ensure_Heirarchy_Removed() + { - var results = _searcher.Search(_searcher.CreateSearchCriteria().Id(1141).Compile()); - Assert.AreEqual(0, results.Count()); + using (var luceneDir = new RandomIdRAMDirectory()) + using (var writer = new IndexWriter(luceneDir, new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_29), IndexWriter.MaxFieldLength.LIMITED)) + using (var indexer = IndexInitializer.GetUmbracoIndexer(writer)) + using (var searcher = IndexInitializer.GetUmbracoSearcher(writer)) + { + indexer.RebuildIndex(); - results = _searcher.Search(_searcher.CreateSearchCriteria().Id(1142).Compile()); - Assert.AreEqual(0, results.Count()); + //now delete a node that has children - } + indexer.DeleteFromIndex(1140.ToString()); + //this node had children: 1141 & 1142, let's ensure they are also removed - #region Private methods and properties + var results = searcher.Search(searcher.CreateSearchCriteria().Id(1141).Compile()); + Assert.AreEqual(0, results.TotalItemCount); - private readonly TestContentService _contentService = new TestContentService(); - private readonly TestMediaService _mediaService = new TestMediaService(); - - private static UmbracoExamineSearcher _searcher; - private static UmbracoContentIndexer _indexer; - - #endregion - - #region Initialize and Cleanup - - private Lucene.Net.Store.Directory _luceneDir; - - public override void TearDown() - { - base.TearDown(); - _luceneDir.Dispose(); + results = searcher.Search(searcher.CreateSearchCriteria().Id(1142).Compile()); + Assert.AreEqual(0, results.TotalItemCount); + } + } + + #region Initialize and Cleanup + + public override void TearDown() + { + base.TearDown(); + UmbracoExamineSearcher.DisableInitializationCheck = null; BaseUmbracoIndexer.DisableInitializationCheck = null; } - - public override void Initialize() - { - base.Initialize(); - _luceneDir = new RAMDirectory(); - _indexer = IndexInitializer.GetUmbracoIndexer(_luceneDir); - _indexer.RebuildIndex(); - _searcher = IndexInitializer.GetUmbracoSearcher(_luceneDir); - } - - - #endregion - } + #endregion + } } \ No newline at end of file diff --git a/src/Umbraco.Tests/UmbracoExamine/RandomIdRAMDirectory.cs b/src/Umbraco.Tests/UmbracoExamine/RandomIdRAMDirectory.cs new file mode 100644 index 0000000000..34b69274c8 --- /dev/null +++ b/src/Umbraco.Tests/UmbracoExamine/RandomIdRAMDirectory.cs @@ -0,0 +1,14 @@ +using System; +using Lucene.Net.Store; + +namespace Umbraco.Tests.UmbracoExamine +{ + public class RandomIdRAMDirectory : RAMDirectory + { + private readonly string _lockId = Guid.NewGuid().ToString(); + public override string GetLockID() + { + return _lockId; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs b/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs index 9b8b3d50d7..f739286f98 100644 --- a/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs +++ b/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs @@ -8,6 +8,8 @@ using Examine.LuceneEngine.Providers; using Lucene.Net.Store; using NUnit.Framework; using Examine.LuceneEngine.SearchCriteria; +using Lucene.Net.Analysis.Standard; +using Lucene.Net.Index; using Umbraco.Tests.TestHelpers; namespace Umbraco.Tests.UmbracoExamine @@ -20,19 +22,17 @@ namespace Umbraco.Tests.UmbracoExamine [Test] public void Test_Sort_Order_Sorting() { - //var newIndexFolder = new DirectoryInfo(Path.Combine("App_Data\\SearchTests", Guid.NewGuid().ToString())); - //System.IO.Directory.CreateDirectory(newIndexFolder.FullName); - - using (var luceneDir = new RAMDirectory()) - //using (var luceneDir = new SimpleFSDirectory(newIndexFolder)) - { - var indexer = IndexInitializer.GetUmbracoIndexer(luceneDir, null, + using (var luceneDir = new RandomIdRAMDirectory()) + using (var writer = new IndexWriter(luceneDir, new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_29), IndexWriter.MaxFieldLength.LIMITED)) + using (var indexer = IndexInitializer.GetUmbracoIndexer(writer, null, new TestDataService() - { - ContentService = new TestContentService(TestFiles.umbraco_sort) - }); - indexer.RebuildIndex(); - var searcher = IndexInitializer.GetUmbracoSearcher(luceneDir); + { + ContentService = new TestContentService(TestFiles.umbraco_sort) + }, + supportUnpublishedContent: true)) + using (var searcher = IndexInitializer.GetUmbracoSearcher(writer)) + { + indexer.RebuildIndex(); var s = (LuceneSearcher)searcher; var luceneSearcher = s.GetSearcher(); @@ -69,25 +69,7 @@ namespace Umbraco.Tests.UmbracoExamine currentSort = sort; } return true; - } - - //[Test] - //public void Test_Index_Type_With_German_Analyzer() - //{ - // using (var luceneDir = new RAMDirectory()) - // { - // var indexer = IndexInitializer.GetUmbracoIndexer(luceneDir, - // new GermanAnalyzer()); - // indexer.RebuildIndex(); - // var searcher = IndexInitializer.GetUmbracoSearcher(luceneDir); - // } - //} - - //private readonly TestContentService _contentService = new TestContentService(); - //private readonly TestMediaService _mediaService = new TestMediaService(); - //private static UmbracoExamineSearcher _searcher; - //private static UmbracoContentIndexer _indexer; - //private Lucene.Net.Store.Directory _luceneDir; + } } } diff --git a/src/Umbraco.Tests/UmbracoExamine/UmbracoContentIndexerTests.cs b/src/Umbraco.Tests/UmbracoExamine/UmbracoContentIndexerTests.cs new file mode 100644 index 0000000000..3278130022 --- /dev/null +++ b/src/Umbraco.Tests/UmbracoExamine/UmbracoContentIndexerTests.cs @@ -0,0 +1,107 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; +using Moq; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Models; +using UmbracoExamine; + +namespace Umbraco.Tests.UmbracoExamine +{ + [TestFixture] + public class UmbracoContentIndexerTests : ExamineBaseTest + { + [Test] + public void Get_Serialized_Content_No_Published_Content() + { + var contentSet = new List + { + Mock.Of(c => c.Id == 1 && c.Path == "-1,1" && c.Published && c.Level == 1), + Mock.Of(c => c.Id == 2 && c.Path == "-1,2" && c.Published && c.Level == 1), + Mock.Of(c => c.Id == 3 && c.Path == "-1,3" && c.Published == false && c.Level == 1), // no + Mock.Of(c => c.Id == 4 && c.Path == "-1,4" && c.Published == false && c.Level == 1), // no + + Mock.Of(c => c.Id == 5 && c.Path == "-1,1,5" && c.Published && c.Level == 2), + Mock.Of(c => c.Id == 6 && c.Path == "-1,2,6" && c.Published == false && c.Level == 2), // no + Mock.Of(c => c.Id == 7 && c.Path == "-1,3,7" && c.Published && c.Level == 2), // no + Mock.Of(c => c.Id == 8 && c.Path == "-1,4,8" && c.Published && c.Level == 2), // no + Mock.Of(c => c.Id == 9 && c.Path == "-1,4,9" && c.Published && c.Level == 2), // no + + Mock.Of(c => c.Id == 10 && c.Path == "-1,1,5,10" && c.Published && c.Level == 3), + Mock.Of(c => c.Id == 15 && c.Path == "-1,1,5,15" && c.Published && c.Level == 3), + Mock.Of(c => c.Id == 11 && c.Path == "-1,2,6,11" && c.Published && c.Level == 3), // no + Mock.Of(c => c.Id == 16 && c.Path == "-1,2,6,16" && c.Published && c.Level == 3), // no + Mock.Of(c => c.Id == 12 && c.Path == "-1,3,7,12" && c.Published && c.Level == 3), // no + Mock.Of(c => c.Id == 17 && c.Path == "-1,3,7,17" && c.Published && c.Level == 3), // no + Mock.Of(c => c.Id == 13 && c.Path == "-1,4,8,13" && c.Published && c.Level == 3), // no + Mock.Of(c => c.Id == 18 && c.Path == "-1,4,8,18" && c.Published && c.Level == 3), // no + Mock.Of(c => c.Id == 14 && c.Path == "-1,4,9,14" && c.Published && c.Level == 3), // no + Mock.Of(c => c.Id == 19 && c.Path == "-1,4,9,19" && c.Published && c.Level == 3), // no + }; + + //ensure the rest of the required values are populted + foreach (var content in contentSet) + { + var mock = Mock.Get(content); + mock.Setup(x => x.ContentType).Returns(Mock.Of(type => type.Icon == "hello")); + } + + contentSet.Sort((a, b) => Comparer.Default.Compare(a.Level, b.Level)); + + var published = new HashSet(); + + var result = UmbracoContentIndexer.GetSerializedContent(false, content => new XElement("test"), contentSet, published) + .WhereNotNull() + .ToArray(); + + Assert.AreEqual(5, result.Length); + } + + [Test] + public void Get_Serialized_Content_With_Published_Content() + { + var contentSet = new List + { + Mock.Of(c => c.Id == 1 && c.Path == "-1,1" && c.Published && c.Level == 1), + Mock.Of(c => c.Id == 2 && c.Path == "-1,2" && c.Published && c.Level == 1), + Mock.Of(c => c.Id == 3 && c.Path == "-1,3" && c.Published == false && c.Level == 1), + Mock.Of(c => c.Id == 4 && c.Path == "-1,4" && c.Published == false && c.Level == 1), + + Mock.Of(c => c.Id == 5 && c.Path == "-1,1,5" && c.Published && c.Level == 2), + Mock.Of(c => c.Id == 6 && c.Path == "-1,2,6" && c.Published == false && c.Level == 2), + Mock.Of(c => c.Id == 7 && c.Path == "-1,3,7" && c.Published && c.Level == 2), + Mock.Of(c => c.Id == 8 && c.Path == "-1,4,8" && c.Published && c.Level == 2), + Mock.Of(c => c.Id == 9 && c.Path == "-1,4,9" && c.Published && c.Level == 2), + + Mock.Of(c => c.Id == 10 && c.Path == "-1,1,5,10" && c.Published && c.Level == 3), + Mock.Of(c => c.Id == 15 && c.Path == "-1,1,5,15" && c.Published && c.Level == 3), + Mock.Of(c => c.Id == 11 && c.Path == "-1,2,6,11" && c.Published && c.Level == 3), + Mock.Of(c => c.Id == 16 && c.Path == "-1,2,6,16" && c.Published && c.Level == 3), + Mock.Of(c => c.Id == 12 && c.Path == "-1,3,7,12" && c.Published && c.Level == 3), + Mock.Of(c => c.Id == 17 && c.Path == "-1,3,7,17" && c.Published && c.Level == 3), + Mock.Of(c => c.Id == 13 && c.Path == "-1,4,8,13" && c.Published && c.Level == 3), + Mock.Of(c => c.Id == 18 && c.Path == "-1,4,8,18" && c.Published && c.Level == 3), + Mock.Of(c => c.Id == 14 && c.Path == "-1,4,9,14" && c.Published && c.Level == 3), + Mock.Of(c => c.Id == 19 && c.Path == "-1,4,9,19" && c.Published && c.Level == 3), + }; + + //ensure the rest of the required values are populted + foreach (var content in contentSet) + { + var mock = Mock.Get(content); + mock.Setup(x => x.ContentType).Returns(Mock.Of(type => type.Icon == "hello")); + } + + contentSet.Sort((a, b) => Comparer.Default.Compare(a.Level, b.Level)); + + var published = new HashSet(); + + var result = UmbracoContentIndexer.GetSerializedContent(true, content => new XElement("test"), contentSet, published) + .WhereNotNull() + .ToArray(); + + Assert.AreEqual(19, result.Length); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/packages.config b/src/Umbraco.Tests/packages.config index 6428eebadf..b71ba7a170 100644 --- a/src/Umbraco.Tests/packages.config +++ b/src/Umbraco.Tests/packages.config @@ -2,7 +2,7 @@ - + diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtabsnav.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtabsnav.directive.js index 890acf3d6f..50c4775ef9 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtabsnav.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtabsnav.directive.js @@ -43,7 +43,8 @@ templateUrl: "views/components/tabs/umb-tabs-nav.html", scope: { model: "=", - tabdrop: "=" + tabdrop: "=", + idSuffix: "@" }, link: link }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblaunchminieditor.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblaunchminieditor.directive.js index 91212a82f8..11b934ce96 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblaunchminieditor.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblaunchminieditor.directive.js @@ -7,7 +7,7 @@ * Used on a button to launch a mini content editor editor dialog **/ angular.module("umbraco.directives") - .directive('umbLaunchMiniEditor', function (dialogService, editorState, fileManager, contentEditingHelper) { + .directive('umbLaunchMiniEditor', function (miniEditorHelper) { return { restrict: 'A', replace: false, @@ -16,69 +16,8 @@ angular.module("umbraco.directives") }, link: function(scope, element, attrs) { - var launched = false; - element.click(function() { - - if (launched === true) { - return; - } - - launched = true; - - //We need to store the current files selected in the file manager locally because the fileManager - // is a singleton and is shared globally. The mini dialog will also be referencing the fileManager - // and we don't want it to be sharing the same files as the main editor. So we'll store the current files locally here, - // clear them out and then launch the dialog. When the dialog closes, we'll reset the fileManager to it's previous state. - var currFiles = _.groupBy(fileManager.getFiles(), "alias"); - fileManager.clearFiles(); - - //We need to store the original editorState entity because it will need to change when the mini editor is loaded so that - // any property editors that are working with editorState get given the correct entity, otherwise strange things will - // start happening. - var currEditorState = editorState.getCurrent(); - - dialogService.open({ - template: "views/common/dialogs/content/edit.html", - id: scope.node.id, - closeOnSave: true, - tabFilter: ["Generic properties"], - callback: function (data) { - - //set the node name back - scope.node.name = data.name; - - //reset the fileManager to what it was - fileManager.clearFiles(); - _.each(currFiles, function (val, key) { - fileManager.setFiles(key, _.map(currFiles['upload'], function (i) { return i.file; })); - }); - - //reset the editor state - editorState.set(currEditorState); - - //Now we need to check if the content item that was edited was actually the same content item - // as the main content editor and if so, update all property data - if (data.id === currEditorState.id) { - var changed = contentEditingHelper.reBindChangedProperties(currEditorState, data); - } - - launched = false; - }, - closeCallback: function () { - //reset the fileManager to what it was - fileManager.clearFiles(); - _.each(currFiles, function (val, key) { - fileManager.setFiles(key, _.map(currFiles['upload'], function (i) { return i.file; })); - }); - - //reset the editor state - editorState.set(currEditorState); - - launched = false; - } - }); - + miniEditorHelper.launchMiniEditor(scope.node); }); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnodepreview.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnodepreview.directive.js new file mode 100644 index 0000000000..77f6f06a30 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnodepreview.directive.js @@ -0,0 +1,122 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbNodePreview +@restrict E +@scope + +@description +Added in Umbraco v. 7.6: Use this directive to render a node preview. + +

Markup example

+
+    
+ +
+ + +
+ +
+
+ +

Controller example

+
+    (function () {
+        "use strict";
+    
+        function Controller() {
+    
+            var vm = this;
+    
+            vm.allowRemove = true;
+            vm.allowOpen = true;
+            vm.sortable = true;
+    
+            vm.nodes = [
+                {
+                    "icon": "icon-document",
+                    "name": "My node 1",
+                    "published": true,
+                    "description": "A short description of my node"
+                },
+                {
+                    "icon": "icon-document",
+                    "name": "My node 2",
+                    "published": true,
+                    "description": "A short description of my node"
+                }
+            ];
+    
+            vm.remove = remove;
+            vm.open = open;
+    
+            function remove(index, nodes) {
+                alert("remove node");
+            }
+    
+            function open(node) {
+                alert("open node");
+            }
+    
+        }
+    
+        angular.module("umbraco").controller("My.NodePreviewController", Controller);
+    
+    })();
+
+ +@param {string} icon (binding): The node icon. +@param {string} name (binding): The node name. +@param {boolean} published (binding): The node pusblished state. +@param {string} description (binding): A short description. +@param {boolean} sortable (binding): Will add a move cursor on the node preview. Can used in combination with ui-sortable. +@param {boolean} allowRemove (binding): Show/Hide the remove button. +@param {boolean} allowOpen (binding): Show/Hide the open button. +@param {function} onRemove (expression): Callback function when the remove button is clicked. +@param {function} onOpen (expression): Callback function when the open button is clicked. +**/ + +(function () { + 'use strict'; + + function NodePreviewDirective() { + + function link(scope, el, attr, ctrl) { + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-node-preview.html', + scope: { + icon: "=?", + name: "=", + description: "=?", + published: "=?", + sortable: "=?", + allowOpen: "=?", + allowRemove: "=?", + onOpen: "&?", + onRemove: "&?" + }, + link: link + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbNodePreview', NodePreviewDirective); + +})(); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/entity.mocks.js b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/entity.mocks.js index fc60781375..8c03679183 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/entity.mocks.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/entity.mocks.js @@ -33,6 +33,15 @@ angular.module('umbraco.mocks'). return [200, nodes, null]; } + function returnEntityUrl() { + + if (!mocksUtils.checkAuth()) { + return [401, null, null]; + } + + return [200, "url", null]; + + } return { register: function () { @@ -48,6 +57,10 @@ angular.module('umbraco.mocks'). $httpBackend .whenGET(mocksUtils.urlRegex('/umbraco/UmbracoApi/Entity/GetById?')) .respond(returnEntitybyId); + + $httpBackend + .whenGET(mocksUtils.urlRegex('/umbraco/UmbracoApi/Entity/GetUrl?')) + .respond(returnEntityUrl); } }; }]); \ No newline at end of file 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 914b601249..5e0f5deada 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 @@ -56,7 +56,7 @@ function entityResource($q, $http, umbRequestHelper) { * * ##usage *
-         * entityResource.getPath(id)
+         * entityResource.getPath(id, type)
          *    .then(function(pathArray) {
          *        alert('its here!');
          *    });
@@ -77,6 +77,37 @@ function entityResource($q, $http, umbRequestHelper) {
                'Failed to retrieve path for id:' + id);
         },
 
+        /**
+         * @ngdoc method
+         * @name umbraco.resources.entityResource#getUrl
+         * @methodOf umbraco.resources.entityResource
+         *
+         * @description
+         * Returns a url, given a node ID and type
+         *
+         * ##usage
+         * 
+         * entityResource.getUrl(id, type)
+         *    .then(function(url) {
+         *        alert('its here!');
+         *    });
+         * 
+ * + * @param {Int} id Id of node to return the public url to + * @param {string} type Object type name + * @returns {Promise} resourcePromise object containing the url. + * + */ + getUrl: function(id, type) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "entityApiBaseUrl", + "GetUrl", + [{ id: id }, {type: type }])), + 'Failed to retrieve url for id:' + id); + }, + /** * @ngdoc method * @name umbraco.resources.entityResource#getById @@ -140,7 +171,7 @@ function entityResource($q, $http, umbRequestHelper) { query += "ids=" + item + "&"; }); - // if ids array is empty we need a empty variable in the querystring otherwise the service returns a error + // 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=&"; } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/minieditorhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/minieditorhelper.service.js new file mode 100644 index 0000000000..693457c7e8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/services/minieditorhelper.service.js @@ -0,0 +1,89 @@ +(function () { + 'use strict'; + + function miniEditorHelper(dialogService, editorState, fileManager, contentEditingHelper, $q) { + + var launched = false; + + function launchMiniEditor(node) { + + var deferred = $q.defer(); + + launched = true; + + //We need to store the current files selected in the file manager locally because the fileManager + // is a singleton and is shared globally. The mini dialog will also be referencing the fileManager + // and we don't want it to be sharing the same files as the main editor. So we'll store the current files locally here, + // clear them out and then launch the dialog. When the dialog closes, we'll reset the fileManager to it's previous state. + var currFiles = _.groupBy(fileManager.getFiles(), "alias"); + fileManager.clearFiles(); + + //We need to store the original editorState entity because it will need to change when the mini editor is loaded so that + // any property editors that are working with editorState get given the correct entity, otherwise strange things will + // start happening. + var currEditorState = editorState.getCurrent(); + + dialogService.open({ + template: "views/common/dialogs/content/edit.html", + id: node.id, + closeOnSave: true, + tabFilter: ["Generic properties"], + callback: function (data) { + + //set the node name back + node.name = data.name; + + //reset the fileManager to what it was + fileManager.clearFiles(); + _.each(currFiles, function (val, key) { + fileManager.setFiles(key, _.map(currFiles['upload'], function (i) { return i.file; })); + }); + + //reset the editor state + editorState.set(currEditorState); + + //Now we need to check if the content item that was edited was actually the same content item + // as the main content editor and if so, update all property data + if (data.id === currEditorState.id) { + var changed = contentEditingHelper.reBindChangedProperties(currEditorState, data); + } + + launched = false; + + deferred.resolve(data); + + }, + closeCallback: function () { + //reset the fileManager to what it was + fileManager.clearFiles(); + _.each(currFiles, function (val, key) { + fileManager.setFiles(key, _.map(currFiles['upload'], function (i) { return i.file; })); + }); + + //reset the editor state + editorState.set(currEditorState); + + launched = false; + + deferred.reject(); + + } + }); + + return deferred.promise; + + } + + var service = { + launchMiniEditor: launchMiniEditor + }; + + return service; + + } + + + angular.module('umbraco.services').factory('miniEditorHelper', miniEditorHelper); + + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/init.js b/src/Umbraco.Web.UI.Client/src/init.js index ded5ba08e4..3233974cee 100644 --- a/src/Umbraco.Web.UI.Client/src/init.js +++ b/src/Umbraco.Web.UI.Client/src/init.js @@ -1,6 +1,6 @@ /** Executed when the application starts, binds to events and set global state */ -app.run(['userService', '$log', '$rootScope', '$location', 'navigationService', 'appState', 'editorState', 'fileManager', 'assetsService', 'eventsService', '$cookies', '$templateCache', - function (userService, $log, $rootScope, $location, navigationService, appState, editorState, fileManager, assetsService, eventsService, $cookies, $templateCache) { +app.run(['userService', '$log', '$rootScope', '$location', 'navigationService', 'appState', 'editorState', 'fileManager', 'assetsService', 'eventsService', '$cookies', '$templateCache', 'localStorageService', + function (userService, $log, $rootScope, $location, navigationService, appState, editorState, fileManager, assetsService, eventsService, $cookies, $templateCache, localStorageService) { //This sets the default jquery ajax headers to include our csrf token, we // need to user the beforeSend method because our token changes per user/login so @@ -13,6 +13,11 @@ app.run(['userService', '$log', '$rootScope', '$location', 'navigationService', /** Listens for authentication and checks if our required assets are loaded, if/once they are we'll broadcast a ready event */ eventsService.on("app.authenticated", function(evt, data) { + + //Removes all stored LocalStorage browser items - that may contain sensitive data + //So if a machine or computer is shared and a new user logs in, we clear out the previous persons localStorage items + localStorageService.clearAll(); + assetsService._loadInitAssets().then(function() { appState.setGlobalState("isReady", true); diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index c988f9edbb..6fbf12e815 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -125,6 +125,8 @@ @import "components/notifications/umb-notifications.less"; @import "components/umb-file-dropzone.less"; +@import "components/umb-node-preview.less"; +@import "components/umb-mini-editor.less"; // Utilities @import "utilities/_flexbox.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-mini-editor.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-mini-editor.less new file mode 100644 index 0000000000..53e0dcd6b3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-mini-editor.less @@ -0,0 +1,33 @@ +.umb-modal .umb-mini-editor { + + .umb-panel-header { + padding: 20px; + background: @grayLighter; + border-bottom: 1px solid @grayLight; + height: 59px; + + .umb-headline { + margin-left: 0; + margin-right: 0; + margin-bottom: 0; + margin-top: 3px; + } + } + + .umb-panel-body { + padding-left: 0; + padding-right: 0; + } + + .umb-panel-body.with-footer { + bottom: 52px; + } + + .umb-panel-footer { + background: @grayLighter; + border-top: 1px solid @grayLight; + height: 51px; + padding: 0 20px; + } + +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less new file mode 100644 index 0000000000..5b96492b48 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less @@ -0,0 +1,98 @@ +.umb-node-preview { + padding: 5px 15px; + margin-bottom: 5px; + background: @grayLighter; + border-radius: 3px; + display: flex; + align-items: center; + max-width: 66.6%; + box-sizing: border-box; +} + +.umb-node-preview--sortable { + cursor: move; +} + +.umb-node-preview--sortable:hover { + border-color: #d9d9d9; +} + +.umb-node-preview--unpublished { + .umb-node-preview__icon, + .umb-node-preview__name, + .umb-node-preview__description { + opacity: 0.6; + } +} + +.umb-node-preview__icon { + display: flex; + width: 25px; + height: 25px; + justify-content: center; + align-items: center; + font-size: 20px; + margin-right: 10px; + flex: 0 0 auto; +} + +.umb-node-preview__content { + flex: 1 1 auto; +} + +.umb-node-preview__name { + font-size: 13px; + font-weight: bold; + color: @black; +} + +.umb-node-preview__description { + font-size: 11px; + line-height: 1.5em; +} + +.umb-node-preview__actions { + flex: 0 0 auto; + display: flex; + align-items: center; +} + +.umb-node-preview__action { + margin-left: 5px; + margin-right: 5px; + font-size: 13px; + font-weight: bold; + opacity: 0.5; +} + +.umb-node-preview__action:hover { + color: @blue; + text-decoration: none; + opacity: 1; +} + +.umb-node-preview-add { + display: flex; + align-items: center; + justify-content: center; + border: 1px dashed #d9d9d9; + color: @blue; + font-weight: bold; + padding: 5px 15px; + max-width: 66.6%; + box-sizing: border-box; +} + +.umb-node-preview-add:hover { + color: @blue; +} + +.umb-overlay, +.umb-modal { + .umb-node-preview { + max-width: none; + } + .umb-node-preview-add { + max-width: none; + } +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/forms.less b/src/Umbraco.Web.UI.Client/src/less/forms.less index d046ac7104..5b6e97ad71 100644 --- a/src/Umbraco.Web.UI.Client/src/less/forms.less +++ b/src/Umbraco.Web.UI.Client/src/less/forms.less @@ -190,7 +190,7 @@ input[type="tel"], input[type="color"], .uneditable-input { display: inline-block; - height: @baseLineHeight; + height: 30px; padding: 4px 6px; margin-bottom: @baseLineHeight / 2; font-size: @baseFontSize; @@ -198,6 +198,7 @@ input[type="color"], color: @gray; .border-radius(@inputBorderRadius); vertical-align: middle; + box-sizing: border-box; } input.-full-width-input { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/content/edit.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/content/edit.html index 55e0cad2e7..eef28aefa1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/content/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/content/edit.html @@ -2,7 +2,8 @@ ng-controller="Umbraco.Dialogs.Content.EditController" ng-show="loaded" ng-submit="save()" - val-form-manager> + val-form-manager + class="umb-mini-editor">
@@ -12,15 +13,33 @@
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/insert/insert.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/insert/insert.controller.js index 59a00a5797..b99dd289b1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/insert/insert.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/insert/insert.controller.js @@ -1,16 +1,16 @@ (function () { "use strict"; - function InsertOverlayController($scope) { + function InsertOverlayController($scope, localizationService) { var vm = this; if(!$scope.model.title) { - $scope.model.title = "Insert"; + $scope.model.title = localizationService.localize("template_insert"); } if(!$scope.model.subtitle) { - $scope.model.subtitle = "Choose what to insert into your template"; + $scope.model.subtitle = localizationService.localize("template_insertDesc"); } vm.openMacroPicker = openMacroPicker; @@ -22,7 +22,7 @@ vm.macroPickerOverlay = { view: "macropicker", - title: "Insert macro", + title: localizationService.localize("template_insertMacro"), dialogData: {}, show: true, submit: function(model) { @@ -45,8 +45,8 @@ function openPageFieldOverlay() { vm.pageFieldOverlay = { - title: "Insert value", - description: "Select a value from the currentpage", + title: localizationService.localize("template_insertPageField"), + description: localizationService.localize("template_insertPageFieldDesc"), submitButtonLabel: "Insert", closeButtonlabel: "Cancel", view: "insertfield", @@ -78,7 +78,8 @@ treeAlias: "dictionary", entityType: "dictionary", multiPicker: false, - title: "Insert dictionary item", + title: localizationService.localize("template_insertDictionaryItem"), + description: localizationService.localize("template_insertDictionaryItemDesc"), show: true, select: function(node){ @@ -108,7 +109,7 @@ entityType: "partialView", multiPicker: false, show: true, - title: "Insert partial view", + title: localizationService.localize("template_insertPartialView"), select: function(node){ diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/insert/insert.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/insert/insert.html index b2e7cc033c..19005ccc69 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/insert/insert.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/insert/insert.html @@ -19,7 +19,7 @@
-
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/macropicker/macropicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/macropicker/macropicker.html index 418e6b5ca6..6bcbaad50c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/macropicker/macropicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/macropicker/macropicker.html @@ -27,12 +27,12 @@ - - + + There are no macros available to insert - diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js index 43c65d99ee..3b6c39e84a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js @@ -1,7 +1,7 @@ //used for the media picker dialog angular.module("umbraco") .controller("Umbraco.Overlays.MediaPickerController", - function($scope, mediaResource, umbRequestHelper, entityResource, $log, mediaHelper, mediaTypeHelper, eventsService, treeService, $element, $timeout, $cookies, $cookieStore, localizationService) { + function ($scope, mediaResource, umbRequestHelper, entityResource, $log, mediaHelper, mediaTypeHelper, eventsService, treeService, $element, $timeout, $cookies, localStorageService, localizationService) { if (!$scope.model.title) { $scope.model.title = localizationService.localize("defaultdialogs_selectMedia"); @@ -15,7 +15,7 @@ angular.module("umbraco") $scope.multiPicker = (dialogOptions.multiPicker && dialogOptions.multiPicker !== "0") ? true : false; $scope.startNodeId = dialogOptions.startNodeId ? dialogOptions.startNodeId : -1; $scope.cropSize = dialogOptions.cropSize; - $scope.lastOpenedNode = $cookieStore.get("umbLastOpenedMediaNodeId"); + $scope.lastOpenedNode = localStorageService.get("umbLastOpenedMediaNodeId"); if ($scope.onlyImages) { $scope.acceptedFileTypes = mediaHelper .formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes); @@ -133,8 +133,7 @@ angular.module("umbraco") }); $scope.currentFolder = folder; - // for some reason i cannot set cookies with cookieStore - document.cookie = "umbLastOpenedMediaNodeId=" + folder.id; + localStorageService.set("umbLastOpenedMediaNodeId", folder.id); }; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/querybuilder/querybuilder.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/querybuilder/querybuilder.controller.js index 3939882969..3c0199d7bd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/querybuilder/querybuilder.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/querybuilder/querybuilder.controller.js @@ -1,8 +1,14 @@ (function () { "use strict"; - function QueryBuilderOverlayController($scope, templateQueryResource) { + function QueryBuilderOverlayController($scope, templateQueryResource, localizationService) { + var everything = localizationService.localize("template_allContent"); + var myWebsite = localizationService.localize("template_websiteRoot"); + + var ascendingTranslation = localizationService.localize("template_ascending"); + var descendingTranslation = localizationService.localize("template_descending"); + var vm = this; vm.properties = []; @@ -17,10 +23,10 @@ vm.query = { contentType: { - name: "Everything" + name: everything }, source: { - name: "My website" + name: myWebsite }, filters: [ { @@ -33,7 +39,13 @@ alias: "", name: "", }, - direction: "ascending" + direction: "ascending", //This is the value for sorting sent to server + translation: { + currentLabel: ascendingTranslation, //This is the localized UI value in the the dialog + ascending: ascendingTranslation, + descending: descendingTranslation + } + } }; @@ -74,7 +86,6 @@ vm.contentPickerOverlay = { view: "contentpicker", show: true, - submitButtonLabel: "Insert", submit: function(model) { var selectedNodeId = model.selection[0].id; @@ -83,7 +94,7 @@ if (selectedNodeId > 0) { query.source = { id: selectedNodeId, name: selectedNodeName }; } else { - query.source.name = "My website"; + query.source.name = myWebsite; delete query.source.id; } @@ -113,13 +124,21 @@ function trashFilter(query) { query.filters.splice(query, 1); + + //if we remove the last one, add a new one to generate ui for it. + if (query.filters.length == 0) { + query.filters.push({}); + } + } function changeSortOrder(query) { if (query.sort.direction === "ascending") { query.sort.direction = "descending"; + query.sort.translation.currentLabel = query.sort.translation.descending; } else { query.sort.direction = "ascending"; + query.sort.translation.currentLabel = query.sort.translation.ascending; } throttledFunc(); } @@ -128,8 +147,10 @@ query.sort.property = property; if (property.type === "datetime") { query.sort.direction = "descending"; + query.sort.translation.currentLabel = query.sort.translation.descending; } else { query.sort.direction = "ascending"; + query.sort.translation.currentLabel = query.sort.translation.ascending; } throttledFunc(); } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/querybuilder/querybuilder.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/querybuilder/querybuilder.html index e9be97b7df..d3a4835470 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/querybuilder/querybuilder.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/querybuilder/querybuilder.html @@ -5,7 +5,7 @@
- I Want + I want
@@ -22,7 +22,7 @@
- from + from {{vm.query.source.name}} @@ -91,7 +91,7 @@ - + @@ -100,7 +100,7 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/templatesections/templatesections.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/templatesections/templatesections.html index 1a11d6a46e..ee62c3426d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/templatesections/templatesections.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/templatesections/templatesections.html @@ -1,71 +1,59 @@
-
+
-
Render child template
+
- Renders the contents of a child template, by inserting a - @RenderBody() placeholder. +
-
+
-
Render a named section
+
- Renders a named area of a child template, by insert a @RenderSection(name) placeholder. - This renders an area of a child template which is wrapped in a corresponding @section [name]{ ... } definition. +
- + -
- Set the name of the section to render in this area of the template -
+
- If mandatory, the child template must contain a @section definition, otherwise an error is shown. +
-
+
-
Define a named section
+
- Defines a part of your template as a named section by wrapping it in - a @section { ... }. This can be rendered in a - specific area of the master of this template, by using @RenderSection. +
- + -
- Give the section a name -
- -
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/components/tabs/umb-tabs-nav.html b/src/Umbraco.Web.UI.Client/src/views/components/tabs/umb-tabs-nav.html index 8522bc3b6a..808822e138 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/tabs/umb-tabs-nav.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/tabs/umb-tabs-nav.html @@ -1,5 +1,5 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-node-preview.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-node-preview.html new file mode 100644 index 0000000000..f31f595a88 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-node-preview.html @@ -0,0 +1,12 @@ +
+ +
+
{{ name }}
+
{{ description }}
+
+
+ Open + Remove +
+ +
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/packager/views/install-local.controller.js b/src/Umbraco.Web.UI.Client/src/views/packager/views/install-local.controller.js index e34bc48ecd..9ce2506e76 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packager/views/install-local.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/packager/views/install-local.controller.js @@ -34,18 +34,24 @@ fields: {}, file: file }).progress(function (evt) { + + // hack: in some browsers the progress event is called after success + // this prevents the UI from going back to a uploading state + if(vm.zipFile.uploadStatus !== "done" && vm.zipFile.uploadStatus !== "error") { - // set view state to uploading - vm.state = 'uploading'; + // set view state to uploading + vm.state = 'uploading'; - // calculate progress in percentage - var progressPercentage = parseInt(100.0 * evt.loaded / evt.total, 10); + // calculate progress in percentage + var progressPercentage = parseInt(100.0 * evt.loaded / evt.total, 10); - // set percentage property on file - vm.zipFile.uploadProgress = progressPercentage; + // set percentage property on file + vm.zipFile.uploadProgress = progressPercentage; - // set uploading status on file - vm.zipFile.uploadStatus = "uploading"; + // set uploading status on file + vm.zipFile.uploadStatus = "uploading"; + + } }).success(function (data, status, headers, config) { diff --git a/src/Umbraco.Web.UI.Client/src/views/packager/views/installed.html b/src/Umbraco.Web.UI.Client/src/views/packager/views/installed.html index 5da3c33a76..a41a7e72c0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packager/views/installed.html +++ b/src/Umbraco.Web.UI.Client/src/views/packager/views/installed.html @@ -12,8 +12,8 @@
- - + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js index 667dfd0f22..947603794a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js @@ -1,7 +1,7 @@ //this controller simply tells the dialogs service to open a mediaPicker window //with a specified callback, this callback will receive an object with a selection on it -function contentPickerController($scope, dialogService, entityResource, editorState, $log, iconHelper, $routeParams, fileManager, contentEditingHelper, angularHelper, navigationService, $location) { +function contentPickerController($scope, entityResource, editorState, iconHelper, $routeParams, angularHelper, navigationService, $location, miniEditorHelper) { function trim(str, chr) { var rgxtrim = (!chr) ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^' + chr + '+|' + chr + '+$', 'g'); @@ -39,6 +39,9 @@ function contentPickerController($scope, dialogService, entityResource, editorSt else { $scope.contentPickerForm.maxCount.$setValidity("maxCount", true); } + + setSortingState($scope.renderModel); + }); } @@ -59,6 +62,14 @@ function contentPickerController($scope, dialogService, entityResource, editorSt } }; + // sortable options + $scope.sortableOptions = { + distance: 10, + tolerance: "pointer", + scroll: true, + zIndex: 6000 + }; + if ($scope.model.config) { //merge the server config on top of the default config, then set the server config to use the result $scope.model.config = angular.extend(defaultConfig, $scope.model.config); @@ -75,8 +86,9 @@ function contentPickerController($scope, dialogService, entityResource, editorSt : $scope.model.config.startNode.type === "media" ? "Media" : "Document"; - $scope.allowOpenButton = entityType === "Document" || entityType === "Media"; + $scope.allowOpenButton = entityType === "Document"; $scope.allowEditButton = entityType === "Document"; + $scope.allowRemoveButton = true; //the dialog options for the picker var dialogOptions = { @@ -192,14 +204,26 @@ function contentPickerController($scope, dialogService, entityResource, editorSt }); if (currIds.indexOf(item.id) < 0) { - item.icon = iconHelper.convertFromLegacyIcon(item.icon); - $scope.renderModel.push({ name: item.name, id: item.id, icon: item.icon, path: item.path }); + setEntityUrl(item); } }; $scope.clear = function () { $scope.renderModel = []; }; + + $scope.openMiniEditor = function(node) { + miniEditorHelper.launchMiniEditor(node).then(function(updatedNode){ + // update the node + node.name = updatedNode.name; + node.published = updatedNode.hasPublishedVersion; + if(entityType !== "Member") { + entityResource.getUrl(updatedNode.id, entityType).then(function(data){ + node.url = data; + }); + } + }); + }; var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { var currIds = _.map($scope.renderModel, function (i) { @@ -215,26 +239,89 @@ function contentPickerController($scope, dialogService, entityResource, editorSt //load current data var modelIds = $scope.model.value ? $scope.model.value.split(',') : []; + entityResource.getByIds(modelIds, entityType).then(function (data) { - //Ensure we populate the render model in the same order that the ids were stored! _.each(modelIds, function (id, i) { var entity = _.find(data, function (d) { return d.id == id; }); - + if (entity) { - entity.icon = iconHelper.convertFromLegacyIcon(entity.icon); - $scope.renderModel.push({ name: entity.name, id: entity.id, icon: entity.icon, path: entity.path }); + setEntityUrl(entity); } - - }); + }); //everything is loaded, start the watch on the model startWatch(); }); + + function setEntityUrl(entity) { + + // get url for content and media items + if(entityType !== "Member") { + entityResource.getUrl(entity.id, entityType).then(function(data){ + // update url + angular.forEach($scope.renderModel, function(item){ + if(item.id === entity.id) { + item.url = data; + } + }); + }); + } + + // add the selected item to the renderModel + // if it needs to show a url the item will get + // updated when the url comes back from server + addSelectedItem(entity); + + } + + function addSelectedItem(item) { + + // set icon + if(item.icon) { + item.icon = iconHelper.convertFromLegacyIcon(item.icon); + } + + // set default icon + if (!item.icon) { + switch (entityType) { + case "Document": + item.icon = "icon-document"; + break; + case "Media": + item.icon = "icon-picture"; + break; + case "Member": + item.icon = "icon-user"; + break; + } + } + + $scope.renderModel.push({ + "name": item.name, + "id": item.id, + "icon": item.icon, + "path": item.path, + "url": item.url, + "published": (item.metaData && item.metaData.IsPublished === false && entityType === "Document") ? false : true + // only content supports published/unpublished content so we set everything else to published so the UI looks correct + }); + + } + + function setSortingState(items) { + // disable sorting if the list only consist of one item + if(items.length > 1) { + $scope.sortableOptions.disabled = false; + } else { + $scope.sortableOptions.disabled = true; + } + } + } angular.module('umbraco').controller("Umbraco.PropertyEditors.ContentPickerController", contentPickerController); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html index 9f5f3fb60f..900cf6b416 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html @@ -2,32 +2,28 @@ - - - +
+ + +
+ + + Add + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/googlemaps/googlemaps.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/googlemaps/googlemaps.controller.js index f8e89d7200..1d6261c10a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/googlemaps/googlemaps.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/googlemaps/googlemaps.controller.js @@ -2,7 +2,7 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.GoogleMapsController", function ($element, $rootScope, $scope, notificationsService, dialogService, assetsService, $log, $timeout) { - assetsService.loadJs('http://www.google.com/jsapi') + assetsService.loadJs('https://www.google.com/jsapi') .then(function () { google.load("maps", "3", { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.controller.js index 5ebbd37217..aa20a9c43b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.controller.js @@ -8,6 +8,7 @@ function memberGroupPicker($scope, dialogService){ } $scope.renderModel = []; + $scope.allowRemove = true; if ($scope.model.value) { var modelIds = $scope.model.value.split(','); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.html index 5258968c00..60ae9cb202 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.html @@ -1,27 +1,22 @@
+
+ + +
- - - + + Add + - - - + + Add + - Master template: {{ vm.getMasterTemplateName(vm.template.masterTemplateAlias, vm.templates) }} + Master template: + + {{ vm.getMasterTemplateName(vm.template.masterTemplateAlias, vm.templates) }} + No master + @@ -62,7 +66,7 @@
  • Value
  • Partial view
  • Dictionary
  • -
  • Macro
  • +
  • Macro
  • diff --git a/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/content-picker-controller.spec.js b/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/content-picker-controller.spec.js index e6d1312109..d5e3a7a496 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/content-picker-controller.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/content-picker-controller.spec.js @@ -17,7 +17,11 @@ describe('Content picker controller tests', function () { value:"1233,1231,23121", label: "My content picker", description: "desc", - config: {} + config: { + startNode: { + type: "content" + } + } }; //this controller requires an angular form controller applied to it @@ -47,6 +51,12 @@ describe('Content picker controller tests', function () { })); describe('content edit controller save and publish', function () { + + var item = { + name: "meh", + id: 666, + icon: "woop" + }; it('should define the default properties on construction', function () { expect(scope.model.value).toNotBe(undefined); @@ -65,7 +75,6 @@ describe('Content picker controller tests', function () { }); it("Removing an item should update renderModel, ids and model.value", function(){ - scope.remove(1); scope.$apply(); expect(scope.renderModel.length).toBe(2); @@ -73,24 +82,29 @@ describe('Content picker controller tests', function () { }); it("Adding an item should update renderModel, ids and model.value", function(){ - - scope.add({name: "meh", id: 666, icon: "woop"}); + scope.add(item); scope.$apply(); - expect(scope.renderModel.length).toBe(4); - expect(scope.model.value).toBe("1233,1231,23121,666"); + setTimeout(function(){ + expect(scope.renderModel.length).toBe(4); + expect(scope.model.value).toBe("1233,1231,23121,666"); + }, 1000); }); - it("Adding a dublicate item should note update renderModel, ids and model.value", function(){ - - scope.add({ name: "meh", id: 666, icon: "woop" }); + it("Adding a duplicate item should note update renderModel, ids and model.value", function(){ + scope.add(item); scope.$apply(); - expect(scope.renderModel.length).toBe(4); - expect(scope.model.value).toBe("1233,1231,23121,666"); + setTimeout(function(){ + expect(scope.renderModel.length).toBe(4); + expect(scope.model.value).toBe("1233,1231,23121,666"); + }, 1000); - scope.add({ name: "meh 2", id: 666, icon: "woop 2" }); + scope.add(item); scope.$apply(); - expect(scope.renderModel.length).toBe(4); - expect(scope.model.value).toBe("1233,1231,23121,666"); + setTimeout(function(){ + expect(scope.renderModel.length).toBe(4); + expect(scope.model.value).toBe("1233,1231,23121,666"); + }, 1000); + }); }); }); \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 2c4cd4859d..540f008fc8 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -127,8 +127,8 @@ False ..\packages\dotless.1.4.1.0\lib\dotless.Core.dll - - ..\packages\Examine.0.1.70.0\lib\Examine.dll + + ..\packages\Examine.0.1.80\lib\net45\Examine.dll True @@ -138,8 +138,9 @@ ..\packages\ImageProcessor.2.5.1\lib\net45\ImageProcessor.dll - - ..\packages\ImageProcessor.Web.4.7.2\lib\net45\ImageProcessor.Web.dll + + ..\packages\ImageProcessor.Web.4.8.0\lib\net45\ImageProcessor.Web.dll + True False diff --git a/src/Umbraco.Web.UI/Umbraco/dashboard/UserControlProxy.aspx b/src/Umbraco.Web.UI/Umbraco/dashboard/UserControlProxy.aspx index 59a96e9a37..7728d281c3 100644 --- a/src/Umbraco.Web.UI/Umbraco/dashboard/UserControlProxy.aspx +++ b/src/Umbraco.Web.UI/Umbraco/dashboard/UserControlProxy.aspx @@ -13,6 +13,7 @@ + diff --git a/src/Umbraco.Web.UI/Umbraco/dashboard/UserControlProxy.aspx.designer.cs b/src/Umbraco.Web.UI/Umbraco/dashboard/UserControlProxy.aspx.designer.cs index 19ac019a8f..628e82a767 100644 --- a/src/Umbraco.Web.UI/Umbraco/dashboard/UserControlProxy.aspx.designer.cs +++ b/src/Umbraco.Web.UI/Umbraco/dashboard/UserControlProxy.aspx.designer.cs @@ -48,6 +48,15 @@ namespace Umbraco.Web.UI.Umbraco.Dashboard { /// protected global::ClientDependency.Core.Controls.JsInclude JsInclude3; + /// + /// JsInclude4 control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::ClientDependency.Core.Controls.JsInclude JsInclude4; + /// /// JsInclude6 control. /// diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config index 6919e10f64..629438762f 100644 --- a/src/Umbraco.Web.UI/packages.config +++ b/src/Umbraco.Web.UI/packages.config @@ -4,9 +4,9 @@ - + - + diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/umbraco/config/lang/da.xml index b0452fb631..fb032a7f22 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/da.xml @@ -291,6 +291,7 @@ Vælg medlem Vælg medlemsgruppe Der er ingen parametre for denne makro + Der er ikke tilføjet nogen makroer Link dit Fjern link fra dit konto @@ -1004,17 +1005,117 @@ Mange hilsner fra Umbraco robotten Vis prøve Styles + Rediger skabelon + + Sektioner Indsæt indholdsområde - Indsæt indholdsområdemarkering - Indsæt ordbogselement - Indsæt makro - Indsæt Umbraco sidefelt + Indsæt pladsholder for indholdsområde + + Indsæt + Hvad vil du indsætte ? + + Oversættelse + Indsætter en oversætbar tekst, som skifter efter det sprog, som websitet vises i. + + Makro + + En makro er et element, som kan have forskellige indstillinger, når det indsættes. + Brug det som en genbrugelig del af dit design såsom gallerier, formularer og lister. + + + Sideværdi + + Viser værdien af et felt fra den nuværende side. Kan indstilles til at bruge rekursive værdier eller + vise en standardværdi i tilfælde af, at feltet er tomt. + + + Partial view + + Et Partial View er et skabelonelement, som kan indsættes i andre skabeloner og derved + genbruges og deles på tværs af sideskabelonerne. + + Master skabelon - Lynguide til Umbracos skabelontags + Ingen masterskabelon + Ingen master + + Indsæt en underliggende skabelon + + @RenderBody() element. + ]]> + + + + Definer en sektion + + @section { ... }. Herefter kan denne sektion flettes ind i + overliggende skabelon ved at indsætte et @RenderSection element. + ]]> + + + Indsæt en sektion + + @RenderSection(name) element. Den underliggende skabelon skal have + defineret en sektion via et @section [name]{ ... } element. + ]]> + + + Sektionsnavn + Sektionen er obligatorisk + + + Hvis obligatorisk, skal underskabelonen indeholde en @section -definition. + + + + Query builder + sider returneret, på + + Returner + alt indhold + indhold af typen "%0%" + + fra + mit website + hvor + og + + er + ikke er + er før + er før (inkl. valgte dato) + er efter + er efter (inkl. valgte dato) + er + ikke er + indeholder + ikke indeholder + er større end + er større end eller det samme som + er mindre end + er mindre end eller det samme som + + Id + Navn + Oprettelsesdato + Sidste opdatering + + Sortér efter + stigende rækkefølge + faldende rækkefølge + Skabelon + + Alternativt felt Alternativ tekst diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 2021be1d9d..444ecc2e3d 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -305,6 +305,7 @@ Select member group No icons were found There are no parameters for this macro + There are no macros available to insert External login providers Exception Details Stacktrace @@ -1032,6 +1033,9 @@ To manage your website, simply open the Umbraco back office and start adding con Sections Insert content area Insert content area placeholder + + Insert + Choose what to insert into your template Dictionary item A dictionary item is a placeholder for a translatable piece of text, which makes it easy to create designs for multilingual websites. @@ -1053,18 +1057,79 @@ To manage your website, simply open the Umbraco back office and start adding con Master template + No master template + No master + + Render child template + + @RenderBody() placeholder. + ]]> + + + + Define a named section + + @section { ... }. This can be rendered in a + specific area of the parent of this template, by using @RenderSection. + ]]> + + + Render a named section + + @RenderSection(name) placeholder. + This renders an area of a child template which is wrapped in a corresponding @section [name]{ ... } definition. + ]]> + + + Section Name + Section is mandatory + + If mandatory, the child template must contain a @section definition, otherwise an error is shown. + + Query builder items returned, in - I want + I want + all content + content of type "%0%" from + my website where and - everything + is + is not + before + before (including selected date) + after + after (including selected date) + equals + does not equal + contains + does not contain + greater than + greater than or equal to + less than + less than or equal to + + Id + Name + Created Date + Last Updated Date + + order by + ascending + descending Template + diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index 173da6cc9b..aa9f33695d 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -1027,13 +1027,105 @@ To manage your website, simply open the Umbraco back office and start adding con Edit template + + Sections Insert content area Insert content area placeholder - Insert dictionary item - Insert Macro - Insert Umbraco page field + + Insert + Choose what to insert into your template + + Dictionary item + A dictionary item is a placeholder for a translatable piece of text, which makes it easy to create designs for multilingual websites. + + Macro + + A Macro is a configurable component which is great for + reusable parts of your design, where you need the option to provide parameters, + such as galleries, forms and lists. + + + Value + Displays the value of a named field from the current page, with options to modify the value or fallback to alternative values. + + Partial view + + A partial view is a separate template file which can be rendered inside another + template, it's great for reusing markup or for separating complex templates into separate files. + + Master template - Quick Guide to Umbraco template tags + No master template + No master + + Render child template + + @RenderBody() placeholder. + ]]> + + + + Define a named section + + @section { ... }. This can be rendered in a + specific area of the parent of this template, by using @RenderSection. + ]]> + + + Render a named section + + @RenderSection(name) placeholder. + This renders an area of a child template which is wrapped in a corresponding @section [name]{ ... } definition. + ]]> + + + Section Name + Section is mandatory + + If mandatory, the child template must contain a @section definition, otherwise an error is shown. + + + + Query builder + items returned, in + + I want + all content + content of type "%0%" + from + my website + where + and + + is + is not + before + before (including selected date) + after + after (including selected date) + equals + does not equal + contains + does not contain + greater than + greater than or equal to + less than + less than or equal to + + Id + Name + Created Date + Last Updated Date + + order by + ascending + descending + Template @@ -1142,7 +1234,7 @@ To manage your website, simply open the Umbraco back office and start adding con Building models - this can take abit of time, don't worry + this can take a bit of time, don't worry Models generated Models could not be generated Models generation has failed, see exception in U log diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml b/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml index 255bf62d5d..c6f94949d2 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml @@ -27,6 +27,9 @@ Опубликовать Обновить узлы Опубликовать весь сайт + Установить разрешения для страницы '%0%' + Выберите, куда переместить + В структуре документов ниже Восстановить Разрешения Откатить @@ -189,7 +192,7 @@ Роль участника Тип участника Дата не указана - Заголовок страницы + Заголовок ссылки Не является членом групп(ы) Свойства Этот документ опубликован, но скрыт, потому что его родительский документ '%0%' не опубликован @@ -341,9 +344,10 @@ Просмотр элемента кэша Создать папку... Связать с оригиналом + Включая все дочерние Самое дружелюбное сообщество Ссылка на страницу - Открывает документ по ссылке в новом окне или вкладке браузера + Открывать ссылку в новом окне или вкладке браузера Ссылка на медиа-файл Выбрать медиа Выбрать значок diff --git a/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs b/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs index 246571d479..1340545621 100644 --- a/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs @@ -131,9 +131,7 @@ namespace Umbraco.Web.Cache ClearAllIsolatedCacheByEntityType(); ClearAllIsolatedCacheByEntityType(); ClearAllIsolatedCacheByEntityType(); - - //all property type cache - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.PropertyTypeCacheKey); + //all content type property cache ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.ContentTypePropertiesCacheKey); //all content type cache @@ -266,12 +264,6 @@ namespace Umbraco.Web.Cache /// private static void ClearContentTypeCache(JsonPayload payload) { - //clears the cache for each property type associated with the content type - foreach (var pid in payload.PropertyTypeIds) - { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(CacheKeys.PropertyTypeCacheKey + pid); - } - //clears the cache associated with the Content type itself ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(string.Format("{0}{1}", CacheKeys.ContentTypeCacheKey, payload.Id)); //clears the cache associated with the content type properties collection diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 376606ff5c..d7b2cc3994 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -262,7 +262,7 @@ namespace Umbraco.Web.Editors }, { "mediaTypeApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.GetAllowedChildren(0)) + controller => controller.GetAllowedChildren("0")) }, { "macroApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index 596e27e3a5..104c451273 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -17,6 +17,7 @@ using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; using System.Linq; +using System.Net.Http; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models; using Umbraco.Web.WebApi.Filters; @@ -144,6 +145,47 @@ namespace Umbraco.Web.Editors return foundContent.Path.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries).Select(int.Parse); } + + + /// + /// Gets the url of an entity + /// + /// Int id of the entity to fetch URL for + /// The tpye of entity such as Document, Media, Member + /// The URL or path to the item + public HttpResponseMessage GetUrl(int id, UmbracoEntityTypes type) + { + var returnUrl = string.Empty; + + if (type == UmbracoEntityTypes.Document) + { + var foundUrl = Umbraco.Url(id); + if (string.IsNullOrEmpty(foundUrl) == false && foundUrl != "#") + { + returnUrl = foundUrl; + + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(returnUrl) + }; + } + } + + var ancestors = GetAncestors(id, type); + + //if content, skip the first node for replicating NiceUrl defaults + if(type == UmbracoEntityTypes.Document) { + ancestors = ancestors.Skip(1); + } + + returnUrl = "/" + string.Join("/", ancestors.Select(x => x.Name)); + + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(returnUrl) + }; + } + /// /// Gets an entity by it's unique id if the entity supports that /// @@ -614,7 +656,7 @@ namespace Umbraco.Web.Editors .Select(Mapper.Map); // entities are in "some" order, put them back in order - var xref = entities.ToDictionary(x => x.Id); + var xref = entities.ToDictionary(x => x.Key); var result = keysArray.Select(x => xref.ContainsKey(x) ? xref[x] : null).Where(x => x != null); return result; diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index fb17d9462e..2ed01c0192 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -36,6 +36,7 @@ using Umbraco.Core.Configuration; using Umbraco.Core.Persistence.FaultHandling; using Umbraco.Web.UI; using Notification = Umbraco.Web.Models.ContentEditing.Notification; +using Umbraco.Core.Persistence; namespace Umbraco.Web.Editors { @@ -175,7 +176,37 @@ namespace Umbraco.Web.Editors /// Returns the child media objects ///
    [FilterAllowedOutgoingMedia(typeof(IEnumerable>), "Items")] - public PagedResult> GetChildren(int id, + public PagedResult> GetChildren(string id, + int pageNumber = 0, + int pageSize = 0, + string orderBy = "SortOrder", + Direction orderDirection = Direction.Ascending, + bool orderBySystemField = true, + string filter = "") + { + int idInt; Guid idGuid; + + if (Guid.TryParse(id, out idGuid)) + { + var entity = Services.EntityService.GetByKey(idGuid); + if (entity != null) + { + return GetChildren(entity.Id, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); + } + else + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + } + else if (int.TryParse(id, out idInt)) + { + return GetChildren(idInt, pageNumber, pageSize, orderBy, orderDirection, orderBySystemField, filter); + } + + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + private PagedResult> GetChildren(int id, int pageNumber = 0, int pageSize = 0, string orderBy = "SortOrder", @@ -448,12 +479,37 @@ namespace Umbraco.Web.Editors } //get the string json from the request - int parentId; - if (int.TryParse(result.FormData["currentFolder"], out parentId) == false) + int parentId; bool entityFound; + string currentFolderId = result.FormData["currentFolder"]; + if (int.TryParse(currentFolderId, out parentId) == false) { - return Request.CreateValidationErrorResponse("The request was not formatted correctly, the currentFolder is not an integer"); + // if a guid then try to look up the entity + Guid idGuid; + if (Guid.TryParse(currentFolderId, out idGuid)) + { + var entity = Services.EntityService.GetByKey(idGuid); + if (entity != null) + { + entityFound = true; + parentId = entity.Id; + } + else + { + throw new EntityNotFoundException(currentFolderId, "The passed id doesn't exist"); + } + } + else + { + return Request.CreateValidationErrorResponse("The request was not formatted correctly, the currentFolder is not an integer or Guid"); + } + + if (entityFound == false) + { + return Request.CreateValidationErrorResponse("The request was not formatted correctly, the currentFolder is not an integer or Guid"); + } } + //ensure the user has access to this folder by parent id! if (CheckPermissions( new Dictionary(), diff --git a/src/Umbraco.Web/Editors/MediaTypeController.cs b/src/Umbraco.Web/Editors/MediaTypeController.cs index 0f83ad6d03..67b8ae6d1f 100644 --- a/src/Umbraco.Web/Editors/MediaTypeController.cs +++ b/src/Umbraco.Web/Editors/MediaTypeController.cs @@ -11,6 +11,8 @@ using System.Net; using System.Net.Http; using Umbraco.Web.WebApi; using Umbraco.Core.Services; +using Umbraco.Core.Models.EntityBase; +using System; namespace Umbraco.Web.Editors { @@ -174,7 +176,22 @@ namespace Umbraco.Web.Editors ///
    /// [UmbracoTreeAuthorize(Constants.Trees.MediaTypes, Constants.Trees.Media)] - public IEnumerable GetAllowedChildren(int contentId) + public IEnumerable GetAllowedChildren(string contentId) + { + Guid idGuid = Guid.Empty; + int idInt; + if (Guid.TryParse(contentId, out idGuid)) { + var entity = ApplicationContext.Services.EntityService.GetByKey(idGuid); + return GetAllowedChildrenInternal(entity.Id); + } else if (int.TryParse(contentId, out idInt)) + { + return GetAllowedChildrenInternal(idInt); + } + + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + private IEnumerable GetAllowedChildrenInternal(int contentId) { if (contentId == Constants.System.RecycleBinContent) return Enumerable.Empty(); diff --git a/src/Umbraco.Web/Editors/TemplateQueryController.cs b/src/Umbraco.Web/Editors/TemplateQueryController.cs index e3c63dd22e..cd004fe926 100644 --- a/src/Umbraco.Web/Editors/TemplateQueryController.cs +++ b/src/Umbraco.Web/Editors/TemplateQueryController.cs @@ -9,6 +9,7 @@ using System; using System.Diagnostics; using Umbraco.Web.Dynamics; using Umbraco.Web.Models.TemplateQuery; +using Umbraco.Core.Services; namespace Umbraco.Web.Editors { @@ -29,33 +30,44 @@ namespace Umbraco.Web.Editors { } - private static readonly IEnumerable Terms = new List() + private IEnumerable Terms + { + get { - new OperathorTerm("is", Operathor.Equals, new [] {"string"}), - new OperathorTerm("is not", Operathor.NotEquals, new [] {"string"}), - new OperathorTerm("before", Operathor.LessThan, new [] {"datetime"}), - new OperathorTerm("before (including selected date)", Operathor.LessThanEqualTo, new [] {"datetime"}), - new OperathorTerm("after", Operathor.GreaterThan, new [] {"datetime"}), - new OperathorTerm("after (including selected date)", Operathor.GreaterThanEqualTo, new [] {"datetime"}), - new OperathorTerm("equals", Operathor.Equals, new [] {"int"}), - new OperathorTerm("does not equal", Operathor.NotEquals, new [] {"int"}), - new OperathorTerm("contains", Operathor.Contains, new [] {"string"}), - new OperathorTerm("does not contain", Operathor.NotContains, new [] {"string"}), - new OperathorTerm("greater than", Operathor.GreaterThan, new [] {"int"}), - new OperathorTerm("greater than or equal to", Operathor.GreaterThanEqualTo, new [] {"int"}), - new OperathorTerm("less than", Operathor.LessThan, new [] {"int"}), - new OperathorTerm("less than or equal to", Operathor.LessThanEqualTo, new [] {"int"}) - }; + return new List() + { + new OperathorTerm(Services.TextService.Localize("template/is"), Operathor.Equals, new [] {"string"}), + new OperathorTerm(Services.TextService.Localize("template/isNot"), Operathor.NotEquals, new [] {"string"}), + new OperathorTerm(Services.TextService.Localize("template/before"), Operathor.LessThan, new [] {"datetime"}), + new OperathorTerm(Services.TextService.Localize("template/beforeIncDate"), Operathor.LessThanEqualTo, new [] {"datetime"}), + new OperathorTerm(Services.TextService.Localize("template/after"), Operathor.GreaterThan, new [] {"datetime"}), + new OperathorTerm(Services.TextService.Localize("template/afterIncDate"), Operathor.GreaterThanEqualTo, new [] {"datetime"}), + new OperathorTerm(Services.TextService.Localize("template/equals"), Operathor.Equals, new [] {"int"}), + new OperathorTerm(Services.TextService.Localize("template/doesNotEqual"), Operathor.NotEquals, new [] {"int"}), + new OperathorTerm(Services.TextService.Localize("template/contains"), Operathor.Contains, new [] {"string"}), + new OperathorTerm(Services.TextService.Localize("template/doesNotContain"), Operathor.NotContains, new [] {"string"}), + new OperathorTerm(Services.TextService.Localize("template/greaterThan"), Operathor.GreaterThan, new [] {"int"}), + new OperathorTerm(Services.TextService.Localize("template/greaterThanEqual"), Operathor.GreaterThanEqualTo, new [] {"int"}), + new OperathorTerm(Services.TextService.Localize("template/lessThan"), Operathor.LessThan, new [] {"int"}), + new OperathorTerm(Services.TextService.Localize("template/lessThanEqual"), Operathor.LessThanEqualTo, new [] {"int"}) + }; + } + } - private static readonly IEnumerable Properties = new List() + private IEnumerable Properties + { + get { - new PropertyModel() { Name = "Id", Alias = "Id", Type = "int" }, - new PropertyModel() { Name = "Name", Alias = "Name", Type = "string" }, - //new PropertyModel() { Name = "Url", Alias = "url", Type = "string" }, - new PropertyModel() { Name = "Created Date", Alias = "CreateDate", Type = "datetime" }, - new PropertyModel() { Name = "Last Updated Date", Alias = "UpdateDate", Type = "datetime" } - - }; + return new List() + { + new PropertyModel() {Name = Services.TextService.Localize("template/id"), Alias = "Id", Type = "int"}, + new PropertyModel() {Name = Services.TextService.Localize("template/name"), Alias = "Name", Type = "string"}, + //new PropertyModel() { Name = "Url", Alias = "url", Type = "string" }, + new PropertyModel() {Name = Services.TextService.Localize("template/createdDate"), Alias = "CreateDate", Type = "datetime"}, + new PropertyModel() {Name = Services.TextService.Localize("template/lastUpdatedDate"), Alias = "UpdateDate", Type = "datetime"} + }; + } + } public QueryResultModel PostTemplateQuery(QueryModel model) { @@ -297,9 +309,10 @@ namespace Umbraco.Web.Editors { var contentTypes = ApplicationContext.Services.ContentTypeService.GetAllContentTypes() - .Select(x => new ContentTypeModel() { Alias = x.Alias, Name = x.Name }) + .Select(x => new ContentTypeModel() { Alias = x.Alias, Name = Services.TextService.Localize("template/contentOfType", tokens: new string[] { x.Name } ) }) .OrderBy(x => x.Name).ToList(); - contentTypes.Insert(0, new ContentTypeModel() { Alias = string.Empty, Name = "Everything" }); + + contentTypes.Insert(0, new ContentTypeModel() { Alias = string.Empty, Name = Services.TextService.Localize("template/allContent") }); return contentTypes; } diff --git a/src/Umbraco.Web/HtmlHelperRenderExtensions.cs b/src/Umbraco.Web/HtmlHelperRenderExtensions.cs index 3062613b6b..2802e4fe80 100644 --- a/src/Umbraco.Web/HtmlHelperRenderExtensions.cs +++ b/src/Umbraco.Web/HtmlHelperRenderExtensions.cs @@ -228,12 +228,13 @@ namespace Umbraco.Web bool cacheBuster = true, string furtherOptions = null, ImageCropRatioMode? ratioMode = null, - bool upScale = true) + bool upScale = true, + string backgroundColor = null) { return new HtmlString(mediaItem.GetCropUrl(width, height, propertyAlias, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, ratioMode, - upScale)); + upScale, backgroundColor)); } [Obsolete("Use the UrlHelper.GetCropUrl extension instead")] @@ -252,12 +253,13 @@ namespace Umbraco.Web string cacheBusterValue = null, string furtherOptions = null, ImageCropRatioMode? ratioMode = null, - bool upScale = true) + bool upScale = true, + string backgroundColor = null) { return new HtmlString(imageUrl.GetCropUrl(width, height, imageCropperValue, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, - upScale)); + upScale, backgroundColor)); } #endregion diff --git a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs index e518c5e246..08781076dc 100644 --- a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs +++ b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs @@ -81,7 +81,7 @@ namespace Umbraco.Web /// Use focal point, to generate an output image using the focal point instead of the predefined crop /// /// - /// Use crop dimensions to have the output image sized according to the predefined crop sizes, this will override the width and height parameters>. + /// Use crop dimensions to have the output image sized according to the predefined crop sizes, this will override the width and height parameters. /// /// /// Add a serialised date of the last edit of the item to ensure client cache refresh when updated @@ -91,10 +91,13 @@ namespace Umbraco.Web /// /// /// Use a dimension as a ratio - /// + /// /// /// If the image should be upscaled to requested dimensions - /// + /// + /// + /// Changes the background color of the image. Used when adding a background when resizing image formats without an alpha channel. + /// /// /// The . /// @@ -112,7 +115,8 @@ namespace Umbraco.Web bool cacheBuster = true, string furtherOptions = null, ImageCropRatioMode? ratioMode = null, - bool upScale = true) + bool upScale = true, + string backgroundColor = null) { if (mediaItem == null) throw new ArgumentNullException("mediaItem"); @@ -132,7 +136,7 @@ namespace Umbraco.Web mediaItemUrl = stronglyTyped.Src; return GetCropUrl( mediaItemUrl, stronglyTyped, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, - cacheBusterValue, furtherOptions, ratioMode, upScale); + cacheBusterValue, furtherOptions, ratioMode, upScale, backgroundColor); } //this shouldn't be the case but we'll check @@ -143,14 +147,14 @@ namespace Umbraco.Web mediaItemUrl = stronglyTyped.Src; return GetCropUrl( mediaItemUrl, stronglyTyped, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, - cacheBusterValue, furtherOptions, ratioMode, upScale); + cacheBusterValue, furtherOptions, ratioMode, upScale, backgroundColor); } //it's a single string mediaItemUrl = cropperValue.ToString(); return GetCropUrl( mediaItemUrl, width, height, mediaItemUrl, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, - cacheBusterValue, furtherOptions, ratioMode, upScale); + cacheBusterValue, furtherOptions, ratioMode, upScale, backgroundColor); } /// @@ -198,6 +202,9 @@ namespace Umbraco.Web /// /// If the image should be upscaled to requested dimensions /// + /// + /// Changes the background color of the image. Used when adding a background when resizing image formats without an alpha channel. + /// /// /// The . /// @@ -215,7 +222,8 @@ namespace Umbraco.Web string cacheBusterValue = null, string furtherOptions = null, ImageCropRatioMode? ratioMode = null, - bool upScale = true) + bool upScale = true, + string backgroundColor = null) { if (string.IsNullOrEmpty(imageUrl)) return string.Empty; @@ -226,7 +234,7 @@ namespace Umbraco.Web } return GetCropUrl( imageUrl, cropDataSet, width, height, cropAlias, quality, imageCropMode, - imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); + imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale, backgroundColor); } public static string GetCropUrl( @@ -243,7 +251,8 @@ namespace Umbraco.Web string cacheBusterValue = null, string furtherOptions = null, ImageCropRatioMode? ratioMode = null, - bool upScale = true) + bool upScale = true, + string backgroundColor = null) { if (string.IsNullOrEmpty(imageUrl) == false) { @@ -350,6 +359,11 @@ namespace Umbraco.Web imageProcessorUrl.Append("&upscale=false"); } + if (backgroundColor != null) + { + imageProcessorUrl.Append("&bgcolor=" + backgroundColor); + } + if (furtherOptions != null) { imageProcessorUrl.Append(furtherOptions); diff --git a/src/Umbraco.Web/Models/ContentEditing/TemplateDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/TemplateDisplay.cs index 8e813bd3d3..91c1aefdb0 100644 --- a/src/Umbraco.Web/Models/ContentEditing/TemplateDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/TemplateDisplay.cs @@ -36,6 +36,9 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "masterTemplateAlias")] public string MasterTemplateAlias { get; set; } + [DataMember(Name = "isMasterTemplate")] + public bool IsMasterTemplate { get; set; } + /// /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. /// diff --git a/src/Umbraco.Web/Properties/AssemblyInfo.cs b/src/Umbraco.Web/Properties/AssemblyInfo.cs index 89499b833e..7b30b1f4b3 100644 --- a/src/Umbraco.Web/Properties/AssemblyInfo.cs +++ b/src/Umbraco.Web/Properties/AssemblyInfo.cs @@ -34,6 +34,9 @@ using System.Security; [assembly: InternalsVisibleTo("Concorde.Sync")] [assembly: InternalsVisibleTo("Umbraco.Courier.Core")] [assembly: InternalsVisibleTo("Umbraco.Courier.Persistence")] +[assembly: InternalsVisibleTo("Umbraco.Deploy")] +[assembly: InternalsVisibleTo("Umbraco.Deploy.UI")] +[assembly: InternalsVisibleTo("Umbraco.Deploy.Cloud")] [assembly: InternalsVisibleTo("Umbraco.VisualStudio")] [assembly: InternalsVisibleTo("Umbraco.ModelsBuilder")] [assembly: InternalsVisibleTo("Umbraco.ModelsBuilder.AspNet")] diff --git a/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs index 4d58060164..be10cd83e2 100644 --- a/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ContentPickerPropertyEditor.cs @@ -33,17 +33,12 @@ namespace Umbraco.Web.PropertyEditors internal class ContentPickerPreValueEditor : PreValueEditor { - [PreValueField("showOpenButton", "Show open button", "boolean")] + [PreValueField("showOpenButton", "Show open button (this feature is in preview!)", "boolean", Description = " Opens the node in a dialog")] public string ShowOpenButton { get; set; } - - [PreValueField("showEditButton", "Show edit button (this feature is in preview!)", "boolean")] - public string ShowEditButton { get; set; } [PreValueField("startNodeId", "Start node", "treepicker")] public int StartNodeId { get; set; } - [PreValueField("showPathOnHover", "Show path when hovering items", "boolean")] - public bool ShowPathOnHover { get; set; } } } } \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs index b40cf3bbf3..af30b4ceeb 100644 --- a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs @@ -31,6 +31,8 @@ namespace Umbraco.Web.PropertyEditors { try { + //TODO: We should deserialize this to Umbraco.Core.Models.GridValue instead of doing the below + var json = JsonConvert.DeserializeObject(e.Fields[field.Name]); //check if this is formatted for grid json @@ -92,7 +94,12 @@ namespace Umbraco.Web.PropertyEditors //swallow...on purpose, there's a chance that this isn't json and we don't want that to affect // the website. } + catch (ArgumentException) + { + //swallow on purpose to prevent this error: + // Can not add Newtonsoft.Json.Linq.JValue to Newtonsoft.Json.Linq.JObject. + } } } } diff --git a/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs index b3d3f02448..82daf80e34 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiNodeTreePickerPropertyEditor.cs @@ -45,16 +45,9 @@ namespace Umbraco.Web.PropertyEditors [PreValueField("maxNumber", "Maximum number of items", "number")] public string MaxNumber { get; set; } - - [PreValueField("showOpenButton", "Show open button", "boolean")] + [PreValueField("showOpenButton", "Show open button (this feature is in preview!)", "boolean", Description = " Opens the node in a dialog")] public string ShowOpenButton { get; set; } - [PreValueField("showEditButton", "Show edit button (this feature is in preview!)", "boolean")] - public string ShowEditButton { get; set; } - - [PreValueField("showPathOnHover", "Show path when hovering items", "boolean")] - public bool ShowPathOnHover { get; set; } - /// /// This ensures the multiPicker pre-val is set based on the maxNumber of nodes set /// diff --git a/src/Umbraco.Web/Trees/ContentTreeController.cs b/src/Umbraco.Web/Trees/ContentTreeController.cs index 9f33a44ea9..9a5f90ff89 100644 --- a/src/Umbraco.Web/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTreeController.cs @@ -202,11 +202,13 @@ namespace Umbraco.Web.Trees /// protected override bool HasPathAccess(string id, FormDataCollection queryStrings) { - var content = Services.ContentService.GetById(int.Parse(id)); - if (content == null) + var entity = GetEntityFromId(id); + if (entity == null) { return false; } + + IContent content = Services.ContentService.GetById(entity.Id); return Security.CurrentUser.HasPathAccess(content); } diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index 3708c6fd4f..027e4e488c 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -96,7 +96,7 @@ namespace Umbraco.Web.Trees LogHelper.Warn("The user " + Security.CurrentUser.Username + " does not have access to the tree node " + id); return new TreeNodeCollection(); } - + // So there's an alt id specified, it's not the root node and the user has access to it, great! But there's one thing we // need to consider: // If the tree is being rendered in a dialog view we want to render only the children of the specified id, but @@ -110,7 +110,7 @@ namespace Umbraco.Web.Trees id = Constants.System.Root.ToString(CultureInfo.InvariantCulture); } } - + var entities = GetChildEntities(id); nodes.AddRange(entities.Select(entity => GetSingleTreeNode(entity, id, queryStrings)).Where(node => node != null)); return nodes; @@ -122,11 +122,24 @@ namespace Umbraco.Web.Trees protected IEnumerable GetChildEntities(string id) { + // use helper method to ensure we support both integer and guid lookups int iid; - if (int.TryParse(id, out iid) == false) + + // if it's the root node, we won't use the look up + if (id != "-1") { - throw new InvalidCastException("The id for the media tree must be an integer"); + var idEntity = GetEntityFromId(id); + if (idEntity == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + iid = idEntity.Id; } + else + { + iid = int.Parse(id); + } + //if a request is made for the root node data but the user's start node is not the default, then // we need to return their start node data @@ -134,11 +147,12 @@ namespace Umbraco.Web.Trees { //just return their single start node, it will show up under the 'Content' label var startNode = Services.EntityService.Get(UserStartNode, UmbracoObjectType); - if (startNode == null) + if (startNode != null) + return new[] { startNode }; + else { throw new EntityNotFoundException(UserStartNode, "User's start content node could not be found"); } - return new[] { startNode }; } return Services.EntityService.GetChildren(iid, UmbracoObjectType).ToArray(); @@ -176,9 +190,9 @@ namespace Umbraco.Web.Trees { id = altStartId; } - + var nodes = GetTreeNodesInternal(id, queryStrings); - + //only render the recycle bin if we are not in dialog and the start id id still the root if (IsDialog(queryStrings) == false && id == Constants.System.Root.ToInvariantString()) { @@ -210,8 +224,9 @@ namespace Umbraco.Web.Trees /// private TreeNodeCollection GetTreeNodesInternal(string id, FormDataCollection queryStrings) { + IUmbracoEntity current = GetEntityFromId(id); + //before we get the children we need to see if this is a container node - var current = Services.EntityService.Get(int.Parse(id), UmbracoObjectType); //test if the parent is a listview / container if (current != null && current.IsContainer()) @@ -286,5 +301,32 @@ namespace Umbraco.Web.Trees { return allowedUserOptions.Select(x => x.Action).OfType().Any(); } + + /// + /// Get an entity via an id that can be either an integer or a Guid + /// + /// + /// + internal IUmbracoEntity GetEntityFromId(string id) + { + IUmbracoEntity entity; + Guid idGuid = Guid.Empty; + int idInt; + if (Guid.TryParse(id, out idGuid)) + { + entity = Services.EntityService.GetByKey(idGuid, UmbracoObjectType); + + } + else if (int.TryParse(id, out idInt)) + { + entity = Services.EntityService.Get(idInt, UmbracoObjectType); + } + else + { + return null; + } + + return entity; + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 2160297c71..e4bb3c9e88 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -112,8 +112,9 @@ ..\packages\dotless.1.4.1.0\lib\dotless.Core.dll - - ..\packages\Examine.0.1.70.0\lib\Examine.dll + + ..\packages\Examine.0.1.80\lib\net45\Examine.dll + True ..\packages\HtmlAgilityPack.1.4.9\lib\Net45\HtmlAgilityPack.dll diff --git a/src/Umbraco.Web/UrlHelperRenderExtensions.cs b/src/Umbraco.Web/UrlHelperRenderExtensions.cs index 629d69ed4a..76a000aded 100644 --- a/src/Umbraco.Web/UrlHelperRenderExtensions.cs +++ b/src/Umbraco.Web/UrlHelperRenderExtensions.cs @@ -114,6 +114,9 @@ namespace Umbraco.Web /// Whether to HTML encode this URL - default is true - w3c standards require html attributes to be html encoded but this can be /// set to false if using the result of this method for CSS. /// + /// + /// Changes the background color of the image. Used when adding a background when resizing image formats without an alpha channel. + /// /// /// The . /// @@ -132,11 +135,12 @@ namespace Umbraco.Web string furtherOptions = null, ImageCropRatioMode? ratioMode = null, bool upScale = true, - bool htmlEncode = true) + bool htmlEncode = true, + string backgroundColor = null) { var url = mediaItem.GetCropUrl(width, height, propertyAlias, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, ratioMode, - upScale); + upScale, backgroundColor); return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url); } @@ -190,6 +194,9 @@ namespace Umbraco.Web /// Whether to HTML encode this URL - default is true - w3c standards require html attributes to be html encoded but this can be /// set to false if using the result of this method for CSS. /// + /// + /// Changes the background color of the image. Used when adding a background when resizing image formats without an alpha channel. + /// /// /// The . /// @@ -208,11 +215,12 @@ namespace Umbraco.Web string furtherOptions = null, ImageCropRatioMode? ratioMode = null, bool upScale = true, - bool htmlEncode = true) + bool htmlEncode = true, + string backgroundColor = null) { var url = imageUrl.GetCropUrl(width, height, imageCropperValue, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, - upScale); + upScale, backgroundColor); return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url); } @@ -333,7 +341,5 @@ namespace Umbraco.Web { return url.SurfaceAction(action, typeof (T), additionalRouteVals); } - - } } \ No newline at end of file diff --git a/src/Umbraco.Web/WebServices/ExamineManagementApiController.cs b/src/Umbraco.Web/WebServices/ExamineManagementApiController.cs index 9c50468465..79e7fe8f53 100644 --- a/src/Umbraco.Web/WebServices/ExamineManagementApiController.cs +++ b/src/Umbraco.Web/WebServices/ExamineManagementApiController.cs @@ -166,6 +166,8 @@ namespace Umbraco.Web.WebServices var msg = ValidateLuceneIndexer(indexerName, out indexer); if (msg.IsSuccessStatusCode) { + LogHelper.Info(string.Format("Rebuilding index '{0}'", indexerName)); + //remove it in case there's a handler there alraedy indexer.IndexOperationComplete -= Indexer_IndexOperationComplete; //now add a single handler @@ -201,6 +203,8 @@ namespace Umbraco.Web.WebServices //ensure it's not listening anymore indexer.IndexOperationComplete -= Indexer_IndexOperationComplete; + LogHelper.Info(string.Format("Rebuilding index '{0}' done, {1} items committed (can differ from the number of items in the index)", indexer.Name, indexer.CommitCount)); + var cacheKey = "temp_indexing_op_" + indexer.Name; ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(cacheKey); } @@ -266,7 +270,10 @@ namespace Umbraco.Web.WebServices var val = p.GetValue(indexer, null); if (val == null) { - LogHelper.Warn("Property value was null when setting up property on indexer: " + indexer.Name + " property: " + p.Name); + // Do not warn for new new attribute that is optional + if(string.Equals(p.Name, "DirectoryFactory", StringComparison.InvariantCultureIgnoreCase) == false) + LogHelper.Warn("Property value was null when setting up property on indexer: " + indexer.Name + " property: " + p.Name); + val = string.Empty; } indexerModel.ProviderProperties.Add(p.Name, val.ToString()); diff --git a/src/Umbraco.Web/packages.config b/src/Umbraco.Web/packages.config index 520a6e4f76..1382e9ceea 100644 --- a/src/Umbraco.Web/packages.config +++ b/src/Umbraco.Web/packages.config @@ -3,7 +3,7 @@ - + diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/controls/ContentTypeControl.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/controls/ContentTypeControl.cs index 5a8b0b616e..c912db701d 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/controls/ContentTypeControl.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/controls/ContentTypeControl.cs @@ -2,6 +2,7 @@ using System; using System.Web.UI.HtmlControls; using System.Web.UI.WebControls; using System.Collections; +using System.ComponentModel; using System.IO; using Umbraco.Core.IO; using System.Linq; @@ -11,6 +12,8 @@ namespace umbraco.controls /// /// Summary description for ContentTypeControl. /// + [Obsolete("No longer used, will be removed in v8")] + [EditorBrowsable(EditorBrowsableState.Never)] public class ContentTypeControl : uicontrols.TabView { public event System.EventHandler OnSave; diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs index 9734401d95..91a8677c81 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/users/EditUser.aspx.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Configuration.Provider; using System.Globalization; using System.IO; +using System.Linq; using System.Web; using System.Web.Security; using System.Web.UI; @@ -320,13 +321,14 @@ namespace umbraco.cms.presentation.user } // Populate dropdowns - foreach (DocumentType dt in DocumentType.GetAllAsList()) - cDocumentType.Items.Add( - new ListItem(dt.Text, dt.Alias) - ); + var allContentTypes = Services.ContentTypeService.GetAllContentTypes().ToList(); + foreach (var dt in allContentTypes) + { + cDocumentType.Items.Add(new ListItem(dt.Name, dt.Alias)); + } // populate fields - ArrayList fields = new ArrayList(); + var fields = new ArrayList(); cDescription.ID = "cDescription"; cCategories.ID = "cCategories"; cExcerpt.ID = "cExcerpt"; @@ -334,9 +336,9 @@ namespace umbraco.cms.presentation.user cCategories.Items.Add(new ListItem(ui.Text("choose"), "")); cExcerpt.Items.Add(new ListItem(ui.Text("choose"), "")); - foreach (PropertyType pt in PropertyType.GetAll()) + foreach (var pt in allContentTypes.SelectMany(x => x.PropertyTypes).OrderBy(x => x.Name)) { - if (!fields.Contains(pt.Alias)) + if (fields.Contains(pt.Alias) == false) { cDescription.Items.Add(new ListItem(string.Format("{0} ({1})", pt.Name, pt.Alias), pt.Alias)); cCategories.Items.Add(new ListItem(string.Format("{0} ({1})", pt.Name, pt.Alias), pt.Alias)); diff --git a/src/UmbracoExamine/BaseUmbracoIndexer.cs b/src/UmbracoExamine/BaseUmbracoIndexer.cs index d05a47099d..1d87b0dfff 100644 --- a/src/UmbracoExamine/BaseUmbracoIndexer.cs +++ b/src/UmbracoExamine/BaseUmbracoIndexer.cs @@ -55,6 +55,7 @@ namespace UmbracoExamine /// /// /// + /// protected BaseUmbracoIndexer(IIndexCriteria indexerData, DirectoryInfo indexPath, IDataService dataService, Analyzer analyzer, bool async) : base(indexerData, indexPath, analyzer, async) { @@ -67,6 +68,19 @@ namespace UmbracoExamine DataService = dataService; } + /// + /// Creates an NRT indexer + /// + /// + /// + /// + /// + protected BaseUmbracoIndexer(IIndexCriteria indexerData, IndexWriter writer, IDataService dataService, bool async) + : base(indexerData, writer, async) + { + DataService = dataService; + } + #endregion /// @@ -101,7 +115,7 @@ namespace UmbracoExamine /// Determines if the manager will call the indexing methods when content is saved or deleted as /// opposed to cache being updated. /// - public bool SupportUnpublishedContent { get; protected set; } + public bool SupportUnpublishedContent { get; protected internal set; } /// /// The data service used for retreiving and submitting data to the cms diff --git a/src/UmbracoExamine/UmbracoContentIndexer.cs b/src/UmbracoExamine/UmbracoContentIndexer.cs index efc3e4a214..e7df641122 100644 --- a/src/UmbracoExamine/UmbracoContentIndexer.cs +++ b/src/UmbracoExamine/UmbracoContentIndexer.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; +using System.Diagnostics; using System.IO; using System.Linq; +using System.Xml; using System.Xml.Linq; using Examine; using Lucene.Net.Documents; @@ -15,6 +17,7 @@ using Examine.LuceneEngine; using Examine.LuceneEngine.Config; using UmbracoExamine.Config; using Lucene.Net.Analysis; +using Lucene.Net.Index; using Umbraco.Core.Persistence.Querying; using IContentService = Umbraco.Core.Services.IContentService; using IMediaService = Umbraco.Core.Services.IMediaService; @@ -144,6 +147,34 @@ namespace UmbracoExamine _contentTypeService = contentTypeService; } + /// + /// Creates an NRT indexer + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public UmbracoContentIndexer(IIndexCriteria indexerData, IndexWriter writer, IDataService dataService, + IContentService contentService, + IMediaService mediaService, + IDataTypeService dataTypeService, + IUserService userService, + IContentTypeService contentTypeService, + bool async) + : base(indexerData, writer, dataService, async) + { + _contentService = contentService; + _mediaService = mediaService; + _dataTypeService = dataTypeService; + _userService = userService; + _contentTypeService = contentTypeService; + } + #endregion #region Constants & Fields @@ -229,7 +260,13 @@ namespace UmbracoExamine SupportProtectedContent = supportProtected; else SupportProtectedContent = false; - + + bool disableXmlDocLookup; + if (config["disableXmlDocLookup"] != null && bool.TryParse(config["disableXmlDocLookup"], out disableXmlDocLookup)) + DisableXmlDocumentLookup = disableXmlDocLookup; + else + DisableXmlDocumentLookup = false; + base.Initialize(name, config); } @@ -237,6 +274,11 @@ namespace UmbracoExamine #region Properties + /// + /// Whether to use the cmsContentXml data to re-index when possible (i.e. for published content, media and members) + /// + public bool DisableXmlDocumentLookup { get; private set; } + /// /// By default this is false, if set to true then the indexer will include indexing content that is flagged as publicly protected. /// This property is ignored if SupportUnpublishedContent is set to true. @@ -359,131 +401,250 @@ namespace UmbracoExamine } #endregion - #region Protected - - /// - /// This is a static query, it's parameters don't change so store statically - /// - private IQuery _publishedQuery; + #region Protected protected override void PerformIndexAll(string type) { + if (SupportedTypes.Contains(type) == false) + return; + const int pageSize = 10000; var pageIndex = 0; - switch (type) + DataService.LogService.AddInfoLog(-1, string.Format("PerformIndexAll - Start data queries - {0}", type)); + var stopwatch = new Stopwatch(); + stopwatch.Start(); + + try { - case IndexTypes.Content: - var contentParentId = -1; - if (IndexerData.ParentNodeId.HasValue && IndexerData.ParentNodeId.Value > 0) - { - contentParentId = IndexerData.ParentNodeId.Value; - } - IContent[] content; - - //used to track non-published entities so we can determine what items are implicitly not published - var notPublished = new HashSet(); - - do - { - long total; - - IEnumerable descendants; - if (SupportUnpublishedContent) + switch (type) + { + case IndexTypes.Content: + var contentParentId = -1; + if (IndexerData.ParentNodeId.HasValue && IndexerData.ParentNodeId.Value > 0) { - descendants = _contentService.GetPagedDescendants(contentParentId, pageIndex, pageSize, out total); - } - else - { - if (_publishedQuery == null) - { - _publishedQuery = Query.Builder.Where(x => x.Published == true); - } - - //get all paged records but order by level ascending, we need to do this because we need to track which nodes are not published so that we can determine - // which descendent nodes are implicitly not published - descendants = _contentService.GetPagedDescendants(contentParentId, pageIndex, pageSize, out total, "level", Direction.Ascending, true, (string)null); - } - - //if specific types are declared we need to post filter them - //TODO: Update the service layer to join the cmsContentType table so we can query by content type too - if (IndexerData.IncludeNodeTypes.Any()) - { - content = descendants.Where(x => IndexerData.IncludeNodeTypes.Contains(x.ContentType.Alias)).ToArray(); - } - else - { - content = descendants.ToArray(); - } - AddNodesToIndex(GetSerializedContent(content, notPublished).WhereNotNull(), type); - pageIndex++; - } while (content.Length == pageSize); - - notPublished.Clear(); - - break; - case IndexTypes.Media: - var mediaParentId = -1; - - if (IndexerData.ParentNodeId.HasValue && IndexerData.ParentNodeId.Value > 0) - { - mediaParentId = IndexerData.ParentNodeId.Value; - } - - XElement[] mediaXElements; - - var mediaTypes = _contentTypeService.GetAllMediaTypes().ToArray(); - var icons = mediaTypes.ToDictionary(x => x.Id, y => y.Icon); - - do - { - long total; - if (mediaParentId == -1) - { - mediaXElements = _mediaService.GetPagedXmlEntries("-1", pageIndex, pageSize, out total).ToArray(); - } - else - { - //Get the parent - var parent = _mediaService.GetById(mediaParentId); - if (parent == null) - mediaXElements = new XElement[0]; - else - mediaXElements = _mediaService.GetPagedXmlEntries(parent.Path, pageIndex, pageSize, out total).ToArray(); - } - - //if specific types are declared we need to post filter them - //TODO: Update the service layer to join the cmsContentType table so we can query by content type too - if (IndexerData.IncludeNodeTypes.Any()) - { - var includeNodeTypeIds = mediaTypes.Where(x => IndexerData.IncludeNodeTypes.Contains(x.Alias)).Select(x => x.Id); - mediaXElements = mediaXElements.Where(elm => includeNodeTypeIds.Contains(elm.AttributeValue("nodeType"))).ToArray(); + contentParentId = IndexerData.ParentNodeId.Value; } - foreach (var element in mediaXElements) + if (SupportUnpublishedContent == false && DisableXmlDocumentLookup == false) { - element.Add(new XAttribute("icon", icons[element.AttributeValue("nodeType")])); + //get all node Ids that have a published version - this is a fail safe check, in theory + // only document nodes that have a published version would exist in the cmsContentXml table + var allNodesWithPublishedVersions = ApplicationContext.Current.DatabaseContext.Database.Fetch( + "select DISTINCT cmsDocument.nodeId from cmsDocument where cmsDocument.published = 1"); + + XElement last = null; + var trackedIds = new HashSet(); + + ReindexWithXmlEntries(type, contentParentId, + () => _contentTypeService.GetAllContentTypes().ToArray(), + (path, pIndex, pSize) => + { + long totalContent; + + //sorted by: umbracoNode.level, umbracoNode.parentID, umbracoNode.sortOrder + var result = _contentService.GetPagedXmlEntries(path, pIndex, pSize, out totalContent).ToArray(); + + //then like we do in the ContentRepository.BuildXmlCache we need to track what Parents have been processed + // already so that we can then exclude implicitly unpublished content items + var filtered = new List(); + + foreach (var xml in result) + { + var id = xml.AttributeValue("id"); + + //don't include this if it doesn't have a published version + if (allNodesWithPublishedVersions.Contains(id) == false) + continue; + + var parentId = xml.AttributeValue("parentID"); + + if (parentId == null) continue; //this shouldn't happen + + //if the parentid is changing + if (last != null && last.AttributeValue("parentID") != parentId) + { + var found = trackedIds.Contains(parentId); + if (found == false) + { + //Need to short circuit here, if the parent is not there it means that the parent is unpublished + // and therefore the child is not published either so cannot be included in the xml cache + continue; + } + } + + last = xml; + trackedIds.Add(xml.AttributeValue("id")); + + filtered.Add(xml); + } + + return new Tuple(totalContent, filtered.ToArray()); + }, + i => _contentService.GetById(i)); + } + else + { + //used to track non-published entities so we can determine what items are implicitly not published + //currently this is not in use apart form in tests + var notPublished = new HashSet(); + + int currentPageSize; + do + { + long total; + + IContent[] descendants; + if (SupportUnpublishedContent) + { + descendants = _contentService.GetPagedDescendants(contentParentId, pageIndex, pageSize, out total, "umbracoNode.id").ToArray(); + } + else + { + //get all paged records but order by level ascending, we need to do this because we need to track which nodes are not published so that we can determine + // which descendent nodes are implicitly not published + descendants = _contentService.GetPagedDescendants(contentParentId, pageIndex, pageSize, out total, "level", Direction.Ascending, true, (string)null).ToArray(); + } + + // need to store decendants count before filtering, in order for loop to work correctly + currentPageSize = descendants.Length; + + //if specific types are declared we need to post filter them + //TODO: Update the service layer to join the cmsContentType table so we can query by content type too + IEnumerable content; + if (IndexerData.IncludeNodeTypes.Any()) + { + content = descendants.Where(x => IndexerData.IncludeNodeTypes.Contains(x.ContentType.Alias)); + } + else + { + content = descendants; + } + + AddNodesToIndex(GetSerializedContent( + SupportUnpublishedContent, + c => _serializer.Serialize(_contentService, _dataTypeService, _userService, c), + content, notPublished).WhereNotNull(), type); + + pageIndex++; + } while (currentPageSize == pageSize); } - AddNodesToIndex(mediaXElements, type); - pageIndex++; - } while (mediaXElements.Length == pageSize); + break; + case IndexTypes.Media: + var mediaParentId = -1; - break; + if (IndexerData.ParentNodeId.HasValue && IndexerData.ParentNodeId.Value > 0) + { + mediaParentId = IndexerData.ParentNodeId.Value; + } + + ReindexWithXmlEntries(type, mediaParentId, + () => _contentTypeService.GetAllMediaTypes().ToArray(), + (path, pIndex, pSize) => + { + long totalMedia; + var result = _mediaService.GetPagedXmlEntries(path, pIndex, pSize, out totalMedia).ToArray(); + return new Tuple(totalMedia, result); + }, + i => _mediaService.GetById(i)); + + break; + } } + finally + { + stopwatch.Stop(); + } + + DataService.LogService.AddInfoLog(-1, string.Format("PerformIndexAll - End data queries - {0}, took {1}ms", type, stopwatch.ElapsedMilliseconds)); } - private IEnumerable GetSerializedContent(IEnumerable content, ISet notPublished) + /// + /// Performs a reindex of a type based on looking up entries from the cmsContentXml table - but using callbacks to get this data since + /// we don't have a common underlying service interface for the media/content stuff + /// + /// + /// + /// + /// + /// + internal void ReindexWithXmlEntries( + string type, + int parentId, + Func getContentTypes, + Func> getPagedXmlEntries, + Func getContent) + where TContentType: IContentTypeComposition + { + const int pageSize = 10000; + var pageIndex = 0; + + XElement[] xElements; + + var contentTypes = getContentTypes(); + var icons = contentTypes.ToDictionary(x => x.Id, y => y.Icon); + + do + { + long total; + if (parentId == -1) + { + var pagedElements = getPagedXmlEntries("-1", pageIndex, pageSize); + total = pagedElements.Item1; + xElements = pagedElements.Item2; + } + else + { + //Get the parent + var parent = getContent(parentId); + if (parent == null) + xElements = new XElement[0]; + else + { + var pagedElements = getPagedXmlEntries(parent.Path, pageIndex, pageSize); + total = pagedElements.Item1; + xElements = pagedElements.Item2; + } + } + + //if specific types are declared we need to post filter them + //TODO: Update the service layer to join the cmsContentType table so we can query by content type too + if (IndexerData.IncludeNodeTypes.Any()) + { + var includeNodeTypeIds = contentTypes.Where(x => IndexerData.IncludeNodeTypes.Contains(x.Alias)).Select(x => x.Id); + xElements = xElements.Where(elm => includeNodeTypeIds.Contains(elm.AttributeValue("nodeType"))).ToArray(); + } + + foreach (var element in xElements) + { + if (element.Attribute("icon") == null) + { + element.Add(new XAttribute("icon", icons[element.AttributeValue("nodeType")])); + } + } + + AddNodesToIndex(xElements, type); + pageIndex++; + } while (xElements.Length == pageSize); + } + + internal static IEnumerable GetSerializedContent( + bool supportUnpublishdContent, + Func serializer, + IEnumerable content, + ISet notPublished) { foreach (var c in content) { - if (SupportUnpublishedContent == false) + if (supportUnpublishdContent == false) { //if we don't support published content and this is not published then track it and return null if (c.Published == false) { notPublished.Add(c.Path); yield return null; + continue; } //if we don't support published content, check if this content item exists underneath any already tracked @@ -491,14 +652,11 @@ namespace UmbracoExamine if (notPublished.Any(path => c.Path.StartsWith(string.Format("{0},", path)))) { yield return null; + continue; } - } + } - var xml = _serializer.Serialize( - _contentService, - _dataTypeService, - _userService, - c); + var xml = serializer(c); //add a custom 'icon' attribute xml.Add(new XAttribute("icon", c.ContentType.Icon)); @@ -520,7 +678,7 @@ namespace UmbracoExamine public override void RebuildIndex() { - DataService.LogService.AddVerboseLog(-1, "Rebuilding index"); + DataService.LogService.AddInfoLog(-1, "Rebuilding index"); base.RebuildIndex(); } diff --git a/src/UmbracoExamine/UmbracoExamine.csproj b/src/UmbracoExamine/UmbracoExamine.csproj index 1bc438edd7..8720849744 100644 --- a/src/UmbracoExamine/UmbracoExamine.csproj +++ b/src/UmbracoExamine/UmbracoExamine.csproj @@ -82,8 +82,9 @@ ..\Solution Items\TheFARM-Public.snk - - ..\packages\Examine.0.1.70.0\lib\Examine.dll + + ..\packages\Examine.0.1.80\lib\net45\Examine.dll + True ..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll diff --git a/src/UmbracoExamine/UmbracoExamineSearcher.cs b/src/UmbracoExamine/UmbracoExamineSearcher.cs index 7a2a80c3fe..26b02c904e 100644 --- a/src/UmbracoExamine/UmbracoExamineSearcher.cs +++ b/src/UmbracoExamine/UmbracoExamineSearcher.cs @@ -51,12 +51,41 @@ namespace UmbracoExamine /// public override string Name { - get - { - return _name; - } + get { return _name; } + } + + /// + /// Constructor to allow for creating an indexer at runtime + /// + /// + /// + + public UmbracoExamineSearcher(DirectoryInfo indexPath, Analyzer analyzer) + : base(indexPath, analyzer) + { } + /// + /// Constructor to allow for creating an indexer at runtime + /// + /// + /// + public UmbracoExamineSearcher(Lucene.Net.Store.Directory luceneDirectory, Analyzer analyzer) + : base(luceneDirectory, analyzer) + { + } + + /// + /// Creates an NRT searcher + /// + /// + /// + public UmbracoExamineSearcher(IndexWriter writer, Analyzer analyzer) + : base(writer, analyzer) + { + } + + #endregion public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config) { @@ -115,30 +144,6 @@ namespace UmbracoExamine } } - /// - /// Constructor to allow for creating an indexer at runtime - /// - /// - /// - - public UmbracoExamineSearcher(DirectoryInfo indexPath, Analyzer analyzer) - : base(indexPath, analyzer) - { - } - - /// - /// Constructor to allow for creating an indexer at runtime - /// - /// - /// - - public UmbracoExamineSearcher(Lucene.Net.Store.Directory luceneDirectory, Analyzer analyzer) - : base(luceneDirectory, analyzer) - { - } - - #endregion - /// /// Used for unit tests /// diff --git a/src/UmbracoExamine/UmbracoMemberIndexer.cs b/src/UmbracoExamine/UmbracoMemberIndexer.cs index 64a574822f..2e48dff64e 100644 --- a/src/UmbracoExamine/UmbracoMemberIndexer.cs +++ b/src/UmbracoExamine/UmbracoMemberIndexer.cs @@ -9,6 +9,8 @@ using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Services; using UmbracoExamine.Config; using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; using Examine; using System.IO; using UmbracoExamine.DataServices; @@ -24,6 +26,7 @@ namespace UmbracoExamine { private readonly IMemberService _memberService; + private readonly IMemberTypeService _memberTypeService; private readonly IDataTypeService _dataTypeService; /// @@ -33,6 +36,7 @@ namespace UmbracoExamine { _dataTypeService = ApplicationContext.Current.Services.DataTypeService; _memberService = ApplicationContext.Current.Services.MemberService; + _memberTypeService = ApplicationContext.Current.Services.MemberTypeService; } /// @@ -48,6 +52,7 @@ namespace UmbracoExamine { _dataTypeService = ApplicationContext.Current.Services.DataTypeService; _memberService = ApplicationContext.Current.Services.MemberService; + _memberTypeService = ApplicationContext.Current.Services.MemberTypeService; } /// @@ -60,6 +65,8 @@ namespace UmbracoExamine /// /// /// + [Obsolete("Use the ctor specifying all dependencies instead")] + [EditorBrowsable(EditorBrowsableState.Never)] public UmbracoMemberIndexer(IIndexCriteria indexerData, DirectoryInfo indexPath, IDataService dataService, IDataTypeService dataTypeService, IMemberService memberService, @@ -68,9 +75,31 @@ namespace UmbracoExamine { _dataTypeService = dataTypeService; _memberService = memberService; + _memberTypeService = ApplicationContext.Current.Services.MemberTypeService; } - + /// + /// Constructor to allow for creating an indexer at runtime + /// + /// + /// + /// + /// + /// + /// + /// + /// + public UmbracoMemberIndexer(IIndexCriteria indexerData, DirectoryInfo indexPath, IDataService dataService, + IDataTypeService dataTypeService, + IMemberService memberService, + IMemberTypeService memberTypeService, + Analyzer analyzer, bool async) + : base(indexerData, indexPath, dataService, analyzer, async) + { + _dataTypeService = dataTypeService; + _memberService = memberService; + _memberTypeService = memberTypeService; + } /// /// Ensures that the'_searchEmail' is added to the user fields so that it is indexed - without having to modify the config @@ -129,40 +158,68 @@ namespace UmbracoExamine if (SupportedTypes.Contains(type) == false) return; - const int pageSize = 1000; - var pageIndex = 0; + DataService.LogService.AddInfoLog(-1, string.Format("PerformIndexAll - Start data queries - {0}", type)); + var stopwatch = new Stopwatch(); + stopwatch.Start(); - IMember[] members; - - if (IndexerData.IncludeNodeTypes.Any()) + try { - //if there are specific node types then just index those - foreach (var nodeType in IndexerData.IncludeNodeTypes) + if (DisableXmlDocumentLookup == false) { - do + ReindexWithXmlEntries(type, -1, + () => _memberTypeService.GetAll().ToArray(), + (path, pIndex, pSize) => + { + long totalContent; + var result = _memberService.GetPagedXmlEntries(pIndex, pSize, out totalContent).ToArray(); + return new Tuple(totalContent, result); + }, + i => _memberService.GetById(i)); + } + else + { + const int pageSize = 1000; + var pageIndex = 0; + + IMember[] members; + + if (IndexerData.IncludeNodeTypes.Any()) { - long total; - members = _memberService.GetAll(pageIndex, pageSize, out total, "LoginName", Direction.Ascending, true, null, nodeType).ToArray(); + //if there are specific node types then just index those + foreach (var nodeType in IndexerData.IncludeNodeTypes) + { + do + { + long total; + members = _memberService.GetAll(pageIndex, pageSize, out total, "LoginName", Direction.Ascending, true, null, nodeType).ToArray(); - AddNodesToIndex(GetSerializedMembers(members), type); + AddNodesToIndex(GetSerializedMembers(members), type); - pageIndex++; - } while (members.Length == pageSize); + pageIndex++; + } while (members.Length == pageSize); + } + } + else + { + //no node types specified, do all members + do + { + int total; + members = _memberService.GetAll(pageIndex, pageSize, out total).ToArray(); + + AddNodesToIndex(GetSerializedMembers(members), type); + + pageIndex++; + } while (members.Length == pageSize); + } } } - else + finally { - //no node types specified, do all members - do - { - int total; - members = _memberService.GetAll(pageIndex, pageSize, out total).ToArray(); - - AddNodesToIndex(GetSerializedMembers(members), type); - - pageIndex++; - } while (members.Length == pageSize); + stopwatch.Stop(); } + + DataService.LogService.AddInfoLog(-1, string.Format("PerformIndexAll - End data queries - {0}, took {1}ms", type, stopwatch.ElapsedMilliseconds)); } private IEnumerable GetSerializedMembers(IEnumerable members) @@ -181,7 +238,9 @@ namespace UmbracoExamine var fields = base.GetSpecialFieldsToIndex(allValuesForIndexing); //adds the special path property to the index - fields.Add("__key", allValuesForIndexing["__key"]); + string valuesForIndexing; + if (allValuesForIndexing.TryGetValue("__key", out valuesForIndexing)) + fields.Add("__key", valuesForIndexing); return fields; diff --git a/src/UmbracoExamine/packages.config b/src/UmbracoExamine/packages.config index 04734b9fb8..0c85a9c3ca 100644 --- a/src/UmbracoExamine/packages.config +++ b/src/UmbracoExamine/packages.config @@ -1,6 +1,6 @@  - + \ No newline at end of file diff --git a/src/umbraco.MacroEngines/packages.config b/src/umbraco.MacroEngines/packages.config index 9a785e35c3..930b7adbb0 100644 --- a/src/umbraco.MacroEngines/packages.config +++ b/src/umbraco.MacroEngines/packages.config @@ -1,6 +1,6 @@  - + diff --git a/src/umbraco.MacroEngines/umbraco.MacroEngines.csproj b/src/umbraco.MacroEngines/umbraco.MacroEngines.csproj index c9ee70e52c..198867a5d6 100644 --- a/src/umbraco.MacroEngines/umbraco.MacroEngines.csproj +++ b/src/umbraco.MacroEngines/umbraco.MacroEngines.csproj @@ -45,8 +45,9 @@ false - - ..\packages\Examine.0.1.70.0\lib\Examine.dll + + ..\packages\Examine.0.1.80\lib\net45\Examine.dll + True ..\packages\HtmlAgilityPack.1.4.9\lib\Net45\HtmlAgilityPack.dll diff --git a/src/umbraco.cms/businesslogic/Tags/Tag.cs b/src/umbraco.cms/businesslogic/Tags/Tag.cs index 8534d02d26..d4e0c90d00 100644 --- a/src/umbraco.cms/businesslogic/Tags/Tag.cs +++ b/src/umbraco.cms/businesslogic/Tags/Tag.cs @@ -351,7 +351,7 @@ namespace umbraco.cms.businesslogic.Tags { Document cnode = new Document(rr.GetInt("nodeid")); - if (cnode != null && cnode.Published) + if (cnode.Published) docs.Add(cnode); } } diff --git a/src/umbraco.cms/businesslogic/propertytype/propertytype.cs b/src/umbraco.cms/businesslogic/propertytype/propertytype.cs index b9622d7c91..e041306ba3 100644 --- a/src/umbraco.cms/businesslogic/propertytype/propertytype.cs +++ b/src/umbraco.cms/businesslogic/propertytype/propertytype.cs @@ -54,32 +54,31 @@ namespace umbraco.cms.businesslogic.propertytype public PropertyType(int id) { - using (var sqlHelper = Application.SqlHelper) - using (IRecordsReader dr = sqlHelper.ExecuteReader( - "Select mandatory, DataTypeId, propertyTypeGroupId, ContentTypeId, sortOrder, alias, name, validationRegExp, description from cmsPropertyType where id=@id", - sqlHelper.CreateParameter("@id", id))) + var found = ApplicationContext.Current.DatabaseContext.Database + .SingleOrDefault( + "Select mandatory, DataTypeId, propertyTypeGroupId, contentTypeId, sortOrder, alias, name, validationRegExp, description from cmsPropertyType where id=@id", + new {id = id}); + + if (found == null) + throw new ArgumentException("Propertytype with id: " + id + " doesnt exist!"); + + _mandatory = found.mandatory; + _id = id; + + if (found.propertyTypeGroupId != null) { - if (!dr.Read()) - throw new ArgumentException("Propertytype with id: " + id + " doesnt exist!"); - - _mandatory = dr.GetBoolean("mandatory"); - _id = id; - - if (!dr.IsNull("propertyTypeGroupId")) - { - _propertyTypeGroup = dr.GetInt("propertyTypeGroupId"); - //TODO: Remove after refactoring! - _tabId = _propertyTypeGroup; - } - - _sortOrder = dr.GetInt("sortOrder"); - _alias = dr.GetString("alias"); - _name = dr.GetString("Name"); - _validationRegExp = dr.GetString("validationRegExp"); - _DataTypeId = dr.GetInt("DataTypeId"); - _contenttypeid = dr.GetInt("contentTypeId"); - _description = dr.GetString("description"); + _propertyTypeGroup = found.propertyTypeGroupId; + //TODO: Remove after refactoring! + _tabId = _propertyTypeGroup; } + + _sortOrder = found.sortOrder; + _alias = found.alias; + _name = found.name; + _validationRegExp = found.validationRegExp; + _DataTypeId = found.DataTypeId; + _contenttypeid = found.contentTypeId; + _description = found.description; } #endregion @@ -92,7 +91,6 @@ namespace umbraco.cms.businesslogic.propertytype set { _DataTypeId = value.Id; - InvalidateCache(); using (var sqlHelper = Application.SqlHelper) sqlHelper.ExecuteNonQuery( "Update cmsPropertyType set DataTypeId = " + value.Id + " where id=" + Id); @@ -119,7 +117,6 @@ namespace umbraco.cms.businesslogic.propertytype { _tabId = value; PropertyTypeGroup = value; - InvalidateCache(); } } @@ -148,7 +145,6 @@ namespace umbraco.cms.businesslogic.propertytype set { _mandatory = value; - InvalidateCache(); using (var sqlHelper = Application.SqlHelper) sqlHelper.ExecuteNonQuery("Update cmsPropertyType set mandatory = @mandatory where id = @id", sqlHelper.CreateParameter("@mandatory", value), @@ -162,7 +158,6 @@ namespace umbraco.cms.businesslogic.propertytype set { _validationRegExp = value; - InvalidateCache(); using (var sqlHelper = Application.SqlHelper) sqlHelper.ExecuteNonQuery("Update cmsPropertyType set validationRegExp = @validationRegExp where id = @id", sqlHelper.CreateParameter("@validationRegExp", value), sqlHelper.CreateParameter("@id", Id)); @@ -199,7 +194,6 @@ namespace umbraco.cms.businesslogic.propertytype set { _description = value; - InvalidateCache(); using (var sqlHelper = Application.SqlHelper) sqlHelper.ExecuteNonQuery("Update cmsPropertyType set description = @description where id = @id", sqlHelper.CreateParameter("@description", value), @@ -213,7 +207,6 @@ namespace umbraco.cms.businesslogic.propertytype set { _sortOrder = value; - InvalidateCache(); using (var sqlHelper = Application.SqlHelper) sqlHelper.ExecuteNonQuery("Update cmsPropertyType set sortOrder = @sortOrder where id = @id", sqlHelper.CreateParameter("@sortOrder", value), @@ -227,7 +220,6 @@ namespace umbraco.cms.businesslogic.propertytype set { _alias = value; - InvalidateCache(); using (var sqlHelper = Application.SqlHelper) sqlHelper.ExecuteNonQuery("Update cmsPropertyType set alias = @alias where id= @id", sqlHelper.CreateParameter("@alias", Casing.SafeAliasWithForcingCheck(_alias)), @@ -264,7 +256,6 @@ namespace umbraco.cms.businesslogic.propertytype set { _name = value; - InvalidateCache(); using (var sqlHelper = Application.SqlHelper) sqlHelper.ExecuteNonQuery( "UPDATE cmsPropertyType SET name=@name WHERE id=@id", @@ -331,17 +322,17 @@ namespace umbraco.cms.businesslogic.propertytype public static IEnumerable GetPropertyTypes() { var result = new List(); - using (var sqlHelper = Application.SqlHelper) - using (IRecordsReader dr = - sqlHelper.ExecuteReader("select id from cmsPropertyType order by Name")) + + var propertyTypeIds = ApplicationContext.Current.DatabaseContext.Database.Fetch( + "select id from cmsPropertyType order by Name"); + + foreach (var propertyTypeId in propertyTypeIds) { - while (dr.Read()) - { - PropertyType pt = GetPropertyType(dr.GetInt("id")); - if (pt != null) - result.Add(pt); - } + PropertyType pt = GetPropertyType(propertyTypeId); + if (pt != null) + result.Add(pt); } + return result; } @@ -353,18 +344,17 @@ namespace umbraco.cms.businesslogic.propertytype public static IEnumerable GetPropertyTypesByGroup(int groupId) { var result = new List(); - using (var sqlHelper = Application.SqlHelper) - using (IRecordsReader dr = - sqlHelper.ExecuteReader("SELECT id FROM cmsPropertyType WHERE propertyTypeGroupId = @groupId order by SortOrder", - sqlHelper.CreateParameter("@groupId", groupId))) + + var propertyTypeIds = ApplicationContext.Current.DatabaseContext.Database.Fetch( + "SELECT id FROM cmsPropertyType WHERE propertyTypeGroupId = @groupId order by SortOrder", new {groupId = groupId}); + + foreach (var propertyTypeId in propertyTypeIds) { - while (dr.Read()) - { - PropertyType pt = GetPropertyType(dr.GetInt("id")); - if (pt != null) - result.Add(pt); - } + PropertyType pt = GetPropertyType(propertyTypeId); + if (pt != null) + result.Add(pt); } + return result; } @@ -376,20 +366,18 @@ namespace umbraco.cms.businesslogic.propertytype public static IEnumerable GetByDataTypeDefinition(int dataTypeDefId) { var result = new List(); - using (var sqlHelper = Application.SqlHelper) - using (IRecordsReader dr = - sqlHelper.ExecuteReader( - "select id, Name from cmsPropertyType where dataTypeId=@dataTypeId order by Name", - sqlHelper.CreateParameter("@dataTypeId", dataTypeDefId))) + + var propertyTypeIds = ApplicationContext.Current.DatabaseContext.Database.Fetch( + "select id from cmsPropertyType where dataTypeId=@dataTypeId order by Name", new {dataTypeId = dataTypeDefId}); + + foreach (var propertyTypeId in propertyTypeIds) { - while (dr.Read()) - { - PropertyType pt = GetPropertyType(dr.GetInt("id")); - if (pt != null) - result.Add(pt); - } + PropertyType pt = GetPropertyType(propertyTypeId); + if (pt != null) + result.Add(pt); } - return result.ToList(); + + return result; } public void delete() @@ -411,7 +399,6 @@ namespace umbraco.cms.businesslogic.propertytype // delete cache from either master (via tabid) or current contentype FlushCacheBasedOnTab(); - InvalidateCache(); } public void FlushCacheBasedOnTab() @@ -478,8 +465,6 @@ namespace umbraco.cms.businesslogic.propertytype protected virtual void FlushCache() { - // clear local cache - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(GetCacheKey(Id)); // clear cache in contentype ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(CacheKeys.ContentTypePropertiesCacheKey + _contenttypeid); @@ -496,31 +481,9 @@ namespace umbraco.cms.businesslogic.propertytype public static PropertyType GetPropertyType(int id) { - return ApplicationContext.Current.ApplicationCache.RuntimeCache.GetCacheItem( - GetCacheKey(id), - timeout: TimeSpan.FromMinutes(30), - getCacheItem: () => - { - try - { - return new PropertyType(id); - } - catch - { - return null; - } - }); - } - - private void InvalidateCache() - { - ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(GetCacheKey(Id)); - } - - private static string GetCacheKey(int id) - { - return CacheKeys.PropertyTypeCacheKey + id; + return new PropertyType(id); } + #endregion } diff --git a/src/umbraco.cms/businesslogic/web/Document.cs b/src/umbraco.cms/businesslogic/web/Document.cs index 70775f5c71..bd21517d80 100644 --- a/src/umbraco.cms/businesslogic/web/Document.cs +++ b/src/umbraco.cms/businesslogic/web/Document.cs @@ -433,21 +433,20 @@ namespace umbraco.cms.businesslogic.web { XmlDocument xd = new XmlDocument(); - using (var sqlHelper = Application.SqlHelper) - using (IRecordsReader dr = sqlHelper.ExecuteReader("select nodeId from cmsDocument")) + var nodeIds = ApplicationContext.Current.DatabaseContext.Database.Fetch( + "select nodeId from cmsDocument"); + + foreach (var nodeId in nodeIds) { - while (dr.Read()) + try { - try - { - new Document(dr.GetInt("nodeId")).SaveXmlPreview(xd); - } - catch (Exception ee) - { - LogHelper.Error("Error generating preview xml", ee); - } + new Document(nodeId).SaveXmlPreview(xd); } - } + catch (Exception ee) + { + LogHelper.Error("Error generating preview xml", ee); + } + } } /// diff --git a/src/umbraco.editorControls/PickerRelations/PickerRelationsEventHandler.cs b/src/umbraco.editorControls/PickerRelations/PickerRelationsEventHandler.cs index 13f786359b..844d9920e0 100644 --- a/src/umbraco.editorControls/PickerRelations/PickerRelationsEventHandler.cs +++ b/src/umbraco.editorControls/PickerRelations/PickerRelationsEventHandler.cs @@ -2,8 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; - -using umbraco.BusinessLogic; // ApplicationBase +// ApplicationBase using umbraco.businesslogic; using umbraco.cms.businesslogic; // SaveEventArgs using umbraco.cms.businesslogic.media; // Media @@ -12,6 +11,8 @@ using umbraco.cms.businesslogic.web; // Documentusing umbraco.cms.businesslogic. using umbraco.cms.businesslogic.property; using umbraco.cms.businesslogic.relation; using umbraco.DataLayer; +using Umbraco.Core; +using Application = umbraco.BusinessLogic.Application; namespace umbraco.editorControls.PickerRelations { @@ -212,7 +213,7 @@ namespace umbraco.editorControls.PickerRelations private static void DeleteRelations(RelationType relationType, int contentNodeId, bool reverseIndexing, string instanceIdentifier) { //if relationType is bi-directional or a reverse index then we can't get at the relations via the API, so using SQL - string getRelationsSql = "SELECT id FROM umbracoRelation WHERE relType = " + relationType.Id.ToString() + " AND "; + string getRelationsSql = "SELECT id FROM umbracoRelation WHERE relType = " + relationType.Id + " AND "; if (reverseIndexing || relationType.Dual) { @@ -229,19 +230,16 @@ namespace umbraco.editorControls.PickerRelations getRelationsSql += " AND comment = '" + instanceIdentifier + "'"; - using (var sqlHelper = Application.SqlHelper) - using (IRecordsReader relations = sqlHelper.ExecuteReader(getRelationsSql)) - { - //clear data - Relation relation; - while (relations.Read()) - { - relation = new Relation(relations.GetInt("id")); + var relationIds = ApplicationContext.Current.DatabaseContext.Database.Fetch( + getRelationsSql); + foreach (var relationId in relationIds) + { + var relation = new Relation(relationId); - // TODO: [HR] check to see if an instance identifier is used - relation.Delete(); - } - } + // TODO: [HR] check to see if an instance identifier is used + relation.Delete(); + } + } ///