From 546a948d0b3632b925e6fb82917e332ad585b3a0 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 29 Oct 2019 12:13:42 +0100 Subject: [PATCH 01/85] Protect "system media types" from alias changes and deletion --- src/Umbraco.Core/Models/MediaTypeExtensions.cs | 10 ++++++++++ src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../src/views/mediatypes/edit.html | 1 + .../Models/ContentEditing/MediaTypeDisplay.cs | 3 ++- .../Models/Mapping/ContentTypeMapDefinition.cs | 1 + src/Umbraco.Web/Trees/MediaTypeTreeController.cs | 5 ++++- 6 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 src/Umbraco.Core/Models/MediaTypeExtensions.cs diff --git a/src/Umbraco.Core/Models/MediaTypeExtensions.cs b/src/Umbraco.Core/Models/MediaTypeExtensions.cs new file mode 100644 index 0000000000..4e2ae5822a --- /dev/null +++ b/src/Umbraco.Core/Models/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.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 309dc97b81..c0fcfe87f8 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -268,6 +268,7 @@ + diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.html b/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.html index 856089fa32..50975a6d60 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.html @@ -9,6 +9,7 @@ { - + [DataMember(Name = "isSystemMediaType")] + public bool IsSystemMediaType { get; set; } } } diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs index 528d5f6de5..0e3af5d0da 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs @@ -145,6 +145,7 @@ namespace Umbraco.Web.Models.Mapping //default listview target.ListViewEditorName = Constants.Conventions.DataTypes.ListViewPrefix + "Media"; + target.IsSystemMediaType = source.IsSystemMediaType(); if (string.IsNullOrEmpty(source.Name)) return; diff --git a/src/Umbraco.Web/Trees/MediaTypeTreeController.cs b/src/Umbraco.Web/Trees/MediaTypeTreeController.cs index f85aefcace..3d0046c319 100644 --- a/src/Umbraco.Web/Trees/MediaTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/MediaTypeTreeController.cs @@ -120,7 +120,10 @@ namespace Umbraco.Web.Trees } menu.Items.Add(Services.TextService, opensDialog: true); - menu.Items.Add(Services.TextService, opensDialog: true); + if(ct.IsSystemMediaType() == false) + { + menu.Items.Add(Services.TextService, opensDialog: true); + } menu.Items.Add(new RefreshNode(Services.TextService, true)); } From 4ee6fdd642628499272634e536470c569378a876 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 29 Oct 2019 20:39:38 +0100 Subject: [PATCH 02/85] Hide the content picker "max items" help text when it is configured in single picker mode --- .../src/views/propertyeditors/contentpicker/contentpicker.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html index ab9b078433..ba22ca9d80 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html @@ -31,7 +31,7 @@ ... -
+
From bd2c3bdcb3aedd474c2232f8509f9515158c3852 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 27 Nov 2019 14:28:53 +1100 Subject: [PATCH 03/85] Creates test to assert the problem --- .../PublishedContent/NuCacheChildrenTests.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs index 5d32606ee7..cc9ffdba3c 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs @@ -918,6 +918,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"); @@ -934,6 +942,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"); @@ -942,6 +959,8 @@ namespace Umbraco.Tests.PublishedContent documents = snapshot.Content.GetById(2).Children().ToArray(); AssertDocuments(documents, "N9", "N8", "N7"); + + } [Test] From 1513a1254902dd755a2876321b8d50bbcf5ced48 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 10 Dec 2019 13:33:59 +0100 Subject: [PATCH 04/85] Introduce a new IMainDomLock and both default and sql implementations --- .../Migrations/Install/DatabaseDataCreator.cs | 2 + .../Migrations/Upgrade/UmbracoPlan.cs | 1 + .../Upgrade/V_8_6_0/AddMainDomLock.cs | 16 ++ ...AddPropertyTypeValidationMessageColumns.cs | 1 + .../Persistence/Constants-Locks.cs | 5 + .../SqlSyntax/SqlServerSyntaxProvider.cs | 7 +- src/Umbraco.Core/Runtime/CoreRuntime.cs | 2 +- src/Umbraco.Core/{ => Runtime}/IMainDom.cs | 1 + src/Umbraco.Core/Runtime/IMainDomLock.cs | 30 +++ src/Umbraco.Core/{ => Runtime}/MainDom.cs | 97 ++++------ .../Runtime/MainDomSemaphoreLock.cs | 92 +++++++++ src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 178 ++++++++++++++++++ src/Umbraco.Core/Scoping/IScopeProvider.cs | 2 +- src/Umbraco.Core/Umbraco.Core.csproj | 8 +- .../XmlPublishedSnapshotService.cs | 1 + .../LegacyXmlPublishedCache/XmlStore.cs | 1 + 16 files changed, 375 insertions(+), 69 deletions(-) create mode 100644 src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddMainDomLock.cs rename src/Umbraco.Core/{ => Runtime}/IMainDom.cs (96%) create mode 100644 src/Umbraco.Core/Runtime/IMainDomLock.cs rename src/Umbraco.Core/{ => Runtime}/MainDom.cs (77%) create mode 100644 src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs create mode 100644 src/Umbraco.Core/Runtime/SqlMainDomLock.cs diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index 94d8cfbc62..f6daf180b7 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -151,6 +151,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.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index 223603be14..6164f828f0 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -185,6 +185,7 @@ namespace Umbraco.Core.Migrations.Upgrade // to 8.6.0 To("{3D67D2C8-5E65-47D0-A9E1-DC2EE0779D6B}"); + To("{2AB29964-02A1-474D-BD6B-72148D2A53A2}"); //FINAL } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddMainDomLock.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddMainDomLock.cs new file mode 100644 index 0000000000..6ca493ac7e --- /dev/null +++ b/src/Umbraco.Core/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.Core/Migrations/Upgrade/V_8_6_0/AddPropertyTypeValidationMessageColumns.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddPropertyTypeValidationMessageColumns.cs index 30eb30109e..f44695da69 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_6_0/AddPropertyTypeValidationMessageColumns.cs +++ b/src/Umbraco.Core/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.Core/Persistence/Constants-Locks.cs b/src/Umbraco.Core/Persistence/Constants-Locks.cs index 1dcd2408e7..e64f40ced7 100644 --- a/src/Umbraco.Core/Persistence/Constants-Locks.cs +++ b/src/Umbraco.Core/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.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs index 3d0adf175e..6dda49cd5e 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs @@ -250,6 +250,11 @@ where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName) } public override void WriteLock(IDatabase db, params int[] lockIds) + { + WriteLock(db, 1800, lockIds); + } + + public void WriteLock(IDatabase db, int millisecondsTimeout, params int[] lockIds) { // soon as we get Database, a transaction is started @@ -260,7 +265,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 {millisecondsTimeout};"); 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.Core/Runtime/CoreRuntime.cs b/src/Umbraco.Core/Runtime/CoreRuntime.cs index 50653edc7c..49d7658647 100644 --- a/src/Umbraco.Core/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Core/Runtime/CoreRuntime.cs @@ -147,7 +147,7 @@ namespace Umbraco.Core.Runtime // TODO: remove this in netcore, this is purely backwards compat hacks with the empty ctor if (MainDom == null) { - MainDom = new MainDom(Logger); + MainDom = new MainDom(Logger, new MainDomSemaphoreLock()); } diff --git a/src/Umbraco.Core/IMainDom.cs b/src/Umbraco.Core/Runtime/IMainDom.cs similarity index 96% rename from src/Umbraco.Core/IMainDom.cs rename to src/Umbraco.Core/Runtime/IMainDom.cs index 31b2e2eee0..444fc1c7d0 100644 --- a/src/Umbraco.Core/IMainDom.cs +++ b/src/Umbraco.Core/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.Core/Runtime/IMainDomLock.cs b/src/Umbraco.Core/Runtime/IMainDomLock.cs new file mode 100644 index 0000000000..c32e990114 --- /dev/null +++ b/src/Umbraco.Core/Runtime/IMainDomLock.cs @@ -0,0 +1,30 @@ +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 + /// + /// + /// + /// A disposable object which will be disposed in order to release the lock + /// + /// Throws a 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.Core/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs similarity index 77% rename from src/Umbraco.Core/MainDom.cs rename to src/Umbraco.Core/Runtime/MainDom.cs index e2049c0190..406fb9b731 100644 --- a/src/Umbraco.Core/MainDom.cs +++ b/src/Umbraco.Core/Runtime/MainDom.cs @@ -4,10 +4,13 @@ using System.Linq; using System.Security.Cryptography; using System.Threading; using System.Web.Hosting; +using Umbraco.Core; using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; -namespace Umbraco.Core +namespace Umbraco.Core.Runtime { + /// /// Provides the full implementation of . /// @@ -20,18 +23,11 @@ namespace Umbraco.Core #region Vars private readonly ILogger _logger; + 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 @@ -47,32 +43,12 @@ namespace Umbraco.Core #region Ctor // initializes a new instance of MainDom - public MainDom(ILogger logger) + public MainDom(ILogger logger, IMainDomLock systemLock) { HostingEnvironment.RegisterObject(this); _logger = logger; - - // 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(); - - 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 @@ -130,7 +106,6 @@ namespace Umbraco.Core { _logger.Info("Stopping ({SignalSource})", source); foreach (var callback in _callbacks.OrderBy(x => x.Key).Select(x => x.Value)) - { try { callback(); // no timeout on callbacks @@ -140,14 +115,13 @@ namespace Umbraco.Core _logger.Error(e, "Error while running callback"); continue; } - } _logger.Debug("Stopped ({SignalSource})", source); } finally { // in any case... _isMainDom = false; - _systemLocker?.Dispose(); + _mainDomLock.Dispose(); _logger.Info("Released ({SignalSource})", source); } @@ -167,35 +141,11 @@ 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 + _mainDomLock.AcquireLockAsync(LockTimeoutMilliseconds).Wait(); - // 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 - { - _systemLocker = _systemLock.Lock(LockTimeoutMilliseconds); - } - 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(); - } - - //WaitOneAsync (ext method) will wait for a signal without blocking the main thread, the waiting is done on a background thread - - _signal.WaitOneAsync() + // Listen for the signal from another AppDomain coming online to release the lock + _mainDomLock.ListenAsync() .ContinueWith(_ => OnSignal("signal")); _logger.Info("Acquired."); @@ -230,8 +180,7 @@ namespace Umbraco.Core { if (disposing) { - _signal?.Close(); - _signal?.Dispose(); + _mainDomLock.Dispose(); } disposedValue = true; @@ -244,5 +193,25 @@ namespace Umbraco.Core } #endregion + + public static string GetMainDomId() + { + // 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.Core/Runtime/MainDomSemaphoreLock.cs b/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs new file mode 100644 index 0000000000..89eef8a658 --- /dev/null +++ b/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs @@ -0,0 +1,92 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Umbraco.Core.Runtime +{ + /// + /// Uses a system-wide Semaphore and EventWaitHandle to synchronize the current AppDomain + /// + internal 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 IDisposable _lockRelease; + + public MainDomSemaphoreLock() + { + var lockName = "UMBRACO-" + MainDom.GetMainDomId() + "-MAINDOM-LCK"; + _systemLock = new SystemLock(lockName); + + var eventName = "UMBRACO-" + MainDom.GetMainDomId() + "-MAINDOM-EVT"; + _signal = new EventWaitHandle(false, EventResetMode.AutoReset, eventName); + } + + //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) + { + 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.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs new file mode 100644 index 0000000000..28449fe889 --- /dev/null +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -0,0 +1,178 @@ +using System; +using System.Data; +using System.Data.SqlClient; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +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 +{ + internal class SqlMainDomLock : IMainDomLock + { + private string _appDomainId; + private const string MainDomKey = "Umbraco.Core.Runtime.SqlMainDom"; + private readonly ILogger _logger; + private IUmbracoDatabase _db; + private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); + private SqlServerSyntaxProvider _sqlServerSyntax = new SqlServerSyntaxProvider(); + + public SqlMainDomLock(ILogger logger) + { + _appDomainId = AppDomain.CurrentDomain.Id.ToString(); + _logger = logger; + } + + + + public Task AcquireLockAsync(int millisecondsTimeout) + { + var factory = new UmbracoDatabaseFactory( + Constants.System.UmbracoConnectionName, + _logger, + new Lazy(() => new Persistence.Mappers.MapperCollection(Enumerable.Empty()))); + + _db = factory.CreateDatabase(); + + try + { + _db.BeginTransaction(IsolationLevel.ReadCommitted); + + try + { + // wait to get a write lock + _sqlServerSyntax.WriteLock(_db, millisecondsTimeout, Constants.Locks.MainDom); + } + catch (Exception ex) + { + if (IsLockTimeoutException(ex)) + { + return Task.FromResult(false); + } + + // unexpected + throw; + } + + InsertLockRecord(); + + return Task.FromResult(true); + } + catch(Exception) + { + _db.AbortTransaction(); + + // unexpected + throw; + } + finally + { + _db.CompleteTransaction(); + } + } + + public Task ListenAsync() + { + // 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(() => + { + while(true) + { + if (_cancellationTokenSource.IsCancellationRequested) + break; + + // poll every 1 second + Thread.Sleep(1000); + + try + { + _db.BeginTransaction(IsolationLevel.ReadCommitted); + + // get a read lock + _sqlServerSyntax.ReadLock(_db, Constants.Locks.MainDom); + + if (!IsStillMainDom()) + { + // we are no longer main dom, exit + return; + } + } + catch (Exception) + { + _db.AbortTransaction(); + throw; + } + finally + { + _db.CompleteTransaction(); + } + } + + + }, _cancellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); + + } + + /// + /// Inserts or updates the key/value row to check if the current appdomain is registerd as the maindom + /// + private void InsertLockRecord() + { + _db.InsertOrUpdate(new KeyValueDto + { + Key = MainDomKey, + Value = _appDomainId, + Updated = DateTime.Now + }); + } + + /// + /// Checks if the DB row value is our current appdomain value + /// + /// + private bool IsStillMainDom() + { + return _db.ExecuteScalar("SELECT COUNT(*) FROM umbracoKeyValue WHERE [key] = @key AND [value] = @val", + new { key = MainDomKey, val = _appDomainId }) == 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) + { + _db.Dispose(); + _cancellationTokenSource.Cancel(); + _cancellationTokenSource.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.Core/Scoping/IScopeProvider.cs b/src/Umbraco.Core/Scoping/IScopeProvider.cs index 6c9eb63ba0..96bb939f8e 100644 --- a/src/Umbraco.Core/Scoping/IScopeProvider.cs +++ b/src/Umbraco.Core/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.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 6f9ff1e783..8d278f5630 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -128,6 +128,10 @@ --> + + + + @@ -398,7 +402,7 @@ - + @@ -729,7 +733,7 @@ - + diff --git a/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedSnapshotService.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedSnapshotService.cs index ec6b854a46..97fe9057bb 100644 --- a/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedSnapshotService.cs +++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlPublishedSnapshotService.cs @@ -9,6 +9,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.Web; diff --git a/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlStore.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlStore.cs index c452c4792a..803b86aec5 100644 --- a/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlStore.cs +++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/XmlStore.cs @@ -14,6 +14,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; From 654511a6564af56e3d50c527cd7b35921ce120cc Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 10 Dec 2019 13:42:53 +0100 Subject: [PATCH 05/85] adds appSetting to switch maindom lock --- src/Umbraco.Core/Constants-AppSettings.cs | 2 ++ src/Umbraco.Web/UmbracoApplication.cs | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Constants-AppSettings.cs b/src/Umbraco.Core/Constants-AppSettings.cs index 509be46b61..85d6b24ae0 100644 --- a/src/Umbraco.Core/Constants-AppSettings.cs +++ b/src/Umbraco.Core/Constants-AppSettings.cs @@ -9,6 +9,8 @@ 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 diff --git a/src/Umbraco.Web/UmbracoApplication.cs b/src/Umbraco.Web/UmbracoApplication.cs index 94403bc1be..ad8dcb7e8b 100644 --- a/src/Umbraco.Web/UmbracoApplication.cs +++ b/src/Umbraco.Web/UmbracoApplication.cs @@ -1,7 +1,9 @@ -using System.Threading; +using System.Configuration; +using System.Threading; using System.Web; using Umbraco.Core; using Umbraco.Core.Logging.Serilog; +using Umbraco.Core.Runtime; using Umbraco.Web.Runtime; namespace Umbraco.Web @@ -14,7 +16,17 @@ namespace Umbraco.Web protected override IRuntime GetRuntime() { var logger = SerilogLogger.CreateWithDefaultConfiguration(); - return new WebRuntime(this, logger, new MainDom(logger)); + + // Determine if we should use the sql main dom or the default + var appSettingMainDomLock = ConfigurationManager.AppSettings[Constants.AppSettings.MainDomLock]; + + var mainDomLock = appSettingMainDomLock == "SqlMainDomLock" + ? (IMainDomLock)new SqlMainDomLock(logger) + : new MainDomSemaphoreLock(); + + var runtime = new WebRuntime(this, logger, new MainDom(logger, mainDomLock)); + + return runtime; } /// From e919af14d33827de2415f332907cdfc12766ed91 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 10 Dec 2019 13:50:10 +0100 Subject: [PATCH 06/85] adds comments --- src/Umbraco.Core/Runtime/MainDom.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Umbraco.Core/Runtime/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs index 406fb9b731..c6ee819cda 100644 --- a/src/Umbraco.Core/Runtime/MainDom.cs +++ b/src/Umbraco.Core/Runtime/MainDom.cs @@ -155,6 +155,10 @@ namespace Umbraco.Core.Runtime /// /// 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 From aae8dbdc15ec0f10efaa98c87384d2c2a069b936 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 10 Dec 2019 15:44:47 +0100 Subject: [PATCH 07/85] Updates to allow the sql main dom lock to be able to wait until the previous maindom has fully unwound before it can be taken over. --- src/Umbraco.Core/Runtime/MainDom.cs | 13 +- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 204 +++++++++++++++++++-- 2 files changed, 195 insertions(+), 22 deletions(-) diff --git a/src/Umbraco.Core/Runtime/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs index c6ee819cda..85cf5ad6a9 100644 --- a/src/Umbraco.Core/Runtime/MainDom.cs +++ b/src/Umbraco.Core/Runtime/MainDom.cs @@ -144,9 +144,16 @@ namespace Umbraco.Core.Runtime // Get the lock _mainDomLock.AcquireLockAsync(LockTimeoutMilliseconds).Wait(); - // Listen for the signal from another AppDomain coming online to release the lock - _mainDomLock.ListenAsync() - .ContinueWith(_ => OnSignal("signal")); + try + { + // Listen for the signal from another AppDomain coming online to release the lock + _mainDomLock.ListenAsync() + .ContinueWith(_ => OnSignal("signal")); + } + catch (OperationCanceledException) + { + // the waiting task could be canceled if this appdomain is naturally shutting down, we'll just swallow this exception + } _logger.Info("Acquired."); return true; diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index 28449fe889..c077572f85 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -1,6 +1,7 @@ using System; using System.Data; using System.Data.SqlClient; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -14,22 +15,22 @@ namespace Umbraco.Core.Runtime { internal class SqlMainDomLock : IMainDomLock { - private string _appDomainId; + private string _lockId; private const string MainDomKey = "Umbraco.Core.Runtime.SqlMainDom"; private readonly ILogger _logger; private IUmbracoDatabase _db; private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private SqlServerSyntaxProvider _sqlServerSyntax = new SqlServerSyntaxProvider(); + private bool _mainDomChanging = false; public SqlMainDomLock(ILogger logger) { - _appDomainId = AppDomain.CurrentDomain.Id.ToString(); + // 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; } - - - public Task AcquireLockAsync(int millisecondsTimeout) + public async Task AcquireLockAsync(int millisecondsTimeout) { var factory = new UmbracoDatabaseFactory( Constants.System.UmbracoConnectionName, @@ -38,6 +39,8 @@ namespace Umbraco.Core.Runtime _db = factory.CreateDatabase(); + var tempId = Guid.NewGuid().ToString(); + try { _db.BeginTransaction(IsolationLevel.ReadCommitted); @@ -51,18 +54,29 @@ namespace Umbraco.Core.Runtime { if (IsLockTimeoutException(ex)) { - return Task.FromResult(false); + return false; } // unexpected throw; } - InsertLockRecord(); + 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 - return Task.FromResult(true); + // 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 + 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) + catch (Exception) { _db.AbortTransaction(); @@ -73,6 +87,8 @@ namespace Umbraco.Core.Runtime { _db.CompleteTransaction(); } + + return await WaitForExistingAsync(tempId, millisecondsTimeout); } public Task ListenAsync() @@ -96,9 +112,14 @@ namespace Umbraco.Core.Runtime // get a read lock _sqlServerSyntax.ReadLock(_db, Constants.Locks.MainDom); - if (!IsStillMainDom()) + // 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, exit + // we are no longer main dom, another one has come online, exit + _mainDomChanging = true; return; } } @@ -119,26 +140,134 @@ namespace Umbraco.Core.Runtime } /// - /// Inserts or updates the key/value row to check if the current appdomain is registerd as the maindom + /// Wait for any existing MainDom to release so we can continue booting /// - private void InsertLockRecord() + /// + /// + /// + private Task WaitForExistingAsync(string tempId, int millisecondsTimeout) { - _db.InsertOrUpdate(new KeyValueDto + var updatedTempId = tempId + "_updated"; + + return Task.Run(() => + { + 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); + + 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); + + return true; + } + else if (mainDomRows.Count == 1 && mainDomRows[0].Value.EndsWith("_updated")) + { + // in this case, there is a suffixed _updated value but it's not for our ID which means + // another new AppDomain has come online and is wanting to take over. In that case, we will not + // acquire. + + return false; + } + } + catch (Exception ex) + { + _db.AbortTransaction(); + + if (IsLockTimeoutException(ex)) + { + return false; + } + + throw; + } + 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 + + + try + { + _db.BeginTransaction(IsolationLevel.ReadCommitted); + + _sqlServerSyntax.WriteLock(_db, Constants.Locks.MainDom); + + // so now we update the row with our appdomain id + InsertLockRecord(_lockId); + return true; + } + catch (Exception ex) + { + _db.AbortTransaction(); + + if (IsLockTimeoutException(ex)) + { + // something is wrong, we cannot acquire, not much we can do + return false; + } + + throw; + } + finally + { + _db.CompleteTransaction(); + } + } + } + }, _cancellationTokenSource.Token); + } + + /// + /// Inserts or updates the key/value row + /// + private RecordPersistenceType InsertLockRecord(string id) + { + return _db.InsertOrUpdate(new KeyValueDto { Key = MainDomKey, - Value = _appDomainId, + Value = id, Updated = DateTime.Now }); } /// - /// Checks if the DB row value is our current appdomain value + /// Checks if the DB row value is equals the value /// /// - private bool IsStillMainDom() + private bool IsMainDomValue(string val) { return _db.ExecuteScalar("SELECT COUNT(*) FROM umbracoKeyValue WHERE [key] = @key AND [value] = @val", - new { key = MainDomKey, val = _appDomainId }) == 1; + new { key = MainDomKey, val = val }) == 1; } /// @@ -157,9 +286,46 @@ namespace Umbraco.Core.Runtime { if (disposing) { - _db.Dispose(); + // capture locally just in case in some strange way a sub task somehow updates this + var mainDomChanging = _mainDomChanging; + + // immediately cancel all sub-tasks, we don't want them to keep querying _cancellationTokenSource.Cancel(); _cancellationTokenSource.Dispose(); + + 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) + { + _db.Execute("UPDATE umbracoKeyValue SET [value] = [value] + '_updated' WHERE [key] = @key", new { key = MainDomKey }); + } + else + { + _db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); + } + } + catch (Exception) + { + _db.AbortTransaction(); + throw; + } + finally + { + _db.CompleteTransaction(); + + _db.Dispose(); + } } disposedValue = true; From a63de7672bd891a306d5c1a8bc2cc682fd3cc784 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 10 Dec 2019 15:53:57 +0100 Subject: [PATCH 08/85] updates a bool check --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index c077572f85..edd5af7ea8 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -181,9 +181,9 @@ namespace Umbraco.Core.Runtime return true; } - else if (mainDomRows.Count == 1 && mainDomRows[0].Value.EndsWith("_updated")) + else if (mainDomRows.Count == 1 && !mainDomRows[0].Value.StartsWith(tempId)) { - // in this case, there is a suffixed _updated value but it's not for our ID which means + // 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. From 0ef08db7658f1eae04c1d3602200b0c8420e4a88 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 11 Dec 2019 08:55:07 +0100 Subject: [PATCH 09/85] adds error logging and ensure we don't throw exceptions on a background task which would destroy the app domain --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 138 ++++++++++++++------- 1 file changed, 91 insertions(+), 47 deletions(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index edd5af7ea8..c567b76403 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -22,42 +22,45 @@ namespace Umbraco.Core.Runtime private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private SqlServerSyntaxProvider _sqlServerSyntax = new SqlServerSyntaxProvider(); private bool _mainDomChanging = false; + private readonly UmbracoDatabaseFactory _dbFactory; + private bool _hasError; public SqlMainDomLock(ILogger logger) { // 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 Persistence.Mappers.MapperCollection(Enumerable.Empty()))); } public async Task AcquireLockAsync(int millisecondsTimeout) { - var factory = new UmbracoDatabaseFactory( - Constants.System.UmbracoConnectionName, - _logger, - new Lazy(() => new Persistence.Mappers.MapperCollection(Enumerable.Empty()))); - - _db = factory.CreateDatabase(); + var db = GetDatabase(); var tempId = Guid.NewGuid().ToString(); try { - _db.BeginTransaction(IsolationLevel.ReadCommitted); + db.BeginTransaction(IsolationLevel.ReadCommitted); try { // wait to get a write lock - _sqlServerSyntax.WriteLock(_db, millisecondsTimeout, Constants.Locks.MainDom); + _sqlServerSyntax.WriteLock(db, 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 + // unexpected (will be caught below) throw; } @@ -76,16 +79,17 @@ namespace Umbraco.Core.Runtime // 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) + catch (Exception ex) { - _db.AbortTransaction(); - + ResetDatabase(); // unexpected - throw; + _logger.Error(ex, "Unexpected error, cannot acquire MainDom"); + _hasError = true; + return false; } finally { - _db.CompleteTransaction(); + db?.CompleteTransaction(); } return await WaitForExistingAsync(tempId, millisecondsTimeout); @@ -93,6 +97,12 @@ namespace Umbraco.Core.Runtime 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(() => @@ -105,12 +115,14 @@ namespace Umbraco.Core.Runtime // poll every 1 second Thread.Sleep(1000); + var db = GetDatabase(); + try { - _db.BeginTransaction(IsolationLevel.ReadCommitted); + db.BeginTransaction(IsolationLevel.ReadCommitted); // get a read lock - _sqlServerSyntax.ReadLock(_db, Constants.Locks.MainDom); + _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 @@ -123,14 +135,17 @@ namespace Umbraco.Core.Runtime return; } } - catch (Exception) + catch (Exception ex) { - _db.AbortTransaction(); - throw; + ResetDatabase(); + // unexpected + _logger.Error(ex, "Unexpected error, listening is canceled."); + _hasError = true; + return; } finally { - _db.CompleteTransaction(); + db?.CompleteTransaction(); } } @@ -139,6 +154,23 @@ namespace Umbraco.Core.Runtime } + 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 /// @@ -151,6 +183,7 @@ namespace Umbraco.Core.Runtime return Task.Run(() => { + var db = GetDatabase(); var watch = new Stopwatch(); watch.Start(); while(true) @@ -160,12 +193,13 @@ namespace Umbraco.Core.Runtime try { - _db.BeginTransaction(IsolationLevel.ReadCommitted); + db.BeginTransaction(IsolationLevel.ReadCommitted); // get a read lock - _sqlServerSyntax.ReadLock(_db, Constants.Locks.MainDom); + _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); - var mainDomRows = _db.Fetch("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); + // the row + var mainDomRows = db.Fetch("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); if (mainDomRows.Count == 0 || mainDomRows[0].Value == updatedTempId) { @@ -174,7 +208,7 @@ namespace Umbraco.Core.Runtime // which indicates that we // can acquire it and it has shutdown. - _sqlServerSyntax.WriteLock(_db, Constants.Locks.MainDom); + _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); // so now we update the row with our appdomain id InsertLockRecord(_lockId); @@ -192,18 +226,22 @@ namespace Umbraco.Core.Runtime } catch (Exception ex) { - _db.AbortTransaction(); + ResetDatabase(); if (IsLockTimeoutException(ex)) { + _logger.Error(ex, "Sql timeout occurred, waiting for existing MainDom is canceled."); + _hasError = true; return false; } - - throw; + // unexpected + _logger.Error(ex, "Unexpected error, waiting for existing MainDom is canceled."); + _hasError = true; + return false; } finally { - _db.CompleteTransaction(); + db?.CompleteTransaction(); } if (watch.ElapsedMilliseconds >= millisecondsTimeout) @@ -218,9 +256,9 @@ namespace Umbraco.Core.Runtime try { - _db.BeginTransaction(IsolationLevel.ReadCommitted); + db.BeginTransaction(IsolationLevel.ReadCommitted); - _sqlServerSyntax.WriteLock(_db, Constants.Locks.MainDom); + _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); // so now we update the row with our appdomain id InsertLockRecord(_lockId); @@ -228,19 +266,22 @@ namespace Umbraco.Core.Runtime } catch (Exception ex) { - _db.AbortTransaction(); + ResetDatabase(); if (IsLockTimeoutException(ex)) { - // something is wrong, we cannot acquire, not much we can do + // 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; } - - throw; + _logger.Error(ex, "Unexpected error, could not forcibly acquire MainDom."); + _hasError = true; + return false; } finally { - _db.CompleteTransaction(); + db?.CompleteTransaction(); } } } @@ -252,7 +293,8 @@ namespace Umbraco.Core.Runtime /// private RecordPersistenceType InsertLockRecord(string id) { - return _db.InsertOrUpdate(new KeyValueDto + var db = GetDatabase(); + return db.InsertOrUpdate(new KeyValueDto { Key = MainDomKey, Value = id, @@ -266,7 +308,8 @@ namespace Umbraco.Core.Runtime /// private bool IsMainDomValue(string val) { - return _db.ExecuteScalar("SELECT COUNT(*) FROM umbracoKeyValue WHERE [key] = @key AND [value] = @val", + var db = GetDatabase(); + return db.ExecuteScalar("SELECT COUNT(*) FROM umbracoKeyValue WHERE [key] = @key AND [value] = @val", new { key = MainDomKey, val = val }) == 1; } @@ -293,12 +336,13 @@ namespace Umbraco.Core.Runtime _cancellationTokenSource.Cancel(); _cancellationTokenSource.Dispose(); + var db = GetDatabase(); try { - _db.BeginTransaction(IsolationLevel.ReadCommitted); + db.BeginTransaction(IsolationLevel.ReadCommitted); // get a write lock - _sqlServerSyntax.WriteLock(_db, Constants.Locks.MainDom); + _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 @@ -308,23 +352,23 @@ namespace Umbraco.Core.Runtime // Otherwise, if we are just shutting down, we want to just delete the row. if (mainDomChanging) { - _db.Execute("UPDATE umbracoKeyValue SET [value] = [value] + '_updated' WHERE [key] = @key", new { key = MainDomKey }); + db.Execute("UPDATE umbracoKeyValue SET [value] = [value] + '_updated' WHERE [key] = @key", new { key = MainDomKey }); } else { - _db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); + db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); } } - catch (Exception) + catch (Exception ex) { - _db.AbortTransaction(); - throw; + ResetDatabase(); + _logger.Error(ex, "Unexpected error during dipsose."); + _hasError = true; } finally { - _db.CompleteTransaction(); - - _db.Dispose(); + db?.CompleteTransaction(); + ResetDatabase(); } } From 9e9a2d8379877961e2c0563b91286a4ca4f3f3d3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 17 Dec 2019 10:38:03 +1100 Subject: [PATCH 10/85] Adds TestData project with an endpoint to create content/media hieararchy for testing --- .../Properties/AssemblyInfo.cs | 36 +++ src/Umbraco.TestData/Umbraco.TestData.csproj | 69 +++++ .../UmbracoTestDataController.cs | 269 ++++++++++++++++++ src/Umbraco.TestData/readme.md | 4 + src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 6 + src/umbraco.sln | 7 + 6 files changed, 391 insertions(+) create mode 100644 src/Umbraco.TestData/Properties/AssemblyInfo.cs create mode 100644 src/Umbraco.TestData/Umbraco.TestData.csproj create mode 100644 src/Umbraco.TestData/UmbracoTestDataController.cs create mode 100644 src/Umbraco.TestData/readme.md diff --git a/src/Umbraco.TestData/Properties/AssemblyInfo.cs b/src/Umbraco.TestData/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..3c4251cdf6 --- /dev/null +++ b/src/Umbraco.TestData/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Umbraco.TestData")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Umbraco.TestData")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("fb5676ed-7a69-492c-b802-e7b24144c0fc")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Umbraco.TestData/Umbraco.TestData.csproj b/src/Umbraco.TestData/Umbraco.TestData.csproj new file mode 100644 index 0000000000..b0d6b5677b --- /dev/null +++ b/src/Umbraco.TestData/Umbraco.TestData.csproj @@ -0,0 +1,69 @@ + + + + + Debug + AnyCPU + {FB5676ED-7A69-492C-B802-E7B24144C0FC} + Library + Properties + Umbraco.TestData + Umbraco.TestData + v4.7.2 + 512 + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + {31785bc3-256c-4613-b2f5-a1b0bdded8c1} + Umbraco.Core + + + {651e1350-91b6-44b7-bd60-7207006d7003} + Umbraco.Web + + + + + 28.4.4 + + + 5.2.7 + + + + \ No newline at end of file diff --git a/src/Umbraco.TestData/UmbracoTestDataController.cs b/src/Umbraco.TestData/UmbracoTestDataController.cs new file mode 100644 index 0000000000..8912a28940 --- /dev/null +++ b/src/Umbraco.TestData/UmbracoTestDataController.cs @@ -0,0 +1,269 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core; +using System.Web.Mvc; +using Umbraco.Core.Cache; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; +using Umbraco.Web; +using Umbraco.Web.Mvc; +using System.Configuration; +using Bogus; +using Umbraco.Core.Scoping; + +namespace Umbraco.TestData +{ + /// + /// Creates test data + /// + public class UmbracoTestDataController : SurfaceController + { + private const string RichTextDataTypeName = "UmbracoTestDataContent.RTE"; + private const string MediaPickerDataTypeName = "UmbracoTestDataContent.MediaPicker"; + private const string TextDataTypeName = "UmbracoTestDataContent.Text"; + private const string TestDataContentTypeAlias = "umbTestDataContent"; + private const int IdealPerBranch = 5; + private readonly IScopeProvider _scopeProvider; + private readonly PropertyEditorCollection _propertyEditors; + + public UmbracoTestDataController(IScopeProvider scopeProvider, PropertyEditorCollection propertyEditors, IUmbracoContextAccessor umbracoContextAccessor, IUmbracoDatabaseFactory databaseFactory, ServiceContext services, AppCaches appCaches, ILogger logger, IProfilingLogger profilingLogger, UmbracoHelper umbracoHelper) : base(umbracoContextAccessor, databaseFactory, services, appCaches, logger, profilingLogger, umbracoHelper) + { + _scopeProvider = scopeProvider; + _propertyEditors = propertyEditors; + } + + /// + /// Creates a content and associated media tree (hierarchy) + /// + /// + /// + /// + /// + /// + /// Each content item created is associated to a media item via a media picker and therefore a relation is created between the two + /// + public ActionResult CreateTree(int count, int depth, string locale = "en") + { + if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true") + return HttpNotFound(); + + if (!Validate(count, depth, out var message, out var perLevel)) + throw new InvalidOperationException(message); + + var faker = new Faker(locale); + var company = faker.Company.CompanyName(); + + using (var scope = _scopeProvider.CreateScope()) + { + var imageIds = CreateMediaTree(company, faker, count, depth).ToList(); + var contentIds = CreateContentTree(company, faker, count, depth, imageIds, out var root).ToList(); + + Services.ContentService.SaveAndPublishBranch(root, true); + + scope.Complete(); + } + + + return Content("Done"); + } + + private bool Validate(int count, int depth, out string message, out int perLevel) + { + perLevel = 0; + message = null; + + if (count <= 0) + { + message = "Count must be more than 0"; + return false; + } + + perLevel = count / depth; + if (perLevel < 1) + { + message = "Count not high enough for specified for number of levels required"; + return false; + } + + return true; + } + + /// + /// Utility to create a tree hierarchy + /// + /// + /// + /// + /// + /// + /// A callback that returns a tuple of Content and another callback to produce a Container. + /// For media, a container will be another folder, for content the container will be the Content itself. + /// + /// + private IEnumerable CreateHierarchy( + T parent, int count, int depth, + Func container)> creator) + where T: class, IContentBase + { + yield return parent.GetUdi(); + + var totalDescendants = count - 1; + int perLevel = totalDescendants / depth; + var branchCount = perLevel / IdealPerBranch; + var perBranch = perLevel / branchCount; + + var currentPerBranchCount = 0; + + T lastParent = null; + + for (int i = 0; i < count; i++) + { + var created = creator(parent); + var contentItem = created.content; + + yield return contentItem.GetUdi(); + + currentPerBranchCount++; + + if (contentItem.Level < depth) + { + // not at max depth, create below + lastParent = parent; + parent = created.container(); + currentPerBranchCount = 0; + } + else if (currentPerBranchCount == perBranch) + { + parent = lastParent; + currentPerBranchCount = 0; + } + } + } + + /// + /// Creates the media tree hiearachy + /// + /// + /// + /// + /// + /// + private IEnumerable CreateMediaTree(string company, Faker faker, int count, int depth) + { + var parent = Services.MediaService.CreateMediaWithIdentity(company, -1, Constants.Conventions.MediaTypes.Folder); + + return CreateHierarchy(parent, count, depth, currParent => + { + var imageUrl = faker.Image.PicsumUrl(); + var media = Services.MediaService.CreateMedia(faker.Commerce.ProductName(), currParent, Constants.Conventions.MediaTypes.Image); + media.SetValue(Constants.Conventions.Media.File, imageUrl); + Services.MediaService.Save(media); + return (media, () => + { + // create a folder to contain child media + var container = Services.MediaService.CreateMediaWithIdentity(faker.Commerce.Department(), parent, Constants.Conventions.MediaTypes.Folder); + return container; + }); + }); + } + + /// + /// Creates the content tree hiearachy + /// + /// + /// + /// + /// + /// + /// + private IEnumerable CreateContentTree(string company, Faker faker, int count, int depth, List imageIds, out IContent root) + { + var random = new Random(company.GetHashCode()); + + var docType = GetOrCreateContentType(); + + var parent = Services.ContentService.Create(company, -1, docType.Alias); + parent.SetValue("review", faker.Rant.Review()); + parent.SetValue("desc", company); + parent.SetValue("media", imageIds[random.Next(0, imageIds.Count - 1)]); + Services.ContentService.Save(parent); + + root = parent; + + return CreateHierarchy(parent, count, depth, currParent => + { + var content = Services.ContentService.Create(faker.Commerce.ProductName(), currParent, docType.Alias); + content.SetValue("review", faker.Rant.Review()); + content.SetValue("desc", string.Join(", ", Enumerable.Range(0, 5).Select(x => faker.Commerce.ProductAdjective()))); ; + content.SetValue("media", imageIds[random.Next(0, imageIds.Count - 1)]); + + Services.ContentService.Save(content); + return (content, () => content); + }); + + } + + private IContentType GetOrCreateContentType() + { + var docType = Services.ContentTypeService.Get(TestDataContentTypeAlias); + if (docType != null) + return docType; + + docType = new ContentType(-1) + { + Alias = TestDataContentTypeAlias, + Name = "Umbraco Test Data Content", + Icon = "icon-science color-green" + }; + docType.AddPropertyGroup("Content"); + docType.AddPropertyType(new PropertyType(GetOrCreateRichText(), "review") + { + Name = "Review" + }); + docType.AddPropertyType(new PropertyType(GetOrCreateMediaPicker(), "media") + { + Name = "Media" + }); + docType.AddPropertyType(new PropertyType(GetOrCreateText(), "desc") + { + Name = "Description" + }); + Services.ContentTypeService.Save(docType); + docType.AllowedContentTypes = new[] { new ContentTypeSort(docType.Id, 0) }; + Services.ContentTypeService.Save(docType); + return docType; + } + + private IDataType GetOrCreateRichText() => GetOrCreateDataType(RichTextDataTypeName, Constants.PropertyEditors.Aliases.TinyMce); + + private IDataType GetOrCreateMediaPicker() => GetOrCreateDataType(MediaPickerDataTypeName, Constants.PropertyEditors.Aliases.MediaPicker); + + private IDataType GetOrCreateText() => GetOrCreateDataType(TextDataTypeName, Constants.PropertyEditors.Aliases.TextBox); + + private IDataType GetOrCreateDataType(string name, string editorAlias) + { + var dt = Services.DataTypeService.GetDataType(name); + if (dt != null) return dt; + + var editor = _propertyEditors.FirstOrDefault(x => x.Alias == editorAlias); + if (editor == null) + throw new InvalidOperationException($"No {editorAlias} editor found"); + + dt = new DataType(editor) + { + Name = name, + Configuration = editor.GetConfigurationEditor().DefaultConfigurationObject, + DatabaseType = ValueStorageType.Ntext + }; + + Services.DataTypeService.Save(dt); + return dt; + } + } +} diff --git a/src/Umbraco.TestData/readme.md b/src/Umbraco.TestData/readme.md new file mode 100644 index 0000000000..78c08e0957 --- /dev/null +++ b/src/Umbraco.TestData/readme.md @@ -0,0 +1,4 @@ +## Umbraco Test Data + +This project is a utility to be able to generate large amounts of content and media in an +Umbraco installation for testing. diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 43130f34a3..b18856ad2d 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -115,6 +115,12 @@ + + + {fb5676ed-7a69-492c-b802-e7b24144c0fc} + Umbraco.TestData + + {31785bc3-256c-4613-b2f5-a1b0bdded8c1} diff --git a/src/umbraco.sln b/src/umbraco.sln index 938532beb0..0c5f02c7bd 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -102,6 +102,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IssueTemplates", "IssueTemp ..\.github\ISSUE_TEMPLATE\5_Security_issue.md = ..\.github\ISSUE_TEMPLATE\5_Security_issue.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.TestData", "Umbraco.TestData\Umbraco.TestData.csproj", "{FB5676ED-7A69-492C-B802-E7B24144C0FC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -134,6 +136,10 @@ Global {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Debug|Any CPU.Build.0 = Debug|Any CPU {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Release|Any CPU.ActiveCfg = Release|Any CPU {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Release|Any CPU.Build.0 = Release|Any CPU + {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FB5676ED-7A69-492C-B802-E7B24144C0FC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -146,6 +152,7 @@ Global {53594E5B-64A2-4545-8367-E3627D266AE8} = {FD962632-184C-4005-A5F3-E705D92FC645} {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {C7311C00-2184-409B-B506-52A5FAEA8736} = {FD962632-184C-4005-A5F3-E705D92FC645} + {FB5676ED-7A69-492C-B802-E7B24144C0FC} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7A0F2E34-D2AF-4DAB-86A0-7D7764B3D0EC} From 292a40194fccd8e3cfef04213f8cf49321013cea Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 17 Dec 2019 16:46:02 +1100 Subject: [PATCH 11/85] updates readme, adjusts controller --- .../UmbracoTestDataController.cs | 48 +++++++++++-------- src/Umbraco.TestData/readme.md | 45 +++++++++++++++++ 2 files changed, 74 insertions(+), 19 deletions(-) diff --git a/src/Umbraco.TestData/UmbracoTestDataController.cs b/src/Umbraco.TestData/UmbracoTestDataController.cs index 8912a28940..f5ded2a754 100644 --- a/src/Umbraco.TestData/UmbracoTestDataController.cs +++ b/src/Umbraco.TestData/UmbracoTestDataController.cs @@ -28,7 +28,6 @@ namespace Umbraco.TestData private const string MediaPickerDataTypeName = "UmbracoTestDataContent.MediaPicker"; private const string TextDataTypeName = "UmbracoTestDataContent.Text"; private const string TestDataContentTypeAlias = "umbTestDataContent"; - private const int IdealPerBranch = 5; private readonly IScopeProvider _scopeProvider; private readonly PropertyEditorCollection _propertyEditors; @@ -101,48 +100,59 @@ namespace Umbraco.TestData /// /// /// - /// + /// /// A callback that returns a tuple of Content and another callback to produce a Container. /// For media, a container will be another folder, for content the container will be the Content itself. /// /// private IEnumerable CreateHierarchy( T parent, int count, int depth, - Func container)> creator) + Func container)> create) where T: class, IContentBase { yield return parent.GetUdi(); + // This will not calculate a balanced tree but it will ensure that there will be enough nodes deep enough to not fill up the tree. var totalDescendants = count - 1; - int perLevel = totalDescendants / depth; - var branchCount = perLevel / IdealPerBranch; - var perBranch = perLevel / branchCount; + var perLevel = Math.Ceiling(totalDescendants / (double)depth); + var perBranch = Math.Ceiling(perLevel / depth); - var currentPerBranchCount = 0; + var tracked = new Stack<(T parent, int childCount)>(); - T lastParent = null; + var currChildCount = 0; for (int i = 0; i < count; i++) { - var created = creator(parent); + var created = create(parent); var contentItem = created.content; yield return contentItem.GetUdi(); - currentPerBranchCount++; + currChildCount++; - if (contentItem.Level < depth) + if (currChildCount == perBranch) { + // move back up... + + var prev = tracked.Pop(); + + // restore child count + currChildCount = prev.childCount; + // restore the parent + parent = prev.parent; + + } + else if (contentItem.Level < depth) + { + // track the current parent and it's current child count + tracked.Push((parent, currChildCount)); + // not at max depth, create below - lastParent = parent; parent = created.container(); - currentPerBranchCount = 0; - } - else if (currentPerBranchCount == perBranch) - { - parent = lastParent; - currentPerBranchCount = 0; + + currChildCount = 0; } + } } @@ -167,7 +177,7 @@ namespace Umbraco.TestData return (media, () => { // create a folder to contain child media - var container = Services.MediaService.CreateMediaWithIdentity(faker.Commerce.Department(), parent, Constants.Conventions.MediaTypes.Folder); + var container = Services.MediaService.CreateMediaWithIdentity(faker.Commerce.Department(), currParent, Constants.Conventions.MediaTypes.Folder); return container; }); }); diff --git a/src/Umbraco.TestData/readme.md b/src/Umbraco.TestData/readme.md index 78c08e0957..717b6aac57 100644 --- a/src/Umbraco.TestData/readme.md +++ b/src/Umbraco.TestData/readme.md @@ -2,3 +2,48 @@ This project is a utility to be able to generate large amounts of content and media in an Umbraco installation for testing. + +Currently this project is referenced in the Umbraco.Web.UI project but only when it's being built +in Debug mode (i.e. when testing within Visual Studio). + +## Usage + +It has to be enabled by an appSetting: + +```xml + +``` + +Once this is enabled this endpoint can be executed: + +`/umbraco/surface/umbracotestdata/CreateTree?count=100&depth=5` + +The query string options are: + +* `count` = the number of content and media nodes to create +* `depth` = how deep the trees created will be +* `locale` (optional, default = "en") = the language that the data will be generated in + +This creates a content and associated media tree (hierarchy). Each content item created is associated +to a media item via a media picker and therefore a relation is created between the two. Each content and +media tree created have the same root node name so it's easy to know which content branch relates to +which media branch. + +All values are generated using the very handy `Bogus` package. + +## Schema + +This will install some schema items: + +* `umbTestDataContent` Document Type. __TIP__: If you want to delete all of the content data generated with this tool, just delete this content type +* `UmbracoTestDataContent.RTE` Data Type +* `UmbracoTestDataContent.MediaPicker` Data Type +* `UmbracoTestDataContent.Text` Data Type + +For media, the normal folder and image is used + +## Media + +This does not upload physical files, it just uses a randomized online image as the `umbracoFile` value. +This works when viewing the media item in the media section and the image will show up, however when viewing a content item +that has these media items picked, the thumbnail will not show up (which is due to a bug in the CMS that will be fixed in 8.6). From 25d1c454dc10ce6bb6e9e3e9b0b5aad386492f04 Mon Sep 17 00:00:00 2001 From: Shannon Deminick Date: Tue, 17 Dec 2019 16:50:54 +1100 Subject: [PATCH 12/85] Update readme.md --- src/Umbraco.TestData/readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.TestData/readme.md b/src/Umbraco.TestData/readme.md index 717b6aac57..5475da1ff5 100644 --- a/src/Umbraco.TestData/readme.md +++ b/src/Umbraco.TestData/readme.md @@ -8,6 +8,8 @@ in Debug mode (i.e. when testing within Visual Studio). ## Usage +You must use SQL Server for this, using SQLCE will die if you try to bulk create huge amounts of data. + It has to be enabled by an appSetting: ```xml From 000a8d2c946ef64e7e9b025b4f0e1efa7f3cc6b3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 18 Dec 2019 15:37:00 +1100 Subject: [PATCH 13/85] manually merges changes for the NC validation stuff since there was conflicts --- .../NestedContentPropertyEditor.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs index f3e391aeab..564630c574 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs @@ -293,9 +293,19 @@ namespace Umbraco.Web.PropertyEditors if (row.PropType.Mandatory) { if (row.JsonRowValue[row.PropKey] == null) - validationResults.Add(new ValidationResult("Item " + (row.RowIndex + 1) + " '" + row.PropType.Name + "' cannot be null", new[] { row.PropKey })); + { + var message = string.IsNullOrWhiteSpace(row.PropType.MandatoryMessage) + ? $"'{row.PropType.Name}' cannot be null" + : row.PropType.MandatoryMessage; + validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey })); + } else if (row.JsonRowValue[row.PropKey].ToString().IsNullOrWhiteSpace() || (row.JsonRowValue[row.PropKey].Type == JTokenType.Array && !row.JsonRowValue[row.PropKey].HasValues)) - validationResults.Add(new ValidationResult("Item " + (row.RowIndex + 1) + " '" + row.PropType.Name + "' cannot be empty", new[] { row.PropKey })); + { + var message = string.IsNullOrWhiteSpace(row.PropType.MandatoryMessage) + ? $"'{row.PropType.Name}' cannot be empty" + : row.PropType.MandatoryMessage; + validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey })); + } } // Check regex @@ -305,7 +315,10 @@ namespace Umbraco.Web.PropertyEditors var regex = new Regex(row.PropType.ValidationRegExp); if (!regex.IsMatch(row.JsonRowValue[row.PropKey].ToString())) { - validationResults.Add(new ValidationResult("Item " + (row.RowIndex + 1) + " '" + row.PropType.Name + "' is invalid, it does not match the correct pattern", new[] { row.PropKey })); + var message = string.IsNullOrWhiteSpace(row.PropType.ValidationRegExpMessage) + ? $"'{row.PropType.Name}' is invalid, it does not match the correct pattern" + : row.PropType.ValidationRegExpMessage; + validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey })); } } } From 1e35aeb9cf3ef100eb7a2e25d5aca84f9e5bb439 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 18 Dec 2019 16:01:27 +1100 Subject: [PATCH 14/85] ensures .jpg ext is added to test data --- src/Umbraco.TestData/UmbracoTestDataController.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Umbraco.TestData/UmbracoTestDataController.cs b/src/Umbraco.TestData/UmbracoTestDataController.cs index f5ded2a754..402c05cc1c 100644 --- a/src/Umbraco.TestData/UmbracoTestDataController.cs +++ b/src/Umbraco.TestData/UmbracoTestDataController.cs @@ -171,6 +171,15 @@ namespace Umbraco.TestData return CreateHierarchy(parent, count, depth, currParent => { var imageUrl = faker.Image.PicsumUrl(); + + // we are appending a &ext=.jpg to the end of this for a reason. The result of this url will be something like: + // https://picsum.photos/640/480/?image=106 + // and due to the way that we detect images there must be an extension so we'll change it to + // https://picsum.photos/640/480/?image=106&ext=.jpg + // which will trick our app into parsing this and thinking it's an image ... which it is so that's good. + // if we don't do this we don't get thumbnails in the back office. + imageUrl += "&ext=.jpg"; + var media = Services.MediaService.CreateMedia(faker.Commerce.ProductName(), currParent, Constants.Conventions.MediaTypes.Image); media.SetValue(Constants.Conventions.Media.File, imageUrl); Services.MediaService.Save(media); From e0531f1429cc2e1f7c812c7737653f2bc9ed58d0 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 18 Dec 2019 16:10:33 +1100 Subject: [PATCH 15/85] allows ImagesController to still return valid urls even if the uri isn't resolved to a local on-disk file --- src/Umbraco.Web/Editors/ImagesController.cs | 23 +++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/Editors/ImagesController.cs b/src/Umbraco.Web/Editors/ImagesController.cs index b29c166765..db6706d1bb 100644 --- a/src/Umbraco.Web/Editors/ImagesController.cs +++ b/src/Umbraco.Web/Editors/ImagesController.cs @@ -60,8 +60,27 @@ namespace Umbraco.Web.Editors //redirect to ImageProcessor thumbnail with rnd generated from last modified time of original media file var response = Request.CreateResponse(HttpStatusCode.Found); - var imageLastModified = _mediaFileSystem.GetLastModified(imagePath); - response.Headers.Location = new Uri($"{imagePath}?rnd={imageLastModified:yyyyMMddHHmmss}&upscale=false&width={width}&animationprocessmode=first&mode=max", UriKind.Relative); + + DateTimeOffset? imageLastModified = null; + try + { + imageLastModified = _mediaFileSystem.GetLastModified(imagePath); + + } + catch (Exception) + { + // if we get an exception here it's probably because the image path being requested is an image that doesn't exist + // in the local media file system. This can happen if someone is storing an absolute path to an image online, which + // is perfectly legal but in that case the media file system isn't going to resolve it. + // so ignore and we won't set a last modified date. + } + + // TODO: When we abstract imaging for netcore, we are actually just going to be abstracting a URI builder for images, this + // is one of those places where this can be used. + + var rnd = imageLastModified.HasValue ? $"&rnd={imageLastModified:yyyyMMddHHmmss}" : string.Empty; + + response.Headers.Location = new Uri($"{imagePath}?upscale=false&width={width}&animationprocessmode=first&mode=max{rnd}", UriKind.RelativeOrAbsolute); return response; } From ddaafa2abe1c0fb1f65daf7aa692835c604eea2c Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 18 Dec 2019 16:11:31 +1100 Subject: [PATCH 16/85] updates readme --- src/Umbraco.TestData/readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.TestData/readme.md b/src/Umbraco.TestData/readme.md index 5475da1ff5..f943326303 100644 --- a/src/Umbraco.TestData/readme.md +++ b/src/Umbraco.TestData/readme.md @@ -47,5 +47,5 @@ For media, the normal folder and image is used ## Media This does not upload physical files, it just uses a randomized online image as the `umbracoFile` value. -This works when viewing the media item in the media section and the image will show up, however when viewing a content item -that has these media items picked, the thumbnail will not show up (which is due to a bug in the CMS that will be fixed in 8.6). +This works when viewing the media item in the media section and the image will show up and with recent changes this will also work +when editing content to view the thumbnail for the picked media. From 7501629c541633c8bb33077b473514bbe288f359 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 30 Dec 2019 17:15:57 +1100 Subject: [PATCH 17/85] Adds logging to SqlMainDomLock --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index c567b76403..a2560b71a3 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -38,6 +38,8 @@ namespace Umbraco.Core.Runtime public async Task AcquireLockAsync(int millisecondsTimeout) { + _logger.Info("Acquiring lock..."); + var db = GetDatabase(); var tempId = Guid.NewGuid().ToString(); @@ -73,6 +75,7 @@ namespace Umbraco.Core.Runtime // are MainDom? then we don't leave any orphan rows behind. InsertLockRecord(_lockId); // so update with our appdomain id + _logger.Info("Acquired with ID {LockId}", _lockId); return true; } @@ -212,7 +215,7 @@ namespace Umbraco.Core.Runtime // so now we update the row with our appdomain id InsertLockRecord(_lockId); - + _logger.Info("Acquired with ID {LockId}", _lockId); return true; } else if (mainDomRows.Count == 1 && !mainDomRows[0].Value.StartsWith(tempId)) @@ -221,6 +224,8 @@ namespace Umbraco.Core.Runtime // another new AppDomain has come online and is wanting to take over. In that case, we will not // acquire. + _logger.Info("Cannot acquire, another booting application detected."); + return false; } } @@ -253,6 +258,7 @@ namespace Umbraco.Core.Runtime // which isn't ideal. // So... we're going to 'just' take over, if the writelock works then we'll assume we're ok + _logger.Info("Timeout elapsed, assuming orphan row, acquiring MainDom."); try { @@ -262,6 +268,7 @@ namespace Umbraco.Core.Runtime // so now we update the row with our appdomain id InsertLockRecord(_lockId); + _logger.Info("Acquired with ID {LockId}", _lockId); return true; } catch (Exception ex) From 0d1101eaa748180d350662beee6040d203bda37f Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 30 Dec 2019 18:14:18 +1100 Subject: [PATCH 18/85] adds notes/logging --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index a2560b71a3..d7b800c364 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -112,6 +112,10 @@ namespace Umbraco.Core.Runtime { while(true) { + // 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) break; @@ -135,6 +139,7 @@ namespace Umbraco.Core.Runtime { // we are no longer main dom, another one has come online, exit _mainDomChanging = true; + _logger.Info("Detected new booting application, releasing MainDom."); return; } } @@ -359,10 +364,12 @@ namespace Umbraco.Core.Runtime // Otherwise, if we are just shutting down, we want to just delete the row. if (mainDomChanging) { + _logger.Info("Releasing MainDom, updating row, new application is booting."); db.Execute("UPDATE umbracoKeyValue SET [value] = [value] + '_updated' WHERE [key] = @key", new { key = MainDomKey }); } else { + _logger.Info("Releasing MainDom, deleting row, application is shutting down."); db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); } } From 5476388e787c778852802042267f1f20ceb3aae6 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 30 Dec 2019 18:49:33 +1100 Subject: [PATCH 19/85] adds lock else we end up detecting when not needed --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 143 +++++++++++---------- 1 file changed, 74 insertions(+), 69 deletions(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index d7b800c364..bb44bbbfbf 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -24,6 +24,7 @@ namespace Umbraco.Core.Runtime private bool _mainDomChanging = false; private readonly UmbracoDatabaseFactory _dbFactory; private bool _hasError; + private object _locker = new object(); public SqlMainDomLock(ILogger logger) { @@ -112,49 +113,53 @@ namespace Umbraco.Core.Runtime { while(true) { - // 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) - break; - // poll every 1 second Thread.Sleep(1000); - var db = GetDatabase(); - - try + lock(_locker) { - db.BeginTransaction(IsolationLevel.ReadCommitted); + // 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) + break; - // get a read lock - _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); + var db = GetDatabase(); - // 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)) + try { - // we are no longer main dom, another one has come online, exit - _mainDomChanging = true; - _logger.Info("Detected new booting application, releasing MainDom."); + 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.Info("Detected new booting application, releasing MainDom."); + return; + } + } + catch (Exception ex) + { + ResetDatabase(); + // unexpected + _logger.Error(ex, "Unexpected error, listening is canceled."); + _hasError = true; return; } + finally + { + db?.CompleteTransaction(); + } } - catch (Exception ex) - { - ResetDatabase(); - // unexpected - _logger.Error(ex, "Unexpected error, listening is canceled."); - _hasError = true; - return; - } - finally - { - db?.CompleteTransaction(); - } + } @@ -341,48 +346,48 @@ namespace Umbraco.Core.Runtime { if (disposing) { - // capture locally just in case in some strange way a sub task somehow updates this - var mainDomChanging = _mainDomChanging; - - // immediately cancel all sub-tasks, we don't want them to keep querying - _cancellationTokenSource.Cancel(); - _cancellationTokenSource.Dispose(); - - var db = GetDatabase(); - try + lock (_locker) { - db.BeginTransaction(IsolationLevel.ReadCommitted); + // immediately cancel all sub-tasks, we don't want them to keep querying + _cancellationTokenSource.Cancel(); + _cancellationTokenSource.Dispose(); - // 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) + var db = GetDatabase(); + try { - _logger.Info("Releasing MainDom, updating row, new application is booting."); - db.Execute("UPDATE umbracoKeyValue SET [value] = [value] + '_updated' WHERE [key] = @key", new { key = MainDomKey }); + 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.Info("Releasing MainDom, updating row, new application is booting."); + db.Execute("UPDATE umbracoKeyValue SET [value] = [value] + '_updated' WHERE [key] = @key", new { key = MainDomKey }); + } + else + { + _logger.Info("Releasing MainDom, deleting row, application is shutting down."); + db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); + } } - else + catch (Exception ex) { - _logger.Info("Releasing MainDom, deleting row, application is shutting down."); - db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); + ResetDatabase(); + _logger.Error(ex, "Unexpected error during dipsose."); + _hasError = true; + } + finally + { + db?.CompleteTransaction(); + ResetDatabase(); } - } - catch (Exception ex) - { - ResetDatabase(); - _logger.Error(ex, "Unexpected error during dipsose."); - _hasError = true; - } - finally - { - db?.CompleteTransaction(); - ResetDatabase(); } } From b24a87d9868d70bcbc164559f74caab744785b5c Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 30 Dec 2019 18:51:38 +1100 Subject: [PATCH 20/85] Changes logging to debug --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index bb44bbbfbf..847a0c25df 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -39,7 +39,7 @@ namespace Umbraco.Core.Runtime public async Task AcquireLockAsync(int millisecondsTimeout) { - _logger.Info("Acquiring lock..."); + _logger.Debug("Acquiring lock..."); var db = GetDatabase(); @@ -76,7 +76,7 @@ namespace Umbraco.Core.Runtime // are MainDom? then we don't leave any orphan rows behind. InsertLockRecord(_lockId); // so update with our appdomain id - _logger.Info("Acquired with ID {LockId}", _lockId); + _logger.Debug("Acquired with ID {LockId}", _lockId); return true; } @@ -142,7 +142,7 @@ namespace Umbraco.Core.Runtime { // we are no longer main dom, another one has come online, exit _mainDomChanging = true; - _logger.Info("Detected new booting application, releasing MainDom."); + _logger.Debug("Detected new booting application, releasing MainDom."); return; } } @@ -225,7 +225,7 @@ namespace Umbraco.Core.Runtime // so now we update the row with our appdomain id InsertLockRecord(_lockId); - _logger.Info("Acquired with ID {LockId}", _lockId); + _logger.Debug("Acquired with ID {LockId}", _lockId); return true; } else if (mainDomRows.Count == 1 && !mainDomRows[0].Value.StartsWith(tempId)) @@ -234,7 +234,7 @@ namespace Umbraco.Core.Runtime // another new AppDomain has come online and is wanting to take over. In that case, we will not // acquire. - _logger.Info("Cannot acquire, another booting application detected."); + _logger.Debug("Cannot acquire, another booting application detected."); return false; } @@ -268,7 +268,7 @@ namespace Umbraco.Core.Runtime // which isn't ideal. // So... we're going to 'just' take over, if the writelock works then we'll assume we're ok - _logger.Info("Timeout elapsed, assuming orphan row, acquiring MainDom."); + _logger.Debug("Timeout elapsed, assuming orphan row, acquiring MainDom."); try { @@ -278,7 +278,7 @@ namespace Umbraco.Core.Runtime // so now we update the row with our appdomain id InsertLockRecord(_lockId); - _logger.Info("Acquired with ID {LockId}", _lockId); + _logger.Debug("Acquired with ID {LockId}", _lockId); return true; } catch (Exception ex) @@ -368,12 +368,12 @@ namespace Umbraco.Core.Runtime // Otherwise, if we are just shutting down, we want to just delete the row. if (_mainDomChanging) { - _logger.Info("Releasing MainDom, updating row, new application is booting."); + _logger.Debug("Releasing MainDom, updating row, new application is booting."); db.Execute("UPDATE umbracoKeyValue SET [value] = [value] + '_updated' WHERE [key] = @key", new { key = MainDomKey }); } else { - _logger.Info("Releasing MainDom, deleting row, application is shutting down."); + _logger.Debug("Releasing MainDom, deleting row, application is shutting down."); db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); } } From 00c913d6217ecd9dfe16454d0f883cf034e328f4 Mon Sep 17 00:00:00 2001 From: abi Date: Thu, 2 Jan 2020 12:02:48 +0000 Subject: [PATCH 21/85] add InternalSearchConstants --- src/Umbraco.Web/InternalSearchConstants.cs | 32 +++++++++++++++++++ src/Umbraco.Web/Runtime/WebInitialComposer.cs | 2 +- .../Search/IInternalSearchConstants.cs | 13 ++++++++ src/Umbraco.Web/Search/UmbracoTreeSearcher.cs | 12 ++++--- src/Umbraco.Web/Umbraco.Web.csproj | 2 ++ 5 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 src/Umbraco.Web/InternalSearchConstants.cs create mode 100644 src/Umbraco.Web/Search/IInternalSearchConstants.cs diff --git a/src/Umbraco.Web/InternalSearchConstants.cs b/src/Umbraco.Web/InternalSearchConstants.cs new file mode 100644 index 0000000000..012cd98f1d --- /dev/null +++ b/src/Umbraco.Web/InternalSearchConstants.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using Umbraco.Examine; +using Umbraco.Web.Search; + +namespace Umbraco.Web +{ + public class InternalSearchConstants : IInternalSearchConstants + { + private List _backOfficeFields = new List {"id", "__NodeId", "__Key"}; + public List GetBackOfficeFields() + { + return _backOfficeFields; + } + + + private List _backOfficeMembersFields = new List {"email", "loginName"}; + public List GetBackOfficeMembersFields() + { + return _backOfficeMembersFields; + } + private List _backOfficeMediaFields = new List {UmbracoExamineIndex.UmbracoFileFieldName }; + public List GetBackOfficeMediaFields() + { + return _backOfficeMediaFields; + } + private List _backOfficeDocumentFields = new List (); + public List GetBackOfficeDocumentFields() + { + return _backOfficeDocumentFields; + } + } +} diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index 87c0f46fba..b8be2319d8 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -95,7 +95,7 @@ namespace Umbraco.Web.Runtime // a way to inject the UmbracoContext - DO NOT register this as Lifetime.Request since LI will dispose the context // in it's own way but we don't want that to happen, we manage its lifetime ourselves. composition.Register(factory => factory.GetInstance().UmbracoContext); - + composition.RegisterUnique(); composition.Register(factory => { var umbCtx = factory.GetInstance(); diff --git a/src/Umbraco.Web/Search/IInternalSearchConstants.cs b/src/Umbraco.Web/Search/IInternalSearchConstants.cs new file mode 100644 index 0000000000..72a37deaa1 --- /dev/null +++ b/src/Umbraco.Web/Search/IInternalSearchConstants.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace Umbraco.Web.Search +{ + public interface IInternalSearchConstants + { + List GetBackOfficeFields(); + List GetBackOfficeMembersFields(); + + List GetBackOfficeMediaFields(); + List GetBackOfficeDocumentFields(); + } +} diff --git a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs index 463f4b09df..1345ffd4cc 100644 --- a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs @@ -28,13 +28,15 @@ namespace Umbraco.Web.Search private readonly IEntityService _entityService; private readonly UmbracoMapper _mapper; private readonly ISqlContext _sqlContext; + private readonly IInternalSearchConstants _internalSearchConstants; + public UmbracoTreeSearcher(IExamineManager examineManager, UmbracoContext umbracoContext, ILocalizationService languageService, IEntityService entityService, UmbracoMapper mapper, - ISqlContext sqlContext) + ISqlContext sqlContext,IInternalSearchConstants internalSearchConstants) { _examineManager = examineManager ?? throw new ArgumentNullException(nameof(examineManager)); _umbracoContext = umbracoContext; @@ -42,6 +44,7 @@ namespace Umbraco.Web.Search _entityService = entityService; _mapper = mapper; _sqlContext = sqlContext; + _internalSearchConstants = internalSearchConstants; } /// @@ -67,7 +70,7 @@ namespace Umbraco.Web.Search string type; var indexName = Constants.UmbracoIndexes.InternalIndexName; - var fields = new List { "id", "__NodeId", "__Key" }; + var fields = _internalSearchConstants.GetBackOfficeFields(); // TODO: WE should try to allow passing in a lucene raw query, however we will still need to do some manual string // manipulation for things like start paths, member types, etc... @@ -87,7 +90,7 @@ namespace Umbraco.Web.Search case UmbracoEntityTypes.Member: indexName = Constants.UmbracoIndexes.MembersIndexName; type = "member"; - fields.AddRange(new[]{ "email", "loginName"}); + fields.AddRange(_internalSearchConstants.GetBackOfficeMembersFields()); if (searchFrom != null && searchFrom != Constants.Conventions.MemberTypes.AllMembersListId && searchFrom.Trim() != "-1") { sb.Append("+__NodeTypeAlias:"); @@ -97,12 +100,13 @@ namespace Umbraco.Web.Search break; case UmbracoEntityTypes.Media: type = "media"; - fields.AddRange(new[] { UmbracoExamineIndex.UmbracoFileFieldName }); + fields.AddRange(_internalSearchConstants.GetBackOfficeMediaFields()); var allMediaStartNodes = _umbracoContext.Security.CurrentUser.CalculateMediaStartNodeIds(_entityService); AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, ignoreUserStartNodes, _entityService); break; case UmbracoEntityTypes.Document: type = "content"; + fields.AddRange(_internalSearchConstants.GetBackOfficeDocumentFields()); var allContentStartNodes = _umbracoContext.Security.CurrentUser.CalculateContentStartNodeIds(_entityService); AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, ignoreUserStartNodes, _entityService); break; diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 5d29e53d4a..159abd435b 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -155,6 +155,7 @@ + @@ -249,6 +250,7 @@ + From 9614dcf0b08bcca001e423733e2dc7f2826a2de8 Mon Sep 17 00:00:00 2001 From: abi Date: Thu, 2 Jan 2020 13:20:32 +0000 Subject: [PATCH 22/85] move InternalSearchConstants to correct place in source code --- src/Umbraco.Web/{ => Search}/InternalSearchConstants.cs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Umbraco.Web/{ => Search}/InternalSearchConstants.cs (100%) diff --git a/src/Umbraco.Web/InternalSearchConstants.cs b/src/Umbraco.Web/Search/InternalSearchConstants.cs similarity index 100% rename from src/Umbraco.Web/InternalSearchConstants.cs rename to src/Umbraco.Web/Search/InternalSearchConstants.cs From 575ec6fec4e5c0f2aa94a2d4efb216fddef54c18 Mon Sep 17 00:00:00 2001 From: abi Date: Thu, 2 Jan 2020 13:39:23 +0000 Subject: [PATCH 23/85] Correct project references --- src/Umbraco.Web/Umbraco.Web.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 159abd435b..0980b1b3d6 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -155,7 +155,6 @@ - @@ -251,6 +250,7 @@ + From 12f51dec4e6ee3c4b64719799066749feae04cd5 Mon Sep 17 00:00:00 2001 From: abi Date: Thu, 2 Jan 2020 23:00:53 +0000 Subject: [PATCH 24/85] Use immutable types --- .../Search/IInternalSearchConstants.cs | 10 +++++----- .../Search/InternalSearchConstants.cs | 16 ++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web/Search/IInternalSearchConstants.cs b/src/Umbraco.Web/Search/IInternalSearchConstants.cs index 72a37deaa1..06222e2ec7 100644 --- a/src/Umbraco.Web/Search/IInternalSearchConstants.cs +++ b/src/Umbraco.Web/Search/IInternalSearchConstants.cs @@ -4,10 +4,10 @@ namespace Umbraco.Web.Search { public interface IInternalSearchConstants { - List GetBackOfficeFields(); - List GetBackOfficeMembersFields(); - - List GetBackOfficeMediaFields(); - List GetBackOfficeDocumentFields(); + IEnumerable GetBackOfficeFields(); + IEnumerable GetBackOfficeMembersFields(); + + IEnumerable GetBackOfficeMediaFields(); + IEnumerable GetBackOfficeDocumentFields(); } } diff --git a/src/Umbraco.Web/Search/InternalSearchConstants.cs b/src/Umbraco.Web/Search/InternalSearchConstants.cs index 012cd98f1d..51e5df4c4e 100644 --- a/src/Umbraco.Web/Search/InternalSearchConstants.cs +++ b/src/Umbraco.Web/Search/InternalSearchConstants.cs @@ -6,25 +6,25 @@ namespace Umbraco.Web { public class InternalSearchConstants : IInternalSearchConstants { - private List _backOfficeFields = new List {"id", "__NodeId", "__Key"}; - public List GetBackOfficeFields() + private IReadOnlyList _backOfficeFields = new List {"id", "__NodeId", "__Key"}; + public IEnumerable GetBackOfficeFields() { return _backOfficeFields; } - private List _backOfficeMembersFields = new List {"email", "loginName"}; - public List GetBackOfficeMembersFields() + private IReadOnlyList _backOfficeMembersFields = new List {"email", "loginName"}; + public IEnumerable GetBackOfficeMembersFields() { return _backOfficeMembersFields; } - private List _backOfficeMediaFields = new List {UmbracoExamineIndex.UmbracoFileFieldName }; - public List GetBackOfficeMediaFields() + private IReadOnlyList _backOfficeMediaFields = new List {UmbracoExamineIndex.UmbracoFileFieldName }; + public IEnumerable GetBackOfficeMediaFields() { return _backOfficeMediaFields; } - private List _backOfficeDocumentFields = new List (); - public List GetBackOfficeDocumentFields() + private IReadOnlyList _backOfficeDocumentFields = new List (); + public IEnumerable GetBackOfficeDocumentFields() { return _backOfficeDocumentFields; } From 3b8e6d3cc05621c0c402b807902cf0e549e2d6e9 Mon Sep 17 00:00:00 2001 From: abi Date: Thu, 2 Jan 2020 23:05:08 +0000 Subject: [PATCH 25/85] rename to UmbracoTreeSearcherFields --- src/Umbraco.Web/Runtime/WebInitialComposer.cs | 6 +++--- ...hConstants.cs => IUmbracoTreeSearcherFields.cs} | 2 +- src/Umbraco.Web/Search/UmbracoTreeSearcher.cs | 14 +++++++------- ...chConstants.cs => UmbracoTreeSearcherFields.cs} | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) rename src/Umbraco.Web/Search/{IInternalSearchConstants.cs => IUmbracoTreeSearcherFields.cs} (86%) rename src/Umbraco.Web/Search/{InternalSearchConstants.cs => UmbracoTreeSearcherFields.cs} (93%) diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index b8be2319d8..cc6cdc1f60 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -73,7 +73,7 @@ namespace Umbraco.Web.Runtime // register accessors for cultures composition.RegisterUnique(); composition.RegisterUnique(); - + // register the http context and umbraco context accessors // we *should* use the HttpContextUmbracoContextAccessor, however there are cases when // we have no http context, eg when booting Umbraco or in background threads, so instead @@ -95,7 +95,7 @@ namespace Umbraco.Web.Runtime // a way to inject the UmbracoContext - DO NOT register this as Lifetime.Request since LI will dispose the context // in it's own way but we don't want that to happen, we manage its lifetime ourselves. composition.Register(factory => factory.GetInstance().UmbracoContext); - composition.RegisterUnique(); + composition.RegisterUnique(); composition.Register(factory => { var umbCtx = factory.GetInstance(); @@ -268,7 +268,7 @@ namespace Umbraco.Web.Runtime .Append() .Append() .Append(); - + // replace with web implementation composition.RegisterUnique(); diff --git a/src/Umbraco.Web/Search/IInternalSearchConstants.cs b/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields.cs similarity index 86% rename from src/Umbraco.Web/Search/IInternalSearchConstants.cs rename to src/Umbraco.Web/Search/IUmbracoTreeSearcherFields.cs index 06222e2ec7..eaa5d743a1 100644 --- a/src/Umbraco.Web/Search/IInternalSearchConstants.cs +++ b/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; namespace Umbraco.Web.Search { - public interface IInternalSearchConstants + public interface IUmbracoTreeSearcherFields { IEnumerable GetBackOfficeFields(); IEnumerable GetBackOfficeMembersFields(); diff --git a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs index 1345ffd4cc..56c7c6fbf3 100644 --- a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs @@ -28,7 +28,7 @@ namespace Umbraco.Web.Search private readonly IEntityService _entityService; private readonly UmbracoMapper _mapper; private readonly ISqlContext _sqlContext; - private readonly IInternalSearchConstants _internalSearchConstants; + private readonly IUmbracoTreeSearcherFields _umbracoTreeSearcherFields; public UmbracoTreeSearcher(IExamineManager examineManager, @@ -36,7 +36,7 @@ namespace Umbraco.Web.Search ILocalizationService languageService, IEntityService entityService, UmbracoMapper mapper, - ISqlContext sqlContext,IInternalSearchConstants internalSearchConstants) + ISqlContext sqlContext,IUmbracoTreeSearcherFields umbracoTreeSearcherFields) { _examineManager = examineManager ?? throw new ArgumentNullException(nameof(examineManager)); _umbracoContext = umbracoContext; @@ -44,7 +44,7 @@ namespace Umbraco.Web.Search _entityService = entityService; _mapper = mapper; _sqlContext = sqlContext; - _internalSearchConstants = internalSearchConstants; + _umbracoTreeSearcherFields = umbracoTreeSearcherFields; } /// @@ -70,7 +70,7 @@ namespace Umbraco.Web.Search string type; var indexName = Constants.UmbracoIndexes.InternalIndexName; - var fields = _internalSearchConstants.GetBackOfficeFields(); + var fields = _umbracoTreeSearcherFields.GetBackOfficeFields(); // TODO: WE should try to allow passing in a lucene raw query, however we will still need to do some manual string // manipulation for things like start paths, member types, etc... @@ -90,7 +90,7 @@ namespace Umbraco.Web.Search case UmbracoEntityTypes.Member: indexName = Constants.UmbracoIndexes.MembersIndexName; type = "member"; - fields.AddRange(_internalSearchConstants.GetBackOfficeMembersFields()); + fields.AddRange(_umbracoTreeSearcherFields.GetBackOfficeMembersFields()); if (searchFrom != null && searchFrom != Constants.Conventions.MemberTypes.AllMembersListId && searchFrom.Trim() != "-1") { sb.Append("+__NodeTypeAlias:"); @@ -100,13 +100,13 @@ namespace Umbraco.Web.Search break; case UmbracoEntityTypes.Media: type = "media"; - fields.AddRange(_internalSearchConstants.GetBackOfficeMediaFields()); + fields.AddRange(_umbracoTreeSearcherFields.GetBackOfficeMediaFields()); var allMediaStartNodes = _umbracoContext.Security.CurrentUser.CalculateMediaStartNodeIds(_entityService); AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, ignoreUserStartNodes, _entityService); break; case UmbracoEntityTypes.Document: type = "content"; - fields.AddRange(_internalSearchConstants.GetBackOfficeDocumentFields()); + fields.AddRange(_umbracoTreeSearcherFields.GetBackOfficeDocumentFields()); var allContentStartNodes = _umbracoContext.Security.CurrentUser.CalculateContentStartNodeIds(_entityService); AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, ignoreUserStartNodes, _entityService); break; diff --git a/src/Umbraco.Web/Search/InternalSearchConstants.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs similarity index 93% rename from src/Umbraco.Web/Search/InternalSearchConstants.cs rename to src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs index 51e5df4c4e..d1c663024b 100644 --- a/src/Umbraco.Web/Search/InternalSearchConstants.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs @@ -4,7 +4,7 @@ using Umbraco.Web.Search; namespace Umbraco.Web { - public class InternalSearchConstants : IInternalSearchConstants + public class UmbracoTreeSearcherFields : IUmbracoTreeSearcherFields { private IReadOnlyList _backOfficeFields = new List {"id", "__NodeId", "__Key"}; public IEnumerable GetBackOfficeFields() From 358b4c9133ac2f4a2cc45cff21cfbe325c3a57fd Mon Sep 17 00:00:00 2001 From: abi Date: Thu, 2 Jan 2020 23:08:53 +0000 Subject: [PATCH 26/85] Make fields list to allow to add fields --- src/Umbraco.Web/Search/UmbracoTreeSearcher.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs index 56c7c6fbf3..146177f86f 100644 --- a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs @@ -70,7 +70,7 @@ namespace Umbraco.Web.Search string type; var indexName = Constants.UmbracoIndexes.InternalIndexName; - var fields = _umbracoTreeSearcherFields.GetBackOfficeFields(); + var fields = _umbracoTreeSearcherFields.GetBackOfficeFields().ToList(); // TODO: WE should try to allow passing in a lucene raw query, however we will still need to do some manual string // manipulation for things like start paths, member types, etc... From a46e9124d28799067377da5d969d2730425f57bb Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 3 Jan 2020 10:38:48 +1100 Subject: [PATCH 27/85] First commit in fixing deadlock - committing my notes, etc... --- src/Umbraco.Core/Runtime/CoreRuntime.cs | 2 +- src/Umbraco.Core/Runtime/MainDom.cs | 27 ++++- .../Runtime/MainDomSemaphoreLock.cs | 9 +- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 88 +++++++-------- .../PublishedCache/NuCache/ContentStore.cs | 101 ++++++++++++++---- .../NuCache/PublishedSnapshot.cs | 8 ++ .../NuCache/PublishedSnapshotService.cs | 25 ++++- .../NuCache/{notes.txt => readme.md} | 44 ++++---- src/Umbraco.Web/Umbraco.Web.csproj | 4 +- src/Umbraco.Web/UmbracoApplication.cs | 2 +- 10 files changed, 207 insertions(+), 103 deletions(-) rename src/Umbraco.Web/PublishedCache/NuCache/{notes.txt => readme.md} (70%) diff --git a/src/Umbraco.Core/Runtime/CoreRuntime.cs b/src/Umbraco.Core/Runtime/CoreRuntime.cs index b46058fa82..b852aff2ff 100644 --- a/src/Umbraco.Core/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Core/Runtime/CoreRuntime.cs @@ -147,7 +147,7 @@ namespace Umbraco.Core.Runtime // TODO: remove this in netcore, this is purely backwards compat hacks with the empty ctor if (MainDom == null) { - MainDom = new MainDom(Logger, new MainDomSemaphoreLock()); + MainDom = new MainDom(Logger, new MainDomSemaphoreLock(Logger)); } diff --git a/src/Umbraco.Core/Runtime/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs index 85cf5ad6a9..883b69b2a7 100644 --- a/src/Umbraco.Core/Runtime/MainDom.cs +++ b/src/Umbraco.Core/Runtime/MainDom.cs @@ -36,7 +36,7 @@ namespace Umbraco.Core.Runtime // 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 = 10000; // 10 seconds #endregion @@ -106,6 +106,7 @@ namespace Umbraco.Core.Runtime { _logger.Info("Stopping ({SignalSource})", source); foreach (var callback in _callbacks.OrderBy(x => x.Key).Select(x => x.Value)) + { try { callback(); // no timeout on callbacks @@ -115,6 +116,8 @@ namespace Umbraco.Core.Runtime _logger.Error(e, "Error while running callback"); continue; } + } + _logger.Debug("Stopped ({SignalSource})", source); } finally @@ -142,17 +145,33 @@ namespace Umbraco.Core.Runtime _logger.Info("Acquiring."); // Get the lock - _mainDomLock.AcquireLockAsync(LockTimeoutMilliseconds).Wait(); + var acquired = _mainDomLock.AcquireLockAsync(LockTimeoutMilliseconds).Result; + + if (!acquired) + { + _logger.Info("Cannot acquire (timeout)."); + + // TODO: Previously we'd throw an exception and the appdomain would not start, what do we want to do? + + // return false; + + throw new TimeoutException("Cannot acquire MainDom"); + } try { // Listen for the signal from another AppDomain coming online to release the lock _mainDomLock.ListenAsync() - .ContinueWith(_ => OnSignal("signal")); + .ContinueWith(_ => + { + _logger.Debug("Signal heard from other appdomain."); + OnSignal("signal"); + }); } - catch (OperationCanceledException) + catch (OperationCanceledException ex) { // the waiting task could be canceled if this appdomain is naturally shutting down, we'll just swallow this exception + _logger.Warn(ex, ex.Message); } _logger.Info("Acquired."); diff --git a/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs b/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs index 89eef8a658..2dcc37e25f 100644 --- a/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs +++ b/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs @@ -1,6 +1,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using Umbraco.Core.Logging; namespace Umbraco.Core.Runtime { @@ -14,16 +15,17 @@ namespace Umbraco.Core.Runtime // 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() + public MainDomSemaphoreLock(ILogger logger) { var lockName = "UMBRACO-" + MainDom.GetMainDomId() + "-MAINDOM-LCK"; _systemLock = new SystemLock(lockName); var eventName = "UMBRACO-" + MainDom.GetMainDomId() + "-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 @@ -47,8 +49,9 @@ namespace Umbraco.Core.Runtime _lockRelease = _systemLock.Lock(millisecondsTimeout); return Task.FromResult(true); } - catch (TimeoutException) + catch (TimeoutException ex) { + _logger.Error(ex); return Task.FromResult(false); } finally diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index 847a0c25df..31f8b36ed0 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -109,62 +109,62 @@ namespace Umbraco.Core.Runtime // 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(() => + return Task.Factory.StartNew(ListeningLoop, _cancellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); + + } + + private void ListeningLoop() + { + while (true) { - while(true) + // poll every 1 second + Thread.Sleep(1000); + + lock (_locker) { - // poll every 1 second - Thread.Sleep(1000); + // 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; - lock(_locker) + var db = GetDatabase(); + + try { - // 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) - break; + db.BeginTransaction(IsolationLevel.ReadCommitted); - var db = GetDatabase(); + // get a read lock + _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); - try + // 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)) { - 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."); - return; - } - } - catch (Exception ex) - { - ResetDatabase(); - // unexpected - _logger.Error(ex, "Unexpected error, listening is canceled."); - _hasError = true; + // we are no longer main dom, another one has come online, exit + _mainDomChanging = true; + _logger.Debug("Detected new booting application, releasing MainDom lock."); return; } - finally - { - db?.CompleteTransaction(); - } } - + catch (Exception ex) + { + ResetDatabase(); + // unexpected + _logger.Error(ex, "Unexpected error, listening is canceled."); + _hasError = true; + return; + } + finally + { + db?.CompleteTransaction(); + } } - - - }, _cancellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); + } } private void ResetDatabase() diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 550fd507d5..81229af9e1 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -115,11 +115,14 @@ namespace Umbraco.Web.PublishedCache.NuCache // TODO: GetScopedWriter? should the dict have a ref onto the scope provider? public IDisposable GetScopedWriteLock(IScopeProvider scopeProvider) { + _logger.Debug("GetScopedWriteLock"); return ScopeContextualBase.Get(scopeProvider, _instanceId, scoped => new ScopedWriteLock(this, scoped)); } private void Lock(WriteLockInfo lockInfo, bool forceGen = false) { + // TODO: We are in a deadlock here somehow?? + Monitor.Enter(_wlocko, ref lockInfo.Taken); var rtaken = false; @@ -193,8 +196,15 @@ namespace Umbraco.Web.PublishedCache.NuCache _localDb.Commit(); } - if (lockInfo.Count) _wlocked--; - if (lockInfo.Taken) Monitor.Exit(_wlocko); + // TODO: Shouldn't this be in a finally block? + // TODO: Shouldn't this be decremented after we exit?? + // TODO: Shouldn't the locked flag never exceed 1? + if (lockInfo.Count) + _wlocked--; + + // TODO: Shouldn't this be in a finally block? + if (lockInfo.Taken) + Monitor.Exit(_wlocko); } private void Release(ReadLockInfo lockInfo) @@ -232,21 +242,29 @@ namespace Umbraco.Web.PublishedCache.NuCache public void ReleaseLocalDb() { + _logger.Info("Releasing DB..."); var lockInfo = new WriteLockInfo(); try { try { // Trying to lock could throw exceptions so always make sure to clean up. + _logger.Info("Trying to lock before releasing DB (lock count = {LockCount}) ...", _wlocked); + Lock(lockInfo); } finally { try { + _logger.Info("Disposing local DB..."); _localDb?.Dispose(); } - catch { /* TBD: May already be throwing so don't throw again */} + catch (Exception ex) + { + /* TBD: May already be throwing so don't throw again */ + _logger.Error(ex, "Error trying to release DB"); + } finally { _localDb = null; @@ -254,8 +272,17 @@ namespace Umbraco.Web.PublishedCache.NuCache } } + catch (Exception ex) + { + _logger.Error(ex, "Error trying to lock"); + throw; + } finally { + _logger.Info("Releasing ContentStore..."); + + // TODO: I don't understand this, we would have already closed the BPlusTree store above?? + // I guess it's 'safe' and will just decrement the write lock counter? Release(lockInfo); } } @@ -275,6 +302,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var lockInfo = new WriteLockInfo(); try { + _logger.Debug("NewContentTypes"); Lock(lockInfo); foreach (var type in types) @@ -297,6 +325,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var lockInfo = new WriteLockInfo(); try { + _logger.Debug("UpdateContentTypes"); Lock(lockInfo); var index = types.ToDictionary(x => x.Id, x => x); @@ -324,10 +353,13 @@ namespace Umbraco.Web.PublishedCache.NuCache public void SetAllContentTypes(IEnumerable types) { - var lockInfo = new WriteLockInfo(); + // TODO: There should be NO lock here! All calls made to this are wrapped in GetScopedWriteLock + + //var lockInfo = new WriteLockInfo(); try { - Lock(lockInfo); + _logger.Debug("SetAllContentTypes"); + //Lock(lockInfo); // clear all existing content types ClearLocked(_contentTypesById); @@ -345,7 +377,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } finally { - Release(lockInfo); + //Release(lockInfo); } } @@ -362,6 +394,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var lockInfo = new WriteLockInfo(); try { + _logger.Debug("UpdateContentTypes"); Lock(lockInfo); var removedContentTypeNodes = new List(); @@ -435,6 +468,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var lockInfo = new WriteLockInfo(); try { + _logger.Debug("UpdateDataTypes"); Lock(lockInfo); var contentTypes = _contentTypesById @@ -545,10 +579,11 @@ namespace Umbraco.Web.PublishedCache.NuCache _logger.Debug("Set content ID: {KitNodeId}", kit.Node.Id); - var lockInfo = new WriteLockInfo(); + // TODO: There should be NO locks here, all calls made to this are done within GetScopedWriteLock + //var lockInfo = new WriteLockInfo(); try { - Lock(lockInfo); + //Lock(lockInfo); // get existing _contentNodes.TryGetValue(kit.Node.Id, out var link); @@ -594,7 +629,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } finally { - Release(lockInfo); + //Release(lockInfo); } return true; @@ -623,11 +658,16 @@ namespace Umbraco.Web.PublishedCache.NuCache /// internal bool SetAllFastSorted(IEnumerable kits, bool fromDb) { - var lockInfo = new WriteLockInfo(); + + //TODO: There should be NO locks here all calls made to this are done within GetScopedWriteLock + + //var lockInfo = new WriteLockInfo(); var ok = true; try { - Lock(lockInfo); + _logger.Debug("SetAllFastSorted"); + + //Lock(lockInfo); ClearLocked(_contentNodes); ClearRootLocked(); @@ -665,7 +705,7 @@ namespace Umbraco.Web.PublishedCache.NuCache previousNode = null; // there is no previous sibling } - _logger.Debug($"Set {thisNode.Id} with parent {thisNode.ParentContentId}"); + _logger.Verbose($"Set {thisNode.Id} with parent {thisNode.ParentContentId}"); SetValueLocked(_contentNodes, thisNode.Id, thisNode); // if we are initializing from the database source ensure the local db is updated @@ -689,7 +729,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } finally { - Release(lockInfo); + //Release(lockInfo); } return ok; @@ -697,11 +737,15 @@ namespace Umbraco.Web.PublishedCache.NuCache public bool SetAll(IEnumerable kits) { - var lockInfo = new WriteLockInfo(); + //TODO: There should be NO locks here all calls made to this are done within GetScopedWriteLock + + //var lockInfo = new WriteLockInfo(); var ok = true; try { - Lock(lockInfo); + _logger.Debug("SetAll"); + + //Lock(lockInfo); ClearLocked(_contentNodes); ClearRootLocked(); @@ -717,7 +761,7 @@ namespace Umbraco.Web.PublishedCache.NuCache ok = false; continue; // skip that one } - _logger.Debug($"Set {kit.Node.Id} with parent {kit.Node.ParentContentId}"); + _logger.Verbose($"Set {kit.Node.Id} with parent {kit.Node.ParentContentId}"); SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); if (_localDb != null) RegisterChange(kit.Node.Id, kit); @@ -728,7 +772,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } finally { - Release(lockInfo); + //Release(lockInfo); } return ok; @@ -737,11 +781,15 @@ namespace Umbraco.Web.PublishedCache.NuCache // IMPORTANT kits must be sorted out by LEVEL and by SORT ORDER public bool SetBranch(int rootContentId, IEnumerable kits) { - var lockInfo = new WriteLockInfo(); + //TODO: There should be NO locks here all calls made to this are done within GetScopedWriteLock + + //var lockInfo = new WriteLockInfo(); var ok = true; try { - Lock(lockInfo); + _logger.Debug("SetBranch"); + + //Lock(lockInfo); // get existing _contentNodes.TryGetValue(rootContentId, out var link); @@ -773,7 +821,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } finally { - Release(lockInfo); + //Release(lockInfo); } return ok; @@ -781,10 +829,13 @@ namespace Umbraco.Web.PublishedCache.NuCache public bool Clear(int id) { - var lockInfo = new WriteLockInfo(); + // TODO: There should be NO locks here! All calls to this are made within GetScopedWriteLock + //var lockInfo = new WriteLockInfo(); try { - Lock(lockInfo); + _logger.Debug("Clear"); + + //Lock(lockInfo); // try to find the content // if it is not there, nothing to do @@ -804,7 +855,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } finally { - Release(lockInfo); + //Release(lockInfo); } } @@ -1200,6 +1251,8 @@ namespace Umbraco.Web.PublishedCache.NuCache var lockInfo = new ReadLockInfo(); try { + // TODO: This would be much simpler with just a lock(_rlocko) { } + // in this case I see no reason why we are using this syntax?! Lock(lockInfo); // if no next generation is required, and we already have one, @@ -1374,6 +1427,8 @@ namespace Umbraco.Web.PublishedCache.NuCache } } + // TODO: This is never used? Should it be? + public async Task WaitForPendingCollect() { Task task; diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshot.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshot.cs index 3f5c1aa4d5..bbb84fc650 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshot.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshot.cs @@ -39,7 +39,15 @@ namespace Umbraco.Web.PublishedCache.NuCache public void Resync() { + // This is annoying since this can cause deadlocks. Since this will be cleared if something happens in the same + // thread after that calls Elements, it will re-lock the _storesLock but that might already be locked again + // and since we're most likely in a ContentStore write lock, the other thread is probably wanting that one too. + // no lock - published snapshots are single-thread + + // TODO: Instead of clearing, we could hold this value in another var in case the above call to GetElements() Or potentially + // a new TryGetElements() fails (since we might be shutting down). In that case we can use the old value. + _elements?.Dispose(); _elements = null; } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index a866297d72..2439633005 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -190,10 +190,15 @@ namespace Umbraco.Web.PublishedCache.NuCache /// private void MainDomRelease() { + _logger.Debug("Releasing from MainDom..."); + lock (_storesLock) { + _logger.Debug("Releasing content store..."); _contentStore?.ReleaseLocalDb(); //null check because we could shut down before being assigned _localContentDb = null; + + _logger.Debug("Releasing media store..."); _mediaStore?.ReleaseLocalDb(); //null check because we could shut down before being assigned _localMediaDb = null; @@ -663,10 +668,20 @@ namespace Umbraco.Web.PublishedCache.NuCache publishedChanged = publishedChanged2; } + // TODO: These resync's are a problem, they cause deadlocks because when this is called, it's generally called within a writelock + // and then we clear out the snapshot and then if there's some event handler that needs the content cache, it tries to re-get it + // which first tries locking on the _storesLock which may have already been acquired and in this case we deadlock because + // we're still holding the write lock. + // We resync at the end of a ScopedWriteLock + // BUT if we don't resync here then the current snapshot is out of date for any event handlers that wish to use the most up to date + // data... + // so we need to figure out how to deal with the _storesLock + if (draftChanged || publishedChanged) ((PublishedSnapshot)CurrentPublishedSnapshot)?.Resync(); } + // Calling this method means we have a lock on the contentStore (i.e. GetScopedWriteLock) private void NotifyLocked(IEnumerable payloads, out bool draftChanged, out bool publishedChanged) { publishedChanged = false; @@ -676,7 +691,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // content (and content types) are read-locked while reading content // contentStore is wlocked (so readable, only no new views) // and it can be wlocked by 1 thread only at a time - // contentStore is write-locked during changes + // contentStore is write-locked during changes - see note above, calls to this method are wrapped in contentStore.GetScopedWriteLock foreach (var payload in payloads) { @@ -1131,6 +1146,12 @@ namespace Umbraco.Web.PublishedCache.NuCache ContentStore.Snapshot contentSnap, mediaSnap; SnapDictionary.Snapshot domainSnap; IAppCache elementsCache; + + // TODO: Idea... TryGetElements? Might check if we are shutting down and return false and callers to this could handle it? + // Else does a readerwriterlockslim work here? (i don't think so) + // Else we have 2x locks, one for startup/shutdown, the other for getting elements and then we can maybe do a Monitor.Try? + // That is sort of the same as a TryGetElements + lock (_storesLock) { var scopeContext = _scopeProvider.Context; @@ -1156,6 +1177,8 @@ namespace Umbraco.Web.PublishedCache.NuCache // elements // just need to make sure nothing gets elements in another enlisted action... so using // a MaxValue to make sure this one runs last, and it should be ok + // ... else there is potential to deadlock since this would recursively go back into trying + // lock on _storesLock but another thread may have already tried that scopeContext.Enlist("Umbraco.Web.PublishedCache.NuCache.PublishedSnapshotService.Resync", () => this, (completed, svc) => { ((PublishedSnapshot)svc.CurrentPublishedSnapshot)?.Resync(); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/notes.txt b/src/Umbraco.Web/PublishedCache/NuCache/readme.md similarity index 70% rename from src/Umbraco.Web/PublishedCache/NuCache/notes.txt rename to src/Umbraco.Web/PublishedCache/NuCache/readme.md index ff2d8dd48b..c8e8a363e9 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/notes.txt +++ b/src/Umbraco.Web/PublishedCache/NuCache/readme.md @@ -1,10 +1,6 @@ NuCache Documentation ====================== -NOTE - RENAMING -Facade = PublishedSnapshot -and everything needs to be renamed accordingly - HOW IT WORKS ------------- @@ -22,12 +18,12 @@ When reading the cache, we read views up the chain until we find a value (which null) for the given id, and finally we read the store itself. -The FacadeService manages a ContentStore for content, and another for media. -When a Facade is created, the FacadeService gets ContentView objects from the stores. +The PublishedSnapshotService manages a ContentStore for content, and another for media. +When a PublishedSnapshot is created, the PublishedSnapshotService gets ContentView objects from the stores. Views provide an immutable snapshot of the content and media. -When the FacadeService is notified of changes, it notifies the stores. -Then it resync the current Facade, so that it requires new views, etc. +When the PublishedSnapshotService is notified of changes, it notifies the stores. +Then it resync the current PublishedSnapshot, so that it requires new views, etc. Whenever a content, media or member is modified or removed, the cmsContentNu table is updated with a json dictionary of alias => property value, so that a content, @@ -50,32 +46,32 @@ Each ContentStore has a _freezeLock object used to protect the 'Frozen' state of the store. It's a disposable object that releases the lock when disposed, so usage would be: using (store.Frozen) { ... }. -The FacadeService has a _storesLock object used to guarantee atomic access to the +The PublishedSnapshotService has a _storesLock object used to guarantee atomic access to the set of content, media stores. CACHE ------ -For each set of views, the FacadeService creates a SnapshotCache. So a SnapshotCache +For each set of views, the PublishedSnapshotService creates a SnapshotCache. So a SnapshotCache is valid until anything changes in the content or media trees. In other words, things that go in the SnapshotCache stay until a content or media is modified. -For each Facade, the FacadeService creates a FacadeCache. So a FacadeCache is valid -for the duration of the Facade (usually, the request). In other words, things that go -in the FacadeCache stay (and are visible to) for the duration of the request only. +For each PublishedSnapshot, the PublishedSnapshotService creates a PublishedSnapshotCache. So a PublishedSnapshotCache is valid +for the duration of the PublishedSnapshot (usually, the request). In other words, things that go +in the PublishedSnapshotCache stay (and are visible to) for the duration of the request only. -The FacadeService defines a static constant FullCacheWhenPreviewing, that defines +The PublishedSnapshotService defines a static constant FullCacheWhenPreviewing, that defines how caches operate when previewing: - when true, the caches in preview mode work normally. -- when false, everything that would go to the SnapshotCache goes to the FacadeCache. +- when false, everything that would go to the SnapshotCache goes to the PublishedSnapshotCache. At the moment it is true in the code, which means that eg converted values for previewed content will go in the SnapshotCache. Makes for faster preview, but uses more memory on the long term... would need some benchmarking to figure out what is best. -Members only live for the duration of the Facade. So, for members SnapshotCache is -never used, and everything goes to the FacadeCache. +Members only live for the duration of the PublishedSnapshot. So, for members SnapshotCache is +never used, and everything goes to the PublishedSnapshotCache. All cache keys are computed in the CacheKeys static class. @@ -85,15 +81,15 @@ TESTS For testing purposes the following mechanisms exist: -The Facade type has a static Current property that is used to obtain the 'current' -facade in many places, going through the PublishedCachesServiceResolver to get the -current service, and asking the current service for the current facade, which by +The PublishedSnapshot type has a static Current property that is used to obtain the 'current' +PublishedSnapshot in many places, going through the PublishedCachesServiceResolver to get the +current service, and asking the current service for the current PublishedSnapshot, which by default relies on UmbracoContext. For test purposes, it is possible to override the -entire mechanism by defining Facade.GetCurrentFacadeFunc which should return a facade. +entire mechanism by defining PublishedSnapshot.GetCurrentPublishedSnapshotFunc which should return a PublishedSnapshot. A PublishedContent keeps only id-references to its parent and children, and needs a way to retrieve the actual objects from the cache - which depends on the current -facade. It is possible to override the entire mechanism by defining PublishedContent. +PublishedSnapshot. It is possible to override the entire mechanism by defining PublishedContent. GetContentByIdFunc or .GetMediaByIdFunc. Setting these functions must be done before Resolution is frozen. @@ -110,7 +106,7 @@ possible to support detached contents & properties, even those that do not have int id, but again this should be refactored entirely anyway. Not doing any row-version checks (see XmlStore) when reloading from database, though it -is maintained in the database. Two FIXME in FacadeService. Should we do it? +is maintained in the database. Two FIXME in PublishedSnapshotService. Should we do it? There is no on-disk cache at all so everything is reloaded from the cmsContentNu table when the site restarts. This is pretty fast, but we should experiment with solutions to @@ -121,4 +117,4 @@ PublishedMember exposes properties that IPublishedContent does not, and that are to be lost soon as the member is wrapped (content set, model...) - so we probably need some sort of IPublishedMember. -/ \ No newline at end of file +/ diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 5d29e53d4a..861b95691b 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -1216,7 +1216,7 @@ - + @@ -1279,4 +1279,4 @@ - + \ No newline at end of file diff --git a/src/Umbraco.Web/UmbracoApplication.cs b/src/Umbraco.Web/UmbracoApplication.cs index ad8dcb7e8b..f8ee238da7 100644 --- a/src/Umbraco.Web/UmbracoApplication.cs +++ b/src/Umbraco.Web/UmbracoApplication.cs @@ -22,7 +22,7 @@ namespace Umbraco.Web var mainDomLock = appSettingMainDomLock == "SqlMainDomLock" ? (IMainDomLock)new SqlMainDomLock(logger) - : new MainDomSemaphoreLock(); + : new MainDomSemaphoreLock(logger); var runtime = new WebRuntime(this, logger, new MainDom(logger, mainDomLock)); From d33928f4259b01e4045a46adf92c2356d7297406 Mon Sep 17 00:00:00 2001 From: abi Date: Fri, 3 Jan 2020 01:43:37 +0100 Subject: [PATCH 28/85] Correct project references --- src/Umbraco.Web/Umbraco.Web.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 0980b1b3d6..99eadef2bd 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -249,8 +249,8 @@ - - + + From 3d8e9a78e3da9bd8d64abd993f69c1374991c20f Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 3 Jan 2020 12:39:17 +1100 Subject: [PATCH 29/85] Fixes deadlock --- .../NuCache/PublishedSnapshotService.cs | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index 2439633005..97e546e603 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; +using System.Threading; using CSharpTest.Net.Collections; using Newtonsoft.Json; using Umbraco.Core; @@ -54,6 +55,7 @@ namespace Umbraco.Web.PublishedCache.NuCache private readonly ContentStore _mediaStore; private readonly SnapDictionary _domainStore; private readonly object _storesLock = new object(); + private readonly object _elementsLock = new object(); private BPlusTree _localContentDb; private BPlusTree _localMediaDb; @@ -143,7 +145,7 @@ namespace Umbraco.Web.PublishedCache.NuCache _domainStore = new SnapDictionary(); - LoadCachesOnStartup(); + LoadCachesOnStartup(); } Guid GetUid(ContentStore store, int id) => store.LiveSnapshot.Get(id)?.Uid ?? default; @@ -171,7 +173,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var path = GetLocalFilesPath(); var localContentDbPath = Path.Combine(path, "NuCache.Content.db"); var localMediaDbPath = Path.Combine(path, "NuCache.Media.db"); - + _localContentDbExists = File.Exists(localContentDbPath); _localMediaDbExists = File.Exists(localMediaDbPath); @@ -221,7 +223,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { okContent = LockAndLoadContent(scope => LoadContentFromLocalDbLocked(true)); if (!okContent) - _logger.Warn("Loading content from local db raised warnings, will reload from database."); + _logger.Warn("Loading content from local db raised warnings, will reload from database."); } if (_localMediaDbExists) @@ -1147,12 +1149,12 @@ namespace Umbraco.Web.PublishedCache.NuCache SnapDictionary.Snapshot domainSnap; IAppCache elementsCache; - // TODO: Idea... TryGetElements? Might check if we are shutting down and return false and callers to this could handle it? - // Else does a readerwriterlockslim work here? (i don't think so) - // Else we have 2x locks, one for startup/shutdown, the other for getting elements and then we can maybe do a Monitor.Try? - // That is sort of the same as a TryGetElements + // Here we are reading/writing to shared objects so we need to lock (can't be _storesLock which manages the actual nucache files + // and would result in a deadlock). Even though we are locking around underlying readlocks (within CreateSnapshot) it's because + // we need to ensure that the result of contentSnap.Gen (etc) and the re-assignment of these values and _elements cache + // are done atomically. - lock (_storesLock) + lock (_elementsLock) { var scopeContext = _scopeProvider.Context; @@ -1177,14 +1179,14 @@ namespace Umbraco.Web.PublishedCache.NuCache // elements // just need to make sure nothing gets elements in another enlisted action... so using // a MaxValue to make sure this one runs last, and it should be ok - // ... else there is potential to deadlock since this would recursively go back into trying - // lock on _storesLock but another thread may have already tried that + scopeContext.Enlist("Umbraco.Web.PublishedCache.NuCache.PublishedSnapshotService.Resync", () => this, (completed, svc) => { ((PublishedSnapshot)svc.CurrentPublishedSnapshot)?.Resync(); }, int.MaxValue); } + // create a new snapshot cache if snapshots are different gens if (contentSnap.Gen != _contentGen || mediaSnap.Gen != _mediaGen || domainSnap.Gen != _domainGen || _elementsCache == null) { @@ -1351,7 +1353,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { //culture changed on an existing language var cultureChanged = e.SavedEntities.Any(x => !x.WasPropertyDirty(nameof(ILanguage.Id)) && x.WasPropertyDirty(nameof(ILanguage.IsoCode))); - if(cultureChanged) + if (cultureChanged) { RebuildContentDbCache(); } From e6b333a3ec2f5234a081bb7f1ab821e42f332a3e Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 3 Jan 2020 12:39:56 +1100 Subject: [PATCH 30/85] Changes readlocks to normal locks, no need to have extra objects allocated and complex code. --- .../PublishedCache/NuCache/ContentStore.cs | 72 +++++-------------- .../PublishedCache/NuCache/SnapDictionary.cs | 58 ++++----------- 2 files changed, 32 insertions(+), 98 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 81229af9e1..450dc2f283 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -20,6 +20,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // this class is an extended version of SnapDictionary // most of the snapshots management code, etc is an exact copy // SnapDictionary has unit tests to ensure it all works correctly + // For locking information, see SnapDictionary private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; private readonly IVariationContextAccessor _variationContextAccessor; @@ -79,11 +80,6 @@ namespace Umbraco.Web.PublishedCache.NuCache private readonly string _instanceId = Guid.NewGuid().ToString("N"); - private class ReadLockInfo - { - public bool Taken; - } - private class WriteLockInfo { public bool Taken; @@ -121,15 +117,10 @@ namespace Umbraco.Web.PublishedCache.NuCache private void Lock(WriteLockInfo lockInfo, bool forceGen = false) { - // TODO: We are in a deadlock here somehow?? - Monitor.Enter(_wlocko, ref lockInfo.Taken); - var rtaken = false; - try + lock(_rlocko) { - Monitor.Enter(_rlocko, ref rtaken); - // see SnapDictionary try { } finally @@ -146,26 +137,16 @@ namespace Umbraco.Web.PublishedCache.NuCache _nextGen = true; } } - } - finally - { - if (rtaken) Monitor.Exit(_rlocko); - } - } - - private void Lock(ReadLockInfo lockInfo) - { - Monitor.Enter(_rlocko, ref lockInfo.Taken); + } } private void Release(WriteLockInfo lockInfo, bool commit = true) { if (commit == false) { - var rtaken = false; - try + lock(_rlocko) { - Monitor.Enter(_rlocko, ref rtaken); + // see SnapDictionary try { } finally { @@ -173,10 +154,6 @@ namespace Umbraco.Web.PublishedCache.NuCache _liveGen -= 1; } } - finally - { - if (rtaken) Monitor.Exit(_rlocko); - } Rollback(_contentNodes); RollbackRoot(); @@ -207,11 +184,6 @@ namespace Umbraco.Web.PublishedCache.NuCache Monitor.Exit(_wlocko); } - private void Release(ReadLockInfo lockInfo) - { - if (lockInfo.Taken) Monitor.Exit(_rlocko); - } - private void RollbackRoot() { if (_root.Gen <= _liveGen) return; @@ -1248,13 +1220,8 @@ namespace Umbraco.Web.PublishedCache.NuCache public Snapshot CreateSnapshot() { - var lockInfo = new ReadLockInfo(); - try + lock(_rlocko) { - // TODO: This would be much simpler with just a lock(_rlocko) { } - // in this case I see no reason why we are using this syntax?! - Lock(lockInfo); - // if no next generation is required, and we already have one, // use it and create a new snapshot if (_nextGen == false && _genObj != null) @@ -1306,10 +1273,6 @@ namespace Umbraco.Web.PublishedCache.NuCache return snapshot; } - finally - { - Release(lockInfo); - } } public Snapshot LiveSnapshot => new Snapshot(this, _liveGen @@ -1427,18 +1390,17 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - // TODO: This is never used? Should it be? - - public async Task WaitForPendingCollect() - { - Task task; - lock (_rlocko) - { - task = _collectTask; - } - if (task != null) - await task; - } + // TODO: This is never used? Should it be? Maybe move to TestHelper below? + //public async Task WaitForPendingCollect() + //{ + // Task task; + // lock (_rlocko) + // { + // task = _collectTask; + // } + // if (task != null) + // await task; + //} public long GenCount => _genObjs.Count; diff --git a/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs b/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs index 9671949ff0..e0ad26eb81 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs @@ -22,6 +22,11 @@ namespace Umbraco.Web.PublishedCache.NuCache // This class is optimized for many readers, few writers // Readers are lock-free + // NOTE - we used to lock _rlocko the long hand way with Monitor.Enter(_rlocko, ref lockTaken) but this has + // been replaced with a normal c# lock because that's exactly how the normal c# lock works, + // see https://blogs.msdn.microsoft.com/ericlippert/2009/03/06/locks-and-exceptions-do-not-mix/ + // for the readlock, there's no reason here to use the long hand way. + private readonly ConcurrentDictionary> _items; private readonly ConcurrentQueue _genObjs; private GenObj _genObj; @@ -71,16 +76,11 @@ namespace Umbraco.Web.PublishedCache.NuCache // are all ignored - Release is private and meant to be invoked with 'commit' being false only // only on the outermost lock (by SnapDictionaryWriter) - // using (...) {} for locking is prone to nasty leaks in case of weird exceptions - // such as thread-abort or out-of-memory, but let's not worry about it now + // side note - using (...) {} for locking is prone to nasty leaks in case of weird exceptions + // such as thread-abort or out-of-memory, which is why we've moved away from the old using wrapper we had on locking. private readonly string _instanceId = Guid.NewGuid().ToString("N"); - private class ReadLockInfo - { - public bool Taken; - } - private class WriteLockInfo { public bool Taken; @@ -121,18 +121,16 @@ namespace Umbraco.Web.PublishedCache.NuCache { Monitor.Enter(_wlocko, ref lockInfo.Taken); - var rtaken = false; - try + lock(_rlocko) { - Monitor.Enter(_rlocko, ref rtaken); - // assume everything in finally runs atomically // http://stackoverflow.com/questions/18501678/can-this-unexpected-behavior-of-prepareconstrainedregions-and-thread-abort-be-ex // http://joeduffyblog.com/2005/03/18/atomicity-and-asynchronous-exception-failures/ // http://joeduffyblog.com/2007/02/07/introducing-the-new-readerwriterlockslim-in-orcas/ // http://chabster.blogspot.fr/2013/12/readerwriterlockslim-fails-on-dual.html //RuntimeHelpers.PrepareConstrainedRegions(); - try { } finally + try { } + finally { // increment the lock count, and register that this lock is counting _wlocked++; @@ -149,15 +147,6 @@ namespace Umbraco.Web.PublishedCache.NuCache } } } - finally - { - if (rtaken) Monitor.Exit(_rlocko); - } - } - - private void Lock(ReadLockInfo lockInfo) - { - Monitor.Enter(_rlocko, ref lockInfo.Taken); } private void Release(WriteLockInfo lockInfo, bool commit = true) @@ -168,22 +157,17 @@ namespace Umbraco.Web.PublishedCache.NuCache if (commit == false) { - var rtaken = false; - try + lock(_rlocko) { - Monitor.Enter(_rlocko, ref rtaken); - try { } finally + try { } + finally { // forget about the temp. liveGen _nextGen = false; _liveGen -= 1; } } - finally - { - if (rtaken) Monitor.Exit(_rlocko); - } - + foreach (var item in _items) { var link = item.Value; @@ -202,11 +186,6 @@ namespace Umbraco.Web.PublishedCache.NuCache Monitor.Exit(_wlocko); } - private void Release(ReadLockInfo lockInfo) - { - if (lockInfo.Taken) Monitor.Exit(_rlocko); - } - #endregion #region Set, Clear, Get, Has @@ -347,11 +326,8 @@ namespace Umbraco.Web.PublishedCache.NuCache public Snapshot CreateSnapshot() { - var lockInfo = new ReadLockInfo(); - try + lock(_rlocko) { - Lock(lockInfo); - // if no next generation is required, and we already have a gen object, // use it to create a new snapshot if (_nextGen == false && _genObj != null) @@ -398,10 +374,6 @@ namespace Umbraco.Web.PublishedCache.NuCache return snapshot; } - finally - { - Release(lockInfo); - } } public Task CollectAsync() From 8e3b3c83261e5272fefb219aaceaa645775ad164 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 3 Jan 2020 13:21:49 +1100 Subject: [PATCH 31/85] Changes methods that should already be locked to check that they are and changes their names/adds docs --- .../PublishedCache/NuCache/ContentStore.cs | 488 +++++++++--------- .../NuCache/PublishedSnapshotService.cs | 30 +- 2 files changed, 270 insertions(+), 248 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 450dc2f283..82fae0d32a 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -115,6 +115,12 @@ namespace Umbraco.Web.PublishedCache.NuCache return ScopeContextualBase.Get(scopeProvider, _instanceId, scoped => new ScopedWriteLock(this, scoped)); } + private void EnsureLocked() + { + if (!Monitor.IsEntered(_wlocko)) + throw new InvalidOperationException("Write lock must be acquried."); + } + private void Lock(WriteLockInfo lockInfo, bool forceGen = false) { Monitor.Enter(_wlocko, ref lockInfo.Taken); @@ -323,34 +329,36 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - public void SetAllContentTypes(IEnumerable types) + /// + /// Updates/sets data for all content types + /// + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public void SetAllContentTypesLocked(IEnumerable types) { - // TODO: There should be NO lock here! All calls made to this are wrapped in GetScopedWriteLock + EnsureLocked(); - //var lockInfo = new WriteLockInfo(); - try + _logger.Debug("SetAllContentTypes"); + + // clear all existing content types + ClearLocked(_contentTypesById); + ClearLocked(_contentTypesByAlias); + + // set all new content types + foreach (var type in types) { - _logger.Debug("SetAllContentTypes"); - //Lock(lockInfo); - - // clear all existing content types - ClearLocked(_contentTypesById); - ClearLocked(_contentTypesByAlias); - - // set all new content types - foreach (var type in types) - { - SetValueLocked(_contentTypesById, type.Id, type); - SetValueLocked(_contentTypesByAlias, type.Alias, type); - } - - // beware! at that point the cache is inconsistent, - // assuming we are going to SetAll content items! - } - finally - { - //Release(lockInfo); + SetValueLocked(_contentTypesById, type.Id, type); + SetValueLocked(_contentTypesByAlias, type.Alias, type); } + + // beware! at that point the cache is inconsistent, + // assuming we are going to SetAll content items! } public void UpdateContentTypes(IReadOnlyCollection removedIds, IReadOnlyCollection refreshedTypes, IReadOnlyCollection kits) @@ -540,8 +548,22 @@ namespace Umbraco.Web.PublishedCache.NuCache return link; } - public bool Set(ContentNodeKit kit) + /// + /// Sets the data for a + /// + /// + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public bool SetLocked(ContentNodeKit kit) { + EnsureLocked(); + // ReSharper disable LocalizableElement if (kit.IsEmpty) throw new ArgumentException("Kit is empty.", nameof(kit)); @@ -551,58 +573,47 @@ namespace Umbraco.Web.PublishedCache.NuCache _logger.Debug("Set content ID: {KitNodeId}", kit.Node.Id); - // TODO: There should be NO locks here, all calls made to this are done within GetScopedWriteLock - //var lockInfo = new WriteLockInfo(); - try + // get existing + _contentNodes.TryGetValue(kit.Node.Id, out var link); + var existing = link?.Value; + + if (!BuildKit(kit, out var parent)) + return false; + + // moving? + var moving = existing != null && existing.ParentContentId != kit.Node.ParentContentId; + + // manage children + if (existing != null) { - //Lock(lockInfo); - - // get existing - _contentNodes.TryGetValue(kit.Node.Id, out var link); - var existing = link?.Value; - - if (!BuildKit(kit, out var parent)) - return false; - - // moving? - var moving = existing != null && existing.ParentContentId != kit.Node.ParentContentId; - - // manage children - if (existing != null) - { - kit.Node.FirstChildContentId = existing.FirstChildContentId; - kit.Node.LastChildContentId = existing.LastChildContentId; - } - - // set - SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); - if (_localDb != null) RegisterChange(kit.Node.Id, kit); - - // manage the tree - if (existing == null) - { - // new, add to parent - AddTreeNodeLocked(kit.Node, parent); - } - else if (moving || existing.SortOrder != kit.Node.SortOrder) - { - // moved, remove existing from its parent, add content to its parent - RemoveTreeNodeLocked(existing); - AddTreeNodeLocked(kit.Node); - } - else - { - // replacing existing, handle siblings - kit.Node.NextSiblingContentId = existing.NextSiblingContentId; - kit.Node.PreviousSiblingContentId = existing.PreviousSiblingContentId; - } - - _xmap[kit.Node.Uid] = kit.Node.Id; + kit.Node.FirstChildContentId = existing.FirstChildContentId; + kit.Node.LastChildContentId = existing.LastChildContentId; } - finally + + // set + SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); + if (_localDb != null) RegisterChange(kit.Node.Id, kit); + + // manage the tree + if (existing == null) { - //Release(lockInfo); + // new, add to parent + AddTreeNodeLocked(kit.Node, parent); } + else if (moving || existing.SortOrder != kit.Node.SortOrder) + { + // moved, remove existing from its parent, add content to its parent + RemoveTreeNodeLocked(existing); + AddTreeNodeLocked(kit.Node); + } + else + { + // replacing existing, handle siblings + kit.Node.NextSiblingContentId = existing.NextSiblingContentId; + kit.Node.PreviousSiblingContentId = existing.PreviousSiblingContentId; + } + + _xmap[kit.Node.Uid] = kit.Node.Id; return true; } @@ -624,211 +635,222 @@ namespace Umbraco.Web.PublishedCache.NuCache /// True if the data is coming from the database (not the local cache db) /// /// + /// /// This requires that the collection is sorted by Level + ParentId + Sort Order. /// This should be used only on a site startup as the first generations. /// This CANNOT be used after startup since it bypasses all checks for Generations. + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// /// - internal bool SetAllFastSorted(IEnumerable kits, bool fromDb) + /// + /// Thrown if this method is not called within a write lock + /// + public bool SetAllFastSortedLocked(IEnumerable kits, bool fromDb) { + EnsureLocked(); - //TODO: There should be NO locks here all calls made to this are done within GetScopedWriteLock + _logger.Debug("SetAllFastSorted"); - //var lockInfo = new WriteLockInfo(); var ok = true; - try + + ClearLocked(_contentNodes); + ClearRootLocked(); + + // The name of the game here is to populate each kit's + // FirstChildContentId + // LastChildContentId + // NextSiblingContentId + // PreviousSiblingContentId + + ContentNode previousNode = null; + ContentNode parent = null; + + foreach (var kit in kits) { - _logger.Debug("SetAllFastSorted"); - - //Lock(lockInfo); - - ClearLocked(_contentNodes); - ClearRootLocked(); - - // The name of the game here is to populate each kit's - // FirstChildContentId - // LastChildContentId - // NextSiblingContentId - // PreviousSiblingContentId - - ContentNode previousNode = null; - ContentNode parent = null; - - foreach (var kit in kits) + if (!BuildKit(kit, out var parentLink)) { - if (!BuildKit(kit, out var parentLink)) - { - ok = false; - continue; // skip that one - } - - var thisNode = kit.Node; - - if (parent == null) - { - // first parent - parent = parentLink.Value; - parent.FirstChildContentId = thisNode.Id; // this node is the first node - } - else if (parent.Id != parentLink.Value.Id) - { - // new parent - parent = parentLink.Value; - parent.FirstChildContentId = thisNode.Id; // this node is the first node - previousNode = null; // there is no previous sibling - } - - _logger.Verbose($"Set {thisNode.Id} with parent {thisNode.ParentContentId}"); - SetValueLocked(_contentNodes, thisNode.Id, thisNode); - - // if we are initializing from the database source ensure the local db is updated - if (fromDb && _localDb != null) RegisterChange(thisNode.Id, kit); - - // this node is always the last child - parent.LastChildContentId = thisNode.Id; - - // wire previous node as previous sibling - if (previousNode != null) - { - previousNode.NextSiblingContentId = thisNode.Id; - thisNode.PreviousSiblingContentId = previousNode.Id; - } - - // this node becomes the previous node - previousNode = thisNode; - - _xmap[kit.Node.Uid] = kit.Node.Id; + ok = false; + continue; // skip that one } - } - finally - { - //Release(lockInfo); + + var thisNode = kit.Node; + + if (parent == null) + { + // first parent + parent = parentLink.Value; + parent.FirstChildContentId = thisNode.Id; // this node is the first node + } + else if (parent.Id != parentLink.Value.Id) + { + // new parent + parent = parentLink.Value; + parent.FirstChildContentId = thisNode.Id; // this node is the first node + previousNode = null; // there is no previous sibling + } + + _logger.Verbose($"Set {thisNode.Id} with parent {thisNode.ParentContentId}"); + SetValueLocked(_contentNodes, thisNode.Id, thisNode); + + // if we are initializing from the database source ensure the local db is updated + if (fromDb && _localDb != null) RegisterChange(thisNode.Id, kit); + + // this node is always the last child + parent.LastChildContentId = thisNode.Id; + + // wire previous node as previous sibling + if (previousNode != null) + { + previousNode.NextSiblingContentId = thisNode.Id; + thisNode.PreviousSiblingContentId = previousNode.Id; + } + + // this node becomes the previous node + previousNode = thisNode; + + _xmap[kit.Node.Uid] = kit.Node.Id; } return ok; } - public bool SetAll(IEnumerable kits) + /// + /// Set all data for a collection of + /// + /// + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public bool SetAllLocked(IEnumerable kits) { - //TODO: There should be NO locks here all calls made to this are done within GetScopedWriteLock + EnsureLocked(); - //var lockInfo = new WriteLockInfo(); var ok = true; - try + _logger.Debug("SetAll"); + + ClearLocked(_contentNodes); + ClearRootLocked(); + + // do NOT clear types else they are gone! + //ClearLocked(_contentTypesById); + //ClearLocked(_contentTypesByAlias); + + foreach (var kit in kits) { - _logger.Debug("SetAll"); - - //Lock(lockInfo); - - ClearLocked(_contentNodes); - ClearRootLocked(); - - // do NOT clear types else they are gone! - //ClearLocked(_contentTypesById); - //ClearLocked(_contentTypesByAlias); - - foreach (var kit in kits) + if (!BuildKit(kit, out var parent)) { - if (!BuildKit(kit, out var parent)) - { - ok = false; - continue; // skip that one - } - _logger.Verbose($"Set {kit.Node.Id} with parent {kit.Node.ParentContentId}"); - SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); - - if (_localDb != null) RegisterChange(kit.Node.Id, kit); - AddTreeNodeLocked(kit.Node, parent); - - _xmap[kit.Node.Uid] = kit.Node.Id; + ok = false; + continue; // skip that one } - } - finally - { - //Release(lockInfo); + _logger.Verbose($"Set {kit.Node.Id} with parent {kit.Node.ParentContentId}"); + SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); + + if (_localDb != null) RegisterChange(kit.Node.Id, kit); + AddTreeNodeLocked(kit.Node, parent); + + _xmap[kit.Node.Uid] = kit.Node.Id; } return ok; } - // IMPORTANT kits must be sorted out by LEVEL and by SORT ORDER - public bool SetBranch(int rootContentId, IEnumerable kits) + /// + /// Sets data for a branch of + /// + /// + /// + /// + /// + /// + /// IMPORTANT kits must be sorted out by LEVEL and by SORT ORDER + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// + /// Thrown if this method is not called within a write lock + /// + public bool SetBranchLocked(int rootContentId, IEnumerable kits) { - //TODO: There should be NO locks here all calls made to this are done within GetScopedWriteLock + EnsureLocked(); - //var lockInfo = new WriteLockInfo(); var ok = true; - try + _logger.Debug("SetBranch"); + + // get existing + _contentNodes.TryGetValue(rootContentId, out var link); + var existing = link?.Value; + + // clear + if (existing != null) { - _logger.Debug("SetBranch"); - - //Lock(lockInfo); - - // get existing - _contentNodes.TryGetValue(rootContentId, out var link); - var existing = link?.Value; - - // clear - if (existing != null) - { - //this zero's out the branch (recursively), if we're in a new gen this will add a NULL placeholder for the gen - ClearBranchLocked(existing); - //TODO: This removes the current GEN from the tree - do we really want to do that? - RemoveTreeNodeLocked(existing); - } - - // now add them all back - foreach (var kit in kits) - { - if (!BuildKit(kit, out var parent)) - { - ok = false; - continue; // skip that one - } - SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); - if (_localDb != null) RegisterChange(kit.Node.Id, kit); - AddTreeNodeLocked(kit.Node, parent); - - _xmap[kit.Node.Uid] = kit.Node.Id; - } + //this zero's out the branch (recursively), if we're in a new gen this will add a NULL placeholder for the gen + ClearBranchLocked(existing); + //TODO: This removes the current GEN from the tree - do we really want to do that? + RemoveTreeNodeLocked(existing); } - finally + + // now add them all back + foreach (var kit in kits) { - //Release(lockInfo); + if (!BuildKit(kit, out var parent)) + { + ok = false; + continue; // skip that one + } + SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); + if (_localDb != null) RegisterChange(kit.Node.Id, kit); + AddTreeNodeLocked(kit.Node, parent); + + _xmap[kit.Node.Uid] = kit.Node.Id; } return ok; } - public bool Clear(int id) + /// + /// Clears data for a given node id + /// + /// + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public bool ClearLocked(int id) { - // TODO: There should be NO locks here! All calls to this are made within GetScopedWriteLock - //var lockInfo = new WriteLockInfo(); - try - { - _logger.Debug("Clear"); + EnsureLocked(); - //Lock(lockInfo); + _logger.Debug("Clear"); - // try to find the content - // if it is not there, nothing to do - _contentNodes.TryGetValue(id, out var link); // else null - if (link?.Value == null) return false; + // try to find the content + // if it is not there, nothing to do + _contentNodes.TryGetValue(id, out var link); // else null + if (link?.Value == null) return false; - var content = link.Value; - _logger.Debug("Clear content ID: {ContentId}", content.Id); + var content = link.Value; + _logger.Debug("Clear content ID: {ContentId}", content.Id); - // clear the entire branch - ClearBranchLocked(content); + // clear the entire branch + ClearBranchLocked(content); - // manage the tree - RemoveTreeNodeLocked(content); + // manage the tree + RemoveTreeNodeLocked(content); - return true; - } - finally - { - //Release(lockInfo); - } + return true; } private void ClearBranchLocked(int id) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index 97e546e603..b761fe0fa4 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -384,7 +384,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var contentTypes = _serviceContext.ContentTypeService.GetAll() .Select(x => _publishedContentTypeFactory.CreateContentType(x)); - _contentStore.SetAllContentTypes(contentTypes); + _contentStore.SetAllContentTypesLocked(contentTypes); using (_logger.TraceDuration("Loading content from database")) { @@ -395,7 +395,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // IMPORTANT GetAllContentSources sorts kits by level + parentId + sortOrder var kits = _dataSource.GetAllContentSources(scope); - return onStartup ? _contentStore.SetAllFastSorted(kits, true) : _contentStore.SetAll(kits); + return onStartup ? _contentStore.SetAllFastSortedLocked(kits, true) : _contentStore.SetAllLocked(kits); } } @@ -403,7 +403,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { var contentTypes = _serviceContext.ContentTypeService.GetAll() .Select(x => _publishedContentTypeFactory.CreateContentType(x)); - _contentStore.SetAllContentTypes(contentTypes); + _contentStore.SetAllContentTypesLocked(contentTypes); using (_logger.TraceDuration("Loading content from local cache file")) { @@ -455,7 +455,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var mediaTypes = _serviceContext.MediaTypeService.GetAll() .Select(x => _publishedContentTypeFactory.CreateContentType(x)); - _mediaStore.SetAllContentTypes(mediaTypes); + _mediaStore.SetAllContentTypesLocked(mediaTypes); using (_logger.TraceDuration("Loading media from database")) { @@ -467,7 +467,7 @@ namespace Umbraco.Web.PublishedCache.NuCache _logger.Debug("Loading media from database..."); // IMPORTANT GetAllMediaSources sorts kits by level + parentId + sortOrder var kits = _dataSource.GetAllMediaSources(scope); - return onStartup ? _mediaStore.SetAllFastSorted(kits, true) : _mediaStore.SetAll(kits); + return onStartup ? _mediaStore.SetAllFastSortedLocked(kits, true) : _mediaStore.SetAllLocked(kits); } } @@ -475,7 +475,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { var mediaTypes = _serviceContext.MediaTypeService.GetAll() .Select(x => _publishedContentTypeFactory.CreateContentType(x)); - _mediaStore.SetAllContentTypes(mediaTypes); + _mediaStore.SetAllContentTypesLocked(mediaTypes); using (_logger.TraceDuration("Loading media from local cache file")) { @@ -516,7 +516,7 @@ namespace Umbraco.Web.PublishedCache.NuCache return false; } - return onStartup ? store.SetAllFastSorted(kits, false) : store.SetAll(kits); + return onStartup ? store.SetAllFastSortedLocked(kits, false) : store.SetAllLocked(kits); } // keep these around - might be useful @@ -713,7 +713,7 @@ namespace Umbraco.Web.PublishedCache.NuCache if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove)) { - if (_contentStore.Clear(payload.Id)) + if (_contentStore.ClearLocked(payload.Id)) draftChanged = publishedChanged = true; continue; } @@ -736,7 +736,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // ?? should we do some RV check here? // IMPORTANT GetbranchContentSources sorts kits by level and by sort order var kits = _dataSource.GetBranchContentSources(scope, capture.Id); - _contentStore.SetBranch(capture.Id, kits); + _contentStore.SetBranchLocked(capture.Id, kits); } else { @@ -744,11 +744,11 @@ namespace Umbraco.Web.PublishedCache.NuCache var kit = _dataSource.GetContentSource(scope, capture.Id); if (kit.IsEmpty) { - _contentStore.Clear(capture.Id); + _contentStore.ClearLocked(capture.Id); } else { - _contentStore.Set(kit); + _contentStore.SetLocked(kit); } } @@ -806,7 +806,7 @@ namespace Umbraco.Web.PublishedCache.NuCache if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove)) { - if (_mediaStore.Clear(payload.Id)) + if (_mediaStore.ClearLocked(payload.Id)) anythingChanged = true; continue; } @@ -829,7 +829,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // ?? should we do some RV check here? // IMPORTANT GetbranchContentSources sorts kits by level and by sort order var kits = _dataSource.GetBranchMediaSources(scope, capture.Id); - _mediaStore.SetBranch(capture.Id, kits); + _mediaStore.SetBranchLocked(capture.Id, kits); } else { @@ -837,11 +837,11 @@ namespace Umbraco.Web.PublishedCache.NuCache var kit = _dataSource.GetMediaSource(scope, capture.Id); if (kit.IsEmpty) { - _mediaStore.Clear(capture.Id); + _mediaStore.ClearLocked(capture.Id); } else { - _mediaStore.Set(kit); + _mediaStore.SetLocked(kit); } } From 243e76b3cc3e5fccdf9c021354d4a2d88b6bb707 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 3 Jan 2020 15:04:39 +1100 Subject: [PATCH 32/85] Removes ability to have recursive locks in SnapDictionary, changes logic to require locking around the methods just like ContentStore, updates tests --- .../Cache/SnapDictionaryTests.cs | 126 ++++++++------ .../PublishedCache/NuCache/ContentStore.cs | 104 ++++++------ .../NuCache/PublishedSnapshotService.cs | 14 +- .../PublishedCache/NuCache/SnapDictionary.cs | 154 ++++++++---------- 4 files changed, 204 insertions(+), 194 deletions(-) diff --git a/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs b/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs index b435af9e77..86426d196c 100644 --- a/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs +++ b/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs @@ -9,6 +9,47 @@ using Umbraco.Web.PublishedCache.NuCache.Snap; namespace Umbraco.Tests.Cache { + /// + /// Used for tests + /// + 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; + } + } + [TestFixture] public class SnapDictionaryTests { @@ -223,7 +264,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 +362,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 +457,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 +619,7 @@ namespace Umbraco.Tests.Cache { var d = new SnapDictionary(); d.Test.CollectAuto = false; - + d.Set(1, "one"); d.Set(2, "two"); @@ -689,7 +730,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 +768,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 +807,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 +835,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 +883,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 +916,7 @@ namespace Umbraco.Tests.Cache { var d = new SnapDictionary(); d.Test.CollectAuto = false; + // gen 1 d.Set(1, "one"); @@ -894,12 +930,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 +969,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 +1001,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 +1018,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 +1030,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 +1041,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 +1053,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 +1130,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 +1141,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 +1152,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 +1161,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 +1169,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); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 82fae0d32a..17eb2b6457 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -39,7 +39,6 @@ namespace Umbraco.Web.PublishedCache.NuCache private long _liveGen, _floorGen; private bool _nextGen, _collectAuto; private Task _collectTask; - private volatile int _wlocked; private List> _wchanges; // TODO: collection trigger (ok for now) @@ -83,7 +82,6 @@ namespace Umbraco.Web.PublishedCache.NuCache private class WriteLockInfo { public bool Taken; - public bool Count; } // a scope contextual that represents a locked writer to the dictionary @@ -111,7 +109,6 @@ namespace Umbraco.Web.PublishedCache.NuCache // TODO: GetScopedWriter? should the dict have a ref onto the scope provider? public IDisposable GetScopedWriteLock(IScopeProvider scopeProvider) { - _logger.Debug("GetScopedWriteLock"); return ScopeContextualBase.Get(scopeProvider, _instanceId, scoped => new ScopedWriteLock(this, scoped)); } @@ -123,6 +120,9 @@ namespace Umbraco.Web.PublishedCache.NuCache private void Lock(WriteLockInfo lockInfo, bool forceGen = false) { + if (Monitor.IsEntered(_wlocko)) + throw new InvalidOperationException("Recursive locks not allowed"); + Monitor.Enter(_wlocko, ref lockInfo.Taken); lock(_rlocko) @@ -131,9 +131,7 @@ namespace Umbraco.Web.PublishedCache.NuCache try { } finally { - _wlocked++; - lockInfo.Count = true; - if (_nextGen == false || (forceGen && _wlocked == 1)) + if (_nextGen == false || (forceGen)) { // because we are changing things, a new generation // is created, which will trigger a new snapshot @@ -179,12 +177,6 @@ namespace Umbraco.Web.PublishedCache.NuCache _localDb.Commit(); } - // TODO: Shouldn't this be in a finally block? - // TODO: Shouldn't this be decremented after we exit?? - // TODO: Shouldn't the locked flag never exceed 1? - if (lockInfo.Count) - _wlocked--; - // TODO: Shouldn't this be in a finally block? if (lockInfo.Taken) Monitor.Exit(_wlocko); @@ -220,22 +212,18 @@ namespace Umbraco.Web.PublishedCache.NuCache public void ReleaseLocalDb() { - _logger.Info("Releasing DB..."); var lockInfo = new WriteLockInfo(); try { try { - // Trying to lock could throw exceptions so always make sure to clean up. - _logger.Info("Trying to lock before releasing DB (lock count = {LockCount}) ...", _wlocked); - + // Trying to lock could throw exceptions so always make sure to clean up. Lock(lockInfo); } finally { try { - _logger.Info("Disposing local DB..."); _localDb?.Dispose(); } catch (Exception ex) @@ -275,57 +263,61 @@ namespace Umbraco.Web.PublishedCache.NuCache #region Content types - public void NewContentTypes(IEnumerable types) + /// + /// Sets data for new content types + /// + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public void NewContentTypesLocked(IEnumerable types) { - var lockInfo = new WriteLockInfo(); - try - { - _logger.Debug("NewContentTypes"); - Lock(lockInfo); + EnsureLocked(); - foreach (var type in types) - { - SetValueLocked(_contentTypesById, type.Id, type); - SetValueLocked(_contentTypesByAlias, type.Alias, type); - } - } - finally + foreach (var type in types) { - Release(lockInfo); + SetValueLocked(_contentTypesById, type.Id, type); + SetValueLocked(_contentTypesByAlias, type.Alias, type); } } - public void UpdateContentTypes(IEnumerable types) + /// + /// Sets data for updated content types + /// + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public void UpdateContentTypesLocked(IEnumerable types) { //nothing to do if this is empty, no need to lock/allocate/iterate/etc... if (!types.Any()) return; - var lockInfo = new WriteLockInfo(); - try + EnsureLocked(); + + var index = types.ToDictionary(x => x.Id, x => x); + + foreach (var type in index.Values) { - _logger.Debug("UpdateContentTypes"); - Lock(lockInfo); - - var index = types.ToDictionary(x => x.Id, x => x); - - foreach (var type in index.Values) - { - SetValueLocked(_contentTypesById, type.Id, type); - SetValueLocked(_contentTypesByAlias, type.Alias, type); - } - - foreach (var link in _contentNodes.Values) - { - var node = link.Value; - if (node == null) continue; - var contentTypeId = node.ContentType.Id; - if (index.TryGetValue(contentTypeId, out var contentType) == false) continue; - SetValueLocked(_contentNodes, node.Id, new ContentNode(node, contentType)); - } + SetValueLocked(_contentTypesById, type.Id, type); + SetValueLocked(_contentTypesByAlias, type.Alias, type); } - finally + + foreach (var link in _contentNodes.Values) { - Release(lockInfo); + var node = link.Value; + if (node == null) continue; + var contentTypeId = node.ContentType.Id; + if (index.TryGetValue(contentTypeId, out var contentType) == false) continue; + SetValueLocked(_contentNodes, node.Id, new ContentNode(node, contentType)); } } @@ -1256,7 +1248,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // else we need to try to create a new gen ref // whether we are wlocked or not, noone can rlock while we do, // so _liveGen and _nextGen are safe - if (_wlocked > 0) // volatile, cannot ++ but could -- + if (Monitor.IsEntered(_wlocko)) { // write-locked, cannot use latest gen (at least 1) so use previous var snapGen = _nextGen ? _liveGen - 1 : _liveGen; diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index b761fe0fa4..249eb882f9 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -622,7 +622,7 @@ namespace Umbraco.Web.PublishedCache.NuCache .Where(x => x.RootContentId.HasValue && x.LanguageIsoCode.IsNullOrWhiteSpace() == false) .Select(x => new Domain(x.Id, x.DomainName, x.RootContentId.Value, CultureInfo.GetCultureInfo(x.LanguageIsoCode), x.IsWildcard))) { - _domainStore.Set(domain.Id, domain); + _domainStore.SetLocked(domain.Id, domain); } } @@ -980,7 +980,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } break; case DomainChangeTypes.Remove: - _domainStore.Clear(payload.Id); + _domainStore.ClearLocked(payload.Id); break; case DomainChangeTypes.Refresh: var domain = _serviceContext.DomainService.GetById(payload.Id); @@ -988,7 +988,7 @@ namespace Umbraco.Web.PublishedCache.NuCache if (domain.RootContentId.HasValue == false) continue; // anomaly if (domain.LanguageIsoCode.IsNullOrWhiteSpace()) continue; // anomaly var culture = CultureInfo.GetCultureInfo(domain.LanguageIsoCode); - _domainStore.Set(domain.Id, new Domain(domain.Id, domain.DomainName, domain.RootContentId.Value, culture, domain.IsWildcard)); + _domainStore.SetLocked(domain.Id, new Domain(domain.Id, domain.DomainName, domain.RootContentId.Value, culture, domain.IsWildcard)); break; } } @@ -1075,9 +1075,9 @@ namespace Umbraco.Web.PublishedCache.NuCache _contentStore.UpdateContentTypes(removedIds, typesA, kits); if (!otherIds.IsCollectionEmpty()) - _contentStore.UpdateContentTypes(CreateContentTypes(PublishedItemType.Content, otherIds.ToArray())); + _contentStore.UpdateContentTypesLocked(CreateContentTypes(PublishedItemType.Content, otherIds.ToArray())); if (!newIds.IsCollectionEmpty()) - _contentStore.NewContentTypes(CreateContentTypes(PublishedItemType.Content, newIds.ToArray())); + _contentStore.NewContentTypesLocked(CreateContentTypes(PublishedItemType.Content, newIds.ToArray())); scope.Complete(); } } @@ -1106,9 +1106,9 @@ namespace Umbraco.Web.PublishedCache.NuCache _mediaStore.UpdateContentTypes(removedIds, typesA, kits); if (!otherIds.IsCollectionEmpty()) - _mediaStore.UpdateContentTypes(CreateContentTypes(PublishedItemType.Media, otherIds.ToArray()).ToArray()); + _mediaStore.UpdateContentTypesLocked(CreateContentTypes(PublishedItemType.Media, otherIds.ToArray()).ToArray()); if (!newIds.IsCollectionEmpty()) - _mediaStore.NewContentTypes(CreateContentTypes(PublishedItemType.Media, newIds.ToArray()).ToArray()); + _mediaStore.NewContentTypesLocked(CreateContentTypes(PublishedItemType.Media, newIds.ToArray()).ToArray()); scope.Complete(); } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs b/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs index e0ad26eb81..c38940da25 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs @@ -35,7 +35,6 @@ namespace Umbraco.Web.PublishedCache.NuCache private long _liveGen, _floorGen; private bool _nextGen, _collectAuto; private Task _collectTask; - private volatile int _wlocked; // minGenDelta to be adjusted // we may want to throttle collects even if delta is reached @@ -84,7 +83,6 @@ namespace Umbraco.Web.PublishedCache.NuCache private class WriteLockInfo { public bool Taken; - public bool Count; } // a scope contextual that represents a locked writer to the dictionary @@ -92,8 +90,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { private readonly WriteLockInfo _lockinfo = new WriteLockInfo(); private readonly SnapDictionary _dictionary; - private int _released; - + public ScopedWriteLock(SnapDictionary dictionary, bool scoped) { _dictionary = dictionary; @@ -102,8 +99,6 @@ namespace Umbraco.Web.PublishedCache.NuCache public override void Release(bool completed) { - if (Interlocked.CompareExchange(ref _released, 1, 0) != 0) - return; _dictionary.Release(_lockinfo, completed); } } @@ -117,8 +112,17 @@ namespace Umbraco.Web.PublishedCache.NuCache return ScopeContextualBase.Get(scopeProvider, _instanceId, scoped => new ScopedWriteLock(this, scoped)); } + private void EnsureLocked() + { + if (!Monitor.IsEntered(_wlocko)) + throw new InvalidOperationException("Write lock must be acquried."); + } + private void Lock(WriteLockInfo lockInfo, bool forceGen = false) { + if (Monitor.IsEntered(_wlocko)) + throw new InvalidOperationException("Recursive locks not allowed"); + Monitor.Enter(_wlocko, ref lockInfo.Taken); lock(_rlocko) @@ -132,11 +136,7 @@ namespace Umbraco.Web.PublishedCache.NuCache try { } finally { - // increment the lock count, and register that this lock is counting - _wlocked++; - lockInfo.Count = true; - - if (_nextGen == false || (forceGen && _wlocked == 1)) + if (_nextGen == false || (forceGen)) { // because we are changing things, a new generation // is created, which will trigger a new snapshot @@ -181,8 +181,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - // decrement the lock count, if counting, then exit the lock - if (lockInfo.Count) _wlocked--; + // TODO: Shouldn't this be in a finally block? Monitor.Exit(_wlocko); } @@ -198,75 +197,59 @@ namespace Umbraco.Web.PublishedCache.NuCache return link; } - public void Set(TKey key, TValue value) + public void SetLocked(TKey key, TValue value) { - var lockInfo = new WriteLockInfo(); - try - { - Lock(lockInfo); + EnsureLocked(); - // this is safe only because we're write-locked - var link = GetHead(key); - if (link != null) + // this is safe only because we're write-locked + var link = GetHead(key); + if (link != null) + { + // already in the dict + if (link.Gen != _liveGen) { - // already in the dict - if (link.Gen != _liveGen) - { - // for an older gen - if value is different then insert a new - // link for the new gen, with the new value - if (link.Value != value) - _items.TryUpdate(key, new LinkedNode(value, _liveGen, link), link); - } - else - { - // for the live gen - we can fix the live gen - and remove it - // if value is null and there's no next gen - if (value == null && link.Next == null) - _items.TryRemove(key, out link); - else - link.Value = value; - } + // for an older gen - if value is different then insert a new + // link for the new gen, with the new value + if (link.Value != value) + _items.TryUpdate(key, new LinkedNode(value, _liveGen, link), link); } else { - _items.TryAdd(key, new LinkedNode(value, _liveGen)); - } - } - finally - { - Release(lockInfo); - } - } - - public void Clear(TKey key) - { - Set(key, null); - } - - public void Clear() - { - var lockInfo = new WriteLockInfo(); - try - { - Lock(lockInfo); - - // this is safe only because we're write-locked - foreach (var kvp in _items.Where(x => x.Value != null)) - { - if (kvp.Value.Gen < _liveGen) - { - var link = new LinkedNode(null, _liveGen, kvp.Value); - _items.TryUpdate(kvp.Key, link, kvp.Value); - } + // for the live gen - we can fix the live gen - and remove it + // if value is null and there's no next gen + if (value == null && link.Next == null) + _items.TryRemove(key, out link); else - { - kvp.Value.Value = null; - } + link.Value = value; } } - finally + else { - Release(lockInfo); + _items.TryAdd(key, new LinkedNode(value, _liveGen)); + } + } + + public void ClearLocked(TKey key) + { + SetLocked(key, null); + } + + public void ClearLocked() + { + EnsureLocked(); + + // this is safe only because we're write-locked + foreach (var kvp in _items.Where(x => x.Value != null)) + { + if (kvp.Value.Gen < _liveGen) + { + var link = new LinkedNode(null, _liveGen, kvp.Value); + _items.TryUpdate(kvp.Key, link, kvp.Value); + } + else + { + kvp.Value.Value = null; + } } } @@ -336,7 +319,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // else we need to try to create a new gen object // whether we are wlocked or not, noone can rlock while we do, // so _liveGen and _nextGen are safe - if (_wlocked > 0) // volatile, cannot ++ but could -- + if (Monitor.IsEntered(_wlocko)) { // write-locked, cannot use latest gen (at least 1) so use previous var snapGen = _nextGen ? _liveGen - 1 : _liveGen; @@ -468,17 +451,18 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - public /*async*/ Task PendingCollect() - { - Task task; - lock (_rlocko) - { - task = _collectTask; - } - return task ?? Task.CompletedTask; - //if (task != null) - // await task; - } + // TODO: This is never used? Should it be? Maybe move to TestHelper below? + //public /*async*/ Task PendingCollect() + //{ + // Task task; + // lock (_rlocko) + // { + // task = _collectTask; + // } + // return task ?? Task.CompletedTask; + // //if (task != null) + // // await task; + //} public long GenCount => _genObjs.Count; @@ -503,7 +487,7 @@ namespace Umbraco.Web.PublishedCache.NuCache public long LiveGen => _dict._liveGen; public long FloorGen => _dict._floorGen; public bool NextGen => _dict._nextGen; - public int WLocked => _dict._wlocked; + public bool IsLocked => Monitor.IsEntered(_dict._wlocko); public bool CollectAuto { From 7a129f890dc5b87094901d2ac3bbe953486be04b Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 3 Jan 2020 15:07:21 +1100 Subject: [PATCH 33/85] Changes MainDom testing timeout to be larger... but not 1.5 mins! --- src/Umbraco.Core/Runtime/MainDom.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Core/Runtime/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs index 883b69b2a7..d7bd407015 100644 --- a/src/Umbraco.Core/Runtime/MainDom.cs +++ b/src/Umbraco.Core/Runtime/MainDom.cs @@ -36,7 +36,7 @@ namespace Umbraco.Core.Runtime // actions to run before releasing the main domain private readonly List> _callbacks = new List>(); - private const int LockTimeoutMilliseconds = 10000; // 10 seconds + private const int LockTimeoutMilliseconds = 40000; // 40 seconds #endregion @@ -151,22 +151,20 @@ namespace Umbraco.Core.Runtime { _logger.Info("Cannot acquire (timeout)."); - // TODO: Previously we'd throw an exception and the appdomain would not start, what do we want to do? - - // return false; + // TODO: 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; } try { // Listen for the signal from another AppDomain coming online to release the lock - _mainDomLock.ListenAsync() - .ContinueWith(_ => - { - _logger.Debug("Signal heard from other appdomain."); - OnSignal("signal"); - }); + _mainDomLock.ListenAsync().ContinueWith(_ => OnSignal("signal")); } catch (OperationCanceledException ex) { From c2ac5e85311b39ee0d9c8734659a39fb6be55ab9 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 6 Jan 2020 18:34:04 +1100 Subject: [PATCH 34/85] Fixes a couple more issues with recursive locks --- .../PublishedCache/NuCache/ContentStore.cs | 244 +++++++++--------- .../NuCache/PublishedSnapshotService.cs | 8 +- 2 files changed, 130 insertions(+), 122 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 17eb2b6457..8ce299932c 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -14,7 +14,18 @@ using Umbraco.Web.PublishedCache.NuCache.Snap; namespace Umbraco.Web.PublishedCache.NuCache { - // stores content + /// + /// Stores content in memory and persists it back to disk + /// + /// + /// + /// Methods in this class suffixed with the term "Locked" means that those methods can only be called within a WriteLock. A WriteLock + /// is acquired by the GetScopedWriteLock method. Locks are not allowed to be recursive. + /// + /// + /// This class's logic is based on the class but has been slightly modified to suit these purposes. + /// + /// internal class ContentStore { // this class is an extended version of SnapDictionary @@ -336,8 +347,6 @@ namespace Umbraco.Web.PublishedCache.NuCache { EnsureLocked(); - _logger.Debug("SetAllContentTypes"); - // clear all existing content types ClearLocked(_contentTypesById); ClearLocked(_contentTypesByAlias); @@ -353,8 +362,23 @@ namespace Umbraco.Web.PublishedCache.NuCache // assuming we are going to SetAll content items! } - public void UpdateContentTypes(IReadOnlyCollection removedIds, IReadOnlyCollection refreshedTypes, IReadOnlyCollection kits) + /// + /// Updates/sets/removes data for content types + /// + /// + /// + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public void UpdateContentTypesLocked(IReadOnlyCollection removedIds, IReadOnlyCollection refreshedTypes, IReadOnlyCollection kits) { + EnsureLocked(); + var removedIdsA = removedIds ?? Array.Empty(); var refreshedTypesA = refreshedTypes ?? Array.Empty(); var refreshedIdsA = refreshedTypesA.Select(x => x.Id).ToList(); @@ -363,87 +387,82 @@ namespace Umbraco.Web.PublishedCache.NuCache if (kits.Count == 0 && refreshedIdsA.Count == 0 && removedIdsA.Count == 0) return; //exit - there is nothing to do here - var lockInfo = new WriteLockInfo(); - try + var removedContentTypeNodes = new List(); + var refreshedContentTypeNodes = new List(); + + // find all the nodes that are either refreshed or removed, + // because of their content type being either refreshed or removed + foreach (var link in _contentNodes.Values) { - _logger.Debug("UpdateContentTypes"); - Lock(lockInfo); - - var removedContentTypeNodes = new List(); - var refreshedContentTypeNodes = new List(); - - // find all the nodes that are either refreshed or removed, - // because of their content type being either refreshed or removed - foreach (var link in _contentNodes.Values) - { - var node = link.Value; - if (node == null) continue; - var contentTypeId = node.ContentType.Id; - if (removedIdsA.Contains(contentTypeId)) removedContentTypeNodes.Add(node.Id); - if (refreshedIdsA.Contains(contentTypeId)) refreshedContentTypeNodes.Add(node.Id); - } - - // perform deletion of content with removed content type - // removing content types should have removed their content already - // but just to be 100% sure, clear again here - foreach (var node in removedContentTypeNodes) - ClearBranchLocked(node); - - // perform deletion of removed content types - foreach (var id in removedIdsA) - { - if (_contentTypesById.TryGetValue(id, out var link) == false || link.Value == null) - continue; - SetValueLocked(_contentTypesById, id, null); - SetValueLocked(_contentTypesByAlias, link.Value.Alias, null); - } - - // perform update of refreshed content types - foreach (var type in refreshedTypesA) - { - SetValueLocked(_contentTypesById, type.Id, type); - SetValueLocked(_contentTypesByAlias, type.Alias, type); - } - - // perform update of content with refreshed content type - from the kits - // skip missing type, skip missing parents & un-buildable kits - what else could we do? - // kits are ordered by level, so ParentExists is ok here - var visited = new List(); - foreach (var kit in kits.Where(x => - refreshedIdsA.Contains(x.ContentTypeId) && - BuildKit(x, out _))) - { - // replacing the node: must preserve the parents - var node = GetHead(_contentNodes, kit.Node.Id)?.Value; - if (node != null) - kit.Node.FirstChildContentId = node.FirstChildContentId; - - SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); - - visited.Add(kit.Node.Id); - if (_localDb != null) RegisterChange(kit.Node.Id, kit); - } - - // all content should have been refreshed - but... - var orphans = refreshedContentTypeNodes.Except(visited); - foreach (var id in orphans) - ClearBranchLocked(id); + var node = link.Value; + if (node == null) continue; + var contentTypeId = node.ContentType.Id; + if (removedIdsA.Contains(contentTypeId)) removedContentTypeNodes.Add(node.Id); + if (refreshedIdsA.Contains(contentTypeId)) refreshedContentTypeNodes.Add(node.Id); } - finally + + // perform deletion of content with removed content type + // removing content types should have removed their content already + // but just to be 100% sure, clear again here + foreach (var node in removedContentTypeNodes) + ClearBranchLocked(node); + + // perform deletion of removed content types + foreach (var id in removedIdsA) { - Release(lockInfo); + if (_contentTypesById.TryGetValue(id, out var link) == false || link.Value == null) + continue; + SetValueLocked(_contentTypesById, id, null); + SetValueLocked(_contentTypesByAlias, link.Value.Alias, null); } + + // perform update of refreshed content types + foreach (var type in refreshedTypesA) + { + SetValueLocked(_contentTypesById, type.Id, type); + SetValueLocked(_contentTypesByAlias, type.Alias, type); + } + + // perform update of content with refreshed content type - from the kits + // skip missing type, skip missing parents & un-buildable kits - what else could we do? + // kits are ordered by level, so ParentExists is ok here + var visited = new List(); + foreach (var kit in kits.Where(x => + refreshedIdsA.Contains(x.ContentTypeId) && + BuildKit(x, out _))) + { + // replacing the node: must preserve the parents + var node = GetHead(_contentNodes, kit.Node.Id)?.Value; + if (node != null) + kit.Node.FirstChildContentId = node.FirstChildContentId; + + SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); + + visited.Add(kit.Node.Id); + if (_localDb != null) RegisterChange(kit.Node.Id, kit); + } + + // all content should have been refreshed - but... + var orphans = refreshedContentTypeNodes.Except(visited); + foreach (var id in orphans) + ClearBranchLocked(id); } - public void UpdateDataTypes(IEnumerable dataTypeIds, Func getContentType) + /// + /// Updates data types + /// + /// + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public void UpdateDataTypesLocked(IEnumerable dataTypeIds, Func getContentType) { - var lockInfo = new WriteLockInfo(); - try - { - _logger.Debug("UpdateDataTypes"); - Lock(lockInfo); - - var contentTypes = _contentTypesById + var contentTypes = _contentTypesById .Where(kvp => kvp.Value.Value != null && kvp.Value.Value.PropertyTypes.Any(p => dataTypeIds.Contains(p.DataType.Id))) @@ -452,37 +471,32 @@ namespace Umbraco.Web.PublishedCache.NuCache .Where(x => x != null) // poof, gone, very unlikely and probably an anomaly .ToArray(); - var contentTypeIdsA = contentTypes.Select(x => x.Id).ToArray(); - var contentTypeNodes = new Dictionary>(); - foreach (var id in contentTypeIdsA) - contentTypeNodes[id] = new List(); - foreach (var link in _contentNodes.Values) - { - var node = link.Value; - if (node != null && contentTypeIdsA.Contains(node.ContentType.Id)) - contentTypeNodes[node.ContentType.Id].Add(node.Id); - } - - foreach (var contentType in contentTypes) - { - // again, weird situation - if (contentTypeNodes.ContainsKey(contentType.Id) == false) - continue; - - foreach (var id in contentTypeNodes[contentType.Id]) - { - _contentNodes.TryGetValue(id, out var link); - if (link?.Value == null) - continue; - var node = new ContentNode(link.Value, contentType); - SetValueLocked(_contentNodes, id, node); - if (_localDb != null) RegisterChange(id, node.ToKit()); - } - } - } - finally + var contentTypeIdsA = contentTypes.Select(x => x.Id).ToArray(); + var contentTypeNodes = new Dictionary>(); + foreach (var id in contentTypeIdsA) + contentTypeNodes[id] = new List(); + foreach (var link in _contentNodes.Values) { - Release(lockInfo); + var node = link.Value; + if (node != null && contentTypeIdsA.Contains(node.ContentType.Id)) + contentTypeNodes[node.ContentType.Id].Add(node.Id); + } + + foreach (var contentType in contentTypes) + { + // again, weird situation + if (contentTypeNodes.ContainsKey(contentType.Id) == false) + continue; + + foreach (var id in contentTypeNodes[contentType.Id]) + { + _contentNodes.TryGetValue(id, out var link); + if (link?.Value == null) + continue; + var node = new ContentNode(link.Value, contentType); + SetValueLocked(_contentNodes, id, node); + if (_localDb != null) RegisterChange(id, node.ToKit()); + } } } @@ -644,8 +658,6 @@ namespace Umbraco.Web.PublishedCache.NuCache { EnsureLocked(); - _logger.Debug("SetAllFastSorted"); - var ok = true; ClearLocked(_contentNodes); @@ -684,7 +696,7 @@ namespace Umbraco.Web.PublishedCache.NuCache previousNode = null; // there is no previous sibling } - _logger.Verbose($"Set {thisNode.Id} with parent {thisNode.ParentContentId}"); + _logger.Debug($"Set {thisNode.Id} with parent {thisNode.ParentContentId}"); SetValueLocked(_contentNodes, thisNode.Id, thisNode); // if we are initializing from the database source ensure the local db is updated @@ -726,8 +738,7 @@ namespace Umbraco.Web.PublishedCache.NuCache EnsureLocked(); var ok = true; - _logger.Debug("SetAll"); - + ClearLocked(_contentNodes); ClearRootLocked(); @@ -742,7 +753,7 @@ namespace Umbraco.Web.PublishedCache.NuCache ok = false; continue; // skip that one } - _logger.Verbose($"Set {kit.Node.Id} with parent {kit.Node.ParentContentId}"); + _logger.Debug($"Set {kit.Node.Id} with parent {kit.Node.ParentContentId}"); SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); if (_localDb != null) RegisterChange(kit.Node.Id, kit); @@ -777,8 +788,7 @@ namespace Umbraco.Web.PublishedCache.NuCache EnsureLocked(); var ok = true; - _logger.Debug("SetBranch"); - + // get existing _contentNodes.TryGetValue(rootContentId, out var link); var existing = link?.Value; @@ -826,8 +836,6 @@ namespace Umbraco.Web.PublishedCache.NuCache { EnsureLocked(); - _logger.Debug("Clear"); - // try to find the content // if it is not there, nothing to do _contentNodes.TryGetValue(id, out var link); // else null diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index 249eb882f9..c37e5731e4 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -943,14 +943,14 @@ namespace Umbraco.Web.PublishedCache.NuCache using (var scope = _scopeProvider.CreateScope()) { scope.ReadLock(Constants.Locks.ContentTree); - _contentStore.UpdateDataTypes(idsA, id => CreateContentType(PublishedItemType.Content, id)); + _contentStore.UpdateDataTypesLocked(idsA, id => CreateContentType(PublishedItemType.Content, id)); scope.Complete(); } using (var scope = _scopeProvider.CreateScope()) { scope.ReadLock(Constants.Locks.MediaTree); - _mediaStore.UpdateDataTypes(idsA, id => CreateContentType(PublishedItemType.Media, id)); + _mediaStore.UpdateDataTypesLocked(idsA, id => CreateContentType(PublishedItemType.Media, id)); scope.Complete(); } } @@ -1073,7 +1073,7 @@ namespace Umbraco.Web.PublishedCache.NuCache ? Array.Empty() : _dataSource.GetTypeContentSources(scope, refreshedIds).ToArray(); - _contentStore.UpdateContentTypes(removedIds, typesA, kits); + _contentStore.UpdateContentTypesLocked(removedIds, typesA, kits); if (!otherIds.IsCollectionEmpty()) _contentStore.UpdateContentTypesLocked(CreateContentTypes(PublishedItemType.Content, otherIds.ToArray())); if (!newIds.IsCollectionEmpty()) @@ -1104,7 +1104,7 @@ namespace Umbraco.Web.PublishedCache.NuCache ? Array.Empty() : _dataSource.GetTypeMediaSources(scope, refreshedIds).ToArray(); - _mediaStore.UpdateContentTypes(removedIds, typesA, kits); + _mediaStore.UpdateContentTypesLocked(removedIds, typesA, kits); if (!otherIds.IsCollectionEmpty()) _mediaStore.UpdateContentTypesLocked(CreateContentTypes(PublishedItemType.Media, otherIds.ToArray()).ToArray()); if (!newIds.IsCollectionEmpty()) From 95337d5a7008469a1cfc84215b9c98e461798fe1 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 6 Jan 2020 21:39:26 +1100 Subject: [PATCH 35/85] Cleans up old notes --- src/Umbraco.Core/Runtime/MainDom.cs | 2 +- .../Cache/SnapDictionaryTests.cs | 82 +++++++++---------- .../PublishedCache/NuCache/ContentStore.cs | 60 +++++++------- .../NuCache/PublishedSnapshot.cs | 8 -- .../NuCache/PublishedSnapshotService.cs | 10 +-- 5 files changed, 73 insertions(+), 89 deletions(-) diff --git a/src/Umbraco.Core/Runtime/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs index d7bd407015..67c7be3d21 100644 --- a/src/Umbraco.Core/Runtime/MainDom.cs +++ b/src/Umbraco.Core/Runtime/MainDom.cs @@ -151,7 +151,7 @@ namespace Umbraco.Core.Runtime { _logger.Info("Cannot acquire (timeout)."); - // TODO: In previous versions we'd let a TimeoutException be thrown + // 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. diff --git a/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs b/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs index 86426d196c..00ba721bdb 100644 --- a/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs +++ b/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs @@ -9,47 +9,6 @@ using Umbraco.Web.PublishedCache.NuCache.Snap; namespace Umbraco.Tests.Cache { - /// - /// Used for tests - /// - 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; - } - } - [TestFixture] public class SnapDictionaryTests { @@ -1184,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.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 8ce299932c..c959d9f67a 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -157,40 +157,44 @@ namespace Umbraco.Web.PublishedCache.NuCache private void Release(WriteLockInfo lockInfo, bool commit = true) { - if (commit == false) + try { - lock(_rlocko) + if (commit == false) { - // see SnapDictionary - try { } - finally + lock (_rlocko) { - _nextGen = false; - _liveGen -= 1; + // see SnapDictionary + try { } + finally + { + _nextGen = false; + _liveGen -= 1; + } } - } - Rollback(_contentNodes); - RollbackRoot(); - Rollback(_contentTypesById); - Rollback(_contentTypesByAlias); - } - else if (_localDb != null && _wchanges != null) - { - foreach (var change in _wchanges) + Rollback(_contentNodes); + RollbackRoot(); + Rollback(_contentTypesById); + Rollback(_contentTypesByAlias); + } + else if (_localDb != null && _wchanges != null) { - if (change.Value.IsNull) - _localDb.TryRemove(change.Key, out ContentNodeKit unused); - else - _localDb[change.Key] = change.Value; + foreach (var change in _wchanges) + { + if (change.Value.IsNull) + _localDb.TryRemove(change.Key, out ContentNodeKit unused); + else + _localDb[change.Key] = change.Value; + } + _wchanges = null; + _localDb.Commit(); } - _wchanges = null; - _localDb.Commit(); } - - // TODO: Shouldn't this be in a finally block? - if (lockInfo.Taken) - Monitor.Exit(_wlocko); + finally + { + if (lockInfo.Taken) + Monitor.Exit(_wlocko); + } } private void RollbackRoot() @@ -256,10 +260,6 @@ namespace Umbraco.Web.PublishedCache.NuCache } finally { - _logger.Info("Releasing ContentStore..."); - - // TODO: I don't understand this, we would have already closed the BPlusTree store above?? - // I guess it's 'safe' and will just decrement the write lock counter? Release(lockInfo); } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshot.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshot.cs index bbb84fc650..3f5c1aa4d5 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshot.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshot.cs @@ -39,15 +39,7 @@ namespace Umbraco.Web.PublishedCache.NuCache public void Resync() { - // This is annoying since this can cause deadlocks. Since this will be cleared if something happens in the same - // thread after that calls Elements, it will re-lock the _storesLock but that might already be locked again - // and since we're most likely in a ContentStore write lock, the other thread is probably wanting that one too. - // no lock - published snapshots are single-thread - - // TODO: Instead of clearing, we could hold this value in another var in case the above call to GetElements() Or potentially - // a new TryGetElements() fails (since we might be shutting down). In that case we can use the old value. - _elements?.Dispose(); _elements = null; } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index c37e5731e4..aee588d567 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -670,15 +670,7 @@ namespace Umbraco.Web.PublishedCache.NuCache publishedChanged = publishedChanged2; } - // TODO: These resync's are a problem, they cause deadlocks because when this is called, it's generally called within a writelock - // and then we clear out the snapshot and then if there's some event handler that needs the content cache, it tries to re-get it - // which first tries locking on the _storesLock which may have already been acquired and in this case we deadlock because - // we're still holding the write lock. - // We resync at the end of a ScopedWriteLock - // BUT if we don't resync here then the current snapshot is out of date for any event handlers that wish to use the most up to date - // data... - // so we need to figure out how to deal with the _storesLock - + if (draftChanged || publishedChanged) ((PublishedSnapshot)CurrentPublishedSnapshot)?.Resync(); } From cae2c3217296157d83a559fc1427ac99112a1520 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Tue, 7 Jan 2020 20:32:04 +0100 Subject: [PATCH 36/85] Remove SetFlag and UnsetFlag extension methods --- src/Umbraco.Core/EnumExtensions.cs | 38 ----------------- .../CoreThings/EnumExtensionsTests.cs | 42 ------------------- .../Mapping/ContentTypeMapDefinition.cs | 10 ++--- 3 files changed, 5 insertions(+), 85 deletions(-) diff --git a/src/Umbraco.Core/EnumExtensions.cs b/src/Umbraco.Core/EnumExtensions.cs index b2e5f32c9a..1443a27876 100644 --- a/src/Umbraco.Core/EnumExtensions.cs +++ b/src/Umbraco.Core/EnumExtensions.cs @@ -41,43 +41,5 @@ namespace Umbraco.Core 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); - } } } diff --git a/src/Umbraco.Tests/CoreThings/EnumExtensionsTests.cs b/src/Umbraco.Tests/CoreThings/EnumExtensionsTests.cs index 4a0c1d0f41..72a55cee85 100644 --- a/src/Umbraco.Tests/CoreThings/EnumExtensionsTests.cs +++ b/src/Umbraco.Tests/CoreThings/EnumExtensionsTests.cs @@ -51,47 +51,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.Web/Models/Mapping/ContentTypeMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs index 95101da4e3..cdd534a14f 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs @@ -226,8 +226,8 @@ namespace Umbraco.Web.Models.Mapping target.ValidationRegExp = source.Validation.Pattern; target.ValidationRegExpMessage = source.Validation.PatternMessage; target.Variations = source.AllowCultureVariant - ? target.Variations.SetFlag(ContentVariation.Culture) - : target.Variations.UnsetFlag(ContentVariation.Culture); + ? target.Variations | ContentVariation.Culture // Set flag using bitwise logical OR + : target.Variations & ~ContentVariation.Culture; // Remove flag using bitwise logical AND with bitwise complement (reversing the bit) if (source.Id > 0) target.Id = source.Id; @@ -399,9 +399,9 @@ namespace Umbraco.Web.Models.Mapping if (!(target is IMemberType)) { - target.Variations = source.AllowCultureVariant - ? target.Variations.SetFlag(ContentVariation.Culture) - : target.Variations.UnsetFlag(ContentVariation.Culture); + target.Variations = source.AllowCultureVariant + ? target.Variations | ContentVariation.Culture // Set flag using bitwise logical OR + : target.Variations & ~ContentVariation.Culture; // Remove flag using bitwise logical AND with bitwise complement (reversing the bit) } // handle property groups and property types From ec4bc11c1b1163790d0ef705ae0ef2e573371948 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Tue, 7 Jan 2020 20:35:00 +0100 Subject: [PATCH 37/85] Obsolete HasFlagAll extension method --- src/Umbraco.Core/EnumExtensions.cs | 45 +++++++++---------- .../CoreThings/EnumExtensionsTests.cs | 6 ++- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/Umbraco.Core/EnumExtensions.cs b/src/Umbraco.Core/EnumExtensions.cs index 1443a27876..9097432f64 100644 --- a/src/Umbraco.Core/EnumExtensions.cs +++ b/src/Umbraco.Core/EnumExtensions.cs @@ -3,43 +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; + return (v & f) > 0; } } } diff --git a/src/Umbraco.Tests/CoreThings/EnumExtensionsTests.cs b/src/Umbraco.Tests/CoreThings/EnumExtensionsTests.cs index 72a55cee85..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)] From c38240c872428b366cd716c34c8ce468ced91271 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 9 Jan 2020 09:35:37 +0100 Subject: [PATCH 38/85] Add SetVariesBy extension methods for ContentVariation --- .../ContentVariationExtensions.cs | 32 +++++++++++++++++++ .../Mapping/ContentTypeMapDefinition.cs | 12 +++---- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs index 5b157307ab..0b6b3b5c12 100644 --- a/src/Umbraco.Core/ContentVariationExtensions.cs +++ b/src/Umbraco.Core/ContentVariationExtensions.cs @@ -195,5 +195,37 @@ namespace Umbraco.Core return true; } + + /// + /// 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. + public static void SetVariesBy(this IContentTypeBase contentType, ContentVariation variation, bool value = true) => contentType.Variations = contentType.Variations.SetVariesBy(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. + public static void SetVariesBy(this PropertyType propertyType, ContentVariation variation, bool value = true) => propertyType.Variations = propertyType.Variations.SetVariesBy(variation, value); + + /// + /// Sets or removes the variation 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 specified variation set or removed. + /// + public static ContentVariation SetVariesBy(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) + } } } diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs index cdd534a14f..06155d6888 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs @@ -215,7 +215,7 @@ namespace Umbraco.Web.Models.Mapping } // Umbraco.Code.MapAll -CreateDate -DeleteDate -UpdateDate - // Umbraco.Code.MapAll -SupportsPublishing -Key -PropertyEditorAlias -ValueStorageType + // Umbraco.Code.MapAll -SupportsPublishing -Key -PropertyEditorAlias -ValueStorageType -Variations private static void Map(PropertyTypeBasic source, PropertyType target, MapperContext context) { target.Name = source.Label; @@ -225,9 +225,7 @@ namespace Umbraco.Web.Models.Mapping target.MandatoryMessage = source.Validation.MandatoryMessage; target.ValidationRegExp = source.Validation.Pattern; target.ValidationRegExpMessage = source.Validation.PatternMessage; - target.Variations = source.AllowCultureVariant - ? target.Variations | ContentVariation.Culture // Set flag using bitwise logical OR - : target.Variations & ~ContentVariation.Culture; // Remove flag using bitwise logical AND with bitwise complement (reversing the bit) + target.SetVariesBy(ContentVariation.Culture, source.AllowCultureVariant); if (source.Id > 0) target.Id = source.Id; @@ -369,7 +367,7 @@ namespace Umbraco.Web.Models.Mapping target.Validation = source.Validation; } - // Umbraco.Code.MapAll -CreatorId -Level -SortOrder + // Umbraco.Code.MapAll -CreatorId -Level -SortOrder -Variations // Umbraco.Code.MapAll -CreateDate -UpdateDate -DeleteDate // Umbraco.Code.MapAll -ContentTypeComposition (done by AfterMapSaveToType) private static void MapSaveToTypeBase(TSource source, IContentTypeComposition target, MapperContext context) @@ -399,9 +397,7 @@ namespace Umbraco.Web.Models.Mapping if (!(target is IMemberType)) { - target.Variations = source.AllowCultureVariant - ? target.Variations | ContentVariation.Culture // Set flag using bitwise logical OR - : target.Variations & ~ContentVariation.Culture; // Remove flag using bitwise logical AND with bitwise complement (reversing the bit) + target.SetVariesBy(ContentVariation.Culture, source.AllowCultureVariant); } // handle property groups and property types From babe1aa60aa5518967b7be7cc60a00e1517eb9aa Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 9 Jan 2020 09:50:33 +0100 Subject: [PATCH 39/85] Add documentation and clean code --- .../ContentVariationExtensions.cs | 115 +++++++++++++++--- 1 file changed, 100 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs index 0b6b3b5c12..b3688b08b4 100644 --- a/src/Umbraco.Core/ContentVariationExtensions.cs +++ b/src/Umbraco.Core/ContentVariationExtensions.cs @@ -12,119 +12,199 @@ 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. /// + /// 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 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. + /// 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 segment. /// - /// And then it could also vary by culture. + /// 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 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 property type is invariant. /// + /// The property type. + /// + /// A value indicating whether the property type is invariant. + /// public static bool VariesByNothing(this PropertyType propertyType) => propertyType.Variations.VariesByNothing(); /// /// Determines whether the property type varies by culture. /// - /// And then it could also vary by segment. + /// The property type. + /// + /// A value indicating whether the property type varies by culture. + /// public static bool VariesByCulture(this PropertyType propertyType) => propertyType.Variations.VariesByCulture(); /// /// Determines whether the property type varies by segment. /// - /// And then it could also vary by culture. + /// The property type. + /// + /// A value indicating whether the property type varies by segment. + /// public static bool VariesBySegment(this PropertyType propertyType) => propertyType.Variations.VariesBySegment(); /// /// 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 PropertyType 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. /// - /// And then it could also vary by segment. + /// 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 content type varies by segment. /// - /// And then it could also vary by culture. + /// 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 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 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. /// + /// 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 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 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 is invariant. /// + /// The variation. + /// + /// A value indicating whether the variation is invariant. + /// public static bool VariesByNothing(this ContentVariation variation) => variation == ContentVariation.Nothing; /// /// Determines whether a variation varies by culture. /// - /// And then it could also vary by segment. + /// The variation. + /// + /// A value indicating whether the variation varies by culture. + /// public static bool VariesByCulture(this ContentVariation variation) => (variation & ContentVariation.Culture) > 0; /// /// Determines whether a variation varies by segment. /// - /// And then it could also vary by culture. + /// The variation. + /// + /// A value indicating whether the variation varies by segment. + /// public static bool VariesBySegment(this ContentVariation variation) => (variation & ContentVariation.Segment) > 0; /// /// 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; /// @@ -135,16 +215,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(); @@ -161,13 +243,14 @@ namespace Umbraco.Core if (variation.VariesByCulture()) { // varies by culture - // in exact mode, the culture cannot be null + // in exact mode, the culture cannot be null if (exact && culture == null) { if (throwIfInvalid) throw new NotSupportedException($"Culture may not be null because culture variation is enabled."); + return false; - } + } } else { @@ -178,9 +261,10 @@ namespace Umbraco.Core { if (throwIfInvalid) throw new NotSupportedException($"Culture \"{culture}\" is invalid because culture variation is disabled."); + return false; } - } + } // if it does not vary by segment // the segment cannot have a value @@ -190,6 +274,7 @@ namespace Umbraco.Core { if (throwIfInvalid) throw new NotSupportedException($"Segment \"{segment}\" is invalid because segment variation is disabled."); + return false; } From a10c4403abda8ca1817817061c2042b8373a1284 Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Thu, 9 Jan 2020 14:26:18 +0100 Subject: [PATCH 40/85] Remove konamiCode directive since it isn't used anywhere in core (#7437) --- .../directives/util/konami.directive.js | 62 ------------------- 1 file changed, 62 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/src/common/directives/util/konami.directive.js 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); - } - }; - }]); From 16faeb1886276b0843087230d3693f721db62013 Mon Sep 17 00:00:00 2001 From: Steve Megson Date: Fri, 10 Jan 2020 08:34:23 +0000 Subject: [PATCH 41/85] V8: Date pickers - parsing non-standard formats (#6001) --- .../datepicker/datepicker.controller.js | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js index 0f19bf3b1a..24affc6ac1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js @@ -85,13 +85,27 @@ function dateTimePickerController($scope, angularHelper, dateHelper, validationM }; $scope.datePickerChange = function(date) { - setDate(date); + const momentDate = moment(date); + setDate(momentDate); setDatePickerVal(); }; - $scope.inputChanged = function() { - setDate($scope.model.datetimePickerValue); - setDatePickerVal(); + $scope.inputChanged = function () { + if ($scope.model.datetimePickerValue === "" && $scope.hasDatetimePickerValue) { + // $scope.hasDatetimePickerValue indicates that we had a value before the input was changed, + // but now the input is empty. + $scope.clearDate(); + } else if ($scope.model.datetimePickerValue) { + var momentDate = moment($scope.model.datetimePickerValue, $scope.model.config.format, true); + if (!momentDate || !momentDate.isValid()) { + momentDate = moment(new Date($scope.model.datetimePickerValue)); + } + if (momentDate && momentDate.isValid()) { + setDate(momentDate); + } + setDatePickerVal(); + flatPickr.setDate($scope.model.value, false); + } } //here we declare a special method which will be called whenever the value has changed from the server @@ -103,15 +117,14 @@ function dateTimePickerController($scope, angularHelper, dateHelper, validationM var newDate = moment(newVal); if (newDate.isAfter(minDate)) { - setDate(newVal); + setDate(newDate); } else { $scope.clearDate(); } } }; - function setDate(date) { - const momentDate = moment(date); + function setDate(momentDate) { angularHelper.safeApply($scope, function() { // when a date is changed, update the model if (momentDate && momentDate.isValid()) { @@ -123,12 +136,11 @@ function dateTimePickerController($scope, angularHelper, dateHelper, validationM $scope.hasDatetimePickerValue = false; $scope.model.datetimePickerValue = null; } - updateModelValue(date); + updateModelValue(momentDate); }); } - function updateModelValue(date) { - const momentDate = moment(date); + function updateModelValue(momentDate) { if ($scope.hasDatetimePickerValue) { if ($scope.model.config.pickTime) { //check if we are supposed to offset the time From 2b4279315a1d9619455509a29d83aecef56f634c Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Fri, 10 Jan 2020 10:05:52 +0100 Subject: [PATCH 42/85] V8: Make the markdown editor use the Umbraco link picker (#5745) --- .../lib/markdown/markdown.editor.js | 10 ++++++---- .../markdowneditor.controller.js | 20 +++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) 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/views/propertyeditors/markdowneditor/markdowneditor.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.controller.js index f05b1e31d8..bb87f0463d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.controller.js @@ -27,6 +27,20 @@ function MarkdownEditorController($scope, $element, assetsService, editorService editorService.mediaPicker(mediaPicker); } + function openLinkPicker(callback) { + var linkPicker = { + hideTarget: true, + submit: function(model) { + callback(model.target.url, model.target.name); + editorService.close(); + }, + close: function() { + editorService.close(); + } + }; + editorService.linkPicker(linkPicker); + } + assetsService .load([ "lib/markdown/markdown.converter.js", @@ -53,6 +67,12 @@ function MarkdownEditorController($scope, $element, assetsService, editorService return true; // tell the editor that we'll take care of getting the image url }); + //subscribe to the link dialog clicks + editor2.hooks.set("insertLinkDialog", function (callback) { + openLinkPicker(callback); + return true; // tell the editor that we'll take care of getting the link url + }); + editor2.hooks.set("onPreviewRefresh", function () { // We must manually update the model as there is no way to hook into the markdown editor events without exstensive edits to the library. if ($scope.model.value !== $("textarea", $element).val()) { From eeaa5a82d46ebbae5616aafa747a452d36bd05c8 Mon Sep 17 00:00:00 2001 From: Dave Woestenborghs Date: Fri, 10 Jan 2020 10:39:22 +0100 Subject: [PATCH 43/85] V8/doctype tours (#6980) --- .../src/common/resources/tour.resource.js | 13 +++- .../common/services/editorstate.service.js | 3 +- .../src/common/services/tour.service.js | 65 ++++++++++------ .../common/drawers/help/help.controller.js | 35 ++++++++- .../src/views/common/drawers/help/help.html | 74 ++++++++++++------- src/Umbraco.Web/Editors/TourController.cs | 33 +++++++++ src/Umbraco.Web/Models/BackOfficeTour.cs | 3 + 7 files changed, 174 insertions(+), 52 deletions(-) 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/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 e102da5d34..7219395fd6 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,31 +134,33 @@ 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 + 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; + group.tours.push(item); } - groupExists = true; - 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; + } + newGroup.tours.push(item); + groupedTours.push(newGroup); } - newGroup.tours.push(item); - groupedTours.push(newGroup); } }); @@ -188,6 +190,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; + } + /////////// /** @@ -269,7 +289,8 @@ completeTour: completeTour, getCurrentTour: getCurrentTour, getGroupedTours: getGroupedTours, - getTourByAlias: getTourByAlias + getTourByAlias: getTourByAlias, + getToursForDoctype : getToursForDoctype }; return service; 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..822b5bdb19 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,34 @@ - -
+ +
+
Need help editing current item '{{vm.nodeName}}' ?
-
Tours
+
-
+ +
+
+
+
+ {{ tour.name }} +
+
+ +
+
+
+
+
+
+ + +
+ +
Tours
+ +
@@ -51,33 +73,33 @@
-
+
- -
-
-
{{dashboard.label}}
-
-
-
+ +
+
+
{{dashboard.label}}
+
+
+
+
-
- -
-
Articles
-
    -
  • - - - - {{topic.name}} - - - {{topic.description}} - + +
    +
    Articles
    +
    diff --git a/src/Umbraco.Web/Editors/TourController.cs b/src/Umbraco.Web/Editors/TourController.cs index 25b4f7e9fc..8991bcdd6a 100644 --- a/src/Umbraco.Web/Editors/TourController.cs +++ b/src/Umbraco.Web/Editors/TourController.cs @@ -110,6 +110,39 @@ namespace Umbraco.Web.Editors return result.Except(toursToBeRemoved).OrderBy(x => x.FileName, StringComparer.InvariantCultureIgnoreCase); } + /// + /// Gets a tours for a specific doctype + /// + /// The documenttype alias + /// A + public IEnumerable GetToursForDoctype(string doctypeAlias) + { + var tourFiles = this.GetTours(); + + var doctypeAliasWithCompositions = new List + { + doctypeAlias + }; + + var contentType = this.Services.ContentTypeService.Get(doctypeAlias); + + if (contentType != null) + { + doctypeAliasWithCompositions.AddRange(contentType.CompositionAliases()); + } + + return tourFiles.SelectMany(x => x.Tours) + .Where(x => + { + if (string.IsNullOrEmpty(x.ContentType)) + { + return false; + } + var contentTypes = x.ContentType.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(ct => ct.Trim()); + return contentTypes.Intersect(doctypeAliasWithCompositions).Any(); + }); + } + private void TryParseTourFile(string tourFile, ICollection result, List filters, diff --git a/src/Umbraco.Web/Models/BackOfficeTour.cs b/src/Umbraco.Web/Models/BackOfficeTour.cs index d5987ec5bc..3e14f278e2 100644 --- a/src/Umbraco.Web/Models/BackOfficeTour.cs +++ b/src/Umbraco.Web/Models/BackOfficeTour.cs @@ -31,5 +31,8 @@ namespace Umbraco.Web.Models [DataMember(Name = "culture")] public string Culture { get; set; } + + [DataMember(Name = "contentType")] + public string ContentType { get; set; } } } From f92d0b59bd64b0b751e2e329499a2785cf613637 Mon Sep 17 00:00:00 2001 From: Dave Woestenborghs Date: Fri, 10 Jan 2020 10:41:37 +0100 Subject: [PATCH 44/85] Make sure the configured url provider mode is used when getting a url (#7189) --- src/Umbraco.Web/PublishedContentExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 75eb6adbcb..061422859c 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -1193,7 +1193,7 @@ namespace Umbraco.Web /// if any. In addition, when the content type is multi-lingual, this is the url for the /// specified culture. Otherwise, it is the invariant url. /// - public static string Url(this IPublishedContent content, string culture = null, UrlMode mode = UrlMode.Auto) + public static string Url(this IPublishedContent content, string culture = null, UrlMode mode = UrlMode.Default) { var umbracoContext = Composing.Current.UmbracoContext; From 4a44cd3a5823454f3e0c9cb5b44267023a8c98df Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Fri, 10 Jan 2020 20:02:39 +0100 Subject: [PATCH 45/85] =?UTF-8?q?V8:=20If=20Nested=20Content=20has=20multi?= =?UTF-8?q?ple=20item=20types,=20always=20let=20the=20e=E2=80=A6=20(#5429)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lovely work Kenn. Thanks, as ever --- .../nestedcontent/nestedcontent.controller.js | 50 ++++++++++++------- .../nestedcontent.propertyeditor.html | 6 +-- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js index b61f8b94c2..7de3a5b567 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js @@ -41,7 +41,7 @@ if (vm.maxItems === 0) vm.maxItems = 1000; - vm.singleMode = vm.minItems === 1 && vm.maxItems === 1; + vm.singleMode = vm.minItems === 1 && vm.maxItems === 1 && model.config.contentTypes.length === 1;; vm.showIcons = Object.toBoolean(model.config.showIcons); vm.wideMode = Object.toBoolean(model.config.hideLabel); vm.hasContentTypes = model.config.contentTypes.length > 0; @@ -131,6 +131,7 @@ setCurrentNode(newNode); setDirty(); + validate(); }; vm.openNodeTypePicker = function ($event) { @@ -234,12 +235,22 @@ } }; + vm.canDeleteNode = function (idx) { + return (vm.nodes.length > vm.minItems) + ? true + : model.config.contentTypes.length > 1; + } + function deleteNode(idx) { vm.nodes.splice(idx, 1); setDirty(); updateModel(); + validate(); }; vm.requestDeleteNode = function (idx) { + if (!vm.canDeleteNode(idx)) { + return; + } if (model.config.confirmDeletes === true) { localizationService.localizeMany(["content_nestedContentDeleteItem", "general_delete", "general_cancel", "contentTypeEditor_yesDelete"]).then(function (data) { const overlay = { @@ -468,8 +479,8 @@ } } - // Auto-fill with elementTypes, but only if we have one type to choose from, and if this property is empty. - if (vm.singleMode === true && vm.nodes.length === 0 && model.config.minItems > 0) { + // Enforce min items if we only have one scaffold type + if (vm.nodes.length < vm.minItems && vm.scaffolds.length === 1) { for (var i = vm.nodes.length; i < model.config.minItems; i++) { addNode(vm.scaffolds[0].contentTypeAlias); } @@ -480,6 +491,8 @@ setCurrentNode(vm.nodes[0]); } + validate(); + vm.inited = true; updatePropertyActionStates(); @@ -585,25 +598,28 @@ updateModel(); }); + var validate = function () { + if (vm.nodes.length < vm.minItems) { + $scope.nestedContentForm.minCount.$setValidity("minCount", false); + } + else { + $scope.nestedContentForm.minCount.$setValidity("minCount", true); + } + + if (vm.nodes.length > vm.maxItems) { + $scope.nestedContentForm.maxCount.$setValidity("maxCount", false); + } + else { + $scope.nestedContentForm.maxCount.$setValidity("maxCount", true); + } + } + var watcher = $scope.$watch( function () { return vm.nodes.length; }, function () { - //Validate! - if (vm.nodes.length < vm.minItems) { - $scope.nestedContentForm.minCount.$setValidity("minCount", false); - } - else { - $scope.nestedContentForm.minCount.$setValidity("minCount", true); - } - - if (vm.nodes.length > vm.maxItems) { - $scope.nestedContentForm.maxCount.$setValidity("maxCount", false); - } - else { - $scope.nestedContentForm.maxCount.$setValidity("maxCount", true); - } + validate(); } ); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html index b3821cff3d..47293e433b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html @@ -2,7 +2,7 @@ - +
    @@ -17,7 +17,7 @@ {{vm.labels.copy_icon_title}} -
    From 2b1e33095769e1dc112308efb586afbc38216849 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Fri, 10 Jan 2020 20:09:51 +0100 Subject: [PATCH 46/85] =?UTF-8?q?V8:=20Reset=20paging=20in=20user=20search?= =?UTF-8?q?=20when=20free=20text=20searching=20and=20fi=E2=80=A6=20(#7049)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks Kenn :) --- .../src/views/users/views/users/users.controller.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js index 4ee31806a3..935b1bdfb1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js @@ -451,7 +451,7 @@ var search = _.debounce(function () { $scope.$apply(function () { - getUsers(); + changePageNumber(1); }); }, 500); @@ -512,7 +512,7 @@ } updateLocation("userStates", vm.usersOptions.userStates.join(",")); - getUsers(); + changePageNumber(1); } function setUserGroupFilter(userGroup) { @@ -529,7 +529,7 @@ } updateLocation("userGroups", vm.usersOptions.userGroups.join(",")); - getUsers(); + changePageNumber(1); } function setOrderByFilter(value, direction) { From 2906eafa791fc2c3a2e8a270e2a850a7c25f3b01 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Fri, 10 Jan 2020 20:14:11 +0100 Subject: [PATCH 47/85] =?UTF-8?q?V8:=20Make=20it=20possible=20to=20hide=20?= =?UTF-8?q?anchor/querystring=20input=20in=20the=20li=E2=80=A6=20(#7031)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wonderful work --- .../infiniteeditors/linkpicker/linkpicker.controller.js | 1 + .../views/common/infiniteeditors/linkpicker/linkpicker.html | 4 ++-- .../multiurlpicker/multiurlpicker.controller.js | 1 + .../PropertyEditors/MultiUrlPickerConfiguration.cs | 5 ++++- 4 files changed, 8 insertions(+), 3 deletions(-) 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 @@ -
    +
    - + Date: Mon, 13 Jan 2020 17:16:55 +1100 Subject: [PATCH 48/85] Fixes the issue of manipulating data for an existing Gen, adds notes, makes the Gen comparison operator consistent. --- .../PublishedCache/NuCache/ContentNode.cs | 6 ++++++ .../PublishedCache/NuCache/ContentStore.cs | 19 +++++++++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentNode.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentNode.cs index 3b4859432d..a724e78b72 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentNode.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentNode.cs @@ -109,6 +109,8 @@ namespace Umbraco.Web.PublishedCache.NuCache // everything that is common to both draft and published versions // keep this as small as possible + + public readonly int Id; public readonly Guid Uid; public IPublishedContentType ContentType; @@ -116,10 +118,14 @@ namespace Umbraco.Web.PublishedCache.NuCache public readonly string Path; public readonly int SortOrder; public readonly int ParentContentId; + + // TODO: Can we make everything readonly?? This would make it easier to debug and be less error prone especially for new developers. + // Once a Node is created and exists in the cache it is readonly so we should be able to make that happen at the API level too. public int FirstChildContentId; public int LastChildContentId; public int NextSiblingContentId; public int PreviousSiblingContentId; + public readonly DateTime CreateDate; public readonly int CreatorId; diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 550fd507d5..3857a49a28 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -602,7 +602,7 @@ namespace Umbraco.Web.PublishedCache.NuCache private void ClearRootLocked() { - if (_root.Gen < _liveGen) + if (_root.Gen != _liveGen) _root = new LinkedNode(new ContentNode(), _liveGen, _root); else _root.Value.FirstChildContentId = -1; @@ -899,8 +899,18 @@ namespace Umbraco.Web.PublishedCache.NuCache return link; } + /// + /// This removes this current node from the tree hiearchy by removing it from it's parent's linked list + /// + /// + /// + /// This is called within a lock which means a new Gen is being created therefore this will not modify any existing content in a Gen. + /// private void RemoveTreeNodeLocked(ContentNode content) { + // NOTE: DO NOT modify `content` here, this would modify data for an existing Gen, all modifications are done to clones + // which would be targeting the new Gen. + var parentLink = content.ParentContentId < 0 ? _root : GetRequiredLinkedNode(content.ParentContentId, "parent", null); @@ -937,9 +947,6 @@ namespace Umbraco.Web.PublishedCache.NuCache var prev = GenCloneLocked(prevLink); prev.NextSiblingContentId = content.NextSiblingContentId; } - - content.NextSiblingContentId = -1; - content.PreviousSiblingContentId = -1; } private bool ParentPublishedLocked(ContentNodeKit kit) @@ -955,7 +962,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { var node = link.Value; - if (node != null && link.Gen < _liveGen) + if (node != null && link.Gen != _liveGen) { node = new ContentNode(link.Value); if (link == _root) @@ -1109,7 +1116,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // this is safe only because we're write-locked foreach (var kvp in dict.Where(x => x.Value != null)) { - if (kvp.Value.Gen < _liveGen) + if (kvp.Value.Gen != _liveGen) { var link = new LinkedNode(null, _liveGen, kvp.Value); dict.TryUpdate(kvp.Key, link, kvp.Value); From b02360518d2da1efb64be81fc6c724c9047699db Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 13 Jan 2020 22:25:46 +1100 Subject: [PATCH 49/85] Changed to GetAwaiter/GetResult and updates comment --- src/Umbraco.Core/Runtime/IMainDomLock.cs | 3 +-- src/Umbraco.Core/Runtime/MainDom.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/Runtime/IMainDomLock.cs b/src/Umbraco.Core/Runtime/IMainDomLock.cs index c32e990114..6a62f48194 100644 --- a/src/Umbraco.Core/Runtime/IMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/IMainDomLock.cs @@ -16,9 +16,8 @@ namespace Umbraco.Core.Runtime ///
/// /// - /// A disposable object which will be disposed in order to release the lock + /// An awaitable boolean value which will be false if the elapsed millsecondsTimeout value is exceeded /// - /// Throws a if the elapsed millsecondsTimeout value is exceeded Task AcquireLockAsync(int millisecondsTimeout); /// diff --git a/src/Umbraco.Core/Runtime/MainDom.cs b/src/Umbraco.Core/Runtime/MainDom.cs index 67c7be3d21..e6780ec876 100644 --- a/src/Umbraco.Core/Runtime/MainDom.cs +++ b/src/Umbraco.Core/Runtime/MainDom.cs @@ -145,7 +145,7 @@ namespace Umbraco.Core.Runtime _logger.Info("Acquiring."); // Get the lock - var acquired = _mainDomLock.AcquireLockAsync(LockTimeoutMilliseconds).Result; + var acquired = _mainDomLock.AcquireLockAsync(LockTimeoutMilliseconds).GetAwaiter().GetResult(); if (!acquired) { From 3e71de4698f8a46e92ef747e39366b80dcf635aa Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 13 Jan 2020 22:28:25 +1100 Subject: [PATCH 50/85] ensure locks the data types --- src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index c959d9f67a..cecca3eb75 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -462,6 +462,8 @@ namespace Umbraco.Web.PublishedCache.NuCache /// public void UpdateDataTypesLocked(IEnumerable dataTypeIds, Func getContentType) { + EnsureLocked(); + var contentTypes = _contentTypesById .Where(kvp => kvp.Value.Value != null && From d48b33d86d0b81666dec4d33d224b6de2d1ff74c Mon Sep 17 00:00:00 2001 From: Jan Skovgaard Date: Mon, 13 Jan 2020 22:03:34 +0100 Subject: [PATCH 51/85] Icon picker: Accessibility improvements (#6990) --- .../src/less/components/umb-iconpicker.less | 10 ++++++---- .../common/infiniteeditors/iconpicker/iconpicker.html | 7 ++++--- 2 files changed, 10 insertions(+), 7 deletions(-) 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/infiniteeditors/iconpicker/iconpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html index 3caa6ae03d..c2b0d0e458 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html @@ -46,9 +46,10 @@
  • - - - +
From 8176054432b6187b1cec4921f4542bfb45be2469 Mon Sep 17 00:00:00 2001 From: Jan Skovgaard Date: Mon, 13 Jan 2020 22:33:31 +0100 Subject: [PATCH 52/85] Add label and missing translations (#7019) --- .../components/application/umb-search.less | 6 +- .../components/application/umb-search.html | 14 +- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 4737 +++++++++-------- .../Umbraco/config/lang/en_us.xml | 1 + 4 files changed, 2385 insertions(+), 2373 deletions(-) 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/views/components/application/umb-search.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-search.html index 297ade28c0..cc1c8abb7e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-search.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-search.html @@ -2,15 +2,21 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-confirm-action.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-confirm-action.html index f4e4dff3af..b45a3bb843 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-confirm-action.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-confirm-action.html @@ -6,11 +6,11 @@ '-left': direction === 'left'}" on-outside-click="clickCancel()"> - + - +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js index 3db1221f5e..5adf121a56 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js @@ -402,6 +402,15 @@ angular.module("umbraco") eventsService.emit("grid.rowAdded", { scope: $scope, element: $element, row: row }); + // TODO: find a nicer way to do this without relying on setTimeout + setTimeout(function () { + var newRowEl = $element.find("[data-rowid='" + row.$uniqueId + "']"); + + if(newRowEl !== null) { + newRowEl.focus(); + } + }, 0); + }; $scope.removeRow = function (section, $index) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html index e889067321..bde2b9148e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html @@ -95,7 +95,7 @@
- + -
+
+
- +
- +
-
+
@@ -249,14 +249,16 @@
- - - - + + + Add new role + +
@@ -264,10 +266,11 @@

-
+ ng-click="addRow(section, layout)" + type="button">
@@ -285,7 +288,7 @@ {{layout.label || layout.name}} -
+
From 6ca0a8ba665075acf65efeafbfc0eb46dda665ef Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 16 Jan 2020 14:47:25 +0100 Subject: [PATCH 61/85] V8: A few UX updates to the listview move dialog (#6902) * Add some context to the move operation from listviews * Disable submit button in the listview move dialog until a target is chosen * Fix bad merge --- .../common/infiniteeditors/move/move.html | 6 +- src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 1 + src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 1 + .../Umbraco/config/lang/en_us.xml | 4801 +++++++++-------- 4 files changed, 2408 insertions(+), 2401 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/move/move.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/move/move.html index 8424bcefbe..1c9fd5b822 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/move/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/move/move.html @@ -13,6 +13,9 @@ +

+ Choose where to move the selected item(s) +

@@ -75,7 +78,8 @@ button-style="success" label-key="general_submit" state="vm.saveButtonState" - action="vm.submit(model)"> + action="vm.submit(model)" + disabled="!model.target"> diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 08a00f502e..78075d9b7a 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -35,6 +35,7 @@ Sæt rettigheder for siden %0% Vælg hvor du vil kopiere Vælg hvortil du vil flytte + Vælg hvor du vil flytte de valgte elementer hen til i træstrukturen nedenfor Vælg hvor du vil kopiere de valgte elementer til blev flyttet til diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index d8e94017aa..a2cf6ea475 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -37,6 +37,7 @@ Choose where to move to in the tree structure below Choose where to copy the selected item(s) + Choose where to move the selected item(s) was moved to was copied to was deleted diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index 82ba12dc17..56d5f1ade2 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -1,2400 +1,2401 @@ - - - - The Umbraco community - https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - - - Culture and Hostnames - Audit Trail - Browse Node - Change Document Type - Copy - Create - Export - Create Package - Create group - Delete - Disable - Empty recycle bin - Enable - Export Document Type - Import Document Type - Import Package - Edit in Canvas - Exit - Move - Notifications - Public access - Publish - Unpublish - Reload - Republish entire site - Rename - Restore - Set permissions for the page %0% - Choose where to copy - Choose where to move - to in the tree structure below - Choose where to copy the selected item(s) - was moved to - was copied to - was deleted - Permissions - Rollback - Send To Publish - Send To Translation - Set group - Sort - Translate - Update - Set permissions - Unlock - Create Content Template - Resend Invitation - - - Content - Administration - Structure - Other - - - Allow access to assign culture and hostnames - Allow access to view a node's history log - Allow access to view a node - Allow access to change document type for a node - Allow access to copy a node - Allow access to create nodes - Allow access to delete nodes - Allow access to move a node - Allow access to set and change public access for a node - Allow access to publish a node - Allow access to unpublish a node - Allow access to change permissions for a node - Allow access to roll back a node to a previous state - Allow access to send a node for approval before publishing - Allow access to send a node for translation - Allow access to change the sort order for nodes - Allow access to translate a node - Allow access to save a node - Allow access to create a Content Template - - - Content - Info - - - Permission denied. - Add new Domain - remove - Invalid node. - One or more domains have an invalid format. - Domain has already been assigned. - Language - Domain - New domain '%0%' has been created - Domain '%0%' is deleted - Domain '%0%' has already been assigned - Domain '%0%' has been updated - Edit Current Domains - - - Inherit - Culture - or inherit culture from parent nodes. Will also apply
- to the current node, unless a domain below applies too.]]>
- Domains - - - Clear selection - Select - Do something else - Bold - Cancel Paragraph Indent - Insert form field - Insert graphic headline - Edit Html - Indent Paragraph - Italic - Center - Justify Left - Justify Right - Insert Link - Insert local link (anchor) - Bullet List - Numeric List - Insert macro - Insert picture - Publish and close - Publish with descendants - Edit relations - Return to list - Save - Save and close - Save and publish - Save and schedule - Send for approval - Save list view - Schedule - Preview - Preview is disabled because there's no template assigned - Choose style - Show styles - Insert table - Generate models and close - Save and generate models - Undo - Redo - Rollback - Delete tag - Cancel - Confirm - More publishing options - - - Viewing for - Content deleted - Content unpublished - Content unpublished for languages: %0% - Content published - Content published for languages: %0% - Content saved - Content saved for languages: %0% - Content moved - Content copied - Content rolled back - Content sent for publishing - Content sent for publishing for languages: %0% - Sort child items performed by user - Copy - Publish - Publish - Move - Save - Save - Delete - Unpublish - Unpublish - Rollback - Send To Publish - Send To Publish - Sort - History (all variants) - - - To change the document type for the selected content, first select from the list of valid types for this location. - Then confirm and/or amend the mapping of properties from the current type to the new, and click Save. - The content has been re-published. - Current Property - Current type - The document type cannot be changed, as there are no alternatives valid for this location. An alternative will be valid if it is allowed under the parent of the selected content item and that all existing child content items are allowed to be created under it. - Document Type Changed - Map Properties - Map to Property - New Template - New Type - none - Content - Select New Document Type - The document type of the selected content has been successfully changed to [new type] and the following properties mapped: - to - Could not complete property mapping as one or more properties have more than one mapping defined. - Only alternate types valid for the current location are displayed. - - - Failed to create a folder under parent with ID %0% - Failed to create a folder under parent with name %0% - The folder name cannot contain illegal characters. - Failed to delete item: %0% - - - Is Published - About this page - Alias - (how would you describe the picture over the phone) - Alternative Links - Click to edit this item - Created by - Original author - Updated by - Created - Date/time this document was created - Document Type - Editing - Remove at - This item has been changed after publication - This item is not published - Last published - There are no items to show - There are no items to show in the list. - No child items have been added - No members have been added - Media Type - Link to media item(s) - Member Group - Role - Member Type - No changes have been made - No date chosen - Page title - This media item has no link - No content can be added for this item - Properties - This document is published but is not visible because the parent '%0%' is unpublished - This culture is published but is not visible because it is unpublished on parent '%0%' - This document is published but is not in the cache - Could not get the url - This document is published but its url would collide with content %0% - This document is published but its url cannot be routed - Publish - Published - Published (pending changes)> - Publication Status - Publish with descendants to publish %0% and all content items underneath and thereby making their content publicly available.]]> - Publish with descendants to publish the selected languages and the same languages of content items underneath and thereby making their content publicly available.]]> - Publish at - Unpublish at - Clear Date - Set date - Sortorder is updated - To sort the nodes, simply drag the nodes or click one of the column headers. You can select multiple nodes by holding the "shift" or "control" key while selecting - Statistics - Title (optional) - Alternative text (optional) - Type - Unpublish - Draft - Not created - Last edited - Date/time this document was edited - Remove file(s) - Click here to remove the image from the media item - Click here to remove the file from the media item - Link to document - Member of group(s) - Not a member of group(s) - Child items - Target - This translates to the following time on the server: - What does this mean?]]> - Are you sure you want to delete this item? - Are you sure you want to delete all items? - Property %0% uses editor %1% which is not supported by Nested Content. - No content types are configured for this property. - Add element type - Select element type - Select the group whose properties should be displayed. If left blank, the first group on the element type will be used. - Enter an angular expression to evaluate against each item for its name. Use - to display the item index - Add another text box - Remove this text box - Content root - Include drafts: also publish unpublished content items. - This value is hidden. If you need access to view this value please contact your website administrator. - This value is hidden. - What languages would you like to publish? All languages with content are saved! - What languages would you like to publish? - What languages would you like to save? - All languages with content are saved on creation! - What languages would you like to send for approval? - What languages would you like to schedule? - Select the languages to unpublish. Unpublishing a mandatory language will unpublish all languages. - Published Languages - Unpublished Languages - Unmodified Languages - These languages haven't been created - Ready to Publish? - Ready to Save? - Send for approval - Select the date and time to publish and/or unpublish the content item. - Create new - Paste from clipboard - This item is in the Recycle Bin - - - Create a new Content Template from '%0%' - Blank - Select a Content Template - Content Template created - A Content Template was created from '%0%' - Another Content Template with the same name already exists - A Content Template is predefined content that an editor can select to use as the basis for creating new content - - - Click to upload - or click here to choose files - You can drag files here to upload. - Cannot upload this file, it does not have an approved file type - Max file size is - Media root - Failed to move media - Parent and destination folders cannot be the same - Failed to copy media - Failed to create a folder under parent id %0% - Failed to rename the folder with id %0% - Drag and drop your file(s) into the area - - - Create a new member - All Members - Member groups have no additional properties for editing. - - - Where do you want to create the new %0% - Create an item under - Select the document type you want to make a content template for - Enter a folder name - Choose a type and a title - Document Types within the Settings section, by editing the Allowed child node types under Permissions.]]> - Document Types within the Settings section.]]> - The selected page in the content tree doesn't allow for any pages to be created below it. - Edit permissions for this document type - Create a new document type - Document Types within the Settings section, by changing the Allow as root option under Permissions.]]> - Media Types Types within the Settings section, by editing the Allowed child node types under Permissions.]]> - The selected media in the tree doesn't allow for any other media to be created below it. - Edit permissions for this media type Document Type without a template - New folder - New data type - New JavaScript file - New empty partial view - New partial view macro - New partial view from snippet - New partial view macro from snippet - New partial view macro (without macro) - New style sheet file - New Rich Text Editor style sheet file - - - Browse your website - - Hide - If Umbraco isn't opening, you might need to allow popups from this site - has opened in a new window - Restart - Visit - Welcome - - - Stay - Discard changes - You have unsaved changes - Are you sure you want to navigate away from this page? - you have unsaved changes - Publishing will make the selected items visible on the site. - Unpublishing will remove the selected items and all their descendants from the site. - Unpublishing will remove this page and all its descendants from the site. - You have unsaved changes. Making changes to the Document Type will discard the changes. - - - Done - Deleted %0% item - Deleted %0% items - Deleted %0% out of %1% item - Deleted %0% out of %1% items - Published %0% item - Published %0% items - Published %0% out of %1% item - Published %0% out of %1% items - Unpublished %0% item - Unpublished %0% items - Unpublished %0% out of %1% item - Unpublished %0% out of %1% items - Moved %0% item - Moved %0% items - Moved %0% out of %1% item - Moved %0% out of %1% items - Copied %0% item - Copied %0% items - Copied %0% out of %1% item - Copied %0% out of %1% items - - - Link title - Link - Anchor / querystring - Name - Close this window - Are you sure you want to delete - Are you sure you want to disable - Are you sure? - Are you sure? - Cut - Edit Dictionary Item - Edit Language - Edit selected media - Insert local link - Insert character - Insert graphic headline - Insert picture - Insert link - Click to add a Macro - Insert table - This will delete the language - Changing the culture for a language may be an expensive operation and will result in the content cache and indexes being rebuilt - Last Edited - Link - Internal link: - When using local links, insert "#" in front of link - Open in new window? - Macro Settings - This macro does not contain any properties you can edit - Paste - Edit permissions for - Set permissions for - Set permissions for %0% for user group %1% - Select the users groups you want to set permissions for - The items in the recycle bin are now being deleted. Please do not close this window while this operation takes place - The recycle bin is now empty - When items are deleted from the recycle bin, they will be gone forever - regexlib.com's webservice is currently experiencing some problems, which we have no control over. We are very sorry for this inconvenience.]]> - Search for a regular expression to add validation to a form field. Example: 'email, 'zip-code' 'url' - Remove Macro - Required Field - Site is reindexed - The website cache has been refreshed. All publish content is now up to date. While all unpublished content is still unpublished - The website cache will be refreshed. All published content will be updated, while unpublished content will stay unpublished. - Number of columns - Number of rows - Click on the image to see full size - Pick item - View Cache Item - Relate to original - Include descendants - The friendliest community - Link to page - Opens the linked document in a new window or tab - Link to media - Select content start node - Select media - Select media type - Select icon - Select item - Select link - Select macro - Select content - Select content type - Select media start node - Select member - Select member group - Select member type - Select node - Select sections - Select users - No icons were found - There are no parameters for this macro - There are no macros available to insert - External login providers - Exception Details - Stacktrace - Inner Exception - Link your - Un-link your - account - Select editor - Select snippet - This will delete the node and all its languages. If you only want to delete one language, you should unpublish the node in that language instead. - - - There are no dictionary items. - - - %0%' below - ]]> - Culture Name - - Dictionary overview - - - Configured Searchers - Shows properties and tools for any configured Searcher (i.e. such as a multi-index searcher) - Field values - Health status - The health status of the index and if it can be read - Indexers - Index info - Lists the properties of the index - Manage Examine's indexes - Allows you to view the details of each index and provides some tools for managing the indexes - Rebuild index - - Depending on how much content there is in your site this could take a while.
- It is not recommended to rebuild an index during times of high website traffic or when editors are editing content. - ]]> -
- Searchers - Search the index and view the results - Tools - Tools to manage the index - fields - The index cannot be read and will need to be rebuilt - The process is taking longer than expected, check the umbraco log to see if there have been any errors during this operation - This index cannot be rebuilt because it has no assigned - IIndexPopulator - - - Enter your username - Enter your password - Confirm your password - Name the %0%... - Enter a name... - Enter an email... - Enter a username... - Label... - Enter a description... - Type to search... - Type to filter... - Type to add tags (press enter after each tag)... - Enter your email - Enter a message... - Your username is usually your email - #value or ?key=value - Enter alias... - Generating alias... - - - Create custom list view - Remove custom list view - A content type, media type or member type with this alias already exists - - - Renamed - Enter a new folder name here - %0% was renamed to %1% - - - Add prevalue - Database datatype - Property editor GUID - Property editor - Buttons - Enable advanced settings for - Enable context menu - Maximum default size of inserted images - Related stylesheets - Show label - Width and height - All property types & property data - using this data type will be deleted permanently, please confirm you want to delete these as well - Yes, delete - and all property types & property data using this data type - Select the folder to move - to in the tree structure below - was moved underneath - %0% will delete the properties and their data from the following items]]> - I understand this action will delete the properties and data based on this Data Type - - - Your data has been saved, but before you can publish this page there are some errors you need to fix first: - The current membership provider does not support changing password (EnablePasswordRetrieval need to be true) - %0% already exists - There were errors: - There were errors: - The password should be a minimum of %0% characters long and contain at least %1% non-alpha numeric character(s) - %0% must be an integer - The %0% field in the %1% tab is mandatory - %0% is a mandatory field - %0% at %1% is not in a correct format - %0% is not in a correct format - - - Received an error from the server - The specified file type has been disallowed by the administrator - NOTE! Even though CodeMirror is enabled by configuration, it is disabled in Internet Explorer because it's not stable enough. - Please fill both alias and name on the new property type! - There is a problem with read/write access to a specific file or folder - Error loading Partial View script (file: %0%) - Please enter a title - Please choose a type - You're about to make the picture larger than the original size. Are you sure that you want to proceed? - Startnode deleted, please contact your administrator - Please mark content before changing style - No active styles available - Please place cursor at the left of the two cells you wish to merge - You cannot split a cell that hasn't been merged. - This property is invalid - - - Options - About - Action - Actions - Add - Alias - All - Are you sure? - Back - Back to overview - Border - by - Cancel - Cell margin - Choose - Clear - Close - Close Window - Comment - Confirm - Constrain - Constrain proportions - Content - Continue - Copy - Create - Database - Date - Default - Delete - Deleted - Deleting... - Design - Dictionary - Dimensions - Down - Download - Edit - Edited - Elements - Email - Error - Field - Find - First - Focal point - General - Groups - Group - Height - Help - Hide - History - Icon - Id - Import - Include subfolders in search - Info - Inner margin - Insert - Install - Invalid - Justify - Label - Language - Last - Layout - Links - Loading - Locked - Login - Log off - Logout - Macro - Mandatory - Message - Move - Name - New - Next - No - of - Off - OK - Open - On - or - Order by - Password - Path - One moment please... - Previous - Properties - Rebuild - Email to receive form data - Recycle Bin - Your recycle bin is empty - Reload - Remaining - Remove - Rename - Renew - Required - Retrieve - Retry - Permissions - Scheduled Publishing - Search - Sorry, we can not find what you are looking for. - No items have been added - Server - Settings - Show - Show page on Send - Size - Sort - Status - Submit - Type - Type to search... - under - Up - Update - Upgrade - Upload - Url - User - Username - Value - View - Welcome... - Width - Yes - Folder - Search results - Reorder - I am done reordering - Preview - Change password - to - List view - Saving... - current - Embed - selected - Other - Articles - Videos - Clear - Installing - - - Blue - - - Add group - Add property - Add editor - Add template - Add child node - Add child - Edit data type - Navigate sections - Shortcuts - show shortcuts - Toggle list view - Toggle allow as root - Comment/Uncomment lines - Remove line - Copy Lines Up - Copy Lines Down - Move Lines Up - Move Lines Down - General - Editor - Toggle allow culture variants - - - Background color - Bold - Text color - Font - Text - - - Page - - - The installer cannot connect to the database. - Could not save the web.config file. Please modify the connection string manually. - Your database has been found and is identified as - Database configuration - install button to install the Umbraco %0% database - ]]> - Next to proceed.]]> - Database not found! Please check that the information in the "connection string" of the "web.config" file is correct.

-

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

-

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

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

-

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

- ]]>
- Press Next to - proceed. ]]> - next to continue the configuration wizard]]> - The Default users' password needs to be changed!]]> - The Default user has been disabled or has no access to Umbraco!

No further actions needs to be taken. Click Next to proceed.]]> - The Default user's password has been successfully changed since the installation!

No further actions needs to be taken. Click Next to proceed.]]> - The password is changed! - Get a great start, watch our introduction videos - By clicking the next button (or modifying the umbracoConfigurationStatus in web.config), you accept the license for this software as specified in the box below. Notice that this Umbraco distribution consists of two different licenses, the open source MIT license for the framework and the Umbraco freeware license that covers the UI. - Not installed yet. - Affected files and folders - More information on setting up permissions for Umbraco here - You need to grant ASP.NET modify permissions to the following files/folders - Your permission settings are almost perfect!

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

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

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

- You are ready to run Umbraco and install packages!]]>
- Resolving folder issue - Follow this link for more information on problems with ASP.NET and creating folders - Setting up folder permissions - - I want to start from scratch - learn how) - You can still choose to install Runway later on. Please go to the Developer section and choose Packages. - ]]> - You've just set up a clean Umbraco platform. What do you want to do next? - Runway is installed - - This is our list of recommended modules, check off the ones you would like to install, or view the full list of modules - ]]> - Only recommended for experienced users - I want to start with a simple website - - "Runway" is a simple website providing some basic document types and templates. The installer can set up Runway for you automatically, - but you can easily edit, extend or remove it. It's not necessary and you can perfectly use Umbraco without it. However, - Runway offers an easy foundation based on best practices to get you started faster than ever. - If you choose to install Runway, you can optionally select basic building blocks called Runway Modules to enhance your Runway pages. -

- - Included with Runway: Home page, Getting Started page, Installing Modules page.
- Optional Modules: Top Navigation, Sitemap, Contact, Gallery. -
- ]]>
- What is Runway - Step 1/5 Accept license - Step 2/5: Database configuration - Step 3/5: Validating File Permissions - Step 4/5: Check Umbraco security - Step 5/5: Umbraco is ready to get you started - Thank you for choosing Umbraco - Browse your new site -You installed Runway, so why not see how your new website looks.]]> - Further help and information -Get help from our award winning community, browse the documentation or watch some free videos on how to build a simple site, how to use packages and a quick guide to the Umbraco terminology]]> - Umbraco %0% is installed and ready for use - /web.config file and update the AppSetting key UmbracoConfigurationStatus in the bottom to the value of '%0%'.]]> - started instantly by clicking the "Launch Umbraco" button below.
If you are new to Umbraco, -you can find plenty of resources on our getting started pages.]]>
- Launch Umbraco -To manage your website, simply open the Umbraco back office and start adding content, updating the templates and stylesheets or add new functionality]]> - Connection to database failed. - Umbraco Version 3 - Umbraco Version 4 - Watch - Umbraco %0% for a fresh install or upgrading from version 3.0. -

- Press "next" to start the wizard.]]>
- - - Culture Code - Culture Name - - - You've been idle and logout will automatically occur in - Renew now to save your work - - - Happy super Sunday - Happy manic Monday - Happy tubular Tuesday - Happy wonderful Wednesday - Happy thunderous Thursday - Happy funky Friday - Happy Caturday - Log in below - Sign in with - Session timed out - © 2001 - %0%
Umbraco.com

]]>
- Forgotten password? - An email will be sent to the address specified with a link to reset your password - An email with password reset instructions will be sent to the specified address if it matched our records - Show password - Hide password - Return to login form - Please provide a new password - Your Password has been updated - The link you have clicked on is invalid or has expired - Umbraco: Reset Password - - - - - - - - - - - -
- - - - - -
- -
- -
-
- - - - - - -
-
-
- - - - -
- - - - -
-

- Password reset requested -

-

- Your username to login to the Umbraco back-office is: %0% -

-

- - - - - - -
- - Click this link to reset your password - -
-

-

If you cannot click on the link, copy and paste this URL into your browser window:

- - - - -
- - %1% - -
-

-
-
-


-
-
- - - ]]>
- - - Dashboard - Sections - Content - - - Choose page above... - %0% has been copied to %1% - Select where the document %0% should be copied to below - %0% has been moved to %1% - Select where the document %0% should be moved to below - has been selected as the root of your new content, click 'ok' below. - No node selected yet, please select a node in the list above before clicking 'ok' - The current node is not allowed under the chosen node because of its type - The current node cannot be moved to one of its subpages - The current node cannot exist at the root - The action isn't allowed since you have insufficient permissions on 1 or more child documents. - Relate copied items to original - - - %0%]]> - Notification settings saved for - - The following languages have been modified %0% - - - - - - - - - - - -
- - - - - -
- -
- -
-
- - - - - - -
-
-
- - - - -
- - - - -
-

- Hi %0%, -

-

- This is an automated mail to inform you that the task '%1%' has been performed on the page '%2%' by the user '%3%' -

- - - - - - -
- -
- EDIT
-
-

-

Update summary:

- %6% -

-

- Have a nice day!

- Cheers from the Umbraco robot -

-
-
-


-
-
- - - ]]>
- The following languages have been modified:

- %0% - ]]>
- [%0%] Notification about %1% performed on %2% - Notifications - - - Actions - Created - Create package - - button and locating the package. Umbraco packages usually have a ".umb" or ".zip" extension. - ]]> - This will delete the package - Drop to upload - Include all child nodes - or click here to choose package file - Upload package - Install a local package by selecting it from your machine. Only install packages from sources you know and trust - Upload another package - Cancel and upload another package - I accept - terms of use - Path to file - Absolute path to file (ie: /bin/umbraco.bin) - Installed - Installed packages - Install local - Finish - This package has no configuration view - No packages have been created yet - You don’t have any packages installed - 'Packages' icon in the top right of your screen]]> - Package Actions - Author URL - Package Content - Package Files - Icon URL - Install package - License - License URL - Package Properties - Search for packages - Results for - We couldn’t find anything for - Please try searching for another package or browse through the categories - Popular - New releases - has - karma points - Information - Owner - Contributors - Created - Current version - .NET version - Downloads - Likes - Compatibility - This package is compatible with the following versions of Umbraco, as reported by community members. Full compatability cannot be guaranteed for versions reported below 100% - External sources - Author - Documentation - Package meta data - Package name - Package doesn't contain any items -
- You can safely remove this from the system by clicking "uninstall package" below.]]>
- Package options - Package readme - Package repository - Confirm package uninstall - Package was uninstalled - The package was successfully uninstalled - Uninstall package - - Notice: any documents, media etc depending on the items you remove, will stop working, and could lead to system instability, - so uninstall with caution. If in doubt, contact the package author.]]> - Package version - Upgrading from version - Package already installed - This package cannot be installed, it requires a minimum Umbraco version of - Uninstalling... - Downloading... - Importing... - Installing... - Restarting, please wait... - All done, your browser will now refresh, please wait... - Please click 'Finish' to complete installation and reload the page. - Uploading package... - - - Paste with full formatting (Not recommended) - The text you're trying to paste contains special characters or formatting. This could be caused by copying text from Microsoft Word. Umbraco can remove special characters or formatting automatically, so the pasted content will be more suitable for the web. - Paste as raw text without any formatting at all - Paste, but remove formatting (Recommended) - - - Group based protection - If you want to grant access to all members of specific member groups - You need to create a member group before you can use group based authentication - Error Page - Used when people are logged on, but do not have access - %0%]]> - %0% is now protected]]> - %0%]]> - Login Page - Choose the page that contains the login form - Remove protection... - %0%?]]> - Select the pages that contain login form and error messages - %0%]]> - %0%]]> - Specific members protection - If you wish to grant access to specific members - - - Insufficient user permissions to publish all descendant documents - - - - - - - - Validation failed for required language '%0%'. This language was saved but not published. - Publishing in progress - please wait... - %0% out of %1% pages have been published... - %0% has been published - %0% and subpages have been published - Publish %0% and all its subpages - Publish to publish %0% and thereby making its content publicly available.

- You can publish this page and all its subpages by checking Include unpublished subpages below. - ]]>
- - - You have not configured any approved colors - - - You can only select items of type(s): %0% - You have picked a content item currently deleted or in the recycle bin - You have picked content items currently deleted or in the recycle bin - - - Deleted item - You have picked a media item currently deleted or in the recycle bin - You have picked media items currently deleted or in the recycle bin - Trashed - - - enter external link - choose internal page - Caption - Link - Open in new window - enter the display caption - Enter the link - - - Reset crop - Save crop - Add new crop - Done - Undo edits - - - Select a version to compare with the current version - Current version - Red text will not be shown in the selected version. , green means added]]> - Document has been rolled back - This displays the selected version as HTML, if you wish to see the difference between 2 versions at the same time, use the diff view - Rollback to - Select version - View - - - Edit script file - - - Content - Forms - Media - Members - Packages - Settings - Translation - Users - - - Tours - The best Umbraco video tutorials - Visit our.umbraco.com - Visit umbraco.tv - - - Default template - To import a document type, find the ".udt" file on your computer by clicking the "Browse" button and click "Import" (you'll be asked for confirmation on the next screen) - New Tab Title - Node type - Type - Stylesheet - Script - Tab - Tab Title - Tabs - Master Content Type enabled - This Content Type uses - as a Master Content Type. Tabs from Master Content Types are not shown and can only be edited on the Master Content Type itself - No properties defined on this tab. Click on the "add a new property" link at the top to create a new property. - Create matching template - Add icon - - - Sort order - Creation date - Sorting complete. - Drag the different items up or down below to set how they should be arranged. Or click the column headers to sort the entire collection of items - - This node has no child nodes to sort - - - Validation - Validation errors must be fixed before the item can be saved - Failed - Saved - Insufficient user permissions, could not complete the operation - Cancelled - Operation was cancelled by a 3rd party add-in - Property type already exists - Property type created - DataType: %1%]]> - Propertytype deleted - Document Type saved - Tab created - Tab deleted - Tab with id: %0% deleted - Stylesheet not saved - Stylesheet saved - Stylesheet saved without any errors - Datatype saved - Dictionary item saved - Content published - and is visible on the website - %0% documents published and visible on the website - %0% published and visible on the website - %0% documents published for languages %1% and visible on the website - Content saved - Remember to publish to make changes visible - A schedule for publishing has been updated - %0% saved - Sent For Approval - Changes have been sent for approval - %0% changes have been sent for approval - Media saved - Media saved without any errors - Member saved - Member group saved - Stylesheet Property Saved - Stylesheet saved - Template saved - Error saving user (check log) - User Saved - User type saved - User group saved - Cultures and hostnames saved - Error saving cultures and hostnames - File not saved - file could not be saved. Please check file permissions - File saved - File saved without any errors - Language saved - Media Type saved - Member Type saved - Member Group saved - Template not saved - Please make sure that you do not have 2 templates with the same alias - Template saved - Template saved without any errors! - Content unpublished - Content variation %0% unpublished - The mandatory language '%0%' was unpublished. All languages for this content item are now unpublished. - Partial view saved - Partial view saved without any errors! - Partial view not saved - An error occurred saving the file. - Permissions saved for - Deleted %0% user groups - %0% was deleted - Enabled %0% users - Disabled %0% users - %0% is now enabled - %0% is now disabled - User groups have been set - Unlocked %0% users - %0% is now unlocked - Member was exported to file - An error occurred while exporting the member - User %0% was deleted - Invite user - Invitation has been re-sent to %0% - Cannot publish the document since the required '%0%' is not published - Validation failed for language '%0%' - Document type was exported to file - An error occurred while exporting the document type - The release date cannot be in the past - Cannot schedule the document for publishing since the required '%0%' is not published - Cannot schedule the document for publishing since the required '%0%' has a publish date later than a non mandatory language - The expire date cannot be in the past - The expire date cannot be before the release date - - - Add style - Edit style - Rich text editor styles - Define the styles that should be available in the rich text editor for this stylesheet - Edit stylesheet - Edit stylesheet property - The name displayed in the editor style selector - Preview - How the text will look like in the rich text editor. - Selector - Uses CSS syntax, e.g. "h1" or ".redHeader" - Styles - The CSS that should be applied in the rich text editor, e.g. "color:red;" - Code - Rich Text Editor - - - Failed to delete template with ID %0% - Edit template - Sections - Insert content area - Insert content area placeholder - Insert - Choose what to insert into your template - Dictionary item - A dictionary item is a placeholder for a translatable piece of text, which makes it easy to create designs for multilingual websites. - Macro - - A Macro is a configurable component which is great for - reusable parts of your design, where you need the option to provide parameters, - such as galleries, forms and lists. - - Value - Displays the value of a named field from the current page, with options to modify the value or fallback to alternative values. - Partial view - - A partial view is a separate template file which can be rendered inside another - template, it's great for reusing markup or for separating complex templates into separate files. - - Master template - No master - Render child template - @RenderBody() placeholder. - ]]> - Define a named section - @section { ... }. This can be rendered in a - specific area of the parent of this template, by using @RenderSection. - ]]> - Render a named section - @RenderSection(name) placeholder. - This renders an area of a child template which is wrapped in a corresponding @section [name]{ ... } definition. - ]]> - Section Name - Section is mandatory - - If mandatory, the child template must contain a @section definition, otherwise an error is shown. - - Query builder - items returned, in - copy to clipboard - I want - all content - content of type "%0%" - from - my website - where - and - is - is not - before - before (including selected date) - after - after (including selected date) - equals - does not equal - contains - does not contain - greater than - greater than or equal to - less than - less than or equal to - Id - Name - Created Date - Last Updated Date - order by - ascending - descending - Template - - - Image - Macro - Choose type of content - Choose a layout - Add a row - Add content - Drop content - Settings applied - This content is not allowed here - This content is allowed here - Click to embed - Click to insert image - Image caption... - Write here... - Grid Layouts - Layouts are the overall work area for the grid editor, usually you only need one or two different layouts - Add Grid Layout - Adjust the layout by setting column widths and adding additional sections - Row configurations - Rows are predefined cells arranged horizontally - Add row configuration - Adjust the row by setting cell widths and adding additional cells - Columns - Total combined number of columns in the grid layout - Settings - Configure what settings editors can change - Styles - Configure what styling editors can change - Allow all editors - Allow all row configurations - Maximum items - Leave blank or set to 0 for unlimited - Set as default - Choose extra - Choose default - are added - Warning - You are deleting the row configuration - - Deleting a row configuration name will result in loss of data for any existing content that is based on this configuration. - - - - Compositions - Group - You have not added any groups - Add group - Inherited from - Add property - Required label - Enable list view - Configures the content item to show a sortable and searchable list of its children, the children will not be shown in the tree - Allowed Templates - Choose which templates editors are allowed to use on content of this type - Allow as root - Allow editors to create content of this type in the root of the content tree. - Allowed child node types - Allow content of the specified types to be created underneath content of this type. - Choose child node - Inherit tabs and properties from an existing document type. New tabs will be added to the current document type or merged if a tab with an identical name exists. - This content type is used in a composition, and therefore cannot be composed itself. - There are no content types available to use as a composition. - Removing a composition will delete all the associated property data. Once you save the document type there's no way back. - Create new - Use existing - Editor settings - Configuration - Yes, delete - was moved underneath - was copied underneath - Select the folder to move - Select the folder to copy - to in the tree structure below - All Document types - All Documents - All media items - using this document type will be deleted permanently, please confirm you want to delete these as well. - using this media type will be deleted permanently, please confirm you want to delete these as well. - using this member type will be deleted permanently, please confirm you want to delete these as well - and all documents using this type - and all media items using this type - and all members using this type - Member can edit - Allow this property value to be edited by the member on their profile page - Is sensitive data - Hide this property value from content editors that don't have access to view sensitive information - Show on member profile - Allow this property value to be displayed on the member profile page - tab has no sort order - Where is this composition used? - This composition is currently used in the composition of the following content types: - Allow varying by culture - Allow editors to create content of this type in different languages. - Allow varying by culture - Element type - Is an Element type - An Element type is meant to be used for instance in Nested Content, and not in the tree. - This is not applicable for an Element type - You have made changes to this property. Are you sure you want to discard them? - - - Add language - Mandatory language - Properties on this language have to be filled out before the node can be published. - Default language - An Umbraco site can only have one default language set. - Switching default language may result in default content missing. - Falls back to - No fall back language - To allow multi-lingual content to fall back to another language if not present in the requested language, select it here. - Fall back language - none - - - Add parameter - Edit parameter - Enter macro name - Parameters - Define the parameters that should be available when using this macro. - Select partial view macro file - - - Building models - this can take a bit of time, don't worry - Models generated - Models could not be generated - Models generation has failed, see exception in U log - - - Add fallback field - Fallback field - Add default value - Default value - Fallback field - Default value - Casing - Encoding - Choose field - Convert line breaks - Yes, convert line breaks - Replaces line breaks with 'br' html tag - Custom Fields - Date only - Format and encoding - Format as date - Format the value as a date, or a date with time, according to the active culture - HTML encode - Will replace special characters by their HTML equivalent. - Will be inserted after the field value - Will be inserted before the field value - Lowercase - Modify output - None - Output sample - Insert after field - Insert before field - Recursive - Yes, make it recursive - Separator - Standard Fields - Uppercase - URL encode - Will format special characters in URLs - Will only be used when the field values above are empty - This field will only be used if the primary field is empty - Date and time - - - Translation details - Download XML DTD - Fields - Include subpages - - No translator users found. Please create a translator user before you start sending content to translation - The page '%0%' has been send to translation - Send the page '%0%' to translation - Total words - Translate to - Translation completed. - You can preview the pages, you've just translated, by clicking below. If the original page is found, you will get a comparison of the 2 pages. - Translation failed, the XML file might be corrupt - Translation options - Translator - Upload translation XML - - - Content - Content Templates - Media - Cache Browser - Recycle Bin - Created packages - Data Types - Dictionary - Installed packages - Install skin - Install starter kit - Languages - Install local package - Macros - Media Types - Members - Member Groups - Member Roles - Member Types - Document Types - Relation Types - Packages - Packages - Partial Views - Partial View Macro Files - Install from repository - Install Runway - Runway modules - Scripting Files - Scripts - Stylesheets - Templates - Log Viewer - Users - Settings - Templating - Third Party - - - New update ready - %0% is ready, click here for download - No connection to server - Error checking for update. Please review trace-stack for further information - - - Access - Based on the assigned groups and start nodes, the user has access to the following nodes - Assign access - Administrator - Category field - User created - Change Your Password - Change photo - New password - hasn't been locked out - The password hasn't been changed - Confirm new password - You can change your password for accessing the Umbraco Back Office by filling out the form below and click the 'Change Password' button - Content Channel - Create another user - Create new users to give them access to Umbraco. When a new user is created a password will be generated that you can share with the user. - Description field - Disable User - Document Type - Editor - Excerpt field - Failed login attempts - Go to user profile - Add groups to assign access and permissions - Invite another user - Invite new users to give them access to Umbraco. An invite email will be sent to the user with information on how to log in to Umbraco. Invites last for 72 hours. - Language - Set the language you will see in menus and dialogs - Last lockout date - Last login - Password last changed - Username - Media start node - Limit the media library to a specific start node - Media start nodes - Limit the media library to specific start nodes - Sections - Disable Umbraco Access - has not logged in yet - Old password - Password - Reset password - Your password has been changed! - Please confirm the new password - Enter your new password - Your new password cannot be blank! - Current password - Invalid current password - There was a difference between the new password and the confirmed password. Please try again! - The confirmed password doesn't match the new password! - Replace child node permissions - You are currently modifying permissions for the pages: - Select pages to modify their permissions - Remove photo - Default permissions - Granular permissions - Set permissions for specific nodes - Profile - Search all children - Add sections to give users access - Select user groups - No start node selected - No start nodes selected - Content start node - Limit the content tree to a specific start node - Content start nodes - Limit the content tree to specific start nodes - User last updated - has been created - The new user has successfully been created. To log in to Umbraco use the password below. - User management - Name - User permissions - User group - has been invited - An invitation has been sent to the new user with details about how to log in to Umbraco. - Hello there and welcome to Umbraco! In just 1 minute you’ll be good to go, we just need you to setup a password and add a picture for your avatar. - Welcome to Umbraco! Unfortunately your invite has expired. Please contact your administrator and ask them to resend it. - Uploading a photo of yourself will make it easy for other users to recognize you. Click the circle above to upload your photo. - Writer - Change - Your profile - Your recent history - Session expires in - Invite user - Create user - Send invite - Back to users - Umbraco: Invitation - - - - - - - - - - - -
- - - - - -
- -
- -
-
- - - - - - -
-
-
- - - - -
- - - - -
-

- Hi %0%, -

-

- You have been invited by %1% to the Umbraco Back Office. -

-

- Message from %1%: -
- %2% -

- - - - - - -
- - - - - - -
- - Click this link to accept the invite - -
-
-

If you cannot click on the link, copy and paste this URL into your browser window:

- - - - -
- - %3% - -
-

-
-
-


-
-
- - ]]>
- Invite - Resending invitation... - Delete User - Are you sure you wish to delete this user account? - All - Active - Disabled - Locked out - Invited - Inactive - Name (A-Z) - Name (Z-A) - Newest - Oldest - Last login - No user groups have been added - - - Validation - Validate as an email address - Validate as a number - Validate as a URL - ...or enter a custom validation - Field is mandatory - Enter a custom validation error message (optional) - Enter a regular expression - Enter a custom validation error message (optional) - You need to add at least - You can only have - items - items selected - Invalid date - Not a number - Invalid email - Value cannot be null - Value cannot be empty - Value is invalid, it does not match the correct pattern - Custom validation - %1% more.]]> - %1% too many.]]> - - - - Value is set to the recommended value: '%0%'. - Value was set to '%1%' for XPath '%2%' in configuration file '%3%'. - Expected value '%1%' for '%2%' in configuration file '%3%', but found '%0%'. - Found unexpected value '%0%' for '%2%' in configuration file '%3%'. - - Custom errors are set to '%0%'. - Custom errors are currently set to '%0%'. It is recommended to set this to '%1%' before go live. - Custom errors successfully set to '%0%'. - MacroErrors are set to '%0%'. - MacroErrors are set to '%0%' which will prevent some or all pages in your site from loading completely if there are any errors in macros. Rectifying this will set the value to '%1%'. - MacroErrors are now set to '%0%'. - - Try Skip IIS Custom Errors is set to '%0%' and you're using IIS version '%1%'. - Try Skip IIS Custom Errors is currently '%0%'. It is recommended to set this to '%1%' for your IIS version (%2%). - Try Skip IIS Custom Errors successfully set to '%0%'. - - File does not exist: '%0%'. - '%0%' in config file '%1%'.]]> - There was an error, check log for full error: %0%. - Database - The database schema is correct for this version of Umbraco - %0% problems were detected with your database schema (Check the log for details) - Some errors were detected while validating the database schema against the current version of Umbraco. - Your website's certificate is valid. - Certificate validation error: '%0%' - Your website's SSL certificate has expired. - Your website's SSL certificate is expiring in %0% days. - Error pinging the URL %0% - '%1%' - You are currently %0% viewing the site using the HTTPS scheme. - The appSetting 'Umbraco.Core.UseHttps' is set to 'false' in your web.config file. Once you access this site using the HTTPS scheme, that should be set to 'true'. - The appSetting 'Umbraco.Core.UseHttps' is set to '%0%' in your web.config file, your cookies are %1% marked as secure. - Could not update the 'Umbraco.Core.UseHttps' setting in your web.config file. Error: %0% - - Enable HTTPS - Sets umbracoSSL setting to true in the appSettings of the web.config file. - The appSetting 'Umbraco.Core.UseHttps' is now set to 'true' in your web.config file, your cookies will be marked as secure. - Fix - Cannot fix a check with a value comparison type of 'ShouldNotEqual'. - Cannot fix a check with a value comparison type of 'ShouldEqual' with a provided value. - Value to fix check not provided. - Debug compilation mode is disabled. - Debug compilation mode is currently enabled. It is recommended to disable this setting before go live. - Debug compilation mode successfully disabled. - Trace mode is disabled. - Trace mode is currently enabled. It is recommended to disable this setting before go live. - Trace mode successfully disabled. - All folders have the correct permissions set. - - %0%.]]> - %0%. If they aren't being written to no action need be taken.]]> - All files have the correct permissions set. - - %0%.]]> - %0%. If they aren't being written to no action need be taken.]]> - X-Frame-Options used to control whether a site can be IFRAMEd by another was found.]]> - X-Frame-Options used to control whether a site can be IFRAMEd by another was not found.]]> - Set Header in Config - Adds a value to the httpProtocol/customHeaders section of web.config to prevent the site being IFRAMEd by other websites. - A setting to create a header preventing IFRAMEing of the site by other websites has been added to your web.config file. - Could not update web.config file. Error: %0% - X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was found.]]> - X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was not found.]]> - Adds a value to the httpProtocol/customHeaders section of web.config to protect against MIME sniffing vulnerabilities. - A setting to create a header protecting against MIME sniffing vulnerabilities has been added to your web.config file. - Strict-Transport-Security, also known as the HSTS-header, was found.]]> - Strict-Transport-Security was not found.]]> - Adds the header 'Strict-Transport-Security' with the value 'max-age=10886400' to the httpProtocol/customHeaders section of web.config. Use this fix only if you will have your domains running with https for the next 18 weeks (minimum). - The HSTS header has been added to your web.config file. - X-XSS-Protection was found.]]> - X-XSS-Protection was not found.]]> - Adds the header 'X-XSS-Protection' with the value '1; mode=block' to the httpProtocol/customHeaders section of web.config. - The X-XSS-Protection header has been added to your web.config file. - - %0%.]]> - No headers revealing information about the website technology were found. - In the Web.config file, system.net/mailsettings could not be found. - In the Web.config file system.net/mailsettings section, the host is not configured. - SMTP settings are configured correctly and the service is operating as expected. - The SMTP server configured with host '%0%' and port '%1%' could not be reached. Please check to ensure the SMTP settings in the Web.config file system.net/mailsettings are correct. - %0%.]]> - %0%.]]> -

Results of the scheduled Umbraco Health Checks run on %0% at %1% are as follows:

%2%]]>
- Umbraco Health Check Status: %0% - Check All Groups - Check group - - The health checker evaluates various areas of your site for best practice settings, configuration, potential problems, etc. You can easily fix problems by pressing a button. - You can add your own health checks, have a look at the documentation for more information about custom health checks.

- ]]> -
- - - Disable URL tracker - Enable URL tracker - Culture - Original URL - Redirected To - Redirect Url Management - The following URLs redirect to this content item: - No redirects have been made - When a published page gets renamed or moved a redirect will automatically be made to the new page. - Are you sure you want to remove the redirect from '%0%' to '%1%'? - Redirect URL removed. - Error removing redirect URL. - This will remove the redirect - Are you sure you want to disable the URL tracker? - URL tracker has now been disabled. - Error disabling the URL tracker, more information can be found in your log file. - URL tracker has now been enabled. - Error enabling the URL tracker, more information can be found in your log file. - - - No Dictionary items to choose from - - - %0% characters left.]]> - %1% too many.]]> - - - Trashed content with Id: {0} related to original parent content with Id: {1} - Trashed media with Id: {0} related to original parent media item with Id: {1} - Cannot automatically restore this item - There is no location where this item can be automatically restored. You can move the item manually using the tree below. - was restored under - - - Direction - Parent to child - Bidirectional - Parent - Child - Count - Relations - Created - Comment - Name - No relations for this relation type. - Relation Type - Relations - - - Getting Started - Redirect URL Management - Content - Welcome - Examine Management - Published Status - Models Builder - Health Check - Profiling - Getting Started - Install Umbraco Forms - - - Go back - Active layout: - Jump to - group - passed - warning - failed - suggestion - Check passed - Check failed - Open backoffice search - Open/Close backoffice help - Open/Close your profile options - Open context menu for - Current language - Switch language to - Create new folder - Partial View - Partial View Macro - Member - Data type - Search the redirect dashboard - Search the user group section - Search the users section - Create item - Create - Edit - Name - - - References - This Data Type has no references. - Used in Document Types - No references to Document Types. - Used in Media Types - No references to Media Types. - Used in Member Types - No references to Member Types. - Used by - - - Log Levels - Saved Searches - Total Items - Timestamp - Level - Machine - Message - Exception - Properties - Search With Google - Search this message with Google - Search With Bing - Search this message with Bing - Search Our Umbraco - Search this message on Our Umbraco forums and docs - Search Our Umbraco with Google - Search Our Umbraco forums using Google - Search Umbraco Source - Search within Umbraco source code on Github - Search Umbraco Issues - Search Umbraco Issues on Github - Delete this search - Find Logs with Request ID - Find Logs with Namespace - Find Logs with Machine Name - Open - - - Copy %0% - %0% from %1% - Remove all items - - - Open Property Actions - - - Wait - Refresh status - Memory Cache - - - - Reload - Database Cache - - Rebuilding can be expensive. - Use it when reloading is not enough, and you think that the database cache has not been - properly generated—which would indicate some critical Umbraco issue. - ]]> - - Rebuild - Internals - - not need to use it. - ]]> - - Collect - Published Cache Status - Caches - - - Performance profiling - - - Umbraco currently runs in debug mode. This means you can use the built-in performance profiler to assess the performance when rendering pages. -

-

- If you want to activate the profiler for a specific page rendering, simply add umbDebug=true to the querystring when requesting the page. -

-

- If you want the profiler to be activated by default for all page renderings, you can use the toggle below. - It will set a cookie in your browser, which then activates the profiler automatically. - In other words, the profiler will only be active by default in your browser - not everyone else's. -

- ]]> -
- Activate the profiler by default - Friendly reminder - - - You should never let a production site run in debug mode. Debug mode is turned off by setting debug="false" on the <compilation /> element in web.config. -

- ]]> -
- - - Umbraco currently does not run in debug mode, so you can't use the built-in profiler. This is how it should be for a production site. -

-

- Debug mode is turned on by setting debug="true" on the <compilation /> element in web.config. -

- ]]> -
- - - Hours of Umbraco training videos are only a click away - - Want to master Umbraco? Spend a couple of minutes learning some best practices by watching one of these videos about using Umbraco. And visit umbraco.tv for even more Umbraco videos

- ]]> -
- To get you started - - - Start here - This section contains the building blocks for your Umbraco site. Follow the below links to find out more about working with the items in the Settings section - Find out more - - in the Documentation section of Our Umbraco - ]]> - - - Community Forum - ]]> - - - tutorial videos (some are free, some require a subscription) - ]]> - - - productivity boosting tools and commercial support - ]]> - - - training and certification opportunities - ]]> - - - - Welcome to The Friendly CMS - Thank you for choosing Umbraco - we think this could be the beginning of something beautiful. While it may feel overwhelming at first, we've done a lot to make the learning curve as smooth and fast as possible. - - - Umbraco Forms - Create forms using an intuitive drag and drop interface. From simple contact forms that sends e-mails to advanced questionaires that integrate with CRM systems. Your clients will love it! - -
+ + + + The Umbraco community + https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files + + + Culture and Hostnames + Audit Trail + Browse Node + Change Document Type + Copy + Create + Export + Create Package + Create group + Delete + Disable + Empty recycle bin + Enable + Export Document Type + Import Document Type + Import Package + Edit in Canvas + Exit + Move + Notifications + Public access + Publish + Unpublish + Reload + Republish entire site + Rename + Restore + Set permissions for the page %0% + Choose where to copy + Choose where to move + to in the tree structure below + Choose where to copy the selected item(s) + Choose where to move the selected item(s) + was moved to + was copied to + was deleted + Permissions + Rollback + Send To Publish + Send To Translation + Set group + Sort + Translate + Update + Set permissions + Unlock + Create Content Template + Resend Invitation + + + Content + Administration + Structure + Other + + + Allow access to assign culture and hostnames + Allow access to view a node's history log + Allow access to view a node + Allow access to change document type for a node + Allow access to copy a node + Allow access to create nodes + Allow access to delete nodes + Allow access to move a node + Allow access to set and change public access for a node + Allow access to publish a node + Allow access to unpublish a node + Allow access to change permissions for a node + Allow access to roll back a node to a previous state + Allow access to send a node for approval before publishing + Allow access to send a node for translation + Allow access to change the sort order for nodes + Allow access to translate a node + Allow access to save a node + Allow access to create a Content Template + + + Content + Info + + + Permission denied. + Add new Domain + remove + Invalid node. + One or more domains have an invalid format. + Domain has already been assigned. + Language + Domain + New domain '%0%' has been created + Domain '%0%' is deleted + Domain '%0%' has already been assigned + Domain '%0%' has been updated + Edit Current Domains + + + Inherit + Culture + or inherit culture from parent nodes. Will also apply
+ to the current node, unless a domain below applies too.]]>
+ Domains + + + Clear selection + Select + Do something else + Bold + Cancel Paragraph Indent + Insert form field + Insert graphic headline + Edit Html + Indent Paragraph + Italic + Center + Justify Left + Justify Right + Insert Link + Insert local link (anchor) + Bullet List + Numeric List + Insert macro + Insert picture + Publish and close + Publish with descendants + Edit relations + Return to list + Save + Save and close + Save and publish + Save and schedule + Send for approval + Save list view + Schedule + Preview + Preview is disabled because there's no template assigned + Choose style + Show styles + Insert table + Generate models and close + Save and generate models + Undo + Redo + Rollback + Delete tag + Cancel + Confirm + More publishing options + + + Viewing for + Content deleted + Content unpublished + Content unpublished for languages: %0% + Content published + Content published for languages: %0% + Content saved + Content saved for languages: %0% + Content moved + Content copied + Content rolled back + Content sent for publishing + Content sent for publishing for languages: %0% + Sort child items performed by user + Copy + Publish + Publish + Move + Save + Save + Delete + Unpublish + Unpublish + Rollback + Send To Publish + Send To Publish + Sort + History (all variants) + + + To change the document type for the selected content, first select from the list of valid types for this location. + Then confirm and/or amend the mapping of properties from the current type to the new, and click Save. + The content has been re-published. + Current Property + Current type + The document type cannot be changed, as there are no alternatives valid for this location. An alternative will be valid if it is allowed under the parent of the selected content item and that all existing child content items are allowed to be created under it. + Document Type Changed + Map Properties + Map to Property + New Template + New Type + none + Content + Select New Document Type + The document type of the selected content has been successfully changed to [new type] and the following properties mapped: + to + Could not complete property mapping as one or more properties have more than one mapping defined. + Only alternate types valid for the current location are displayed. + + + Failed to create a folder under parent with ID %0% + Failed to create a folder under parent with name %0% + The folder name cannot contain illegal characters. + Failed to delete item: %0% + + + Is Published + About this page + Alias + (how would you describe the picture over the phone) + Alternative Links + Click to edit this item + Created by + Original author + Updated by + Created + Date/time this document was created + Document Type + Editing + Remove at + This item has been changed after publication + This item is not published + Last published + There are no items to show + There are no items to show in the list. + No child items have been added + No members have been added + Media Type + Link to media item(s) + Member Group + Role + Member Type + No changes have been made + No date chosen + Page title + This media item has no link + No content can be added for this item + Properties + This document is published but is not visible because the parent '%0%' is unpublished + This culture is published but is not visible because it is unpublished on parent '%0%' + This document is published but is not in the cache + Could not get the url + This document is published but its url would collide with content %0% + This document is published but its url cannot be routed + Publish + Published + Published (pending changes)> + Publication Status + Publish with descendants to publish %0% and all content items underneath and thereby making their content publicly available.]]> + Publish with descendants to publish the selected languages and the same languages of content items underneath and thereby making their content publicly available.]]> + Publish at + Unpublish at + Clear Date + Set date + Sortorder is updated + To sort the nodes, simply drag the nodes or click one of the column headers. You can select multiple nodes by holding the "shift" or "control" key while selecting + Statistics + Title (optional) + Alternative text (optional) + Type + Unpublish + Draft + Not created + Last edited + Date/time this document was edited + Remove file(s) + Click here to remove the image from the media item + Click here to remove the file from the media item + Link to document + Member of group(s) + Not a member of group(s) + Child items + Target + This translates to the following time on the server: + What does this mean?]]> + Are you sure you want to delete this item? + Are you sure you want to delete all items? + Property %0% uses editor %1% which is not supported by Nested Content. + No content types are configured for this property. + Add element type + Select element type + Select the group whose properties should be displayed. If left blank, the first group on the element type will be used. + Enter an angular expression to evaluate against each item for its name. Use + to display the item index + Add another text box + Remove this text box + Content root + Include drafts: also publish unpublished content items. + This value is hidden. If you need access to view this value please contact your website administrator. + This value is hidden. + What languages would you like to publish? All languages with content are saved! + What languages would you like to publish? + What languages would you like to save? + All languages with content are saved on creation! + What languages would you like to send for approval? + What languages would you like to schedule? + Select the languages to unpublish. Unpublishing a mandatory language will unpublish all languages. + Published Languages + Unpublished Languages + Unmodified Languages + These languages haven't been created + Ready to Publish? + Ready to Save? + Send for approval + Select the date and time to publish and/or unpublish the content item. + Create new + Paste from clipboard + This item is in the Recycle Bin + + + Create a new Content Template from '%0%' + Blank + Select a Content Template + Content Template created + A Content Template was created from '%0%' + Another Content Template with the same name already exists + A Content Template is predefined content that an editor can select to use as the basis for creating new content + + + Click to upload + or click here to choose files + You can drag files here to upload. + Cannot upload this file, it does not have an approved file type + Max file size is + Media root + Failed to move media + Parent and destination folders cannot be the same + Failed to copy media + Failed to create a folder under parent id %0% + Failed to rename the folder with id %0% + Drag and drop your file(s) into the area + + + Create a new member + All Members + Member groups have no additional properties for editing. + + + Where do you want to create the new %0% + Create an item under + Select the document type you want to make a content template for + Enter a folder name + Choose a type and a title + Document Types within the Settings section, by editing the Allowed child node types under Permissions.]]> + Document Types within the Settings section.]]> + The selected page in the content tree doesn't allow for any pages to be created below it. + Edit permissions for this document type + Create a new document type + Document Types within the Settings section, by changing the Allow as root option under Permissions.]]> + Media Types Types within the Settings section, by editing the Allowed child node types under Permissions.]]> + The selected media in the tree doesn't allow for any other media to be created below it. + Edit permissions for this media type Document Type without a template + New folder + New data type + New JavaScript file + New empty partial view + New partial view macro + New partial view from snippet + New partial view macro from snippet + New partial view macro (without macro) + New style sheet file + New Rich Text Editor style sheet file + + + Browse your website + - Hide + If Umbraco isn't opening, you might need to allow popups from this site + has opened in a new window + Restart + Visit + Welcome + + + Stay + Discard changes + You have unsaved changes + Are you sure you want to navigate away from this page? - you have unsaved changes + Publishing will make the selected items visible on the site. + Unpublishing will remove the selected items and all their descendants from the site. + Unpublishing will remove this page and all its descendants from the site. + You have unsaved changes. Making changes to the Document Type will discard the changes. + + + Done + Deleted %0% item + Deleted %0% items + Deleted %0% out of %1% item + Deleted %0% out of %1% items + Published %0% item + Published %0% items + Published %0% out of %1% item + Published %0% out of %1% items + Unpublished %0% item + Unpublished %0% items + Unpublished %0% out of %1% item + Unpublished %0% out of %1% items + Moved %0% item + Moved %0% items + Moved %0% out of %1% item + Moved %0% out of %1% items + Copied %0% item + Copied %0% items + Copied %0% out of %1% item + Copied %0% out of %1% items + + + Link title + Link + Anchor / querystring + Name + Close this window + Are you sure you want to delete + Are you sure you want to disable + Are you sure? + Are you sure? + Cut + Edit Dictionary Item + Edit Language + Edit selected media + Insert local link + Insert character + Insert graphic headline + Insert picture + Insert link + Click to add a Macro + Insert table + This will delete the language + Changing the culture for a language may be an expensive operation and will result in the content cache and indexes being rebuilt + Last Edited + Link + Internal link: + When using local links, insert "#" in front of link + Open in new window? + Macro Settings + This macro does not contain any properties you can edit + Paste + Edit permissions for + Set permissions for + Set permissions for %0% for user group %1% + Select the users groups you want to set permissions for + The items in the recycle bin are now being deleted. Please do not close this window while this operation takes place + The recycle bin is now empty + When items are deleted from the recycle bin, they will be gone forever + regexlib.com's webservice is currently experiencing some problems, which we have no control over. We are very sorry for this inconvenience.]]> + Search for a regular expression to add validation to a form field. Example: 'email, 'zip-code' 'url' + Remove Macro + Required Field + Site is reindexed + The website cache has been refreshed. All publish content is now up to date. While all unpublished content is still unpublished + The website cache will be refreshed. All published content will be updated, while unpublished content will stay unpublished. + Number of columns + Number of rows + Click on the image to see full size + Pick item + View Cache Item + Relate to original + Include descendants + The friendliest community + Link to page + Opens the linked document in a new window or tab + Link to media + Select content start node + Select media + Select media type + Select icon + Select item + Select link + Select macro + Select content + Select content type + Select media start node + Select member + Select member group + Select member type + Select node + Select sections + Select users + No icons were found + There are no parameters for this macro + There are no macros available to insert + External login providers + Exception Details + Stacktrace + Inner Exception + Link your + Un-link your + account + Select editor + Select snippet + This will delete the node and all its languages. If you only want to delete one language, you should unpublish the node in that language instead. + + + There are no dictionary items. + + + %0%' below + ]]> + Culture Name + + Dictionary overview + + + Configured Searchers + Shows properties and tools for any configured Searcher (i.e. such as a multi-index searcher) + Field values + Health status + The health status of the index and if it can be read + Indexers + Index info + Lists the properties of the index + Manage Examine's indexes + Allows you to view the details of each index and provides some tools for managing the indexes + Rebuild index + + Depending on how much content there is in your site this could take a while.
+ It is not recommended to rebuild an index during times of high website traffic or when editors are editing content. + ]]> +
+ Searchers + Search the index and view the results + Tools + Tools to manage the index + fields + The index cannot be read and will need to be rebuilt + The process is taking longer than expected, check the umbraco log to see if there have been any errors during this operation + This index cannot be rebuilt because it has no assigned + IIndexPopulator + + + Enter your username + Enter your password + Confirm your password + Name the %0%... + Enter a name... + Enter an email... + Enter a username... + Label... + Enter a description... + Type to search... + Type to filter... + Type to add tags (press enter after each tag)... + Enter your email + Enter a message... + Your username is usually your email + #value or ?key=value + Enter alias... + Generating alias... + + + Create custom list view + Remove custom list view + A content type, media type or member type with this alias already exists + + + Renamed + Enter a new folder name here + %0% was renamed to %1% + + + Add prevalue + Database datatype + Property editor GUID + Property editor + Buttons + Enable advanced settings for + Enable context menu + Maximum default size of inserted images + Related stylesheets + Show label + Width and height + All property types & property data + using this data type will be deleted permanently, please confirm you want to delete these as well + Yes, delete + and all property types & property data using this data type + Select the folder to move + to in the tree structure below + was moved underneath + %0% will delete the properties and their data from the following items]]> + I understand this action will delete the properties and data based on this Data Type + + + Your data has been saved, but before you can publish this page there are some errors you need to fix first: + The current membership provider does not support changing password (EnablePasswordRetrieval need to be true) + %0% already exists + There were errors: + There were errors: + The password should be a minimum of %0% characters long and contain at least %1% non-alpha numeric character(s) + %0% must be an integer + The %0% field in the %1% tab is mandatory + %0% is a mandatory field + %0% at %1% is not in a correct format + %0% is not in a correct format + + + Received an error from the server + The specified file type has been disallowed by the administrator + NOTE! Even though CodeMirror is enabled by configuration, it is disabled in Internet Explorer because it's not stable enough. + Please fill both alias and name on the new property type! + There is a problem with read/write access to a specific file or folder + Error loading Partial View script (file: %0%) + Please enter a title + Please choose a type + You're about to make the picture larger than the original size. Are you sure that you want to proceed? + Startnode deleted, please contact your administrator + Please mark content before changing style + No active styles available + Please place cursor at the left of the two cells you wish to merge + You cannot split a cell that hasn't been merged. + This property is invalid + + + Options + About + Action + Actions + Add + Alias + All + Are you sure? + Back + Back to overview + Border + by + Cancel + Cell margin + Choose + Clear + Close + Close Window + Comment + Confirm + Constrain + Constrain proportions + Content + Continue + Copy + Create + Database + Date + Default + Delete + Deleted + Deleting... + Design + Dictionary + Dimensions + Down + Download + Edit + Edited + Elements + Email + Error + Field + Find + First + Focal point + General + Groups + Group + Height + Help + Hide + History + Icon + Id + Import + Include subfolders in search + Info + Inner margin + Insert + Install + Invalid + Justify + Label + Language + Last + Layout + Links + Loading + Locked + Login + Log off + Logout + Macro + Mandatory + Message + Move + Name + New + Next + No + of + Off + OK + Open + On + or + Order by + Password + Path + One moment please... + Previous + Properties + Rebuild + Email to receive form data + Recycle Bin + Your recycle bin is empty + Reload + Remaining + Remove + Rename + Renew + Required + Retrieve + Retry + Permissions + Scheduled Publishing + Search + Sorry, we can not find what you are looking for. + No items have been added + Server + Settings + Show + Show page on Send + Size + Sort + Status + Submit + Type + Type to search... + under + Up + Update + Upgrade + Upload + Url + User + Username + Value + View + Welcome... + Width + Yes + Folder + Search results + Reorder + I am done reordering + Preview + Change password + to + List view + Saving... + current + Embed + selected + Other + Articles + Videos + Clear + Installing + + + Blue + + + Add group + Add property + Add editor + Add template + Add child node + Add child + Edit data type + Navigate sections + Shortcuts + show shortcuts + Toggle list view + Toggle allow as root + Comment/Uncomment lines + Remove line + Copy Lines Up + Copy Lines Down + Move Lines Up + Move Lines Down + General + Editor + Toggle allow culture variants + + + Background color + Bold + Text color + Font + Text + + + Page + + + The installer cannot connect to the database. + Could not save the web.config file. Please modify the connection string manually. + Your database has been found and is identified as + Database configuration + install button to install the Umbraco %0% database + ]]> + Next to proceed.]]> + Database not found! Please check that the information in the "connection string" of the "web.config" file is correct.

+

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

+

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

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

+

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

+ ]]>
+ Press Next to + proceed. ]]> + next to continue the configuration wizard]]> + The Default users' password needs to be changed!]]> + The Default user has been disabled or has no access to Umbraco!

No further actions needs to be taken. Click Next to proceed.]]> + The Default user's password has been successfully changed since the installation!

No further actions needs to be taken. Click Next to proceed.]]> + The password is changed! + Get a great start, watch our introduction videos + By clicking the next button (or modifying the umbracoConfigurationStatus in web.config), you accept the license for this software as specified in the box below. Notice that this Umbraco distribution consists of two different licenses, the open source MIT license for the framework and the Umbraco freeware license that covers the UI. + Not installed yet. + Affected files and folders + More information on setting up permissions for Umbraco here + You need to grant ASP.NET modify permissions to the following files/folders + Your permission settings are almost perfect!

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

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

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

+ You are ready to run Umbraco and install packages!]]>
+ Resolving folder issue + Follow this link for more information on problems with ASP.NET and creating folders + Setting up folder permissions + + I want to start from scratch + learn how) + You can still choose to install Runway later on. Please go to the Developer section and choose Packages. + ]]> + You've just set up a clean Umbraco platform. What do you want to do next? + Runway is installed + + This is our list of recommended modules, check off the ones you would like to install, or view the full list of modules + ]]> + Only recommended for experienced users + I want to start with a simple website + + "Runway" is a simple website providing some basic document types and templates. The installer can set up Runway for you automatically, + but you can easily edit, extend or remove it. It's not necessary and you can perfectly use Umbraco without it. However, + Runway offers an easy foundation based on best practices to get you started faster than ever. + If you choose to install Runway, you can optionally select basic building blocks called Runway Modules to enhance your Runway pages. +

+ + Included with Runway: Home page, Getting Started page, Installing Modules page.
+ Optional Modules: Top Navigation, Sitemap, Contact, Gallery. +
+ ]]>
+ What is Runway + Step 1/5 Accept license + Step 2/5: Database configuration + Step 3/5: Validating File Permissions + Step 4/5: Check Umbraco security + Step 5/5: Umbraco is ready to get you started + Thank you for choosing Umbraco + Browse your new site +You installed Runway, so why not see how your new website looks.]]> + Further help and information +Get help from our award winning community, browse the documentation or watch some free videos on how to build a simple site, how to use packages and a quick guide to the Umbraco terminology]]> + Umbraco %0% is installed and ready for use + /web.config file and update the AppSetting key UmbracoConfigurationStatus in the bottom to the value of '%0%'.]]> + started instantly by clicking the "Launch Umbraco" button below.
If you are new to Umbraco, +you can find plenty of resources on our getting started pages.]]>
+ Launch Umbraco +To manage your website, simply open the Umbraco back office and start adding content, updating the templates and stylesheets or add new functionality]]> + Connection to database failed. + Umbraco Version 3 + Umbraco Version 4 + Watch + Umbraco %0% for a fresh install or upgrading from version 3.0. +

+ Press "next" to start the wizard.]]>
+ + + Culture Code + Culture Name + + + You've been idle and logout will automatically occur in + Renew now to save your work + + + Happy super Sunday + Happy manic Monday + Happy tubular Tuesday + Happy wonderful Wednesday + Happy thunderous Thursday + Happy funky Friday + Happy Caturday + Log in below + Sign in with + Session timed out + © 2001 - %0%
Umbraco.com

]]>
+ Forgotten password? + An email will be sent to the address specified with a link to reset your password + An email with password reset instructions will be sent to the specified address if it matched our records + Show password + Hide password + Return to login form + Please provide a new password + Your Password has been updated + The link you have clicked on is invalid or has expired + Umbraco: Reset Password + + + + + + + + + + + +
+ + + + + +
+ +
+ +
+
+ + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Password reset requested +

+

+ Your username to login to the Umbraco back-office is: %0% +

+

+ + + + + + +
+ + Click this link to reset your password + +
+

+

If you cannot click on the link, copy and paste this URL into your browser window:

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

+
+
+


+
+
+ + + ]]>
+ + + Dashboard + Sections + Content + + + Choose page above... + %0% has been copied to %1% + Select where the document %0% should be copied to below + %0% has been moved to %1% + Select where the document %0% should be moved to below + has been selected as the root of your new content, click 'ok' below. + No node selected yet, please select a node in the list above before clicking 'ok' + The current node is not allowed under the chosen node because of its type + The current node cannot be moved to one of its subpages + The current node cannot exist at the root + The action isn't allowed since you have insufficient permissions on 1 or more child documents. + Relate copied items to original + + + %0%]]> + Notification settings saved for + + The following languages have been modified %0% + + + + + + + + + + + +
+ + + + + +
+ +
+ +
+
+ + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Hi %0%, +

+

+ This is an automated mail to inform you that the task '%1%' has been performed on the page '%2%' by the user '%3%' +

+ + + + + + +
+ +
+ EDIT
+
+

+

Update summary:

+ %6% +

+

+ Have a nice day!

+ Cheers from the Umbraco robot +

+
+
+


+
+
+ + + ]]>
+ The following languages have been modified:

+ %0% + ]]>
+ [%0%] Notification about %1% performed on %2% + Notifications + + + Actions + Created + Create package + + button and locating the package. Umbraco packages usually have a ".umb" or ".zip" extension. + ]]> + This will delete the package + Drop to upload + Include all child nodes + or click here to choose package file + Upload package + Install a local package by selecting it from your machine. Only install packages from sources you know and trust + Upload another package + Cancel and upload another package + I accept + terms of use + Path to file + Absolute path to file (ie: /bin/umbraco.bin) + Installed + Installed packages + Install local + Finish + This package has no configuration view + No packages have been created yet + You don’t have any packages installed + 'Packages' icon in the top right of your screen]]> + Package Actions + Author URL + Package Content + Package Files + Icon URL + Install package + License + License URL + Package Properties + Search for packages + Results for + We couldn’t find anything for + Please try searching for another package or browse through the categories + Popular + New releases + has + karma points + Information + Owner + Contributors + Created + Current version + .NET version + Downloads + Likes + Compatibility + This package is compatible with the following versions of Umbraco, as reported by community members. Full compatability cannot be guaranteed for versions reported below 100% + External sources + Author + Documentation + Package meta data + Package name + Package doesn't contain any items +
+ You can safely remove this from the system by clicking "uninstall package" below.]]>
+ Package options + Package readme + Package repository + Confirm package uninstall + Package was uninstalled + The package was successfully uninstalled + Uninstall package + + Notice: any documents, media etc depending on the items you remove, will stop working, and could lead to system instability, + so uninstall with caution. If in doubt, contact the package author.]]> + Package version + Upgrading from version + Package already installed + This package cannot be installed, it requires a minimum Umbraco version of + Uninstalling... + Downloading... + Importing... + Installing... + Restarting, please wait... + All done, your browser will now refresh, please wait... + Please click 'Finish' to complete installation and reload the page. + Uploading package... + + + Paste with full formatting (Not recommended) + The text you're trying to paste contains special characters or formatting. This could be caused by copying text from Microsoft Word. Umbraco can remove special characters or formatting automatically, so the pasted content will be more suitable for the web. + Paste as raw text without any formatting at all + Paste, but remove formatting (Recommended) + + + Group based protection + If you want to grant access to all members of specific member groups + You need to create a member group before you can use group based authentication + Error Page + Used when people are logged on, but do not have access + %0%]]> + %0% is now protected]]> + %0%]]> + Login Page + Choose the page that contains the login form + Remove protection... + %0%?]]> + Select the pages that contain login form and error messages + %0%]]> + %0%]]> + Specific members protection + If you wish to grant access to specific members + + + Insufficient user permissions to publish all descendant documents + + + + + + + + Validation failed for required language '%0%'. This language was saved but not published. + Publishing in progress - please wait... + %0% out of %1% pages have been published... + %0% has been published + %0% and subpages have been published + Publish %0% and all its subpages + Publish to publish %0% and thereby making its content publicly available.

+ You can publish this page and all its subpages by checking Include unpublished subpages below. + ]]>
+ + + You have not configured any approved colors + + + You can only select items of type(s): %0% + You have picked a content item currently deleted or in the recycle bin + You have picked content items currently deleted or in the recycle bin + + + Deleted item + You have picked a media item currently deleted or in the recycle bin + You have picked media items currently deleted or in the recycle bin + Trashed + + + enter external link + choose internal page + Caption + Link + Open in new window + enter the display caption + Enter the link + + + Reset crop + Save crop + Add new crop + Done + Undo edits + + + Select a version to compare with the current version + Current version + Red text will not be shown in the selected version. , green means added]]> + Document has been rolled back + This displays the selected version as HTML, if you wish to see the difference between 2 versions at the same time, use the diff view + Rollback to + Select version + View + + + Edit script file + + + Content + Forms + Media + Members + Packages + Settings + Translation + Users + + + Tours + The best Umbraco video tutorials + Visit our.umbraco.com + Visit umbraco.tv + + + Default template + To import a document type, find the ".udt" file on your computer by clicking the "Browse" button and click "Import" (you'll be asked for confirmation on the next screen) + New Tab Title + Node type + Type + Stylesheet + Script + Tab + Tab Title + Tabs + Master Content Type enabled + This Content Type uses + as a Master Content Type. Tabs from Master Content Types are not shown and can only be edited on the Master Content Type itself + No properties defined on this tab. Click on the "add a new property" link at the top to create a new property. + Create matching template + Add icon + + + Sort order + Creation date + Sorting complete. + Drag the different items up or down below to set how they should be arranged. Or click the column headers to sort the entire collection of items + + This node has no child nodes to sort + + + Validation + Validation errors must be fixed before the item can be saved + Failed + Saved + Insufficient user permissions, could not complete the operation + Cancelled + Operation was cancelled by a 3rd party add-in + Property type already exists + Property type created + DataType: %1%]]> + Propertytype deleted + Document Type saved + Tab created + Tab deleted + Tab with id: %0% deleted + Stylesheet not saved + Stylesheet saved + Stylesheet saved without any errors + Datatype saved + Dictionary item saved + Content published + and is visible on the website + %0% documents published and visible on the website + %0% published and visible on the website + %0% documents published for languages %1% and visible on the website + Content saved + Remember to publish to make changes visible + A schedule for publishing has been updated + %0% saved + Sent For Approval + Changes have been sent for approval + %0% changes have been sent for approval + Media saved + Media saved without any errors + Member saved + Member group saved + Stylesheet Property Saved + Stylesheet saved + Template saved + Error saving user (check log) + User Saved + User type saved + User group saved + Cultures and hostnames saved + Error saving cultures and hostnames + File not saved + file could not be saved. Please check file permissions + File saved + File saved without any errors + Language saved + Media Type saved + Member Type saved + Member Group saved + Template not saved + Please make sure that you do not have 2 templates with the same alias + Template saved + Template saved without any errors! + Content unpublished + Content variation %0% unpublished + The mandatory language '%0%' was unpublished. All languages for this content item are now unpublished. + Partial view saved + Partial view saved without any errors! + Partial view not saved + An error occurred saving the file. + Permissions saved for + Deleted %0% user groups + %0% was deleted + Enabled %0% users + Disabled %0% users + %0% is now enabled + %0% is now disabled + User groups have been set + Unlocked %0% users + %0% is now unlocked + Member was exported to file + An error occurred while exporting the member + User %0% was deleted + Invite user + Invitation has been re-sent to %0% + Cannot publish the document since the required '%0%' is not published + Validation failed for language '%0%' + Document type was exported to file + An error occurred while exporting the document type + The release date cannot be in the past + Cannot schedule the document for publishing since the required '%0%' is not published + Cannot schedule the document for publishing since the required '%0%' has a publish date later than a non mandatory language + The expire date cannot be in the past + The expire date cannot be before the release date + + + Add style + Edit style + Rich text editor styles + Define the styles that should be available in the rich text editor for this stylesheet + Edit stylesheet + Edit stylesheet property + The name displayed in the editor style selector + Preview + How the text will look like in the rich text editor. + Selector + Uses CSS syntax, e.g. "h1" or ".redHeader" + Styles + The CSS that should be applied in the rich text editor, e.g. "color:red;" + Code + Rich Text Editor + + + Failed to delete template with ID %0% + Edit template + Sections + Insert content area + Insert content area placeholder + Insert + Choose what to insert into your template + Dictionary item + A dictionary item is a placeholder for a translatable piece of text, which makes it easy to create designs for multilingual websites. + Macro + + A Macro is a configurable component which is great for + reusable parts of your design, where you need the option to provide parameters, + such as galleries, forms and lists. + + Value + Displays the value of a named field from the current page, with options to modify the value or fallback to alternative values. + Partial view + + A partial view is a separate template file which can be rendered inside another + template, it's great for reusing markup or for separating complex templates into separate files. + + Master template + No master + Render child template + @RenderBody() placeholder. + ]]> + Define a named section + @section { ... }. This can be rendered in a + specific area of the parent of this template, by using @RenderSection. + ]]> + Render a named section + @RenderSection(name) placeholder. + This renders an area of a child template which is wrapped in a corresponding @section [name]{ ... } definition. + ]]> + Section Name + Section is mandatory + + If mandatory, the child template must contain a @section definition, otherwise an error is shown. + + Query builder + items returned, in + copy to clipboard + I want + all content + content of type "%0%" + from + my website + where + and + is + is not + before + before (including selected date) + after + after (including selected date) + equals + does not equal + contains + does not contain + greater than + greater than or equal to + less than + less than or equal to + Id + Name + Created Date + Last Updated Date + order by + ascending + descending + Template + + + Image + Macro + Choose type of content + Choose a layout + Add a row + Add content + Drop content + Settings applied + This content is not allowed here + This content is allowed here + Click to embed + Click to insert image + Image caption... + Write here... + Grid Layouts + Layouts are the overall work area for the grid editor, usually you only need one or two different layouts + Add Grid Layout + Adjust the layout by setting column widths and adding additional sections + Row configurations + Rows are predefined cells arranged horizontally + Add row configuration + Adjust the row by setting cell widths and adding additional cells + Columns + Total combined number of columns in the grid layout + Settings + Configure what settings editors can change + Styles + Configure what styling editors can change + Allow all editors + Allow all row configurations + Maximum items + Leave blank or set to 0 for unlimited + Set as default + Choose extra + Choose default + are added + Warning + You are deleting the row configuration + + Deleting a row configuration name will result in loss of data for any existing content that is based on this configuration. + + + + Compositions + Group + You have not added any groups + Add group + Inherited from + Add property + Required label + Enable list view + Configures the content item to show a sortable and searchable list of its children, the children will not be shown in the tree + Allowed Templates + Choose which templates editors are allowed to use on content of this type + Allow as root + Allow editors to create content of this type in the root of the content tree. + Allowed child node types + Allow content of the specified types to be created underneath content of this type. + Choose child node + Inherit tabs and properties from an existing document type. New tabs will be added to the current document type or merged if a tab with an identical name exists. + This content type is used in a composition, and therefore cannot be composed itself. + There are no content types available to use as a composition. + Removing a composition will delete all the associated property data. Once you save the document type there's no way back. + Create new + Use existing + Editor settings + Configuration + Yes, delete + was moved underneath + was copied underneath + Select the folder to move + Select the folder to copy + to in the tree structure below + All Document types + All Documents + All media items + using this document type will be deleted permanently, please confirm you want to delete these as well. + using this media type will be deleted permanently, please confirm you want to delete these as well. + using this member type will be deleted permanently, please confirm you want to delete these as well + and all documents using this type + and all media items using this type + and all members using this type + Member can edit + Allow this property value to be edited by the member on their profile page + Is sensitive data + Hide this property value from content editors that don't have access to view sensitive information + Show on member profile + Allow this property value to be displayed on the member profile page + tab has no sort order + Where is this composition used? + This composition is currently used in the composition of the following content types: + Allow varying by culture + Allow editors to create content of this type in different languages. + Allow varying by culture + Element type + Is an Element type + An Element type is meant to be used for instance in Nested Content, and not in the tree. + This is not applicable for an Element type + You have made changes to this property. Are you sure you want to discard them? + + + Add language + Mandatory language + Properties on this language have to be filled out before the node can be published. + Default language + An Umbraco site can only have one default language set. + Switching default language may result in default content missing. + Falls back to + No fall back language + To allow multi-lingual content to fall back to another language if not present in the requested language, select it here. + Fall back language + none + + + Add parameter + Edit parameter + Enter macro name + Parameters + Define the parameters that should be available when using this macro. + Select partial view macro file + + + Building models + this can take a bit of time, don't worry + Models generated + Models could not be generated + Models generation has failed, see exception in U log + + + Add fallback field + Fallback field + Add default value + Default value + Fallback field + Default value + Casing + Encoding + Choose field + Convert line breaks + Yes, convert line breaks + Replaces line breaks with 'br' html tag + Custom Fields + Date only + Format and encoding + Format as date + Format the value as a date, or a date with time, according to the active culture + HTML encode + Will replace special characters by their HTML equivalent. + Will be inserted after the field value + Will be inserted before the field value + Lowercase + Modify output + None + Output sample + Insert after field + Insert before field + Recursive + Yes, make it recursive + Separator + Standard Fields + Uppercase + URL encode + Will format special characters in URLs + Will only be used when the field values above are empty + This field will only be used if the primary field is empty + Date and time + + + Translation details + Download XML DTD + Fields + Include subpages + + No translator users found. Please create a translator user before you start sending content to translation + The page '%0%' has been send to translation + Send the page '%0%' to translation + Total words + Translate to + Translation completed. + You can preview the pages, you've just translated, by clicking below. If the original page is found, you will get a comparison of the 2 pages. + Translation failed, the XML file might be corrupt + Translation options + Translator + Upload translation XML + + + Content + Content Templates + Media + Cache Browser + Recycle Bin + Created packages + Data Types + Dictionary + Installed packages + Install skin + Install starter kit + Languages + Install local package + Macros + Media Types + Members + Member Groups + Member Roles + Member Types + Document Types + Relation Types + Packages + Packages + Partial Views + Partial View Macro Files + Install from repository + Install Runway + Runway modules + Scripting Files + Scripts + Stylesheets + Templates + Log Viewer + Users + Settings + Templating + Third Party + + + New update ready + %0% is ready, click here for download + No connection to server + Error checking for update. Please review trace-stack for further information + + + Access + Based on the assigned groups and start nodes, the user has access to the following nodes + Assign access + Administrator + Category field + User created + Change Your Password + Change photo + New password + hasn't been locked out + The password hasn't been changed + Confirm new password + You can change your password for accessing the Umbraco Back Office by filling out the form below and click the 'Change Password' button + Content Channel + Create another user + Create new users to give them access to Umbraco. When a new user is created a password will be generated that you can share with the user. + Description field + Disable User + Document Type + Editor + Excerpt field + Failed login attempts + Go to user profile + Add groups to assign access and permissions + Invite another user + Invite new users to give them access to Umbraco. An invite email will be sent to the user with information on how to log in to Umbraco. Invites last for 72 hours. + Language + Set the language you will see in menus and dialogs + Last lockout date + Last login + Password last changed + Username + Media start node + Limit the media library to a specific start node + Media start nodes + Limit the media library to specific start nodes + Sections + Disable Umbraco Access + has not logged in yet + Old password + Password + Reset password + Your password has been changed! + Please confirm the new password + Enter your new password + Your new password cannot be blank! + Current password + Invalid current password + There was a difference between the new password and the confirmed password. Please try again! + The confirmed password doesn't match the new password! + Replace child node permissions + You are currently modifying permissions for the pages: + Select pages to modify their permissions + Remove photo + Default permissions + Granular permissions + Set permissions for specific nodes + Profile + Search all children + Add sections to give users access + Select user groups + No start node selected + No start nodes selected + Content start node + Limit the content tree to a specific start node + Content start nodes + Limit the content tree to specific start nodes + User last updated + has been created + The new user has successfully been created. To log in to Umbraco use the password below. + User management + Name + User permissions + User group + has been invited + An invitation has been sent to the new user with details about how to log in to Umbraco. + Hello there and welcome to Umbraco! In just 1 minute you’ll be good to go, we just need you to setup a password and add a picture for your avatar. + Welcome to Umbraco! Unfortunately your invite has expired. Please contact your administrator and ask them to resend it. + Uploading a photo of yourself will make it easy for other users to recognize you. Click the circle above to upload your photo. + Writer + Change + Your profile + Your recent history + Session expires in + Invite user + Create user + Send invite + Back to users + Umbraco: Invitation + + + + + + + + + + + +
+ + + + + +
+ +
+ +
+
+ + + + + + +
+
+
+ + + + +
+ + + + +
+

+ Hi %0%, +

+

+ You have been invited by %1% to the Umbraco Back Office. +

+

+ Message from %1%: +
+ %2% +

+ + + + + + +
+ + + + + + +
+ + Click this link to accept the invite + +
+
+

If you cannot click on the link, copy and paste this URL into your browser window:

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

+
+
+


+
+
+ + ]]>
+ Invite + Resending invitation... + Delete User + Are you sure you wish to delete this user account? + All + Active + Disabled + Locked out + Invited + Inactive + Name (A-Z) + Name (Z-A) + Newest + Oldest + Last login + No user groups have been added + + + Validation + Validate as an email address + Validate as a number + Validate as a URL + ...or enter a custom validation + Field is mandatory + Enter a custom validation error message (optional) + Enter a regular expression + Enter a custom validation error message (optional) + You need to add at least + You can only have + items + items selected + Invalid date + Not a number + Invalid email + Value cannot be null + Value cannot be empty + Value is invalid, it does not match the correct pattern + Custom validation + %1% more.]]> + %1% too many.]]> + + + + Value is set to the recommended value: '%0%'. + Value was set to '%1%' for XPath '%2%' in configuration file '%3%'. + Expected value '%1%' for '%2%' in configuration file '%3%', but found '%0%'. + Found unexpected value '%0%' for '%2%' in configuration file '%3%'. + + Custom errors are set to '%0%'. + Custom errors are currently set to '%0%'. It is recommended to set this to '%1%' before go live. + Custom errors successfully set to '%0%'. + MacroErrors are set to '%0%'. + MacroErrors are set to '%0%' which will prevent some or all pages in your site from loading completely if there are any errors in macros. Rectifying this will set the value to '%1%'. + MacroErrors are now set to '%0%'. + + Try Skip IIS Custom Errors is set to '%0%' and you're using IIS version '%1%'. + Try Skip IIS Custom Errors is currently '%0%'. It is recommended to set this to '%1%' for your IIS version (%2%). + Try Skip IIS Custom Errors successfully set to '%0%'. + + File does not exist: '%0%'. + '%0%' in config file '%1%'.]]> + There was an error, check log for full error: %0%. + Database - The database schema is correct for this version of Umbraco + %0% problems were detected with your database schema (Check the log for details) + Some errors were detected while validating the database schema against the current version of Umbraco. + Your website's certificate is valid. + Certificate validation error: '%0%' + Your website's SSL certificate has expired. + Your website's SSL certificate is expiring in %0% days. + Error pinging the URL %0% - '%1%' + You are currently %0% viewing the site using the HTTPS scheme. + The appSetting 'Umbraco.Core.UseHttps' is set to 'false' in your web.config file. Once you access this site using the HTTPS scheme, that should be set to 'true'. + The appSetting 'Umbraco.Core.UseHttps' is set to '%0%' in your web.config file, your cookies are %1% marked as secure. + Could not update the 'Umbraco.Core.UseHttps' setting in your web.config file. Error: %0% + + Enable HTTPS + Sets umbracoSSL setting to true in the appSettings of the web.config file. + The appSetting 'Umbraco.Core.UseHttps' is now set to 'true' in your web.config file, your cookies will be marked as secure. + Fix + Cannot fix a check with a value comparison type of 'ShouldNotEqual'. + Cannot fix a check with a value comparison type of 'ShouldEqual' with a provided value. + Value to fix check not provided. + Debug compilation mode is disabled. + Debug compilation mode is currently enabled. It is recommended to disable this setting before go live. + Debug compilation mode successfully disabled. + Trace mode is disabled. + Trace mode is currently enabled. It is recommended to disable this setting before go live. + Trace mode successfully disabled. + All folders have the correct permissions set. + + %0%.]]> + %0%. If they aren't being written to no action need be taken.]]> + All files have the correct permissions set. + + %0%.]]> + %0%. If they aren't being written to no action need be taken.]]> + X-Frame-Options used to control whether a site can be IFRAMEd by another was found.]]> + X-Frame-Options used to control whether a site can be IFRAMEd by another was not found.]]> + Set Header in Config + Adds a value to the httpProtocol/customHeaders section of web.config to prevent the site being IFRAMEd by other websites. + A setting to create a header preventing IFRAMEing of the site by other websites has been added to your web.config file. + Could not update web.config file. Error: %0% + X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was found.]]> + X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was not found.]]> + Adds a value to the httpProtocol/customHeaders section of web.config to protect against MIME sniffing vulnerabilities. + A setting to create a header protecting against MIME sniffing vulnerabilities has been added to your web.config file. + Strict-Transport-Security, also known as the HSTS-header, was found.]]> + Strict-Transport-Security was not found.]]> + Adds the header 'Strict-Transport-Security' with the value 'max-age=10886400' to the httpProtocol/customHeaders section of web.config. Use this fix only if you will have your domains running with https for the next 18 weeks (minimum). + The HSTS header has been added to your web.config file. + X-XSS-Protection was found.]]> + X-XSS-Protection was not found.]]> + Adds the header 'X-XSS-Protection' with the value '1; mode=block' to the httpProtocol/customHeaders section of web.config. + The X-XSS-Protection header has been added to your web.config file. + + %0%.]]> + No headers revealing information about the website technology were found. + In the Web.config file, system.net/mailsettings could not be found. + In the Web.config file system.net/mailsettings section, the host is not configured. + SMTP settings are configured correctly and the service is operating as expected. + The SMTP server configured with host '%0%' and port '%1%' could not be reached. Please check to ensure the SMTP settings in the Web.config file system.net/mailsettings are correct. + %0%.]]> + %0%.]]> +

Results of the scheduled Umbraco Health Checks run on %0% at %1% are as follows:

%2%]]>
+ Umbraco Health Check Status: %0% + Check All Groups + Check group + + The health checker evaluates various areas of your site for best practice settings, configuration, potential problems, etc. You can easily fix problems by pressing a button. + You can add your own health checks, have a look at the documentation for more information about custom health checks.

+ ]]> +
+ + + Disable URL tracker + Enable URL tracker + Culture + Original URL + Redirected To + Redirect Url Management + The following URLs redirect to this content item: + No redirects have been made + When a published page gets renamed or moved a redirect will automatically be made to the new page. + Are you sure you want to remove the redirect from '%0%' to '%1%'? + Redirect URL removed. + Error removing redirect URL. + This will remove the redirect + Are you sure you want to disable the URL tracker? + URL tracker has now been disabled. + Error disabling the URL tracker, more information can be found in your log file. + URL tracker has now been enabled. + Error enabling the URL tracker, more information can be found in your log file. + + + No Dictionary items to choose from + + + %0% characters left.]]> + %1% too many.]]> + + + Trashed content with Id: {0} related to original parent content with Id: {1} + Trashed media with Id: {0} related to original parent media item with Id: {1} + Cannot automatically restore this item + There is no location where this item can be automatically restored. You can move the item manually using the tree below. + was restored under + + + Direction + Parent to child + Bidirectional + Parent + Child + Count + Relations + Created + Comment + Name + No relations for this relation type. + Relation Type + Relations + + + Getting Started + Redirect URL Management + Content + Welcome + Examine Management + Published Status + Models Builder + Health Check + Profiling + Getting Started + Install Umbraco Forms + + + Go back + Active layout: + Jump to + group + passed + warning + failed + suggestion + Check passed + Check failed + Open backoffice search + Open/Close backoffice help + Open/Close your profile options + Open context menu for + Current language + Switch language to + Create new folder + Partial View + Partial View Macro + Member + Data type + Search the redirect dashboard + Search the user group section + Search the users section + Create item + Create + Edit + Name + + + References + This Data Type has no references. + Used in Document Types + No references to Document Types. + Used in Media Types + No references to Media Types. + Used in Member Types + No references to Member Types. + Used by + + + Log Levels + Saved Searches + Total Items + Timestamp + Level + Machine + Message + Exception + Properties + Search With Google + Search this message with Google + Search With Bing + Search this message with Bing + Search Our Umbraco + Search this message on Our Umbraco forums and docs + Search Our Umbraco with Google + Search Our Umbraco forums using Google + Search Umbraco Source + Search within Umbraco source code on Github + Search Umbraco Issues + Search Umbraco Issues on Github + Delete this search + Find Logs with Request ID + Find Logs with Namespace + Find Logs with Machine Name + Open + + + Copy %0% + %0% from %1% + Remove all items + + + Open Property Actions + + + Wait + Refresh status + Memory Cache + + + + Reload + Database Cache + + Rebuilding can be expensive. + Use it when reloading is not enough, and you think that the database cache has not been + properly generated—which would indicate some critical Umbraco issue. + ]]> + + Rebuild + Internals + + not need to use it. + ]]> + + Collect + Published Cache Status + Caches + + + Performance profiling + + + Umbraco currently runs in debug mode. This means you can use the built-in performance profiler to assess the performance when rendering pages. +

+

+ If you want to activate the profiler for a specific page rendering, simply add umbDebug=true to the querystring when requesting the page. +

+

+ If you want the profiler to be activated by default for all page renderings, you can use the toggle below. + It will set a cookie in your browser, which then activates the profiler automatically. + In other words, the profiler will only be active by default in your browser - not everyone else's. +

+ ]]> +
+ Activate the profiler by default + Friendly reminder + + + You should never let a production site run in debug mode. Debug mode is turned off by setting debug="false" on the <compilation /> element in web.config. +

+ ]]> +
+ + + Umbraco currently does not run in debug mode, so you can't use the built-in profiler. This is how it should be for a production site. +

+

+ Debug mode is turned on by setting debug="true" on the <compilation /> element in web.config. +

+ ]]> +
+ + + Hours of Umbraco training videos are only a click away + + Want to master Umbraco? Spend a couple of minutes learning some best practices by watching one of these videos about using Umbraco. And visit umbraco.tv for even more Umbraco videos

+ ]]> +
+ To get you started + + + Start here + This section contains the building blocks for your Umbraco site. Follow the below links to find out more about working with the items in the Settings section + Find out more + + in the Documentation section of Our Umbraco + ]]> + + + Community Forum + ]]> + + + tutorial videos (some are free, some require a subscription) + ]]> + + + productivity boosting tools and commercial support + ]]> + + + training and certification opportunities + ]]> + + + + Welcome to The Friendly CMS + Thank you for choosing Umbraco - we think this could be the beginning of something beautiful. While it may feel overwhelming at first, we've done a lot to make the learning curve as smooth and fast as possible. + + + Umbraco Forms + Create forms using an intuitive drag and drop interface. From simple contact forms that sends e-mails to advanced questionaires that integrate with CRM systems. Your clients will love it! + +
From e85640c48338a2e407b181930b940ef53ab3fb39 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 16 Jan 2020 15:15:04 +0100 Subject: [PATCH 62/85] V8: Set tab focus on newly created nested content items (#6914) * Set tab focus on newly created nested content items * Fix bad merge * Undo unintentional change --- .../nestedcontent/nestedcontent.propertyeditor.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html index 47293e433b..91afbf3ba9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html @@ -8,7 +8,7 @@
-
+
From dbe088eedb89b18a3813af1ae523c01e6e93b75d Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 16 Jan 2020 15:36:54 +0100 Subject: [PATCH 63/85] V8: Add infinite editing to datatype references tab (#6907) * Add infinite editing to datatype references tab * Use correct save button label in infinite editing mode --- .../src/common/services/editor.service.js | 18 ++++++ .../views/datatype.info.controller.js | 56 ++++++++++++++++++- .../views/datatypes/views/datatype.info.html | 6 +- .../src/views/membertypes/edit.controller.js | 34 ++++++++--- .../src/views/membertypes/edit.html | 11 +++- 5 files changed, 112 insertions(+), 13 deletions(-) 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/views/datatypes/views/datatype.info.controller.js b/src/Umbraco.Web.UI.Client/src/views/datatypes/views/datatype.info.controller.js index d4aff871a2..be8ddba592 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatypes/views/datatype.info.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/datatypes/views/datatype.info.controller.js @@ -6,7 +6,7 @@ * @description * The controller for the info view of the datatype editor */ -function DataTypeInfoController($scope, $routeParams, dataTypeResource, eventsService, $timeout) { +function DataTypeInfoController($scope, $routeParams, dataTypeResource, eventsService, $timeout, editorService) { var vm = this; var evts = []; @@ -17,6 +17,9 @@ function DataTypeInfoController($scope, $routeParams, dataTypeResource, eventsSe vm.view = {}; vm.view.loading = true; + vm.openDocumentType = openDocumentType; + vm.openMediaType = openMediaType; + vm.openMemberType = openMemberType; /** Loads in the data type references one time */ function loadRelations() { @@ -31,6 +34,57 @@ function DataTypeInfoController($scope, $routeParams, dataTypeResource, eventsSe } } + function openDocumentType(id, event) { + open(id, event, "documentType"); + } + + function openMediaType(id, event) { + open(id, event, "mediaType"); + } + + function openMemberType(id, event) { + open(id, event, "memberType"); + } + + function open(id, event, type) { + // targeting a new tab/window? + if (event.ctrlKey || + event.shiftKey || + event.metaKey || // apple + (event.button && event.button === 1) // middle click, >IE9 + everyone else + ) { + // yes, let the link open itself + return; + } + event.stopPropagation(); + event.preventDefault(); + + const editor = { + id: id, + submit: function (model) { + editorService.close(); + vm.view.loading = true; + referencesLoaded = false; + loadRelations(); + }, + close: function () { + editorService.close(); + } + }; + + switch (type) { + case "documentType": + editorService.documentTypeEditor(editor); + break; + case "mediaType": + editorService.mediaTypeEditor(editor); + break; + case "memberType": + editorService.memberTypeEditor(editor); + break; + } + } + // load data type references when the references tab is activated evts.push(eventsService.on("app.tabChange", function (event, args) { $timeout(function () { diff --git a/src/Umbraco.Web.UI.Client/src/views/datatypes/views/datatype.info.html b/src/Umbraco.Web.UI.Client/src/views/datatypes/views/datatype.info.html index 16b2d4b263..0af1634b29 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatypes/views/datatype.info.html +++ b/src/Umbraco.Web.UI.Client/src/views/datatypes/views/datatype.info.html @@ -43,7 +43,7 @@
{{::reference.name}}
{{::reference.alias}}
{{::reference.properties | umbCmsJoinArray:', ':'name'}}
- +
@@ -73,7 +73,7 @@
{{::reference.name}}
{{::reference.alias}}
{{::reference.properties | umbCmsJoinArray:', ':'name'}}
- +
@@ -104,7 +104,7 @@
{{::reference.name}}
{{::reference.alias}}
{{::reference.properties | umbCmsJoinArray:', ':'name'}}
- +
diff --git a/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.controller.js index ab54a453b5..b2e515e187 100644 --- a/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.controller.js @@ -13,8 +13,13 @@ var evts = []; var vm = this; + var infiniteMode = $scope.model && $scope.model.infiniteMode; + var memberTypeId = infiniteMode ? $scope.model.id : $routeParams.id; + var create = infiniteMode ? $scope.model.create : $routeParams.create; vm.save = save; + vm.close = close; + vm.editorfor = "visuallyHiddenTexts_newMember"; vm.header = {}; vm.header.editorfor = "content_membergroup"; @@ -25,6 +30,7 @@ vm.page.loading = false; vm.page.saveButtonState = "init"; vm.labels = {}; + vm.saveButtonKey = infiniteMode ? "buttons_saveAndClose" : "buttons_save"; var labelKeys = [ "general_design", @@ -86,7 +92,7 @@ vm.page.defaultButton = { hotKey: "ctrl+s", hotKeyWhenHidden: true, - labelKey: "buttons_save", + labelKey: vm.saveButtonKey, letter: "S", type: "submit", handler: function () { vm.save(); } @@ -94,7 +100,7 @@ vm.page.subButtons = [{ hotKey: "ctrl+g", hotKeyWhenHidden: true, - labelKey: "buttons_saveAndGenerateModels", + labelKey: infiniteMode ? "buttons_generateModelsAndClose" : "buttons_saveAndGenerateModels", letter: "G", handler: function () { @@ -147,12 +153,12 @@ } }); - if ($routeParams.create) { + if (create) { vm.page.loading = true; //we are creating so get an empty data type item - memberTypeResource.getScaffold($routeParams.id) + memberTypeResource.getScaffold(memberTypeId) .then(function (dt) { init(dt); @@ -163,10 +169,12 @@ vm.page.loading = true; - memberTypeResource.getById($routeParams.id).then(function (dt) { + memberTypeResource.getById(memberTypeId).then(function (dt) { init(dt); - syncTreeNode(vm.contentType, dt.path, true); + if(!infiniteMode) { + syncTreeNode(vm.contentType, dt.path, true); + } vm.page.loading = false; }); @@ -219,10 +227,16 @@ } }).then(function (data) { //success - syncTreeNode(vm.contentType, data.path); + if(!infiniteMode) { + syncTreeNode(vm.contentType, data.path); + } vm.page.saveButtonState = "success"; + if(infiniteMode && $scope.model.submit) { + $scope.model.submit(); + } + deferred.resolve(data); }, function (err) { //error @@ -307,6 +321,12 @@ }); } + + function close() { + if(infiniteMode && $scope.model.close) { + $scope.model.close(); + } + } evts.push(eventsService.on("editors.groupsBuilder.changed", function(name, args) { angularHelper.getCurrentForm($scope).$setDirty(); diff --git a/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.html b/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.html index 07824bc7ec..c4c521c857 100644 --- a/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.html @@ -40,6 +40,14 @@ + + + + label-key="{{vm.saveButtonKey}}"> Date: Thu, 16 Jan 2020 16:11:08 +0100 Subject: [PATCH 64/85] Responsive Angular API documentation (#7030) --- src/Umbraco.Web.UI.Docs/umb-docs.css | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Umbraco.Web.UI.Docs/umb-docs.css b/src/Umbraco.Web.UI.Docs/umb-docs.css index 931c22b255..0f2e3e7f74 100644 --- a/src/Umbraco.Web.UI.Docs/umb-docs.css +++ b/src/Umbraco.Web.UI.Docs/umb-docs.css @@ -7,6 +7,21 @@ body { } +.container, .navbar-static-top .container, .navbar-fixed-top .container, .navbar-fixed-bottom .container { + max-width: 1500px; + width: 95%; +} + +.span3 { + width: 220px; + width: calc(90% / 12 * 3); +} + +.span9 { + width: 700px; + width: calc(90% / 12 * 9); +} + .h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 { font-family: inherit; font-weight: 400; From 4c68f7e53a324aae9fb0dbc57de5f0ab967e4bde Mon Sep 17 00:00:00 2001 From: Jan Skovgaard Date: Thu, 16 Jan 2020 16:13:25 +0100 Subject: [PATCH 65/85] List view configuration - Sort order distance fix (#6905) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks, Jan. You're a 🌟 --- .../lib/bootstrap/less/type.less | 4 ++++ .../listview/orderDirection.prevalues.html | 12 +++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) 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/src/views/propertyeditors/listview/orderDirection.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/orderDirection.prevalues.html index 83a905ccf7..7ec0895936 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/orderDirection.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/orderDirection.prevalues.html @@ -1,10 +1,16 @@ 
- - +
    +
  • + +
  • +
  • + +
  • +
- Required + Required
From 4d5f34a63ddb00507bffbf409366834bdb59c627 Mon Sep 17 00:00:00 2001 From: Kieron Boswell Date: Thu, 16 Jan 2020 16:15:07 +0000 Subject: [PATCH 66/85] Spelling mistake on word Success in instructional comment. (#6966) --- .../Umbraco/PartialViewMacros/Templates/EditProfile.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/EditProfile.cshtml b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/EditProfile.cshtml index 7034404ca3..74ec033f25 100644 --- a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/EditProfile.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/EditProfile.cshtml @@ -23,7 +23,7 @@ { if (success) { - @* This message will show if RedirectOnSucces is set to false (default) *@ + @* This message will show if profileModel.RedirectUrl is not defined (default) *@

Profile updated

} From f5a5a9a2971fd87829f5a7561e8a0afc0f9d2003 Mon Sep 17 00:00:00 2001 From: Kieron Boswell Date: Thu, 16 Jan 2020 16:19:54 +0000 Subject: [PATCH 67/85] Spelling mistake in comments of partial snippet (#6975) --- .../Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml index 17ed95ea31..804e2307f0 100644 --- a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/RegisterMember.cshtml @@ -45,7 +45,7 @@ @if (success) { - @* This message will show if RedirectOnSucces is set to false (default) *@ + @* This message will show if registerModel.RedirectUrl is not defined (default) *@

Registration succeeded.

} else From f3d9a01800765b26478a2495c02c9c072571f7af Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 16 Jan 2020 23:00:28 +0100 Subject: [PATCH 68/85] Hide the "Add" button for Nested Content in "single mode" (#7327) --- .../nestedcontent/nestedcontent.propertyeditor.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html index 91afbf3ba9..da6e466b50 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html @@ -40,7 +40,7 @@ - -
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/create.html b/src/Umbraco.Web.UI.Client/src/views/content/create.html index cfb4b2c573..1d4b646e05 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/create.html @@ -36,7 +36,7 @@
    -
  • +
diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.controller.js b/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.controller.js index 1d865f2fa0..53a098a26e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.controller.js @@ -1,7 +1,7 @@ (function() { 'use strict'; - function PermissionsController($scope, mediaTypeResource, iconHelper, contentTypeHelper, localizationService, overlayService) { + function PermissionsController($scope, $timeout, mediaTypeResource, iconHelper, contentTypeHelper, localizationService, overlayService) { /* ----------- SCOPE VARIABLES ----------- */ @@ -13,6 +13,7 @@ vm.addChild = addChild; vm.removeChild = removeChild; + vm.sortChildren = sortChildren; vm.toggle = toggle; /* ---------- INIT ---------- */ @@ -71,6 +72,13 @@ $scope.model.allowedContentTypes.splice(selectedChildIndex, 1); } + function sortChildren() { + // we need to wait until the next digest cycle for vm.selectedChildren to be updated + $timeout(function () { + $scope.model.allowedContentTypes = _.pluck(vm.selectedChildren, "id"); + }); + } + /** * Toggle the $scope.model.allowAsRoot value to either true or false */ diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.html b/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.html index caa4c4dc11..09e7ab4f41 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.html +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/views/permissions/permissions.html @@ -27,7 +27,8 @@ parent-icon="model.icon" parent-id="model.id" on-add="vm.addChild" - on-remove="vm.removeChild"> + on-remove="vm.removeChild" + on-sort="vm.sortChildren">
diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index a566c2cdc7..9c248f186b 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -432,11 +432,11 @@ namespace Umbraco.Web.Editors } var contentType = Services.ContentTypeBaseServices.GetContentTypeOf(contentItem); - var ids = contentType.AllowedContentTypes.Select(x => x.Id.Value).ToArray(); + var ids = contentType.AllowedContentTypes.OrderBy(c => c.SortOrder).Select(x => x.Id.Value).ToArray(); if (ids.Any() == false) return Enumerable.Empty(); - types = Services.ContentTypeService.GetAll(ids).ToList(); + types = Services.ContentTypeService.GetAll(ids).OrderBy(c => ids.IndexOf(c.Id)).ToList(); } var basics = types.Where(type => type.IsElement == false).Select(Mapper.Map).ToList(); @@ -459,7 +459,7 @@ namespace Umbraco.Web.Editors } } - return basics; + return basics.OrderBy(c => contentId == Constants.System.Root ? c.Name : string.Empty); } /// diff --git a/src/Umbraco.Web/Editors/MediaTypeController.cs b/src/Umbraco.Web/Editors/MediaTypeController.cs index 43569c77e2..3a4026423a 100644 --- a/src/Umbraco.Web/Editors/MediaTypeController.cs +++ b/src/Umbraco.Web/Editors/MediaTypeController.cs @@ -240,11 +240,11 @@ namespace Umbraco.Web.Editors } var contentType = Services.MediaTypeService.Get(contentItem.ContentTypeId); - var ids = contentType.AllowedContentTypes.Select(x => x.Id.Value).ToArray(); + var ids = contentType.AllowedContentTypes.OrderBy(c => c.SortOrder).Select(x => x.Id.Value).ToArray(); if (ids.Any() == false) return Enumerable.Empty(); - types = Services.MediaTypeService.GetAll(ids).ToList(); + types = Services.MediaTypeService.GetAll(ids).OrderBy(c => ids.IndexOf(c.Id)).ToList(); } var basics = types.Select(Mapper.Map).ToList(); @@ -255,7 +255,7 @@ namespace Umbraco.Web.Editors basic.Description = TranslateItem(basic.Description); } - return basics.OrderBy(x => x.Name); + return basics.OrderBy(c => contentId == Constants.System.Root ? c.Name : string.Empty); } /// diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs index 266cd894b6..f18c481297 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs @@ -493,7 +493,7 @@ namespace Umbraco.Web.Models.Mapping target.Udi = MapContentTypeUdi(source); target.UpdateDate = source.UpdateDate; - target.AllowedContentTypes = source.AllowedContentTypes.Select(x => x.Id.Value); + target.AllowedContentTypes = source.AllowedContentTypes.OrderBy(c => c.SortOrder).Select(x => x.Id.Value); target.CompositeContentTypes = source.ContentTypeComposition.Select(x => x.Alias); target.LockedCompositeContentTypes = MapLockedCompositions(source); } From 0d12852cd8520dd21c4e33136a93d4747506d816 Mon Sep 17 00:00:00 2001 From: abi Date: Tue, 21 Jan 2020 09:07:10 +0000 Subject: [PATCH 75/85] Address Shannon comments Add code documentation, return empty enumerable instead of allocate empty list --- .../Search/IUmbracoTreeSearcherFields.cs | 16 +++++++++++++++- .../Search/UmbracoTreeSearcherFields.cs | 7 +++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields.cs b/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields.cs index eaa5d743a1..c5a6c53d19 100644 --- a/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields.cs +++ b/src/Umbraco.Web/Search/IUmbracoTreeSearcherFields.cs @@ -2,12 +2,26 @@ using System.Collections.Generic; namespace Umbraco.Web.Search { + /// + /// Used to propagate hardcoded internal Field lists + /// public interface IUmbracoTreeSearcherFields { + /// + /// Propagate list of searchable fields for all node types + /// IEnumerable GetBackOfficeFields(); + /// + /// Propagate list of searchable fields for Members + /// IEnumerable GetBackOfficeMembersFields(); - + /// + /// Propagate list of searchable fields for Media + /// IEnumerable GetBackOfficeMediaFields(); + /// + /// Propagate list of searchable fields for Documents + /// IEnumerable GetBackOfficeDocumentFields(); } } diff --git a/src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs index d1c663024b..f90d7bc6b6 100644 --- a/src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcherFields.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; +using System.Linq; using Umbraco.Examine; -using Umbraco.Web.Search; -namespace Umbraco.Web +namespace Umbraco.Web.Search { public class UmbracoTreeSearcherFields : IUmbracoTreeSearcherFields { @@ -23,10 +23,9 @@ namespace Umbraco.Web { return _backOfficeMediaFields; } - private IReadOnlyList _backOfficeDocumentFields = new List (); public IEnumerable GetBackOfficeDocumentFields() { - return _backOfficeDocumentFields; + return Enumerable.Empty(); } } } From 163c1f7028eb419eda33e8a0c0548f66fd1ee8e2 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 21 Jan 2020 19:16:07 +0100 Subject: [PATCH 76/85] Fix automatic merge gone wrong --- src/Umbraco.Web.UI.Client/src/common/services/tour.service.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 91b41cc68d..1c2da43814 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 @@ -168,12 +168,12 @@ } } - }); + } deferred.resolve(groupedTours); }); return deferred.promise; - } + }); /** * @ngdoc method From 80309011847af5664d3765d824dd2ec3e93985c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 22 Jan 2020 08:36:52 +0100 Subject: [PATCH 77/85] Fix mistakes made in commit a4a6b77 --- .../src/common/services/tour.service.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) 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 1c2da43814..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 @@ -162,18 +162,19 @@ 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); + } } } - } + }); deferred.resolve(groupedTours); }); return deferred.promise; - }); + } /** * @ngdoc method From 10f15a7648c939c0469892bf04f9ae6e5930ef6f Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Wed, 22 Jan 2020 11:10:28 +0100 Subject: [PATCH 78/85] Remove duplicate VariesBySegment overload after merge --- src/Umbraco.Core/ContentVariationExtensions.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs index 8939f7133b..6430c75686 100644 --- a/src/Umbraco.Core/ContentVariationExtensions.cs +++ b/src/Umbraco.Core/ContentVariationExtensions.cs @@ -27,11 +27,6 @@ namespace Umbraco.Core /// public static bool VariesByNothing(this IContentTypeBase contentType) => contentType.Variations.VariesByNothing(); - /// - /// 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. /// From 6c2e1b29fd7221a259be97e39eb0ee3ae0f657cd Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Wed, 22 Jan 2020 11:10:56 +0100 Subject: [PATCH 79/85] Use SetVariesBy extension methods instead of SetFlag/UnsetFlag --- src/Umbraco.TestData/SegmentTestController.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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"); From ac675d14e952788881e4980e0f20c57aae967d9c Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Wed, 22 Jan 2020 11:13:49 +0100 Subject: [PATCH 80/85] Fixes build errors --- src/Umbraco.Core/ContentVariationExtensions.cs | 13 ++++--------- src/Umbraco.TestData/SegmentTestController.cs | 8 ++++---- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs index 8939f7133b..4fc6f418aa 100644 --- a/src/Umbraco.Core/ContentVariationExtensions.cs +++ b/src/Umbraco.Core/ContentVariationExtensions.cs @@ -30,6 +30,10 @@ namespace Umbraco.Core /// /// 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(); /// @@ -122,15 +126,6 @@ namespace Umbraco.Core /// 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. /// diff --git a/src/Umbraco.TestData/SegmentTestController.cs b/src/Umbraco.TestData/SegmentTestController.cs index 33badbbb55..c78d12ab0e 100644 --- a/src/Umbraco.TestData/SegmentTestController.cs +++ b/src/Umbraco.TestData/SegmentTestController.cs @@ -30,9 +30,9 @@ 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.Variations = ct.Variations.SetVariesBy(ContentVariation.Segment); + + propType.Variations = propType.Variations.SetVariesBy(ContentVariation.Segment); Services.ContentTypeService.Save(ct); return Content($"The document type {alias} and property type {propertyTypeAlias} now allows segments"); @@ -50,7 +50,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.Variations = ct.Variations.SetVariesBy(ContentVariation.Nothing); Services.ContentTypeService.Save(ct); return Content($"The document type {alias} no longer allows segments"); From 1cc6a8cb4511c74ad5d5a96fed9ca1805bf7831a Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Wed, 22 Jan 2020 11:14:31 +0100 Subject: [PATCH 81/85] Rename SetVariesBy to SetFlag on the enum overload --- src/Umbraco.Core/ContentVariationExtensions.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs index 6430c75686..29442b78e6 100644 --- a/src/Umbraco.Core/ContentVariationExtensions.cs +++ b/src/Umbraco.Core/ContentVariationExtensions.cs @@ -234,7 +234,7 @@ namespace Umbraco.Core /// /// 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.SetVariesBy(variation, value); + 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. @@ -245,7 +245,7 @@ namespace Umbraco.Core /// /// This method does not support setting the variation to nothing. /// - public static void SetVariesBy(this PropertyType propertyType, ContentVariation variation, bool value = true) => propertyType.Variations = propertyType.Variations.SetVariesBy(variation, value); + public static void SetVariesBy(this PropertyType 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. @@ -259,7 +259,7 @@ namespace Umbraco.Core /// /// This method does not support setting the variation to nothing. /// - public static ContentVariation SetVariesBy(this ContentVariation variations, ContentVariation variation, bool value = true) + public static ContentVariation SetFlag(this ContentVariation variations, ContentVariation variation, bool value = true) { return value ? variations | variation // Set flag using bitwise logical OR From d61090a225c9e99606f096c18971e5beb5c6c9a7 Mon Sep 17 00:00:00 2001 From: Dave Woestenborghs Date: Tue, 14 Jan 2020 14:02:10 +0100 Subject: [PATCH 82/85] #7452 set save state correctly for variant when setting schedule publishing --- .../src/common/directives/components/content/edit.controller.js | 1 + 1 file changed, 1 insertion(+) 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"; From 201927580c299b223c5eb17f798e1aaa263944aa Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 23 Jan 2020 16:23:27 +1100 Subject: [PATCH 83/85] Fixing accidental api signature breaking change --- .../Persistence/SqlSyntax/SqlServerSyntaxProvider.cs | 6 +++--- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 11 ++++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs index 6dda49cd5e..bb50fa98a1 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs @@ -251,10 +251,10 @@ where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName) public override void WriteLock(IDatabase db, params int[] lockIds) { - WriteLock(db, 1800, lockIds); + WriteLock(db, TimeSpan.FromMilliseconds(1800), lockIds); } - public void WriteLock(IDatabase db, int millisecondsTimeout, params int[] lockIds) + public void WriteLock(IDatabase db, TimeSpan timeout, params int[] lockIds) { // soon as we get Database, a transaction is started @@ -265,7 +265,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 {millisecondsTimeout};"); + 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.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index 31f8b36ed0..418e4c13fc 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -39,6 +39,11 @@ namespace Umbraco.Core.Runtime 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(); @@ -52,7 +57,7 @@ namespace Umbraco.Core.Runtime try { // wait to get a write lock - _sqlServerSyntax.WriteLock(db, millisecondsTimeout, Constants.Locks.MainDom); + _sqlServerSyntax.WriteLock(db, TimeSpan.FromMilliseconds(millisecondsTimeout), Constants.Locks.MainDom); } catch (Exception ex) { @@ -136,7 +141,7 @@ namespace Umbraco.Core.Runtime db.BeginTransaction(IsolationLevel.ReadCommitted); // get a read lock - _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); + _dbFactory.SqlContext.SqlSyntax.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 @@ -209,7 +214,7 @@ namespace Umbraco.Core.Runtime db.BeginTransaction(IsolationLevel.ReadCommitted); // get a read lock - _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); + _dbFactory.SqlContext.SqlSyntax.ReadLock(db, Constants.Locks.MainDom); // the row var mainDomRows = db.Fetch("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); From 8d7ed7dd32b6ff0bb415d126b56f4c0a57b49491 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 23 Jan 2020 16:26:13 +1100 Subject: [PATCH 84/85] adds const fixing naming --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index 418e4c13fc..37db2899c6 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -17,6 +17,7 @@ namespace Umbraco.Core.Runtime { 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(); @@ -197,7 +198,7 @@ namespace Umbraco.Core.Runtime /// private Task WaitForExistingAsync(string tempId, int millisecondsTimeout) { - var updatedTempId = tempId + "_updated"; + var updatedTempId = tempId + UpdatedSuffix; return Task.Run(() => { @@ -343,11 +344,11 @@ namespace Umbraco.Core.Runtime private bool IsLockTimeoutException(Exception exception) => exception is SqlException sqlException && sqlException.Number == 1222; #region IDisposable Support - private bool disposedValue = false; // To detect redundant calls + private bool _disposedValue = false; // To detect redundant calls protected virtual void Dispose(bool disposing) { - if (!disposedValue) + if (!_disposedValue) { if (disposing) { @@ -374,7 +375,7 @@ namespace Umbraco.Core.Runtime if (_mainDomChanging) { _logger.Debug("Releasing MainDom, updating row, new application is booting."); - db.Execute("UPDATE umbracoKeyValue SET [value] = [value] + '_updated' WHERE [key] = @key", new { key = MainDomKey }); + db.Execute($"UPDATE umbracoKeyValue SET [value] = [value] + '{UpdatedSuffix}' WHERE [key] = @key", new { key = MainDomKey }); } else { @@ -396,7 +397,7 @@ namespace Umbraco.Core.Runtime } } - disposedValue = true; + _disposedValue = true; } } From 52b93bfc9c0c11a1225383681de5f6a716b67730 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 23 Jan 2020 18:33:55 +1100 Subject: [PATCH 85/85] ensures only _sqlServerSyntax is used --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index 37db2899c6..4433a8e307 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -142,7 +142,7 @@ namespace Umbraco.Core.Runtime db.BeginTransaction(IsolationLevel.ReadCommitted); // get a read lock - _dbFactory.SqlContext.SqlSyntax.ReadLock(db, Constants.Locks.MainDom); + _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 @@ -215,7 +215,7 @@ namespace Umbraco.Core.Runtime db.BeginTransaction(IsolationLevel.ReadCommitted); // get a read lock - _dbFactory.SqlContext.SqlSyntax.ReadLock(db, Constants.Locks.MainDom); + _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); // the row var mainDomRows = db.Fetch("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey });