From 7a7b8461c51715f7e0fccb6f2c405ed5c117c26f Mon Sep 17 00:00:00 2001 From: fg-gsc <161019175+fg-gsc@users.noreply.github.com> Date: Wed, 28 Feb 2024 16:54:50 +0100 Subject: [PATCH 01/18] Added the possibility to select all the cultures at once in the save and save&publish dialogs (#15776) --- .../EmbeddedResources/Lang/en.xml | 1 + .../EmbeddedResources/Lang/en_us.xml | 1 + .../content/overlays/publish.controller.js | 21 ++++++++++- .../src/views/content/overlays/publish.html | 13 ++++++- .../overlays/publishdescendants.controller.js | 18 +++++++++- .../content/overlays/publishdescendants.html | 11 ++++++ .../views/content/overlays/save.controller.js | 16 +++++++++ .../src/views/content/overlays/save.html | 11 ++++++ .../overlays/sendtopublish.controller.js | 36 +++++++++++++++++-- .../views/content/overlays/sendtopublish.html | 11 ++++++ 10 files changed, 134 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml index 0000f92695..b344ed784a 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml @@ -336,6 +336,7 @@ Send for approval is not allowed Schedule is not allowed Unpublish is not allowed + Select all variants %0%]]> diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index 608c6943ea..58a6f8b704 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -334,6 +334,7 @@ Send for approval is not allowed Schedule is not allowed Unpublish is not allowed + Select all variants %0%]]> diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js index e2fe85c323..0bd4f8e568 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js @@ -6,8 +6,10 @@ var vm = this; vm.loading = true; vm.isNew = true; + vm.publishAll = false; vm.changeSelection = changeSelection; + vm.changePublishAllSelection = changePublishAllSelection; function allowPublish (variant) { return variant.allowedActions.includes("U"); @@ -38,6 +40,20 @@ $scope.model.disableSubmitButton = !canPublish(); //need to set the Save state to same as publish. variant.save = variant.publish; + //update publishAll checkbox value + updatePublishAllSelectionStatus(); + } + + function changePublishAllSelection(){ + vm.availableVariants.forEach(variant => { + variant.publish = vm.publishAll; + variant.save = variant.publish; + }); + $scope.model.disableSubmitButton = !canPublish(); + } + + function updatePublishAllSelectionStatus(){ + vm.publishAll = vm.availableVariants.every(x => x.publish); } function hasAnyDataFilter(variant) { @@ -138,7 +154,7 @@ vm.availableVariants = vm.variants.filter(publishableVariantFilter); vm.missingMandatoryVariants = vm.variants.filter(notPublishableButMandatoryFilter); - + // if any active varaiant that is available for publish, we set it to be published: vm.availableVariants.forEach(v => { if(v.active && allowPublish(v)) { @@ -152,6 +168,9 @@ $scope.model.disableSubmitButton = !canPublish(); + //update publishAll checkbox value + updatePublishAllSelectionStatus(); + const localizeKey = vm.missingMandatoryVariants.length > 0 ? 'content_notReadyToPublish' : !$scope.model.title ? 'content_readyToPublish' : ''; diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html index 56ca5ddedf..8605d57afb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html @@ -3,7 +3,7 @@
- +
@@ -12,6 +12,17 @@

+
+ + + Select all variants + +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.controller.js index da8ee2d3a8..1b4c16b28f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.controller.js @@ -5,9 +5,11 @@ var vm = this; vm.includeUnpublished = $scope.model.includeUnpublished || false; + vm.publishAll = false; vm.changeSelection = changeSelection; vm.toggleIncludeUnpublished = toggleIncludeUnpublished; + vm.changePublishAllSelection = changePublishAllSelection; function onInit() { @@ -48,8 +50,10 @@ active.publish = active.save = true; } - $scope.model.disableSubmitButton = !canPublish(); + updatePublishAllSelectionStatus(); + $scope.model.disableSubmitButton = !canPublish(); + } else { // localize help text for invariant content vm.labels.help = { @@ -97,8 +101,20 @@ $scope.model.disableSubmitButton = !canPublish(); //need to set the Save state to true if publish is true variant.save = variant.publish; + updatePublishAllSelectionStatus(); } + function changePublishAllSelection(){ + vm.displayVariants.forEach(variant => { + variant.publish = vm.publishAll; + variant.save = variant.publish; + }); + $scope.model.disableSubmitButton = !canPublish(); + } + + function updatePublishAllSelectionStatus(){ + vm.publishAll = vm.displayVariants.every(x => x.publish); + } function isMandatoryFilter(variant) { //determine a variant is 'dirty' (meaning it will show up as publish-able) if it's diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html index 54a0e9c6df..cbd78fb2d5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html @@ -38,6 +38,17 @@
+
+ + + Select all variants + +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.controller.js index 308673bf21..1581e5ce1c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.controller.js @@ -7,14 +7,28 @@ vm.loading = true; vm.hasPristineVariants = false; vm.isNew = true; + vm.saveAll = false; vm.changeSelection = changeSelection; + vm.changeSaveAllSelection = changeSaveAllSelection; function changeSelection(variant) { var firstSelected = _.find(vm.variants, function (v) { return v.save; }); $scope.model.disableSubmitButton = !firstSelected; //disable submit button if there is none selected + updateSaveAllSelectionStatus(); + } + + function changeSaveAllSelection(){ + vm.availableVariants.forEach(variant => { + variant.save = vm.saveAll; + }); + $scope.model.disableSubmitButton = !vm.saveAll; + } + + function updateSaveAllSelectionStatus(){ + vm.saveAll = vm.availableVariants.every(x => x.save); } function allowUpdate (variant) { @@ -92,6 +106,8 @@ vm.availableVariants = contentEditingHelper.getSortedVariantsAndSegments(vm.availableVariants); + updateSaveAllSelectionStatus(); + } else { //disable save button if we have nothing to save $scope.model.disableSubmitButton = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html index 9153ae1650..1b564cb0b3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html @@ -13,6 +13,17 @@
+
+ + + Select all variants + +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.controller.js index c1c91057da..ce04d20ace 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.controller.js @@ -6,8 +6,10 @@ var vm = this; vm.loading = true; vm.selectedVariants = []; + vm.sendToPublishAll = false; vm.changeSelection = changeSelection; + vm.changeSendToPublishAllSelection = changeSendToPublishAllSelection; function onInit() { @@ -29,8 +31,9 @@ if (vm.availableVariants.length !== 0) { vm.availableVariants = contentEditingHelper.getSortedVariantsAndSegments(vm.availableVariants); + updateSendToPublishAllSelectionStatus(); } - + $scope.model.disableSubmitButton = true; vm.loading = false; } @@ -55,10 +58,39 @@ let firstSelected = vm.variants.find(v => v.save); $scope.model.disableSubmitButton = !firstSelected; + updateSendToPublishAllSelectionStatus(); + } + + function changeSendToPublishAllSelection(){ + + vm.availableVariants.forEach(variant => { + + variant.publish = vm.sendToPublishAll; + let foundVariant = vm.selectedVariants.find(x => x.compositeId === variant.compositeId); + + if (foundVariant === undefined) { + variant.save = true; + vm.selectedVariants.push(variant); + } else { + if(!vm.sendToPublishAll){ + variant.save = false; + let index = vm.selectedVariants.indexOf(foundVariant); + if (index !== -1) { + vm.selectedVariants.splice(index, 1); + } + } + } + }); + let firstSelected = vm.variants.find(v => v.save); + $scope.model.disableSubmitButton = !firstSelected; + } + + function updateSendToPublishAllSelectionStatus(){ + vm.sendToPublishAll = vm.availableVariants.every(x => x.publish); } - function isMandatoryFilter(variant) { + function isMandatoryFilter(variant) { //determine a variant is 'dirty' (meaning it will show up as publish-able) if it's // * has a mandatory language // * without having a segment, segments cant be mandatory at current state of code. diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.html index b9061a8eb6..4d51ae1c30 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.html @@ -10,6 +10,17 @@
+
+ + + Select all variants + +
+
From beb431900a667deb3eb777aebcfd6ff6e4904b01 Mon Sep 17 00:00:00 2001 From: Owain Jones Date: Fri, 23 Feb 2024 17:34:53 +0000 Subject: [PATCH 02/18] Update Welsh lang file --- .../EmbeddedResources/Lang/cy.xml | 77 ++++++++++++++++++- 1 file changed, 75 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/cy.xml b/src/Umbraco.Core/EmbeddedResources/Lang/cy.xml index 1e76e92382..c91dabbad9 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/cy.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/cy.xml @@ -459,9 +459,10 @@ Ydych chi'n sicr? Ydych chi'n sicr? Torri - Golygu Eitem Geiriadur - Golygu Iaith + Golygu Eitem Geiriadur + Golygu Iaith Golygu cyfrwng a dewiswyd + Golygu bachyn gwe Mewnosod dolen leol Mewnosod nod Mewnosod pennawd graffig @@ -504,6 +505,7 @@ Agor y ddolen ddogfen mewn ffenestr neu tab newydd Dolen i gyfrwng Dewis nod cychwyn cynnwys + Dewis digwyddiad Dewis cyfrwng Dewis y math o gyfrwng Dewis eicon @@ -857,6 +859,7 @@ Gwybodaeth Umbraco Neidio i'r dewislen Neidio i'r cynnwys + Prif Glas @@ -1061,6 +1064,7 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang Mewngofnodwch isod Mewngofnodwch gyda Sesiwn wedi cyrraedd terfyn amser + Wps! Mae mewngofnodi wedi methu. Gwiriwch eich manylion a thrio eto. © 2001 - %0%
Umbraco.com

]]>
Wedi anghofio eich cyfrinair? Bydd ebost yn cael ei anfon i'r cyfeiriad darparwyd gyda dolen i ailosod eich cyfrinair @@ -1424,6 +1428,44 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang Gallwch ond ddewis eitemau o'r math(au): %0% Rydych wedi dewis eitem gynnwys sydd naill ai wedi'i ddileu neu yn y bin ailgylchu Rydych wedi dewis eitemau gynnwys sydd naill ai wedi'u dileu neu yn y bin ailgylchu + Nodi gwraidd + Dewiswch nod gwraidd + Nodi gwraidd efo XPath + Nodi Gwraidd Dynamig + Nod cychwyn + Ymholiad XPath + + + Ymholiad Gwraidd Dynamig + Dewiswch darddiad + Diffiniwch darddiad eich Ymholiad Gwraidd Dynamig + Gwraidd + Nod gwraidd y sesiwn olygu hon + Rhiant + Nod rhiant y ffynhonnell yn y sesiwn olygu hon + Cyfredol + Y nod cynnwys sy'n ffynhonnell ar gyfer y sesiwn olygu hon + Gwefan + Canfod nod agosaf gydag enw gwesteiwr + Nod penodol + Dewiswch Nod penodol fel tarddiad yr ymholiad hwn + Atodwch y cam i'r ymholiad + Diffiniwch y cam nesaf yn eich Ymholiad Gwraidd Dynamig + Hynafiad Agosaf Neu Hunan + Holwch yr hynafiad agosaf neu hunan sy'n cyd-fynd ag un o'r mathau sydd wedi'u ffurfweddu + Hynafiad Pellaf Neu Hunan + Holwch yr hynafiad pellaf neu'r hunan sy'n cyd-fynd ag un o'r mathau sydd wedi'u ffurfweddu + Disgynnydd Agosaf Neu Hunan + Holwch y disgynnydd agosaf neu'r hunan sy'n cyd-fynd ag un o'r mathau sydd wedi'u ffurfweddu + Disgynnydd Pellaf Neu Hunan + Holwch y disgynnydd pellaf neu'r hunan sy'n cyd-fynd ag un o'r mathau sydd wedi'u ffurfweddu + Arferiad + Ymholiad gan ddefnyddio Cam Ymholiad arferiad + Ychwanegu cam ymholiad + Sy'n cyfateb i'r mathau: + Dim cynnwys cyfatebol + Nid yw ffurfweddiad yr eiddo hwn yn cyfateb i unrhyw gynnwys. Creu'r cynnwys coll neu cysylltwch â'ch gweinyddwr i addasu gosodiadau Gwraidd Dynamig ar gyfer yr eiddo hwn. + Canslo a chlirio ymholiad Rydych wedi dewis eitem gyfrwng sydd naill ai wedi'i ddileu neu yn y bin ailgylchu @@ -1629,6 +1671,7 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang Digwyddodd gwall wrth analluogi glanhau fersiwn ar gyfer %0% Mae gwybodaeth eich system wedi'i chopïo'n llwyddiannus i'r clipfwrdd Methu â chopïo gwybodaeth eich system i'r clipfwrdd + Bachyn gwe wedi arbed Ychwanegu ardull @@ -1873,6 +1916,33 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang NODYN! Mae glanhau fersiynau cynnwys hanesyddol wedi'u hanalluogi'n fyd-eang. Ni fydd y gosodiadau hyn yn dod i rym cyn iddo gael ei alluogi. Mae newid math o ddata gyda gwerthoedd storio wedi'i analluogi. I ganiatáu hyn gallwch newid y gosodiad Umbraco:CMS:DataTypes:CanBeChanged yn appssettings.json. + + Creu bachyn gwe + Ychwanegu pennyn bachyn gwe + Ychwanegu Math o Ddogfen + Ychwanegu Math o Gyfrwng + Creu pennawd + Danfoniadau + Nid oes penawdau bachyn gwe wedi'u hychwanegu + Ni chanfuwyd unrhyw ddigwyddiadau. + Wedi'i alluogi + DigwyddiadauD + Digwyddiad + Url + Mathau + Allwedd bachyn gwe + Cyfrif o Ailgeision + Togl modd dadfygio am ragor o wybodaeth. + Cod statws ddim yn OK + Yr url i'w alw pan fydd y bachyn gwe yn cael ei sbarduno. + Y digwyddiadau y dylid sbarduno'r bachyn gwe. + Sbardunwch y bachyn gwe am fath penodol o gynnwys yn unig. + A yw'r bachyn gwe wedi'i alluogi? + Penawdau arferu i'w cynnwys yn y cais bachyn gwe. + Math o Gynnwys + Penawdau + Dewiswch ddigwyddiad yn gyntaf. + Ychwanegu iaith Iath gorfodol @@ -2011,6 +2081,7 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang Gosodiadau Templedi Trydydd parti + Bachau gwe Diweddariad newydd yn barod @@ -2769,6 +2840,8 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang Ffurfweddu ardal Dileu ardal Ychwanegu opsiwn rhychwantu %0% colofn + Mewnosod Bloc + Arddangos yn fewnol â'r testun Beth yw Templedi Gynnwys From 4f9c793d0022731774a154a9cef4706069b3f8c0 Mon Sep 17 00:00:00 2001 From: MihailichenkoPavel Date: Thu, 29 Feb 2024 13:36:43 +0200 Subject: [PATCH 03/18] fix user login --- .../src/context/auth.repository.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Web.UI.Login/src/context/auth.repository.ts b/src/Umbraco.Web.UI.Login/src/context/auth.repository.ts index 69743bcee7..f0ec89075d 100644 --- a/src/Umbraco.Web.UI.Login/src/context/auth.repository.ts +++ b/src/Umbraco.Web.UI.Login/src/context/auth.repository.ts @@ -25,26 +25,20 @@ export class UmbAuthRepository { }); const response = await fetch(request); - const responseData: LoginResponse = { - status: response.status, - }; - - if (!response.ok) { - responseData.error = await this.#getErrorText(response); - return responseData; - } + let responseData: any = undefined; try { const text = await response.text(); if (text) { - responseData.data = JSON.parse(this.#removeAngularJSResponseData(text)); + responseData = JSON.parse(this.#removeAngularJSResponseData(text)); } } catch { } return { status: response.status, - data: responseData?.data, + error: response.ok ? undefined : await this.#getErrorText(response), + data: responseData, twoFactorView: responseData?.twoFactorView, }; } catch (error) { From 0cf436fa5cb8db5edc92d6b058841fee9d6adfbb Mon Sep 17 00:00:00 2001 From: Ethan Nagano Date: Sat, 16 Dec 2023 22:45:08 -0800 Subject: [PATCH 04/18] Add handling of failed publish. When publishing, if the selected page does not yet have a page created, handle the FailedPublishNothingToPublish error so that the user is not presented with an exception. A warning is shown to the user that some languages failed to publish due to nothing to publish and to check that a page has been created for selected languages. Additionally fixed a validation issue where publish would always succeed if all languages were selected. Issue: 15352 --- .../Controllers/ContentController.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index a1ed75332b..81b37bb108 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -2061,7 +2061,7 @@ public class ContentController : ContentControllerBase var languageCount = _allLangs.Value.Count(); // If there is no culture specified or the cultures specified are equal to the total amount of languages, publish the content in all cultures. - if (model.Cultures == null || !model.Cultures.Any() || model.Cultures.Length == languageCount) + if (model.Cultures == null || !model.Cultures.Any()) { return PostPublishById(model.Id); } @@ -2322,7 +2322,7 @@ public class ContentController : ContentControllerBase } var languageCount = _allLangs.Value.Count(); - if (model.Cultures?.Length == 0 || model.Cultures?.Length == languageCount) + if (model.Cultures?.Length == 0) { //this means that the entire content item will be unpublished PublishResult unpublishResult = _contentService.Unpublish(foundContent, userId: _backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId().Result ?? -1); @@ -2786,6 +2786,7 @@ public class ContentController : ContentControllerBase case PublishResultType.FailedPublishIsTrashed: case PublishResultType.FailedPublishContentInvalid: case PublishResultType.FailedPublishMandatoryCultureMissing: + case PublishResultType.FailedPublishNothingToPublish: //the rest that we are looking for each belong in their own group return x.Result; default: @@ -2928,6 +2929,11 @@ public class ContentController : ContentControllerBase _localizedTextService.Localize(null, "publish"), "publish/contentPublishedFailedByCulture"); break; + case PublishResultType.FailedPublishNothingToPublish: + display.AddWarningNotification( + _localizedTextService.Localize(null, "publish"), + $"Nothing to publish for some languages. Ensure selected languages have a page created."); + break; default: throw new IndexOutOfRangeException($"PublishedResultType \"{status.Key}\" was not expected."); } From 21126b7ba6b95ea10e4099633ee0e4f4fcd1cee9 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 4 Mar 2024 12:50:24 +0100 Subject: [PATCH 05/18] Localize validation messages with user lang files (#15820) * Localize validation messages with user lang files * Fixed unit tests --- .../Mapping/ContentPropertyDisplayMapper.cs | 2 ++ .../Services/PropertyValidationService.cs | 20 +++++++++++++++++-- .../Services/ContentServiceTests.cs | 3 ++- .../Umbraco.Core/Models/VariationTests.cs | 4 +++- .../PropertyValidationServiceTests.cs | 3 ++- 5 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/Models/Mapping/ContentPropertyDisplayMapper.cs b/src/Umbraco.Core/Models/Mapping/ContentPropertyDisplayMapper.cs index 237f1f2f40..a3c16ba148 100644 --- a/src/Umbraco.Core/Models/Mapping/ContentPropertyDisplayMapper.cs +++ b/src/Umbraco.Core/Models/Mapping/ContentPropertyDisplayMapper.cs @@ -86,5 +86,7 @@ internal class ContentPropertyDisplayMapper : ContentPropertyBasicMapper()) + { + } + + public PropertyValidationService( + PropertyEditorCollection propertyEditors, + IDataTypeService dataTypeService, + ILocalizedTextService textService, + IValueEditorCache valueEditorCache, + ICultureDictionary cultureDictionary) { _propertyEditors = propertyEditors; _dataTypeService = dataTypeService; _textService = textService; _valueEditorCache = valueEditorCache; + _cultureDictionary = cultureDictionary; } /// @@ -76,13 +92,13 @@ public class PropertyValidationService : IPropertyValidationService if (isRequired && !string.IsNullOrWhiteSpace(isRequiredMessage) && requiredDefaultMessages.Contains(validationResult.ErrorMessage, StringComparer.OrdinalIgnoreCase)) { - validationResult.ErrorMessage = isRequiredMessage; + validationResult.ErrorMessage = _textService.UmbracoDictionaryTranslate(_cultureDictionary, isRequiredMessage); } if (!string.IsNullOrWhiteSpace(validationRegExp) && !string.IsNullOrWhiteSpace(validationRegExpMessage) && formatDefaultMessages.Contains(validationResult.ErrorMessage, StringComparer.OrdinalIgnoreCase)) { - validationResult.ErrorMessage = validationRegExpMessage; + validationResult.ErrorMessage = _textService.UmbracoDictionaryTranslate(_cultureDictionary, validationRegExpMessage); } yield return validationResult; diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs index aba329404a..a624d6d885 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs @@ -7,6 +7,7 @@ using Moq; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Dictionary; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; @@ -1195,7 +1196,7 @@ public class ContentServiceTests : UmbracoIntegrationTestWithContent // content cannot publish values because they are invalid var propertyValidationService = new PropertyValidationService(PropertyEditorCollection, DataTypeService, - TextService, ValueEditorCache); + TextService, ValueEditorCache, Mock.Of()); var isValid = propertyValidationService.IsPropertyDataValid(content, out var invalidProperties, CultureImpact.Invariant); Assert.IsFalse(isValid); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/VariationTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/VariationTests.cs index ca2ae76428..06edf59d3e 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/VariationTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/VariationTests.cs @@ -5,6 +5,7 @@ using Moq; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Dictionary; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; @@ -615,6 +616,7 @@ public class VariationTests propertyEditorCollection, dataTypeService, localizedTextService, - new ValueEditorCache()); + new ValueEditorCache(), + Mock.Of()); } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/PropertyValidationServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/PropertyValidationServiceTests.cs index 736e124324..160e9e7caa 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/PropertyValidationServiceTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/PropertyValidationServiceTests.cs @@ -6,6 +6,7 @@ using Moq; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Dictionary; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.PropertyEditors; @@ -51,7 +52,7 @@ public class PropertyValidationServiceTests var propEditors = new PropertyEditorCollection(new DataEditorCollection(() => new[] { dataEditor })); - validationService = new PropertyValidationService(propEditors, dataTypeService.Object, Mock.Of(), new ValueEditorCache()); + validationService = new PropertyValidationService(propEditors, dataTypeService.Object, Mock.Of(), new ValueEditorCache(), Mock.Of()); } [Test] From ca9a3bbf1b06c44e14c9aa51e0ec2b20561e53f3 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 4 Mar 2024 13:17:38 +0100 Subject: [PATCH 06/18] Use same cache level for MNTP no matter the output channel (#15828) --- .../ValueConverters/MultiNodeTreePickerValueConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs index 7d512fbb2b..3ddb98b40e 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs @@ -181,7 +181,7 @@ public class MultiNodeTreePickerValueConverter : PropertyValueConverterBase, IDe return source; } - public PropertyCacheLevel GetDeliveryApiPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Elements; + public PropertyCacheLevel GetDeliveryApiPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot; public PropertyCacheLevel GetDeliveryApiPropertyCacheLevelForExpansion(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot; From 3b4ca0b0f5d2a4ed702bca0f2a5ee840e250d6c7 Mon Sep 17 00:00:00 2001 From: Steve Morgan Date: Tue, 5 Mar 2024 10:13:44 +0000 Subject: [PATCH 07/18] Media Picker - upload button disabled bug #13383 (#15115) --- .../mediapicker/mediapicker.controller.js | 92 ++++++++++--------- 1 file changed, 49 insertions(+), 43 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js index ce6a09fa3f..b3d3bc2dc3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js @@ -103,43 +103,43 @@ angular.module("umbraco") $scope.target = dialogOptions.currentTarget; } - function setTitle(data) { - if (!$scope.model.title) - $scope.model.title = data[0]; - } - - function setNavigation(data) { - if (!vm.navigation.length) { - vm.navigation = [{ - "alias": "empty", - "name": data[0], - "icon": "icon-umb-media", - "active": true, - "view": "" - }]; - if (vm.clipboardItems) { - vm.navigation.push({ - "alias": "clipboard", - "name": data[1], - "icon": "icon-paste-in", - "view": "", - "disabled": vm.clipboardItems.length === 0 - }); + function setTitle(data) { + if (!$scope.model.title) + $scope.model.title = data[0]; + } + + function setNavigation(data) { + if (!vm.navigation.length) { + vm.navigation = [{ + "alias": "empty", + "name": data[0], + "icon": "icon-umb-media", + "active": true, + "view": "" + }]; + if (vm.clipboardItems) { + vm.navigation.push({ + "alias": "clipboard", + "name": data[1], + "icon": "icon-paste-in", + "view": "", + "disabled": vm.clipboardItems.length === 0 + }); + } + vm.activeTab = vm.navigation[0]; + } } - vm.activeTab = vm.navigation[0]; - } - } - function onInit() { + function onInit() { - localizationService.localizeMany(["defaultdialogs_selectMedia", "mediaPicker_tabClipboard"]) - .then(function (localizationResult) { - setTitle(localizationResult); - setNavigation(localizationResult); - }); - + localizationService.localizeMany(["defaultdialogs_selectMedia", "mediaPicker_tabClipboard"]) + .then(function (localizationResult) { + setTitle(localizationResult); + setNavigation(localizationResult); + }); + userService.getCurrentUser().then(function (userData) { userStartNodes = userData.startMediaIds; @@ -264,18 +264,24 @@ angular.module("umbraco") function (f) { return f.path.indexOf($scope.startNodeId) !== -1; }); + folder.path = $scope.path[0].path; + performGotoFolder(folder); }); } else { $scope.path = []; + performGotoFolder(folder); } + } + function performGotoFolder(folder) { mediaTypeHelper.getAllowedImagetypes(folder.id).then(function (types) { vm.acceptedMediatypes = types; }); + $scope.lockedFolder = (folder.id === -1 && $scope.model.startNodeIsVirtual) || hasFolderAccess(folder) === false; $scope.currentFolder = folder; localStorageService.set("umbLastOpenedMediaNodeId", folder.id); - return getChildren(folder.id); + getChildren(folder.id); } function toggleListView() { @@ -431,9 +437,9 @@ angular.module("umbraco") }; function onNavigationChanged(tab) { - vm.activeTab.active = false; - vm.activeTab = tab; - vm.activeTab.active = true; + vm.activeTab.active = false; + vm.activeTab = tab; + vm.activeTab.active = true; }; function clickClearClipboard() { @@ -490,7 +496,7 @@ angular.module("umbraco") if (data.items) { var allowedTypes = dialogOptions.filter ? dialogOptions.filter.split(",") : null; - data.items.forEach(function(mediaItem) { + data.items.forEach(function (mediaItem) { setMediaMetaData(mediaItem); mediaItem.filtered = allowedTypes && allowedTypes.indexOf(mediaItem.metaData.ContentTypeAlias) < 0; }); @@ -575,12 +581,12 @@ angular.module("umbraco") if (item.metaData.MediaPath !== null) { item.thumbnail = mediaHelper.resolveFileFromEntity(item, true); item.image = mediaHelper.resolveFileFromEntity(item, false); - } - if (item.metaData.UpdateDate !== null) { - userService.getCurrentUser().then(currentUser => { - item.updateDate = dateHelper.getLocalDate(item.metaData.UpdateDate, currentUser.locale, "LLL"); - }); - + } + if (item.metaData.UpdateDate !== null) { + userService.getCurrentUser().then(currentUser => { + item.updateDate = dateHelper.getLocalDate(item.metaData.UpdateDate, currentUser.locale, "LLL"); + }); + } } From 7dff3a37553ad1003e8422872e17851b73321586 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Wed, 6 Mar 2024 11:01:37 +0100 Subject: [PATCH 08/18] bump version --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 44ba7dcf8f..3c26ae287d 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "13.2.0-rc2", + "version": "13.2.0", "assemblyVersion": { "precision": "build" }, From b86eeb6b520e7e5003e367f0cb875973b28e0a0c Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Thu, 7 Mar 2024 09:54:14 +0100 Subject: [PATCH 09/18] bumb version --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 3c26ae287d..f70819e8b4 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "13.2.0", + "version": "13.3.0-rc", "assemblyVersion": { "precision": "build" }, From 90820483d5c009cc7075c7f1a641dae783b47f86 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> Date: Fri, 8 Mar 2024 08:36:00 +0100 Subject: [PATCH 10/18] Try to parse string as int and return id.ToString(CultureInfo.InvariantCulture) (#15848) --- src/Umbraco.Examine.Lucene/DeliveryApiContentIndex.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Examine.Lucene/DeliveryApiContentIndex.cs b/src/Umbraco.Examine.Lucene/DeliveryApiContentIndex.cs index a3508a3fed..c572fc85f7 100644 --- a/src/Umbraco.Examine.Lucene/DeliveryApiContentIndex.cs +++ b/src/Umbraco.Examine.Lucene/DeliveryApiContentIndex.cs @@ -1,9 +1,9 @@ +using System.Globalization; using Examine; using Examine.Lucene; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Umbraco.Cms.Core; using Umbraco.Cms.Core.DeliveryApi; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Hosting; @@ -125,8 +125,14 @@ public class DeliveryApiContentIndex : UmbracoExamineIndex private (string? ContentId, string? Culture) ParseItemId(string id) { + if (int.TryParse(id, out _)) + { + return (id, null); + } + DeliveryApiIndexCompositeIdModel compositeIdModel = _deliveryApiCompositeIdHandler.Decompose(id); - return (compositeIdModel.Id.ToString() ?? id, compositeIdModel.Culture); + + return (compositeIdModel.Id?.ToString(CultureInfo.InvariantCulture), compositeIdModel.Culture); } protected override void OnTransformingIndexValues(IndexingItemEventArgs e) From ed517ecd861fca7d64193db8a1f6e104aeb1e505 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 14 Mar 2024 13:31:30 +0000 Subject: [PATCH 11/18] Update Imagesharp (#15885) --- src/Directory.Packages.props | 103 +++++++++++++++++------------------ 1 file changed, 50 insertions(+), 53 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index a906a587ef..4d6bf155d3 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -4,67 +4,64 @@ true NU1507 - - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - + + + + + - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - + + + + + + + - - - + + + - - + \ No newline at end of file From 659e74d831e6646321f48dda21f97f026b173e53 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 15 Mar 2024 08:08:27 +0100 Subject: [PATCH 12/18] Update imagesharp 3 --- src/Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 152f7cb91e..15801621da 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -56,7 +56,7 @@ - + From 0e43c43609007786fd82e8993ed109f19e18bdee Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 15 Mar 2024 08:09:39 +0100 Subject: [PATCH 13/18] Update imagesharp 3 --- src/Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 15801621da..b149d1d611 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -56,8 +56,8 @@ - - + + From 7585918821f6e0e2f1c40a478736ac1b8a8b1a7e Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 15 Mar 2024 08:20:50 +0100 Subject: [PATCH 14/18] Revert ImageSharp 3 upgrade due to breaking changes --- src/Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index b149d1d611..15801621da 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -56,8 +56,8 @@ - - + + From d82e262e139538f5bff26199791c24c9e6af0ed2 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 15 Mar 2024 08:22:53 +0100 Subject: [PATCH 15/18] Update imagesharp 3 --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index c80183b9c1..02dbaba948 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -73,7 +73,7 @@ - + @@ -90,4 +90,4 @@ - \ No newline at end of file + From e03ff2661b9bbd5c36e79dda092616f85039ad72 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 15 Mar 2024 09:35:19 +0100 Subject: [PATCH 16/18] Ignore test of lazy locks --- .../Umbraco.Infrastructure/Persistence/LocksTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs index 88b48dfaae..8ae6818aeb 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs @@ -125,6 +125,7 @@ public class LocksTests : UmbracoIntegrationTest } } + [NUnit.Framework.Ignore("We currently do not have a way to force lazy locks")] [Test] public void GivenNonEagerLocking_WhenNoDbIsAccessed_ThenNoSqlIsExecuted() { From 3718a9cdd48ea4e57024b4c433cf2c55a2e91c53 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 15 Mar 2024 09:47:18 +0100 Subject: [PATCH 17/18] Revert "Ignore test of lazy locks" This reverts commit e03ff2661b9bbd5c36e79dda092616f85039ad72. --- .../Umbraco.Infrastructure/Persistence/LocksTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs index 8ae6818aeb..88b48dfaae 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs @@ -125,7 +125,6 @@ public class LocksTests : UmbracoIntegrationTest } } - [NUnit.Framework.Ignore("We currently do not have a way to force lazy locks")] [Test] public void GivenNonEagerLocking_WhenNoDbIsAccessed_ThenNoSqlIsExecuted() { From 2c23e67c65a98e0573924cd1a55bf03df6e13410 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 15 Mar 2024 09:56:02 +0000 Subject: [PATCH 18/18] Fixing locking issues for document type saves. (#15854) * Added ExecuteNonQuery(DbCommand command) on database to ensure we call OnExecutingCommand and OnExecutedCommand when executing DbCommands * Added Cache Instructions lock, to avoid deadlocks * Optimized read locks for nucache when only one content type is rebuilt * Optimized the SqlServer locks, so only one command is executed (and thereby roundtrip) per lock instead of two * Avoid breaking changes * Cosmetic changes * Take locks if everything is rebuild * Use same lock in scopes, to avoid potential deadlocks between the two * Use eager locks in PublishedSnapshotService.cs * Added timeouts to some of the application locks * Revert "Use eager locks in PublishedSnapshotService.cs" This reverts commit 01873aae978ffa6e6686d253e482c493715e3a96. * Revert "Added Cache Instructions lock, to avoid deadlocks" This reverts commit e3fca7c12a804bb32ca1156b8abd42a957e9dc21. * Use single readlock call to lock many * Use eager locks for reads * Eager write locks * Ignore test of lazy locks * Unique timeout exception messages --------- Co-authored-by: kjac --- .../SqlServerDistributedLockingMechanism.cs | 10 +++-- .../SqliteDistributedLockingMechanism.cs | 2 +- src/Umbraco.Core/Cache/ObjectCacheAppCache.cs | 39 +++++++++++++++---- .../Persistence/IUmbracoDatabase.cs | 4 ++ .../Persistence/UmbracoDatabase.cs | 8 ++++ src/Umbraco.Infrastructure/Scoping/Scope.cs | 26 ++++++------- .../ContentStore.cs | 9 ++++- .../Persistence/NuCacheContentService.cs | 22 +++++++++-- .../SnapDictionary.cs | 9 ++++- .../Persistence/LocksTests.cs | 38 +++++++++++++++--- 10 files changed, 129 insertions(+), 38 deletions(-) diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDistributedLockingMechanism.cs b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDistributedLockingMechanism.cs index 358eae2e38..e0bf4767bf 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDistributedLockingMechanism.cs +++ b/src/Umbraco.Cms.Persistence.SqlServer/Services/SqlServerDistributedLockingMechanism.cs @@ -134,9 +134,10 @@ public class SqlServerDistributedLockingMechanism : IDistributedLockingMechanism const string query = "SELECT value FROM umbracoLock WITH (REPEATABLEREAD) WHERE id=@id"; - db.Execute("SET LOCK_TIMEOUT " + _timeout.TotalMilliseconds + ";"); + var lockTimeoutQuery = $"SET LOCK_TIMEOUT {_timeout.TotalMilliseconds}"; - var i = db.ExecuteScalar(query, new { id = LockId }); + // execute the lock timeout query and the actual query in a single server roundtrip + var i = db.ExecuteScalar($"{lockTimeoutQuery};{query}", new { id = LockId }); if (i == null) { @@ -169,9 +170,10 @@ public class SqlServerDistributedLockingMechanism : IDistributedLockingMechanism const string query = @"UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id"; - db.Execute("SET LOCK_TIMEOUT " + _timeout.TotalMilliseconds + ";"); + var lockTimeoutQuery = $"SET LOCK_TIMEOUT {_timeout.TotalMilliseconds}"; - var i = db.Execute(query, new { id = LockId }); + // execute the lock timeout query and the actual query in a single server roundtrip + var i = db.Execute($"{lockTimeoutQuery};{query}", new { id = LockId }); if (i == 0) { diff --git a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDistributedLockingMechanism.cs b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDistributedLockingMechanism.cs index c3d6f8b29d..9f25f77c2c 100644 --- a/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDistributedLockingMechanism.cs +++ b/src/Umbraco.Cms.Persistence.Sqlite/Services/SqliteDistributedLockingMechanism.cs @@ -154,7 +154,7 @@ public class SqliteDistributedLockingMechanism : IDistributedLockingMechanism try { - var i = command.ExecuteNonQuery(); + var i = db.ExecuteNonQuery(command); if (i == 0) { diff --git a/src/Umbraco.Core/Cache/ObjectCacheAppCache.cs b/src/Umbraco.Core/Cache/ObjectCacheAppCache.cs index dcd83ece94..ff1c9b3bfd 100644 --- a/src/Umbraco.Core/Cache/ObjectCacheAppCache.cs +++ b/src/Umbraco.Core/Cache/ObjectCacheAppCache.cs @@ -9,6 +9,9 @@ namespace Umbraco.Cms.Core.Cache; /// public class ObjectCacheAppCache : IAppPolicyCache, IDisposable { + private static readonly TimeSpan _readLockTimeout = TimeSpan.FromSeconds(5); + private static readonly TimeSpan _writeLockTimeout = TimeSpan.FromSeconds(5); + private readonly ReaderWriterLockSlim _locker = new(LockRecursionPolicy.SupportsRecursion); private bool _disposedValue; @@ -33,7 +36,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable Lazy? result; try { - _locker.EnterReadLock(); + if (_locker.TryEnterReadLock(_readLockTimeout) is false) + { + throw new TimeoutException("Timeout exceeded to the memory cache when getting item"); + } result = MemoryCache.Get(key) as Lazy; // null if key not found } finally @@ -195,7 +201,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable { try { - _locker.EnterWriteLock(); + if (_locker.TryEnterWriteLock(_writeLockTimeout) is false) + { + throw new TimeoutException("Timeout exceeded to the memory cache when clearing item"); + } if (MemoryCache[key] == null) { return; @@ -223,8 +232,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable var isInterface = type.IsInterface; try { - _locker.EnterWriteLock(); - + if (_locker.TryEnterWriteLock(_writeLockTimeout) is false) + { + throw new TimeoutException("Timeout exceeded to the memory cache when clearing by type"); + } // ToArray required to remove foreach (var key in MemoryCache .Where(x => @@ -259,7 +270,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable { try { - _locker.EnterWriteLock(); + if (_locker.TryEnterWriteLock(_writeLockTimeout) is false) + { + throw new TimeoutException("Timeout exceeded to the memory cache when clearing by generic type"); + } Type typeOfT = typeof(T); var isInterface = typeOfT.IsInterface; @@ -296,7 +310,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable { try { - _locker.EnterWriteLock(); + if (_locker.TryEnterWriteLock(_writeLockTimeout) is false) + { + throw new TimeoutException("Timeout exceeded to the memory cache when clearing generic type with predicate"); + } Type typeOfT = typeof(T); var isInterface = typeOfT.IsInterface; @@ -338,7 +355,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable { try { - _locker.EnterWriteLock(); + if (_locker.TryEnterWriteLock(_writeLockTimeout) is false) + { + throw new TimeoutException("Timeout exceeded to the memory cache when clearing with prefix"); + } // ToArray required to remove foreach (var key in MemoryCache @@ -365,7 +385,10 @@ public class ObjectCacheAppCache : IAppPolicyCache, IDisposable try { - _locker.EnterWriteLock(); + if (_locker.TryEnterWriteLock(_writeLockTimeout) is false) + { + throw new TimeoutException("Timeout exceeded to the memory cach when clearing by regex"); + } // ToArray required to remove foreach (var key in MemoryCache diff --git a/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabase.cs b/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabase.cs index 431ddeb5e8..b86648f05c 100644 --- a/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabase.cs +++ b/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabase.cs @@ -1,3 +1,4 @@ +using System.Data.Common; using NPoco; using Umbraco.Cms.Infrastructure.Migrations.Install; @@ -33,4 +34,7 @@ public interface IUmbracoDatabase : IDatabase bool IsUmbracoInstalled(); DatabaseSchemaResult ValidateSchema(); + + /// The number of rows affected. + int ExecuteNonQuery(DbCommand command) => command.ExecuteNonQuery(); } diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs index 425629dc4b..74f8c297ef 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabase.cs @@ -224,6 +224,14 @@ public class UmbracoDatabase : Database, IUmbracoDatabase return databaseSchemaValidationResult ?? new DatabaseSchemaResult(); } + public int ExecuteNonQuery(DbCommand command) + { + OnExecutingCommand(command); + var i = command.ExecuteNonQuery(); + OnExecutedCommand(command); + return i; + } + /// /// Returns true if Umbraco database tables are detected to be installed /// diff --git a/src/Umbraco.Infrastructure/Scoping/Scope.cs b/src/Umbraco.Infrastructure/Scoping/Scope.cs index 000b6a602e..0db9656fc5 100644 --- a/src/Umbraco.Infrastructure/Scoping/Scope.cs +++ b/src/Umbraco.Infrastructure/Scoping/Scope.cs @@ -23,10 +23,9 @@ namespace Umbraco.Cms.Infrastructure.Scoping private readonly bool _autoComplete; private readonly CoreDebugSettings _coreDebugSettings; - private readonly object _dictionaryLocker; private readonly IEventAggregator _eventAggregator; private readonly IsolationLevel _isolationLevel; - private readonly object _lockQueueLocker = new(); + private readonly object _locker = new(); private readonly ILogger _logger; private readonly MediaFileManager _mediaFileManager; private readonly RepositoryCacheMode _repositoryCacheMode; @@ -87,7 +86,6 @@ namespace Umbraco.Cms.Infrastructure.Scoping _scopeFileSystem = scopeFileSystems; _autoComplete = autoComplete; Detachable = detachable; - _dictionaryLocker = new object(); #if DEBUG_SCOPES _scopeProvider.RegisterScope(this); @@ -562,7 +560,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping DisposeLastScope(); } - lock (_lockQueueLocker) + lock (_locker) { _queuedLocks?.Clear(); } @@ -573,24 +571,24 @@ namespace Umbraco.Cms.Infrastructure.Scoping public void EagerReadLock(params int[] lockIds) => EagerReadLockInner(InstanceId, null, lockIds); /// - public void ReadLock(params int[] lockIds) => LazyReadLockInner(InstanceId, lockIds); + public void ReadLock(params int[] lockIds) => EagerReadLockInner(InstanceId, null, lockIds); public void EagerReadLock(TimeSpan timeout, int lockId) => EagerReadLockInner(InstanceId, timeout, lockId); /// - public void ReadLock(TimeSpan timeout, int lockId) => LazyReadLockInner(InstanceId, timeout, lockId); + public void ReadLock(TimeSpan timeout, int lockId) => EagerReadLockInner(InstanceId, timeout, lockId); public void EagerWriteLock(params int[] lockIds) => EagerWriteLockInner(InstanceId, null, lockIds); /// - public void WriteLock(params int[] lockIds) => LazyWriteLockInner(InstanceId, lockIds); + public void WriteLock(params int[] lockIds) => EagerWriteLockInner(InstanceId, null, lockIds); public void EagerWriteLock(TimeSpan timeout, int lockId) => EagerWriteLockInner(InstanceId, timeout, lockId); /// - public void WriteLock(TimeSpan timeout, int lockId) => LazyWriteLockInner(InstanceId, timeout, lockId); + public void WriteLock(TimeSpan timeout, int lockId) => EagerWriteLockInner(InstanceId, timeout, lockId); /// /// Used for testing. Ensures and gets any queued read locks. @@ -659,7 +657,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping } else { - lock (_lockQueueLocker) + lock (_locker) { if (_queuedLocks?.Count > 0) { @@ -970,7 +968,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping } else { - lock (_dictionaryLocker) + lock (_locker) { _readLocksDictionary?.Remove(instanceId); _writeLocksDictionary?.Remove(instanceId); @@ -1045,7 +1043,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping private void LazyLockInner(DistributedLockType lockType, Guid instanceId, params int[] lockIds) { - lock (_lockQueueLocker) + lock (_locker) { if (_queuedLocks == null) { @@ -1061,7 +1059,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping private void LazyLockInner(DistributedLockType lockType, Guid instanceId, TimeSpan timeout, int lockId) { - lock (_lockQueueLocker) + lock (_locker) { if (_queuedLocks == null) { @@ -1088,7 +1086,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping } else { - lock (_dictionaryLocker) + lock (_locker) { foreach (var lockId in lockIds) { @@ -1122,7 +1120,7 @@ namespace Umbraco.Cms.Infrastructure.Scoping } else { - lock (_dictionaryLocker) + lock (_locker) { foreach (var lockId in lockIds) { diff --git a/src/Umbraco.PublishedCache.NuCache/ContentStore.cs b/src/Umbraco.PublishedCache.NuCache/ContentStore.cs index f38b9dd2fc..0230032dc2 100644 --- a/src/Umbraco.PublishedCache.NuCache/ContentStore.cs +++ b/src/Umbraco.PublishedCache.NuCache/ContentStore.cs @@ -27,6 +27,8 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache; /// public class ContentStore { + private static readonly TimeSpan _monitorTimeout = TimeSpan.FromSeconds(30); + // TODO: collection trigger (ok for now) // see SnapDictionary notes private const long CollectMinGenDelta = 8; @@ -330,7 +332,12 @@ public class ContentStore throw new InvalidOperationException("Recursive locks not allowed"); } - Monitor.Enter(_wlocko, ref lockInfo.Taken); + Monitor.TryEnter(_wlocko, _monitorTimeout, ref lockInfo.Taken); + + if (Monitor.IsEntered(_wlocko) is false) + { + throw new TimeoutException("Could not enter monitor before timeout in content store"); + } lock (_rlocko) { diff --git a/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentService.cs b/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentService.cs index 09855f5682..13e911f137 100644 --- a/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentService.cs +++ b/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentService.cs @@ -127,9 +127,25 @@ public class NuCacheContentService : RepositoryService, INuCacheContentService { using (ICoreScope scope = ScopeProvider.CreateCoreScope(repositoryCacheMode: RepositoryCacheMode.Scoped)) { - scope.ReadLock(Constants.Locks.ContentTree); - scope.ReadLock(Constants.Locks.MediaTree); - scope.ReadLock(Constants.Locks.MemberTree); + if (contentTypeIds is null && mediaTypeIds is null && memberTypeIds is null) + { + scope.ReadLock(Constants.Locks.ContentTree,Constants.Locks.MediaTree,Constants.Locks.MemberTree); + } + + if (contentTypeIds is not null && contentTypeIds.Any()) + { + scope.ReadLock(Constants.Locks.ContentTree); + } + + if (mediaTypeIds is not null && mediaTypeIds.Any()) + { + scope.ReadLock(Constants.Locks.MediaTree); + } + + if (memberTypeIds is not null && memberTypeIds.Any()) + { + scope.ReadLock(Constants.Locks.MemberTree); + } _repository.Rebuild(contentTypeIds, mediaTypeIds, memberTypeIds); diff --git a/src/Umbraco.PublishedCache.NuCache/SnapDictionary.cs b/src/Umbraco.PublishedCache.NuCache/SnapDictionary.cs index 0d042380d2..b6c87e22bb 100644 --- a/src/Umbraco.PublishedCache.NuCache/SnapDictionary.cs +++ b/src/Umbraco.PublishedCache.NuCache/SnapDictionary.cs @@ -9,6 +9,8 @@ public class SnapDictionary where TValue : class where TKey : notnull { + private static readonly TimeSpan _monitorTimeout = TimeSpan.FromSeconds(30); + // minGenDelta to be adjusted // we may want to throttle collects even if delta is reached // we may want to force collect if delta is not reached but very old @@ -198,7 +200,12 @@ public class SnapDictionary throw new InvalidOperationException("Recursive locks not allowed"); } - Monitor.Enter(_wlocko, ref lockInfo.Taken); + Monitor.TryEnter(_wlocko, _monitorTimeout, ref lockInfo.Taken); + + if (Monitor.IsEntered(_wlocko) is false) + { + throw new TimeoutException("Could not enter the monitor before timeout in SnapDictionary"); + } lock (_rlocko) { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs index a7390ba6ef..0d1b464efa 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/LocksTests.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Text; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using NPoco; @@ -15,7 +10,6 @@ using Umbraco.Cms.Persistence.Sqlite.Interceptors; using Umbraco.Cms.Tests.Common.Attributes; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; -using Umbraco.Extensions; namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence; @@ -126,6 +120,7 @@ public class LocksTests : UmbracoIntegrationTest } } + [NUnit.Framework.Ignore("We currently do not have a way to force lazy locks")] [Test] public void GivenNonEagerLocking_WhenNoDbIsAccessed_ThenNoSqlIsExecuted() { @@ -155,6 +150,37 @@ public class LocksTests : UmbracoIntegrationTest Assert.AreEqual(0, sqlCount); } + [Test] + public void GivenNonEagerLocking_WhenDbIsAccessed_ThenSqlIsExecuted() + { + var sqlCount = 0; + + using (var scope = ScopeProvider.CreateScope()) + { + var db = ScopeAccessor.AmbientScope.Database; + try + { + db.EnableSqlCount = true; + + // Issue a lock request, but we are using non-eager + // locks so this only queues the request. + // The lock will not be issued unless we resolve + // scope.Database + scope.WriteLock(Constants.Locks.Servers); + + scope.Database.ExecuteScalar("SELECT 1"); + + sqlCount = db.SqlCount; + } + finally + { + db.EnableSqlCount = false; + } + } + + Assert.AreEqual(2,sqlCount); + } + [Test] [LongRunning] public void ConcurrentWritersTest()