diff --git a/build/NuSpecs/UmbracoCms.Core.nuspec b/build/NuSpecs/UmbracoCms.Core.nuspec index 5e1c088822..a1a46e71c6 100644 --- a/build/NuSpecs/UmbracoCms.Core.nuspec +++ b/build/NuSpecs/UmbracoCms.Core.nuspec @@ -20,7 +20,7 @@ the latter would pick anything below 3.0.0 and that includes prereleases such as 3.0.0-alpha, and we do not want this to happen as the alpha of the next major is, really, the next major already. --> - + diff --git a/build/NuSpecs/tools/trees.config.install.xdt b/build/NuSpecs/tools/trees.config.install.xdt index dc59b5db10..42adf3cb0e 100644 --- a/build/NuSpecs/tools/trees.config.install.xdt +++ b/build/NuSpecs/tools/trees.config.install.xdt @@ -4,18 +4,18 @@ - - - - @@ -23,39 +23,39 @@ - - - - - - - - - - @@ -66,12 +66,12 @@ - - @@ -80,17 +80,17 @@ xdt:Locator="Match(application,alias)" xdt:Transform="SetAttributes(sortOrder)" /> - - - - @@ -99,7 +99,7 @@ xdt:Transform="Remove" /> - @@ -111,30 +111,30 @@ xdt:Transform="Remove" /> - - - - - public string Username { get; private set; } - [Obsolete("Use the method that has the affectedUser parameter instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public IdentityAuditEventArgs(AuditEvent action, string ipAddress, int performingUser = -1) - { - DateTimeUtc = DateTime.UtcNow; - Action = action; - - IpAddress = ipAddress; - - PerformingUser = performingUser == -1 - ? GetCurrentRequestBackofficeUserId() - : performingUser; - } /// /// Default constructor diff --git a/src/Umbraco.Core/Cache/CacheHelper.cs b/src/Umbraco.Core/Cache/CacheHelper.cs index 5480e199f5..2b43e857f0 100644 --- a/src/Umbraco.Core/Cache/CacheHelper.cs +++ b/src/Umbraco.Core/Cache/CacheHelper.cs @@ -1,8 +1,5 @@ using System; -using System.Collections.Generic; -using System.ComponentModel; using System.Web; -using System.Web.Caching; namespace Umbraco.Core.Cache { @@ -87,317 +84,6 @@ namespace Umbraco.Core.Cache /// public IsolatedRuntimeCache IsolatedRuntimeCache { get; internal set; } - #region Legacy Runtime/Http Cache accessors - - /// - /// Clears the item in umbraco's runtime cache - /// - [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] - [EditorBrowsable(EditorBrowsableState.Never)] - public void ClearAllCache() - { - RuntimeCache.ClearAllCache(); - IsolatedRuntimeCache.ClearAllCaches(); - } - - /// - /// Clears the item in umbraco's runtime cache with the given key - /// - /// Key - [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] - [EditorBrowsable(EditorBrowsableState.Never)] - public void ClearCacheItem(string key) - { - RuntimeCache.ClearCacheItem(key); - } - - - /// - /// Clears all objects in the System.Web.Cache with the System.Type name as the - /// input parameter. (using [object].GetType()) - /// - /// The name of the System.Type which should be cleared from cache ex "System.Xml.XmlDocument" - [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] - public void ClearCacheObjectTypes(string typeName) - { - RuntimeCache.ClearCacheObjectTypes(typeName); - } - - /// - /// Clears all objects in the System.Web.Cache with the System.Type specified - /// - [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] - [EditorBrowsable(EditorBrowsableState.Never)] - public void ClearCacheObjectTypes() - { - RuntimeCache.ClearCacheObjectTypes(); - } - - /// - /// Clears all cache items that starts with the key passed. - /// - /// The start of the key - [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] - [EditorBrowsable(EditorBrowsableState.Never)] - public void ClearCacheByKeySearch(string keyStartsWith) - { - RuntimeCache.ClearCacheByKeySearch(keyStartsWith); - } - - /// - /// Clears all cache items that have a key that matches the regular expression - /// - /// - [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] - [EditorBrowsable(EditorBrowsableState.Never)] - public void ClearCacheByKeyExpression(string regexString) - { - RuntimeCache.ClearCacheByKeyExpression(regexString); - } - - [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] - [EditorBrowsable(EditorBrowsableState.Never)] - public IEnumerable GetCacheItemsByKeySearch(string keyStartsWith) - { - return RuntimeCache.GetCacheItemsByKeySearch(keyStartsWith); - } - - /// - /// Returns a cache item by key, does not update the cache if it isn't there. - /// - /// - /// - /// - [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] - [EditorBrowsable(EditorBrowsableState.Never)] - public TT GetCacheItem(string cacheKey) - { - return RuntimeCache.GetCacheItem(cacheKey); - } - - /// - /// Gets (and adds if necessary) an item from the cache with all of the default parameters - /// - /// - /// - /// - /// - [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] - [EditorBrowsable(EditorBrowsableState.Never)] - public TT GetCacheItem(string cacheKey, Func getCacheItem) - { - return RuntimeCache.GetCacheItem(cacheKey, getCacheItem); - - } - - /// - /// Gets (and adds if necessary) an item from the cache with the specified absolute expiration date (from NOW) - /// - /// - /// - /// This will set an absolute expiration from now until the timeout - /// - /// - [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] - [EditorBrowsable(EditorBrowsableState.Never)] - public TT GetCacheItem(string cacheKey, - TimeSpan timeout, Func getCacheItem) - { - return RuntimeCache.GetCacheItem(cacheKey, getCacheItem, timeout); - - } - - /// - /// Gets (and adds if necessary) an item from the cache with the specified absolute expiration date (from NOW) - /// - /// - /// - /// - /// This will set an absolute expiration from now until the timeout - /// - /// - [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] - [EditorBrowsable(EditorBrowsableState.Never)] - public TT GetCacheItem(string cacheKey, - CacheItemRemovedCallback refreshAction, TimeSpan timeout, - Func getCacheItem) - { - return RuntimeCache.GetCacheItem(cacheKey, getCacheItem, timeout, removedCallback: refreshAction); - - } - - /// - /// Gets (and adds if necessary) an item from the cache with the specified absolute expiration date (from NOW) - /// - /// - /// - /// - /// - /// This will set an absolute expiration from now until the timeout - /// - /// - [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] - [EditorBrowsable(EditorBrowsableState.Never)] - public TT GetCacheItem(string cacheKey, - CacheItemPriority priority, CacheItemRemovedCallback refreshAction, TimeSpan timeout, - Func getCacheItem) - { - return RuntimeCache.GetCacheItem(cacheKey, getCacheItem, timeout, false, priority, refreshAction); - - } - - /// - /// Gets (and adds if necessary) an item from the cache with the specified absolute expiration date (from NOW) - /// - /// - /// - /// - /// - /// - /// This will set an absolute expiration from now until the timeout - /// - /// - [Obsolete("Do not use this method, we no longer support the caching overloads with references to CacheDependency, use the overloads specifying a file collection instead")] - public TT GetCacheItem(string cacheKey, - CacheItemPriority priority, - CacheItemRemovedCallback refreshAction, - CacheDependency cacheDependency, - TimeSpan timeout, - Func getCacheItem) - { - var cache = GetHttpRuntimeCacheProvider(RuntimeCache); - if (cache != null) - { - var result = cache.GetCacheItem(cacheKey, () => getCacheItem(), timeout, false, priority, refreshAction, cacheDependency); - return result == null ? default(TT) : result.TryConvertTo().Result; - } - throw new InvalidOperationException("Cannot use this obsoleted overload when the current provider is not of type " + typeof(HttpRuntimeCacheProvider)); - } - - /// - /// Gets (and adds if necessary) an item from the cache - /// - /// - /// - /// - /// - /// - /// - [Obsolete("Do not use this method, we no longer support the caching overloads with references to CacheDependency, use the overloads specifying a file collection instead")] - public TT GetCacheItem(string cacheKey, - CacheItemPriority priority, - CacheDependency cacheDependency, - Func getCacheItem) - { - var cache = GetHttpRuntimeCacheProvider(RuntimeCache); - if (cache != null) - { - var result = cache.GetCacheItem(cacheKey, () => getCacheItem(), null, false, priority, null, cacheDependency); - return result == null ? default(TT) : result.TryConvertTo().Result; - } - throw new InvalidOperationException("Cannot use this obsoleted overload when the current provider is not of type " + typeof(HttpRuntimeCacheProvider)); - } - - /// - /// Inserts an item into the cache, if it already exists in the cache it will be replaced - /// - /// - /// - /// - /// - [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] - [EditorBrowsable(EditorBrowsableState.Never)] - public void InsertCacheItem(string cacheKey, - CacheItemPriority priority, - Func getCacheItem) - { - RuntimeCache.InsertCacheItem(cacheKey, getCacheItem, priority: priority); - - } - - /// - /// Inserts an item into the cache, if it already exists in the cache it will be replaced - /// - /// - /// - /// - /// This will set an absolute expiration from now until the timeout - /// - [Obsolete("Do not use this method, access the runtime cache from the RuntimeCache property")] - [EditorBrowsable(EditorBrowsableState.Never)] - public void InsertCacheItem(string cacheKey, - CacheItemPriority priority, - TimeSpan timeout, - Func getCacheItem) - { - RuntimeCache.InsertCacheItem(cacheKey, getCacheItem, timeout, priority: priority); - } - - /// - /// Inserts an item into the cache, if it already exists in the cache it will be replaced - /// - /// - /// - /// - /// - /// This will set an absolute expiration from now until the timeout - /// - [Obsolete("Do not use this method, we no longer support the caching overloads with references to CacheDependency, use the overloads specifying a file collection instead")] - public void InsertCacheItem(string cacheKey, - CacheItemPriority priority, - CacheDependency cacheDependency, - TimeSpan timeout, - Func getCacheItem) - { - var cache = GetHttpRuntimeCacheProvider(RuntimeCache); - if (cache != null) - { - cache.InsertCacheItem(cacheKey, () => getCacheItem(), timeout, false, priority, null, cacheDependency); - } - throw new InvalidOperationException("Cannot use this obsoleted overload when the current provider is not of type " + typeof(HttpRuntimeCacheProvider)); - } - - /// - /// Inserts an item into the cache, if it already exists in the cache it will be replaced - /// - /// - /// - /// - /// - /// - /// This will set an absolute expiration from now until the timeout - /// - [Obsolete("Do not use this method, we no longer support the caching overloads with references to CacheDependency, use the overloads specifying a file collection instead")] - public void InsertCacheItem(string cacheKey, - CacheItemPriority priority, - CacheItemRemovedCallback refreshAction, - CacheDependency cacheDependency, - TimeSpan? timeout, - Func getCacheItem) - { - var cache = GetHttpRuntimeCacheProvider(RuntimeCache); - if (cache != null) - { - cache.InsertCacheItem(cacheKey, () => getCacheItem(), timeout, false, priority, refreshAction, cacheDependency); - } - throw new InvalidOperationException("Cannot use this obsoleted overload when the current provider is not of type " + typeof(HttpRuntimeCacheProvider)); - } - #endregion - - private HttpRuntimeCacheProvider GetHttpRuntimeCacheProvider(IRuntimeCacheProvider runtimeCache) - { - HttpRuntimeCacheProvider cache; - var wrapper = RuntimeCache as IRuntimeCacheProviderWrapper; - if (wrapper != null) - { - cache = wrapper.InnerProvider as HttpRuntimeCacheProvider; - } - else - { - cache = RuntimeCache as HttpRuntimeCacheProvider; - } - return cache; - } } } diff --git a/src/Umbraco.Core/Cache/CacheKeys.cs b/src/Umbraco.Core/Cache/CacheKeys.cs index 1058fa6181..e7dcf7cd8c 100644 --- a/src/Umbraco.Core/Cache/CacheKeys.cs +++ b/src/Umbraco.Core/Cache/CacheKeys.cs @@ -1,6 +1,4 @@ -using System; -using System.ComponentModel; -using Umbraco.Core.CodeAnnotations; +using Umbraco.Core.CodeAnnotations; namespace Umbraco.Core.Cache { @@ -12,14 +10,6 @@ namespace Umbraco.Core.Cache public const string ApplicationTreeCacheKey = "ApplicationTreeCache"; public const string ApplicationsCacheKey = "ApplicationCache"; - [Obsolete("This is no longer used and will be removed from the codebase in the future")] - [EditorBrowsable(EditorBrowsableState.Never)] - public const string UserTypeCacheKey = "UserTypeCache"; - - [Obsolete("This is no longer used and will be removed from the codebase in the future - it is referenced but no cache is stored against this key")] - [EditorBrowsable(EditorBrowsableState.Never)] - public const string ContentItemCacheKey = "contentItem"; - [UmbracoWillObsolete("This cache key is only used for the legacy 'library' caching, remove in v8")] public const string MediaCacheKey = "UL_GetMedia"; diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs index 2a3a6efca2..2dcbe06458 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs @@ -52,9 +52,6 @@ namespace Umbraco.Core.Configuration.UmbracoSettings [ConfigurationProperty("PreviewBadge")] internal InnerTextConfigurationElement PreviewBadge => GetOptionalTextElement("PreviewBadge", DefaultPreviewBadge); - [ConfigurationProperty("UmbracoLibraryCacheDuration")] - internal InnerTextConfigurationElement UmbracoLibraryCacheDuration => GetOptionalTextElement("UmbracoLibraryCacheDuration", 1800); - [ConfigurationProperty("MacroErrors")] internal InnerTextConfigurationElement MacroErrors => GetOptionalTextElement("MacroErrors", MacroErrorBehaviour.Inline); @@ -121,8 +118,6 @@ namespace Umbraco.Core.Configuration.UmbracoSettings string IContentSection.PreviewBadge => PreviewBadge; - int IContentSection.UmbracoLibraryCacheDuration => UmbracoLibraryCacheDuration; - MacroErrorBehaviour IContentSection.MacroErrorBehaviour => MacroErrors; IEnumerable IContentSection.DisallowedUploadFiles => DisallowedUploadFiles; diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs index 176b53fdcb..7f6f57f4cf 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs @@ -43,8 +43,6 @@ namespace Umbraco.Core.Configuration.UmbracoSettings string PreviewBadge { get; } - int UmbracoLibraryCacheDuration { get; } - MacroErrorBehaviour MacroErrorBehaviour { get; } IEnumerable DisallowedUploadFiles { get; } diff --git a/src/Umbraco.Core/Constants-Applications.cs b/src/Umbraco.Core/Constants-Applications.cs index c4d73558c6..9a1883a065 100644 --- a/src/Umbraco.Core/Constants-Applications.cs +++ b/src/Umbraco.Core/Constants-Applications.cs @@ -92,7 +92,7 @@ /// alias for the dictionary tree. /// public const string Dictionary = "dictionary"; - + public const string Stylesheets = "stylesheets"; /// diff --git a/src/Umbraco.Core/Constants-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs index ed3e7d682c..524ae580a3 100644 --- a/src/Umbraco.Core/Constants-Conventions.cs +++ b/src/Umbraco.Core/Constants-Conventions.cs @@ -26,18 +26,8 @@ namespace Umbraco.Core [Obsolete("No longer supported, this is here for backwards compatibility only")] public const string MemberIdRuleType = "MemberId"; - [Obsolete("No longer supported, this is here for backwards compatibility only")] - public const string MemberGroupIdRuleType = "MemberGroupId"; } - public static class Localization - { - /// - /// The root id for all top level dictionary items - /// - [Obsolete("There is no dictionary root item id anymore, it is simply null")] - public const string DictionaryItemRootId = "41c7638d-f529-4bff-853e-59a0c2fb1bde"; - } public static class DataTypes { diff --git a/src/Umbraco.Core/Constants-ObjectTypes.cs b/src/Umbraco.Core/Constants-ObjectTypes.cs index fb629d382e..790c143bbf 100644 --- a/src/Umbraco.Core/Constants-ObjectTypes.cs +++ b/src/Umbraco.Core/Constants-ObjectTypes.cs @@ -127,11 +127,6 @@ namespace Umbraco.Core public static readonly Guid ContentItem = new Guid(Strings.ContentItem); - [Obsolete("This no longer exists in the database")] - internal static readonly Guid Stylesheet = new Guid(Strings.Stylesheet); - - [Obsolete("This no longer exists in the database")] - internal static readonly Guid StylesheetProperty = new Guid(Strings.StylesheetProperty); } } } diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs index 275cc61425..092de4d6d6 100644 --- a/src/Umbraco.Core/ContentVariationExtensions.cs +++ b/src/Umbraco.Core/ContentVariationExtensions.cs @@ -1,82 +1,162 @@ -using Umbraco.Core.Models; +using System; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core { /// - /// Provides extension methods for various enumerations. + /// Provides extension methods for content variations. /// public static class ContentVariationExtensions { /// - /// Determines whether a variation has all flags set. + /// Determines whether the content type is invariant. /// - public static bool Has(this ContentVariation variation, ContentVariation values) - => (variation & values) == values; + public static bool VariesByNothing(this IContentTypeBase contentType) => contentType.Variations.VariesByNothing(); /// - /// Determines whether a variation has at least a flag set. + /// Determines whether the content type varies by culture. /// - public static bool HasAny(this ContentVariation variation, ContentVariation values) - => (variation & values) != ContentVariation.Unknown; + /// And then it could also vary by segment. + public static bool VariesByCulture(this IContentTypeBase contentType) => contentType.Variations.VariesByCulture(); /// - /// Determines whether a variation does not support culture variations + /// Determines whether the content type varies by segment. /// - /// - /// - public static bool DoesNotSupportCulture(this ContentVariation variation) + /// And then it could also vary by culture. + public static bool VariesBySegment(this IContentTypeBase contentType) => contentType.Variations.VariesBySegment(); + + /// + /// Determines whether the content type varies by culture and segment. + /// + public static bool VariesByCultureAndSegment(this IContentTypeBase contentType) => contentType.Variations.VariesByCultureAndSegment(); + + /// + /// Determines whether the property type is invariant. + /// + public static bool VariesByNothing(this PropertyType propertyType) => propertyType.Variations.VariesByNothing(); + + /// + /// Determines whether the property type varies by culture. + /// + /// And then it could also vary by segment. + public static bool VariesByCulture(this PropertyType propertyType) => propertyType.Variations.VariesByCulture(); + + /// + /// Determines whether the property type varies by segment. + /// + /// And then it could also vary by culture. + public static bool VariesBySegment(this PropertyType propertyType) => propertyType.Variations.VariesBySegment(); + + /// + /// Determines whether the property type varies by culture and segment. + /// + public static bool VariesByCultureAndSegment(this PropertyType propertyType) => propertyType.Variations.VariesByCultureAndSegment(); + + /// + /// Determines whether the content type is invariant. + /// + public static bool VariesByNothing(this PublishedContentType contentType) => contentType.Variations.VariesByNothing(); + + /// + /// Determines whether the content type varies by culture. + /// + /// And then it could also vary by segment. + public static bool VariesByCulture(this PublishedContentType contentType) => contentType.Variations.VariesByCulture(); + + /// + /// Determines whether the content type varies by segment. + /// + /// And then it could also vary by culture. + public static bool VariesBySegment(this PublishedContentType contentType) => contentType.Variations.VariesBySegment(); + + /// + /// Determines whether the content type varies by culture and segment. + /// + public static bool VariesByCultureAndSegment(this PublishedContentType contentType) => contentType.Variations.VariesByCultureAndSegment(); + + /// + /// Determines whether a variation is invariant. + /// + public static bool VariesByNothing(this ContentVariation variation) => variation == ContentVariation.Nothing; + + /// + /// Determines whether a variation varies by culture. + /// + /// And then it could also vary by segment. + public static bool VariesByCulture(this ContentVariation variation) => (variation & ContentVariation.Culture) > 0; + + /// + /// Determines whether a variation varies by segment. + /// + /// And then it could also vary by culture. + public static bool VariesBySegment(this ContentVariation variation) => (variation & ContentVariation.Segment) > 0; + + /// + /// Determines whether a variation varies by culture and segment. + /// + public static bool VariesByCultureAndSegment(this ContentVariation variation) => (variation & ContentVariation.CultureAndSegment) > 0; + + /// + /// Validates that a combination of culture and segment is valid for the variation. + /// + /// The variation. + /// The culture. + /// The segment. + /// A value indicating whether to perform exact validation. + /// A value indicating whether to support wildcards. + /// A value indicating whether to throw a when the combination is invalid. + /// True if the combination is valid; otherwise false. + /// + /// When validation is exact, the combination must match the variation exactly. For instance, if the variation is Culture, then + /// a culture is required. When validation is not strict, the combination must be equivalent, or more restrictive: if the variation is + /// Culture, an invariant combination is ok. + /// Basically, exact is for one content type, or one property type, and !exact is for "all property types" of one content type. + /// Both and can be "*" to indicate "all of them". + /// + /// Occurs when the combination is invalid, and is true. + public static bool ValidateVariation(this ContentVariation variation, string culture, string segment, bool exact, bool wildcards, bool throwIfInvalid) { - return !variation.HasAny(ContentVariation.CultureNeutral | ContentVariation.CultureSegment); - } + culture = culture.NullOrWhiteSpaceAsNull(); + segment = segment.NullOrWhiteSpaceAsNull(); - /// - /// Determines whether a variation does support culture variations - /// - /// - /// - public static bool DoesSupportCulture(this ContentVariation variation) - { - return variation.HasAny(ContentVariation.CultureNeutral | ContentVariation.CultureSegment); - } + bool Validate(bool variesBy, string value) + { + if (variesBy) + { + // varies by + // in exact mode, the value cannot be null (but it can be a wildcard) + // in !wildcards mode, the value cannot be a wildcard (but it can be null) + if ((exact && value == null) || (!wildcards && value == "*")) + return false; + } + else + { + // does not vary by value + // the value cannot have a value + // unless wildcards and it's "*" + if (value != null && (!wildcards || value != "*")) + return false; + } - /// - /// Determines whether a variation does not support invariant variations - /// - /// - /// - public static bool DoesNotSupportInvariant(this ContentVariation variation) - { - return !variation.HasAny(ContentVariation.InvariantNeutral | ContentVariation.InvariantSegment); - } + return true; + } - /// - /// Determines whether a variation does support invariant variations - /// - /// - /// - public static bool DoesSupportInvariant(this ContentVariation variation) - { - return variation.HasAny(ContentVariation.InvariantNeutral | ContentVariation.InvariantSegment); - } + if (!Validate(variation.VariesByCulture(), culture)) + { + if (throwIfInvalid) + throw new NotSupportedException($"Culture value \"{culture ?? ""}\" is invalid."); + return false; + } - /// - /// Determines whether a variation does not support segment variations - /// - /// - /// - public static bool DoesNotSupportSegment(this ContentVariation variation) - { - return !variation.HasAny(ContentVariation.InvariantSegment | ContentVariation.CultureSegment); - } + if (!Validate(variation.VariesBySegment(), segment)) + { + if (throwIfInvalid) + throw new NotSupportedException($"Segment value \"{segment ?? ""}\" is invalid."); + return false; + } - /// - /// Determines whether a variation does not support neutral variations - /// - /// - /// - public static bool DoesNotSupportNeutral(this ContentVariation variation) - { - return !variation.HasAny(ContentVariation.InvariantNeutral | ContentVariation.CultureNeutral); + return true; } } } diff --git a/src/Umbraco.Core/Events/MoveEventArgs.cs b/src/Umbraco.Core/Events/MoveEventArgs.cs index 29483e5e01..aaeddc6921 100644 --- a/src/Umbraco.Core/Events/MoveEventArgs.cs +++ b/src/Umbraco.Core/Events/MoveEventArgs.cs @@ -88,15 +88,6 @@ namespace Umbraco.Core.Events EventObject = moveInfo.First().Entity; } - [Obsolete("Use the overload that specifies the MoveEventInfo object")] - public MoveEventArgs(TEntity eventObject, bool canCancel, int parentId) - : base(eventObject, canCancel) - { } - - [Obsolete("Use the overload that specifies the MoveEventInfo object")] - public MoveEventArgs(TEntity eventObject, int parentId) - : base(eventObject) - { } /// /// Gets all MoveEventInfo objects used to create the object @@ -119,15 +110,6 @@ namespace Umbraco.Core.Events } } - /// - /// The entity being moved - /// - [Obsolete("Retrieve the entity object from the MoveInfoCollection property instead")] - public TEntity Entity - { - get { return EventObject; } - } - public bool Equals(MoveEventArgs other) { if (other is null) return false; diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index 8063ba9f46..7c02f7ad75 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -156,10 +156,10 @@ namespace Umbraco.Core.Migrations.Install private void CreateContentTypeData() { - _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 532, NodeId = 1031, Alias = Constants.Conventions.MediaTypes.Folder, Icon = "icon-folder", Thumbnail = "icon-folder", IsContainer = false, AllowAtRoot = true, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 533, NodeId = 1032, Alias = Constants.Conventions.MediaTypes.Image, Icon = "icon-picture", Thumbnail = "icon-picture", AllowAtRoot = true, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 534, NodeId = 1033, Alias = Constants.Conventions.MediaTypes.File, Icon = "icon-document", Thumbnail = "icon-document", AllowAtRoot = true, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 531, NodeId = 1044, Alias = Constants.Conventions.MemberTypes.DefaultAlias, Icon = "icon-user", Thumbnail = "icon-user", Variations = (byte) ContentVariation.InvariantNeutral }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 532, NodeId = 1031, Alias = Constants.Conventions.MediaTypes.Folder, Icon = "icon-folder", Thumbnail = "icon-folder", IsContainer = false, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 533, NodeId = 1032, Alias = Constants.Conventions.MediaTypes.Image, Icon = "icon-picture", Thumbnail = "icon-picture", AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 534, NodeId = 1033, Alias = Constants.Conventions.MediaTypes.File, Icon = "icon-document", Thumbnail = "icon-document", AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 531, NodeId = 1044, Alias = Constants.Conventions.MemberTypes.DefaultAlias, Icon = "icon-user", Thumbnail = "icon-user", Variations = (byte) ContentVariation.Nothing }); } private void CreateUserData() @@ -212,23 +212,23 @@ namespace Umbraco.Core.Migrations.Install private void CreatePropertyTypeData() { - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 6, UniqueId = 6.ToGuid(), DataTypeId = 1043, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.File, Name = "Upload image", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 7, UniqueId = 7.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Width, Name = "Width", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 8, UniqueId = 8.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Height, Name = "Height", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 9, UniqueId = 9.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 10, UniqueId = 10.ToGuid(), DataTypeId = -92, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 24, UniqueId = 24.ToGuid(), DataTypeId = -90, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.File, Name = "Upload file", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 25, UniqueId = 25.ToGuid(), DataTypeId = -92, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 26, UniqueId = 26.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 27, UniqueId = 27.ToGuid(), DataTypeId = Constants.DataTypes.DefaultMediaListView, ContentTypeId = 1031, PropertyTypeGroupId = 5, Alias = "contents", Name = "Contents:", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 6, UniqueId = 6.ToGuid(), DataTypeId = 1043, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.File, Name = "Upload image", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 7, UniqueId = 7.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Width, Name = "Width", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 8, UniqueId = 8.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Height, Name = "Height", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 9, UniqueId = 9.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 10, UniqueId = 10.ToGuid(), DataTypeId = -92, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 24, UniqueId = 24.ToGuid(), DataTypeId = -90, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.File, Name = "Upload file", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 25, UniqueId = 25.ToGuid(), DataTypeId = -92, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 26, UniqueId = 26.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 27, UniqueId = 27.ToGuid(), DataTypeId = Constants.DataTypes.DefaultMediaListView, ContentTypeId = 1031, PropertyTypeGroupId = 5, Alias = "contents", Name = "Contents:", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); //membership property types - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 28, UniqueId = 28.ToGuid(), DataTypeId = -89, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.Comments, Name = Constants.Conventions.Member.CommentsLabel, SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 29, UniqueId = 29.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.FailedPasswordAttempts, Name = Constants.Conventions.Member.FailedPasswordAttemptsLabel, SortOrder = 1, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 30, UniqueId = 30.ToGuid(), DataTypeId = -49, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.IsApproved, Name = Constants.Conventions.Member.IsApprovedLabel, SortOrder = 2, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 31, UniqueId = 31.ToGuid(), DataTypeId = -49, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.IsLockedOut, Name = Constants.Conventions.Member.IsLockedOutLabel, SortOrder = 3, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 32, UniqueId = 32.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastLockoutDate, Name = Constants.Conventions.Member.LastLockoutDateLabel, SortOrder = 4, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 33, UniqueId = 33.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastLoginDate, Name = Constants.Conventions.Member.LastLoginDateLabel, SortOrder = 5, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 34, UniqueId = 34.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastPasswordChangeDate, Name = Constants.Conventions.Member.LastPasswordChangeDateLabel, SortOrder = 6, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.InvariantNeutral }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 28, UniqueId = 28.ToGuid(), DataTypeId = -89, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.Comments, Name = Constants.Conventions.Member.CommentsLabel, SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 29, UniqueId = 29.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.FailedPasswordAttempts, Name = Constants.Conventions.Member.FailedPasswordAttemptsLabel, SortOrder = 1, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 30, UniqueId = 30.ToGuid(), DataTypeId = -49, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.IsApproved, Name = Constants.Conventions.Member.IsApprovedLabel, SortOrder = 2, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 31, UniqueId = 31.ToGuid(), DataTypeId = -49, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.IsLockedOut, Name = Constants.Conventions.Member.IsLockedOutLabel, SortOrder = 3, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 32, UniqueId = 32.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastLockoutDate, Name = Constants.Conventions.Member.LastLockoutDateLabel, SortOrder = 4, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 33, UniqueId = 33.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastLoginDate, Name = Constants.Conventions.Member.LastLoginDateLabel, SortOrder = 5, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 34, UniqueId = 34.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastPasswordChangeDate, Name = Constants.Conventions.Member.LastPasswordChangeDateLabel, SortOrder = 6, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); } diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs index 94988e2687..5a4290aadd 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs @@ -1,448 +1,452 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NPoco; -using Umbraco.Core.Events; -using Umbraco.Core.Logging; -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; -using Umbraco.Core.Persistence.Dtos; -using Umbraco.Core.Persistence.SqlSyntax; - -namespace Umbraco.Core.Migrations.Install -{ - /// - /// Creates the initial database schema during install. - /// - internal class DatabaseSchemaCreator - { - private readonly IUmbracoDatabase _database; - private readonly ILogger _logger; - - public DatabaseSchemaCreator(IUmbracoDatabase database, ILogger logger) - { - _database = database; - _logger = logger; - } - - private ISqlSyntaxProvider SqlSyntax => _database.SqlContext.SqlSyntax; - - // all tables, in order - public static readonly List OrderedTables = new List - { - typeof (UserDto), - typeof (NodeDto), - typeof (ContentTypeDto), - typeof (TemplateDto), - typeof (ContentDto), - typeof (ContentVersionDto), - typeof (MediaVersionDto), - typeof (DocumentDto), - typeof (ContentTypeTemplateDto), - typeof (DataTypeDto), - typeof (DictionaryDto), - typeof (LanguageDto), - typeof (LanguageTextDto), - typeof (DomainDto), - typeof (LogDto), - typeof (MacroDto), - typeof (MacroPropertyDto), - typeof (MemberTypeDto), - typeof (MemberDto), - typeof (Member2MemberGroupDto), - typeof (ContentXmlDto), - typeof (PreviewXmlDto), - typeof (PropertyTypeGroupDto), - typeof (PropertyTypeDto), - typeof (PropertyDataDto), - typeof (RelationTypeDto), - typeof (RelationDto), - typeof (TagDto), - typeof (TagRelationshipDto), - typeof (TaskTypeDto), - typeof (TaskDto), - typeof (ContentType2ContentTypeDto), - typeof (ContentTypeAllowedContentTypeDto), - typeof (User2NodeNotifyDto), - typeof (ServerRegistrationDto), - typeof (AccessDto), - typeof (AccessRuleDto), - typeof (CacheInstructionDto), - typeof (ExternalLoginDto), - typeof (RedirectUrlDto), - typeof (LockDto), - typeof (UserGroupDto), - typeof (User2UserGroupDto), - typeof (UserGroup2NodePermissionDto), - typeof (UserGroup2AppDto), - typeof (UserStartNodeDto), - typeof (ContentNuDto), - typeof (DocumentVersionDto), - typeof (KeyValueDto), - typeof (UserLoginDto), - typeof (ConsentDto), - typeof (AuditEntryDto), - typeof (ContentVersionCultureVariationDto), - typeof (DocumentCultureVariationDto) - }; - - /// - /// Drops all Umbraco tables in the db. - /// - internal void UninstallDatabaseSchema() - { - _logger.Info("Start UninstallDatabaseSchema"); - - foreach (var table in OrderedTables.AsEnumerable().Reverse()) - { - var tableNameAttribute = table.FirstAttribute(); - var tableName = tableNameAttribute == null ? table.Name : tableNameAttribute.Value; - - _logger.Info(() => $"Uninstall {tableName}"); - - try - { - if (TableExists(tableName)) - DropTable(tableName); - } - catch (Exception ex) - { - //swallow this for now, not sure how best to handle this with diff databases... though this is internal - // and only used for unit tests. If this fails its because the table doesn't exist... generally! - _logger.Error("Could not drop table " + tableName, ex); - } - } - } - - /// - /// Initializes the database by creating the umbraco db schema. - /// - public void InitializeDatabaseSchema() - { - var e = new DatabaseCreationEventArgs(); - FireBeforeCreation(e); - - if (e.Cancel == false) - { - var dataCreation = new DatabaseDataCreator(_database, _logger); - foreach (var table in OrderedTables) - CreateTable(false, table, dataCreation); - } - - FireAfterCreation(e); - } - - /// - /// Validates the schema of the current database. - /// - public DatabaseSchemaResult ValidateSchema() - { - var result = new DatabaseSchemaResult(SqlSyntax); - - //get the db index defs - result.DbIndexDefinitions = SqlSyntax.GetDefinedIndexes(_database) - .Select(x => new DbIndexDefinition - { - TableName = x.Item1, - IndexName = x.Item2, - ColumnName = x.Item3, - IsUnique = x.Item4 - }).ToArray(); - - result.TableDefinitions.AddRange(OrderedTables - .Select(x => DefinitionFactory.GetTableDefinition(x, SqlSyntax))); - - ValidateDbTables(result); - ValidateDbColumns(result); - ValidateDbIndexes(result); - ValidateDbConstraints(result); - - return result; - } - - private void ValidateDbConstraints(DatabaseSchemaResult result) - { - //MySql doesn't conform to the "normal" naming of constraints, so there is currently no point in doing these checks. - //TODO: At a later point we do other checks for MySql, but ideally it should be necessary to do special checks for different providers. - // ALso note that to get the constraints for MySql we have to open a connection which we currently have not. - if (SqlSyntax is MySqlSyntaxProvider) - return; - - //Check constraints in configured database against constraints in schema - var constraintsInDatabase = SqlSyntax.GetConstraintsPerColumn(_database).DistinctBy(x => x.Item3).ToList(); - var foreignKeysInDatabase = constraintsInDatabase.Where(x => x.Item3.InvariantStartsWith("FK_")).Select(x => x.Item3).ToList(); - var primaryKeysInDatabase = constraintsInDatabase.Where(x => x.Item3.InvariantStartsWith("PK_")).Select(x => x.Item3).ToList(); - var indexesInDatabase = constraintsInDatabase.Where(x => x.Item3.InvariantStartsWith("IX_")).Select(x => x.Item3).ToList(); - var indexesInSchema = result.TableDefinitions.SelectMany(x => x.Indexes.Select(y => y.Name)).ToList(); - var unknownConstraintsInDatabase = - constraintsInDatabase.Where( - x => - x.Item3.InvariantStartsWith("FK_") == false && x.Item3.InvariantStartsWith("PK_") == false && - x.Item3.InvariantStartsWith("IX_") == false).Select(x => x.Item3).ToList(); - var foreignKeysInSchema = result.TableDefinitions.SelectMany(x => x.ForeignKeys.Select(y => y.Name)).ToList(); - var primaryKeysInSchema = result.TableDefinitions.SelectMany(x => x.Columns.Select(y => y.PrimaryKeyName)) - .Where(x => x.IsNullOrWhiteSpace() == false).ToList(); - - //Add valid and invalid foreign key differences to the result object - // We'll need to do invariant contains with case insensitivity because foreign key, primary key, and even index naming w/ MySQL is not standardized - // In theory you could have: FK_ or fk_ ...or really any standard that your development department (or developer) chooses to use. - foreach (var unknown in unknownConstraintsInDatabase) - { - if (foreignKeysInSchema.InvariantContains(unknown) || primaryKeysInSchema.InvariantContains(unknown) || indexesInSchema.InvariantContains(unknown)) - { - result.ValidConstraints.Add(unknown); - } - else - { - result.Errors.Add(new Tuple("Unknown", unknown)); - } - } - - //Foreign keys: - - var validForeignKeyDifferences = foreignKeysInDatabase.Intersect(foreignKeysInSchema, StringComparer.InvariantCultureIgnoreCase); - foreach (var foreignKey in validForeignKeyDifferences) - { - result.ValidConstraints.Add(foreignKey); - } - var invalidForeignKeyDifferences = - foreignKeysInDatabase.Except(foreignKeysInSchema, StringComparer.InvariantCultureIgnoreCase) - .Union(foreignKeysInSchema.Except(foreignKeysInDatabase, StringComparer.InvariantCultureIgnoreCase)); - foreach (var foreignKey in invalidForeignKeyDifferences) - { - result.Errors.Add(new Tuple("Constraint", foreignKey)); - } - - - //Primary keys: - - //Add valid and invalid primary key differences to the result object - var validPrimaryKeyDifferences = primaryKeysInDatabase.Intersect(primaryKeysInSchema, StringComparer.InvariantCultureIgnoreCase); - foreach (var primaryKey in validPrimaryKeyDifferences) - { - result.ValidConstraints.Add(primaryKey); - } - var invalidPrimaryKeyDifferences = - primaryKeysInDatabase.Except(primaryKeysInSchema, StringComparer.InvariantCultureIgnoreCase) - .Union(primaryKeysInSchema.Except(primaryKeysInDatabase, StringComparer.InvariantCultureIgnoreCase)); - foreach (var primaryKey in invalidPrimaryKeyDifferences) - { - result.Errors.Add(new Tuple("Constraint", primaryKey)); - } - - //Constaints: - - //NOTE: SD: The colIndex checks above should really take care of this but I need to keep this here because it was here before - // and some schema validation checks might rely on this data remaining here! - //Add valid and invalid index differences to the result object - var validIndexDifferences = indexesInDatabase.Intersect(indexesInSchema, StringComparer.InvariantCultureIgnoreCase); - foreach (var index in validIndexDifferences) - { - result.ValidConstraints.Add(index); - } - var invalidIndexDifferences = - indexesInDatabase.Except(indexesInSchema, StringComparer.InvariantCultureIgnoreCase) - .Union(indexesInSchema.Except(indexesInDatabase, StringComparer.InvariantCultureIgnoreCase)); - foreach (var index in invalidIndexDifferences) - { - result.Errors.Add(new Tuple("Constraint", index)); - } - } - - private void ValidateDbColumns(DatabaseSchemaResult result) - { - //Check columns in configured database against columns in schema - var columnsInDatabase = SqlSyntax.GetColumnsInSchema(_database); - var columnsPerTableInDatabase = columnsInDatabase.Select(x => string.Concat(x.TableName, ",", x.ColumnName)).ToList(); - var columnsPerTableInSchema = result.TableDefinitions.SelectMany(x => x.Columns.Select(y => string.Concat(y.TableName, ",", y.Name))).ToList(); - //Add valid and invalid column differences to the result object - var validColumnDifferences = columnsPerTableInDatabase.Intersect(columnsPerTableInSchema, StringComparer.InvariantCultureIgnoreCase); - foreach (var column in validColumnDifferences) - { - result.ValidColumns.Add(column); - } - - var invalidColumnDifferences = - columnsPerTableInDatabase.Except(columnsPerTableInSchema, StringComparer.InvariantCultureIgnoreCase) - .Union(columnsPerTableInSchema.Except(columnsPerTableInDatabase, StringComparer.InvariantCultureIgnoreCase)); - foreach (var column in invalidColumnDifferences) - { - result.Errors.Add(new Tuple("Column", column)); - } - } - - private void ValidateDbTables(DatabaseSchemaResult result) - { - //Check tables in configured database against tables in schema - var tablesInDatabase = SqlSyntax.GetTablesInSchema(_database).ToList(); - var tablesInSchema = result.TableDefinitions.Select(x => x.Name).ToList(); - //Add valid and invalid table differences to the result object - var validTableDifferences = tablesInDatabase.Intersect(tablesInSchema, StringComparer.InvariantCultureIgnoreCase); - foreach (var tableName in validTableDifferences) - { - result.ValidTables.Add(tableName); - } - - var invalidTableDifferences = - tablesInDatabase.Except(tablesInSchema, StringComparer.InvariantCultureIgnoreCase) - .Union(tablesInSchema.Except(tablesInDatabase, StringComparer.InvariantCultureIgnoreCase)); - foreach (var tableName in invalidTableDifferences) - { - result.Errors.Add(new Tuple("Table", tableName)); - } - } - - private void ValidateDbIndexes(DatabaseSchemaResult result) - { - //These are just column indexes NOT constraints or Keys - //var colIndexesInDatabase = result.DbIndexDefinitions.Where(x => x.IndexName.InvariantStartsWith("IX_")).Select(x => x.IndexName).ToList(); - var colIndexesInDatabase = result.DbIndexDefinitions.Select(x => x.IndexName).ToList(); - var indexesInSchema = result.TableDefinitions.SelectMany(x => x.Indexes.Select(y => y.Name)).ToList(); - - //Add valid and invalid index differences to the result object - var validColIndexDifferences = colIndexesInDatabase.Intersect(indexesInSchema, StringComparer.InvariantCultureIgnoreCase); - foreach (var index in validColIndexDifferences) - { - result.ValidIndexes.Add(index); - } - - var invalidColIndexDifferences = - colIndexesInDatabase.Except(indexesInSchema, StringComparer.InvariantCultureIgnoreCase) - .Union(indexesInSchema.Except(colIndexesInDatabase, StringComparer.InvariantCultureIgnoreCase)); - foreach (var index in invalidColIndexDifferences) - { - result.Errors.Add(new Tuple("Index", index)); - } - } - - #region Events - - /// - /// The save event handler - /// - internal delegate void DatabaseEventHandler(DatabaseCreationEventArgs e); - - /// - /// Occurs when [before save]. - /// - internal static event DatabaseEventHandler BeforeCreation; - /// - /// Raises the event. - /// - /// The instance containing the event data. - internal virtual void FireBeforeCreation(DatabaseCreationEventArgs e) - { - BeforeCreation?.Invoke(e); - } - - /// - /// Occurs when [after save]. - /// - internal static event DatabaseEventHandler AfterCreation; - /// - /// Raises the event. - /// - /// The instance containing the event data. - internal virtual void FireAfterCreation(DatabaseCreationEventArgs e) - { - AfterCreation?.Invoke(e); - } - - #endregion - - #region Utilities - - public bool TableExists(string tableName) - { - return SqlSyntax.DoesTableExist(_database, tableName); - } - - // this is used in tests - internal void CreateTable(bool overwrite = false) - where T : new() - { - var tableType = typeof(T); - CreateTable(overwrite, tableType, new DatabaseDataCreator(_database, _logger)); - } - - public void CreateTable(bool overwrite, Type modelType, DatabaseDataCreator dataCreation) - { - var tableDefinition = DefinitionFactory.GetTableDefinition(modelType, SqlSyntax); - var tableName = tableDefinition.Name; - - var createSql = SqlSyntax.Format(tableDefinition); - var createPrimaryKeySql = SqlSyntax.FormatPrimaryKey(tableDefinition); - var foreignSql = SqlSyntax.Format(tableDefinition.ForeignKeys); - var indexSql = SqlSyntax.Format(tableDefinition.Indexes); - - var tableExist = TableExists(tableName); - if (overwrite && tableExist) - { - DropTable(tableName); - tableExist = false; - } - - if (tableExist == false) - { - using (var transaction = _database.GetTransaction()) - { - //Execute the Create Table sql - var created = _database.Execute(new Sql(createSql)); - _logger.Info(() => $"Create Table '{tableName}' ({created}):\n {createSql}"); - - //If any statements exists for the primary key execute them here - if (string.IsNullOrEmpty(createPrimaryKeySql) == false) - { - var createdPk = _database.Execute(new Sql(createPrimaryKeySql)); - _logger.Info(() => $"Create Primary Key ({createdPk}):\n {createPrimaryKeySql}"); - } - - //Turn on identity insert if db provider is not mysql - if (SqlSyntax.SupportsIdentityInsert() && tableDefinition.Columns.Any(x => x.IsIdentity)) - _database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(tableName)} ON ")); - - //Call the NewTable-event to trigger the insert of base/default data - //OnNewTable(tableName, _db, e, _logger); - - dataCreation.InitializeBaseData(tableName); - - //Turn off identity insert if db provider is not mysql - if (SqlSyntax.SupportsIdentityInsert() && tableDefinition.Columns.Any(x => x.IsIdentity)) - _database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(tableName)} OFF;")); - - //Special case for MySql - if (SqlSyntax is MySqlSyntaxProvider && tableName.Equals("umbracoUser")) - { - _database.Update("SET id = @IdAfter WHERE id = @IdBefore AND userLogin = @Login", new { IdAfter = 0, IdBefore = 1, Login = "admin" }); - } - - //Loop through index statements and execute sql - foreach (var sql in indexSql) - { - var createdIndex = _database.Execute(new Sql(sql)); - _logger.Info(() => $"Create Index ({createdIndex}):\n {sql}"); - } - - //Loop through foreignkey statements and execute sql - foreach (var sql in foreignSql) - { - var createdFk = _database.Execute(new Sql(sql)); - _logger.Info(() => $"Create Foreign Key ({createdFk}):\n {sql}"); - } - - transaction.Complete(); - } - } - - _logger.Info(() => $"Created table '{tableName}'"); - } - - public void DropTable(string tableName) - { - var sql = new Sql(string.Format(SqlSyntax.DropTable, SqlSyntax.GetQuotedTableName(tableName))); - _database.Execute(sql); - } - - #endregion - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using NPoco; +using Umbraco.Core.Events; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.Dtos; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Migrations.Install +{ + /// + /// Creates the initial database schema during install. + /// + internal class DatabaseSchemaCreator + { + private readonly IUmbracoDatabase _database; + private readonly ILogger _logger; + + public DatabaseSchemaCreator(IUmbracoDatabase database, ILogger logger) + { + _database = database; + _logger = logger; + } + + private ISqlSyntaxProvider SqlSyntax => _database.SqlContext.SqlSyntax; + + // all tables, in order + public static readonly List OrderedTables = new List + { + typeof (UserDto), + typeof (NodeDto), + typeof (ContentTypeDto), + typeof (TemplateDto), + typeof (ContentDto), + typeof (ContentVersionDto), + typeof (MediaVersionDto), + typeof (DocumentDto), + typeof (ContentTypeTemplateDto), + typeof (DataTypeDto), + typeof (DictionaryDto), + typeof (LanguageDto), + typeof (LanguageTextDto), + typeof (DomainDto), + typeof (LogDto), + typeof (MacroDto), + typeof (MacroPropertyDto), + typeof (MemberTypeDto), + typeof (MemberDto), + typeof (Member2MemberGroupDto), + typeof (ContentXmlDto), + typeof (PreviewXmlDto), + typeof (PropertyTypeGroupDto), + typeof (PropertyTypeDto), + typeof (PropertyDataDto), + typeof (RelationTypeDto), + typeof (RelationDto), + typeof (TagDto), + typeof (TagRelationshipDto), + typeof (TaskTypeDto), + typeof (TaskDto), + typeof (ContentType2ContentTypeDto), + typeof (ContentTypeAllowedContentTypeDto), + typeof (User2NodeNotifyDto), + typeof (ServerRegistrationDto), + typeof (AccessDto), + typeof (AccessRuleDto), + typeof (CacheInstructionDto), + typeof (ExternalLoginDto), + typeof (RedirectUrlDto), + typeof (LockDto), + typeof (UserGroupDto), + typeof (User2UserGroupDto), + typeof (UserGroup2NodePermissionDto), + typeof (UserGroup2AppDto), + typeof (UserStartNodeDto), + typeof (ContentNuDto), + typeof (DocumentVersionDto), + typeof (KeyValueDto), + typeof (UserLoginDto), + typeof (ConsentDto), + typeof (AuditEntryDto), + typeof (ContentVersionCultureVariationDto), + typeof (DocumentCultureVariationDto) + }; + + /// + /// Drops all Umbraco tables in the db. + /// + internal void UninstallDatabaseSchema() + { + _logger.Info("Start UninstallDatabaseSchema"); + + foreach (var table in OrderedTables.AsEnumerable().Reverse()) + { + var tableNameAttribute = table.FirstAttribute(); + var tableName = tableNameAttribute == null ? table.Name : tableNameAttribute.Value; + + _logger.Info(() => $"Uninstall {tableName}"); + + try + { + if (TableExists(tableName)) + DropTable(tableName); + } + catch (Exception ex) + { + //swallow this for now, not sure how best to handle this with diff databases... though this is internal + // and only used for unit tests. If this fails its because the table doesn't exist... generally! + _logger.Error("Could not drop table " + tableName, ex); + } + } + } + + /// + /// Initializes the database by creating the umbraco db schema. + /// + public void InitializeDatabaseSchema() + { + var e = new DatabaseCreationEventArgs(); + FireBeforeCreation(e); + + if (e.Cancel == false) + { + var dataCreation = new DatabaseDataCreator(_database, _logger); + foreach (var table in OrderedTables) + CreateTable(false, table, dataCreation); + } + + FireAfterCreation(e); + } + + /// + /// Validates the schema of the current database. + /// + public DatabaseSchemaResult ValidateSchema() + { + var result = new DatabaseSchemaResult(SqlSyntax); + + //get the db index defs + result.DbIndexDefinitions = SqlSyntax.GetDefinedIndexes(_database) + .Select(x => new DbIndexDefinition + { + TableName = x.Item1, + IndexName = x.Item2, + ColumnName = x.Item3, + IsUnique = x.Item4 + }).ToArray(); + + result.TableDefinitions.AddRange(OrderedTables + .Select(x => DefinitionFactory.GetTableDefinition(x, SqlSyntax))); + + ValidateDbTables(result); + ValidateDbColumns(result); + ValidateDbIndexes(result); + ValidateDbConstraints(result); + + return result; + } + + private void ValidateDbConstraints(DatabaseSchemaResult result) + { + //MySql doesn't conform to the "normal" naming of constraints, so there is currently no point in doing these checks. + //TODO: At a later point we do other checks for MySql, but ideally it should be necessary to do special checks for different providers. + // ALso note that to get the constraints for MySql we have to open a connection which we currently have not. + if (SqlSyntax is MySqlSyntaxProvider) + return; + + //Check constraints in configured database against constraints in schema + var constraintsInDatabase = SqlSyntax.GetConstraintsPerColumn(_database).DistinctBy(x => x.Item3).ToList(); + var foreignKeysInDatabase = constraintsInDatabase.Where(x => x.Item3.InvariantStartsWith("FK_")).Select(x => x.Item3).ToList(); + var primaryKeysInDatabase = constraintsInDatabase.Where(x => x.Item3.InvariantStartsWith("PK_")).Select(x => x.Item3).ToList(); + var indexesInDatabase = constraintsInDatabase.Where(x => x.Item3.InvariantStartsWith("IX_")).Select(x => x.Item3).ToList(); + var indexesInSchema = result.TableDefinitions.SelectMany(x => x.Indexes.Select(y => y.Name)).ToList(); + var unknownConstraintsInDatabase = + constraintsInDatabase.Where( + x => + x.Item3.InvariantStartsWith("FK_") == false && x.Item3.InvariantStartsWith("PK_") == false && + x.Item3.InvariantStartsWith("IX_") == false).Select(x => x.Item3).ToList(); + var foreignKeysInSchema = result.TableDefinitions.SelectMany(x => x.ForeignKeys.Select(y => y.Name)).ToList(); + var primaryKeysInSchema = result.TableDefinitions.SelectMany(x => x.Columns.Select(y => y.PrimaryKeyName)) + .Where(x => x.IsNullOrWhiteSpace() == false).ToList(); + + //Add valid and invalid foreign key differences to the result object + // We'll need to do invariant contains with case insensitivity because foreign key, primary key, and even index naming w/ MySQL is not standardized + // In theory you could have: FK_ or fk_ ...or really any standard that your development department (or developer) chooses to use. + foreach (var unknown in unknownConstraintsInDatabase) + { + if (foreignKeysInSchema.InvariantContains(unknown) || primaryKeysInSchema.InvariantContains(unknown) || indexesInSchema.InvariantContains(unknown)) + { + result.ValidConstraints.Add(unknown); + } + else + { + result.Errors.Add(new Tuple("Unknown", unknown)); + } + } + + //Foreign keys: + + var validForeignKeyDifferences = foreignKeysInDatabase.Intersect(foreignKeysInSchema, StringComparer.InvariantCultureIgnoreCase); + foreach (var foreignKey in validForeignKeyDifferences) + { + result.ValidConstraints.Add(foreignKey); + } + var invalidForeignKeyDifferences = + foreignKeysInDatabase.Except(foreignKeysInSchema, StringComparer.InvariantCultureIgnoreCase) + .Union(foreignKeysInSchema.Except(foreignKeysInDatabase, StringComparer.InvariantCultureIgnoreCase)); + foreach (var foreignKey in invalidForeignKeyDifferences) + { + result.Errors.Add(new Tuple("Constraint", foreignKey)); + } + + + //Primary keys: + + //Add valid and invalid primary key differences to the result object + var validPrimaryKeyDifferences = primaryKeysInDatabase.Intersect(primaryKeysInSchema, StringComparer.InvariantCultureIgnoreCase); + foreach (var primaryKey in validPrimaryKeyDifferences) + { + result.ValidConstraints.Add(primaryKey); + } + var invalidPrimaryKeyDifferences = + primaryKeysInDatabase.Except(primaryKeysInSchema, StringComparer.InvariantCultureIgnoreCase) + .Union(primaryKeysInSchema.Except(primaryKeysInDatabase, StringComparer.InvariantCultureIgnoreCase)); + foreach (var primaryKey in invalidPrimaryKeyDifferences) + { + result.Errors.Add(new Tuple("Constraint", primaryKey)); + } + + //Constaints: + + //NOTE: SD: The colIndex checks above should really take care of this but I need to keep this here because it was here before + // and some schema validation checks might rely on this data remaining here! + //Add valid and invalid index differences to the result object + var validIndexDifferences = indexesInDatabase.Intersect(indexesInSchema, StringComparer.InvariantCultureIgnoreCase); + foreach (var index in validIndexDifferences) + { + result.ValidConstraints.Add(index); + } + var invalidIndexDifferences = + indexesInDatabase.Except(indexesInSchema, StringComparer.InvariantCultureIgnoreCase) + .Union(indexesInSchema.Except(indexesInDatabase, StringComparer.InvariantCultureIgnoreCase)); + foreach (var index in invalidIndexDifferences) + { + result.Errors.Add(new Tuple("Constraint", index)); + } + } + + private void ValidateDbColumns(DatabaseSchemaResult result) + { + //Check columns in configured database against columns in schema + var columnsInDatabase = SqlSyntax.GetColumnsInSchema(_database); + var columnsPerTableInDatabase = columnsInDatabase.Select(x => string.Concat(x.TableName, ",", x.ColumnName)).ToList(); + var columnsPerTableInSchema = result.TableDefinitions.SelectMany(x => x.Columns.Select(y => string.Concat(y.TableName, ",", y.Name))).ToList(); + //Add valid and invalid column differences to the result object + var validColumnDifferences = columnsPerTableInDatabase.Intersect(columnsPerTableInSchema, StringComparer.InvariantCultureIgnoreCase); + foreach (var column in validColumnDifferences) + { + result.ValidColumns.Add(column); + } + + var invalidColumnDifferences = + columnsPerTableInDatabase.Except(columnsPerTableInSchema, StringComparer.InvariantCultureIgnoreCase) + .Union(columnsPerTableInSchema.Except(columnsPerTableInDatabase, StringComparer.InvariantCultureIgnoreCase)); + foreach (var column in invalidColumnDifferences) + { + result.Errors.Add(new Tuple("Column", column)); + } + } + + private void ValidateDbTables(DatabaseSchemaResult result) + { + //Check tables in configured database against tables in schema + var tablesInDatabase = SqlSyntax.GetTablesInSchema(_database).ToList(); + var tablesInSchema = result.TableDefinitions.Select(x => x.Name).ToList(); + //Add valid and invalid table differences to the result object + var validTableDifferences = tablesInDatabase.Intersect(tablesInSchema, StringComparer.InvariantCultureIgnoreCase); + foreach (var tableName in validTableDifferences) + { + result.ValidTables.Add(tableName); + } + + var invalidTableDifferences = + tablesInDatabase.Except(tablesInSchema, StringComparer.InvariantCultureIgnoreCase) + .Union(tablesInSchema.Except(tablesInDatabase, StringComparer.InvariantCultureIgnoreCase)); + foreach (var tableName in invalidTableDifferences) + { + result.Errors.Add(new Tuple("Table", tableName)); + } + } + + private void ValidateDbIndexes(DatabaseSchemaResult result) + { + //These are just column indexes NOT constraints or Keys + //var colIndexesInDatabase = result.DbIndexDefinitions.Where(x => x.IndexName.InvariantStartsWith("IX_")).Select(x => x.IndexName).ToList(); + var colIndexesInDatabase = result.DbIndexDefinitions.Select(x => x.IndexName).ToList(); + var indexesInSchema = result.TableDefinitions.SelectMany(x => x.Indexes.Select(y => y.Name)).ToList(); + + //Add valid and invalid index differences to the result object + var validColIndexDifferences = colIndexesInDatabase.Intersect(indexesInSchema, StringComparer.InvariantCultureIgnoreCase); + foreach (var index in validColIndexDifferences) + { + result.ValidIndexes.Add(index); + } + + var invalidColIndexDifferences = + colIndexesInDatabase.Except(indexesInSchema, StringComparer.InvariantCultureIgnoreCase) + .Union(indexesInSchema.Except(colIndexesInDatabase, StringComparer.InvariantCultureIgnoreCase)); + foreach (var index in invalidColIndexDifferences) + { + result.Errors.Add(new Tuple("Index", index)); + } + } + + #region Events + + /// + /// The save event handler + /// + internal delegate void DatabaseEventHandler(DatabaseCreationEventArgs e); + + /// + /// Occurs when [before save]. + /// + internal static event DatabaseEventHandler BeforeCreation; + /// + /// Raises the event. + /// + /// The instance containing the event data. + internal virtual void FireBeforeCreation(DatabaseCreationEventArgs e) + { + BeforeCreation?.Invoke(e); + } + + /// + /// Occurs when [after save]. + /// + internal static event DatabaseEventHandler AfterCreation; + /// + /// Raises the event. + /// + /// The instance containing the event data. + internal virtual void FireAfterCreation(DatabaseCreationEventArgs e) + { + AfterCreation?.Invoke(e); + } + + #endregion + + #region Utilities + + public bool TableExists(string tableName) + { + return SqlSyntax.DoesTableExist(_database, tableName); + } + + public bool TableExists() { + var table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax); + return table != null && TableExists(table.Name); } + + // this is used in tests + internal void CreateTable(bool overwrite = false) + where T : new() + { + var tableType = typeof(T); + CreateTable(overwrite, tableType, new DatabaseDataCreator(_database, _logger)); + } + + public void CreateTable(bool overwrite, Type modelType, DatabaseDataCreator dataCreation) + { + var tableDefinition = DefinitionFactory.GetTableDefinition(modelType, SqlSyntax); + var tableName = tableDefinition.Name; + + var createSql = SqlSyntax.Format(tableDefinition); + var createPrimaryKeySql = SqlSyntax.FormatPrimaryKey(tableDefinition); + var foreignSql = SqlSyntax.Format(tableDefinition.ForeignKeys); + var indexSql = SqlSyntax.Format(tableDefinition.Indexes); + + var tableExist = TableExists(tableName); + if (overwrite && tableExist) + { + DropTable(tableName); + tableExist = false; + } + + if (tableExist == false) + { + using (var transaction = _database.GetTransaction()) + { + //Execute the Create Table sql + var created = _database.Execute(new Sql(createSql)); + _logger.Info(() => $"Create Table '{tableName}' ({created}):\n {createSql}"); + + //If any statements exists for the primary key execute them here + if (string.IsNullOrEmpty(createPrimaryKeySql) == false) + { + var createdPk = _database.Execute(new Sql(createPrimaryKeySql)); + _logger.Info(() => $"Create Primary Key ({createdPk}):\n {createPrimaryKeySql}"); + } + + //Turn on identity insert if db provider is not mysql + if (SqlSyntax.SupportsIdentityInsert() && tableDefinition.Columns.Any(x => x.IsIdentity)) + _database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(tableName)} ON ")); + + //Call the NewTable-event to trigger the insert of base/default data + //OnNewTable(tableName, _db, e, _logger); + + dataCreation.InitializeBaseData(tableName); + + //Turn off identity insert if db provider is not mysql + if (SqlSyntax.SupportsIdentityInsert() && tableDefinition.Columns.Any(x => x.IsIdentity)) + _database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(tableName)} OFF;")); + + //Special case for MySql + if (SqlSyntax is MySqlSyntaxProvider && tableName.Equals("umbracoUser")) + { + _database.Update("SET id = @IdAfter WHERE id = @IdBefore AND userLogin = @Login", new { IdAfter = 0, IdBefore = 1, Login = "admin" }); + } + + //Loop through index statements and execute sql + foreach (var sql in indexSql) + { + var createdIndex = _database.Execute(new Sql(sql)); + _logger.Info(() => $"Create Index ({createdIndex}):\n {sql}"); + } + + //Loop through foreignkey statements and execute sql + foreach (var sql in foreignSql) + { + var createdFk = _database.Execute(new Sql(sql)); + _logger.Info(() => $"Create Foreign Key ({createdFk}):\n {sql}"); + } + + transaction.Complete(); + } + } + + _logger.Info(() => $"Created table '{tableName}'"); + } + + public void DropTable(string tableName) + { + var sql = new Sql(string.Format(SqlSyntax.DropTable, SqlSyntax.GetQuotedTableName(tableName))); + _database.Execute(sql); + } + + #endregion + } +} diff --git a/src/Umbraco.Core/Migrations/MigrationPlan.cs b/src/Umbraco.Core/Migrations/MigrationPlan.cs index 7bf51d74f1..bea98feac3 100644 --- a/src/Umbraco.Core/Migrations/MigrationPlan.cs +++ b/src/Umbraco.Core/Migrations/MigrationPlan.cs @@ -97,12 +97,7 @@ namespace Umbraco.Core.Migrations throw new InvalidOperationException($"A transition from state \"{sourceState}\" has already been defined."); // register the transition - _transitions[sourceState] = new Transition - { - SourceState = sourceState, - TargetState = targetState, - MigrationType = migration - }; + _transitions[sourceState] = new Transition(sourceState, targetState, migration); // register the target state if we don't know it already // this is how we keep track of the final state - because @@ -112,7 +107,7 @@ namespace Umbraco.Core.Migrations _transitions.Add(targetState, null); _prevState = targetState; - _finalState = null; + _finalState = null; // force re-validation return this; } @@ -145,24 +140,87 @@ namespace Umbraco.Core.Migrations return this; } + /// + /// Copies a chain. + /// + /// Copies the chain going from startState to endState, with new states going from sourceState to targetState. + public MigrationPlan CopyChain(string sourceState, string startState, string endState, string targetState) + { + if (sourceState == null) throw new ArgumentNullException(nameof(sourceState)); + if (string.IsNullOrWhiteSpace(startState)) throw new ArgumentNullOrEmptyException(nameof(startState)); + if (string.IsNullOrWhiteSpace(endState)) throw new ArgumentNullOrEmptyException(nameof(endState)); + if (string.IsNullOrWhiteSpace(targetState)) throw new ArgumentNullOrEmptyException(nameof(targetState)); + if (sourceState == targetState) throw new ArgumentException("Source and target states cannot be identical."); + if (startState == endState) throw new ArgumentException("Start and end states cannot be identical."); + + sourceState = sourceState.Trim(); + startState = startState.Trim(); + endState = endState.Trim(); + targetState = targetState.Trim(); + + var state = startState; + var visited = new HashSet(); + + while (state != endState) + { + if (visited.Contains(state)) + throw new InvalidOperationException("A loop was detected in the copied chain."); + visited.Add(state); + + if (!_transitions.TryGetValue(state, out var transition)) + throw new InvalidOperationException($"There is no transition from state \"{sourceState}\"."); + + var newTargetState = transition.TargetState == endState + ? targetState + : Guid.NewGuid().ToString("B").ToUpper(); + Add(sourceState, newTargetState, transition.MigrationType); + sourceState = newTargetState; + state = transition.TargetState; + } + + return this; + } + + /// + /// Copies a chain. + /// + /// Copies the chain going from startState to endState, with new states going from chain to targetState. + public MigrationPlan CopyChain(string startState, string endState, string targetState) + => CopyChain(_prevState, startState, endState, targetState); + /// /// Gets the initial state. /// - /// The initial state is the state when no entry for the plan - /// could be found in the database (i.e. the plan has never run). + /// The initial state is the state when the plan has never + /// run. By default, it is the empty string, but plans may override + /// it if they have other ways of determining where to start from. public virtual string InitialState => string.Empty; /// /// Gets the final state. /// - public string FinalState => _finalState ?? (_finalState = Validate()); + public string FinalState + { + get + { + // modifying the plan clears _finalState + // Validate() either sets _finalState, or throws + if (_finalState == null) + Validate(); + + return _finalState; + } + } /// /// Validates the plan. /// /// The plan's final state. - public string Validate() + public void Validate() { + if (_finalState != null) + return; + // quick check for dead ends - a dead end is a transition that has a target state // that is not null and does not match any source state. such a target state has // been registered as a source state with a null transition. so there should be only @@ -194,7 +252,7 @@ namespace Umbraco.Core.Migrations verified.AddRange(visited); } - return finalState; + _finalState = finalState; } /// @@ -245,11 +303,43 @@ namespace Umbraco.Core.Migrations return origState; } + /// + /// Represents a plan transition. + /// private class Transition { - public string SourceState { get; set; } - public string TargetState { get; set; } - public Type MigrationType { get; set; } + /// + /// Initializes a new instance of the class. + /// + public Transition(string sourceState, string targetState, Type migrationTtype) + { + SourceState = sourceState; + TargetState = targetState; + MigrationType = migrationTtype; + } + + /// + /// Gets the source state. + /// + public string SourceState { get; } + + /// + /// Gets the target state. + /// + public string TargetState { get; } + + /// + /// Gets the migration type. + /// + public Type MigrationType { get; } + + /// + public override string ToString() + { + return MigrationType == typeof(NoopMigration) + ? $"{(SourceState == "" ? "" : SourceState)} --> {TargetState}" + : $"{SourceState} -- ({MigrationType.FullName}) --> {TargetState}"; + } } } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index b7a77b10ce..3729ae1877 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -3,13 +3,7 @@ using System.Configuration; using Semver; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; -using Umbraco.Core.Migrations.Upgrade.V_7_10_0; -using Umbraco.Core.Migrations.Upgrade.V_7_5_0; -using Umbraco.Core.Migrations.Upgrade.V_7_5_5; -using Umbraco.Core.Migrations.Upgrade.V_7_6_0; -using Umbraco.Core.Migrations.Upgrade.V_7_7_0; -using Umbraco.Core.Migrations.Upgrade.V_7_8_0; -using Umbraco.Core.Migrations.Upgrade.V_7_9_0; +using Umbraco.Core.Migrations.Upgrade.V_7_12_0; using Umbraco.Core.Migrations.Upgrade.V_8_0_0; namespace Umbraco.Core.Migrations.Upgrade @@ -37,7 +31,7 @@ namespace Umbraco.Core.Migrations.Upgrade /// /// The default initial state in plans is string.Empty. /// When upgrading from version 7, we want to use specific initial states - /// that are e.g. "{orig-7.9.3}", "{orig-7.11.1}", etc. so we can chain the proper + /// that are e.g. "{init-7.9.3}", "{init-7.11.1}", etc. so we can chain the proper /// migrations. /// This is also where we detect the current version, and reject invalid /// upgrades (from a tool old version, or going back in time, etc). @@ -50,65 +44,50 @@ namespace Umbraco.Core.Migrations.Upgrade if (!SemVersion.TryParse(ConfigurationManager.AppSettings["umbracoConfigurationStatus"], out var currentVersion)) throw new InvalidOperationException("Could not get current version from web.config umbracoConfigurationStatus appSetting."); - // must be at least 7.? - fixme adjust when releasing + // we currently support upgrading from 7.10.0 and later if (currentVersion < new SemVersion(7, 10)) - throw new InvalidOperationException($"Version {currentVersion} cannot be upgraded to {UmbracoVersion.SemanticVersion}."); + throw new InvalidOperationException($"Version {currentVersion} cannot be migrated to {UmbracoVersion.SemanticVersion}."); // cannot go back in time if (currentVersion > UmbracoVersion.SemanticVersion) throw new InvalidOperationException($"Version {currentVersion} cannot be downgraded to {UmbracoVersion.SemanticVersion}."); - switch (currentVersion.Major) - { - case 7: - // upgrading from version 7 - return "{orig-" + currentVersion + "}"; - case 8: // fixme remove when releasing - // upgrading from version 8 - // should never happen, this is very temp and for my own website - zpqrtbnk - return "{04F54303-3055-4700-8F76-35A37F232FF5}"; // right before the variants migration - default: - throw new InvalidOperationException($"Version {currentVersion} is not supported by the migration plan."); - } + // upgrading from version 7 => initial state is eg "{init-7.10.0}" + // anything else is not supported - ie if 8 and above, we should have an initial state already + if (currentVersion.Major != 7) + throw new InvalidOperationException($"Version {currentVersion} is not supported by the migration plan."); + return "{init-" + currentVersion + "}"; } } /// protected override void DefinePlan() { - // NOTE: MODIFYING THE PLAN + // MODIFYING THE PLAN // // Please take great care when modifying the plan! // // * Creating a migration for version 8: - // Append the migration to the main version 8 chain, using a new guid. - // Update the final state (see end of file) to that guid. - // Append the migration to version 7 upgrade chains. - // * Porting a migration from version 7: - // Append the migration to the main version 8 chain, using a new guid. - // Update the final state (see end of file) to that guid. - // Update all init-7.x.y chains. - - - // UPGRADE FROM 7, OLDEST + // Append the migration to the main chain, using a new guid, before the "//FINAL" comment // - // When upgrading from version 7, the state is automatically set to {init-7.x.y} where - // 7.x.y is the version. We need to define a chain starting at that state and taking - // us to version 8. And we need such a chain for each 7.x.y version that can be upgraded - // to version 8, bearing in mind that new releases of version 7 will probably be - // created *after* the first released of version 8. + // If the new migration causes a merge conflict, because someone else also added another + // new migration, you NEED to fix the conflict by providing one default path, and paths + // out of the conflict states (see example below). // - // fixme adjust when releasing the first public (alpha?) version + // * Porting from version 7: + // Append the ported migration to the main chain, using a new guid (same as above). + // Create a new special chain from the {init-...} state to the main chain (see example + // below). - // we don't support upgrading from versions older than 7.? - // and then we only need to run v8 migrations + + // plan starts at 7.10.0 (anything before 7.10.0 is not supported) + // upgrades from 7 to 8, and then takes care of all eventual upgrades // From("{init-7.10.0}"); - Chain("{7C447271-CA3F-4A6A-A913-5D77015655CB}"); + Chain("{7C447271-CA3F-4A6A-A913-5D77015655CB}"); Chain("{CBFF58A2-7B50-4F75-8E98-249920DB0F37}"); Chain("{3D18920C-E84D-405C-A06A-B7CEE52FE5DD}"); - Chain("{FB0A5429-587E-4BD0-8A67-20F0E7E62FF7}"); Chain("{F0C42457-6A3B-4912-A7EA-F27ED85A2092}"); Chain("{8640C9E4-A1C0-4C59-99BB-609B4E604981}"); @@ -116,115 +95,45 @@ namespace Umbraco.Core.Migrations.Upgrade Chain("{9DF05B77-11D1-475C-A00A-B656AF7E0908}"); Chain("{6FE3EF34-44A0-4992-B379-B40BC4EF1C4D}"); Chain("{7F59355A-0EC9-4438-8157-EB517E6D2727}"); - Chain("{66B6821A-0DE3-4DF8-A6A4-65ABD211EDDE}"); - Chain("{49506BAE-CEBB-4431-A1A6-24AD6EBBBC57}"); - Chain("{083A9894-903D-41B7-B6B3-9EAF2D4CCED0}"); - Chain("{42097524-0F8C-482C-BD79-AC7407D8A028}"); - Chain("{3608CD41-792A-4E9A-A97D-42A5E797EE31}"); - // must chain to v8 final state (see at end of file) - Chain("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); - - - // UPGRADE FROM 7, MORE RECENT - // - // handle more recent versions - not yet - - // for more recent versions... - // 7.10.x = same as 7.10.0? - //From("{init-7.10.1}").Chain("{init-7.10.0}"); - - - // VERSION 8 PLAN - // - // this is the master Umbraco migration plan, starting from the very first version 8 - // release, which was a pre-pre-alpha, years ago. It contains migrations created - // for version 8, along with migrations ported from version 7 as version 7 evolves. - // It is therefore *normal* that some pure version 8 migrations are mixed with - // migrations merged from version 7. - // - // new migrations should always be *appended* to the *end* of the chain. - - // 8.0.0 - From("{init-origin}"); // "origin" was 7.4.something - Chain("{98347B5E-65BF-4DD7-BB43-A09CB7AF4FCA}"); - Chain("{1E8165C4-942D-40DC-AC76-C5FF8831E400}"); - Chain("{39E15568-7AAD-4D54-81D0-758CCFC529F8}"); - Chain("{55C3F97D-BDA7-4FB1-A743-B0456B56EAA3}"); - - // merging from 7.5.0 - Chain("{287F9E39-F673-42F7-908C-21659AB13B13}"); - Chain("{2D08588A-AD90-479C-9F6E-A99B60BA7226}"); - Chain("{2D917FF8-AC81-4C00-A407-1F4B1DF6089C}"); - - // merging from 7.5.5 - Chain("{44484C32-EEB3-4A12-B1CB-11E02CE22AB2}"); - - // merging from 7.6.0 - Chain("{3586E4E9-2922-49EB-8E2A-A530CE6DBDE0}"); - Chain("{D4A5674F-654D-4CC7-85E5-CFDBC533A318}"); - Chain("{7F828EDD-6622-4A8D-AD80-EEAF46C11680}"); - Chain("{F30AC223-D277-4D1F-B2AB-F0F0D3546CE1}"); - Chain("{7C27E310-CF48-4637-A22E-8D87355161C1}"); - Chain("{7D2ABA16-EE48-4569-8827-E81370FC4871}"); - Chain("{02879EDF-13A8-43AF-87A5-DD85723D0016}"); - Chain("{5496C6CC-3AE0-4789-AF49-5BB4E28FA424}"); - Chain("{8995332B-085E-4C0C-849E-9A77E79F4293}"); - - // merging from 7.7.0 - Chain("{74319856-7681-46B1-AA0D-F7E896FBE6A1}"); - Chain("{0427B0A2-994A-4AB4-BFF3-31B20614F6C9}"); - Chain("{F0D6F782-E432-46DE-A3A7-2AF06DB8853B}"); - Chain("{AEB2BA2B-71E4-4B1B-AB6C-CEFB7F06FEEB}"); - Chain("{B5A6C799-B91E-496F-A1FE-7B4FE98BF6AB}"); - Chain("{04F54303-3055-4700-8F76-35A37F232FF5}"); - - // 8.0.0 - Chain("{6550C7E8-77B7-4DE3-9B58-E31C81CB9504}"); - Chain("{E3388F73-89FA-45FE-A539-C7FACC8D63DD}"); - Chain("{82C4BA1D-7720-46B1-BBD7-07F3F73800E6}"); - Chain("{139F26D7-7E08-48E3-81D9-E50A21A72F67}"); - Chain("{CC1B1201-1328-443C-954A-E0BBB8CCC1B5}"); - Chain("{CA7DB949-3EF4-403D-8464-F9BA36A52E87}"); - Chain("{7F0BF916-F64E-4B25-864A-170D6E6B68E5}"); - - // merging from 7.8.0 - Chain("{FDCB727A-EFB6-49F3-89E4-A346503AB849}"); - Chain("{2A796A08-4FE4-4783-A1A5-B8A6C8AA4A92}"); - Chain("{1A46A98B-2AAB-4C8E-870F-A2D55A97FD1F}"); - Chain("{0AE053F6-2683-4234-87B2-E963F8CE9498}"); - Chain("{D454541C-15C5-41CF-8109-937F26A78E71}"); - - // merging from 7.9.0 - Chain("{89A728D1-FF4C-4155-A269-62CC09AD2131}"); - Chain("{FD8631BC-0388-425C-A451-5F58574F6F05}"); - Chain("{2821F53E-C58B-4812-B184-9CD240F990D7}"); - Chain("{8918450B-3DA0-4BB7-886A-6FA8B7E4186E}"); - - // mergin from 7.10.0 - Chain("{79591E91-01EA-43F7-AC58-7BD286DB1E77}"); - - // 8.0.0 // AddVariationTables1 has been superceeded by AddVariationTables2 //Chain("{941B2ABA-2D06-4E04-81F5-74224F1DB037}"); Chain("{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}"); - - // however, need to take care of ppl in post-AddVariationTables1 state + // however, provide a path out of the old state Add("{941B2ABA-2D06-4E04-81F5-74224F1DB037}", "{76DF5CD7-A884-41A5-8DC6-7860D95B1DF5}"); + // resume at {76DF5CD7-A884-41A5-8DC6-7860D95B1DF5} ... - // 8.0.0 Chain("{A7540C58-171D-462A-91C5-7A9AA5CB8BFD}"); - - // merge - Chain("{3E44F712-E2E3-473A-AE49-5D7F8E67CE3F}"); // shannon added that one - let's keep it as the default path - Chain("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); // then add stephan's - to new final state - //Chain("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}"); // stephan added that one - need a path to final state - Add("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}", "{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); - // FINAL STATE - MUST MATCH LAST ONE ABOVE ! - // whenever this changes, update all references in this file! + Chain("{3E44F712-E2E3-473A-AE49-5D7F8E67CE3F}"); // shannon added that one - let's keep it as the default path + //Chain("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}"); // stephan added that one = merge conflict, remove, + Chain("{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); // but it after shannon's, with a new target state, + Add("{65D6B71C-BDD5-4A2E-8D35-8896325E9151}", "{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); // and provide a path out of the conflict state + // resume at {4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4} ... - Add(string.Empty, "{4CACE351-C6B9-4F0C-A6BA-85A02BBD39E4}"); + Chain("{1350617A-4930-4D61-852F-E3AA9E692173}"); + Chain("{39E5B1F7-A50B-437E-B768-1723AEC45B65}"); // from 7.12.0 + //FINAL + + + + + // and then, need to support upgrading from more recent 7.x + // + From("{init-7.10.1}").Chain("{init-7.10.0}"); // same as 7.10.0 + From("{init-7.10.2}").Chain("{init-7.10.0}"); // same as 7.10.0 + From("{init-7.10.3}").Chain("{init-7.10.0}"); // same as 7.10.0 + From("{init-7.10.4}").Chain("{init-7.10.0}"); // same as 7.10.0 + From("{init-7.11.0}").Chain("{init-7.10.0}"); // same as 7.10.0 + From("{init-7.11.1}").Chain("{init-7.10.0}"); // same as 7.10.0 + + // 7.12.0 has a migration, define a custom chain which copies the chain + // going from {init-7.10.0} to former final, and then goes straight to + // main chain, skipping the migration + // + From("{init-7.12.0}"); + // copy from copy to (former final) main chain + CopyChain("{init-7.10.0}", "{1350617A-4930-4D61-852F-E3AA9E692173}", "{39E5B1F7-A50B-437E-B768-1723AEC45B65}"); } } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/UpdateUmbracoConsent.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/UpdateUmbracoConsent.cs new file mode 100644 index 0000000000..d8c6e145b1 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/UpdateUmbracoConsent.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Core.Migrations.Upgrade.V_7_12_0 +{ public class UpdateUmbracoConsent : MigrationBase { + public UpdateUmbracoConsent(IMigrationContext context) + : base(context) + { } + + public override void Migrate() { Alter.Table("umbracoConsent").AlterColumn("comment").AsString().Nullable(); } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs new file mode 100644 index 0000000000..23c835a327 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ContentVariationMigration.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 +{ + public class ContentVariationMigration : MigrationBase + { + public ContentVariationMigration(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + byte GetNewValue(byte oldValue) + { + switch (oldValue) + { + case 0: // Unknown + case 1: // InvariantNeutral + return 0; // Unknown + case 2: // CultureNeutral + case 3: // CultureNeutral | InvariantNeutral + return 1; // Culture + case 4: // InvariantSegment + case 5: // InvariantSegment | InvariantNeutral + return 2; // Segment + case 6: // InvariantSegment | CultureNeutral + case 7: // InvariantSegment | CultureNeutral | InvariantNeutral + case 8: // CultureSegment + case 9: // CultureSegment | InvariantNeutral + case 10: // CultureSegment | CultureNeutral + case 11: // CultureSegment | CultureNeutral | InvariantNeutral + case 12: // etc + case 13: + case 14: + case 15: + return 3; // Culture | Segment + default: + throw new NotSupportedException($"Invalid value {oldValue}."); + } + } + + var propertyTypes = Database.Fetch(Sql().Select().From()); + foreach (var dto in propertyTypes) + { + dto.Variations = GetNewValue(dto.Variations); + Database.Update(dto); + } + + var contentTypes = Database.Fetch(Sql().Select().From()); + foreach (var dto in contentTypes) + { + dto.Variations = GetNewValue(dto.Variations); + Database.Update(dto); + } + } + } +} diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs index e671b45968..10869c62da 100644 --- a/src/Umbraco.Core/Models/Content.cs +++ b/src/Umbraco.Core/Models/Content.cs @@ -21,7 +21,8 @@ namespace Umbraco.Core.Models private DateTime? _releaseDate; private DateTime? _expireDate; private Dictionary _publishInfos; - private HashSet _edited; + private Dictionary _publishInfosOrig; + private HashSet _editedCultures; private static readonly Lazy Ps = new Lazy(); @@ -188,123 +189,118 @@ namespace Umbraco.Core.Models [IgnoreDataMember] public IContentType ContentType => _contentType; + /// [IgnoreDataMember] - public DateTime? PublishDate { get; internal set; } + public DateTime? PublishDate { get; internal set; } // set by persistence + /// [IgnoreDataMember] - public int? PublisherId { get; internal set; } + public int? PublisherId { get; internal set; } // set by persistence + /// [IgnoreDataMember] - public ITemplate PublishTemplate { get; internal set; } + public ITemplate PublishTemplate { get; internal set; } // set by persistence + /// [IgnoreDataMember] - public string PublishName { get; internal set; } + public string PublishName { get; internal set; } // set by persistence - // sets publish infos - // internal for repositories - // clear by clearing name - internal void SetPublishInfos(string culture, string name, DateTime date) - { - if (string.IsNullOrWhiteSpace(name)) - throw new ArgumentNullOrEmptyException(nameof(name)); + /// + [IgnoreDataMember] + public IEnumerable EditedCultures => CultureNames.Keys.Where(IsCultureEdited); - // this is the only place where we set PublishName (apart from factories etc), and we must ensure - // that we do have an invariant name, as soon as we have a variant name, else we would end up not - // being able to publish - and not being able to change the name, as PublishName is readonly. - // see also: DocumentRepository.EnsureInvariantNameValues() - which deals with Name. - // see also: U4-11286 - if (culture == null || string.IsNullOrEmpty(PublishName)) - { - PublishName = name; - PublishDate = date; - } + /// + [IgnoreDataMember] + public IEnumerable PublishedCultures => _publishInfos?.Keys ?? Enumerable.Empty(); - if (culture != null) - { - // private method, assume that culture is valid + /// + public bool IsCulturePublished(string culture) + => _publishInfos != null && _publishInfos.ContainsKey(culture); - if (_publishInfos == null) - _publishInfos = new Dictionary(StringComparer.OrdinalIgnoreCase); + /// + public bool WasCulturePublished(string culture) + => _publishInfosOrig != null && _publishInfosOrig.ContainsKey(culture); - _publishInfos[culture] = (name, date); - } - } + /// + public bool IsCultureEdited(string culture) + => !IsCulturePublished(culture) || (_editedCultures != null && _editedCultures.Contains(culture)); /// [IgnoreDataMember] - public IReadOnlyDictionary PublishCultureNames => _publishInfos?.ToDictionary(x => x.Key, x => x.Value.Name, StringComparer.OrdinalIgnoreCase) ?? NoNames; + public IReadOnlyDictionary PublishNames => _publishInfos?.ToDictionary(x => x.Key, x => x.Value.Name, StringComparer.OrdinalIgnoreCase) ?? NoNames; /// public string GetPublishName(string culture) { - if (culture == null) return PublishName; + if (culture.IsNullOrWhiteSpace()) return PublishName; + if (!ContentTypeBase.VariesByCulture()) return null; if (_publishInfos == null) return null; return _publishInfos.TryGetValue(culture, out var infos) ? infos.Name : null; } - // clears a publish name - private void ClearPublishName(string culture) + /// + public DateTime? GetPublishDate(string culture) { - if (culture == null) - { - PublishName = null; - return; - } - - if (_publishInfos == null) return; - _publishInfos.Remove(culture); - if (_publishInfos.Count == 0) - _publishInfos = null; + if (culture.IsNullOrWhiteSpace()) return PublishDate; + if (!ContentTypeBase.VariesByCulture()) return null; + if (_publishInfos == null) return null; + return _publishInfos.TryGetValue(culture, out var infos) ? infos.Date : (DateTime?) null; } - // clears all publish names - private void ClearPublishNames() + // internal for repository + internal void SetPublishInfo(string culture, string name, DateTime date) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentNullOrEmptyException(nameof(name)); + + if (culture.IsNullOrWhiteSpace()) + throw new ArgumentNullOrEmptyException(nameof(culture)); + + if (_publishInfos == null) + _publishInfos = new Dictionary(StringComparer.OrdinalIgnoreCase); + + _publishInfos[culture] = (name, date); + } + + private void ClearPublishInfos() { - PublishName = null; _publishInfos = null; } - /// - public bool IsCulturePublished(string culture) - => !string.IsNullOrWhiteSpace(GetPublishName(culture)); - - /// - public DateTime GetCulturePublishDate(string culture) + private void ClearPublishInfo(string culture) { - if (_publishInfos != null && _publishInfos.TryGetValue(culture, out var infos)) - return infos.Date; - throw new InvalidOperationException($"Culture \"{culture}\" is not published."); - } + if (culture.IsNullOrWhiteSpace()) + throw new ArgumentNullOrEmptyException(nameof(culture)); - /// - public IEnumerable PublishedCultures => _publishInfos?.Keys ?? Enumerable.Empty(); - - /// - public bool IsCultureEdited(string culture) - { - return string.IsNullOrWhiteSpace(GetPublishName(culture)) || (_edited != null && _edited.Contains(culture)); + if (_publishInfos == null) return; + _publishInfos.Remove(culture); + if (_publishInfos.Count == 0) _publishInfos = null; } // sets a publish edited internal void SetCultureEdited(string culture) { - if (_edited == null) - _edited = new HashSet(StringComparer.OrdinalIgnoreCase); - _edited.Add(culture); + if (culture.IsNullOrWhiteSpace()) + throw new ArgumentNullOrEmptyException(nameof(culture)); + if (_editedCultures == null) + _editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase); + _editedCultures.Add(culture); } // sets all publish edited internal void SetCultureEdited(IEnumerable cultures) { - _edited = new HashSet(cultures, StringComparer.OrdinalIgnoreCase); + if (cultures == null) + { + _editedCultures = null; + } + else + { + var editedCultures = new HashSet(cultures.Where(x => !x.IsNullOrWhiteSpace()), StringComparer.OrdinalIgnoreCase); + _editedCultures = editedCultures.Count > 0 ? editedCultures : null; + } } - /// - public IEnumerable EditedCultures => CultureNames.Keys.Where(IsCultureEdited); - - /// - public IEnumerable AvailableCultures => CultureNames.Keys; - [IgnoreDataMember] public int PublishedVersionId { get; internal set; } @@ -312,251 +308,73 @@ namespace Umbraco.Core.Models public bool Blueprint { get; internal set; } /// - internal virtual bool TryPublishAllValues() + public virtual bool PublishCulture(string culture = "*") { - // the values we want to publish should be valid - if (ValidateAllProperties().Any()) - return false; //fixme this should return an attempt with error results + culture = culture.NullOrWhiteSpaceAsNull(); - // Name and PublishName are managed by the repository, but Names and PublishNames - // must be managed here as they depend on the existing / supported variations. - if (string.IsNullOrWhiteSpace(Name)) - throw new InvalidOperationException($"Cannot publish invariant culture without a name."); - PublishName = Name; - var now = DateTime.Now; - foreach (var (culture, name) in CultureNames) - { - if (string.IsNullOrWhiteSpace(name)) - return false; //fixme this should return an attempt with error results - - SetPublishInfos(culture, name, now); - } - - // property.PublishAllValues only deals with supported variations (if any) - foreach (var property in Properties) - property.PublishAllValues(); - - _publishedState = PublishedState.Publishing; - return true; - } - - /// - public virtual bool TryPublishValues(string culture = null, string segment = null) - { - // the variation should be supported by the content type - ContentType.ValidateVariation(culture, segment, throwIfInvalid: true); + // the variation should be supported by the content type properties + // if the content type is invariant, only '*' and 'null' is ok + // if the content type varies, everything is ok because some properties may be invariant + if (!ContentType.SupportsPropertyVariation(culture, "*", true)) + throw new NotSupportedException($"Culture \"{culture}\" is not supported by content type \"{ContentType.Alias}\" with variation \"{ContentType.Variations}\"."); // the values we want to publish should be valid - if (ValidateProperties(culture, segment).Any()) - return false; //fixme this should return an attempt with error results - - // Name and PublishName are managed by the repository, but Names and PublishNames - // must be managed here as they depend on the existing / supported variations. - if (segment == null) - { - var name = GetName(culture); - if (string.IsNullOrWhiteSpace(name)) - return false; //fixme this should return an attempt with error results - - SetPublishInfos(culture, name, DateTime.Now); - } - - // property.PublishValue throws on invalid variation, so filter them out - foreach (var property in Properties.Where(x => x.PropertyType.ValidateVariation(culture, segment, throwIfInvalid: false))) - property.PublishValue(culture, segment); - - _publishedState = PublishedState.Publishing; - return true; - } - - /// - internal virtual bool PublishCultureValues(string culture = null) - { - //fixme - needs API review as this is not used apart from in tests - - // the values we want to publish should be valid - if (ValidatePropertiesForCulture(culture).Any()) + if (ValidateProperties(culture).Any()) return false; - // Name and PublishName are managed by the repository, but Names and PublishNames - // must be managed here as they depend on the existing / supported variations. - var name = GetName(culture); - if (string.IsNullOrWhiteSpace(name)) - throw new InvalidOperationException($"Cannot publish {culture ?? "invariant"} culture without a name."); - SetPublishInfos(culture, name, DateTime.Now); + var alsoInvariant = false; + if (culture == "*") // all cultures + { + foreach (var c in AvailableCultures) + { + var name = GetCultureName(c); + if (string.IsNullOrWhiteSpace(name)) + return false; + SetPublishInfo(c, name, DateTime.Now); + } + } + else // one single culture + { + var name = GetCultureName(culture); + if (string.IsNullOrWhiteSpace(name)) + return false; + SetPublishInfo(culture, name, DateTime.Now); + alsoInvariant = true; // we also want to publish invariant values + } - // property.PublishCultureValues only deals with supported variations (if any) + // property.PublishValues only publishes what is valid, variation-wise foreach (var property in Properties) - property.PublishCultureValues(culture); + { + property.PublishValues(culture); + if (alsoInvariant) + property.PublishValues(null); + } _publishedState = PublishedState.Publishing; return true; } /// - public virtual void ClearAllPublishedValues() + public virtual void UnpublishCulture(string culture = "*") { - // property.ClearPublishedAllValues only deals with supported variations (if any) - foreach (var property in Properties) - property.ClearPublishedAllValues(); + culture = culture.NullOrWhiteSpaceAsNull(); - // Name and PublishName are managed by the repository, but Names and PublishNames - // must be managed here as they depend on the existing / supported variations. - ClearPublishNames(); + // the variation should be supported by the content type properties + if (!ContentType.SupportsPropertyVariation(culture, "*", true)) + throw new NotSupportedException($"Culture \"{culture}\" is not supported by content type \"{ContentType.Alias}\" with variation \"{ContentType.Variations}\"."); + + if (culture == "*") // all cultures + ClearPublishInfos(); + else // one single culture + ClearPublishInfo(culture); + + // property.PublishValues only publishes what is valid, variation-wise + foreach (var property in Properties) + property.UnpublishValues(culture); _publishedState = PublishedState.Publishing; } - /// - public virtual void ClearPublishedValues(string culture = null, string segment = null) - { - // the variation should be supported by the content type - ContentType.ValidateVariation(culture, segment, throwIfInvalid: true); - - // property.ClearPublishedValue throws on invalid variation, so filter them out - foreach (var property in Properties.Where(x => x.PropertyType.ValidateVariation(culture, segment, throwIfInvalid: false))) - property.ClearPublishedValue(culture, segment); - - // Name and PublishName are managed by the repository, but Names and PublishNames - // must be managed here as they depend on the existing / supported variations. - ClearPublishName(culture); - - _publishedState = PublishedState.Publishing; - } - - /// - public virtual void ClearCulturePublishedValues(string culture = null) - { - // property.ClearPublishedCultureValues only deals with supported variations (if any) - foreach (var property in Properties) - property.ClearPublishedCultureValues(culture); - - // Name and PublishName are managed by the repository, but Names and PublishNames - // must be managed here as they depend on the existing / supported variations. - ClearPublishName(culture); - - _publishedState = PublishedState.Publishing; - } - - private bool CopyingFromSelf(IContent other) - { - // copying from the same Id and VersionPk - return Id == other.Id && VersionId == other.VersionId; - } - - /// - public virtual void CopyAllValues(IContent other) - { - if (other.ContentTypeId != ContentTypeId) - throw new InvalidOperationException("Cannot copy values from a different content type."); - - // we could copy from another document entirely, - // or from another version of the same document, - // in which case there is a special case. - var published = CopyingFromSelf(other); - - // note: use property.SetValue(), don't assign pvalue.EditValue, else change tracking fails - - // clear all existing properties - foreach (var property in Properties) - foreach (var pvalue in property.Values) - if (property.PropertyType.ValidateVariation(pvalue.Culture, pvalue.Segment, false)) - property.SetValue(null, pvalue.Culture, pvalue.Segment); - - // copy other properties - var otherProperties = other.Properties; - foreach (var otherProperty in otherProperties) - { - var alias = otherProperty.PropertyType.Alias; - foreach (var pvalue in otherProperty.Values) - { - if (!otherProperty.PropertyType.ValidateVariation(pvalue.Culture, pvalue.Segment, false)) - continue; - var value = published ? pvalue.PublishedValue : pvalue.EditedValue; - SetValue(alias, value, pvalue.Culture, pvalue.Segment); - } - } - - // copy names - ClearNames(); - foreach (var (culture, name) in other.CultureNames) - SetName(name, culture); - Name = other.Name; - } - - /// - public virtual void CopyValues(IContent other, string culture = null, string segment = null) - { - if (other.ContentTypeId != ContentTypeId) - throw new InvalidOperationException("Cannot copy values from a different content type."); - - var published = CopyingFromSelf(other); - - // segment is invariant in comparisons - segment = segment?.ToLowerInvariant(); - - // note: use property.SetValue(), don't assign pvalue.EditValue, else change tracking fails - - // clear all existing properties - foreach (var property in Properties) - { - if (!property.PropertyType.ValidateVariation(culture, segment, false)) - continue; - - foreach (var pvalue in property.Values) - if (pvalue.Culture.InvariantEquals(culture) && pvalue.Segment.InvariantEquals(segment)) - property.SetValue(null, pvalue.Culture, pvalue.Segment); - } - - // copy other properties - var otherProperties = other.Properties; - foreach (var otherProperty in otherProperties) - { - if (!otherProperty.PropertyType.ValidateVariation(culture, segment, false)) - continue; - - var alias = otherProperty.PropertyType.Alias; - SetValue(alias, otherProperty.GetValue(culture, segment, published), culture, segment); - } - - // copy name - SetName(other.GetName(culture), culture); - } - - /// - public virtual void CopyCultureValues(IContent other, string culture = null) - { - if (other.ContentTypeId != ContentTypeId) - throw new InvalidOperationException("Cannot copy values from a different content type."); - - var published = CopyingFromSelf(other); - - // note: use property.SetValue(), don't assign pvalue.EditValue, else change tracking fails - - // clear all existing properties - foreach (var property in Properties) - foreach (var pvalue in property.Values) - if (pvalue.Culture.InvariantEquals(culture) && property.PropertyType.ValidateVariation(pvalue.Culture, pvalue.Segment, false)) - property.SetValue(null, pvalue.Culture, pvalue.Segment); - - // copy other properties - var otherProperties = other.Properties; - foreach (var otherProperty in otherProperties) - { - var alias = otherProperty.PropertyType.Alias; - foreach (var pvalue in otherProperty.Values) - { - if (pvalue.Culture != culture || !otherProperty.PropertyType.ValidateVariation(pvalue.Culture, pvalue.Segment, false)) - continue; - var value = published ? pvalue.PublishedValue : pvalue.EditedValue; - SetValue(alias, value, pvalue.Culture, pvalue.Segment); - } - } - - // copy name - SetName(other.GetName(culture), culture); - } - /// /// Changes the for the current content object /// @@ -598,6 +416,11 @@ namespace Umbraco.Core.Models // take care of the published state _publishedState = _published ? PublishedState.Published : PublishedState.Unpublished; + + // take care of publish infos + _publishInfosOrig = _publishInfos == null + ? null + : new Dictionary(_publishInfos, StringComparer.OrdinalIgnoreCase); } /// @@ -630,7 +453,6 @@ namespace Umbraco.Core.Models clone.EnableChangeTracking(); return clone; - } } } diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs index 04fbe269f8..29e36829d2 100644 --- a/src/Umbraco.Core/Models/ContentBase.cs +++ b/src/Umbraco.Core/Models/ContentBase.cs @@ -56,7 +56,7 @@ namespace Umbraco.Core.Models Id = 0; // no identity VersionId = 0; // no versions - SetName(name, culture); + SetCultureName(name, culture); _contentTypeId = contentType.Id; _properties = properties ?? throw new ArgumentNullException(nameof(properties)); @@ -139,104 +139,101 @@ namespace Umbraco.Core.Models #region Cultures + // notes - common rules + // - setting a variant value on an invariant content type throws + // - getting a variant value on an invariant content type returns null + // - setting and getting the invariant value is always possible + // - setting a null value clears the value + + /// + public IEnumerable AvailableCultures + => _cultureInfos?.Select(x => x.Key) ?? Enumerable.Empty(); + + /// + public bool IsCultureAvailable(string culture) + => _cultureInfos != null && _cultureInfos.ContainsKey(culture); + /// [DataMember] public virtual IReadOnlyDictionary CultureNames => _cultureInfos?.ToDictionary(x => x.Key, x => x.Value.Name, StringComparer.OrdinalIgnoreCase) ?? NoNames; - // sets culture infos - // internal for repositories - // clear by clearing name - internal void SetCultureInfos(string culture, string name, DateTime date) - { - if (string.IsNullOrWhiteSpace(name)) - throw new ArgumentNullOrEmptyException(nameof(name)); - - if (culture == null) - { - Name = name; - return; - } - - // private method, assume that culture is valid - - if (_cultureInfos == null) - _cultureInfos = new Dictionary(StringComparer.OrdinalIgnoreCase); - - _cultureInfos[culture] = (name, date); - } - /// - public virtual void SetName(string name, string culture) + public virtual string GetCultureName(string culture) { - if (string.IsNullOrWhiteSpace(name)) - { - ClearName(culture); - return; - } - - if (culture == null) - { - Name = name; - return; - } - - if (ContentTypeBase.Variations.DoesNotSupportCulture()) - throw new NotSupportedException("Content type does not support varying name by culture."); - - if (_cultureInfos == null) - _cultureInfos = new Dictionary(StringComparer.OrdinalIgnoreCase); - - _cultureInfos[culture] = (name, DateTime.Now); - OnPropertyChanged(Ps.Value.NamesSelector); - } - - /// - public virtual string GetName(string culture) - { - if (culture == null) return Name; + if (culture.IsNullOrWhiteSpace()) return Name; + if (!ContentTypeBase.VariesByCulture()) return null; if (_cultureInfos == null) return null; return _cultureInfos.TryGetValue(culture, out var infos) ? infos.Name : null; } /// - public bool IsCultureAvailable(string culture) - => !string.IsNullOrWhiteSpace(GetName(culture)); - - private void ClearName(string culture) + public DateTime? GetCultureDate(string culture) { - if (culture == null) - { - Name = null; - return; - } - - if (ContentTypeBase.Variations.DoesNotSupportCulture()) - throw new NotSupportedException("Content type does not support varying name by culture."); - - if (_cultureInfos == null) return; - if (!_cultureInfos.ContainsKey(culture)) - throw new InvalidOperationException($"Cannot unpublish culture {culture}, the document contains only cultures {string.Join(", ", _cultureInfos.Keys)}"); - - _cultureInfos.Remove(culture); - if (_cultureInfos.Count == 0) - _cultureInfos = null; + if (culture.IsNullOrWhiteSpace()) return null; + if (!ContentTypeBase.VariesByCulture()) return null; + if (_cultureInfos == null) return null; + return _cultureInfos.TryGetValue(culture, out var infos) ? infos.Date : (DateTime?) null; } - protected virtual void ClearNames() + /// + public virtual void SetCultureName(string name, string culture) { - if (ContentTypeBase.Variations.DoesNotSupportCulture()) - throw new NotSupportedException("Content type does not support varying name by culture."); + if (ContentTypeBase.VariesByCulture()) // set on variant content type + { + if (culture.IsNullOrWhiteSpace()) // invariant is ok + { + Name = name; // may be null + } + else if (name.IsNullOrWhiteSpace()) // clear + { + ClearCultureInfo(culture); + } + else // set + { + SetCultureInfo(culture, name, DateTime.Now); + } + } + else // set on invariant content type + { + if (!culture.IsNullOrWhiteSpace()) // invariant is NOT ok + throw new NotSupportedException("Content type does not vary by culture."); + Name = name; // may be null + } + } + + protected void ClearCultureInfos() + { _cultureInfos = null; OnPropertyChanged(Ps.Value.NamesSelector); } - /// - public DateTime GetCultureDate(string culture) + protected void ClearCultureInfo(string culture) { - if (_cultureInfos != null && _cultureInfos.TryGetValue(culture, out var infos)) - return infos.Date; - throw new InvalidOperationException($"Culture \"{culture}\" is not available."); + if (culture.IsNullOrWhiteSpace()) + throw new ArgumentNullOrEmptyException(nameof(culture)); + + if (_cultureInfos == null) return; + _cultureInfos.Remove(culture); + if (_cultureInfos.Count == 0) + _cultureInfos = null; + OnPropertyChanged(Ps.Value.NamesSelector); + } + + // internal for repository + internal void SetCultureInfo(string culture, string name, DateTime date) + { + if (name.IsNullOrWhiteSpace()) + throw new ArgumentNullOrEmptyException(nameof(name)); + + if (culture.IsNullOrWhiteSpace()) + throw new ArgumentNullOrEmptyException(nameof(culture)); + + if (_cultureInfos == null) + _cultureInfos = new Dictionary(StringComparer.OrdinalIgnoreCase); + + _cultureInfos[culture.ToLowerInvariant()] = (name, date); + OnPropertyChanged(Ps.Value.NamesSelector); } #endregion @@ -305,45 +302,113 @@ namespace Umbraco.Core.Models #endregion + #region Copy + + /// + public virtual void CopyFrom(IContent other, string culture = "*") + { + if (other.ContentTypeId != ContentTypeId) + throw new InvalidOperationException("Cannot copy values from a different content type."); + + culture = culture?.ToLowerInvariant().NullOrWhiteSpaceAsNull(); + + // the variation should be supported by the content type properties + // if the content type is invariant, only '*' and 'null' is ok + // if the content type varies, everything is ok because some properties may be invariant + if (!ContentTypeBase.SupportsPropertyVariation(culture, "*", true)) + throw new NotSupportedException($"Culture \"{culture}\" is not supported by content type \"{ContentTypeBase.Alias}\" with variation \"{ContentTypeBase.Variations}\"."); + + // copying from the same Id and VersionPk + var copyingFromSelf = Id == other.Id && VersionId == other.VersionId; + var published = copyingFromSelf; + + // note: use property.SetValue(), don't assign pvalue.EditValue, else change tracking fails + + // clear all existing properties for the specified culture + foreach (var property in Properties) + { + // each property type may or may not support the variation + if (!property.PropertyType.SupportsVariation(culture, "*", wildcards: true)) + continue; + + foreach (var pvalue in property.Values) + if (property.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment, wildcards: true) && + (culture == "*" || pvalue.Culture.InvariantEquals(culture))) + { + property.SetValue(null, pvalue.Culture, pvalue.Segment); + } + } + + // copy properties from 'other' + var otherProperties = other.Properties; + foreach (var otherProperty in otherProperties) + { + if (!otherProperty.PropertyType.SupportsVariation(culture, "*", wildcards: true)) + continue; + + var alias = otherProperty.PropertyType.Alias; + foreach (var pvalue in otherProperty.Values) + { + if (otherProperty.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment, wildcards: true) && + (culture == "*" || pvalue.Culture.InvariantEquals(culture))) + { + var value = published ? pvalue.PublishedValue : pvalue.EditedValue; + SetValue(alias, value, pvalue.Culture, pvalue.Segment); + } + } + } + + // copy names, too + + if (culture == "*") + ClearCultureInfos(); + + if (culture == null || culture == "*") + Name = other.Name; + + foreach (var (otherCulture, otherName) in other.CultureNames) + { + if (culture == "*" || culture == otherCulture) + SetCultureName(otherName, otherCulture); + } + } + + #endregion + #region Validation - internal virtual Property[] ValidateAllProperties() - { - //fixme - needs API review as this is not used apart from in tests - - return Properties.Where(x => !x.IsAllValid()).ToArray(); - } - /// - public bool IsValid(string culture = null, string segment = null) + public bool IsValid(string culture = "*") { - var name = GetName(culture); - if (name.IsNullOrWhiteSpace()) return false; - return ValidateProperties(culture, segment).Length == 0; - } + culture = culture.NullOrWhiteSpaceAsNull(); - /// - public virtual Property[] ValidateProperties(string culture = null, string segment = null) - { - return Properties.Where(x => + if (culture == null) { - if (!culture.IsNullOrWhiteSpace() && x.PropertyType.Variations.DoesNotSupportCulture()) - return false; //has a culture, this prop is only culture invariant, ignore - if (culture.IsNullOrWhiteSpace() && x.PropertyType.Variations.DoesNotSupportInvariant()) - return false; //no culture, this prop is only culture variant, ignore - if (!segment.IsNullOrWhiteSpace() && x.PropertyType.Variations.DoesNotSupportSegment()) - return false; //has segment, this prop is only segment neutral, ignore - if (segment.IsNullOrWhiteSpace() && x.PropertyType.Variations.DoesNotSupportNeutral()) - return false; //no segment, this prop is only non segment neutral, ignore - return !x.IsValid(culture, segment); - }).ToArray(); + if (Name.IsNullOrWhiteSpace()) return false; + return ValidateProperties(null).Length == 0; + } + + if (culture != "*") + { + var name = GetCultureName(culture); + if (name.IsNullOrWhiteSpace()) return false; + return ValidateProperties(culture).Length == 0; + } + + // 'all cultures' + // those that have a name are ok, those without a name... we don't validate + return AvailableCultures.All(c => ValidateProperties(c).Length == 0); } - internal virtual Property[] ValidatePropertiesForCulture(string culture = null) + /// + public virtual Property[] ValidateProperties(string culture = "*") { - //fixme - needs API review as this is not used apart from in tests + var alsoInvariant = culture != null && culture != "*"; - return Properties.Where(x => !x.IsCultureValid(culture)).ToArray(); + return Properties.Where(x => // select properties... + x.PropertyType.SupportsVariation(culture, "*", true) && // that support the variation + (!x.IsValid(culture) || (alsoInvariant && !x.IsValid(null)))) // and are not valid + .ToArray(); } #endregion diff --git a/src/Umbraco.Core/Models/ContentExtensions.cs b/src/Umbraco.Core/Models/ContentExtensions.cs index 2c013e9b90..d8eb900bb2 100644 --- a/src/Umbraco.Core/Models/ContentExtensions.cs +++ b/src/Umbraco.Core/Models/ContentExtensions.cs @@ -5,13 +5,10 @@ using System.IO; using System.Linq; using System.Web; using System.Xml.Linq; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using Umbraco.Core.Composing; using Umbraco.Core.IO; using Umbraco.Core.Models.Entities; using Umbraco.Core.Models.Membership; -using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; @@ -98,12 +95,6 @@ namespace Umbraco.Core.Models return mediaService.GetAncestors(media); } - [Obsolete("Use the overload with the service reference instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public static IEnumerable Ancestors(this IMedia media) - { - return Current.Services.MediaService.GetAncestors(media); - } /// /// Returns a list of the current medias children. @@ -116,12 +107,6 @@ namespace Umbraco.Core.Models return mediaService.GetChildren(media.Id); } - [Obsolete("Use the overload with the service reference instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public static IEnumerable Children(this IMedia media) - { - return Current.Services.MediaService.GetChildren(media.Id); - } /// /// Returns a list of the current medias descendants, not including the media itself. @@ -134,12 +119,6 @@ namespace Umbraco.Core.Models return mediaService.GetDescendants(media); } - [Obsolete("Use the overload with the service reference instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public static IEnumerable Descendants(this IMedia media) - { - return Current.Services.MediaService.GetDescendants(media); - } /// /// Returns the parent of the current media. @@ -152,12 +131,6 @@ namespace Umbraco.Core.Models return mediaService.GetById(media.ParentId); } - [Obsolete("Use the overload with the service reference instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public static IMedia Parent(this IMedia media) - { - return Current.Services.MediaService.GetById(media.ParentId); - } #endregion /// @@ -339,14 +312,6 @@ namespace Umbraco.Core.Models #region User/Profile methods - - [Obsolete("Use the overload that declares the IUserService to use")] - [EditorBrowsable(EditorBrowsableState.Never)] - public static IProfile GetCreatorProfile(this IMedia media) - { - return Current.Services.UserService.GetProfileById(media.CreatorId); - } - /// /// Gets the for the Creator of this media item. /// @@ -454,13 +419,6 @@ namespace Umbraco.Core.Models return packagingService.Export(media, true, raiseEvents: false); } - [Obsolete("Use the overload that declares the IPackagingService to use")] - public static XElement ToXml(this IContent content, bool isPreview) - { - //TODO Do a proper implementation of this - //If current IContent is published we should get latest unpublished version - return content.ToXml(); - } /// /// Creates the xml representation for the object @@ -476,11 +434,6 @@ namespace Umbraco.Core.Models return content.ToXml(packagingService); } - [Obsolete("Use the overload that declares the IPackagingService to use")] - public static XElement ToXml(this IMember member) - { - return ((PackagingService)(Current.Services.PackagingService)).Export(member); - } /// /// Creates the xml representation for the object diff --git a/src/Umbraco.Core/Models/ContentType.cs b/src/Umbraco.Core/Models/ContentType.cs index 4a39166dc8..196c68b23c 100644 --- a/src/Umbraco.Core/Models/ContentType.cs +++ b/src/Umbraco.Core/Models/ContentType.cs @@ -29,15 +29,6 @@ namespace Umbraco.Core.Models _allowedTemplates = new List(); } - /// - /// Constuctor for creating a ContentType with the parent as an inherited type. - /// - /// Use this to ensure inheritance from parent. - /// - [Obsolete("This method is obsolete, use ContentType(IContentType parent, string alias) instead.", false)] - public ContentType(IContentType parent) : this(parent, null) - { - } /// /// Constuctor for creating a ContentType with the parent as an inherited type. diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index f1b61f424a..6f90b5201d 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -46,7 +46,7 @@ namespace Umbraco.Core.Models _propertyTypes = new PropertyTypeCollection(IsPublishing); _propertyTypes.CollectionChanged += PropertyTypesChanged; - _variations = ContentVariation.InvariantNeutral; + _variations = ContentVariation.Nothing; } protected ContentTypeBase(IContentTypeBase parent) @@ -67,7 +67,7 @@ namespace Umbraco.Core.Models _propertyTypes = new PropertyTypeCollection(IsPublishing); _propertyTypes.CollectionChanged += PropertyTypesChanged; - _variations = ContentVariation.InvariantNeutral; + _variations = ContentVariation.Nothing; } /// @@ -201,33 +201,22 @@ namespace Umbraco.Core.Models set => SetPropertyValueAndDetectChanges(value, ref _variations, Ps.Value.VaryBy); } - /// - /// Validates that a variation is valid for the content type. - /// - public bool ValidateVariation(string culture, string segment, bool throwIfInvalid) + /// + public bool SupportsVariation(string culture, string segment, bool wildcards = false) { - ContentVariation variation; - if (culture != null) - { - variation = segment != null - ? ContentVariation.CultureSegment - : ContentVariation.CultureNeutral; - } - else if (segment != null) - { - variation = ContentVariation.InvariantSegment; - } - else - { - variation = ContentVariation.InvariantNeutral; - } - if (!Variations.Has(variation)) - { - if (throwIfInvalid) - throw new NotSupportedException($"Variation {variation} is invalid for content type \"{Alias}\"."); - return false; - } - return true; + // exact validation: cannot accept a 'null' culture if the property type varies + // by culture, and likewise for segment + // wildcard validation: can accept a '*' culture or segment + return Variations.ValidateVariation(culture, segment, true, wildcards, false); + } + + /// + public bool SupportsPropertyVariation(string culture, string segment, bool wildcards = false) + { + // non-exact validation: can accept a 'null' culture if the property type varies + // by culture, and likewise for segment + // wildcard validation: can accept a '*' culture or segment + return Variations.ValidateVariation(culture, segment, false, true, false); } /// diff --git a/src/Umbraco.Core/Models/ContentTypeSort.cs b/src/Umbraco.Core/Models/ContentTypeSort.cs index 000d80777c..e9442ed4c6 100644 --- a/src/Umbraco.Core/Models/ContentTypeSort.cs +++ b/src/Umbraco.Core/Models/ContentTypeSort.cs @@ -8,10 +8,8 @@ namespace Umbraco.Core.Models /// public class ContentTypeSort : IValueObject, IDeepCloneable { - [Obsolete("This parameterless constructor should never be used")] - public ContentTypeSort() - { - } + // this parameterless ctor should never be used BUT is required by AutoMapper in EntityMapperProfile + internal ContentTypeSort() { } /// /// Initializes a new instance of the class. diff --git a/src/Umbraco.Core/Models/ContentVariation.cs b/src/Umbraco.Core/Models/ContentVariation.cs index 5b775b52b9..c122f0b114 100644 --- a/src/Umbraco.Core/Models/ContentVariation.cs +++ b/src/Umbraco.Core/Models/ContentVariation.cs @@ -3,34 +3,34 @@ namespace Umbraco.Core.Models { /// - /// Indicates the values accepted by a property. + /// Indicates how values can vary. /// - [Flags] + /// + /// Values can vary by nothing, or culture, or segment, or both. + /// Varying by culture implies that each culture version of a document can + /// be available or not, and published or not, individually. Varying by segment + /// is a property-level thing. + /// public enum ContentVariation : byte { /// - /// Unknown. + /// Values do not vary. /// - Unknown = 0, + Nothing = 0, /// - /// Accepts values for the invariant culture and the neutral segment. + /// Values vary by culture. /// - InvariantNeutral = 1, + Culture = 1, /// - /// Accepts values for a specified culture and the neutral segment. + /// Values vary by segment. /// - CultureNeutral = 2, + Segment = 2, /// - /// Accepts values for the invariant culture and a specified segment. + /// Values vary by culture and segment. /// - InvariantSegment = 4, - - /// - /// Accepts values for a specified culture and a specified segment. - /// - CultureSegment = 8 + CultureAndSegment = Culture | Segment } } diff --git a/src/Umbraco.Core/Models/EntityExtensions.cs b/src/Umbraco.Core/Models/EntityExtensions.cs index fa7d9b6110..5ef68e99ea 100644 --- a/src/Umbraco.Core/Models/EntityExtensions.cs +++ b/src/Umbraco.Core/Models/EntityExtensions.cs @@ -4,15 +4,6 @@ namespace Umbraco.Core.Models { public static class EntityExtensions { - - /// - /// Determines whether the entity was just created and persisted. - /// - public static bool IsNewEntity(this IRememberBeingDirty entity) - { - return entity.WasPropertyDirty("Id"); - } - /// /// Gets additional data. /// diff --git a/src/Umbraco.Core/Models/IContent.cs b/src/Umbraco.Core/Models/IContent.cs index 13797425ed..9e79a75e25 100644 --- a/src/Umbraco.Core/Models/IContent.cs +++ b/src/Umbraco.Core/Models/IContent.cs @@ -81,7 +81,7 @@ namespace Umbraco.Core.Models ContentStatus Status { get; } /// - /// Gets a value indicating whether a given culture is published. + /// Gets a value indicating whether a culture is published. /// /// /// A culture becomes published whenever values for this culture are published, @@ -90,10 +90,18 @@ namespace Umbraco.Core.Models /// bool IsCulturePublished(string culture); + /// + /// Gets a value indicating whether a culture was published. + /// + /// + /// Mirrors whenever the content item is saved. + /// + bool WasCulturePublished(string culture); + /// /// Gets the date a culture was published. /// - DateTime GetCulturePublishDate(string culture); + DateTime? GetPublishDate(string culture); /// /// Gets a value indicated whether a given culture is edited. @@ -121,12 +129,7 @@ namespace Umbraco.Core.Models /// Because a dictionary key cannot be null this cannot get the invariant /// name, which must be get via the property. /// - IReadOnlyDictionary PublishCultureNames { get; } - - /// - /// Gets the available cultures. - /// - IEnumerable AvailableCultures { get; } + IReadOnlyDictionary PublishNames { get; } /// /// Gets the published cultures. @@ -162,67 +165,22 @@ namespace Umbraco.Core.Models IContent DeepCloneWithResetIdentities(); /// - /// Publishes all values. + /// Registers a culture to be published. /// - /// A value indicating whether the values could be published. + /// A value indicating whether the culture can be published. /// - /// The document must then be published via the content service. - /// Values are not published if they are not valid. + /// Fails if values cannot be published, e.g. if some values are not valid. + /// Publishing must be finalized via the content service SavePublishing method. /// - //fixme return an Attempt with some error results if it doesn't work - //fixme - needs API review as this is not used apart from in tests - //bool TryPublishAllValues(); + // fixme - should return an attempt with error results + bool PublishCulture(string culture = "*"); /// - /// Publishes values. + /// Registers a culture to be unpublished. /// - /// A value indicating whether the values could be published. /// - /// The document must then be published via the content service. - /// Values are not published if they are not valid. + /// Unpublishing must be finalized via the content service SavePublishing method. /// - //fixme return an Attempt with some error results if it doesn't work - bool TryPublishValues(string culture = null, string segment = null); - - /// - /// Publishes the culture/any values. - /// - /// A value indicating whether the values could be published. - /// - /// The document must then be published via the content service. - /// Values are not published if they are not valie. - /// - //fixme - needs API review as this is not used apart from in tests - //bool PublishCultureValues(string culture = null); - - /// - /// Clears all published values. - /// - void ClearAllPublishedValues(); - - /// - /// Clears published values. - /// - void ClearPublishedValues(string culture = null, string segment = null); - - /// - /// Clears the culture/any published values. - /// - void ClearCulturePublishedValues(string culture = null); - - /// - /// Copies values from another document. - /// - void CopyAllValues(IContent other); - - /// - /// Copies values from another document. - /// - void CopyValues(IContent other, string culture = null, string segment = null); - - /// - /// Copies culture/any values from another document. - /// - void CopyCultureValues(IContent other, string culture = null); + void UnpublishCulture(string culture = "*"); } } diff --git a/src/Umbraco.Core/Models/IContentBase.cs b/src/Umbraco.Core/Models/IContentBase.cs index 1605c1da01..3c56b2c737 100644 --- a/src/Umbraco.Core/Models/IContentBase.cs +++ b/src/Umbraco.Core/Models/IContentBase.cs @@ -29,45 +29,64 @@ namespace Umbraco.Core.Models int VersionId { get; } /// - /// Sets the name of the content item for a specified language. + /// Sets the name of the content item for a specified culture. /// /// - /// When is null, sets the invariant - /// language, which sets the property. + /// When is null, sets the invariant + /// culture name, which sets the property. + /// When is not null, throws if the content + /// type does not vary by culture. /// - void SetName(string value, string culture); + void SetCultureName(string value, string culture); /// /// Gets the name of the content item for a specified language. /// /// - /// When is null, gets the invariant - /// language, which is the value of the property. + /// When is null, gets the invariant + /// culture name, which is the value of the property. + /// When is not null, and the content type + /// does not vary by culture, returns null. /// - string GetName(string culture); + string GetCultureName(string culture); /// /// Gets the names of the content item. /// /// - /// Because a dictionary key cannot be null this cannot get the invariant - /// name, which must be get or set via the property. + /// Because a dictionary key cannot be null this cannot contain the invariant + /// culture name, which must be get or set via the property. /// IReadOnlyDictionary CultureNames { get; } + /// + /// Gets the available cultures. + /// + /// + /// Cannot contain the invariant culture, which is always available. + /// + IEnumerable AvailableCultures { get; } + /// /// Gets a value indicating whether a given culture is available. /// /// /// A culture becomes available whenever the content name for this culture is /// non-null, and it becomes unavailable whenever the content name is null. + /// Returns false for the invariant culture, in order to be consistent + /// with , even though the invariant culture is + /// always available. /// bool IsCultureAvailable(string culture); /// /// Gets the date a culture was created. /// - DateTime GetCultureDate(string culture); + /// + /// When is null, returns null. + /// If the specified culture is not available, returns null. + /// + DateTime? GetCultureDate(string culture); /// /// List of properties, which make up all the data available for this Content object @@ -97,47 +116,40 @@ namespace Umbraco.Core.Models /// /// Gets the value of a Property /// + /// Values 'null' and 'empty' are equivalent for culture and segment. object GetValue(string propertyTypeAlias, string culture = null, string segment = null, bool published = false); /// /// Gets the typed value of a Property /// + /// Values 'null' and 'empty' are equivalent for culture and segment. TValue GetValue(string propertyTypeAlias, string culture = null, string segment = null, bool published = false); /// /// Sets the (edited) value of a Property /// + /// Values 'null' and 'empty' are equivalent for culture and segment. void SetValue(string propertyTypeAlias, object value, string culture = null, string segment = null); + /// + /// Copies values from another document. + /// + void CopyFrom(IContent other, string culture = "*"); + + // fixme validate published cultures? + /// /// Checks if the content and property values are valid in order to be persisted. /// - /// - /// - /// - bool IsValid(string culture = null, string segment = null); + /// If the content type is variant, then culture can be either '*' or an actual culture, but neither 'null' nor + /// 'empty'. If the content type is invariant, then culture can be either '*' or null or empty. + bool IsValid(string culture = "*"); /// - /// Gets a value indicating if all properties values are valid. + /// Validates the content item's properties. /// - //fixme - needs API review as this is not used apart from in tests - //Property[] ValidateAllProperties(); - - /// - /// Validates the content item's properties for the provided culture/segment - /// - /// - /// - /// - /// - /// This will not perform validation for properties that do not match the required ContentVariation based on the culture/segment values provided - /// - Property[] ValidateProperties(string culture = null, string segment = null); - - /// - /// Gets a value indicating if the culture properties values are valid. - /// - //fixme - needs API review as this is not used apart from in tests - //Property[] ValidatePropertiesForCulture(string culture = null); + /// If the content type is variant, then culture can be either '*' or an actual culture, but neither 'null' nor + /// 'empty'. If the content type is invariant, then culture can be either '*' or null or empty. + Property[] ValidateProperties(string culture = "*"); } } diff --git a/src/Umbraco.Core/Models/IContentTypeBase.cs b/src/Umbraco.Core/Models/IContentTypeBase.cs index df171d5efc..ef5988344e 100644 --- a/src/Umbraco.Core/Models/IContentTypeBase.cs +++ b/src/Umbraco.Core/Models/IContentTypeBase.cs @@ -49,9 +49,31 @@ namespace Umbraco.Core.Models ContentVariation Variations { get; set; } /// - /// Validates that a variation is valid for the content type. + /// Validates that a combination of culture and segment is valid for the content type. /// - bool ValidateVariation(string culture, string segment, bool throwIfInvalid); + /// The culture. + /// The segment. + /// A value indicating whether wilcards are supported. + /// True if the combination is valid; otherwise false. + /// + /// The combination must match the content type variation exactly. For instance, if the content type varies by culture, + /// then an invariant culture would be invalid. + /// + bool SupportsVariation(string culture, string segment, bool wildcards = false); + + /// + /// Validates that a combination of culture and segment is valid for the content type properties. + /// + /// The culture. + /// The segment. + /// A value indicating whether wilcards are supported. + /// True if the combination is valid; otherwise false. + /// + /// The combination must be valid for properties of the content type. For instance, if the content type varies by culture, + /// then an invariant culture is valid, because some properties may be invariant. On the other hand, if the content type is invariant, + /// then a variant culture is invalid, because no property could possibly vary by culture. + /// + bool SupportsPropertyVariation(string culture, string segment, bool wildcards = false); /// /// Gets or Sets a list of integer Ids of the ContentTypes allowed under the ContentType diff --git a/src/Umbraco.Core/Models/PartialView.cs b/src/Umbraco.Core/Models/PartialView.cs index 73ae8d83dd..a0b32baecf 100644 --- a/src/Umbraco.Core/Models/PartialView.cs +++ b/src/Umbraco.Core/Models/PartialView.cs @@ -10,11 +10,6 @@ namespace Umbraco.Core.Models [DataContract(IsReference = true)] public class PartialView : File, IPartialView { - [Obsolete("Use the ctor that explicitely sets the view type.")] - public PartialView(string path) - : this(PartialViewType.PartialView, path, null) - { } - public PartialView(PartialViewType viewType, string path) : this(viewType, path, null) { } diff --git a/src/Umbraco.Core/Models/Property.cs b/src/Umbraco.Core/Models/Property.cs index ac6a9b09f0..c0dd97ff87 100644 --- a/src/Umbraco.Core/Models/Property.cs +++ b/src/Umbraco.Core/Models/Property.cs @@ -16,44 +16,83 @@ namespace Umbraco.Core.Models [DataContract(IsReference = true)] public class Property : EntityBase { + // _values contains all property values, including the invariant-neutral value private List _values = new List(); + + // _pvalue contains the invariant-neutral property value private PropertyValue _pvalue; + + // _vvalues contains the (indexed) variant property values private Dictionary _vvalues; private static readonly Lazy Ps = new Lazy(); + /// + /// Initializes a new instance of the class. + /// protected Property() { } + /// + /// Initializes a new instance of the class. + /// public Property(PropertyType propertyType) { PropertyType = propertyType; } + /// + /// Initializes a new instance of the class. + /// public Property(int id, PropertyType propertyType) { Id = id; PropertyType = propertyType; } + /// + /// Represents a property value. + /// public class PropertyValue { private string _culture; private string _segment; + /// + /// Gets or sets the culture of the property. + /// + /// The culture is either null (invariant) or a non-empty string. If the property is + /// set with an empty or whitespace value, its value is converted to null. public string Culture { get => _culture; - internal set => _culture = value?.ToLowerInvariant(); + internal set => _culture = value.IsNullOrWhiteSpace() ? null : value.ToLowerInvariant(); } + + /// + /// Gets or sets the segment of the property. + /// + /// The segment is either null (neutral) or a non-empty string. If the property is + /// set with an empty or whitespace value, its value is converted to null. public string Segment { get => _segment; internal set => _segment = value?.ToLowerInvariant(); } + + /// + /// Gets or sets the edited value of the property. + /// public object EditedValue { get; internal set; } + + /// + /// Gets or sets the published value of the property. + /// public object PublishedValue { get; internal set; } + /// + /// Clones the property value. + /// public PropertyValue Clone() => new PropertyValue { _culture = _culture, _segment = _segment, PublishedValue = PublishedValue, EditedValue = EditedValue }; } @@ -101,7 +140,7 @@ namespace Umbraco.Core.Models { // make sure we filter out invalid variations // make sure we leave _vvalues null if possible - _values = value.Where(x => PropertyType.ValidateVariation(x.Culture, x.Segment, false)).ToList(); + _values = value.Where(x => PropertyType.SupportsVariation(x.Culture, x.Segment)).ToList(); _pvalue = _values.FirstOrDefault(x => x.Culture == null && x.Segment == null); _vvalues = _values.Count > (_pvalue == null ? 0 : 1) ? _values.Where(x => x != _pvalue).ToDictionary(x => new CompositeNStringNStringKey(x.Culture, x.Segment), x => x) @@ -135,7 +174,11 @@ namespace Umbraco.Core.Models /// public object GetValue(string culture = null, string segment = null, bool published = false) { - if (!PropertyType.ValidateVariation(culture, segment, false)) return null; + // ensure null or whitespace are nulls + culture = culture.NullOrWhiteSpaceAsNull(); + segment = segment.NullOrWhiteSpaceAsNull(); + + if (!PropertyType.SupportsVariation(culture, segment)) return null; if (culture == null && segment == null) return GetPropertyValue(_pvalue, published); if (_vvalues == null) return null; return _vvalues.TryGetValue(new CompositeNStringNStringKey(culture, segment), out var pvalue) @@ -154,97 +197,56 @@ namespace Umbraco.Core.Models // internal - must be invoked by the content item // does *not* validate the value - content item must validate first - internal void PublishAllValues() + internal void PublishValues(string culture = "*", string segment = "*") { - // if invariant-neutral is supported, publish invariant-neutral - if (PropertyType.ValidateVariation(null, null, false)) - PublishPropertyValue(_pvalue); + culture = culture.NullOrWhiteSpaceAsNull(); + segment = segment.NullOrWhiteSpaceAsNull(); - // publish everything not invariant-neutral that is supported - if (_vvalues != null) - { - var pvalues = _vvalues - .Where(x => PropertyType.ValidateVariation(x.Value.Culture, x.Value.Segment, false)) - .Select(x => x.Value); - foreach (var pvalue in pvalues) - PublishPropertyValue(pvalue); - } + // if invariant or all, and invariant-neutral is supported, publish invariant-neutral + if ((culture == null || culture == "*") && (segment == null || segment == "*") && PropertyType.SupportsVariation(null, null)) + PublishValue(_pvalue); + + // then deal with everything that varies + if (_vvalues == null) return; + + // get the property values that are still relevant (wrt the property type variation), + // and match the specified culture and segment (or anything when '*'). + var pvalues = _vvalues.Where(x => + PropertyType.SupportsVariation(x.Value.Culture, x.Value.Segment, true) && // the value variation is ok + (culture == "*" || x.Value.Culture.InvariantEquals(culture)) && // the culture matches + (segment == "*" || x.Value.Segment.InvariantEquals(segment))) // the segment matches + .Select(x => x.Value); + + foreach (var pvalue in pvalues) + PublishValue(pvalue); } // internal - must be invoked by the content item - // does *not* validate the value - content item must validate first - internal void PublishValue(string culture = null, string segment = null) + internal void UnpublishValues(string culture = "*", string segment = "*") { - PropertyType.ValidateVariation(culture, segment, true); + culture = culture.NullOrWhiteSpaceAsNull(); + segment = segment.NullOrWhiteSpaceAsNull(); - var (pvalue, _) = GetPValue(culture, segment, false); - if (pvalue == null) return; - PublishPropertyValue(pvalue); + // if invariant or all, and invariant-neutral is supported, publish invariant-neutral + if ((culture == null || culture == "*") && (segment == null || segment == "*") && PropertyType.SupportsVariation(null, null)) + UnpublishValue(_pvalue); + + // then deal with everything that varies + if (_vvalues == null) return; + + // get the property values that are still relevant (wrt the property type variation), + // and match the specified culture and segment (or anything when '*'). + var pvalues = _vvalues.Where(x => + PropertyType.SupportsVariation(x.Value.Culture, x.Value.Segment, true) && // the value variation is ok + (culture == "*" || x.Value.Culture.InvariantEquals(culture)) && // the culture matches + (segment == "*" || x.Value.Segment.InvariantEquals(segment))) // the segment matches + .Select(x => x.Value); + + foreach (var pvalue in pvalues) + UnpublishValue(pvalue); } - // internal - must be invoked by the content item - // does *not* validate the value - content item must validate first - internal void PublishCultureValues(string culture = null) - { - // if invariant and invariant-neutral is supported, publish invariant-neutral - if (culture == null && PropertyType.ValidateVariation(null, null, false)) - PublishPropertyValue(_pvalue); - - // publish everything not invariant-neutral that matches the culture and is supported - if (_vvalues != null) - { - var pvalues = _vvalues - .Where(x => x.Value.Culture.InvariantEquals(culture)) - .Where(x => PropertyType.ValidateVariation(culture, x.Value.Segment, false)) - .Select(x => x.Value); - foreach (var pvalue in pvalues) - PublishPropertyValue(pvalue); - } - } - - // internal - must be invoked by the content item - internal void ClearPublishedAllValues() - { - if (PropertyType.ValidateVariation(null, null, false)) - ClearPublishedPropertyValue(_pvalue); - - if (_vvalues != null) - { - var pvalues = _vvalues - .Where(x => PropertyType.ValidateVariation(x.Value.Culture, x.Value.Segment, false)) - .Select(x => x.Value); - foreach (var pvalue in pvalues) - ClearPublishedPropertyValue(pvalue); - } - } - - // internal - must be invoked by the content item - internal void ClearPublishedValue(string culture = null, string segment = null) - { - PropertyType.ValidateVariation(culture, segment, true); - var (pvalue, _) = GetPValue(culture, segment, false); - if (pvalue == null) return; - ClearPublishedPropertyValue(pvalue); - } - - // internal - must be invoked by the content item - internal void ClearPublishedCultureValues(string culture = null) - { - if (culture == null && PropertyType.ValidateVariation(null, null, false)) - ClearPublishedPropertyValue(_pvalue); - - if (_vvalues != null) - { - var pvalues = _vvalues - .Where(x => x.Value.Culture.InvariantEquals(culture)) - .Where(x => PropertyType.ValidateVariation(culture, x.Value.Segment, false)) - .Select(x => x.Value); - foreach (var pvalue in pvalues) - ClearPublishedPropertyValue(pvalue); - } - } - - private void PublishPropertyValue(PropertyValue pvalue) + private void PublishValue(PropertyValue pvalue) { if (pvalue == null) return; @@ -255,7 +257,7 @@ namespace Umbraco.Core.Models DetectChanges(pvalue.EditedValue, origValue, Ps.Value.ValuesSelector, Ps.Value.PropertyValueComparer, false); } - private void ClearPublishedPropertyValue(PropertyValue pvalue) + private void UnpublishValue(PropertyValue pvalue) { if (pvalue == null) return; @@ -271,7 +273,12 @@ namespace Umbraco.Core.Models /// public void SetValue(object value, string culture = null, string segment = null) { - PropertyType.ValidateVariation(culture, segment, true); + culture = culture.NullOrWhiteSpaceAsNull(); + segment = segment.NullOrWhiteSpaceAsNull(); + + if (!PropertyType.SupportsVariation(culture, segment)) + throw new NotSupportedException($"Variation \"{culture??""},{segment??""}\" is not supported by the property type."); + var (pvalue, change) = GetPValue(culture, segment, true); var origValue = pvalue.EditedValue; @@ -332,67 +339,40 @@ namespace Umbraco.Core.Models } /// - /// Gets a value indicating whether all properties are valid. + /// Gets a value indicating whether the property has valid values. /// - /// - internal bool IsAllValid() + internal bool IsValid(string culture = "*", string segment = "*") { - //fixme - needs API review as this is not used apart from in tests + culture = culture.NullOrWhiteSpaceAsNull(); + segment = segment.NullOrWhiteSpaceAsNull(); - // invariant-neutral is supported, validate invariant-neutral - // includes mandatory validation - if (PropertyType.ValidateVariation(null, null, false) && !IsValidValue(_pvalue)) return false; + // if validating invariant/neutral, and it is supported, validate + // (including ensuring that the value exists, if mandatory) + if ((culture == null || culture == "*") && (segment == null || segment == "*") && PropertyType.SupportsVariation(null, null)) + if (!IsValidValue(_pvalue?.EditedValue)) + return false; + + // if validating only invariant/neutral, we are good + if (culture == null && segment == null) + return true; + + // if nothing else to validate, we are good + if ((culture == null || culture == "*") && (segment == null || segment == "*") && !PropertyType.VariesByCulture()) + return true; - // either invariant-neutral is not supported, or it is valid // for anything else, validate the existing values (including mandatory), // but we cannot validate mandatory globally (we don't know the possible cultures and segments) - if (_vvalues == null) return true; + if (_vvalues == null) return culture == "*" || IsValidValue(null); - var pvalues = _vvalues - .Where(x => PropertyType.ValidateVariation(x.Value.Culture, x.Value.Segment, false)) + var pvalues = _vvalues.Where(x => + PropertyType.SupportsVariation(x.Value.Culture, x.Value.Segment, true) && // the value variation is ok + (culture == "*" || x.Value.Culture.InvariantEquals(culture)) && // the culture matches + (segment == "*" || x.Value.Segment.InvariantEquals(segment))) // the segment matches .Select(x => x.Value) - .ToArray(); + .ToList(); - return pvalues.Length == 0 || pvalues.All(x => IsValidValue(x.EditedValue)); - } - - /// - /// Gets a value indicating whether the culture/any values are valid. - /// - /// An invalid value can be saved, but only valid values can be published. - internal bool IsCultureValid(string culture) - { - //fixme - needs API review as this is not used apart from in tests - - // culture-neutral is supported, validate culture-neutral - // includes mandatory validation - if (PropertyType.ValidateVariation(culture, null, false) && !IsValidValue(GetValue(culture))) - return false; - - // either culture-neutral is not supported, or it is valid - // for anything non-neutral, validate the existing values (including mandatory), - // but we cannot validate mandatory globally (we don't know the possible segments) - - if (_vvalues == null) return true; - - var pvalues = _vvalues - .Where(x => x.Value.Culture.InvariantEquals(culture)) - .Where(x => PropertyType.ValidateVariation(culture, x.Value.Segment, false)) - .Select(x => x.Value) - .ToArray(); - - return pvalues.Length == 0 || pvalues.All(x => IsValidValue(x.EditedValue)); - } - - /// - /// Gets a value indicating whether the value is valid. - /// - /// An invalid value can be saved, but only valid values can be published. - public bool IsValid(string culture = null, string segment = null) - { - // single value -> validates mandatory - return IsValidValue(GetValue(culture, segment)); + return pvalues.Count == 0 || pvalues.All(x => IsValidValue(x.EditedValue)); } /// diff --git a/src/Umbraco.Core/Models/PropertyType.cs b/src/Umbraco.Core/Models/PropertyType.cs index 587af74aa2..3d5fac2077 100644 --- a/src/Umbraco.Core/Models/PropertyType.cs +++ b/src/Umbraco.Core/Models/PropertyType.cs @@ -47,7 +47,7 @@ namespace Umbraco.Core.Models _propertyEditorAlias = dataType.EditorAlias; _valueStorageType = dataType.DatabaseType; - _variations = ContentVariation.InvariantNeutral; + _variations = ContentVariation.Nothing; } /// @@ -84,7 +84,7 @@ namespace Umbraco.Core.Models _valueStorageType = valueStorageType; _forceValueStorageType = forceValueStorageType; _alias = propertyTypeAlias == null ? null : SanitizeAlias(propertyTypeAlias); - _variations = ContentVariation.InvariantNeutral; + _variations = ContentVariation.Nothing; } private static PropertySelectors Selectors => _selectors ?? (_selectors = new PropertySelectors()); @@ -224,32 +224,17 @@ namespace Umbraco.Core.Models } /// - /// Validates that a variation is valid for the property type. + /// Determines whether the property type supports a combination of culture and segment. /// - public bool ValidateVariation(string culture, string segment, bool throwIfInvalid) + /// The culture. + /// The segment. + /// A value indicating whether wildcards are valid. + public bool SupportsVariation(string culture, string segment, bool wildcards = false) { - ContentVariation variation; - if (culture != null) - { - variation = segment != null - ? ContentVariation.CultureSegment - : ContentVariation.CultureNeutral; - } - else if (segment != null) - { - variation = ContentVariation.InvariantSegment; - } - else - { - variation = ContentVariation.InvariantNeutral; - } - if (!Variations.Has(variation)) - { - if (throwIfInvalid) - throw new NotSupportedException($"Variation {variation} is invalid for property type \"{Alias}\"."); - return false; - } - return true; + // exact validation: cannot accept a 'null' culture if the property type varies + // by culture, and likewise for segment + // wildcard validation: can accept a '*' culture or segment + return Variations.ValidateVariation(culture, segment, true, wildcards, false); } /// diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs index bc403b904d..e611ded6c8 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs @@ -81,7 +81,7 @@ namespace Umbraco.Core.Models.PublishedContent foreach ((var alias, (var dataTypeId, var editorAlias)) in BuiltinMemberProperties) { if (aliases.Contains(alias)) continue; - propertyTypes.Add(factory.CreatePropertyType(this, alias, dataTypeId, ContentVariation.InvariantNeutral)); + propertyTypes.Add(factory.CreatePropertyType(this, alias, dataTypeId, ContentVariation.Nothing)); } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs index 3ba908b9bf..5de5842eda 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs @@ -32,13 +32,13 @@ namespace Umbraco.Core.Models.PublishedContent } // for tests - internal PublishedContentType CreateContentType(int id, string alias, IEnumerable propertyTypes, ContentVariation variations = ContentVariation.InvariantNeutral) + internal PublishedContentType CreateContentType(int id, string alias, IEnumerable propertyTypes, ContentVariation variations = ContentVariation.Nothing) { return new PublishedContentType(id, alias, PublishedItemType.Content, Enumerable.Empty(), propertyTypes, variations); } // for tests - internal PublishedContentType CreateContentType(int id, string alias, IEnumerable compositionAliases, IEnumerable propertyTypes, ContentVariation variations = ContentVariation.InvariantNeutral) + internal PublishedContentType CreateContentType(int id, string alias, IEnumerable compositionAliases, IEnumerable propertyTypes, ContentVariation variations = ContentVariation.Nothing) { return new PublishedContentType(id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, variations); } @@ -50,13 +50,13 @@ namespace Umbraco.Core.Models.PublishedContent } /// - public PublishedPropertyType CreatePropertyType(PublishedContentType contentType, string propertyTypeAlias, int dataTypeId, ContentVariation variations = ContentVariation.InvariantNeutral) + public PublishedPropertyType CreatePropertyType(PublishedContentType contentType, string propertyTypeAlias, int dataTypeId, ContentVariation variations = ContentVariation.Nothing) { return new PublishedPropertyType(contentType, propertyTypeAlias, dataTypeId, true, variations, _propertyValueConverters, _publishedModelFactory, this); } // for tests - internal PublishedPropertyType CreatePropertyType(string propertyTypeAlias, int dataTypeId, bool umbraco = false, ContentVariation variations = ContentVariation.InvariantNeutral) + internal PublishedPropertyType CreatePropertyType(string propertyTypeAlias, int dataTypeId, bool umbraco = false, ContentVariation variations = ContentVariation.Nothing) { return new PublishedPropertyType(propertyTypeAlias, dataTypeId, umbraco, variations, _propertyValueConverters, _publishedModelFactory, this); } diff --git a/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs b/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs index 5dc4a280e6..7469222ab0 100644 --- a/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs +++ b/src/Umbraco.Core/Models/PublishedContent/RawValueProperty.cs @@ -41,7 +41,7 @@ namespace Umbraco.Core.Models.PublishedContent public RawValueProperty(PublishedPropertyType propertyType, IPublishedElement content, object sourceValue, bool isPreviewing = false) : base(propertyType, PropertyCacheLevel.Unknown) // cache level is ignored { - if (propertyType.Variations != ContentVariation.InvariantNeutral) + if (propertyType.Variations != ContentVariation.Nothing) throw new ArgumentException("Property types with variations are not supported here.", nameof(propertyType)); _sourceValue = sourceValue; diff --git a/src/Umbraco.Core/Persistence/Dtos/ConsentDto.cs b/src/Umbraco.Core/Persistence/Dtos/ConsentDto.cs index 763df352de..b66cd2d020 100644 --- a/src/Umbraco.Core/Persistence/Dtos/ConsentDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/ConsentDto.cs @@ -37,6 +37,7 @@ namespace Umbraco.Core.Persistence.Dtos public int State { get; set; } [Column("comment")] + [NullSetting(NullSetting = NullSettings.Null)] public string Comment { get; set; } } } diff --git a/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs b/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs index c3b4b0d24c..ec364c7c6a 100644 --- a/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs @@ -159,8 +159,7 @@ namespace Umbraco.Core.Persistence.Factories /// public static DocumentDto BuildDto(IContent entity, Guid objectType) { - var contentBase = (Content) entity; - var contentDto = BuildContentDto(contentBase, objectType); + var contentDto = BuildContentDto(entity, objectType); var dto = new DocumentDto { @@ -170,7 +169,7 @@ namespace Umbraco.Core.Persistence.Factories ExpiresDate = entity.ExpireDate, ContentDto = contentDto, - DocumentVersionDto = BuildDocumentVersionDto(contentBase, contentDto) + DocumentVersionDto = BuildDocumentVersionDto(entity, contentDto) }; return dto; @@ -181,14 +180,13 @@ namespace Umbraco.Core.Persistence.Factories /// public static MediaDto BuildDto(IMedia entity) { - var contentBase = (Models.Media) entity; - var contentDto = BuildContentDto(contentBase, Constants.ObjectTypes.Media); + var contentDto = BuildContentDto(entity, Constants.ObjectTypes.Media); var dto = new MediaDto { NodeId = entity.Id, ContentDto = contentDto, - MediaVersionDto = BuildMediaVersionDto(contentBase, contentDto) + MediaVersionDto = BuildMediaVersionDto(entity, contentDto) }; return dto; @@ -199,8 +197,7 @@ namespace Umbraco.Core.Persistence.Factories /// public static MemberDto BuildDto(IMember entity) { - var member = (Member) entity; - var contentDto = BuildContentDto(member, Constants.ObjectTypes.Member); + var contentDto = BuildContentDto(entity, Constants.ObjectTypes.Member); var dto = new MemberDto { @@ -210,12 +207,12 @@ namespace Umbraco.Core.Persistence.Factories Password = entity.RawPasswordValue, ContentDto = contentDto, - ContentVersionDto = BuildContentVersionDto(member, contentDto) + ContentVersionDto = BuildContentVersionDto(entity, contentDto) }; return dto; } - private static ContentDto BuildContentDto(ContentBase entity, Guid objectType) + private static ContentDto BuildContentDto(IContentBase entity, Guid objectType) { var dto = new ContentDto { @@ -228,7 +225,7 @@ namespace Umbraco.Core.Persistence.Factories return dto; } - private static NodeDto BuildNodeDto(ContentBase entity, Guid objectType) + private static NodeDto BuildNodeDto(IContentBase entity, Guid objectType) { var dto = new NodeDto { @@ -250,7 +247,7 @@ namespace Umbraco.Core.Persistence.Factories // always build the current / VersionPk dto // we're never going to build / save old versions (which are immutable) - private static ContentVersionDto BuildContentVersionDto(ContentBase entity, ContentDto contentDto) + private static ContentVersionDto BuildContentVersionDto(IContentBase entity, ContentDto contentDto) { var dto = new ContentVersionDto { @@ -269,7 +266,7 @@ namespace Umbraco.Core.Persistence.Factories // always build the current / VersionPk dto // we're never going to build / save old versions (which are immutable) - private static DocumentVersionDto BuildDocumentVersionDto(Content entity, ContentDto contentDto) + private static DocumentVersionDto BuildDocumentVersionDto(IContent entity, ContentDto contentDto) { var dto = new DocumentVersionDto { @@ -283,7 +280,7 @@ namespace Umbraco.Core.Persistence.Factories return dto; } - private static MediaVersionDto BuildMediaVersionDto(Models.Media entity, ContentDto contentDto) + private static MediaVersionDto BuildMediaVersionDto(IMedia entity, ContentDto contentDto) { // try to get a path from the string being stored for media // fixme - only considering umbracoFile ?! diff --git a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs index b0e3e2dc7d..3bea84e619 100644 --- a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs @@ -99,13 +99,7 @@ namespace Umbraco.Core.Persistence.Factories { if (property.PropertyType.IsPublishing) { - // fixme - // why only CultureNeutral? - // then, the tree can only show when a CultureNeutral value has been modified, but not when - // a CultureSegment has been modified, so if I edit some french/mobile thing, the tree will - // NOT tell me that I have changes? - - var editingCultures = property.PropertyType.Variations.Has(ContentVariation.CultureNeutral); + var editingCultures = property.PropertyType.VariesByCulture(); if (editingCultures && editedCultures == null) editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase); // publishing = deal with edit and published values diff --git a/src/Umbraco.Core/Persistence/LocalDb.cs b/src/Umbraco.Core/Persistence/LocalDb.cs index 995508ecf7..6eb9cbc443 100644 --- a/src/Umbraco.Core/Persistence/LocalDb.cs +++ b/src/Umbraco.Core/Persistence/LocalDb.cs @@ -141,7 +141,7 @@ namespace Umbraco.Core.Persistence { EnsureAvailable(); var instances = GetInstances(); - return instances != null && instances.Contains(instanceName); + return instances != null && instances.Contains(instanceName, StringComparer.OrdinalIgnoreCase); } /// diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs index fe2a915c9f..403efd7e59 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs @@ -158,7 +158,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement totalRecords = page.TotalItems; var items = page.Items.Select( - dto => new AuditItem(dto.Id, dto.Comment, Enum.Parse(dto.Header), dto.UserId ?? Constants.Security.UnknownUserId)).ToArray(); + dto => new AuditItem(dto.Id, dto.Comment, Enum.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId ?? Constants.Security.UnknownUserId)).ToArray(); // map the DateStamp for (var i = 0; i < items.Length; i++) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index f2a91c11a1..093723cea5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -232,7 +232,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistNewItem(IContent entity) { - //fixme - stop doing this just so we have access to AddingEntity + // fixme - stop doing this - sort out IContent vs Content + // however, it's not just so we have access to AddingEntity + // there are tons of things at the end of the methods, that can only work with a true Content + // and basically, the repository requires a Content, not an IContent var content = (Content) entity; content.AddingEntity(); @@ -242,11 +245,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (entity.Template == null) entity.Template = entity.ContentType.DefaultTemplate; - // sanitize names: ensure we have an invariant name, and names are unique-ish - // (well, only invariant name is unique at the moment) - EnsureUniqueVariationNames(entity); - EnsureInvariantNameValues(entity, publishing); - entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name); + // sanitize names + SanitizeNames(content, publishing); // ensure that strings don't contain characters that are invalid in xml // fixme - do we really want to keep doing this here? @@ -295,7 +295,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var contentVersionDto = dto.DocumentVersionDto.ContentVersionDto; contentVersionDto.NodeId = nodeDto.NodeId; contentVersionDto.Current = !publishing; - contentVersionDto.Text = publishing ? content.PublishName : content.Name; + contentVersionDto.Text = content.Name; Database.Insert(contentVersionDto); content.VersionId = contentVersionDto.Id; @@ -312,7 +312,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement content.PublishedVersionId = content.VersionId; contentVersionDto.Id = 0; contentVersionDto.Current = true; - contentVersionDto.Text = content.PublishName; + contentVersionDto.Text = content.Name; Database.Insert(contentVersionDto); content.VersionId = contentVersionDto.Id; @@ -326,8 +326,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); - // name also impacts 'edited' - if (content.PublishName != content.Name) + // if !publishing, we may have a new name != current publish name, + // also impacts 'edited' + if (!publishing && content.PublishName != content.Name) edited = true; // persist the document dto @@ -340,7 +341,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Insert(dto); // persist the variations - if (content.ContentType.Variations.DoesSupportCulture()) + if (content.ContentType.VariesByCulture()) { // names also impact 'edited' foreach (var (culture, name) in content.CultureNames) @@ -355,8 +356,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } // refresh content - if (editedCultures != null) - content.SetCultureEdited(editedCultures); + content.SetCultureEdited(editedCultures); // trigger here, before we reset Published etc OnUowRefreshedEntity(new ScopedEntityEventArgs(AmbientScope, entity)); @@ -369,6 +369,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement content.Published = true; content.PublishTemplate = content.Template; content.PublisherId = content.WriterId; + content.PublishName = content.Name; content.PublishDate = content.UpdateDate; SetEntityTags(entity, _tagRepository); @@ -378,6 +379,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement content.Published = false; content.PublishTemplate = null; content.PublisherId = null; + content.PublishName = null; content.PublishDate = null; ClearEntityTags(entity, _tagRepository); @@ -400,6 +402,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistUpdatedItem(IContent entity) { + // fixme - stop doing this - sort out IContent vs Content + // however, it's not just so we have access to AddingEntity + // there are tons of things at the end of the methods, that can only work with a true Content + // and basically, the repository requires a Content, not an IContent var content = (Content) entity; // check if we need to make any database changes at all @@ -423,11 +429,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Execute(Sql().Update(u => u.Set(x => x.Published, false)).Where(x => x.Id == content.PublishedVersionId)); } - // sanitize names: ensure we have an invariant name, and names are unique-ish - // (well, only invariant name is unique at the moment) - EnsureUniqueVariationNames(entity); - EnsureInvariantNameValues(entity, publishing); - entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id); + // sanitize names + SanitizeNames(content, publishing); // ensure that strings don't contain characters that are invalid in xml // fixme - do we really want to keep doing this here? @@ -460,7 +463,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { documentVersionDto.Published = true; // now published contentVersionDto.Current = false; // no more current - contentVersionDto.Text = content.PublishName; + contentVersionDto.Text = content.Name; } Database.Update(contentVersionDto); Database.Update(documentVersionDto); @@ -491,11 +494,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); - // name also impacts 'edited' - if (content.PublishName != content.Name) + // if !publishing, we may have a new name != current publish name, + // also impacts 'edited' + if (!publishing && content.PublishName != content.Name) edited = true; - if (content.ContentType.Variations.DoesSupportCulture()) + if (content.ContentType.VariesByCulture()) { // names also impact 'edited' foreach (var (culture, name) in content.CultureNames) @@ -511,7 +515,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var deleteDocumentVariations = Sql().Delete().Where(x => x.NodeId == content.Id); Database.Execute(deleteDocumentVariations); - // fixme is we'd like to use the native NPoco InsertBulk here but it causes problems (not sure exaclty all scenarios) but by using SQL Server and updating a variants name will cause: Unable to cast object of type 'Umbraco.Core.Persistence.FaultHandling.RetryDbConnection' to type 'System.Data.SqlClient.SqlConnection'. + // fixme NPoco InsertBulk issue? + // we should use the native NPoco InsertBulk here but it causes problems (not sure exaclty all scenarios) + // but by using SQL Server and updating a variants name will cause: Unable to cast object of type + // 'Umbraco.Core.Persistence.FaultHandling.RetryDbConnection' to type 'System.Data.SqlClient.SqlConnection'. // (same in PersistNewItem above) // insert content variations @@ -522,8 +529,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } // refresh content - if (editedCultures != null) - content.SetCultureEdited(editedCultures); + content.SetCultureEdited(editedCultures); // update the document dto // at that point, when un/publishing, the entity still has its old Published value @@ -550,6 +556,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement content.Published = true; content.PublishTemplate = content.Template; content.PublisherId = content.WriterId; + content.PublishName = content.Name; content.PublishDate = content.UpdateDate; SetEntityTags(entity, _tagRepository); @@ -559,6 +566,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement content.Published = false; content.PublishTemplate = null; content.PublisherId = null; + content.PublishName = null; content.PublishDate = null; ClearEntityTags(entity, _tagRepository); @@ -904,7 +912,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } // set variations, if varying - temps = temps.Where(x => x.ContentType.Variations.DoesSupportCulture()).ToList(); + temps = temps.Where(x => x.ContentType.VariesByCulture()).ToList(); if (temps.Count > 0) { // load all variations for all documents from database, in one query @@ -939,7 +947,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement content.Properties = properties[dto.DocumentVersionDto.Id]; // set variations, if varying - if (contentType.Variations.DoesSupportCulture()) + if (contentType.VariesByCulture()) { var contentVariations = GetContentVariations(ltemp); var documentVariations = GetDocumentVariations(ltemp); @@ -955,10 +963,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { if (contentVariations.TryGetValue(content.VersionId, out var contentVariation)) foreach (var v in contentVariation) - content.SetCultureInfos(v.Culture, v.Name, v.Date); + content.SetCultureInfo(v.Culture, v.Name, v.Date); if (content.PublishedVersionId > 0 && contentVariations.TryGetValue(content.PublishedVersionId, out contentVariation)) foreach (var v in contentVariation) - content.SetPublishInfos(v.Culture, v.Name, v.Date); + content.SetPublishInfo(v.Culture, v.Name, v.Date); if (documentVariations.TryGetValue(content.Id, out var documentVariation)) foreach (var v in documentVariation.Where(x => x.Edited)) content.SetCultureEdited(v.Culture); @@ -1038,7 +1046,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."), Culture = culture, Name = name, - Date = content.GetCultureDate(culture) + Date = content.GetCultureDate(culture) ?? DateTime.MinValue // we *know* there is a value }; // if not publishing, we're just updating the 'current' (non-published) version, @@ -1046,14 +1054,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (!publishing) yield break; // create dtos for the 'published' version, for published cultures (those having a name) - foreach (var (culture, name) in content.PublishCultureNames) + foreach (var (culture, name) in content.PublishNames) yield return new ContentVersionCultureVariationDto { VersionId = content.PublishedVersionId, LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."), Culture = culture, Name = name, - Date = content.GetCulturePublishDate(culture) + Date = content.GetPublishDate(culture) ?? DateTime.MinValue // we *know* there is a value }; } @@ -1084,90 +1092,103 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #region Utilities - /// - /// Ensures that the Name/PublishName properties are filled in and validates if all names are null - /// - private void EnsureInvariantNameValues(IContent content, bool publishing) + private void SanitizeNames(Content content, bool publishing) { - // here we have to ensure we have names and publish names, and to try and fix the situation if we have no name, see also: U4-11286 + // a content item *must* have an invariant name, and invariant published name + // else we just cannot write the invariant rows (node, content version...) to the database - // invariant content must have an invariant name - if (content.ContentType.Variations.DoesNotSupportCulture() && string.IsNullOrWhiteSpace(content.Name)) - throw new InvalidOperationException("Cannot save content with an empty name."); + // ensure that we have an invariant name + // invariant content = must be there already, else throw + // variant content = update with default culture or anything really + EnsureInvariantNameExists(content); - // variant content must have at least one variant name - if (content.ContentType.Variations.DoesSupportCulture()) + // ensure that that invariant name is unique + EnsureInvariantNameIsUnique(content); + + // and finally, + // ensure that each culture has a unique node name + // no published name = not published + // else, it needs to be unique + EnsureVariantNamesAreUnique(content, publishing); + } + + private void EnsureInvariantNameExists(Content content) + { + if (content.ContentType.VariesByCulture()) { + // content varies by culture + // then it must have at least a variant name, else it makes no sense if (content.CultureNames.Count == 0) throw new InvalidOperationException("Cannot save content with an empty name."); - // cannot save with an empty invariant name, - // if invariant name is missing, derive it from variant names - // fixme should we always sync the invariant name with the default culture name when updating? - if (string.IsNullOrWhiteSpace(content.Name)) - { - var defaultCulture = LanguageRepository.GetDefaultIsoCode(); - if (defaultCulture != null && content.CultureNames.TryGetValue(defaultCulture, out var cultureName)) - content.Name = cultureName; - else - content.Name = content.CultureNames.First().Value; // only option is to take the first - } + // and then, we need to set the invariant name implicitely, + // using the default culture if it has a name, otherwise anything we can + var defaultCulture = LanguageRepository.GetDefaultIsoCode(); + content.Name = defaultCulture != null && content.CultureNames.TryGetValue(defaultCulture, out var cultureName) + ? cultureName + : content.CultureNames.First().Value; } - - // cannot publish without an invariant name - if (publishing && string.IsNullOrWhiteSpace(content.PublishName)) + else { - // no variant name = error - if (content.PublishCultureNames.Count == 0) - throw new InvalidOperationException("Cannot publish content with an empty name."); - - // else... we cannot deal with it here because PublishName is readonly, so in reality, PublishName - // should not be null because it should have been set when preparing the content for publish. - // see also: Content.SetPublishInfos() - it deals with PublishName + // content is invariant, and invariant content must have an explicit invariant name + if (string.IsNullOrWhiteSpace(content.Name)) + throw new InvalidOperationException("Cannot save content with an empty name."); } } + private void EnsureInvariantNameIsUnique(Content content) + { + content.Name = EnsureUniqueNodeName(content.ParentId, content.Name, content.Id); + } + protected override string EnsureUniqueNodeName(int parentId, string nodeName, int id = 0) { return EnsureUniqueNaming == false ? nodeName : base.EnsureUniqueNodeName(parentId, nodeName, id); } - private SqlTemplate SqlEnsureUniqueVariationNames => SqlContext.Templates.Get("Umbraco.Core.DomainRepository.EnsureUniqueVariationNames", tsql => tsql + private SqlTemplate SqlEnsureVariantNamesAreUnique => SqlContext.Templates.Get("Umbraco.Core.DomainRepository.EnsureVariantNamesAreUnique", tsql => tsql .Select(x => x.Id, x => x.Name, x => x.LanguageId) .From() - .InnerJoin() - .On(x => x.Id, x => x.VersionId) - .InnerJoin() - .On(x => x.NodeId, x => x.NodeId) + .InnerJoin().On(x => x.Id, x => x.VersionId) + .InnerJoin().On(x => x.NodeId, x => x.NodeId) .Where(x => x.Current == SqlTemplate.Arg("current")) .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && x.ParentId == SqlTemplate.Arg("parentId") && x.NodeId != SqlTemplate.Arg("id")) .OrderBy(x => x.LanguageId)); - private void EnsureUniqueVariationNames(IContent content) + private void EnsureVariantNamesAreUnique(Content content, bool publishing) { - if (!EnsureUniqueNaming || content.CultureNames.Count == 0) return; + if (!EnsureUniqueNaming || !content.ContentType.VariesByCulture() || content.CultureNames.Count == 0) return; - //Get all culture names at the same level - - var sql = SqlEnsureUniqueVariationNames.Sql(true, NodeObjectTypeId, content.ParentId, content.Id); + // get names per culture, at same level (ie all siblings) + var sql = SqlEnsureVariantNamesAreUnique.Sql(true, NodeObjectTypeId, content.ParentId, content.Id); var names = Database.Fetch(sql) .GroupBy(x => x.LanguageId) .ToDictionary(x => x.Key, x => x); if (names.Count == 0) return; - foreach(var n in content.CultureNames) + // note: the code below means we are going to unique-ify every culture names, regardless + // of whether the name has changed (ie the culture has been updated) - some saving culture + // fr-FR could cause culture en-UK name to change - not sure that is clean + + foreach(var (culture, name) in content.CultureNames) { - var langId = LanguageRepository.GetIdByIsoCode(n.Key); + var langId = LanguageRepository.GetIdByIsoCode(culture); if (!langId.HasValue) continue; - if (names.TryGetValue(langId.Value, out var cultureNames)) - { - var otherLangNames = cultureNames.Select(x => new SimilarNodeName { Id = x.Id, Name = x.Name }); - var uniqueName = SimilarNodeName.GetUniqueName(otherLangNames, 0, n.Value); - content.SetName(uniqueName, n.Key); - } + if (!names.TryGetValue(langId.Value, out var cultureNames)) continue; + + // get a unique name + var otherNames = cultureNames.Select(x => new SimilarNodeName { Id = x.Id, Name = x.Name }); + var uniqueName = SimilarNodeName.GetUniqueName(otherNames, 0, name); + + if (uniqueName == content.GetCultureName(culture)) continue; + + // update the name, and the publish name if published + content.SetCultureName(uniqueName, culture); + if (publishing && content.PublishNames.ContainsKey(culture)) + content.SetPublishInfo(culture, uniqueName, DateTime.Now); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index 75dbeca559..340eecb538 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -905,7 +905,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement entity.Edited = dto.Edited; entity.Published = dto.Published; - if (dto.Variations.Has(ContentVariation.CultureNeutral) && dto.VariationInfo != null && dto.VariationInfo.Count > 0) + if (dto.Variations.VariesByCulture() && dto.VariationInfo != null && dto.VariationInfo.Count > 0) { var variantInfo = new Dictionary(StringComparer.InvariantCultureIgnoreCase); foreach (var info in dto.VariationInfo) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs index db2e1124a2..740015683a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs @@ -52,7 +52,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement sql.OrderBy(dto => dto.Id); // get languages - var languages = Database.Fetch(sql).Select(ConvertFromDto).ToList(); + var languages = Database.Fetch(sql).Select(ConvertFromDto).OrderBy(x => x.Id).ToList(); + + // fix inconsistencies: there has to be a default language, and it has to be mandatory + var defaultLanguage = languages.FirstOrDefault(x => x.IsDefaultVariantLanguage) ?? languages.First(); + defaultLanguage.IsDefaultVariantLanguage = true; + defaultLanguage.Mandatory = true; // initialize the code-id map lock (_codeIdMap) diff --git a/src/Umbraco.Core/PropertyEditors/TagConfiguration.cs b/src/Umbraco.Core/PropertyEditors/TagConfiguration.cs index 7fbf1dc393..4a0eeebc9b 100644 --- a/src/Umbraco.Core/PropertyEditors/TagConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/TagConfiguration.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.PropertyEditors public string Group { get; set; } = "default"; [ConfigurationField("storageType", "Storage Type", "views/propertyeditors/tags/tags.prevalues.html", - Description = "Select whether to store the tags in cache as CSV (default) or as JSON. The only benefits of storage as JSON is that you are able to have commas in a tag value but this will require parsing the json in your views or using a property value converter")] + Description = "Select whether to store the tags in cache as CSV (default) or as JSON. The only benefits of storage as JSON is that you are able to have commas in a tag value")] public TagsStorageType StorageType { get; set; } = TagsStorageType.Csv; // not a field diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs index 3117fc0b54..5a642bedc7 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/GridValueConverter.cs @@ -53,7 +53,7 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters Current.ApplicationCache.RuntimeCache, new DirectoryInfo(IOHelper.MapPath(SystemDirectories.AppPlugins)), new DirectoryInfo(IOHelper.MapPath(SystemDirectories.Config)), - HttpContext.Current.IsDebuggingEnabled); + Current.RuntimeState.Debug); var sections = GetArray(obj, "sections"); foreach (var section in sections.Cast()) diff --git a/src/Umbraco.Core/Publishing/ScheduledPublisher.cs b/src/Umbraco.Core/Publishing/ScheduledPublisher.cs index b429a142e7..1d2d62b929 100644 --- a/src/Umbraco.Core/Publishing/ScheduledPublisher.cs +++ b/src/Umbraco.Core/Publishing/ScheduledPublisher.cs @@ -41,8 +41,8 @@ namespace Umbraco.Core.Publishing try { d.ReleaseDate = null; - d.TryPublishValues(); // fixme variants? - var result = _contentService.SaveAndPublish(d, _userService.GetProfileById(d.WriterId).Id); + d.PublishCulture(); // fixme variants? + var result = _contentService.SaveAndPublish(d, userId: _userService.GetProfileById(d.WriterId).Id); _logger.Debug(() => $"Result of publish attempt: {result.Result}"); if (result.Success == false) { diff --git a/src/Umbraco.Core/Runtime/CoreRuntime.cs b/src/Umbraco.Core/Runtime/CoreRuntime.cs index 98767f2aa2..93f475f7a6 100644 --- a/src/Umbraco.Core/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Core/Runtime/CoreRuntime.cs @@ -258,13 +258,21 @@ namespace Umbraco.Core.Runtime logger.Debug("No local version, need to install Umbraco."); _state.Level = RuntimeLevel.Install; } - else if (localVersion != codeVersion) + else if (localVersion < codeVersion) { // there *is* a local version, but it does not match the code version // need to upgrade - logger.Debug(() => $"Local version \"{localVersion}\" != code version \"{codeVersion}\", need to upgrade Umbraco."); + logger.Debug(() => $"Local version \"{localVersion}\" < code version \"{codeVersion}\", need to upgrade Umbraco."); _state.Level = RuntimeLevel.Upgrade; } + else if (localVersion > codeVersion) + { + logger.Warn(() => $"Local version \"{localVersion}\" > code version \"{codeVersion}\", downgrading is not supported."); + _state.Level = RuntimeLevel.BootFailed; + + // in fact, this is bad enough that we want to throw + throw new BootFailedException($"Local version \"{localVersion}\" > code version \"{codeVersion}\", downgrading is not supported."); + } else if (databaseFactory.Configured == false) { // local version *does* match code version, but the database is not configured diff --git a/src/Umbraco.Core/Security/EmailService.cs b/src/Umbraco.Core/Security/EmailService.cs index f6e32409cf..e6454544ab 100644 --- a/src/Umbraco.Core/Security/EmailService.cs +++ b/src/Umbraco.Core/Security/EmailService.cs @@ -21,12 +21,6 @@ namespace Umbraco.Core.Security _defaultEmailSender = defaultEmailSender; } - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("Use the constructor specifying all dependencies")] - public EmailService() - : this(UmbracoConfig.For.UmbracoSettings().Content.NotificationEmailAddress, new EmailSender()) - { - } public async Task SendAsync(IdentityMessage message) { diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index edc10353c9..2e788382fe 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -348,23 +348,48 @@ namespace Umbraco.Core.Services /// /// Saves and publishes a document. /// - /// Property values must first be published at document level. - PublishResult SaveAndPublish(IContent content, int userId = 0, bool raiseEvents = true); + /// + /// By default, publishes all variations of the document, but it is possible to specify a culture to be published. + /// When a culture is being published, it includes all varying values along with all invariant values. For + /// anything more complicated, see . + /// The document is *always* saved, even when publishing fails. + /// If the content type is variant, then culture can be either '*' or an actual culture, but neither 'null' nor + /// 'empty'. If the content type is invariant, then culture can be either '*' or null or empty. + /// + PublishResult SaveAndPublish(IContent content, string culture = "*", int userId = 0, bool raiseEvents = true); + + /// + /// Saves and publishes a publishing document. + /// + /// + /// A publishing document is a document with values that are being published, i.e. + /// that have been published or cleared via and + /// . + /// The document is *always* saved, even when publishing fails. + /// + PublishResult SavePublishing(IContent content, int userId = 0, bool raiseEvents = true); /// /// Saves and publishes a document branch. /// - IEnumerable SaveAndPublishBranch(IContent content, bool force, string culture = null, string segment = null, int userId = 0); + IEnumerable SaveAndPublishBranch(IContent content, bool force, string culture = "*", int userId = 0); /// /// Saves and publishes a document branch. /// - IEnumerable SaveAndPublishBranch(IContent content, bool force, Func editing, Func publishValues, int userId = 0); + IEnumerable SaveAndPublishBranch(IContent content, bool force, Func editing, Func publishCultures, int userId = 0); /// - /// Unpublishes a document or optionally unpublishes a culture and/or segment for the document. + /// Unpublishes a document. /// - UnpublishResult Unpublish(IContent content, string culture = null, string segment = null, int userId = 0); + /// + /// By default, unpublishes the document as a whole, but it is possible to specify a culture to be + /// unpublished. Depending on whether that culture is mandatory, and other cultures remain published, + /// the document as a whole may or may not remain published. + /// If the content type is variant, then culture can be either '*' or an actual culture, but neither null nor + /// empty. If the content type is invariant, then culture can be either '*' or null or empty. + /// + UnpublishResult Unpublish(IContent content, string culture = "*", int userId = 0); /// /// Gets a value indicating whether a document is path-publishable. diff --git a/src/Umbraco.Core/Services/IMediaService.cs b/src/Umbraco.Core/Services/IMediaService.cs index ac161018ba..31c2e74fd4 100644 --- a/src/Umbraco.Core/Services/IMediaService.cs +++ b/src/Umbraco.Core/Services/IMediaService.cs @@ -10,51 +10,6 @@ using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Services { - /// - /// A temporary interface until we are in v8, this is used to return a different result for the same method and this interface gets implemented - /// explicitly. These methods will replace the normal ones in IContentService in v8 and this will be removed. - /// - public interface IMediaServiceOperations - { - //TODO: Remove this class in v8 - - //TODO: There's probably more that needs to be added like the EmptyRecycleBin, etc... - - /// - /// Deletes an object by moving it to the Recycle Bin - /// - /// The to delete - /// Id of the User deleting the Media - Attempt MoveToRecycleBin(IMedia media, int userId = 0); - - /// - /// Permanently deletes an object - /// - /// - /// Please note that this method will completely remove the Media from the database, - /// but current not from the file system. - /// - /// The to delete - /// Id of the User deleting the Media - Attempt Delete(IMedia media, int userId = 0); - - /// - /// Saves a single object - /// - /// The to save - /// Id of the User saving the Media - /// Optional boolean indicating whether or not to raise events. - Attempt Save(IMedia media, int userId = 0, bool raiseEvents = true); - - /// - /// Saves a collection of objects - /// - /// Collection of to save - /// Id of the User saving the Media - /// Optional boolean indicating whether or not to raise events. - Attempt Save(IEnumerable medias, int userId = 0, bool raiseEvents = true); - } - /// /// Defines the Media Service, which is an easy access to operations involving /// @@ -237,13 +192,13 @@ namespace Umbraco.Core.Services /// Id of the Media's new Parent /// Id of the User moving the Media void Move(IMedia media, int parentId, int userId = 0); - + /// /// Deletes an object by moving it to the Recycle Bin /// /// The to delete /// Id of the User deleting the Media - void MoveToRecycleBin(IMedia media, int userId = 0); + Attempt MoveToRecycleBin(IMedia media, int userId = 0); /// /// Empties the Recycle Bin by deleting all that resides in the bin @@ -265,7 +220,7 @@ namespace Umbraco.Core.Services /// Ids of the s /// Optional Id of the user issueing the delete operation void DeleteMediaOfTypes(IEnumerable mediaTypeIds, int userId = 0); - + /// /// Permanently deletes an object /// @@ -275,7 +230,7 @@ namespace Umbraco.Core.Services /// /// The to delete /// Id of the User deleting the Media - void Delete(IMedia media, int userId = 0); + Attempt Delete(IMedia media, int userId = 0); /// /// Saves a single object @@ -283,7 +238,7 @@ namespace Umbraco.Core.Services /// The to save /// Id of the User saving the Media /// Optional boolean indicating whether or not to raise events. - void Save(IMedia media, int userId = 0, bool raiseEvents = true); + Attempt Save(IMedia media, int userId = 0, bool raiseEvents = true); /// /// Saves a collection of objects @@ -291,7 +246,7 @@ namespace Umbraco.Core.Services /// Collection of to save /// Id of the User saving the Media /// Optional boolean indicating whether or not to raise events. - void Save(IEnumerable medias, int userId = 0, bool raiseEvents = true); + Attempt Save(IEnumerable medias, int userId = 0, bool raiseEvents = true); /// /// Gets an object by its 'UniqueId' diff --git a/src/Umbraco.Core/Services/IRelationService.cs b/src/Umbraco.Core/Services/IRelationService.cs index 7b49baafe9..e2733a311d 100644 --- a/src/Umbraco.Core/Services/IRelationService.cs +++ b/src/Umbraco.Core/Services/IRelationService.cs @@ -189,6 +189,15 @@ namespace Umbraco.Core.Services IEnumerable relations, bool loadBaseType = false); + /// + /// Relates two objects by their entity Ids. + /// + /// Id of the parent + /// Id of the child + /// The type of relation to create + /// The created + IRelation Relate(int parentId, int childId, IRelationType relationType); + /// /// Relates two objects that are based on the interface. /// @@ -198,6 +207,15 @@ namespace Umbraco.Core.Services /// The created IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, IRelationType relationType); + /// + /// Relates two objects by their entity Ids. + /// + /// Id of the parent + /// Id of the child + /// Alias of the type of relation to create + /// The created + IRelation Relate(int parentId, int childId, string relationTypeAlias); + /// /// Relates two objects that are based on the interface. /// diff --git a/src/Umbraco.Core/Services/Implement/ConsentService.cs b/src/Umbraco.Core/Services/Implement/ConsentService.cs index 21ec5f4434..acc4683d64 100644 --- a/src/Umbraco.Core/Services/Implement/ConsentService.cs +++ b/src/Umbraco.Core/Services/Implement/ConsentService.cs @@ -9,7 +9,7 @@ using Umbraco.Core.Scoping; namespace Umbraco.Core.Services.Implement { /// - /// Implements . + /// Implements . /// internal class ConsentService : ScopeRepositoryService, IConsentService { diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 0c0015019a..e953a6073e 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using Umbraco.Core.Composing; using Umbraco.Core.Events; using Umbraco.Core.Exceptions; using Umbraco.Core.IO; @@ -862,7 +863,7 @@ namespace Umbraco.Core.Services.Implement { var publishedState = content.PublishedState; if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished) - throw new InvalidOperationException("Cannot save a (un)publishing, use the dedicated (un)publish method."); + throw new InvalidOperationException("Cannot save (un)publishing content, use the dedicated SavePublished method."); var evtMsgs = EventMessagesFactory.Get(); @@ -875,8 +876,6 @@ namespace Umbraco.Core.Services.Implement return OperationResult.Cancel(evtMsgs); } - var isNew = content.IsNewEntity(); - scope.WriteLock(Constants.Locks.ContentTree); if (content.HasIdentity == false) @@ -890,9 +889,9 @@ namespace Umbraco.Core.Services.Implement saveEventArgs.CanCancel = false; scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); } - var changeType = isNew ? TreeChangeTypes.RefreshBranch : TreeChangeTypes.RefreshNode; + var changeType = TreeChangeTypes.RefreshNode; scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); - Audit(AuditType.Save, "Save Content performed by user", userId, content.Id); + Audit(AuditType.Save, "Saved by user", userId, content.Id); scope.Complete(); } @@ -914,8 +913,7 @@ namespace Umbraco.Core.Services.Implement return OperationResult.Cancel(evtMsgs); } - var treeChanges = contentsA.Select(x => new TreeChange(x, - x.IsNewEntity() ? TreeChangeTypes.RefreshBranch : TreeChangeTypes.RefreshNode)); + var treeChanges = contentsA.Select(x => new TreeChange(x, TreeChangeTypes.RefreshNode)); scope.WriteLock(Constants.Locks.ContentTree); foreach (var content in contentsA) @@ -933,7 +931,7 @@ namespace Umbraco.Core.Services.Implement scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); } scope.Events.Dispatch(TreeChanged, this, treeChanges.ToEventArgs()); - Audit(AuditType.Save, "Bulk Save content performed by user", userId == -1 ? 0 : userId, Constants.System.Root); + Audit(AuditType.Save, "Bulk-saved by user", userId == -1 ? 0 : userId, Constants.System.Root); scope.Complete(); } @@ -942,21 +940,169 @@ namespace Umbraco.Core.Services.Implement } /// - public PublishResult SaveAndPublish(IContent content, int userId = 0, bool raiseEvents = true) + public PublishResult SaveAndPublish(IContent content, string culture = "*", int userId = 0, bool raiseEvents = true) { var evtMsgs = EventMessagesFactory.Get(); - PublishResult result; - if (((Content) content).PublishedState != PublishedState.Publishing && content.Published) + var publishedState = content.PublishedState; + if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished) + throw new InvalidOperationException($"Cannot save-and-publish (un)publishing content, use the dedicated {nameof(SavePublishing)} method."); + + // cannot accept invariant (null or empty) culture for variant content type + // cannot accept a specific culture for invariant content type (but '*' is ok) + if (content.ContentType.VariesByCulture()) { - // already published, and values haven't changed - i.e. not changing anything - // nothing to do - // fixme - unless we *want* to bump dates? - return new PublishResult(PublishResultType.SuccessAlready, evtMsgs, content); + if (culture.IsNullOrWhiteSpace()) + throw new NotSupportedException("Invariant culture is not supported by variant content types."); } + else + { + if (!culture.IsNullOrWhiteSpace() && culture != "*") + throw new NotSupportedException($"Culture \"{culture}\" is not supported by invariant content types."); + } + + // if culture is specific, first publish the invariant values, then publish the culture itself. + // if culture is '*', then publish them all (including variants) + + // explicitely SaveAndPublish a specific culture also publishes invariant values + if (!culture.IsNullOrWhiteSpace() && culture != "*") + { + // publish the invariant values + var publishInvariant = content.PublishCulture(null); + if (!publishInvariant) + return new PublishResult(PublishResultType.FailedContentInvalid, evtMsgs, content); + } + + // publish the culture(s) + var publishCulture = content.PublishCulture(culture); + if (!publishCulture) + return new PublishResult(PublishResultType.FailedContentInvalid, evtMsgs, content); + + // finally, "save publishing" + // what happens next depends on whether the content can be published or not + return SavePublishing(content, userId, raiseEvents); + } + + /// + public UnpublishResult Unpublish(IContent content, string culture = "*", int userId = 0) + { + var evtMsgs = EventMessagesFactory.Get(); + + culture = culture.NullOrWhiteSpaceAsNull(); + + var publishedState = content.PublishedState; + if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished) + throw new InvalidOperationException($"Cannot save-and-publish (un)publishing content, use the dedicated {nameof(SavePublishing)} method."); + + // cannot accept invariant (null or empty) culture for variant content type + // cannot accept a specific culture for invariant content type (but '*' is ok) + if (content.ContentType.VariesByCulture()) + { + if (culture == null) + throw new NotSupportedException("Invariant culture is not supported by variant content types."); + } + else + { + if (culture != null && culture != "*") + throw new NotSupportedException($"Culture \"{culture}\" is not supported by invariant content types."); + } + + // if the content is not published, nothing to do + if (!content.Published) + return new UnpublishResult(UnpublishResultType.SuccessAlready, evtMsgs, content); + + // all cultures = unpublish whole + if (culture == "*" || (!content.ContentType.VariesByCulture() && culture == null)) + { + ((Content) content).PublishedState = PublishedState.Unpublishing; + } + else + { + // if the culture we want to unpublish was already unpublished, nothing to do + if (!content.WasCulturePublished(culture)) + return new UnpublishResult(UnpublishResultType.SuccessAlready, evtMsgs, content); + + // unpublish the culture + content.UnpublishCulture(culture); + } + + // finally, "save publishing" + // what happens next depends on whether the content can be published or not + using (var scope = ScopeProvider.CreateScope()) + { + var saved = SavePublishing(content, userId); + if (saved.Success) + { + UnpublishResultType result; + if (culture == "*" || culture == null) + { + Audit(AuditType.UnPublish, "Unpublished by user", userId, content.Id); + result = UnpublishResultType.Success; + } + else + { + Audit(AuditType.UnPublish, $"Culture \"{culture}\" unpublished by user", userId, content.Id); + if (!content.Published) + Audit(AuditType.UnPublish, $"Unpublished (culture \"{culture}\" is mandatory) by user", userId, content.Id); + result = content.Published ? UnpublishResultType.SuccessCulture : UnpublishResultType.SuccessMandatoryCulture; + } + scope.Complete(); + return new UnpublishResult(result, evtMsgs, content); + } + + // failed - map result + var r = saved.Result == PublishResultType.FailedCancelledByEvent + ? UnpublishResultType.FailedCancelledByEvent + : UnpublishResultType.Failed; + return new UnpublishResult(r, evtMsgs, content); + } + } + + /// + public PublishResult SavePublishing(IContent content, int userId = 0, bool raiseEvents = true) + { + var evtMsgs = EventMessagesFactory.Get(); + PublishResult publishResult = null; + UnpublishResult unpublishResult = null; + + // nothing set = republish it all + if (content.PublishedState != PublishedState.Publishing && content.PublishedState != PublishedState.Unpublishing) + ((Content) content).PublishedState = PublishedState.Publishing; + + // state here is either Publishing or Unpublishing + var publishing = content.PublishedState == PublishedState.Publishing; + var unpublishing = content.PublishedState == PublishedState.Unpublishing; using (var scope = ScopeProvider.CreateScope()) { + // is the content going to end up published, or unpublished? + if (publishing && content.ContentType.VariesByCulture()) + { + var publishedCultures = content.PublishedCultures.ToList(); + var cannotBePublished = publishedCultures.Count == 0; // no published cultures = cannot be published + if (!cannotBePublished) + { + var mandatoryCultures = _languageRepository.GetMany().Where(x => x.Mandatory).Select(x => x.IsoCode); + cannotBePublished = mandatoryCultures.Any(x => !publishedCultures.Contains(x, StringComparer.OrdinalIgnoreCase)); // missing mandatory culture = cannot be published + } + + if (cannotBePublished) + { + publishing = false; + unpublishing = content.Published; // if not published yet, nothing to do + + // we may end up in a state where we won't publish nor unpublish + // keep going, though, as we want to save anways + } + } + + var isNew = !content.HasIdentity; + var changeType = isNew ? TreeChangeTypes.RefreshNode : TreeChangeTypes.RefreshBranch; + var previouslyPublished = content.HasIdentity && content.Published; + + scope.WriteLock(Constants.Locks.ContentTree); + + // always save var saveEventArgs = new SaveEventArgs(content, evtMsgs); if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving")) { @@ -964,183 +1110,118 @@ namespace Umbraco.Core.Services.Implement return new PublishResult(PublishResultType.FailedCancelledByEvent, evtMsgs, content); } - var isNew = content.IsNewEntity(); - var changeType = isNew ? TreeChangeTypes.RefreshBranch : TreeChangeTypes.RefreshNode; - var previouslyPublished = content.HasIdentity && content.Published; + if (publishing) + { + // ensure that the document can be published, and publish + // handling events, business rules, etc + // note: StrategyPublish flips the PublishedState to Publishing! + publishResult = StrategyCanPublish(scope, content, userId, /*checkPath:*/ true, evtMsgs); + if (publishResult.Success) + publishResult = StrategyPublish(scope, content, /*canPublish:*/ true, userId, evtMsgs); + if (!publishResult.Success) + ((Content) content).Published = content.Published; // reset published state = save unchanged + } - scope.WriteLock(Constants.Locks.ContentTree); + if (unpublishing) + { + var newest = GetById(content.Id); // ensure we have the newest version - in scope + if (content.VersionId != newest.VersionId) // but use the original object if it's already the newest version + content = newest; - // ensure that the document can be published, and publish - // handling events, business rules, etc - result = StrategyCanPublish(scope, content, userId, /*checkPath:*/ true, evtMsgs); - if (result.Success) - result = StrategyPublish(scope, content, /*canPublish:*/ true, userId, evtMsgs); + if (content.Published) + { + // ensure that the document can be unpublished, and unpublish + // handling events, business rules, etc + // note: StrategyUnpublish flips the PublishedState to Unpublishing! + unpublishResult = StrategyCanUnpublish(scope, content, userId, evtMsgs); + if (unpublishResult.Success) + unpublishResult = StrategyUnpublish(scope, content, true, userId, evtMsgs); + if (!unpublishResult.Success) + ((Content) content).Published = content.Published; // reset published state = save unchanged + } + else + { + // already unpublished - optimistic concurrency collision, really, + // and I am not sure at all what we should do, better die fast, else + // we may end up corrupting the db + throw new InvalidOperationException("Concurrency collision."); + } + } - // save - always, even if not publishing (this is SaveAndPublish) + // save, always if (content.HasIdentity == false) content.CreatorId = userId; content.WriterId = userId; - // if not going to publish, must reset the published state - if (!result.Success) - ((Content) content).Published = content.Published; - + // saving does NOT change the published version, unless PublishedState is Publishing or Unpublishing _documentRepository.Save(content); - if (raiseEvents) // always + // raise the Saved event, always + if (raiseEvents) { saveEventArgs.CanCancel = false; scope.Events.Dispatch(Saved, this, saveEventArgs, "Saved"); } - if (result.Success == false) + if (unpublishing) // we have tried to unpublish { + if (unpublishResult.Success) // and succeeded, trigger events + { + // events and audit + scope.Events.Dispatch(UnPublished, this, new PublishEventArgs(content, false, false), "UnPublished"); + scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, TreeChangeTypes.RefreshBranch).ToEventArgs()); + Audit(AuditType.UnPublish, "Unpublished by user", userId, content.Id); + scope.Complete(); + return new PublishResult(PublishResultType.Success, evtMsgs, content); + } + + // or, failed scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); scope.Complete(); // compete the save - return result; + return new PublishResult(PublishResultType.FailedToUnpublish, evtMsgs, content); // bah } - if (isNew == false && previouslyPublished == false) - changeType = TreeChangeTypes.RefreshBranch; // whole branch - - // invalidate the node/branch - scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); - - scope.Events.Dispatch(Published, this, new PublishEventArgs(content, false, false), "Published"); - - // if was not published and now is... descendants that were 'published' (but - // had an unpublished ancestor) are 're-published' ie not explicitely published - // but back as 'published' nevertheless - if (isNew == false && previouslyPublished == false && HasChildren(content.Id)) + if (publishing) // we have tried to publish { - var descendants = GetPublishedDescendantsLocked(content).ToArray(); - scope.Events.Dispatch(Published, this, new PublishEventArgs(descendants, false, false), "Published"); - } - - Audit(AuditType.Publish, "Save and Publish performed by user", userId, content.Id); - - scope.Complete(); - } - - return result; - } - - /// - public UnpublishResult Unpublish(IContent content, string culture = null, string segment = null, int userId = 0) - { - var evtMsgs = EventMessagesFactory.Get(); - - using (var scope = ScopeProvider.CreateScope()) - { - scope.WriteLock(Constants.Locks.ContentTree); - - var tryUnpublishVariation = TryUnpublishVariationInternal(scope, content, evtMsgs, culture, segment, userId); - - if (tryUnpublishVariation) return tryUnpublishVariation.Result; - - //continue the normal Unpublish operation to unpublish the entire content item - var result = UnpublishInternal(scope, content, evtMsgs, userId); - - //not succesful, so return it - if (!result.Success) return result; - - //else check if there was a status returned from TryUnpublishVariationInternal and if so use that - return tryUnpublishVariation.Result ?? result; - } - } - - /// - /// Used to unpublish the requested variant if possible - /// - /// - /// - /// - /// - /// - /// - /// - /// A successful attempt if a variant was unpublished and it wasn't the last, else a failed result if it's an invariant document or if it's the last. - /// A failed result indicates that a normal unpublish operation will need to be performed. - /// - private Attempt TryUnpublishVariationInternal(IScope scope, IContent content, EventMessages evtMsgs, string culture, string segment, int userId) - { - if (!culture.IsNullOrWhiteSpace() || !segment.IsNullOrWhiteSpace()) - { - //now we need to check if this is not the last culture/segment that is published - if (!culture.IsNullOrWhiteSpace()) - { - //capture before we clear the values - var publishedCultureCount = content.PublishedCultures.Count(); - - //we need to unpublish everything if this is a mandatory language - var unpublishAll = _languageRepository.GetMany().Where(x => x.Mandatory).Select(x => x.IsoCode).Contains(culture, StringComparer.InvariantCultureIgnoreCase); - - content.ClearPublishedValues(culture, segment); - //now we just publish with the name cleared - SaveAndPublish(content, userId); - Audit(AuditType.UnPublish, $"UnPublish variation culture: {culture ?? string.Empty}, segment: {segment ?? string.Empty} performed by user", userId, content.Id); - - //We don't need to unpublish all and this is not the last one, so complete the scope and return - if (!unpublishAll && publishedCultureCount > 1) + if (publishResult.Success) // and succeeded, trigger events { + if (isNew == false && previouslyPublished == false) + changeType = TreeChangeTypes.RefreshBranch; // whole branch + + // invalidate the node/branch + scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); + scope.Events.Dispatch(Published, this, new PublishEventArgs(content, false, false), "Published"); + + // if was not published and now is... descendants that were 'published' (but + // had an unpublished ancestor) are 're-published' ie not explicitely published + // but back as 'published' nevertheless + if (isNew == false && previouslyPublished == false && HasChildren(content.Id)) + { + var descendants = GetPublishedDescendantsLocked(content).ToArray(); + scope.Events.Dispatch(Published, this, new PublishEventArgs(descendants, false, false), "Published"); + } + + Audit(AuditType.Publish, "Published by user", userId, content.Id); scope.Complete(); - return Attempt.Succeed(new UnpublishResult(UnpublishResultType.SuccessVariant, evtMsgs, content)); + return publishResult; } - if (unpublishAll) - { - //return a fail with a custom status here so the normal unpublish operation takes place but the result will be this flag - return Attempt.Fail(new UnpublishResult(UnpublishResultType.SuccessMandatoryCulture, evtMsgs, content)); - } + // or, failed + scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); + scope.Complete(); // compete the save + return publishResult; } - if (!segment.IsNullOrWhiteSpace()) - { - //TODO: When segments are supported, implement this, a discussion needs to happen about what how a segment is 'published' or not - // since currently this is very different from a culture. - throw new NotImplementedException("Segments are currently not supported in Umbraco"); - } + // both publishing and unpublishing are false + // this means that we wanted to publish, in a variant scenario, a document that + // was not published yet, and we could not, due to cultures issues + // + // raise event (we saved), report + + scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, changeType).ToEventArgs()); + scope.Complete(); // compete the save + return new PublishResult(PublishResultType.FailedByCulture, evtMsgs, content); } - - //This is either a non variant document or this is the last one or this was a mandatory variant, so return a failed result which indicates that a normal unpublish operation needs to also take place - return Attempt.Fail(); - } - - /// - /// Performs the logic to unpublish an entire document - /// - /// - /// - /// - /// - /// - private UnpublishResult UnpublishInternal(IScope scope, IContent content, EventMessages evtMsgs, int userId) - { - var newest = GetById(content.Id); // ensure we have the newest version - if (content.VersionId != newest.VersionId) // but use the original object if it's already the newest version - content = newest; - if (content.Published == false) - { - scope.Complete(); - return new UnpublishResult(UnpublishResultType.SuccessAlready, evtMsgs, content); // already unpublished - } - - // strategy - // fixme should we still complete the uow? don't want to rollback here! - var attempt = StrategyCanUnpublish(scope, content, userId, evtMsgs); - if (attempt.Success == false) return attempt; // causes rollback - attempt = StrategyUnpublish(scope, content, true, userId, evtMsgs); - if (attempt.Success == false) return attempt; // causes rollback - - content.WriterId = userId; - _documentRepository.Save(content); - - scope.Events.Dispatch(UnPublished, this, new PublishEventArgs(content, false, false), "UnPublished"); - scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, TreeChangeTypes.RefreshBranch).ToEventArgs()); - Audit(AuditType.UnPublish, "UnPublish performed by user", userId, content.Id); - - scope.Complete(); - return new UnpublishResult(evtMsgs, content); } /// @@ -1156,8 +1237,8 @@ namespace Umbraco.Core.Services.Implement try { d.ReleaseDate = null; - d.TryPublishValues(); // fixme variants? - result = SaveAndPublish(d, d.WriterId); + d.PublishCulture(); // fixme variants? + result = SaveAndPublish(d, userId: d.WriterId); if (result.Success == false) Logger.Error($"Failed to publish document id={d.Id}, reason={result.Result}."); } @@ -1189,19 +1270,22 @@ namespace Umbraco.Core.Services.Implement } /// - public IEnumerable SaveAndPublishBranch(IContent content, bool force, string culture = null, string segment = null, int userId = 0) + public IEnumerable SaveAndPublishBranch(IContent content, bool force, string culture = "*", int userId = 0) { - segment = segment?.ToLowerInvariant(); + // note: EditedValue and PublishedValue are objects here, so it is important to .Equals() + // and not to == them, else we would be comparing references, and that is a bad thing - bool IsEditing(IContent c, string l, string s) - => c.Properties.Any(x => x.Values.Where(y => y.Culture == l && y.Segment == s).Any(y => y.EditedValue != y.PublishedValue)); + bool IsEditing(IContent c, string l) + => c.PublishName != c.Name || + c.PublishedCultures.Any(x => c.GetCultureName(x) != c.GetPublishName(x)) || + c.Properties.Any(x => x.Values.Where(y => culture == "*" || y.Culture == l).Any(y => !y.EditedValue.Equals(y.PublishedValue))); - return SaveAndPublishBranch(content, force, document => IsEditing(document, culture, segment), document => document.TryPublishValues(culture, segment), userId); + return SaveAndPublishBranch(content, force, document => IsEditing(document, culture), document => document.PublishCulture(culture), userId); } /// public IEnumerable SaveAndPublishBranch(IContent document, bool force, - Func editing, Func publishValues, int userId = 0) + Func editing, Func publishCultures, int userId = 0) { var evtMsgs = EventMessagesFactory.Get(); var results = new List(); @@ -1221,7 +1305,7 @@ namespace Umbraco.Core.Services.Implement throw new InvalidOperationException("Do not publish values when publishing branches."); // deal with the branch root - if it fails, abort - var result = SaveAndPublishBranchOne(scope, document, editing, publishValues, true, publishedDocuments, evtMsgs, userId); + var result = SaveAndPublishBranchOne(scope, document, editing, publishCultures, true, publishedDocuments, evtMsgs, userId); results.Add(result); if (!result.Success) return results; @@ -1241,7 +1325,7 @@ namespace Umbraco.Core.Services.Implement // no need to check path here, // 1. because we know the parent is path-published (we just published it) // 2. because it would not work as nothing's been written out to the db until the uow completes - result = SaveAndPublishBranchOne(scope, d, editing, publishValues, false, publishedDocuments, evtMsgs, userId); + result = SaveAndPublishBranchOne(scope, d, editing, publishCultures, false, publishedDocuments, evtMsgs, userId); results.Add(result); if (result.Success) continue; @@ -1251,7 +1335,7 @@ namespace Umbraco.Core.Services.Implement scope.Events.Dispatch(TreeChanged, this, new TreeChange(document, TreeChangeTypes.RefreshBranch).ToEventArgs()); scope.Events.Dispatch(Published, this, new PublishEventArgs(publishedDocuments, false, false), "Published"); - Audit(AuditType.Publish, "SaveAndPublishBranch performed by user", userId, document.Id); + Audit(AuditType.Publish, "Branch published by user", userId, document.Id); scope.Complete(); } @@ -1320,7 +1404,7 @@ namespace Umbraco.Core.Services.Implement DeleteLocked(scope, content); scope.Events.Dispatch(TreeChanged, this, new TreeChange(content, TreeChangeTypes.Remove).ToEventArgs()); - Audit(AuditType.Delete, "Delete Content performed by user", userId, content.Id); + Audit(AuditType.Delete, "Deleted by user", userId, content.Id); scope.Complete(); } @@ -1388,7 +1472,7 @@ namespace Umbraco.Core.Services.Implement deleteRevisionsEventArgs.CanCancel = false; scope.Events.Dispatch(DeletedVersions, this, deleteRevisionsEventArgs); - Audit(AuditType.Delete, "Delete Content by version date performed by user", userId, Constants.System.Root); + Audit(AuditType.Delete, "Delete (by version date) by user", userId, Constants.System.Root); scope.Complete(); } @@ -1425,7 +1509,7 @@ namespace Umbraco.Core.Services.Implement _documentRepository.DeleteVersion(versionId); scope.Events.Dispatch(DeletedVersions, this, new DeleteRevisionsEventArgs(id, false,/* specificVersion:*/ versionId)); - Audit(AuditType.Delete, "Delete Content by version performed by user", userId, Constants.System.Root); + Audit(AuditType.Delete, "Delete (by version) by user", userId, Constants.System.Root); scope.Complete(); } @@ -1470,7 +1554,7 @@ namespace Umbraco.Core.Services.Implement moveEventArgs.CanCancel = false; moveEventArgs.MoveInfoCollection = moveInfo; scope.Events.Dispatch(Trashed, this, moveEventArgs, nameof(Trashed)); - Audit(AuditType.Move, "Move Content to Recycle Bin performed by user", userId, content.Id); + Audit(AuditType.Move, "Moved to Recycle Bin by user", userId, content.Id); scope.Complete(); } @@ -1542,7 +1626,7 @@ namespace Umbraco.Core.Services.Implement moveEventArgs.MoveInfoCollection = moveInfo; moveEventArgs.CanCancel = false; scope.Events.Dispatch(Moved, this, moveEventArgs, nameof(Moved)); - Audit(AuditType.Move, "Move Content performed by user", userId, content.Id); + Audit(AuditType.Move, "Moved by user", userId, content.Id); scope.Complete(); } @@ -1639,7 +1723,7 @@ namespace Umbraco.Core.Services.Implement recycleBinEventArgs.RecycleBinEmptiedSuccessfully = true; // oh my?! scope.Events.Dispatch(EmptiedRecycleBin, this, recycleBinEventArgs); scope.Events.Dispatch(TreeChanged, this, deleted.Select(x => new TreeChange(x, TreeChangeTypes.Remove)).ToEventArgs()); - Audit(AuditType.Delete, "Empty Content Recycle Bin performed by user", 0, Constants.System.RecycleBinContent); + Audit(AuditType.Delete, "Recycle Bin emptied by user", 0, Constants.System.RecycleBinContent); scope.Complete(); } diff --git a/src/Umbraco.Core/Services/Implement/MediaService.cs b/src/Umbraco.Core/Services/Implement/MediaService.cs index 504750874c..f2d873f2ca 100644 --- a/src/Umbraco.Core/Services/Implement/MediaService.cs +++ b/src/Umbraco.Core/Services/Implement/MediaService.cs @@ -19,7 +19,7 @@ namespace Umbraco.Core.Services.Implement /// /// Represents the Media Service, which is an easy access to operations involving /// - public class MediaService : ScopeRepositoryService, IMediaService, IMediaServiceOperations + public class MediaService : ScopeRepositoryService, IMediaService { private readonly IMediaRepository _mediaRepository; private readonly IMediaTypeRepository _mediaTypeRepository; @@ -749,18 +749,7 @@ namespace Umbraco.Core.Services.Implement /// The to save /// Id of the User saving the Media /// Optional boolean indicating whether or not to raise events. - public void Save(IMedia media, int userId = 0, bool raiseEvents = true) - { - ((IMediaServiceOperations) this).Save(media, userId, raiseEvents); - } - - /// - /// Saves a single object - /// - /// The to save - /// Id of the User saving the Media - /// Optional boolean indicating whether or not to raise events. - Attempt IMediaServiceOperations.Save(IMedia media, int userId, bool raiseEvents) + public Attempt Save(IMedia media, int userId = 0, bool raiseEvents = true) { var evtMsgs = EventMessagesFactory.Get(); @@ -779,8 +768,6 @@ namespace Umbraco.Core.Services.Implement if (string.IsNullOrWhiteSpace(media.Name)) throw new ArgumentException("Media has no name.", nameof(media)); - var isNew = media.IsNewEntity(); - scope.WriteLock(Constants.Locks.MediaTree); if (media.HasIdentity == false) media.CreatorId = userId; @@ -791,7 +778,7 @@ namespace Umbraco.Core.Services.Implement saveEventArgs.CanCancel = false; scope.Events.Dispatch(Saved, this, saveEventArgs); } - var changeType = isNew ? TreeChangeTypes.RefreshBranch : TreeChangeTypes.RefreshNode; + var changeType = TreeChangeTypes.RefreshNode; scope.Events.Dispatch(TreeChanged, this, new TreeChange(media, changeType).ToEventArgs()); Audit(AuditType.Save, "Save Media performed by user", userId, media.Id); @@ -807,18 +794,7 @@ namespace Umbraco.Core.Services.Implement /// Collection of to save /// Id of the User saving the Media /// Optional boolean indicating whether or not to raise events. - public void Save(IEnumerable medias, int userId = 0, bool raiseEvents = true) - { - ((IMediaServiceOperations) this).Save(medias, userId, raiseEvents); - } - - /// - /// Saves a collection of objects - /// - /// Collection of to save - /// Id of the User saving the Media - /// Optional boolean indicating whether or not to raise events. - Attempt IMediaServiceOperations.Save(IEnumerable medias, int userId, bool raiseEvents) + public Attempt Save(IEnumerable medias, int userId = 0, bool raiseEvents = true) { var evtMsgs = EventMessagesFactory.Get(); var mediasA = medias.ToArray(); @@ -832,7 +808,7 @@ namespace Umbraco.Core.Services.Implement return OperationResult.Attempt.Cancel(evtMsgs); } - var treeChanges = mediasA.Select(x => new TreeChange(x, x.IsNewEntity() ? TreeChangeTypes.RefreshBranch : TreeChangeTypes.RefreshNode)); + var treeChanges = mediasA.Select(x => new TreeChange(x, TreeChangeTypes.RefreshNode)); scope.WriteLock(Constants.Locks.MediaTree); foreach (var media in mediasA) @@ -859,27 +835,13 @@ namespace Umbraco.Core.Services.Implement #endregion #region Delete - - /// - /// Permanently deletes an object as well as all of its Children. - /// - /// - /// Please note that this method will completely remove the Media from the database, - /// as well as associated media files from the file system. - /// - /// The to delete - /// Id of the User deleting the Media - public void Delete(IMedia media, int userId = 0) - { - ((IMediaServiceOperations) this).Delete(media, userId); - } - + /// /// Permanently deletes an object /// /// The to delete /// Id of the User deleting the Media - Attempt IMediaServiceOperations.Delete(IMedia media, int userId) + public Attempt Delete(IMedia media, int userId = 0) { var evtMsgs = EventMessagesFactory.Get(); @@ -1034,17 +996,7 @@ namespace Umbraco.Core.Services.Implement /// /// The to delete /// Id of the User deleting the Media - public void MoveToRecycleBin(IMedia media, int userId = 0) - { - ((IMediaServiceOperations) this).MoveToRecycleBin(media, userId); - } - - /// - /// Deletes an object by moving it to the Recycle Bin - /// - /// The to delete - /// Id of the User deleting the Media - Attempt IMediaServiceOperations.MoveToRecycleBin(IMedia media, int userId) + public Attempt MoveToRecycleBin(IMedia media, int userId = 0) { var evtMsgs = EventMessagesFactory.Get(); var moves = new List>(); diff --git a/src/Umbraco.Core/Services/Implement/RelationService.cs b/src/Umbraco.Core/Services/Implement/RelationService.cs index f3af15c882..a4c1b977a0 100644 --- a/src/Umbraco.Core/Services/Implement/RelationService.cs +++ b/src/Umbraco.Core/Services/Implement/RelationService.cs @@ -217,7 +217,6 @@ namespace Umbraco.Core.Services.Implement { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { - var rtQuery = Query().Where(x => x.Alias == relationTypeAlias); var relationType = _relationTypeRepository.Get(rtQuery).FirstOrDefault(); if (relationType == null) @@ -374,19 +373,19 @@ namespace Umbraco.Core.Services.Implement } /// - /// Relates two objects that are based on the interface. + /// Relates two objects by their entity Ids. /// - /// Parent entity - /// Child entity + /// Id of the parent + /// Id of the child /// The type of relation to create /// The created - public IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, IRelationType relationType) + public IRelation Relate(int parentId, int childId, IRelationType relationType) { - //Ensure that the RelationType has an indentity before using it to relate two entities + // Ensure that the RelationType has an indentity before using it to relate two entities if (relationType.HasIdentity == false) Save(relationType); - var relation = new Relation(parent.Id, child.Id, relationType); + var relation = new Relation(parentId, childId, relationType); using (var scope = ScopeProvider.CreateScope()) { @@ -401,9 +400,36 @@ namespace Umbraco.Core.Services.Implement saveEventArgs.CanCancel = false; scope.Events.Dispatch(SavedRelation, this, saveEventArgs); scope.Complete(); + return relation; } + } - return relation; + /// + /// Relates two objects that are based on the interface. + /// + /// Parent entity + /// Child entity + /// The type of relation to create + /// The created + public IRelation Relate(IUmbracoEntity parent, IUmbracoEntity child, IRelationType relationType) + { + return Relate(parent.Id, child.Id, relationType); + } + + /// + /// Relates two objects by their entity Ids. + /// + /// Id of the parent + /// Id of the child + /// Alias of the type of relation to create + /// The created + public IRelation Relate(int parentId, int childId, string relationTypeAlias) + { + var relationType = GetRelationTypeByAlias(relationTypeAlias); + if (relationType == null || string.IsNullOrEmpty(relationType.Alias)) + throw new ArgumentNullException(string.Format("No RelationType with Alias '{0}' exists.", relationTypeAlias)); + + return Relate(parentId, childId, relationType); } /// @@ -417,26 +443,9 @@ namespace Umbraco.Core.Services.Implement { var relationType = GetRelationTypeByAlias(relationTypeAlias); if (relationType == null || string.IsNullOrEmpty(relationType.Alias)) - throw new ArgumentNullException($"No RelationType with Alias '{relationTypeAlias}' exists."); + throw new ArgumentNullException(string.Format("No RelationType with Alias '{0}' exists.", relationTypeAlias)); - var relation = new Relation(parent.Id, child.Id, relationType); - - using (var scope = ScopeProvider.CreateScope()) - { - var saveEventArgs = new SaveEventArgs(relation); - if (scope.Events.DispatchCancelable(SavingRelation, this, saveEventArgs)) - { - scope.Complete(); - return relation; // fixme - returning sth that does not exist here?! // fixme - returning sth that does not exist here?! - } - - _relationRepository.Save(relation); - saveEventArgs.CanCancel = false; - scope.Events.Dispatch(SavedRelation, this, saveEventArgs); - scope.Complete(); - } - - return relation; + return Relate(parent.Id, child.Id, relationType); } /// diff --git a/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs b/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs index 367a870896..b41e7739ba 100644 --- a/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs +++ b/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs @@ -12,10 +12,10 @@ namespace Umbraco.Core.Services /// public static class LocalizedTextServiceExtensions { - public static string Localize(this ILocalizedTextService manager, params string[] keyParts) + public static string Localize(this ILocalizedTextService manager, string area, string key) { - var key = string.Join("/", keyParts); - return manager.Localize(key, Thread.CurrentThread.CurrentUICulture); + var fullKey = string.Join("/", area, key); + return manager.Localize(fullKey, Thread.CurrentThread.CurrentUICulture); } /// diff --git a/src/Umbraco.Core/Services/PublishResultType.cs b/src/Umbraco.Core/Services/PublishResultType.cs index b4bfe078b7..15b2f503c7 100644 --- a/src/Umbraco.Core/Services/PublishResultType.cs +++ b/src/Umbraco.Core/Services/PublishResultType.cs @@ -18,7 +18,7 @@ /// The item was already published. /// SuccessAlready = 1, - + /// /// The operation failed. /// @@ -58,8 +58,23 @@ FailedContentInvalid = Failed | 6, /// - /// The document could not be published because it does not have published values. + /// Cannot republish a document that hasn't been published. /// - FailedNoPublishedValues = Failed | 7 + FailedNoPublishedValues = Failed | 7, // in ContentService.StrategyCanPublish - fixme weird + + /// + /// Some mandatory cultures are missing, or are not valid. + /// + FailedCannotPublish = Failed | 8, // in ContentController.PublishInternal - fixme // FailedByCulture? + + /// + /// Publishing changes triggered an unpublishing, due to missing mandatory cultures, and unpublishing failed. + /// + FailedToUnpublish = Failed | 9, // in ContentService.SavePublishing + + /// + /// Some mandatory cultures are missing. + /// + FailedByCulture = Failed | 10, // in ContentService.SavePublishing } } diff --git a/src/Umbraco.Core/Services/ServiceWithResultExtensions.cs b/src/Umbraco.Core/Services/ServiceWithResultExtensions.cs deleted file mode 100644 index f64a10540f..0000000000 --- a/src/Umbraco.Core/Services/ServiceWithResultExtensions.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Umbraco.Core.Services -{ - /// - /// These are used currently to return the temporary 'operation' interfaces for services - /// which are used to return a status from operational methods so we can determine if things are - /// cancelled, etc... - /// - /// These will be obsoleted in v8 since all real services methods will be changed to have the correct result. - /// - public static class ServiceWithResultExtensions - { - public static IMediaServiceOperations WithResult(this IMediaService mediaService) - { - return (IMediaServiceOperations)mediaService; - } - } -} diff --git a/src/Umbraco.Core/Services/UnpublishResultType.cs b/src/Umbraco.Core/Services/UnpublishResultType.cs index 010c37d7a5..e61e786a05 100644 --- a/src/Umbraco.Core/Services/UnpublishResultType.cs +++ b/src/Umbraco.Core/Services/UnpublishResultType.cs @@ -18,7 +18,7 @@ /// /// The specified variant was unpublished, the content item itself remains published. /// - SuccessVariant = 2, + SuccessCulture = 2, /// /// The specified variant was a mandatory culture therefore it was unpublished and the content item itself is unpublished diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index a2f5727ae4..aa7075f418 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -693,13 +693,6 @@ namespace Umbraco.Core return s.LastIndexOf(value, StringComparison.OrdinalIgnoreCase); } - [Obsolete("Use Guid.TryParse instead")] - [EditorBrowsable(EditorBrowsableState.Never)] - public static bool IsGuid(this string str, bool withHyphens) - { - Guid g; - return Guid.TryParse(str, out g); - } /// /// Tries to parse a string into the supplied type by finding and using the Type's "Parse" method @@ -1504,7 +1497,7 @@ namespace Umbraco.Core /// /// Turns an null-or-whitespace string into a null string. /// - public static string NullEmpty(this string text) + public static string NullOrWhiteSpaceAsNull(this string text) => string.IsNullOrWhiteSpace(text) ? null : text; } } diff --git a/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs b/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs index 09feeff338..9472ff4823 100644 --- a/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs +++ b/src/Umbraco.Core/Strings/DefaultUrlSegmentProvider.cs @@ -24,7 +24,7 @@ namespace Umbraco.Core.Strings if (content.HasProperty(Constants.Conventions.Content.UrlName)) source = (content.GetValue(Constants.Conventions.Content.UrlName, culture) ?? string.Empty).Trim(); if (string.IsNullOrWhiteSpace(source)) - source = content.GetName(culture); + source = content.GetCultureName(culture); return source; } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 264fcc9420..31d4548198 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -57,7 +57,7 @@ - + @@ -319,6 +319,7 @@ + @@ -1100,6 +1101,7 @@ + @@ -1427,7 +1429,6 @@ - diff --git a/src/Umbraco.Examine/LoggingLevel.cs b/src/Umbraco.Examine/LoggingLevel.cs deleted file mode 100644 index 7ab8fbfad8..0000000000 --- a/src/Umbraco.Examine/LoggingLevel.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace Umbraco.Examine -{ - [Obsolete("This object is no longer used since we support the log levels that are available with LogHelper")] - public enum LoggingLevel - { - Verbose, Normal - } -} diff --git a/src/Umbraco.Examine/Umbraco.Examine.csproj b/src/Umbraco.Examine/Umbraco.Examine.csproj index 1621f8ade2..1771ca4d2b 100644 --- a/src/Umbraco.Examine/Umbraco.Examine.csproj +++ b/src/Umbraco.Examine/Umbraco.Examine.csproj @@ -61,7 +61,6 @@ - diff --git a/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs b/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs index f360262cd0..fe32a43cc9 100644 --- a/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs +++ b/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs @@ -147,11 +147,6 @@ namespace Umbraco.Tests.Configurations.UmbracoSettings Assert.IsTrue(SettingsSection.Content.PreviewBadge == @"In Preview Mode - click to end"); } [Test] - public void UmbracoLibraryCacheDuration() - { - Assert.IsTrue(SettingsSection.Content.UmbracoLibraryCacheDuration == 1800); - } - [Test] public void ResolveUrlsFromTextString() { Assert.IsFalse(SettingsSection.Content.ResolveUrlsFromTextString); diff --git a/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config b/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config index 4541632264..5373afd1b5 100644 --- a/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config +++ b/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config @@ -77,10 +77,6 @@ In Preview Mode - click to end]]> - - - 1800 - 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 index d04de47757..2d3d2cfdae 100644 --- 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 @@ -2,7 +2,7 @@
-
{{ name }}
+
{{ name }}
{{ description }}
@@ -13,9 +13,9 @@
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html b/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html index 39363be827..7b6c7ca0e1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/users/change-password.html @@ -31,8 +31,7 @@ class="input-block-level umb-textstring textstring" required val-server-field="oldPassword" - no-dirty-check - autocomplete="off" /> + no-dirty-check /> Required {{passwordForm.oldPassword.errorMsg}} @@ -45,8 +44,7 @@ required val-server-field="password" ng-minlength="{{config.minPasswordLength}}" - no-dirty-check - autocomplete="off" /> + no-dirty-check /> Required Minimum {{config.minPasswordLength}} characters @@ -58,8 +56,7 @@ + no-dirty-check /> The confirmed password doesn't match the new password! diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js index f456d7f13e..c48099c5fe 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js @@ -27,6 +27,12 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.CopyController", var node = dialogOptions.currentNode; + function treeLoadedHandler(args) { + if (node && node.path) { + $scope.dialogTreeApi.syncTree({ path: node.path, activate: false }); + } + } + function nodeSelectHandler(args) { if(args && args.event) { @@ -105,6 +111,7 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.CopyController", }; $scope.onTreeInit = function () { + $scope.dialogTreeApi.callbacks.treeLoaded(treeLoadedHandler); $scope.dialogTreeApi.callbacks.treeNodeSelect(nodeSelectHandler); $scope.dialogTreeApi.callbacks.treeNodeExpanded(nodeExpandedHandler); } diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.move.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.move.controller.js index d6b5c44523..383a764a43 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.move.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.move.controller.js @@ -25,6 +25,12 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.MoveController", var node = dialogOptions.currentNode; + function treeLoadedHandler(args) { + if (node && node.path) { + $scope.dialogTreeApi.syncTree({ path: node.path, activate: false }); + } + } + function nodeSelectHandler(args) { if(args && args.event) { @@ -107,6 +113,7 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.MoveController", }; $scope.onTreeInit = function () { + $scope.dialogTreeApi.callbacks.treeLoaded(treeLoadedHandler); $scope.dialogTreeApi.callbacks.treeNodeSelect(nodeSelectHandler); $scope.dialogTreeApi.callbacks.treeNodeExpanded(nodeExpandedHandler); } diff --git a/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.edit.controller.js index c3cbb63704..85ec8461f2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.edit.controller.js @@ -50,8 +50,9 @@ function DataTypeEditController($scope, $routeParams, $location, appState, navig $scope.preValues = []; if ($routeParams.create) { - + $scope.page.loading = true; + $scope.showIdentifier = false; //we are creating so get an empty data type item dataTypeResource.getScaffold($routeParams.id) @@ -77,6 +78,8 @@ function DataTypeEditController($scope, $routeParams, $location, appState, navig $scope.page.loading = true; + $scope.showIdentifier = true; + //we are editing so get the content item from the server dataTypeResource.getById($routeParams.id) .then(function(data) { diff --git a/src/Umbraco.Web.UI.Client/src/views/datatypes/edit.html b/src/Umbraco.Web.UI.Client/src/views/datatypes/edit.html index 458be071a7..7533209cd3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatypes/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/datatypes/edit.html @@ -25,14 +25,19 @@ + +
{{content.id}}
+ {{content.key}} +
+
- - Required + + Required
@@ -42,12 +47,11 @@
{{content.selectedEditor}}
- + - +
diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/create.html b/src/Umbraco.Web.UI.Client/src/views/dictionary/create.html new file mode 100644 index 0000000000..5b73df3f86 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/create.html @@ -0,0 +1,19 @@ +
+ +
+
Create an item under {{currentNode.name}}
+
+ +
+
+ + + + + + +
+
+
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/delete.html b/src/Umbraco.Web.UI.Client/src/views/dictionary/delete.html new file mode 100644 index 0000000000..61c26c09a7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/delete.html @@ -0,0 +1,12 @@ +
+
+ +

+ Are you sure you want to delete {{currentNode.name}} ? +

+ + + + +
+
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.create.controller.js b/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.create.controller.js new file mode 100644 index 0000000000..f47244bb10 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.create.controller.js @@ -0,0 +1,44 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.Dictionary.CreateController + * @function + * + * @description + * The controller for creating dictionary items + */ +function DictionaryCreateController($scope, $location, dictionaryResource, navigationService, notificationsService, formHelper, appState) { + var vm = this; + + vm.itemKey = ""; + + function createItem() { + + var node = $scope.dialogOptions.currentNode; + + dictionaryResource.create(node.id, vm.itemKey).then(function (data) { + navigationService.hideMenu(); + + // set new item as active in tree + var currPath = node.path ? node.path : "-1"; + navigationService.syncTree({ tree: "dictionary", path: currPath + "," + data, forceReload: true, activate: true }); + + // reset form state + formHelper.resetForm({ scope: $scope }); + + // navigate to edit view + var currentSection = appState.getSectionState("currentSection"); + $location.path("/" + currentSection + "/dictionary/edit/" + data); + + + }, function (err) { + if (err.data && err.data.message) { + notificationsService.error(err.data.message); + navigationService.hideMenu(); + } + }); + } + + vm.createItem = createItem; +} + +angular.module("umbraco").controller("Umbraco.Editors.Dictionary.CreateController", DictionaryCreateController); diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.delete.controller.js b/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.delete.controller.js new file mode 100644 index 0000000000..43d6bac401 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.delete.controller.js @@ -0,0 +1,50 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.Dictionary.DeleteController + * @function + * + * @description + * The controller for deleting dictionary items + */ +function DictionaryDeleteController($scope, $location, dictionaryResource, treeService, navigationService, appState) { + var vm = this; + + function cancel() { + navigationService.hideDialog(); + } + + function performDelete() { + // stop from firing again on double-click + if ($scope.busy) { return false; } + + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + $scope.busy = true; + + dictionaryResource.deleteById($scope.currentNode.id).then(function () { + $scope.currentNode.loading = false; + + // get the parent id + var parentId = $scope.currentNode.parentId; + + treeService.removeNode($scope.currentNode); + + navigationService.hideMenu(); + + var currentSection = appState.getSectionState("currentSection"); + if (parentId !== "-1") { + // set the view of the parent item + $location.path("/" + currentSection + "/dictionary/edit/" + parentId); + } else { + // we have no parent, so redirect to section + $location.path("/" + currentSection + "/"); + } + + }); + } + + vm.cancel = cancel; + vm.performDelete = performDelete; +} + +angular.module("umbraco").controller("Umbraco.Editors.Dictionary.DeleteController", DictionaryDeleteController); diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.edit.controller.js new file mode 100644 index 0000000000..1b9d5c3f4c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.edit.controller.js @@ -0,0 +1,115 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.Dictionary.EditController + * @function + * + * @description + * The controller for editing dictionary items + */ +function DictionaryEditController($scope, $routeParams, dictionaryResource, treeService, navigationService, appState, editorState, contentEditingHelper, formHelper, notificationsService, localizationService) { + var vm = this; + + //setup scope vars + vm.nameDirty = false; + vm.page = {}; + vm.page.loading = false; + vm.page.nameLocked = false; + vm.page.menu = {}; + vm.page.menu.currentSection = appState.getSectionState("currentSection"); + vm.page.menu.currentNode = null; + vm.description = ""; + + function loadDictionary() { + + vm.page.loading = true; + + //we are editing so get the content item from the server + dictionaryResource.getById($routeParams.id) + .then(function (data) { + + bindDictionary(data); + + vm.page.loading = false; + }); + } + + function createTranslationProperty(translation) { + return { + alias: translation.isoCode, + label: translation.displayName, + hideLabel : false + } + } + + function bindDictionary(data) { + localizationService.localize("dictionaryItem_description").then(function (value) { + vm.description = value.replace("%0%", data.name); + }); + + // create data for umb-property displaying + for (var i = 0; i < data.translations.length; i++) { + data.translations[i].property = createTranslationProperty(data.translations[i]); + } + + contentEditingHelper.handleSuccessfulSave({ + scope: $scope, + savedContent: data + }); + + // set content + vm.content = data; + + //share state + editorState.set(vm.content); + + navigationService.syncTree({ tree: "dictionary", path: data.path, forceReload: true }).then(function (syncArgs) { + vm.page.menu.currentNode = syncArgs.node; + }); + } + + function onInit() { + loadDictionary(); + } + + function saveDictionary() { + if (formHelper.submitForm({ scope: $scope, statusMessage: "Saving..." })) { + + vm.page.saveButtonState = "busy"; + + dictionaryResource.save(vm.content, vm.nameDirty) + .then(function (data) { + + formHelper.resetForm({ scope: $scope, notifications: data.notifications }); + + bindDictionary(data); + + + vm.page.saveButtonState = "success"; + }, + function (err) { + + contentEditingHelper.handleSaveError({ + redirectOnFailure: false, + err: err + }); + + notificationsService.error(err.data.message); + + vm.page.saveButtonState = "error"; + }); + } + } + + vm.save = saveDictionary; + + $scope.$watch("vm.content.name", function (newVal, oldVal) { + //when the value changes, we need to set the name dirty + if (newVal && (newVal !== oldVal) && typeof(oldVal) !== "undefined") { + vm.nameDirty = true; + } + }); + + onInit(); +} + +angular.module("umbraco").controller("Umbraco.Editors.Dictionary.EditController", DictionaryEditController); diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.list.controller.js b/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.list.controller.js new file mode 100644 index 0000000000..35739b3db7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/dictionary.list.controller.js @@ -0,0 +1,47 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.Dictionary.ListController + * @function + * + * @description + * The controller for listting dictionary items + */ +function DictionaryListController($scope, $location, dictionaryResource, localizationService, appState) { + var vm = this; + vm.title = "Dictionary overview"; + vm.loading = false; + vm.items = []; + + function loadList() { + + vm.loading = true; + + dictionaryResource.getList() + .then(function (data) { + + vm.items = data; + + vm.loading = false; + }); + } + + function clickItem(id) { + var currentSection = appState.getSectionState("currentSection"); + $location.path("/" + currentSection + "/dictionary/edit/" + id); + } + + vm.clickItem = clickItem; + + function onInit() { + localizationService.localize("dictionaryItem_overviewTitle").then(function (value) { + vm.title = value; + }); + + loadList(); + } + + onInit(); +} + + +angular.module("umbraco").controller("Umbraco.Editors.Dictionary.ListController", DictionaryListController); diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/edit.html b/src/Umbraco.Web.UI.Client/src/views/dictionary/edit.html new file mode 100644 index 0000000000..c016d37eca --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/edit.html @@ -0,0 +1,43 @@ +
+ + +
+ + + + + +

+ + + +
+ + + + + + + + + + + +
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/list.html b/src/Umbraco.Web.UI.Client/src/views/dictionary/list.html new file mode 100644 index 0000000000..82b088df49 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/list.html @@ -0,0 +1,46 @@ +
+ + + + + + + +
+
+
+
+
+ Name +
+
+ + + +
+
+
+
+
+
+
+ + +
+
+ +
+
+
+
+
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.controller.js index 10e04c0384..80fb69aa47 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.controller.js @@ -6,12 +6,13 @@ * @description * The controller for the doc type creation dialog */ -function DocumentTypesCreateController($scope, $location, navigationService, contentTypeResource, formHelper, appState, notificationsService, localizationService) { +function DocumentTypesCreateController($scope, $location, navigationService, contentTypeResource, formHelper, appState, notificationsService, localizationService, iconHelper) { $scope.model = { allowCreateFolder: $scope.dialogOptions.currentNode.parentId === null || $scope.dialogOptions.currentNode.nodeType === "container", folderName: "", creatingFolder: false, + creatingDoctypeCollection: false }; var disableTemplates = Umbraco.Sys.ServerVariables.features.disabledFeatures.disableTemplates; @@ -23,6 +24,10 @@ function DocumentTypesCreateController($scope, $location, navigationService, con $scope.model.creatingFolder = true; }; + $scope.showCreateDocTypeCollection = function () { + $scope.model.creatingDoctypeCollection = true; + }; + $scope.createContainer = function () { if (formHelper.submitForm({ scope: $scope, formCtrl: this.createFolderForm })) { @@ -52,6 +57,54 @@ function DocumentTypesCreateController($scope, $location, navigationService, con } }; + $scope.createCollection = function () { + + if (formHelper.submitForm({ scope: $scope, formCtrl: this.createDoctypeCollectionForm, statusMessage: "Creating Doctype Collection..." })) { + + // see if we can find matching icons + var collectionIcon = "icon-folders", collectionItemIcon = "icon-document"; + iconHelper.getIcons().then(function (icons) { + + for (var i = 0; i < icons.length; i++) { + // for matching we'll require a full match for collection, partial match for item + if (icons[i].substring(5) == $scope.model.collectionName.toLowerCase()) { + collectionIcon = icons[i]; + } else if (icons[i].substring(5).indexOf($scope.model.collectionItemName.toLowerCase()) > -1) { + collectionItemIcon = icons[i]; + } + } + + contentTypeResource.createCollection(node.id, $scope.model.collectionName, $scope.model.collectionItemName, collectionIcon, collectionItemIcon).then(function (collectionData) { + + navigationService.hideMenu(); + $location.search('create', null); + $location.search('notemplate', null); + + formHelper.resetForm({ + scope: $scope + }); + + var section = appState.getSectionState("currentSection"); + + // redirect to the item id + $location.path("/settings/documenttypes/edit/" + collectionData.itemId); + + }, function (err) { + + $scope.error = err; + + //show any notifications + if (angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } + }); + }); + } + + }; + // Disabling logic for creating document type with template if disableTemplates is set to true if (!disableTemplates) { $scope.createDocType = function () { diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.html index e5043be785..549ad0452b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.html @@ -1,6 +1,6 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/media/media.move.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/media.move.controller.js index fd0267859f..7480b0a8c0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/media.move.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/media/media.move.controller.js @@ -13,6 +13,12 @@ angular.module("umbraco").controller("Umbraco.Editors.Media.MoveController", $scope.treeModel.hideHeader = userData.startMediaIds.length > 0 && userData.startMediaIds.indexOf(-1) == -1; }); + function treeLoadedHandler(args) { + if (node && node.path) { + $scope.dialogTreeApi.syncTree({ path: node.path, activate: false }); + } + } + function nodeSelectHandler(args) { if(args && args.event) { @@ -39,6 +45,7 @@ angular.module("umbraco").controller("Umbraco.Editors.Media.MoveController", } $scope.onTreeInit = function () { + $scope.dialogTreeApi.callbacks.treeLoaded(treeLoadedHandler); $scope.dialogTreeApi.callbacks.treeNodeSelect(nodeSelectHandler); $scope.dialogTreeApi.callbacks.treeNodeExpanded(nodeExpandedHandler); } diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.controller.js index 2608ddf300..14b3a56c9b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.controller.js @@ -182,7 +182,7 @@ contentTypeHelper.generateModels().then(function (result) { - if (result.success) { + if (!result.lastError) { //re-check model status contentTypeHelper.checkModelsBuilderStatus().then(function (statusResult) { diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.controller.js b/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.controller.js index 758bc8cc1c..e424d58929 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.controller.js @@ -13,6 +13,7 @@ vm.addChild = addChild; vm.removeChild = removeChild; + vm.toggle = toggle; /* ---------- INIT ---------- */ @@ -67,6 +68,18 @@ $scope.model.allowedContentTypes.splice(selectedChildIndex, 1); } + /** + * Toggle the $scope.model.allowAsRoot value to either true or false + */ + function toggle(){ + if($scope.model.allowAsRoot){ + $scope.model.allowAsRoot = false; + return; + } + + $scope.model.allowAsRoot = true; + } + } angular.module("umbraco").controller("Umbraco.Editors.MediaType.PermissionsController", PermissionsController); diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.html b/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.html index d34b31c8ea..9fa2efe9fc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.html +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.html @@ -8,10 +8,12 @@
- + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.controller.js index 27fc36bc0c..947269a98a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.controller.js @@ -108,7 +108,7 @@ contentTypeHelper.generateModels().then(function (result) { - if (result.success) { + if (!result.lastError) { //re-check model status contentTypeHelper.checkModelsBuilderStatus().then(function (statusResult) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.controller.js index 1e7d9836f9..729d899439 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.controller.js @@ -16,6 +16,10 @@ function booleanEditorController($scope, $rootScope, assetsService) { setupViewModel(); + if( $scope.model && !$scope.model.value ) { + $scope.model.value = ($scope.renderModel.value === true) ? '1' : '0'; + } + //here we declare a special method which will be called whenever the value has changed from the server //this is instead of doing a watch on the model.value = faster $scope.model.onValueChanged = function (newVal, oldVal) { 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 919f0fee39..0543fc0c66 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 @@ -120,6 +120,7 @@ function contentPickerController($scope, entityResource, editorState, iconHelper entityType: entityType, filterCssClass: "not-allowed not-published", startNodeId: null, + currentNode: editorState ? editorState.current : null, callback: function (data) { if (angular.isArray(data)) { _.each(data, function (item, i) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js index 728c820ca9..efafb9066d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js @@ -106,7 +106,8 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop $scope.overlayMenu = { show: false, - style: {} + style: {}, + showFilter: false }; // helper to force the current form into the dirty state @@ -344,6 +345,8 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop $scope.currentNode = $scope.nodes[0]; } + $scope.overlayMenu.showFilter = $scope.scaffolds.length > 15; + inited = true; } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html index bc3e4547b9..eb7c98d016 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html @@ -47,8 +47,20 @@
+ + +
    -
  • +
  • {{scaffold.name}} diff --git a/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js index 8eb4488a9c..0f95f78adc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/templates/edit.controller.js @@ -543,9 +543,13 @@ var availableMasterTemplates = []; // filter out the current template and the selected master template - angular.forEach(vm.templates, function(template){ - if(template.alias !== vm.template.alias && template.alias !== vm.template.masterTemplateAlias) { - availableMasterTemplates.push(template); + angular.forEach(vm.templates, function (template) { + if (template.alias !== vm.template.alias && template.alias !== vm.template.masterTemplateAlias) { + var templatePathArray = template.path.split(','); + // filter descendant templates of current template + if (templatePathArray.indexOf(String(vm.template.id)) === -1) { + availableMasterTemplates.push(template); + } } }); diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index a65b9602ec..48ccef0e51 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -31,6 +31,8 @@ vm.disableUser = disableUser; vm.enableUser = enableUser; vm.unlockUser = unlockUser; + vm.resendInvite = resendInvite; + vm.deleteNonLoggedInUser = deleteNonLoggedInUser; vm.changeAvatar = changeAvatar; vm.clearAvatar = clearAvatar; vm.save = save; @@ -49,7 +51,9 @@ "sections_users", "content_contentRoot", "media_mediaRoot", - "user_noStartNodes" + "user_noStartNodes", + "user_defaultInvitationMessage", + "user_deleteUserConfirmation" ]; localizationService.localizeMany(labelKeys).then(function (values) { @@ -61,6 +65,8 @@ vm.labels.contentRoot = values[5]; vm.labels.mediaRoot = values[6]; vm.labels.noStartNodes = values[7]; + vm.labels.defaultInvitationMessage = values[8]; + vm.labels.deleteUserConfirmation = values[9]; }); // get user @@ -330,6 +336,44 @@ }); } + function resendInvite() { + vm.resendInviteButtonState = "busy"; + + if (vm.resendInviteMessage) { + vm.user.message = vm.resendInviteMessage; + } + else { + vm.user.message = vm.labels.defaultInvitationMessage; + } + + usersResource.inviteUser(vm.user).then(function (data) { + vm.resendInviteButtonState = "success"; + vm.resendInviteMessage = ""; + formHelper.showNotifications(data); + }, function (error) { + vm.resendInviteButtonState = "error"; + formHelper.showNotifications(error.data); + }); + } + + function deleteNonLoggedInUser() { + vm.deleteNotLoggedInUserButtonState = "busy"; + + var confirmationMessage = vm.labels.deleteUserConfirmation; + if (!confirm(confirmationMessage)) { + vm.deleteNotLoggedInUserButtonState = "danger"; + return; + } + + usersResource.deleteNonLoggedInUser(vm.user.id).then(function (data) { + formHelper.showNotifications(data); + goToPage(vm.breadcrumbs[0]); + }, function (error) { + vm.deleteNotLoggedInUserButtonState = "error"; + formHelper.showNotifications(error.data); + }); + } + function clearAvatar() { // get user usersResource.clearAvatar(vm.user.id).then(function (data) { diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html index 599389a9f0..a6ff881f7b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html @@ -8,7 +8,7 @@ - + @@ -250,7 +251,7 @@
    -
    - + + + +
    + + + +
    +
    Last login: diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 271d9eaf42..eb634e3f88 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -83,7 +83,7 @@ - + @@ -241,6 +241,7 @@ + diff --git a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml index 3b486e626a..a3fb34d4bf 100644 --- a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml @@ -47,7 +47,7 @@ @if (success) { @* This message will show if RedirectOnSucces is set to false (default) *@ -

    Registration succeeeded.

    +

    Registration succeeded.

    } else { @@ -101,4 +101,4 @@ else } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI/Umbraco/config/create/UI.Release.xml b/src/Umbraco.Web.UI/Umbraco/config/create/UI.Release.xml index b24cb65fea..52abf37133 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/create/UI.Release.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/create/UI.Release.xml @@ -1,229 +1,90 @@ - - -
    Template
    - /create/simple.ascx - - - - -
    - -
    Template
    - /create/simple.ascx - - - -
    - -
    Macro
    - /create/simple.ascx - - - -
    - -
    Macro
    - /create/simple.ascx - - - - -
    - -
    User
    - /create/user.ascx - - - - -
    - -
    User
    - /create/simple.ascx - - - -
    - -
    membergroup
    - /create/simple.ascx - - - -
    - -
    Stylesheet
    - /create/simple.ascx - - - -
    - -
    member
    - /create/member.ascx - - - -
    - -
    member
    - /create/member.ascx - - - -
    - -
    membergroup
    - /create/simple.ascx - - - -
    - -
    Stylesheet editor egenskab
    - /create/simple.ascx - - - - -
    - -
    Stylesheet editor egenskab
    - /create/simple.ascx - - - -
    - -
    Dictionary editor egenskab
    - /create/simple.ascx - - - - -
    - - - - - - -
    Scripting file
    - /create/DLRScripting.ascx - - - -
    - -
    Macro
    - /create/DLRScripting.ascx - - - -
    - -
    Scripting file
    - /create/DLRScripting.ascx - - - -
    - -
    Macro
    - /create/DLRScripting.ascx - - - -
    - -
    Script file
    - /create/script.ascx - - - -
    - -
    Script file
    - /create/script.ascx - - - - -
    - -
    Macro
    - /create/script.ascx - - - -
    - -
    Package
    - /create/simple.ascx - - - -
    - -
    Package
    - /create/simple.ascx - - - -
    - -
    Package
    - /create/simple.ascx - - - -
    - -
    Package
    - /create/simple.ascx - - - -
    - -
    User Types
    - /create/simple.ascx - - - - -
    - -
    Macro
    - /Create/PartialView.ascx - - - - -
    - -
    Macro
    - /Create/PartialViewMacro.ascx - - - - -
    - -
    Macro
    - /Create/PartialView.ascx - - - - -
    - -
    Macro
    - /Create/PartialViewMacro.ascx - - - - -
    + + +
    Macro
    + /create/simple.ascx + + + +
    + +
    Macro
    + /create/simple.ascx + + + + +
    + +
    membergroup
    + /create/simple.ascx + + + +
    + +
    membergroup
    + /create/simple.ascx + + + +
    + +
    Stylesheet
    + /create/simple.ascx + + + +
    + +
    Stylesheet
    + /create/simple.ascx + + + + +
    + +
    Stylesheet editor egenskab
    + /create/simple.ascx + + + + +
    + +
    Stylesheet editor egenskab
    + /create/simple.ascx + + + +
    + +
    Package
    + /create/simple.ascx + + + +
    + +
    Package
    + /create/simple.ascx + + + +
    + +
    Package
    + /create/simple.ascx + + + +
    + +
    Package
    + /create/simple.ascx + + + +
    diff --git a/src/Umbraco.Web.UI/Umbraco/config/create/UI.xml b/src/Umbraco.Web.UI/Umbraco/config/create/UI.xml index 370bc441bd..82adc71525 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/create/UI.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/create/UI.xml @@ -1,238 +1,90 @@  - - -
    Template
    - /create/simple.ascx - - - - -
    - -
    Template
    - /create/simple.ascx - - - -
    - -
    Macro
    - /create/simple.ascx - - - -
    - -
    Macro
    - /create/simple.ascx - - - - -
    - - -
    User
    - /create/user.ascx - - - - -
    - -
    User
    - /create/simple.ascx - - - -
    - -
    membergroup
    - /create/simple.ascx - - - -
    - -
    Stylesheet
    - /create/simple.ascx - - - -
    - -
    Stylesheet
    - /create/simple.ascx - - - - -
    - -
    membergroup
    - /create/simple.ascx - - - -
    - -
    Stylesheet editor egenskab
    - /create/simple.ascx - - - - -
    - -
    Stylesheet editor egenskab
    - /create/simple.ascx - - - -
    - -
    Dictionary editor egenskab
    - /create/simple.ascx - - - - -
    - - - - - - -
    Scripting file
    - /create/DLRScripting.ascx - - - -
    - -
    Macro
    - /create/DLRScripting.ascx - - - -
    - -
    Scripting file
    - /create/DLRScripting.ascx - - - -
    - -
    Macro
    - /create/DLRScripting.ascx - - - -
    - -
    Script file
    - /create/script.ascx - - - -
    - -
    Script file
    - /create/script.ascx - - - - -
    - -
    Macro
    - /create/script.ascx - - - -
    - -
    Package
    - /create/simple.ascx - - - -
    - -
    Package
    - /create/simple.ascx - - - -
    - -
    Package
    - /create/simple.ascx - - - -
    - -
    Package
    - /create/simple.ascx - - - -
    - -
    User Types
    - /create/simple.ascx - - - - -
    - -
    Macro
    - /Create/PartialView.ascx - - - - -
    - -
    Macro
    - /Create/PartialView.ascx - - - -
    - -
    Macro
    - /Create/PartialViewMacro.ascx - - - - -
    - -
    Macro
    - /Create/PartialViewMacro.ascx - - - -
    - -
    Macro
    - /Create/PartialView.ascx - - - - -
    - -
    Macro
    - /Create/PartialViewMacro.ascx - - - - -
    + + +
    Macro
    + /create/simple.ascx + + + +
    + +
    Macro
    + /create/simple.ascx + + + + +
    + +
    membergroup
    + /create/simple.ascx + + + +
    + +
    membergroup
    + /create/simple.ascx + + + +
    + +
    Stylesheet
    + /create/simple.ascx + + + +
    + +
    Stylesheet
    + /create/simple.ascx + + + + +
    + +
    Stylesheet editor egenskab
    + /create/simple.ascx + + + + +
    + +
    Stylesheet editor egenskab
    + /create/simple.ascx + + + +
    + +
    Package
    + /create/simple.ascx + + + +
    + +
    Package
    + /create/simple.ascx + + + +
    + +
    Package
    + /create/simple.ascx + + + +
    + +
    Package
    + /create/simple.ascx + + + +
    diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 2614c3d6f5..5d1746466b 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -97,9 +97,6 @@ Domæner - - For - Ryd valg Vælg @@ -128,8 +125,8 @@ Gem og planlæg Gem og send til udgivelse Gem listevisning - Se siden - Preview er deaktiveret fordi der ikke er nogen skabelon tildelt + Forhåndsvisning + Forhåndsvisning er deaktiveret fordi der ikke er nogen skabelon tildelt Vælg formattering Vis koder Indsæt tabel @@ -138,6 +135,27 @@ Fortryd Genskab + + For + Brugeren har slettet indholdet + Brugeren har afpubliceret indholdet + Brugeren har gemt og udgivet indholdet + Brugeren har gemt indholdet + Brugeren har flyttet indholdet + Brugeren har kopieret indholdet + Brugeren har tilbagerullet indholdet til en tidligere tilstand + Brugeren har sendt indholdet til udgivelse + Brugeren har sendt indholdet til oversættelse + Kopieret + Udgivet + Flyttet + Gemt + Slettet + Afpubliceret + Indhold tilbagerullet + Sendt til udgivelse + Sendt til oversættelse + For at skifte det valgte indholds dokumenttype, skal du først vælge en ny dokumenttype, som er gyldig på denne placering. Kontroller derefter, at alle egenskaber bliver overført rigtigt til den nye dokumenttype, og klik derefter på Gem. @@ -193,7 +211,10 @@ Upd: dette dokument er udgiver, men er ikke i cachen (intern fejl) Kunne ikke hente url'en Dette dokument er udgivet, men dets url ville kollidere med indholdet %0% - Udgivet + Udgiv + Udgivet + Udgivet (Ventede ændringer) + Udgivelsesstatus Udgivelsesdato Afpubliceringsdato @@ -206,6 +227,7 @@ Alternativ tekst (valgfri) Type Afpublicér + Afpubliceret Sidst redigeret Tidspunkt for seneste redigering Fjern fil @@ -379,6 +401,7 @@ Vælg brugere Ingen ikoner blev fundet Der er ingen parametre for denne makro + Der er ikke tilføjet nogen makroer Link dit Fjern link fra dit konto @@ -645,6 +668,16 @@ Brug listevisning Tillad på rodniveau + + Comment/Uncomment lines + Remove line + Copy Lines Up + Copy Lines Down + Move Lines Up + Move Lines Down + + General + Editor @@ -752,6 +785,8 @@ Glemt adgangskode? En e-mail vil blive sendt til den angivne adresse med et link til at nulstille din adgangskode En e-mail med instruktioner for nulstilling af adgangskoden vil blive sendt til den angivne adresse, hvis det matcher vores optegnelser + Vis adgangskode + Skjul adgangskode Tilbage til login formular Angiv en ny adgangskode Din adgangskode er blevet opdateret @@ -888,7 +923,7 @@ Mange hilsner fra Umbraco robotten Rollebaseret beskyttelse Hvis du ønsker at kontrollere adgang til siden ved hjælp af rollebaseret godkendelse via Umbracos medlemsgrupper. - rollebaseret godkendelse]]> + Du skal oprette en medlemsgruppe før du kan bruge rollebaseret godkendelse Fejlside Brugt når folk er logget ind, men ingen adgang Vælg hvordan siden skal beskyttes @@ -1093,31 +1128,131 @@ Mange hilsner fra Umbraco robotten Forhåndsvisning 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 + Rich Text Editor - Image + Billede Macro Embed - Headline - Quote + Overskrift + Citat Vælg indholdstype Vælg layout Tilføj række Tilføj indhold Slip indhold Indstillinger tilføjet - + Indholdet er ikke tilladt her Indholdet er tilladt her @@ -1126,9 +1261,9 @@ Mange hilsner fra Umbraco robotten Billedtekst... Skriv her... - Gitterlayout - Et layout er det overordnede arbejdsområde til dit gitter - du vil typisk kun behøve ét eller to - Tilføj gitterlayout + Grid layout + Et layout er det overordnede arbejdsområde til dit grid - du vil typisk kun behøve ét eller to + Tilføj grid layout Juster dit layout ved at justere kolonnebredder og tilføj yderligere sektioner Rækkekonfigurationer @@ -1137,7 +1272,7 @@ Mange hilsner fra Umbraco robotten Juster rækken ved at indstille cellebredder og tilføje yderligere celler Kolonner - Det totale antaller kolonner i gitteret + Det totale antal kolonner i dit grid Indstillinger Konfigurer, hvilket indstillinger, brugeren kan ændre @@ -1239,10 +1374,10 @@ Mange hilsner fra Umbraco robotten Fjern paragraf-tags Fjerner eventuelle &lt;P&gt; omkring teksten Standard felter - Uppercase + Store bogstaver URL encode Hvis indholdet af felterne skal sendes til en url, skal denne slåes til så specialtegn formateres - Denne tekst vil blive brugt hvis ovenstående felter er tomme + Denne tekst bruges hvis ovenstående felter er tomme Dette felt vil blive brugt hvis ovenstående felt er tomt Ja, med klokkeslæt. Dato/tid separator: @@ -1298,6 +1433,8 @@ Mange hilsner fra Umbraco robotten Relationstyper Pakker Pakker + Partial Views + Partial View Makro Filer Python Installer fra "repository" Installer Runway @@ -1425,7 +1562,7 @@ Mange hilsner fra Umbraco robotten - Validation + Validering Valider som e-mail Valider som tal Valider som Url @@ -1460,4 +1597,7 @@ Mange hilsner fra Umbraco robotten Ingen ordbog elementer at vælge imellem - + + Karakterer tilbage + + \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 39e578df45..382cb29d9f 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -34,7 +34,7 @@ Restore Set permissions for the page %0% Choose where to move - to in the tree structure below + In the tree structure below Permissions Rollback Send To Publish @@ -46,7 +46,34 @@ Update Set permissions Unlock - Default value + Create Content Template + Resend Invitation + + + Content + Administration + Structure + Other + + + Allow access to assign culture and hostnames + Allow access to view a node's history log + Allow access to view a node + Allow access to change document type for a node + Allow access to copy a node + Allow access to create nodes + Allow access to delete nodes + Allow access to move a node + Allow access to set and change public access for a node + Allow access to publish a node + Allow access to change permissions for a node + Allow access to roll back a node to a previous state + Allow access to send a node for approval before publishing + Allow access to send a node for translation + Allow access to change the sort order for nodes + Allow access to translate a node + Allow access to save a node + Allow access to create a Content Template Permission denied. @@ -62,18 +89,19 @@ Domain '%0%' has already been assigned Domain '%0%' has been updated Edit Current Domains - + + should be avoided. Better use the culture setting above.]]> + Inherit Culture - or inherit culture from parent nodes. Will also apply
    - to the current node, unless a domain below applies too.]]>
    + + or inherit culture from parent nodes. Will also apply
    + to the current node, unless a domain below applies too.]]> +
    Domains - - Viewing for - Clear selection Select @@ -98,7 +126,7 @@ Edit relations Return to list Save - Publish + Save and publish Save and schedule Save and send for approval Save list view @@ -109,6 +137,29 @@ Insert table Generate models Save and generate models + Undo + Redo + + + Viewing for + Delete Content performed by user + UnPublish performed by user + Save and Publish performed by user + Save Content performed by user + Move Content performed by user + Copy Content performed by user + Content rollback performed by user + Content Send To Publish performed by user + Content Send To Translation performed by user + Copy + Publish + Move + Save + Delete + Unpublish + Rollback + Send To Publish + Send To Translation To change the document type for the selected content, first select from the list of valid types for this location. @@ -147,6 +198,8 @@ Remove at This item has been changed after publication This item is not published + Culture '%0%' for this item is not published + Culture '%0%' for this item is not available Last published There are no items to show There are no items to show in the list. @@ -163,9 +216,11 @@ This media item has no link Properties This document is published but is not visible because the parent '%0%' is unpublished + This culture is published but is not visible because it is unpublished on parent '%0%' This document is published but is not in the cache Could not get the url This document is published but its url would collide with content %0% + This document is published but its url cannot be routed Publish Published Published (pending changes) @@ -192,9 +247,23 @@ Target This translates to the following time on the server: What does this mean?
    ]]> + Are you sure you want to delete this item? + Property %0% uses editor %1% which is not supported by Nested Content. + Add another text box + Remove this text box + Content root This value is hidden. If you need access to view this value please contact your website administrator. This value is hidden. + + Create a new Content Template from '%0%' + Blank + Select a Content Template + Content Template created + A Content Template was created from '%0%' + Another Content Template with the same name already exists + A Content Template is pre-defined content that an editor can select to use as the basis for creating new content + Click to upload Drop your files here... @@ -203,6 +272,7 @@ Only allowed file types are Cannot upload this file, it does not have an approved file type Max file size is + Media root Create a new member @@ -219,6 +289,13 @@ Document Type without a template New folder New data type + New javascript file + New empty partial view + New partial view macro + New partial view from snippet + New empty partial view macro + New partial view macro from snippet + New partial view macro (without macro) Browse your website @@ -234,6 +311,7 @@ Discard changes You have unsaved changes Are you sure you want to navigate away from this page? - you have unsaved changes + Unpublishing will remove this page and all its descendants from the site. Done @@ -308,10 +386,14 @@ The website cache will be refreshed. All published content will be updated, while unpublished content will stay unpublished. Number of columns Number of rows - Set a placeholder id by setting an ID on your placeholder you can inject content into this template from child templates, - by referring this ID using a <asp:content /> element.]]> - Select a placeholder id from the list below. You can only - choose Id's from the current template's master.]]> + + Set a placeholder id by setting an ID on your placeholder you can inject content into this template from child templates, + by referring this ID using a <asp:content /> element.]]> + + + Select a placeholder id from the list below. You can only + choose Id's from the current template's master.]]> + Click on the image to see full size Pick item View Cache Item @@ -322,6 +404,7 @@ Link to page Opens the linked document in a new window or tab Link to media + Link to file Select content start node Select media Select icon @@ -337,6 +420,7 @@ Select users No icons were found There are no parameters for this macro + There are no macros available to insert External login providers Exception Details Stacktrace @@ -345,11 +429,14 @@ Un-link your account Select editor + Select snippet - + %0%' below
    You can add additional languages under the 'languages' in the menu on the left - ]]>
    + ]]> + Culture Name Edit the key of the dictionary item. @@ -357,6 +444,7 @@ The key '%0%' already exists. ]]> + Dictionary overview Enter your username @@ -371,7 +459,7 @@ Type to search... Type to filter... Type to add tags (press enter after each tag)... - Enter your email + Enter your email... Enter a message... Your username is usually your email @@ -410,6 +498,13 @@ Related stylesheets Show label Width and height + All property types & property data + using this data type will be deleted permanently, please confirm you want to delete these as well + Yes, delete + and all property types & property data using this data type + Select the folder to move + to in the tree structure below + was moved underneath Your data has been saved, but before you can publish this page there are some errors you need to fix first: @@ -465,6 +560,7 @@ Close Window Comment Confirm + Constrain Constrain proportions Continue Copy @@ -476,6 +572,7 @@ Deleted Deleting... Design + Dictionary Dimensions Down Download @@ -485,6 +582,7 @@ Email Error Find + First General Groups Height @@ -501,6 +599,7 @@ Justify Label Language + Last Layout Links Loading @@ -534,13 +633,17 @@ Recycle Bin Your recycle bin is empty Remaining + Remove Rename Renew Required + Retrieve Retry Permissions Scheduled Publishing Search + Sorry, we can not find what you are looking for + No items have been added Server Settings Show @@ -618,6 +721,15 @@ Toggle list view Toggle allow as root + Comment/Uncomment lines + Remove line + Copy Lines Up + Copy Lines Down + Move Lines Up + Move Lines Down + + General + Editor Background colour @@ -634,28 +746,37 @@ Could not save the web.config file. Please modify the connection string manually. Your database has been found and is identified as Database configuration - + install button to install the Umbraco %0% database - ]]> + ]]> + Next to proceed.]]> - Database not found! Please check that the information in the "connection string" of the "web.config" file is correct.

    + + Database not found! Please check that the information in the "connection string" of the "web.config" file is correct.

    To proceed, please edit the "web.config" file (using Visual Studio or your favourite text editor), scroll to the bottom, add the connection string for your database in the key named "UmbracoDbDSN" and save the file.

    Click the retry button when done.
    - More information on editing web.config here.

    ]]>
    - + More information on editing web.config here.

    ]]> +
    + + Please contact your ISP if necessary. - If you're installing on a local machine or server you might need information from your system administrator.]]> - + + + Press the upgrade button to upgrade your database to Umbraco %0%

    Don't worry - no content will be deleted and everything will continue working afterwards!

    + ]]>
    - Press Next to proceed. ]]> + Press Next to + proceed. ]]> next to continue the configuration wizard]]> The Default users' password needs to be changed!]]> @@ -668,41 +789,56 @@ Affected files and folders More information on setting up permissions for Umbraco here You need to grant ASP.NET modify permissions to the following files/folders - Your permission settings are almost perfect!

    - You can run Umbraco without problems, but you will not be able to install packages which are recommended to take full advantage of Umbraco.]]>
    + + Your permission settings are almost perfect!

    + You can run Umbraco without problems, but you will not be able to install packages which are recommended to take full advantage of Umbraco.]]> +
    How to Resolve Click here to read the text version video tutorial on setting up folder permissions for Umbraco or read the text version.]]> - Your permission settings might be an issue! + + Your permission settings might be an issue!

    - You can run Umbraco without problems, but you will not be able to create folders or install packages which are recommended to take full advantage of Umbraco.]]>
    - Your permission settings are not ready for Umbraco! + You can run Umbraco without problems, but you will not be able to create folders or install packages which are recommended to take full advantage of Umbraco.]]> + + + Your permission settings are not ready for Umbraco!

    - In order to run Umbraco, you'll need to update your permission settings.]]>
    - Your permission settings are perfect!

    - You are ready to run Umbraco and install packages!]]>
    + In order to run Umbraco, you'll need to update your permission settings.]]> +
    + + Your permission settings are perfect!

    + You are ready to run Umbraco and install packages!]]> +
    Resolving folder issue Follow this link for more information on problems with ASP.NET and creating folders Setting up folder permissions - + + ]]> +
    I want to start from scratch - + learn how) You can still choose to install Runway later on. Please go to the Developer section and choose Packages. - ]]> + ]]> + You've just set up a clean Umbraco platform. What do you want to do next? Runway is installed - + This is our list of recommended modules, check off the ones you would like to install, or view the full list of modules - ]]> + ]]> + Only recommended for experienced users I want to start with a simple website - + "Runway" is a simple website providing some basic document types and templates. The installer can set up Runway for you automatically, but you can easily edit, extend or remove it. It's not necessary and you can perfectly use Umbraco without it. However, @@ -713,7 +849,8 @@ Included with Runway: Home page, Getting Started page, Installing Modules page.
    Optional Modules: Top Navigation, Sitemap, Contact, Gallery. - ]]>
    + ]]> + What is Runway Step 1/5 Accept license Step 2/5: Database configuration @@ -721,24 +858,36 @@ Step 4/5: Check Umbraco security Step 5/5: Umbraco is ready to get you started Thank you for choosing Umbraco - Browse your new site -You installed Runway, so why not see how your new website looks.]]> - Further help and information -Get help from our award winning community, browse the documentation or watch some free videos on how to build a simple site, how to use packages and a quick guide to the Umbraco terminology]]> + + Browse your new site +You installed Runway, so why not see how your new website looks.]]> + + + Further help and information +Get help from our award winning community, browse the documentation or watch some free videos on how to build a simple site, how to use packages and a quick guide to the Umbraco terminology]]> + Umbraco %0% is installed and ready for use - /web.config file and update the AppSetting key UmbracoConfigurationStatus in the bottom to the value of '%0%'.]]> - started instantly by clicking the "Launch Umbraco" button below.
    If you are new to Umbraco, -you can find plenty of resources on our getting started pages.]]>
    - Launch Umbraco -To manage your website, simply open the Umbraco back office and start adding content, updating the templates and stylesheets or add new functionality]]> + + /web.config file and update the AppSetting key UmbracoConfigurationStatus in the bottom to the value of '%0%'.]]> + + + started instantly by clicking the "Launch Umbraco" button below.
    If you are new to Umbraco, +you can find plenty of resources on our getting started pages.]]> +
    + + Launch Umbraco +To manage your website, simply open the Umbraco back office and start adding content, updating the templates and stylesheets or add new functionality]]> + Connection to database failed. Umbraco Version 3 Umbraco Version 4 Watch - Umbraco %0% for a fresh install or upgrading from version 3.0. + + Umbraco %0% for a fresh install or upgrading from version 3.0.

    - Press "next" to start the wizard.]]>
    + Press "next" to start the wizard.]]> +
    Culture Code @@ -763,6 +912,8 @@ To manage your website, simply open the Umbraco back office and start adding con Forgotten password? An email will be sent to the address specified with a link to reset your password An email with password reset instructions will be sent to the specified address if it matched our records + Show password + Hide password Return to login form Please provide a new password Your Password has been updated @@ -873,7 +1024,8 @@ To manage your website, simply open the Umbraco back office and start adding con Edit your notification for %0% - + An error occurred while unlocking the user Member was exported to file An error occurred while exporting the member + User %0% was deleted + Invite user + Invitation has been re-sent to %0% Uses CSS syntax ex: h1, .redHeader, .blueTex @@ -1519,7 +1674,7 @@ To manage your website, simply open the Umbraco back office and start adding con Hide this property value from content editors that don't have access to view sensitive information Show on member profile Allow this property value to be displayed on the member profile page - + tab has no sort order Where is this composition used? @@ -1867,6 +2022,9 @@ To manage your website, simply open the Umbraco back office and start adding con ]]> Invite + Resending invitation... + Delete User + Are you sure you wish to delete this user account? @@ -1927,6 +2085,10 @@ To manage your website, simply open the Umbraco back office and start adding con Media - Total XML: %0%, Total: %1%, Total invalid: %2% Content - Total XML: %0%, Total published: %1%, Total invalid: %2% + Database - The database schema is correct for this version of Umbraco + %0% problems were detected with your database schema (Check the log for details) + Some errors were detected while validating the database schema against the current version of Umbraco. + Your website's certificate is valid. Certificate validation error: '%0%' Your website's SSL certificate has expired. 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 28b8f65dd3..89f9c7df00 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -46,6 +46,7 @@ Set permissions Unlock Create Content Template + Resend Invitation Content @@ -100,9 +101,6 @@ Domains - - Viewing for - Clear selection Select @@ -128,8 +126,9 @@ Edit relations Return to list Save + Save and close - Publish + Save and publish Publish… Save and schedule Save and send for approval @@ -145,6 +144,28 @@ Undo Redo + + Viewing for + Delete Content performed by user + UnPublish performed by user + Save and Publish performed by user + Save Content performed by user + Move Content performed by user + Copy Content performed by user + Content rollback performed by user + Content Send To Publish performed by user + Content Send To Translation performed by user + Copy + Publish + Move + Save + Delete + Unpublish + Rollback + Send To Publish + Send To Translation + + To change the document type for the selected content, first select from the list of valid types for this location. Then confirm and/or amend the mapping of properties from the current type to the new, and click Save. @@ -182,6 +203,8 @@ Remove at This item has been changed after publication This item is not published + Culture '%0%' for this item is not published + Culture '%0%' for this item is not available Last published There are no items to show There are no items to show in the list. @@ -198,9 +221,11 @@ This media item has no link Properties This document is published but is not visible because the parent '%0%' is unpublished + This culture is published but is not visible because it is unpublished on parent '%0%' This document is published but is not in the cache Could not get the url This document is published but its url would collide with content %0% + This document is published but its url cannot be routed Publish Published Published (pending changes) @@ -294,6 +319,7 @@ Discard changes You have unsaved changes Are you sure you want to navigate away from this page? - you have unsaved changes + Unpublishing will remove this page and all its descendants from the site. Done @@ -426,6 +452,7 @@ The key '%0%' already exists. ]]> + Dictionary overview Enter your username @@ -891,6 +918,8 @@ To manage your website, simply open the Umbraco back office and start adding con Forgotten password? An email will be sent to the address specified with a link to reset your password An email with password reset instructions will be sent to the specified address if it matched our records + Show password + Hide password Return to login form Please provide a new password Your Password has been updated @@ -1418,6 +1447,9 @@ To manage your website, simply open the Umbraco back office and start adding con An error occurred while unlocking the user Member was exported to file An error occurred while exporting the member + User %0% was deleted + Invite user + Invitation has been re-sent to %0% Cannot publish the document since the required '%0%' is not published Validation failed for language '%0%' Unexpected validation failed for language '%0%' @@ -1647,7 +1679,7 @@ To manage your website, simply open the Umbraco back office and start adding con Hide this property value from content editors that don't have access to view sensitive information Show on member profile Allow this property value to be displayed on the member profile page - + tab has no sort order Where is this composition used? @@ -2004,6 +2036,9 @@ To manage your website, simply open the Umbraco back office and start adding con ]]> Invite + Resending invitation... + Delete User + Are you sure you wish to delete this user account? Validation @@ -2063,6 +2098,10 @@ To manage your website, simply open the Umbraco back office and start adding con Media - Total XML: %0%, Total: %1%, Total invalid: %2% Content - Total XML: %0%, Total published: %1%, Total invalid: %2% + Database - The database schema is correct for this version of Umbraco + %0% problems were detected with your database schema (Check the log for details) + Some errors were detected while validating the database schema against the current version of Umbraco. + Your website's certificate is valid. Certificate validation error: '%0%' Your website's SSL certificate has expired. diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/it.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/it.xml index 0e856a286c..cd2eeb2a51 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/it.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/it.xml @@ -128,38 +128,38 @@ Benvenuto - Stay - Discard changes - You have unsaved changes - Are you sure you want to navigate away from this page? - you have unsaved changes + Rimani + Scarta le modifiche + Hai delle modifiche non salvate + Sei sicuro di voler lasciare questa pagina? - hai delle modifiche non salvate - Done + Fatto - Deleted %0% item - Deleted %0% items - Deleted %0% out of %1% item - Deleted %0% out of %1% items + Elimianto %0% elemento + Elimianto %0% elementi + Eliminato %0% su %1% elemento + Eliminato %0% su %1% elementi - Published %0% item - Published %0% items - Published %0% out of %1% item - Published %0% out of %1% items + Pubblicato %0% elemento + Pubblicato %0% elementi + Pubblicato %0% su %1% elemento + Pubblicato %0% su %1% elementi - Unpublished %0% item - Unpublished %0% items - Unpublished %0% out of %1% item - Unpublished %0% out of %1% items + %0% elemento non pubblicato + %0% elementi non pubblicati + Elementi non pubblicati - %0% su %1% + Elementi non pubblicati - %0% su %1% - Moved %0% item - Moved %0% items - Moved %0% out of %1% item - Moved %0% out of %1% items + Spostato %0% elemento + Spsotato %0% elementi + Spostato %0% su %1% elemento + Spostato %0% su %1% elementi - Copied %0% item - Copied %0% items - Copied %0% out of %1% item - Copied %0% out of %1% items + Copiato %0% elemento + Copiato %0% elementi + Copiato %0% su %1% elemento + Copiato %0% su %1% elementi Titolo del Link @@ -571,7 +571,7 @@ Per gestire il tuo sito web, è sufficiente aprire il back office di Umbraco e i usando i gruppi di membri di Umbraco.]]> - l'autenticazione basata sui ruoli.]]> + Devi creare un gruppo di membri prima di utilizzare l'autenticazione basata sui ruoli @@ -732,43 +732,43 @@ Per gestire il tuo sito web, è sufficiente aprire il back office di Umbraco e i Embed Headline Quote - Choose type of content - Choose a layout - Add a row - Add content - Drop content - Settings applied + Seleziona il tipo di contenuto + Seleziona un layout + Aggiungi una riga + Aggiungi contenuto + Elimina contenuto + Impostazioni applicati - This content is not allowed here - This content is allowed here + Questo contenuto non è consentito qui + Questo contenuto è consentito qui - Click to embed - Click to insert image - Image caption... - Write here... + Clicca per incorporare + Clicca per inserire l'immagine + Didascalia dell'immagine... + Scrivi qui... - Grid Layouts - Layouts are the overall work area for the grid editor, usually you only need one or two different layouts - Add Grid Layout - Adjust the layout by setting column widths and adding additional sections - Row configurations - Rows are predefined cells arranged horizontally - Add row configuration - Adjust the row by setting cell widths and adding additional cells + I Grid Layout + I layout sono l'area globale di lavoro per il grid editor, di solito ti serve solo uno o due layout differenti + Aggiungi un Grid Layout + Sistema il layout impostando la larghezza della colonna ed aggiungendo ulteriori sezioni + Configurazioni della riga + Le righe sono le colonne predefinite disposte orizzontalmente + Aggiungi configurazione della riga + Sistema la riga impostando la larghezza della colonna ed aggiungendo ulteriori colonne - Columns - Total combined number of columns in the grid layout + Colonne + Totale combinazioni delle colonne nel grid layout - Settings - Configure what settings editors can change + Impostazioni + Configura le impostazioni che possono essere cambiate dai editori - Styles - Configure what styling editors can change + Stili + Configura i stili che possono essere cambiati dai editori - Settings will only save if the entered json configuration is valid + Le impostazioni verranno salvate soltanto se è valido il json inserito - Allow all editors - Allow all row configurations + Permetti tutti i editor + Permetti tutte le configurazioni della riga diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/nl.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/nl.xml index d0d90216c7..72ece3d46e 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/nl.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/nl.xml @@ -172,6 +172,13 @@ Doel Dit betekend de volgende tijd op de server: Wat houd dit in?]]> + Ben je er zeker van dat je dit item wilt verwijderen? + Eigenschap %0% gebruikt editor %1% welke niet wordt ondersteund door Nested Content. + Voeg nog een tekstvak toe + Verwijder dit tekstvak + Content root + Deze waarde is verborgen. Indien u toegang nodig heeft om deze waarde te bekijken, contacteer dan uw website administrator. + Deze waarde is verborgen Klik om te uploaden @@ -214,6 +221,7 @@ Negeer wijzigingen Wijzigingen niet opgeslagen Weet je zeker dat deze pagina wilt verlaten? - er zijn onopgeslagen wijzigingen + Depubliceren zal deze pagina en alle onderliggend paginas verwijderen van de site. Done @@ -1137,7 +1145,12 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je die gebruik maken van deze editor zullen geupdate worden met deze nieuwe instellingen Lid kan bewerken + Toestaan dat deze eigenschap kan worden gewijzigd door het lid op zijn profiel pagina. + Omvat gevoelige gegevens + Verberg deze eigenschap voor de content editor die geen toegang heeft tot het bekijken van gevoelige informatie. Toon in het profiel van leden + Toelaten dat deze eigenschap wordt getoond op de profiel pagina van het lid. + tab heeft geen sorteervolgorde diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/ru.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/ru.xml index 8f9fa63244..36a4a8fa01 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/ru.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/ru.xml @@ -19,6 +19,7 @@ Отключить Очистить корзину Включить + Экспорт Экспортировать Импортировать Импортировать пакет @@ -158,8 +159,9 @@ Вернуться к списку Сохранить Сохранить и построить модели - Сохранить и опубликовать - Сохранить и направить на публикацию + Опубликовать + Запланировать + Направить на публикацию Сохранить список Выбрать Выбрать текущую папку @@ -201,7 +203,20 @@ Желтый Оранжевый Синий + Серо-синий + Серый + Коричневый + Светло-синий + Голубой + Светло-зеленый + Лайм + Янтарный + Рыжий Красный + Розовый + Лиловый + Темно-лиловый + Индиго Об этой странице @@ -221,6 +236,8 @@ Скрыть ВНИМАНИЕ: невозможно получить URL документа (внутренняя ошибка - подробности в системном журнале) Опубликовано + Это значение скрыто. Если Вам нужен доступ к просмотру этого значения, свяжитесь с администратором веб-сайта. + Это значение скрыто. Этот документ был изменен после публикации Этот документ не опубликован Документ опубликован @@ -236,19 +253,24 @@ Тип участника Вы уверены, что хотите удалить этот элемент? Свойство '%0%' использует редактор '%1%', который не поддерживается для вложенного содержимого. + Не было сделано никаких изменений Дата не указана Заголовок страницы + Этот медиа-элемент не содержит ссылки Доступные группы Свойства Этот документ опубликован, но скрыт, потому что его родительский документ '%0%' не опубликован ВНИМАНИЕ: этот документ опубликован, но его нет в глобальном кэше (внутренняя ошибка - подробности в системном журнале) Опубликовать + Опубликовано + Опубликовано (есть измененения) Состояние публикации Опубликовать Очистить дату ВНИМАНИЕ: этот документ опубликован, но его URL вступает в противоречие с документом %0% Это время будет соответствовать следующему времени на сервере: Что это означает?]]> + Задать дату Порядок сортировки обновлен Для сортировки узлов просто перетаскивайте узлы или нажмите на заголовке столбца. Вы можете выбрать несколько узлов, удерживая клавиши "shift" или "ctrl" при пометке Статистика @@ -256,6 +278,7 @@ Заголовок (необязательно) Тип Скрыть + Распубликовано Распубликовать Последняя правка Дата/время редактирования документа @@ -265,6 +288,10 @@ Добавить новое поле текста Удалить это поле текста + + Выбран элемент содержимого, который в настоящее время удален или находится в корзине + Выбраны элементы содержимого, которые в настоящее время удалены или находятся в корзине + Композиции Вы не добавили ни одной вкладки @@ -289,6 +316,8 @@ Выбрать дочерний узел Унаследовать вкладки и свойства из уже существующего типа документов. Вкладки будут либо добавлены в создаваемый тип, либо в случае совпадения названий вкладок будут добавлены наследуемые свойства. Этот тип документов уже участвует в композиции другого типа, поэтому сам не может быть композицией. + Где используется эта композиция? + Эта композиция сейчас используется при создании следующих типов документов: В настоящее время нет типов документов, допустимых для построения композиции. Доступные редакторы @@ -320,7 +349,11 @@ , использующие этот редактор, будут обновлены с применением этих установок Участник может изменить + Разрешает редактирование значение данного свойства участником в своем профиле + Конфеденциальные данные + Скрывает значение это свойства от редакторов содержимого, не имеющих доступа к конфеденциальной информации Показать в профиле участника + Разрешает показ данного свойства в профиле участника для вкладки не указан порядок сортировки @@ -447,6 +480,7 @@ Ключ '%0%' уже существует в словаре. ]]> + Обзор словаря Допустим как корневой @@ -565,13 +599,16 @@ Ошибка Найти Начало + Общее Группы Папка Высота Справка Скрыть + История Иконка Импорт + Инфо Внутренний отступ Вставить Установить @@ -581,6 +618,7 @@ Язык Конец Макет + Ссылки Загрузка БЛОКИРОВКА Войти @@ -601,6 +639,7 @@ Ok Открыть Вкл + Варианты или Сортировка по Пароль @@ -620,6 +659,7 @@ Получить Повторить Разрешения + Публикация по расписанию Поиск К сожалению, ничего подходящего не нашлось Результаты поиска @@ -653,7 +693,8 @@ Сохранение... текущий выбрано - Внедрить + Встроить + Получить Цвет фона @@ -663,12 +704,12 @@ Текст - Rich Text Editor - Image - Macro - Embed - Headline - Quote + Редактор текста + Изображение + Макрос + Встраивание + Заголовок + Цитата Добавить содержимое Сбросить содержимое Добавить шаблон сетки @@ -750,9 +791,10 @@ Медиа - всего в XML: %0%, всего: %1%Б с ошибками: %2% Содержимое - всего в XML: %0%, всего опубликовано: %1%, с ошибками: %2% + Ошибка проверки адреса URL %0% - '%1%' + Сертификат Вашего веб-сайта отмечен как проверенный. Ошибка проверки сертификата: '%0%' - Ошибка проверки адреса URL %0% - '%1%' Сейчас Вы %0% просматриваете сайт, используя протокол HTTPS. Параметр 'umbracoUseSSL' в секции 'appSetting' установлен в 'false' в файле web.config. Если Вам необходим доступ к сайту по протоколу HTTPS, нужно установить данный параметр в 'true'. Параметр 'umbracoUseSSL' в секции 'appSetting' в файле установлен в '%0%', значения cookies %1% маркированы как безопасные. @@ -792,11 +834,27 @@ X-Frame-Options, использующийся для управления возможностью помещать сайт в IFRAME на другом сайте.]]> X-Frame-Options, использующийся для управления возможностью помещать сайт в IFRAME на другом сайте, не обнаружен.]]> - Установить заголовок в файле конфигурации Добавляет значение в секцию 'httpProtocol/customHeaders' файла web.config, препятствующее возможному использованию этого сайта внутри IFRAME на другом сайте. Значение, добавляющее заголовок, препятствующий использованию этого сайта внутри IFRAME другого сайта, успешно добавлено в файл web.config. + + Установить заголовок в файле конфигурации Невозможно обновить файл web.config. Ошибка: %0% + X-Content-Type-Options, использующиеся для защиты от MIME-уязвимостей, обнаружены.]]> + X-Content-Type-Options, использующиеся для защиты от MIME-уязвимостей, не найдены.]]> + Добавляет значение в секцию httpProtocol/customHeaders файла web.config, препятствующее использованию MIME-уязвимостей. + Значение, добавляющее заголовок, препятствующий использованию MIME-уязвимостей, успешно добавлено в файл web.config. + + Strict-Transport-Security, известный также как HSTS-header, обнаружен.]]> + Strict-Transport-Security не найден.]]> + Добавляет заголовок 'Strict-Transport-Security' и его значение 'max-age=10886400; preload' в секцию httpProtocol/customHeaders файла web.config. Применяйте этот способ только в случае, если доступ к Вашим сайтам будет осуществляться по протоколу https как минимум ближайшие 18 недель. + Заголовок HSTS-header успешно добавлен в файл web.config. + + X-XSS-Protection обнаружен.]]> + X-XSS-Protection не найден.]]> + Добавляет заголовок 'X-XSS-Protection' и его значение '1; mode=block' в секцию httpProtocol/customHeaders файла web.config. + Заголовок X-XSS-Protection успешно добавлен в файл web.config. + @@ -966,7 +1024,87 @@ Ссылка, по которой Вы попали сюда, неверна или устарела Umbraco: сброс пароля -

    Ваше имя пользователя для входа в панель администрирования Umbraco: %0%

    Перейдите по этой ссылке для того, чтобы сбросить Ваш пароль, или скопируйте текст ссылки и вставьте в адресную строку своего браузера:

    %1%

    ]]> + + + + + + + + + + + +
    + + + + + +
    + +
    + +
    +
    + + + + + + +
    +
    +
    + + + + +
    + + + + +
    +

    + Запрошен сброс пароля +

    +

    + Ваше имя пользователя для входа в административную панель Umbraco: %0% +

    +

    + + + + + + +
    + + Нажмите на эту ссылку для того, чтобы сбросить пароль + +
    +

    +

    Если Вы не имеете возможности нажать на сслыку, скопируйте следующий адрес (URL) и вставьте в адресную строку Вашего браузера:

    + + + + +
    + + %1% + +
    +

    +
    +
    +


    +
    +
    + + + ]]>
    @@ -984,6 +1122,11 @@ Максимально допустимый размер файла: Начальный узел медиа + + Выбран медиа-элемент, который в настоящее время удален или находится в корзине + Выбраны медиа-элементы, которые в настоящее время удалены или находятся в корзине + Удаленный элемент + Создать нового участника Все участники @@ -1023,34 +1166,88 @@ Удачи! Генератор уведомлений Umbraco. - ]]> - Здравствуйте, %0%

    - -

    Это автоматически сгенерированное уведомление. Операция '%1%' - была произведена на странице '%2%' - пользователем '%3%'.

    - - - -

    -

    Сводка обновлений:

    - - %6% -
    -

    - - - -

    Удачи!

    Генератор уведомлений Umbraco -

    ]]>
    + ]]> + + + + + + + + + + + + + +
    + + + + + +
    + +
    + +
    +
    + + + + + + +
    +
    +
    + + + + +
    + + + + +
    +

    + Здравствуйте, %0%, +

    +

    + Это автоматически сгенерированное сообщение, отправленное, чтобы уведомить Вас о том, что операция '%1%' была выполнена на странице '%2%' пользователем '%3%' +

    + + + + + + +
    + +
    + ВНЕСТИ ИЗМЕНЕНИЯ
    +
    +

    +

    Обзор обновления:

    + + %6% +
    +

    +

    + Удачного дня!

    + К Вашим услугам, почтовый робот Umbraco +

    +
    +
    +


    +
    +
    + + + ]]> +
    [%0%] Уведомление об операции %1% над документом %2% Уведомления @@ -1400,6 +1597,8 @@ При разблокировке пользователей произошла ошибка '%0%' сейчас разблокирован При разблокировке пользователя произошла ошибка + Данные участника успешно экспортированы в файл + Во время экспортирования данных участника произошла ошибка Используется синтаксис селекторов CSS, например: h1, .redHeader, .blueTex @@ -1672,9 +1871,103 @@ Неудачных попыток входа К профилю пользователя Добавьте пользователя в группу(ы) для задания прав доступа + Пригласить Приглашение в панель администрирования Umbraco

    Здравствуйте, %0%,

    Вы были приглашены пользователем %1%, и Вам предоставлен доступ в панель администрирования Umbraco.

    Сообщение от %1%: %2%

    Перейдите по этой ссылке, чтобы принять приглашение.

    Если Вы не имеете возможности перейти по ссылке, скопируйте нижеследующий текст ссылки и вставьте в адресную строку Вашего браузера.

    %3%

    ]]> + + + + + + + + + + + + +
    + + + + + +
    + +
    + +
    +
    + + + + + + +
    +
    +
    + + + + +
    + + + + +
    +

    + Здравствуйте, %0%, +

    +

    + Вы были приглашены пользователем %1% в панель администрирования веб-сайта. +

    +

    + Сообщение от пользователя %1%: +
    + %2% +

    + + + + + + +
    + + + + + + +
    + + Нажмите на эту ссылку, чтобы принять приглашение + +
    +
    +

    Если Вы не имеете возможности нажать на ссылку, скопируйте следующий адрес (URL) и вставьте в адресную строку Вашего браузера:

    + + + + +
    + + %3% + +
    +

    +
    +
    +


    +
    +
    + + + ]]>
    Пригласить еще одного пользователя Пригласить пользователя @@ -1725,7 +2018,7 @@ Имя (Я-А) Сначала новые Сначала старые - Недавно зашедшие + Недавно заходившие Активные Все Отключенные diff --git a/src/Umbraco.Web.UI/Umbraco/developer/Packages/installer.aspx b/src/Umbraco.Web.UI/Umbraco/developer/Packages/installer.aspx new file mode 100644 index 0000000000..1beda45dae --- /dev/null +++ b/src/Umbraco.Web.UI/Umbraco/developer/Packages/installer.aspx @@ -0,0 +1,10 @@ +<%@ Page Language="c#" MasterPageFile="../../masterpages/umbracoPage.Master" +AutoEventWireup="True" Inherits="umbraco.presentation.developer.packages.Installer" Trace="false" ValidateRequest="false" %> +<%@ Register TagPrefix="cc1" Namespace="Umbraco.Web._Legacy.Controls" Assembly="Umbraco.Web" %> + + + + + + + diff --git a/src/Umbraco.Web.UI/Umbraco/dialogs/ChangeDocType.aspx.cs b/src/Umbraco.Web.UI/Umbraco/dialogs/ChangeDocType.aspx.cs index cbd6ebbd75..2297ea1aa7 100644 --- a/src/Umbraco.Web.UI/Umbraco/dialogs/ChangeDocType.aspx.cs +++ b/src/Umbraco.Web.UI/Umbraco/dialogs/ChangeDocType.aspx.cs @@ -251,7 +251,7 @@ namespace Umbraco.Web.UI.Umbraco.Dialogs if (wasPublished) { // no values to publish, really - Services.ContentService.SaveAndPublish(_content, user.Id); + Services.ContentService.SaveAndPublish(_content, userId: user.Id); } // Sync the tree diff --git a/src/Umbraco.Web.UI/config/EmbeddedMedia.Release.config b/src/Umbraco.Web.UI/config/EmbeddedMedia.Release.config index 46f366a75a..32a165399c 100644 --- a/src/Umbraco.Web.UI/config/EmbeddedMedia.Release.config +++ b/src/Umbraco.Web.UI/config/EmbeddedMedia.Release.config @@ -1,119 +1,134 @@ - + - + - + - + xml - + - + - + - + - - + + - + - - + + - + - - + + - + - - + + - + - - + + + + + + json + + + + - + - - + + - + - + - + 1 xml https - + - + - + - + xml - + - + xml - + - + - + - + - + - + - + + + + + + + + diff --git a/src/Umbraco.Web.UI/config/EmbeddedMedia.config b/src/Umbraco.Web.UI/config/EmbeddedMedia.config index cec2dd17b3..ac8c5cc901 100644 --- a/src/Umbraco.Web.UI/config/EmbeddedMedia.config +++ b/src/Umbraco.Web.UI/config/EmbeddedMedia.config @@ -1,119 +1,134 @@  - + - + - + - + xml - + - + - + - + - - + + - + - - + + - + - - + + - + - - + + - + - - + + + + + + json + + + + - + - - + + - + - + - + 1 xml https - + - + - + - + xml - + - + xml - + - + - + - + - + - + - + + + + + + + + diff --git a/src/Umbraco.Web.UI/config/trees.Release.config b/src/Umbraco.Web.UI/config/trees.Release.config index 95123c0ce0..c5a56be290 100644 --- a/src/Umbraco.Web.UI/config/trees.Release.config +++ b/src/Umbraco.Web.UI/config/trees.Release.config @@ -1,37 +1,39 @@  - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - + + + + + + + + + diff --git a/src/Umbraco.Web.UI/config/trees.config b/src/Umbraco.Web.UI/config/trees.config index 007604e3a0..c445e92807 100644 --- a/src/Umbraco.Web.UI/config/trees.config +++ b/src/Umbraco.Web.UI/config/trees.config @@ -1,41 +1,42 @@  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.config b/src/Umbraco.Web.UI/config/umbracoSettings.config index e60626f14f..7632111b63 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.config @@ -14,7 +14,7 @@ - umbracoWidth + umbracoWidth umbracoHeight umbracoBytes umbracoExtension @@ -78,9 +78,6 @@ In Preview Mode - click to end ]]> - - - 1800 diff --git a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs index 579c71e206..0696595ef4 100644 --- a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs +++ b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs @@ -1,5 +1,4 @@ -using System; -using System.Linq; +using System.Linq; using Umbraco.Core.Models; using Umbraco.Core.Services.Changes; @@ -175,17 +174,6 @@ namespace Umbraco.Web.Cache dc.Remove(MemberCacheRefresher.UniqueId, x => x.Id, members); } - [Obsolete("Use the RefreshMemberCache with strongly typed IMember objects instead")] - public static void RefreshMemberCache(this DistributedCache dc, int memberId) - { - dc.Refresh(MemberCacheRefresher.UniqueId, memberId); - } - - [Obsolete("Use the RemoveMemberCache with strongly typed IMember objects instead")] - public static void RemoveMemberCache(this DistributedCache dc, int memberId) - { - dc.Remove(MemberCacheRefresher.UniqueId, memberId); - } #endregion diff --git a/src/Umbraco.Web/Composing/CompositionRoots/WebMappingProfilesCompositionRoot.cs b/src/Umbraco.Web/Composing/CompositionRoots/WebMappingProfilesCompositionRoot.cs index 0c73e64ff2..43544c5af0 100644 --- a/src/Umbraco.Web/Composing/CompositionRoots/WebMappingProfilesCompositionRoot.cs +++ b/src/Umbraco.Web/Composing/CompositionRoots/WebMappingProfilesCompositionRoot.cs @@ -14,6 +14,7 @@ namespace Umbraco.Web.Composing.CompositionRoots container.Register(); container.Register(); container.Register(); + container.Register(); container.Register(); container.Register(); container.Register(); diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 69b4cd3590..30ccb3dbb7 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -380,7 +380,7 @@ namespace Umbraco.Web.Editors { if (loginInfo == null) throw new ArgumentNullException("loginInfo"); if (response == null) throw new ArgumentNullException("response"); - + ExternalSignInAutoLinkOptions autoLinkOptions = null; //Here we can check if the provider associated with the request has been configured to allow // new users (auto-linked external accounts). This would never be used with public providers such as @@ -391,8 +391,10 @@ namespace Umbraco.Web.Editors { Logger.Warn("Could not find external authentication provider registered: " + loginInfo.Login.LoginProvider); } - - var autoLinkOptions = authType.GetExternalAuthenticationOptions(); + else + { + autoLinkOptions = authType.GetExternalAuthenticationOptions(); + } // Sign in the user with this external login provider if the user already has a login var user = await UserManager.FindAsync(loginInfo.Login); diff --git a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs index e550083f66..5e91cbe853 100644 --- a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs @@ -277,6 +277,10 @@ namespace Umbraco.Web.Editors "publishedStatusBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( controller => controller.GetPublishedStatusUrl()) }, + { + "dictionaryApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( + controller => controller.DeleteById(int.MaxValue)) + }, { "nuCacheStatusBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( controller => controller.GetStatus()) diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 184c442568..b8dcb9e8fa 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -74,7 +74,7 @@ namespace Umbraco.Web.Editors public bool AllowsCultureVariation() { var contentTypes = Services.ContentTypeService.GetAll(); - return contentTypes.Any(contentType => contentType.Variations.DoesSupportCulture()); + return contentTypes.Any(contentType => contentType.VariesByCulture()); } /// @@ -310,7 +310,7 @@ namespace Umbraco.Web.Editors var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == Constants.Conventions.PropertyGroups.ListViewGroupName); mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); - if (contentType.Variations.Has(ContentVariation.CultureNeutral)) + if (contentType.VariesByCulture()) { //Remove all variants except for the default since currently the default must be saved before other variants can be edited //TODO: Allow for editing all variants at once ... this will be a future task @@ -709,11 +709,10 @@ namespace Umbraco.Web.Editors /// private void PublishInternal(ContentItemSave contentItem, ref PublishResult publishStatus, ref bool wasCancelled) { - if (!contentItem.PersistedContent.ContentType.Variations.Has(ContentVariation.CultureNeutral)) + if (!contentItem.PersistedContent.ContentType.VariesByCulture()) { //its invariant, proceed normally - contentItem.PersistedContent.TryPublishValues(); - publishStatus = Services.ContentService.SaveAndPublish(contentItem.PersistedContent, Security.CurrentUser.Id); + publishStatus = Services.ContentService.SaveAndPublish(contentItem.PersistedContent, userId: Security.CurrentUser.Id); wasCancelled = publishStatus.Result == PublishResultType.FailedCancelledByEvent; } else @@ -760,20 +759,20 @@ namespace Umbraco.Web.Editors if (canPublish) { //try to publish all the values on the model - canPublish = TryPublishValues(contentItem, otherVariantsToValidate, allLangs); + canPublish = PublishCulture(contentItem, otherVariantsToValidate, allLangs); } if (canPublish) { //proceed to publish if all validation still succeeds - publishStatus = Services.ContentService.SaveAndPublish(contentItem.PersistedContent, Security.CurrentUser.Id); + publishStatus = Services.ContentService.SavePublishing(contentItem.PersistedContent, Security.CurrentUser.Id); wasCancelled = publishStatus.Result == PublishResultType.FailedCancelledByEvent; } else { //can only save var saveResult = Services.ContentService.Save(contentItem.PersistedContent, Security.CurrentUser.Id); - publishStatus = new PublishResult(PublishResultType.Failed, null, contentItem.PersistedContent); + publishStatus = new PublishResult(PublishResultType.FailedCannotPublish, null, contentItem.PersistedContent); wasCancelled = saveResult.Result == OperationResultType.FailedCancelledByEvent; } } @@ -786,16 +785,15 @@ namespace Umbraco.Web.Editors /// /// /// - private bool TryPublishValues(ContentItemSave contentItem, IEnumerable otherVariantsToValidate, IDictionary allLangs) + private bool PublishCulture(ContentItemSave contentItem, IEnumerable otherVariantsToValidate, IDictionary allLangs) { var culturesToPublish = new List { contentItem.Culture }; - if (!contentItem.Culture.IsNullOrWhiteSpace()) - culturesToPublish.Add(null); //we need to publish the invariant values if culture is specified, so we can pass in null culturesToPublish.AddRange(otherVariantsToValidate.Select(x => x.Culture)); foreach(var culture in culturesToPublish) { - var valid = contentItem.PersistedContent.TryPublishValues(culture); + // publishing any culture, implies the invariant culture + var valid = contentItem.PersistedContent.PublishCulture(culture); if (!valid) { var errMsg = Services.TextService.Localize("speechBubbles/contentCultureUnexpectedValidationError", new[] { allLangs[culture].CultureName }); @@ -827,8 +825,7 @@ namespace Umbraco.Web.Editors return HandleContentNotFound(id, false); } - foundContent.TryPublishValues(); // fixme variants? - var publishResult = Services.ContentService.SaveAndPublish(foundContent, Security.GetUserId().ResultOr(0)); + var publishResult = Services.ContentService.SavePublishing(foundContent, Security.GetUserId().ResultOr(0)); if (publishResult.Success == false) { var notificationModel = new SimpleNotificationModel(); @@ -1006,7 +1003,7 @@ namespace Umbraco.Web.Editors if (foundContent == null) HandleContentNotFound(id); - + var unpublishResult = Services.ContentService.Unpublish(foundContent, culture: culture, userId: Security.GetUserId().ResultOr(0)); var content = MapToDisplay(foundContent, culture); @@ -1022,7 +1019,7 @@ namespace Umbraco.Web.Editors content.AddSuccessNotification( Services.TextService.Localize("content/unPublish"), - unpublishResult.Result == UnpublishResultType.SuccessVariant + unpublishResult.Result == UnpublishResultType.SuccessCulture ? Services.TextService.Localize("speechBubbles/contentVariationUnpublished", new[] { culture }) : Services.TextService.Localize("speechBubbles/contentUnpublished")); @@ -1037,13 +1034,13 @@ namespace Umbraco.Web.Editors private void MapPropertyValues(ContentItemSave contentItem) { //Don't update the name if it is empty - if (contentItem.Name.IsNullOrWhiteSpace() == false) + if (!contentItem.Name.IsNullOrWhiteSpace()) { - //set the name according to the culture settings - if (contentItem.PersistedContent.ContentType.Variations.HasFlag(ContentVariation.CultureNeutral)) + if (contentItem.PersistedContent.ContentType.VariesByCulture()) { - if (contentItem.Culture.IsNullOrWhiteSpace()) throw new InvalidOperationException($"Cannot save a content item that is {ContentVariation.CultureNeutral} with a culture specified"); - contentItem.PersistedContent.SetName(contentItem.Name, contentItem.Culture); + if (contentItem.Culture.IsNullOrWhiteSpace()) + throw new InvalidOperationException($"Cannot set culture name without a culture."); + contentItem.PersistedContent.SetCultureName(contentItem.Name, contentItem.Culture); } else { @@ -1074,7 +1071,7 @@ namespace Umbraco.Web.Editors } } - bool Varies(Property property) => property.PropertyType.Variations.Has(ContentVariation.CultureNeutral); + bool Varies(Property property) => property.PropertyType.VariesByCulture(); MapPropertyValues( contentItem, @@ -1153,7 +1150,7 @@ namespace Umbraco.Web.Editors display.AddWarningNotification( Services.TextService.Localize("publish"), Services.TextService.Localize("publish/contentPublishedFailedByParent", - new[] { string.Format("{0} ({1})", status.Content.Name, status.Content.Id) }).Trim()); + new[] { $"{status.Content.Name} ({status.Content.Id})" }).Trim()); break; case PublishResultType.FailedCancelledByEvent: AddCancelMessage(display, "publish", "speechBubbles/contentPublishedFailedByEvent"); @@ -1162,32 +1159,36 @@ namespace Umbraco.Web.Editors display.AddWarningNotification( Services.TextService.Localize("publish"), Services.TextService.Localize("publish/contentPublishedFailedAwaitingRelease", - new[] { string.Format("{0} ({1})", status.Content.Name, status.Content.Id) }).Trim()); + new[] { $"{status.Content.Name} ({status.Content.Id})" }).Trim()); break; case PublishResultType.FailedHasExpired: display.AddWarningNotification( Services.TextService.Localize("publish"), Services.TextService.Localize("publish/contentPublishedFailedExpired", - new[] - { - string.Format("{0} ({1})", status.Content.Name, status.Content.Id), - }).Trim()); + new[] { $"{status.Content.Name} ({status.Content.Id})", }).Trim()); break; case PublishResultType.FailedIsTrashed: - //TODO: We should add proper error messaging for this! + display.AddWarningNotification( + Services.TextService.Localize("publish"), + "publish/contentPublishedFailedIsTrashed"); // fixme properly localize! break; case PublishResultType.FailedContentInvalid: display.AddWarningNotification( Services.TextService.Localize("publish"), Services.TextService.Localize("publish/contentPublishedFailedInvalid", - new[] - { - string.Format("{0} ({1})", status.Content.Name, status.Content.Id), - string.Join(",", status.InvalidProperties.Select(x => x.Alias)) - }).Trim()); + new[] + { + $"{status.Content.Name} ({status.Content.Id})", + string.Join(",", status.InvalidProperties.Select(x => x.Alias)) + }).Trim()); + break; + case PublishResultType.FailedByCulture: + display.AddWarningNotification( + Services.TextService.Localize("publish"), + "publish/contentPublishedFailedByCulture"); // fixme properly localize! break; default: - throw new IndexOutOfRangeException(); + throw new IndexOutOfRangeException($"PublishedResultType \"{status.Result}\" was not expected."); } } @@ -1276,7 +1277,7 @@ namespace Umbraco.Web.Editors { //A culture must exist in the mapping context if this content type is CultureNeutral since for a culture variant to be edited, // the Cuture property of ContentItemDisplay must exist (at least currently). - if (culture == null && content.ContentType.Variations.Has(ContentVariation.CultureNeutral)) + if (culture == null && content.ContentType.VariesByCulture()) { //If a culture is not explicitly sent up, then it means that the user is editing the default variant language. culture = Services.LocalizationService.GetDefaultLanguageIsoCode(); @@ -1287,6 +1288,5 @@ namespace Umbraco.Web.Editors return display; } - } } diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index ea5a0da6b5..7365c4f4f3 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -184,6 +184,62 @@ namespace Umbraco.Web.Editors ? Request.CreateResponse(HttpStatusCode.OK, result.Result) //return the id : Request.CreateNotificationValidationErrorResponse(result.Exception.Message); } + + public CreatedContentTypeCollectionResult PostCreateCollection(int parentId, string collectionName, string collectionItemName, string collectionIcon, string collectionItemIcon) + { + var storeInContainer = false; + var allowUnderDocType = -1; + // check if it's a folder + if (Services.ContentTypeService.Get(parentId) == null) + { + storeInContainer = true; + } else + { + // if it's not a container, we'll change the parentid to the root, + // and use the parent id as the doc type the collection should be allowed under + allowUnderDocType = parentId; + parentId = -1; + } + + // create item doctype + var itemDocType = new ContentType(parentId); + itemDocType.Name = collectionItemName; + itemDocType.Alias = collectionItemName.ToSafeAlias(); + itemDocType.Icon = collectionItemIcon; + Services.ContentTypeService.Save(itemDocType); + + // create collection doctype + var collectionDocType = new ContentType(parentId); + collectionDocType.Name = collectionName; + collectionDocType.Alias = collectionName.ToSafeAlias(); + collectionDocType.Icon = collectionIcon; + collectionDocType.IsContainer = true; + collectionDocType.AllowedContentTypes = new List() + { + new ContentTypeSort(itemDocType.Id, 0) + }; + Services.ContentTypeService.Save(collectionDocType); + + // test if the parent id exist and then allow the collection underneath + if (storeInContainer == false && allowUnderDocType != -1) + { + var parentCt = Services.ContentTypeService.Get(allowUnderDocType); + if (parentCt != null) + { + var allowedCts = parentCt.AllowedContentTypes.ToList(); + allowedCts.Add(new ContentTypeSort(collectionDocType.Id, allowedCts.Count())); + parentCt.AllowedContentTypes = allowedCts; + Services.ContentTypeService.Save(parentCt); + } + } + + + return new CreatedContentTypeCollectionResult + { + CollectionId = collectionDocType.Id, + ContainerId = itemDocType.Id + }; + } public DocumentTypeDisplay PostSave(DocumentTypeSave contentTypeSave) { diff --git a/src/Umbraco.Web/Editors/DictionaryController.cs b/src/Umbraco.Web/Editors/DictionaryController.cs new file mode 100644 index 0000000000..faaad9407c --- /dev/null +++ b/src/Umbraco.Web/Editors/DictionaryController.cs @@ -0,0 +1,234 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Web.Http; +using AutoMapper; +using Umbraco.Core.Models; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Mvc; +using Umbraco.Web.UI; +using Umbraco.Web.WebApi; +using Umbraco.Web.WebApi.Filters; +using Constants = Umbraco.Core.Constants; +using Notification = Umbraco.Web.Models.ContentEditing.Notification; + +namespace Umbraco.Web.Editors +{ + /// + /// + /// The API controller used for editing dictionary items + /// + /// + /// The security for this controller is defined to allow full CRUD access to dictionary if the user has access to either: + /// Dictionar + /// + [PluginController("UmbracoApi")] + [UmbracoTreeAuthorize(Constants.Trees.Dictionary)] + [EnableOverrideAuthorization] + public class DictionaryController : BackOfficeNotificationsController + { + /// + /// Deletes a data type wth a given ID + /// + /// + /// + [HttpDelete] + [HttpPost] + public HttpResponseMessage DeleteById(int id) + { + var foundDictionary = Services.LocalizationService.GetDictionaryItemById(id); + + if (foundDictionary == null) + throw new HttpResponseException(HttpStatusCode.NotFound); + + Services.LocalizationService.Delete(foundDictionary, Security.CurrentUser.Id); + + return Request.CreateResponse(HttpStatusCode.OK); + } + + /// + /// Creates a new dictoinairy item + /// + /// + /// The parent id. + /// + /// + /// The key. + /// + /// + /// The . + /// + [HttpPost] + public HttpResponseMessage Create(int parentId, string key) + { + if (string.IsNullOrEmpty(key)) + return Request + .CreateNotificationValidationErrorResponse("Key can not be empty;"); // TODO translate + + if (Services.LocalizationService.DictionaryItemExists(key)) + { + var message = Services.TextService.Localize( + "dictionaryItem/changeKeyError", + Security.CurrentUser.GetUserCulture(Services.TextService, GlobalSettings), + new Dictionary { { "0", key } }); + return Request.CreateNotificationValidationErrorResponse(message); + } + + try + { + Guid? parentGuid = null; + + if (parentId > 0) + parentGuid = Services.LocalizationService.GetDictionaryItemById(parentId).Key; + + var item = Services.LocalizationService.CreateDictionaryItemWithIdentity( + key, + parentGuid, + string.Empty); + + + return Request.CreateResponse(HttpStatusCode.OK, item.Id); + } + catch (Exception exception) + { + Logger.Error(GetType(), "Error creating dictionary", exception); + return Request.CreateNotificationValidationErrorResponse("Error creating dictionary item"); + } + } + + /// + /// Gets a dictionary item by id + /// + /// + /// The id. + /// + /// + /// The . + /// + /// + /// Returrns a not found response when dictionary item does not exist + /// + public DictionaryDisplay GetById(int id) + { + var dictionary = Services.LocalizationService.GetDictionaryItemById(id); + + if (dictionary == null) + throw new HttpResponseException(HttpStatusCode.NotFound); + + return Mapper.Map(dictionary); + } + + /// + /// Saves a dictionary item + /// + /// + /// The dictionary. + /// + /// + /// The . + /// + public DictionaryDisplay PostSave(DictionarySave dictionary) + { + var dictionaryItem = + Services.LocalizationService.GetDictionaryItemById(int.Parse(dictionary.Id.ToString())); + + if (dictionaryItem == null) + throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse("Dictionary item does not exist")); + + var userCulture = Security.CurrentUser.GetUserCulture(Services.TextService, GlobalSettings); + + if (dictionary.NameIsDirty) + { + // if the name (key) has changed, we need to check if the new key does not exist + var dictionaryByKey = Services.LocalizationService.GetDictionaryItemByKey(dictionary.Name); + + if (dictionaryByKey != null && dictionaryItem.Id != dictionaryByKey.Id) + { + + var message = Services.TextService.Localize( + "dictionaryItem/changeKeyError", + userCulture, + new Dictionary { { "0", dictionary.Name } }); + ModelState.AddModelError("Name", message); + throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); + } + + dictionaryItem.ItemKey = dictionary.Name; + } + + foreach (var translation in dictionary.Translations) + { + Services.LocalizationService.AddOrUpdateDictionaryValue(dictionaryItem, + Services.LocalizationService.GetLanguageById(translation.LanguageId), translation.Translation); + } + + try + { + Services.LocalizationService.Save(dictionaryItem); + + var model = Mapper.Map(dictionaryItem); + + model.Notifications.Add(new Notification( + Services.TextService.Localize("speechBubbles/dictionaryItemSaved", userCulture), string.Empty, + SpeechBubbleIcon.Success)); + + return model; + } + catch (Exception e) + { + Logger.Error(GetType(), "Error saving dictionary", e); + throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse("Something went wrong saving dictionary")); + } + } + + /// + /// Retrieves a list with all dictionary items + /// + /// + /// The . + /// + public IEnumerable GetList() + { + var list = new List(); + + const int level = 0; + + foreach (var dictionaryItem in Services.LocalizationService.GetRootDictionaryItems()) + { + var item = Mapper.Map(dictionaryItem); + item.Level = 0; + list.Add(item); + + GetChildItemsForList(dictionaryItem, level + 1, list); + } + + return list; + } + + /// + /// Get child items for list. + /// + /// + /// The dictionary item. + /// + /// + /// The level. + /// + /// + /// The list. + /// + private void GetChildItemsForList(IDictionaryItem dictionaryItem, int level, List list) + { + foreach (var childItem in Services.LocalizationService.GetDictionaryItemChildren( + dictionaryItem.Key)) + { + var item = Mapper.Map(childItem); + item.Level = level; + list.Add(item); + + GetChildItemsForList(childItem, level + 1, list); + } + } + } +} diff --git a/src/Umbraco.Web/Editors/MacroController.cs b/src/Umbraco.Web/Editors/MacroController.cs index 88b78e6a81..d625e3a575 100644 --- a/src/Umbraco.Web/Editors/MacroController.cs +++ b/src/Umbraco.Web/Editors/MacroController.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Net; using System.Net.Http; using System.Text; +using System.Threading; using System.Web.Http; using System.Web.SessionState; using AutoMapper; @@ -121,7 +123,18 @@ namespace Umbraco.Web.Editors //the 'easiest' way might be to create an IPublishedContent manually and populate the legacy 'page' object with that //and then set the legacy parameters. + // When rendering the macro in the backoffice the default setting would be to use the Culture of the logged in user. + // Since a Macro might contain thing thats related to the culture of the "IPublishedContent" (ie Dictionary keys) we want + // to set the current culture to the culture related to the content item. This is hacky but it works. + var publishedContent = UmbracoContext.ContentCache.GetById(doc.Id); + var culture = publishedContent?.GetCulture(); + if (culture != null) + { + Thread.CurrentThread.CurrentCulture = Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(culture.Culture); + } + var legacyPage = new global::umbraco.page(doc, _variationContextAccessor); + UmbracoContext.HttpContext.Items["pageID"] = doc.Id; UmbracoContext.HttpContext.Items["pageElements"] = legacyPage.Elements; UmbracoContext.HttpContext.Items[global::Umbraco.Core.Constants.Conventions.Url.AltTemplate] = null; diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 2401798ce2..3fde20df28 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -376,7 +376,7 @@ namespace Umbraco.Web.Editors //if the current item is in the recycle bin if (foundMedia.Trashed == false) { - var moveResult = Services.MediaService.WithResult().MoveToRecycleBin(foundMedia, (int)Security.CurrentUser.Id); + var moveResult = Services.MediaService.MoveToRecycleBin(foundMedia, (int)Security.CurrentUser.Id); if (moveResult == false) { //returning an object of INotificationModel will ensure that any pending @@ -386,7 +386,7 @@ namespace Umbraco.Web.Editors } else { - var deleteResult = Services.MediaService.WithResult().Delete(foundMedia, (int)Security.CurrentUser.Id); + var deleteResult = Services.MediaService.Delete(foundMedia, (int)Security.CurrentUser.Id); if (deleteResult == false) { //returning an object of INotificationModel will ensure that any pending @@ -464,7 +464,7 @@ namespace Umbraco.Web.Editors } //save the item - var saveStatus = Services.MediaService.WithResult().Save(contentItem.PersistedContent, (int)Security.CurrentUser.Id); + var saveStatus = Services.MediaService.Save(contentItem.PersistedContent, (int)Security.CurrentUser.Id); //return the updated model var display = ContextMapper.Map(contentItem.PersistedContent, UmbracoContext); @@ -694,7 +694,7 @@ namespace Umbraco.Web.Editors f.SetValue(Constants.Conventions.Media.File, fileName, fs); } - var saveResult = mediaService.WithResult().Save(f, Security.CurrentUser.Id); + var saveResult = mediaService.Save(f, Security.CurrentUser.Id); if (saveResult == false) { AddCancelMessage(tempFiles, diff --git a/src/Umbraco.Web/Editors/MemberController.cs b/src/Umbraco.Web/Editors/MemberController.cs index a230a6af75..513f69f778 100644 --- a/src/Umbraco.Web/Editors/MemberController.cs +++ b/src/Umbraco.Web/Editors/MemberController.cs @@ -124,13 +124,16 @@ namespace Umbraco.Web.Editors /// public MemberListDisplay GetListNodeDisplay(string listName) { + var foundType = Services.MemberTypeService.Get(listName); + var name = foundType != null ? foundType.Name : listName; + var display = new MemberListDisplay { ContentTypeAlias = listName, - ContentTypeName = listName, + ContentTypeName = name, Id = listName, IsContainer = true, - Name = listName == Constants.Conventions.MemberTypes.AllMembersListId ? "All Members" : listName, + Name = listName == Constants.Conventions.MemberTypes.AllMembersListId ? "All Members" : name, Path = "-1," + listName, ParentId = -1 }; diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index cabf4364b1..73a4d9c910 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -387,6 +387,8 @@ namespace Umbraco.Web.Editors await SendUserInviteEmailAsync(display, Security.CurrentUser.Name, Security.CurrentUser.Email, user, userSave.Message); + display.AddSuccessNotification(Services.TextService.Localize("speechBubbles/resendInviteHeader"), Services.TextService.Localize("speechBubbles/resendInviteSuccess", new[] { user.Name })); + return display; } @@ -675,6 +677,36 @@ namespace Umbraco.Web.Editors Services.TextService.Localize("speechBubbles/setUserGroupOnUsersSuccess")); } + /// + /// Deletes the non-logged in user provided id + /// + /// User Id + /// + /// Limited to users that haven't logged in to avoid issues with related records constrained + /// with a foreign key on the user Id + /// + public async Task PostDeleteNonLoggedInUser(int id) + { + var user = Services.UserService.GetUserById(id); + if (user == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + // Check user hasn't logged in. If they have they may have made content changes which will mean + // the Id is associated with audit trails, versions etc. and can't be removed. + if (user.LastLoginDate != default(DateTime)) + { + throw new HttpResponseException(HttpStatusCode.BadRequest); + } + + var userName = user.Name; + Services.UserService.Delete(user, true); + + return Request.CreateNotificationSuccessResponse( + Services.TextService.Localize("speechBubbles/deleteUserSuccess", new[] { userName })); + } + public class PagedUserResult : PagedResult { public PagedUserResult(long totalItems, long pageNumber, long pageSize) : base(totalItems, pageNumber, pageSize) diff --git a/src/Umbraco.Web/HealthCheck/Checks/DataIntegrity/DatabaseSchemaValidationHealthCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/DataIntegrity/DatabaseSchemaValidationHealthCheck.cs new file mode 100644 index 0000000000..569373f0bb --- /dev/null +++ b/src/Umbraco.Web/HealthCheck/Checks/DataIntegrity/DatabaseSchemaValidationHealthCheck.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Logging; +using Umbraco.Core.Migrations.Install; +using Umbraco.Core.Services; + +namespace Umbraco.Web.HealthCheck.Checks.DataIntegrity +{ + /// + /// U4-9544 Health check to detect if the database has any missing indexes or constraints + /// + [HealthCheck( + "0873D589-2064-4EA3-A152-C43417FE00A4", + "Database Schema Validation", + Description = "This checks the Umbraco database by doing a comparison of current indexes and schema items with the current state of the database and returns any problems it found. Useful to detect if the database hasn't been upgraded correctly.", + Group = "Data Integrity")] + public class DatabaseSchemaValidationHealthCheck : HealthCheck + { + private readonly DatabaseBuilder _databaseBuilder; + private readonly ILocalizedTextService _textService; + private readonly ILogger _logger; + + public DatabaseSchemaValidationHealthCheck(DatabaseBuilder databaseBuilder, ILocalizedTextService textService, ILogger logger) + { + _databaseBuilder = databaseBuilder ?? throw new ArgumentNullException(nameof(databaseBuilder)); + _textService = textService ?? throw new ArgumentNullException(nameof(textService)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) + { + return CheckDatabase(); + } + + public override IEnumerable GetStatus() + { + //return the statuses + return new[] { CheckDatabase() }; + } + + private HealthCheckStatus CheckDatabase() + { + var results = _databaseBuilder.ValidateDatabaseSchema(); + + _logger.Warn(typeof(DatabaseSchemaValidationHealthCheck), _textService.Localize("databaseSchemaValidationCheckDatabaseLogMessage")); + + foreach(var error in results.Errors) + { + _logger.Warn(typeof(DatabaseSchemaValidationHealthCheck), error.Item1 + ": " + error.Item2); + } + + if(results.Errors.Count > 0) + { + return new HealthCheckStatus(_textService.Localize("healthcheck/databaseSchemaValidationCheckDatabaseErrors", new[] { results.Errors.Count.ToString() })) + { + ResultType = StatusResultType.Error, + View = "Umbraco.Dashboard.DatabaseSchemaValidationController" + }; + } + + return new HealthCheckStatus(_textService.Localize("healthcheck/databaseSchemaValidationCheckDatabaseOk")) + { + ResultType = StatusResultType.Success + }; + } + } +} diff --git a/src/Umbraco.Web/Media/EmbedProviders/Flickr.cs b/src/Umbraco.Web/Media/EmbedProviders/Flickr.cs deleted file mode 100644 index 7d6c887d4a..0000000000 --- a/src/Umbraco.Web/Media/EmbedProviders/Flickr.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.ComponentModel; -using System.Web; - -namespace Umbraco.Web.Media.EmbedProviders -{ - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("This is no longer used and will be removed from the codebase in the future, for Flickr, use the Umbraco.Web.Media.EmbedProviders.OEmbedPhoto provider")] - public class Flickr : AbstractOEmbedProvider - { - public override string GetMarkup(string url, int maxWidth, int maxHeight) - { - var flickrUrl = BuildFullUrl(url, maxWidth, maxHeight); - var doc = GetXmlResponse(flickrUrl); - - string imageUrl = doc.SelectSingleNode("/oembed/url").InnerText; - string imageWidth = doc.SelectSingleNode("/oembed/width").InnerText; - string imageHeight = doc.SelectSingleNode("/oembed/height").InnerText; - string imageTitle = doc.SelectSingleNode("/oembed/title").InnerText; - - return string.Format("\"{3}\"", - imageUrl, imageWidth, imageHeight, HttpUtility.HtmlEncode(imageTitle)); - } - } -} diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs index 751b53cf5c..08b95a9029 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Runtime.Serialization; using Umbraco.Core.Models; +using Umbraco.Web.Routing; namespace Umbraco.Web.Models.ContentEditing { @@ -41,7 +42,7 @@ namespace Umbraco.Web.Models.ContentEditing public ContentTypeBasic DocumentType { get; set; } [DataMember(Name = "urls")] - public string[] Urls { get; set; } + public UrlInfo[] Urls { get; set; } /// /// Determines whether previewing is allowed for this node diff --git a/src/Umbraco.Web/Models/ContentEditing/CreatedDocumentTypeCollectionResult.cs b/src/Umbraco.Web/Models/ContentEditing/CreatedDocumentTypeCollectionResult.cs new file mode 100644 index 0000000000..cbf23743ba --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/CreatedDocumentTypeCollectionResult.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// The result of creating a content type collection in the UI + /// + [DataContract(Name = "contentTypeCollection", Namespace = "")] + public class CreatedContentTypeCollectionResult + { + [DataMember(Name = "collectionId")] + public int CollectionId { get; set; } + + [DataMember(Name = "containerId")] + public int ContainerId { get; set; } + } +} diff --git a/src/Umbraco.Web/Models/ContentEditing/DictionaryDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/DictionaryDisplay.cs new file mode 100644 index 0000000000..bb93c72ac7 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/DictionaryDisplay.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// The dictionary display model + /// + [DataContract(Name = "dictionary", Namespace = "")] + public class DictionaryDisplay : EntityBasic, INotificationModel + { + /// + /// Initializes a new instance of the class. + /// + public DictionaryDisplay() + { + this.Notifications = new List(); + this.Translations = new List(); + } + + /// + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + public List Notifications { get; private set; } + + /// + /// Gets or sets the parent id. + /// + [DataMember(Name = "parentId")] + public new Guid ParentId { get; set; } + + /// + /// Gets the translations. + /// + [DataMember(Name = "translations")] + public List Translations { get; private set; } + } +} diff --git a/src/Umbraco.Web/Models/ContentEditing/DictionaryOverviewDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/DictionaryOverviewDisplay.cs new file mode 100644 index 0000000000..7941b0ac44 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/DictionaryOverviewDisplay.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// The dictionary overview display. + /// + [DataContract(Name = "dictionary", Namespace = "")] + public class DictionaryOverviewDisplay + { + /// + /// Initializes a new instance of the class. + /// + public DictionaryOverviewDisplay() + { + Translations = new List(); + } + + /// + /// Gets or sets the key. + /// + [DataMember(Name = "name")] + public string Name { get; set; } + + /// + /// Gets or sets the id. + /// + [DataMember(Name = "id")] + public int Id { get; set; } + + /// + /// Gets or sets the level. + /// + [DataMember(Name = "level")] + public int Level { get; set; } + + /// + /// Gets or sets the translations. + /// + [DataMember(Name = "translations")] + public List Translations { get; set; } + } +} diff --git a/src/Umbraco.Web/Models/ContentEditing/DictionaryOverviewTranslationDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/DictionaryOverviewTranslationDisplay.cs new file mode 100644 index 0000000000..9f08617921 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/DictionaryOverviewTranslationDisplay.cs @@ -0,0 +1,23 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// The dictionary translation overview display. + /// + [DataContract(Name = "dictionaryTranslation", Namespace = "")] + public class DictionaryOverviewTranslationDisplay + { + /// + /// Gets or sets the display name. + /// + [DataMember(Name = "displayName")] + public string DisplayName { get; set; } + + /// + /// Gets or sets a value indicating whether has translation. + /// + [DataMember(Name = "hasTranslation")] + public bool HasTranslation { get; set; } + } +} diff --git a/src/Umbraco.Web/Models/ContentEditing/DictionarySave.cs b/src/Umbraco.Web/Models/ContentEditing/DictionarySave.cs new file mode 100644 index 0000000000..e54d1fab45 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/DictionarySave.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// Dictionary Save model + /// + [DataContract(Name = "dictionary", Namespace = "")] + public class DictionarySave : EntityBasic + { + /// + /// Initializes a new instance of the class. + /// + public DictionarySave() + { + this.Translations = new List(); + } + + /// + /// Gets or sets a value indicating whether name is dirty. + /// + [DataMember(Name = "nameIsDirty")] + public bool NameIsDirty { get; set; } + + /// + /// Gets the translations. + /// + [DataMember(Name = "translations")] + public List Translations { get; private set; } + + /// + /// Gets or sets the parent id. + /// + [DataMember(Name = "parentId")] + public new Guid ParentId { get; set; } + } +} diff --git a/src/Umbraco.Web/Models/ContentEditing/DictionaryTranslationDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/DictionaryTranslationDisplay.cs new file mode 100644 index 0000000000..2437de6ffd --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/DictionaryTranslationDisplay.cs @@ -0,0 +1,18 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// + /// The dictionary translation display model + /// + [DataContract(Name = "dictionaryTranslation", Namespace = "")] + public class DictionaryTranslationDisplay : DictionaryTranslationSave + { + /// + /// Gets or sets the display name. + /// + [DataMember(Name = "displayName")] + public string DisplayName { get; set; } + } +} diff --git a/src/Umbraco.Web/Models/ContentEditing/DictionaryTranslationSave.cs b/src/Umbraco.Web/Models/ContentEditing/DictionaryTranslationSave.cs new file mode 100644 index 0000000000..a0ab02768c --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/DictionaryTranslationSave.cs @@ -0,0 +1,29 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// The dictionary translation save model + /// + [DataContract(Name = "dictionaryTranslation", Namespace = "")] + public class DictionaryTranslationSave + { + /// + /// Gets or sets the iso code. + /// + [DataMember(Name = "isoCode")] + public string IsoCode { get; set; } + + /// + /// Gets or sets the translation. + /// + [DataMember(Name = "translation")] + public string Translation { get; set; } + + /// + /// Gets or sets the language id. + /// + [DataMember(Name = "languageId")] + public int LanguageId { get; set; } + } +} diff --git a/src/Umbraco.Web/Models/LoginStatusModel.cs b/src/Umbraco.Web/Models/LoginStatusModel.cs index 3115e3d730..8578055fae 100644 --- a/src/Umbraco.Web/Models/LoginStatusModel.cs +++ b/src/Umbraco.Web/Models/LoginStatusModel.cs @@ -37,13 +37,6 @@ namespace Umbraco.Web.Models } } - /// - /// This will construct a new LoginStatusModel and perform a lookup for hte curently logged in member - /// - [Obsolete("Do not use this ctor as it will perform business logic lookups. Use the MembershipHelper.GetCurrentLoginStatus or the static LoginStatusModel.CreateModel() to create an empty model.")] - public LoginStatusModel() - : this(true) - { } /// /// The name of the member diff --git a/src/Umbraco.Web/Models/Mapping/ContentItemDisplayNameResolver.cs b/src/Umbraco.Web/Models/Mapping/ContentItemDisplayNameResolver.cs index 2e8155e1a7..101fed8a06 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentItemDisplayNameResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentItemDisplayNameResolver.cs @@ -14,12 +14,9 @@ namespace Umbraco.Web.Models.Mapping public string Resolve(IContent source, ContentItemDisplay destination, string destMember, ResolutionContext context) { var culture = context.GetCulture(); - if (culture != null && source.ContentType.Variations.Has(ContentVariation.CultureNeutral)) - { - //return the culture name being requested - return source.GetName(culture); - } - return source.Name; + return source.ContentType.VariesByCulture() && culture != null + ? source.GetCultureName(culture) + : source.Name; } } } diff --git a/src/Umbraco.Web/Models/Mapping/ContentItemDisplayVariationResolver.cs b/src/Umbraco.Web/Models/Mapping/ContentItemDisplayVariationResolver.cs index d1673c2a5b..cb6e2938be 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentItemDisplayVariationResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentItemDisplayVariationResolver.cs @@ -25,7 +25,7 @@ namespace Umbraco.Web.Models.Mapping public IEnumerable Resolve(IContent source, ContentItemDisplay destination, IEnumerable destMember, ResolutionContext context) { - if (!source.ContentType.Variations.Has(Core.Models.ContentVariation.CultureNeutral)) + if (!source.ContentType.VariesByCulture()) return Enumerable.Empty(); var allLanguages = _localizationService.GetAllLanguages().OrderBy(x => x.Id).ToList(); @@ -36,7 +36,7 @@ namespace Umbraco.Web.Models.Mapping { Language = x, Mandatory = x.Mandatory, - Name = source.GetName(x.IsoCode), + Name = source.GetCultureName(x.IsoCode), Exists = source.IsCultureAvailable(x.IsoCode), // segments ?? PublishedState = (source.PublishedState == PublishedState.Unpublished //if the entire document is unpublished, then flag every variant as unpublished ? PublishedState.Unpublished diff --git a/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicConverter.cs b/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicConverter.cs index 5353614033..ffcd39856e 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicConverter.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicConverter.cs @@ -68,11 +68,11 @@ namespace Umbraco.Web.Models.Mapping var culture = context.GetCulture(); //a culture needs to be in the context for a property type that can vary - if (culture == null && property.PropertyType.Variations.Has(ContentVariation.CultureNeutral)) + if (culture == null && property.PropertyType.VariesByCulture()) throw new InvalidOperationException($"No languageId found in mapping operation when one is required for the culture neutral property type {property.PropertyType.Alias}"); //set the culture to null if it's an invariant property type - culture = !property.PropertyType.Variations.Has(ContentVariation.CultureNeutral) ? null : culture; + culture = !property.PropertyType.VariesByCulture() ? null : culture; // if no 'IncludeProperties' were specified or this property is set to be included - we will map the value and return. result.Value = editor.GetValueEditor().ToEditor(property, DataTypeService, culture); diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeMapperProfile.cs index 2ee9e38ff5..407fd64372 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeMapperProfile.cs @@ -112,7 +112,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember(dto => dto.AllowedTemplates, opt => opt.Ignore()) .ForMember(dto => dto.DefaultTemplate, opt => opt.Ignore()) .ForMember(display => display.Notifications, opt => opt.Ignore()) - .ForMember(display => display.AllowCultureVariant, opt => opt.MapFrom(type => type.Variations.HasFlag(ContentVariation.CultureNeutral))) + .ForMember(display => display.AllowCultureVariant, opt => opt.MapFrom(type => type.VariesByCulture())) .AfterMap((source, dest) => { //sync templates diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeVariationsResolver.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeVariationsResolver.cs index fcfe9a47cc..e25568868f 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeVariationsResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeVariationsResolver.cs @@ -13,12 +13,10 @@ namespace Umbraco.Web.Models.Mapping public ContentVariation Resolve(TSource source, TDestination destination, ContentVariation destMember, ResolutionContext context) { //this will always be the case, a content type will always be allowed to be invariant - var result = ContentVariation.InvariantNeutral; + var result = ContentVariation.Nothing; if (source.AllowCultureVariant) - { - result |= ContentVariation.CultureNeutral; - } + result |= ContentVariation.Culture; return result; } diff --git a/src/Umbraco.Web/Models/Mapping/ContentUrlResolver.cs b/src/Umbraco.Web/Models/Mapping/ContentUrlResolver.cs index e311190d67..a278a4a8c5 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentUrlResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentUrlResolver.cs @@ -8,7 +8,7 @@ using Umbraco.Web.Routing; namespace Umbraco.Web.Models.Mapping { - internal class ContentUrlResolver : IValueResolver + internal class ContentUrlResolver : IValueResolver { private readonly ILocalizedTextService _textService; private readonly IContentService _contentService; @@ -21,12 +21,12 @@ namespace Umbraco.Web.Models.Mapping _logger = logger; } - public string[] Resolve(IContent source, ContentItemDisplay destination, string[] destMember, ResolutionContext context) + public UrlInfo[] Resolve(IContent source, ContentItemDisplay destination, UrlInfo[] destMember, ResolutionContext context) { var umbracoContext = context.GetUmbracoContext(throwIfMissing: false); var urls = umbracoContext == null - ? new[] {"Cannot generate urls without a current Umbraco Context"} + ? new[] { UrlInfo.Message("Cannot generate urls without a current Umbraco Context") } : source.GetContentUrls(umbracoContext.UrlProvider, _textService, _contentService, _logger).ToArray(); return urls; diff --git a/src/Umbraco.Web/Models/Mapping/DictionaryMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/DictionaryMapperProfile.cs new file mode 100644 index 0000000000..ebd675f572 --- /dev/null +++ b/src/Umbraco.Web/Models/Mapping/DictionaryMapperProfile.cs @@ -0,0 +1,127 @@ +using AutoMapper; +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Services; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Models.Mapping +{ + /// + /// + /// The dictionary model mapper. + /// + internal class DictionaryMapperProfile : Profile + { + public DictionaryMapperProfile(ILocalizationService localizationService) + { + CreateMap() + .ForMember(x => x.Translations, expression => expression.Ignore()) + .ForMember(x => x.Notifications, expression => expression.Ignore()) + .ForMember(x => x.Icon, expression => expression.Ignore()) + .ForMember(x => x.Trashed, expression => expression.Ignore()) + .ForMember(x => x.Alias, expression => expression.Ignore()) + .ForMember(x => x.Path, expression => expression.Ignore()) + .ForMember(x => x.AdditionalData, expression => expression.Ignore()) + .ForMember( + x => x.Udi, + expression => expression.MapFrom( + content => Udi.Create(Constants.UdiEntityType.DictionaryItem, content.Key))).ForMember( + x => x.Name, + expression => expression.MapFrom(content => content.ItemKey)) + .AfterMap( + (src, dest) => + { + // build up the path to make it possible to set active item in tree + // TODO check if there is a better way + if (src.ParentId.HasValue) + { + var ids = new List { -1 }; + + + var parentIds = new List(); + + this.GetParentId(src.ParentId.Value, localizationService, parentIds); + + parentIds.Reverse(); + + ids.AddRange(parentIds); + + ids.Add(src.Id); + + dest.Path = string.Join(",", ids); + } + else + { + dest.Path = "-1," + src.Id; + } + + // add all languages and the translations + foreach (var lang in localizationService.GetAllLanguages()) + { + var langId = lang.Id; + var translation = src.Translations.FirstOrDefault(x => x.LanguageId == langId); + + dest.Translations.Add(new DictionaryTranslationDisplay + { + IsoCode = lang.IsoCode, + DisplayName = lang.CultureInfo.DisplayName, + Translation = (translation != null) ? translation.Value : string.Empty, + LanguageId = lang.Id + }); + } + }); + + CreateMap() + .ForMember(dest => dest.Level, expression => expression.Ignore()) + .ForMember(dest => dest.Translations, expression => expression.Ignore()) + .ForMember( + x => x.Name, + expression => expression.MapFrom(content => content.ItemKey)) + .AfterMap( + (src, dest) => + { + // add all languages and the translations + foreach (var lang in localizationService.GetAllLanguages()) + { + var langId = lang.Id; + var translation = src.Translations.FirstOrDefault(x => x.LanguageId == langId); + + dest.Translations.Add( + new DictionaryOverviewTranslationDisplay + { + DisplayName = lang.CultureInfo.DisplayName, + HasTranslation = translation != null && string.IsNullOrEmpty(translation.Value) == false + }); + } + }); + } + + /// + /// Goes up the dictoinary tree to get all parent ids + /// + /// + /// The parent id. + /// + /// + /// The localization service. + /// + /// + /// The ids. + /// + private void GetParentId(Guid parentId, ILocalizationService localizationService, List ids) + { + var dictionary = localizationService.GetDictionaryItemById(parentId); + + if (dictionary == null) + return; + + ids.Add(dictionary.Id); + + if (dictionary.ParentId.HasValue) + GetParentId(dictionary.ParentId.Value, localizationService, ids); + } + } +} diff --git a/src/Umbraco.Web/Models/Mapping/LanguageMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/LanguageMapperProfile.cs index b00a0949ec..b305ee2824 100644 --- a/src/Umbraco.Web/Models/Mapping/LanguageMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/LanguageMapperProfile.cs @@ -28,23 +28,7 @@ namespace Umbraco.Web.Models.Mapping { public IEnumerable Convert(IEnumerable source, IEnumerable destination, ResolutionContext context) { - var allLanguages = source.OrderBy(x => x.Id).ToList(); - var langs = new List(allLanguages.Select(x => context.Mapper.Map(x, null, context))); - - //if there's only one language, by default it is the default - if (langs.Count == 1) - { - langs[0].IsDefaultVariantLanguage = true; - langs[0].Mandatory = true; - } - else if (allLanguages.All(x => !x.IsDefaultVariantLanguage)) - { - //if no language has the default flag, then the defaul language is the one with the lowest id - langs[0].IsDefaultVariantLanguage = true; - langs[0].Mandatory = true; - } - - return langs.OrderBy(x => x.Name); + return source.Select(x => context.Mapper.Map(x, null, context)).OrderBy(x => x.Name); } } } diff --git a/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupResolver.cs b/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupResolver.cs index 63c8c5e97a..fbd7be4ecd 100644 --- a/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupResolver.cs @@ -221,7 +221,7 @@ namespace Umbraco.Web.Models.Mapping SortOrder = p.SortOrder, ContentTypeId = contentType.Id, ContentTypeName = contentType.Name, - AllowCultureVariant = p.Variations.HasFlag(Core.Models.ContentVariation.CultureNeutral) + AllowCultureVariant = p.VariesByCulture() }); } diff --git a/src/Umbraco.Web/Models/Mapping/PropertyTypeVariationsResolver.cs b/src/Umbraco.Web/Models/Mapping/PropertyTypeVariationsResolver.cs index 6c0fa9915e..00472a291c 100644 --- a/src/Umbraco.Web/Models/Mapping/PropertyTypeVariationsResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/PropertyTypeVariationsResolver.cs @@ -12,12 +12,7 @@ namespace Umbraco.Web.Models.Mapping { public ContentVariation Resolve(PropertyTypeBasic source, PropertyType destination, ContentVariation destMember, ResolutionContext context) { - //A property type should only be one type of culture variation. - //If a property type allows both variant and invariant then it generally won't be able to save because validation - //occurs when performing something like IContent.TryPublishAllValues and it will result in validation problems because - //the invariant value will not be set since in the UI only the variant values are edited if it supports it. - var result = source.AllowCultureVariant ? ContentVariation.CultureNeutral : ContentVariation.InvariantNeutral; - return result; + return source.AllowCultureVariant ? ContentVariation.Culture : ContentVariation.Nothing; } } } diff --git a/src/Umbraco.Web/Models/PartialViewMacroModel.cs b/src/Umbraco.Web/Models/PartialViewMacroModel.cs index 562f275a3f..ae4becc7cf 100644 --- a/src/Umbraco.Web/Models/PartialViewMacroModel.cs +++ b/src/Umbraco.Web/Models/PartialViewMacroModel.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using Umbraco.Core.Models; +using System.Collections.Generic; using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Web.Models @@ -24,27 +21,11 @@ namespace Umbraco.Web.Models MacroAlias = macroAlias; MacroId = macroId; } - - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("Use the constructor accepting the macro id instead")] - public PartialViewMacroModel(IPublishedContent page, IDictionary macroParams) - { - Content = page; - MacroParameters = macroParams; - } - - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("Use the Content property instead")] - public IPublishedContent CurrentPage - { - get { return Content; } - } - + public IPublishedContent Content { get; private set; } public string MacroName { get; private set; } public string MacroAlias { get; private set; } public int MacroId { get; private set; } public IDictionary MacroParameters { get; private set; } - } } diff --git a/src/Umbraco.Web/Models/ProfileModel.cs b/src/Umbraco.Web/Models/ProfileModel.cs index dcb983fea7..c7329aa2f5 100644 --- a/src/Umbraco.Web/Models/ProfileModel.cs +++ b/src/Umbraco.Web/Models/ProfileModel.cs @@ -33,11 +33,6 @@ namespace Umbraco.Web.Models } } - [Obsolete("Do not use this ctor as it will perform business logic lookups. Use the MembershipHelper.CreateProfileModel or the static ProfileModel.CreateModel() to create an empty model.")] - public ProfileModel() - :this(true) - { - } [Required] [RegularExpression(@"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?", diff --git a/src/Umbraco.Web/Models/RegisterModel.cs b/src/Umbraco.Web/Models/RegisterModel.cs index 56aaeb7ed1..dc1707aa2d 100644 --- a/src/Umbraco.Web/Models/RegisterModel.cs +++ b/src/Umbraco.Web/Models/RegisterModel.cs @@ -37,13 +37,9 @@ namespace Umbraco.Web.Models } } - [Obsolete("Do not use this ctor as it will perform business logic lookups. Use the MembershipHelper.CreateRegistrationModel or the static RegisterModel.CreateModel() to create an empty model.")] - public RegisterModel() - : this(true) - { } [Required] - [RegularExpression(@"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?", + [RegularExpression(@"[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?\.)+[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?", ErrorMessage = "Please enter a valid e-mail address")] public string Email { get; set; } diff --git a/src/Umbraco.Web/Mvc/UmbracoPageResult.cs b/src/Umbraco.Web/Mvc/UmbracoPageResult.cs index 8f4499c91d..f23407ab4c 100644 --- a/src/Umbraco.Web/Mvc/UmbracoPageResult.cs +++ b/src/Umbraco.Web/Mvc/UmbracoPageResult.cs @@ -22,13 +22,6 @@ namespace Umbraco.Web.Mvc _profilingLogger = profilingLogger; } - [Obsolete("Use the ctor specifying all depenendencies instead")] - public UmbracoPageResult() - : this(new ProfilingLogger(Current.Logger, Current.Profiler)) - { - - } - public override void ExecuteResult(ControllerContext context) { ResetRouteData(context.RouteData); diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/FlexibleDropdownPropertyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/FlexibleDropdownPropertyValueConverter.cs new file mode 100644 index 0000000000..5fa537f561 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/FlexibleDropdownPropertyValueConverter.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors.ValueConverters +{ + [DefaultPropertyValueConverter] + public class FlexibleDropdownPropertyValueConverter : PropertyValueConverterBase + { + public override bool IsConverter(PublishedPropertyType propertyType) + { + return propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.DropDownListFlexible); + } + + public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + { + return source?.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + } + + public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + { + if (inter == null) + { + return null; + } + + var selectedValues = (string[])inter; + if (selectedValues.Any()) + { + if (propertyType.DataType.ConfigurationAs().Multiple) + { + return selectedValues; + } + + return selectedValues.First(); + } + + return inter; + } + + public override Type GetPropertyValueType(PublishedPropertyType propertyType) + { + return propertyType.DataType.ConfigurationAs().Multiple + ? typeof(IEnumerable) + : typeof(string); + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MacroContainerValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MacroContainerValueConverter.cs index e6d44c0a8f..2785f160bf 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/MacroContainerValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MacroContainerValueConverter.cs @@ -72,7 +72,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters // ensure string is parsed for macros and macros are executed correctly sourceString = RenderMacros(sourceString, preview); - return sourceString; + return new HtmlString(sourceString); } } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentNodeKit.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentNodeKit.cs index f284d54cf1..5a47b99382 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentNodeKit.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentNodeKit.cs @@ -17,9 +17,9 @@ namespace Umbraco.Web.PublishedCache.NuCache public static ContentNodeKit Null { get; } = new ContentNodeKit { ContentTypeId = -1 }; - public void Build(PublishedContentType contentType, IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor) + public void Build(PublishedContentType contentType, IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor, bool canBePublished) { - Node.SetContentTypeAndData(contentType, DraftData, PublishedData, publishedSnapshotAccessor, variationContextAccessor); + Node.SetContentTypeAndData(contentType, DraftData, canBePublished ? PublishedData : null, publishedSnapshotAccessor, variationContextAccessor); } } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 303f4fc2ba..834594af9e 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -415,8 +415,11 @@ namespace Umbraco.Web.PublishedCache.NuCache if (_contentTypesById.TryGetValue(kit.ContentTypeId, out LinkedNode link) == false || link.Value == null) return false; + // check whether parent is published + var canBePublished = ParentPublishedLocked(kit); + // and use - kit.Build(link.Value, _publishedSnapshotAccessor, _variationContextAccessor); + kit.Build(link.Value, _publishedSnapshotAccessor, _variationContextAccessor, canBePublished); return true; } @@ -643,6 +646,15 @@ namespace Umbraco.Web.PublishedCache.NuCache return link?.Value != null; } + private bool ParentPublishedLocked(ContentNodeKit kit) + { + if (kit.Node.ParentContentId < 0) + return true; + var link = GetParentLink(kit.Node); + var node = link?.Value; + return node?.Published != null; + } + private void AddToParentLocked(ContentNode content) { // add to root content index, diff --git a/src/Umbraco.Web/PublishedCache/NuCache/Property.cs b/src/Umbraco.Web/PublishedCache/NuCache/Property.cs index 39fb005ba3..2d24efdd67 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/Property.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/Property.cs @@ -191,10 +191,9 @@ namespace Umbraco.Web.PublishedCache.NuCache if (culture != null && segment != null) return; // use context values - // fixme CultureSegment? var publishedVariationContext = _content.VariationContextAccessor?.VariationContext; - if (culture == null) culture = _variations.Has(ContentVariation.CultureNeutral) ? publishedVariationContext?.Culture : ""; - if (segment == null) segment = _variations.Has(ContentVariation.CultureNeutral) ? publishedVariationContext?.Segment : ""; + if (culture == null) culture = _variations.VariesByCulture() ? publishedVariationContext?.Culture : ""; + if (segment == null) segment = _variations.VariesBySegment() ? publishedVariationContext?.Segment : ""; } public override object GetValue(string culture = null, string segment = null) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs index 567201ef6f..a4610e82db 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs @@ -178,10 +178,10 @@ namespace Umbraco.Web.PublishedCache.NuCache { get { - if (!ContentType.Variations.Has(ContentVariation.CultureNeutral)) // fixme CultureSegment? + if (!ContentType.VariesByCulture()) return _contentData.Name; - var culture = VariationContextAccessor.VariationContext.Culture; + var culture = VariationContextAccessor?.VariationContext?.Culture ?? ""; if (culture == "") return _contentData.Name; @@ -194,10 +194,10 @@ namespace Umbraco.Web.PublishedCache.NuCache { get { - if (!ContentType.Variations.Has(ContentVariation.CultureNeutral)) // fixme CultureSegment? + if (!ContentType.VariesByCulture()) return _urlSegment; - var culture = VariationContextAccessor.VariationContext.Culture; + var culture = VariationContextAccessor?.VariationContext?.Culture ?? ""; if (culture == "") return _urlSegment; @@ -244,7 +244,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { // handle context culture if (culture == null) - culture = VariationContextAccessor.VariationContext.Culture; + culture = VariationContextAccessor?.VariationContext?.Culture ?? ""; // no invariant culture infos if (culture == "") return null; @@ -258,7 +258,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { get { - if (!ContentType.Variations.Has(ContentVariation.CultureNeutral)) // fixme CultureSegment? + if (!ContentType.VariesByCulture()) return NoCultureInfos; if (_cultureInfos != null) return _cultureInfos; diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index fca9458565..34efa7136a 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -1188,13 +1188,13 @@ namespace Umbraco.Web.PublishedCache.NuCache var names = content is IContent document ? (published - ? document.PublishCultureNames + ? document.PublishNames : document.CultureNames) : content.CultureNames; foreach (var (culture, name) in names) { - cultureData[culture] = new CultureVariation { Name = name, Date = content.GetCultureDate(culture) }; + cultureData[culture] = new CultureVariation { Name = name, Date = content.GetCultureDate(culture) ?? DateTime.MinValue }; } //the dictionary that will be serialized diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs index f73e09dc2d..6cf8b85ed1 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs @@ -493,69 +493,88 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache /// private IEnumerable GetChildrenMedia(int parentId, XPathNavigator xpath = null) { - - //if there is no navigator, try examine first, then re-look it up - if (xpath == null) + // if there *is* a navigator, directly look it up + if (xpath != null) { - var searchProvider = GetSearchProviderSafe(); + return ToIPublishedContent(parentId, xpath); + } - if (searchProvider != null) + // otherwise, try examine first, then re-look it up + var searchProvider = GetSearchProviderSafe(); + + if (searchProvider != null) + { + try { - try + //first check in Examine as this is WAY faster + var criteria = searchProvider.CreateCriteria("media"); + + var filter = criteria.ParentId(parentId).Not().Field(UmbracoExamineIndexer.IndexPathFieldName, "-1,-21,".MultipleCharacterWildcard()); + //the above filter will create a query like this, NOTE: That since the use of the wildcard, it automatically escapes it in Lucene. + //+(+parentId:3113 -__Path:-1,-21,*) +__IndexType:media + + // sort with the Sort field (updated for 8.0) + var results = searchProvider.Search( + filter.And().OrderBy(new SortableField("sortOrder", SortType.Int)).Compile()); + + if (results.Any()) { - //first check in Examine as this is WAY faster - var criteria = searchProvider.CreateCriteria("media"); - - var filter = criteria.ParentId(parentId).Not().Field(UmbracoExamineIndexer.IndexPathFieldName, "-1,-21,".MultipleCharacterWildcard()); - //the above filter will create a query like this, NOTE: That since the use of the wildcard, it automatically escapes it in Lucene. - //+(+parentId:3113 -__Path:-1,-21,*) +__IndexType:media - - // sort with the Sort field (updated for 8.0) - var results = searchProvider.Search( - filter.And().OrderBy(new SortableField("sortOrder", SortType.Int)).Compile()); - - if (results.Any()) + // var medias = results.Select(ConvertFromSearchResult); + var medias = results.Select(x => { - // var medias = results.Select(ConvertFromSearchResult); - var medias = results.Select(x => - { - int nid; - if (int.TryParse(x["__NodeId"], out nid) == false && int.TryParse(x["NodeId"], out nid) == false) - throw new Exception("Failed to extract NodeId from search result."); - var cacheValues = GetCacheValues(nid, id => ConvertFromSearchResult(x)); - return CreateFromCacheValues(cacheValues); - }); + int nid; + if (int.TryParse(x["__NodeId"], out nid) == false && int.TryParse(x["NodeId"], out nid) == false) + throw new Exception("Failed to extract NodeId from search result."); + var cacheValues = GetCacheValues(nid, id => ConvertFromSearchResult(x)); + return CreateFromCacheValues(cacheValues); + }); - return medias; - } - - //if there's no result then return null. Previously we defaulted back to library.GetMedia below - //but this will always get called for when we are getting descendents since many items won't have - //children and then we are hitting the database again! - //So instead we're going to rely on Examine to have the correct results like it should. - return Enumerable.Empty(); + return medias; } - catch (FileNotFoundException) - { - //Currently examine is throwing FileNotFound exceptions when we have a loadbalanced filestore and a node is published in umbraco - //See this thread: http://examine.cdodeplex.com/discussions/264341 - //Catch the exception here for the time being, and just fallback to GetMedia - } - } - //falling back to get media - - var media = library.GetMedia(parentId, true); - if (media?.Current != null) - { - xpath = media.Current; - } - else - { + //if there's no result then return null. Previously we defaulted back to library.GetMedia below + //but this will always get called for when we are getting descendents since many items won't have + //children and then we are hitting the database again! + //So instead we're going to rely on Examine to have the correct results like it should. return Enumerable.Empty(); } + catch (FileNotFoundException) + { + //Currently examine is throwing FileNotFound exceptions when we have a loadbalanced filestore and a node is published in umbraco + //See this thread: http://examine.cdodeplex.com/discussions/264341 + //Catch the exception here for the time being, and just fallback to GetMedia + } } + // falling back to get media + // was library.GetMedia which had its own cache, but MediaService *also* caches + // so, library.GetMedia is gone and now we directly work with MediaService + // (code below copied from what library was doing) + var media = Current.Services.MediaService.GetById(parentId); + if (media == null) + { + return Enumerable.Empty(); + } + + var serialized = EntityXmlSerializer.Serialize( + Current.Services.MediaService, + Current.Services.DataTypeService, + Current.Services.UserService, + Current.Services.LocalizationService, + Current.UrlSegmentProviders, + media, + true); + + var mediaIterator = serialized.CreateNavigator().Select("/"); + + return mediaIterator.Current == null + ? Enumerable.Empty() + : ToIPublishedContent(parentId, mediaIterator.Current); + } + + + internal IEnumerable ToIPublishedContent(int parentId, XPathNavigator xpath) + { var mediaList = new List(); // this is so bad, really @@ -578,32 +597,10 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache mediaList.Add(CreateFromCacheValues(cacheValues)); } - ////The xpath might be the whole xpath including the current ones ancestors so we need to select the current node - //var item = xpath.Select("//*[@id='" + parentId + "']"); - //if (item.Current == null) - //{ - // return Enumerable.Empty(); - //} - //var children = item.Current.SelectChildren(XPathNodeType.Element); - - //foreach(XPathNavigator x in children) - //{ - // //NOTE: I'm not sure why this is here, it is from legacy code of ExamineBackedMedia, but - // // will leave it here as it must have done something! - // if (x.Name != "contents") - // { - // //make sure it's actually a node, not a property - // if (!string.IsNullOrEmpty(x.GetAttribute("path", "")) && - // !string.IsNullOrEmpty(x.GetAttribute("id", ""))) - // { - // mediaList.Add(ConvertFromXPathNavigator(x)); - // } - // } - //} - return mediaList; } + internal void Resync() { // clear recursive properties cached by XmlPublishedContent.GetProperty diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 1adfb55ca9..5e1708f118 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -37,17 +37,6 @@ namespace Umbraco.Web return content.Url; } - /// - /// Gets the absolute url for the content. - /// - /// The content. - /// The absolute url for the content. - //[Obsolete("UrlWithDomain() is obsolete, use the UrlAbsolute() method instead.")] - public static string UrlWithDomain(this IPublishedContent content) - { - return content.UrlAbsolute(); - } - /// /// Gets the absolute url for the content. /// @@ -82,11 +71,15 @@ namespace Umbraco.Web public static string GetUrlSegment(this IPublishedContent content, string culture = null) { // for invariant content, return the invariant url segment - if (!content.ContentType.Variations.Has(ContentVariation.CultureNeutral)) + if (!content.ContentType.VariesByCulture()) return content.UrlSegment; + // content.GetCulture(culture) will use the 'current' culture (via accessor) in case 'culture' + // is null (meaning, 'current') - and can return 'null' if that culture is not published - and + // will return 'null' if the content is variant and culture is invariant + // else try and get the culture info - // return the corresponding url segment, or null if none (ie the culture is not published) + // return the corresponding url segment, or null if none var cultureInfo = content.GetCulture(culture); return cultureInfo?.UrlSegment; } diff --git a/src/Umbraco.Web/Routing/DefaultUrlProvider.cs b/src/Umbraco.Web/Routing/DefaultUrlProvider.cs index e40dcd0560..9e89459774 100644 --- a/src/Umbraco.Web/Routing/DefaultUrlProvider.cs +++ b/src/Umbraco.Web/Routing/DefaultUrlProvider.cs @@ -79,23 +79,18 @@ namespace Umbraco.Web.Routing /// public virtual IEnumerable GetOtherUrls(UmbracoContext umbracoContext, int id, Uri current) { - //get the invariant route for this item, this will give us the Id of it's domain node if one is assigned - var invariantRoute = umbracoContext.ContentCache.GetRouteById(id); - - if (string.IsNullOrWhiteSpace(invariantRoute)) - { - _logger.Debug(() => $"Couldn't find any page with nodeId={id}. This is most likely caused by the page not being published."); - return null; - } - + var node = umbracoContext.ContentCache.GetById(id); var domainHelper = umbracoContext.GetDomainHelper(_siteDomainHelper); - // extract domainUri and path - // route is / or / - var pos = invariantRoute.IndexOf('/'); - var path = pos == 0 ? invariantRoute : invariantRoute.Substring(pos); - var domainUris = pos == 0 ? null : domainHelper.DomainsForNode(int.Parse(invariantRoute.Substring(0, pos)), current); + var n = node; + var domainUris = domainHelper.DomainsForNode(n.Id, current, false); + while (domainUris == null && n != null) // n is null at root + { + n = n.Parent; // move to parent node + domainUris = n == null ? null : domainHelper.DomainsForNode(n.Id, current, excludeDefault: true); + } + // no domains = exit if (domainUris ==null) return Enumerable.Empty(); @@ -107,8 +102,8 @@ namespace Umbraco.Web.Routing if (route == null) continue; //need to strip off the leading ID for the route if it exists (occurs if the route is for a node with a domain assigned) - pos = route.IndexOf('/'); - path = pos == 0 ? route : route.Substring(pos); + var pos = route.IndexOf('/'); + var path = pos == 0 ? route : route.Substring(pos); var uri = new Uri(CombinePaths(d.Uri.GetLeftPart(UriPartial.Path), path)); uri = UriUtility.UriFromUmbraco(uri, _globalSettings, _requestSettings); diff --git a/src/Umbraco.Web/Routing/DomainHelper.cs b/src/Umbraco.Web/Routing/DomainHelper.cs index 67bd27f959..b6d79e788a 100644 --- a/src/Umbraco.Web/Routing/DomainHelper.cs +++ b/src/Umbraco.Web/Routing/DomainHelper.cs @@ -135,8 +135,8 @@ namespace Umbraco.Web.Routing return null; // sanitize cultures - culture = culture.NullEmpty(); - defaultCulture = defaultCulture.NullEmpty(); + culture = culture.NullOrWhiteSpaceAsNull(); + defaultCulture = defaultCulture.NullOrWhiteSpaceAsNull(); if (uri == null) { diff --git a/src/Umbraco.Web/Routing/PublishedRouter.cs b/src/Umbraco.Web/Routing/PublishedRouter.cs index 6b9140e2e0..8b222a3079 100644 --- a/src/Umbraco.Web/Routing/PublishedRouter.cs +++ b/src/Umbraco.Web/Routing/PublishedRouter.cs @@ -296,7 +296,7 @@ namespace Umbraco.Web.Routing return false; // invariant - always published - if (!domainDocument.ContentType.Variations.Has(ContentVariation.CultureNeutral)) + if (!domainDocument.ContentType.VariesByCulture()) return true; // variant, ensure that the culture corresponding to the domain's language is published diff --git a/src/Umbraco.Web/Routing/UrlInfo.cs b/src/Umbraco.Web/Routing/UrlInfo.cs new file mode 100644 index 0000000000..cd77bb7006 --- /dev/null +++ b/src/Umbraco.Web/Routing/UrlInfo.cs @@ -0,0 +1,50 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Web.Routing +{ + /// + /// Represents infos for a url. + /// + [DataContract(Name = "urlInfo", Namespace = "")] + public class UrlInfo + { + /// + /// Initializes a new instance of the class. + /// + private UrlInfo(string text, bool isUrl, string culture) + { + IsUrl = isUrl; + Text = text; + Culture = culture; + } + + /// + /// Gets the culture. + /// + [DataMember(Name = "culture")] + public string Culture { get; } + + /// + /// Gets a value indicating whether the url is a true url. + /// + /// Otherwise, it is a message. + [DataMember(Name = "isUrl")] + public bool IsUrl { get; } + + /// + /// Gets the text, which is either the url, or a message. + /// + [DataMember(Name = "text")] + public string Text { get; } + + /// + /// Creates a instance representing a true url. + /// + public static UrlInfo Url(string text, string culture = null) => new UrlInfo(text, true, culture); + + /// + /// Creates a instance representing a message. + /// + public static UrlInfo Message(string text, string culture = null) => new UrlInfo(text, false, culture); + } +} diff --git a/src/Umbraco.Web/Routing/UrlProviderExtensions.cs b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs index f7a671b1e3..9f1d42d6b1 100644 --- a/src/Umbraco.Web/Routing/UrlProviderExtensions.cs +++ b/src/Umbraco.Web/Routing/UrlProviderExtensions.cs @@ -1,27 +1,24 @@ using System; using System.Collections.Generic; +using System.Linq; using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Core; using Umbraco.Core.Logging; using LightInject; -using Umbraco.Web.Composing; namespace Umbraco.Web.Routing { internal static class UrlProviderExtensions { /// - /// Gets the URLs for the content item + /// Gets the Urls of the content item. /// - /// - /// - /// /// - /// Use this when displaying URLs, if there are errors genertaing the urls the urls themselves will - /// contain the errors. + /// Use when displaying Urls. If errors occur when generating the Urls, they will show in the list. + /// Contains all the Urls that we can figure out (based upon domains, etc). /// - public static IEnumerable GetContentUrls(this IContent content, UrlProvider urlProvider, ILocalizedTextService textService, IContentService contentService, ILogger logger) + public static IEnumerable GetContentUrls(this IContent content, UrlProvider urlProvider, ILocalizedTextService textService, IContentService contentService, ILogger logger) { if (content == null) throw new ArgumentNullException(nameof(content)); if (urlProvider == null) throw new ArgumentNullException(nameof(urlProvider)); @@ -29,89 +26,150 @@ namespace Umbraco.Web.Routing if (contentService == null) throw new ArgumentNullException(nameof(contentService)); if (logger == null) throw new ArgumentNullException(nameof(logger)); - var urls = new HashSet(); + var urls = new List(); if (content.Published == false) { - urls.Add(textService.Localize("content/itemNotPublished")); + urls.Add(UrlInfo.Message(textService.Localize("content/itemNotPublished"))); return urls; } - string url; - try - { - url = urlProvider.GetUrl(content.Id); - } - catch (Exception e) - { - logger.Error("GetUrl exception.", e); - url = "#ex"; - } - if (url == "#") - { - // document as a published version yet it's url is "#" => a parent must be - // unpublished, walk up the tree until we find it, and report. - var parent = content; - do - { - parent = parent.ParentId > 0 ? parent.Parent(contentService) : null; - } - while (parent != null && parent.Published); + // fixme inject + // fixme PublishedRouter is stateless and should be a singleton! + var localizationService = Core.Composing.Current.Services.LocalizationService; + var publishedRouter = Core.Composing.Current.Container.GetInstance(); - urls.Add(parent == null - ? textService.Localize("content/parentNotPublishedAnomaly") // oops - internal error - : textService.Localize("content/parentNotPublished", new[] { parent.Name })); - } - else if (url == "#ex") - { - urls.Add(textService.Localize("content/getUrlException")); - } - else - { - // test for collisions - var uri = new Uri(url.TrimEnd('/'), UriKind.RelativeOrAbsolute); - if (uri.IsAbsoluteUri == false) uri = uri.MakeAbsolute(UmbracoContext.Current.CleanedUmbracoUrl); - uri = UriUtility.UriToUmbraco(uri); - var r = Core.Composing.Current.Container.GetInstance(); // fixme inject or ? - var pcr = r.CreateRequest(UmbracoContext.Current, uri); - r.TryRouteRequest(pcr); + // build a list of urls, for the back-office + // which will contain + // - the 'main' urls, which is what .Url would return, for each culture + // - the 'other' urls we know (based upon domains, etc) + // + // need to work on each culture. + // on invariant trees, each culture return the same thing + // but, we don't know if the tree to this content is invariant - if (pcr.HasPublishedContent == false) - { - urls.Add(textService.Localize("content/routeError", new[] { "(error)" })); - } - else if (pcr.PublishedContent.Id != content.Id) - { - var o = pcr.PublishedContent; - string s; - if (o == null) - { - s = "(unknown)"; - } - else - { - var l = new List(); - while (o != null) - { - l.Add(o.Name); - o = o.Parent; - } - l.Reverse(); - s = "/" + string.Join("/", l) + " (id=" + pcr.PublishedContent.Id + ")"; + var cultures = localizationService.GetAllLanguages().Select(x => x.IsoCode).ToList(); - } - urls.Add(textService.Localize("content/routeError", s)); - } - else + foreach (var culture in cultures) + { + // if content is variant, and culture is not published, skip + if (content.ContentType.VariesByCulture() && !content.IsCulturePublished(culture)) + continue; + + // if it's variant and culture is published, or if it's invariant, proceed + + string url; + try { - urls.Add(url); - foreach(var otherUrl in urlProvider.GetOtherUrls(content.Id)) - { - urls.Add(otherUrl); - } + url = urlProvider.GetUrl(content.Id, culture); + } + catch (Exception e) + { + logger.Error("GetUrl exception.", e); + url = "#ex"; + } + + switch (url) + { + // deal with 'could not get the url' + case "#": + HandleCouldNotGetUrl(content, culture, urls, contentService, textService); + break; + + // deal with exceptions + case "#ex": + urls.Add(UrlInfo.Message(textService.Localize("content/getUrlException"), culture)); + break; + + // got a url, deal with collisions, add url + default: + if (!DetectCollision(content, url, urls, culture, publishedRouter, textService)) // detect collisions, etc + urls.Add(UrlInfo.Url(url, culture)); + break; } } - return urls; + + // prepare for de-duplication + var durl = new Dictionary>(); + var dmsg = new Dictionary>(); + foreach (var url in urls) + { + var d = url.IsUrl ? durl : dmsg; + if (!d.TryGetValue(url.Text, out var l)) + d[url.Text] = l = new List(); + l.Add(url); + } + + // deduplicate, order urls first then messages, concatenate cultures (hide if 'all') + var ret = new List(); + foreach (var (text, infos) in durl) + ret.Add(UrlInfo.Url(text, infos.Count == cultures.Count ? null : string.Join(", ", infos.Select(x => x.Culture)))); + foreach (var (text, infos) in dmsg) + ret.Add(UrlInfo.Message(text, infos.Count == cultures.Count ? null : string.Join(", ", infos.Select(x => x.Culture)))); + + // fixme - need to add 'others' urls + // but, when? + //// get the 'other' urls + //foreach(var otherUrl in urlProvider.GetOtherUrls(content.Id)) + // urls.Add(otherUrl); + + return ret; + } + + private static void HandleCouldNotGetUrl(IContent content, string culture, List urls, IContentService contentService, ILocalizedTextService textService) + { + // document has a published version yet its url is "#" => a parent must be + // unpublished, walk up the tree until we find it, and report. + var parent = content; + do + { + parent = parent.ParentId > 0 ? parent.Parent(contentService) : null; + } + while (parent != null && parent.Published && (!parent.ContentType.VariesByCulture() || parent.IsCulturePublished(culture))); + + if (parent == null) // oops, internal error + urls.Add(UrlInfo.Message(textService.Localize("content/parentNotPublishedAnomaly"), culture)); + + else if (!parent.Published) // totally not published + urls.Add(UrlInfo.Message(textService.Localize("content/parentNotPublished", new[] { parent.Name }), culture)); + + else // culture not published + urls.Add(UrlInfo.Message(textService.Localize("content/parentCultureNotPublished", new[] { parent.Name }), culture)); + } + + private static bool DetectCollision(IContent content, string url, List urls, string culture, PublishedRouter publishedRouter, ILocalizedTextService textService) + { + // test for collisions on the 'main' url + var uri = new Uri(url.TrimEnd('/'), UriKind.RelativeOrAbsolute); + if (uri.IsAbsoluteUri == false) uri = uri.MakeAbsolute(UmbracoContext.Current.CleanedUmbracoUrl); + uri = UriUtility.UriToUmbraco(uri); + var pcr = publishedRouter.CreateRequest(UmbracoContext.Current, uri); + publishedRouter.TryRouteRequest(pcr); + + if (pcr.HasPublishedContent == false) + { + urls.Add(UrlInfo.Message(textService.Localize("content/routeErrorCannotRoute"), culture)); + return true; + } + + if (pcr.PublishedContent.Id != content.Id) + { + var o = pcr.PublishedContent; + var l = new List(); + while (o != null) + { + l.Add(o.Name); + o = o.Parent; + } + l.Reverse(); + var s = "/" + string.Join("/", l) + " (id=" + pcr.PublishedContent.Id + ")"; + + urls.Add(UrlInfo.Message(textService.Localize("content/routeError", new[] { s }), culture)); + return true; + } + + // no collision + return false; } } } diff --git a/src/Umbraco.Web/Security/Identity/ExternalSignInAutoLinkOptions.cs b/src/Umbraco.Web/Security/Identity/ExternalSignInAutoLinkOptions.cs index 1526cfb2a1..fd074b9e96 100644 --- a/src/Umbraco.Web/Security/Identity/ExternalSignInAutoLinkOptions.cs +++ b/src/Umbraco.Web/Security/Identity/ExternalSignInAutoLinkOptions.cs @@ -43,12 +43,6 @@ namespace Umbraco.Web.Security.Identity /// public Func OnExternalLogin { get; set; } - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("Use the overload specifying user groups instead")] - public string GetDefaultUserType(UmbracoContext umbracoContext, ExternalLoginInfo loginInfo) - { - return _defaultUserGroups.Length == 0 ? "editor" : _defaultUserGroups[0]; - } /// /// The default User group aliases to use for auto-linking users @@ -61,13 +55,6 @@ namespace Umbraco.Web.Security.Identity return _defaultUserGroups; } - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("The default sections are based on the default user group, this is no longer used")] - public string[] GetDefaultAllowedSections(UmbracoContext umbracoContext, ExternalLoginInfo loginInfo) - { - return new string[0]; - } - private readonly bool _autoLinkExternalAccount; /// diff --git a/src/Umbraco.Web/TagQuery.cs b/src/Umbraco.Web/TagQuery.cs index 7a1dc2115e..9527e72453 100644 --- a/src/Umbraco.Web/TagQuery.cs +++ b/src/Umbraco.Web/TagQuery.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using AutoMapper; -using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Services; using Umbraco.Web.Models; @@ -22,20 +21,6 @@ namespace Umbraco.Web private readonly ITagService _tagService; private readonly IPublishedContentQuery _contentQuery; - [Obsolete("Use the alternate constructor specifying the contentQuery instead")] - public TagQuery(ITagService tagService) - : this(tagService, new PublishedContentQuery(UmbracoContext.Current.ContentCache, UmbracoContext.Current.MediaCache)) - { - } - - [Obsolete("Use the alternate constructor specifying the ITypedPublishedContentQuery instead")] - public TagQuery(ITagService tagService, PublishedContentQuery contentQuery) - { - if (tagService == null) throw new ArgumentNullException("tagService"); - if (contentQuery == null) throw new ArgumentNullException("contentQuery"); - _tagService = tagService; - _contentQuery = contentQuery; - } /// /// Constructor for wrapping ITagQuery, see http://issues.umbraco.org/issue/U4-6899 diff --git a/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs b/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs index 1d55072bd7..145a0f5947 100644 --- a/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentBlueprintTreeController.cs @@ -19,7 +19,7 @@ namespace Umbraco.Web.Trees /// This authorizes based on access to the content section even though it exists in the settings /// [UmbracoApplicationAuthorize(Constants.Applications.Content)] - [Tree(Constants.Applications.Settings, Constants.Trees.ContentBlueprints, null, sortOrder: 8)] + [Tree(Constants.Applications.Settings, Constants.Trees.ContentBlueprints, null, sortOrder: 10)] [PluginController("UmbracoTrees")] [CoreTree] public class ContentBlueprintTreeController : TreeController diff --git a/src/Umbraco.Web/Trees/ContentTreeController.cs b/src/Umbraco.Web/Trees/ContentTreeController.cs index 3e24e037df..da5ba8fd74 100644 --- a/src/Umbraco.Web/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTreeController.cs @@ -48,19 +48,21 @@ namespace Umbraco.Web.Trees protected override TreeNode GetSingleTreeNode(IEntitySlim entity, string parentId, FormDataCollection queryStrings) { var langId = queryStrings?["culture"]; - + var allowedUserOptions = GetAllowedUserMenuItemsForNode(entity); if (CanUserAccessNode(entity, allowedUserOptions, langId)) { //Special check to see if it ia a container, if so then we'll hide children. var isContainer = entity.IsContainer; // && (queryStrings.Get("isDialog") != "true"); + var hasChildren = ShouldRenderChildrenOfContainer(entity); + var node = CreateTreeNode( entity, Constants.ObjectTypes.Document, parentId, queryStrings, - entity.HasChildren && !isContainer); + hasChildren); // entity is either a container, or a document if (isContainer) @@ -126,7 +128,8 @@ namespace Umbraco.Web.Trees } // add default actions for *all* users - menu.Items.Add(Services.TextService.Localize("actions", ActionRePublish.Instance.Alias)).ConvertLegacyMenuItem(null, "content", "content"); + // fixme - temp disable RePublish as the page itself (republish.aspx) has been temp disabled + //menu.Items.Add(Services.TextService.Localize("actions", ActionRePublish.Instance.Alias)).ConvertLegacyMenuItem(null, "content", "content"); menu.Items.Add(Services.TextService.Localize("actions", ActionRefresh.Instance.Alias), true); return menu; @@ -155,16 +158,13 @@ namespace Umbraco.Web.Trees return menu; } - var nodeMenu = GetAllNodeMenuItems(item); - var allowedMenuItems = GetAllowedUserMenuItemsForNode(item); - - FilterUserAllowedMenuItems(nodeMenu, allowedMenuItems); - - //if the media item is in the recycle bin, don't have a default menu, just show the regular menu + var nodeMenu = GetAllNodeMenuItems(item); + + //if the content node is in the recycle bin, don't have a default menu, just show the regular menu if (item.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Contains(RecycleBinId.ToInvariantString())) { nodeMenu.DefaultMenuAlias = null; - nodeMenu.Items.Insert(2, new MenuItem(ActionRestore.Instance, Services.TextService.Localize("actions", ActionRestore.Instance.Alias))); + nodeMenu = GetNodeMenuItemsForDeletedContent(item); } else { @@ -172,6 +172,8 @@ namespace Umbraco.Web.Trees nodeMenu.DefaultMenuAlias = ActionNew.Instance.Alias; } + var allowedMenuItems = GetAllowedUserMenuItemsForNode(item); + FilterUserAllowedMenuItems(nodeMenu, allowedMenuItems); return nodeMenu; } @@ -205,7 +207,7 @@ namespace Umbraco.Web.Trees foreach (var entity in result) EnsureName(entity, cultureVal); - return result; + return result; } /// @@ -243,6 +245,23 @@ namespace Umbraco.Web.Trees return menu; } + /// + /// Returns a collection of all menu items that can be on a deleted (in recycle bin) content node + /// + /// + /// + protected MenuItemCollection GetNodeMenuItemsForDeletedContent(IUmbracoEntity item) + { + var menu = new MenuItemCollection(); + menu.Items.Add(Services.TextService.Localize("actions", ActionRestore.Instance.Alias)); + menu.Items.Add(Services.TextService.Localize("actions", ActionDelete.Instance.Alias)); + + menu.Items.Add(Services.TextService.Localize("actions", ActionRefresh.Instance.Alias), true); + + return menu; + } + + /// /// set name according to variations /// @@ -264,7 +283,7 @@ namespace Umbraco.Web.Trees // for those items that DO support cultures, we need to get the proper name, IF it exists // otherwise, invariant is fine - if (docEntity.Variations.Has(Core.Models.ContentVariation.CultureNeutral) && + if (docEntity.Variations.VariesByCulture() && docEntity.CultureNames.TryGetValue(culture, out var name) && !string.IsNullOrWhiteSpace(name)) { diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index e530ce3aed..4d4f1be483 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -280,6 +280,37 @@ namespace Umbraco.Web.Trees return GetTreeNodesInternal(id, queryStrings); } + /// + /// Check to see if we should return children of a container node + /// + /// + /// + /// + /// This is required in case a user has custom start nodes that are children of a list view since in that case we'll need to render the tree node. In normal cases we don't render + /// children of a list view. + /// + protected bool ShouldRenderChildrenOfContainer(IEntitySlim e) + { + var isContainer = e.IsContainer; + + var renderChildren = e.HasChildren && (isContainer == false); + + //Here we need to figure out if the node is a container and if so check if the user has a custom start node, then check if that start node is a child + // of this container node. If that is true, the HasChildren must be true so that the tree node still renders even though this current node is a container/list view. + if (isContainer && UserStartNodes.Length > 0 && UserStartNodes.Contains(Constants.System.Root) == false) + { + var startNodes = Services.EntityService.GetAll(UmbracoObjectType, UserStartNodes); + //if any of these start nodes' parent is current, then we need to render children normally so we need to switch some logic and tell + // the UI that this node does have children and that it isn't a container + if (startNodes.Any(x => x.ParentId == e.Id)) + { + renderChildren = true; + } + } + + return renderChildren; + } + /// /// Before we make a call to get the tree nodes we have to check if they can actually be rendered /// @@ -296,7 +327,7 @@ namespace Umbraco.Web.Trees //before we get the children we need to see if this is a container node //test if the parent is a listview / container - if (current != null && current.IsContainer) + if (current != null && ShouldRenderChildrenOfContainer(current) == false) { //no children! return new TreeNodeCollection(); diff --git a/src/Umbraco.Web/Trees/DataTypeTreeController.cs b/src/Umbraco.Web/Trees/DataTypeTreeController.cs index a4cfe33e5b..99b94b544c 100644 --- a/src/Umbraco.Web/Trees/DataTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/DataTypeTreeController.cs @@ -17,7 +17,7 @@ using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Trees { [UmbracoTreeAuthorize(Constants.Trees.DataTypes)] - [Tree(Constants.Applications.Settings, Constants.Trees.DataTypes, null, sortOrder:1)] + [Tree(Constants.Applications.Settings, Constants.Trees.DataTypes, null, sortOrder:7)] [PluginController("UmbracoTrees")] [CoreTree] public class DataTypeTreeController : TreeController, ISearchableTree diff --git a/src/Umbraco.Web/Trees/DictionaryTreeController.cs b/src/Umbraco.Web/Trees/DictionaryTreeController.cs index a70eba29e2..c2491fefe0 100644 --- a/src/Umbraco.Web/Trees/DictionaryTreeController.cs +++ b/src/Umbraco.Web/Trees/DictionaryTreeController.cs @@ -1,125 +1,109 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Net.Http.Formatting; using Umbraco.Core; -using Umbraco.Core.Models; -using Umbraco.Core.Models.Entities; using Umbraco.Core.Services; +using Umbraco.Web._Legacy.Actions; using Umbraco.Web.Models.Trees; using Umbraco.Web.WebApi.Filters; -using Umbraco.Web._Legacy.Actions; namespace Umbraco.Web.Trees { [UmbracoTreeAuthorize(Constants.Trees.Dictionary)] - [Tree(Constants.Applications.Settings, Constants.Trees.Dictionary, null, sortOrder: 3)] [Mvc.PluginController("UmbracoTrees")] [CoreTree] + [Tree(Constants.Applications.Settings, Constants.Trees.Dictionary, null, sortOrder: 3)] public class DictionaryTreeController : TreeController { protected override TreeNode CreateRootNode(FormDataCollection queryStrings) { - var node = base.CreateRootNode(queryStrings); + var root = base.CreateRootNode(queryStrings); - // For now, this is using the legacy webforms view but will need refactoring - // when the dictionary has been converted to Angular. - node.RoutePath = String.Format("{0}/framed/{1}", queryStrings.GetValue("application"), - Uri.EscapeDataString("settings/DictionaryItemList.aspx")); + // the default section is settings, falling back to this if we can't + // figure out where we are from the querystring parameters + var section = Constants.Applications.Settings; + if (queryStrings["application"] != null) + section = queryStrings["application"]; - return node; + // this will load in a custom UI instead of the dashboard for the root node + root.RoutePath = $"{section}/{Constants.Trees.Dictionary}/list"; + + return root; } + /// + /// The method called to render the contents of the tree structure + /// + /// The id of the tree item + /// + /// All of the query string parameters passed from jsTree + /// + /// + /// We are allowing an arbitrary number of query strings to be pased in so that developers are able to persist custom data from the front-end + /// to the back end to be used in the query for model data. + /// protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) { - var intId = ValidateId(id); + var intId = id.TryConvertTo(); + if (intId == false) + throw new InvalidOperationException("Id must be an integer"); var nodes = new TreeNodeCollection(); - nodes.AddRange(GetDictionaryItems(intId) - .OrderBy(dictionaryItem => dictionaryItem.ItemKey) - .Select(dictionaryItem => CreateTreeNode(id, queryStrings, dictionaryItem))); + + if (id == Constants.System.Root.ToInvariantString()) + { + nodes.AddRange( + Services.LocalizationService.GetRootDictionaryItems().Select( + x => CreateTreeNode( + x.Id.ToInvariantString(), + id, + queryStrings, + x.ItemKey, + "icon-book-alt", + Services.LocalizationService.GetDictionaryItemChildren(x.Key).Any()))); + } + else + { + // maybe we should use the guid as url param to avoid the extra call for getting dictionary item + var parentDictionary = Services.LocalizationService.GetDictionaryItemById(intId.Result); + if (parentDictionary == null) + return nodes; + + nodes.AddRange(Services.LocalizationService.GetDictionaryItemChildren(parentDictionary.Key).ToList().OrderByDescending(item => item.Key).Select( + x => CreateTreeNode( + x.Id.ToInvariantString(), + id, + queryStrings, + x.ItemKey, + "icon-book-alt", + Services.LocalizationService.GetDictionaryItemChildren(x.Key).Any()))); + } return nodes; } + /// + /// Returns the menu structure for the node + /// + /// The id of the tree item + /// + /// All of the query string parameters passed from jsTree + /// + /// protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings) { - var intId = ValidateId(id); - var menu = new MenuItemCollection(); - if (intId == Constants.System.Root) - { - // Again, menu actions will need to use legacy views as this section hasn't been converted to Angular (yet!) - menu.Items.Add(Services.TextService.Localize("actions", ActionNew.Instance.Alias)) - .ConvertLegacyMenuItem(null, "dictionary", queryStrings.GetValue("application")); + menu.Items.Add(Services.TextService.Localize($"actions/{ActionNew.Instance.Alias}")); - menu.Items.Add( - Services.TextService.Localize("actions", ActionRefresh.Instance.Alias), true); - } - else - { + if (id != Constants.System.Root.ToInvariantString()) + menu.Items.Add(Services.TextService.Localize( + $"actions/{ActionDelete.Instance.Alias}"), true); - var dictionaryItem = Services.LocalizationService.GetDictionaryItemById(intId); - var entity = new EntitySlim - { - Id = dictionaryItem.Id, - Level = 1, - ParentId = -1, - Name = dictionaryItem.ItemKey - }; - - menu.Items.Add(Services.TextService.Localize("actions", ActionNew.Instance.Alias)) - .ConvertLegacyMenuItem(entity, "dictionary", queryStrings.GetValue("application")); - - menu.Items.Add(Services.TextService.Localize("actions", ActionDelete.Instance.Alias)) - .ConvertLegacyMenuItem(null, "dictionary", queryStrings.GetValue("application")); - - menu.Items.Add( - Services.TextService.Localize("actions", ActionRefresh.Instance.Alias), true); - } + menu.Items.Add(Services.TextService.Localize( + $"actions/{ActionRefresh.Instance.Alias}"), true); return menu; } - - private IEnumerable GetDictionaryItems(int id) - { - if (id > Constants.System.Root) - { - var dictionaryItem = Services.LocalizationService.GetDictionaryItemById(id); - - if (dictionaryItem != null) - { - return Services.LocalizationService.GetDictionaryItemChildren(dictionaryItem.Key); - } - } - - return Services.LocalizationService.GetRootDictionaryItems(); - } - - private TreeNode CreateTreeNode(string id, FormDataCollection queryStrings, IDictionaryItem dictionaryItem) - { - var hasChildren = Services.LocalizationService.GetDictionaryItemChildren(dictionaryItem.Key).Any(); - - // Again, menu actions will need to use legacy views as this section hasn't been converted to Angular (yet!) - var node = CreateTreeNode(dictionaryItem.Id.ToInvariantString(), id, queryStrings, dictionaryItem.ItemKey, - "icon-book-alt", hasChildren, - String.Format("{0}/framed/{1}", queryStrings.GetValue("application"), - Uri.EscapeDataString("settings/editDictionaryItem.aspx?id=" + - dictionaryItem.Id))); - - return node; - } - - private int ValidateId(string id) - { - var intId = id.TryConvertTo(); - if (intId == false) - { - throw new InvalidOperationException("Id must be an integer"); - } - - return intId.Result; - } } } diff --git a/src/Umbraco.Web/Trees/LanguageTreeController.cs b/src/Umbraco.Web/Trees/LanguageTreeController.cs index 9a9f32128c..eadb5c50d0 100644 --- a/src/Umbraco.Web/Trees/LanguageTreeController.cs +++ b/src/Umbraco.Web/Trees/LanguageTreeController.cs @@ -7,7 +7,7 @@ using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Trees { [UmbracoTreeAuthorize(Constants.Trees.Languages)] - [Tree(Constants.Applications.Settings, Constants.Trees.Languages, null, sortOrder: 4)] + [Tree(Constants.Applications.Settings, Constants.Trees.Languages, null, sortOrder: 5)] [PluginController("UmbracoTrees")] [CoreTree] public class LanguageTreeController : TreeController diff --git a/src/Umbraco.Web/Trees/MediaTypeTreeController.cs b/src/Umbraco.Web/Trees/MediaTypeTreeController.cs index e853ff7c7d..086c1a5194 100644 --- a/src/Umbraco.Web/Trees/MediaTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/MediaTypeTreeController.cs @@ -16,7 +16,7 @@ using Umbraco.Web.Search; namespace Umbraco.Web.Trees { [UmbracoTreeAuthorize(Constants.Trees.MediaTypes)] - [Tree(Constants.Applications.Settings, Constants.Trees.MediaTypes, null, sortOrder:8)] + [Tree(Constants.Applications.Settings, Constants.Trees.MediaTypes, null, sortOrder:9)] [Mvc.PluginController("UmbracoTrees")] [CoreTree] public class MediaTypeTreeController : TreeController, ISearchableTree diff --git a/src/Umbraco.Web/Trees/ScriptsTreeController.cs b/src/Umbraco.Web/Trees/ScriptsTreeController.cs index 47d7aa6b8f..57a50cde5d 100644 --- a/src/Umbraco.Web/Trees/ScriptsTreeController.cs +++ b/src/Umbraco.Web/Trees/ScriptsTreeController.cs @@ -6,7 +6,7 @@ using Umbraco.Web.Models.Trees; namespace Umbraco.Web.Trees { - [Tree(Constants.Applications.Settings, Constants.Trees.Scripts, "Scripts", "icon-folder", "icon-folder", sortOrder: 2)] + [Tree(Constants.Applications.Settings, Constants.Trees.Scripts, "Scripts", "icon-folder", "icon-folder", sortOrder: 4)] public class ScriptsTreeController : FileSystemTreeController { protected override IFileSystem FileSystem => Current.FileSystems.ScriptsFileSystem; // fixme inject diff --git a/src/Umbraco.Web/Trees/TreeControllerBase.cs b/src/Umbraco.Web/Trees/TreeControllerBase.cs index f4debe5ccf..ebf2f74e07 100644 --- a/src/Umbraco.Web/Trees/TreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/TreeControllerBase.cs @@ -233,6 +233,7 @@ namespace Umbraco.Web.Trees { var contentTypeIcon = entity is IContentEntitySlim contentEntity ? contentEntity.ContentTypeIcon : null; var treeNode = CreateTreeNode(entity.Id.ToInvariantString(), parentId, queryStrings, entity.Name, contentTypeIcon); + treeNode.Path = entity.Path; treeNode.Udi = Udi.Create(ObjectTypes.GetUdiType(entityObjectType), entity.Key); treeNode.HasChildren = hasChildren; return treeNode; @@ -252,6 +253,7 @@ namespace Umbraco.Web.Trees { var treeNode = CreateTreeNode(entity.Id.ToInvariantString(), parentId, queryStrings, entity.Name, icon); treeNode.Udi = Udi.Create(ObjectTypes.GetUdiType(entityObjectType), entity.Key); + treeNode.Path = entity.Path; treeNode.HasChildren = hasChildren; return treeNode; } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 35d6d240e9..203782e125 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -58,7 +58,7 @@ - + @@ -118,6 +118,7 @@ + @@ -131,6 +132,7 @@ + @@ -207,6 +209,13 @@ + + + + + + + @@ -234,6 +243,7 @@ + @@ -321,6 +331,7 @@ + @@ -397,6 +408,7 @@ + @@ -428,6 +440,9 @@ + + ASPXCodeBehind + ASPXCodeBehind @@ -488,7 +503,6 @@ - @@ -1193,9 +1207,6 @@ Code - - Code - Code @@ -1284,9 +1295,6 @@ SendPublish.aspx - - Code - editPackage.aspx ASPXCodeBehind diff --git a/src/Umbraco.Web/UmbracoHelper.cs b/src/Umbraco.Web/UmbracoHelper.cs index 0835b113bd..7aa434ab7e 100644 --- a/src/Umbraco.Web/UmbracoHelper.cs +++ b/src/Umbraco.Web/UmbracoHelper.cs @@ -1,24 +1,19 @@ using System; -using System.ComponentModel; +using System.Collections.Generic; +using System.Linq; using System.Web; -using System.Web.Security; using System.Xml.XPath; using Umbraco.Core; using Umbraco.Core.Dictionary; -using Umbraco.Core.Security; -using Umbraco.Core.Services; -using Umbraco.Core.Xml; -using Umbraco.Web.Routing; -using Umbraco.Web.Security; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Web.Mvc; -using Umbraco.Core.Cache; using Umbraco.Core.Exceptions; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Services; +using Umbraco.Core.Xml; using Umbraco.Web.Composing; +using Umbraco.Core.Cache; +using Umbraco.Web.Routing; +using Umbraco.Web.Security; namespace Umbraco.Web { @@ -180,29 +175,44 @@ namespace Umbraco.Web ?? (_componentRenderer = new UmbracoComponentRenderer(UmbracoContext)); /// - /// Returns the current IPublishedContent item assigned to the UmbracoHelper + /// Returns the current item + /// assigned to the UmbracoHelper. /// /// - /// Note that this is the assigned IPublishedContent item to the UmbracoHelper, this is not necessarily the Current IPublishedContent item - /// being rendered. This IPublishedContent object is contextual to the current UmbracoHelper instance. - /// - /// In some cases accessing this property will throw an exception if there is not IPublishedContent assigned to the Helper - /// this will only ever happen if the Helper is constructed with an UmbracoContext and it is not a front-end request + /// + /// Note that this is the assigned IPublishedContent item to the + /// UmbracoHelper, this is not necessarily the Current IPublishedContent + /// item being rendered. This IPublishedContent object is contextual to + /// the current UmbracoHelper instance. + /// + /// + /// In some cases accessing this property will throw an exception if + /// there is not IPublishedContent assigned to the Helper this will + /// only ever happen if the Helper is constructed with an UmbracoContext + /// and it is not a front-end request. + /// /// - /// Thrown if the UmbracoHelper is constructed with an UmbracoContext and it is not a front-end request + /// Thrown if the + /// UmbracoHelper is constructed with an UmbracoContext and it is not a + /// front-end request. public IPublishedContent AssignedContentItem { get { - if (_currentPage == null) - throw new InvalidOperationException("Cannot return the " + typeof(IPublishedContent).Name + " because the " + typeof(UmbracoHelper).Name + " was constructed with an " + typeof(UmbracoContext).Name + " and the current request is not a front-end request."); + if (_currentPage != null) + { + return _currentPage; + } + + throw new InvalidOperationException( + $"Cannot return the {nameof(IPublishedContent)} because the {nameof(UmbracoHelper)} was constructed with an {nameof(UmbracoContext)} and the current request is not a front-end request." + ); - return _currentPage; } } /// - /// Renders the template for the specified pageId and an optional altTemplateId. + /// Renders the template for the specified pageId and an optional altTemplateId /// /// /// If not specified, will use the template assigned to the node @@ -286,30 +296,6 @@ namespace Umbraco.Web #region Membership - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("Use the IsProtected method that only specifies path")] - public bool IsProtected(int documentId, string path) - { - return IsProtected(path.EnsureEndsWith("," + documentId)); - } - - /// - /// Check if a document object is protected by the "Protect Pages" functionality in umbraco - /// - /// The full path of the document object to check - /// True if the document object is protected - public bool IsProtected(string path) - { - return MembershipHelper.IsProtected(path); - } - - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("Use the MemberHasAccess method that only specifies path")] - public bool MemberHasAccess(int nodeId, string path) - { - return MemberHasAccess(path.EnsureEndsWith("," + nodeId)); - } - /// /// Check if the current user has access to a document /// @@ -601,37 +587,40 @@ namespace Umbraco.Web return ContentQuery.ContentAtRoot(); } - private static bool ConvertIdObjectToInt(object id, out int intId) + /// Had to change to internal for testing. + internal static bool ConvertIdObjectToInt(object id, out int intId) { - var s = id as string; - if (s != null) + switch (id) { - return int.TryParse(s, out intId); - } + case string s: + return int.TryParse(s, out intId); - if (id is int) - { - intId = (int) id; - return true; + case int i: + intId = i; + return true; + + default: + intId = default; + return false; } - intId = default(int); - return false; } - private static bool ConvertIdObjectToGuid(object id, out Guid guidId) + /// Had to change to internal for testing. + internal static bool ConvertIdObjectToGuid(object id, out Guid guidId) { - var s = id as string; - if (s != null) + switch (id) { - return Guid.TryParse(s, out guidId); + case string s: + return Guid.TryParse(s, out guidId); + + case Guid g: + guidId = g; + return true; + + default: + guidId = default; + return false; } - if (id is Guid) - { - guidId = (Guid) id; - return true; - } - guidId = default(Guid); - return false; } private static bool ConvertIdsObjectToInts(IEnumerable ids, out IEnumerable intIds) @@ -665,19 +654,25 @@ namespace Umbraco.Web return true; } - private static bool ConvertIdObjectToUdi(object id, out Udi guidId) + /// Had to change to internal for testing. + internal static bool ConvertIdObjectToUdi(object id, out Udi guidId) { - if (id is string s) - return Udi.TryParse(s, out guidId); - if (id is Udi) + switch (id) { - guidId = (Udi) id; - return true; + case string s: + return Udi.TryParse(s, out guidId); + + case Udi u: + guidId = u; + return true; + + default: + guidId = default; + return false; } - guidId = null; - return false; } + #endregion #region Media @@ -886,7 +881,7 @@ namespace Umbraco.Web { return StringUtilities.ReplaceLineBreaksForHtml(text); } - + /// /// Generates a hash based on the text string passed in. This method will detect the /// security requirements (is FIPS enabled) and return an appropriate hash. @@ -1051,7 +1046,7 @@ namespace Umbraco.Web } #endregion - + /// /// This is used in methods like BeginUmbracoForm and SurfaceAction to generate an encrypted string which gets submitted in a request for which /// Umbraco can decrypt during the routing process in order to delegate the request to a specific MVC Controller. diff --git a/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs b/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs index da7f723e12..1df1445979 100644 --- a/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs +++ b/src/Umbraco.Web/WebApi/Binders/ContentItemBinder.cs @@ -58,10 +58,10 @@ namespace Umbraco.Web.WebApi.Binders protected override bool ValidateCultureVariant(ContentItemSave postedItem, HttpActionContext actionContext) { var contentType = postedItem.PersistedContent.GetContentType(); - if (contentType.Variations.DoesSupportCulture() && postedItem.Culture.IsNullOrWhiteSpace()) + if (contentType.VariesByCulture() && postedItem.Culture.IsNullOrWhiteSpace()) { //we cannot save a content item that is culture variant if no culture was specified in the request! - actionContext.Response = actionContext.Request.CreateValidationErrorResponse($"No 'Culture' found in request. Cannot save a content item that is of a {Core.Models.ContentVariation.CultureNeutral} content type without a specified culture."); + actionContext.Response = actionContext.Request.CreateValidationErrorResponse($"No culture found in request. Cannot save a content item that varies by culture, without a specified culture."); return false; } return true; diff --git a/src/Umbraco.Web/WebServices/BulkPublishController.cs b/src/Umbraco.Web/WebServices/BulkPublishController.cs index 04699536e9..e6810f2a78 100644 --- a/src/Umbraco.Web/WebServices/BulkPublishController.cs +++ b/src/Umbraco.Web/WebServices/BulkPublishController.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Linq; +using System.Linq; using System.Text; using System.Web.Mvc; using Umbraco.Core; @@ -29,8 +28,8 @@ namespace Umbraco.Web.WebServices if (publishDescendants == false) { - content.TryPublishValues(); // fixme variants? validation - when this returns null? - var result = Services.ContentService.SaveAndPublish(content, Security.CurrentUser.Id); + // fixme variants? + var result = Services.ContentService.SaveAndPublish(content, userId: Security.CurrentUser.Id); return Json(new { success = result.Success, @@ -40,12 +39,12 @@ namespace Umbraco.Web.WebServices else { // fixme variants? - var result = Services.ContentService.SaveAndPublishBranch(content, includeUnpublished); + var result = Services.ContentService.SaveAndPublishBranch(content, includeUnpublished).ToArray(); return Json(new { success = result.All(x => x.Success), - message = GetMessageForStatuses(result.ToArray(), content) + message = GetMessageForStatuses(result, content) }); } } @@ -79,19 +78,19 @@ namespace Umbraco.Web.WebServices return Services.TextService.Localize("publish/nodePublish", new[] { status.Content.Name}); case PublishResultType.FailedPathNotPublished: return Services.TextService.Localize("publish/contentPublishedFailedByParent", - new [] { string.Format("{0} ({1})", status.Content.Name, status.Content.Id) }); + new [] { $"{status.Content.Name} ({status.Content.Id})" }); case PublishResultType.FailedHasExpired: case PublishResultType.FailedAwaitingRelease: case PublishResultType.FailedIsTrashed: return "Cannot publish document with a status of " + status.Result; case PublishResultType.FailedCancelledByEvent: return Services.TextService.Localize("publish/contentPublishedFailedByEvent", - new [] { string.Format("'{0}' ({1})", status.Content.Name, status.Content.Id) }); + new [] { $"'{status.Content.Name}' ({status.Content.Id})" }); case PublishResultType.FailedContentInvalid: return Services.TextService.Localize("publish/contentPublishedFailedInvalid", new []{ - string.Format("'{0}' ({1})", status.Content.Name, status.Content.Id), - string.Format("'{0}'", string.Join(", ", status.InvalidProperties.Select(x => x.Alias))) + $"'{status.Content.Name}' ({status.Content.Id})", + $"'{string.Join(", ", status.InvalidProperties.Select(x => x.Alias))}'" }); default: return status.Result.ToString(); diff --git a/src/Umbraco.Web/_Legacy/Packager/data.cs b/src/Umbraco.Web/_Legacy/Packager/data.cs index dd14c54cec..9353d52461 100644 --- a/src/Umbraco.Web/_Legacy/Packager/data.cs +++ b/src/Umbraco.Web/_Legacy/Packager/data.cs @@ -1,8 +1,6 @@ using System; using System.Xml; -using System.Xml.XPath; using System.Collections.Generic; -using System.ComponentModel; using System.IO; using Umbraco.Core; using Umbraco.Core.Configuration; @@ -264,17 +262,6 @@ namespace umbraco.cms.businesslogic.packager } - [EditorBrowsable(EditorBrowsableState.Never)] - [Obsolete("This method is no longer in use and will be removed in the future")] - public static void UpdateValue(XmlNode n, string Value) - { - if (n.FirstChild != null) - n.FirstChild.Value = Value; - else - { - n.AppendChild(Source.CreateTextNode(Value)); - } - } public static void Save(PackageInstance package, string dataSource) { diff --git a/src/Umbraco.Web/umbraco.presentation/item.cs b/src/Umbraco.Web/umbraco.presentation/item.cs index 3937b5675c..226db9bf90 100644 --- a/src/Umbraco.Web/umbraco.presentation/item.cs +++ b/src/Umbraco.Web/umbraco.presentation/item.cs @@ -60,7 +60,9 @@ namespace umbraco if (_fieldName.StartsWith("#")) { - _fieldContent = library.GetDictionaryItem(_fieldName.Substring(1, _fieldName.Length - 1)); + var umbHelper = new UmbracoHelper(Current.UmbracoContext, Current.Services, Current.ApplicationCache); + + _fieldContent = umbHelper.GetDictionaryValue(_fieldName.Substring(1, _fieldName.Length - 1)); } else { diff --git a/src/Umbraco.Web/umbraco.presentation/library.cs b/src/Umbraco.Web/umbraco.presentation/library.cs deleted file mode 100644 index e51de7a877..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/library.cs +++ /dev/null @@ -1,1574 +0,0 @@ -using System; -using System.ComponentModel; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Mail; -using System.Text; -using System.Text.RegularExpressions; -using System.Web; -using System.Web.UI; -using System.Xml; -using System.Xml.Linq; -using System.Xml.XPath; -using Newtonsoft.Json; -using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Configuration; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.Services; -using Umbraco.Web; -using Umbraco.Web.Templates; -using Umbraco.Core.IO; -using Umbraco.Core.Xml; -using Umbraco.Web.Composing; -using Umbraco.Web.PublishedCache.XmlPublishedCache; - -namespace umbraco -{ - /// - /// Function library for umbraco. Includes various helper-methods and methods to - /// save and load data from umbraco. - /// - /// Especially usefull in XSLT where any of these methods can be accesed using the umbraco.library name-space. Example: - /// <xsl:value-of select="umbraco.library:NiceUrl(@id)"/> - /// - [Obsolete("v8.kill.kill")] - public class library - { - /// - /// Returns a new UmbracoHelper so that we can start moving the logic from some of these methods to it - /// - /// - private static UmbracoHelper GetUmbracoHelper() - { - return new UmbracoHelper(Current.UmbracoContext, Current.Services, Current.ApplicationCache); - } - - #region Declarations - - /// - /// Used by umbraco's publishing enginge, to determine if publishing is currently active - /// - public static bool IsPublishing = false; - /// - /// Used by umbraco's publishing enginge, to how many nodes is publish in the current publishing cycle - /// - public static int NodesPublished = 0; - /// - /// Used by umbraco's publishing enginge, to determine the start time of the current publishing cycle. - /// - public static DateTime PublishStart; - private page _page; - - #endregion - - #region Constructors - - /// - /// Empty constructor - /// - public library() - { - } - - public library(int id) - { - var content = GetSafeContentCache().GetById(id); - _page = new page(content); - } - - /// - /// Initializes a new instance of the class. - /// - /// The page. - public library(page page) - { - _page = page; - } - - #endregion - - #region Xslt Helper functions - - /// - /// This will convert a json structure to xml for use in xslt - /// - /// - /// - public static XPathNodeIterator JsonToXml(string json) - { - try - { - if (json.StartsWith("[")) - { - //we'll assume it's an array, in which case we need to add a root - json = "{\"arrayitem\":" + json + "}"; - } - var xml = JsonConvert.DeserializeXmlNode(json, "json", false); - return xml.CreateNavigator().Select("/json"); - } - catch (Exception ex) - { - var xd = new XmlDocument(); - xd.LoadXml(string.Format("Could not convert JSON to XML. Error: {0}", ex)); - return xd.CreateNavigator().Select("/error"); - } - } - - /// - /// Returns a string with a friendly url from a node. - /// IE.: Instead of having /482 (id) as an url, you can have - /// /screenshots/developer/macros (spoken url) - /// - /// Identifier for the node that should be returned - /// String with a friendly url from a node - public static string NiceUrl(int nodeID) - { - return GetUmbracoHelper().Url(nodeID); - } - - /// - /// This method will always add the domain to the path if the hostnames are set up correctly. - /// - /// Identifier for the node that should be returned - /// String with a friendly url with full domain from a node - public static string NiceUrlWithDomain(int nodeId) - { - return GetUmbracoHelper().UrlAbsolute(nodeId); - } - - /// - /// This method will always add the domain to the path. - /// - /// Identifier for the node that should be returned - /// Ignores the umbraco hostnames and returns the url prefixed with the requested host (including scheme and port number) - /// String with a friendly url with full domain from a node - internal static string NiceUrlWithDomain(int nodeId, bool ignoreUmbracoHostNames) - { - if (ignoreUmbracoHostNames) - return HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority) + NiceUrl(nodeId); - - return NiceUrlWithDomain(nodeId); - } - - public static string ResolveVirtualPath(string path) - { - return IOHelper.ResolveUrl(path); - } - - - /// - /// Returns a string with the data from the given element of a node. Both elements (data-fields) - /// and properties can be used - ie: - /// getItem(1, nodeName) will return a string with the name of the node with id=1 even though - /// nodeName is a property and not an element (data-field). - /// - /// Identifier for the node that should be returned - /// The element that should be returned - /// Returns a string with the data from the given element of a node - public static string GetItem(int nodeId, string alias) - { - var doc = UmbracoContext.Current.ContentCache.GetById(nodeId); - - if (doc == null) - return string.Empty; - - switch (alias) - { - case "id": - return doc.Id.ToString(); - case "parentID": - return doc.Parent.Id.ToString(); - case "level": - return doc.Level.ToString(); - case "writerID": - return doc.WriterId.ToString(); - case "template": - return doc.TemplateId.ToString(); - case "sortOrder": - return doc.SortOrder.ToString(); - case "createDate": - return doc.CreateDate.ToString("yyyy-MM-dd'T'HH:mm:ss"); - case "updateDate": - return doc.UpdateDate.ToString("yyyy-MM-dd'T'HH:mm:ss"); - case "nodeName": - return doc.Name; - case "writerName": - return doc.WriterName; - case "path": - return doc.Path; - case "creatorName": - return doc.CreatorName; - } - - // in 4.9.0 the method returned the raw XML from the cache, unparsed - // starting with 5c20f4f (4.10?) the method returns prop.Value.ToString() - // where prop.Value is parsed for internal links + resolve urls - but not for macros - // comments say "fixing U4-917 and U4-821" which are not related - // if we return DataValue.ToString() we're back to the original situation - // if we return Value.ToString() we'll have macros parsed and that's nice - // - // so, use Value.ToString() here. - var prop = doc.GetProperty(alias); - return prop == null ? string.Empty : prop.GetValue().ToString(); - } - - /// - /// Returns a string with the data from the given element of the current node. Both elements (data-fields) - /// and properties can be used - ie: - /// getItem(nodeName) will return a string with the name of the current node/page even though - /// nodeName is a property and not an element (data-field). - /// - /// - /// - public static string GetItem(string alias) - { - try - { - int currentID = int.Parse(HttpContext.Current.Items["pageID"].ToString()); - return GetItem(currentID, alias); - } - catch (Exception ItemException) - { - HttpContext.Current.Trace.Warn("library.GetItem", "Error reading '" + alias + "'", ItemException); - return string.Empty; - } - } - - /// - /// Get a media object as an xml object - /// - /// The identifier of the media object to be returned - /// If true, children of the media object is returned - /// An umbraco xml node of the media (same format as a document node) - public static XPathNodeIterator GetMedia(int MediaId, bool deep) - { - try - { - if (UmbracoConfig.For.UmbracoSettings().Content.UmbracoLibraryCacheDuration > 0) - { - var xml = Current.ApplicationCache.RuntimeCache.GetCacheItem( - $"{CacheKeys.MediaCacheKey}_{MediaId}_{deep}", - timeout: TimeSpan.FromSeconds(UmbracoConfig.For.UmbracoSettings().Content.UmbracoLibraryCacheDuration), - getCacheItem: () => GetMediaDo(MediaId, deep).Item1); - - if (xml != null) - { - //returning the root element of the Media item fixes the problem - return xml.CreateNavigator().Select("/"); - } - - } - else - { - var xml = GetMediaDo(MediaId, deep).Item1; - - //returning the root element of the Media item fixes the problem - return xml.CreateNavigator().Select("/"); - } - } - catch(Exception ex) - { - Current.Logger.Error("An error occurred looking up media", ex); - } - - Current.Logger.Debug(() => $"No media result for id {MediaId}"); - - var errorXml = new XElement("error", string.Format("No media is maching '{0}'", MediaId)); - return errorXml.CreateNavigator().Select("/"); - } - - private static Tuple GetMediaDo(int mediaId, bool deep) - { - var media = Current.Services.MediaService.GetById(mediaId); - if (media == null) return null; - - var serialized = EntityXmlSerializer.Serialize( - Current.Services.MediaService, - Current.Services.DataTypeService, - Current.Services.UserService, - Current.Services.LocalizationService, - Current.UrlSegmentProviders, - media, - deep); - return Tuple.Create(serialized, media.Path); - } - - /// - /// Get a member as an xml object - /// - /// The identifier of the member object to be returned - /// An umbraco xml node of the member (same format as a document node), but with two additional attributes on the "node" element: - /// "email" and "loginName". - /// - public static XPathNodeIterator GetMember(int MemberId) - { - try - { - if (UmbracoConfig.For.UmbracoSettings().Content.UmbracoLibraryCacheDuration > 0) - { - var xml = Current.ApplicationCache.RuntimeCache.GetCacheItem( - string.Format( - "{0}_{1}", CacheKeys.MemberLibraryCacheKey, MemberId), - timeout: TimeSpan.FromSeconds(UmbracoConfig.For.UmbracoSettings().Content.UmbracoLibraryCacheDuration), - getCacheItem: () => GetMemberDo(MemberId)); - - if (xml != null) - { - return xml.CreateNavigator().Select("/"); - } - } - else - { - return GetMemberDo(MemberId).CreateNavigator().Select("/"); - } - } - catch (Exception ex) - { - Current.Logger.Error("An error occurred looking up member", ex); - } - - Current.Logger.Debug(() => $"No member result for id {MemberId}"); - - var xd = new XmlDocument(); - xd.LoadXml(string.Format("No member is maching '{0}'", MemberId)); - return xd.CreateNavigator().Select("/"); - } - - private static XElement GetMemberDo(int MemberId) - { - var member = Current.Services.MemberService.GetById(MemberId); - if (member == null) return null; - - var serialized = EntityXmlSerializer.Serialize( - Current.Services.DataTypeService, Current.Services.LocalizationService, member); - return serialized; - } - - /// - /// Whether or not the current user is logged in (as a member) - /// - /// True is the current user is logged in - public static bool IsLoggedOn() - { - return GetUmbracoHelper().MemberIsLoggedOn(); - } - - public static XPathNodeIterator AllowedGroups(int documentId, string path) - { - XmlDocument xd = new XmlDocument(); - xd.LoadXml(""); - foreach (string role in GetAccessingMembershipRoles(documentId, path)) - xd.DocumentElement.AppendChild(XmlHelper.AddTextNode(xd, "role", role)); - return xd.CreateNavigator().Select("."); - } - - private static string[] GetAccessingMembershipRoles(int documentId, string path) - { - var entry = Current.Services.PublicAccessService.GetEntryForContent(path.EnsureEndsWith("," + documentId)); - if (entry == null) return new string[] { }; - - var memberGroupRoleRules = entry.Rules.Where(x => x.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType); - return memberGroupRoleRules.Select(x => x.RuleValue).ToArray(); - - } - - /// - /// Check if a document object is protected by the "Protect Pages" functionality in umbraco - /// - /// The identifier of the document object to check - /// The full path of the document object to check - /// True if the document object is protected - public static bool IsProtected(int DocumentId, string Path) - { - return GetUmbracoHelper().IsProtected(DocumentId, Path); - } - - /// - /// Check if the current user has access to a document - /// - /// The identifier of the document object to check - /// The full path of the document object to check - /// True if the current user has access or if the current document isn't protected - public static bool HasAccess(int NodeId, string Path) - { - return GetUmbracoHelper().MemberHasAccess(NodeId, Path); - } - - /// - /// Generates a hash based on the text string passed in. This method will detect the - /// security requirements (is FIPS enabled) and return an appropriate hash. - /// - /// The text to create a hash from - /// hash of the string - public static string CreateHash(string text) - { - return text.GenerateHash(); - } - - /// - /// Compare two dates - /// - /// The first date to compare - /// The second date to compare - /// True if the first date is greater than the second date - public static bool DateGreaterThan(string firstDate, string secondDate) - { - if (DateTime.Parse(firstDate) > DateTime.Parse(secondDate)) - return true; - else - return false; - } - - /// - /// Compare two dates - /// - /// The first date to compare - /// The second date to compare - /// True if the first date is greater than or equal the second date - public static bool DateGreaterThanOrEqual(string firstDate, string secondDate) - { - if (DateTime.Parse(firstDate) >= DateTime.Parse(secondDate)) - return true; - else - return false; - } - - /// - /// Check if a date is greater than today - /// - /// The date to check - /// True if the date is greater that today (ie. at least the day of tomorrow) - public static bool DateGreaterThanToday(string firstDate) - { - DateTime first = DateTime.Parse(firstDate); - first = new DateTime(first.Year, first.Month, first.Day); - DateTime today = new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day); - TimeSpan TS = new TimeSpan(first.Ticks - today.Ticks); - if (TS.Days > 0) - return true; - else - return false; - } - - /// - /// Check if a date is greater than or equal today - /// - /// The date to check - /// True if the date is greater that or equal today (ie. at least today or the day of tomorrow) - public static bool DateGreaterThanOrEqualToday(string firstDate) - { - DateTime first = DateTime.Parse(firstDate); - first = new DateTime(first.Year, first.Month, first.Day); - DateTime today = new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day); - TimeSpan TS = new TimeSpan(first.Ticks - today.Ticks); - if (TS.Days >= 0) - return true; - else - return false; - } - - /// - /// Get the current date - /// - /// Current date i xml format (ToString("s")) - public static string CurrentDate() - { - return DateTime.Now.ToString("s"); - } - - /// - /// Add a value to a date - /// - /// The Date to user - /// The type to add: "y": year, "m": month, "d": day, "h": hour, "min": minutes, "s": seconds - /// An integer value to add - /// A date in xml format (ToString("s")) - public static string DateAdd(string Date, string AddType, int add) - { - return DateAddWithDateTimeObject(DateTime.Parse(Date), AddType, add); - } - - /// - /// Get the day of week from a date matching the current culture settings - /// - /// The date to use - /// A string with the DayOfWeek matching the current contexts culture settings - public static string GetWeekDay(string Date) - { - return DateTime.Parse(Date).ToString("dddd"); - } - - /// - /// Add a value to a date. Similar to the other overload, but uses a datetime object instead of a string - /// - /// The Date to user - /// The type to add: "y": year, "m": month, "d": day, "h": hour, "min": minutes, "s": seconds - /// An integer value to add - /// A date in xml format (ToString("s")) - public static string DateAddWithDateTimeObject(DateTime Date, string AddType, int add) - { - switch (AddType.ToLower()) - { - case "y": - Date = Date.AddYears(add); - break; - case "m": - Date = Date.AddMonths(add); - break; - case "d": - Date = Date.AddDays(add); - break; - case "h": - Date = Date.AddHours(add); - break; - case "min": - Date = Date.AddMinutes(add); - break; - case "s": - Date = Date.AddSeconds(add); - break; - } - - return Date.ToString("s"); - } - - /// - /// Return the difference between 2 dates, in either minutes, seconds or years. - /// - /// The first date. - /// The second date. - /// format to return, can only be: s,m or y: s = seconds, m = minutes, y = years. - /// A timespan as a integer - public static int DateDiff(string firstDate, string secondDate, string diffType) - { - TimeSpan TS = DateTime.Parse(firstDate).Subtract(DateTime.Parse(secondDate)); - - switch (diffType.ToLower()) - { - case "m": - return Convert.ToInt32(TS.TotalMinutes); - case "s": - return Convert.ToInt32(TS.TotalSeconds); - case "y": - return Convert.ToInt32(TS.TotalDays / 365); - } - // return default - return 0; - } - - /// - /// Formats a string to the specified formate. - /// - /// The date. - /// The format, compatible with regular .net date formats - /// A date in the new format as a string - public static string FormatDateTime(string Date, string Format) - { - DateTime result; - if (DateTime.TryParse(Date, out result)) - return result.ToString(Format); - return string.Empty; - } - - /// - /// Converts a string to Long Date and returns it as a string - /// - /// The date. - /// if set to true the date will include time. - /// The splitter between date and time. - /// A Long Date as a string. - public static string LongDate(string Date, bool WithTime, string TimeSplitter) - { - DateTime result; - if (DateTime.TryParse(Date, out result)) - { - if (WithTime) - return result.ToLongDateString() + TimeSplitter + result.ToLongTimeString(); - return result.ToLongDateString(); - } - return string.Empty; - } - - /// - /// Checks whether the Culture with the specified name exixts in the standard .net cultureInfo. - /// - /// Name of the culture. - /// - public static bool CultureExists(string cultureName) - { - CultureInfo[] ci = CultureInfo.GetCultures(CultureTypes.AllCultures); - CultureInfo c = Array.Find(ci, delegate(CultureInfo culture) { return culture.Name == cultureName; }); - return c != null; - } - - /// - /// Converts a string to datetime in the longdate with day name format. - /// - /// The date. - /// String between day name and date - /// if set to true the datetiem will include time. - /// String between date and time. - /// Culture name. - /// A datetime in the longdate formate with day name, as a string - public static string LongDateWithDayName(string Date, string DaySplitter, bool WithTime, string TimeSplitter, - string GlobalAlias) - { - if (!CultureExists(GlobalAlias)) - return string.Empty; - - DateTime result; - CultureInfo.GetCultureInfo(GlobalAlias); - DateTimeFormatInfo dtInfo = CultureInfo.GetCultureInfo(GlobalAlias).DateTimeFormat; - if (DateTime.TryParse(Date, dtInfo, DateTimeStyles.None, out result)) - { - if (WithTime) - return - result.ToString(dtInfo.LongDatePattern) + TimeSplitter + result.ToString(dtInfo.LongTimePattern); - return result.ToString(dtInfo.LongDatePattern); - } - return string.Empty; - } - - /// - /// Converts a string to a Long Date and returns it as a string - /// - /// The date. - /// A Long Date as a string. - public static string LongDate(string Date) - { - DateTime result; - if (DateTime.TryParse(Date, out result)) - return result.ToLongDateString(); - return string.Empty; - } - - /// - /// Converts a string to a Short Date and returns it as a string - /// - /// The date. - /// A Short Date as a string. - public static string ShortDate(string Date) - { - DateTime result; - if (DateTime.TryParse(Date, out result)) - return result.ToShortDateString(); - return string.Empty; - } - - /// - /// Converts a string to a Short Date, with a specific culture, and returns it as a string - /// - /// The date. - /// Culture name - /// A short date with a specific culture, as a string - public static string ShortDateWithGlobal(string Date, string GlobalAlias) - { - if (!CultureExists(GlobalAlias)) - return string.Empty; - - DateTime result; - if (DateTime.TryParse(Date, out result)) - { - DateTimeFormatInfo dtInfo = CultureInfo.GetCultureInfo(GlobalAlias).DateTimeFormat; - return result.ToString(dtInfo.ShortDatePattern); - } - return string.Empty; - } - - /// - /// Converts a string to a Short Date with time, with a specific culture, and returns it as a string - /// - /// The date. - /// Culture name - /// A short date withi time, with a specific culture, as a string - public static string ShortDateWithTimeAndGlobal(string Date, string GlobalAlias) - { - if (!CultureExists(GlobalAlias)) - return string.Empty; - - DateTime result; - if (DateTime.TryParse(Date, out result)) - { - DateTimeFormatInfo dtInfo = CultureInfo.GetCultureInfo(GlobalAlias).DateTimeFormat; - return result.ToString(dtInfo.ShortDatePattern) + " " + - result.ToString(dtInfo.ShortTimePattern); - } - return string.Empty; - } - - /// - /// Converts a datetime string to the ShortTime format. - /// - /// The date. - /// - public static string ShortTime(string Date) - { - DateTime result; - if (DateTime.TryParse(Date, out result)) - return result.ToShortTimeString(); - return string.Empty; - } - - /// - /// Converts a datetime string to the ShortDate format. - /// - /// The date. - /// if set to true the date will include time. - /// String dividing date and time - /// - public static string ShortDate(string Date, bool WithTime, string TimeSplitter) - { - DateTime result; - if (DateTime.TryParse(Date, out result)) - { - if (WithTime) - return result.ToShortDateString() + TimeSplitter + result.ToLongTimeString(); - return result.ToShortDateString(); - } - return string.Empty; - } - - /// - /// Replaces text line breaks with html line breaks - /// - /// The text. - /// The text with text line breaks replaced with html linebreaks (
    )
    - public static string ReplaceLineBreaks(string text) - { - return GetUmbracoHelper().ReplaceLineBreaksForHtml(text); - } - - /// - /// Renders a template. - /// - /// The page id. - /// The template id. - /// The rendered template as a string - public static string RenderTemplate(int PageId, int TemplateId) - { - using (var sw = new StringWriter()) - { - try - { - var altTemplate = TemplateId == -1 ? null : (int?)TemplateId; - var templateRenderer = new TemplateRenderer(Umbraco.Web.UmbracoContext.Current, PageId, altTemplate); - templateRenderer.Render(sw); - } - catch (Exception ee) - { - sw.Write("", PageId, ee); - } - - return sw.ToString(); - } - } - - /// - /// Renders the default template for a specific page. - /// - /// The page id. - /// The rendered template as a string. - public static string RenderTemplate(int PageId) - { - return RenderTemplate(PageId, -1); - } - - /// - /// Registers the client script block. - /// - /// The key. - /// The script. - /// if set to true [add script tags]. - public static void RegisterClientScriptBlock(string key, string script, bool addScriptTags) - { - Page p = HttpContext.Current.CurrentHandler as Page; - - if (p != null) - p.ClientScript.RegisterClientScriptBlock(p.GetType(), key, script, addScriptTags); - } - - /// - /// Registers the client script include. - /// - /// The key. - /// The URL. - public static void RegisterStyleSheetFile(string key, string url) - { - Page p = HttpContext.Current.CurrentHandler as Page; - - if (p != null) - { - System.Web.UI.HtmlControls.HtmlGenericControl include = new System.Web.UI.HtmlControls.HtmlGenericControl("link"); - include.ID = key; - include.Attributes.Add("rel", "stylesheet"); - include.Attributes.Add("type", "text/css"); - include.Attributes.Add("href", url); - - if (p.Header != null) - { - if (p.Header.FindControl(key) == null) - { - p.Header.Controls.Add(include); - } - } - else - { - //This is a fallback in case there is no header - p.ClientScript.RegisterClientScriptBlock(p.GetType(), key, ""); - } - } - } - - /// - /// Registers the client script include. - /// - /// The key. - /// The URL. - public static void RegisterJavaScriptFile(string key, string url) - { - Page p = HttpContext.Current.CurrentHandler as Page; - - if (p != null) - { - - if (ClientDependency.Core.Controls.ClientDependencyLoader.Instance == null) - { - System.Web.UI.HtmlControls.HtmlGenericControl include = new System.Web.UI.HtmlControls.HtmlGenericControl("script"); - include.ID = key; - include.Attributes.Add("type", "text/javascript"); - include.Attributes.Add("src", url); - - if (p.Header != null) - { - if (p.Header.FindControl(key) == null) - { - p.Header.Controls.Add(include); - } - } - else - { - //This is a fallback in case there is no header - p.ClientScript.RegisterClientScriptInclude(p.GetType(), key, url); - } - } - else - { - ClientDependency.Core.Controls.ClientDependencyLoader.Instance.RegisterDependency(url, ClientDependency.Core.ClientDependencyType.Javascript); - } - } - } - - /// - /// Adds a reference to the jQuery javascript file from the client/ui folder using "jQuery" as a key - /// Recommended to use instead of RegisterJavaScriptFile for all nitros/packages that uses jQuery - /// - public static void AddJquery() - { - RegisterJavaScriptFile("jQuery", String.Format("{0}/ui/jquery.js", IOHelper.ResolveUrl(SystemDirectories.UmbracoClient))); - } - - - /// - /// Strips all html from a string. - /// - /// The text. - /// Returns the string without any html tags. - public static string StripHtml(string text) - { - string pattern = @"<(.|\n)*?>"; - return Regex.Replace(text, pattern, string.Empty); - } - - /// - /// Truncates a string if it's too long - /// - /// The text to eventually truncate - /// The maximum number of characters (length) - /// String to append if text is truncated (ie "...") - /// A truncated string if text if longer than MaxLength appended with the addString parameters. If text is shorter - /// then MaxLength then the full - non-truncated - string is returned - public static string TruncateString(string Text, int MaxLength, string AddString) - { - if (Text.Length > MaxLength) - return Text.Substring(0, MaxLength - AddString.Length) + AddString; - else - return Text; - } - - /// - /// Split a string into xml elements - /// - /// The full text to spil - /// The separator - /// An XPathNodeIterator containing the substrings in the format of - public static XPathNodeIterator Split(string StringToSplit, string Separator) - { - string[] values = StringToSplit.Split(Convert.ToChar(Separator)); - XmlDocument xd = new XmlDocument(); - xd.LoadXml(""); - foreach (string id in values) - { - XmlNode node = XmlHelper.AddTextNode(xd, "value", id); - xd.DocumentElement.AppendChild(node); - } - XPathNavigator xp = xd.CreateNavigator(); - return xp.Select("/values"); - } - - /// - /// Removes the starting and ending paragraph tags in a string. - /// - /// The text. - /// Returns the string without starting and endning paragraph tags - public static string RemoveFirstParagraphTag(string text) - { - if (String.IsNullOrEmpty(text)) - return ""; - - if (text.Length > 5) - { - if (text.ToUpper().Substring(0, 3) == "

    ") - text = text.Substring(3, text.Length - 3); - if (text.ToUpper().Substring(text.Length - 4, 4) == "

    ") - text = text.Substring(0, text.Length - 4); - } - return text; - } - - /// - /// Replaces a specified value with a new one. - /// - /// The text. - /// The old value. - /// The new value. - /// - public static string Replace(string text, string oldValue, string newValue) - { - return text.Replace(oldValue, newValue); - } - - /// - /// Returns the Last index of the specified value - /// - /// The text. - /// The value. - /// Return the last index of a value as a integer - public static int LastIndexOf(string Text, string Value) - { - return Text.LastIndexOf(Value); - } - - /// - /// Gets the dictionary item with the specified key. - /// - /// The key. - /// A dictionary items value as a string. - public static string GetDictionaryItem(string Key) - { - return GetUmbracoHelper().GetDictionaryValue(Key); - } - - /// - /// Gets the current page. - /// - /// An XpathNodeIterator containing the current page as Xml. - public static XPathNodeIterator GetXmlNodeCurrent() - { - var pageId = ""; - - try - { - var nav = UmbracoContext.Current.ContentCache.CreateNavigator(); - pageId = HttpContext.Current.Items["pageID"]?.ToString(); - - if (pageId == null) - throw new NullReferenceException("pageID not found in the current HTTP context"); - - nav.MoveToId(pageId); - return nav.Select("."); - } - catch (Exception ex) - { - Current.Logger.Error($"Could not retrieve current xml node for page Id {pageId}.", ex); - } - - XmlDocument xd = new XmlDocument(); - xd.LoadXml("No current node exists"); - return xd.CreateNavigator().Select("/"); - } - - /// - /// Gets the page with the specified id. - /// - /// The id. - /// Returns the node with the specified id as xml in the form of a XPathNodeIterator - public static XPathNodeIterator GetXmlNodeById(string id) - { - var nav = GetSafeContentCache().CreateNavigator(); - - if (nav.MoveToId(id)) - return nav.Select("."); - - var xd = new XmlDocument(); - xd.LoadXml(string.Format("No published item exist with id {0}", id)); - return xd.CreateNavigator().Select("."); - } - - // legacy would access the raw XML from content.Instance ie a static thing - // now that we use a PublishedSnapshotService, we need to have a "context" to handle a cache. - // UmbracoContext does it for most cases but in some cases we might not have an - // UmbracoContext. For backward compatibility, try to do something here... - internal static PublishedContentCache GetSafeContentCache() - { - PublishedContentCache contentCache; - - if (UmbracoContext.Current != null) - { - contentCache = UmbracoContext.Current.ContentCache as PublishedContentCache; - } - else - { - var publishedSnapshot = Current.PublishedSnapshot - ?? Current.PublishedSnapshotService.CreatePublishedSnapshot(null); - contentCache = publishedSnapshot.Content as PublishedContentCache; - } - - if (contentCache == null) - throw new InvalidOperationException("Unsupported IPublishedContentCache, only the Xml one is supported."); - - return contentCache; - } - - /// - /// Queries the umbraco Xml cache with the specified Xpath query - /// - /// The XPath query - /// Returns nodes matching the xpath query as a XpathNodeIterator - public static XPathNodeIterator GetXmlNodeByXPath(string xpathQuery) - { - return GetSafeContentCache().CreateNavigator().Select(xpathQuery); - } - - /// - /// Gets the entire umbraco xml cache. - /// - /// Returns the entire umbraco Xml cache as a XPathNodeIterator - public static XPathNodeIterator GetXmlAll() - { - return GetSafeContentCache().CreateNavigator().Select("/root"); - } - - /// - /// Fetches a xml file from the specified path on the server. - /// The path can be relative ("/path/to/file.xml") or absolute ("c:\folder\file.xml") - /// - /// The path. - /// if set to true the path is relative. - /// The xml file as a XpathNodeIterator - public static XPathNodeIterator GetXmlDocument(string Path, bool Relative) - { - XmlDocument xmlDoc = new XmlDocument(); - try - { - if (Relative) - xmlDoc.Load(IOHelper.MapPath(Path)); - else - xmlDoc.Load(Path); - } - catch (Exception err) - { - xmlDoc.LoadXml(string.Format("{2}", - HttpContext.Current.Server.HtmlEncode(Path), Relative, err)); - } - XPathNavigator xp = xmlDoc.CreateNavigator(); - return xp.Select("/"); - } - - /// - /// Fetches a xml file from the specified url. - /// the Url can be a local url or even from a remote server. - /// - /// The URL. - /// The xml file as a XpathNodeIterator - public static XPathNodeIterator GetXmlDocumentByUrl(string Url) - { - XmlDocument xmlDoc = new XmlDocument(); - WebRequest request = WebRequest.Create(Url); - try - { - WebResponse response = request.GetResponse(); - Stream responseStream = response.GetResponseStream(); - XmlTextReader reader = new XmlTextReader(responseStream); - - xmlDoc.Load(reader); - - response.Close(); - responseStream.Close(); - } - catch (Exception err) - { - xmlDoc.LoadXml(string.Format("{1}", - HttpContext.Current.Server.HtmlEncode(Url), err)); - } - XPathNavigator xp = xmlDoc.CreateNavigator(); - return xp.Select("/"); - } - - /// - /// Gets the XML document by URL Cached. - /// - /// The URL. - /// The cache in seconds (so 900 would be 15 minutes). This is independent of the global cache refreshing, as it doesn't gets flushed on publishing (like the macros do) - /// - public static XPathNodeIterator GetXmlDocumentByUrl(string Url, int CacheInSeconds) - { - - object urlCache = - HttpContext.Current.Cache.Get("GetXmlDoc_" + Url); - if (urlCache != null) - return (XPathNodeIterator)urlCache; - else - { - XPathNodeIterator result = - GetXmlDocumentByUrl(Url); - - HttpContext.Current.Cache.Insert("GetXmlDoc_" + Url, - result, null, DateTime.Now.Add(new TimeSpan(0, 0, CacheInSeconds)), TimeSpan.Zero, System.Web.Caching.CacheItemPriority.Low, null); - return result; - } - - } - - /// - /// Returns the Xpath query for a node with the specified id - /// - /// The id. - /// The Xpath query for the node with the specified id as a string - public static string QueryForNode(string id) - { - var xpathQuery = string.Empty; - var preview = UmbracoContext.Current != null && UmbracoContext.Current.InPreviewMode; - var xml = GetSafeContentCache().GetXml(preview); - var elt = xml.GetElementById(id); - - if (elt == null) return xpathQuery; - - var path = elt.Attributes["path"].Value.Split((",").ToCharArray()); - for (var i = 1; i < path.Length; i++) - { - if (i > 1) - xpathQuery += "/node [@id = " + path[i] + "]"; - else - xpathQuery += " [@id = " + path[i] + "]"; - } - - return xpathQuery; - } - - /// - /// Helper function to get a value from a comma separated string. Usefull to get - /// a node identifier from a Page's path string - /// - /// The comma separated string - /// The index to be returned - /// A string with the value of the index - public static string GetNodeFromLevel(string path, int level) - { - try - { - string[] newPath = path.Split(','); - if (newPath.Length >= level) - return newPath[level].ToString(); - else - return string.Empty; - } - catch - { - return ""; - } - } - - /// - /// Sends an e-mail using the System.Net.Mail.MailMessage object - /// - /// The sender of the e-mail - /// The recipient(s) of the e-mail, add multiple email addresses by using a semicolon between them - /// E-mail subject - /// The complete content of the e-mail - /// Set to true when using Html formatted mails - public static void SendMail(string fromMail, string toMail, string subject, string body, bool isHtml) - { - try - { - var mailSender = new EmailSender(); - using (var mail = new MailMessage()) - { - mail.From = new MailAddress(fromMail.Trim()); - foreach (var mailAddress in toMail.Split(';')) - mail.To.Add(new MailAddress(mailAddress.Trim())); - mail.Subject = subject; - mail.IsBodyHtml = isHtml; - mail.Body = body; - mailSender.Send(mail); - } - } - catch (Exception ee) - { - Current.Logger.Error("umbraco.library.SendMail: Error sending mail.", ee); - } - } - - /// - /// These random methods are from Eli Robillards blog - kudos for the work :-) - /// http://weblogs.asp.net/erobillard/archive/2004/05/06/127374.aspx - /// - /// Get a Random object which is cached between requests. - /// The advantage over creating the object locally is that the .Next - /// call will always return a new value. If creating several times locally - /// with a generated seed (like millisecond ticks), the same number can be - /// returned. - /// - /// A Random object which is cached between calls. - public static Random GetRandom(int seed) - { - Random r = (Random)HttpContext.Current.Cache.Get("RandomNumber"); - if (r == null) - { - if (seed == 0) - r = new Random(); - else - r = new Random(seed); - HttpContext.Current.Cache.Insert("RandomNumber", r); - } - return r; - } - - /// - /// GetRandom with no parameters. - /// - /// A Random object which is cached between calls. - public static Random GetRandom() - { - return GetRandom(0); - } - - /// - /// Get any value from the current Request collection. Please note that there also specialized methods for - /// Querystring, Form, Servervariables and Cookie collections - /// - /// Name of the Request element to be returned - /// A string with the value of the Requested element - public static string Request(string key) - { - if (HttpContext.Current.Request[key] != null) - return HttpContext.Current.Request[key]; - else - return string.Empty; - } - - /// - /// Changes the mime type of the current page. - /// - /// The mime type (like text/xml) - public static void ChangeContentType(string MimeType) - { - if (!String.IsNullOrEmpty(MimeType)) - { - HttpContext.Current.Response.ContentType = MimeType; - } - } - - /// - /// Get any value from the current Items collection. - /// - /// Name of the Items element to be returned - /// A string with the value of the Items element - public static string ContextKey(string key) - { - if (HttpContext.Current.Items[key] != null) - return HttpContext.Current.Items[key].ToString(); - else - return string.Empty; - } - - /// - /// Get any value from the current Http Items collection - /// - /// Name of the Item element to be returned - /// A string with the value of the Requested element - public static string GetHttpItem(string key) - { - if (HttpContext.Current.Items[key] != null) - return HttpContext.Current.Items[key].ToString(); - else - return string.Empty; - } - - /// - /// Get any value from the current Form collection - /// - /// Name of the Form element to be returned - /// A string with the value of the form element - public static string RequestForm(string key) - { - if (HttpContext.Current.Request.Form[key] != null) - return HttpContext.Current.Request.Form[key]; - else - return string.Empty; - } - - /// - /// Get any value from the current Querystring collection - /// - /// Name of the querystring element to be returned - /// A string with the value of the querystring element - public static string RequestQueryString(string key) - { - if (HttpContext.Current.Request.QueryString[key] != null) - return HttpContext.Current.Request.QueryString[key]; - else - return string.Empty; - } - - /// - /// Get any value from the users cookie collection - /// - /// Name of the cookie to return - /// A string with the value of the cookie - public static string RequestCookies(string key) - { - // zb-00004 #29956 : refactor cookies handling - var value = HttpContext.Current.Request.GetCookieValue(key); - return value ?? ""; - } - - /// - /// Get any element from the server variables collection - /// - /// The key for the element to be returned - /// A string with the value of the requested element - public static string RequestServerVariables(string key) - { - if (HttpContext.Current.Request.ServerVariables[key] != null) - return HttpContext.Current.Request.ServerVariables[key]; - else - return string.Empty; - } - - /// - /// Get any element from current user session - /// - /// The key for the element to be returned - /// A string with the value of the requested element - public static string Session(string key) - { - if (HttpContext.Current.Session != null && HttpContext.Current.Session[key] != null) - return HttpContext.Current.Session[key].ToString(); - else - return string.Empty; - } - - /// - /// Returns the current ASP.NET session identifier - /// - /// The current ASP.NET session identifier - public static string SessionId() - { - if (HttpContext.Current.Session != null) - return HttpContext.Current.Session.SessionID; - else - return string.Empty; - } - - /// - /// URL-encodes a string - /// - /// The string to be encoded - /// A URL-encoded string - public static string UrlEncode(string Text) - { - return HttpUtility.UrlEncode(Text); - } - - /// - /// HTML-encodes a string - /// - /// The string to be encoded - /// A HTML-encoded string - public static string HtmlEncode(string Text) - { - return HttpUtility.HtmlEncode(Text); - } - - public static IRelation[] GetRelatedNodes(int nodeId) - { - return Current.Services.RelationService.GetByParentOrChildId(nodeId).ToArray(); - } - - /// - /// Gets the related nodes, of the node with the specified Id, as XML. - /// - /// The node id. - /// The related nodes as a XpathNodeIterator in the format: - /// - /// - /// [standard umbraco node Xml] - /// - /// - /// - public static XPathNodeIterator GetRelatedNodesAsXml(int NodeId) - { - XmlDocument xd = new XmlDocument(); - xd.LoadXml(""); - var rels = Current.Services.RelationService.GetByParentOrChildId(NodeId); - - const bool published = true; // work with published versions? - - foreach (var r in rels) - { - XmlElement n = xd.CreateElement("relation"); - n.AppendChild(XmlHelper.AddCDataNode(xd, "comment", r.Comment)); - n.Attributes.Append(XmlHelper.AddAttribute(xd, "typeId", r.RelationTypeId.ToString())); - n.Attributes.Append(XmlHelper.AddAttribute(xd, "typeName", r.RelationType.Name)); - n.Attributes.Append(XmlHelper.AddAttribute(xd, "createDate", r.CreateDate.ToString(CultureInfo.InvariantCulture))); - n.Attributes.Append(XmlHelper.AddAttribute(xd, "parentId", r.ParentId.ToString())); - n.Attributes.Append(XmlHelper.AddAttribute(xd, "childId", r.ChildId.ToString())); - - // Append the node that isn't the one we're getting the related nodes from - if (NodeId == r.ChildId) - { - var parent = Current.Services.ContentService.GetById(r.ParentId); - if (parent != null) - { - var x = EntityXmlSerializer.Serialize( - Current.Services.ContentService, - Current.Services.DataTypeService, - Current.Services.UserService, - Current.Services.LocalizationService, - Current.UrlSegmentProviders, parent, published).GetXmlNode(xd); - n.AppendChild(x); - } - } - else - { - var child = Current.Services.ContentService.GetById(r.ChildId); - if (child != null) - { - var x = EntityXmlSerializer.Serialize( - Current.Services.ContentService, - Current.Services.DataTypeService, - Current.Services.UserService, - Current.Services.LocalizationService, - Current.UrlSegmentProviders, child, published).GetXmlNode(xd); - n.AppendChild(x); - } - } - - xd.DocumentElement.AppendChild(n); - } - XPathNavigator xp = xd.CreateNavigator(); - return xp.Select("."); - } - - /// - /// Returns the identifier of the current page - /// - /// The identifier of the current page - public int PageId() - { - if (_page != null) - return _page.PageID; - else - return -1; - } - - /// - /// Returns the title of the current page - /// - /// The title of the current page - public string PageName() - { - if (_page != null) - return _page.PageName; - else - return string.Empty; - } - - /// - /// Returns any element from the currentpage including generic properties - /// - /// The name of the page element to return - /// A string with the element value - public string PageElement(string key) - { - if (_page != null) - { - if (_page.Elements[key] != null) - return _page.Elements[key].ToString(); - else - return string.Empty; - } - else - return string.Empty; - } - - - - - #endregion - - #region Template Control Mapping Functions - - /// - /// Creates an Umbraco item for the specified field of the specified node. - /// This brings the umbraco:Item element functionality to XSLT documents, - /// which enables Live Editing of XSLT generated content. - /// - /// The ID of the node to create. - /// Name of the field to create. - /// An Umbraco item. - public string Item(int nodeId, string fieldName) - { - return Item(nodeId, fieldName, null); - } - - /// - /// Creates an Umbraco item for the specified field of the specified node. - /// This brings the umbraco:Item element functionality to XSLT documents, - /// which enables Live Editing of XSLT generated content. - /// - /// The ID of the node to create. - /// Name of the field to create. - /// - /// Value that is displayed to the user, which can be different from the field value. - /// Ignored if null. - /// Inside an XSLT document, an XPath expression might be useful to generate this value, - /// analogous to the functionality of the Xslt property of an umbraco:Item element. - /// - /// An Umbraco item. - public string Item(int nodeId, string fieldName, string displayValue) - { - // require a field name - if (String.IsNullOrEmpty(fieldName)) - throw new ArgumentNullException("fieldName"); - - // encode the display value, if present, as an inline XSLT expression - // escaping is disabled, since the user can choose to set - // disable-output-escaping="yes" on the value-of element calling this function. - string xslt = displayValue == null - ? String.Empty - : string.Format("xslt=\"'{0}'\" xsltdisableescaping=\"true\"", - HttpUtility.HtmlEncode(displayValue).Replace("'", "&apos;")); - - // return a placeholder, the actual item will be created later on - // in the CreateControlsFromText method of macro - return string.Format("[[[[umbraco:Item nodeId=\"{0}\" field=\"{1}\" {2}]]]]", nodeId, fieldName, xslt); - } - - #endregion - } -} diff --git a/src/Umbraco.Web/umbraco.presentation/page.cs b/src/Umbraco.Web/umbraco.presentation/page.cs index d61d21700a..a1da34d46e 100644 --- a/src/Umbraco.Web/umbraco.presentation/page.cs +++ b/src/Umbraco.Web/umbraco.presentation/page.cs @@ -478,14 +478,14 @@ namespace umbraco { get { - if (!_inner.ContentType.Variations.HasFlag(ContentVariation.CultureNeutral)) // fixme CultureSegment? + if (!_inner.ContentType.VariesByCulture()) return NoCultureInfos; if (_cultureInfos != null) return _cultureInfos; - return _cultureInfos = _inner.PublishCultureNames - .ToDictionary(x => x.Key, x => new PublishedCultureInfo(x.Key, x.Value, _inner.GetCulturePublishDate(x.Key))); + return _cultureInfos = _inner.PublishNames + .ToDictionary(x => x.Key, x => new PublishedCultureInfo(x.Key, x.Value, _inner.GetPublishDate(x.Key) ?? DateTime.MinValue)); } } diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/XmlTree.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/XmlTree.cs index e4460bda3a..56f794caa0 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/XmlTree.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/XmlTree.cs @@ -5,12 +5,9 @@ using System.Xml.Schema; using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Web.Script.Serialization; -using Umbraco.Core.IO; using Umbraco.Web.Composing; using Umbraco.Web.UI.Pages; using Umbraco.Web._Legacy.Actions; -using Action = Umbraco.Web._Legacy.Actions.Action; namespace umbraco.cms.presentation.Trees { @@ -263,30 +260,7 @@ namespace umbraco.cms.presentation.Trees set { m_nodeType = value; } } - /// - /// Used by the content tree and flagged as true if the node is not published - /// - [Obsolete("Use the XmlTreeNode.NodeStyle object to set node styles")] - public bool? NotPublished - { - get { return m_notPublished; } - set { m_notPublished = value; } - } - /// - /// Used by the content tree and flagged as true if the node is protected - /// - [Obsolete("Use the XmlTreeNode.NodeStyle object to set node styles")] - public bool? IsProtected - { - get { return m_isProtected; } - set - { - m_isProtected = value; - if (m_isProtected.HasValue && m_isProtected.Value) - this.Style.SecureNode(); - } - } /// /// Returns the styling object used to add common styles to a node @@ -353,17 +327,6 @@ namespace umbraco.cms.presentation.Trees } } - /// - /// Dims the color of the node - /// - /// - ///This adds the class to the existing icon class as to not override anything. - /// - [Obsolete("Use XmlTreeNode.Style to style nodes. Example: myNode.Style.DimNode();")] - public void DimNode() - { - this.Style.DimNode(); - } #region IXmlSerializable Members diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/controls/ContentPicker.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/controls/ContentPicker.cs index 3a61f4dd7a..018c49a249 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/controls/ContentPicker.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/controls/ContentPicker.cs @@ -17,18 +17,6 @@ namespace umbraco.controls TreeAlias = "content"; } - [Obsolete("Use Value property instead, this simply wraps it.")] - public string Text - { - get - { - return this.Value; - } - set - { - this.Value = value; - } - } public string AppAlias { get; set; } public string TreeAlias { get; set; } diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/create/dialogHandler_temp.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/create/dialogHandler_temp.cs deleted file mode 100644 index de494494f5..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/create/dialogHandler_temp.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Web; -using Umbraco.Web.UI; -using Umbraco.Web; -using Umbraco.Web._Legacy.UI; - -namespace umbraco.presentation.create -{ - /// - /// Summary description for dialogHandler_temp. - /// - [Obsolete("This class is no longer used, it has been replaced by Umbraco.Web.UI.LegacyDialogHandler which will also eventually be deprecated")] - public class dialogHandler_temp - { - public static void Delete(string NodeType, int NodeId) - { - Delete(NodeType, NodeId, ""); - } - public static void Delete(string NodeType, int NodeId, string Text) - { - LegacyDialogHandler.Delete( - new HttpContextWrapper(HttpContext.Current), - UmbracoContext.Current.Security.CurrentUser, - NodeType, NodeId, Text); - } - - public static string Create(string NodeType, int NodeId, string Text) - { - return Create(NodeType, 0, NodeId, Text); - } - - public static string Create(string NodeType, int TypeId, int NodeId, string Text) - { - return LegacyDialogHandler.Create( - new HttpContextWrapper(HttpContext.Current), - UmbracoContext.Current.Security.CurrentUser, - NodeType, NodeId, Text, TypeId); - } - } -} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/editPackage.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/editPackage.aspx.cs index 62bec41e0f..e498d3326b 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/editPackage.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/editPackage.aspx.cs @@ -258,11 +258,6 @@ namespace umbraco.presentation.developer.packages } } - [Obsolete("This is not used")] - [EditorBrowsable(EditorBrowsableState.Never)] - protected void generateXML(object sender, EventArgs e) - { - } private void SavePackage(bool showNotification) { diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installer.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installer.aspx.cs new file mode 100644 index 0000000000..2d1f36ce40 --- /dev/null +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/Packages/installer.aspx.cs @@ -0,0 +1,100 @@ +using System; +using System.Web.UI; +using umbraco.cms.presentation.Trees; +using Umbraco.Core; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Web; +using Umbraco.Web._Legacy.Controls; +using Umbraco.Web.UI.Pages; + +namespace umbraco.presentation.developer.packages +{ + /// + /// Summary description for packager. + /// + [Obsolete("This should not be used and will be removed in v8, this is kept here only for backwards compat reasons, this page should never be rendered/used")] + public class Installer : UmbracoEnsuredPage + { + + private Control _configControl; + private readonly cms.businesslogic.packager.Installer _installer; + protected Pane pane_installing; + protected Pane pane_optional; + + public Installer() + { + CurrentApp = Constants.Applications.Developer; + _installer = new cms.businesslogic.packager.Installer(Security.CurrentUser.Id); + } + + protected void Page_Load(object sender, EventArgs e) + { + if (string.IsNullOrEmpty(Request.GetItemAsString("installing"))) + return; + + pane_optional.Visible = false; + pane_installing.Visible = true; + ProcessInstall(Request.GetItemAsString("installing")); + } + + private void ProcessInstall(string currentStep) + { + var dir = Request.GetItemAsString("dir"); + + int.TryParse(Request.GetItemAsString("pId"), out var packageId); + + switch (currentStep.ToLowerInvariant()) + { + case "custominstaller": + var customControl = Request.GetItemAsString("customControl"); + + if (customControl.IsNullOrWhiteSpace() == false) + { + pane_optional.Visible = false; + + _configControl = LoadControl(SystemDirectories.Root + customControl); + _configControl.ID = "packagerConfigControl"; + + pane_optional.Controls.Add(_configControl); + pane_optional.Visible = true; + + if (IsPostBack == false) + { + //We still need to clean everything up which is normally done in the Finished Action + PerformPostInstallCleanup(packageId, dir); + } + + } + else + { + //if the custom installer control is empty here (though it should never be because we've already checked for it previously) + //then we should run the normal FinishedAction + PerformFinishedAction(packageId, dir); + } + break; + default: + break; + } + } + + private void PerformPostInstallCleanup(int packageId, string dir) + { + _installer.InstallCleanUp(packageId, dir); + + // Update ClientDependency version + var clientDependencyConfig = new Umbraco.Core.Configuration.ClientDependencyConfiguration(Logger); + clientDependencyConfig.IncreaseVersionNumber(); + + //clear the tree cache - we'll do this here even though the browser will reload, but just in case it doesn't can't hurt. + ClientTools.ClearClientTreeCache().RefreshTree("packager"); + TreeDefinitionCollection.Instance.ReRegisterTrees(); + } + + private void PerformFinishedAction(int packageId, string dir) + { + pane_optional.Visible = false; + PerformPostInstallCleanup(packageId, dir); + } + } +} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/templateControls/Item.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/templateControls/Item.cs index a1a8491716..14b4824171 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/templateControls/Item.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/templateControls/Item.cs @@ -265,17 +265,6 @@ namespace umbraco.presentation.templateControls return FindAttribute(new AttributeCollectionAdapter(Attributes), "recursive") == "true"; } - /// - /// Determines whether field uses the API to lookup the value - /// (if a NodeId attribute is specified and is different from the current page id). - /// - /// true if API lookup is used; otherwise, false. - [Obsolete("Method never implemented", true)] - protected virtual bool FieldIsApiLookup() - { - // TODO: remove false and add security - return false; - } /// /// Gets a value indicating whether the current item is editable by the current user. diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/translation/details.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/translation/details.aspx.cs index 0e6e5e5f85..a73094d871 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/translation/details.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/translation/details.aspx.cs @@ -5,6 +5,8 @@ using Umbraco.Core; using Umbraco.Core.Services; using Umbraco.Web.Composing; using Umbraco.Web._Legacy.BusinessLogic; +using Umbraco.Web; + namespace umbraco.presentation.umbraco.translation { public partial class details : Umbraco.Web.UI.Pages.UmbracoEnsuredPage { @@ -49,7 +51,11 @@ namespace umbraco.presentation.umbraco.translation { //pp_totalWords.Text = Services.TextService.Localize("translation/totalWords"); lt = new Literal(); - lt.Text = library.ReplaceLineBreaks(t.Comment); + + + var umbHelper = new UmbracoHelper(Current.UmbracoContext, Current.Services, Current.ApplicationCache); + lt.Text = umbHelper.ReplaceLineBreaksForHtml(t.Comment); + pp_comment.Controls.Add(lt); pp_comment.Text = Services.TextService.Localize("comment"); diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/legacyAjaxCalls.asmx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/legacyAjaxCalls.asmx.cs index d1efa5feea..28ba66709a 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/legacyAjaxCalls.asmx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/webservices/legacyAjaxCalls.asmx.cs @@ -9,6 +9,7 @@ using Umbraco.Web.WebServices; using Umbraco.Core.Models.Membership; using Umbraco.Web.Composing; using Umbraco.Web._Legacy.UI; +using Umbraco.Core.Services; namespace umbraco.presentation.webservices { @@ -123,7 +124,9 @@ namespace umbraco.presentation.webservices AuthorizeRequest(true); - return library.NiceUrl(nodeId); + var umbHelper = new UmbracoHelper(Current.UmbracoContext, Current.Services, Current.ApplicationCache); + + return umbHelper.Url(nodeId); } [WebMethod] diff --git a/src/umbraco.sln b/src/umbraco.sln index 3cf4223926..e70f4b60d4 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -8,7 +8,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{2849E9D4 ProjectSection(SolutionItems) = preProject ..\build\build-bootstrap.ps1 = ..\build\build-bootstrap.ps1 ..\build\build.ps1 = ..\build\build.ps1 - ..\build\BuildDocs.ps1 = ..\build\BuildDocs.ps1 ..\NuGet.Config = ..\NuGet.Config ..\build\RevertToCleanInstall.bat = ..\build\RevertToCleanInstall.bat ..\build\RevertToEmptyInstall.bat = ..\build\RevertToEmptyInstall.bat