diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index e009ee2294..cea5859486 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -60,10 +60,10 @@ Great question! The short version goes like this:

- * **Switch to the correct branch** - switch to the v8-dev branch
+ * **Switch to the correct branch** - switch to the `v8/contrib` branch
* **Build** - build your fork of Umbraco locally as described in [building Umbraco from source code](BUILD.md)
* **Change** - make your changes, experiment, have fun, explore and learn, and don't be afraid. We welcome all contributions and will [happily give feedback](#questions)
- * **Commit** - done? Yay! 🎉 **Important:** create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-12345`. This means it's a temporary branch for the particular issue you're working on, in this case `12345`. When you have a branch, commit your changes. Don't commit to `v8/dev`, create a new branch first.
+ * **Commit** - done? Yay! 🎉 **Important:** create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-12345`. This means it's a temporary branch for the particular issue you're working on, in this case `12345`. When you have a branch, commit your changes. Don't commit to `v8/contrib`, create a new branch first.
* **Push** - great, now you can push the changes up to your fork on GitHub
* **Create pull request** - exciting! You're ready to show us your changes (or not quite ready, you just need some feedback to progress - you can now make use of GitHub's draft pull request status, detailed [here] (https://github.blog/2019-02-14-introducing-draft-pull-requests/)). GitHub has picked up on the new branch you've pushed and will offer to create a Pull Request. Click that green button and away you go.
@@ -158,7 +158,7 @@ To find the general areas for something you're looking to fix or improve, have a
### Which branch should I target for my contributions?
-We like to use [Gitflow as much as possible](https://jeffkreeftmeijer.com/git-flow/), but don't worry if you are not familiar with it. The most important thing you need to know is that when you fork the Umbraco repository, the default branch is set to something, usually `v8/dev`. If you are working on v8, this is the branch you should be targetting. For v7 contributions, please target 'v7/dev'.
+We like to use [Gitflow as much as possible](https://jeffkreeftmeijer.com/git-flow/), but don't worry if you are not familiar with it. The most important thing you need to know is that when you fork the Umbraco repository, the default branch is set to something, usually `v8/contrib`. If you are working on v8, this is the branch you should be targetting. For v7 contributions, please target 'v7/dev'.
Please note: we are no longer accepting features for v7 but will continue to merge bug fixes as and when they arise.
@@ -184,10 +184,10 @@ Then when you want to get the changes from the main repository:
```
git fetch upstream
-git rebase upstream/v8/dev
+git rebase upstream/v8/contrib
```
-In this command we're syncing with the `v8/dev` branch, but you can of course choose another one if needed.
+In this command we're syncing with the `v8/contrib` branch, but you can of course choose another one if needed.
(More info on how this works: [http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated](http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated))
diff --git a/.github/README.md b/.github/README.md
index d6d978c3d6..467ca6e5e6 100644
--- a/.github/README.md
+++ b/.github/README.md
@@ -1,4 +1,4 @@
-# [Umbraco CMS](https://umbraco.com) · [](../LICENSE.md) [](https://umbraco.visualstudio.com/Umbraco%20Cms/_build?definitionId=75) [](CONTRIBUTING.md) [](https://twitter.com/intent/follow?screen_name=umbraco)
+# [Umbraco CMS](https://umbraco.com) · [](../LICENSE.md) [](https://umbraco.visualstudio.com/Umbraco%20Cms/_build?definitionId=75) [](CONTRIBUTING.md) [](https://twitter.com/intent/follow?screen_name=umbraco)
Umbraco is the friendliest, most flexible and fastest growing ASP.NET CMS, and used by more than 500,000 websites worldwide. Our mission is to help you deliver delightful digital experiences by making Umbraco friendly, simpler and social.
diff --git a/.github/img/defaultbranch.png b/.github/img/defaultbranch.png
index f3a5b9efbc..3550b5c34c 100644
Binary files a/.github/img/defaultbranch.png and b/.github/img/defaultbranch.png differ
diff --git a/src/Umbraco.Abstractions/Constants-AppSettings.cs b/src/Umbraco.Abstractions/Constants-AppSettings.cs
index 2217863122..4c47f12ba0 100644
--- a/src/Umbraco.Abstractions/Constants-AppSettings.cs
+++ b/src/Umbraco.Abstractions/Constants-AppSettings.cs
@@ -9,6 +9,12 @@ namespace Umbraco.Core
///
public static class AppSettings
{
+ public const string MainDomLock = "Umbraco.Core.MainDom.Lock";
+
+ // TODO: Kill me - still used in Umbraco.Core.IO.SystemFiles:27
+ [Obsolete("We need to kill this appsetting as we do not use XML content cache umbraco.config anymore due to NuCache")]
+ public const string ContentXML = "Umbraco.Core.ContentXML"; //umbracoContentXML
+
///
/// TODO: FILL ME IN
///
diff --git a/src/Umbraco.Abstractions/ContentVariationExtensions.cs b/src/Umbraco.Abstractions/ContentVariationExtensions.cs
index a1b374f3c8..1c34a61c3a 100644
--- a/src/Umbraco.Abstractions/ContentVariationExtensions.cs
+++ b/src/Umbraco.Abstractions/ContentVariationExtensions.cs
@@ -12,126 +12,260 @@ namespace Umbraco.Core
///
/// Determines whether the content type is invariant.
///
+ /// The content type.
+ ///
+ /// A value indicating whether the content type is invariant.
+ ///
public static bool VariesByNothing(this ISimpleContentType contentType) => contentType.Variations.VariesByNothing();
- ///
- /// Determines whether the content type varies by culture.
- ///
- public static bool VariesByCulture(this ISimpleContentType contentType) => contentType.Variations.VariesByCulture();
-
- ///
- /// Determines whether the content type varies by segment.
- ///
- public static bool VariesBySegment(this ISimpleContentType contentType) => contentType.Variations.VariesBySegment();
-
///
/// Determines whether the content type is invariant.
///
+ /// The content type.
+ ///
+ /// A value indicating whether the content type is invariant.
+ ///
public static bool VariesByNothing(this IContentTypeBase 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 IContentTypeBase 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 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 IPropertyType 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 IPropertyType 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 IPropertyType propertyType) => propertyType.Variations.VariesBySegment();
-
- ///
- /// Determines whether the property type varies by culture and segment.
- ///
- public static bool VariesByCultureAndSegment(this IPropertyType propertyType) => propertyType.Variations.VariesByCultureAndSegment();
-
///
/// Determines whether the content type is invariant.
///
+ /// The content type.
+ ///
+ /// A value indicating whether the content type is invariant.
+ ///
public static bool VariesByNothing(this IPublishedContentType contentType) => contentType.Variations.VariesByNothing();
///
- /// Determines whether the content type varies by culture.
+ /// Determines whether the property type is invariant.
///
- /// And then it could also vary by segment.
- public static bool VariesByCulture(this IPublishedContentType 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 IPublishedContentType contentType) => contentType.Variations.VariesBySegment();
-
- ///
- /// Determines whether the content type varies by culture and segment.
- ///
- public static bool VariesByCultureAndSegment(this IPublishedContentType contentType) => contentType.Variations.VariesByCultureAndSegment();
+ /// The property type.
+ ///
+ /// A value indicating whether the property type is invariant.
+ ///
+ public static bool VariesByNothing(this IPropertyType propertyType) => propertyType.Variations.VariesByNothing();
///
/// Determines whether the property type is invariant.
///
+ /// The property type.
+ ///
+ /// A value indicating whether the property type is invariant.
+ ///
public static bool VariesByNothing(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByNothing();
- ///
- /// Determines whether the property type varies by culture.
- ///
- public static bool VariesByCulture(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByCulture();
-
- ///
- /// Determines whether the property type varies by segment.
- ///
- public static bool VariesBySegment(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesBySegment();
-
- ///
- /// Determines whether the property type varies by culture and segment.
- ///
- public static bool VariesByCultureAndSegment(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByCultureAndSegment();
-
///
/// Determines whether a variation is invariant.
///
+ /// The variation.
+ ///
+ /// A value indicating whether the variation is invariant.
+ ///
public static bool VariesByNothing(this ContentVariation variation) => variation == ContentVariation.Nothing;
+ ///
+ /// Determines whether the content type varies by culture.
+ ///
+ /// The content type.
+ ///
+ /// A value indicating whether the content type varies by culture.
+ ///
+ public static bool VariesByCulture(this ISimpleContentType contentType) => contentType.Variations.VariesByCulture();
+
+ ///
+ /// Determines whether the content type varies by culture.
+ ///
+ /// The content type.
+ ///
+ /// A value indicating whether the content type varies by culture.
+ ///
+ public static bool VariesByCulture(this IContentTypeBase contentType) => contentType.Variations.VariesByCulture();
+
+ ///
+ /// Determines whether the content type varies by culture.
+ ///
+ /// The content type.
+ ///
+ /// A value indicating whether the content type varies by culture.
+ ///
+ public static bool VariesByCulture(this IPublishedContentType contentType) => contentType.Variations.VariesByCulture();
+
+ ///
+ /// Determines whether the property type varies by culture.
+ ///
+ /// The property type.
+ ///
+ /// A value indicating whether the property type varies by culture.
+ ///
+ public static bool VariesByCulture(this IPropertyType propertyType) => propertyType.Variations.VariesByCulture();
+
+ ///
+ /// Determines whether the property type varies by culture.
+ ///
+ /// The property type.
+ ///
+ /// A value indicating whether the property type varies by culture.
+ ///
+ public static bool VariesByCulture(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByCulture();
+
///
/// Determines whether a variation varies by culture.
///
- /// And then it could also vary by segment.
+ /// The variation.
+ ///
+ /// A value indicating whether the variation varies by culture.
+ ///
public static bool VariesByCulture(this ContentVariation variation) => (variation & ContentVariation.Culture) > 0;
+ ///
+ /// Determines whether the content type varies by segment.
+ ///
+ /// The content type.
+ ///
+ /// A value indicating whether the content type varies by segment.
+ ///
+ public static bool VariesBySegment(this ISimpleContentType contentType) => contentType.Variations.VariesBySegment();
+
+ ///
+ /// Determines whether the content type varies by segment.
+ ///
+ /// The content type.
+ ///
+ /// A value indicating whether the content type varies by segment.
+ ///
+ public static bool VariesBySegment(this IContentTypeBase contentType) => contentType.Variations.VariesBySegment();
+
+ ///
+ /// Determines whether the content type varies by segment.
+ ///
+ /// The content type.
+ ///
+ /// A value indicating whether the content type varies by segment.
+ ///
+ public static bool VariesBySegment(this IPublishedContentType contentType) => contentType.Variations.VariesBySegment();
+
+ ///
+ /// Determines whether the property type varies by segment.
+ ///
+ /// The property type.
+ ///
+ /// A value indicating whether the property type varies by segment.
+ ///
+ public static bool VariesBySegment(this IPropertyType propertyType) => propertyType.Variations.VariesBySegment();
+
+ ///
+ /// Determines whether the property type varies by segment.
+ ///
+ /// The property type.
+ ///
+ /// A value indicating whether the property type varies by segment.
+ ///
+ public static bool VariesBySegment(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesBySegment();
+
///
/// Determines whether a variation varies by segment.
///
- /// And then it could also vary by culture.
+ /// The variation.
+ ///
+ /// A value indicating whether the variation varies by segment.
+ ///
public static bool VariesBySegment(this ContentVariation variation) => (variation & ContentVariation.Segment) > 0;
+ ///
+ /// Determines whether the content type varies by culture and segment.
+ ///
+ /// The content type.
+ ///
+ /// A value indicating whether the content type varies by culture and segment.
+ ///
+ public static bool VariesByCultureAndSegment(this ISimpleContentType contentType) => contentType.Variations.VariesByCultureAndSegment();
+
+ ///
+ /// Determines whether the content type varies by culture and segment.
+ ///
+ /// The content type.
+ ///
+ /// A value indicating whether the content type varies by culture and segment.
+ ///
+ public static bool VariesByCultureAndSegment(this IContentTypeBase contentType) => contentType.Variations.VariesByCultureAndSegment();
+
+ ///
+ /// Determines whether the content type varies by culture and segment.
+ ///
+ /// The content type.
+ ///
+ /// A value indicating whether the content type varies by culture and segment.
+ ///
+ public static bool VariesByCultureAndSegment(this IPublishedContentType contentType) => contentType.Variations.VariesByCultureAndSegment();
+
+ ///
+ /// Determines whether the property type varies by culture and segment.
+ ///
+ /// The property type.
+ ///
+ /// A value indicating whether the property type varies by culture and segment.
+ ///
+ public static bool VariesByCultureAndSegment(this IPropertyType propertyType) => propertyType.Variations.VariesByCultureAndSegment();
+
+ ///
+ /// Determines whether the property type varies by culture and segment.
+ ///
+ /// The property type.
+ ///
+ /// A value indicating whether the property type varies by culture and segment.
+ ///
+ public static bool VariesByCultureAndSegment(this IPublishedPropertyType propertyType) => propertyType.Variations.VariesByCultureAndSegment();
+
///
/// Determines whether a variation varies by culture and segment.
///
+ /// The variation.
+ ///
+ /// A value indicating whether the variation varies by culture and segment.
+ ///
public static bool VariesByCultureAndSegment(this ContentVariation variation) => (variation & ContentVariation.CultureAndSegment) == ContentVariation.CultureAndSegment;
+ ///
+ /// Sets or removes the content type variation depending on the specified value.
+ ///
+ /// The content type.
+ /// The variation to set or remove.
+ /// If set to true sets the variation; otherwise, removes the variation.
+ ///
+ /// This method does not support setting the variation to nothing.
+ ///
+ public static void SetVariesBy(this IContentTypeBase contentType, ContentVariation variation, bool value = true) => contentType.Variations = contentType.Variations.SetFlag(variation, value);
+
+ ///
+ /// Sets or removes the property type variation depending on the specified value.
+ ///
+ /// The property type.
+ /// The variation to set or remove.
+ /// If set to true sets the variation; otherwise, removes the variation.
+ ///
+ /// This method does not support setting the variation to nothing.
+ ///
+ public static void SetVariesBy(this IPropertyType propertyType, ContentVariation variation, bool value = true) => propertyType.Variations = propertyType.Variations.SetFlag(variation, value);
+
+ ///
+ /// Returns the variations with the variation set or removed depending on the specified value.
+ ///
+ /// The existing variations.
+ /// The variation to set or remove.
+ /// If set to true sets the variation; otherwise, removes the variation.
+ ///
+ /// The variations with the variation set or removed.
+ ///
+ ///
+ /// This method does not support setting the variation to nothing.
+ ///
+ public static ContentVariation SetFlag(this ContentVariation variations, ContentVariation variation, bool value = true)
+ {
+ return value
+ ? variations | variation // Set flag using bitwise logical OR
+ : variations & ~variation; // Remove flag using bitwise logical AND with bitwise complement (reversing the bit)
+ }
+
///
/// Validates that a combination of culture and segment is valid for the variation.
///
@@ -140,16 +274,18 @@ namespace Umbraco.Core
/// 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.
+ /// A value indicating whether to throw a when the combination is invalid.
+ ///
+ /// true if the combination is valid; otherwise false.
+ ///
+ /// Occurs when the combination is invalid, and is true.
///
/// 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".
+ /// 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)
{
culture = culture.NullOrWhiteSpaceAsNull();
@@ -171,6 +307,7 @@ namespace Umbraco.Core
{
if (throwIfInvalid)
throw new NotSupportedException($"Culture may not be null because culture variation is enabled.");
+
return false;
}
}
@@ -183,6 +320,7 @@ namespace Umbraco.Core
{
if (throwIfInvalid)
throw new NotSupportedException($"Culture \"{culture}\" is invalid because culture variation is disabled.");
+
return false;
}
}
@@ -195,6 +333,7 @@ namespace Umbraco.Core
{
if (throwIfInvalid)
throw new NotSupportedException($"Segment \"{segment}\" is invalid because segment variation is disabled.");
+
return false;
}
diff --git a/src/Umbraco.Abstractions/EnumExtensions.cs b/src/Umbraco.Abstractions/EnumExtensions.cs
index b2e5f32c9a..9097432f64 100644
--- a/src/Umbraco.Abstractions/EnumExtensions.cs
+++ b/src/Umbraco.Abstractions/EnumExtensions.cs
@@ -3,81 +3,42 @@
namespace Umbraco.Core
{
///
- /// Provides extension methods to enums.
+ /// Provides extension methods to .
///
public static class EnumExtensions
{
- // note:
- // - no need to HasFlagExact, that's basically an == test
- // - HasFlagAll cannot be named HasFlag because ext. methods never take priority over instance methods
-
///
- /// Determines whether a flag enum has all the specified values.
+ /// Determines whether all the flags/bits are set within the enum value.
///
- ///
- /// True when all bits set in are set in , though other bits may be set too.
- /// This is the behavior of the original method.
- ///
- public static bool HasFlagAll(this T use, T uses)
+ /// The enum type.
+ /// The enum value.
+ /// The flags.
+ ///
+ /// true if all the flags/bits are set within the enum value; otherwise, false.
+ ///
+ [Obsolete("Use Enum.HasFlag() or bitwise operations (if performance is important) instead.")]
+ public static bool HasFlagAll(this T value, T flags)
where T : Enum
{
- var num = Convert.ToUInt64(use);
- var nums = Convert.ToUInt64(uses);
-
- return (num & nums) == nums;
+ return value.HasFlag(flags);
}
///
- /// Determines whether a flag enum has any of the specified values.
+ /// Determines whether any of the flags/bits are set within the enum value.
///
- ///
- /// True when at least one of the bits set in is set in .
- ///
- public static bool HasFlagAny(this T use, T uses)
+ /// The enum type.
+ /// The value.
+ /// The flags.
+ ///
+ /// true if any of the flags/bits are set within the enum value; otherwise, false.
+ ///
+ public static bool HasFlagAny(this T value, T flags)
where T : Enum
{
- var num = Convert.ToUInt64(use);
- var nums = Convert.ToUInt64(uses);
+ var v = Convert.ToUInt64(value);
+ var f = Convert.ToUInt64(flags);
- return (num & nums) > 0;
- }
-
- ///
- /// Sets a flag of the given input enum
- ///
- ///
- /// Enum to set flag of
- /// Flag to set
- /// A new enum with the flag set
- public static T SetFlag(this T input, T flag)
- where T : Enum
- {
- var i = Convert.ToUInt64(input);
- var f = Convert.ToUInt64(flag);
-
- // bitwise OR to set flag f of enum i
- var result = i | f;
-
- return (T)Enum.ToObject(typeof(T), result);
- }
-
- ///
- /// Unsets a flag of the given input enum
- ///
- ///
- /// Enum to unset flag of
- /// Flag to unset
- /// A new enum with the flag unset
- public static T UnsetFlag(this T input, T flag)
- where T : Enum
- {
- var i = Convert.ToUInt64(input);
- var f = Convert.ToUInt64(flag);
-
- // bitwise AND combined with bitwise complement to unset flag f of enum i
- var result = i & ~f;
-
- return (T)Enum.ToObject(typeof(T), result);
+ return (v & f) > 0;
}
}
}
diff --git a/src/Umbraco.Abstractions/MediaTypeExtensions.cs b/src/Umbraco.Abstractions/MediaTypeExtensions.cs
new file mode 100644
index 0000000000..4e2ae5822a
--- /dev/null
+++ b/src/Umbraco.Abstractions/MediaTypeExtensions.cs
@@ -0,0 +1,10 @@
+namespace Umbraco.Core.Models
+{
+ internal static class MediaTypeExtensions
+ {
+ internal static bool IsSystemMediaType(this IMediaType mediaType) =>
+ mediaType.Alias == Constants.Conventions.MediaTypes.File
+ || mediaType.Alias == Constants.Conventions.MediaTypes.Folder
+ || mediaType.Alias == Constants.Conventions.MediaTypes.Image;
+ }
+}
diff --git a/src/Umbraco.Abstractions/Persistence/Constants-Locks.cs b/src/Umbraco.Abstractions/Persistence/Constants-Locks.cs
index 1dcd2408e7..e64f40ced7 100644
--- a/src/Umbraco.Abstractions/Persistence/Constants-Locks.cs
+++ b/src/Umbraco.Abstractions/Persistence/Constants-Locks.cs
@@ -8,6 +8,11 @@ namespace Umbraco.Core
///
public static class Locks
{
+ ///
+ /// The lock
+ ///
+ public const int MainDom = -1000;
+
///
/// All servers.
///
diff --git a/src/Umbraco.Abstractions/PropertyEditors/MultiUrlPickerConfiguration.cs b/src/Umbraco.Abstractions/PropertyEditors/MultiUrlPickerConfiguration.cs
index 73a098cc9d..239569478f 100644
--- a/src/Umbraco.Abstractions/PropertyEditors/MultiUrlPickerConfiguration.cs
+++ b/src/Umbraco.Abstractions/PropertyEditors/MultiUrlPickerConfiguration.cs
@@ -16,6 +16,9 @@ namespace Umbraco.Web.PropertyEditors
Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")]
public bool IgnoreUserStartNodes { get; set; }
-
+ [ConfigurationField("hideAnchor",
+ "Hide anchor/query string input", "boolean",
+ Description = "Selecting this hides the anchor/query string input field in the linkpicker overlay.")]
+ public bool HideAnchor { get; set; }
}
}
diff --git a/src/Umbraco.Abstractions/IMainDom.cs b/src/Umbraco.Abstractions/Runtime/IMainDom.cs
similarity index 96%
rename from src/Umbraco.Abstractions/IMainDom.cs
rename to src/Umbraco.Abstractions/Runtime/IMainDom.cs
index 31b2e2eee0..444fc1c7d0 100644
--- a/src/Umbraco.Abstractions/IMainDom.cs
+++ b/src/Umbraco.Abstractions/Runtime/IMainDom.cs
@@ -1,5 +1,6 @@
using System;
+// TODO: Can't change namespace due to breaking changes, change in netcore
namespace Umbraco.Core
{
///
diff --git a/src/Umbraco.Abstractions/Runtime/IMainDomLock.cs b/src/Umbraco.Abstractions/Runtime/IMainDomLock.cs
new file mode 100644
index 0000000000..6a62f48194
--- /dev/null
+++ b/src/Umbraco.Abstractions/Runtime/IMainDomLock.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Umbraco.Core.Runtime
+{
+ ///
+ /// An application-wide distributed lock
+ ///
+ ///
+ /// Disposing releases the lock
+ ///
+ public interface IMainDomLock : IDisposable
+ {
+ ///
+ /// Acquires an application-wide distributed lock
+ ///
+ ///
+ ///
+ /// An awaitable boolean value which will be false if the elapsed millsecondsTimeout value is exceeded
+ ///
+ Task AcquireLockAsync(int millisecondsTimeout);
+
+ ///
+ /// Wait on a background thread to receive a signal from another AppDomain
+ ///
+ ///
+ Task ListenAsync();
+ }
+}
diff --git a/src/Umbraco.Infrastructure/MainDom.cs b/src/Umbraco.Abstractions/Runtime/MainDom.cs
similarity index 76%
rename from src/Umbraco.Infrastructure/MainDom.cs
rename to src/Umbraco.Abstractions/Runtime/MainDom.cs
index be962beffc..2c56852095 100644
--- a/src/Umbraco.Infrastructure/MainDom.cs
+++ b/src/Umbraco.Abstractions/Runtime/MainDom.cs
@@ -6,8 +6,9 @@ using System.Threading;
using Umbraco.Core.Hosting;
using Umbraco.Core.Logging;
-namespace Umbraco.Core
+namespace Umbraco.Core.Runtime
{
+
///
/// Provides the full implementation of .
///
@@ -21,18 +22,11 @@ namespace Umbraco.Core
private readonly ILogger _logger;
private readonly IHostingEnvironment _hostingEnvironment;
+ private readonly IMainDomLock _mainDomLock;
// our own lock for local consistency
private object _locko = new object();
- // async lock representing the main domain lock
- private readonly SystemLock _systemLock;
- private IDisposable _systemLocker;
-
- // event wait handle used to notify current main domain that it should
- // release the lock because a new domain wants to be the main domain
- private readonly EventWaitHandle _signal;
-
private bool _isInitialized;
// indicates whether...
private bool _isMainDom; // we are the main domain
@@ -41,41 +35,20 @@ namespace Umbraco.Core
// actions to run before releasing the main domain
private readonly List> _callbacks = new List>();
- private const int LockTimeoutMilliseconds = 90000; // (1.5 * 60 * 1000) == 1 min 30 seconds
+ private const int LockTimeoutMilliseconds = 40000; // 40 seconds
#endregion
#region Ctor
// initializes a new instance of MainDom
- public MainDom(ILogger logger, IHostingEnvironment hostingEnvironment)
+ public MainDom(ILogger logger, IHostingEnvironment hostingEnvironment, IMainDomLock systemLock)
{
-
hostingEnvironment.RegisterObject(this);
_logger = logger;
_hostingEnvironment = hostingEnvironment;
-
- // HostingEnvironment.ApplicationID is null in unit tests, making ReplaceNonAlphanumericChars fail
- var appId = hostingEnvironment.ApplicationId?.ReplaceNonAlphanumericChars(string.Empty);
-
- // combining with the physical path because if running on eg IIS Express,
- // two sites could have the same appId even though they are different.
- //
- // now what could still collide is... two sites, running in two different processes
- // and having the same appId, and running on the same app physical path
- //
- // we *cannot* use the process ID here because when an AppPool restarts it is
- // a new process for the same application path
-
- var appPath = hostingEnvironment.ApplicationPhysicalPath?.ToLowerInvariant() ?? string.Empty;
- var hash = (appId + ":::" + appPath).GenerateHash();
-
- var lockName = "UMBRACO-" + hash + "-MAINDOM-LCK";
- _systemLock = new SystemLock(lockName);
-
- var eventName = "UMBRACO-" + hash + "-MAINDOM-EVT";
- _signal = new EventWaitHandle(false, EventResetMode.AutoReset, eventName);
+ _mainDomLock = systemLock;
}
#endregion
@@ -144,13 +117,14 @@ namespace Umbraco.Core
continue;
}
}
+
_logger.Debug("Stopped ({SignalSource})", source);
}
finally
{
// in any case...
_isMainDom = false;
- _systemLocker?.Dispose();
+ _mainDomLock.Dispose();
_logger.Info("Released ({SignalSource})", source);
}
@@ -170,37 +144,34 @@ namespace Umbraco.Core
_logger.Info("Acquiring.");
- // signal other instances that we want the lock, then wait one the lock,
- // which may timeout, and this is accepted - see comments below
+ // Get the lock
+ var acquired = _mainDomLock.AcquireLockAsync(LockTimeoutMilliseconds).GetAwaiter().GetResult();
- // signal, then wait for the lock, then make sure the event is
- // reset (maybe there was noone listening..)
- _signal.Set();
+ if (!acquired)
+ {
+ _logger.Info("Cannot acquire (timeout).");
- // if more than 1 instance reach that point, one will get the lock
- // and the other one will timeout, which is accepted
+ // In previous versions we'd let a TimeoutException be thrown
+ // and the appdomain would not start. We have the opportunity to allow it to
+ // start without having MainDom? This would mean that it couldn't write
+ // to nucache/examine and would only be ok if this was a super short lived appdomain.
+ // maybe safer to just keep throwing in this case.
+
+ throw new TimeoutException("Cannot acquire MainDom");
+ // return false;
+ }
- //This can throw a TimeoutException - in which case should this be in a try/finally to ensure the signal is always reset.
try
{
- _systemLocker = _systemLock.Lock(LockTimeoutMilliseconds);
+ // Listen for the signal from another AppDomain coming online to release the lock
+ _mainDomLock.ListenAsync().ContinueWith(_ => OnSignal("signal"));
}
- finally
+ catch (OperationCanceledException ex)
{
- // we need to reset the event, because otherwise we would end up
- // signaling ourselves and committing suicide immediately.
- // only 1 instance can reach that point, but other instances may
- // have started and be trying to get the lock - they will timeout,
- // which is accepted
-
- _signal.Reset();
+ // the waiting task could be canceled if this appdomain is naturally shutting down, we'll just swallow this exception
+ _logger.Warn(ex, ex.Message);
}
- //WaitOneAsync (ext method) will wait for a signal without blocking the main thread, the waiting is done on a background thread
-
- _signal.WaitOneAsync()
- .ContinueWith(_ => OnSignal("signal"));
-
_logger.Info("Acquired.");
return true;
}
@@ -208,6 +179,10 @@ namespace Umbraco.Core
///
/// Gets a value indicating whether the current domain is the main domain.
///
+ ///
+ /// The lazy initializer call will only call the Acquire callback when it's not been initialized, else it will just return
+ /// the value from _isMainDom which means when we set _isMainDom to false again after being signaled, this will return false;
+ ///
public bool IsMainDom => LazyInitializer.EnsureInitialized(ref _isMainDom, ref _isInitialized, ref _locko, () => Acquire());
// IRegisteredObject
@@ -233,8 +208,7 @@ namespace Umbraco.Core
{
if (disposing)
{
- _signal?.Close();
- _signal?.Dispose();
+ _mainDomLock.Dispose();
}
disposedValue = true;
@@ -247,5 +221,25 @@ namespace Umbraco.Core
}
#endregion
+
+ public static string GetMainDomId(IHostingEnvironment hostingEnvironment)
+ {
+ // HostingEnvironment.ApplicationID is null in unit tests, making ReplaceNonAlphanumericChars fail
+ var appId = hostingEnvironment.ApplicationId?.ReplaceNonAlphanumericChars(string.Empty) ?? string.Empty;
+
+ // combining with the physical path because if running on eg IIS Express,
+ // two sites could have the same appId even though they are different.
+ //
+ // now what could still collide is... two sites, running in two different processes
+ // and having the same appId, and running on the same app physical path
+ //
+ // we *cannot* use the process ID here because when an AppPool restarts it is
+ // a new process for the same application path
+
+ var appPath = hostingEnvironment.ApplicationPhysicalPath?.ToLowerInvariant() ?? string.Empty;
+ var hash = (appId + ":::" + appPath).GenerateHash();
+
+ return hash;
+ }
}
}
diff --git a/src/Umbraco.Abstractions/Services/IContentTypeServiceBase.cs b/src/Umbraco.Abstractions/Services/IContentTypeServiceBase.cs
index 51e5d756eb..6ed3c85e91 100644
--- a/src/Umbraco.Abstractions/Services/IContentTypeServiceBase.cs
+++ b/src/Umbraco.Abstractions/Services/IContentTypeServiceBase.cs
@@ -39,6 +39,11 @@ namespace Umbraco.Core.Services
int Count();
+ ///
+ /// Returns true or false depending on whether content nodes have been created based on the provided content type id.
+ ///
+ bool HasContentNodes(int id);
+
IEnumerable GetAll(params int[] ids);
IEnumerable GetAll(IEnumerable ids);
diff --git a/src/Umbraco.Abstractions/Umbraco.Abstractions.csproj b/src/Umbraco.Abstractions/Umbraco.Abstractions.csproj
index 2eff63c4b8..fb5d256c95 100644
--- a/src/Umbraco.Abstractions/Umbraco.Abstractions.csproj
+++ b/src/Umbraco.Abstractions/Umbraco.Abstractions.csproj
@@ -27,6 +27,7 @@
+
diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs
index bd0b733623..da6574670b 100644
--- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs
@@ -155,6 +155,8 @@ namespace Umbraco.Core.Migrations.Install
_database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.Domains, Name = "Domains" });
_database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.KeyValues, Name = "KeyValues" });
_database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.Languages, Name = "Languages" });
+
+ _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.MainDom, Name = "MainDom" });
}
private void CreateContentTypeData()
diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs
index 2bb9b404f4..67dfe346d6 100644
--- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs
@@ -186,13 +186,12 @@ namespace Umbraco.Core.Migrations.Upgrade
To("{0372A42B-DECF-498D-B4D1-6379E907EB94}");
To("{5B1E0D93-F5A3-449B-84BA-65366B84E2D4}");
- // to 8.5.0...
+ // to 8.6.0...
To("{4759A294-9860-46BC-99F9-B4C975CAE580}");
To("{0BC866BC-0665-487A-9913-0290BD0169AD}");
-
- // to 8.6.0
To("{3D67D2C8-5E65-47D0-A9E1-DC2EE0779D6B}");
To("{EE288A91-531B-4995-8179-1D62D9AA3E2E}");
+ To("{2AB29964-02A1-474D-BD6B-72148D2A53A2}");
//FINAL
}
diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddMainDomLock.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddMainDomLock.cs
new file mode 100644
index 0000000000..6ca493ac7e
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddMainDomLock.cs
@@ -0,0 +1,16 @@
+using Umbraco.Core.Persistence.Dtos;
+
+namespace Umbraco.Core.Migrations.Upgrade.V_8_6_0
+{
+ public class AddMainDomLock : MigrationBase
+ {
+ public AddMainDomLock(IMigrationContext context)
+ : base(context)
+ { }
+
+ public override void Migrate()
+ {
+ Database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.MainDom, Name = "MainDom" });
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddPropertyTypeValidationMessageColumns.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddPropertyTypeValidationMessageColumns.cs
index 30eb30109e..f44695da69 100644
--- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddPropertyTypeValidationMessageColumns.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_6_0/AddPropertyTypeValidationMessageColumns.cs
@@ -3,6 +3,7 @@ using Umbraco.Core.Persistence.Dtos;
namespace Umbraco.Core.Migrations.Upgrade.V_8_6_0
{
+
public class AddPropertyTypeValidationMessageColumns : MigrationBase
{
public AddPropertyTypeValidationMessageColumns(IMigrationContext context)
diff --git a/src/Umbraco.Infrastructure/Models/BackOfficeTour.cs b/src/Umbraco.Infrastructure/Models/BackOfficeTour.cs
index 7391765193..7396d3d00d 100644
--- a/src/Umbraco.Infrastructure/Models/BackOfficeTour.cs
+++ b/src/Umbraco.Infrastructure/Models/BackOfficeTour.cs
@@ -40,5 +40,8 @@ namespace Umbraco.Web.Models
[DataMember(Name = "culture")]
public string Culture { get; set; }
+
+ [DataMember(Name = "contentType")]
+ public string ContentType { get; set; }
}
}
diff --git a/src/Umbraco.Infrastructure/Models/ContentEditing/MediaTypeDisplay.cs b/src/Umbraco.Infrastructure/Models/ContentEditing/MediaTypeDisplay.cs
index 40227184db..ea0393336c 100644
--- a/src/Umbraco.Infrastructure/Models/ContentEditing/MediaTypeDisplay.cs
+++ b/src/Umbraco.Infrastructure/Models/ContentEditing/MediaTypeDisplay.cs
@@ -5,6 +5,7 @@ namespace Umbraco.Web.Models.ContentEditing
[DataContract(Name = "contentType", Namespace = "")]
public class MediaTypeDisplay : ContentTypeCompositionDisplay
{
-
+ [DataMember(Name = "isSystemMediaType")]
+ public bool IsSystemMediaType { get; set; }
}
}
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/IContentTypeRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/IContentTypeRepositoryBase.cs
index 69b0698a96..254e04d2d5 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/IContentTypeRepositoryBase.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/IContentTypeRepositoryBase.cs
@@ -26,5 +26,10 @@ namespace Umbraco.Core.Persistence.Repositories
///
///
bool HasContainerInPath(string contentPath);
+
+ ///
+ /// Returns true or false depending on whether content nodes have been created based on the provided content type id.
+ ///
+ bool HasContentNodes(int id);
}
}
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs
index 8890a859a8..b716a121be 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs
@@ -1326,6 +1326,17 @@ WHERE {Constants.DatabaseSchema.Tables.Content}.nodeId IN (@ids) AND cmsContentT
return Database.ExecuteScalar(sql) > 0;
}
+ ///
+ /// Returns true or false depending on whether content nodes have been created based on the provided content type id.
+ ///
+ public bool HasContentNodes(int id)
+ {
+ var sql = new Sql(
+ $"SELECT CASE WHEN EXISTS (SELECT * FROM {Constants.DatabaseSchema.Tables.Content} WHERE contentTypeId = @id) THEN 1 ELSE 0 END",
+ new { id });
+ return Database.ExecuteScalar(sql) == 1;
+ }
+
protected override IEnumerable GetDeleteClauses()
{
// in theory, services should have ensured that content items of the given content type
diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs
index 37375ef25d..a478c88412 100644
--- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs
+++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs
@@ -251,6 +251,11 @@ where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName)
}
public override void WriteLock(IDatabase db, params int[] lockIds)
+ {
+ WriteLock(db, TimeSpan.FromMilliseconds(1800), lockIds);
+ }
+
+ public void WriteLock(IDatabase db, TimeSpan timeout, params int[] lockIds)
{
// soon as we get Database, a transaction is started
@@ -261,7 +266,7 @@ where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName)
// *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks
foreach (var lockId in lockIds)
{
- db.Execute(@"SET LOCK_TIMEOUT 1800;");
+ db.Execute($"SET LOCK_TIMEOUT {timeout.TotalMilliseconds};");
var i = db.Execute(@"UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id", new { id = lockId });
if (i == 0) // ensure we are actually locking!
throw new ArgumentException($"LockObject with id={lockId} does not exist.");
diff --git a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs
index 26b71b1325..8e4401495d 100644
--- a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs
+++ b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs
@@ -167,6 +167,18 @@ namespace Umbraco.Core.Runtime
// type finder/loader
var typeLoader = new TypeLoader(IOHelper, TypeFinder, appCaches.RuntimeCache, new DirectoryInfo(HostingEnvironment.LocalTempPath), ProfilingLogger);
+ // runtime state
+ // beware! must use '() => _factory.GetInstance()' and NOT '_factory.GetInstance'
+ // as the second one captures the current value (null) and therefore fails
+ _state = new RuntimeState(Logger,
+ Configs.Settings(), Configs.Global(),
+ new Lazy(() => _factory.GetInstance()),
+ new Lazy(() => _factory.GetInstance()),
+ UmbracoVersion, HostingEnvironment, BackOfficeInfo)
+ {
+ Level = RuntimeLevel.Boot
+ };
+
// create the composition
composition = new Composition(register, typeLoader, ProfilingLogger, _state, Configs, IOHelper, appCaches);
composition.RegisterEssentials(Logger, Profiler, ProfilingLogger, MainDom, appCaches, databaseFactory, typeLoader, _state, TypeFinder, IOHelper, UmbracoVersion, DbProviderFactoryCreator);
diff --git a/src/Umbraco.Infrastructure/Runtime/MainDomSemaphoreLock.cs b/src/Umbraco.Infrastructure/Runtime/MainDomSemaphoreLock.cs
new file mode 100644
index 0000000000..419a1781a2
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Runtime/MainDomSemaphoreLock.cs
@@ -0,0 +1,96 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Umbraco.Core.Hosting;
+using Umbraco.Core.Logging;
+
+namespace Umbraco.Core.Runtime
+{
+ ///
+ /// Uses a system-wide Semaphore and EventWaitHandle to synchronize the current AppDomain
+ ///
+ public class MainDomSemaphoreLock : IMainDomLock
+ {
+ private readonly SystemLock _systemLock;
+
+ // event wait handle used to notify current main domain that it should
+ // release the lock because a new domain wants to be the main domain
+ private readonly EventWaitHandle _signal;
+ private readonly ILogger _logger;
+ private IDisposable _lockRelease;
+
+ public MainDomSemaphoreLock(ILogger logger, IHostingEnvironment hostingEnvironment)
+ {
+ var lockName = "UMBRACO-" + MainDom.GetMainDomId(hostingEnvironment) + "-MAINDOM-LCK";
+ _systemLock = new SystemLock(lockName);
+
+ var eventName = "UMBRACO-" + MainDom.GetMainDomId(hostingEnvironment) + "-MAINDOM-EVT";
+ _signal = new EventWaitHandle(false, EventResetMode.AutoReset, eventName);
+ _logger = logger;
+ }
+
+ //WaitOneAsync (ext method) will wait for a signal without blocking the main thread, the waiting is done on a background thread
+ public Task ListenAsync() => _signal.WaitOneAsync();
+
+ public Task AcquireLockAsync(int millisecondsTimeout)
+ {
+ // signal other instances that we want the lock, then wait on the lock,
+ // which may timeout, and this is accepted - see comments below
+
+ // signal, then wait for the lock, then make sure the event is
+ // reset (maybe there was noone listening..)
+ _signal.Set();
+
+ // if more than 1 instance reach that point, one will get the lock
+ // and the other one will timeout, which is accepted
+
+ //This can throw a TimeoutException - in which case should this be in a try/finally to ensure the signal is always reset.
+ try
+ {
+ _lockRelease = _systemLock.Lock(millisecondsTimeout);
+ return Task.FromResult(true);
+ }
+ catch (TimeoutException ex)
+ {
+ _logger.Error(ex);
+ return Task.FromResult(false);
+ }
+ finally
+ {
+ // we need to reset the event, because otherwise we would end up
+ // signaling ourselves and committing suicide immediately.
+ // only 1 instance can reach that point, but other instances may
+ // have started and be trying to get the lock - they will timeout,
+ // which is accepted
+
+ _signal.Reset();
+ }
+ }
+
+ #region IDisposable Support
+ private bool disposedValue = false; // To detect redundant calls
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposedValue)
+ {
+ if (disposing)
+ {
+ _lockRelease?.Dispose();
+ _signal.Close();
+ _signal.Dispose();
+ }
+
+ disposedValue = true;
+ }
+ }
+
+ // This code added to correctly implement the disposable pattern.
+ public void Dispose()
+ {
+ // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
+ Dispose(true);
+ }
+ #endregion
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs b/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs
new file mode 100644
index 0000000000..4e1feb221a
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs
@@ -0,0 +1,416 @@
+using System;
+using System.Data;
+using System.Data.SqlClient;
+using System.Diagnostics;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Umbraco.Core.Configuration;
+using Umbraco.Core.Logging;
+using Umbraco.Core.Persistence;
+using Umbraco.Core.Persistence.Dtos;
+using Umbraco.Core.Persistence.Mappers;
+using Umbraco.Core.Persistence.SqlSyntax;
+
+namespace Umbraco.Core.Runtime
+{
+ public class SqlMainDomLock : IMainDomLock
+ {
+ private string _lockId;
+ private const string MainDomKey = "Umbraco.Core.Runtime.SqlMainDom";
+ private const string UpdatedSuffix = "_updated";
+ private readonly ILogger _logger;
+ private IUmbracoDatabase _db;
+ private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
+ private SqlServerSyntaxProvider _sqlServerSyntax = new SqlServerSyntaxProvider();
+ private bool _mainDomChanging = false;
+ private readonly UmbracoDatabaseFactory _dbFactory;
+ private bool _hasError;
+ private object _locker = new object();
+
+ public SqlMainDomLock(ILogger logger, Configs configs, IDbProviderFactoryCreator dbProviderFactoryCreator)
+ {
+ // unique id for our appdomain, this is more unique than the appdomain id which is just an INT counter to its safer
+ _lockId = Guid.NewGuid().ToString();
+ _logger = logger;
+ _dbFactory = new UmbracoDatabaseFactory(
+ Constants.System.UmbracoConnectionName,
+ _logger,
+ new Lazy(() => new MapperCollection(Enumerable.Empty())),
+ configs, dbProviderFactoryCreator
+ );
+ }
+
+ public async Task AcquireLockAsync(int millisecondsTimeout)
+ {
+ if (!(_dbFactory.SqlContext.SqlSyntax is SqlServerSyntaxProvider sqlServerSyntaxProvider))
+ throw new NotSupportedException("SqlMainDomLock is only supported for Sql Server");
+
+ _sqlServerSyntax = sqlServerSyntaxProvider;
+
+ _logger.Debug("Acquiring lock...");
+
+ var db = GetDatabase();
+
+ var tempId = Guid.NewGuid().ToString();
+
+ try
+ {
+ db.BeginTransaction(IsolationLevel.ReadCommitted);
+
+ try
+ {
+ // wait to get a write lock
+ _sqlServerSyntax.WriteLock(db, TimeSpan.FromMilliseconds(millisecondsTimeout), Constants.Locks.MainDom);
+ }
+ catch (Exception ex)
+ {
+ if (IsLockTimeoutException(ex))
+ {
+ _logger.Error(ex, "Sql timeout occurred, could not acquire MainDom.");
+ _hasError = true;
+ return false;
+ }
+
+ // unexpected (will be caught below)
+ throw;
+ }
+
+ var result = InsertLockRecord(tempId); //we change the row to a random Id to signal other MainDom to shutdown
+ if (result == RecordPersistenceType.Insert)
+ {
+ // if we've inserted, then there was no MainDom so we can instantly acquire
+
+ // TODO: see the other TODO, could we just delete the row and that would indicate that we
+ // are MainDom? then we don't leave any orphan rows behind.
+
+ InsertLockRecord(_lockId); // so update with our appdomain id
+ _logger.Debug("Acquired with ID {LockId}", _lockId);
+ return true;
+ }
+
+ // if we've updated, this means there is an active MainDom, now we need to wait to
+ // for the current MainDom to shutdown which also requires releasing our write lock
+ }
+ catch (Exception ex)
+ {
+ ResetDatabase();
+ // unexpected
+ _logger.Error(ex, "Unexpected error, cannot acquire MainDom");
+ _hasError = true;
+ return false;
+ }
+ finally
+ {
+ db?.CompleteTransaction();
+ }
+
+ return await WaitForExistingAsync(tempId, millisecondsTimeout);
+ }
+
+ public Task ListenAsync()
+ {
+ if (_hasError)
+ {
+ _logger.Warn("Could not acquire MainDom, listening is canceled.");
+ return Task.CompletedTask;
+ }
+
+ // Create a long running task (dedicated thread)
+ // to poll to check if we are still the MainDom registered in the DB
+ return Task.Factory.StartNew(ListeningLoop, _cancellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
+
+ }
+
+ private void ListeningLoop()
+ {
+ while (true)
+ {
+ // poll every 1 second
+ Thread.Sleep(1000);
+
+ lock (_locker)
+ {
+ // If cancellation has been requested we will just exit. Depending on timing of the shutdown,
+ // we will have already flagged _mainDomChanging = true, or we're shutting down faster than
+ // the other MainDom is taking to startup. In this case the db row will just be deleted and the
+ // new MainDom will just take over.
+ if (_cancellationTokenSource.IsCancellationRequested)
+ return;
+
+ var db = GetDatabase();
+
+ try
+ {
+ db.BeginTransaction(IsolationLevel.ReadCommitted);
+
+ // get a read lock
+ _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom);
+
+ // TODO: We could in theory just check if the main dom row doesn't exist, that could indicate that
+ // we are still the maindom. An empty value might be better because then we won't have any orphan rows
+ // if the app is terminated. Could that work?
+
+ if (!IsMainDomValue(_lockId))
+ {
+ // we are no longer main dom, another one has come online, exit
+ _mainDomChanging = true;
+ _logger.Debug("Detected new booting application, releasing MainDom lock.");
+ return;
+ }
+ }
+ catch (Exception ex)
+ {
+ ResetDatabase();
+ // unexpected
+ _logger.Error(ex, "Unexpected error, listening is canceled.");
+ _hasError = true;
+ return;
+ }
+ finally
+ {
+ db?.CompleteTransaction();
+ }
+ }
+
+ }
+ }
+
+ private void ResetDatabase()
+ {
+ if (_db.InTransaction)
+ _db.AbortTransaction();
+ _db.Dispose();
+ _db = null;
+ }
+
+ private IUmbracoDatabase GetDatabase()
+ {
+ if (_db != null)
+ return _db;
+
+ _db = _dbFactory.CreateDatabase();
+ return _db;
+ }
+
+ ///
+ /// Wait for any existing MainDom to release so we can continue booting
+ ///
+ ///
+ ///
+ ///
+ private Task WaitForExistingAsync(string tempId, int millisecondsTimeout)
+ {
+ var updatedTempId = tempId + UpdatedSuffix;
+
+ return Task.Run(() =>
+ {
+ var db = GetDatabase();
+ var watch = new Stopwatch();
+ watch.Start();
+ while(true)
+ {
+ // poll very often, we need to take over as fast as we can
+ Thread.Sleep(100);
+
+ try
+ {
+ db.BeginTransaction(IsolationLevel.ReadCommitted);
+
+ // get a read lock
+ _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom);
+
+ // the row
+ var mainDomRows = db.Fetch("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey });
+
+ if (mainDomRows.Count == 0 || mainDomRows[0].Value == updatedTempId)
+ {
+ // the other main dom has updated our record
+ // Or the other maindom shutdown super fast and just deleted the record
+ // which indicates that we
+ // can acquire it and it has shutdown.
+
+ _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom);
+
+ // so now we update the row with our appdomain id
+ InsertLockRecord(_lockId);
+ _logger.Debug("Acquired with ID {LockId}", _lockId);
+ return true;
+ }
+ else if (mainDomRows.Count == 1 && !mainDomRows[0].Value.StartsWith(tempId))
+ {
+ // in this case, the prefixed ID is different which means
+ // another new AppDomain has come online and is wanting to take over. In that case, we will not
+ // acquire.
+
+ _logger.Debug("Cannot acquire, another booting application detected.");
+
+ return false;
+ }
+ }
+ catch (Exception ex)
+ {
+ ResetDatabase();
+
+ if (IsLockTimeoutException(ex))
+ {
+ _logger.Error(ex, "Sql timeout occurred, waiting for existing MainDom is canceled.");
+ _hasError = true;
+ return false;
+ }
+ // unexpected
+ _logger.Error(ex, "Unexpected error, waiting for existing MainDom is canceled.");
+ _hasError = true;
+ return false;
+ }
+ finally
+ {
+ db?.CompleteTransaction();
+ }
+
+ if (watch.ElapsedMilliseconds >= millisecondsTimeout)
+ {
+ // if the timeout has elapsed, it either means that the other main dom is taking too long to shutdown,
+ // or it could mean that the previous appdomain was terminated and didn't clear out the main dom SQL row
+ // and it's just been left as an orphan row.
+ // There's really know way of knowing unless we are constantly updating the row for the current maindom
+ // which isn't ideal.
+ // So... we're going to 'just' take over, if the writelock works then we'll assume we're ok
+
+ _logger.Debug("Timeout elapsed, assuming orphan row, acquiring MainDom.");
+
+ try
+ {
+ db.BeginTransaction(IsolationLevel.ReadCommitted);
+
+ _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom);
+
+ // so now we update the row with our appdomain id
+ InsertLockRecord(_lockId);
+ _logger.Debug("Acquired with ID {LockId}", _lockId);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ ResetDatabase();
+
+ if (IsLockTimeoutException(ex))
+ {
+ // something is wrong, we cannot acquire, not much we can do
+ _logger.Error(ex, "Sql timeout occurred, could not forcibly acquire MainDom.");
+ _hasError = true;
+ return false;
+ }
+ _logger.Error(ex, "Unexpected error, could not forcibly acquire MainDom.");
+ _hasError = true;
+ return false;
+ }
+ finally
+ {
+ db?.CompleteTransaction();
+ }
+ }
+ }
+ }, _cancellationTokenSource.Token);
+ }
+
+ ///
+ /// Inserts or updates the key/value row
+ ///
+ private RecordPersistenceType InsertLockRecord(string id)
+ {
+ var db = GetDatabase();
+ return db.InsertOrUpdate(new KeyValueDto
+ {
+ Key = MainDomKey,
+ Value = id,
+ Updated = DateTime.Now
+ });
+ }
+
+ ///
+ /// Checks if the DB row value is equals the value
+ ///
+ ///
+ private bool IsMainDomValue(string val)
+ {
+ var db = GetDatabase();
+ return db.ExecuteScalar("SELECT COUNT(*) FROM umbracoKeyValue WHERE [key] = @key AND [value] = @val",
+ new { key = MainDomKey, val = val }) == 1;
+ }
+
+ ///
+ /// Checks if the exception is an SQL timeout
+ ///
+ ///
+ ///
+ private bool IsLockTimeoutException(Exception exception) => exception is SqlException sqlException && sqlException.Number == 1222;
+
+ #region IDisposable Support
+ private bool _disposedValue = false; // To detect redundant calls
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposedValue)
+ {
+ if (disposing)
+ {
+ lock (_locker)
+ {
+ // immediately cancel all sub-tasks, we don't want them to keep querying
+ _cancellationTokenSource.Cancel();
+ _cancellationTokenSource.Dispose();
+
+ var db = GetDatabase();
+ try
+ {
+ db.BeginTransaction(IsolationLevel.ReadCommitted);
+
+ // get a write lock
+ _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom);
+
+ // When we are disposed, it means we have released the MainDom lock
+ // and called all MainDom release callbacks, in this case
+ // if another maindom is actually coming online we need
+ // to signal to the MainDom coming online that we have shutdown.
+ // To do that, we update the existing main dom DB record with a suffixed "_updated" string.
+ // Otherwise, if we are just shutting down, we want to just delete the row.
+ if (_mainDomChanging)
+ {
+ _logger.Debug("Releasing MainDom, updating row, new application is booting.");
+ db.Execute($"UPDATE umbracoKeyValue SET [value] = [value] + '{UpdatedSuffix}' WHERE [key] = @key", new { key = MainDomKey });
+ }
+ else
+ {
+ _logger.Debug("Releasing MainDom, deleting row, application is shutting down.");
+ db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey });
+ }
+ }
+ catch (Exception ex)
+ {
+ ResetDatabase();
+ _logger.Error(ex, "Unexpected error during dipsose.");
+ _hasError = true;
+ }
+ finally
+ {
+ db?.CompleteTransaction();
+ ResetDatabase();
+ }
+ }
+ }
+
+ _disposedValue = true;
+ }
+ }
+
+ // This code added to correctly implement the disposable pattern.
+ public void Dispose()
+ {
+ // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
+ Dispose(true);
+ }
+ #endregion
+
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Scoping/IScopeProvider.cs b/src/Umbraco.Infrastructure/Scoping/IScopeProvider.cs
index 4a7ccae481..dce6658f16 100644
--- a/src/Umbraco.Infrastructure/Scoping/IScopeProvider.cs
+++ b/src/Umbraco.Infrastructure/Scoping/IScopeProvider.cs
@@ -13,7 +13,7 @@ namespace Umbraco.Core.Scoping
/// Provides scopes.
///
public interface IScopeProvider
- {
+ {
///
/// Creates an ambient scope.
///
diff --git a/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs
index 45baf9720f..50f12ba73e 100644
--- a/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs
+++ b/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs
@@ -369,6 +369,15 @@ namespace Umbraco.Core.Services.Implement
}
}
+ public bool HasContentNodes(int id)
+ {
+ using (var scope = ScopeProvider.CreateScope(autoComplete: true))
+ {
+ scope.ReadLock(ReadLockIds);
+ return Repository.HasContentNodes(id);
+ }
+ }
+
#endregion
#region Save
diff --git a/src/Umbraco.TestData/SegmentTestController.cs b/src/Umbraco.TestData/SegmentTestController.cs
index 33badbbb55..650820760e 100644
--- a/src/Umbraco.TestData/SegmentTestController.cs
+++ b/src/Umbraco.TestData/SegmentTestController.cs
@@ -30,9 +30,8 @@ namespace Umbraco.TestData
if (ct.Variations.VariesBySegment())
return Content($"The document type {alias} already allows segments, nothing has been changed");
- ct.Variations = ct.Variations.SetFlag(ContentVariation.Segment);
-
- propType.Variations = propType.Variations.SetFlag(ContentVariation.Segment);
+ ct.SetVariesBy(ContentVariation.Segment);
+ propType.SetVariesBy(ContentVariation.Segment);
Services.ContentTypeService.Save(ct);
return Content($"The document type {alias} and property type {propertyTypeAlias} now allows segments");
@@ -50,7 +49,7 @@ namespace Umbraco.TestData
if (!ct.VariesBySegment())
return Content($"The document type {alias} does not allow segments, nothing has been changed");
- ct.Variations = ct.Variations.UnsetFlag(ContentVariation.Segment);
+ ct.SetVariesBy(ContentVariation.Segment, false);
Services.ContentTypeService.Save(ct);
return Content($"The document type {alias} no longer allows segments");
diff --git a/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs b/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs
index b435af9e77..00ba721bdb 100644
--- a/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs
+++ b/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs
@@ -223,7 +223,7 @@ namespace Umbraco.Tests.Cache
{
var d = new SnapDictionary();
d.Test.CollectAuto = false;
-
+
// gen 1
d.Set(1, "one");
Assert.AreEqual(1, d.Test.GetValues(1).Length);
@@ -321,7 +321,7 @@ namespace Umbraco.Tests.Cache
{
var d = new SnapDictionary();
d.Test.CollectAuto = false;
-
+
Assert.AreEqual(0, d.Test.GetValues(1).Length);
// gen 1
@@ -416,7 +416,7 @@ namespace Umbraco.Tests.Cache
{
var d = new SnapDictionary();
d.Test.CollectAuto = false;
-
+
// gen 1
d.Set(1, "one");
Assert.AreEqual(1, d.Test.GetValues(1).Length);
@@ -578,7 +578,7 @@ namespace Umbraco.Tests.Cache
{
var d = new SnapDictionary();
d.Test.CollectAuto = false;
-
+
d.Set(1, "one");
d.Set(2, "two");
@@ -689,7 +689,7 @@ namespace Umbraco.Tests.Cache
{
// gen 3
Assert.AreEqual(2, d.Test.GetValues(1).Length);
- d.Set(1, "ein");
+ d.SetLocked(1, "ein");
Assert.AreEqual(3, d.Test.GetValues(1).Length);
Assert.AreEqual(3, d.Test.LiveGen);
@@ -727,31 +727,25 @@ namespace Umbraco.Tests.Cache
using (var w1 = d.GetScopedWriteLock(scopeProvider))
{
Assert.AreEqual(1, t.LiveGen);
- Assert.AreEqual(1, t.WLocked);
+ Assert.IsTrue(t.IsLocked);
Assert.IsTrue(t.NextGen);
- using (var w2 = d.GetScopedWriteLock(scopeProvider))
+ Assert.Throws(() =>
{
- Assert.AreEqual(1, t.LiveGen);
- Assert.AreEqual(2, t.WLocked);
- Assert.IsTrue(t.NextGen);
-
- Assert.AreNotSame(w1, w2); // get a new writer each time
-
- d.Set(1, "one");
-
- Assert.AreEqual(0, d.CreateSnapshot().Gen);
- }
+ using (var w2 = d.GetScopedWriteLock(scopeProvider))
+ {
+ }
+ });
Assert.AreEqual(1, t.LiveGen);
- Assert.AreEqual(1, t.WLocked);
+ Assert.IsTrue(t.IsLocked);
Assert.IsTrue(t.NextGen);
Assert.AreEqual(0, d.CreateSnapshot().Gen);
}
Assert.AreEqual(1, t.LiveGen);
- Assert.AreEqual(0, t.WLocked);
+ Assert.IsFalse(t.IsLocked);
Assert.IsTrue(t.NextGen);
Assert.AreEqual(1, d.CreateSnapshot().Gen);
@@ -772,11 +766,14 @@ namespace Umbraco.Tests.Cache
using (var w1 = d.GetScopedWriteLock(scopeProvider))
{
+ // This one is interesting, although we don't allow recursive locks, since this is
+ // using the same ScopeContext/key, the lock acquisition is only done once
+
using (var w2 = d.GetScopedWriteLock(scopeProvider))
{
Assert.AreSame(w1, w2);
- d.Set(1, "one");
+ d.SetLocked(1, "one");
}
}
}
@@ -797,19 +794,16 @@ namespace Umbraco.Tests.Cache
using (var w1 = d.GetScopedWriteLock(scopeProvider1))
{
Assert.AreEqual(1, t.LiveGen);
- Assert.AreEqual(1, t.WLocked);
+ Assert.IsTrue(t.IsLocked);
Assert.IsTrue(t.NextGen);
- using (var w2 = d.GetScopedWriteLock(scopeProvider2))
+ Assert.Throws(() =>
{
- Assert.AreEqual(1, t.LiveGen);
- Assert.AreEqual(2, t.WLocked);
- Assert.IsTrue(t.NextGen);
+ using (var w2 = d.GetScopedWriteLock(scopeProvider2))
+ {
+ }
+ });
- Assert.AreNotSame(w1, w2);
-
- d.Set(1, "one");
- }
}
}
@@ -848,13 +842,13 @@ namespace Umbraco.Tests.Cache
Assert.IsFalse(d.Test.NextGen);
Assert.AreEqual("uno", s2.Get(1));
- var scopeProvider = GetScopeProvider();
+ var scopeProvider = GetScopeProvider();
using (d.GetScopedWriteLock(scopeProvider))
{
// gen 3
Assert.AreEqual(2, d.Test.GetValues(1).Length);
- d.Set(1, "ein");
+ d.SetLocked(1, "ein");
Assert.AreEqual(3, d.Test.GetValues(1).Length);
Assert.AreEqual(3, d.Test.LiveGen);
@@ -881,6 +875,7 @@ namespace Umbraco.Tests.Cache
{
var d = new SnapDictionary();
d.Test.CollectAuto = false;
+
// gen 1
d.Set(1, "one");
@@ -894,12 +889,11 @@ namespace Umbraco.Tests.Cache
Assert.AreEqual("uno", s2.Get(1));
var scopeProvider = GetScopeProvider();
-
using (d.GetScopedWriteLock(scopeProvider))
{
// creating a snapshot in a write-lock does NOT return the "current" content
// it uses the previous snapshot, so new snapshot created only on release
- d.Set(1, "ein");
+ d.SetLocked(1, "ein");
var s3 = d.CreateSnapshot();
Assert.AreEqual(2, s3.Gen);
Assert.AreEqual("uno", s3.Get(1));
@@ -934,12 +928,11 @@ namespace Umbraco.Tests.Cache
var scopeContext = new ScopeContext();
var scopeProvider = GetScopeProvider(scopeContext);
-
using (d.GetScopedWriteLock(scopeProvider))
{
// creating a snapshot in a write-lock does NOT return the "current" content
// it uses the previous snapshot, so new snapshot created only on release
- d.Set(1, "ein");
+ d.SetLocked(1, "ein");
var s3 = d.CreateSnapshot();
Assert.AreEqual(2, s3.Gen);
Assert.AreEqual("uno", s3.Get(1));
@@ -967,7 +960,7 @@ namespace Umbraco.Tests.Cache
var d = new SnapDictionary();
var t = d.Test;
t.CollectAuto = false;
-
+
// gen 1
d.Set(1, "one");
var s1 = d.CreateSnapshot();
@@ -984,12 +977,11 @@ namespace Umbraco.Tests.Cache
var scopeContext = new ScopeContext();
var scopeProvider = GetScopeProvider(scopeContext);
-
using (d.GetScopedWriteLock(scopeProvider))
{
// creating a snapshot in a write-lock does NOT return the "current" content
// it uses the previous snapshot, so new snapshot created only on release
- d.Set(1, "ein");
+ d.SetLocked(1, "ein");
var s3 = d.CreateSnapshot();
Assert.AreEqual(2, s3.Gen);
Assert.AreEqual("uno", s3.Get(1));
@@ -997,7 +989,7 @@ namespace Umbraco.Tests.Cache
// we made some changes, so a next gen is required
Assert.AreEqual(3, t.LiveGen);
Assert.IsTrue(t.NextGen);
- Assert.AreEqual(1, t.WLocked);
+ Assert.IsTrue(t.IsLocked);
// but live snapshot contains changes
var ls = t.LiveSnapshot;
@@ -1008,7 +1000,7 @@ namespace Umbraco.Tests.Cache
// nothing is committed until scope exits
Assert.AreEqual(3, t.LiveGen);
Assert.IsTrue(t.NextGen);
- Assert.AreEqual(1, t.WLocked);
+ Assert.IsTrue(t.IsLocked);
// no changes until exit
var s4 = d.CreateSnapshot();
@@ -1020,7 +1012,7 @@ namespace Umbraco.Tests.Cache
// now things have changed
Assert.AreEqual(2, t.LiveGen);
Assert.IsFalse(t.NextGen);
- Assert.AreEqual(0, t.WLocked);
+ Assert.IsFalse(t.IsLocked);
// no changes since not completed
var s5 = d.CreateSnapshot();
@@ -1097,9 +1089,10 @@ namespace Umbraco.Tests.Cache
// writer is scope contextual and scoped
// when disposed, nothing happens
// when the context exists, the writer is released
+
using (d.GetScopedWriteLock(scopeProvider))
{
- d.Set(1, "ein");
+ d.SetLocked(1, "ein");
Assert.IsTrue(d.Test.NextGen);
Assert.AreEqual(3, d.Test.LiveGen);
Assert.IsNotNull(d.Test.GenObj);
@@ -1107,7 +1100,7 @@ namespace Umbraco.Tests.Cache
}
// writer has not released
- Assert.AreEqual(1, d.Test.WLocked);
+ Assert.IsTrue(d.Test.IsLocked);
Assert.IsNotNull(d.Test.GenObj);
Assert.AreEqual(2, d.Test.GenObj.Gen);
@@ -1118,7 +1111,7 @@ namespace Umbraco.Tests.Cache
// panic!
var s2 = d.CreateSnapshot();
- Assert.AreEqual(1, d.Test.WLocked);
+ Assert.IsTrue(d.Test.IsLocked);
Assert.IsNotNull(d.Test.GenObj);
Assert.AreEqual(2, d.Test.GenObj.Gen);
Assert.AreEqual(3, d.Test.LiveGen);
@@ -1127,7 +1120,7 @@ namespace Umbraco.Tests.Cache
// release writer
scopeContext.ScopeExit(true);
- Assert.AreEqual(0, d.Test.WLocked);
+ Assert.IsFalse(d.Test.IsLocked);
Assert.IsNotNull(d.Test.GenObj);
Assert.AreEqual(2, d.Test.GenObj.Gen);
Assert.AreEqual(3, d.Test.LiveGen);
@@ -1135,7 +1128,7 @@ namespace Umbraco.Tests.Cache
var s3 = d.CreateSnapshot();
- Assert.AreEqual(0, d.Test.WLocked);
+ Assert.IsFalse(d.Test.IsLocked);
Assert.IsNotNull(d.Test.GenObj);
Assert.AreEqual(3, d.Test.GenObj.Gen);
Assert.AreEqual(3, d.Test.LiveGen);
@@ -1150,4 +1143,45 @@ namespace Umbraco.Tests.Cache
return scopeProvider;
}
}
+
+ ///
+ /// Used for tests so that we don't have to wrap every Set/Clear call in locks
+ ///
+ public static class SnapDictionaryExtensions
+ {
+ internal static void Set(this SnapDictionary d, TKey key, TValue value)
+ where TValue : class
+ {
+ using (d.GetScopedWriteLock(GetScopeProvider()))
+ {
+ d.SetLocked(key, value);
+ }
+ }
+
+ internal static void Clear(this SnapDictionary d)
+ where TValue : class
+ {
+ using (d.GetScopedWriteLock(GetScopeProvider()))
+ {
+ d.ClearLocked();
+ }
+ }
+
+ internal static void Clear(this SnapDictionary d, TKey key)
+ where TValue : class
+ {
+ using (d.GetScopedWriteLock(GetScopeProvider()))
+ {
+ d.ClearLocked(key);
+ }
+ }
+
+ private static IScopeProvider GetScopeProvider()
+ {
+ var scopeProvider = Mock.Of();
+ Mock.Get(scopeProvider)
+ .Setup(x => x.Context).Returns(() => null);
+ return scopeProvider;
+ }
+ }
}
diff --git a/src/Umbraco.Tests/CoreThings/EnumExtensionsTests.cs b/src/Umbraco.Tests/CoreThings/EnumExtensionsTests.cs
index 4a0c1d0f41..faa15b0077 100644
--- a/src/Umbraco.Tests/CoreThings/EnumExtensionsTests.cs
+++ b/src/Umbraco.Tests/CoreThings/EnumExtensionsTests.cs
@@ -1,6 +1,7 @@
-using NUnit.Framework;
-using Umbraco.Web.Trees;
+using System;
+using NUnit.Framework;
using Umbraco.Core;
+using Umbraco.Web.Trees;
namespace Umbraco.Tests.CoreThings
{
@@ -22,6 +23,7 @@ namespace Umbraco.Tests.CoreThings
Assert.IsFalse(value.HasFlag(test));
}
+ [Obsolete]
[TestCase(TreeUse.Dialog, TreeUse.Dialog, true)]
[TestCase(TreeUse.Dialog, TreeUse.Main, false)]
[TestCase(TreeUse.Dialog | TreeUse.Main, TreeUse.Dialog, true)]
@@ -51,47 +53,5 @@ namespace Umbraco.Tests.CoreThings
else
Assert.IsFalse(value.HasFlagAny(test));
}
-
- [TestCase(TreeUse.None, TreeUse.None, TreeUse.None)]
- [TestCase(TreeUse.None, TreeUse.Main, TreeUse.Main)]
- [TestCase(TreeUse.None, TreeUse.Dialog, TreeUse.Dialog)]
- [TestCase(TreeUse.None, TreeUse.Main | TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog)]
- [TestCase(TreeUse.Main, TreeUse.None, TreeUse.Main)]
- [TestCase(TreeUse.Main, TreeUse.Main, TreeUse.Main)]
- [TestCase(TreeUse.Main, TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog)]
- [TestCase(TreeUse.Main, TreeUse.Main | TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog)]
- [TestCase(TreeUse.Dialog, TreeUse.None, TreeUse.Dialog)]
- [TestCase(TreeUse.Dialog, TreeUse.Main, TreeUse.Main | TreeUse.Dialog)]
- [TestCase(TreeUse.Dialog, TreeUse.Dialog, TreeUse.Dialog)]
- [TestCase(TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog)]
- [TestCase(TreeUse.Main | TreeUse.Dialog, TreeUse.None, TreeUse.Main | TreeUse.Dialog)]
- [TestCase(TreeUse.Main | TreeUse.Dialog, TreeUse.Main, TreeUse.Main | TreeUse.Dialog)]
- [TestCase(TreeUse.Main | TreeUse.Dialog, TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog)]
- [TestCase(TreeUse.Main | TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog)]
- public void SetFlagTests(TreeUse value, TreeUse flag, TreeUse expected)
- {
- Assert.AreEqual(expected, value.SetFlag(flag));
- }
-
- [TestCase(TreeUse.None, TreeUse.None, TreeUse.None)]
- [TestCase(TreeUse.None, TreeUse.Main, TreeUse.None)]
- [TestCase(TreeUse.None, TreeUse.Dialog, TreeUse.None)]
- [TestCase(TreeUse.None, TreeUse.Main | TreeUse.Dialog, TreeUse.None)]
- [TestCase(TreeUse.Main, TreeUse.None, TreeUse.Main)]
- [TestCase(TreeUse.Main, TreeUse.Main, TreeUse.None)]
- [TestCase(TreeUse.Main, TreeUse.Dialog, TreeUse.Main)]
- [TestCase(TreeUse.Main, TreeUse.Main | TreeUse.Dialog, TreeUse.None)]
- [TestCase(TreeUse.Dialog, TreeUse.None, TreeUse.Dialog)]
- [TestCase(TreeUse.Dialog, TreeUse.Main, TreeUse.Dialog)]
- [TestCase(TreeUse.Dialog, TreeUse.Dialog, TreeUse.None)]
- [TestCase(TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog, TreeUse.None)]
- [TestCase(TreeUse.Main | TreeUse.Dialog, TreeUse.None, TreeUse.Main | TreeUse.Dialog)]
- [TestCase(TreeUse.Main | TreeUse.Dialog, TreeUse.Main, TreeUse.Dialog)]
- [TestCase(TreeUse.Main | TreeUse.Dialog, TreeUse.Dialog, TreeUse.Main)]
- [TestCase(TreeUse.Main | TreeUse.Dialog, TreeUse.Main | TreeUse.Dialog, TreeUse.None)]
- public void UnsetFlagTests(TreeUse value, TreeUse flag, TreeUse expected)
- {
- Assert.AreEqual(expected, value.UnsetFlag(flag));
- }
}
}
diff --git a/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedSnapshotService.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedSnapshotService.cs
index c723c14c80..2e8f52a6a7 100644
--- a/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedSnapshotService.cs
+++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedSnapshotService.cs
@@ -10,6 +10,7 @@ using Umbraco.Core.Models;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Persistence.Repositories;
+using Umbraco.Core.Runtime;
using Umbraco.Core.Scoping;
using Umbraco.Core.Services;
using Umbraco.Core.Strings;
diff --git a/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlStore.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlStore.cs
index f989fd3832..df46ab755f 100644
--- a/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlStore.cs
+++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlStore.cs
@@ -15,6 +15,7 @@ using Umbraco.Core.Models;
using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.Repositories;
using Umbraco.Core.Persistence.Repositories.Implement;
+using Umbraco.Core.Runtime;
using Umbraco.Core.Scoping;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Changes;
diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs
index 07f74e29e6..53a632132d 100644
--- a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs
+++ b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs
@@ -971,6 +971,32 @@ namespace Umbraco.Tests.Persistence.Repositories
}
}
+ [Test]
+ public void Can_Verify_Content_Type_Has_Content_Nodes()
+ {
+ // Arrange
+ var provider = TestObjects.GetScopeProvider(Logger);
+ using (var scope = provider.CreateScope())
+ {
+ ContentTypeRepository repository;
+ var contentRepository = CreateRepository((IScopeAccessor)provider, out repository);
+ var contentTypeId = NodeDto.NodeIdSeed + 1;
+ var contentType = repository.Get(contentTypeId);
+
+ // Act
+ var result = repository.HasContentNodes(contentTypeId);
+
+ var subpage = MockedContent.CreateTextpageContent(contentType, "Test Page 1", contentType.Id);
+ contentRepository.Save(subpage);
+
+ var result2 = repository.HasContentNodes(contentTypeId);
+
+ // Assert
+ Assert.That(result, Is.False);
+ Assert.That(result2, Is.True);
+ }
+ }
+
public void CreateTestData()
{
//Create and Save ContentType "umbTextpage" -> (NodeDto.NodeIdSeed)
diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs
index b03ee25e4a..5b2838df39 100644
--- a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs
+++ b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs
@@ -925,6 +925,14 @@ namespace Umbraco.Tests.PublishedContent
var snapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null);
_snapshotAccessor.PublishedSnapshot = snapshot;
+ var snapshotService = (PublishedSnapshotService)_snapshotService;
+ var contentStore = snapshotService.GetContentStore();
+
+ var parentNodes = contentStore.Test.GetValues(1);
+ var parentNode = parentNodes[0];
+ AssertLinkedNode(parentNode.contentNode, -1, -1, 2, 4, 6);
+ Assert.AreEqual(1, parentNode.gen);
+
var documents = snapshot.Content.GetAtRoot().ToArray();
AssertDocuments(documents, "N1", "N2", "N3");
@@ -941,6 +949,15 @@ namespace Umbraco.Tests.PublishedContent
new ContentCacheRefresher.JsonPayload(2, Guid.Empty, TreeChangeTypes.RefreshNode),
}, out _, out _);
+ parentNodes = contentStore.Test.GetValues(1);
+ Assert.AreEqual(2, parentNodes.Length);
+ parentNode = parentNodes[1]; // get the first gen
+ AssertLinkedNode(parentNode.contentNode, -1, -1, 2, 4, 6); // the structure should have remained the same
+ Assert.AreEqual(1, parentNode.gen);
+ parentNode = parentNodes[0]; // get the latest gen
+ AssertLinkedNode(parentNode.contentNode, -1, -1, 2, 4, 6); // the structure should have remained the same
+ Assert.AreEqual(2, parentNode.gen);
+
documents = snapshot.Content.GetAtRoot().ToArray();
AssertDocuments(documents, "N1", "N2", "N3");
@@ -949,6 +966,8 @@ namespace Umbraco.Tests.PublishedContent
documents = snapshot.Content.GetById(2).Children(_variationAccesor).ToArray();
AssertDocuments(documents, "N9", "N8", "N7");
+
+
}
[Test]
diff --git a/src/Umbraco.Tests/TestHelpers/TestHelper.cs b/src/Umbraco.Tests/TestHelpers/TestHelper.cs
index 2064051a11..eb126edcff 100644
--- a/src/Umbraco.Tests/TestHelpers/TestHelper.cs
+++ b/src/Umbraco.Tests/TestHelpers/TestHelper.cs
@@ -22,6 +22,7 @@ using Umbraco.Core.Models.Entities;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Persistence;
using Umbraco.Core.PropertyEditors;
+using Umbraco.Core.Runtime;
using Umbraco.Core.Services;
using Umbraco.Core.Strings;
using Umbraco.Core.Sync;
@@ -96,7 +97,7 @@ namespace Umbraco.Tests.TestHelpers
public static IIOHelper IOHelper { get; } = new IOHelper(GetHostingEnvironment());
- public static IMainDom MainDom { get; } = new MainDom(Mock.Of(), GetHostingEnvironment());
+ public static IMainDom MainDom { get; } = new MainDom(Mock.Of(), GetHostingEnvironment(), new MainDomSemaphoreLock(Mock.Of(), GetHostingEnvironment()));
///
/// Maps the given making it rooted on . must start with ~/
///
diff --git a/src/Umbraco.Tests/Views/web.config b/src/Umbraco.Tests/Views/web.config
new file mode 100644
index 0000000000..efd80424e5
--- /dev/null
+++ b/src/Umbraco.Tests/Views/web.config
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Umbraco.Web.UI.Client/lib/bootstrap/less/type.less b/src/Umbraco.Web.UI.Client/lib/bootstrap/less/type.less
index bf1167f950..3f93deaf56 100644
--- a/src/Umbraco.Web.UI.Client/lib/bootstrap/less/type.less
+++ b/src/Umbraco.Web.UI.Client/lib/bootstrap/less/type.less
@@ -132,6 +132,10 @@ ol.inline {
display: inline-block;
padding-left: 5px;
padding-right: 5px;
+
+ &.-no-padding-left{
+ padding-left: 0;
+ }
}
}
diff --git a/src/Umbraco.Web.UI.Client/lib/markdown/markdown.editor.js b/src/Umbraco.Web.UI.Client/lib/markdown/markdown.editor.js
index a75a7f1f3c..60118dbdb3 100644
--- a/src/Umbraco.Web.UI.Client/lib/markdown/markdown.editor.js
+++ b/src/Umbraco.Web.UI.Client/lib/markdown/markdown.editor.js
@@ -60,6 +60,7 @@
* its own image insertion dialog, this hook should return true, and the callback should be called with the chosen
* image url (or null if the user cancelled). If this hook returns false, the default dialog will be used.
*/
+ hooks.addFalse("insertLinkDialog");
this.getConverter = function () { return markdownConverter; }
@@ -1636,7 +1637,7 @@
var that = this;
// The function to be executed when you enter a link and press OK or Cancel.
// Marks up the link and adds the ref.
- var linkEnteredCallback = function (link) {
+ var linkEnteredCallback = function (link, title) {
if (link !== null) {
// ( $1
@@ -1667,10 +1668,10 @@
if (!chunk.selection) {
if (isImage) {
- chunk.selection = "enter image description here";
+ chunk.selection = title || "enter image description here";
}
else {
- chunk.selection = "enter link description here";
+ chunk.selection = title || "enter link description here";
}
}
}
@@ -1683,7 +1684,8 @@
ui.prompt('Insert Image', imageDialogText, imageDefaultText, linkEnteredCallback);
}
else {
- ui.prompt('Insert Link', linkDialogText, linkDefaultText, linkEnteredCallback);
+ if (!this.hooks.insertLinkDialog(linkEnteredCallback))
+ ui.prompt('Insert Link', linkDialogText, linkDefaultText, linkEnteredCallback);
}
return true;
}
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js
index cab71842b1..7429f60e0d 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js
@@ -791,6 +791,7 @@
$scope.content.variants[i].expireDate = model.variants[i].expireDate;
$scope.content.variants[i].releaseDateFormatted = model.variants[i].releaseDateFormatted;
$scope.content.variants[i].expireDateFormatted = model.variants[i].expireDateFormatted;
+ $scope.content.variants[i].save = model.variants[i].save;
}
model.submitButtonState = "busy";
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js
index b3948bd7c4..fe2a6aa40a 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js
@@ -17,8 +17,8 @@
scope.isNew = scope.content.state == "NotCreated";
localizationService.localizeMany([
- scope.isNew ? "placeholders_a11yCreateItem" : "placeholders_a11yEdit",
- "placeholders_a11yName",
+ scope.isNew ? "visuallyHiddenTexts_createItem" : "visuallyHiddenTexts_edit",
+ "visuallyHiddenTexts_name",
scope.isNew ? "general_new" : "general_edit"]
).then(function (data) {
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js
index 431a05778c..87053c083c 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js
@@ -233,8 +233,8 @@ Use this directive to construct a header inside the main editor window.
editorState.current.id === "-1";
var localizeVars = [
- scope.isNew ? "placeholders_a11yCreateItem" : "placeholders_a11yEdit",
- "placeholders_a11yName",
+ scope.isNew ? "visuallyHiddenTexts_createItem" : "visuallyHiddenTexts_edit",
+ "visuallyHiddenTexts_name",
scope.isNew ? "general_new" : "general_edit"
];
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js
index bc3993458e..fa1f4227a2 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js
@@ -213,7 +213,6 @@ Opens an overlay to show a custom YSOD.
var unsubscribe = [];
function activate() {
-
setView();
setButtonText();
@@ -247,10 +246,20 @@ Opens an overlay to show a custom YSOD.
setOverlayIndent();
+ focusOnOverlayHeading()
});
}
+ // Ideally this would focus on the first natively focusable element in the overlay, but as the content can be dynamic, it is focusing on the heading.
+ function focusOnOverlayHeading() {
+ var heading = el.find(".umb-overlay__title");
+
+ if(heading) {
+ heading.focus();
+ }
+ }
+
function setView() {
if (scope.view) {
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbchildselector.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbchildselector.directive.js
index 4fc22c4b74..a33fd4be53 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbchildselector.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbchildselector.directive.js
@@ -188,6 +188,22 @@ Use this directive to render a ui component for selecting child items to a paren
syncParentIcon();
}));
+ // sortable options for allowed child content types
+ scope.sortableOptions = {
+ axis: "y",
+ containment: "parent",
+ distance: 10,
+ opacity: 0.7,
+ tolerance: "pointer",
+ scroll: true,
+ zIndex: 6000,
+ update: function (e, ui) {
+ if(scope.onSort) {
+ scope.onSort();
+ }
+ }
+ };
+
// clean up
scope.$on('$destroy', function(){
// unbind watchers
@@ -209,7 +225,8 @@ Use this directive to render a ui component for selecting child items to a paren
parentIcon: "=",
parentId: "=",
onRemove: "=",
- onAdd: "="
+ onAdd: "=",
+ onSort: "="
},
link: link
};
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/util/konami.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/util/konami.directive.js
deleted file mode 100644
index 7914dfc3f0..0000000000
--- a/src/Umbraco.Web.UI.Client/src/common/directives/util/konami.directive.js
+++ /dev/null
@@ -1,62 +0,0 @@
-/**
- * Konami Code directive for AngularJS
- * @version v0.0.1
- * @license MIT License, https://www.opensource.org/licenses/MIT
- */
-
-angular.module('umbraco.directives')
- .directive('konamiCode', ['$document', function ($document) {
- var konamiKeysDefault = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65];
-
- return {
- restrict: 'A',
- link: function (scope, element, attr) {
-
- if (!attr.konamiCode) {
- throw ('Konami directive must receive an expression as value.');
- }
-
- // Let user define a custom code.
- var konamiKeys = attr.konamiKeys || konamiKeysDefault;
- var keyIndex = 0;
-
- /**
- * Fired when konami code is type.
- */
- function activated() {
- if ('konamiOnce' in attr) {
- stopListening();
- }
- // Execute expression.
- scope.$eval(attr.konamiCode);
- }
-
- /**
- * Handle keydown events.
- */
- function keydown(e) {
- if (e.keyCode === konamiKeys[keyIndex++]) {
- if (keyIndex === konamiKeys.length) {
- keyIndex = 0;
- activated();
- }
- } else {
- keyIndex = 0;
- }
- }
-
- /**
- * Stop to listen typing.
- */
- function stopListening() {
- $document.off('keydown', keydown);
- }
-
- // Start listening to key typing.
- $document.on('keydown', keydown);
-
- // Stop listening when scope is destroyed.
- scope.$on('$destroy', stopListening);
- }
- };
- }]);
diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js
index 64accc18c1..97bebef062 100644
--- a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js
+++ b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js
@@ -351,6 +351,16 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter, loca
return umbRequestHelper.resourcePromise(
$http.post(umbRequestHelper.getApiUrl("contentTypeApiBaseUrl", "PostCreateDefaultTemplate", { id: id })),
'Failed to create default template for content type with id ' + id);
+ },
+
+ hasContentNodes: function (id) {
+ return umbRequestHelper.resourcePromise(
+ $http.get(
+ umbRequestHelper.getApiUrl(
+ "contentTypeApiBaseUrl",
+ "HasContentNodes",
+ [{ id: id }])),
+ 'Failed to retrieve indication for whether content type with id ' + id + ' has associated content nodes');
}
};
}
diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js
index 9cf1181cfa..61d646afc0 100644
--- a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js
+++ b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js
@@ -127,6 +127,25 @@ function entityResource($q, $http, umbRequestHelper) {
'Failed to retrieve url for id:' + id);
},
+ getUrlByUdi: function (udi, culture) {
+
+ if (!udi) {
+ return "";
+ }
+
+ if (!culture) {
+ culture = "";
+ }
+
+ return umbRequestHelper.resourcePromise(
+ $http.get(
+ umbRequestHelper.getApiUrl(
+ "entityApiBaseUrl",
+ "GetUrl",
+ [{ udi: udi }, {culture: culture }])),
+ 'Failed to retrieve url for UDI:' + udi);
+ },
+
/**
* @ngdoc method
* @name umbraco.resources.entityResource#getById
@@ -166,18 +185,22 @@ function entityResource($q, $http, umbRequestHelper) {
},
- getUrlAndAnchors: function (id) {
+ getUrlAndAnchors: function (id, culture) {
if (id === -1 || id === "-1") {
return null;
}
+ if (!culture) {
+ culture = "";
+ }
+
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"entityApiBaseUrl",
"GetUrlAndAnchors",
- [{ id: id }])),
+ [{ id: id }, {culture: culture }])),
'Failed to retrieve url and anchors data for id ' + id);
},
diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/tour.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/tour.resource.js
index 40baf0f389..485b0d299a 100644
--- a/src/Umbraco.Web.UI.Client/src/common/resources/tour.resource.js
+++ b/src/Umbraco.Web.UI.Client/src/common/resources/tour.resource.js
@@ -20,10 +20,21 @@
"GetTours")),
'Failed to get tours');
}
+
+ function getToursForDoctype(doctypeAlias) {
+ return umbRequestHelper.resourcePromise(
+ $http.get(
+ umbRequestHelper.getApiUrl(
+ "tourApiBaseUrl",
+ "GetToursForDoctype",
+ [{ doctypeAlias: doctypeAlias }])),
+ 'Failed to get tours');
+ }
var resource = {
- getTours: getTours
+ getTours: getTours,
+ getToursForDoctype: getToursForDoctype
};
return resource;
diff --git a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js
index 1d80d3a3ed..284a7db4d8 100644
--- a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js
+++ b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js
@@ -640,6 +640,23 @@ When building a custom infinite editor view you can use the same components as a
editor.view = "views/mediatypes/edit.html";
open(editor);
}
+
+ /**
+ * @ngdoc method
+ * @name umbraco.services.editorService#memberTypeEditor
+ * @methodOf umbraco.services.editorService
+ *
+ * @description
+ * Opens the member type editor in infinite editing, the submit callback returns the saved member type
+ * @param {Object} editor rendering options
+ * @param {Callback} editor.submit Submits the editor
+ * @param {Callback} editor.close Closes the editor
+ * @returns {Object} editor object
+ */
+ function memberTypeEditor(editor) {
+ editor.view = "views/membertypes/edit.html";
+ open(editor);
+ }
/**
* @ngdoc method
@@ -1011,6 +1028,7 @@ When building a custom infinite editor view you can use the same components as a
iconPicker: iconPicker,
documentTypeEditor: documentTypeEditor,
mediaTypeEditor: mediaTypeEditor,
+ memberTypeEditor: memberTypeEditor,
queryBuilder: queryBuilder,
treePicker: treePicker,
nodePermissions: nodePermissions,
diff --git a/src/Umbraco.Web.UI.Client/src/common/services/editorstate.service.js b/src/Umbraco.Web.UI.Client/src/common/services/editorstate.service.js
index 97a9ac5c4b..28daa3f245 100644
--- a/src/Umbraco.Web.UI.Client/src/common/services/editorstate.service.js
+++ b/src/Umbraco.Web.UI.Client/src/common/services/editorstate.service.js
@@ -10,7 +10,7 @@
*
* it is possible to modify this object, so should be used with care
*/
-angular.module('umbraco.services').factory("editorState", function ($rootScope) {
+angular.module('umbraco.services').factory("editorState", function ($rootScope, eventsService) {
var current = null;
@@ -30,6 +30,7 @@ angular.module('umbraco.services').factory("editorState", function ($rootScope)
*/
set: function (entity) {
current = entity;
+ eventsService.emit("editorState.changed", { entity: entity });
},
/**
diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js
index 62af17146c..8fcab445b3 100644
--- a/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js
+++ b/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js
@@ -134,36 +134,38 @@
var groupedTours = [];
tours.forEach(function (item) {
- var groupExists = false;
- var newGroup = {
- "group": "",
- "tours": []
- };
+ if (item.contentType === null || item.contentType === '') {
+ var groupExists = false;
+ var newGroup = {
+ "group": "",
+ "tours": []
+ };
- groupedTours.forEach(function(group){
- // extend existing group if it is already added
- if(group.group === item.group) {
- if(item.groupOrder) {
- group.groupOrder = item.groupOrder
- }
- groupExists = true;
+ groupedTours.forEach(function (group) {
+ // extend existing group if it is already added
+ if (group.group === item.group) {
+ if (item.groupOrder) {
+ group.groupOrder = item.groupOrder;
+ }
+ groupExists = true;
if(item.hidden === false){
group.tours.push(item);
}
- }
- });
+ }
+ });
- // push new group to array if it doesn't exist
- if(!groupExists) {
- newGroup.group = item.group;
- if(item.groupOrder) {
- newGroup.groupOrder = item.groupOrder
- }
+ // push new group to array if it doesn't exist
+ if (!groupExists) {
+ newGroup.group = item.group;
+ if (item.groupOrder) {
+ newGroup.groupOrder = item.groupOrder;
+ }
- if(item.hidden === false){
- newGroup.tours.push(item);
- groupedTours.push(newGroup);
+ if(item.hidden === false){
+ newGroup.tours.push(item);
+ groupedTours.push(newGroup);
+ }
}
}
@@ -194,6 +196,24 @@
return deferred.promise;
}
+ /**
+ * @ngdoc method
+ * @name umbraco.services.tourService#getToursForDoctype
+ * @methodOf umbraco.services.tourService
+ *
+ * @description
+ * Returns a promise of the tours found by documenttype alias.
+ * @param {Object} doctypeAlias The doctype alias for which the tours which should be returned
+ * @returns {Array} An array of tour objects for the doctype
+ */
+ function getToursForDoctype(doctypeAlias) {
+ var deferred = $q.defer();
+ tourResource.getToursForDoctype(doctypeAlias).then(function (tours) {
+ deferred.resolve(tours);
+ });
+ return deferred.promise;
+ }
+
///////////
/**
@@ -275,7 +295,8 @@
completeTour: completeTour,
getCurrentTour: getCurrentTour,
getGroupedTours: getGroupedTours,
- getTourByAlias: getTourByAlias
+ getTourByAlias: getTourByAlias,
+ getToursForDoctype : getToursForDoctype
};
return service;
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-search.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-search.less
index 70e4f3d372..2f9430ef41 100644
--- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-search.less
+++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-search.less
@@ -16,6 +16,10 @@
box-shadow: 0 10px 20px rgba(0,0,0,.12),0 6px 6px rgba(0,0,0,.14);
}
+.umb-search__label{
+ margin: 0;
+}
+
/*
Search field
*/
@@ -107,4 +111,4 @@
.umb-search-result__description {
color: @gray-5;
font-size: 13px;
-}
\ No newline at end of file
+}
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-child-selector.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-child-selector.less
index b6cdc0e8d9..da690663d0 100644
--- a/src/Umbraco.Web.UI.Client/src/less/components/umb-child-selector.less
+++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-child-selector.less
@@ -30,6 +30,9 @@
.umb-child-selector__children-container {
margin-left: 30px;
+ .umb-child-selector__child {
+ cursor: move;
+ }
}
.umb-child-selector__child-description {
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less
index 479074fee9..26d61412ae 100644
--- a/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less
+++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less
@@ -162,6 +162,8 @@
}
.umb-grid .umb-row .umb-cell-placeholder {
+ display: block;
+ width: 100%;
min-height: 88px;
border-width: 1px;
border-style: dashed;
@@ -226,6 +228,7 @@
.umb-grid .cell-tools-add.-bar {
display: block;
+ width: calc(100% - 20px);
text-align: center;
padding: 5px;
border: 1px dashed @ui-action-discreet-border;
diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-iconpicker.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-iconpicker.less
index e8a62f739d..98b2b1d72d 100644
--- a/src/Umbraco.Web.UI.Client/src/less/components/umb-iconpicker.less
+++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-iconpicker.less
@@ -15,7 +15,9 @@
overflow: hidden;
}
-.umb-iconpicker-item a {
+.umb-iconpicker-item button {
+ background: transparent;
+ border: 0 none;
display: flex;
justify-content: center;
align-items: center;
@@ -26,8 +28,8 @@
border-radius: 3px;
}
-.umb-iconpicker-item a:hover,
-.umb-iconpicker-item a:focus {
+.umb-iconpicker-item button:hover,
+.umb-iconpicker-item button:focus {
background: @gray-10;
outline: none;
}
@@ -39,7 +41,7 @@
box-sizing: border-box;
}
-.umb-iconpicker-item a:active {
+.umb-iconpicker-item button:active {
background: @gray-10;
}
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js
index 3323f2bfb3..268bfb3a8c 100644
--- a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js
@@ -1,7 +1,7 @@
(function () {
"use strict";
- function HelpDrawerController($scope, $routeParams, $timeout, dashboardResource, localizationService, userService, eventsService, helpService, appState, tourService, $filter) {
+ function HelpDrawerController($scope, $routeParams, $timeout, dashboardResource, localizationService, userService, eventsService, helpService, appState, tourService, $filter, editorState) {
var vm = this;
var evts = [];
@@ -18,6 +18,10 @@
vm.startTour = startTour;
vm.getTourGroupCompletedPercentage = getTourGroupCompletedPercentage;
vm.showTourButton = showTourButton;
+
+ vm.showDocTypeTour = false;
+ vm.docTypeTours = [];
+ vm.nodeName = '';
function startTour(tour) {
tourService.startTour(tour);
@@ -58,9 +62,16 @@
handleSectionChange();
}));
+ evts.push(eventsService.on("editorState.changed",
+ function (e, args) {
+ setDocTypeTour(args.entity);
+ }));
+
findHelp(vm.section, vm.tree, vm.userType, vm.userLang);
});
+
+ setDocTypeTour(editorState.getCurrent());
// check if a tour is running - if it is open the matching group
var currentTour = tourService.getCurrentTour();
@@ -84,7 +95,7 @@
setSectionName();
findHelp(vm.section, vm.tree, vm.userType, vm.userLang);
-
+ setDocTypeTour();
}
});
}
@@ -168,6 +179,26 @@
});
}
+ function setDocTypeTour(node) {
+ vm.showDocTypeTour = false;
+ vm.docTypeTours = [];
+ vm.nodeName = '';
+
+ if (vm.section === 'content' && vm.tree === 'content') {
+
+ if (node) {
+ tourService.getToursForDoctype(node.contentTypeAlias).then(function (data) {
+ if (data && data.length > 0) {
+ vm.docTypeTours = data;
+ var currentVariant = _.find(node.variants, (x) => x.active);
+ vm.nodeName = currentVariant.name;
+ vm.showDocTypeTour = true;
+ }
+ });
+ }
+ }
+ }
+
evts.push(eventsService.on("appState.tour.complete", function (event, tour) {
tourService.getGroupedTours().then(function(groupedTours) {
vm.tours = groupedTours;
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html
index 96f4a404bc..aa6126e73e 100644
--- a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html
+++ b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html
@@ -8,12 +8,36 @@
-
-
+
+
+
Need help editing current item '{{vm.nodeName}}' ?
-
Tours
+
-
+
+
+
+
+
+ {{ tour.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Tours
+
+
+
@@ -25,7 +49,9 @@
{{tourGroup.group}}
- Other
+
+ Other
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js
index b5043293e5..47607b7f0b 100644
--- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js
@@ -33,6 +33,7 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController",
};
$scope.showTarget = $scope.model.hideTarget !== true;
+ $scope.showAnchor = $scope.model.hideAnchor !== true;
// this ensures that we only sync the tree once and only when it's ready
var oneTimeTreeSync = {
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html
index a7d2dbbee2..ad0aaab57c 100644
--- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html
+++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html
@@ -14,7 +14,7 @@
-