From 463dfc45a60593bc3dd40a924b72fbf1b3ec8ce9 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 6 Jun 2019 09:34:27 +0200 Subject: [PATCH 0001/1001] Copy all items from one NC to another (WIP) --- .../nestedcontent/nestedcontent.controller.js | 52 +++++++++++++++++-- .../nestedcontent/nestedcontent.html | 3 ++ src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 1 + .../Umbraco/config/lang/en_us.xml | 1 + 4 files changed, 53 insertions(+), 4 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 4e8b35a276..b02ab3ffff 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 @@ -93,8 +93,10 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop "iconHelper", "clipboardService", "eventsService", - - function ($scope, $interpolate, $filter, $timeout, contentResource, localizationService, iconHelper, clipboardService, eventsService) { + "$routeParams", + "editorState", + + function ($scope, $interpolate, $filter, $timeout, contentResource, localizationService, iconHelper, clipboardService, eventsService, $routeParams, editorState) { var contentTypeAliases = []; _.each($scope.model.config.contentTypes, function (contentType) { @@ -160,7 +162,15 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop view: "itempicker", event: $event, clickPasteItem: function(item) { - $scope.pasteFromClipboard(item.data); + if (item.alias === "nc_pasteAllItems") { + _.each(item.data, function (node) { + delete node.$$hashKey; + $scope.pasteFromClipboard(node); + }); + } else { + $scope.pasteFromClipboard(item.data); + } + $scope.overlayMenu.show = false; $scope.overlayMenu = null; }, @@ -194,6 +204,20 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop $scope.overlayMenu.size = $scope.overlayMenu.availableItems.length > 6 ? "medium" : "small"; $scope.overlayMenu.pasteItems = []; + var nestedContentForPaste = clipboardService.retriveDataOfType("nestedContent", ["nc_copyOfAllItems"]); + _.each(nestedContentForPaste, function (nestedContent) { + if (_.every(nestedContent.nodes, + function(node) { + return contentTypeAliases.indexOf(node.contentTypeAlias) >= 0; + })) { + $scope.overlayMenu.pasteItems.push({ + alias: "nc_pasteAllItems", + name: nestedContent.name, // source property name + data: nestedContent.nodes, // all items from source property + icon: "icon-bulleted-list" + }); + } + }); var availableNodesForPaste = clipboardService.retriveDataOfType("elementType", contentTypeAliases); _.each(availableNodesForPaste, function (node) { $scope.overlayMenu.pasteItems.push({ @@ -210,6 +234,7 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop $event.stopPropagation(); $event.preventDefault(); clipboardService.clearEntriesOfType("elementType", contentTypeAliases); + clipboardService.clearEntriesOfType("nestedContent", ["nc_copyOfAllItems"]); $scope.overlayMenu.pasteItems = [];// This dialog is not connected via the clipboardService events, so we need to update manually. }; @@ -359,7 +384,26 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop clipboardService.copy("elementType", node.contentTypeAlias, node); $event.stopPropagation(); } - + + $scope.clickCopyAll = function () { + + syncCurrentNode(); + + var culture = $routeParams.cculture ? $routeParams.cculture : $routeParams.mculture; + var activeVariant = _.find(editorState.current.variants, function (v) { + return !v.language || v.language.culture === culture; + }); + + localizationService.localize("content_nestedContentCopyAllItemsName", [$scope.model.label, activeVariant.name]).then(function(data) { + var model = { + nodes: $scope.nodes, + key: "nc_" + $scope.model.alias, + name: data + }; + clipboardService.copy("nestedContent", "nc_copyOfAllItems", model); + }); + } + $scope.pasteFromClipboard = function(newNode) { if (newNode === undefined) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html index 1eb7311ca7..a3196e8928 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html @@ -42,6 +42,9 @@ + + + diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 189bd9f10b..c439ed01c3 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -273,6 +273,7 @@ Are you sure you want to delete this item? Property %0% uses editor %1% which is not supported by Nested Content. No content types are configured for this property. + %0% from %1% Add another text box Remove this text box Content root 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 6ce6f82ccc..efcfcdc056 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -277,6 +277,7 @@ Are you sure you want to delete this item? Property %0% uses editor %1% which is not supported by Nested Content. No content types are configured for this property. + %0% from %1% Add another text box Remove this text box Content root From c0f26f93b3f2ba4b6d7c2af0be30154853f7ac05 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 20 Jun 2019 22:07:59 +0200 Subject: [PATCH 0002/1001] WIP for property level copying --- .../PropertyEditors/DataEditorAttribute.cs | 5 +++++ src/Umbraco.Core/PropertyEditors/DataValueEditor.cs | 7 +++++++ .../PropertyEditors/IDataValueEditor.cs | 5 +++++ .../components/property/umbproperty.directive.js | 4 ++++ .../src/views/components/property/umb-property.html | 3 +++ .../nestedcontent/nestedcontent.controller.js | 13 ++++++++++--- .../Models/ContentEditing/ContentPropertyDisplay.cs | 3 +++ .../Models/Mapping/ContentPropertyDisplayMapper.cs | 1 + .../PropertyEditors/NestedContentPropertyEditor.cs | 2 +- 9 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs b/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs index ca08127d51..bf67522449 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs @@ -111,6 +111,11 @@ namespace Umbraco.Core.PropertyEditors /// public bool HideLabel { get; set; } + /// + /// Gets or sets a value indicating whether the editor value can be copied + /// + public bool CanCopy { get; set; } + /// /// Gets or sets an optional icon. /// diff --git a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs index c4380f032c..2f215c6032 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs @@ -57,6 +57,7 @@ namespace Umbraco.Core.PropertyEditors View = view; ValueType = attribute.ValueType; HideLabel = attribute.HideLabel; + CanCopy = attribute.CanCopy; } /// @@ -133,6 +134,12 @@ namespace Umbraco.Core.PropertyEditors [JsonProperty("hideLabel")] public bool HideLabel { get; set; } + /// + /// If this is true then the editor value can be copied + /// + [JsonProperty("canCopy")] + public bool CanCopy { get; set; } + /// /// Set this to true if the property editor is for display purposes only /// diff --git a/src/Umbraco.Core/PropertyEditors/IDataValueEditor.cs b/src/Umbraco.Core/PropertyEditors/IDataValueEditor.cs index cb68531cc7..18670ccf4f 100644 --- a/src/Umbraco.Core/PropertyEditors/IDataValueEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/IDataValueEditor.cs @@ -34,6 +34,11 @@ namespace Umbraco.Core.PropertyEditors /// bool HideLabel { get; } + /// + /// Gets a value indicating whether the value can be copied + /// + bool CanCopy { get; } + /// /// Validates a property value. /// diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js index 302378b8c0..1e4c810fec 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js @@ -32,6 +32,10 @@ angular.module("umbraco.directives") self.setPropertyError = function (errorMsg) { $scope.property.propertyErrorMessage = errorMsg; }; + + $scope.onCopy = function () { + $scope.$broadcast("propertyCopy"); + } } }; }); diff --git a/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html index 9d2588484c..01d8f2b616 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html @@ -17,6 +17,9 @@ * + + + 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 4e8b35a276..2884212c85 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 @@ -547,12 +547,19 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop $scope.realCurrentNode = newVal; }); - var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { + var unsubscribe = []; + unsubscribe.push($scope.$on("formSubmitting", function (ev, args) { updateModel(); - }); + })); + unsubscribe.push($scope.$on("propertyCopy", function (ev, args) { + console.log("NC: I be the copy function by broadcast", $scope.model.alias); + })); $scope.$on("$destroy", function () { - unsubscribe(); + console.log("Unsubscribing", unsubscribe.length) + for (var u in unsubscribe) { + unsubscribe[u](); + } }); } diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentPropertyDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentPropertyDisplay.cs index 39a4718dd0..add557c8fc 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentPropertyDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentPropertyDisplay.cs @@ -33,6 +33,9 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "hideLabel")] public bool HideLabel { get; set; } + [DataMember(Name = "canCopy")] + public bool CanCopy { get; set; } + [DataMember(Name = "validation")] public PropertyTypeValidation Validation { get; set; } diff --git a/src/Umbraco.Web/Models/Mapping/ContentPropertyDisplayMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentPropertyDisplayMapper.cs index 8a45548e9c..7a10f7062b 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentPropertyDisplayMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentPropertyDisplayMapper.cs @@ -39,6 +39,7 @@ namespace Umbraco.Web.Models.Mapping dest.Description = originalProp.PropertyType.Description; dest.Label = originalProp.PropertyType.Name; dest.HideLabel = valEditor.HideLabel; + dest.CanCopy = valEditor.CanCopy; //add the validation information dest.Validation.Mandatory = originalProp.PropertyType.Mandatory; diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs index 6dee2f78b5..6fcb7ea642 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs @@ -18,7 +18,7 @@ namespace Umbraco.Web.PropertyEditors /// /// Represents a nested content property editor. /// - [DataEditor(Constants.PropertyEditors.Aliases.NestedContent, "Nested Content", "nestedcontent", ValueType = "JSON", Group = "lists", Icon = "icon-thumbnail-list")] + [DataEditor(Constants.PropertyEditors.Aliases.NestedContent, "Nested Content", "nestedcontent", ValueType = "JSON", Group = "lists", Icon = "icon-thumbnail-list", CanCopy = true)] public class NestedContentPropertyEditor : DataEditor { private readonly Lazy _propertyEditors; From 959a4b77bbb9358b02dd5072371f3e2e6232a7b6 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 20 Jun 2019 22:24:46 +0200 Subject: [PATCH 0003/1001] Remove inline property copying for NC --- .../propertyeditors/nestedcontent/nestedcontent.controller.js | 2 +- .../src/views/propertyeditors/nestedcontent/nestedcontent.html | 3 --- 2 files changed, 1 insertion(+), 4 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 2f4fa08e32..5c97a935a1 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 @@ -596,7 +596,7 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop updateModel(); })); unsubscribe.push($scope.$on("propertyCopy", function (ev, args) { - console.log("NC: I be the copy function by broadcast", $scope.model.alias); + $scope.clickCopyAll(); })); $scope.$on("$destroy", function () { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html index a3196e8928..1eb7311ca7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html @@ -42,9 +42,6 @@ - - - From 5fa0641a5ce9009c9d3f5717ebdecf8c4d5a5cf0 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 20 Jun 2019 22:39:06 +0200 Subject: [PATCH 0004/1001] Add hover state to the property copy icon --- src/Umbraco.Web.UI.Client/src/less/property-editors.less | 9 +++++++++ .../src/views/components/property/umb-property.html | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index 9e8dd37ab9..93cbeb60d1 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -59,7 +59,16 @@ border-radius: 3px; } +.umb-property .umb-property__copy { + font-size: 16px; + padding: 4px; + line-height: 10px; + &:hover { + color: @ui-option-type-hover; + text-decoration: none; + } +} // // Content picker diff --git a/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html index 01d8f2b616..7f48ca250d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html @@ -18,7 +18,7 @@ * - + From cb16eb1826b3cc3282beeb4c945106f990e8bd59 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sat, 22 Jun 2019 10:43:40 +0200 Subject: [PATCH 0005/1001] Don't expand the last item when pasting multiple items --- .../nestedcontent/nestedcontent.controller.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 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 5c97a935a1..440f391a44 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 @@ -165,10 +165,10 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop if (item.alias === "nc_pasteAllItems") { _.each(item.data, function (node) { delete node.$$hashKey; - $scope.pasteFromClipboard(node); + $scope.pasteFromClipboard(node, false); }); } else { - $scope.pasteFromClipboard(item.data); + $scope.pasteFromClipboard(item.data, true); } $scope.overlayMenu.show = false; @@ -404,7 +404,7 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop }); } - $scope.pasteFromClipboard = function(newNode) { + $scope.pasteFromClipboard = function(newNode, setCurrentNode) { if (newNode === undefined) { return; @@ -415,9 +415,14 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop $scope.nodes.push(newNode); $scope.setDirty(); - //updateModel();// done by setting current node... + + if (setCurrentNode) { + $scope.currentNode = newNode; + } + else { + updateModel(); + } - $scope.currentNode = newNode; } function checkAbilityToPasteContent() { From d38c12115e5a4b5d59db0d3b9f8cccffee1e1546 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sat, 22 Jun 2019 13:23:50 +0200 Subject: [PATCH 0006/1001] Remove some debugging stuff --- .../propertyeditors/nestedcontent/nestedcontent.controller.js | 1 - 1 file changed, 1 deletion(-) 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 440f391a44..567f78bc39 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 @@ -605,7 +605,6 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop })); $scope.$on("$destroy", function () { - console.log("Unsubscribing", unsubscribe.length) for (var u in unsubscribe) { unsubscribe[u](); } From df896af47672b37b70526492e197c98077ce7d1e Mon Sep 17 00:00:00 2001 From: Claus Date: Wed, 10 Jul 2019 12:58:15 +0200 Subject: [PATCH 0007/1001] adding build targets package. --- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 2 ++ src/Umbraco.Web.UI/packages.config | 1 + 2 files changed, 3 insertions(+) diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index a0624a2dd7..646b86f665 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -1,5 +1,6 @@  + @@ -1088,5 +1089,6 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.1\x86\*.* "$(TargetDir)x86\" + \ No newline at end of file diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config index 18cbfdb042..a8271c1370 100644 --- a/src/Umbraco.Web.UI/packages.config +++ b/src/Umbraco.Web.UI/packages.config @@ -32,6 +32,7 @@ + From d31e71a838abbbe5391049ad9cfc331ec31e895b Mon Sep 17 00:00:00 2001 From: skttl Date: Fri, 12 Jul 2019 07:33:17 +0200 Subject: [PATCH 0008/1001] changes innerjoin to left join, to allow folders in sql query --- src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs index 7b10b19e1e..c8fdd8da57 100644 --- a/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs @@ -370,7 +370,7 @@ namespace Umbraco.Core.Persistence.Repositories if (isMedia) { - entitySql.InnerJoin("cmsMedia media").On("media.nodeId = umbracoNode.id"); + entitySql.LeftJoin("cmsMedia media").On("media.nodeId = umbracoNode.id"); } entitySql.LeftJoin("cmsContentType contenttype").On("contenttype.nodeId = content.contentType"); From 59bf24c46110730ff59b070e47545316f9fb48a0 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 12 Jul 2019 11:22:31 +0200 Subject: [PATCH 0009/1001] Added null checks when *.dataTypeId is used --- .../src/common/resources/entity.resource.js | 4 +++- .../common/dialogs/linkpicker.controller.js | 6 +++++- .../linkpicker/linkpicker.controller.js | 15 ++++++++++++-- .../mediaPicker/mediapicker.controller.js | 20 ++++++++++++++++--- .../treepicker/treepicker.controller.js | 7 ++++++- .../contentpicker/contentpicker.controller.js | 7 ++++++- .../grid/editors/media.controller.js | 7 ++++++- .../grid/editors/rte.controller.js | 14 +++++++++++-- .../mediapicker/mediapicker.controller.js | 6 +++++- .../multiurlpicker.controller.js | 7 ++++++- .../relatedlinks/relatedlinks.controller.js | 14 +++++++++++-- .../propertyeditors/rte/rte.controller.js | 13 ++++++++++-- 12 files changed, 102 insertions(+), 18 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js index c85a85bb57..4e164cacf6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js @@ -409,7 +409,8 @@ function entityResource($q, $http, umbRequestHelper) { pageNumber: 100, filter: '', orderDirection: "Ascending", - orderBy: "SortOrder" + orderBy: "SortOrder", + dataTypeId: null }; if (options === undefined) { options = {}; @@ -426,6 +427,7 @@ function entityResource($q, $http, umbRequestHelper) { options.orderDirection = "Descending"; } + return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/linkpicker.controller.js index e08178ec93..876a9f9426 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/linkpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/linkpicker.controller.js @@ -8,13 +8,17 @@ angular.module("umbraco").controller("Umbraco.Dialogs.LinkPickerController", searchText = value + "..."; }); + var dataTypeId = null; + if($scope.model && $scope.model.dataTypeId) { + dataTypeId = $scope.model.dataTypeId; + } $scope.dialogTreeEventHandler = $({}); $scope.target = {}; $scope.searchInfo = { searchFromId: null, searchFromName: null, showSearch: false, - dataTypeId: $scope.model.dataTypeId, + dataTypeId: dataTypeId, results: [], selectedSearchResults: [] } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js index 2b6b77ed41..0c5641ba0a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js @@ -12,13 +12,19 @@ angular.module("umbraco").controller("Umbraco.Overlays.LinkPickerController", $scope.model.title = localizationService.localize("defaultdialogs_selectLink"); } + var dataTypeId = null; + if(dialogOptions && dialogOptions.dataTypeId){ + dataTypeId = dialogOptions.dataTypeId; + } + + $scope.dialogTreeEventHandler = $({}); $scope.model.target = {}; $scope.searchInfo = { searchFromId: null, searchFromName: null, showSearch: false, - dataTypeId: dialogOptions.dataTypeId, + dataTypeId: dataTypeId, results: [], selectedSearchResults: [] }; @@ -115,12 +121,17 @@ angular.module("umbraco").controller("Umbraco.Overlays.LinkPickerController", startNodeId = -1; startNodeIsVirtual = true; } + var dataTypeId = null; + if(dialogOptions && dialogOptions.dataTypeId){ + dataTypeId = dialogOptions.dataTypeId; + } + $scope.mediaPickerOverlay = { view: "mediapicker", startNodeId: startNodeId, startNodeIsVirtual: startNodeIsVirtual, show: true, - dataTypeId: dialogOptions.dataTypeId, + dataTypeId: dataTypeId, submit: function (model) { var media = model.selectedImages[0]; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js index bb34ba379c..61f9619a52 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js @@ -44,13 +44,17 @@ angular.module("umbraco") $scope.acceptedMediatypes = types; }); + var dataTypeId = null; + if($scope.model && $scope.model.dataTypeId) { + dataTypeId = $scope.model.dataTypeId; + } $scope.searchOptions = { pageNumber: 1, pageSize: 100, totalItems: 0, totalPages: 0, filter: '', - dataTypeId: $scope.model.dataTypeId + dataTypeId: dataTypeId }; //preload selected item @@ -157,7 +161,12 @@ angular.module("umbraco") } if (folder.id > 0) { - entityResource.getAncestors(folder.id, "media", { dataTypeId: $scope.model.dataTypeId }) + var dataTypeId = null; + if($scope.model && $scope.model.dataTypeId) { + dataTypeId = $scope.model.dataTypeId; + } + + entityResource.getAncestors(folder.id, "media", { dataTypeId: dataTypeId }) .then(function (anc) { $scope.path = _.filter(anc, function (f) { @@ -309,6 +318,11 @@ angular.module("umbraco") if ($scope.searchOptions.filter) { searchMedia(); } else { + var dataTypeId = null; + if($scope.model && $scope.model.dataTypeId) { + dataTypeId = $scope.model.dataTypeId; + } + // reset pagination $scope.searchOptions = { pageNumber: 1, @@ -316,7 +330,7 @@ angular.module("umbraco") totalItems: 0, totalPages: 0, filter: '', - dataTypeId: $scope.model.dataTypeId + dataTypeId: dataTypeId }; getChildren($scope.currentFolder.id); } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js index b4dd34dfff..851d270789 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js @@ -4,6 +4,11 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", var tree = null; var dialogOptions = $scope.model; + + var dataTypeId = null; + if(dialogOptions && dialogOptions.dataTypeId){ + dataTypeId = dialogOptions.dataTypeId; + } $scope.treeReady = false; $scope.dialogTreeEventHandler = $({}); $scope.section = dialogOptions.section; @@ -16,7 +21,7 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", searchFromId: dialogOptions.startNodeId, searchFromName: null, showSearch: false, - dataTypeId: dialogOptions.dataTypeId, + dataTypeId: dataTypeId, results: [], selectedSearchResults: [] } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js index 187b86a811..37a372a5b1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js @@ -180,10 +180,15 @@ function contentPickerController($scope, entityResource, editorState, iconHelper //dialog $scope.openContentPicker = function() { + var dataTypeId = null; + if($scope.model && $scope.model.dataTypeId) { + dataTypeId = $scope.model.dataTypeId; + } + $scope.contentPickerOverlay = dialogOptions; $scope.contentPickerOverlay.view = "treepicker"; $scope.contentPickerOverlay.show = true; - $scope.contentPickerOverlay.dataTypeId = $scope.model.dataTypeId; + $scope.contentPickerOverlay.dataTypeId = dataTypeId; $scope.contentPickerOverlay.submit = function(model) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js index c61f5419ca..e9c095c1b9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js @@ -16,11 +16,16 @@ angular.module("umbraco") } $scope.setImage = function(){ + var dataTypeId = null; + if($scope.model && $scope.model.dataTypeId) { + dataTypeId = $scope.model.dataTypeId; + } + $scope.mediaPickerOverlay = {}; $scope.mediaPickerOverlay.view = "mediapicker"; $scope.mediaPickerOverlay.startNodeId = $scope.model.config && $scope.model.config.startNodeId ? $scope.model.config.startNodeId : undefined; $scope.mediaPickerOverlay.startNodeIsVirtual = $scope.mediaPickerOverlay.startNodeId ? $scope.model.config.startNodeIsVirtual : undefined; - $scope.mediaPickerOverlay.dataTypeId = $scope.model.dataTypeId; + $scope.mediaPickerOverlay.dataTypeId = dataTypeId; $scope.mediaPickerOverlay.cropSize = $scope.control.editor.config && $scope.control.editor.config.size ? $scope.control.editor.config.size : undefined; $scope.mediaPickerOverlay.showDetails = true; $scope.mediaPickerOverlay.disableFolderSelect = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js index da869dbf01..580d231de8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js @@ -13,11 +13,16 @@ function openLinkPicker(editor, currentTarget, anchorElement) { entityResource.getAnchors(JSON.stringify($scope.model.value)).then(function(anchorValues) { + var dataTypeId = null; + if($scope.model && $scope.model.dataTypeId) { + dataTypeId = $scope.model.dataTypeId; + } + vm.linkPickerOverlay = { view: "linkpicker", currentTarget: currentTarget, anchors: anchorValues, - dataTypeId: $scope.model.dataTypeId, + dataTypeId: dataTypeId, ignoreUserStartNodes : $scope.model.config.ignoreUserStartNodes, show: true, submit: function(model) { @@ -40,13 +45,18 @@ startNodeIsVirtual = true; } + var dataTypeId = null; + if($scope.model && $scope.model.dataTypeId) { + dataTypeId = $scope.model.dataTypeId; + } + vm.mediaPickerOverlay = { currentTarget: currentTarget, onlyImages: true, showDetails: true, startNodeId: startNodeId, startNodeIsVirtual: startNodeIsVirtual, - dataTypeId: $scope.model.dataTypeId, + dataTypeId: dataTypeId, view: "mediapicker", show: true, submit: function(model) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js index 138bb386be..63beddb49c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js @@ -107,13 +107,17 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl }; $scope.add = function() { + var dataTypeId = null; + if($scope.model && $scope.model.dataTypeId) { + dataTypeId = $scope.model.dataTypeId; + } $scope.mediaPickerOverlay = { view: "mediapicker", title: "Select media", startNodeId: $scope.model.config.startNodeId, startNodeIsVirtual: $scope.model.config.startNodeIsVirtual, - dataTypeId: $scope.model.dataTypeId, + dataTypeId: dataTypeId, multiPicker: multiPicker, onlyImages: onlyImages, disableFolderSelect: disableFolderSelect, diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js index 124f342741..cab124ad23 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js @@ -68,10 +68,15 @@ function multiUrlPickerController($scope, angularHelper, localizationService, en target: link.target } : null; + var dataTypeId = null; + if($scope.model && $scope.model.dataTypeId) { + dataTypeId = $scope.model.dataTypeId; + } + $scope.linkPickerOverlay = { view: "linkpicker", currentTarget: target, - dataTypeId: $scope.model.dataTypeId, + dataTypeId: dataTypeId, ignoreUserStartNodes : $scope.model.config.ignoreUserStartNodes, show: true, submit: function (model) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js index 26c6a0c051..fa92442eac 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js @@ -19,13 +19,18 @@ $scope.hasError = false; $scope.internal = function($event) { + var dataTypeId = null; + if($scope.model && $scope.model.dataTypeId) { + dataTypeId = $scope.model.dataTypeId; + } + $scope.currentEditLink = null; $scope.contentPickerOverlay = {}; $scope.contentPickerOverlay.view = "contentpicker"; $scope.contentPickerOverlay.multiPicker = false; $scope.contentPickerOverlay.show = true; - $scope.contentPickerOverlay.dataTypeId = $scope.model.dataTypeId; + $scope.contentPickerOverlay.dataTypeId = dataTypeId; $scope.contentPickerOverlay.idType = $scope.model.config.idType ? $scope.model.config.idType : "int"; $scope.contentPickerOverlay.submit = function(model) { @@ -45,13 +50,18 @@ }; $scope.selectInternal = function ($event, link) { + var dataTypeId = null; + if($scope.model && $scope.model.dataTypeId) { + dataTypeId = $scope.model.dataTypeId; + } + $scope.currentEditLink = link; $scope.contentPickerOverlay = {}; $scope.contentPickerOverlay.view = "contentpicker"; $scope.contentPickerOverlay.multiPicker = false; $scope.contentPickerOverlay.show = true; - $scope.contentPickerOverlay.dataTypeId = $scope.model.dataTypeId; + $scope.contentPickerOverlay.dataTypeId = dataTypeId; $scope.contentPickerOverlay.idType = $scope.model.config.idType ? $scope.model.config.idType : "int"; $scope.contentPickerOverlay.submit = function(model) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js index dd9fb404c1..2b3ee930d9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js @@ -272,11 +272,16 @@ angular.module("umbraco") tinyMceService.createLinkPicker(editor, $scope, function(currentTarget, anchorElement) { entityResource.getAnchors($scope.model.value).then(function(anchorValues){ + var dataTypeId = null; + if($scope.model && $scope.model.dataTypeId) { + dataTypeId = $scope.model.dataTypeId; + } + $scope.linkPickerOverlay = { view: "linkpicker", currentTarget: currentTarget, anchors: anchorValues, - dataTypeId: $scope.model.dataTypeId, + dataTypeId: dataTypeId, ignoreUserStartNodes: $scope.model.config.ignoreUserStartNodes, show: true, submit: function(model) { @@ -300,6 +305,10 @@ angular.module("umbraco") startNodeId = -1; startNodeIsVirtual = true; } + var dataTypeId = null; + if($scope.model && $scope.model.dataTypeId) { + dataTypeId = $scope.model.dataTypeId; + } $scope.mediaPickerOverlay = { currentTarget: currentTarget, @@ -308,7 +317,7 @@ angular.module("umbraco") disableFolderSelect: true, startNodeId: startNodeId, startNodeIsVirtual: startNodeIsVirtual, - dataTypeId: $scope.model.dataTypeId, + dataTypeId: dataTypeId, view: "mediapicker", show: true, submit: function(model) { From 5d5bd54aee50a92fef3e096c1c97524c958b1c4f Mon Sep 17 00:00:00 2001 From: skttl Date: Fri, 12 Jul 2019 21:07:55 +0200 Subject: [PATCH 0010/1001] 5867 enables tabbing to field-lock-toggler, and adds tab-focus styling --- .../src/less/components/umb-locked-field.less | 19 ++++++++++++++++--- .../views/components/umb-locked-field.html | 8 ++++---- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-locked-field.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-locked-field.less index 8d9ae86ce7..5a14c82210 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-locked-field.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-locked-field.less @@ -6,13 +6,26 @@ } .umb-locked-field__wrapper { - display: flex; - align-items: center; - margin-bottom: 5px; + display: flex; + align-items: center; + margin-bottom: 5px; } .umb-locked-field__toggle { margin-right: 3px; + padding: 0; + background: none; + border: 0; + font-size: inherit; + line-height: inherit; + + &:focus { + outline: none; + + .tabbing-active & { + outline: 2px solid @inputBorderTabFocus; + } + } } .umb-locked-field__toggle:hover, diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-locked-field.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-locked-field.html index 624ca8e923..5d5f10218f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-locked-field.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-locked-field.html @@ -2,13 +2,13 @@
- + - + Date: Mon, 15 Jul 2019 12:23:09 +0200 Subject: [PATCH 0011/1001] Fix YSOD when editing media types --- .../PublishedCache/NuCache/PublishedSnapshotService.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index dad9811af8..f26122d4d0 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -1057,7 +1057,10 @@ namespace Umbraco.Web.PublishedCache.NuCache _mediaStore.UpdateContentTypes(removedIds, typesA, kits); _mediaStore.UpdateContentTypes(CreateContentTypes(PublishedItemType.Media, otherIds.ToArray()).ToArray()); - _mediaStore.NewContentTypes(CreateContentTypes(PublishedItemType.Media, newIds.ToArray()).ToArray()); + if(newIds != null && newIds.Any()) + { + _mediaStore.NewContentTypes(CreateContentTypes(PublishedItemType.Media, newIds.ToArray()).ToArray()); + } scope.Complete(); } } From f6a8d487ccd6a29487f6ef5f8d9e9aad0e6a54f6 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 15 Jul 2019 13:06:47 +0200 Subject: [PATCH 0012/1001] AB#1695 - https://github.com/umbraco/Umbraco-CMS/issues/5846 Give better exception, if a json exception happens doing migration of grid properties in ConvertTinyMceAndGridMediaUrlsToLocalLink --- ...nvertTinyMceAndGridMediaUrlsToLocalLink.cs | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs index bf048bf2bd..5f49680d89 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using Newtonsoft.Json; @@ -36,6 +37,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_1_0 var properties = Database.Fetch(sqlPropertyData); + var exceptions = new List(); foreach (var property in properties) { var value = property.TextValue; @@ -43,19 +45,34 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_1_0 if (property.PropertyTypeDto.DataTypeDto.EditorAlias == Constants.PropertyEditors.Aliases.Grid) { - var obj = JsonConvert.DeserializeObject(value); - var allControls = obj.SelectTokens("$.sections..rows..areas..controls"); - - foreach (var control in allControls.SelectMany(c => c)) + try { - var controlValue = control["value"]; - if (controlValue.Type == JTokenType.String) + var obj = JsonConvert.DeserializeObject(value); + var allControls = obj.SelectTokens("$.sections..rows..areas..controls"); + + foreach (var control in allControls.SelectMany(c => c)) { - control["value"] = UpdateMediaUrls(mediaLinkPattern, controlValue.Value()); + var controlValue = control["value"]; + if (controlValue.Type == JTokenType.String) + { + control["value"] = UpdateMediaUrls(mediaLinkPattern, controlValue.Value()); + } } + + property.TextValue = JsonConvert.SerializeObject(obj); + } + catch (JsonException e) + { + exceptions.Add(new InvalidOperationException( + "Cannot deserialize the value as json. This can be because the property editor " + + "type is changed from another type into a grid. Old versions of the value in this " + + "property can have the structure from the old property editor type. This needs to be " + + "changed manually before updating the database.\n" + + $"Property info: Id = {property.Id}, LanguageId = {property.LanguageId}, VersionId = {property.VersionId}, Value = {property.Value}" + , e)); + continue; } - property.TextValue = JsonConvert.SerializeObject(obj); } else { @@ -65,6 +82,12 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_1_0 Database.Update(property); } + + if (exceptions.Any()) + { + throw new AggregateException(exceptions); + } + Context.AddPostMigration(); } From da71a94c39fbd5403c114344a5d5627adacc88b0 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 16 Jul 2019 12:45:27 +0200 Subject: [PATCH 0013/1001] Use IsCollectionEmpty() as per review --- .../PublishedCache/NuCache/PublishedSnapshotService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index f26122d4d0..b78c806824 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -1057,7 +1057,7 @@ namespace Umbraco.Web.PublishedCache.NuCache _mediaStore.UpdateContentTypes(removedIds, typesA, kits); _mediaStore.UpdateContentTypes(CreateContentTypes(PublishedItemType.Media, otherIds.ToArray()).ToArray()); - if(newIds != null && newIds.Any()) + if(newIds.IsCollectionEmpty() == false) { _mediaStore.NewContentTypes(CreateContentTypes(PublishedItemType.Media, newIds.ToArray()).ToArray()); } From 14c4c4815d20de5d2cc69868d303ce58309bd7ba Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 17 Jul 2019 21:35:43 +1000 Subject: [PATCH 0014/1001] bumps version --- src/SolutionInfo.cs | 4 ++-- src/Umbraco.Core/Configuration/UmbracoVersion.cs | 2 +- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 0d6b3bcc6b..3a85915cf5 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -11,5 +11,5 @@ using System.Resources; [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyFileVersion("7.15.0")] -[assembly: AssemblyInformationalVersion("7.15.0")] +[assembly: AssemblyFileVersion("7.15.1")] +[assembly: AssemblyInformationalVersion("7.15.1")] diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index 6bb5dbf78e..f23078dbd0 100644 --- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs @@ -6,7 +6,7 @@ namespace Umbraco.Core.Configuration { public class UmbracoVersion { - private static readonly Version Version = new Version("7.15.0"); + private static readonly Version Version = new Version("7.15.1"); /// /// Gets the current version of Umbraco. diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index a0624a2dd7..6884f21bcf 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -1027,9 +1027,9 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.1\x86\*.* "$(TargetDir)x86\" True True - 7150 + 7151 / - http://localhost:7150 + http://localhost:7151 False False From 79159b684dabcbe8b54597691380bb17982285cf Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 17 Jul 2019 14:30:02 +0200 Subject: [PATCH 0015/1001] https://github.com/umbraco/Umbraco-CMS/issues/5902 - Fixed issue with migration of Umbraco.Date if it was never saved in v7, the default format was added the time-part. - Fixed issue with warnings of migrations of build in types, that was renamed between v7 and v8. These are handled correct. - Added constants for some of the legacy property editor aliases. --- src/Umbraco.Core/Constants-PropertyEditors.cs | 17 ++++++++++ .../ConvertRelatedLinksToMultiUrlPicker.cs | 4 +-- .../Upgrade/V_8_0_0/DataTypeMigration.cs | 34 +++++++++++++++---- .../ContentPickerPreValueMigrator.cs | 2 +- .../DataTypes/MediaPickerPreValueMigrator.cs | 6 ++-- .../MergeDateAndDateTimePropertyEditor.cs | 10 +++++- .../V_8_0_0/PropertyEditorsMigration.cs | 14 ++++---- 7 files changed, 66 insertions(+), 21 deletions(-) diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs index 0c2e246721..b48286f197 100644 --- a/src/Umbraco.Core/Constants-PropertyEditors.cs +++ b/src/Umbraco.Core/Constants-PropertyEditors.cs @@ -14,6 +14,23 @@ namespace Umbraco.Core /// public const string InternalGenericPropertiesPrefix = "_umb_"; + public static class Legacy + { + public static class Aliases + { + public const string Textbox = "Umbraco.Textbox"; + public const string Date = "Umbraco.Date"; + public const string ContentPicker2 = "Umbraco.ContentPicker2"; + public const string MediaPicker2 = "Umbraco.MediaPicker2"; + public const string MemberPicker2 = "Umbraco.MemberPicker2"; + public const string MultiNodeTreePicker2 = "Umbraco.MultiNodeTreePicker2"; + public const string TextboxMultiple = "Umbraco.TextboxMultiple"; + public const string RelatedLinks2 = "Umbraco.RelatedLinks2"; + public const string RelatedLinks = "Umbraco.RelatedLinks"; + + } + } + /// /// Defines Umbraco built-in property editor aliases. /// diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs index 44f7affe8e..4802750f4b 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs @@ -19,8 +19,8 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 var sqlDataTypes = Sql() .Select() .From() - .Where(x => x.EditorAlias == "Umbraco.RelatedLinks" - || x.EditorAlias == "Umbraco.RelatedLinks2"); + .Where(x => x.EditorAlias == Constants.PropertyEditors.Legacy.Aliases.RelatedLinks + || x.EditorAlias == Constants.PropertyEditors.Legacy.Aliases.RelatedLinks2); var dataTypes = Database.Fetch(sqlDataTypes); var dataTypeIds = dataTypes.Select(x => x.NodeId).ToList(); diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs index 438b02385b..7b2daa99ef 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using Umbraco.Core.Composing; @@ -18,6 +19,18 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 private readonly PropertyEditorCollection _propertyEditors; private readonly ILogger _logger; + private static readonly ISet LegacyAliases = new HashSet() + { + Constants.PropertyEditors.Legacy.Aliases.Date, + Constants.PropertyEditors.Legacy.Aliases.Textbox, + Constants.PropertyEditors.Legacy.Aliases.ContentPicker2, + Constants.PropertyEditors.Legacy.Aliases.MediaPicker2, + Constants.PropertyEditors.Legacy.Aliases.MemberPicker2, + Constants.PropertyEditors.Legacy.Aliases.RelatedLinks2, + Constants.PropertyEditors.Legacy.Aliases.TextboxMultiple, + Constants.PropertyEditors.Legacy.Aliases.MultiNodeTreePicker2, + }; + public DataTypeMigration(IMigrationContext context, PreValueMigratorCollection preValueMigrators, PropertyEditorCollection propertyEditors, ILogger logger) : base(context) { @@ -70,16 +83,23 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 var newAlias = migrator.GetNewAlias(dataType.EditorAlias); if (newAlias == null) { - _logger.Warn("Skipping validation of configuration for data type {NodeId} : {EditorAlias}." - + " Please ensure that the configuration is valid. The site may fail to start and / or load data types and run.", - dataType.NodeId, dataType.EditorAlias); + if (!LegacyAliases.Contains(dataType.EditorAlias)) + { + _logger.Warn( + "Skipping validation of configuration for data type {NodeId} : {EditorAlias}." + + " Please ensure that the configuration is valid. The site may fail to start and / or load data types and run.", + dataType.NodeId, dataType.EditorAlias); + } } else if (!_propertyEditors.TryGet(newAlias, out var propertyEditor)) { - _logger.Warn("Skipping validation of configuration for data type {NodeId} : {NewEditorAlias} (was: {EditorAlias})" - + " because no property editor with that alias was found." - + " Please ensure that the configuration is valid. The site may fail to start and / or load data types and run.", - dataType.NodeId, newAlias, dataType.EditorAlias); + if (!LegacyAliases.Contains(newAlias)) + { + _logger.Warn("Skipping validation of configuration for data type {NodeId} : {NewEditorAlias} (was: {EditorAlias})" + + " because no property editor with that alias was found." + + " Please ensure that the configuration is valid. The site may fail to start and / or load data types and run.", + dataType.NodeId, newAlias, dataType.EditorAlias); + } } else { diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ContentPickerPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ContentPickerPreValueMigrator.cs index 2e341ad091..f445742aa9 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ContentPickerPreValueMigrator.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ContentPickerPreValueMigrator.cs @@ -3,7 +3,7 @@ class ContentPickerPreValueMigrator : DefaultPreValueMigrator { public override bool CanMigrate(string editorAlias) - => editorAlias == "Umbraco.ContentPicker2"; + => editorAlias == Constants.PropertyEditors.Legacy.Aliases.ContentPicker2; public override string GetNewAlias(string editorAlias) => null; diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/MediaPickerPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/MediaPickerPreValueMigrator.cs index a46b1eefb7..922d886595 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/MediaPickerPreValueMigrator.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/MediaPickerPreValueMigrator.cs @@ -6,15 +6,15 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes { private readonly string[] _editors = { - "Umbraco.MediaPicker2", - "Umbraco.MediaPicker" + Constants.PropertyEditors.Legacy.Aliases.MediaPicker2, + Constants.PropertyEditors.Aliases.MediaPicker }; public override bool CanMigrate(string editorAlias) => _editors.Contains(editorAlias); public override string GetNewAlias(string editorAlias) - => "Umbraco.MediaPicker"; + => Constants.PropertyEditors.Aliases.MediaPicker; // you wish - but MediaPickerConfiguration lives in Umbraco.Web /* diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs index a434b9f1c1..0d451e8460 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs @@ -16,7 +16,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 public override void Migrate() { - var dataTypes = GetDataTypes("Umbraco.Date"); + var dataTypes = GetDataTypes(Constants.PropertyEditors.Legacy.Aliases.Date); foreach (var dataType in dataTypes) { @@ -25,6 +25,14 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 { config = (DateTimeConfiguration) new CustomDateTimeConfigurationEditor().FromDatabase( dataType.Configuration); + + // If the Umbraco.Date type is the default from V7 and it has never been updated, then the + // configuration is empty, and the format stuff is handled by in JS by moment.js. - We can't do that + // after the migration, so we force the format to the default from V7. + if (string.IsNullOrEmpty(dataType.Configuration)) + { + config.Format = "YYYY-MM-DD"; + }; } catch (Exception ex) { diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs index dac62abb75..89a8f010ec 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs @@ -12,12 +12,12 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 public override void Migrate() { - RenameDataType(Constants.PropertyEditors.Aliases.ContentPicker + "2", Constants.PropertyEditors.Aliases.ContentPicker); - RenameDataType(Constants.PropertyEditors.Aliases.MediaPicker + "2", Constants.PropertyEditors.Aliases.MediaPicker); - RenameDataType(Constants.PropertyEditors.Aliases.MemberPicker + "2", Constants.PropertyEditors.Aliases.MemberPicker); - RenameDataType(Constants.PropertyEditors.Aliases.MultiNodeTreePicker + "2", Constants.PropertyEditors.Aliases.MultiNodeTreePicker); - RenameDataType("Umbraco.TextboxMultiple", Constants.PropertyEditors.Aliases.TextArea, false); - RenameDataType("Umbraco.Textbox", Constants.PropertyEditors.Aliases.TextBox, false); + RenameDataType(Constants.PropertyEditors.Legacy.Aliases.ContentPicker2, Constants.PropertyEditors.Aliases.ContentPicker); + RenameDataType(Constants.PropertyEditors.Legacy.Aliases.MediaPicker2, Constants.PropertyEditors.Aliases.MediaPicker); + RenameDataType(Constants.PropertyEditors.Legacy.Aliases.MemberPicker2, Constants.PropertyEditors.Aliases.MemberPicker); + RenameDataType(Constants.PropertyEditors.Legacy.Aliases.MultiNodeTreePicker2, Constants.PropertyEditors.Aliases.MultiNodeTreePicker); + RenameDataType(Constants.PropertyEditors.Legacy.Aliases.TextboxMultiple, Constants.PropertyEditors.Aliases.TextArea, false); + RenameDataType(Constants.PropertyEditors.Legacy.Aliases.Textbox, Constants.PropertyEditors.Aliases.TextBox, false); } private void RenameDataType(string fromAlias, string toAlias, bool checkCollision = true) @@ -43,7 +43,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 $"Property Editors. Before upgrading to v8, any Data Types using property editors that are named with the prefix '(Obsolete)' must be migrated " + $"to the non-obsolete v7 property editors of the same type."); } - + } Database.Execute(Sql() From 36030904212d321af75f96bfc894742e86629ec3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 17 Jul 2019 22:51:14 +1000 Subject: [PATCH 0016/1001] Fixes race condition in PublishedSnapshotService and more... --- src/Umbraco.Core/AsyncLock.cs | 47 ++++++++-------- src/Umbraco.Core/MainDom.cs | 7 ++- src/Umbraco.Core/Runtime/CoreRuntime.cs | 6 +- src/Umbraco.Core/RuntimeState.cs | 5 ++ src/Umbraco.Core/WaitHandleExtensions.cs | 2 + .../PublishedContent/NuCacheChildrenTests.cs | 2 +- .../PublishedContent/NuCacheTests.cs | 2 +- .../Scoping/ScopedNuCacheTests.cs | 2 +- .../ContentTypeServiceVariantsTests.cs | 2 +- .../PublishedCache/NuCache/NuCacheComposer.cs | 2 +- .../NuCache/PublishedSnapshotService.cs | 56 +++++++++---------- .../PublishedSnapshotServiceOptions.cs | 28 ++++++++++ src/Umbraco.Web/Scheduling/KeepAlive.cs | 2 +- src/Umbraco.Web/Umbraco.Web.csproj | 1 + 14 files changed, 101 insertions(+), 63 deletions(-) create mode 100644 src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotServiceOptions.cs diff --git a/src/Umbraco.Core/AsyncLock.cs b/src/Umbraco.Core/AsyncLock.cs index 158b132f26..6dd866705e 100644 --- a/src/Umbraco.Core/AsyncLock.cs +++ b/src/Umbraco.Core/AsyncLock.cs @@ -67,31 +67,34 @@ namespace Umbraco.Core : new NamedSemaphoreReleaser(_semaphore2); } - public Task LockAsync() - { - var wait = _semaphore != null - ? _semaphore.WaitAsync() - : _semaphore2.WaitOneAsync(); + //NOTE: We don't use the "Async" part of this lock at all + //TODO: Remove this and rename this class something like SystemWideLock, then we can re-instate this logic if we ever need an Async lock again - return wait.IsCompleted - ? _releaserTask ?? Task.FromResult(CreateReleaser()) // anonymous vs named - : wait.ContinueWith((_, state) => (((AsyncLock) state).CreateReleaser()), - this, CancellationToken.None, - TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); - } + //public Task LockAsync() + //{ + // var wait = _semaphore != null + // ? _semaphore.WaitAsync() + // : _semaphore2.WaitOneAsync(); - public Task LockAsync(int millisecondsTimeout) - { - var wait = _semaphore != null - ? _semaphore.WaitAsync(millisecondsTimeout) - : _semaphore2.WaitOneAsync(millisecondsTimeout); + // return wait.IsCompleted + // ? _releaserTask ?? Task.FromResult(CreateReleaser()) // anonymous vs named + // : wait.ContinueWith((_, state) => (((AsyncLock) state).CreateReleaser()), + // this, CancellationToken.None, + // TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + //} - return wait.IsCompleted - ? _releaserTask ?? Task.FromResult(CreateReleaser()) // anonymous vs named - : wait.ContinueWith((_, state) => (((AsyncLock)state).CreateReleaser()), - this, CancellationToken.None, - TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); - } + //public Task LockAsync(int millisecondsTimeout) + //{ + // var wait = _semaphore != null + // ? _semaphore.WaitAsync(millisecondsTimeout) + // : _semaphore2.WaitOneAsync(millisecondsTimeout); + + // return wait.IsCompleted + // ? _releaserTask ?? Task.FromResult(CreateReleaser()) // anonymous vs named + // : wait.ContinueWith((_, state) => (((AsyncLock)state).CreateReleaser()), + // this, CancellationToken.None, + // TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + //} public IDisposable Lock() { diff --git a/src/Umbraco.Core/MainDom.cs b/src/Umbraco.Core/MainDom.cs index ca875c2167..d1012fb669 100644 --- a/src/Umbraco.Core/MainDom.cs +++ b/src/Umbraco.Core/MainDom.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Web.Hosting; @@ -113,7 +114,7 @@ namespace Umbraco.Core lock (_locko) { - _logger.Debug("Signaled {Signaled} ({SignalSource})", _signaled ? "(again)" : string.Empty, source); + _logger.Debug("Signaled ({Signaled}) ({SignalSource})", _signaled ? "again" : "first", source); if (_signaled) return; if (_isMainDom == false) return; // probably not needed _signaled = true; @@ -171,6 +172,7 @@ namespace Umbraco.Core // if more than 1 instance reach that point, one will get the lock // and the other one will timeout, which is accepted + //TODO: This can throw a TimeoutException - in which case should this be in a try/finally to ensure the signal is always reset? _asyncLocker = _asyncLock.Lock(LockTimeoutMilliseconds); _isMainDom = true; @@ -181,6 +183,9 @@ namespace Umbraco.Core // 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() .ContinueWith(_ => OnSignal("signal")); diff --git a/src/Umbraco.Core/Runtime/CoreRuntime.cs b/src/Umbraco.Core/Runtime/CoreRuntime.cs index f9a41b4f66..5b069641c4 100644 --- a/src/Umbraco.Core/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Core/Runtime/CoreRuntime.cs @@ -139,7 +139,7 @@ namespace Umbraco.Core.Runtime // there should be none, really - this is here "just in case" Compose(composition); - // acquire the main domain + // acquire the main domain - if this fails then anything that should be registered with MainDom will not operate AcquireMainDom(mainDom); // determine our runtime level @@ -218,13 +218,13 @@ namespace Umbraco.Core.Runtime IOHelper.SetRootDirectory(path); } - private void AcquireMainDom(MainDom mainDom) + private bool AcquireMainDom(MainDom mainDom) { using (var timer = ProfilingLogger.DebugDuration("Acquiring MainDom.", "Acquired.")) { try { - mainDom.Acquire(); + return mainDom.Acquire(); } catch { diff --git a/src/Umbraco.Core/RuntimeState.cs b/src/Umbraco.Core/RuntimeState.cs index 6fb8a04c0d..5d34fe70a1 100644 --- a/src/Umbraco.Core/RuntimeState.cs +++ b/src/Umbraco.Core/RuntimeState.cs @@ -97,6 +97,11 @@ namespace Umbraco.Core /// internal void EnsureApplicationUrl(HttpRequestBase request = null) { + //Fixme: This causes problems with site swap on azure because azure pre-warms a site by calling into `localhost` and when it does that + // it changes the URL to `localhost:80` which actually doesn't work for pinging itself, it only works internally in Azure. The ironic part + // about this is that this is here specifically for the slot swap scenario https://issues.umbraco.org/issue/U4-10626 + + // see U4-10626 - in some cases we want to reset the application url // (this is a simplified version of what was in 7.x) // note: should this be optional? is it expensive? diff --git a/src/Umbraco.Core/WaitHandleExtensions.cs b/src/Umbraco.Core/WaitHandleExtensions.cs index 0d840a2496..7a9294c113 100644 --- a/src/Umbraco.Core/WaitHandleExtensions.cs +++ b/src/Umbraco.Core/WaitHandleExtensions.cs @@ -23,6 +23,8 @@ namespace Umbraco.Core handle, (state, timedOut) => { + //TODO: We aren't checking if this is timed out + tcs.SetResult(null); // we take a lock here to make sure the outer method has completed setting the local variable callbackHandle. diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs index f3a520ead1..55304d257b 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs @@ -124,7 +124,7 @@ namespace Umbraco.Tests.PublishedContent _source = new TestDataSource(kits); // at last, create the complete NuCache snapshot service! - var options = new PublishedSnapshotService.Options { IgnoreLocalDb = true }; + var options = new PublishedSnapshotServiceOptions { IgnoreLocalDb = true }; _snapshotService = new PublishedSnapshotService(options, null, runtime, diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs index b66404c954..399b0c1342 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs @@ -169,7 +169,7 @@ namespace Umbraco.Tests.PublishedContent _variationAccesor = new TestVariationContextAccessor(); // at last, create the complete NuCache snapshot service! - var options = new PublishedSnapshotService.Options { IgnoreLocalDb = true }; + var options = new PublishedSnapshotServiceOptions { IgnoreLocalDb = true }; _snapshotService = new PublishedSnapshotService(options, null, runtime, diff --git a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs index 397a22fc62..c974fed99e 100644 --- a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs @@ -72,7 +72,7 @@ namespace Umbraco.Tests.Scoping protected override IPublishedSnapshotService CreatePublishedSnapshotService() { - var options = new PublishedSnapshotService.Options { IgnoreLocalDb = true }; + var options = new PublishedSnapshotServiceOptions { IgnoreLocalDb = true }; var publishedSnapshotAccessor = new UmbracoContextPublishedSnapshotAccessor(Umbraco.Web.Composing.Current.UmbracoContextAccessor); var runtimeStateMock = new Mock(); runtimeStateMock.Setup(x => x.Level).Returns(() => RuntimeLevel.Run); diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs index 3121988bfe..18ea95cd98 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs @@ -42,7 +42,7 @@ namespace Umbraco.Tests.Services protected override IPublishedSnapshotService CreatePublishedSnapshotService() { - var options = new PublishedSnapshotService.Options { IgnoreLocalDb = true }; + var options = new PublishedSnapshotServiceOptions { IgnoreLocalDb = true }; var publishedSnapshotAccessor = new UmbracoContextPublishedSnapshotAccessor(Umbraco.Web.Composing.Current.UmbracoContextAccessor); var runtimeStateMock = new Mock(); runtimeStateMock.Setup(x => x.Level).Returns(() => RuntimeLevel.Run); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs index 72fa39e7e4..f748fd555c 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs @@ -15,7 +15,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // register the NuCache published snapshot service // must register default options, required in the service ctor - composition.Register(factory => new PublishedSnapshotService.Options()); + composition.Register(factory => new PublishedSnapshotServiceOptions()); composition.SetPublishedSnapshotService(); // add the NuCache health check (hidden from type finder) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index dad9811af8..5e7c0542db 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -30,7 +30,8 @@ using File = System.IO.File; namespace Umbraco.Web.PublishedCache.NuCache { - class PublishedSnapshotService : PublishedSnapshotServiceBase + + internal class PublishedSnapshotService : PublishedSnapshotServiceBase { private readonly ServiceContext _serviceContext; private readonly IPublishedContentTypeFactory _publishedContentTypeFactory; @@ -56,7 +57,7 @@ namespace Umbraco.Web.PublishedCache.NuCache private BPlusTree _localContentDb; private BPlusTree _localMediaDb; - private readonly bool _localDbExists; + private bool _localDbExists; // define constant - determines whether to use cache when previewing // to store eg routes, property converted values, anything - caching @@ -68,7 +69,7 @@ namespace Umbraco.Web.PublishedCache.NuCache //private static int _singletonCheck; - public PublishedSnapshotService(Options options, IMainDom mainDom, IRuntimeState runtime, + public PublishedSnapshotService(PublishedSnapshotServiceOptions options, IMainDom mainDom, IRuntimeState runtime, ServiceContext serviceContext, IPublishedContentTypeFactory publishedContentTypeFactory, IdkMap idkMap, IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor, ILogger logger, IScopeProvider scopeProvider, IDocumentRepository documentRepository, IMediaRepository mediaRepository, IMemberRepository memberRepository, @@ -115,30 +116,36 @@ namespace Umbraco.Web.PublishedCache.NuCache if (options.IgnoreLocalDb == false) { var registered = mainDom.Register( - null, () => { + //"install" phase of MainDom + //this is inside of a lock in MainDom so this is guaranteed to run if MainDom was acquired and guaranteed + //to not run if MainDom wasn't acquired. + //If MainDom was not acquired, then _localContentDb and _localMediaDb will remain null which means this appdomain + //will load in published content via the DB and in that case this appdomain will probably not exist long enough to + //serve more than a page of content. + + var path = GetLocalFilesPath(); + var localContentDbPath = Path.Combine(path, "NuCache.Content.db"); + var localMediaDbPath = Path.Combine(path, "NuCache.Media.db"); + _localDbExists = File.Exists(localContentDbPath) && File.Exists(localMediaDbPath); + // if both local databases exist then GetTree will open them, else new databases will be created + _localContentDb = BTree.GetTree(localContentDbPath, _localDbExists); + _localMediaDb = BTree.GetTree(localMediaDbPath, _localDbExists); + }, + () => + { + //"release" phase of MainDom + lock (_storesLock) { - _contentStore.ReleaseLocalDb(); + _contentStore?.ReleaseLocalDb(); //null check because we could shut down before being assigned _localContentDb = null; - _mediaStore.ReleaseLocalDb(); + _mediaStore?.ReleaseLocalDb(); //null check because we could shut down before being assigned _localMediaDb = null; } }); - if (registered) - { - var path = GetLocalFilesPath(); - var localContentDbPath = Path.Combine(path, "NuCache.Content.db"); - var localMediaDbPath = Path.Combine(path, "NuCache.Media.db"); - _localDbExists = System.IO.File.Exists(localContentDbPath) && System.IO.File.Exists(localMediaDbPath); - - // if both local databases exist then GetTree will open them, else new databases will be created - _localContentDb = BTree.GetTree(localContentDbPath, _localDbExists); - _localMediaDb = BTree.GetTree(localMediaDbPath, _localDbExists); - } - // stores are created with a db so they can write to it, but they do not read from it, // stores need to be populated, happens in OnResolutionFrozen which uses _localDbExists to // figure out whether it can read the databases or it should populate them from sql @@ -251,19 +258,6 @@ namespace Umbraco.Web.PublishedCache.NuCache base.Dispose(); } - public class Options - { - // disabled: prevents the published snapshot from updating and exposing changes - // or even creating a new published snapshot to see changes, uses old cache = bad - // - //// indicates that the snapshot cache should reuse the application request cache - //// otherwise a new cache object would be created for the snapshot specifically, - //// which is the default - web boot manager uses this to optimize facades - //public bool PublishedSnapshotCacheIsApplicationRequestCache; - - public bool IgnoreLocalDb; - } - #endregion #region Local files diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotServiceOptions.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotServiceOptions.cs new file mode 100644 index 0000000000..ef9d83a802 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotServiceOptions.cs @@ -0,0 +1,28 @@ +namespace Umbraco.Web.PublishedCache.NuCache +{ + /// + /// Options class for configuring the + /// + public class PublishedSnapshotServiceOptions + { + // disabled: prevents the published snapshot from updating and exposing changes + // or even creating a new published snapshot to see changes, uses old cache = bad + // + //// indicates that the snapshot cache should reuse the application request cache + //// otherwise a new cache object would be created for the snapshot specifically, + //// which is the default - web boot manager uses this to optimize facades + //public bool PublishedSnapshotCacheIsApplicationRequestCache; + + + /// + /// If true this disables the persisted local cache files for content and media + /// + /// + /// By default this is false which means umbraco will use locally persisted cache files for reading in all published content and media on application startup. + /// The reason for this is to improve startup times because the alternative to populating the published content and media on application startup is to read + /// these values from the database. In scenarios where sites are relatively small (below a few thousand nodes) reading the content/media from the database to populate + /// the in memory cache isn't that slow and is only marginally slower than reading from the locally persisted cache files. + /// + public bool IgnoreLocalDb { get; set; } + } +} diff --git a/src/Umbraco.Web/Scheduling/KeepAlive.cs b/src/Umbraco.Web/Scheduling/KeepAlive.cs index 6dd7572b87..c07430df04 100644 --- a/src/Umbraco.Web/Scheduling/KeepAlive.cs +++ b/src/Umbraco.Web/Scheduling/KeepAlive.cs @@ -64,7 +64,7 @@ namespace Umbraco.Web.Scheduling } catch (Exception ex) { - _logger.Error(ex, "Failed (at '{UmbracoAppUrl}').", umbracoAppUrl); + _logger.Error(ex, "Keep alive failed (at '{UmbracoAppUrl}').", umbracoAppUrl); } } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 41a3393fa4..331b2ac7d0 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -224,6 +224,7 @@ + From f3bfc1ffb27618f129d1fdb2e05872e14b3a9f15 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 18 Jul 2019 16:18:11 +1000 Subject: [PATCH 0017/1001] Puts back UmbracoAntiForgeryAdditionalDataProvider for backwards compat reasons but it is not used --- ...mbracoAntiForgeryAdditionalDataProvider.cs | 91 +++++++++++++++++++ src/Umbraco.Web/Umbraco.Web.csproj | 1 + 2 files changed, 92 insertions(+) create mode 100644 src/Umbraco.Web/Security/UmbracoAntiForgeryAdditionalDataProvider.cs diff --git a/src/Umbraco.Web/Security/UmbracoAntiForgeryAdditionalDataProvider.cs b/src/Umbraco.Web/Security/UmbracoAntiForgeryAdditionalDataProvider.cs new file mode 100644 index 0000000000..12d7cce753 --- /dev/null +++ b/src/Umbraco.Web/Security/UmbracoAntiForgeryAdditionalDataProvider.cs @@ -0,0 +1,91 @@ +using System; +using Umbraco.Web.Mvc; +using Umbraco.Core; +using System.Web.Helpers; +using System.Web; +using Newtonsoft.Json; +using System.ComponentModel; + +namespace Umbraco.Web.Security +{ + [Obsolete("This is no longer used and will be removed from the codebase in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] + public class UmbracoAntiForgeryAdditionalDataProvider : IAntiForgeryAdditionalDataProvider + { + private readonly IAntiForgeryAdditionalDataProvider _defaultProvider; + + /// + /// Constructor, allows wrapping a default provider + /// + /// + public UmbracoAntiForgeryAdditionalDataProvider(IAntiForgeryAdditionalDataProvider defaultProvider) + { + _defaultProvider = defaultProvider; + } + + public string GetAdditionalData(HttpContextBase context) + { + return JsonConvert.SerializeObject(new AdditionalData + { + Stamp = DateTime.UtcNow.Ticks, + //this value will be here if this is a BeginUmbracoForms form + Ufprt = context.Items["ufprt"]?.ToString(), + //if there was a wrapped provider, add it's value to the json, else just a static value + WrappedValue = _defaultProvider?.GetAdditionalData(context) ?? "default" + }); + } + + public bool ValidateAdditionalData(HttpContextBase context, string additionalData) + { + if (!additionalData.DetectIsJson()) + return false; //must be json + + AdditionalData json; + try + { + json = JsonConvert.DeserializeObject(additionalData); + } + catch + { + return false; //couldn't parse + } + + if (json.Stamp == default) return false; + + //if there was a wrapped provider, validate it, else validate the static value + var validateWrapped = _defaultProvider?.ValidateAdditionalData(context, json.WrappedValue) ?? json.WrappedValue == "default"; + if (!validateWrapped) + return false; + + var ufprtRequest = context.Request["ufprt"]?.ToString(); + + //if the custom BeginUmbracoForms route value is not there, then it's nothing more to validate + if (ufprtRequest.IsNullOrWhiteSpace() && json.Ufprt.IsNullOrWhiteSpace()) + return true; + + //if one or the other is null then something is wrong + if (!ufprtRequest.IsNullOrWhiteSpace() && json.Ufprt.IsNullOrWhiteSpace()) return false; + if (ufprtRequest.IsNullOrWhiteSpace() && !json.Ufprt.IsNullOrWhiteSpace()) return false; + + if (!UmbracoHelper.DecryptAndValidateEncryptedRouteString(json.Ufprt, out var additionalDataParts)) + return false; + + if (!UmbracoHelper.DecryptAndValidateEncryptedRouteString(ufprtRequest, out var requestParts)) + return false; + + //ensure they all match + return additionalDataParts.Count == requestParts.Count + && additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Controller] == requestParts[RenderRouteHandler.ReservedAdditionalKeys.Controller] + && additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Action] == requestParts[RenderRouteHandler.ReservedAdditionalKeys.Action] + && additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Area] == requestParts[RenderRouteHandler.ReservedAdditionalKeys.Area]; + } + + internal class AdditionalData + { + public string Ufprt { get; set; } + public long Stamp { get; set; } + public string WrappedValue { get; set; } + } + + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 2fb54a2c80..01524e5962 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -341,6 +341,7 @@ + From 6544b5c27242be6a80bd1da7b05a774521a75e75 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Thu, 18 Jul 2019 13:03:14 +0100 Subject: [PATCH 0018/1001] Update src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs Co-Authored-By: Bjarke Berg --- .../V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs index 5f49680d89..1b6597a660 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs @@ -85,7 +85,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_1_0 if (exceptions.Any()) { - throw new AggregateException(exceptions); + throw new AggregateException("One or more errors related to unexpected data in grid values occurred.", exceptions); } Context.AddPostMigration(); From 929c84822aaabe9437756c5272d13b32245d3f6a Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 19 Jul 2019 15:18:00 +1000 Subject: [PATCH 0019/1001] Fixes the Children ext method and adds unit tests --- .../PublishedContentExtensions.cs | 4 - .../PublishedContent/NuCacheChildrenTests.cs | 306 ++++++++++++------ 2 files changed, 209 insertions(+), 101 deletions(-) diff --git a/src/Umbraco.Core/PublishedContentExtensions.cs b/src/Umbraco.Core/PublishedContentExtensions.cs index f220f307d6..e09e76d06a 100644 --- a/src/Umbraco.Core/PublishedContentExtensions.cs +++ b/src/Umbraco.Core/PublishedContentExtensions.cs @@ -111,10 +111,6 @@ namespace Umbraco.Core /// public static IEnumerable Children(this IPublishedContent content, string culture = null) { - // invariant has invariant value (whatever the requested culture) - if (!content.ContentType.VariesByCulture() && culture != "*") - culture = ""; - // handle context culture for variant if (culture == null) culture = VariationContextAccessor?.VariationContext?.Culture ?? ""; diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs index f3a520ead1..cc6bcab036 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs @@ -151,117 +151,139 @@ namespace Umbraco.Tests.PublishedContent Mock.Get(factory).Setup(x => x.GetInstance(typeof(IVariationContextAccessor))).Returns(_variationAccesor); } + private IEnumerable GetNestedVariantKits() + { + var paths = new Dictionary { { -1, "-1" } }; + + //1x variant (root) + yield return CreateVariantKit(1, -1, 1, paths); + + //1x invariant under root + yield return CreateInvariantKit(4, 1, 1, paths); + + //1x variant under root + yield return CreateVariantKit(7, 1, 4, paths); + + //2x mixed under invariant + yield return CreateVariantKit(10, 4, 1, paths); + yield return CreateInvariantKit(11, 4, 2, paths); + + //2x mixed under variant + yield return CreateVariantKit(12, 7, 1, paths); + yield return CreateInvariantKit(13, 7, 2, paths); + } + private IEnumerable GetInvariantKits() { var paths = new Dictionary { { -1, "-1" } }; - ContentNodeKit CreateKit(int id, int parentId, int sortOrder) + yield return CreateInvariantKit(1, -1, 1, paths); + yield return CreateInvariantKit(2, -1, 2, paths); + yield return CreateInvariantKit(3, -1, 3, paths); + + yield return CreateInvariantKit(4, 1, 1, paths); + yield return CreateInvariantKit(5, 1, 2, paths); + yield return CreateInvariantKit(6, 1, 3, paths); + + yield return CreateInvariantKit(7, 2, 3, paths); + yield return CreateInvariantKit(8, 2, 2, paths); + yield return CreateInvariantKit(9, 2, 1, paths); + + yield return CreateInvariantKit(10, 3, 1, paths); + + yield return CreateInvariantKit(11, 4, 1, paths); + yield return CreateInvariantKit(12, 4, 2, paths); + } + + private ContentNodeKit CreateInvariantKit(int id, int parentId, int sortOrder, Dictionary paths) + { + if (!paths.TryGetValue(parentId, out var parentPath)) + throw new Exception("Unknown parent."); + + var path = paths[id] = parentPath + "," + id; + var level = path.Count(x => x == ','); + var now = DateTime.Now; + + return new ContentNodeKit { - if (!paths.TryGetValue(parentId, out var parentPath)) - throw new Exception("Unknown parent."); - - var path = paths[id] = parentPath + "," + id; - var level = path.Count(x => x == ','); - var now = DateTime.Now; - - return new ContentNodeKit + ContentTypeId = _contentTypeInvariant.Id, + Node = new ContentNode(id, Guid.NewGuid(), level, path, sortOrder, parentId, DateTime.Now, 0), + DraftData = null, + PublishedData = new ContentData { - ContentTypeId = _contentTypeInvariant.Id, - Node = new ContentNode(id, Guid.NewGuid(), level, path, sortOrder, parentId, DateTime.Now, 0), - DraftData = null, - PublishedData = new ContentData - { - Name = "N" + id, - Published = true, - TemplateId = 0, - VersionId = 1, - VersionDate = now, - WriterId = 0, - Properties = new Dictionary(), - CultureInfos = new Dictionary() - } - }; - } - - yield return CreateKit(1, -1, 1); - yield return CreateKit(2, -1, 2); - yield return CreateKit(3, -1, 3); - - yield return CreateKit(4, 1, 1); - yield return CreateKit(5, 1, 2); - yield return CreateKit(6, 1, 3); - - yield return CreateKit(7, 2, 3); - yield return CreateKit(8, 2, 2); - yield return CreateKit(9, 2, 1); - - yield return CreateKit(10, 3, 1); - - yield return CreateKit(11, 4, 1); - yield return CreateKit(12, 4, 2); + Name = "N" + id, + Published = true, + TemplateId = 0, + VersionId = 1, + VersionDate = now, + WriterId = 0, + Properties = new Dictionary(), + CultureInfos = new Dictionary() + } + }; } private IEnumerable GetVariantKits() { var paths = new Dictionary { { -1, "-1" } }; - Dictionary GetCultureInfos(int id, DateTime now) + yield return CreateVariantKit(1, -1, 1, paths); + yield return CreateVariantKit(2, -1, 2, paths); + yield return CreateVariantKit(3, -1, 3, paths); + + yield return CreateVariantKit(4, 1, 1, paths); + yield return CreateVariantKit(5, 1, 2, paths); + yield return CreateVariantKit(6, 1, 3, paths); + + yield return CreateVariantKit(7, 2, 3, paths); + yield return CreateVariantKit(8, 2, 2, paths); + yield return CreateVariantKit(9, 2, 1, paths); + + yield return CreateVariantKit(10, 3, 1, paths); + + yield return CreateVariantKit(11, 4, 1, paths); + yield return CreateVariantKit(12, 4, 2, paths); + } + + private static Dictionary GetCultureInfos(int id, DateTime now) + { + var en = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }; + var fr = new[] { 1, 3, 4, 6, 7, 9, 10, 12 }; + + var infos = new Dictionary(); + if (en.Contains(id)) + infos["en-US"] = new CultureVariation { Name = "N" + id + "-" + "en-US", Date = now, IsDraft = false }; + if (fr.Contains(id)) + infos["fr-FR"] = new CultureVariation { Name = "N" + id + "-" + "fr-FR", Date = now, IsDraft = false }; + return infos; + } + + private ContentNodeKit CreateVariantKit(int id, int parentId, int sortOrder, Dictionary paths) + { + if (!paths.TryGetValue(parentId, out var parentPath)) + throw new Exception("Unknown parent."); + + var path = paths[id] = parentPath + "," + id; + var level = path.Count(x => x == ','); + var now = DateTime.Now; + + return new ContentNodeKit { - var en = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }; - var fr = new[] { 1, 3, 4, 6, 7, 9, 10, 12 }; - - var infos = new Dictionary(); - if (en.Contains(id)) - infos["en-US"] = new CultureVariation { Name = "N" + id + "-" + "en-US", Date = now, IsDraft = false }; - if (fr.Contains(id)) - infos["fr-FR"] = new CultureVariation { Name = "N" + id + "-" + "fr-FR", Date = now, IsDraft = false }; - return infos; - } - - ContentNodeKit CreateKit(int id, int parentId, int sortOrder) - { - if (!paths.TryGetValue(parentId, out var parentPath)) - throw new Exception("Unknown parent."); - - var path = paths[id] = parentPath + "," + id; - var level = path.Count(x => x == ','); - var now = DateTime.Now; - - return new ContentNodeKit + ContentTypeId = _contentTypeVariant.Id, + Node = new ContentNode(id, Guid.NewGuid(), level, path, sortOrder, parentId, DateTime.Now, 0), + DraftData = null, + PublishedData = new ContentData { - ContentTypeId = _contentTypeVariant.Id, - Node = new ContentNode(id, Guid.NewGuid(), level, path, sortOrder, parentId, DateTime.Now, 0), - DraftData = null, - PublishedData = new ContentData - { - Name = "N" + id, - Published = true, - TemplateId = 0, - VersionId = 1, - VersionDate = now, - WriterId = 0, - Properties = new Dictionary(), - CultureInfos = GetCultureInfos(id, now) - } - }; - } - - yield return CreateKit(1, -1, 1); - yield return CreateKit(2, -1, 2); - yield return CreateKit(3, -1, 3); - - yield return CreateKit(4, 1, 1); - yield return CreateKit(5, 1, 2); - yield return CreateKit(6, 1, 3); - - yield return CreateKit(7, 2, 3); - yield return CreateKit(8, 2, 2); - yield return CreateKit(9, 2, 1); - - yield return CreateKit(10, 3, 1); - - yield return CreateKit(11, 4, 1); - yield return CreateKit(12, 4, 2); + Name = "N" + id, + Published = true, + TemplateId = 0, + VersionId = 1, + VersionDate = now, + WriterId = 0, + Properties = new Dictionary(), + CultureInfos = GetCultureInfos(id, now) + } + }; } private IEnumerable GetVariantWithDraftKits() @@ -660,6 +682,96 @@ namespace Umbraco.Tests.PublishedContent Assert.AreEqual(1, snapshot.Content.GetById(7).Parent?.Id); } + [Test] + public void NestedVariationChildrenTest() + { + var mixedKits = GetNestedVariantKits(); + Init(mixedKits); + + var snapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null); + _snapshotAccessor.PublishedSnapshot = snapshot; + + //TEST with en-us variation context + + _variationAccesor.VariationContext = new VariationContext("en-US"); + + var documents = snapshot.Content.GetAtRoot().ToArray(); + AssertDocuments(documents, "N1-en-US"); + + documents = snapshot.Content.GetById(1).Children().ToArray(); + AssertDocuments(documents, "N4", "N7-en-US"); + + //Get the invariant and list children, there's a variation context so it should return invariant AND en-us variants + documents = snapshot.Content.GetById(4).Children().ToArray(); + AssertDocuments(documents, "N10-en-US", "N11"); + + //Get the variant and list children, there's a variation context so it should return invariant AND en-us variants + documents = snapshot.Content.GetById(7).Children().ToArray(); + AssertDocuments(documents, "N12-en-US", "N13"); + + //TEST with fr-fr variation context + + _variationAccesor.VariationContext = new VariationContext("fr-FR"); + + documents = snapshot.Content.GetAtRoot().ToArray(); + AssertDocuments(documents, "N1-fr-FR"); + + documents = snapshot.Content.GetById(1).Children().ToArray(); + AssertDocuments(documents, "N4", "N7-fr-FR"); + + //Get the invariant and list children, there's a variation context so it should return invariant AND en-us variants + documents = snapshot.Content.GetById(4).Children().ToArray(); + AssertDocuments(documents, "N10-fr-FR", "N11"); + + //Get the variant and list children, there's a variation context so it should return invariant AND en-us variants + documents = snapshot.Content.GetById(7).Children().ToArray(); + AssertDocuments(documents, "N12-fr-FR", "N13"); + + //TEST specific cultures + + documents = snapshot.Content.GetAtRoot("fr-FR").ToArray(); + AssertDocuments(documents, "N1-fr-FR"); + + documents = snapshot.Content.GetById(1).Children("fr-FR").ToArray(); + AssertDocuments(documents, "N4", "N7-fr-FR"); //TODO: Returns invariant ... is that expected? + documents = snapshot.Content.GetById(1).Children("").ToArray(); + AssertDocuments(documents, "N4"); //Only returns invariant since that is what was requested + + documents = snapshot.Content.GetById(4).Children("fr-FR").ToArray(); + AssertDocuments(documents, "N10-fr-FR", "N11"); //TODO: Returns invariant ... is that expected? + documents = snapshot.Content.GetById(4).Children("").ToArray(); + AssertDocuments(documents, "N11"); //Only returns invariant since that is what was requested + + documents = snapshot.Content.GetById(7).Children("fr-FR").ToArray(); + AssertDocuments(documents, "N12-fr-FR", "N13"); //TODO: Returns invariant ... is that expected? + documents = snapshot.Content.GetById(7).Children("").ToArray(); + AssertDocuments(documents, "N13"); //Only returns invariant since that is what was requested + + //TEST without variation context + // This will actually convert the culture to "" which will be invariant since that's all it will know how to do + // This will return a NULL name for culture specific entities because there is no variation context + + _variationAccesor.VariationContext = null; + + documents = snapshot.Content.GetAtRoot().ToArray(); + //will return nothing because there's only variant at root + Assert.AreEqual(0, documents.Length); + //so we'll continue to getting the known variant, do not fully assert this because the Name will NULL + documents = snapshot.Content.GetAtRoot("fr-FR").ToArray(); + Assert.AreEqual(1, documents.Length); + + documents = snapshot.Content.GetById(1).Children().ToArray(); + AssertDocuments(documents, "N4"); + + //Get the invariant and list children + documents = snapshot.Content.GetById(4).Children().ToArray(); + AssertDocuments(documents, "N11"); + + //Get the variant and list children + documents = snapshot.Content.GetById(7).Children().ToArray(); + AssertDocuments(documents, "N13"); + } + [Test] public void VariantChildrenTest() { From 487e45b45bf8d74095838bd1268c809c1e3c49b3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 19 Jul 2019 15:26:31 +1000 Subject: [PATCH 0020/1001] updates note --- src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs index cc6bcab036..65736aabd4 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs @@ -733,17 +733,17 @@ namespace Umbraco.Tests.PublishedContent AssertDocuments(documents, "N1-fr-FR"); documents = snapshot.Content.GetById(1).Children("fr-FR").ToArray(); - AssertDocuments(documents, "N4", "N7-fr-FR"); //TODO: Returns invariant ... is that expected? + AssertDocuments(documents, "N4", "N7-fr-FR"); //NOTE: Returns invariant, this is expected documents = snapshot.Content.GetById(1).Children("").ToArray(); AssertDocuments(documents, "N4"); //Only returns invariant since that is what was requested documents = snapshot.Content.GetById(4).Children("fr-FR").ToArray(); - AssertDocuments(documents, "N10-fr-FR", "N11"); //TODO: Returns invariant ... is that expected? + AssertDocuments(documents, "N10-fr-FR", "N11"); //NOTE: Returns invariant, this is expected documents = snapshot.Content.GetById(4).Children("").ToArray(); AssertDocuments(documents, "N11"); //Only returns invariant since that is what was requested documents = snapshot.Content.GetById(7).Children("fr-FR").ToArray(); - AssertDocuments(documents, "N12-fr-FR", "N13"); //TODO: Returns invariant ... is that expected? + AssertDocuments(documents, "N12-fr-FR", "N13"); //NOTE: Returns invariant, this is expected documents = snapshot.Content.GetById(7).Children("").ToArray(); AssertDocuments(documents, "N13"); //Only returns invariant since that is what was requested From b07df634224443152e8d4aebce967c12fe2c39f4 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 19 Jul 2019 15:49:39 +1000 Subject: [PATCH 0021/1001] Adds some better notes to the api docs --- src/Umbraco.Core/PublishedContentExtensions.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/PublishedContentExtensions.cs b/src/Umbraco.Core/PublishedContentExtensions.cs index e09e76d06a..921883b822 100644 --- a/src/Umbraco.Core/PublishedContentExtensions.cs +++ b/src/Umbraco.Core/PublishedContentExtensions.cs @@ -103,11 +103,23 @@ namespace Umbraco.Core /// Gets the children of the content item. /// /// The content item. - /// The specific culture to get the url children for. If null is used the current culture is used (Default is null). + /// + /// The specific culture to get the url children for. Default is null which will use the current culture in + /// /// /// Gets children that are available for the specified culture. /// Children are sorted by their sortOrder. - /// The '*' culture and supported and returns everything. + /// + /// For culture, + /// if null is used the current culture is used. + /// If an empty string is used only invariant children are returned. + /// If "*" is used all children are returned. + /// + /// + /// If a variant culture is specified or there is a current culture in the then the Children returned + /// will include both the variant children matching the culture AND the invariant children because the invariant children flow with the current culture. + /// However, if an empty string is specified only invariant children are returned. + /// /// public static IEnumerable Children(this IPublishedContent content, string culture = null) { From 02f49a003931a82d0a827e641206ab15d5acd150 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 19 Jul 2019 16:00:42 +1000 Subject: [PATCH 0022/1001] Cherry picks fix for 5894 and makes adjustments --- .../PublishedCache/NuCache/PublishedSnapshotService.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index dad9811af8..75f6d2f5c3 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -1056,8 +1056,10 @@ namespace Umbraco.Web.PublishedCache.NuCache : _dataSource.GetTypeMediaSources(scope, refreshedIds).ToArray(); _mediaStore.UpdateContentTypes(removedIds, typesA, kits); - _mediaStore.UpdateContentTypes(CreateContentTypes(PublishedItemType.Media, otherIds.ToArray()).ToArray()); - _mediaStore.NewContentTypes(CreateContentTypes(PublishedItemType.Media, newIds.ToArray()).ToArray()); + if (!otherIds.IsCollectionEmpty()) + _mediaStore.UpdateContentTypes(CreateContentTypes(PublishedItemType.Media, otherIds.ToArray()).ToArray()); + if (!newIds.IsCollectionEmpty()) + _mediaStore.NewContentTypes(CreateContentTypes(PublishedItemType.Media, newIds.ToArray()).ToArray()); scope.Complete(); } } From 47c3e3a79f4c40a53390b1a3867a13bbf50e895b Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 19 Jul 2019 11:20:31 +0200 Subject: [PATCH 0023/1001] https://github.com/umbraco/Umbraco-CMS/issues/5921 - AB#1794 - Remove trashed nodes from sql when building xml for previews --- .../Repositories/ContentRepository.cs | 2745 +++++++++-------- 1 file changed, 1373 insertions(+), 1372 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index be5cc20cb6..a0b211b6b2 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -1,1372 +1,1373 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Globalization; -using System.Linq; -using System.Xml; -using System.Xml.Linq; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.Models.EntityBase; -using Umbraco.Core.Models.Membership; -using Umbraco.Core.Models.Rdbms; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; -using Umbraco.Core.Persistence.Factories; -using Umbraco.Core.Persistence.Querying; -using Umbraco.Core.Cache; -using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Core.Persistence.SqlSyntax; -using Umbraco.Core.Persistence.UnitOfWork; - -namespace Umbraco.Core.Persistence.Repositories -{ - /// - /// Represents a repository for doing CRUD operations for - /// - internal class ContentRepository : RecycleBinRepository, IContentRepository - { - private readonly IContentTypeRepository _contentTypeRepository; - private readonly ITemplateRepository _templateRepository; - private readonly ITagRepository _tagRepository; - private readonly ContentPreviewRepository _contentPreviewRepository; - private readonly ContentXmlRepository _contentXmlRepository; - private readonly PermissionRepository _permissionRepository; - private readonly ContentByGuidReadRepository _contentByGuidReadRepository; - - public ContentRepository(IScopeUnitOfWork work, CacheHelper cacheHelper, ILogger logger, ISqlSyntaxProvider syntaxProvider, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, IContentSection contentSection) - : base(work, cacheHelper, logger, syntaxProvider, contentSection) - { - if (contentTypeRepository == null) throw new ArgumentNullException("contentTypeRepository"); - if (templateRepository == null) throw new ArgumentNullException("templateRepository"); - if (tagRepository == null) throw new ArgumentNullException("tagRepository"); - _contentTypeRepository = contentTypeRepository; - _templateRepository = templateRepository; - _tagRepository = tagRepository; - _contentPreviewRepository = new ContentPreviewRepository(work, CacheHelper.NoCache, logger, syntaxProvider); - _contentXmlRepository = new ContentXmlRepository(work, CacheHelper.NoCache, logger, syntaxProvider); - _permissionRepository = new PermissionRepository(UnitOfWork, cacheHelper, Logger, SqlSyntax); - _contentByGuidReadRepository = new ContentByGuidReadRepository(this, work, cacheHelper, logger, syntaxProvider); - EnsureUniqueNaming = true; - } - - public bool EnsureUniqueNaming { get; set; } - - #region Overrides of RepositoryBase - - protected override IContent PerformGet(int id) - { - var sql = GetBaseQuery(BaseQueryType.FullSingle) - .Where(GetBaseWhereClause(), new { Id = id }) - .Where(x => x.Newest, SqlSyntax) - .OrderByDescending(x => x.VersionDate, SqlSyntax); - - var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); - - if (dto == null) - return null; - - var content = CreateContentFromDto(dto, sql); - - return content; - } - - protected override IEnumerable PerformGetAll(params int[] ids) - { - Func translate = s => - { - if (ids.Any()) - { - s.Where("umbracoNode.id in (@ids)", new { ids }); - } - //we only want the newest ones with this method - s.Where(x => x.Newest, SqlSyntax); - return s; - }; - - var sqlBaseFull = GetBaseQuery(BaseQueryType.FullMultiple); - var sqlBaseIds = GetBaseQuery(BaseQueryType.Ids); - - return ProcessQuery(translate(sqlBaseFull), new PagingSqlQuery(translate(sqlBaseIds))); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlBaseFull = GetBaseQuery(BaseQueryType.FullMultiple); - var sqlBaseIds = GetBaseQuery(BaseQueryType.Ids); - - Func, Sql> translate = (translator) => - { - return translator.Translate() - .Where(x => x.Newest, SqlSyntax) - .OrderByDescending(x => x.VersionDate, SqlSyntax) - .OrderBy(x => x.SortOrder, SqlSyntax); - }; - - var translatorFull = new SqlTranslator(sqlBaseFull, query); - var translatorIds = new SqlTranslator(sqlBaseIds, query); - - return ProcessQuery(translate(translatorFull), new PagingSqlQuery(translate(translatorIds))); - } - - #endregion - - #region Overrides of PetaPocoRepositoryBase - - /// - /// Returns the base query to return Content - /// - /// - /// - /// - /// Content queries will differ depending on what needs to be returned: - /// * FullSingle: When querying for a single document, this will include the Outer join to fetch the content item's published version info - /// * FullMultiple: When querying for multiple documents, this will exclude the Outer join to fetch the content item's published version info - this info would need to be fetched separately - /// * Ids: This would essentially be the same as FullMultiple however the columns specified will only return the Ids for the documents - /// * Count: A query to return the count for documents - /// - protected override Sql GetBaseQuery(BaseQueryType queryType) - { - var sql = new Sql(); - sql.Select(queryType == BaseQueryType.Count ? "COUNT(*)" : (queryType == BaseQueryType.Ids ? "cmsDocument.nodeId" : "*")) - .From(SqlSyntax) - .InnerJoin(SqlSyntax) - .On(SqlSyntax, left => left.VersionId, right => right.VersionId) - .InnerJoin(SqlSyntax) - .On(SqlSyntax, left => left.NodeId, right => right.NodeId) - .InnerJoin(SqlSyntax) - .On(SqlSyntax, left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.ContentTypeId); - //TODO: IF we want to enable querying on content type information this will need to be joined - //.InnerJoin(SqlSyntax) - //.On(SqlSyntax, left => left.ContentTypeId, right => right.NodeId, SqlSyntax); - - if (queryType == BaseQueryType.FullSingle) - { - //The only reason we apply this left outer join is to be able to pull back the DocumentPublishedReadOnlyDto - //information with the entire data set, so basically this will get both the latest document and also it's published - //version if it has one. When performing a count or when retrieving Ids like in paging, this is unecessary - //and causes huge performance overhead for the SQL server, especially when sorting the result. - //We also don't include this outer join when querying for multiple entities since it is much faster to fetch this information - //in a separate query. For a single entity this is ok. - - var sqlx = string.Format("LEFT OUTER JOIN {0} {1} ON ({1}.{2}={0}.{2} AND {1}.{3}=1)", - SqlSyntax.GetQuotedTableName("cmsDocument"), - SqlSyntax.GetQuotedTableName("cmsDocument2"), - SqlSyntax.GetQuotedColumnName("nodeId"), - SqlSyntax.GetQuotedColumnName("published")); - - // cannot do this because PetaPoco does not know how to alias the table - //.LeftOuterJoin() - //.On(left => left.NodeId, right => right.NodeId) - // so have to rely on writing our own SQL - sql.Append(sqlx /*, new { @published = true }*/); - } - - sql.Where(x => x.NodeObjectType == NodeObjectTypeId, SqlSyntax); - - return sql; - } - - protected override Sql GetBaseQuery(bool isCount) - { - return GetBaseQuery(isCount ? BaseQueryType.Count : BaseQueryType.FullSingle); - } - - protected override string GetBaseWhereClause() - { - return "umbracoNode.id = @Id"; - } - - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - "DELETE FROM umbracoRedirectUrl WHERE contentKey IN (SELECT uniqueID FROM umbracoNode WHERE id = @Id)", - "DELETE FROM cmsTask WHERE nodeId = @Id", - "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @Id", - "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @Id", - "DELETE FROM umbracoUserStartNode WHERE startNode = @Id", - "UPDATE umbracoUserGroup SET startContentId = NULL WHERE startContentId = @Id", - "DELETE FROM umbracoRelation WHERE parentId = @Id", - "DELETE FROM umbracoRelation WHERE childId = @Id", - "DELETE FROM cmsTagRelationship WHERE nodeId = @Id", - "DELETE FROM umbracoDomains WHERE domainRootStructureID = @Id", - "DELETE FROM cmsDocument WHERE nodeId = @Id", - "DELETE FROM cmsPropertyData WHERE contentNodeId = @Id", - "DELETE FROM cmsPreviewXml WHERE nodeId = @Id", - "DELETE FROM cmsContentVersion WHERE ContentId = @Id", - "DELETE FROM cmsContentXml WHERE nodeId = @Id", - "DELETE FROM cmsContent WHERE nodeId = @Id", - "DELETE FROM umbracoAccess WHERE nodeId = @Id", - "DELETE FROM umbracoNode WHERE id = @Id" - }; - return list; - } - - protected override Guid NodeObjectTypeId - { - get { return Constants.ObjectTypes.DocumentGuid; } - } - - #endregion - - #region Overrides of VersionableRepositoryBase - - public void RebuildXmlStructures(Func serializer, int groupSize = 200, IEnumerable contentTypeIds = null) - { - // the previous way of doing this was to run it all in one big transaction, - // and to bulk-insert groups of xml rows - which works, until the transaction - // times out - and besides, because v7 transactions are ReadCommited, it does - // not bring much safety - so this reverts to updating each record individually, - // and it may be slower in the end, but should be more resilient. - - var contentTypeIdsA = contentTypeIds == null ? new int[0] : contentTypeIds.ToArray(); - - Func translate = (bId, sql) => - { - if (contentTypeIdsA.Length > 0) - { - sql.WhereIn(x => x.ContentTypeId, contentTypeIdsA, SqlSyntax); - } - - sql - .Where(x => x.NodeId > bId && x.Trashed == false, SqlSyntax) - .Where(x => x.Published, SqlSyntax) - .OrderBy(x => x.NodeId, SqlSyntax); - - return sql; - }; - - var baseId = 0; - - while (true) - { - // get the next group of nodes - var sqlFull = translate(baseId, GetBaseQuery(BaseQueryType.FullMultiple)); - var sqlIds = translate(baseId, GetBaseQuery(BaseQueryType.Ids)); - - var xmlItems = ProcessQuery(SqlSyntax.SelectTop(sqlFull, groupSize), new PagingSqlQuery(SqlSyntax.SelectTop(sqlIds, groupSize))) - .Select(x => new ContentXmlDto { NodeId = x.Id, Xml = serializer(x).ToString() }) - .ToList(); - - // no more nodes, break - if (xmlItems.Count == 0) break; - - foreach (var xmlItem in xmlItems) - { - try - { - // should happen in most cases, then it tries to insert, and it should work - // unless the node has been deleted, and we just report the exception - Database.InsertOrUpdate(xmlItem); - } - catch (Exception e) - { - Logger.Error("Could not rebuild XML for nodeId=" + xmlItem.NodeId, e); - } - } - baseId = xmlItems[xmlItems.Count - 1].NodeId; - } - - //now delete the items that shouldn't be there - var sqlAllIds = translate(0, GetBaseQuery(BaseQueryType.Ids)); - var allContentIds = Database.Fetch(sqlAllIds); - var docObjectType = NodeObjectTypeId; - var xmlIdsQuery = new Sql() - .Select("DISTINCT cmsContentXml.nodeId") - .From(SqlSyntax) - .InnerJoin(SqlSyntax) - .On(SqlSyntax, left => left.NodeId, right => right.NodeId); - - if (contentTypeIdsA.Length > 0) - { - xmlIdsQuery.InnerJoin(SqlSyntax) - .On(SqlSyntax, left => left.NodeId, right => right.NodeId) - .InnerJoin(SqlSyntax) - .On(SqlSyntax, left => left.NodeId, right => right.ContentTypeId) - .WhereIn(x => x.ContentTypeId, contentTypeIdsA, SqlSyntax); - } - - xmlIdsQuery.Where(dto => dto.NodeObjectType == docObjectType, SqlSyntax); - - var allXmlIds = Database.Fetch(xmlIdsQuery); - - var toRemove = allXmlIds.Except(allContentIds).ToArray(); - if (toRemove.Length > 0) - { - foreach (var idGroup in toRemove.InGroupsOf(2000)) - { - Database.Execute("DELETE FROM cmsContentXml WHERE nodeId IN (@ids)", new { ids = idGroup }); - } - } - - } - - public override IEnumerable GetAllVersions(int id) - { - Func translate = s => - { - return s.Where(GetBaseWhereClause(), new {Id = id}) - .OrderByDescending(x => x.VersionDate, SqlSyntax); - }; - - var sqlFull = translate(GetBaseQuery(BaseQueryType.FullMultiple)); - var sqlIds = translate(GetBaseQuery(BaseQueryType.Ids)); - - return ProcessQuery(sqlFull, new PagingSqlQuery(sqlIds), true, includeAllVersions:true); - } - - public override IContent GetByVersion(Guid versionId) - { - var sql = GetBaseQuery(BaseQueryType.FullSingle); - //TODO: cmsContentVersion.VersionId has a Unique Index constraint applied, seems silly then to also add OrderByDescending since it would be impossible to return more than one. - sql.Where("cmsContentVersion.VersionId = @VersionId", new { VersionId = versionId }); - sql.OrderByDescending(x => x.VersionDate, SqlSyntax); - - var dto = Database.Fetch(sql).FirstOrDefault(); - - if (dto == null) - return null; - - var content = CreateContentFromDto(dto, sql); - - return content; - } - - public override void DeleteVersion(Guid versionId) - { - var sql = new Sql() - .Select("*") - .From(SqlSyntax) - .InnerJoin(SqlSyntax) - .On(SqlSyntax, left => left.VersionId, right => right.VersionId) - .Where(x => x.VersionId == versionId, SqlSyntax) - .Where(x => x.Newest != true, SqlSyntax); - var dto = Database.Fetch(sql).FirstOrDefault(); - - if (dto == null) return; - - using (var transaction = Database.GetTransaction()) - { - PerformDeleteVersion(dto.NodeId, versionId); - - transaction.Complete(); - } - } - - public override void DeleteVersions(int id, DateTime versionDate) - { - var sql = new Sql() - .Select("*") - .From() - .InnerJoin() - .On(left => left.VersionId, right => right.VersionId) - .Where(x => x.NodeId == id) - .Where(x => x.VersionDate < versionDate) - .Where(x => x.Newest != true); - var list = Database.Fetch(sql); - if (list.Any() == false) return; - - using (var transaction = Database.GetTransaction()) - { - foreach (var dto in list) - { - PerformDeleteVersion(id, dto.VersionId); - } - - transaction.Complete(); - } - } - - protected override void PerformDeleteVersion(int id, Guid versionId) - { - Database.Delete("WHERE nodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); - Database.Delete("WHERE contentNodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); - Database.Delete("WHERE ContentId = @Id AND VersionId = @VersionId", new { Id = id, VersionId = versionId }); - Database.Delete("WHERE nodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); - } - - #endregion - - #region Unit of Work Implementation - - protected override void PersistDeletedItem(IContent entity) - { - //We need to clear out all access rules but we need to do this in a manual way since - // nothing in that table is joined to a content id - var subQuery = new Sql() - .Select("umbracoAccessRule.accessId") - .From(SqlSyntax) - .InnerJoin(SqlSyntax) - .On(SqlSyntax, left => left.AccessId, right => right.Id) - .Where(dto => dto.NodeId == entity.Id); - Database.Execute(SqlSyntax.GetDeleteSubquery("umbracoAccessRule", "accessId", subQuery)); - - //now let the normal delete clauses take care of everything else - base.PersistDeletedItem(entity); - } - - protected override void PersistNewItem(IContent entity) - { - ((Content)entity).AddingEntity(); - - //ensure the default template is assigned - if (entity.Template == null) - { - entity.Template = entity.ContentType.DefaultTemplate; - } - - //Ensure unique name on the same level - entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name); - - //Ensure that strings don't contain characters that are invalid in XML - entity.SanitizeEntityPropertiesForXmlStorage(); - - var factory = new ContentFactory(NodeObjectTypeId, entity.Id); - var dto = factory.BuildDto(entity); - - //NOTE Should the logic below have some kind of fallback for empty parent ids ? - //Logic for setting Path, Level and SortOrder - var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); - var level = parent.Level + 1; - var maxSortOrder = Database.ExecuteScalar( - "SELECT coalesce(max(sortOrder),-1) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", - new { /*ParentId =*/ entity.ParentId, NodeObjectType = NodeObjectTypeId }); - var sortOrder = maxSortOrder + 1; - - //Create the (base) node data - umbracoNode - var nodeDto = dto.ContentVersionDto.ContentDto.NodeDto; - nodeDto.Path = parent.Path; - nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); - nodeDto.SortOrder = sortOrder; - - // note: - // there used to be a check on Database.IsNew(nodeDto) here to either Insert or Update, - // but I cannot figure out what was the point, as the node should obviously be new if - // we reach that point - removed. - - // see if there's a reserved identifier for this unique id - var sql = new Sql("SELECT id FROM umbracoNode WHERE uniqueID=@0 AND nodeObjectType=@1", nodeDto.UniqueId, Constants.ObjectTypes.IdReservationGuid); - var id = Database.ExecuteScalar(sql); - if (id > 0) - { - nodeDto.NodeId = id; - Database.Update(nodeDto); - } - else - { - Database.Insert(nodeDto); - } - - //Update with new correct path - nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); - nodeDto.ValidatePathWithException(); - Database.Update(nodeDto); - - //Update entity with correct values - entity.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set - entity.Path = nodeDto.Path; - entity.SortOrder = sortOrder; - entity.Level = level; - - //Create the Content specific data - cmsContent - var contentDto = dto.ContentVersionDto.ContentDto; - contentDto.NodeId = nodeDto.NodeId; - Database.Insert(contentDto); - - //Create the first version - cmsContentVersion - //Assumes a new Version guid and Version date (modified date) has been set - var contentVersionDto = dto.ContentVersionDto; - contentVersionDto.NodeId = nodeDto.NodeId; - Database.Insert(contentVersionDto); - - //Create the Document specific data for this version - cmsDocument - //Assumes a new Version guid has been generated - dto.NodeId = nodeDto.NodeId; - Database.Insert(dto); - - //Create the PropertyData for this version - cmsPropertyData - var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); - var propertyDataDtos = propertyFactory.BuildDto(entity.Properties); - var keyDictionary = new Dictionary(); - - //Add Properties - foreach (var propertyDataDto in propertyDataDtos) - { - var primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); - keyDictionary.Add(propertyDataDto.PropertyTypeId, primaryKey); - } - - //Update Properties with its newly set Id - foreach (var property in entity.Properties) - { - property.Id = keyDictionary[property.PropertyTypeId]; - } - - //lastly, check if we are a creating a published version , then update the tags table - if (entity.Published) - { - UpdatePropertyTags(entity, _tagRepository); - } - - // published => update published version infos, else leave it blank - if (entity.Published) - { - dto.DocumentPublishedReadOnlyDto = new DocumentPublishedReadOnlyDto - { - VersionId = dto.VersionId, - VersionDate = dto.UpdateDate, - Newest = true, - NodeId = dto.NodeId, - Published = true - }; - ((Content) entity).PublishedVersionGuid = dto.VersionId; - ((Content) entity).PublishedDate = dto.UpdateDate; - } - - entity.ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(IContent entity) - { - var publishedState = ((Content)entity).PublishedState; - - //check if we need to make any database changes at all - if (entity.RequiresSaving(publishedState) == false) - { - entity.ResetDirtyProperties(); - return; - } - - //check if we need to create a new version - bool shouldCreateNewVersion = entity.ShouldCreateNewVersion(publishedState); - if (shouldCreateNewVersion) - { - //Updates Modified date and Version Guid - ((Content)entity).UpdatingEntity(); - } - else - { - if (entity.IsPropertyDirty("UpdateDate") == false || entity.UpdateDate == default(DateTime)) - entity.UpdateDate = DateTime.Now; - } - - //Ensure unique name on the same level - entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id); - - //Ensure that strings don't contain characters that are invalid in XML - entity.SanitizeEntityPropertiesForXmlStorage(); - - //Look up parent to get and set the correct Path and update SortOrder if ParentId has changed - if (entity.IsPropertyDirty("ParentId")) - { - var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); - entity.Path = string.Concat(parent.Path, ",", entity.Id); - entity.Level = parent.Level + 1; - var maxSortOrder = - Database.ExecuteScalar( - "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", - new { ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId }); - entity.SortOrder = maxSortOrder + 1; - - //Question: If we move a node, should we update permissions to inherit from the new parent if the parent has permissions assigned? - // if we do that, then we'd need to propogate permissions all the way downward which might not be ideal for many people. - // Gonna just leave it as is for now, and not re-propogate permissions. - } - - var factory = new ContentFactory(NodeObjectTypeId, entity.Id); - //Look up Content entry to get Primary for updating the DTO - var contentDto = Database.SingleOrDefault("WHERE nodeId = @Id", new { Id = entity.Id }); - factory.SetPrimaryKey(contentDto.PrimaryKey); - var dto = factory.BuildDto(entity); - - //Updates the (base) node data - umbracoNode - var nodeDto = dto.ContentVersionDto.ContentDto.NodeDto; - nodeDto.ValidatePathWithException(); - var o = Database.Update(nodeDto); - - //Only update this DTO if the contentType has actually changed - if (contentDto.ContentTypeId != entity.ContentTypeId) - { - //Create the Content specific data - cmsContent - var newContentDto = dto.ContentVersionDto.ContentDto; - Database.Update(newContentDto); - } - - //a flag that we'll use later to create the tags in the tag db table - var publishedStateChanged = false; - - //If Published state has changed then previous versions should have their publish state reset. - //If state has been changed to unpublished the previous versions publish state should also be reset. - //if (((ICanBeDirty)entity).IsPropertyDirty("Published") && (entity.Published || publishedState == PublishedState.Unpublished)) - if (entity.ShouldClearPublishedFlagForPreviousVersions(publishedState, shouldCreateNewVersion)) - { - //TODO: This perf can be improved, it could easily be UPDATE WHERE.... (one SQL call instead of many) - var publishedDocs = Database.Fetch("WHERE nodeId = @Id AND published = @IsPublished", new { Id = entity.Id, IsPublished = true }); - foreach (var doc in publishedDocs) - { - var docDto = doc; - docDto.Published = false; - Database.Update(docDto); - } - - //this is a newly published version so we'll update the tags table too (end of this method) - publishedStateChanged = true; - } - - //Look up (newest) entries by id in cmsDocument table to set newest = false - //TODO: This perf can be improved, it could easily be UPDATE WHERE.... (one SQL call instead of many) - var documentDtos = Database.Fetch("WHERE nodeId = @Id AND newest = @IsNewest", new { Id = entity.Id, IsNewest = true }); - foreach (var documentDto in documentDtos) - { - var docDto = documentDto; - docDto.Newest = false; - Database.Update(docDto); - } - - var contentVersionDto = dto.ContentVersionDto; - if (shouldCreateNewVersion) - { - //Create a new version - cmsContentVersion - //Assumes a new Version guid and Version date (modified date) has been set - Database.Insert(contentVersionDto); - //Create the Document specific data for this version - cmsDocument - //Assumes a new Version guid has been generated - Database.Insert(dto); - } - else - { - //In order to update the ContentVersion we need to retrieve its primary key id - var contentVerDto = Database.SingleOrDefault("WHERE VersionId = @Version", new { Version = entity.Version }); - if (contentVerDto != null) - { - contentVersionDto.Id = contentVerDto.Id; - Database.Update(contentVersionDto); - } - - Database.Update(dto); - } - - //Create the PropertyData for this version - cmsPropertyData - var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); - var propertyDataDtos = propertyFactory.BuildDto(entity.Properties); - var keyDictionary = new Dictionary(); - - //Add Properties - foreach (var propertyDataDto in propertyDataDtos) - { - if (shouldCreateNewVersion == false && propertyDataDto.Id > 0) - { - Database.Update(propertyDataDto); - } - else - { - int primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); - keyDictionary.Add(propertyDataDto.PropertyTypeId, primaryKey); - } - } - - //Update Properties with its newly set Id - if (keyDictionary.Any()) - { - foreach (var property in entity.Properties) - { - if (keyDictionary.ContainsKey(property.PropertyTypeId) == false) continue; - - property.Id = keyDictionary[property.PropertyTypeId]; - } - } - - //lastly, check if we are a newly published version and then update the tags table - if (publishedStateChanged && entity.Published) - { - UpdatePropertyTags(entity, _tagRepository); - } - else if (publishedStateChanged && (entity.Trashed || entity.Published == false)) - { - //it's in the trash or not published remove all entity tags - ClearEntityTags(entity, _tagRepository); - } - - // published => update published version infos, - // else if unpublished then clear published version infos - if (entity.Published) - { - dto.DocumentPublishedReadOnlyDto = new DocumentPublishedReadOnlyDto - { - VersionId = dto.VersionId, - VersionDate = dto.UpdateDate, - Newest = true, - NodeId = dto.NodeId, - Published = true - }; - ((Content) entity).PublishedVersionGuid = dto.VersionId; - ((Content) entity).PublishedDate = dto.UpdateDate; - } - else if (publishedStateChanged) - { - dto.DocumentPublishedReadOnlyDto = new DocumentPublishedReadOnlyDto - { - VersionId = default (Guid), - VersionDate = default (DateTime), - Newest = false, - NodeId = dto.NodeId, - Published = false - }; - ((Content) entity).PublishedVersionGuid = default(Guid); - ((Content) entity).PublishedDate = default (DateTime); - } - - entity.ResetDirtyProperties(); - } - - - #endregion - - #region Implementation of IContentRepository - - public IEnumerable GetByPublishedVersion(IQuery query) - { - Func, Sql> translate = t => - { - return t.Translate() - .Where(x => x.Published, SqlSyntax) - .OrderBy(x => x.Level, SqlSyntax) - .OrderBy(x => x.SortOrder, SqlSyntax); - }; - - // we WANT to return contents in top-down order, ie parents should come before children - // ideal would be pure xml "document order" which can be achieved with: - // ORDER BY substring(path, 1, len(path) - charindex(',', reverse(path))), sortOrder - // but that's probably an overkill - sorting by level,sortOrder should be enough - - var sqlFull = GetBaseQuery(BaseQueryType.FullMultiple); - var translatorFull = new SqlTranslator(sqlFull, query); - var sqlIds = GetBaseQuery(BaseQueryType.Ids); - var translatorIds = new SqlTranslator(sqlIds, query); - - return ProcessQuery(translate(translatorFull), new PagingSqlQuery(translate(translatorIds)), true); - } - - public IEnumerable GetBlueprints(IQuery query) - { - Func, Sql> translate = t => t.Translate(); - - var sqlFull = GetBaseQuery(BaseQueryType.FullMultiple); - var translatorFull = new SqlTranslator(sqlFull, query); - var sqlIds = GetBaseQuery(BaseQueryType.Ids); - var translatorIds = new SqlTranslator(sqlIds, query); - - return ProcessQuery(translate(translatorFull), new PagingSqlQuery(translate(translatorIds)), true); - } - - /// - /// This builds the Xml document used for the XML cache - /// - /// - public XmlDocument BuildXmlCache() - { - //TODO: This is what we should do , but converting to use XDocument would be breaking unless we convert - // to XmlDocument at the end of this, but again, this would be bad for memory... though still not nearly as - // bad as what is happening before! - // We'll keep using XmlDocument for now though, but XDocument xml generation is much faster: - // https://blogs.msdn.microsoft.com/codejunkie/2008/10/08/xmldocument-vs-xelement-performance/ - // I think we already have code in here to convert XDocument to XmlDocument but in case we don't here - // it is: https://blogs.msdn.microsoft.com/marcelolr/2009/03/13/fast-way-to-convert-xmldocument-into-xdocument/ - - //// Prepare an XmlDocument with an appropriate inline DTD to match - //// the expected content - //var parent = new XElement("root", new XAttribute("id", "-1")); - //var xmlDoc = new XDocument( - // new XDocumentType("root", null, null, DocumentType.GenerateDtd()), - // parent); - - var xmlDoc = new XmlDocument(); - var doctype = xmlDoc.CreateDocumentType("root", null, null, - ApplicationContext.Current.Services.ContentTypeService.GetContentTypesDtd()); - xmlDoc.AppendChild(doctype); - var parent = xmlDoc.CreateElement("root"); - var pIdAtt = xmlDoc.CreateAttribute("id"); - pIdAtt.Value = "-1"; - parent.Attributes.Append(pIdAtt); - xmlDoc.AppendChild(parent); - - //Ensure that only nodes that have published versions are selected - var sql = string.Format(@"select umbracoNode.id, umbracoNode.parentID, umbracoNode.sortOrder, cmsContentXml.{0}, umbracoNode.{1} from umbracoNode -inner join cmsContentXml on cmsContentXml.nodeId = umbracoNode.id and umbracoNode.nodeObjectType = @type -where umbracoNode.id in (select cmsDocument.nodeId from cmsDocument where cmsDocument.published = 1) -order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", - SqlSyntax.GetQuotedColumnName("xml"), - SqlSyntax.GetQuotedColumnName("level"), - SqlSyntax.GetQuotedColumnName("level")); - - XmlElement last = null; - - //NOTE: Query creates a reader - does not load all into memory - foreach (var row in Database.Query(sql, new { type = NodeObjectTypeId })) - { - string parentId = ((int)row.parentID).ToInvariantString(); - string xml = row.xml; - int sortOrder = row.sortOrder; - - //if the parentid is changing - if (last != null && last.GetAttribute("parentID") != parentId) - { - parent = xmlDoc.GetElementById(parentId); - if (parent == null) - { - //Need to short circuit here, if the parent is not there it means that the parent is unpublished - // and therefore the child is not published either so cannot be included in the xml cache - continue; - } - } - - var xmlDocFragment = xmlDoc.CreateDocumentFragment(); - xmlDocFragment.InnerXml = xml; - - last = (XmlElement)parent.AppendChild(xmlDocFragment); - - // fix sortOrder - see notes in UpdateSortOrder - last.Attributes["sortOrder"].Value = sortOrder.ToInvariantString(); - } - - return xmlDoc; - - } - - public XmlDocument BuildPreviewXmlCache() - { - var xmlDoc = new XmlDocument(); - var doctype = xmlDoc.CreateDocumentType("root", null, null, - ApplicationContext.Current.Services.ContentTypeService.GetContentTypesDtd()); - xmlDoc.AppendChild(doctype); - var parent = xmlDoc.CreateElement("root"); - var pIdAtt = xmlDoc.CreateAttribute("id"); - pIdAtt.Value = "-1"; - parent.Attributes.Append(pIdAtt); - xmlDoc.AppendChild(parent); - - //Ensure that only nodes that have published versions are selected - var sql = string.Format(@"select umbracoNode.id, umbracoNode.parentID, umbracoNode.sortOrder, cmsPreviewXml.{0}, umbracoNode.{1} from umbracoNode -inner join cmsPreviewXml on cmsPreviewXml.nodeId = umbracoNode.id and umbracoNode.nodeObjectType = @type -inner join cmsDocument on cmsPreviewXml.versionId = cmsDocument.versionId and cmsDocument.newest=1 -order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", - SqlSyntax.GetQuotedColumnName("xml"), - SqlSyntax.GetQuotedColumnName("level"), - SqlSyntax.GetQuotedColumnName("level")); - - XmlElement last = null; - - //NOTE: Query creates a reader - does not load all into memory - foreach (var row in Database.Query(sql, new { type = NodeObjectTypeId })) - { - string parentId = ((int)row.parentID).ToInvariantString(); - string xml = row.xml; - int sortOrder = row.sortOrder; - - //if the parentid is changing - if (last != null && last.GetAttribute("parentID") != parentId) - { - parent = xmlDoc.GetElementById(parentId); - if (parent == null) - { - //Need to short circuit here, if the parent is not there it means that the parent is unpublished - // and therefore the child is not published either so cannot be included in the xml cache - continue; - } - } - - var xmlDocFragment = xmlDoc.CreateDocumentFragment(); - xmlDocFragment.InnerXml = xml; - - last = (XmlElement)parent.AppendChild(xmlDocFragment); - - // fix sortOrder - see notes in UpdateSortOrder - last.Attributes["sortOrder"].Value = sortOrder.ToInvariantString(); - } - - return xmlDoc; - - } - - public int CountPublished(string contentTypeAlias = null) - { - if (contentTypeAlias.IsNullOrWhiteSpace()) - { - var sql = GetBaseQuery(true).Where(x => x.Trashed == false) - .Where(x => x.Published == true); - return Database.ExecuteScalar(sql); - } - else - { - var sql = GetBaseQuery(true).Where(x => x.Trashed == false) - .Where(x => x.Published == true) - .Where(x => x.Alias == contentTypeAlias); - return Database.ExecuteScalar(sql); - } - } - - public void ReplaceContentPermissions(EntityPermissionSet permissionSet) - { - _permissionRepository.ReplaceEntityPermissions(permissionSet); - } - - public void ClearPublished(IContent content) - { - var sql = "UPDATE cmsDocument SET published=0 WHERE nodeId=@id AND published=1"; - Database.Execute(sql, new {id = content.Id}); - } - - /// - /// Assigns a single permission to the current content item for the specified user group ids - /// - /// - /// - /// - public void AssignEntityPermission(IContent entity, char permission, IEnumerable groupIds) - { - _permissionRepository.AssignEntityPermission(entity, permission, groupIds); - } - - /// - /// Gets the explicit list of permissions for the content item - /// - /// - /// - public EntityPermissionCollection GetPermissionsForEntity(int entityId) - { - return _permissionRepository.GetPermissionsForEntity(entityId); - } - - /// - /// Adds/updates content/published xml - /// - /// - /// - public void AddOrUpdateContentXml(IContent content, Func xml) - { - _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); - } - - /// - /// Used to add/update a permission for a content item - /// - /// - public void AddOrUpdatePermissions(ContentPermissionSet permission) - { - _permissionRepository.AddOrUpdate(permission); - } - - /// - /// Used to remove the content xml for a content item - /// - /// - public void DeleteContentXml(IContent content) - { - _contentXmlRepository.Delete(new ContentXmlEntity(content)); - } - - /// - /// Adds/updates preview xml - /// - /// - /// - public void AddOrUpdatePreviewXml(IContent content, Func xml) - { - _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(content, xml)); - } - - /// - /// Gets paged content results - /// - /// Query to excute - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Flag to indicate when ordering by system field - /// Search text filter - /// An Enumerable list of objects - public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter = null) - { - - //NOTE: This uses the GetBaseQuery method but that does not take into account the required 'newest' field which is - // what we always require for a paged result, so we'll ensure it's included in the filter - - var filterSql = new Sql().Append("AND (cmsDocument.newest = 1)"); - if (filter != null) - { - foreach (var filterClause in filter.GetWhereClauses()) - { - filterSql.Append(string.Format("AND ({0})", filterClause.Item1), filterClause.Item2); - } - } - - Func> filterCallback = () => new Tuple(filterSql.SQL, filterSql.Arguments); - - return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, - new Tuple("cmsDocument", "nodeId"), - (sqlFull, pagingSqlQuery) => ProcessQuery(sqlFull, pagingSqlQuery), orderBy, orderDirection, orderBySystemField, - filterCallback); - - } - - #endregion - - #region IRecycleBinRepository members - - protected override int RecycleBinId - { - get { return Constants.System.RecycleBinContent; } - } - - #endregion - - #region Read Repository implementation for GUID keys - public IContent Get(Guid id) - { - return _contentByGuidReadRepository.Get(id); - } - - IEnumerable IReadRepository.GetAll(params Guid[] ids) - { - return _contentByGuidReadRepository.GetAll(ids); - } - - public bool Exists(Guid id) - { - return _contentByGuidReadRepository.Exists(id); - } - - /// - /// A reading repository purely for looking up by GUID - /// - /// - /// TODO: This is ugly and to fix we need to decouple the IRepositoryQueryable -> IRepository -> IReadRepository which should all be separate things! - /// Then we can do the same thing with repository instances and we wouldn't need to leave all these methods as not implemented because we wouldn't need to implement them - /// - private class ContentByGuidReadRepository : PetaPocoRepositoryBase - { - private readonly ContentRepository _outerRepo; - - public ContentByGuidReadRepository(ContentRepository outerRepo, - IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) - : base(work, cache, logger, sqlSyntax) - { - _outerRepo = outerRepo; - } - - protected override IContent PerformGet(Guid id) - { - var sql = _outerRepo.GetBaseQuery(BaseQueryType.FullSingle) - .Where(GetBaseWhereClause(), new { Id = id }) - .Where(x => x.Newest, SqlSyntax) - .OrderByDescending(x => x.VersionDate, SqlSyntax); - - var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); - - if (dto == null) - return null; - - var content = _outerRepo.CreateContentFromDto(dto, sql); - - return content; - } - - protected override IEnumerable PerformGetAll(params Guid[] ids) - { - Func translate = s => - { - if (ids.Any()) - { - s.Where("umbracoNode.uniqueID in (@ids)", new { ids }); - } - //we only want the newest ones with this method - s.Where(x => x.Newest, SqlSyntax); - return s; - }; - - var sqlBaseFull = _outerRepo.GetBaseQuery(BaseQueryType.FullMultiple); - var sqlBaseIds = _outerRepo.GetBaseQuery(BaseQueryType.Ids); - - return _outerRepo.ProcessQuery(translate(sqlBaseFull), new PagingSqlQuery(translate(sqlBaseIds))); - } - - protected override Sql GetBaseQuery(bool isCount) - { - return _outerRepo.GetBaseQuery(isCount); - } - - protected override string GetBaseWhereClause() - { - return "umbracoNode.uniqueID = @Id"; - } - - protected override Guid NodeObjectTypeId - { - get { return _outerRepo.NodeObjectTypeId; } - } - - #region Not needed to implement - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - throw new NotImplementedException(); - } - protected override IEnumerable GetDeleteClauses() - { - throw new NotImplementedException(); - } - protected override void PersistNewItem(IContent entity) - { - throw new NotImplementedException(); - } - protected override void PersistUpdatedItem(IContent entity) - { - throw new NotImplementedException(); - } - #endregion - } - #endregion - - protected override string GetDatabaseFieldNameForOrderBy(string orderBy) - { - //Some custom ones - switch (orderBy.ToUpperInvariant()) - { - case "NAME": - return "cmsDocument.text"; - case "UPDATER": - //TODO: This isn't going to work very nicely because it's going to order by ID, not by letter - return "cmsDocument.documentUser"; - } - - return base.GetDatabaseFieldNameForOrderBy(orderBy); - } - - /// - /// This is the underlying method that processes most queries for this repository - /// - /// - /// The FullMultiple SQL without the outer join to return all data required to create an IContent excluding it's published state data which this will query separately - /// - /// - /// The Id SQL without the outer join to just return all document ids - used to process the properties for the content item - /// - /// - /// - /// Generally when querying for content we only want to return the most recent version of the content item, however in some cases like when - /// we want to return all versions of a content item, we can't simply return the latest - /// - /// - private IEnumerable ProcessQuery(Sql sqlFull, PagingSqlQuery pagingSqlQuery, bool withCache = false, bool includeAllVersions = false) - { - // fetch returns a list so it's ok to iterate it in this method - var dtos = Database.Fetch(sqlFull); - if (dtos.Count == 0) return Enumerable.Empty(); - - //Go and get all of the published version data separately for this data, this is because when we are querying - //for multiple content items we don't include the outer join to fetch this data in the same query because - //it is insanely slow. Instead we just fetch the published version data separately in one query. - - //we need to parse the original SQL statement and reduce the columns to just cmsDocument.nodeId so that we can use - // the statement to go get the published data for all of the items by using an inner join - var parsedOriginalSql = "SELECT cmsDocument.nodeId " + sqlFull.SQL.Substring(sqlFull.SQL.IndexOf("FROM", StringComparison.Ordinal)); - //now remove everything from an Orderby clause and beyond - if (parsedOriginalSql.InvariantContains("ORDER BY ")) - { - parsedOriginalSql = parsedOriginalSql.Substring(0, parsedOriginalSql.LastIndexOf("ORDER BY ", StringComparison.Ordinal)); - } - - //order by update date DESC, if there is corrupted published flags we only want the latest! - var publishedSql = new Sql(@"SELECT cmsDocument.nodeId, cmsDocument.published, cmsDocument.versionId, cmsDocument.updateDate, cmsDocument.newest -FROM cmsDocument INNER JOIN cmsContentVersion ON cmsContentVersion.VersionId = cmsDocument.versionId -WHERE cmsDocument.published = 1 AND cmsDocument.nodeId IN -(" + parsedOriginalSql + @") -ORDER BY cmsContentVersion.id DESC -", sqlFull.Arguments); - - //go and get the published version data, we do a Query here and not a Fetch so we are - //not allocating a whole list to memory just to allocate another list in memory since - //we are assigning this data to a keyed collection for fast lookup below - var publishedData = Database.Query(publishedSql); - var publishedDataCollection = new DocumentPublishedReadOnlyDtoCollection(); - foreach (var publishedDto in publishedData) - { - //double check that there's no corrupt db data, there should only be a single published item - if (publishedDataCollection.Contains(publishedDto.NodeId) == false) - publishedDataCollection.Add(publishedDto); - } - - //This is a tuple list identifying if the content item came from the cache or not - var content = new List>(); - var defs = new DocumentDefinitionCollection(includeAllVersions); - var templateIds = new List(); - - //track the looked up content types, even though the content types are cached - // they still need to be deep cloned out of the cache and we don't want to add - // the overhead of deep cloning them on every item in this loop - var contentTypes = new Dictionary(); - - foreach (var dto in dtos) - { - DocumentPublishedReadOnlyDto publishedDto; - publishedDataCollection.TryGetValue(dto.NodeId, out publishedDto); - - // if the cache contains the published version, use it - if (withCache) - { - var cached = IsolatedCache.GetCacheItem(GetCacheIdKey(dto.NodeId)); - //only use this cached version if the dto returned is also the publish version, they must match and be teh same version - if (cached != null && cached.Version == dto.VersionId && cached.Published && dto.Published) - { - content.Add(new Tuple(cached, true)); - continue; - } - } - - // else, need to fetch from the database - // content type repository is full-cache so OK to get each one independently - - IContentType contentType; - if (contentTypes.ContainsKey(dto.ContentVersionDto.ContentDto.ContentTypeId)) - { - contentType = contentTypes[dto.ContentVersionDto.ContentDto.ContentTypeId]; - } - else - { - contentType = _contentTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); - contentTypes[dto.ContentVersionDto.ContentDto.ContentTypeId] = contentType; - } - - // track the definition and if it's successfully added or updated then processed - if (defs.AddOrUpdate(new DocumentDefinition(dto, contentType))) - { - // assign template - if (dto.TemplateId.HasValue && dto.TemplateId.Value > 0) - templateIds.Add(dto.TemplateId.Value); - - content.Add(new Tuple(ContentFactory.BuildEntity(dto, contentType, publishedDto), false)); - } - } - - // load all required templates in 1 query - var templates = _templateRepository.GetAll(templateIds.ToArray()) - .ToDictionary(x => x.Id, x => x); - - // load all properties for all documents from database in 1 query - var propertyData = GetPropertyCollection(pagingSqlQuery, defs); - - // assign template and property data - foreach (var contentItem in content) - { - var cc = contentItem.Item1; - var fromCache = contentItem.Item2; - - //if this has come from cache, we do not need to build up it's structure - if (fromCache) continue; - - var def = defs[includeAllVersions ? (ValueType)cc.Version : cc.Id]; - - ITemplate template = null; - if (def.DocumentDto.TemplateId.HasValue) - templates.TryGetValue(def.DocumentDto.TemplateId.Value, out template); // else null - cc.Template = template; - if (propertyData.ContainsKey(cc.Version)) - { - cc.Properties = propertyData[cc.Version]; - } - else - { - throw new InvalidOperationException($"No property data found for version: '{cc.Version}'."); - } - - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - cc.ResetDirtyProperties(false); - } - - return content.Select(x => x.Item1).ToArray(); - } - - /// - /// Private method to create a content object from a DocumentDto, which is used by Get and GetByVersion. - /// - /// - /// - /// - private IContent CreateContentFromDto(DocumentDto dto, Sql docSql) - { - var contentType = _contentTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); - - var content = ContentFactory.BuildEntity(dto, contentType); - - //Check if template id is set on DocumentDto, and get ITemplate if it is. - if (dto.TemplateId.HasValue && dto.TemplateId.Value > 0) - { - content.Template = _templateRepository.Get(dto.TemplateId.Value); - } - - var docDef = new DocumentDefinition(dto, contentType); - - var properties = GetPropertyCollection(docSql, new[] { docDef }); - - content.Properties = properties[dto.VersionId]; - - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - ((Entity)content).ResetDirtyProperties(false); - return content; - } - - private string EnsureUniqueNodeName(int parentId, string nodeName, int id = 0) - { - if (EnsureUniqueNaming == false) - return nodeName; - - var names = Database.Fetch("SELECT id, text AS name FROM umbracoNode WHERE nodeObjectType=@objectType AND parentId=@parentId", - new { objectType = NodeObjectTypeId, parentId }); - - return SimilarNodeName.GetUniqueName(names, id, nodeName); - } - - /// - /// Dispose disposable properties - /// - /// - /// Ensure the unit of work is disposed - /// - protected override void DisposeResources() - { - _contentTypeRepository.Dispose(); - _templateRepository.Dispose(); - _tagRepository.Dispose(); - _contentPreviewRepository.Dispose(); - _contentXmlRepository.Dispose(); - } - - /// - /// A keyed collection for fast lookup when retrieving a separate list of published version data - /// - private class DocumentPublishedReadOnlyDtoCollection : KeyedCollection - { - protected override int GetKeyForItem(DocumentPublishedReadOnlyDto item) - { - return item.NodeId; - } - - public bool TryGetValue(int key, out DocumentPublishedReadOnlyDto val) - { - if (Dictionary == null) - { - val = null; - return false; - } - return Dictionary.TryGetValue(key, out val); - } - } - } -} +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using System.Linq; +using System.Xml; +using System.Xml.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.Factories; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Cache; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.Persistence.UnitOfWork; + +namespace Umbraco.Core.Persistence.Repositories +{ + /// + /// Represents a repository for doing CRUD operations for + /// + internal class ContentRepository : RecycleBinRepository, IContentRepository + { + private readonly IContentTypeRepository _contentTypeRepository; + private readonly ITemplateRepository _templateRepository; + private readonly ITagRepository _tagRepository; + private readonly ContentPreviewRepository _contentPreviewRepository; + private readonly ContentXmlRepository _contentXmlRepository; + private readonly PermissionRepository _permissionRepository; + private readonly ContentByGuidReadRepository _contentByGuidReadRepository; + + public ContentRepository(IScopeUnitOfWork work, CacheHelper cacheHelper, ILogger logger, ISqlSyntaxProvider syntaxProvider, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, IContentSection contentSection) + : base(work, cacheHelper, logger, syntaxProvider, contentSection) + { + if (contentTypeRepository == null) throw new ArgumentNullException("contentTypeRepository"); + if (templateRepository == null) throw new ArgumentNullException("templateRepository"); + if (tagRepository == null) throw new ArgumentNullException("tagRepository"); + _contentTypeRepository = contentTypeRepository; + _templateRepository = templateRepository; + _tagRepository = tagRepository; + _contentPreviewRepository = new ContentPreviewRepository(work, CacheHelper.NoCache, logger, syntaxProvider); + _contentXmlRepository = new ContentXmlRepository(work, CacheHelper.NoCache, logger, syntaxProvider); + _permissionRepository = new PermissionRepository(UnitOfWork, cacheHelper, Logger, SqlSyntax); + _contentByGuidReadRepository = new ContentByGuidReadRepository(this, work, cacheHelper, logger, syntaxProvider); + EnsureUniqueNaming = true; + } + + public bool EnsureUniqueNaming { get; set; } + + #region Overrides of RepositoryBase + + protected override IContent PerformGet(int id) + { + var sql = GetBaseQuery(BaseQueryType.FullSingle) + .Where(GetBaseWhereClause(), new { Id = id }) + .Where(x => x.Newest, SqlSyntax) + .OrderByDescending(x => x.VersionDate, SqlSyntax); + + var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); + + if (dto == null) + return null; + + var content = CreateContentFromDto(dto, sql); + + return content; + } + + protected override IEnumerable PerformGetAll(params int[] ids) + { + Func translate = s => + { + if (ids.Any()) + { + s.Where("umbracoNode.id in (@ids)", new { ids }); + } + //we only want the newest ones with this method + s.Where(x => x.Newest, SqlSyntax); + return s; + }; + + var sqlBaseFull = GetBaseQuery(BaseQueryType.FullMultiple); + var sqlBaseIds = GetBaseQuery(BaseQueryType.Ids); + + return ProcessQuery(translate(sqlBaseFull), new PagingSqlQuery(translate(sqlBaseIds))); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + var sqlBaseFull = GetBaseQuery(BaseQueryType.FullMultiple); + var sqlBaseIds = GetBaseQuery(BaseQueryType.Ids); + + Func, Sql> translate = (translator) => + { + return translator.Translate() + .Where(x => x.Newest, SqlSyntax) + .OrderByDescending(x => x.VersionDate, SqlSyntax) + .OrderBy(x => x.SortOrder, SqlSyntax); + }; + + var translatorFull = new SqlTranslator(sqlBaseFull, query); + var translatorIds = new SqlTranslator(sqlBaseIds, query); + + return ProcessQuery(translate(translatorFull), new PagingSqlQuery(translate(translatorIds))); + } + + #endregion + + #region Overrides of PetaPocoRepositoryBase + + /// + /// Returns the base query to return Content + /// + /// + /// + /// + /// Content queries will differ depending on what needs to be returned: + /// * FullSingle: When querying for a single document, this will include the Outer join to fetch the content item's published version info + /// * FullMultiple: When querying for multiple documents, this will exclude the Outer join to fetch the content item's published version info - this info would need to be fetched separately + /// * Ids: This would essentially be the same as FullMultiple however the columns specified will only return the Ids for the documents + /// * Count: A query to return the count for documents + /// + protected override Sql GetBaseQuery(BaseQueryType queryType) + { + var sql = new Sql(); + sql.Select(queryType == BaseQueryType.Count ? "COUNT(*)" : (queryType == BaseQueryType.Ids ? "cmsDocument.nodeId" : "*")) + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.VersionId, right => right.VersionId) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.ContentTypeId); + //TODO: IF we want to enable querying on content type information this will need to be joined + //.InnerJoin(SqlSyntax) + //.On(SqlSyntax, left => left.ContentTypeId, right => right.NodeId, SqlSyntax); + + if (queryType == BaseQueryType.FullSingle) + { + //The only reason we apply this left outer join is to be able to pull back the DocumentPublishedReadOnlyDto + //information with the entire data set, so basically this will get both the latest document and also it's published + //version if it has one. When performing a count or when retrieving Ids like in paging, this is unecessary + //and causes huge performance overhead for the SQL server, especially when sorting the result. + //We also don't include this outer join when querying for multiple entities since it is much faster to fetch this information + //in a separate query. For a single entity this is ok. + + var sqlx = string.Format("LEFT OUTER JOIN {0} {1} ON ({1}.{2}={0}.{2} AND {1}.{3}=1)", + SqlSyntax.GetQuotedTableName("cmsDocument"), + SqlSyntax.GetQuotedTableName("cmsDocument2"), + SqlSyntax.GetQuotedColumnName("nodeId"), + SqlSyntax.GetQuotedColumnName("published")); + + // cannot do this because PetaPoco does not know how to alias the table + //.LeftOuterJoin() + //.On(left => left.NodeId, right => right.NodeId) + // so have to rely on writing our own SQL + sql.Append(sqlx /*, new { @published = true }*/); + } + + sql.Where(x => x.NodeObjectType == NodeObjectTypeId, SqlSyntax); + + return sql; + } + + protected override Sql GetBaseQuery(bool isCount) + { + return GetBaseQuery(isCount ? BaseQueryType.Count : BaseQueryType.FullSingle); + } + + protected override string GetBaseWhereClause() + { + return "umbracoNode.id = @Id"; + } + + protected override IEnumerable GetDeleteClauses() + { + var list = new List + { + "DELETE FROM umbracoRedirectUrl WHERE contentKey IN (SELECT uniqueID FROM umbracoNode WHERE id = @Id)", + "DELETE FROM cmsTask WHERE nodeId = @Id", + "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @Id", + "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @Id", + "DELETE FROM umbracoUserStartNode WHERE startNode = @Id", + "UPDATE umbracoUserGroup SET startContentId = NULL WHERE startContentId = @Id", + "DELETE FROM umbracoRelation WHERE parentId = @Id", + "DELETE FROM umbracoRelation WHERE childId = @Id", + "DELETE FROM cmsTagRelationship WHERE nodeId = @Id", + "DELETE FROM umbracoDomains WHERE domainRootStructureID = @Id", + "DELETE FROM cmsDocument WHERE nodeId = @Id", + "DELETE FROM cmsPropertyData WHERE contentNodeId = @Id", + "DELETE FROM cmsPreviewXml WHERE nodeId = @Id", + "DELETE FROM cmsContentVersion WHERE ContentId = @Id", + "DELETE FROM cmsContentXml WHERE nodeId = @Id", + "DELETE FROM cmsContent WHERE nodeId = @Id", + "DELETE FROM umbracoAccess WHERE nodeId = @Id", + "DELETE FROM umbracoNode WHERE id = @Id" + }; + return list; + } + + protected override Guid NodeObjectTypeId + { + get { return Constants.ObjectTypes.DocumentGuid; } + } + + #endregion + + #region Overrides of VersionableRepositoryBase + + public void RebuildXmlStructures(Func serializer, int groupSize = 200, IEnumerable contentTypeIds = null) + { + // the previous way of doing this was to run it all in one big transaction, + // and to bulk-insert groups of xml rows - which works, until the transaction + // times out - and besides, because v7 transactions are ReadCommited, it does + // not bring much safety - so this reverts to updating each record individually, + // and it may be slower in the end, but should be more resilient. + + var contentTypeIdsA = contentTypeIds == null ? new int[0] : contentTypeIds.ToArray(); + + Func translate = (bId, sql) => + { + if (contentTypeIdsA.Length > 0) + { + sql.WhereIn(x => x.ContentTypeId, contentTypeIdsA, SqlSyntax); + } + + sql + .Where(x => x.NodeId > bId && x.Trashed == false, SqlSyntax) + .Where(x => x.Published, SqlSyntax) + .OrderBy(x => x.NodeId, SqlSyntax); + + return sql; + }; + + var baseId = 0; + + while (true) + { + // get the next group of nodes + var sqlFull = translate(baseId, GetBaseQuery(BaseQueryType.FullMultiple)); + var sqlIds = translate(baseId, GetBaseQuery(BaseQueryType.Ids)); + + var xmlItems = ProcessQuery(SqlSyntax.SelectTop(sqlFull, groupSize), new PagingSqlQuery(SqlSyntax.SelectTop(sqlIds, groupSize))) + .Select(x => new ContentXmlDto { NodeId = x.Id, Xml = serializer(x).ToString() }) + .ToList(); + + // no more nodes, break + if (xmlItems.Count == 0) break; + + foreach (var xmlItem in xmlItems) + { + try + { + // should happen in most cases, then it tries to insert, and it should work + // unless the node has been deleted, and we just report the exception + Database.InsertOrUpdate(xmlItem); + } + catch (Exception e) + { + Logger.Error("Could not rebuild XML for nodeId=" + xmlItem.NodeId, e); + } + } + baseId = xmlItems[xmlItems.Count - 1].NodeId; + } + + //now delete the items that shouldn't be there + var sqlAllIds = translate(0, GetBaseQuery(BaseQueryType.Ids)); + var allContentIds = Database.Fetch(sqlAllIds); + var docObjectType = NodeObjectTypeId; + var xmlIdsQuery = new Sql() + .Select("DISTINCT cmsContentXml.nodeId") + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId); + + if (contentTypeIdsA.Length > 0) + { + xmlIdsQuery.InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.ContentTypeId) + .WhereIn(x => x.ContentTypeId, contentTypeIdsA, SqlSyntax); + } + + xmlIdsQuery.Where(dto => dto.NodeObjectType == docObjectType, SqlSyntax); + + var allXmlIds = Database.Fetch(xmlIdsQuery); + + var toRemove = allXmlIds.Except(allContentIds).ToArray(); + if (toRemove.Length > 0) + { + foreach (var idGroup in toRemove.InGroupsOf(2000)) + { + Database.Execute("DELETE FROM cmsContentXml WHERE nodeId IN (@ids)", new { ids = idGroup }); + } + } + + } + + public override IEnumerable GetAllVersions(int id) + { + Func translate = s => + { + return s.Where(GetBaseWhereClause(), new {Id = id}) + .OrderByDescending(x => x.VersionDate, SqlSyntax); + }; + + var sqlFull = translate(GetBaseQuery(BaseQueryType.FullMultiple)); + var sqlIds = translate(GetBaseQuery(BaseQueryType.Ids)); + + return ProcessQuery(sqlFull, new PagingSqlQuery(sqlIds), true, includeAllVersions:true); + } + + public override IContent GetByVersion(Guid versionId) + { + var sql = GetBaseQuery(BaseQueryType.FullSingle); + //TODO: cmsContentVersion.VersionId has a Unique Index constraint applied, seems silly then to also add OrderByDescending since it would be impossible to return more than one. + sql.Where("cmsContentVersion.VersionId = @VersionId", new { VersionId = versionId }); + sql.OrderByDescending(x => x.VersionDate, SqlSyntax); + + var dto = Database.Fetch(sql).FirstOrDefault(); + + if (dto == null) + return null; + + var content = CreateContentFromDto(dto, sql); + + return content; + } + + public override void DeleteVersion(Guid versionId) + { + var sql = new Sql() + .Select("*") + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.VersionId, right => right.VersionId) + .Where(x => x.VersionId == versionId, SqlSyntax) + .Where(x => x.Newest != true, SqlSyntax); + var dto = Database.Fetch(sql).FirstOrDefault(); + + if (dto == null) return; + + using (var transaction = Database.GetTransaction()) + { + PerformDeleteVersion(dto.NodeId, versionId); + + transaction.Complete(); + } + } + + public override void DeleteVersions(int id, DateTime versionDate) + { + var sql = new Sql() + .Select("*") + .From() + .InnerJoin() + .On(left => left.VersionId, right => right.VersionId) + .Where(x => x.NodeId == id) + .Where(x => x.VersionDate < versionDate) + .Where(x => x.Newest != true); + var list = Database.Fetch(sql); + if (list.Any() == false) return; + + using (var transaction = Database.GetTransaction()) + { + foreach (var dto in list) + { + PerformDeleteVersion(id, dto.VersionId); + } + + transaction.Complete(); + } + } + + protected override void PerformDeleteVersion(int id, Guid versionId) + { + Database.Delete("WHERE nodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); + Database.Delete("WHERE contentNodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); + Database.Delete("WHERE ContentId = @Id AND VersionId = @VersionId", new { Id = id, VersionId = versionId }); + Database.Delete("WHERE nodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); + } + + #endregion + + #region Unit of Work Implementation + + protected override void PersistDeletedItem(IContent entity) + { + //We need to clear out all access rules but we need to do this in a manual way since + // nothing in that table is joined to a content id + var subQuery = new Sql() + .Select("umbracoAccessRule.accessId") + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.AccessId, right => right.Id) + .Where(dto => dto.NodeId == entity.Id); + Database.Execute(SqlSyntax.GetDeleteSubquery("umbracoAccessRule", "accessId", subQuery)); + + //now let the normal delete clauses take care of everything else + base.PersistDeletedItem(entity); + } + + protected override void PersistNewItem(IContent entity) + { + ((Content)entity).AddingEntity(); + + //ensure the default template is assigned + if (entity.Template == null) + { + entity.Template = entity.ContentType.DefaultTemplate; + } + + //Ensure unique name on the same level + entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name); + + //Ensure that strings don't contain characters that are invalid in XML + entity.SanitizeEntityPropertiesForXmlStorage(); + + var factory = new ContentFactory(NodeObjectTypeId, entity.Id); + var dto = factory.BuildDto(entity); + + //NOTE Should the logic below have some kind of fallback for empty parent ids ? + //Logic for setting Path, Level and SortOrder + var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); + var level = parent.Level + 1; + var maxSortOrder = Database.ExecuteScalar( + "SELECT coalesce(max(sortOrder),-1) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", + new { /*ParentId =*/ entity.ParentId, NodeObjectType = NodeObjectTypeId }); + var sortOrder = maxSortOrder + 1; + + //Create the (base) node data - umbracoNode + var nodeDto = dto.ContentVersionDto.ContentDto.NodeDto; + nodeDto.Path = parent.Path; + nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); + nodeDto.SortOrder = sortOrder; + + // note: + // there used to be a check on Database.IsNew(nodeDto) here to either Insert or Update, + // but I cannot figure out what was the point, as the node should obviously be new if + // we reach that point - removed. + + // see if there's a reserved identifier for this unique id + var sql = new Sql("SELECT id FROM umbracoNode WHERE uniqueID=@0 AND nodeObjectType=@1", nodeDto.UniqueId, Constants.ObjectTypes.IdReservationGuid); + var id = Database.ExecuteScalar(sql); + if (id > 0) + { + nodeDto.NodeId = id; + Database.Update(nodeDto); + } + else + { + Database.Insert(nodeDto); + } + + //Update with new correct path + nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); + nodeDto.ValidatePathWithException(); + Database.Update(nodeDto); + + //Update entity with correct values + entity.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set + entity.Path = nodeDto.Path; + entity.SortOrder = sortOrder; + entity.Level = level; + + //Create the Content specific data - cmsContent + var contentDto = dto.ContentVersionDto.ContentDto; + contentDto.NodeId = nodeDto.NodeId; + Database.Insert(contentDto); + + //Create the first version - cmsContentVersion + //Assumes a new Version guid and Version date (modified date) has been set + var contentVersionDto = dto.ContentVersionDto; + contentVersionDto.NodeId = nodeDto.NodeId; + Database.Insert(contentVersionDto); + + //Create the Document specific data for this version - cmsDocument + //Assumes a new Version guid has been generated + dto.NodeId = nodeDto.NodeId; + Database.Insert(dto); + + //Create the PropertyData for this version - cmsPropertyData + var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); + var propertyDataDtos = propertyFactory.BuildDto(entity.Properties); + var keyDictionary = new Dictionary(); + + //Add Properties + foreach (var propertyDataDto in propertyDataDtos) + { + var primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); + keyDictionary.Add(propertyDataDto.PropertyTypeId, primaryKey); + } + + //Update Properties with its newly set Id + foreach (var property in entity.Properties) + { + property.Id = keyDictionary[property.PropertyTypeId]; + } + + //lastly, check if we are a creating a published version , then update the tags table + if (entity.Published) + { + UpdatePropertyTags(entity, _tagRepository); + } + + // published => update published version infos, else leave it blank + if (entity.Published) + { + dto.DocumentPublishedReadOnlyDto = new DocumentPublishedReadOnlyDto + { + VersionId = dto.VersionId, + VersionDate = dto.UpdateDate, + Newest = true, + NodeId = dto.NodeId, + Published = true + }; + ((Content) entity).PublishedVersionGuid = dto.VersionId; + ((Content) entity).PublishedDate = dto.UpdateDate; + } + + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IContent entity) + { + var publishedState = ((Content)entity).PublishedState; + + //check if we need to make any database changes at all + if (entity.RequiresSaving(publishedState) == false) + { + entity.ResetDirtyProperties(); + return; + } + + //check if we need to create a new version + bool shouldCreateNewVersion = entity.ShouldCreateNewVersion(publishedState); + if (shouldCreateNewVersion) + { + //Updates Modified date and Version Guid + ((Content)entity).UpdatingEntity(); + } + else + { + if (entity.IsPropertyDirty("UpdateDate") == false || entity.UpdateDate == default(DateTime)) + entity.UpdateDate = DateTime.Now; + } + + //Ensure unique name on the same level + entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id); + + //Ensure that strings don't contain characters that are invalid in XML + entity.SanitizeEntityPropertiesForXmlStorage(); + + //Look up parent to get and set the correct Path and update SortOrder if ParentId has changed + if (entity.IsPropertyDirty("ParentId")) + { + var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; + var maxSortOrder = + Database.ExecuteScalar( + "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", + new { ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId }); + entity.SortOrder = maxSortOrder + 1; + + //Question: If we move a node, should we update permissions to inherit from the new parent if the parent has permissions assigned? + // if we do that, then we'd need to propogate permissions all the way downward which might not be ideal for many people. + // Gonna just leave it as is for now, and not re-propogate permissions. + } + + var factory = new ContentFactory(NodeObjectTypeId, entity.Id); + //Look up Content entry to get Primary for updating the DTO + var contentDto = Database.SingleOrDefault("WHERE nodeId = @Id", new { Id = entity.Id }); + factory.SetPrimaryKey(contentDto.PrimaryKey); + var dto = factory.BuildDto(entity); + + //Updates the (base) node data - umbracoNode + var nodeDto = dto.ContentVersionDto.ContentDto.NodeDto; + nodeDto.ValidatePathWithException(); + var o = Database.Update(nodeDto); + + //Only update this DTO if the contentType has actually changed + if (contentDto.ContentTypeId != entity.ContentTypeId) + { + //Create the Content specific data - cmsContent + var newContentDto = dto.ContentVersionDto.ContentDto; + Database.Update(newContentDto); + } + + //a flag that we'll use later to create the tags in the tag db table + var publishedStateChanged = false; + + //If Published state has changed then previous versions should have their publish state reset. + //If state has been changed to unpublished the previous versions publish state should also be reset. + //if (((ICanBeDirty)entity).IsPropertyDirty("Published") && (entity.Published || publishedState == PublishedState.Unpublished)) + if (entity.ShouldClearPublishedFlagForPreviousVersions(publishedState, shouldCreateNewVersion)) + { + //TODO: This perf can be improved, it could easily be UPDATE WHERE.... (one SQL call instead of many) + var publishedDocs = Database.Fetch("WHERE nodeId = @Id AND published = @IsPublished", new { Id = entity.Id, IsPublished = true }); + foreach (var doc in publishedDocs) + { + var docDto = doc; + docDto.Published = false; + Database.Update(docDto); + } + + //this is a newly published version so we'll update the tags table too (end of this method) + publishedStateChanged = true; + } + + //Look up (newest) entries by id in cmsDocument table to set newest = false + //TODO: This perf can be improved, it could easily be UPDATE WHERE.... (one SQL call instead of many) + var documentDtos = Database.Fetch("WHERE nodeId = @Id AND newest = @IsNewest", new { Id = entity.Id, IsNewest = true }); + foreach (var documentDto in documentDtos) + { + var docDto = documentDto; + docDto.Newest = false; + Database.Update(docDto); + } + + var contentVersionDto = dto.ContentVersionDto; + if (shouldCreateNewVersion) + { + //Create a new version - cmsContentVersion + //Assumes a new Version guid and Version date (modified date) has been set + Database.Insert(contentVersionDto); + //Create the Document specific data for this version - cmsDocument + //Assumes a new Version guid has been generated + Database.Insert(dto); + } + else + { + //In order to update the ContentVersion we need to retrieve its primary key id + var contentVerDto = Database.SingleOrDefault("WHERE VersionId = @Version", new { Version = entity.Version }); + if (contentVerDto != null) + { + contentVersionDto.Id = contentVerDto.Id; + Database.Update(contentVersionDto); + } + + Database.Update(dto); + } + + //Create the PropertyData for this version - cmsPropertyData + var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); + var propertyDataDtos = propertyFactory.BuildDto(entity.Properties); + var keyDictionary = new Dictionary(); + + //Add Properties + foreach (var propertyDataDto in propertyDataDtos) + { + if (shouldCreateNewVersion == false && propertyDataDto.Id > 0) + { + Database.Update(propertyDataDto); + } + else + { + int primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); + keyDictionary.Add(propertyDataDto.PropertyTypeId, primaryKey); + } + } + + //Update Properties with its newly set Id + if (keyDictionary.Any()) + { + foreach (var property in entity.Properties) + { + if (keyDictionary.ContainsKey(property.PropertyTypeId) == false) continue; + + property.Id = keyDictionary[property.PropertyTypeId]; + } + } + + //lastly, check if we are a newly published version and then update the tags table + if (publishedStateChanged && entity.Published) + { + UpdatePropertyTags(entity, _tagRepository); + } + else if (publishedStateChanged && (entity.Trashed || entity.Published == false)) + { + //it's in the trash or not published remove all entity tags + ClearEntityTags(entity, _tagRepository); + } + + // published => update published version infos, + // else if unpublished then clear published version infos + if (entity.Published) + { + dto.DocumentPublishedReadOnlyDto = new DocumentPublishedReadOnlyDto + { + VersionId = dto.VersionId, + VersionDate = dto.UpdateDate, + Newest = true, + NodeId = dto.NodeId, + Published = true + }; + ((Content) entity).PublishedVersionGuid = dto.VersionId; + ((Content) entity).PublishedDate = dto.UpdateDate; + } + else if (publishedStateChanged) + { + dto.DocumentPublishedReadOnlyDto = new DocumentPublishedReadOnlyDto + { + VersionId = default (Guid), + VersionDate = default (DateTime), + Newest = false, + NodeId = dto.NodeId, + Published = false + }; + ((Content) entity).PublishedVersionGuid = default(Guid); + ((Content) entity).PublishedDate = default (DateTime); + } + + entity.ResetDirtyProperties(); + } + + + #endregion + + #region Implementation of IContentRepository + + public IEnumerable GetByPublishedVersion(IQuery query) + { + Func, Sql> translate = t => + { + return t.Translate() + .Where(x => x.Published, SqlSyntax) + .OrderBy(x => x.Level, SqlSyntax) + .OrderBy(x => x.SortOrder, SqlSyntax); + }; + + // we WANT to return contents in top-down order, ie parents should come before children + // ideal would be pure xml "document order" which can be achieved with: + // ORDER BY substring(path, 1, len(path) - charindex(',', reverse(path))), sortOrder + // but that's probably an overkill - sorting by level,sortOrder should be enough + + var sqlFull = GetBaseQuery(BaseQueryType.FullMultiple); + var translatorFull = new SqlTranslator(sqlFull, query); + var sqlIds = GetBaseQuery(BaseQueryType.Ids); + var translatorIds = new SqlTranslator(sqlIds, query); + + return ProcessQuery(translate(translatorFull), new PagingSqlQuery(translate(translatorIds)), true); + } + + public IEnumerable GetBlueprints(IQuery query) + { + Func, Sql> translate = t => t.Translate(); + + var sqlFull = GetBaseQuery(BaseQueryType.FullMultiple); + var translatorFull = new SqlTranslator(sqlFull, query); + var sqlIds = GetBaseQuery(BaseQueryType.Ids); + var translatorIds = new SqlTranslator(sqlIds, query); + + return ProcessQuery(translate(translatorFull), new PagingSqlQuery(translate(translatorIds)), true); + } + + /// + /// This builds the Xml document used for the XML cache + /// + /// + public XmlDocument BuildXmlCache() + { + //TODO: This is what we should do , but converting to use XDocument would be breaking unless we convert + // to XmlDocument at the end of this, but again, this would be bad for memory... though still not nearly as + // bad as what is happening before! + // We'll keep using XmlDocument for now though, but XDocument xml generation is much faster: + // https://blogs.msdn.microsoft.com/codejunkie/2008/10/08/xmldocument-vs-xelement-performance/ + // I think we already have code in here to convert XDocument to XmlDocument but in case we don't here + // it is: https://blogs.msdn.microsoft.com/marcelolr/2009/03/13/fast-way-to-convert-xmldocument-into-xdocument/ + + //// Prepare an XmlDocument with an appropriate inline DTD to match + //// the expected content + //var parent = new XElement("root", new XAttribute("id", "-1")); + //var xmlDoc = new XDocument( + // new XDocumentType("root", null, null, DocumentType.GenerateDtd()), + // parent); + + var xmlDoc = new XmlDocument(); + var doctype = xmlDoc.CreateDocumentType("root", null, null, + ApplicationContext.Current.Services.ContentTypeService.GetContentTypesDtd()); + xmlDoc.AppendChild(doctype); + var parent = xmlDoc.CreateElement("root"); + var pIdAtt = xmlDoc.CreateAttribute("id"); + pIdAtt.Value = "-1"; + parent.Attributes.Append(pIdAtt); + xmlDoc.AppendChild(parent); + + //Ensure that only nodes that have published versions are selected + var sql = string.Format(@"select umbracoNode.id, umbracoNode.parentID, umbracoNode.sortOrder, cmsContentXml.{0}, umbracoNode.{1} from umbracoNode +inner join cmsContentXml on cmsContentXml.nodeId = umbracoNode.id and umbracoNode.nodeObjectType = @type +where umbracoNode.id in (select cmsDocument.nodeId from cmsDocument where cmsDocument.published = 1) +order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", + SqlSyntax.GetQuotedColumnName("xml"), + SqlSyntax.GetQuotedColumnName("level"), + SqlSyntax.GetQuotedColumnName("level")); + + XmlElement last = null; + + //NOTE: Query creates a reader - does not load all into memory + foreach (var row in Database.Query(sql, new { type = NodeObjectTypeId })) + { + string parentId = ((int)row.parentID).ToInvariantString(); + string xml = row.xml; + int sortOrder = row.sortOrder; + + //if the parentid is changing + if (last != null && last.GetAttribute("parentID") != parentId) + { + parent = xmlDoc.GetElementById(parentId); + if (parent == null) + { + //Need to short circuit here, if the parent is not there it means that the parent is unpublished + // and therefore the child is not published either so cannot be included in the xml cache + continue; + } + } + + var xmlDocFragment = xmlDoc.CreateDocumentFragment(); + xmlDocFragment.InnerXml = xml; + + last = (XmlElement)parent.AppendChild(xmlDocFragment); + + // fix sortOrder - see notes in UpdateSortOrder + last.Attributes["sortOrder"].Value = sortOrder.ToInvariantString(); + } + + return xmlDoc; + + } + + public XmlDocument BuildPreviewXmlCache() + { + var xmlDoc = new XmlDocument(); + var doctype = xmlDoc.CreateDocumentType("root", null, null, + ApplicationContext.Current.Services.ContentTypeService.GetContentTypesDtd()); + xmlDoc.AppendChild(doctype); + var parent = xmlDoc.CreateElement("root"); + var pIdAtt = xmlDoc.CreateAttribute("id"); + pIdAtt.Value = "-1"; + parent.Attributes.Append(pIdAtt); + xmlDoc.AppendChild(parent); + + //Ensure that only nodes that have published versions are selected + var sql = string.Format(@"select umbracoNode.id, umbracoNode.parentID, umbracoNode.sortOrder, cmsPreviewXml.{0}, umbracoNode.{1} from umbracoNode +inner join cmsPreviewXml on cmsPreviewXml.nodeId = umbracoNode.id and umbracoNode.nodeObjectType = @type +inner join cmsDocument on cmsPreviewXml.versionId = cmsDocument.versionId and cmsDocument.newest=1 +where umbracoNode.trashed = 0 +order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", + SqlSyntax.GetQuotedColumnName("xml"), + SqlSyntax.GetQuotedColumnName("level"), + SqlSyntax.GetQuotedColumnName("level")); + + XmlElement last = null; + + //NOTE: Query creates a reader - does not load all into memory + foreach (var row in Database.Query(sql, new { type = NodeObjectTypeId })) + { + string parentId = ((int)row.parentID).ToInvariantString(); + string xml = row.xml; + int sortOrder = row.sortOrder; + + //if the parentid is changing + if (last != null && last.GetAttribute("parentID") != parentId) + { + parent = xmlDoc.GetElementById(parentId); + if (parent == null) + { + //Need to short circuit here, if the parent is not there it means that the parent is unpublished + // and therefore the child is not published either so cannot be included in the xml cache + continue; + } + } + + var xmlDocFragment = xmlDoc.CreateDocumentFragment(); + xmlDocFragment.InnerXml = xml; + + last = (XmlElement)parent.AppendChild(xmlDocFragment); + + // fix sortOrder - see notes in UpdateSortOrder + last.Attributes["sortOrder"].Value = sortOrder.ToInvariantString(); + } + + return xmlDoc; + + } + + public int CountPublished(string contentTypeAlias = null) + { + if (contentTypeAlias.IsNullOrWhiteSpace()) + { + var sql = GetBaseQuery(true).Where(x => x.Trashed == false) + .Where(x => x.Published == true); + return Database.ExecuteScalar(sql); + } + else + { + var sql = GetBaseQuery(true).Where(x => x.Trashed == false) + .Where(x => x.Published == true) + .Where(x => x.Alias == contentTypeAlias); + return Database.ExecuteScalar(sql); + } + } + + public void ReplaceContentPermissions(EntityPermissionSet permissionSet) + { + _permissionRepository.ReplaceEntityPermissions(permissionSet); + } + + public void ClearPublished(IContent content) + { + var sql = "UPDATE cmsDocument SET published=0 WHERE nodeId=@id AND published=1"; + Database.Execute(sql, new {id = content.Id}); + } + + /// + /// Assigns a single permission to the current content item for the specified user group ids + /// + /// + /// + /// + public void AssignEntityPermission(IContent entity, char permission, IEnumerable groupIds) + { + _permissionRepository.AssignEntityPermission(entity, permission, groupIds); + } + + /// + /// Gets the explicit list of permissions for the content item + /// + /// + /// + public EntityPermissionCollection GetPermissionsForEntity(int entityId) + { + return _permissionRepository.GetPermissionsForEntity(entityId); + } + + /// + /// Adds/updates content/published xml + /// + /// + /// + public void AddOrUpdateContentXml(IContent content, Func xml) + { + _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); + } + + /// + /// Used to add/update a permission for a content item + /// + /// + public void AddOrUpdatePermissions(ContentPermissionSet permission) + { + _permissionRepository.AddOrUpdate(permission); + } + + /// + /// Used to remove the content xml for a content item + /// + /// + public void DeleteContentXml(IContent content) + { + _contentXmlRepository.Delete(new ContentXmlEntity(content)); + } + + /// + /// Adds/updates preview xml + /// + /// + /// + public void AddOrUpdatePreviewXml(IContent content, Func xml) + { + _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(content, xml)); + } + + /// + /// Gets paged content results + /// + /// Query to excute + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// Search text filter + /// An Enumerable list of objects + public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter = null) + { + + //NOTE: This uses the GetBaseQuery method but that does not take into account the required 'newest' field which is + // what we always require for a paged result, so we'll ensure it's included in the filter + + var filterSql = new Sql().Append("AND (cmsDocument.newest = 1)"); + if (filter != null) + { + foreach (var filterClause in filter.GetWhereClauses()) + { + filterSql.Append(string.Format("AND ({0})", filterClause.Item1), filterClause.Item2); + } + } + + Func> filterCallback = () => new Tuple(filterSql.SQL, filterSql.Arguments); + + return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, + new Tuple("cmsDocument", "nodeId"), + (sqlFull, pagingSqlQuery) => ProcessQuery(sqlFull, pagingSqlQuery), orderBy, orderDirection, orderBySystemField, + filterCallback); + + } + + #endregion + + #region IRecycleBinRepository members + + protected override int RecycleBinId + { + get { return Constants.System.RecycleBinContent; } + } + + #endregion + + #region Read Repository implementation for GUID keys + public IContent Get(Guid id) + { + return _contentByGuidReadRepository.Get(id); + } + + IEnumerable IReadRepository.GetAll(params Guid[] ids) + { + return _contentByGuidReadRepository.GetAll(ids); + } + + public bool Exists(Guid id) + { + return _contentByGuidReadRepository.Exists(id); + } + + /// + /// A reading repository purely for looking up by GUID + /// + /// + /// TODO: This is ugly and to fix we need to decouple the IRepositoryQueryable -> IRepository -> IReadRepository which should all be separate things! + /// Then we can do the same thing with repository instances and we wouldn't need to leave all these methods as not implemented because we wouldn't need to implement them + /// + private class ContentByGuidReadRepository : PetaPocoRepositoryBase + { + private readonly ContentRepository _outerRepo; + + public ContentByGuidReadRepository(ContentRepository outerRepo, + IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + : base(work, cache, logger, sqlSyntax) + { + _outerRepo = outerRepo; + } + + protected override IContent PerformGet(Guid id) + { + var sql = _outerRepo.GetBaseQuery(BaseQueryType.FullSingle) + .Where(GetBaseWhereClause(), new { Id = id }) + .Where(x => x.Newest, SqlSyntax) + .OrderByDescending(x => x.VersionDate, SqlSyntax); + + var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); + + if (dto == null) + return null; + + var content = _outerRepo.CreateContentFromDto(dto, sql); + + return content; + } + + protected override IEnumerable PerformGetAll(params Guid[] ids) + { + Func translate = s => + { + if (ids.Any()) + { + s.Where("umbracoNode.uniqueID in (@ids)", new { ids }); + } + //we only want the newest ones with this method + s.Where(x => x.Newest, SqlSyntax); + return s; + }; + + var sqlBaseFull = _outerRepo.GetBaseQuery(BaseQueryType.FullMultiple); + var sqlBaseIds = _outerRepo.GetBaseQuery(BaseQueryType.Ids); + + return _outerRepo.ProcessQuery(translate(sqlBaseFull), new PagingSqlQuery(translate(sqlBaseIds))); + } + + protected override Sql GetBaseQuery(bool isCount) + { + return _outerRepo.GetBaseQuery(isCount); + } + + protected override string GetBaseWhereClause() + { + return "umbracoNode.uniqueID = @Id"; + } + + protected override Guid NodeObjectTypeId + { + get { return _outerRepo.NodeObjectTypeId; } + } + + #region Not needed to implement + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + throw new NotImplementedException(); + } + protected override IEnumerable GetDeleteClauses() + { + throw new NotImplementedException(); + } + protected override void PersistNewItem(IContent entity) + { + throw new NotImplementedException(); + } + protected override void PersistUpdatedItem(IContent entity) + { + throw new NotImplementedException(); + } + #endregion + } + #endregion + + protected override string GetDatabaseFieldNameForOrderBy(string orderBy) + { + //Some custom ones + switch (orderBy.ToUpperInvariant()) + { + case "NAME": + return "cmsDocument.text"; + case "UPDATER": + //TODO: This isn't going to work very nicely because it's going to order by ID, not by letter + return "cmsDocument.documentUser"; + } + + return base.GetDatabaseFieldNameForOrderBy(orderBy); + } + + /// + /// This is the underlying method that processes most queries for this repository + /// + /// + /// The FullMultiple SQL without the outer join to return all data required to create an IContent excluding it's published state data which this will query separately + /// + /// + /// The Id SQL without the outer join to just return all document ids - used to process the properties for the content item + /// + /// + /// + /// Generally when querying for content we only want to return the most recent version of the content item, however in some cases like when + /// we want to return all versions of a content item, we can't simply return the latest + /// + /// + private IEnumerable ProcessQuery(Sql sqlFull, PagingSqlQuery pagingSqlQuery, bool withCache = false, bool includeAllVersions = false) + { + // fetch returns a list so it's ok to iterate it in this method + var dtos = Database.Fetch(sqlFull); + if (dtos.Count == 0) return Enumerable.Empty(); + + //Go and get all of the published version data separately for this data, this is because when we are querying + //for multiple content items we don't include the outer join to fetch this data in the same query because + //it is insanely slow. Instead we just fetch the published version data separately in one query. + + //we need to parse the original SQL statement and reduce the columns to just cmsDocument.nodeId so that we can use + // the statement to go get the published data for all of the items by using an inner join + var parsedOriginalSql = "SELECT cmsDocument.nodeId " + sqlFull.SQL.Substring(sqlFull.SQL.IndexOf("FROM", StringComparison.Ordinal)); + //now remove everything from an Orderby clause and beyond + if (parsedOriginalSql.InvariantContains("ORDER BY ")) + { + parsedOriginalSql = parsedOriginalSql.Substring(0, parsedOriginalSql.LastIndexOf("ORDER BY ", StringComparison.Ordinal)); + } + + //order by update date DESC, if there is corrupted published flags we only want the latest! + var publishedSql = new Sql(@"SELECT cmsDocument.nodeId, cmsDocument.published, cmsDocument.versionId, cmsDocument.updateDate, cmsDocument.newest +FROM cmsDocument INNER JOIN cmsContentVersion ON cmsContentVersion.VersionId = cmsDocument.versionId +WHERE cmsDocument.published = 1 AND cmsDocument.nodeId IN +(" + parsedOriginalSql + @") +ORDER BY cmsContentVersion.id DESC +", sqlFull.Arguments); + + //go and get the published version data, we do a Query here and not a Fetch so we are + //not allocating a whole list to memory just to allocate another list in memory since + //we are assigning this data to a keyed collection for fast lookup below + var publishedData = Database.Query(publishedSql); + var publishedDataCollection = new DocumentPublishedReadOnlyDtoCollection(); + foreach (var publishedDto in publishedData) + { + //double check that there's no corrupt db data, there should only be a single published item + if (publishedDataCollection.Contains(publishedDto.NodeId) == false) + publishedDataCollection.Add(publishedDto); + } + + //This is a tuple list identifying if the content item came from the cache or not + var content = new List>(); + var defs = new DocumentDefinitionCollection(includeAllVersions); + var templateIds = new List(); + + //track the looked up content types, even though the content types are cached + // they still need to be deep cloned out of the cache and we don't want to add + // the overhead of deep cloning them on every item in this loop + var contentTypes = new Dictionary(); + + foreach (var dto in dtos) + { + DocumentPublishedReadOnlyDto publishedDto; + publishedDataCollection.TryGetValue(dto.NodeId, out publishedDto); + + // if the cache contains the published version, use it + if (withCache) + { + var cached = IsolatedCache.GetCacheItem(GetCacheIdKey(dto.NodeId)); + //only use this cached version if the dto returned is also the publish version, they must match and be teh same version + if (cached != null && cached.Version == dto.VersionId && cached.Published && dto.Published) + { + content.Add(new Tuple(cached, true)); + continue; + } + } + + // else, need to fetch from the database + // content type repository is full-cache so OK to get each one independently + + IContentType contentType; + if (contentTypes.ContainsKey(dto.ContentVersionDto.ContentDto.ContentTypeId)) + { + contentType = contentTypes[dto.ContentVersionDto.ContentDto.ContentTypeId]; + } + else + { + contentType = _contentTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); + contentTypes[dto.ContentVersionDto.ContentDto.ContentTypeId] = contentType; + } + + // track the definition and if it's successfully added or updated then processed + if (defs.AddOrUpdate(new DocumentDefinition(dto, contentType))) + { + // assign template + if (dto.TemplateId.HasValue && dto.TemplateId.Value > 0) + templateIds.Add(dto.TemplateId.Value); + + content.Add(new Tuple(ContentFactory.BuildEntity(dto, contentType, publishedDto), false)); + } + } + + // load all required templates in 1 query + var templates = _templateRepository.GetAll(templateIds.ToArray()) + .ToDictionary(x => x.Id, x => x); + + // load all properties for all documents from database in 1 query + var propertyData = GetPropertyCollection(pagingSqlQuery, defs); + + // assign template and property data + foreach (var contentItem in content) + { + var cc = contentItem.Item1; + var fromCache = contentItem.Item2; + + //if this has come from cache, we do not need to build up it's structure + if (fromCache) continue; + + var def = defs[includeAllVersions ? (ValueType)cc.Version : cc.Id]; + + ITemplate template = null; + if (def.DocumentDto.TemplateId.HasValue) + templates.TryGetValue(def.DocumentDto.TemplateId.Value, out template); // else null + cc.Template = template; + if (propertyData.ContainsKey(cc.Version)) + { + cc.Properties = propertyData[cc.Version]; + } + else + { + throw new InvalidOperationException($"No property data found for version: '{cc.Version}'."); + } + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + cc.ResetDirtyProperties(false); + } + + return content.Select(x => x.Item1).ToArray(); + } + + /// + /// Private method to create a content object from a DocumentDto, which is used by Get and GetByVersion. + /// + /// + /// + /// + private IContent CreateContentFromDto(DocumentDto dto, Sql docSql) + { + var contentType = _contentTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); + + var content = ContentFactory.BuildEntity(dto, contentType); + + //Check if template id is set on DocumentDto, and get ITemplate if it is. + if (dto.TemplateId.HasValue && dto.TemplateId.Value > 0) + { + content.Template = _templateRepository.Get(dto.TemplateId.Value); + } + + var docDef = new DocumentDefinition(dto, contentType); + + var properties = GetPropertyCollection(docSql, new[] { docDef }); + + content.Properties = properties[dto.VersionId]; + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + ((Entity)content).ResetDirtyProperties(false); + return content; + } + + private string EnsureUniqueNodeName(int parentId, string nodeName, int id = 0) + { + if (EnsureUniqueNaming == false) + return nodeName; + + var names = Database.Fetch("SELECT id, text AS name FROM umbracoNode WHERE nodeObjectType=@objectType AND parentId=@parentId", + new { objectType = NodeObjectTypeId, parentId }); + + return SimilarNodeName.GetUniqueName(names, id, nodeName); + } + + /// + /// Dispose disposable properties + /// + /// + /// Ensure the unit of work is disposed + /// + protected override void DisposeResources() + { + _contentTypeRepository.Dispose(); + _templateRepository.Dispose(); + _tagRepository.Dispose(); + _contentPreviewRepository.Dispose(); + _contentXmlRepository.Dispose(); + } + + /// + /// A keyed collection for fast lookup when retrieving a separate list of published version data + /// + private class DocumentPublishedReadOnlyDtoCollection : KeyedCollection + { + protected override int GetKeyForItem(DocumentPublishedReadOnlyDto item) + { + return item.NodeId; + } + + public bool TryGetValue(int key, out DocumentPublishedReadOnlyDto val) + { + if (Dictionary == null) + { + val = null; + return false; + } + return Dictionary.TryGetValue(key, out val); + } + } + } +} From 67f680a675e6f6bd2637e6b5aed94b8dcda3567c Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 19 Jul 2019 11:24:01 +0200 Subject: [PATCH 0024/1001] Revert "https://github.com/umbraco/Umbraco-CMS/issues/5921 - AB#1794 - Remove trashed nodes from sql when building xml for previews" This reverts commit 47c3e3a7 --- src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index a0b211b6b2..f559b91ba5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -851,7 +851,6 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", var sql = string.Format(@"select umbracoNode.id, umbracoNode.parentID, umbracoNode.sortOrder, cmsPreviewXml.{0}, umbracoNode.{1} from umbracoNode inner join cmsPreviewXml on cmsPreviewXml.nodeId = umbracoNode.id and umbracoNode.nodeObjectType = @type inner join cmsDocument on cmsPreviewXml.versionId = cmsDocument.versionId and cmsDocument.newest=1 -where umbracoNode.trashed = 0 order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", SqlSyntax.GetQuotedColumnName("xml"), SqlSyntax.GetQuotedColumnName("level"), @@ -1185,7 +1184,7 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", //order by update date DESC, if there is corrupted published flags we only want the latest! var publishedSql = new Sql(@"SELECT cmsDocument.nodeId, cmsDocument.published, cmsDocument.versionId, cmsDocument.updateDate, cmsDocument.newest FROM cmsDocument INNER JOIN cmsContentVersion ON cmsContentVersion.VersionId = cmsDocument.versionId -WHERE cmsDocument.published = 1 AND cmsDocument.nodeId IN +WHERE cmsDocument.published = 1 AND cmsDocument.nodeId IN (" + parsedOriginalSql + @") ORDER BY cmsContentVersion.id DESC ", sqlFull.Arguments); From cd27bb210f24f1b37cbca34547a52d1aa46031cb Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 19 Jul 2019 11:25:33 +0200 Subject: [PATCH 0025/1001] https://github.com/umbraco/Umbraco-CMS/issues/5921 - AB#1794 - Remove trashed nodes from sql when building xml for previews --- src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index f559b91ba5..a0b211b6b2 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -851,6 +851,7 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", var sql = string.Format(@"select umbracoNode.id, umbracoNode.parentID, umbracoNode.sortOrder, cmsPreviewXml.{0}, umbracoNode.{1} from umbracoNode inner join cmsPreviewXml on cmsPreviewXml.nodeId = umbracoNode.id and umbracoNode.nodeObjectType = @type inner join cmsDocument on cmsPreviewXml.versionId = cmsDocument.versionId and cmsDocument.newest=1 +where umbracoNode.trashed = 0 order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", SqlSyntax.GetQuotedColumnName("xml"), SqlSyntax.GetQuotedColumnName("level"), @@ -1184,7 +1185,7 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", //order by update date DESC, if there is corrupted published flags we only want the latest! var publishedSql = new Sql(@"SELECT cmsDocument.nodeId, cmsDocument.published, cmsDocument.versionId, cmsDocument.updateDate, cmsDocument.newest FROM cmsDocument INNER JOIN cmsContentVersion ON cmsContentVersion.VersionId = cmsDocument.versionId -WHERE cmsDocument.published = 1 AND cmsDocument.nodeId IN +WHERE cmsDocument.published = 1 AND cmsDocument.nodeId IN (" + parsedOriginalSql + @") ORDER BY cmsContentVersion.id DESC ", sqlFull.Arguments); From 968463912ab548808ff87bc58760b2b2bc7bb45e Mon Sep 17 00:00:00 2001 From: Steve Megson Date: Sun, 21 Jul 2019 14:47:37 +0100 Subject: [PATCH 0026/1001] Include CheckBoxList in ValueListPreValueMigrator --- .../Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs index 7249ebd6ec..07fefc8e85 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs @@ -9,6 +9,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes private readonly string[] _editors = { "Umbraco.RadioButtonList", + "Umbraco.CheckBoxList", "Umbraco.DropDown", "Umbraco.DropdownlistPublishingKeys", "Umbraco.DropDownMultiple", From 0320a56cb56606bd468cdda87c24358c6e332834 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 22 Jul 2019 13:27:51 +1000 Subject: [PATCH 0027/1001] Fixes up dataTypeId var declarations --- .../common/dialogs/linkpicker.controller.js | 6 +- .../linkpicker/linkpicker.controller.js | 9 +- .../mediaPicker/mediapicker.controller.js | 14 +--- .../treepicker/treepicker.controller.js | 6 +- .../contentpicker/contentpicker.controller.js | 84 +++++++++---------- .../grid/editors/media.controller.js | 14 ++-- .../grid/editors/rte.controller.js | 13 +-- .../mediapicker/mediapicker.controller.js | 8 +- .../multiurlpicker.controller.js | 7 +- .../relatedlinks/relatedlinks.controller.js | 14 +--- .../propertyeditors/rte/rte.controller.js | 14 +--- 11 files changed, 66 insertions(+), 123 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/linkpicker.controller.js index 876a9f9426..8cf7f937a3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/linkpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/linkpicker.controller.js @@ -8,17 +8,13 @@ angular.module("umbraco").controller("Umbraco.Dialogs.LinkPickerController", searchText = value + "..."; }); - var dataTypeId = null; - if($scope.model && $scope.model.dataTypeId) { - dataTypeId = $scope.model.dataTypeId; - } $scope.dialogTreeEventHandler = $({}); $scope.target = {}; $scope.searchInfo = { searchFromId: null, searchFromName: null, showSearch: false, - dataTypeId: dataTypeId, + dataTypeId: ($scope.model && $scope.model.dataTypeId) ? $scope.model.dataTypeId : null, results: [], selectedSearchResults: [] } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js index 0c5641ba0a..f1241c1976 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js @@ -17,7 +17,6 @@ angular.module("umbraco").controller("Umbraco.Overlays.LinkPickerController", dataTypeId = dialogOptions.dataTypeId; } - $scope.dialogTreeEventHandler = $({}); $scope.model.target = {}; $scope.searchInfo = { @@ -28,7 +27,7 @@ angular.module("umbraco").controller("Umbraco.Overlays.LinkPickerController", results: [], selectedSearchResults: [] }; - $scope.customTreeParams = dialogOptions.dataTypeId ? "dataTypeId=" + dialogOptions.dataTypeId : ""; + $scope.customTreeParams = dataTypeId !== null ? "dataTypeId=" + dataTypeId : ""; $scope.showTarget = $scope.model.hideTarget !== true; if (dialogOptions.currentTarget) { @@ -121,11 +120,7 @@ angular.module("umbraco").controller("Umbraco.Overlays.LinkPickerController", startNodeId = -1; startNodeIsVirtual = true; } - var dataTypeId = null; - if(dialogOptions && dialogOptions.dataTypeId){ - dataTypeId = dialogOptions.dataTypeId; - } - + $scope.mediaPickerOverlay = { view: "mediapicker", startNodeId: startNodeId, diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js index 61f9619a52..bae4882a3e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js @@ -54,7 +54,7 @@ angular.module("umbraco") totalItems: 0, totalPages: 0, filter: '', - dataTypeId: dataTypeId + dataTypeId: dataTypeId, }; //preload selected item @@ -161,11 +161,7 @@ angular.module("umbraco") } if (folder.id > 0) { - var dataTypeId = null; - if($scope.model && $scope.model.dataTypeId) { - dataTypeId = $scope.model.dataTypeId; - } - + entityResource.getAncestors(folder.id, "media", { dataTypeId: dataTypeId }) .then(function (anc) { $scope.path = _.filter(anc, @@ -318,11 +314,7 @@ angular.module("umbraco") if ($scope.searchOptions.filter) { searchMedia(); } else { - var dataTypeId = null; - if($scope.model && $scope.model.dataTypeId) { - dataTypeId = $scope.model.dataTypeId; - } - + // reset pagination $scope.searchOptions = { pageNumber: 1, diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js index 851d270789..2d5fb132b7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js @@ -5,10 +5,6 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", var tree = null; var dialogOptions = $scope.model; - var dataTypeId = null; - if(dialogOptions && dialogOptions.dataTypeId){ - dataTypeId = dialogOptions.dataTypeId; - } $scope.treeReady = false; $scope.dialogTreeEventHandler = $({}); $scope.section = dialogOptions.section; @@ -21,7 +17,7 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", searchFromId: dialogOptions.startNodeId, searchFromName: null, showSearch: false, - dataTypeId: dataTypeId, + dataTypeId: (dialogOptions && dialogOptions.dataTypeId) ? dialogOptions.dataTypeId : null, results: [], selectedSearchResults: [] } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js index 37a372a5b1..61e841d1af 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js @@ -68,11 +68,11 @@ function contentPickerController($scope, entityResource, editorState, iconHelper showPathOnHover: false, dataTypeId: null, maxNumber: 1, - minNumber : 0, + minNumber: 0, startNode: { query: "", type: "content", - id: $scope.model.config.startNodeId ? $scope.model.config.startNodeId : -1 // get start node for simple Content Picker + id: $scope.model.config.startNodeId ? $scope.model.config.startNodeId : -1 // get start node for simple Content Picker } }; @@ -104,8 +104,8 @@ function contentPickerController($scope, entityResource, editorState, iconHelper var entityType = $scope.model.config.startNode.type === "member" ? "Member" : $scope.model.config.startNode.type === "media" - ? "Media" - : "Document"; + ? "Media" + : "Document"; $scope.allowOpenButton = entityType === "Document"; $scope.allowEditButton = entityType === "Document"; $scope.allowRemoveButton = true; @@ -144,7 +144,7 @@ function contentPickerController($scope, entityResource, editorState, iconHelper dialogOptions.filterCssClass = "not-allowed"; var currFilter = dialogOptions.filter; //now change the filter to be a method - dialogOptions.filter = function(i) { + dialogOptions.filter = function (i) { //filter out the list view nodes if (i.metaData.isContainer) { return true; @@ -179,34 +179,30 @@ function contentPickerController($scope, entityResource, editorState, iconHelper } //dialog - $scope.openContentPicker = function() { - var dataTypeId = null; - if($scope.model && $scope.model.dataTypeId) { - dataTypeId = $scope.model.dataTypeId; + $scope.openContentPicker = function () { + + $scope.contentPickerOverlay = dialogOptions; + $scope.contentPickerOverlay.view = "treepicker"; + $scope.contentPickerOverlay.show = true; + $scope.contentPickerOverlay.dataTypeId = ($scope.model && $scope.model.dataTypeId) ? $scope.model.dataTypeId : null; + + $scope.contentPickerOverlay.submit = function (model) { + + if (angular.isArray(model.selection)) { + _.each(model.selection, function (item, i) { + $scope.add(item); + }); + angularHelper.getCurrentForm($scope).$setDirty(); + } + + $scope.contentPickerOverlay.show = false; + $scope.contentPickerOverlay = null; } - $scope.contentPickerOverlay = dialogOptions; - $scope.contentPickerOverlay.view = "treepicker"; - $scope.contentPickerOverlay.show = true; - $scope.contentPickerOverlay.dataTypeId = dataTypeId; - - $scope.contentPickerOverlay.submit = function(model) { - - if (angular.isArray(model.selection)) { - _.each(model.selection, function (item, i) { - $scope.add(item); - }); - angularHelper.getCurrentForm($scope).$setDirty(); - } - - $scope.contentPickerOverlay.show = false; - $scope.contentPickerOverlay = null; - } - - $scope.contentPickerOverlay.close = function(oldModel) { - $scope.contentPickerOverlay.show = false; - $scope.contentPickerOverlay = null; - } + $scope.contentPickerOverlay.close = function (oldModel) { + $scope.contentPickerOverlay.show = false; + $scope.contentPickerOverlay = null; + } }; @@ -246,13 +242,13 @@ function contentPickerController($scope, entityResource, editorState, iconHelper $scope.renderModel = []; }; - $scope.openMiniEditor = function(node) { - miniEditorHelper.launchMiniEditor(node).then(function(updatedNode){ + $scope.openMiniEditor = function (node) { + miniEditorHelper.launchMiniEditor(node).then(function (updatedNode) { // update the node node.name = updatedNode.name; node.published = updatedNode.hasPublishedVersion; - if(entityType !== "Member") { - entityResource.getUrl(updatedNode.id, entityType).then(function(data){ + if (entityType !== "Member") { + entityResource.getUrl(updatedNode.id, entityType).then(function (data) { node.url = data; }); } @@ -261,7 +257,7 @@ function contentPickerController($scope, entityResource, editorState, iconHelper //when the scope is destroyed we need to unsubscribe $scope.$on('$destroy', function () { - if(unsubscribe) { + if (unsubscribe) { unsubscribe(); } }); @@ -270,12 +266,12 @@ function contentPickerController($scope, entityResource, editorState, iconHelper //load current data if anything selected if (modelIds.length > 0) { - entityResource.getByIds(modelIds, entityType).then(function(data) { + entityResource.getByIds(modelIds, entityType).then(function (data) { _.each(modelIds, - function(id, i) { + function (id, i) { var entity = _.find(data, - function(d) { + function (d) { return $scope.model.config.idType === "udi" ? (d.udi == id) : (d.id == id); }); @@ -299,10 +295,10 @@ function contentPickerController($scope, entityResource, editorState, iconHelper function setEntityUrl(entity) { // get url for content and media items - if(entityType !== "Member") { - entityResource.getUrl(entity.id, entityType).then(function(data){ + if (entityType !== "Member") { + entityResource.getUrl(entity.id, entityType).then(function (data) { // update url - angular.forEach($scope.renderModel, function(item){ + angular.forEach($scope.renderModel, function (item) { if (item.id === entity.id) { if (entity.trashed) { item.url = localizationService.dictionary.general_recycleBin; @@ -324,7 +320,7 @@ function contentPickerController($scope, entityResource, editorState, iconHelper function addSelectedItem(item) { // set icon - if(item.icon) { + if (item.icon) { item.icon = iconHelper.convertFromLegacyIcon(item.icon); } @@ -359,7 +355,7 @@ function contentPickerController($scope, entityResource, editorState, iconHelper function setSortingState(items) { // disable sorting if the list only consist of one item - if(items.length > 1) { + if (items.length > 1) { $scope.sortableOptions.disabled = false; } else { $scope.sortableOptions.disabled = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js index e9c095c1b9..3d0d568866 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js @@ -16,17 +16,13 @@ angular.module("umbraco") } $scope.setImage = function(){ - var dataTypeId = null; - if($scope.model && $scope.model.dataTypeId) { - dataTypeId = $scope.model.dataTypeId; - } - + $scope.mediaPickerOverlay = {}; $scope.mediaPickerOverlay.view = "mediapicker"; - $scope.mediaPickerOverlay.startNodeId = $scope.model.config && $scope.model.config.startNodeId ? $scope.model.config.startNodeId : undefined; - $scope.mediaPickerOverlay.startNodeIsVirtual = $scope.mediaPickerOverlay.startNodeId ? $scope.model.config.startNodeIsVirtual : undefined; - $scope.mediaPickerOverlay.dataTypeId = dataTypeId; - $scope.mediaPickerOverlay.cropSize = $scope.control.editor.config && $scope.control.editor.config.size ? $scope.control.editor.config.size : undefined; + $scope.mediaPickerOverlay.startNodeId = $scope.model.config && $scope.model.config.startNodeId ? $scope.model.config.startNodeId : null; + $scope.mediaPickerOverlay.startNodeIsVirtual = $scope.mediaPickerOverlay.startNodeId ? $scope.model.config.startNodeIsVirtual : null; + $scope.mediaPickerOverlay.dataTypeId = ($scope.model && $scope.model.dataTypeId) ? $scope.model.dataTypeId : null; + $scope.mediaPickerOverlay.cropSize = $scope.control.editor.config && $scope.control.editor.config.size ? $scope.control.editor.config.size : null; $scope.mediaPickerOverlay.showDetails = true; $scope.mediaPickerOverlay.disableFolderSelect = true; $scope.mediaPickerOverlay.onlyImages = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js index 580d231de8..94f2c83cbf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js @@ -10,14 +10,12 @@ vm.openMacroPicker = openMacroPicker; vm.openEmbed = openEmbed; + const dataTypeId = ($scope.model && $scope.model.dataTypeId) ? $scope.model.dataTypeId : null; + function openLinkPicker(editor, currentTarget, anchorElement) { entityResource.getAnchors(JSON.stringify($scope.model.value)).then(function(anchorValues) { - var dataTypeId = null; - if($scope.model && $scope.model.dataTypeId) { - dataTypeId = $scope.model.dataTypeId; - } - + vm.linkPickerOverlay = { view: "linkpicker", currentTarget: currentTarget, @@ -45,11 +43,6 @@ startNodeIsVirtual = true; } - var dataTypeId = null; - if($scope.model && $scope.model.dataTypeId) { - dataTypeId = $scope.model.dataTypeId; - } - vm.mediaPickerOverlay = { currentTarget: currentTarget, onlyImages: true, diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js index 63beddb49c..8aafba8ca1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js @@ -107,17 +107,13 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl }; $scope.add = function() { - var dataTypeId = null; - if($scope.model && $scope.model.dataTypeId) { - dataTypeId = $scope.model.dataTypeId; - } - + $scope.mediaPickerOverlay = { view: "mediapicker", title: "Select media", startNodeId: $scope.model.config.startNodeId, startNodeIsVirtual: $scope.model.config.startNodeIsVirtual, - dataTypeId: dataTypeId, + dataTypeId: ($scope.model && $scope.model.dataTypeId) ? $scope.model.dataTypeId : null, multiPicker: multiPicker, onlyImages: onlyImages, disableFolderSelect: disableFolderSelect, diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js index cab124ad23..a5fc5c50d2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js @@ -68,15 +68,10 @@ function multiUrlPickerController($scope, angularHelper, localizationService, en target: link.target } : null; - var dataTypeId = null; - if($scope.model && $scope.model.dataTypeId) { - dataTypeId = $scope.model.dataTypeId; - } - $scope.linkPickerOverlay = { view: "linkpicker", currentTarget: target, - dataTypeId: dataTypeId, + dataTypeId: ($scope.model && $scope.model.dataTypeId) ? $scope.model.dataTypeId : null, ignoreUserStartNodes : $scope.model.config.ignoreUserStartNodes, show: true, submit: function (model) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js index fa92442eac..78adf2fee5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js @@ -18,12 +18,10 @@ $scope.currentEditLink = null; $scope.hasError = false; - $scope.internal = function($event) { - var dataTypeId = null; - if($scope.model && $scope.model.dataTypeId) { - dataTypeId = $scope.model.dataTypeId; - } + const dataTypeId = ($scope.model && $scope.model.dataTypeId) ? $scope.model.dataTypeId : null; + $scope.internal = function($event) { + $scope.currentEditLink = null; $scope.contentPickerOverlay = {}; @@ -50,11 +48,7 @@ }; $scope.selectInternal = function ($event, link) { - var dataTypeId = null; - if($scope.model && $scope.model.dataTypeId) { - dataTypeId = $scope.model.dataTypeId; - } - + $scope.currentEditLink = link; $scope.contentPickerOverlay = {}; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js index 2b3ee930d9..d92319a3fc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js @@ -52,6 +52,8 @@ angular.module("umbraco") editorConfig.maxImageSize = tinyMceService.defaultPrevalues().maxImageSize; } + const dataTypeId = ($scope.model && $scope.model.dataTypeId) ? $scope.model.dataTypeId : null; + //queue file loading if (typeof tinymce === "undefined") { // Don't reload tinymce if already loaded await.push(assetsService.loadJs("lib/tinymce/tinymce.min.js", $scope)); @@ -272,11 +274,7 @@ angular.module("umbraco") tinyMceService.createLinkPicker(editor, $scope, function(currentTarget, anchorElement) { entityResource.getAnchors($scope.model.value).then(function(anchorValues){ - var dataTypeId = null; - if($scope.model && $scope.model.dataTypeId) { - dataTypeId = $scope.model.dataTypeId; - } - + $scope.linkPickerOverlay = { view: "linkpicker", currentTarget: currentTarget, @@ -305,11 +303,7 @@ angular.module("umbraco") startNodeId = -1; startNodeIsVirtual = true; } - var dataTypeId = null; - if($scope.model && $scope.model.dataTypeId) { - dataTypeId = $scope.model.dataTypeId; - } - + $scope.mediaPickerOverlay = { currentTarget: currentTarget, onlyImages: true, From 6148ce4e52e66ca7e4edbba8228b067cd09ff112 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 22 Jul 2019 13:41:26 +1000 Subject: [PATCH 0028/1001] oops no es6 support in 7.x --- .../views/common/overlays/mediaPicker/mediapicker.controller.js | 2 +- .../src/views/propertyeditors/grid/editors/rte.controller.js | 2 +- .../propertyeditors/relatedlinks/relatedlinks.controller.js | 2 +- .../src/views/propertyeditors/rte/rte.controller.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js index bae4882a3e..5f76528f5d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js @@ -54,7 +54,7 @@ angular.module("umbraco") totalItems: 0, totalPages: 0, filter: '', - dataTypeId: dataTypeId, + dataTypeId: dataTypeId }; //preload selected item diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js index 94f2c83cbf..e36e398024 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js @@ -10,7 +10,7 @@ vm.openMacroPicker = openMacroPicker; vm.openEmbed = openEmbed; - const dataTypeId = ($scope.model && $scope.model.dataTypeId) ? $scope.model.dataTypeId : null; + var dataTypeId = ($scope.model && $scope.model.dataTypeId) ? $scope.model.dataTypeId : null; function openLinkPicker(editor, currentTarget, anchorElement) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js index 78adf2fee5..392e0dc0cd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js @@ -18,7 +18,7 @@ $scope.currentEditLink = null; $scope.hasError = false; - const dataTypeId = ($scope.model && $scope.model.dataTypeId) ? $scope.model.dataTypeId : null; + var dataTypeId = ($scope.model && $scope.model.dataTypeId) ? $scope.model.dataTypeId : null; $scope.internal = function($event) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js index d92319a3fc..109aa37fbb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js @@ -52,7 +52,7 @@ angular.module("umbraco") editorConfig.maxImageSize = tinyMceService.defaultPrevalues().maxImageSize; } - const dataTypeId = ($scope.model && $scope.model.dataTypeId) ? $scope.model.dataTypeId : null; + var dataTypeId = ($scope.model && $scope.model.dataTypeId) ? $scope.model.dataTypeId : null; //queue file loading if (typeof tinymce === "undefined") { // Don't reload tinymce if already loaded From 5126521d37fcc1050a5da44f667560e29752d412 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 12 Jul 2019 13:15:41 +1000 Subject: [PATCH 0029/1001] Fixes #5789 --- src/Umbraco.Web/IPublishedContentQuery.cs | 8 ++++---- src/Umbraco.Web/PublishedContentQuery.cs | 25 +++++++++++++++-------- src/Umbraco.Web/UmbracoContextFactory.cs | 8 ++++++++ 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Web/IPublishedContentQuery.cs b/src/Umbraco.Web/IPublishedContentQuery.cs index 76e7be5e97..35db121a60 100644 --- a/src/Umbraco.Web/IPublishedContentQuery.cs +++ b/src/Umbraco.Web/IPublishedContentQuery.cs @@ -39,10 +39,10 @@ namespace Umbraco.Web /// Optional culture. /// Optional index name. /// - /// When the is not specified, all cultures are searched. + /// When the is not specified or is *, all cultures are searched. To search only invariant use null. /// While enumerating results, the ambient culture is changed to be the searched culture. /// - IEnumerable Search(string term, string culture = null, string indexName = null); + IEnumerable Search(string term, string culture = "*", string indexName = null); /// /// Searches content. @@ -54,10 +54,10 @@ namespace Umbraco.Web /// Optional culture. /// Optional index name. /// - /// When the is not specified, all cultures are searched. + /// When the is not specified or is *, all cultures are searched. To search only invariant use null. /// While enumerating results, the ambient culture is changed to be the searched culture. /// - IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = null, string indexName = null); + IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = null); /// /// Executes the query and converts the results to PublishedSearchResult. diff --git a/src/Umbraco.Web/PublishedContentQuery.cs b/src/Umbraco.Web/PublishedContentQuery.cs index 61180580cb..2772cc94f6 100644 --- a/src/Umbraco.Web/PublishedContentQuery.cs +++ b/src/Umbraco.Web/PublishedContentQuery.cs @@ -175,13 +175,13 @@ namespace Umbraco.Web #region Search /// - public IEnumerable Search(string term, string culture = null, string indexName = null) + public IEnumerable Search(string term, string culture = "*", string indexName = null) { return Search(term, 0, 0, out _, culture, indexName); } /// - public IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = null, string indexName = null) + public IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = null) { indexName = string.IsNullOrEmpty(indexName) ? Constants.UmbracoIndexes.ExternalIndexName @@ -195,20 +195,29 @@ namespace Umbraco.Web // default to max 500 results var count = skip == 0 && take == 0 ? 500 : skip + take; - //set this to the specific culture or to the culture in the request - culture = culture ?? _variationContextAccessor.VariationContext.Culture; - ISearchResults results; - if (culture.IsNullOrWhiteSpace()) + if (culture == "*") { + //search everything + results = searcher.Search(term, count); } + else if (culture.IsNullOrWhiteSpace()) + { + //only search invariant + + var qry = searcher.CreateQuery().Field(UmbracoContentIndex.VariesByCultureFieldName, "n"); //must not vary by culture + qry = qry.And().ManagedQuery(term); + results = qry.Execute(count); + } else { + //search only the specified culture + //get all index fields suffixed with the culture name supplied - var cultureFields = umbIndex.GetCultureFields(culture); + var cultureFields = umbIndex.GetCultureFields(culture).ToArray(); var qry = searcher.CreateQuery().Field(UmbracoContentIndex.VariesByCultureFieldName, "y"); //must vary by culture - qry = qry.And().ManagedQuery(term, cultureFields.ToArray()); + qry = qry.And().ManagedQuery(term, cultureFields); results = qry.Execute(count); } diff --git a/src/Umbraco.Web/UmbracoContextFactory.cs b/src/Umbraco.Web/UmbracoContextFactory.cs index 2a812036bf..11d8952fa6 100644 --- a/src/Umbraco.Web/UmbracoContextFactory.cs +++ b/src/Umbraco.Web/UmbracoContextFactory.cs @@ -53,7 +53,15 @@ namespace Umbraco.Web { // make sure we have a variation context if (_variationContextAccessor.VariationContext == null) + { + // TODO: By using _defaultCultureAccessor.DefaultCulture this means that the VariationContext will always return a variant culture, it will never + // return an empty string signifying that the culture is invariant. But does this matter? Are we actually expecting this to return an empty string + // for invariant routes? From what i can tell throughout the codebase is that whenever we are checking against the VariationContext.Culture we are + // also checking if the content type varies by culture or not. This is fine, however the code in the ctor of VariationContext is then misleading + // since it's assuming that the Culture can be empty (invariant) when in reality of a website this will never be empty since a real culture is always set here. _variationContextAccessor.VariationContext = new VariationContext(_defaultCultureAccessor.DefaultCulture); + } + var webSecurity = new WebSecurity(httpContext, _userService, _globalSettings); From dc371c1979e5d3df409da4e4fae229d52944ae72 Mon Sep 17 00:00:00 2001 From: Steve Megson Date: Sun, 21 Jul 2019 15:47:37 +0200 Subject: [PATCH 0030/1001] Include CheckBoxList in ValueListPreValueMigrator --- .../Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs index 7249ebd6ec..07fefc8e85 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs @@ -9,6 +9,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes private readonly string[] _editors = { "Umbraco.RadioButtonList", + "Umbraco.CheckBoxList", "Umbraco.DropDown", "Umbraco.DropdownlistPublishingKeys", "Umbraco.DropDownMultiple", From 1c6bf55e5d3a65dd63ba5dcbd05bffc69a89f537 Mon Sep 17 00:00:00 2001 From: Frans de Jong Date: Fri, 19 Jul 2019 14:42:44 +0200 Subject: [PATCH 0031/1001] GetCurrentLoginStatus() In the summary of MembershipHelper.GetCurrentLoginStatus() it states that null will be returned when no member is logged in but that isn't the case (anymore?). --- src/Umbraco.Web/Security/MembershipHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Security/MembershipHelper.cs b/src/Umbraco.Web/Security/MembershipHelper.cs index cdf696c520..f74897d565 100644 --- a/src/Umbraco.Web/Security/MembershipHelper.cs +++ b/src/Umbraco.Web/Security/MembershipHelper.cs @@ -542,7 +542,7 @@ namespace Umbraco.Web.Security } /// - /// Returns the login status model of the currently logged in member, if no member is logged in it returns null; + /// Returns the login status model of the currently logged in member. /// /// public virtual LoginStatusModel GetCurrentLoginStatus() From a8ed7f2c17512f36ef883a9805603488df6c5b42 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 23 Jul 2019 22:11:02 +1000 Subject: [PATCH 0032/1001] adds new method --- src/Umbraco.Examine/ExamineExtensions.cs | 25 ++++++++++++++++++++++++ src/Umbraco.Web/PublishedContentQuery.cs | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Examine/ExamineExtensions.cs b/src/Umbraco.Examine/ExamineExtensions.cs index 1b8033c458..d97278f31c 100644 --- a/src/Umbraco.Examine/ExamineExtensions.cs +++ b/src/Umbraco.Examine/ExamineExtensions.cs @@ -48,6 +48,31 @@ namespace Umbraco.Examine } } + /// + /// Returns all index fields that are culture specific (suffixed) or invariant + /// + /// + /// + /// + public static IEnumerable GetCultureAndInvariantFields(this IUmbracoIndex index, string culture) + { + var allFields = index.GetFields(); + // ReSharper disable once LoopCanBeConvertedToQuery + foreach (var field in allFields) + { + var match = CultureIsoCodeFieldNameMatchExpression.Match(field); + if (match.Success && match.Groups.Count == 3 && culture.InvariantEquals(match.Groups[2].Value)) + { + yield return field; //matches this culture field + } + else if (!match.Success) + { + yield return field; //matches no culture field (invariant) + } + + } + } + internal static bool TryParseLuceneQuery(string query) { // TODO: I'd assume there would be a more strict way to parse the query but not that i can find yet, for now we'll diff --git a/src/Umbraco.Web/PublishedContentQuery.cs b/src/Umbraco.Web/PublishedContentQuery.cs index 2772cc94f6..19e303602d 100644 --- a/src/Umbraco.Web/PublishedContentQuery.cs +++ b/src/Umbraco.Web/PublishedContentQuery.cs @@ -215,7 +215,7 @@ namespace Umbraco.Web //search only the specified culture //get all index fields suffixed with the culture name supplied - var cultureFields = umbIndex.GetCultureFields(culture).ToArray(); + var cultureFields = umbIndex.GetCultureAndInvariantFields(culture).ToArray(); var qry = searcher.CreateQuery().Field(UmbracoContentIndex.VariesByCultureFieldName, "y"); //must vary by culture qry = qry.And().ManagedQuery(term, cultureFields); results = qry.Execute(count); From 2f8979bbc3975dea34d1cfd8c60a0ebb90b0896b Mon Sep 17 00:00:00 2001 From: Shannon Deminick Date: Wed, 24 Jul 2019 17:29:00 +1000 Subject: [PATCH 0033/1001] Update src/Umbraco.Web/PublishedContentQuery.cs Co-Authored-By: Bjarke Berg --- src/Umbraco.Web/PublishedContentQuery.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PublishedContentQuery.cs b/src/Umbraco.Web/PublishedContentQuery.cs index 19e303602d..5af5837495 100644 --- a/src/Umbraco.Web/PublishedContentQuery.cs +++ b/src/Umbraco.Web/PublishedContentQuery.cs @@ -216,7 +216,7 @@ namespace Umbraco.Web //get all index fields suffixed with the culture name supplied var cultureFields = umbIndex.GetCultureAndInvariantFields(culture).ToArray(); - var qry = searcher.CreateQuery().Field(UmbracoContentIndex.VariesByCultureFieldName, "y"); //must vary by culture + var qry = searcher.CreateQuery(); qry = qry.And().ManagedQuery(term, cultureFields); results = qry.Execute(count); } From beb8c7ac7c1076eabc132e3eb824e8ea8c6cd667 Mon Sep 17 00:00:00 2001 From: Shannon Deminick Date: Wed, 24 Jul 2019 17:30:29 +1000 Subject: [PATCH 0034/1001] Update src/Umbraco.Web/PublishedContentQuery.cs Co-Authored-By: Bjarke Berg --- src/Umbraco.Web/PublishedContentQuery.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PublishedContentQuery.cs b/src/Umbraco.Web/PublishedContentQuery.cs index 5af5837495..cfdd31f138 100644 --- a/src/Umbraco.Web/PublishedContentQuery.cs +++ b/src/Umbraco.Web/PublishedContentQuery.cs @@ -217,7 +217,7 @@ namespace Umbraco.Web //get all index fields suffixed with the culture name supplied var cultureFields = umbIndex.GetCultureAndInvariantFields(culture).ToArray(); var qry = searcher.CreateQuery(); - qry = qry.And().ManagedQuery(term, cultureFields); + qry = qry.ManagedQuery(term, cultureFields); results = qry.Execute(count); } From 210e43fcb084c977a514e92f0f4376442ea85bb3 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 24 Jul 2019 09:53:51 +0200 Subject: [PATCH 0035/1001] Fix for build --- src/Umbraco.Web/PublishedContentQuery.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web/PublishedContentQuery.cs b/src/Umbraco.Web/PublishedContentQuery.cs index cfdd31f138..887368a3e9 100644 --- a/src/Umbraco.Web/PublishedContentQuery.cs +++ b/src/Umbraco.Web/PublishedContentQuery.cs @@ -216,8 +216,7 @@ namespace Umbraco.Web //get all index fields suffixed with the culture name supplied var cultureFields = umbIndex.GetCultureAndInvariantFields(culture).ToArray(); - var qry = searcher.CreateQuery(); - qry = qry.ManagedQuery(term, cultureFields); + var qry = searcher.CreateQuery().ManagedQuery(term, cultureFields); results = qry.Execute(count); } @@ -313,7 +312,7 @@ namespace Umbraco.Web } } - + #endregion From 8a43e3a87ec28b997878e2433e464b07680bf5e2 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Fri, 12 Jul 2019 19:20:15 +0200 Subject: [PATCH 0036/1001] Make sure save options are up to date with the content state --- .../common/directives/components/content/edit.controller.js | 4 ++++ 1 file changed, 4 insertions(+) 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 a548820138..58a22ac74e 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 @@ -94,6 +94,10 @@ content.apps[0].active = true; $scope.appChanged(content.apps[0]); } + // otherwise make sure the save options are up to date with the current content state + else { + createButtons($scope.content); + } editorState.set(content); From 2c795662d2d5493039eda2a85120c2e3d39b3758 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 24 Jul 2019 18:46:14 +1000 Subject: [PATCH 0037/1001] Adds tests for PublishedContentQuery search --- .../TestHelpers/RandomIdRamDirectory.cs | 22 +++ src/Umbraco.Tests/Umbraco.Tests.csproj | 2 + .../Web/PublishedContentQueryTests.cs | 157 ++++++++++++++++++ src/Umbraco.Tests/Web/UmbracoHelperTests.cs | 10 +- src/Umbraco.Web/IPublishedContentQuery.cs | 12 +- src/Umbraco.Web/PublishedContentQuery.cs | 17 +- 6 files changed, 210 insertions(+), 10 deletions(-) create mode 100644 src/Umbraco.Tests/TestHelpers/RandomIdRamDirectory.cs create mode 100644 src/Umbraco.Tests/Web/PublishedContentQueryTests.cs diff --git a/src/Umbraco.Tests/TestHelpers/RandomIdRamDirectory.cs b/src/Umbraco.Tests/TestHelpers/RandomIdRamDirectory.cs new file mode 100644 index 0000000000..34904db1ae --- /dev/null +++ b/src/Umbraco.Tests/TestHelpers/RandomIdRamDirectory.cs @@ -0,0 +1,22 @@ +using Lucene.Net.Store; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Tests.TestHelpers +{ + + /// + /// Used for tests with Lucene so that each RAM directory is unique + /// + public class RandomIdRAMDirectory : RAMDirectory + { + private readonly string _lockId = Guid.NewGuid().ToString(); + public override string GetLockId() + { + return _lockId; + } + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index f41ff1dd07..717006b702 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -157,6 +157,7 @@ + @@ -268,6 +269,7 @@ + diff --git a/src/Umbraco.Tests/Web/PublishedContentQueryTests.cs b/src/Umbraco.Tests/Web/PublishedContentQueryTests.cs new file mode 100644 index 0000000000..b2a2741bcf --- /dev/null +++ b/src/Umbraco.Tests/Web/PublishedContentQueryTests.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Examine; +using Examine.LuceneEngine.Providers; +using Lucene.Net.Store; +using Moq; +using NUnit.Framework; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Examine; +using Umbraco.Tests.TestHelpers; +using Umbraco.Web; +using Umbraco.Web.PublishedCache; + +namespace Umbraco.Tests.Web +{ + [TestFixture] + public class PublishedContentQueryTests + { + + private class TestIndex : LuceneIndex, IUmbracoIndex + { + private readonly string[] _fieldNames; + + public TestIndex(string name, Directory luceneDirectory, string[] fieldNames) + : base(name, luceneDirectory, null, null, null, null) + { + _fieldNames = fieldNames; + } + public bool EnableDefaultEventHandler => throw new NotImplementedException(); + public bool PublishedValuesOnly => throw new NotImplementedException(); + public IEnumerable GetFields() => _fieldNames; + } + + private TestIndex CreateTestIndex(Directory luceneDirectory, string[] fieldNames) + { + var indexer = new TestIndex("TestIndex", luceneDirectory, fieldNames); + + //populate with some test data + indexer.IndexItem(new ValueSet("1", "content", new Dictionary + { + [fieldNames[0]] = "Hello world, there are products here", + [UmbracoContentIndex.VariesByCultureFieldName] = "n" + })); + indexer.IndexItem(new ValueSet("2", "content", new Dictionary + { + [fieldNames[1]] = "Hello world, there are products here", + [UmbracoContentIndex.VariesByCultureFieldName] = "y" + })); + indexer.IndexItem(new ValueSet("3", "content", new Dictionary + { + [fieldNames[2]] = "Hello world, there are products here", + [UmbracoContentIndex.VariesByCultureFieldName] = "y" + })); + return indexer; + } + + private PublishedContentQuery CreatePublishedContentQuery(IIndex indexer) + { + var examineManager = new Mock(); + IIndex outarg = indexer; + examineManager.Setup(x => x.TryGetIndex("TestIndex", out outarg)).Returns(true); + + var contentCache = new Mock(); + contentCache.Setup(x => x.GetById(It.IsAny())).Returns((int intId) => Mock.Of(x => x.Id == intId)); + var snapshot = Mock.Of(x => x.Content == contentCache.Object); + var variationContext = new VariationContext(); + var variationContextAccessor = Mock.Of(x => x.VariationContext == variationContext); + + return new PublishedContentQuery(snapshot, variationContextAccessor, examineManager.Object); + } + + [Test] + public void Search_Wildcard() + { + using (var luceneDir = new RandomIdRAMDirectory()) + { + var fieldNames = new[] { "title", "title_en-us", "title_fr-fr" }; + using (var indexer = CreateTestIndex(luceneDir, fieldNames)) + { + var pcq = CreatePublishedContentQuery(indexer); + + var results = pcq.Search("Products", "*", "TestIndex"); + + var ids = results.Select(x => x.Content.Id).ToList(); + Assert.AreEqual(3, ids.Count); + + //returns results for all fields and document types + Assert.IsTrue(ids.Contains(1) && ids.Contains(2) && ids.Contains(3)); + } + } + } + + [Test] + public void Search_Invariant() + { + using (var luceneDir = new RandomIdRAMDirectory()) + { + var fieldNames = new[] { "title", "title_en-us", "title_fr-fr" }; + using (var indexer = CreateTestIndex(luceneDir, fieldNames)) + { + var pcq = CreatePublishedContentQuery(indexer); + + var results = pcq.Search("Products", null, "TestIndex"); + + var ids = results.Select(x => x.Content.Id).ToList(); + Assert.AreEqual(1, ids.Count); + + //returns results for only invariant fields and invariant documents + Assert.IsTrue(ids.Contains(1) && !ids.Contains(2) && !ids.Contains(3)); + } + } + } + + [Test] + public void Search_Culture1() + { + using (var luceneDir = new RandomIdRAMDirectory()) + { + var fieldNames = new[] { "title", "title_en-us", "title_fr-fr" }; + using (var indexer = CreateTestIndex(luceneDir, fieldNames)) + { + var pcq = CreatePublishedContentQuery(indexer); + + var results = pcq.Search("Products", "en-us", "TestIndex"); + + var ids = results.Select(x => x.Content.Id).ToList(); + Assert.AreEqual(2, ids.Count); + + //returns results for en-us fields and invariant fields for all document types + Assert.IsTrue(ids.Contains(1) && ids.Contains(2) && !ids.Contains(3)); + } + } + } + + [Test] + public void Search_Culture2() + { + using (var luceneDir = new RandomIdRAMDirectory()) + { + var fieldNames = new[] { "title", "title_en-us", "title_fr-fr" }; + using (var indexer = CreateTestIndex(luceneDir, fieldNames)) + { + var pcq = CreatePublishedContentQuery(indexer); + + var results = pcq.Search("Products", "fr-fr", "TestIndex"); + + var ids = results.Select(x => x.Content.Id).ToList(); + Assert.AreEqual(2, ids.Count); + + //returns results for fr-fr fields and invariant fields for all document types + Assert.IsTrue(ids.Contains(1) && !ids.Contains(2) && ids.Contains(3)); + } + } + } + } +} diff --git a/src/Umbraco.Tests/Web/UmbracoHelperTests.cs b/src/Umbraco.Tests/Web/UmbracoHelperTests.cs index b23b5bd6b7..26d85f60cf 100644 --- a/src/Umbraco.Tests/Web/UmbracoHelperTests.cs +++ b/src/Umbraco.Tests/Web/UmbracoHelperTests.cs @@ -1,5 +1,8 @@ using System; using System.Text; +using Examine.LuceneEngine; +using Lucene.Net.Analysis; +using Lucene.Net.Analysis.Standard; using Moq; using NUnit.Framework; using Umbraco.Core; @@ -13,18 +16,19 @@ using Umbraco.Web; namespace Umbraco.Tests.Web { + [TestFixture] public class UmbracoHelperTests - { + { [TearDown] public void TearDown() { Current.Reset(); } - - + + // ------- Int32 conversion tests [Test] public static void Converting_Boxed_34_To_An_Int_Returns_34() diff --git a/src/Umbraco.Web/IPublishedContentQuery.cs b/src/Umbraco.Web/IPublishedContentQuery.cs index 35db121a60..8a8d678aba 100644 --- a/src/Umbraco.Web/IPublishedContentQuery.cs +++ b/src/Umbraco.Web/IPublishedContentQuery.cs @@ -39,7 +39,11 @@ namespace Umbraco.Web /// Optional culture. /// Optional index name. /// - /// When the is not specified or is *, all cultures are searched. To search only invariant use null. + /// + /// When the is not specified or is *, all cultures are searched. + /// To search for only invariant documents and fields use null. + /// When searching on a specific culture, all culture specific fields are searched for the provided culture and all invariant fields for all documents. + /// /// While enumerating results, the ambient culture is changed to be the searched culture. /// IEnumerable Search(string term, string culture = "*", string indexName = null); @@ -54,7 +58,11 @@ namespace Umbraco.Web /// Optional culture. /// Optional index name. /// - /// When the is not specified or is *, all cultures are searched. To search only invariant use null. + /// + /// When the is not specified or is *, all cultures are searched. + /// To search for only invariant documents and fields use null. + /// When searching on a specific culture, all culture specific fields are searched for the provided culture and all invariant fields for all documents. + /// /// While enumerating results, the ambient culture is changed to be the searched culture. /// IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = null); diff --git a/src/Umbraco.Web/PublishedContentQuery.cs b/src/Umbraco.Web/PublishedContentQuery.cs index cfdd31f138..2dbe4de4c5 100644 --- a/src/Umbraco.Web/PublishedContentQuery.cs +++ b/src/Umbraco.Web/PublishedContentQuery.cs @@ -20,14 +20,22 @@ namespace Umbraco.Web { private readonly IPublishedSnapshot _publishedSnapshot; private readonly IVariationContextAccessor _variationContextAccessor; + private readonly IExamineManager _examineManager; + + [Obsolete("Use the constructor with all parameters instead")] + public PublishedContentQuery(IPublishedSnapshot publishedSnapshot, IVariationContextAccessor variationContextAccessor) + : this (publishedSnapshot, variationContextAccessor, ExamineManager.Instance) + { + } /// /// Initializes a new instance of the class. /// - public PublishedContentQuery(IPublishedSnapshot publishedSnapshot, IVariationContextAccessor variationContextAccessor) + public PublishedContentQuery(IPublishedSnapshot publishedSnapshot, IVariationContextAccessor variationContextAccessor, IExamineManager examineManager) { _publishedSnapshot = publishedSnapshot ?? throw new ArgumentNullException(nameof(publishedSnapshot)); _variationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); + _examineManager = examineManager ?? throw new ArgumentNullException(nameof(examineManager)); } #region Content @@ -187,7 +195,7 @@ namespace Umbraco.Web ? Constants.UmbracoIndexes.ExternalIndexName : indexName; - if (!ExamineManager.Instance.TryGetIndex(indexName, out var index) || !(index is IUmbracoIndex umbIndex)) + if (!_examineManager.TryGetIndex(indexName, out var index) || !(index is IUmbracoIndex umbIndex)) throw new InvalidOperationException($"No index found by name {indexName} or is not of type {typeof(IUmbracoIndex)}"); var searcher = umbIndex.GetSearcher(); @@ -216,8 +224,7 @@ namespace Umbraco.Web //get all index fields suffixed with the culture name supplied var cultureFields = umbIndex.GetCultureAndInvariantFields(culture).ToArray(); - var qry = searcher.CreateQuery(); - qry = qry.ManagedQuery(term, cultureFields); + var qry = searcher.CreateQuery().ManagedQuery(term, cultureFields); results = qry.Execute(count); } @@ -313,7 +320,7 @@ namespace Umbraco.Web } } - + #endregion From 342b2087e999f508adcfdcbf4000504dbdf0f0f4 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Fri, 12 Jul 2019 19:20:15 +0200 Subject: [PATCH 0038/1001] Make sure save options are up to date with the content state (cherry picked from commit 8a43e3a87ec28b997878e2433e464b07680bf5e2) --- .../common/directives/components/content/edit.controller.js | 4 ++++ 1 file changed, 4 insertions(+) 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 a548820138..58a22ac74e 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 @@ -94,6 +94,10 @@ content.apps[0].active = true; $scope.appChanged(content.apps[0]); } + // otherwise make sure the save options are up to date with the current content state + else { + createButtons($scope.content); + } editorState.set(content); From 308f929f7b5ba1bfd6b8fc373427a426929db2a2 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 24 Jul 2019 11:13:21 +0200 Subject: [PATCH 0039/1001] Refactored tests to avoid duplicate code --- .../Web/PublishedContentQueryTests.cs | 78 ++----------------- 1 file changed, 8 insertions(+), 70 deletions(-) diff --git a/src/Umbraco.Tests/Web/PublishedContentQueryTests.cs b/src/Umbraco.Tests/Web/PublishedContentQueryTests.cs index b2a2741bcf..2cff946372 100644 --- a/src/Umbraco.Tests/Web/PublishedContentQueryTests.cs +++ b/src/Umbraco.Tests/Web/PublishedContentQueryTests.cs @@ -70,8 +70,11 @@ namespace Umbraco.Tests.Web return new PublishedContentQuery(snapshot, variationContextAccessor, examineManager.Object); } - [Test] - public void Search_Wildcard() + [TestCase("fr-fr", ExpectedResult = "1, 3", TestName = "Search Culture: fr-fr. Must return both fr-fr and invariant results")] + [TestCase("en-us", ExpectedResult = "1, 2", TestName = "Search Culture: en-us. Must return both en-us and invariant results")] + [TestCase("*", ExpectedResult = "1, 2, 3", TestName = "Search Culture: *. Must return all cultures and all invariant results")] + [TestCase(null, ExpectedResult = "1", TestName = "Search Culture: null. Must return only invariant results")] + public string Search(string culture) { using (var luceneDir = new RandomIdRAMDirectory()) { @@ -80,76 +83,11 @@ namespace Umbraco.Tests.Web { var pcq = CreatePublishedContentQuery(indexer); - var results = pcq.Search("Products", "*", "TestIndex"); + var results = pcq.Search("Products", culture, "TestIndex"); - var ids = results.Select(x => x.Content.Id).ToList(); - Assert.AreEqual(3, ids.Count); + var ids = results.Select(x => x.Content.Id).ToArray(); - //returns results for all fields and document types - Assert.IsTrue(ids.Contains(1) && ids.Contains(2) && ids.Contains(3)); - } - } - } - - [Test] - public void Search_Invariant() - { - using (var luceneDir = new RandomIdRAMDirectory()) - { - var fieldNames = new[] { "title", "title_en-us", "title_fr-fr" }; - using (var indexer = CreateTestIndex(luceneDir, fieldNames)) - { - var pcq = CreatePublishedContentQuery(indexer); - - var results = pcq.Search("Products", null, "TestIndex"); - - var ids = results.Select(x => x.Content.Id).ToList(); - Assert.AreEqual(1, ids.Count); - - //returns results for only invariant fields and invariant documents - Assert.IsTrue(ids.Contains(1) && !ids.Contains(2) && !ids.Contains(3)); - } - } - } - - [Test] - public void Search_Culture1() - { - using (var luceneDir = new RandomIdRAMDirectory()) - { - var fieldNames = new[] { "title", "title_en-us", "title_fr-fr" }; - using (var indexer = CreateTestIndex(luceneDir, fieldNames)) - { - var pcq = CreatePublishedContentQuery(indexer); - - var results = pcq.Search("Products", "en-us", "TestIndex"); - - var ids = results.Select(x => x.Content.Id).ToList(); - Assert.AreEqual(2, ids.Count); - - //returns results for en-us fields and invariant fields for all document types - Assert.IsTrue(ids.Contains(1) && ids.Contains(2) && !ids.Contains(3)); - } - } - } - - [Test] - public void Search_Culture2() - { - using (var luceneDir = new RandomIdRAMDirectory()) - { - var fieldNames = new[] { "title", "title_en-us", "title_fr-fr" }; - using (var indexer = CreateTestIndex(luceneDir, fieldNames)) - { - var pcq = CreatePublishedContentQuery(indexer); - - var results = pcq.Search("Products", "fr-fr", "TestIndex"); - - var ids = results.Select(x => x.Content.Id).ToList(); - Assert.AreEqual(2, ids.Count); - - //returns results for fr-fr fields and invariant fields for all document types - Assert.IsTrue(ids.Contains(1) && !ids.Contains(2) && ids.Contains(3)); + return string.Join(", ", ids); } } } From a97604d6c405c7ebf6372f69807193e8cbbc7a50 Mon Sep 17 00:00:00 2001 From: Steve Megson Date: Sun, 21 Jul 2019 15:19:17 +0100 Subject: [PATCH 0040/1001] Handle CSV values in TagsPropertyEditor --- .../PropertyEditors/TagsPropertyEditor.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs index 90527a8b8d..b7101aa764 100644 --- a/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using Newtonsoft.Json.Linq; @@ -38,9 +39,15 @@ namespace Umbraco.Web.PropertyEditors /// public override object FromEditor(ContentPropertyData editorValue, object currentValue) { - return editorValue.Value is JArray json - ? json.Select(x => x.Value()) - : null; + if (editorValue.Value is JArray json) + { + return json.Select(x => x.Value()); + } + else if ( editorValue.Value is string stringValue && !String.IsNullOrWhiteSpace(stringValue)) + { + return stringValue.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); + } + return null; } /// From 186460ac08de66428a54357857c630093b8c9674 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 24 Jul 2019 19:23:05 +1000 Subject: [PATCH 0041/1001] removes refs from old packages.umbraco.org --- src/Umbraco.Web/Properties/Settings.settings | 3 - .../Properties/Settings1.Designer.cs | 12 +- src/Umbraco.Web/Umbraco.Web.csproj | 22 - .../org.umbraco.our/Reference.cs | 1046 ----------------- .../org.umbraco.our/Reference.map | 7 - .../org.umbraco.our/repository.disco | 6 - .../org.umbraco.our/repository.wsdl | 995 ---------------- 7 files changed, 1 insertion(+), 2090 deletions(-) delete mode 100644 src/Umbraco.Web/Web References/org.umbraco.our/Reference.cs delete mode 100644 src/Umbraco.Web/Web References/org.umbraco.our/Reference.map delete mode 100644 src/Umbraco.Web/Web References/org.umbraco.our/repository.disco delete mode 100644 src/Umbraco.Web/Web References/org.umbraco.our/repository.wsdl diff --git a/src/Umbraco.Web/Properties/Settings.settings b/src/Umbraco.Web/Properties/Settings.settings index da9e6f69c0..a7b911ae3b 100644 --- a/src/Umbraco.Web/Properties/Settings.settings +++ b/src/Umbraco.Web/Properties/Settings.settings @@ -11,8 +11,5 @@ Somthing - - https://our.umbraco.com/umbraco/webservices/api/repository.asmx - \ No newline at end of file diff --git a/src/Umbraco.Web/Properties/Settings1.Designer.cs b/src/Umbraco.Web/Properties/Settings1.Designer.cs index e7028f28df..3847901f20 100644 --- a/src/Umbraco.Web/Properties/Settings1.Designer.cs +++ b/src/Umbraco.Web/Properties/Settings1.Designer.cs @@ -12,7 +12,7 @@ namespace Umbraco.Web.Properties { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.8.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.2.0.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); @@ -50,15 +50,5 @@ namespace Umbraco.Web.Properties { return ((string)(this["test"])); } } - - [global::System.Configuration.ApplicationScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.SpecialSettingAttribute(global::System.Configuration.SpecialSetting.WebServiceUrl)] - [global::System.Configuration.DefaultSettingValueAttribute("https://our.umbraco.com/umbraco/webservices/api/repository.asmx")] - public string umbraco_org_umbraco_our_Repository { - get { - return ((string)(this["umbraco_org_umbraco_our_Repository"])); - } - } } } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 167c99b9b9..5705f6b7eb 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -812,11 +812,6 @@ - - True - True - Reference.map - @@ -1828,11 +1823,6 @@ Mvc\web.config - - MSDiscoCodeGenerator - Reference.cs - - @@ -1883,7 +1873,6 @@ - Reference.map @@ -1977,17 +1966,6 @@ - - Dynamic - Web References\org.umbraco.our\ - https://our.umbraco.com/umbraco/webservices/api/repository.asmx - - - - - Settings - umbraco_org_umbraco_our_Repository - Dynamic Web References\org.umbraco.update\ diff --git a/src/Umbraco.Web/Web References/org.umbraco.our/Reference.cs b/src/Umbraco.Web/Web References/org.umbraco.our/Reference.cs deleted file mode 100644 index d0c8990660..0000000000 --- a/src/Umbraco.Web/Web References/org.umbraco.our/Reference.cs +++ /dev/null @@ -1,1046 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -// -// This source code was auto-generated by Microsoft.VSDesigner, Version 4.0.30319.42000. -// -#pragma warning disable 1591 - -namespace Umbraco.Web.org.umbraco.our { - using System; - using System.Web.Services; - using System.Diagnostics; - using System.Web.Services.Protocols; - using System.Xml.Serialization; - using System.ComponentModel; - - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3056.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - [System.Web.Services.WebServiceBindingAttribute(Name="RepositorySoap", Namespace="http://packages.umbraco.org/webservices/")] - public partial class Repository : System.Web.Services.Protocols.SoapHttpClientProtocol { - - private System.Threading.SendOrPostCallback CategoriesOperationCompleted; - - private System.Threading.SendOrPostCallback ModulesOperationCompleted; - - private System.Threading.SendOrPostCallback ModulesCategorizedOperationCompleted; - - private System.Threading.SendOrPostCallback NitrosOperationCompleted; - - private System.Threading.SendOrPostCallback NitrosCategorizedOperationCompleted; - - private System.Threading.SendOrPostCallback authenticateOperationCompleted; - - private System.Threading.SendOrPostCallback fetchPackageOperationCompleted; - - private System.Threading.SendOrPostCallback fetchPackageByVersionOperationCompleted; - - private System.Threading.SendOrPostCallback fetchProtectedPackageOperationCompleted; - - private System.Threading.SendOrPostCallback SubmitPackageOperationCompleted; - - private System.Threading.SendOrPostCallback PackageByGuidOperationCompleted; - - private bool useDefaultCredentialsSetExplicitly; - - /// - public Repository() { - this.Url = global::Umbraco.Web.Properties.Settings.Default.umbraco_org_umbraco_our_Repository; - if ((this.IsLocalFileSystemWebService(this.Url) == true)) { - this.UseDefaultCredentials = true; - this.useDefaultCredentialsSetExplicitly = false; - } - else { - this.useDefaultCredentialsSetExplicitly = true; - } - } - - public new string Url { - get { - return base.Url; - } - set { - if ((((this.IsLocalFileSystemWebService(base.Url) == true) - && (this.useDefaultCredentialsSetExplicitly == false)) - && (this.IsLocalFileSystemWebService(value) == false))) { - base.UseDefaultCredentials = false; - } - base.Url = value; - } - } - - public new bool UseDefaultCredentials { - get { - return base.UseDefaultCredentials; - } - set { - base.UseDefaultCredentials = value; - this.useDefaultCredentialsSetExplicitly = true; - } - } - - /// - public event CategoriesCompletedEventHandler CategoriesCompleted; - - /// - public event ModulesCompletedEventHandler ModulesCompleted; - - /// - public event ModulesCategorizedCompletedEventHandler ModulesCategorizedCompleted; - - /// - public event NitrosCompletedEventHandler NitrosCompleted; - - /// - public event NitrosCategorizedCompletedEventHandler NitrosCategorizedCompleted; - - /// - public event authenticateCompletedEventHandler authenticateCompleted; - - /// - public event fetchPackageCompletedEventHandler fetchPackageCompleted; - - /// - public event fetchPackageByVersionCompletedEventHandler fetchPackageByVersionCompleted; - - /// - public event fetchProtectedPackageCompletedEventHandler fetchProtectedPackageCompleted; - - /// - public event SubmitPackageCompletedEventHandler SubmitPackageCompleted; - - /// - public event PackageByGuidCompletedEventHandler PackageByGuidCompleted; - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/Categories", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public Category[] Categories(string repositoryGuid) { - object[] results = this.Invoke("Categories", new object[] { - repositoryGuid}); - return ((Category[])(results[0])); - } - - /// - public void CategoriesAsync(string repositoryGuid) { - this.CategoriesAsync(repositoryGuid, null); - } - - /// - public void CategoriesAsync(string repositoryGuid, object userState) { - if ((this.CategoriesOperationCompleted == null)) { - this.CategoriesOperationCompleted = new System.Threading.SendOrPostCallback(this.OnCategoriesOperationCompleted); - } - this.InvokeAsync("Categories", new object[] { - repositoryGuid}, this.CategoriesOperationCompleted, userState); - } - - private void OnCategoriesOperationCompleted(object arg) { - if ((this.CategoriesCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.CategoriesCompleted(this, new CategoriesCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/Modules", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public Package[] Modules() { - object[] results = this.Invoke("Modules", new object[0]); - return ((Package[])(results[0])); - } - - /// - public void ModulesAsync() { - this.ModulesAsync(null); - } - - /// - public void ModulesAsync(object userState) { - if ((this.ModulesOperationCompleted == null)) { - this.ModulesOperationCompleted = new System.Threading.SendOrPostCallback(this.OnModulesOperationCompleted); - } - this.InvokeAsync("Modules", new object[0], this.ModulesOperationCompleted, userState); - } - - private void OnModulesOperationCompleted(object arg) { - if ((this.ModulesCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.ModulesCompleted(this, new ModulesCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/ModulesCategorized", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public Category[] ModulesCategorized() { - object[] results = this.Invoke("ModulesCategorized", new object[0]); - return ((Category[])(results[0])); - } - - /// - public void ModulesCategorizedAsync() { - this.ModulesCategorizedAsync(null); - } - - /// - public void ModulesCategorizedAsync(object userState) { - if ((this.ModulesCategorizedOperationCompleted == null)) { - this.ModulesCategorizedOperationCompleted = new System.Threading.SendOrPostCallback(this.OnModulesCategorizedOperationCompleted); - } - this.InvokeAsync("ModulesCategorized", new object[0], this.ModulesCategorizedOperationCompleted, userState); - } - - private void OnModulesCategorizedOperationCompleted(object arg) { - if ((this.ModulesCategorizedCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.ModulesCategorizedCompleted(this, new ModulesCategorizedCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/Nitros", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public Package[] Nitros() { - object[] results = this.Invoke("Nitros", new object[0]); - return ((Package[])(results[0])); - } - - /// - public void NitrosAsync() { - this.NitrosAsync(null); - } - - /// - public void NitrosAsync(object userState) { - if ((this.NitrosOperationCompleted == null)) { - this.NitrosOperationCompleted = new System.Threading.SendOrPostCallback(this.OnNitrosOperationCompleted); - } - this.InvokeAsync("Nitros", new object[0], this.NitrosOperationCompleted, userState); - } - - private void OnNitrosOperationCompleted(object arg) { - if ((this.NitrosCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.NitrosCompleted(this, new NitrosCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/NitrosCategorized", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public Category[] NitrosCategorized() { - object[] results = this.Invoke("NitrosCategorized", new object[0]); - return ((Category[])(results[0])); - } - - /// - public void NitrosCategorizedAsync() { - this.NitrosCategorizedAsync(null); - } - - /// - public void NitrosCategorizedAsync(object userState) { - if ((this.NitrosCategorizedOperationCompleted == null)) { - this.NitrosCategorizedOperationCompleted = new System.Threading.SendOrPostCallback(this.OnNitrosCategorizedOperationCompleted); - } - this.InvokeAsync("NitrosCategorized", new object[0], this.NitrosCategorizedOperationCompleted, userState); - } - - private void OnNitrosCategorizedOperationCompleted(object arg) { - if ((this.NitrosCategorizedCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.NitrosCategorizedCompleted(this, new NitrosCategorizedCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/authenticate", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public string authenticate(string email, string md5Password) { - object[] results = this.Invoke("authenticate", new object[] { - email, - md5Password}); - return ((string)(results[0])); - } - - /// - public void authenticateAsync(string email, string md5Password) { - this.authenticateAsync(email, md5Password, null); - } - - /// - public void authenticateAsync(string email, string md5Password, object userState) { - if ((this.authenticateOperationCompleted == null)) { - this.authenticateOperationCompleted = new System.Threading.SendOrPostCallback(this.OnauthenticateOperationCompleted); - } - this.InvokeAsync("authenticate", new object[] { - email, - md5Password}, this.authenticateOperationCompleted, userState); - } - - private void OnauthenticateOperationCompleted(object arg) { - if ((this.authenticateCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.authenticateCompleted(this, new authenticateCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/fetchPackage", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - [return: System.Xml.Serialization.XmlElementAttribute(DataType="base64Binary")] - public byte[] fetchPackage(string packageGuid) { - object[] results = this.Invoke("fetchPackage", new object[] { - packageGuid}); - return ((byte[])(results[0])); - } - - /// - public void fetchPackageAsync(string packageGuid) { - this.fetchPackageAsync(packageGuid, null); - } - - /// - public void fetchPackageAsync(string packageGuid, object userState) { - if ((this.fetchPackageOperationCompleted == null)) { - this.fetchPackageOperationCompleted = new System.Threading.SendOrPostCallback(this.OnfetchPackageOperationCompleted); - } - this.InvokeAsync("fetchPackage", new object[] { - packageGuid}, this.fetchPackageOperationCompleted, userState); - } - - private void OnfetchPackageOperationCompleted(object arg) { - if ((this.fetchPackageCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.fetchPackageCompleted(this, new fetchPackageCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/fetchPackageByVersion", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - [return: System.Xml.Serialization.XmlElementAttribute(DataType="base64Binary")] - public byte[] fetchPackageByVersion(string packageGuid, string repoVersion) { - object[] results = this.Invoke("fetchPackageByVersion", new object[] { - packageGuid, - repoVersion}); - return ((byte[])(results[0])); - } - - /// - public void fetchPackageByVersionAsync(string packageGuid, string repoVersion) { - this.fetchPackageByVersionAsync(packageGuid, repoVersion, null); - } - - /// - public void fetchPackageByVersionAsync(string packageGuid, string repoVersion, object userState) { - if ((this.fetchPackageByVersionOperationCompleted == null)) { - this.fetchPackageByVersionOperationCompleted = new System.Threading.SendOrPostCallback(this.OnfetchPackageByVersionOperationCompleted); - } - this.InvokeAsync("fetchPackageByVersion", new object[] { - packageGuid, - repoVersion}, this.fetchPackageByVersionOperationCompleted, userState); - } - - private void OnfetchPackageByVersionOperationCompleted(object arg) { - if ((this.fetchPackageByVersionCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.fetchPackageByVersionCompleted(this, new fetchPackageByVersionCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/fetchProtectedPackage", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - [return: System.Xml.Serialization.XmlElementAttribute(DataType="base64Binary")] - public byte[] fetchProtectedPackage(string packageGuid, string memberKey) { - object[] results = this.Invoke("fetchProtectedPackage", new object[] { - packageGuid, - memberKey}); - return ((byte[])(results[0])); - } - - /// - public void fetchProtectedPackageAsync(string packageGuid, string memberKey) { - this.fetchProtectedPackageAsync(packageGuid, memberKey, null); - } - - /// - public void fetchProtectedPackageAsync(string packageGuid, string memberKey, object userState) { - if ((this.fetchProtectedPackageOperationCompleted == null)) { - this.fetchProtectedPackageOperationCompleted = new System.Threading.SendOrPostCallback(this.OnfetchProtectedPackageOperationCompleted); - } - this.InvokeAsync("fetchProtectedPackage", new object[] { - packageGuid, - memberKey}, this.fetchProtectedPackageOperationCompleted, userState); - } - - private void OnfetchProtectedPackageOperationCompleted(object arg) { - if ((this.fetchProtectedPackageCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.fetchProtectedPackageCompleted(this, new fetchProtectedPackageCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/SubmitPackage", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public SubmitStatus SubmitPackage(string repositoryGuid, string authorGuid, string packageGuid, [System.Xml.Serialization.XmlElementAttribute(DataType="base64Binary")] byte[] packageFile, [System.Xml.Serialization.XmlElementAttribute(DataType="base64Binary")] byte[] packageDoc, [System.Xml.Serialization.XmlElementAttribute(DataType="base64Binary")] byte[] packageThumbnail, string name, string author, string authorUrl, string description) { - object[] results = this.Invoke("SubmitPackage", new object[] { - repositoryGuid, - authorGuid, - packageGuid, - packageFile, - packageDoc, - packageThumbnail, - name, - author, - authorUrl, - description}); - return ((SubmitStatus)(results[0])); - } - - /// - public void SubmitPackageAsync(string repositoryGuid, string authorGuid, string packageGuid, byte[] packageFile, byte[] packageDoc, byte[] packageThumbnail, string name, string author, string authorUrl, string description) { - this.SubmitPackageAsync(repositoryGuid, authorGuid, packageGuid, packageFile, packageDoc, packageThumbnail, name, author, authorUrl, description, null); - } - - /// - public void SubmitPackageAsync(string repositoryGuid, string authorGuid, string packageGuid, byte[] packageFile, byte[] packageDoc, byte[] packageThumbnail, string name, string author, string authorUrl, string description, object userState) { - if ((this.SubmitPackageOperationCompleted == null)) { - this.SubmitPackageOperationCompleted = new System.Threading.SendOrPostCallback(this.OnSubmitPackageOperationCompleted); - } - this.InvokeAsync("SubmitPackage", new object[] { - repositoryGuid, - authorGuid, - packageGuid, - packageFile, - packageDoc, - packageThumbnail, - name, - author, - authorUrl, - description}, this.SubmitPackageOperationCompleted, userState); - } - - private void OnSubmitPackageOperationCompleted(object arg) { - if ((this.SubmitPackageCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.SubmitPackageCompleted(this, new SubmitPackageCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/PackageByGuid", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public Package PackageByGuid(string packageGuid) { - object[] results = this.Invoke("PackageByGuid", new object[] { - packageGuid}); - return ((Package)(results[0])); - } - - /// - public void PackageByGuidAsync(string packageGuid) { - this.PackageByGuidAsync(packageGuid, null); - } - - /// - public void PackageByGuidAsync(string packageGuid, object userState) { - if ((this.PackageByGuidOperationCompleted == null)) { - this.PackageByGuidOperationCompleted = new System.Threading.SendOrPostCallback(this.OnPackageByGuidOperationCompleted); - } - this.InvokeAsync("PackageByGuid", new object[] { - packageGuid}, this.PackageByGuidOperationCompleted, userState); - } - - private void OnPackageByGuidOperationCompleted(object arg) { - if ((this.PackageByGuidCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.PackageByGuidCompleted(this, new PackageByGuidCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - public new void CancelAsync(object userState) { - base.CancelAsync(userState); - } - - private bool IsLocalFileSystemWebService(string url) { - if (((url == null) - || (url == string.Empty))) { - return false; - } - System.Uri wsUri = new System.Uri(url); - if (((wsUri.Port >= 1024) - && (string.Compare(wsUri.Host, "localHost", System.StringComparison.OrdinalIgnoreCase) == 0))) { - return true; - } - return false; - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.7.3056.0")] - [System.SerializableAttribute()] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - [System.Xml.Serialization.XmlTypeAttribute(Namespace="http://packages.umbraco.org/webservices/")] - public partial class Category { - - private string textField; - - private string descriptionField; - - private string urlField; - - private int idField; - - private Package[] packagesField; - - /// - public string Text { - get { - return this.textField; - } - set { - this.textField = value; - } - } - - /// - public string Description { - get { - return this.descriptionField; - } - set { - this.descriptionField = value; - } - } - - /// - public string Url { - get { - return this.urlField; - } - set { - this.urlField = value; - } - } - - /// - public int Id { - get { - return this.idField; - } - set { - this.idField = value; - } - } - - /// - public Package[] Packages { - get { - return this.packagesField; - } - set { - this.packagesField = value; - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.7.3056.0")] - [System.SerializableAttribute()] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - [System.Xml.Serialization.XmlTypeAttribute(Namespace="http://packages.umbraco.org/webservices/")] - public partial class Package { - - private System.Guid repoGuidField; - - private string textField; - - private string descriptionField; - - private string iconField; - - private string thumbnailField; - - private string documentationField; - - private string demoField; - - private bool acceptedField; - - private bool isModuleField; - - private bool editorsPickField; - - private bool protectedField; - - private bool hasUpgradeField; - - private string upgradeVersionField; - - private string upgradeReadMeField; - - private string urlField; - - /// - public System.Guid RepoGuid { - get { - return this.repoGuidField; - } - set { - this.repoGuidField = value; - } - } - - /// - public string Text { - get { - return this.textField; - } - set { - this.textField = value; - } - } - - /// - public string Description { - get { - return this.descriptionField; - } - set { - this.descriptionField = value; - } - } - - /// - public string Icon { - get { - return this.iconField; - } - set { - this.iconField = value; - } - } - - /// - public string Thumbnail { - get { - return this.thumbnailField; - } - set { - this.thumbnailField = value; - } - } - - /// - public string Documentation { - get { - return this.documentationField; - } - set { - this.documentationField = value; - } - } - - /// - public string Demo { - get { - return this.demoField; - } - set { - this.demoField = value; - } - } - - /// - public bool Accepted { - get { - return this.acceptedField; - } - set { - this.acceptedField = value; - } - } - - /// - public bool IsModule { - get { - return this.isModuleField; - } - set { - this.isModuleField = value; - } - } - - /// - public bool EditorsPick { - get { - return this.editorsPickField; - } - set { - this.editorsPickField = value; - } - } - - /// - public bool Protected { - get { - return this.protectedField; - } - set { - this.protectedField = value; - } - } - - /// - public bool HasUpgrade { - get { - return this.hasUpgradeField; - } - set { - this.hasUpgradeField = value; - } - } - - /// - public string UpgradeVersion { - get { - return this.upgradeVersionField; - } - set { - this.upgradeVersionField = value; - } - } - - /// - public string UpgradeReadMe { - get { - return this.upgradeReadMeField; - } - set { - this.upgradeReadMeField = value; - } - } - - /// - public string Url { - get { - return this.urlField; - } - set { - this.urlField = value; - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.7.3056.0")] - [System.SerializableAttribute()] - [System.Xml.Serialization.XmlTypeAttribute(Namespace="http://packages.umbraco.org/webservices/")] - public enum SubmitStatus { - - /// - Complete, - - /// - Exists, - - /// - NoAccess, - - /// - Error, - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3056.0")] - public delegate void CategoriesCompletedEventHandler(object sender, CategoriesCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3056.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class CategoriesCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal CategoriesCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public Category[] Result { - get { - this.RaiseExceptionIfNecessary(); - return ((Category[])(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3056.0")] - public delegate void ModulesCompletedEventHandler(object sender, ModulesCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3056.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class ModulesCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal ModulesCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public Package[] Result { - get { - this.RaiseExceptionIfNecessary(); - return ((Package[])(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3056.0")] - public delegate void ModulesCategorizedCompletedEventHandler(object sender, ModulesCategorizedCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3056.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class ModulesCategorizedCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal ModulesCategorizedCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public Category[] Result { - get { - this.RaiseExceptionIfNecessary(); - return ((Category[])(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3056.0")] - public delegate void NitrosCompletedEventHandler(object sender, NitrosCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3056.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class NitrosCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal NitrosCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public Package[] Result { - get { - this.RaiseExceptionIfNecessary(); - return ((Package[])(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3056.0")] - public delegate void NitrosCategorizedCompletedEventHandler(object sender, NitrosCategorizedCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3056.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class NitrosCategorizedCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal NitrosCategorizedCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public Category[] Result { - get { - this.RaiseExceptionIfNecessary(); - return ((Category[])(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3056.0")] - public delegate void authenticateCompletedEventHandler(object sender, authenticateCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3056.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class authenticateCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal authenticateCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public string Result { - get { - this.RaiseExceptionIfNecessary(); - return ((string)(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3056.0")] - public delegate void fetchPackageCompletedEventHandler(object sender, fetchPackageCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3056.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class fetchPackageCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal fetchPackageCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public byte[] Result { - get { - this.RaiseExceptionIfNecessary(); - return ((byte[])(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3056.0")] - public delegate void fetchPackageByVersionCompletedEventHandler(object sender, fetchPackageByVersionCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3056.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class fetchPackageByVersionCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal fetchPackageByVersionCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public byte[] Result { - get { - this.RaiseExceptionIfNecessary(); - return ((byte[])(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3056.0")] - public delegate void fetchProtectedPackageCompletedEventHandler(object sender, fetchProtectedPackageCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3056.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class fetchProtectedPackageCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal fetchProtectedPackageCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public byte[] Result { - get { - this.RaiseExceptionIfNecessary(); - return ((byte[])(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3056.0")] - public delegate void SubmitPackageCompletedEventHandler(object sender, SubmitPackageCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3056.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class SubmitPackageCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal SubmitPackageCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public SubmitStatus Result { - get { - this.RaiseExceptionIfNecessary(); - return ((SubmitStatus)(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3056.0")] - public delegate void PackageByGuidCompletedEventHandler(object sender, PackageByGuidCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3056.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class PackageByGuidCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal PackageByGuidCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public Package Result { - get { - this.RaiseExceptionIfNecessary(); - return ((Package)(this.results[0])); - } - } - } -} - -#pragma warning restore 1591 \ No newline at end of file diff --git a/src/Umbraco.Web/Web References/org.umbraco.our/Reference.map b/src/Umbraco.Web/Web References/org.umbraco.our/Reference.map deleted file mode 100644 index cd819c5591..0000000000 --- a/src/Umbraco.Web/Web References/org.umbraco.our/Reference.map +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/src/Umbraco.Web/Web References/org.umbraco.our/repository.disco b/src/Umbraco.Web/Web References/org.umbraco.our/repository.disco deleted file mode 100644 index fcc65dea0c..0000000000 --- a/src/Umbraco.Web/Web References/org.umbraco.our/repository.disco +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/Umbraco.Web/Web References/org.umbraco.our/repository.wsdl b/src/Umbraco.Web/Web References/org.umbraco.our/repository.wsdl deleted file mode 100644 index 17d24e9c54..0000000000 --- a/src/Umbraco.Web/Web References/org.umbraco.our/repository.wsdl +++ /dev/null @@ -1,995 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From 28a80271795e2b2d149b8fa815271a5994394baf Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 24 Jul 2019 19:25:32 +1000 Subject: [PATCH 0042/1001] removes reference to old packages.umbraco.org --- .../Properties/Settings.Designer.cs | 12 +- src/Umbraco.Web/Properties/Settings.settings | 3 - src/Umbraco.Web/Umbraco.Web.csproj | 22 - .../org.umbraco.our/Reference.cs | 1046 ----------------- .../org.umbraco.our/Reference.map | 7 - .../org.umbraco.our/repository.disco | 6 - .../org.umbraco.our/repository.wsdl | 995 ---------------- 7 files changed, 1 insertion(+), 2090 deletions(-) delete mode 100644 src/Umbraco.Web/Web References/org.umbraco.our/Reference.cs delete mode 100644 src/Umbraco.Web/Web References/org.umbraco.our/Reference.map delete mode 100644 src/Umbraco.Web/Web References/org.umbraco.our/repository.disco delete mode 100644 src/Umbraco.Web/Web References/org.umbraco.our/repository.wsdl diff --git a/src/Umbraco.Web/Properties/Settings.Designer.cs b/src/Umbraco.Web/Properties/Settings.Designer.cs index 5a5a863f4f..5f7ccbd7e1 100644 --- a/src/Umbraco.Web/Properties/Settings.Designer.cs +++ b/src/Umbraco.Web/Properties/Settings.Designer.cs @@ -12,7 +12,7 @@ namespace Umbraco.Web.Properties { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.7.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.2.0.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); @@ -50,15 +50,5 @@ namespace Umbraco.Web.Properties { return ((string)(this["test"])); } } - - [global::System.Configuration.ApplicationScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.SpecialSettingAttribute(global::System.Configuration.SpecialSetting.WebServiceUrl)] - [global::System.Configuration.DefaultSettingValueAttribute("https://our.umbraco.com/umbraco/webservices/api/repository.asmx")] - public string umbraco_org_umbraco_our_Repository { - get { - return ((string)(this["umbraco_org_umbraco_our_Repository"])); - } - } } } diff --git a/src/Umbraco.Web/Properties/Settings.settings b/src/Umbraco.Web/Properties/Settings.settings index 757b363da2..3a6f68023e 100644 --- a/src/Umbraco.Web/Properties/Settings.settings +++ b/src/Umbraco.Web/Properties/Settings.settings @@ -11,8 +11,5 @@ Something - - https://our.umbraco.com/umbraco/webservices/api/repository.asmx - \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index dcaa3494fd..730a321a16 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -906,11 +906,6 @@ - - True - True - Reference.map - @@ -1220,12 +1215,6 @@ Mvc\web.config - - MSDiscoCodeGenerator - Reference.cs - - - Reference.map @@ -1238,17 +1227,6 @@ - - Dynamic - Web References\org.umbraco.our\ - http://our.umbraco.org/umbraco/webservices/api/repository.asmx - - - - - Settings - umbraco_org_umbraco_our_Repository - Dynamic Web References\org.umbraco.update\ diff --git a/src/Umbraco.Web/Web References/org.umbraco.our/Reference.cs b/src/Umbraco.Web/Web References/org.umbraco.our/Reference.cs deleted file mode 100644 index caa2887797..0000000000 --- a/src/Umbraco.Web/Web References/org.umbraco.our/Reference.cs +++ /dev/null @@ -1,1046 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -// -// This source code was auto-generated by Microsoft.VSDesigner, Version 4.0.30319.42000. -// -#pragma warning disable 1591 - -namespace Umbraco.Web.org.umbraco.our { - using System; - using System.Web.Services; - using System.Diagnostics; - using System.Web.Services.Protocols; - using System.Xml.Serialization; - using System.ComponentModel; - - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - [System.Web.Services.WebServiceBindingAttribute(Name="RepositorySoap", Namespace="http://packages.umbraco.org/webservices/")] - public partial class Repository : System.Web.Services.Protocols.SoapHttpClientProtocol { - - private System.Threading.SendOrPostCallback CategoriesOperationCompleted; - - private System.Threading.SendOrPostCallback ModulesOperationCompleted; - - private System.Threading.SendOrPostCallback ModulesCategorizedOperationCompleted; - - private System.Threading.SendOrPostCallback NitrosOperationCompleted; - - private System.Threading.SendOrPostCallback NitrosCategorizedOperationCompleted; - - private System.Threading.SendOrPostCallback authenticateOperationCompleted; - - private System.Threading.SendOrPostCallback fetchPackageOperationCompleted; - - private System.Threading.SendOrPostCallback fetchPackageByVersionOperationCompleted; - - private System.Threading.SendOrPostCallback fetchProtectedPackageOperationCompleted; - - private System.Threading.SendOrPostCallback SubmitPackageOperationCompleted; - - private System.Threading.SendOrPostCallback PackageByGuidOperationCompleted; - - private bool useDefaultCredentialsSetExplicitly; - - /// - public Repository() { - this.Url = "http://our.umbraco.org/umbraco/webservices/api/repository.asmx"; - if ((this.IsLocalFileSystemWebService(this.Url) == true)) { - this.UseDefaultCredentials = true; - this.useDefaultCredentialsSetExplicitly = false; - } - else { - this.useDefaultCredentialsSetExplicitly = true; - } - } - - public new string Url { - get { - return base.Url; - } - set { - if ((((this.IsLocalFileSystemWebService(base.Url) == true) - && (this.useDefaultCredentialsSetExplicitly == false)) - && (this.IsLocalFileSystemWebService(value) == false))) { - base.UseDefaultCredentials = false; - } - base.Url = value; - } - } - - public new bool UseDefaultCredentials { - get { - return base.UseDefaultCredentials; - } - set { - base.UseDefaultCredentials = value; - this.useDefaultCredentialsSetExplicitly = true; - } - } - - /// - public event CategoriesCompletedEventHandler CategoriesCompleted; - - /// - public event ModulesCompletedEventHandler ModulesCompleted; - - /// - public event ModulesCategorizedCompletedEventHandler ModulesCategorizedCompleted; - - /// - public event NitrosCompletedEventHandler NitrosCompleted; - - /// - public event NitrosCategorizedCompletedEventHandler NitrosCategorizedCompleted; - - /// - public event authenticateCompletedEventHandler authenticateCompleted; - - /// - public event fetchPackageCompletedEventHandler fetchPackageCompleted; - - /// - public event fetchPackageByVersionCompletedEventHandler fetchPackageByVersionCompleted; - - /// - public event fetchProtectedPackageCompletedEventHandler fetchProtectedPackageCompleted; - - /// - public event SubmitPackageCompletedEventHandler SubmitPackageCompleted; - - /// - public event PackageByGuidCompletedEventHandler PackageByGuidCompleted; - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/Categories", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public Category[] Categories(string repositoryGuid) { - object[] results = this.Invoke("Categories", new object[] { - repositoryGuid}); - return ((Category[])(results[0])); - } - - /// - public void CategoriesAsync(string repositoryGuid) { - this.CategoriesAsync(repositoryGuid, null); - } - - /// - public void CategoriesAsync(string repositoryGuid, object userState) { - if ((this.CategoriesOperationCompleted == null)) { - this.CategoriesOperationCompleted = new System.Threading.SendOrPostCallback(this.OnCategoriesOperationCompleted); - } - this.InvokeAsync("Categories", new object[] { - repositoryGuid}, this.CategoriesOperationCompleted, userState); - } - - private void OnCategoriesOperationCompleted(object arg) { - if ((this.CategoriesCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.CategoriesCompleted(this, new CategoriesCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/Modules", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public Package[] Modules() { - object[] results = this.Invoke("Modules", new object[0]); - return ((Package[])(results[0])); - } - - /// - public void ModulesAsync() { - this.ModulesAsync(null); - } - - /// - public void ModulesAsync(object userState) { - if ((this.ModulesOperationCompleted == null)) { - this.ModulesOperationCompleted = new System.Threading.SendOrPostCallback(this.OnModulesOperationCompleted); - } - this.InvokeAsync("Modules", new object[0], this.ModulesOperationCompleted, userState); - } - - private void OnModulesOperationCompleted(object arg) { - if ((this.ModulesCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.ModulesCompleted(this, new ModulesCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/ModulesCategorized", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public Category[] ModulesCategorized() { - object[] results = this.Invoke("ModulesCategorized", new object[0]); - return ((Category[])(results[0])); - } - - /// - public void ModulesCategorizedAsync() { - this.ModulesCategorizedAsync(null); - } - - /// - public void ModulesCategorizedAsync(object userState) { - if ((this.ModulesCategorizedOperationCompleted == null)) { - this.ModulesCategorizedOperationCompleted = new System.Threading.SendOrPostCallback(this.OnModulesCategorizedOperationCompleted); - } - this.InvokeAsync("ModulesCategorized", new object[0], this.ModulesCategorizedOperationCompleted, userState); - } - - private void OnModulesCategorizedOperationCompleted(object arg) { - if ((this.ModulesCategorizedCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.ModulesCategorizedCompleted(this, new ModulesCategorizedCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/Nitros", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public Package[] Nitros() { - object[] results = this.Invoke("Nitros", new object[0]); - return ((Package[])(results[0])); - } - - /// - public void NitrosAsync() { - this.NitrosAsync(null); - } - - /// - public void NitrosAsync(object userState) { - if ((this.NitrosOperationCompleted == null)) { - this.NitrosOperationCompleted = new System.Threading.SendOrPostCallback(this.OnNitrosOperationCompleted); - } - this.InvokeAsync("Nitros", new object[0], this.NitrosOperationCompleted, userState); - } - - private void OnNitrosOperationCompleted(object arg) { - if ((this.NitrosCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.NitrosCompleted(this, new NitrosCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/NitrosCategorized", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public Category[] NitrosCategorized() { - object[] results = this.Invoke("NitrosCategorized", new object[0]); - return ((Category[])(results[0])); - } - - /// - public void NitrosCategorizedAsync() { - this.NitrosCategorizedAsync(null); - } - - /// - public void NitrosCategorizedAsync(object userState) { - if ((this.NitrosCategorizedOperationCompleted == null)) { - this.NitrosCategorizedOperationCompleted = new System.Threading.SendOrPostCallback(this.OnNitrosCategorizedOperationCompleted); - } - this.InvokeAsync("NitrosCategorized", new object[0], this.NitrosCategorizedOperationCompleted, userState); - } - - private void OnNitrosCategorizedOperationCompleted(object arg) { - if ((this.NitrosCategorizedCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.NitrosCategorizedCompleted(this, new NitrosCategorizedCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/authenticate", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public string authenticate(string email, string md5Password) { - object[] results = this.Invoke("authenticate", new object[] { - email, - md5Password}); - return ((string)(results[0])); - } - - /// - public void authenticateAsync(string email, string md5Password) { - this.authenticateAsync(email, md5Password, null); - } - - /// - public void authenticateAsync(string email, string md5Password, object userState) { - if ((this.authenticateOperationCompleted == null)) { - this.authenticateOperationCompleted = new System.Threading.SendOrPostCallback(this.OnauthenticateOperationCompleted); - } - this.InvokeAsync("authenticate", new object[] { - email, - md5Password}, this.authenticateOperationCompleted, userState); - } - - private void OnauthenticateOperationCompleted(object arg) { - if ((this.authenticateCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.authenticateCompleted(this, new authenticateCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/fetchPackage", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - [return: System.Xml.Serialization.XmlElementAttribute(DataType="base64Binary")] - public byte[] fetchPackage(string packageGuid) { - object[] results = this.Invoke("fetchPackage", new object[] { - packageGuid}); - return ((byte[])(results[0])); - } - - /// - public void fetchPackageAsync(string packageGuid) { - this.fetchPackageAsync(packageGuid, null); - } - - /// - public void fetchPackageAsync(string packageGuid, object userState) { - if ((this.fetchPackageOperationCompleted == null)) { - this.fetchPackageOperationCompleted = new System.Threading.SendOrPostCallback(this.OnfetchPackageOperationCompleted); - } - this.InvokeAsync("fetchPackage", new object[] { - packageGuid}, this.fetchPackageOperationCompleted, userState); - } - - private void OnfetchPackageOperationCompleted(object arg) { - if ((this.fetchPackageCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.fetchPackageCompleted(this, new fetchPackageCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/fetchPackageByVersion", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - [return: System.Xml.Serialization.XmlElementAttribute(DataType="base64Binary")] - public byte[] fetchPackageByVersion(string packageGuid, string repoVersion) { - object[] results = this.Invoke("fetchPackageByVersion", new object[] { - packageGuid, - repoVersion}); - return ((byte[])(results[0])); - } - - /// - public void fetchPackageByVersionAsync(string packageGuid, string repoVersion) { - this.fetchPackageByVersionAsync(packageGuid, repoVersion, null); - } - - /// - public void fetchPackageByVersionAsync(string packageGuid, string repoVersion, object userState) { - if ((this.fetchPackageByVersionOperationCompleted == null)) { - this.fetchPackageByVersionOperationCompleted = new System.Threading.SendOrPostCallback(this.OnfetchPackageByVersionOperationCompleted); - } - this.InvokeAsync("fetchPackageByVersion", new object[] { - packageGuid, - repoVersion}, this.fetchPackageByVersionOperationCompleted, userState); - } - - private void OnfetchPackageByVersionOperationCompleted(object arg) { - if ((this.fetchPackageByVersionCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.fetchPackageByVersionCompleted(this, new fetchPackageByVersionCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/fetchProtectedPackage", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - [return: System.Xml.Serialization.XmlElementAttribute(DataType="base64Binary")] - public byte[] fetchProtectedPackage(string packageGuid, string memberKey) { - object[] results = this.Invoke("fetchProtectedPackage", new object[] { - packageGuid, - memberKey}); - return ((byte[])(results[0])); - } - - /// - public void fetchProtectedPackageAsync(string packageGuid, string memberKey) { - this.fetchProtectedPackageAsync(packageGuid, memberKey, null); - } - - /// - public void fetchProtectedPackageAsync(string packageGuid, string memberKey, object userState) { - if ((this.fetchProtectedPackageOperationCompleted == null)) { - this.fetchProtectedPackageOperationCompleted = new System.Threading.SendOrPostCallback(this.OnfetchProtectedPackageOperationCompleted); - } - this.InvokeAsync("fetchProtectedPackage", new object[] { - packageGuid, - memberKey}, this.fetchProtectedPackageOperationCompleted, userState); - } - - private void OnfetchProtectedPackageOperationCompleted(object arg) { - if ((this.fetchProtectedPackageCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.fetchProtectedPackageCompleted(this, new fetchProtectedPackageCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/SubmitPackage", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public SubmitStatus SubmitPackage(string repositoryGuid, string authorGuid, string packageGuid, [System.Xml.Serialization.XmlElementAttribute(DataType="base64Binary")] byte[] packageFile, [System.Xml.Serialization.XmlElementAttribute(DataType="base64Binary")] byte[] packageDoc, [System.Xml.Serialization.XmlElementAttribute(DataType="base64Binary")] byte[] packageThumbnail, string name, string author, string authorUrl, string description) { - object[] results = this.Invoke("SubmitPackage", new object[] { - repositoryGuid, - authorGuid, - packageGuid, - packageFile, - packageDoc, - packageThumbnail, - name, - author, - authorUrl, - description}); - return ((SubmitStatus)(results[0])); - } - - /// - public void SubmitPackageAsync(string repositoryGuid, string authorGuid, string packageGuid, byte[] packageFile, byte[] packageDoc, byte[] packageThumbnail, string name, string author, string authorUrl, string description) { - this.SubmitPackageAsync(repositoryGuid, authorGuid, packageGuid, packageFile, packageDoc, packageThumbnail, name, author, authorUrl, description, null); - } - - /// - public void SubmitPackageAsync(string repositoryGuid, string authorGuid, string packageGuid, byte[] packageFile, byte[] packageDoc, byte[] packageThumbnail, string name, string author, string authorUrl, string description, object userState) { - if ((this.SubmitPackageOperationCompleted == null)) { - this.SubmitPackageOperationCompleted = new System.Threading.SendOrPostCallback(this.OnSubmitPackageOperationCompleted); - } - this.InvokeAsync("SubmitPackage", new object[] { - repositoryGuid, - authorGuid, - packageGuid, - packageFile, - packageDoc, - packageThumbnail, - name, - author, - authorUrl, - description}, this.SubmitPackageOperationCompleted, userState); - } - - private void OnSubmitPackageOperationCompleted(object arg) { - if ((this.SubmitPackageCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.SubmitPackageCompleted(this, new SubmitPackageCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/PackageByGuid", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public Package PackageByGuid(string packageGuid) { - object[] results = this.Invoke("PackageByGuid", new object[] { - packageGuid}); - return ((Package)(results[0])); - } - - /// - public void PackageByGuidAsync(string packageGuid) { - this.PackageByGuidAsync(packageGuid, null); - } - - /// - public void PackageByGuidAsync(string packageGuid, object userState) { - if ((this.PackageByGuidOperationCompleted == null)) { - this.PackageByGuidOperationCompleted = new System.Threading.SendOrPostCallback(this.OnPackageByGuidOperationCompleted); - } - this.InvokeAsync("PackageByGuid", new object[] { - packageGuid}, this.PackageByGuidOperationCompleted, userState); - } - - private void OnPackageByGuidOperationCompleted(object arg) { - if ((this.PackageByGuidCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.PackageByGuidCompleted(this, new PackageByGuidCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - public new void CancelAsync(object userState) { - base.CancelAsync(userState); - } - - private bool IsLocalFileSystemWebService(string url) { - if (((url == null) - || (url == string.Empty))) { - return false; - } - System.Uri wsUri = new System.Uri(url); - if (((wsUri.Port >= 1024) - && (string.Compare(wsUri.Host, "localHost", System.StringComparison.OrdinalIgnoreCase) == 0))) { - return true; - } - return false; - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.7.3062.0")] - [System.SerializableAttribute()] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - [System.Xml.Serialization.XmlTypeAttribute(Namespace="http://packages.umbraco.org/webservices/")] - public partial class Category { - - private string textField; - - private string descriptionField; - - private string urlField; - - private int idField; - - private Package[] packagesField; - - /// - public string Text { - get { - return this.textField; - } - set { - this.textField = value; - } - } - - /// - public string Description { - get { - return this.descriptionField; - } - set { - this.descriptionField = value; - } - } - - /// - public string Url { - get { - return this.urlField; - } - set { - this.urlField = value; - } - } - - /// - public int Id { - get { - return this.idField; - } - set { - this.idField = value; - } - } - - /// - public Package[] Packages { - get { - return this.packagesField; - } - set { - this.packagesField = value; - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.7.3062.0")] - [System.SerializableAttribute()] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - [System.Xml.Serialization.XmlTypeAttribute(Namespace="http://packages.umbraco.org/webservices/")] - public partial class Package { - - private System.Guid repoGuidField; - - private string textField; - - private string descriptionField; - - private string iconField; - - private string thumbnailField; - - private string documentationField; - - private string demoField; - - private bool acceptedField; - - private bool isModuleField; - - private bool editorsPickField; - - private bool protectedField; - - private bool hasUpgradeField; - - private string upgradeVersionField; - - private string upgradeReadMeField; - - private string urlField; - - /// - public System.Guid RepoGuid { - get { - return this.repoGuidField; - } - set { - this.repoGuidField = value; - } - } - - /// - public string Text { - get { - return this.textField; - } - set { - this.textField = value; - } - } - - /// - public string Description { - get { - return this.descriptionField; - } - set { - this.descriptionField = value; - } - } - - /// - public string Icon { - get { - return this.iconField; - } - set { - this.iconField = value; - } - } - - /// - public string Thumbnail { - get { - return this.thumbnailField; - } - set { - this.thumbnailField = value; - } - } - - /// - public string Documentation { - get { - return this.documentationField; - } - set { - this.documentationField = value; - } - } - - /// - public string Demo { - get { - return this.demoField; - } - set { - this.demoField = value; - } - } - - /// - public bool Accepted { - get { - return this.acceptedField; - } - set { - this.acceptedField = value; - } - } - - /// - public bool IsModule { - get { - return this.isModuleField; - } - set { - this.isModuleField = value; - } - } - - /// - public bool EditorsPick { - get { - return this.editorsPickField; - } - set { - this.editorsPickField = value; - } - } - - /// - public bool Protected { - get { - return this.protectedField; - } - set { - this.protectedField = value; - } - } - - /// - public bool HasUpgrade { - get { - return this.hasUpgradeField; - } - set { - this.hasUpgradeField = value; - } - } - - /// - public string UpgradeVersion { - get { - return this.upgradeVersionField; - } - set { - this.upgradeVersionField = value; - } - } - - /// - public string UpgradeReadMe { - get { - return this.upgradeReadMeField; - } - set { - this.upgradeReadMeField = value; - } - } - - /// - public string Url { - get { - return this.urlField; - } - set { - this.urlField = value; - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.7.3062.0")] - [System.SerializableAttribute()] - [System.Xml.Serialization.XmlTypeAttribute(Namespace="http://packages.umbraco.org/webservices/")] - public enum SubmitStatus { - - /// - Complete, - - /// - Exists, - - /// - NoAccess, - - /// - Error, - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void CategoriesCompletedEventHandler(object sender, CategoriesCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class CategoriesCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal CategoriesCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public Category[] Result { - get { - this.RaiseExceptionIfNecessary(); - return ((Category[])(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void ModulesCompletedEventHandler(object sender, ModulesCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class ModulesCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal ModulesCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public Package[] Result { - get { - this.RaiseExceptionIfNecessary(); - return ((Package[])(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void ModulesCategorizedCompletedEventHandler(object sender, ModulesCategorizedCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class ModulesCategorizedCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal ModulesCategorizedCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public Category[] Result { - get { - this.RaiseExceptionIfNecessary(); - return ((Category[])(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void NitrosCompletedEventHandler(object sender, NitrosCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class NitrosCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal NitrosCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public Package[] Result { - get { - this.RaiseExceptionIfNecessary(); - return ((Package[])(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void NitrosCategorizedCompletedEventHandler(object sender, NitrosCategorizedCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class NitrosCategorizedCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal NitrosCategorizedCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public Category[] Result { - get { - this.RaiseExceptionIfNecessary(); - return ((Category[])(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void authenticateCompletedEventHandler(object sender, authenticateCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class authenticateCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal authenticateCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public string Result { - get { - this.RaiseExceptionIfNecessary(); - return ((string)(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void fetchPackageCompletedEventHandler(object sender, fetchPackageCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class fetchPackageCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal fetchPackageCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public byte[] Result { - get { - this.RaiseExceptionIfNecessary(); - return ((byte[])(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void fetchPackageByVersionCompletedEventHandler(object sender, fetchPackageByVersionCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class fetchPackageByVersionCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal fetchPackageByVersionCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public byte[] Result { - get { - this.RaiseExceptionIfNecessary(); - return ((byte[])(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void fetchProtectedPackageCompletedEventHandler(object sender, fetchProtectedPackageCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class fetchProtectedPackageCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal fetchProtectedPackageCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public byte[] Result { - get { - this.RaiseExceptionIfNecessary(); - return ((byte[])(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void SubmitPackageCompletedEventHandler(object sender, SubmitPackageCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class SubmitPackageCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal SubmitPackageCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public SubmitStatus Result { - get { - this.RaiseExceptionIfNecessary(); - return ((SubmitStatus)(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void PackageByGuidCompletedEventHandler(object sender, PackageByGuidCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class PackageByGuidCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal PackageByGuidCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public Package Result { - get { - this.RaiseExceptionIfNecessary(); - return ((Package)(this.results[0])); - } - } - } -} - -#pragma warning restore 1591 \ No newline at end of file diff --git a/src/Umbraco.Web/Web References/org.umbraco.our/Reference.map b/src/Umbraco.Web/Web References/org.umbraco.our/Reference.map deleted file mode 100644 index 8d133063a5..0000000000 --- a/src/Umbraco.Web/Web References/org.umbraco.our/Reference.map +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/src/Umbraco.Web/Web References/org.umbraco.our/repository.disco b/src/Umbraco.Web/Web References/org.umbraco.our/repository.disco deleted file mode 100644 index 4bf1a61fca..0000000000 --- a/src/Umbraco.Web/Web References/org.umbraco.our/repository.disco +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/Umbraco.Web/Web References/org.umbraco.our/repository.wsdl b/src/Umbraco.Web/Web References/org.umbraco.our/repository.wsdl deleted file mode 100644 index 2a3f3502db..0000000000 --- a/src/Umbraco.Web/Web References/org.umbraco.our/repository.wsdl +++ /dev/null @@ -1,995 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From ca75a258029bf076ed1c00bbeb7e7456da2f10cd Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Wed, 24 Jul 2019 11:32:18 +0200 Subject: [PATCH 0043/1001] Makes checks a little more robust --- .../PropertyEditors/TagsPropertyEditor.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs index b7101aa764..578b6fcd00 100644 --- a/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs @@ -39,14 +39,23 @@ namespace Umbraco.Web.PropertyEditors /// public override object FromEditor(ContentPropertyData editorValue, object currentValue) { + var value = editorValue?.Value?.ToString(); + + if (string.IsNullOrEmpty(value)) + { + return null; + } + if (editorValue.Value is JArray json) { return json.Select(x => x.Value()); } - else if ( editorValue.Value is string stringValue && !String.IsNullOrWhiteSpace(stringValue)) + + if (string.IsNullOrWhiteSpace(value) == false) { - return stringValue.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); + return value.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); } + return null; } From 361557dc39969299edb0f57d89b47d0e76d31369 Mon Sep 17 00:00:00 2001 From: Steve Megson Date: Sun, 21 Jul 2019 15:19:17 +0100 Subject: [PATCH 0044/1001] Handle CSV values in TagsPropertyEditor (cherry picked from commit a97604d6c405c7ebf6372f69807193e8cbbc7a50) --- .../PropertyEditors/TagsPropertyEditor.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs index 90527a8b8d..b7101aa764 100644 --- a/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using Newtonsoft.Json.Linq; @@ -38,9 +39,15 @@ namespace Umbraco.Web.PropertyEditors /// public override object FromEditor(ContentPropertyData editorValue, object currentValue) { - return editorValue.Value is JArray json - ? json.Select(x => x.Value()) - : null; + if (editorValue.Value is JArray json) + { + return json.Select(x => x.Value()); + } + else if ( editorValue.Value is string stringValue && !String.IsNullOrWhiteSpace(stringValue)) + { + return stringValue.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); + } + return null; } /// From 17527c99ebbf80de6ec2d5e75a044eca717a5be9 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Wed, 24 Jul 2019 11:32:18 +0200 Subject: [PATCH 0045/1001] Makes checks a little more robust (cherry picked from commit ca75a258029bf076ed1c00bbeb7e7456da2f10cd) --- .../PropertyEditors/TagsPropertyEditor.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs index b7101aa764..578b6fcd00 100644 --- a/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs @@ -39,14 +39,23 @@ namespace Umbraco.Web.PropertyEditors /// public override object FromEditor(ContentPropertyData editorValue, object currentValue) { + var value = editorValue?.Value?.ToString(); + + if (string.IsNullOrEmpty(value)) + { + return null; + } + if (editorValue.Value is JArray json) { return json.Select(x => x.Value()); } - else if ( editorValue.Value is string stringValue && !String.IsNullOrWhiteSpace(stringValue)) + + if (string.IsNullOrWhiteSpace(value) == false) { - return stringValue.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); + return value.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); } + return null; } From efd6bd82bd9f31c32d2b507cb79e140ff070129e Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 12 Jul 2019 11:23:36 +1000 Subject: [PATCH 0046/1001] Revert "V8: Don't show multiple open menus (take two) (#5609)" This reverts commit 9ce996cbba49199423f4e79e223497d5c3c8cd4f. (cherry picked from commit 1d49c8626ec40519a19764959b18efb2868d8f61) --- .../components/events/events.directive.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js index 47e6818466..15e74bbd90 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js @@ -101,14 +101,14 @@ angular.module('umbraco.directives') var eventBindings = []; function oneTimeClick(event) { - // ignore clicks on button groups toggles (i.e. the save and publish button) - var parents = $(event.target).closest("[data-element='button-group-toggle']"); - if (parents.length > 0) { - return; - } + var el = event.target.nodeName; + + //ignore link and button clicks + var els = ["INPUT", "A", "BUTTON"]; + if (els.indexOf(el) >= 0) { return; } // ignore clicks on new overlay - parents = $(event.target).parents(".umb-overlay,.umb-tour"); + var parents = $(event.target).parents("a,button,.umb-overlay,.umb-tour"); if (parents.length > 0) { return; } @@ -131,12 +131,6 @@ angular.module('umbraco.directives') return; } - // ignore clicks on dialog actions - var actions = $(event.target).parents(".umb-action"); - if (actions.length === 1) { - return; - } - //ignore clicks inside this element if ($(element).has($(event.target)).length > 0) { return; From 7bc7dba86d4d7bd3d45f8b73c53006cc7f3ef61f Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 24 Jul 2019 21:46:19 +1000 Subject: [PATCH 0047/1001] fixes timing issue --- .../Web/PublishedContentQueryTests.cs | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/Umbraco.Tests/Web/PublishedContentQueryTests.cs b/src/Umbraco.Tests/Web/PublishedContentQueryTests.cs index 2cff946372..a3505aeb0e 100644 --- a/src/Umbraco.Tests/Web/PublishedContentQueryTests.cs +++ b/src/Umbraco.Tests/Web/PublishedContentQueryTests.cs @@ -36,22 +36,26 @@ namespace Umbraco.Tests.Web { var indexer = new TestIndex("TestIndex", luceneDirectory, fieldNames); - //populate with some test data - indexer.IndexItem(new ValueSet("1", "content", new Dictionary + using (indexer.ProcessNonAsync()) { - [fieldNames[0]] = "Hello world, there are products here", - [UmbracoContentIndex.VariesByCultureFieldName] = "n" - })); - indexer.IndexItem(new ValueSet("2", "content", new Dictionary - { - [fieldNames[1]] = "Hello world, there are products here", - [UmbracoContentIndex.VariesByCultureFieldName] = "y" - })); - indexer.IndexItem(new ValueSet("3", "content", new Dictionary - { - [fieldNames[2]] = "Hello world, there are products here", - [UmbracoContentIndex.VariesByCultureFieldName] = "y" - })); + //populate with some test data + indexer.IndexItem(new ValueSet("1", "content", new Dictionary + { + [fieldNames[0]] = "Hello world, there are products here", + [UmbracoContentIndex.VariesByCultureFieldName] = "n" + })); + indexer.IndexItem(new ValueSet("2", "content", new Dictionary + { + [fieldNames[1]] = "Hello world, there are products here", + [UmbracoContentIndex.VariesByCultureFieldName] = "y" + })); + indexer.IndexItem(new ValueSet("3", "content", new Dictionary + { + [fieldNames[2]] = "Hello world, there are products here", + [UmbracoContentIndex.VariesByCultureFieldName] = "y" + })); + } + return indexer; } @@ -80,7 +84,7 @@ namespace Umbraco.Tests.Web { var fieldNames = new[] { "title", "title_en-us", "title_fr-fr" }; using (var indexer = CreateTestIndex(luceneDir, fieldNames)) - { + { var pcq = CreatePublishedContentQuery(indexer); var results = pcq.Search("Products", culture, "TestIndex"); From 0ab84fc44357928e580329f19a6ede5da9ce365a Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 25 Jul 2019 10:28:16 +0200 Subject: [PATCH 0048/1001] Allow removing user start nodes --- .../src/views/users/views/user/details.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html index 793f404f0c..881d7a9c0d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html @@ -90,6 +90,7 @@ ng-repeat="node in model.user.startContentIds" icon="node.icon" name="node.name" + allow-remove="true" on-remove="model.removeSelectedItem($index, model.user.startContentIds)"> @@ -114,6 +115,7 @@ ng-repeat="node in model.user.startMediaIds" icon="node.icon" name="node.name" + allow-remove="true" on-remove="model.removeSelectedItem($index, model.user.startMediaIds)"> From 4a24064783efb45f7eed1db9833ca1f8b9012f3d Mon Sep 17 00:00:00 2001 From: arkadiuszbiel Date: Sat, 27 Jul 2019 20:41:15 +0100 Subject: [PATCH 0049/1001] Value Set builder shouldn't stop indexing when one field is empty in value sets --- src/Umbraco.Examine/BaseValueSetBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Examine/BaseValueSetBuilder.cs b/src/Umbraco.Examine/BaseValueSetBuilder.cs index 22d379d148..93cee88231 100644 --- a/src/Umbraco.Examine/BaseValueSetBuilder.cs +++ b/src/Umbraco.Examine/BaseValueSetBuilder.cs @@ -45,7 +45,7 @@ namespace Umbraco.Examine continue; case string strVal: { - if (strVal.IsNullOrWhiteSpace()) return; + if (strVal.IsNullOrWhiteSpace()) continue; var key = $"{keyVal.Key}{cultureSuffix}"; if (values.TryGetValue(key, out var v)) values[key] = new List(v) { val }.ToArray(); From 41093e5b8b59ecd7a71dae7ec3fddb5e672e9a2d Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 29 Jul 2019 15:38:08 +1000 Subject: [PATCH 0050/1001] Value Set builder shouldn't stop indexing when one field is empty in value sets --- src/Umbraco.Examine/BaseValueSetBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Examine/BaseValueSetBuilder.cs b/src/Umbraco.Examine/BaseValueSetBuilder.cs index 22d379d148..93cee88231 100644 --- a/src/Umbraco.Examine/BaseValueSetBuilder.cs +++ b/src/Umbraco.Examine/BaseValueSetBuilder.cs @@ -45,7 +45,7 @@ namespace Umbraco.Examine continue; case string strVal: { - if (strVal.IsNullOrWhiteSpace()) return; + if (strVal.IsNullOrWhiteSpace()) continue; var key = $"{keyVal.Key}{cultureSuffix}"; if (values.TryGetValue(key, out var v)) values[key] = new List(v) { val }.ToArray(); From 4612c036576468ad6f24240686b823034cf7254f Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 29 Jul 2019 15:43:03 +1000 Subject: [PATCH 0051/1001] Fixes #5789 - PublishedContentQuery Search always searches on culture --- src/Umbraco.Examine/ExamineExtensions.cs | 25 +++++ .../TestHelpers/RandomIdRamDirectory.cs | 22 +++++ src/Umbraco.Tests/Umbraco.Tests.csproj | 2 + .../Web/PublishedContentQueryTests.cs | 99 +++++++++++++++++++ src/Umbraco.Tests/Web/UmbracoHelperTests.cs | 10 +- src/Umbraco.Web/IPublishedContentQuery.cs | 16 ++- src/Umbraco.Web/PublishedContentQuery.cs | 40 +++++--- src/Umbraco.Web/UmbracoContextFactory.cs | 8 ++ 8 files changed, 203 insertions(+), 19 deletions(-) create mode 100644 src/Umbraco.Tests/TestHelpers/RandomIdRamDirectory.cs create mode 100644 src/Umbraco.Tests/Web/PublishedContentQueryTests.cs diff --git a/src/Umbraco.Examine/ExamineExtensions.cs b/src/Umbraco.Examine/ExamineExtensions.cs index 43a3ccc196..376950c95e 100644 --- a/src/Umbraco.Examine/ExamineExtensions.cs +++ b/src/Umbraco.Examine/ExamineExtensions.cs @@ -48,6 +48,31 @@ namespace Umbraco.Examine } } + /// + /// Returns all index fields that are culture specific (suffixed) or invariant + /// + /// + /// + /// + public static IEnumerable GetCultureAndInvariantFields(this IUmbracoIndex index, string culture) + { + var allFields = index.GetFields(); + // ReSharper disable once LoopCanBeConvertedToQuery + foreach (var field in allFields) + { + var match = CultureIsoCodeFieldNameMatchExpression.Match(field); + if (match.Success && match.Groups.Count == 3 && culture.InvariantEquals(match.Groups[2].Value)) + { + yield return field; //matches this culture field + } + else if (!match.Success) + { + yield return field; //matches no culture field (invariant) + } + + } + } + internal static bool TryParseLuceneQuery(string query) { // TODO: I'd assume there would be a more strict way to parse the query but not that i can find yet, for now we'll diff --git a/src/Umbraco.Tests/TestHelpers/RandomIdRamDirectory.cs b/src/Umbraco.Tests/TestHelpers/RandomIdRamDirectory.cs new file mode 100644 index 0000000000..34904db1ae --- /dev/null +++ b/src/Umbraco.Tests/TestHelpers/RandomIdRamDirectory.cs @@ -0,0 +1,22 @@ +using Lucene.Net.Store; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Tests.TestHelpers +{ + + /// + /// Used for tests with Lucene so that each RAM directory is unique + /// + public class RandomIdRAMDirectory : RAMDirectory + { + private readonly string _lockId = Guid.NewGuid().ToString(); + public override string GetLockId() + { + return _lockId; + } + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index f41ff1dd07..717006b702 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -157,6 +157,7 @@ + @@ -268,6 +269,7 @@ + diff --git a/src/Umbraco.Tests/Web/PublishedContentQueryTests.cs b/src/Umbraco.Tests/Web/PublishedContentQueryTests.cs new file mode 100644 index 0000000000..a3505aeb0e --- /dev/null +++ b/src/Umbraco.Tests/Web/PublishedContentQueryTests.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Examine; +using Examine.LuceneEngine.Providers; +using Lucene.Net.Store; +using Moq; +using NUnit.Framework; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Examine; +using Umbraco.Tests.TestHelpers; +using Umbraco.Web; +using Umbraco.Web.PublishedCache; + +namespace Umbraco.Tests.Web +{ + [TestFixture] + public class PublishedContentQueryTests + { + + private class TestIndex : LuceneIndex, IUmbracoIndex + { + private readonly string[] _fieldNames; + + public TestIndex(string name, Directory luceneDirectory, string[] fieldNames) + : base(name, luceneDirectory, null, null, null, null) + { + _fieldNames = fieldNames; + } + public bool EnableDefaultEventHandler => throw new NotImplementedException(); + public bool PublishedValuesOnly => throw new NotImplementedException(); + public IEnumerable GetFields() => _fieldNames; + } + + private TestIndex CreateTestIndex(Directory luceneDirectory, string[] fieldNames) + { + var indexer = new TestIndex("TestIndex", luceneDirectory, fieldNames); + + using (indexer.ProcessNonAsync()) + { + //populate with some test data + indexer.IndexItem(new ValueSet("1", "content", new Dictionary + { + [fieldNames[0]] = "Hello world, there are products here", + [UmbracoContentIndex.VariesByCultureFieldName] = "n" + })); + indexer.IndexItem(new ValueSet("2", "content", new Dictionary + { + [fieldNames[1]] = "Hello world, there are products here", + [UmbracoContentIndex.VariesByCultureFieldName] = "y" + })); + indexer.IndexItem(new ValueSet("3", "content", new Dictionary + { + [fieldNames[2]] = "Hello world, there are products here", + [UmbracoContentIndex.VariesByCultureFieldName] = "y" + })); + } + + return indexer; + } + + private PublishedContentQuery CreatePublishedContentQuery(IIndex indexer) + { + var examineManager = new Mock(); + IIndex outarg = indexer; + examineManager.Setup(x => x.TryGetIndex("TestIndex", out outarg)).Returns(true); + + var contentCache = new Mock(); + contentCache.Setup(x => x.GetById(It.IsAny())).Returns((int intId) => Mock.Of(x => x.Id == intId)); + var snapshot = Mock.Of(x => x.Content == contentCache.Object); + var variationContext = new VariationContext(); + var variationContextAccessor = Mock.Of(x => x.VariationContext == variationContext); + + return new PublishedContentQuery(snapshot, variationContextAccessor, examineManager.Object); + } + + [TestCase("fr-fr", ExpectedResult = "1, 3", TestName = "Search Culture: fr-fr. Must return both fr-fr and invariant results")] + [TestCase("en-us", ExpectedResult = "1, 2", TestName = "Search Culture: en-us. Must return both en-us and invariant results")] + [TestCase("*", ExpectedResult = "1, 2, 3", TestName = "Search Culture: *. Must return all cultures and all invariant results")] + [TestCase(null, ExpectedResult = "1", TestName = "Search Culture: null. Must return only invariant results")] + public string Search(string culture) + { + using (var luceneDir = new RandomIdRAMDirectory()) + { + var fieldNames = new[] { "title", "title_en-us", "title_fr-fr" }; + using (var indexer = CreateTestIndex(luceneDir, fieldNames)) + { + var pcq = CreatePublishedContentQuery(indexer); + + var results = pcq.Search("Products", culture, "TestIndex"); + + var ids = results.Select(x => x.Content.Id).ToArray(); + + return string.Join(", ", ids); + } + } + } + } +} diff --git a/src/Umbraco.Tests/Web/UmbracoHelperTests.cs b/src/Umbraco.Tests/Web/UmbracoHelperTests.cs index b23b5bd6b7..26d85f60cf 100644 --- a/src/Umbraco.Tests/Web/UmbracoHelperTests.cs +++ b/src/Umbraco.Tests/Web/UmbracoHelperTests.cs @@ -1,5 +1,8 @@ using System; using System.Text; +using Examine.LuceneEngine; +using Lucene.Net.Analysis; +using Lucene.Net.Analysis.Standard; using Moq; using NUnit.Framework; using Umbraco.Core; @@ -13,18 +16,19 @@ using Umbraco.Web; namespace Umbraco.Tests.Web { + [TestFixture] public class UmbracoHelperTests - { + { [TearDown] public void TearDown() { Current.Reset(); } - - + + // ------- Int32 conversion tests [Test] public static void Converting_Boxed_34_To_An_Int_Returns_34() diff --git a/src/Umbraco.Web/IPublishedContentQuery.cs b/src/Umbraco.Web/IPublishedContentQuery.cs index 76e7be5e97..8a8d678aba 100644 --- a/src/Umbraco.Web/IPublishedContentQuery.cs +++ b/src/Umbraco.Web/IPublishedContentQuery.cs @@ -39,10 +39,14 @@ namespace Umbraco.Web /// Optional culture. /// Optional index name. /// - /// When the is not specified, all cultures are searched. + /// + /// When the is not specified or is *, all cultures are searched. + /// To search for only invariant documents and fields use null. + /// When searching on a specific culture, all culture specific fields are searched for the provided culture and all invariant fields for all documents. + /// /// While enumerating results, the ambient culture is changed to be the searched culture. /// - IEnumerable Search(string term, string culture = null, string indexName = null); + IEnumerable Search(string term, string culture = "*", string indexName = null); /// /// Searches content. @@ -54,10 +58,14 @@ namespace Umbraco.Web /// Optional culture. /// Optional index name. /// - /// When the is not specified, all cultures are searched. + /// + /// When the is not specified or is *, all cultures are searched. + /// To search for only invariant documents and fields use null. + /// When searching on a specific culture, all culture specific fields are searched for the provided culture and all invariant fields for all documents. + /// /// While enumerating results, the ambient culture is changed to be the searched culture. /// - IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = null, string indexName = null); + IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = null); /// /// Executes the query and converts the results to PublishedSearchResult. diff --git a/src/Umbraco.Web/PublishedContentQuery.cs b/src/Umbraco.Web/PublishedContentQuery.cs index 61180580cb..2dbe4de4c5 100644 --- a/src/Umbraco.Web/PublishedContentQuery.cs +++ b/src/Umbraco.Web/PublishedContentQuery.cs @@ -20,14 +20,22 @@ namespace Umbraco.Web { private readonly IPublishedSnapshot _publishedSnapshot; private readonly IVariationContextAccessor _variationContextAccessor; + private readonly IExamineManager _examineManager; + + [Obsolete("Use the constructor with all parameters instead")] + public PublishedContentQuery(IPublishedSnapshot publishedSnapshot, IVariationContextAccessor variationContextAccessor) + : this (publishedSnapshot, variationContextAccessor, ExamineManager.Instance) + { + } /// /// Initializes a new instance of the class. /// - public PublishedContentQuery(IPublishedSnapshot publishedSnapshot, IVariationContextAccessor variationContextAccessor) + public PublishedContentQuery(IPublishedSnapshot publishedSnapshot, IVariationContextAccessor variationContextAccessor, IExamineManager examineManager) { _publishedSnapshot = publishedSnapshot ?? throw new ArgumentNullException(nameof(publishedSnapshot)); _variationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); + _examineManager = examineManager ?? throw new ArgumentNullException(nameof(examineManager)); } #region Content @@ -175,19 +183,19 @@ namespace Umbraco.Web #region Search /// - public IEnumerable Search(string term, string culture = null, string indexName = null) + public IEnumerable Search(string term, string culture = "*", string indexName = null) { return Search(term, 0, 0, out _, culture, indexName); } /// - public IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = null, string indexName = null) + public IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = null) { indexName = string.IsNullOrEmpty(indexName) ? Constants.UmbracoIndexes.ExternalIndexName : indexName; - if (!ExamineManager.Instance.TryGetIndex(indexName, out var index) || !(index is IUmbracoIndex umbIndex)) + if (!_examineManager.TryGetIndex(indexName, out var index) || !(index is IUmbracoIndex umbIndex)) throw new InvalidOperationException($"No index found by name {indexName} or is not of type {typeof(IUmbracoIndex)}"); var searcher = umbIndex.GetSearcher(); @@ -195,20 +203,28 @@ namespace Umbraco.Web // default to max 500 results var count = skip == 0 && take == 0 ? 500 : skip + take; - //set this to the specific culture or to the culture in the request - culture = culture ?? _variationContextAccessor.VariationContext.Culture; - ISearchResults results; - if (culture.IsNullOrWhiteSpace()) + if (culture == "*") { + //search everything + results = searcher.Search(term, count); } + else if (culture.IsNullOrWhiteSpace()) + { + //only search invariant + + var qry = searcher.CreateQuery().Field(UmbracoContentIndex.VariesByCultureFieldName, "n"); //must not vary by culture + qry = qry.And().ManagedQuery(term); + results = qry.Execute(count); + } else { + //search only the specified culture + //get all index fields suffixed with the culture name supplied - var cultureFields = umbIndex.GetCultureFields(culture); - var qry = searcher.CreateQuery().Field(UmbracoContentIndex.VariesByCultureFieldName, "y"); //must vary by culture - qry = qry.And().ManagedQuery(term, cultureFields.ToArray()); + var cultureFields = umbIndex.GetCultureAndInvariantFields(culture).ToArray(); + var qry = searcher.CreateQuery().ManagedQuery(term, cultureFields); results = qry.Execute(count); } @@ -304,7 +320,7 @@ namespace Umbraco.Web } } - + #endregion diff --git a/src/Umbraco.Web/UmbracoContextFactory.cs b/src/Umbraco.Web/UmbracoContextFactory.cs index 2a812036bf..11d8952fa6 100644 --- a/src/Umbraco.Web/UmbracoContextFactory.cs +++ b/src/Umbraco.Web/UmbracoContextFactory.cs @@ -53,7 +53,15 @@ namespace Umbraco.Web { // make sure we have a variation context if (_variationContextAccessor.VariationContext == null) + { + // TODO: By using _defaultCultureAccessor.DefaultCulture this means that the VariationContext will always return a variant culture, it will never + // return an empty string signifying that the culture is invariant. But does this matter? Are we actually expecting this to return an empty string + // for invariant routes? From what i can tell throughout the codebase is that whenever we are checking against the VariationContext.Culture we are + // also checking if the content type varies by culture or not. This is fine, however the code in the ctor of VariationContext is then misleading + // since it's assuming that the Culture can be empty (invariant) when in reality of a website this will never be empty since a real culture is always set here. _variationContextAccessor.VariationContext = new VariationContext(_defaultCultureAccessor.DefaultCulture); + } + var webSecurity = new WebSecurity(httpContext, _userService, _globalSettings); From 49b3351c72cf5e53f67f583d996d60b88ca52c22 Mon Sep 17 00:00:00 2001 From: Rasmus John Pedersen Date: Mon, 29 Jul 2019 12:59:29 +0200 Subject: [PATCH 0052/1001] Fix exception when stored media url is already absolute --- src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs | 13 +++++++++++++ src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs | 4 ++++ 2 files changed, 17 insertions(+) diff --git a/src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs b/src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs index 5af48e64b1..6489417dc7 100644 --- a/src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs +++ b/src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs @@ -81,6 +81,19 @@ namespace Umbraco.Tests.Routing Assert.AreEqual(expected, resolvedUrl); } + [Test] + public void Get_Media_Url_Returns_Absolute_Url_If_Stored_Url_Is_Absolute() + { + const string expected = "http://localhost/media/rfeiw584/test.jpg"; + + var umbracoContext = GetUmbracoContext("http://localhost", mediaUrlProviders: new[] { _mediaUrlProvider }); + var publishedContent = CreatePublishedContent(Constants.PropertyEditors.Aliases.UploadField, expected, null); + + var resolvedUrl = umbracoContext.UrlProvider.GetMediaUrl(publishedContent, UrlMode.Relative); + + Assert.AreEqual(expected, resolvedUrl); + } + [Test] public void Get_Media_Url_Returns_Empty_String_When_PropertyType_Is_Not_Supported() { diff --git a/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs b/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs index 18d10e577d..02dc4ebf29 100644 --- a/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs +++ b/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs @@ -45,6 +45,10 @@ namespace Umbraco.Web.Routing if (string.IsNullOrEmpty(path)) return null; + // the stored path is absolute so we just return it as is + if(Uri.IsWellFormedUriString(path, UriKind.Absolute)) + return new Uri(path); + Uri uri; if (current == null) From ca56655d07f86d5b21abb2f6d5560e98be2d05e5 Mon Sep 17 00:00:00 2001 From: Rasmus John Pedersen Date: Mon, 29 Jul 2019 12:59:29 +0200 Subject: [PATCH 0053/1001] Fix exception when stored media url is already absolute (cherry picked from commit 49b3351c72cf5e53f67f583d996d60b88ca52c22) --- src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs | 13 +++++++++++++ src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs | 4 ++++ 2 files changed, 17 insertions(+) diff --git a/src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs b/src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs index 5af48e64b1..6489417dc7 100644 --- a/src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs +++ b/src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs @@ -81,6 +81,19 @@ namespace Umbraco.Tests.Routing Assert.AreEqual(expected, resolvedUrl); } + [Test] + public void Get_Media_Url_Returns_Absolute_Url_If_Stored_Url_Is_Absolute() + { + const string expected = "http://localhost/media/rfeiw584/test.jpg"; + + var umbracoContext = GetUmbracoContext("http://localhost", mediaUrlProviders: new[] { _mediaUrlProvider }); + var publishedContent = CreatePublishedContent(Constants.PropertyEditors.Aliases.UploadField, expected, null); + + var resolvedUrl = umbracoContext.UrlProvider.GetMediaUrl(publishedContent, UrlMode.Relative); + + Assert.AreEqual(expected, resolvedUrl); + } + [Test] public void Get_Media_Url_Returns_Empty_String_When_PropertyType_Is_Not_Supported() { diff --git a/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs b/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs index 18d10e577d..02dc4ebf29 100644 --- a/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs +++ b/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs @@ -45,6 +45,10 @@ namespace Umbraco.Web.Routing if (string.IsNullOrEmpty(path)) return null; + // the stored path is absolute so we just return it as is + if(Uri.IsWellFormedUriString(path, UriKind.Absolute)) + return new Uri(path); + Uri uri; if (current == null) From 365ad2981d7cfcf7bfd01fa620bb53f1208e94db Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 30 Jul 2019 19:02:03 +1000 Subject: [PATCH 0054/1001] Fixing issue when changing a property type from invariant to variant and back again and the document always reporting pending changes. --- .../Persistence/Factories/PropertyFactory.cs | 43 ++++++++++++------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs index f1473b5888..f0937a3781 100644 --- a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Persistence.Factories public static IEnumerable BuildEntities(PropertyType[] propertyTypes, IReadOnlyCollection dtos, int publishedVersionId, ILanguageRepository languageRepository) { var properties = new List(); - var xdtos = dtos.GroupBy(x => x.PropertyTypeId).ToDictionary(x => x.Key, x => (IEnumerable) x); + var xdtos = dtos.GroupBy(x => x.PropertyTypeId).ToDictionary(x => x.Key, x => (IEnumerable)x); foreach (var propertyType in propertyTypes) { @@ -130,6 +130,9 @@ namespace Umbraco.Core.Persistence.Factories // publishing = deal with edit and published values foreach (var propertyValue in property.Values) { + var isInvariantValue = propertyValue.Culture == null; + var isCultureValue = propertyValue.Culture != null && propertyValue.Segment == null; + // deal with published value if (propertyValue.PublishedValue != null && publishedVersionId > 0) propertyDataDtos.Add(BuildDto(publishedVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.PublishedValue)); @@ -138,26 +141,34 @@ namespace Umbraco.Core.Persistence.Factories if (propertyValue.EditedValue != null) propertyDataDtos.Add(BuildDto(currentVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.EditedValue)); + // property.Values will contain ALL of it's values, both variant and invariant which will be populated if the administrator has previously + // changed the property type to be variant vs invariant. + // We need to check for this scenario here because otherwise the editedCultures and edited flags + // will end up incorrectly so here we need to only process edited cultures based on the + // current value type and how the property varies. + + if (property.PropertyType.VariesByCulture() && isInvariantValue) continue; + if (!property.PropertyType.VariesByCulture() && isCultureValue) continue; + // use explicit equals here, else object comparison fails at comparing eg strings var sameValues = propertyValue.PublishedValue == null ? propertyValue.EditedValue == null : propertyValue.PublishedValue.Equals(propertyValue.EditedValue); + edited |= !sameValues; - if (entityVariesByCulture // cultures can be edited, ie CultureNeutral is supported - && propertyValue.Culture != null && propertyValue.Segment == null // and value is CultureNeutral - && !sameValues) // and edited and published are different + if (entityVariesByCulture && !sameValues) { - editedCultures.Add(propertyValue.Culture); // report culture as edited - } + if (isCultureValue) + { + editedCultures.Add(propertyValue.Culture); // report culture as edited + } + else if (isInvariantValue) + { + // flag culture as edited if it contains an edited invariant property + if (defaultCulture == null) + defaultCulture = languageRepository.GetDefaultIsoCode(); - // flag culture as edited if it contains an edited invariant property - if (propertyValue.Culture == null //invariant property - && !sameValues // and edited and published are different - && entityVariesByCulture) //only when the entity is variant - { - if (defaultCulture == null) - defaultCulture = languageRepository.GetDefaultIsoCode(); - - editedCultures.Add(defaultCulture); + editedCultures.Add(defaultCulture); + } } } } @@ -167,7 +178,7 @@ namespace Umbraco.Core.Persistence.Factories { // not publishing = only deal with edit values if (propertyValue.EditedValue != null) - propertyDataDtos.Add(BuildDto(currentVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.EditedValue)); + propertyDataDtos.Add(BuildDto(currentVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.EditedValue)); } edited = true; } From 3930ae244c7c07f4d245c4f6c58d3ce767a9be90 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 24 Jul 2019 19:40:51 +1000 Subject: [PATCH 0055/1001] Created unit test to show the problem. --- .../Services/ContentServiceTests.cs | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 222f40aeed..a719e04b2a 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -761,6 +761,61 @@ namespace Umbraco.Tests.Services } + [Test] + public void Unpublish_All_Cultures_Has_Unpublished_State() + { + // Arrange + + var langUk = new Language("en-GB") { IsDefault = true }; + var langFr = new Language("fr-FR"); + + ServiceContext.LocalizationService.Save(langFr); + ServiceContext.LocalizationService.Save(langUk); + + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + IContent content = new Content("content", Constants.System.Root, contentType); + content.SetCultureName("content-fr", langFr.IsoCode); + content.SetCultureName("content-en", langUk.IsoCode); + + var published = ServiceContext.ContentService.SaveAndPublish(content, new[] { langFr.IsoCode, langUk.IsoCode }); + Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); + Assert.IsTrue(published.Success); + Assert.AreEqual(PublishedState.Published, content.PublishedState); + + //re-get + content = ServiceContext.ContentService.GetById(content.Id); + Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); + Assert.AreEqual(PublishedState.Published, content.PublishedState); + + var unpublished = ServiceContext.ContentService.Unpublish(content, langFr.IsoCode); + Assert.IsTrue(unpublished.Success); + Assert.AreEqual(PublishResultType.SuccessUnpublishCulture, unpublished.Result); + Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); + Assert.AreEqual(PublishedState.Published, content.PublishedState); //still published + + //re-get + content = ServiceContext.ContentService.GetById(content.Id); + Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); + + unpublished = ServiceContext.ContentService.Unpublish(content, langUk.IsoCode); + Assert.IsTrue(unpublished.Success); + Assert.AreEqual(PublishResultType.SuccessUnpublishCulture, unpublished.Result); + Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsFalse(content.IsCulturePublished(langUk.IsoCode)); + Assert.AreEqual(PublishedState.Unpublished, content.PublishedState); //the last culture was unpublished so the document should also reflect this + + //re-get + content = ServiceContext.ContentService.GetById(content.Id); + Assert.AreEqual(PublishedState.Unpublished, content.PublishedState); //just double checking + } + [Test] public void Pending_Invariant_Property_Changes_Affect_Default_Language_Edited_State() { From 1bfcc9053ebbe5f35321f89194b08e75d5837a31 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 24 Jul 2019 23:59:31 +1000 Subject: [PATCH 0056/1001] Fixes issues: we never properly used SuccessUnpublishMandatoryCulture, fixed issue of unpublishing last culture with a new status --- src/Umbraco.Core/Exceptions/PanicException.cs | 28 +++ .../Services/Implement/ContentService.cs | 172 +++++++++++++----- .../Services/PublishResultType.cs | 7 +- src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../Services/ContentServiceTests.cs | 38 +++- 5 files changed, 199 insertions(+), 47 deletions(-) create mode 100644 src/Umbraco.Core/Exceptions/PanicException.cs diff --git a/src/Umbraco.Core/Exceptions/PanicException.cs b/src/Umbraco.Core/Exceptions/PanicException.cs new file mode 100644 index 0000000000..4d41cafc65 --- /dev/null +++ b/src/Umbraco.Core/Exceptions/PanicException.cs @@ -0,0 +1,28 @@ +using System; +using System.Runtime.Serialization; + +namespace Umbraco.Core.Exceptions +{ + /// + /// Internal exception that in theory should never ben thrown, it is only thrown in circumstances that should never happen + /// + [Serializable] + internal class PanicException : Exception + { + public PanicException() + { + } + + public PanicException(string message) : base(message) + { + } + + public PanicException(string message, Exception innerException) : base(message, innerException) + { + } + + protected PanicException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index e49dcf4a12..b4e5f26de5 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -886,6 +886,8 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.ContentTree); + var allLangs = _languageRepository.GetMany().ToList(); + var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs); if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving))) return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content); @@ -894,13 +896,13 @@ namespace Umbraco.Core.Services.Implement // if culture is '*', then publish them all (including variants) //this will create the correct culture impact even if culture is * or null - var impact = CultureImpact.Create(culture, _languageRepository.IsDefault(culture), content); + var impact = CultureImpact.Create(culture, IsDefaultCulture(allLangs, culture), content); // publish the culture(s) // we don't care about the response here, this response will be rechecked below but we need to set the culture info values now. content.PublishCulture(impact); - var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, userId, raiseEvents); + var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, allLangs, userId, raiseEvents); scope.Complete(); return result; } @@ -921,6 +923,8 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.ContentTree); + var allLangs = _languageRepository.GetMany().ToList(); + var evtMsgs = EventMessagesFactory.Get(); var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs); if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving))) @@ -939,14 +943,14 @@ namespace Umbraco.Core.Services.Implement if (cultures.Any(x => x == null || x == "*")) throw new InvalidOperationException("Only valid cultures are allowed to be used in this method, wildcards or nulls are not allowed"); - var impacts = cultures.Select(x => CultureImpact.Explicit(x, _languageRepository.IsDefault(x))); + var impacts = cultures.Select(x => CultureImpact.Explicit(x, IsDefaultCulture(allLangs, x))); // publish the culture(s) // we don't care about the response here, this response will be rechecked below but we need to set the culture info values now. foreach (var impact in impacts) content.PublishCulture(impact); - var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, userId, raiseEvents); + var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, allLangs, userId, raiseEvents); scope.Complete(); return result; } @@ -986,6 +990,8 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.ContentTree); + var allLangs = _languageRepository.GetMany().ToList(); + var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs); if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving))) return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content); @@ -993,22 +999,39 @@ namespace Umbraco.Core.Services.Implement // all cultures = unpublish whole if (culture == "*" || (!content.ContentType.VariesByCulture() && culture == null)) { + // It's important to understand that when the document varies by culture but the "*" is used, + // we are just unpublishing the whole document but leaving all of the culture's as-is. This is expected + // because we don't want to actually unpublish every culture and then the document, we just want everything + // to be non-routable so that when it's re-published all variants were as they were. + content.PublishedState = PublishedState.Unpublishing; } else { - // If the culture we want to unpublish was already unpublished, nothing to do. - // To check for that we need to lookup the persisted content item - var persisted = content.HasIdentity ? GetById(content.Id) : null; - - if (persisted != null && !persisted.IsCulturePublished(culture)) - return new PublishResult(PublishResultType.SuccessUnpublishAlready, evtMsgs, content); - - // unpublish the culture + // unpublish the culture, this will change the document state to Publishing! ... which is expected because this will + // essentially be re-publishing the document with the requested culture removed. + // The call to CommitDocumentChangesInternal will perform all the checks like if this is a mandatory culture or the last culture being unpublished + // and will then unpublish the document accordingly. content.UnpublishCulture(culture); + + + //TODO: Move this logic into CommitDocumentChangesInternal, we are already looking up the item there + + //// We are unpublishing a culture but there's a chance that the user might be trying + //// to unpublish a culture that is already unpublished so we need to lookup the current + //// persisted item and check since it would be quicker to do that than continue and re-publish everything. + //var persisted = content.HasIdentity ? GetById(content.Id) : null; + + //if (persisted != null && !persisted.IsCulturePublished(culture)) + //{ + // //it was already unpublished + // //TODO: We then need to check if all cultures are unpublished + // return new PublishResult(PublishResultType.SuccessUnpublishAlready, evtMsgs, content); + //} + } - var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, userId); + var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, allLangs, userId); scope.Complete(); return result; } @@ -1047,15 +1070,35 @@ namespace Umbraco.Core.Services.Implement if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving))) return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content); - var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, userId, raiseEvents); + var allLangs = _languageRepository.GetMany().ToList(); + + var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, allLangs, userId, raiseEvents); scope.Complete(); return result; } } + /// + /// Handles a lot of business logic cases for how the document should be persisted + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// Business logic cases such: as unpublishing a mandatory culture, or unpublishing the last culture, checking for pending scheduled publishing, etc... is dealt with in this method. + /// There is quite a lot of cases to take into account along with logic that needs to deal with scheduled saving/publishing, branch saving/publishing, etc... + /// + /// private PublishResult CommitDocumentChangesInternal(IScope scope, IContent content, - ContentSavingEventArgs saveEventArgs, - int userId = Constants.Security.SuperUserId, bool raiseEvents = true, bool branchOne = false, bool branchRoot = false) + ContentSavingEventArgs saveEventArgs, IReadOnlyCollection allLangs, + int userId = Constants.Security.SuperUserId, + bool raiseEvents = true, bool branchOne = false, bool branchRoot = false) { if (scope == null) throw new ArgumentNullException(nameof(scope)); if (content == null) throw new ArgumentNullException(nameof(content)); @@ -1070,8 +1113,8 @@ namespace Umbraco.Core.Services.Implement if (content.PublishedState != PublishedState.Publishing && content.PublishedState != PublishedState.Unpublishing) content.PublishedState = PublishedState.Publishing; - // state here is either Publishing or Unpublishing - // (even though, Publishing to unpublish a culture may end up unpublishing everything) + // State here is either Publishing or Unpublishing + // Publishing to unpublish a culture may end up unpublishing everything so these flags can be flipped later var publishing = content.PublishedState == PublishedState.Publishing; var unpublishing = content.PublishedState == PublishedState.Unpublishing; @@ -1090,6 +1133,9 @@ namespace Umbraco.Core.Services.Implement if (publishing) { + //TODO: Check if it's a culture being unpublished and that's why we are publishing the document. In that case + // we should check if the culture is already unpublished since it might be the user is trying to unpublish an already unpublished culture? + //determine cultures publishing/unpublishing which will be based on previous calls to content.PublishCulture and ClearPublishInfo culturesUnpublishing = content.GetCulturesUnpublishing(); culturesPublishing = variesByCulture @@ -1097,11 +1143,21 @@ namespace Umbraco.Core.Services.Implement : null; // ensure that the document can be published, and publish handling events, business rules, etc - publishResult = StrategyCanPublish(scope, content, /*checkPath:*/ (!branchOne || branchRoot), culturesPublishing, culturesUnpublishing, evtMsgs, saveEventArgs); + publishResult = StrategyCanPublish(scope, content, /*checkPath:*/ (!branchOne || branchRoot), culturesPublishing, culturesUnpublishing, evtMsgs, saveEventArgs, allLangs); if (publishResult.Success) { // note: StrategyPublish flips the PublishedState to Publishing! publishResult = StrategyPublish(content, culturesPublishing, culturesUnpublishing, evtMsgs); + + //check if a culture has been unpublished and if there are no cultures left, and then unpublish document as a whole + if (publishResult.Result == PublishResultType.SuccessUnpublishCulture && content.PublishCultureInfos.Count == 0) + { + publishing = false; + unpublishing = content.Published; // if not published yet, nothing to do + + // we may end up in a state where we won't publish nor unpublish + // keep going, though, as we want to save anyways + } } else { @@ -1186,17 +1242,34 @@ namespace Umbraco.Core.Services.Implement if (culturesUnpublishing != null) { - //If we are here, it means we tried unpublishing a culture but it was mandatory so now everything is unpublished - var langs = string.Join(", ", _languageRepository.GetMany() + // This will mean that that we unpublished a mandatory culture or we unpublished the last culture. + + var langs = string.Join(", ", allLangs .Where(x => culturesUnpublishing.InvariantContains(x.IsoCode)) .Select(x => x.CultureName)); Audit(AuditType.UnpublishVariant, userId, content.Id, $"Unpublished languages: {langs}", langs); - //log that the whole content item has been unpublished due to mandatory culture unpublished - Audit(AuditType.Unpublish, userId, content.Id, "Unpublished (mandatory language unpublished)"); - } - else - Audit(AuditType.Unpublish, userId, content.Id); + if (publishResult == null) + throw new PanicException("publishResult == null - should not happen"); + + switch(publishResult.Result) + { + case PublishResultType.FailedPublishMandatoryCultureMissing: + //occurs when a mandatory culture was unpublished (which means we tried publishing the document without a mandatory culture) + + //log that the whole content item has been unpublished due to mandatory culture unpublished + Audit(AuditType.Unpublish, userId, content.Id, "Unpublished (mandatory language unpublished)"); + return new PublishResult(PublishResultType.SuccessUnpublishMandatoryCulture, evtMsgs, content); + case PublishResultType.SuccessUnpublishCulture: + //occurs when the last culture is unpublished + + Audit(AuditType.Unpublish, userId, content.Id, "Unpublished (last language unpublished)"); + return new PublishResult(PublishResultType.SuccessUnpublishLastCulture, evtMsgs, content); + } + + } + + Audit(AuditType.Unpublish, userId, content.Id); return new PublishResult(PublishResultType.SuccessUnpublish, evtMsgs, content); } @@ -1236,7 +1309,7 @@ namespace Umbraco.Core.Services.Implement case PublishResultType.SuccessPublishCulture: if (culturesPublishing != null) { - var langs = string.Join(", ", _languageRepository.GetMany() + var langs = string.Join(", ", allLangs .Where(x => culturesPublishing.InvariantContains(x.IsoCode)) .Select(x => x.CultureName)); Audit(AuditType.PublishVariant, userId, content.Id, $"Published languages: {langs}", langs); @@ -1245,7 +1318,7 @@ namespace Umbraco.Core.Services.Implement case PublishResultType.SuccessUnpublishCulture: if (culturesUnpublishing != null) { - var langs = string.Join(", ", _languageRepository.GetMany() + var langs = string.Join(", ", allLangs .Where(x => culturesUnpublishing.InvariantContains(x.IsoCode)) .Select(x => x.CultureName)); Audit(AuditType.UnpublishVariant, userId, content.Id, $"Unpublished languages: {langs}", langs); @@ -1259,14 +1332,14 @@ namespace Umbraco.Core.Services.Implement // should not happen if (branchOne && !branchRoot) - throw new Exception("panic"); + throw new PanicException("branchOne && !branchRoot - should not happen"); //if publishing didn't happen or if it has failed, we still need to log which cultures were saved if (!branchOne && (publishResult == null || !publishResult.Success)) { if (culturesChanging != null) { - var langs = string.Join(", ", _languageRepository.GetMany() + var langs = string.Join(", ", allLangs .Where(x => culturesChanging.InvariantContains(x.IsoCode)) .Select(x => x.CultureName)); Audit(AuditType.SaveVariant, userId, content.Id, $"Saved languages: {langs}", langs); @@ -1297,6 +1370,8 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.ContentTree); + var allLangs = _languageRepository.GetMany().ToList(); + foreach (var d in _documentRepository.GetContentForRelease(date)) { PublishResult result; @@ -1325,7 +1400,7 @@ namespace Umbraco.Core.Services.Implement //publish the culture values and validate the property values, if validation fails, log the invalid properties so the develeper has an idea of what has failed Property[] invalidProperties = null; - var impact = CultureImpact.Explicit(culture, _languageRepository.IsDefault(culture)); + var impact = CultureImpact.Explicit(culture, IsDefaultCulture(allLangs, culture)); var tryPublish = d.PublishCulture(impact) && _propertyValidationService.Value.IsPropertyDataValid(d, out invalidProperties, impact); if (invalidProperties != null && invalidProperties.Length > 0) Logger.Warn("Scheduled publishing will fail for document {DocumentId} and culture {Culture} because of invalid properties {InvalidProperties}", @@ -1340,7 +1415,7 @@ namespace Umbraco.Core.Services.Implement else if (!publishing) result = new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, d); else - result = CommitDocumentChangesInternal(scope, d, saveEventArgs, d.WriterId); + result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs, d.WriterId); if (result.Success == false) @@ -1390,7 +1465,7 @@ namespace Umbraco.Core.Services.Implement d.UnpublishCulture(c); } - result = CommitDocumentChangesInternal(scope, d, saveEventArgs, d.WriterId); + result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs, d.WriterId); if (result.Success == false) Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); yield return result; @@ -1416,7 +1491,7 @@ namespace Umbraco.Core.Services.Implement } // utility 'PublishCultures' func used by SaveAndPublishBranch - private bool SaveAndPublishBranch_PublishCultures(IContent content, HashSet culturesToPublish) + private bool SaveAndPublishBranch_PublishCultures(IContent content, HashSet culturesToPublish, IReadOnlyCollection allLangs) { //TODO: This does not support being able to return invalid property details to bubble up to the UI @@ -1426,7 +1501,7 @@ namespace Umbraco.Core.Services.Implement { return culturesToPublish.All(culture => { - var impact = CultureImpact.Create(culture, _languageRepository.IsDefault(culture), content); + var impact = CultureImpact.Create(culture, IsDefaultCulture(allLangs, culture), content); return content.PublishCulture(impact) && _propertyValidationService.Value.IsPropertyDataValid(content, out _, impact); }); } @@ -1535,7 +1610,7 @@ namespace Umbraco.Core.Services.Implement internal IEnumerable SaveAndPublishBranch(IContent document, bool force, Func> shouldPublish, - Func, bool> publishCultures, + Func, IReadOnlyCollection, bool> publishCultures, int userId = Constants.Security.SuperUserId) { if (shouldPublish == null) throw new ArgumentNullException(nameof(shouldPublish)); @@ -1549,6 +1624,8 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.ContentTree); + var allLangs = _languageRepository.GetMany().ToList(); + if (!document.HasIdentity) throw new InvalidOperationException("Cannot not branch-publish a new document."); @@ -1557,7 +1634,7 @@ namespace Umbraco.Core.Services.Implement throw new InvalidOperationException("Cannot mix PublishCulture and SaveAndPublishBranch."); // deal with the branch root - if it fails, abort - var result = SaveAndPublishBranchItem(scope, document, shouldPublish, publishCultures, true, publishedDocuments, evtMsgs, userId); + var result = SaveAndPublishBranchItem(scope, document, shouldPublish, publishCultures, true, publishedDocuments, evtMsgs, userId, allLangs); if (result != null) { results.Add(result); @@ -1588,7 +1665,7 @@ namespace Umbraco.Core.Services.Implement } // no need to check path here, parent has to be published here - result = SaveAndPublishBranchItem(scope, d, shouldPublish, publishCultures, false, publishedDocuments, evtMsgs, userId); + result = SaveAndPublishBranchItem(scope, d, shouldPublish, publishCultures, false, publishedDocuments, evtMsgs, userId, allLangs); if (result != null) { results.Add(result); @@ -1620,10 +1697,10 @@ namespace Umbraco.Core.Services.Implement // publishValues: a function publishing values (using the appropriate PublishCulture calls) private PublishResult SaveAndPublishBranchItem(IScope scope, IContent document, Func> shouldPublish, - Func, bool> publishCultures, + Func, IReadOnlyCollection, bool> publishCultures, bool isRoot, ICollection publishedDocuments, - EventMessages evtMsgs, int userId) + EventMessages evtMsgs, int userId, IReadOnlyCollection allLangs) { var culturesToPublish = shouldPublish(document); if (culturesToPublish == null) // null = do not include @@ -1636,13 +1713,13 @@ namespace Umbraco.Core.Services.Implement return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, document); // publish & check if values are valid - if (!publishCultures(document, culturesToPublish)) + if (!publishCultures(document, culturesToPublish, allLangs)) { //TODO: Based on this callback behavior there is no way to know which properties may have been invalid if this failed, see other results of FailedPublishContentInvalid return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, document); } - var result = CommitDocumentChangesInternal(scope, document, saveEventArgs, userId, branchOne: true, branchRoot: isRoot); + var result = CommitDocumentChangesInternal(scope, document, saveEventArgs, allLangs, userId, branchOne: true, branchRoot: isRoot); if (result.Success) publishedDocuments.Add(document); return result; @@ -2343,6 +2420,9 @@ namespace Umbraco.Core.Services.Implement _auditRepository.Save(new AuditItem(objectId, type, userId, ObjectTypes.GetName(UmbracoObjectTypes.Document), message, parameters)); } + private bool IsDefaultCulture(IReadOnlyCollection langs, string culture) => langs.Any(x => x.IsDefault && x.IsoCode.InvariantEquals(culture)); + private bool IsMandatoryCulture(IReadOnlyCollection langs, string culture) => langs.Any(x => x.IsMandatory && x.IsoCode.InvariantEquals(culture)); + #endregion #region Event Handlers @@ -2497,7 +2577,9 @@ namespace Umbraco.Core.Services.Implement /// /// /// - private PublishResult StrategyCanPublish(IScope scope, IContent content, bool checkPath, IReadOnlyList culturesPublishing, IReadOnlyCollection culturesUnpublishing, EventMessages evtMsgs, ContentSavingEventArgs savingEventArgs) + private PublishResult StrategyCanPublish(IScope scope, IContent content, bool checkPath, IReadOnlyList culturesPublishing, + IReadOnlyCollection culturesUnpublishing, EventMessages evtMsgs, ContentSavingEventArgs savingEventArgs, + IReadOnlyCollection allLangs) { // raise Publishing event if (scope.Events.DispatchCancelable(Publishing, this, savingEventArgs.ToContentPublishingEventArgs())) @@ -2510,7 +2592,7 @@ namespace Umbraco.Core.Services.Implement var impactsToPublish = culturesPublishing == null ? new[] {CultureImpact.Invariant} //if it's null it's invariant - : culturesPublishing.Select(x => CultureImpact.Explicit(x, _languageRepository.IsDefault(x))).ToArray(); + : culturesPublishing.Select(x => CultureImpact.Explicit(x, allLangs.Any(lang => lang.IsoCode.InvariantEquals(x) && lang.IsMandatory))).ToArray(); // publish the culture(s) if (!impactsToPublish.All(content.PublishCulture)) @@ -2535,7 +2617,7 @@ namespace Umbraco.Core.Services.Implement return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content); // missing mandatory culture = cannot be published - var mandatoryCultures = _languageRepository.GetMany().Where(x => x.IsMandatory).Select(x => x.IsoCode); + var mandatoryCultures = allLangs.Where(x => x.IsMandatory).Select(x => x.IsoCode); var mandatoryMissing = mandatoryCultures.Any(x => !content.PublishedCultures.Contains(x, StringComparer.OrdinalIgnoreCase)); if (mandatoryMissing) return new PublishResult(PublishResultType.FailedPublishMandatoryCultureMissing, evtMsgs, content); diff --git a/src/Umbraco.Core/Services/PublishResultType.cs b/src/Umbraco.Core/Services/PublishResultType.cs index 1a2b52f9c9..66514a26fb 100644 --- a/src/Umbraco.Core/Services/PublishResultType.cs +++ b/src/Umbraco.Core/Services/PublishResultType.cs @@ -49,6 +49,11 @@ /// SuccessUnpublishMandatoryCulture = 6, + /// + /// The specified document culture was unpublished, and was the last published culture in the document, therefore the document itself was unpublished. + /// + SuccessUnpublishLastCulture = 8, + #endregion #region Success - Mixed @@ -115,7 +120,7 @@ /// /// The document could not be published because it has no publishing flags or values. /// - FailedPublishNothingToPublish = FailedPublish | 9, // TODO: in ContentService.StrategyCanPublish - weird + FailedPublishNothingToPublish = FailedPublish | 9, // TODO: in ContentService.StrategyCanPublish - weird - do we have a test for that case? /// /// The document could not be published because some mandatory cultures are missing. diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index ca9b7d4034..74af58b94f 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -204,6 +204,7 @@ + diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index a719e04b2a..07f4cdb661 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -806,7 +806,7 @@ namespace Umbraco.Tests.Services unpublished = ServiceContext.ContentService.Unpublish(content, langUk.IsoCode); Assert.IsTrue(unpublished.Success); - Assert.AreEqual(PublishResultType.SuccessUnpublishCulture, unpublished.Result); + Assert.AreEqual(PublishResultType.SuccessUnpublishLastCulture, unpublished.Result); Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); Assert.IsFalse(content.IsCulturePublished(langUk.IsoCode)); Assert.AreEqual(PublishedState.Unpublished, content.PublishedState); //the last culture was unpublished so the document should also reflect this @@ -816,6 +816,42 @@ namespace Umbraco.Tests.Services Assert.AreEqual(PublishedState.Unpublished, content.PublishedState); //just double checking } + [Test] + public void Unpublishing_Mandatory_Language_Unpublishes_Document() + { + // Arrange + + var langUk = new Language("en-GB") { IsDefault = true, IsMandatory = true }; + var langFr = new Language("fr-FR"); + + ServiceContext.LocalizationService.Save(langFr); + ServiceContext.LocalizationService.Save(langUk); + + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + IContent content = new Content("content", Constants.System.Root, contentType); + content.SetCultureName("content-fr", langFr.IsoCode); + content.SetCultureName("content-en", langUk.IsoCode); + + var published = ServiceContext.ContentService.SaveAndPublish(content, new[] { langFr.IsoCode, langUk.IsoCode }); + Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); + Assert.IsTrue(published.Success); + Assert.AreEqual(PublishedState.Published, content.PublishedState); + + //re-get + content = ServiceContext.ContentService.GetById(content.Id); + + var unpublished = ServiceContext.ContentService.Unpublish(content, langUk.IsoCode); //unpublish mandatory lang + Assert.IsTrue(unpublished.Success); + Assert.AreEqual(PublishResultType.SuccessUnpublishMandatoryCulture, unpublished.Result); + Assert.IsFalse(content.IsCulturePublished(langUk.IsoCode)); + Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); //remains published + Assert.AreEqual(PublishedState.Unpublished, content.PublishedState); + } + [Test] public void Pending_Invariant_Property_Changes_Affect_Default_Language_Edited_State() { From 5a9ca8d09f30f396cdeed8fdebd961d93469b4ef Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 29 Jul 2019 11:37:11 +1000 Subject: [PATCH 0057/1001] wip added more tests and notes --- .../Models/ContentRepositoryExtensions.cs | 56 ++++++++++++++----- .../Services/Implement/ContentService.cs | 12 +++- .../Services/ContentServiceTests.cs | 42 ++++++++++++++ 3 files changed, 95 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs b/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs index bf7228ca47..f9efc60142 100644 --- a/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs +++ b/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs @@ -222,7 +222,13 @@ namespace Umbraco.Core.Models return true; } - public static void UnpublishCulture(this IContent content, string culture = "*") + /// + /// Returns false if the culture is already unpublished + /// + /// + /// + /// + public static bool UnpublishCulture(this IContent content, string culture = "*") { culture = culture.NullOrWhiteSpaceAsNull(); @@ -230,16 +236,31 @@ namespace Umbraco.Core.Models if (!content.ContentType.SupportsPropertyVariation(culture, "*", true)) throw new NotSupportedException($"Culture \"{culture}\" is not supported by content type \"{content.ContentType.Alias}\" with variation \"{content.ContentType.Variations}\"."); - if (culture == "*") // all cultures + + var keepProcessing = true; + + if (culture == "*") + { + // all cultures content.ClearPublishInfos(); - else // one single culture - content.ClearPublishInfo(culture); + } + else + { + // one single culture + keepProcessing = content.ClearPublishInfo(culture); + } + - // property.PublishValues only publishes what is valid, variation-wise - foreach (var property in content.Properties) - property.UnpublishValues(culture); + if (keepProcessing) + { + // property.PublishValues only publishes what is valid, variation-wise + foreach (var property in content.Properties) + property.UnpublishValues(culture); - content.PublishedState = PublishedState.Publishing; + content.PublishedState = PublishedState.Publishing; + } + + return keepProcessing; } public static void ClearPublishInfos(this IContent content) @@ -247,15 +268,24 @@ namespace Umbraco.Core.Models content.PublishCultureInfos = null; } - public static void ClearPublishInfo(this IContent content, string culture) + /// + /// Returns false if the culture is already unpublished + /// + /// + /// + /// + public static bool ClearPublishInfo(this IContent content, string culture) { if (culture.IsNullOrWhiteSpace()) throw new ArgumentNullOrEmptyException(nameof(culture)); - content.PublishCultureInfos.Remove(culture); - - // set the culture to be dirty - it's been modified - content.TouchCulture(culture); + var removed = content.PublishCultureInfos.Remove(culture); + if (removed) + { + // set the culture to be dirty - it's been modified + content.TouchCulture(culture); + } + return removed; } /// diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index b4e5f26de5..91c26701b3 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -1012,7 +1012,9 @@ namespace Umbraco.Core.Services.Implement // essentially be re-publishing the document with the requested culture removed. // The call to CommitDocumentChangesInternal will perform all the checks like if this is a mandatory culture or the last culture being unpublished // and will then unpublish the document accordingly. - content.UnpublishCulture(culture); + var removed = content.UnpublishCulture(culture); + + //TODO: if !removed then there is really nothing to process and we should exit here with SuccessUnpublishAlready. //TODO: Move this logic into CommitDocumentChangesInternal, we are already looking up the item there @@ -2613,8 +2615,14 @@ namespace Umbraco.Core.Services.Implement if (culturesPublishing == null) throw new InvalidOperationException("Internal error, variesByCulture but culturesPublishing is null."); - if (content.Published && culturesPublishing.Count == 0 && culturesUnpublishing.Count == 0) // no published cultures = cannot be published + if (content.Published && culturesPublishing.Count == 0 && culturesUnpublishing.Count == 0) + { + // no published cultures = cannot be published + // This will occur if for example, a culture that is already unpublished is sent to be unpublished again, or vice versa, in that case + // there will be nothing to publish/unpublish. return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content); + } + // missing mandatory culture = cannot be published var mandatoryCultures = allLangs.Where(x => x.IsMandatory).Select(x => x.IsoCode); diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 07f4cdb661..a903bcedb0 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -852,6 +852,48 @@ namespace Umbraco.Tests.Services Assert.AreEqual(PublishedState.Unpublished, content.PublishedState); } + [Test] + public void Unpublishing_Already_Unpublished_Culture() + { + // Arrange + + var langUk = new Language("en-GB") { IsDefault = true }; + var langFr = new Language("fr-FR"); + + ServiceContext.LocalizationService.Save(langFr); + ServiceContext.LocalizationService.Save(langUk); + + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + IContent content = new Content("content", Constants.System.Root, contentType); + content.SetCultureName("content-fr", langFr.IsoCode); + content.SetCultureName("content-en", langUk.IsoCode); + + var published = ServiceContext.ContentService.SaveAndPublish(content, new[] { langFr.IsoCode, langUk.IsoCode }); + Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); + Assert.IsTrue(published.Success); + Assert.AreEqual(PublishedState.Published, content.PublishedState); + + //re-get + content = ServiceContext.ContentService.GetById(content.Id); + + var unpublished = ServiceContext.ContentService.Unpublish(content, langUk.IsoCode); + Assert.IsTrue(unpublished.Success); + Assert.AreEqual(PublishResultType.SuccessUnpublishCulture, unpublished.Result); + Assert.IsFalse(content.IsCulturePublished(langUk.IsoCode)); + + content = ServiceContext.ContentService.GetById(content.Id); + + unpublished = ServiceContext.ContentService.Unpublish(content, langUk.IsoCode); //unpublish again + Assert.IsTrue(unpublished.Success); + Assert.AreEqual(PublishResultType.SuccessUnpublishAlready, unpublished.Result); + Assert.IsFalse(content.IsCulturePublished(langUk.IsoCode)); + + } + [Test] public void Pending_Invariant_Property_Changes_Affect_Default_Language_Edited_State() { From 40a55cb9dfe9978d7dfbab5067f932e2c400803f Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 29 Jul 2019 15:28:50 +1000 Subject: [PATCH 0058/1001] Fixes issue/tests --- .../Services/Implement/ContentService.cs | 35 ++++++++----------- .../Services/PublishResultType.cs | 4 +-- .../Services/ContentServiceTests.cs | 8 +++++ 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 91c26701b3..a8b40b1bb1 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -937,6 +937,7 @@ namespace Umbraco.Core.Services.Implement //no cultures specified and doesn't vary, so publish it, else nothing to publish return !varies ? SaveAndPublish(content, userId: userId, raiseEvents: raiseEvents) + //TODO: Though we may not have cultures to publish, shouldn't we continue to Save in this case?? : new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content); } @@ -1005,39 +1006,33 @@ namespace Umbraco.Core.Services.Implement // to be non-routable so that when it's re-published all variants were as they were. content.PublishedState = PublishedState.Unpublishing; + var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, allLangs, userId); + scope.Complete(); + return result; } else { - // unpublish the culture, this will change the document state to Publishing! ... which is expected because this will + // Unpublish the culture, this will change the document state to Publishing! ... which is expected because this will // essentially be re-publishing the document with the requested culture removed. // The call to CommitDocumentChangesInternal will perform all the checks like if this is a mandatory culture or the last culture being unpublished // and will then unpublish the document accordingly. + // If the result of this is false it means there was no culture to unpublish (i.e. it was already unpublished or it did not exist) var removed = content.UnpublishCulture(culture); - //TODO: if !removed then there is really nothing to process and we should exit here with SuccessUnpublishAlready. + //save and publish any changes + var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, allLangs, userId); + scope.Complete(); - //TODO: Move this logic into CommitDocumentChangesInternal, we are already looking up the item there - - //// We are unpublishing a culture but there's a chance that the user might be trying - //// to unpublish a culture that is already unpublished so we need to lookup the current - //// persisted item and check since it would be quicker to do that than continue and re-publish everything. - //var persisted = content.HasIdentity ? GetById(content.Id) : null; - - //if (persisted != null && !persisted.IsCulturePublished(culture)) - //{ - // //it was already unpublished - // //TODO: We then need to check if all cultures are unpublished - // return new PublishResult(PublishResultType.SuccessUnpublishAlready, evtMsgs, content); - //} + // In one case the result will be PublishStatusType.FailedPublishNothingToPublish which means that no cultures + // were specified to be published which will be the case when removed is false. In that case + // we want to swap the result type to PublishResultType.SuccessUnpublishAlready (that was the expectation before). + if (result.Result == PublishResultType.FailedPublishNothingToPublish && !removed) + return new PublishResult(PublishResultType.SuccessUnpublishAlready, evtMsgs, content); + return result; } - - var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, allLangs, userId); - scope.Complete(); - return result; } - } /// diff --git a/src/Umbraco.Core/Services/PublishResultType.cs b/src/Umbraco.Core/Services/PublishResultType.cs index 66514a26fb..66c1e38267 100644 --- a/src/Umbraco.Core/Services/PublishResultType.cs +++ b/src/Umbraco.Core/Services/PublishResultType.cs @@ -118,9 +118,9 @@ FailedPublishContentInvalid = FailedPublish | 8, /// - /// The document could not be published because it has no publishing flags or values. + /// The document could not be published because it has no publishing flags or values or if its a variant document, no cultures were specified to be published. /// - FailedPublishNothingToPublish = FailedPublish | 9, // TODO: in ContentService.StrategyCanPublish - weird - do we have a test for that case? + FailedPublishNothingToPublish = FailedPublish | 9, /// /// The document could not be published because some mandatory cultures are missing. diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index a903bcedb0..44b2eec66d 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -887,11 +887,19 @@ namespace Umbraco.Tests.Services content = ServiceContext.ContentService.GetById(content.Id); + //Change some data since Unpublish should always Save + content.SetCultureName("content-en-updated", langUk.IsoCode); + //var saveResult = ServiceContext.ContentService.Save(content); + //content = ServiceContext.ContentService.GetById(content.Id); + unpublished = ServiceContext.ContentService.Unpublish(content, langUk.IsoCode); //unpublish again Assert.IsTrue(unpublished.Success); Assert.AreEqual(PublishResultType.SuccessUnpublishAlready, unpublished.Result); Assert.IsFalse(content.IsCulturePublished(langUk.IsoCode)); + content = ServiceContext.ContentService.GetById(content.Id); + //ensure that even though the culture was already unpublished that the data was still persisted + Assert.AreEqual("content-en-updated", content.GetCultureName(langUk.IsoCode)); } [Test] From 415916c527a93944196ed93136a950b607c9d42b Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 29 Jul 2019 16:09:45 +1000 Subject: [PATCH 0059/1001] Fixes an issue with the content service + test --- .../Services/Implement/ContentService.cs | 7 +-- .../Services/ContentServiceTests.cs | 43 ++++++++++++++++++- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index a8b40b1bb1..b1aa92c4c8 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -932,13 +932,10 @@ namespace Umbraco.Core.Services.Implement var varies = content.ContentType.VariesByCulture(); - if (cultures.Length == 0) + if (cultures.Length == 0 && !varies) { //no cultures specified and doesn't vary, so publish it, else nothing to publish - return !varies - ? SaveAndPublish(content, userId: userId, raiseEvents: raiseEvents) - //TODO: Though we may not have cultures to publish, shouldn't we continue to Save in this case?? - : new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content); + return SaveAndPublish(content, userId: userId, raiseEvents: raiseEvents); } if (cultures.Any(x => x == null || x == "*")) diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 44b2eec66d..b8ed0e1f41 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -889,8 +889,6 @@ namespace Umbraco.Tests.Services //Change some data since Unpublish should always Save content.SetCultureName("content-en-updated", langUk.IsoCode); - //var saveResult = ServiceContext.ContentService.Save(content); - //content = ServiceContext.ContentService.GetById(content.Id); unpublished = ServiceContext.ContentService.Unpublish(content, langUk.IsoCode); //unpublish again Assert.IsTrue(unpublished.Success); @@ -902,6 +900,47 @@ namespace Umbraco.Tests.Services Assert.AreEqual("content-en-updated", content.GetCultureName(langUk.IsoCode)); } + [Test] + public void Publishing_No_Cultures_Still_Saves() + { + // Arrange + + var langUk = new Language("en-GB") { IsDefault = true }; + var langFr = new Language("fr-FR"); + + ServiceContext.LocalizationService.Save(langFr); + ServiceContext.LocalizationService.Save(langUk); + + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + IContent content = new Content("content", Constants.System.Root, contentType); + content.SetCultureName("content-fr", langFr.IsoCode); + content.SetCultureName("content-en", langUk.IsoCode); + + var published = ServiceContext.ContentService.SaveAndPublish(content, new[] { langFr.IsoCode, langUk.IsoCode }); + Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); + Assert.IsTrue(published.Success); + Assert.AreEqual(PublishedState.Published, content.PublishedState); + + //re-get + content = ServiceContext.ContentService.GetById(content.Id); + + //Change some data since SaveAndPublish should always Save + content.SetCultureName("content-en-updated", langUk.IsoCode); + + var saved = ServiceContext.ContentService.SaveAndPublish(content, new string [] { }); //save without cultures + Assert.AreEqual(PublishResultType.FailedPublishNothingToPublish, saved.Result); + + //re-get + content = ServiceContext.ContentService.GetById(content.Id); + //ensure that even though nothing was published that the data was still persisted + Assert.AreEqual("content-en-updated", content.GetCultureName(langUk.IsoCode)); + } + + [Test] public void Pending_Invariant_Property_Changes_Affect_Default_Language_Edited_State() { From a2ba0f6a4c9a91c0a183b6f60d2be78ad989fd93 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 29 Jul 2019 17:06:09 +1000 Subject: [PATCH 0060/1001] more tests - failing that need fixing --- .../Services/ContentServiceTests.cs | 151 +++++++++--------- 1 file changed, 75 insertions(+), 76 deletions(-) diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index b8ed0e1f41..7954f0f024 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -717,21 +717,8 @@ namespace Umbraco.Tests.Services [Test] public void Can_Unpublish_Content_Variation() { - // Arrange + var content = CreateEnglishAndFrenchDocument(out var langUk, out var langFr, out var contentType); - var langUk = new Language("en-GB") { IsDefault = true }; - var langFr = new Language("fr-FR"); - - ServiceContext.LocalizationService.Save(langFr); - ServiceContext.LocalizationService.Save(langUk); - - var contentType = MockedContentTypes.CreateBasicContentType(); - contentType.Variations = ContentVariation.Culture; - ServiceContext.ContentTypeService.Save(contentType); - - IContent content = new Content("content", Constants.System.Root, contentType); - content.SetCultureName("content-fr", langFr.IsoCode); - content.SetCultureName("content-en", langUk.IsoCode); content.PublishCulture(CultureImpact.Explicit(langFr.IsoCode, langFr.IsDefault)); content.PublishCulture(CultureImpact.Explicit(langUk.IsoCode, langUk.IsDefault)); Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); @@ -761,25 +748,52 @@ namespace Umbraco.Tests.Services } + [Test] + public void Can_Publish_Culture_After_Last_Culture_Unpublished() + { + var content = CreateEnglishAndFrenchDocument(out var langUk, out var langFr, out var contentType); + + var published = ServiceContext.ContentService.SaveAndPublish(content, new[] { langFr.IsoCode, langUk.IsoCode }); + Assert.AreEqual(PublishedState.Published, content.PublishedState); + + //re-get + content = ServiceContext.ContentService.GetById(content.Id); + + var unpublished = ServiceContext.ContentService.Unpublish(content, langUk.IsoCode); //first culture + Assert.IsTrue(unpublished.Success); + Assert.AreEqual(PublishResultType.SuccessUnpublishCulture, unpublished.Result); + Assert.IsFalse(content.IsCulturePublished(langUk.IsoCode)); + Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); + + content = ServiceContext.ContentService.GetById(content.Id); + + unpublished = ServiceContext.ContentService.Unpublish(content, langFr.IsoCode); //last culture + Assert.IsTrue(unpublished.Success); + Assert.AreEqual(PublishResultType.SuccessUnpublishLastCulture, unpublished.Result); + Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsFalse(content.IsCulturePublished(langUk.IsoCode)); + + content = ServiceContext.ContentService.GetById(content.Id); + + published = ServiceContext.ContentService.SaveAndPublish(content, langUk.IsoCode); + Assert.AreEqual(PublishedState.Published, content.PublishedState); + Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); + Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); + + content = ServiceContext.ContentService.GetById(content.Id); //reget + Assert.AreEqual(PublishedState.Published, content.PublishedState); + Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); + Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); + + } + + + [Test] public void Unpublish_All_Cultures_Has_Unpublished_State() { - // Arrange + var content = CreateEnglishAndFrenchDocument(out var langUk, out var langFr, out var contentType); - var langUk = new Language("en-GB") { IsDefault = true }; - var langFr = new Language("fr-FR"); - - ServiceContext.LocalizationService.Save(langFr); - ServiceContext.LocalizationService.Save(langUk); - - var contentType = MockedContentTypes.CreateBasicContentType(); - contentType.Variations = ContentVariation.Culture; - ServiceContext.ContentTypeService.Save(contentType); - - IContent content = new Content("content", Constants.System.Root, contentType); - content.SetCultureName("content-fr", langFr.IsoCode); - content.SetCultureName("content-en", langUk.IsoCode); - var published = ServiceContext.ContentService.SaveAndPublish(content, new[] { langFr.IsoCode, langUk.IsoCode }); Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); @@ -792,7 +806,7 @@ namespace Umbraco.Tests.Services Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); Assert.AreEqual(PublishedState.Published, content.PublishedState); - var unpublished = ServiceContext.ContentService.Unpublish(content, langFr.IsoCode); + var unpublished = ServiceContext.ContentService.Unpublish(content, langFr.IsoCode); //first culture Assert.IsTrue(unpublished.Success); Assert.AreEqual(PublishResultType.SuccessUnpublishCulture, unpublished.Result); Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); @@ -804,7 +818,7 @@ namespace Umbraco.Tests.Services Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); Assert.IsTrue(content.IsCulturePublished(langUk.IsoCode)); - unpublished = ServiceContext.ContentService.Unpublish(content, langUk.IsoCode); + unpublished = ServiceContext.ContentService.Unpublish(content, langUk.IsoCode); //last culture Assert.IsTrue(unpublished.Success); Assert.AreEqual(PublishResultType.SuccessUnpublishLastCulture, unpublished.Result); Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); @@ -814,13 +828,13 @@ namespace Umbraco.Tests.Services //re-get content = ServiceContext.ContentService.GetById(content.Id); Assert.AreEqual(PublishedState.Unpublished, content.PublishedState); //just double checking + Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); + Assert.IsFalse(content.IsCulturePublished(langUk.IsoCode)); } [Test] public void Unpublishing_Mandatory_Language_Unpublishes_Document() { - // Arrange - var langUk = new Language("en-GB") { IsDefault = true, IsMandatory = true }; var langFr = new Language("fr-FR"); @@ -855,21 +869,7 @@ namespace Umbraco.Tests.Services [Test] public void Unpublishing_Already_Unpublished_Culture() { - // Arrange - - var langUk = new Language("en-GB") { IsDefault = true }; - var langFr = new Language("fr-FR"); - - ServiceContext.LocalizationService.Save(langFr); - ServiceContext.LocalizationService.Save(langUk); - - var contentType = MockedContentTypes.CreateBasicContentType(); - contentType.Variations = ContentVariation.Culture; - ServiceContext.ContentTypeService.Save(contentType); - - IContent content = new Content("content", Constants.System.Root, contentType); - content.SetCultureName("content-fr", langFr.IsoCode); - content.SetCultureName("content-en", langUk.IsoCode); + var content = CreateEnglishAndFrenchDocument(out var langUk, out var langFr, out var contentType); var published = ServiceContext.ContentService.SaveAndPublish(content, new[] { langFr.IsoCode, langUk.IsoCode }); Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); @@ -903,21 +903,7 @@ namespace Umbraco.Tests.Services [Test] public void Publishing_No_Cultures_Still_Saves() { - // Arrange - - var langUk = new Language("en-GB") { IsDefault = true }; - var langFr = new Language("fr-FR"); - - ServiceContext.LocalizationService.Save(langFr); - ServiceContext.LocalizationService.Save(langUk); - - var contentType = MockedContentTypes.CreateBasicContentType(); - contentType.Variations = ContentVariation.Culture; - ServiceContext.ContentTypeService.Save(contentType); - - IContent content = new Content("content", Constants.System.Root, contentType); - content.SetCultureName("content-fr", langFr.IsoCode); - content.SetCultureName("content-en", langUk.IsoCode); + var content = CreateEnglishAndFrenchDocument(out var langUk, out var langFr, out var contentType); var published = ServiceContext.ContentService.SaveAndPublish(content, new[] { langFr.IsoCode, langUk.IsoCode }); Assert.IsTrue(content.IsCulturePublished(langFr.IsoCode)); @@ -991,17 +977,7 @@ namespace Umbraco.Tests.Services [Test] public void Can_Publish_Content_Variation_And_Detect_Changed_Cultures() { - // Arrange - - var langGB = new Language("en-GB") { IsDefault = true }; - var langFr = new Language("fr-FR"); - - ServiceContext.LocalizationService.Save(langFr); - ServiceContext.LocalizationService.Save(langGB); - - var contentType = MockedContentTypes.CreateBasicContentType(); - contentType.Variations = ContentVariation.Culture; - ServiceContext.ContentTypeService.Save(contentType); + CreateEnglishAndFrenchDocumentType(out var langUk, out var langFr, out var contentType); IContent content = new Content("content", Constants.System.Root, contentType); content.SetCultureName("content-fr", langFr.IsoCode); @@ -1012,8 +988,8 @@ namespace Umbraco.Tests.Services //re-get content = ServiceContext.ContentService.GetById(content.Id); - content.SetCultureName("content-en", langGB.IsoCode); - published = ServiceContext.ContentService.SaveAndPublish(content, langGB.IsoCode); + content.SetCultureName("content-en", langUk.IsoCode); + published = ServiceContext.ContentService.SaveAndPublish(content, langUk.IsoCode); //audit log will only show that english was published lastLog = ServiceContext.AuditService.GetLogs(content.Id).Last(); Assert.AreEqual($"Published languages: English (United Kingdom)", lastLog.Comment); @@ -3193,5 +3169,28 @@ namespace Umbraco.Tests.Services var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); return repository; } + + private void CreateEnglishAndFrenchDocumentType(out Language langUk, out Language langFr, out ContentType contentType) + { + langUk = new Language("en-GB") { IsDefault = true }; + langFr = new Language("fr-FR"); + ServiceContext.LocalizationService.Save(langFr); + ServiceContext.LocalizationService.Save(langUk); + + contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + } + + private IContent CreateEnglishAndFrenchDocument(out Language langUk, out Language langFr, out ContentType contentType) + { + CreateEnglishAndFrenchDocumentType(out langUk, out langFr, out contentType); + + IContent content = new Content("content", Constants.System.Root, contentType); + content.SetCultureName("content-fr", langFr.IsoCode); + content.SetCultureName("content-en", langUk.IsoCode); + + return content; + } } } From 45227357fd38b1498f4d53dbabb88de8520a6d6b Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 29 Jul 2019 17:41:37 +1000 Subject: [PATCH 0061/1001] adds notes on why this is failing --- src/Umbraco.Tests/Services/ContentServiceTests.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 7954f0f024..01eb9a0e27 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -829,6 +829,13 @@ namespace Umbraco.Tests.Services content = ServiceContext.ContentService.GetById(content.Id); Assert.AreEqual(PublishedState.Unpublished, content.PublishedState); //just double checking Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); + //TODO: This fails!? Why? Because in the ContentService.CommitDocumentChangesInternal method when we detect it's the last lang being unpublished, + // we swap the unpublishing flag to true which means we end up setting the property content.PublishedState = PublishedState.Unpublishing, because of this + // inside of the DocumentRepository we treat many things differently including not publishing the removed culture. + // So how do we fix that? Well when we unpublish the last culture we want to both save the unpublishing of the culture AND unpublish the document, the easy + // way to do this will be to perform a double save operation, though that's not the prettiest way to do it. Ideally we take care of this all in one process + // in the DocumentRepository but that might require another transitory status like PublishedState.UnpublishingLastCulture ... though that's not pretty either. + // It might be possible in some other magicaly way, i'm just leaving these notes here mostly for myself :) Assert.IsFalse(content.IsCulturePublished(langUk.IsoCode)); } From b5b4ee79b406ade86d78a6ea0578683ee426c260 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 30 Jul 2019 16:00:42 +1000 Subject: [PATCH 0062/1001] Adds notes, fixes logic with a double save --- .../TagsPropertyEditorAttribute.cs | 1 + .../Services/Implement/ContentService.cs | 34 +++++++++++++------ .../Services/ContentServiceTests.cs | 4 +++ 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs b/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs index 2439c7c02e..6549f1a233 100644 --- a/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs +++ b/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs @@ -55,6 +55,7 @@ namespace Umbraco.Core.PropertyEditors /// /// Gets the type of the dynamic configuration provider. /// + //TODO: This is not used and should be implemented in a nicer way, see https://github.com/umbraco/Umbraco-CMS/issues/6017#issuecomment-516253562 public Type TagsConfigurationProviderType { get; } } } diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index b1aa92c4c8..7b003d0a26 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -1125,6 +1125,18 @@ namespace Umbraco.Core.Services.Implement var changeType = isNew ? TreeChangeTypes.RefreshNode : TreeChangeTypes.RefreshBranch; var previouslyPublished = content.HasIdentity && content.Published; + //inline method to persist the document with the documentRepository since this logic could be called a couple times below + void SaveDocument(IContent c) + { + // save, always + if (c.HasIdentity == false) + c.CreatorId = userId; + c.WriterId = userId; + + // saving does NOT change the published version, unless PublishedState is Publishing or Unpublishing + _documentRepository.Save(c); + } + if (publishing) { //TODO: Check if it's a culture being unpublished and that's why we are publishing the document. In that case @@ -1146,11 +1158,15 @@ namespace Umbraco.Core.Services.Implement //check if a culture has been unpublished and if there are no cultures left, and then unpublish document as a whole if (publishResult.Result == PublishResultType.SuccessUnpublishCulture && content.PublishCultureInfos.Count == 0) { - publishing = false; - unpublishing = content.Published; // if not published yet, nothing to do + // This is a special case! We are unpublishing the last culture and to persist that we need to re-publish without any cultures + // so the state needs to remain Publishing to do that. However, we then also need to unpublish the document and to do that + // the state needs to be Unpublishing and it cannot be both. This state is used within the documentRepository to know how to + // persist certain things. So before proceeding below, we need to save the Publishing state to publish no cultures, then we can + // mark the document for Unpublishing. + SaveDocument(content); - // we may end up in a state where we won't publish nor unpublish - // keep going, though, as we want to save anyways + //set the flag to unpublish and continue + unpublishing = content.Published; // if not published yet, nothing to do } } else @@ -1212,13 +1228,8 @@ namespace Umbraco.Core.Services.Implement } } - // save, always - if (content.HasIdentity == false) - content.CreatorId = userId; - content.WriterId = userId; - - // saving does NOT change the published version, unless PublishedState is Publishing or Unpublishing - _documentRepository.Save(content); + //Persist the document + SaveDocument(content); // raise the Saved event, always if (raiseEvents) @@ -2758,6 +2769,7 @@ namespace Umbraco.Core.Services.Implement { var attempt = new PublishResult(PublishResultType.SuccessUnpublish, evtMsgs, content); + //TODO: What is this check?? we just created this attempt and of course it is Success?! if (attempt.Success == false) return attempt; diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 01eb9a0e27..10bb63211b 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -836,6 +836,10 @@ namespace Umbraco.Tests.Services // way to do this will be to perform a double save operation, though that's not the prettiest way to do it. Ideally we take care of this all in one process // in the DocumentRepository but that might require another transitory status like PublishedState.UnpublishingLastCulture ... though that's not pretty either. // It might be possible in some other magicaly way, i'm just leaving these notes here mostly for myself :) + // UPDATE - ok, i 'think' instead of doing a check inside of CommitDocumentChangesInternal for the last culture being unpublished, we could actually do this check + // directly inside of the DocumentRepository + // UPDATE 2 - ok that won't work because there is some business logic that needs to occur in CommitDocumentChangesInternal when it needs to be unpublished, this will + // be tricky without doing a double save, hrm. Assert.IsFalse(content.IsCulturePublished(langUk.IsoCode)); } From 27bc309c557f9b0adeca9f587feed9a199f80e36 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 30 Jul 2019 18:49:48 +1000 Subject: [PATCH 0063/1001] Removes unused code --- .../Implement/ContentRepositoryBase.cs | 35 ++++++++++--------- .../Services/Implement/ContentService.cs | 3 -- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index aeb4c3774f..60beaca018 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -512,20 +512,21 @@ namespace Umbraco.Core.Persistence.Repositories.Implement foreach (var a in allPropertyDataDtos) a.PropertyTypeDto = indexedPropertyTypeDtos[a.PropertyTypeId]; - // prefetch configuration for tag properties - var tagEditors = new Dictionary(); - foreach (var propertyTypeDto in indexedPropertyTypeDtos.Values) - { - var editorAlias = propertyTypeDto.DataTypeDto.EditorAlias; - var editorAttribute = PropertyEditors[editorAlias].GetTagAttribute(); - if (editorAttribute == null) continue; - var tagConfigurationSource = propertyTypeDto.DataTypeDto.Configuration; - var tagConfiguration = string.IsNullOrWhiteSpace(tagConfigurationSource) - ? new TagConfiguration() - : JsonConvert.DeserializeObject(tagConfigurationSource); - if (tagConfiguration.Delimiter == default) tagConfiguration.Delimiter = editorAttribute.Delimiter; - tagEditors[editorAlias] = tagConfiguration; - } + //TODO: This logic was here but then we never used the data being passed to GetPropertyCollections, this will save a bit of CPU without it! + //// prefetch configuration for tag properties + //var tagEditors = new Dictionary(); + //foreach (var propertyTypeDto in indexedPropertyTypeDtos.Values) + //{ + // var editorAlias = propertyTypeDto.DataTypeDto.EditorAlias; + // var editorAttribute = PropertyEditors[editorAlias].GetTagAttribute(); + // if (editorAttribute == null) continue; + // var tagConfigurationSource = propertyTypeDto.DataTypeDto.Configuration; + // var tagConfiguration = string.IsNullOrWhiteSpace(tagConfigurationSource) + // ? new TagConfiguration() + // : JsonConvert.DeserializeObject(tagConfigurationSource); + // if (tagConfiguration.Delimiter == default) tagConfiguration.Delimiter = editorAttribute.Delimiter; + // tagEditors[editorAlias] = tagConfiguration; + //} // now we have // - the definitions @@ -533,10 +534,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // - tag editors // and we need to build the proper property collections - return GetPropertyCollections(temps, allPropertyDataDtos, tagEditors); + return GetPropertyCollections(temps, allPropertyDataDtos + /*, tagEditors*/); } - private IDictionary GetPropertyCollections(List> temps, IEnumerable allPropertyDataDtos, Dictionary tagConfigurations) + private IDictionary GetPropertyCollections(List> temps, IEnumerable allPropertyDataDtos + /*, Dictionary tagConfigurations*/) where T : class, IContentBase { var result = new Dictionary(); diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 7b003d0a26..5492f97409 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -1139,9 +1139,6 @@ namespace Umbraco.Core.Services.Implement if (publishing) { - //TODO: Check if it's a culture being unpublished and that's why we are publishing the document. In that case - // we should check if the culture is already unpublished since it might be the user is trying to unpublish an already unpublished culture? - //determine cultures publishing/unpublishing which will be based on previous calls to content.PublishCulture and ClearPublishInfo culturesUnpublishing = content.GetCulturesUnpublishing(); culturesPublishing = variesByCulture From 20d28ea2159f90b833ea14f8317f644763fbb3ea Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 30 Jul 2019 19:08:37 +1000 Subject: [PATCH 0064/1001] removes commented out code --- .../Implement/ContentRepositoryBase.cs | 24 +++---------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 60beaca018..7ab73f3f2d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -512,34 +512,16 @@ namespace Umbraco.Core.Persistence.Repositories.Implement foreach (var a in allPropertyDataDtos) a.PropertyTypeDto = indexedPropertyTypeDtos[a.PropertyTypeId]; - //TODO: This logic was here but then we never used the data being passed to GetPropertyCollections, this will save a bit of CPU without it! - //// prefetch configuration for tag properties - //var tagEditors = new Dictionary(); - //foreach (var propertyTypeDto in indexedPropertyTypeDtos.Values) - //{ - // var editorAlias = propertyTypeDto.DataTypeDto.EditorAlias; - // var editorAttribute = PropertyEditors[editorAlias].GetTagAttribute(); - // if (editorAttribute == null) continue; - // var tagConfigurationSource = propertyTypeDto.DataTypeDto.Configuration; - // var tagConfiguration = string.IsNullOrWhiteSpace(tagConfigurationSource) - // ? new TagConfiguration() - // : JsonConvert.DeserializeObject(tagConfigurationSource); - // if (tagConfiguration.Delimiter == default) tagConfiguration.Delimiter = editorAttribute.Delimiter; - // tagEditors[editorAlias] = tagConfiguration; - //} - // now we have // - the definitions // - all property data dtos - // - tag editors + // - tag editors (Actually ... no we don't since i removed that code, but we don't need them anyways it seems) // and we need to build the proper property collections - return GetPropertyCollections(temps, allPropertyDataDtos - /*, tagEditors*/); + return GetPropertyCollections(temps, allPropertyDataDtos); } - private IDictionary GetPropertyCollections(List> temps, IEnumerable allPropertyDataDtos - /*, Dictionary tagConfigurations*/) + private IDictionary GetPropertyCollections(List> temps, IEnumerable allPropertyDataDtos) where T : class, IContentBase { var result = new Dictionary(); From 108416638c837ad25a52dab9a142db909d789d46 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 30 Jul 2019 22:28:06 +1000 Subject: [PATCH 0065/1001] removes commented out code --- src/Umbraco.Tests/Services/ContentServiceTests.cs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 10bb63211b..39bebbe90b 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -828,18 +828,7 @@ namespace Umbraco.Tests.Services //re-get content = ServiceContext.ContentService.GetById(content.Id); Assert.AreEqual(PublishedState.Unpublished, content.PublishedState); //just double checking - Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); - //TODO: This fails!? Why? Because in the ContentService.CommitDocumentChangesInternal method when we detect it's the last lang being unpublished, - // we swap the unpublishing flag to true which means we end up setting the property content.PublishedState = PublishedState.Unpublishing, because of this - // inside of the DocumentRepository we treat many things differently including not publishing the removed culture. - // So how do we fix that? Well when we unpublish the last culture we want to both save the unpublishing of the culture AND unpublish the document, the easy - // way to do this will be to perform a double save operation, though that's not the prettiest way to do it. Ideally we take care of this all in one process - // in the DocumentRepository but that might require another transitory status like PublishedState.UnpublishingLastCulture ... though that's not pretty either. - // It might be possible in some other magicaly way, i'm just leaving these notes here mostly for myself :) - // UPDATE - ok, i 'think' instead of doing a check inside of CommitDocumentChangesInternal for the last culture being unpublished, we could actually do this check - // directly inside of the DocumentRepository - // UPDATE 2 - ok that won't work because there is some business logic that needs to occur in CommitDocumentChangesInternal when it needs to be unpublished, this will - // be tricky without doing a double save, hrm. + Assert.IsFalse(content.IsCulturePublished(langFr.IsoCode)); Assert.IsFalse(content.IsCulturePublished(langUk.IsoCode)); } From 16d8f8846e1656ffb799c65a9404ffece60dc851 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 30 Jul 2019 22:40:15 +1000 Subject: [PATCH 0066/1001] Adds PanicException with details instead of throwing "panic" strings --- src/Umbraco.Core/Exceptions/PanicException.cs | 28 +++++++++++++++++++ src/Umbraco.Core/Mapping/UmbracoMapper.cs | 5 ++-- .../DataTypes/RenamingPreValueMigrator.cs | 3 +- .../Models/PublishedContent/ModelType.cs | 4 +-- .../Implement/ContentTypeCommonRepository.cs | 3 +- .../Implement/ContentTypeRepository.cs | 3 +- .../Implement/MediaTypeRepository.cs | 3 +- .../Implement/MemberTypeRepository.cs | 3 +- src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../Testing/TestOptionAttributeBase.cs | 5 ++-- .../Mapping/ContentTypeMapDefinition.cs | 3 +- .../MultipleTextStringPropertyEditor.cs | 3 +- .../PublishedCache/NuCache/ContentStore.cs | 3 +- .../PublishedCache/NuCache/SnapDictionary.cs | 3 +- 14 files changed, 55 insertions(+), 15 deletions(-) create mode 100644 src/Umbraco.Core/Exceptions/PanicException.cs diff --git a/src/Umbraco.Core/Exceptions/PanicException.cs b/src/Umbraco.Core/Exceptions/PanicException.cs new file mode 100644 index 0000000000..4d41cafc65 --- /dev/null +++ b/src/Umbraco.Core/Exceptions/PanicException.cs @@ -0,0 +1,28 @@ +using System; +using System.Runtime.Serialization; + +namespace Umbraco.Core.Exceptions +{ + /// + /// Internal exception that in theory should never ben thrown, it is only thrown in circumstances that should never happen + /// + [Serializable] + internal class PanicException : Exception + { + public PanicException() + { + } + + public PanicException(string message) : base(message) + { + } + + public PanicException(string message, Exception innerException) : base(message, innerException) + { + } + + protected PanicException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } +} diff --git a/src/Umbraco.Core/Mapping/UmbracoMapper.cs b/src/Umbraco.Core/Mapping/UmbracoMapper.cs index 8915ebcf74..976f50b499 100644 --- a/src/Umbraco.Core/Mapping/UmbracoMapper.cs +++ b/src/Umbraco.Core/Mapping/UmbracoMapper.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Exceptions; namespace Umbraco.Core.Mapping { @@ -259,7 +260,7 @@ namespace Umbraco.Core.Mapping if (typeof(TTarget).IsArray) { var elementType = typeof(TTarget).GetElementType(); - if (elementType == null) throw new Exception("panic"); + if (elementType == null) throw new PanicException("elementType == null which should never occur"); var targetArray = Array.CreateInstance(elementType, targetList.Count); targetList.CopyTo(targetArray, 0); target = targetArray; @@ -382,7 +383,7 @@ namespace Umbraco.Core.Mapping { if (type.IsArray) return type.GetElementType(); if (type.IsGenericType) return type.GenericTypeArguments[0]; - throw new Exception("panic"); + throw new PanicException($"Could not get enumerable or array type from {type}"); } /// diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/RenamingPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/RenamingPreValueMigrator.cs index c04e7c8fda..89a71fdaf4 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/RenamingPreValueMigrator.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/RenamingPreValueMigrator.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using Umbraco.Core.Exceptions; namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes { @@ -20,7 +21,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes case "Umbraco.NoEdit": return Constants.PropertyEditors.Aliases.Label; default: - throw new Exception("panic"); + throw new PanicException($"The alias {editorAlias} is not supported"); } } } diff --git a/src/Umbraco.Core/Models/PublishedContent/ModelType.cs b/src/Umbraco.Core/Models/PublishedContent/ModelType.cs index 540abda2c5..318ccc916e 100644 --- a/src/Umbraco.Core/Models/PublishedContent/ModelType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/ModelType.cs @@ -75,7 +75,7 @@ namespace Umbraco.Core.Models.PublishedContent return type; var def = type.GetGenericTypeDefinition(); if (def == null) - throw new InvalidOperationException("panic"); + throw new PanicException($"The type {type} has not generic type definition"); var args = type.GetGenericArguments().Select(x => Map(x, modelTypes, true)).ToArray(); return def.MakeGenericType(args); @@ -114,7 +114,7 @@ namespace Umbraco.Core.Models.PublishedContent return type.FullName; var def = type.GetGenericTypeDefinition(); if (def == null) - throw new InvalidOperationException("panic"); + throw new PanicException($"The type {type} has not generic type definition"); var args = type.GetGenericArguments().Select(x => MapToName(x, map, true)).ToArray(); var defFullName = def.FullName.Substring(0, def.FullName.IndexOf('`')); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs index 6b751eb8ff..4393d365f8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using NPoco; using Umbraco.Core.Cache; +using Umbraco.Core.Exceptions; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; @@ -90,7 +91,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement contentType = ContentTypeFactory.BuildContentTypeEntity(contentTypeDto); else if (contentTypeDto.NodeDto.NodeObjectType == Constants.ObjectTypes.MemberType) contentType = ContentTypeFactory.BuildMemberTypeEntity(contentTypeDto); - else throw new Exception("panic"); + else throw new PanicException($"The node object type {contentTypeDto.NodeDto.NodeObjectType} is not supported"); contentTypes.Add(contentType.Id, contentType); // map allowed content types diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs index 9d77eb0990..38988657d1 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using NPoco; using Umbraco.Core.Cache; +using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; @@ -56,7 +57,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { // the cache policy will always want everything // even GetMany(ids) gets everything and filters afterwards - if (ids.Any()) throw new Exception("panic"); + if (ids.Any()) throw new PanicException("There can be no ids specified"); return CommonRepository.GetAllTypes().OfType(); } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaTypeRepository.cs index 1abc75cf3a..604f18cdbd 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaTypeRepository.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using NPoco; using Umbraco.Core.Cache; +using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; @@ -50,7 +51,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { // the cache policy will always want everything // even GetMany(ids) gets everything and filters afterwards - if (ids.Any()) throw new Exception("panic"); + if (ids.Any()) throw new PanicException("There can be no ids specified"); return CommonRepository.GetAllTypes().OfType(); } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs index d96854743e..395723a9f1 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using NPoco; using Umbraco.Core.Cache; +using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; @@ -57,7 +58,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { // the cache policy will always want everything // even GetMany(ids) gets everything and filters afterwards - if (ids.Any()) throw new Exception("panic"); + if (ids.Any()) throw new PanicException("There can be no ids specified"); return CommonRepository.GetAllTypes().OfType(); } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index ca9b7d4034..74af58b94f 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -204,6 +204,7 @@ + diff --git a/src/Umbraco.Tests/Testing/TestOptionAttributeBase.cs b/src/Umbraco.Tests/Testing/TestOptionAttributeBase.cs index db7dd25152..9f72a022f3 100644 --- a/src/Umbraco.Tests/Testing/TestOptionAttributeBase.cs +++ b/src/Umbraco.Tests/Testing/TestOptionAttributeBase.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Reflection; using NUnit.Framework; +using Umbraco.Core.Exceptions; namespace Umbraco.Tests.Testing { @@ -29,7 +30,7 @@ namespace Umbraco.Tests.Testing var methodName = test.MethodName; var type = Type.GetType(typeName, true); if (type == null) - throw new Exception("panic"); // makes no sense + throw new PanicException($"Could not resolve the type from type name {typeName}"); // makes no sense var methodInfo = type.GetMethod(methodName); // what about overloads? var options = GetTestOptions(methodInfo); return options; @@ -53,7 +54,7 @@ namespace Umbraco.Tests.Testing { if (other == null) throw new ArgumentNullException(nameof(other)); if (!(Merge((TestOptionAttributeBase) other) is TOptions merged)) - throw new Exception("panic"); + throw new PanicException("Could not merge test options"); return merged; } diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs index fc029eabe4..528d5f6de5 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs @@ -8,6 +8,7 @@ using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; using Umbraco.Web.Models.ContentEditing; using Umbraco.Core.Services; +using Umbraco.Core.Exceptions; namespace Umbraco.Web.Models.Mapping { @@ -577,7 +578,7 @@ namespace Umbraco.Web.Models.Mapping udiType = Constants.UdiEntityType.DocumentType; break; default: - throw new Exception("panic"); + throw new PanicException($"Source is of type {source.GetType()} which isn't supported here"); } return Udi.Create(udiType, source.Key); diff --git a/src/Umbraco.Web/PropertyEditors/MultipleTextStringPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MultipleTextStringPropertyEditor.cs index 141bdea7b6..abb4eae13f 100644 --- a/src/Umbraco.Web/PropertyEditors/MultipleTextStringPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/MultipleTextStringPropertyEditor.cs @@ -2,6 +2,7 @@ using System.Linq; using Newtonsoft.Json.Linq; using Umbraco.Core; +using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Editors; @@ -56,7 +57,7 @@ namespace Umbraco.Web.PropertyEditors } if (!(editorValue.DataTypeConfiguration is MultipleTextStringConfiguration config)) - throw new Exception("panic"); + throw new PanicException($"editorValue.DataTypeConfiguration is {editorValue.DataTypeConfiguration.GetType()} but must be {typeof(MultipleTextStringConfiguration)}"); var max = config.Maximum; //The legacy property editor saved this data as new line delimited! strange but we have to maintain that. diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 2d501fa3b5..8e2cf7bc3c 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using CSharpTest.Net.Collections; using Umbraco.Core; +using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Scoping; @@ -1044,7 +1045,7 @@ namespace Umbraco.Web.PublishedCache.NuCache if (_genObj == null) _genObjs.Enqueue(_genObj = new GenObj(snapGen)); else if (_genObj.Gen != snapGen) - throw new Exception("panic"); + throw new PanicException($"The generation {_genObj.Gen} does not equal the snapshot generation {snapGen}"); } else { diff --git a/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs b/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs index c5b1df1206..9671949ff0 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Umbraco.Core.Exceptions; using Umbraco.Core.Scoping; using Umbraco.Web.PublishedCache.NuCache.Snap; @@ -371,7 +372,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // if we have one already, ensure it's consistent else if (_genObj.Gen != snapGen) - throw new Exception("panic"); + throw new PanicException($"The generation {_genObj.Gen} does not equal the snapshot generation {snapGen}"); } else { From 6482d849e31a949c40deaa8418d9ec6fc8a07321 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 30 Jul 2019 22:59:33 +1000 Subject: [PATCH 0067/1001] Moves test to different fixture --- .../Services/ContentTypeServiceTests.cs | 355 +---------------- .../ContentTypeServiceVariantsTests.cs | 356 ++++++++++++++++++ 2 files changed, 357 insertions(+), 354 deletions(-) diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs index a0b5f01a1f..f2a4368ae4 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs @@ -53,360 +53,7 @@ namespace Umbraco.Tests.Services Assert.IsTrue(contentType.IsElement); } - [Test] - public void Change_Content_Type_Variation_Clears_Redirects() - { - //create content type with a property type that varies by culture - var contentType = MockedContentTypes.CreateBasicContentType(); - contentType.Variations = ContentVariation.Nothing; - var contentCollection = new PropertyTypeCollection(true); - contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) - { - Alias = "title", - Name = "Title", - Description = "", - Mandatory = false, - SortOrder = 1, - DataTypeId = -88, - Variations = ContentVariation.Nothing - }); - contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); - ServiceContext.ContentTypeService.Save(contentType); - var contentType2 = MockedContentTypes.CreateBasicContentType("test"); - ServiceContext.ContentTypeService.Save(contentType2); - - //create some content of this content type - IContent doc = MockedContent.CreateBasicContent(contentType); - doc.Name = "Hello1"; - ServiceContext.ContentService.Save(doc); - - IContent doc2 = MockedContent.CreateBasicContent(contentType2); - ServiceContext.ContentService.Save(doc2); - - ServiceContext.RedirectUrlService.Register("hello/world", doc.Key); - ServiceContext.RedirectUrlService.Register("hello2/world2", doc2.Key); - - Assert.AreEqual(1, ServiceContext.RedirectUrlService.GetContentRedirectUrls(doc.Key).Count()); - Assert.AreEqual(1, ServiceContext.RedirectUrlService.GetContentRedirectUrls(doc2.Key).Count()); - - //change variation - contentType.Variations = ContentVariation.Culture; - ServiceContext.ContentTypeService.Save(contentType); - - Assert.AreEqual(0, ServiceContext.RedirectUrlService.GetContentRedirectUrls(doc.Key).Count()); - Assert.AreEqual(1, ServiceContext.RedirectUrlService.GetContentRedirectUrls(doc2.Key).Count()); - - } - - [Test] - public void Change_Content_Type_From_Invariant_Variant() - { - //create content type with a property type that varies by culture - var contentType = MockedContentTypes.CreateBasicContentType(); - contentType.Variations = ContentVariation.Nothing; - var contentCollection = new PropertyTypeCollection(true); - contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) - { - Alias = "title", - Name = "Title", - Description = "", - Mandatory = false, - SortOrder = 1, - DataTypeId = -88, - Variations = ContentVariation.Nothing - }); - contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); - ServiceContext.ContentTypeService.Save(contentType); - - //create some content of this content type - IContent doc = MockedContent.CreateBasicContent(contentType); - doc.Name = "Hello1"; - doc.SetValue("title", "hello world"); - ServiceContext.ContentService.Save(doc); - - Assert.AreEqual("Hello1", doc.Name); - Assert.AreEqual("hello world", doc.GetValue("title")); - - //change the content type to be variant, we will also update the name here to detect the copy changes - doc.Name = "Hello2"; - ServiceContext.ContentService.Save(doc); - contentType.Variations = ContentVariation.Culture; - ServiceContext.ContentTypeService.Save(contentType); - doc = ServiceContext.ContentService.GetById(doc.Id); //re-get - - Assert.AreEqual("Hello2", doc.GetCultureName("en-US")); - Assert.AreEqual("hello world", doc.GetValue("title")); //We are not checking against en-US here because properties will remain invariant - - //change back property type to be invariant, we will also update the name here to detect the copy changes - doc.SetCultureName("Hello3", "en-US"); - ServiceContext.ContentService.Save(doc); - contentType.Variations = ContentVariation.Nothing; - ServiceContext.ContentTypeService.Save(contentType); - doc = ServiceContext.ContentService.GetById(doc.Id); //re-get - - Assert.AreEqual("Hello3", doc.Name); - Assert.AreEqual("hello world", doc.GetValue("title")); - } - - [Test] - public void Change_Content_Type_From_Variant_Invariant() - { - //create content type with a property type that varies by culture - var contentType = MockedContentTypes.CreateBasicContentType(); - contentType.Variations = ContentVariation.Culture; - var contentCollection = new PropertyTypeCollection(true); - contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) - { - Alias = "title", - Name = "Title", - Description = "", - Mandatory = false, - SortOrder = 1, - DataTypeId = -88, - Variations = ContentVariation.Culture - }); - contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); - ServiceContext.ContentTypeService.Save(contentType); - - //create some content of this content type - IContent doc = MockedContent.CreateBasicContent(contentType); - doc.SetCultureName("Hello1", "en-US"); - doc.SetValue("title", "hello world", "en-US"); - ServiceContext.ContentService.Save(doc); - - Assert.AreEqual("Hello1", doc.GetCultureName("en-US")); - Assert.AreEqual("hello world", doc.GetValue("title", "en-US")); - - //change the content type to be invariant, we will also update the name here to detect the copy changes - doc.SetCultureName("Hello2", "en-US"); - ServiceContext.ContentService.Save(doc); - contentType.Variations = ContentVariation.Nothing; - ServiceContext.ContentTypeService.Save(contentType); - doc = ServiceContext.ContentService.GetById(doc.Id); //re-get - - Assert.AreEqual("Hello2", doc.Name); - Assert.AreEqual("hello world", doc.GetValue("title")); - - //change back property type to be variant, we will also update the name here to detect the copy changes - doc.Name = "Hello3"; - ServiceContext.ContentService.Save(doc); - contentType.Variations = ContentVariation.Culture; - ServiceContext.ContentTypeService.Save(contentType); - doc = ServiceContext.ContentService.GetById(doc.Id); //re-get - - //at this stage all property types were switched to invariant so even though the variant value - //exists it will not be returned because the property type is invariant, - //so this check proves that null will be returned - Assert.IsNull(doc.GetValue("title", "en-US")); - - //we can now switch the property type to be variant and the value can be returned again - contentType.PropertyTypes.First().Variations = ContentVariation.Culture; - ServiceContext.ContentTypeService.Save(contentType); - doc = ServiceContext.ContentService.GetById(doc.Id); //re-get - - Assert.AreEqual("Hello3", doc.GetCultureName("en-US")); - Assert.AreEqual("hello world", doc.GetValue("title", "en-US")); - - } - - [Test] - public void Change_Property_Type_From_Invariant_Variant() - { - //create content type with a property type that varies by culture - var contentType = MockedContentTypes.CreateBasicContentType(); - contentType.Variations = ContentVariation.Nothing; - var contentCollection = new PropertyTypeCollection(true); - contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) - { - Alias = "title", - Name = "Title", - Description = "", - Mandatory = false, - SortOrder = 1, - DataTypeId = -88, - Variations = ContentVariation.Nothing - }); - contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); - ServiceContext.ContentTypeService.Save(contentType); - - //create some content of this content type - IContent doc = MockedContent.CreateBasicContent(contentType); - doc.Name = "Home"; - doc.SetValue("title", "hello world"); - ServiceContext.ContentService.Save(doc); - - Assert.AreEqual("hello world", doc.GetValue("title")); - - //change the property type to be variant - contentType.PropertyTypes.First().Variations = ContentVariation.Culture; - ServiceContext.ContentTypeService.Save(contentType); - doc = ServiceContext.ContentService.GetById(doc.Id); //re-get - - Assert.AreEqual("hello world", doc.GetValue("title", "en-US")); - - //change back property type to be invariant - contentType.PropertyTypes.First().Variations = ContentVariation.Nothing; - ServiceContext.ContentTypeService.Save(contentType); - doc = ServiceContext.ContentService.GetById(doc.Id); //re-get - - Assert.AreEqual("hello world", doc.GetValue("title")); - } - - [Test] - public void Change_Property_Type_From_Variant_Invariant() - { - //create content type with a property type that varies by culture - var contentType = MockedContentTypes.CreateBasicContentType(); - contentType.Variations = ContentVariation.Culture; - var contentCollection = new PropertyTypeCollection(true); - contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) - { - Alias = "title", - Name = "Title", - Description = "", - Mandatory = false, - SortOrder = 1, - DataTypeId = -88, - Variations = ContentVariation.Culture - }); - contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); - ServiceContext.ContentTypeService.Save(contentType); - - //create some content of this content type - IContent doc = MockedContent.CreateBasicContent(contentType); - doc.SetCultureName("Home", "en-US"); - doc.SetValue("title", "hello world", "en-US"); - ServiceContext.ContentService.Save(doc); - - Assert.AreEqual("hello world", doc.GetValue("title", "en-US")); - - //change the property type to be invariant - contentType.PropertyTypes.First().Variations = ContentVariation.Nothing; - ServiceContext.ContentTypeService.Save(contentType); - doc = ServiceContext.ContentService.GetById(doc.Id); //re-get - - Assert.AreEqual("hello world", doc.GetValue("title")); - - //change back property type to be variant - contentType.PropertyTypes.First().Variations = ContentVariation.Culture; - ServiceContext.ContentTypeService.Save(contentType); - doc = ServiceContext.ContentService.GetById(doc.Id); //re-get - - Assert.AreEqual("hello world", doc.GetValue("title", "en-US")); - } - - [Test] - public void Change_Property_Type_From_Variant_Invariant_On_A_Composition() - { - //create content type with a property type that varies by culture - var contentType = MockedContentTypes.CreateBasicContentType(); - contentType.Variations = ContentVariation.Culture; - var contentCollection = new PropertyTypeCollection(true); - contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) - { - Alias = "title", - Name = "Title", - Description = "", - Mandatory = false, - SortOrder = 1, - DataTypeId = -88, - Variations = ContentVariation.Culture - }); - contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); - ServiceContext.ContentTypeService.Save(contentType); - - //compose this from the other one - var contentType2 = MockedContentTypes.CreateBasicContentType("test"); - contentType2.Variations = ContentVariation.Culture; - contentType2.AddContentType(contentType); - ServiceContext.ContentTypeService.Save(contentType2); - - //create some content of this content type - IContent doc = MockedContent.CreateBasicContent(contentType); - doc.SetCultureName("Home", "en-US"); - doc.SetValue("title", "hello world", "en-US"); - ServiceContext.ContentService.Save(doc); - - IContent doc2 = MockedContent.CreateBasicContent(contentType2); - doc2.SetCultureName("Home", "en-US"); - doc2.SetValue("title", "hello world", "en-US"); - ServiceContext.ContentService.Save(doc2); - - //change the property type to be invariant - contentType.PropertyTypes.First().Variations = ContentVariation.Nothing; - ServiceContext.ContentTypeService.Save(contentType); - doc = ServiceContext.ContentService.GetById(doc.Id); //re-get - doc2 = ServiceContext.ContentService.GetById(doc2.Id); //re-get - - Assert.AreEqual("hello world", doc.GetValue("title")); - Assert.AreEqual("hello world", doc2.GetValue("title")); - - //change back property type to be variant - contentType.PropertyTypes.First().Variations = ContentVariation.Culture; - ServiceContext.ContentTypeService.Save(contentType); - doc = ServiceContext.ContentService.GetById(doc.Id); //re-get - doc2 = ServiceContext.ContentService.GetById(doc2.Id); //re-get - - Assert.AreEqual("hello world", doc.GetValue("title", "en-US")); - Assert.AreEqual("hello world", doc2.GetValue("title", "en-US")); - } - - [Test] - public void Change_Content_Type_From_Variant_Invariant_On_A_Composition() - { - //create content type with a property type that varies by culture - var contentType = MockedContentTypes.CreateBasicContentType(); - contentType.Variations = ContentVariation.Culture; - var contentCollection = new PropertyTypeCollection(true); - contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) - { - Alias = "title", - Name = "Title", - Description = "", - Mandatory = false, - SortOrder = 1, - DataTypeId = -88, - Variations = ContentVariation.Culture - }); - contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); - ServiceContext.ContentTypeService.Save(contentType); - - //compose this from the other one - var contentType2 = MockedContentTypes.CreateBasicContentType("test"); - contentType2.Variations = ContentVariation.Culture; - contentType2.AddContentType(contentType); - ServiceContext.ContentTypeService.Save(contentType2); - - //create some content of this content type - IContent doc = MockedContent.CreateBasicContent(contentType); - doc.SetCultureName("Home", "en-US"); - doc.SetValue("title", "hello world", "en-US"); - ServiceContext.ContentService.Save(doc); - - IContent doc2 = MockedContent.CreateBasicContent(contentType2); - doc2.SetCultureName("Home", "en-US"); - doc2.SetValue("title", "hello world", "en-US"); - ServiceContext.ContentService.Save(doc2); - - //change the content type to be invariant - contentType.Variations = ContentVariation.Nothing; - ServiceContext.ContentTypeService.Save(contentType); - doc = ServiceContext.ContentService.GetById(doc.Id); //re-get - doc2 = ServiceContext.ContentService.GetById(doc2.Id); //re-get - - Assert.AreEqual("hello world", doc.GetValue("title")); - Assert.AreEqual("hello world", doc2.GetValue("title")); - - //change back content type to be variant - contentType.Variations = ContentVariation.Culture; - ServiceContext.ContentTypeService.Save(contentType); - doc = ServiceContext.ContentService.GetById(doc.Id); //re-get - doc2 = ServiceContext.ContentService.GetById(doc2.Id); //re-get - - //this will be null because the doc type was changed back to variant but it's property types don't get changed back - Assert.IsNull(doc.GetValue("title", "en-US")); - Assert.IsNull(doc2.GetValue("title", "en-US")); - } + [Test] public void Deleting_Content_Type_With_Hierarchy_Of_Content_Items_Moves_Orphaned_Content_To_Recycle_Bin() diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs index 18ea95cd98..2748502e2c 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs @@ -17,6 +17,7 @@ using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; using Umbraco.Core.Strings; using Umbraco.Core.Sync; +using Umbraco.Tests.TestHelpers.Entities; using Umbraco.Tests.Testing; using Umbraco.Web.PublishedCache; using Umbraco.Web.PublishedCache.NuCache; @@ -106,6 +107,361 @@ namespace Umbraco.Tests.Services } } + [Test] + public void Change_Content_Type_Variation_Clears_Redirects() + { + //create content type with a property type that varies by culture + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Nothing; + var contentCollection = new PropertyTypeCollection(true); + contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) + { + Alias = "title", + Name = "Title", + Description = "", + Mandatory = false, + SortOrder = 1, + DataTypeId = -88, + Variations = ContentVariation.Nothing + }); + contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); + ServiceContext.ContentTypeService.Save(contentType); + var contentType2 = MockedContentTypes.CreateBasicContentType("test"); + ServiceContext.ContentTypeService.Save(contentType2); + + //create some content of this content type + IContent doc = MockedContent.CreateBasicContent(contentType); + doc.Name = "Hello1"; + ServiceContext.ContentService.Save(doc); + + IContent doc2 = MockedContent.CreateBasicContent(contentType2); + ServiceContext.ContentService.Save(doc2); + + ServiceContext.RedirectUrlService.Register("hello/world", doc.Key); + ServiceContext.RedirectUrlService.Register("hello2/world2", doc2.Key); + + Assert.AreEqual(1, ServiceContext.RedirectUrlService.GetContentRedirectUrls(doc.Key).Count()); + Assert.AreEqual(1, ServiceContext.RedirectUrlService.GetContentRedirectUrls(doc2.Key).Count()); + + //change variation + contentType.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + Assert.AreEqual(0, ServiceContext.RedirectUrlService.GetContentRedirectUrls(doc.Key).Count()); + Assert.AreEqual(1, ServiceContext.RedirectUrlService.GetContentRedirectUrls(doc2.Key).Count()); + + } + + [Test] + public void Change_Content_Type_From_Invariant_Variant() + { + //create content type with a property type that varies by culture + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Nothing; + var contentCollection = new PropertyTypeCollection(true); + contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) + { + Alias = "title", + Name = "Title", + Description = "", + Mandatory = false, + SortOrder = 1, + DataTypeId = -88, + Variations = ContentVariation.Nothing + }); + contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); + ServiceContext.ContentTypeService.Save(contentType); + + //create some content of this content type + IContent doc = MockedContent.CreateBasicContent(contentType); + doc.Name = "Hello1"; + doc.SetValue("title", "hello world"); + ServiceContext.ContentService.Save(doc); + + Assert.AreEqual("Hello1", doc.Name); + Assert.AreEqual("hello world", doc.GetValue("title")); + + //change the content type to be variant, we will also update the name here to detect the copy changes + doc.Name = "Hello2"; + ServiceContext.ContentService.Save(doc); + contentType.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + + Assert.AreEqual("Hello2", doc.GetCultureName("en-US")); + Assert.AreEqual("hello world", doc.GetValue("title")); //We are not checking against en-US here because properties will remain invariant + + //change back property type to be invariant, we will also update the name here to detect the copy changes + doc.SetCultureName("Hello3", "en-US"); + ServiceContext.ContentService.Save(doc); + contentType.Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + + Assert.AreEqual("Hello3", doc.Name); + Assert.AreEqual("hello world", doc.GetValue("title")); + } + + [Test] + public void Change_Content_Type_From_Variant_Invariant() + { + //create content type with a property type that varies by culture + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Culture; + var contentCollection = new PropertyTypeCollection(true); + contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) + { + Alias = "title", + Name = "Title", + Description = "", + Mandatory = false, + SortOrder = 1, + DataTypeId = -88, + Variations = ContentVariation.Culture + }); + contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); + ServiceContext.ContentTypeService.Save(contentType); + + //create some content of this content type + IContent doc = MockedContent.CreateBasicContent(contentType); + doc.SetCultureName("Hello1", "en-US"); + doc.SetValue("title", "hello world", "en-US"); + ServiceContext.ContentService.Save(doc); + + Assert.AreEqual("Hello1", doc.GetCultureName("en-US")); + Assert.AreEqual("hello world", doc.GetValue("title", "en-US")); + + //change the content type to be invariant, we will also update the name here to detect the copy changes + doc.SetCultureName("Hello2", "en-US"); + ServiceContext.ContentService.Save(doc); + contentType.Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + + Assert.AreEqual("Hello2", doc.Name); + Assert.AreEqual("hello world", doc.GetValue("title")); + + //change back property type to be variant, we will also update the name here to detect the copy changes + doc.Name = "Hello3"; + ServiceContext.ContentService.Save(doc); + contentType.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + + //at this stage all property types were switched to invariant so even though the variant value + //exists it will not be returned because the property type is invariant, + //so this check proves that null will be returned + Assert.IsNull(doc.GetValue("title", "en-US")); + + //we can now switch the property type to be variant and the value can be returned again + contentType.PropertyTypes.First().Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + + Assert.AreEqual("Hello3", doc.GetCultureName("en-US")); + Assert.AreEqual("hello world", doc.GetValue("title", "en-US")); + + } + + [Test] + public void Change_Property_Type_From_Invariant_Variant() + { + //create content type with a property type that varies by culture + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Nothing; + var contentCollection = new PropertyTypeCollection(true); + contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) + { + Alias = "title", + Name = "Title", + Description = "", + Mandatory = false, + SortOrder = 1, + DataTypeId = -88, + Variations = ContentVariation.Nothing + }); + contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); + ServiceContext.ContentTypeService.Save(contentType); + + //create some content of this content type + IContent doc = MockedContent.CreateBasicContent(contentType); + doc.Name = "Home"; + doc.SetValue("title", "hello world"); + ServiceContext.ContentService.Save(doc); + + Assert.AreEqual("hello world", doc.GetValue("title")); + + //change the property type to be variant + contentType.PropertyTypes.First().Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + + Assert.AreEqual("hello world", doc.GetValue("title", "en-US")); + + //change back property type to be invariant + contentType.PropertyTypes.First().Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + + Assert.AreEqual("hello world", doc.GetValue("title")); + } + + [Test] + public void Change_Property_Type_From_Variant_Invariant() + { + //create content type with a property type that varies by culture + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Culture; + var contentCollection = new PropertyTypeCollection(true); + contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) + { + Alias = "title", + Name = "Title", + Description = "", + Mandatory = false, + SortOrder = 1, + DataTypeId = -88, + Variations = ContentVariation.Culture + }); + contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); + ServiceContext.ContentTypeService.Save(contentType); + + //create some content of this content type + IContent doc = MockedContent.CreateBasicContent(contentType); + doc.SetCultureName("Home", "en-US"); + doc.SetValue("title", "hello world", "en-US"); + ServiceContext.ContentService.Save(doc); + + Assert.AreEqual("hello world", doc.GetValue("title", "en-US")); + + //change the property type to be invariant + contentType.PropertyTypes.First().Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + + Assert.AreEqual("hello world", doc.GetValue("title")); + + //change back property type to be variant + contentType.PropertyTypes.First().Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + + Assert.AreEqual("hello world", doc.GetValue("title", "en-US")); + } + + [Test] + public void Change_Property_Type_From_Variant_Invariant_On_A_Composition() + { + //create content type with a property type that varies by culture + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Culture; + var contentCollection = new PropertyTypeCollection(true); + contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) + { + Alias = "title", + Name = "Title", + Description = "", + Mandatory = false, + SortOrder = 1, + DataTypeId = -88, + Variations = ContentVariation.Culture + }); + contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); + ServiceContext.ContentTypeService.Save(contentType); + + //compose this from the other one + var contentType2 = MockedContentTypes.CreateBasicContentType("test"); + contentType2.Variations = ContentVariation.Culture; + contentType2.AddContentType(contentType); + ServiceContext.ContentTypeService.Save(contentType2); + + //create some content of this content type + IContent doc = MockedContent.CreateBasicContent(contentType); + doc.SetCultureName("Home", "en-US"); + doc.SetValue("title", "hello world", "en-US"); + ServiceContext.ContentService.Save(doc); + + IContent doc2 = MockedContent.CreateBasicContent(contentType2); + doc2.SetCultureName("Home", "en-US"); + doc2.SetValue("title", "hello world", "en-US"); + ServiceContext.ContentService.Save(doc2); + + //change the property type to be invariant + contentType.PropertyTypes.First().Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + doc2 = ServiceContext.ContentService.GetById(doc2.Id); //re-get + + Assert.AreEqual("hello world", doc.GetValue("title")); + Assert.AreEqual("hello world", doc2.GetValue("title")); + + //change back property type to be variant + contentType.PropertyTypes.First().Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + doc2 = ServiceContext.ContentService.GetById(doc2.Id); //re-get + + Assert.AreEqual("hello world", doc.GetValue("title", "en-US")); + Assert.AreEqual("hello world", doc2.GetValue("title", "en-US")); + } + + [Test] + public void Change_Content_Type_From_Variant_Invariant_On_A_Composition() + { + //create content type with a property type that varies by culture + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Culture; + var contentCollection = new PropertyTypeCollection(true); + contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) + { + Alias = "title", + Name = "Title", + Description = "", + Mandatory = false, + SortOrder = 1, + DataTypeId = -88, + Variations = ContentVariation.Culture + }); + contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); + ServiceContext.ContentTypeService.Save(contentType); + + //compose this from the other one + var contentType2 = MockedContentTypes.CreateBasicContentType("test"); + contentType2.Variations = ContentVariation.Culture; + contentType2.AddContentType(contentType); + ServiceContext.ContentTypeService.Save(contentType2); + + //create some content of this content type + IContent doc = MockedContent.CreateBasicContent(contentType); + doc.SetCultureName("Home", "en-US"); + doc.SetValue("title", "hello world", "en-US"); + ServiceContext.ContentService.Save(doc); + + IContent doc2 = MockedContent.CreateBasicContent(contentType2); + doc2.SetCultureName("Home", "en-US"); + doc2.SetValue("title", "hello world", "en-US"); + ServiceContext.ContentService.Save(doc2); + + //change the content type to be invariant + contentType.Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + doc2 = ServiceContext.ContentService.GetById(doc2.Id); //re-get + + Assert.AreEqual("hello world", doc.GetValue("title")); + Assert.AreEqual("hello world", doc2.GetValue("title")); + + //change back content type to be variant + contentType.Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + doc2 = ServiceContext.ContentService.GetById(doc2.Id); //re-get + + //this will be null because the doc type was changed back to variant but it's property types don't get changed back + Assert.IsNull(doc.GetValue("title", "en-US")); + Assert.IsNull(doc2.GetValue("title", "en-US")); + } + [Test] public void Change_Variations_SimpleContentType_VariantToInvariantAndBack() { From 5c8cd6027518acf45e2d960b66b28f564952a33b Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 31 Jul 2019 00:13:40 +1000 Subject: [PATCH 0068/1001] Writes up a test to show the issue with edited culture flags, this shows that the current fix works but there's still an issue --- .../ContentTypeServiceVariantsTests.cs | 138 +++++++++++++++--- 1 file changed, 121 insertions(+), 17 deletions(-) diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs index 2748502e2c..f5fa4e8795 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs @@ -468,10 +468,7 @@ namespace Umbraco.Tests.Services // one simple content type, variant, with both variant and invariant properties // can change it to invariant and back - var languageEn = new Language("en") { IsDefault = true }; - ServiceContext.LocalizationService.Save(languageEn); - var languageFr = new Language("fr"); - ServiceContext.LocalizationService.Save(languageFr); + CreateFrenchAndEnglishLangs(); var contentType = new ContentType(-1) { @@ -499,7 +496,7 @@ namespace Umbraco.Tests.Services contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); ServiceContext.ContentTypeService.Save(contentType); - var document = (IContent) new Content("document", -1, contentType); + var document = (IContent)new Content("document", -1, contentType); document.SetCultureName("doc1en", "en"); document.SetCultureName("doc1fr", "fr"); document.SetValue("value1", "v1en", "en"); @@ -682,10 +679,7 @@ namespace Umbraco.Tests.Services // one simple content type, variant, with both variant and invariant properties // can change an invariant property to variant and back - var languageEn = new Language("en") { IsDefault = true }; - ServiceContext.LocalizationService.Save(languageEn); - var languageFr = new Language("fr"); - ServiceContext.LocalizationService.Save(languageFr); + CreateFrenchAndEnglishLangs(); var contentType = new ContentType(-1) { @@ -785,6 +779,114 @@ namespace Umbraco.Tests.Services "{'properties':{'value1':[{'culture':'en','seg':'','val':'v1en'},{'culture':'fr','seg':'','val':'v1fr'}],'value2':[{'culture':'en','seg':'','val':'v2'}]},'cultureData':"); } + [Test] + public void Change_Variations_SimpleContentType_VariantPropertyToInvariantAndBack_While_Publishing() + { + // one simple content type, variant, with both variant and invariant properties + // can change an invariant property to variant and back + + CreateFrenchAndEnglishLangs(); + + var contentType = new ContentType(-1) + { + Alias = "contentType", + Name = "contentType", + Variations = ContentVariation.Culture + }; + + var properties = new PropertyTypeCollection(true) + { + new PropertyType("value1", ValueStorageType.Ntext) + { + Alias = "value1", + DataTypeId = -88, + Variations = ContentVariation.Culture + } + }; + + contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); + ServiceContext.ContentTypeService.Save(contentType); + + var document = (IContent)new Content("document", -1, contentType); + document.SetCultureName("doc1en", "en"); + document.SetCultureName("doc1fr", "fr"); + document.SetValue("value1", "v1en", "en"); + document.SetValue("value1", "v1fr", "fr"); + ServiceContext.ContentService.Save(document); + + //at this stage there will be 4 property values stored in the DB for "value1" against "en" and "fr" for both edited/published versions, + //for the published values, these will be null + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.AreEqual("doc1en", document.GetCultureName("en")); + Assert.AreEqual("doc1fr", document.GetCultureName("fr")); + Assert.AreEqual("v1en", document.GetValue("value1", "en")); + Assert.AreEqual("v1fr", document.GetValue("value1", "fr")); + Assert.IsTrue(document.IsCultureEdited("en")); //This will be true because the edited value isn't the same as the published value + Assert.IsTrue(document.IsCultureEdited("fr")); //This will be true because the edited value isn't the same as the published value + Assert.IsTrue(document.Edited); + + // switch property type to Nothing + contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(contentType); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.IsTrue(document.Edited); + + // publish the document + document.SetValue("value1", "v1inv"); //update the invariant value + ServiceContext.ContentService.SaveAndPublish(document); + + //at this stage there will be 6 property values stored in the DB for "value1" against "en", "fr" and null for both edited/published versions, + //for the published values for the cultures, these will still be null but the invariant edited/published values will both be "v1inv". + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.AreEqual("doc1en", document.GetCultureName("en")); + Assert.AreEqual("doc1fr", document.GetCultureName("fr")); + Assert.IsNull(document.GetValue("value1", "en")); //The values are there but the business logic returns null + Assert.IsNull(document.GetValue("value1", "fr")); //The values are there but the business logic returns null + Assert.AreEqual("v1inv", document.GetValue("value1")); + Assert.IsFalse(document.IsCultureEdited("en")); //This returns false, the culture shouldn't be marked as edited because the invariant property value is published + Assert.IsFalse(document.IsCultureEdited("fr")); //This returns false, the culture shouldn't be marked as edited because the invariant property value is published + Assert.IsFalse(document.Edited); + + // switch property back to Culture + contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("v1inv", document.GetValue("value1", "en")); //The invariant property value gets copied over to the default language + Assert.AreEqual("v1fr", document.GetValue("value1", "fr")); + Assert.IsFalse(document.IsCultureEdited("en")); //The invariant published AND edited values are copied over to the default language + //TODO: This fails - for some reason in the DB, the DocumentCultureVariationDto Edited flag is set to false for the FR culture + // I'm assuming this is somehow done during the ContentTypeService.Save method when changing variation. It should not be false but instead + // true because the values for it's edited vs published version are different. Perhaps it's false because that is the default boolean value and + // this row gets deleted somewhere along the way? + Assert.IsTrue(document.IsCultureEdited("fr")); //The previously existing french values are there and there is no published value + Assert.IsTrue(document.Edited); + + // publish again + document.SetValue("value1", "v1en2", "en"); //update the value now that it's variant again + document.SetValue("value1", "v1fr2", "fr"); //update the value now that it's variant again + ServiceContext.ContentService.SaveAndPublish(document); + + //at this stage there will be 4 property values stored in the DB for "value1" against "en", "fr" for both edited/published versions, + //with the change back to Culture, it deletes the invariant property values. + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.AreEqual("doc1en", document.GetCultureName("en")); + Assert.AreEqual("doc1fr", document.GetCultureName("fr")); + Assert.AreEqual("v1en2", document.GetValue("value1", "en")); + Assert.AreEqual("v1fr2", document.GetValue("value1", "fr")); + Assert.IsNull(document.GetValue("value1")); //The value is there but the business logic returns null + Assert.IsFalse(document.IsCultureEdited("en")); //This returns false, the variant property value has been published + Assert.IsFalse(document.IsCultureEdited("fr")); //This returns false, the variant property value has been published + Assert.IsFalse(document.Edited); + } + [Test] public void Change_Variations_ComposedContentType_1() { @@ -793,10 +895,7 @@ namespace Umbraco.Tests.Services // can change the composing content type to invariant and back // can change the composed content type to invariant and back - var languageEn = new Language("en") { IsDefault = true }; - ServiceContext.LocalizationService.Save(languageEn); - var languageFr = new Language("fr"); - ServiceContext.LocalizationService.Save(languageFr); + CreateFrenchAndEnglishLangs(); var composing = new ContentType(-1) { @@ -925,10 +1024,7 @@ namespace Umbraco.Tests.Services // can change the composing content type to invariant and back // can change the variant composed content type to invariant and back - var languageEn = new Language("en") { IsDefault = true }; - ServiceContext.LocalizationService.Save(languageEn); - var languageFr = new Language("fr"); - ServiceContext.LocalizationService.Save(languageFr); + CreateFrenchAndEnglishLangs(); var composing = new ContentType(-1) { @@ -1110,5 +1206,13 @@ namespace Umbraco.Tests.Services AssertJsonStartsWith(document2.Id, "{'properties':{'value11':[{'culture':'','seg':'','val':'v11'}],'value12':[{'culture':'','seg':'','val':'v12'}],'value31':[{'culture':'','seg':'','val':'v31'}],'value32':[{'culture':'','seg':'','val':'v32'}]},'cultureData':"); } + + private void CreateFrenchAndEnglishLangs() + { + var languageEn = new Language("en") { IsDefault = true }; + ServiceContext.LocalizationService.Save(languageEn); + var languageFr = new Language("fr"); + ServiceContext.LocalizationService.Save(languageFr); + } } } From 87e7cec02eb2d7eb062aaef2c37f226fa5c286b6 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 31 Jul 2019 18:30:34 +1000 Subject: [PATCH 0069/1001] WIP - commiting what i have. Solved part of the problem but there are others. --- .../Persistence/Factories/PropertyFactory.cs | 21 +- .../Implement/ContentTypeRepository.cs | 4 +- .../Implement/ContentTypeRepositoryBase.cs | 263 +++++++++++++++++- .../Implement/DocumentRepository.cs | 25 +- .../Implement/MediaTypeRepository.cs | 4 +- .../Implement/MemberTypeRepository.cs | 4 +- .../Repositories/ContentTypeRepositoryTest.cs | 9 +- .../Repositories/DocumentRepositoryTest.cs | 2 +- .../Repositories/DomainRepositoryTest.cs | 2 +- .../Repositories/MediaRepositoryTest.cs | 3 +- .../Repositories/MediaTypeRepositoryTest.cs | 3 +- .../Repositories/MemberRepositoryTest.cs | 3 +- .../Repositories/MemberTypeRepositoryTest.cs | 3 +- .../PublicAccessRepositoryTest.cs | 2 +- .../Repositories/TagRepositoryTest.cs | 5 +- .../Repositories/TemplateRepositoryTest.cs | 2 +- .../Repositories/UserRepositoryTest.cs | 5 +- .../Services/ContentServicePerformanceTest.cs | 12 +- .../Services/ContentServiceTests.cs | 2 +- .../ContentTypeServiceVariantsTests.cs | 31 ++- 20 files changed, 344 insertions(+), 61 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs index f0937a3781..4e7dc1e982 100644 --- a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs @@ -105,9 +105,14 @@ namespace Umbraco.Core.Persistence.Factories /// /// out parameter indicating that one or more properties have been edited /// out parameter containing a collection of edited cultures when the contentVariation varies by culture + /// + /// out parameter containing a collection of edited cultures that are currently persisted in the database which is used to maintain the edited state + /// of each culture value (in cases where variance is being switched) + /// /// public static IEnumerable BuildDtos(ContentVariation contentVariation, int currentVersionId, int publishedVersionId, IEnumerable properties, - ILanguageRepository languageRepository, out bool edited, out HashSet editedCultures) + ILanguageRepository languageRepository, out bool edited, + out HashSet editedCultures) { var propertyDataDtos = new List(); edited = false; @@ -141,14 +146,14 @@ namespace Umbraco.Core.Persistence.Factories if (propertyValue.EditedValue != null) propertyDataDtos.Add(BuildDto(currentVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.EditedValue)); - // property.Values will contain ALL of it's values, both variant and invariant which will be populated if the administrator has previously - // changed the property type to be variant vs invariant. - // We need to check for this scenario here because otherwise the editedCultures and edited flags - // will end up incorrectly so here we need to only process edited cultures based on the - // current value type and how the property varies. + //// property.Values will contain ALL of it's values, both variant and invariant which will be populated if the administrator has previously + //// changed the property type to be variant vs invariant. + //// We need to check for this scenario here because otherwise the editedCultures and edited flags + //// will end up incorrectly so here we need to only process edited cultures based on the + //// current value type and how the property varies. - if (property.PropertyType.VariesByCulture() && isInvariantValue) continue; - if (!property.PropertyType.VariesByCulture() && isCultureValue) continue; + //if (property.PropertyType.VariesByCulture() && isInvariantValue) continue; + //if (!property.PropertyType.VariesByCulture() && isCultureValue) continue; // use explicit equals here, else object comparison fails at comparing eg strings var sameValues = propertyValue.PublishedValue == null ? propertyValue.EditedValue == null : propertyValue.PublishedValue.Equals(propertyValue.EditedValue); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs index 9d77eb0990..8b84145027 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs @@ -18,8 +18,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// internal class ContentTypeRepository : ContentTypeRepositoryBase, IContentTypeRepository { - public ContentTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository) - : base(scopeAccessor, cache, logger, commonRepository) + public ContentTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository, ILanguageRepository languageRepository) + : base(scopeAccessor, cache, logger, commonRepository, languageRepository) { } protected override bool SupportsPublishing => ContentType.SupportsPublishingConst; diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 22c9244d8f..756435692a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -15,6 +15,7 @@ using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Scoping; using Umbraco.Core.Services; +using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; namespace Umbraco.Core.Persistence.Repositories.Implement { @@ -26,14 +27,15 @@ namespace Umbraco.Core.Persistence.Repositories.Implement internal abstract class ContentTypeRepositoryBase : NPocoRepositoryBase, IReadRepository where TEntity : class, IContentTypeComposition { - protected ContentTypeRepositoryBase(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository) + protected ContentTypeRepositoryBase(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository, ILanguageRepository languageRepository) : base(scopeAccessor, cache, logger) { CommonRepository = commonRepository; + LanguageRepository = languageRepository; } protected IContentTypeCommonRepository CommonRepository { get; } - + protected ILanguageRepository LanguageRepository { get; } protected abstract bool SupportsPublishing { get; } public IEnumerable> Move(TEntity moving, EntityContainer container) @@ -646,10 +648,16 @@ AND umbracoNode.id <> @id", case ContentVariation.Culture: CopyPropertyData(null, defaultLanguageId, propertyTypeIds, impactedL); CopyTagData(null, defaultLanguageId, propertyTypeIds, impactedL); + RenormalizeDocumentCultureVariations(propertyTypeIds, impactedL); + //TODO: Here we need to normalize the umbracoDocumentCultureVariation table for it's edited flags which are calculated based + //on changed property or name values break; case ContentVariation.Nothing: CopyPropertyData(defaultLanguageId, null, propertyTypeIds, impactedL); CopyTagData(defaultLanguageId, null, propertyTypeIds, impactedL); + RenormalizeDocumentCultureVariations(propertyTypeIds, impactedL); + //TODO: Here we need to normalize the umbracoDocumentCultureVariation table for it's edited flags which are calculated based + //on changed property or name values break; case ContentVariation.CultureAndSegment: case ContentVariation.Segment: @@ -659,6 +667,55 @@ AND umbracoNode.id <> @id", } } + //private HashSet GetEditedCultures(ContentVariation contentVariation, int currentVersionId, int publishedVersionId, IEnumerable properties) + //{ + // HashSet editedCultures = null; // don't allocate unless necessary + // string defaultCulture = null; //don't allocate unless necessary + + // var entityVariesByCulture = contentVariation.VariesByCulture(); + + // // create dtos for each property values, but only for values that do actually exist + // // ie have a non-null value, everything else is just ignored and won't have a db row + + // foreach (var property in properties) + // { + // if (property.PropertyType.SupportsPublishing) + // { + // //create the resulting hashset if it's not created and the entity varies by culture + // if (entityVariesByCulture && editedCultures == null) + // editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase); + + // // publishing = deal with edit and published values + // foreach (var propertyValue in property.Values) + // { + // var isInvariantValue = propertyValue.Culture == null; + // var isCultureValue = propertyValue.Culture != null && propertyValue.Segment == null; + + // // use explicit equals here, else object comparison fails at comparing eg strings + // var sameValues = propertyValue.PublishedValue == null ? propertyValue.EditedValue == null : propertyValue.PublishedValue.Equals(propertyValue.EditedValue); + + // if (entityVariesByCulture && !sameValues) + // { + // if (isCultureValue) + // { + // editedCultures.Add(propertyValue.Culture); // report culture as edited + // } + // else if (isInvariantValue) + // { + // // flag culture as edited if it contains an edited invariant property + // if (defaultCulture == null) + // defaultCulture = languageRepository.GetDefaultIsoCode(); + + // editedCultures.Add(defaultCulture); + // } + // } + // } + // } + // } + + // return editedCultures; + //} + /// /// Moves variant data for a content type variation change. /// @@ -963,6 +1020,208 @@ AND umbracoNode.id <> @id", Database.Execute(sqlDelete); } + + } + + /// + /// Re-normalizes the edited value in the umbracoDocumentCultureVariation table when property variations are changed + /// + /// + /// + /// + /// If this is not done, then in some cases the "edited" value for a particular culture for a document will remain true when it should be false + /// if the property was changed to invariant. In order to do this we need to recalculate this value based on the values stored for each + /// property, culture and current/published version. The end result is to update the edited value in the umbracoDocumentCultureVariation table so we + /// make sure to join this table with the lookups so that only relevant data is returned. + /// + private void RenormalizeDocumentCultureVariations(IReadOnlyCollection propertyTypeIds, IReadOnlyCollection contentTypeIds = null) + { + + var defaultLang = LanguageRepository.GetDefaultId(); + + //This will build up a query to get the property values of both the current and the published version so that we can check + //based on the current variance of each item to see if it's 'edited' value should be true/false. + + var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); + if (whereInArgsCount > 2000) + throw new NotSupportedException("Too many property/content types."); + + var propertySql = Sql() + .Select() + .AndSelect(x => x.NodeId, x => x.Current) + .AndSelect(x => x.Published) + .AndSelect(x => x.Variations) + .From() + .InnerJoin().On((left, right) => left.Id == right.VersionId) + .InnerJoin().On((left, right) => left.Id == right.PropertyTypeId); + + if (contentTypeIds != null) + { + propertySql.InnerJoin().On((c, cversion) => c.NodeId == cversion.NodeId); + } + + propertySql.LeftJoin().On((docversion, cversion) => cversion.Id == docversion.Id) + .Where((docversion, cversion) => cversion.Current || docversion.Published) + .WhereIn(x => x.PropertyTypeId, propertyTypeIds); + + if (contentTypeIds != null) + { + propertySql.WhereIn(x => x.ContentTypeId, contentTypeIds); + } + + propertySql + .OrderBy(x => x.NodeId) + .OrderBy(x => x.PropertyTypeId, x => x.LanguageId, x => x.VersionId); + + //keep track of this node/lang to mark or unmark as edited + var editedVersions = new Dictionary<(int nodeId, int? langId), bool>(); + + var nodeId = -1; + var propertyTypeId = -1; + PropertyValueVersionDto pubRow = null; + + //This is a QUERY we are not fetching this all into memory so we cannot make any changes during this iteration, we are just collecting data. + //Published data will always come before Current data based on the version id sort. + //There will only be one published row (max) and one current row per property. + foreach (var row in Database.Query(propertySql)) + { + //make sure to reset on each node/property change + if (nodeId != row.NodeId || propertyTypeId != row.PropertyTypeId) + { + nodeId = row.NodeId; + propertyTypeId = row.PropertyTypeId; + pubRow = null; + } + + if (row.Published) + pubRow = row; + + if (row.Current) + { + var propVariations = (ContentVariation)row.Variations; + + //if this prop doesn't vary but the row has a lang assigned or vice versa, flag this as not edited + if (!propVariations.VariesByCulture() && row.LanguageId.HasValue + || propVariations.VariesByCulture() && !row.LanguageId.HasValue) + { + //Flag this as not edited for this node/lang if the key doesn't exist + if (!editedVersions.TryGetValue((row.NodeId, row.LanguageId), out _)) + editedVersions.Add((row.NodeId, row.LanguageId), false); + } + else if (pubRow == null) + { + //this would mean that that this property is 'edited' since there is no published version + editedVersions.Add((row.NodeId, row.LanguageId), true); + } + //compare the property values, if they differ from versions then flag the current version as edited + else if (IsPropertyValueChanged(pubRow, row)) + { + //Here we would check if the property is invariant, in which case the edited language should be indicated by the default lang + editedVersions[(row.NodeId, !propVariations.VariesByCulture() ? defaultLang : row.LanguageId)] = true; + } + + //reset + pubRow = null; + } + } + + //lookup all matching rows in umbracoDocumentCultureVariation + var docCultureVariationsToUpdate = Database.Fetch( + Sql().Select().From() + .WhereIn(x => x.LanguageId, editedVersions.Keys.Select(x => x.langId).ToList()) + .WhereIn(x => x.NodeId, editedVersions.Keys.Select(x => x.nodeId))) + //convert to dictionary with the same key type + .ToDictionary(x => (x.NodeId, (int?)x.LanguageId), x => x); + + foreach (var ev in editedVersions) + { + if (docCultureVariationsToUpdate.TryGetValue(ev.Key, out var docVariations)) + { + //check if it needs updating + if (docVariations.Edited != ev.Value) + { + docVariations.Edited = ev.Value; + Database.Update(docVariations); + } + } + else + { + //the row doesn't exist but needs creating + //TODO: Does this ever happen?? Need to see if we can test this + } + } + + ////Generate SQL to lookup the current name vs the publish name for each language + //var nameSql = Sql() + // .Select("cv1", x => x.NodeId, x => Alias(x.Id, "currentVersion")) + // .AndSelect("cvcv1", x => x.LanguageId, x => Alias(x.Name, "currentName")) + // .AndSelect("cvcv2", x => Alias(x.Name, "publishedName")) + // .AndSelect("dv", x => Alias(x.Id, "publishedVersion")) + // .AndSelect("dcv", x => x.Id, x => x.Edited) + // .From("cvcv1") + // .InnerJoin("cv1") + // .On((left, right) => left.Id == right.VersionId, "cv1", "cvcv1") + // .InnerJoin("dcv") + // .On((left, right, other) => left.NodeId == right.NodeId && left.LanguageId == other.LanguageId, "dcv", "cv1", "cvcv1") + // .LeftJoin(nested => + // nested.InnerJoin("dv") + // .On((left, right) => left.Id == right.Id && right.Published, "cv2", "dv"), "cv2") + // .On((left, right) => left.NodeId == right.NodeId, "cv1", "cv2") + // .LeftJoin("cvcv2") + // .On((left, right, other) => left.VersionId == right.Id && left.LanguageId == other.LanguageId, "cvcv2", "cv2", "cvcv1") + // .Where(x => x.Current, "cv1") + // .OrderBy("cv1.nodeId, cvcv1.versionId, cvcv1.languageId"); + + //var names = Database.Fetch(nameSql); + + } + + private static bool IsPropertyValueChanged(PropertyValueVersionDto pubRow, PropertyValueVersionDto row) + { + return !pubRow.TextValue.IsNullOrWhiteSpace() && pubRow.TextValue != row.TextValue + || !pubRow.VarcharValue.IsNullOrWhiteSpace() && pubRow.VarcharValue != row.VarcharValue + || pubRow.DateValue.HasValue && pubRow.DateValue != row.DateValue + || pubRow.DecimalValue.HasValue && pubRow.DecimalValue != row.DecimalValue + || pubRow.IntValue.HasValue && pubRow.IntValue != row.IntValue; + } + + private class NameCompareDto + { + public int NodeId { get; set; } + public int CurrentVersion { get; set; } + public int LanguageId { get; set; } + public string CurrentName { get; set; } + public string PublishedName { get; set; } + public int? PublishedVersion { get; set; } + public int Id { get; set; } // the Id of the DocumentCultureVariationDto + public bool Edited { get; set; } + } + + private class PropertyValueVersionDto + { + public int VersionId { get; set; } + public int PropertyTypeId { get; set; } + public int? LanguageId { get; set; } + public string Segment { get; set; } + public int? IntValue { get; set; } + + private decimal? _decimalValue; + [Column("decimalValue")] + public decimal? DecimalValue + { + get => _decimalValue; + set => _decimalValue = value?.Normalize(); + } + + public DateTime? DateValue { get; set; } + public string VarcharValue { get; set; } + public string TextValue { get; set; } + + public int NodeId { get; set; } + public bool Current { get; set; } + public bool Published { get; set; } + + public byte Variations { get; set; } } private void DeletePropertyType(int contentTypeId, int propertyTypeId) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index 30a2927cc8..0dbfc61b8c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -386,7 +386,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.BulkInsertRecords(GetContentVariationDtos(entity, publishing)); // insert document variations - Database.BulkInsertRecords(GetDocumentVariationDtos(entity, publishing, editedCultures)); + Database.BulkInsertRecords(GetDocumentVariationDtos(entity, editedCultures)); } // refresh content @@ -571,7 +571,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.BulkInsertRecords(GetContentVariationDtos(entity, publishing)); // insert document variations - Database.BulkInsertRecords(GetDocumentVariationDtos(entity, publishing, editedCultures)); + Database.BulkInsertRecords(GetDocumentVariationDtos(entity, editedCultures)); } // refresh content @@ -1297,25 +1297,30 @@ namespace Umbraco.Core.Persistence.Repositories.Implement }; } - private IEnumerable GetDocumentVariationDtos(IContent content, bool publishing, HashSet editedCultures) + private IEnumerable GetDocumentVariationDtos(IContent content, HashSet editedCultures) { var allCultures = content.AvailableCultures.Union(content.PublishedCultures); // union = distinct foreach (var culture in allCultures) - yield return new DocumentCultureVariationDto + { + var dto = new DocumentCultureVariationDto { NodeId = content.Id, LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."), Culture = culture, Name = content.GetCultureName(culture) ?? content.GetPublishName(culture), - - // note: can't use IsCultureEdited at that point - hasn't been updated yet - see PersistUpdatedItem - Available = content.IsCultureAvailable(culture), - Published = content.IsCulturePublished(culture), - Edited = content.IsCultureAvailable(culture) && - (!content.IsCulturePublished(culture) || (editedCultures != null && editedCultures.Contains(culture))) + Published = content.IsCulturePublished(culture) }; + + // note: can't use IsCultureEdited at that point - hasn't been updated yet - see PersistUpdatedItem + + dto.Edited = content.IsCultureAvailable(culture) && + (!content.IsCulturePublished(culture) || (editedCultures != null && editedCultures.Contains(culture))); + + yield return dto; + } + } private class ContentVariation diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaTypeRepository.cs index 1abc75cf3a..a2c7cc3f44 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaTypeRepository.cs @@ -17,8 +17,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// internal class MediaTypeRepository : ContentTypeRepositoryBase, IMediaTypeRepository { - public MediaTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository) - : base(scopeAccessor, cache, logger, commonRepository) + public MediaTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository, ILanguageRepository languageRepository) + : base(scopeAccessor, cache, logger, commonRepository, languageRepository) { } protected override bool SupportsPublishing => MediaType.SupportsPublishingConst; diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs index d96854743e..b4d6033b9b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs @@ -18,8 +18,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// internal class MemberTypeRepository : ContentTypeRepositoryBase, IMemberTypeRepository { - public MemberTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository) - : base(scopeAccessor, cache, logger, commonRepository) + public MemberTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository, ILanguageRepository languageRepository) + : base(scopeAccessor, cache, logger, commonRepository, languageRepository) { } protected override bool SupportsPublishing => MemberType.SupportsPublishingConst; diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs index 53f150f140..f953b9cce6 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -29,10 +29,11 @@ namespace Umbraco.Tests.Persistence.Repositories private DocumentRepository CreateRepository(IScopeAccessor scopeAccessor, out ContentTypeRepository contentTypeRepository) { + var langRepository = new LanguageRepository(scopeAccessor, AppCaches.Disabled, Logger); var templateRepository = new TemplateRepository(scopeAccessor, AppCaches.Disabled, Logger, TestObjects.GetFileSystemsMock()); var tagRepository = new TagRepository(scopeAccessor, AppCaches.Disabled, Logger); var commonRepository = new ContentTypeCommonRepository(scopeAccessor, templateRepository, AppCaches.Disabled); - contentTypeRepository = new ContentTypeRepository(scopeAccessor, AppCaches.Disabled, Logger, commonRepository); + contentTypeRepository = new ContentTypeRepository(scopeAccessor, AppCaches.Disabled, Logger, commonRepository, langRepository); var languageRepository = new LanguageRepository(scopeAccessor, AppCaches.Disabled, Logger); var repository = new DocumentRepository(scopeAccessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); return repository; @@ -40,9 +41,10 @@ namespace Umbraco.Tests.Persistence.Repositories private ContentTypeRepository CreateRepository(IScopeAccessor scopeAccessor) { + var langRepository = new LanguageRepository(scopeAccessor, AppCaches.Disabled, Logger); var templateRepository = new TemplateRepository(scopeAccessor, AppCaches.Disabled, Logger, TestObjects.GetFileSystemsMock()); var commonRepository = new ContentTypeCommonRepository(scopeAccessor, templateRepository, AppCaches.Disabled); - var contentTypeRepository = new ContentTypeRepository(scopeAccessor, AppCaches.Disabled, Logger, commonRepository); + var contentTypeRepository = new ContentTypeRepository(scopeAccessor, AppCaches.Disabled, Logger, commonRepository, langRepository); return contentTypeRepository; } @@ -50,7 +52,8 @@ namespace Umbraco.Tests.Persistence.Repositories { var templateRepository = new TemplateRepository(scopeAccessor, AppCaches.Disabled, Logger, TestObjects.GetFileSystemsMock()); var commonRepository = new ContentTypeCommonRepository(scopeAccessor, templateRepository, AppCaches.Disabled); - var contentTypeRepository = new MediaTypeRepository(scopeAccessor, AppCaches.Disabled, Logger, commonRepository); + var langRepository = new LanguageRepository(scopeAccessor, AppCaches.Disabled, Logger); + var contentTypeRepository = new MediaTypeRepository(scopeAccessor, AppCaches.Disabled, Logger, commonRepository, langRepository); return contentTypeRepository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs index fd797662c0..4d62ec8301 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DocumentRepositoryTest.cs @@ -67,8 +67,8 @@ namespace Umbraco.Tests.Persistence.Repositories templateRepository = new TemplateRepository(scopeAccessor, appCaches, Logger, TestObjects.GetFileSystemsMock()); var tagRepository = new TagRepository(scopeAccessor, appCaches, Logger); var commonRepository = new ContentTypeCommonRepository(scopeAccessor, templateRepository, appCaches); - contentTypeRepository = new ContentTypeRepository(scopeAccessor, appCaches, Logger, commonRepository); var languageRepository = new LanguageRepository(scopeAccessor, appCaches, Logger); + contentTypeRepository = new ContentTypeRepository(scopeAccessor, appCaches, Logger, commonRepository, languageRepository); var repository = new DocumentRepository(scopeAccessor, appCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs index f00b2fd046..628f8d75a7 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs @@ -23,8 +23,8 @@ namespace Umbraco.Tests.Persistence.Repositories var templateRepository = new TemplateRepository(accessor, Core.Cache.AppCaches.Disabled, Logger, TestObjects.GetFileSystemsMock()); var tagRepository = new TagRepository(accessor, Core.Cache.AppCaches.Disabled, Logger); var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches); - contentTypeRepository = new ContentTypeRepository(accessor, Core.Cache.AppCaches.Disabled, Logger, commonRepository); languageRepository = new LanguageRepository(accessor, Core.Cache.AppCaches.Disabled, Logger); + contentTypeRepository = new ContentTypeRepository(accessor, Core.Cache.AppCaches.Disabled, Logger, commonRepository, languageRepository); documentRepository = new DocumentRepository(accessor, Core.Cache.AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); var domainRepository = new DomainRepository(accessor, Core.Cache.AppCaches.Disabled, Logger); return domainRepository; diff --git a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs index 1d9cf6d022..e2123df9e3 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs @@ -38,7 +38,8 @@ namespace Umbraco.Tests.Persistence.Repositories var templateRepository = new TemplateRepository(scopeAccessor, appCaches, Logger, TestObjects.GetFileSystemsMock()); var commonRepository = new ContentTypeCommonRepository(scopeAccessor, templateRepository, appCaches); - mediaTypeRepository = new MediaTypeRepository(scopeAccessor, appCaches, Logger, commonRepository); + var languageRepository = new LanguageRepository(scopeAccessor, appCaches, Logger); + mediaTypeRepository = new MediaTypeRepository(scopeAccessor, appCaches, Logger, commonRepository, languageRepository); var tagRepository = new TagRepository(scopeAccessor, appCaches, Logger); var repository = new MediaRepository(scopeAccessor, appCaches, Logger, mediaTypeRepository, tagRepository, Mock.Of()); return repository; diff --git a/src/Umbraco.Tests/Persistence/Repositories/MediaTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MediaTypeRepositoryTest.cs index f302d1d992..bb3286daed 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MediaTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MediaTypeRepositoryTest.cs @@ -23,7 +23,8 @@ namespace Umbraco.Tests.Persistence.Repositories var cacheHelper = AppCaches.Disabled; var templateRepository = new TemplateRepository((IScopeAccessor)provider, cacheHelper, Logger, TestObjects.GetFileSystemsMock()); var commonRepository = new ContentTypeCommonRepository((IScopeAccessor)provider, templateRepository, AppCaches); - return new MediaTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, commonRepository); + var languageRepository = new LanguageRepository((IScopeAccessor)provider, AppCaches, Logger); + return new MediaTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, commonRepository, languageRepository); } private EntityContainerRepository CreateContainerRepository(IScopeProvider provider) diff --git a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs index a5f7f08f22..17b16ad7ab 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs @@ -31,7 +31,8 @@ namespace Umbraco.Tests.Persistence.Repositories var accessor = (IScopeAccessor) provider; var templateRepository = Mock.Of(); var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches); - memberTypeRepository = new MemberTypeRepository(accessor, AppCaches.Disabled, Logger, commonRepository); + var languageRepository = new LanguageRepository(accessor, AppCaches.Disabled, Logger); + memberTypeRepository = new MemberTypeRepository(accessor, AppCaches.Disabled, Logger, commonRepository, languageRepository); memberGroupRepository = new MemberGroupRepository(accessor, AppCaches.Disabled, Logger); var tagRepo = new TagRepository(accessor, AppCaches.Disabled, Logger); var repository = new MemberRepository(accessor, AppCaches.Disabled, Logger, memberTypeRepository, memberGroupRepository, tagRepo, Mock.Of()); diff --git a/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs index 79e8e43804..4b9f3096ce 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs @@ -24,7 +24,8 @@ namespace Umbraco.Tests.Persistence.Repositories { var templateRepository = Mock.Of(); var commonRepository = new ContentTypeCommonRepository((IScopeAccessor)provider, templateRepository, AppCaches); - return new MemberTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Mock.Of(), commonRepository); + var languageRepository = new LanguageRepository((IScopeAccessor)provider, AppCaches.Disabled, Mock.Of()); + return new MemberTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Mock.Of(), commonRepository, languageRepository); } [Test] diff --git a/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs index 803eff25af..56041c24aa 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs @@ -308,8 +308,8 @@ namespace Umbraco.Tests.Persistence.Repositories var templateRepository = new TemplateRepository(accessor, AppCaches, Logger, TestObjects.GetFileSystemsMock()); var tagRepository = new TagRepository(accessor, AppCaches, Logger); var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches); - contentTypeRepository = new ContentTypeRepository(accessor, AppCaches, Logger, commonRepository); var languageRepository = new LanguageRepository(accessor, AppCaches, Logger); + contentTypeRepository = new ContentTypeRepository(accessor, AppCaches, Logger, commonRepository, languageRepository); var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs index b6cc4dc50d..e3de2c2892 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs @@ -956,8 +956,8 @@ namespace Umbraco.Tests.Persistence.Repositories var templateRepository = new TemplateRepository(accessor, AppCaches.Disabled, Logger, TestObjects.GetFileSystemsMock()); var tagRepository = new TagRepository(accessor, AppCaches.Disabled, Logger); var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches.Disabled); - contentTypeRepository = new ContentTypeRepository(accessor, AppCaches.Disabled, Logger, commonRepository); var languageRepository = new LanguageRepository(accessor, AppCaches.Disabled, Logger); + contentTypeRepository = new ContentTypeRepository(accessor, AppCaches.Disabled, Logger, commonRepository, languageRepository); var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); return repository; } @@ -968,7 +968,8 @@ namespace Umbraco.Tests.Persistence.Repositories var templateRepository = new TemplateRepository(accessor, AppCaches.Disabled, Logger, TestObjects.GetFileSystemsMock()); var tagRepository = new TagRepository(accessor, AppCaches.Disabled, Logger); var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches.Disabled); - mediaTypeRepository = new MediaTypeRepository(accessor, AppCaches.Disabled, Logger, commonRepository); + var languageRepository = new LanguageRepository(accessor, AppCaches.Disabled, Logger); + mediaTypeRepository = new MediaTypeRepository(accessor, AppCaches.Disabled, Logger, commonRepository, languageRepository); var repository = new MediaRepository(accessor, AppCaches.Disabled, Logger, mediaTypeRepository, tagRepository, Mock.Of()); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs index 13cbd463fb..b0f9a5335b 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs @@ -239,8 +239,8 @@ namespace Umbraco.Tests.Persistence.Repositories var tagRepository = new TagRepository((IScopeAccessor) ScopeProvider, AppCaches.Disabled, Logger); var commonRepository = new ContentTypeCommonRepository(ScopeProvider, templateRepository, AppCaches); - var contentTypeRepository = new ContentTypeRepository((IScopeAccessor) ScopeProvider, AppCaches.Disabled, Logger, commonRepository); var languageRepository = new LanguageRepository((IScopeAccessor) ScopeProvider, AppCaches.Disabled, Logger); + var contentTypeRepository = new ContentTypeRepository((IScopeAccessor) ScopeProvider, AppCaches.Disabled, Logger, commonRepository, languageRepository); var contentRepo = new DocumentRepository((IScopeAccessor) ScopeProvider, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage2", "Textpage"); diff --git a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs index b550091591..3e5919d7f3 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/UserRepositoryTest.cs @@ -26,7 +26,8 @@ namespace Umbraco.Tests.Persistence.Repositories var accessor = (IScopeAccessor) provider; var templateRepository = new TemplateRepository(accessor, AppCaches.Disabled, Logger, TestObjects.GetFileSystemsMock()); var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches); - mediaTypeRepository = new MediaTypeRepository(accessor, AppCaches, Mock.Of(), commonRepository); + var languageRepository = new LanguageRepository(accessor, AppCaches, Logger); + mediaTypeRepository = new MediaTypeRepository(accessor, AppCaches, Mock.Of(), commonRepository, languageRepository); var tagRepository = new TagRepository(accessor, AppCaches, Mock.Of()); var repository = new MediaRepository(accessor, AppCaches, Mock.Of(), mediaTypeRepository, tagRepository, Mock.Of()); return repository; @@ -44,8 +45,8 @@ namespace Umbraco.Tests.Persistence.Repositories templateRepository = new TemplateRepository(accessor, AppCaches, Logger, TestObjects.GetFileSystemsMock()); var tagRepository = new TagRepository(accessor, AppCaches, Logger); var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches); - contentTypeRepository = new ContentTypeRepository(accessor, AppCaches, Logger, commonRepository); var languageRepository = new LanguageRepository(accessor, AppCaches, Logger); + contentTypeRepository = new ContentTypeRepository(accessor, AppCaches, Logger, commonRepository, languageRepository); var repository = new DocumentRepository(accessor, AppCaches, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); return repository; } diff --git a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs index ed5e6073ac..ef80672baf 100644 --- a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs +++ b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs @@ -166,8 +166,8 @@ namespace Umbraco.Tests.Services var tRepository = new TemplateRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, TestObjects.GetFileSystemsMock()); var tagRepo = new TagRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger); var commonRepository = new ContentTypeCommonRepository((IScopeAccessor)provider, tRepository, AppCaches); - var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, commonRepository); var languageRepository = new LanguageRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger); + var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, commonRepository, languageRepository); var repository = new DocumentRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository); // Act @@ -200,8 +200,8 @@ namespace Umbraco.Tests.Services var tRepository = new TemplateRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, TestObjects.GetFileSystemsMock()); var tagRepo = new TagRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger); var commonRepository = new ContentTypeCommonRepository((IScopeAccessor)provider, tRepository, AppCaches); - var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, commonRepository); var languageRepository = new LanguageRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger); + var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, commonRepository, languageRepository); var repository = new DocumentRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository); // Act @@ -232,8 +232,8 @@ namespace Umbraco.Tests.Services var tRepository = new TemplateRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, TestObjects.GetFileSystemsMock()); var tagRepo = new TagRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger); var commonRepository = new ContentTypeCommonRepository((IScopeAccessor) provider, tRepository, AppCaches); - var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, commonRepository); - var languageRepository = new LanguageRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger); + var languageRepository = new LanguageRepository((IScopeAccessor)provider, AppCaches.Disabled, Logger); + var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, commonRepository, languageRepository); var repository = new DocumentRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository); // Act @@ -267,8 +267,8 @@ namespace Umbraco.Tests.Services var tRepository = new TemplateRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, TestObjects.GetFileSystemsMock()); var tagRepo = new TagRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger); var commonRepository = new ContentTypeCommonRepository((IScopeAccessor)provider, tRepository, AppCaches); - var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, commonRepository); - var languageRepository = new LanguageRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger); + var languageRepository = new LanguageRepository((IScopeAccessor)provider, AppCaches.Disabled, Logger); + var ctRepository = new ContentTypeRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, commonRepository, languageRepository); var repository = new DocumentRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, ctRepository, tRepository, tagRepo, languageRepository); // Act diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 222f40aeed..7acfd994d3 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -3008,8 +3008,8 @@ namespace Umbraco.Tests.Services var templateRepository = new TemplateRepository(accessor, AppCaches.Disabled, Logger, TestObjects.GetFileSystemsMock()); var tagRepository = new TagRepository(accessor, AppCaches.Disabled, Logger); var commonRepository = new ContentTypeCommonRepository(accessor, templateRepository, AppCaches); - contentTypeRepository = new ContentTypeRepository(accessor, AppCaches.Disabled, Logger, commonRepository); var languageRepository = new LanguageRepository(accessor, AppCaches.Disabled, Logger); + contentTypeRepository = new ContentTypeRepository(accessor, AppCaches.Disabled, Logger, commonRepository, languageRepository); var repository = new DocumentRepository(accessor, AppCaches.Disabled, Logger, contentTypeRepository, templateRepository, tagRepository, languageRepository); return repository; } diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs index f5fa4e8795..53a39ffe40 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs @@ -810,12 +810,18 @@ namespace Umbraco.Tests.Services var document = (IContent)new Content("document", -1, contentType); document.SetCultureName("doc1en", "en"); document.SetCultureName("doc1fr", "fr"); - document.SetValue("value1", "v1en", "en"); - document.SetValue("value1", "v1fr", "fr"); - ServiceContext.ContentService.Save(document); + document.SetValue("value1", "v1en-init", "en"); + document.SetValue("value1", "v1fr-init", "fr"); + ServiceContext.ContentService.SaveAndPublish(document); //all values are published which means the document is not 'edited' - //at this stage there will be 4 property values stored in the DB for "value1" against "en" and "fr" for both edited/published versions, - //for the published values, these will be null + document = ServiceContext.ContentService.GetById(document.Id); + Assert.IsFalse(document.IsCultureEdited("en")); + Assert.IsFalse(document.IsCultureEdited("fr")); + Assert.IsFalse(document.Edited); + + document.SetValue("value1", "v1en", "en"); //change the property culture value, so now this culture will be edited + document.SetValue("value1", "v1fr", "fr"); //change the property culture value, so now this culture will be edited + ServiceContext.ContentService.Save(document); document = ServiceContext.ContentService.GetById(document.Id); Assert.AreEqual("doc1en", document.Name); @@ -829,18 +835,17 @@ namespace Umbraco.Tests.Services // switch property type to Nothing contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Nothing; - ServiceContext.ContentTypeService.Save(contentType); + ServiceContext.ContentTypeService.Save(contentType); //This is going to have to re-normalize the "Edited" flag document = ServiceContext.ContentService.GetById(document.Id); + Assert.IsTrue(document.IsCultureEdited("en")); //This will remain true because there is now a pending change for the invariant property data which is flagged under the default lang + Assert.IsFalse(document.IsCultureEdited("fr")); //This will be false because nothing has changed for this culture and the property no longer reflects variant changes Assert.IsTrue(document.Edited); - // publish the document - document.SetValue("value1", "v1inv"); //update the invariant value + //update the invariant value and publish + document.SetValue("value1", "v1inv"); ServiceContext.ContentService.SaveAndPublish(document); - //at this stage there will be 6 property values stored in the DB for "value1" against "en", "fr" and null for both edited/published versions, - //for the published values for the cultures, these will still be null but the invariant edited/published values will both be "v1inv". - document = ServiceContext.ContentService.GetById(document.Id); Assert.AreEqual("doc1en", document.Name); Assert.AreEqual("doc1en", document.GetCultureName("en")); @@ -848,8 +853,8 @@ namespace Umbraco.Tests.Services Assert.IsNull(document.GetValue("value1", "en")); //The values are there but the business logic returns null Assert.IsNull(document.GetValue("value1", "fr")); //The values are there but the business logic returns null Assert.AreEqual("v1inv", document.GetValue("value1")); - Assert.IsFalse(document.IsCultureEdited("en")); //This returns false, the culture shouldn't be marked as edited because the invariant property value is published - Assert.IsFalse(document.IsCultureEdited("fr")); //This returns false, the culture shouldn't be marked as edited because the invariant property value is published + Assert.IsFalse(document.IsCultureEdited("en")); //This returns false, everything is published - however in the DB this is not the case for the "en" version of the property + Assert.IsFalse(document.IsCultureEdited("fr")); //This returns false, everything is published Assert.IsFalse(document.Edited); // switch property back to Culture From ff952a6df169bbbe11d8dad29c653c7433fcbf05 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 1 Aug 2019 15:54:44 +1000 Subject: [PATCH 0070/1001] Passes test! Now to add more tests/assertions and then the logic to renormalize based on name changes. --- .../Persistence/Factories/PropertyFactory.cs | 23 ++-- .../Implement/ContentTypeRepositoryBase.cs | 112 +++++++++++------- .../Implement/DocumentRepository.cs | 2 +- .../ContentTypeServiceVariantsTests.cs | 21 ++-- 4 files changed, 95 insertions(+), 63 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs index 4e7dc1e982..33dabe1b24 100644 --- a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs @@ -104,10 +104,9 @@ namespace Umbraco.Core.Persistence.Factories /// The properties to map /// /// out parameter indicating that one or more properties have been edited - /// out parameter containing a collection of edited cultures when the contentVariation varies by culture - /// - /// out parameter containing a collection of edited cultures that are currently persisted in the database which is used to maintain the edited state - /// of each culture value (in cases where variance is being switched) + /// + /// Out parameter containing a collection of edited cultures when the contentVariation varies by culture. + /// The value of this will be used to populate the edited cultures in the umbracoDocumentCultureVariation table. /// /// public static IEnumerable BuildDtos(ContentVariation contentVariation, int currentVersionId, int publishedVersionId, IEnumerable properties, @@ -146,14 +145,16 @@ namespace Umbraco.Core.Persistence.Factories if (propertyValue.EditedValue != null) propertyDataDtos.Add(BuildDto(currentVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.EditedValue)); - //// property.Values will contain ALL of it's values, both variant and invariant which will be populated if the administrator has previously - //// changed the property type to be variant vs invariant. - //// We need to check for this scenario here because otherwise the editedCultures and edited flags - //// will end up incorrectly so here we need to only process edited cultures based on the - //// current value type and how the property varies. + // property.Values will contain ALL of it's values, both variant and invariant which will be populated if the + // administrator has previously changed the property type to be variant vs invariant. + // We need to check for this scenario here because otherwise the editedCultures and edited flags + // will end up incorrectly set in the umbracoDocumentCultureVariation table so here we need to + // only process edited cultures based on the current value type and how the property varies. + // The above logic will still persist the currently saved property value for each culture in case the admin + // decides to swap the property's variance again, in which case the edited flag will be recalculated. - //if (property.PropertyType.VariesByCulture() && isInvariantValue) continue; - //if (!property.PropertyType.VariesByCulture() && isCultureValue) continue; + if (property.PropertyType.VariesByCulture() && isInvariantValue || !property.PropertyType.VariesByCulture() && isCultureValue) + continue; // use explicit equals here, else object comparison fails at comparing eg strings var sameValues = propertyValue.PublishedValue == null ? propertyValue.EditedValue == null : propertyValue.PublishedValue.Equals(propertyValue.EditedValue); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 756435692a..b29565a35e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -648,14 +648,14 @@ AND umbracoNode.id <> @id", case ContentVariation.Culture: CopyPropertyData(null, defaultLanguageId, propertyTypeIds, impactedL); CopyTagData(null, defaultLanguageId, propertyTypeIds, impactedL); - RenormalizeDocumentCultureVariations(propertyTypeIds, impactedL); + RenormalizeDocumentEditedFlags(propertyTypeIds, impactedL); //TODO: Here we need to normalize the umbracoDocumentCultureVariation table for it's edited flags which are calculated based //on changed property or name values break; case ContentVariation.Nothing: CopyPropertyData(defaultLanguageId, null, propertyTypeIds, impactedL); CopyTagData(defaultLanguageId, null, propertyTypeIds, impactedL); - RenormalizeDocumentCultureVariations(propertyTypeIds, impactedL); + RenormalizeDocumentEditedFlags(propertyTypeIds, impactedL); //TODO: Here we need to normalize the umbracoDocumentCultureVariation table for it's edited flags which are calculated based //on changed property or name values break; @@ -1024,19 +1024,17 @@ AND umbracoNode.id <> @id", } /// - /// Re-normalizes the edited value in the umbracoDocumentCultureVariation table when property variations are changed + /// Re-normalizes the edited value in the umbracoDocumentCultureVariation and umbracoDocument table when variations are changed /// /// /// /// /// If this is not done, then in some cases the "edited" value for a particular culture for a document will remain true when it should be false /// if the property was changed to invariant. In order to do this we need to recalculate this value based on the values stored for each - /// property, culture and current/published version. The end result is to update the edited value in the umbracoDocumentCultureVariation table so we - /// make sure to join this table with the lookups so that only relevant data is returned. + /// property, culture and current/published version. /// - private void RenormalizeDocumentCultureVariations(IReadOnlyCollection propertyTypeIds, IReadOnlyCollection contentTypeIds = null) + private void RenormalizeDocumentEditedFlags(IReadOnlyCollection propertyTypeIds, IReadOnlyCollection contentTypeIds = null) { - var defaultLang = LanguageRepository.GetDefaultId(); //This will build up a query to get the property values of both the current and the published version so that we can check @@ -1073,14 +1071,16 @@ AND umbracoNode.id <> @id", .OrderBy(x => x.NodeId) .OrderBy(x => x.PropertyTypeId, x => x.LanguageId, x => x.VersionId); - //keep track of this node/lang to mark or unmark as edited - var editedVersions = new Dictionary<(int nodeId, int? langId), bool>(); - + //keep track of this node/lang to mark or unmark a culture as edited + var editedLanguageVersions = new Dictionary<(int nodeId, int? langId), bool>(); + //keep track of which node to mark or unmark as edited + var editedDocument = new Dictionary(); var nodeId = -1; var propertyTypeId = -1; + PropertyValueVersionDto pubRow = null; - //This is a QUERY we are not fetching this all into memory so we cannot make any changes during this iteration, we are just collecting data. + //This is a reader (Query), we are not fetching this all into memory so we cannot make any changes during this iteration, we are just collecting data. //Published data will always come before Current data based on the version id sort. //There will only be one published row (max) and one current row per property. foreach (var row in Database.Query(propertySql)) @@ -1105,19 +1105,24 @@ AND umbracoNode.id <> @id", || propVariations.VariesByCulture() && !row.LanguageId.HasValue) { //Flag this as not edited for this node/lang if the key doesn't exist - if (!editedVersions.TryGetValue((row.NodeId, row.LanguageId), out _)) - editedVersions.Add((row.NodeId, row.LanguageId), false); + if (!editedLanguageVersions.TryGetValue((row.NodeId, row.LanguageId), out _)) + editedLanguageVersions.Add((row.NodeId, row.LanguageId), false); + + //mark as false if the item doesn't exist, else coerce to true + editedDocument[row.NodeId] = editedDocument.TryGetValue(row.NodeId, out var edited) ? (edited |= false) : false; } else if (pubRow == null) { //this would mean that that this property is 'edited' since there is no published version - editedVersions.Add((row.NodeId, row.LanguageId), true); + editedLanguageVersions.Add((row.NodeId, row.LanguageId), true); + editedDocument[row.NodeId] = true; } //compare the property values, if they differ from versions then flag the current version as edited else if (IsPropertyValueChanged(pubRow, row)) { //Here we would check if the property is invariant, in which case the edited language should be indicated by the default lang - editedVersions[(row.NodeId, !propVariations.VariesByCulture() ? defaultLang : row.LanguageId)] = true; + editedLanguageVersions[(row.NodeId, !propVariations.VariesByCulture() ? defaultLang : row.LanguageId)] = true; + editedDocument[row.NodeId] = true; } //reset @@ -1125,32 +1130,6 @@ AND umbracoNode.id <> @id", } } - //lookup all matching rows in umbracoDocumentCultureVariation - var docCultureVariationsToUpdate = Database.Fetch( - Sql().Select().From() - .WhereIn(x => x.LanguageId, editedVersions.Keys.Select(x => x.langId).ToList()) - .WhereIn(x => x.NodeId, editedVersions.Keys.Select(x => x.nodeId))) - //convert to dictionary with the same key type - .ToDictionary(x => (x.NodeId, (int?)x.LanguageId), x => x); - - foreach (var ev in editedVersions) - { - if (docCultureVariationsToUpdate.TryGetValue(ev.Key, out var docVariations)) - { - //check if it needs updating - if (docVariations.Edited != ev.Value) - { - docVariations.Edited = ev.Value; - Database.Update(docVariations); - } - } - else - { - //the row doesn't exist but needs creating - //TODO: Does this ever happen?? Need to see if we can test this - } - } - ////Generate SQL to lookup the current name vs the publish name for each language //var nameSql = Sql() // .Select("cv1", x => x.NodeId, x => Alias(x.Id, "currentVersion")) @@ -1172,8 +1151,57 @@ AND umbracoNode.id <> @id", // .Where(x => x.Current, "cv1") // .OrderBy("cv1.nodeId, cvcv1.versionId, cvcv1.languageId"); - //var names = Database.Fetch(nameSql); + ////This is a reader (Query), we are not fetching this all into memory so we cannot make any changes during this iteration, we are just collecting data. + //foreach (var name in Database.Query(nameSql)) + //{ + // if (name.CurrentName != name.PublishedName) + // { + // } + //} + + //lookup all matching rows in umbracoDocumentCultureVariation + var docCultureVariationsToUpdate = editedLanguageVersions.InGroupsOf(2000) + .SelectMany(_ => Database.Fetch( + Sql().Select().From() + .WhereIn(x => x.LanguageId, editedLanguageVersions.Keys.Select(x => x.langId).ToList()) + .WhereIn(x => x.NodeId, editedLanguageVersions.Keys.Select(x => x.nodeId)))) + //convert to dictionary with the same key type + .ToDictionary(x => (x.NodeId, (int?)x.LanguageId), x => x); + + var toUpdate = new List(); + foreach (var ev in editedLanguageVersions) + { + if (docCultureVariationsToUpdate.TryGetValue(ev.Key, out var docVariations)) + { + //check if it needs updating + if (docVariations.Edited != ev.Value) + { + docVariations.Edited = ev.Value; + toUpdate.Add(docVariations); + } + } + else + { + //the row doesn't exist but needs creating + //TODO: Does this ever happen?? Need to see if we can test this + throw new PanicException($"The existing DocumentCultureVariationDto was not found for node {ev.Key.nodeId} and language {ev.Key.langId}"); + } + } + + //Now bulk update the table DocumentCultureVariationDto, once for edited = true, another for edited = false + foreach (var editValue in toUpdate.GroupBy(x => x.Edited)) + { + Database.Execute(Sql().Update(u => u.Set(x => x.Edited, editValue.Key)) + .WhereIn(x => x.Id, editValue.Select(x => x.Id))); + } + + //Now bulk update the umbracoDocument table + foreach(var editValue in editedDocument.GroupBy(x => x.Value)) + { + Database.Execute(Sql().Update(u => u.Set(x => x.Edited, editValue.Key)) + .WhereIn(x => x.NodeId, editValue.Select(x => x.Key))); + } } private static bool IsPropertyValueChanged(PropertyValueVersionDto pubRow, PropertyValueVersionDto row) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index 0dbfc61b8c..c18921b59e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -511,7 +511,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement entity.VersionId = documentVersionDto.Id = contentVersionDto.Id; // get the new id documentVersionDto.Published = false; // non-published version - Database.Insert(documentVersionDto); + Database.Insert(documentVersionDto); } // replace the property data (rather than updating) diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs index 53a39ffe40..2fed37c389 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs @@ -828,12 +828,14 @@ namespace Umbraco.Tests.Services Assert.AreEqual("doc1en", document.GetCultureName("en")); Assert.AreEqual("doc1fr", document.GetCultureName("fr")); Assert.AreEqual("v1en", document.GetValue("value1", "en")); + Assert.AreEqual("v1en-init", document.GetValue("value1", "en", published: true)); Assert.AreEqual("v1fr", document.GetValue("value1", "fr")); + Assert.AreEqual("v1fr-init", document.GetValue("value1", "fr", published: true)); Assert.IsTrue(document.IsCultureEdited("en")); //This will be true because the edited value isn't the same as the published value Assert.IsTrue(document.IsCultureEdited("fr")); //This will be true because the edited value isn't the same as the published value Assert.IsTrue(document.Edited); - // switch property type to Nothing + // switch property type to Invariant contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Nothing; ServiceContext.ContentTypeService.Save(contentType); //This is going to have to re-normalize the "Edited" flag @@ -852,9 +854,12 @@ namespace Umbraco.Tests.Services Assert.AreEqual("doc1fr", document.GetCultureName("fr")); Assert.IsNull(document.GetValue("value1", "en")); //The values are there but the business logic returns null Assert.IsNull(document.GetValue("value1", "fr")); //The values are there but the business logic returns null + Assert.IsNull(document.GetValue("value1", "en", published: true)); //The values are there but the business logic returns null + Assert.IsNull(document.GetValue("value1", "fr", published: true)); //The values are there but the business logic returns null Assert.AreEqual("v1inv", document.GetValue("value1")); - Assert.IsFalse(document.IsCultureEdited("en")); //This returns false, everything is published - however in the DB this is not the case for the "en" version of the property - Assert.IsFalse(document.IsCultureEdited("fr")); //This returns false, everything is published + Assert.AreEqual("v1inv", document.GetValue("value1", published: true)); + Assert.IsFalse(document.IsCultureEdited("en")); //This returns false, everything is published + Assert.IsFalse(document.IsCultureEdited("fr")); //This will be false because nothing has changed for this culture and the property no longer reflects variant changes Assert.IsFalse(document.Edited); // switch property back to Culture @@ -863,14 +868,12 @@ namespace Umbraco.Tests.Services document = ServiceContext.ContentService.GetById(document.Id); Assert.AreEqual("v1inv", document.GetValue("value1", "en")); //The invariant property value gets copied over to the default language - Assert.AreEqual("v1fr", document.GetValue("value1", "fr")); + Assert.AreEqual("v1inv", document.GetValue("value1", "en", published: true)); + Assert.AreEqual("v1fr", document.GetValue("value1", "fr")); //values are still retained + Assert.AreEqual("v1fr-init", document.GetValue("value1", "fr", published: true)); //values are still retained Assert.IsFalse(document.IsCultureEdited("en")); //The invariant published AND edited values are copied over to the default language - //TODO: This fails - for some reason in the DB, the DocumentCultureVariationDto Edited flag is set to false for the FR culture - // I'm assuming this is somehow done during the ContentTypeService.Save method when changing variation. It should not be false but instead - // true because the values for it's edited vs published version are different. Perhaps it's false because that is the default boolean value and - // this row gets deleted somewhere along the way? Assert.IsTrue(document.IsCultureEdited("fr")); //The previously existing french values are there and there is no published value - Assert.IsTrue(document.Edited); + Assert.IsTrue(document.Edited); //Will be flagged edited again because the french culture had pending changes // publish again document.SetValue("value1", "v1en2", "en"); //update the value now that it's variant again From 10958158781737e970ceca2d6a845a8d9110333e Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 1 Aug 2019 16:09:52 +1000 Subject: [PATCH 0071/1001] removes notes --- src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs index 2fed37c389..0c54cf5975 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs @@ -880,9 +880,6 @@ namespace Umbraco.Tests.Services document.SetValue("value1", "v1fr2", "fr"); //update the value now that it's variant again ServiceContext.ContentService.SaveAndPublish(document); - //at this stage there will be 4 property values stored in the DB for "value1" against "en", "fr" for both edited/published versions, - //with the change back to Culture, it deletes the invariant property values. - document = ServiceContext.ContentService.GetById(document.Id); Assert.AreEqual("doc1en", document.Name); Assert.AreEqual("doc1en", document.GetCultureName("en")); From 9538981730d71698e90a00aba92fea700b587eef Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 1 Aug 2019 18:49:05 +1000 Subject: [PATCH 0072/1001] Adds more assertions and tests and validates variations --- src/Umbraco.Core/Models/IContentTypeBase.cs | 2 +- .../Implement/ContentTypeRepositoryBase.cs | 68 ++-- .../Implement/DocumentRepository.cs | 10 +- .../ContentTypeServiceVariantsTests.cs | 290 ++++++------------ 4 files changed, 133 insertions(+), 237 deletions(-) diff --git a/src/Umbraco.Core/Models/IContentTypeBase.cs b/src/Umbraco.Core/Models/IContentTypeBase.cs index 5f1fe6ed49..ed87c5f320 100644 --- a/src/Umbraco.Core/Models/IContentTypeBase.cs +++ b/src/Umbraco.Core/Models/IContentTypeBase.cs @@ -101,7 +101,7 @@ namespace Umbraco.Core.Models PropertyGroupCollection PropertyGroups { get; set; } /// - /// Gets all local property types belonging to a group, across all local property groups. + /// Gets all local property types all local property groups or ungrouped. /// IEnumerable PropertyTypes { get; } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index b29565a35e..8f88c71bf0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -100,6 +100,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected void PersistNewBaseContentType(IContentTypeComposition entity) { + ValidateVariations(entity); + var dto = ContentTypeFactory.BuildContentTypeDto(entity); //Cannot add a duplicate content type @@ -165,11 +167,11 @@ AND umbracoNode.nodeObjectType = @objectType", foreach (var allowedContentType in entity.AllowedContentTypes) { Database.Insert(new ContentTypeAllowedContentTypeDto - { - Id = entity.Id, - AllowedId = allowedContentType.Id.Value, - SortOrder = allowedContentType.SortOrder - }); + { + Id = entity.Id, + AllowedId = allowedContentType.Id.Value, + SortOrder = allowedContentType.SortOrder + }); } @@ -216,6 +218,8 @@ AND umbracoNode.nodeObjectType = @objectType", protected void PersistUpdatedBaseContentType(IContentTypeComposition entity) { + ValidateVariations(entity); + var dto = ContentTypeFactory.BuildContentTypeDto(entity); // ensure the alias is not used already @@ -372,7 +376,7 @@ AND umbracoNode.id <> @id", foreach (var propertyGroup in entity.PropertyGroups) { // insert or update group - var groupDto = PropertyGroupFactory.BuildGroupDto(propertyGroup,entity.Id); + var groupDto = PropertyGroupFactory.BuildGroupDto(propertyGroup, entity.Id); var groupId = propertyGroup.HasIdentity ? Database.Update(groupDto) : Convert.ToInt32(Database.Insert(groupDto)); @@ -390,7 +394,7 @@ AND umbracoNode.id <> @id", //check if the content type variation has been changed var contentTypeVariationDirty = entity.IsPropertyDirty("Variations"); - var oldContentTypeVariation = (ContentVariation) dtoPk.Variations; + var oldContentTypeVariation = (ContentVariation)dtoPk.Variations; var newContentTypeVariation = entity.Variations; var contentTypeVariationChanging = contentTypeVariationDirty && oldContentTypeVariation != newContentTypeVariation; if (contentTypeVariationChanging) @@ -451,7 +455,7 @@ AND umbracoNode.id <> @id", // via composition, with their original variations (ie not filtered by this // content type variations - we need this true value to make decisions. - foreach (var propertyType in ((ContentTypeCompositionBase) entity).RawComposedPropertyTypes) + foreach (var propertyType in ((ContentTypeCompositionBase)entity).RawComposedPropertyTypes) { if (propertyType.VariesBySegment() || newContentTypeVariation.VariesBySegment()) throw new NotSupportedException(); // TODO: support this @@ -520,6 +524,19 @@ AND umbracoNode.id <> @id", CommonRepository.ClearCache(); // always } + /// + /// Ensures that no property types are flagged for a variance that is not supported by the content type itself + /// + /// + private void ValidateVariations(IContentTypeComposition entity) + { + //if the entity does not vary at all, then the property cannot have a variance value greater than it + if (entity.Variations == ContentVariation.Nothing) + foreach (var prop in entity.PropertyTypes) + if (prop.Variations > entity.Variations) + throw new InvalidOperationException($"The property {prop.Alias} cannot have variations of {prop.Variations} with the content type variations of {entity.Variations}"); + } + private IEnumerable GetImpactedContentTypes(IContentTypeComposition contentType, IEnumerable all) { var impact = new List(); @@ -527,12 +544,12 @@ AND umbracoNode.id <> @id", var tree = new Dictionary>(); foreach (var x in all) - foreach (var y in x.ContentTypeComposition) - { - if (!tree.TryGetValue(y.Id, out var list)) - list = tree[y.Id] = new List(); - list.Add(x); - } + foreach (var y in x.ContentTypeComposition) + { + if (!tree.TryGetValue(y.Id, out var list)) + list = tree[y.Id] = new List(); + list.Add(x); + } var nset = new List(); do @@ -574,7 +591,7 @@ AND umbracoNode.id <> @id", // new property type, ignore if (!oldVariations.TryGetValue(propertyType.Id, out var oldVariationB)) continue; - var oldVariation = (ContentVariation) oldVariationB; // NPoco cannot fetch directly + var oldVariation = (ContentVariation)oldVariationB; // NPoco cannot fetch directly // only those property types that *actually* changed var newVariation = propertyType.Variations; @@ -638,7 +655,7 @@ AND umbracoNode.id <> @id", var impactedL = impacted.Select(x => x.Id).ToList(); //Group by the "To" variation so we can bulk update in the correct batches - foreach(var grouping in propertyTypeChanges.GroupBy(x => x.Value.ToVariation)) + foreach (var grouping in propertyTypeChanges.GroupBy(x => x.Value.ToVariation)) { var propertyTypeIds = grouping.Select(x => x.Key).ToList(); var toVariation = grouping.Key; @@ -1037,8 +1054,8 @@ AND umbracoNode.id <> @id", { var defaultLang = LanguageRepository.GetDefaultId(); - //This will build up a query to get the property values of both the current and the published version so that we can check - //based on the current variance of each item to see if it's 'edited' value should be true/false. + //This will build up a query to get the property values of both the current and the published version so that we can check + //based on the current variance of each item to see if it's 'edited' value should be true/false. var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); if (whereInArgsCount > 2000) @@ -1077,7 +1094,7 @@ AND umbracoNode.id <> @id", var editedDocument = new Dictionary(); var nodeId = -1; var propertyTypeId = -1; - + PropertyValueVersionDto pubRow = null; //This is a reader (Query), we are not fetching this all into memory so we cannot make any changes during this iteration, we are just collecting data. @@ -1087,7 +1104,7 @@ AND umbracoNode.id <> @id", { //make sure to reset on each node/property change if (nodeId != row.NodeId || propertyTypeId != row.PropertyTypeId) - { + { nodeId = row.NodeId; propertyTypeId = row.PropertyTypeId; pubRow = null; @@ -1114,7 +1131,7 @@ AND umbracoNode.id <> @id", else if (pubRow == null) { //this would mean that that this property is 'edited' since there is no published version - editedLanguageVersions.Add((row.NodeId, row.LanguageId), true); + editedLanguageVersions[(row.NodeId, row.LanguageId)] = true; editedDocument[row.NodeId] = true; } //compare the property values, if they differ from versions then flag the current version as edited @@ -1181,10 +1198,9 @@ AND umbracoNode.id <> @id", toUpdate.Add(docVariations); } } - else + else if (ev.Key.langId.HasValue) { - //the row doesn't exist but needs creating - //TODO: Does this ever happen?? Need to see if we can test this + //This should never happen! If a property culture is flagged as edited then the culture must exist at the document level throw new PanicException($"The existing DocumentCultureVariationDto was not found for node {ev.Key.nodeId} and language {ev.Key.langId}"); } } @@ -1197,7 +1213,7 @@ AND umbracoNode.id <> @id", } //Now bulk update the umbracoDocument table - foreach(var editValue in editedDocument.GroupBy(x => x.Value)) + foreach (var editValue in editedDocument.GroupBy(x => x.Value)) { Database.Execute(Sql().Update(u => u.Set(x => x.Edited, editValue.Key)) .WhereIn(x => x.NodeId, editValue.Select(x => x.Key))); @@ -1226,7 +1242,7 @@ AND umbracoNode.id <> @id", } private class PropertyValueVersionDto - { + { public int VersionId { get; set; } public int PropertyTypeId { get; set; } public int? LanguageId { get; set; } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index c18921b59e..344557d815 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -1310,14 +1310,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Name = content.GetCultureName(culture) ?? content.GetPublishName(culture), Available = content.IsCultureAvailable(culture), - Published = content.IsCulturePublished(culture) + Published = content.IsCulturePublished(culture), + // note: can't use IsCultureEdited at that point - hasn't been updated yet - see PersistUpdatedItem + Edited = content.IsCultureAvailable(culture) && + (!content.IsCulturePublished(culture) || (editedCultures != null && editedCultures.Contains(culture))) }; - // note: can't use IsCultureEdited at that point - hasn't been updated yet - see PersistUpdatedItem - - dto.Edited = content.IsCultureAvailable(culture) && - (!content.IsCulturePublished(culture) || (editedCultures != null && editedCultures.Contains(culture))); - yield return dto; } diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs index 0c54cf5975..4b8262d4a5 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs @@ -110,7 +110,6 @@ namespace Umbraco.Tests.Services [Test] public void Change_Content_Type_Variation_Clears_Redirects() { - //create content type with a property type that varies by culture var contentType = MockedContentTypes.CreateBasicContentType(); contentType.Variations = ContentVariation.Nothing; var contentCollection = new PropertyTypeCollection(true); @@ -154,8 +153,7 @@ namespace Umbraco.Tests.Services [Test] public void Change_Content_Type_From_Invariant_Variant() - { - //create content type with a property type that varies by culture + { var contentType = MockedContentTypes.CreateBasicContentType(); contentType.Variations = ContentVariation.Nothing; var contentCollection = new PropertyTypeCollection(true); @@ -205,7 +203,6 @@ namespace Umbraco.Tests.Services [Test] public void Change_Content_Type_From_Variant_Invariant() { - //create content type with a property type that varies by culture var contentType = MockedContentTypes.CreateBasicContentType(); contentType.Variations = ContentVariation.Culture; var contentCollection = new PropertyTypeCollection(true); @@ -264,39 +261,49 @@ namespace Umbraco.Tests.Services } [Test] - public void Change_Property_Type_From_Invariant_Variant() + public void Change_Property_Type_From_To_Variant_On_Invariant_Content_Type() { - //create content type with a property type that varies by culture var contentType = MockedContentTypes.CreateBasicContentType(); contentType.Variations = ContentVariation.Nothing; - var contentCollection = new PropertyTypeCollection(true); - contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) - { - Alias = "title", - Name = "Title", - Description = "", - Mandatory = false, - SortOrder = 1, - DataTypeId = -88, - Variations = ContentVariation.Nothing - }); - contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); + var properties = CreatePropertyCollection(("title", ContentVariation.Nothing)); + contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); + ServiceContext.ContentTypeService.Save(contentType); + + //change the property type to be variant + contentType.PropertyTypes.First().Variations = ContentVariation.Culture; + + //Cannot change a property type to be variant if the content type itself is not variant + Assert.Throws(() => ServiceContext.ContentTypeService.Save(contentType)); + } + + [Test] + public void Change_Property_Type_From_Invariant_Variant() + { + var contentType = MockedContentTypes.CreateBasicContentType(); + contentType.Variations = ContentVariation.Culture; + var properties = CreatePropertyCollection(("title", ContentVariation.Nothing)); + contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); ServiceContext.ContentTypeService.Save(contentType); //create some content of this content type IContent doc = MockedContent.CreateBasicContent(contentType); - doc.Name = "Home"; + doc.SetCultureName("Home", "en-US"); doc.SetValue("title", "hello world"); ServiceContext.ContentService.Save(doc); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get Assert.AreEqual("hello world", doc.GetValue("title")); - + Assert.IsTrue(doc.IsCultureEdited("en-US")); //invariant prop changes show up on default lang + Assert.IsTrue(doc.Edited); + //change the property type to be variant contentType.PropertyTypes.First().Variations = ContentVariation.Culture; ServiceContext.ContentTypeService.Save(contentType); doc = ServiceContext.ContentService.GetById(doc.Id); //re-get Assert.AreEqual("hello world", doc.GetValue("title", "en-US")); + Assert.IsTrue(doc.IsCultureEdited("en-US")); + Assert.IsTrue(doc.Edited); //change back property type to be invariant contentType.PropertyTypes.First().Variations = ContentVariation.Nothing; @@ -304,6 +311,8 @@ namespace Umbraco.Tests.Services doc = ServiceContext.ContentService.GetById(doc.Id); //re-get Assert.AreEqual("hello world", doc.GetValue("title")); + Assert.IsTrue(doc.IsCultureEdited("en-US")); //invariant prop changes show up on default lang + Assert.IsTrue(doc.Edited); } [Test] @@ -470,28 +479,11 @@ namespace Umbraco.Tests.Services CreateFrenchAndEnglishLangs(); - var contentType = new ContentType(-1) - { - Alias = "contentType", - Name = "contentType", - Variations = ContentVariation.Culture - }; + var contentType = CreateContentType(ContentVariation.Culture); - var properties = new PropertyTypeCollection(true) - { - new PropertyType("value1", ValueStorageType.Ntext) - { - Alias = "value1", - DataTypeId = -88, - Variations = ContentVariation.Culture - }, - new PropertyType("value2", ValueStorageType.Ntext) - { - Alias = "value2", - DataTypeId = -88, - Variations = ContentVariation.Nothing - } - }; + var properties = CreatePropertyCollection( + ("value1", ContentVariation.Culture), + ("value2", ContentVariation.Nothing)); contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); ServiceContext.ContentTypeService.Save(contentType); @@ -579,28 +571,11 @@ namespace Umbraco.Tests.Services var languageFr = new Language("fr"); ServiceContext.LocalizationService.Save(languageFr); - var contentType = new ContentType(-1) - { - Alias = "contentType", - Name = "contentType", - Variations = ContentVariation.Nothing - }; + var contentType = CreateContentType(ContentVariation.Nothing); - var properties = new PropertyTypeCollection(true) - { - new PropertyType("value1", ValueStorageType.Ntext) - { - Alias = "value1", - DataTypeId = -88, - Variations = ContentVariation.Nothing - }, - new PropertyType("value2", ValueStorageType.Ntext) - { - Alias = "value2", - DataTypeId = -88, - Variations = ContentVariation.Nothing - } - }; + var properties = CreatePropertyCollection( + ("value1", ContentVariation.Nothing), + ("value2", ContentVariation.Nothing)); contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); ServiceContext.ContentTypeService.Save(contentType); @@ -681,28 +656,11 @@ namespace Umbraco.Tests.Services CreateFrenchAndEnglishLangs(); - var contentType = new ContentType(-1) - { - Alias = "contentType", - Name = "contentType", - Variations = ContentVariation.Culture - }; + var contentType = CreateContentType(ContentVariation.Culture); - var properties = new PropertyTypeCollection(true) - { - new PropertyType("value1", ValueStorageType.Ntext) - { - Alias = "value1", - DataTypeId = -88, - Variations = ContentVariation.Culture - }, - new PropertyType("value2", ValueStorageType.Ntext) - { - Alias = "value2", - DataTypeId = -88, - Variations = ContentVariation.Nothing - } - }; + var properties = CreatePropertyCollection( + ("value1", ContentVariation.Culture), + ("value2", ContentVariation.Nothing)); contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); ServiceContext.ContentTypeService.Save(contentType); @@ -780,29 +738,16 @@ namespace Umbraco.Tests.Services } [Test] - public void Change_Variations_SimpleContentType_VariantPropertyToInvariantAndBack_While_Publishing() + public void Change_Property_Variations_From_Variant_To_Invariant_And_Ensure_Edited_Values_Are_Renormalized() { // one simple content type, variant, with both variant and invariant properties // can change an invariant property to variant and back CreateFrenchAndEnglishLangs(); - var contentType = new ContentType(-1) - { - Alias = "contentType", - Name = "contentType", - Variations = ContentVariation.Culture - }; + var contentType = CreateContentType(ContentVariation.Culture); - var properties = new PropertyTypeCollection(true) - { - new PropertyType("value1", ValueStorageType.Ntext) - { - Alias = "value1", - DataTypeId = -88, - Variations = ContentVariation.Culture - } - }; + var properties = CreatePropertyCollection(("value1", ContentVariation.Culture)); contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); ServiceContext.ContentTypeService.Save(contentType); @@ -902,54 +847,20 @@ namespace Umbraco.Tests.Services CreateFrenchAndEnglishLangs(); - var composing = new ContentType(-1) - { - Alias = "composing", - Name = "composing", - Variations = ContentVariation.Culture - }; + var composing = CreateContentType(ContentVariation.Culture, "composing"); - var properties1 = new PropertyTypeCollection(true) - { - new PropertyType("value11", ValueStorageType.Ntext) - { - Alias = "value11", - DataTypeId = -88, - Variations = ContentVariation.Culture - }, - new PropertyType("value12", ValueStorageType.Ntext) - { - Alias = "value12", - DataTypeId = -88, - Variations = ContentVariation.Nothing - } - }; + var properties1 = CreatePropertyCollection( + ("value11", ContentVariation.Culture), + ("value12", ContentVariation.Nothing)); composing.PropertyGroups.Add(new PropertyGroup(properties1) { Name = "Content" }); ServiceContext.ContentTypeService.Save(composing); - var composed = new ContentType(-1) - { - Alias = "composed", - Name = "composed", - Variations = ContentVariation.Culture - }; + var composed = CreateContentType(ContentVariation.Culture, "composed"); - var properties2 = new PropertyTypeCollection(true) - { - new PropertyType("value21", ValueStorageType.Ntext) - { - Alias = "value21", - DataTypeId = -88, - Variations = ContentVariation.Culture - }, - new PropertyType("value22", ValueStorageType.Ntext) - { - Alias = "value22", - DataTypeId = -88, - Variations = ContentVariation.Nothing - } - }; + var properties2 = CreatePropertyCollection( + ("value21", ContentVariation.Culture), + ("value22", ContentVariation.Nothing)); composed.PropertyGroups.Add(new PropertyGroup(properties2) { Name = "Content" }); composed.AddContentType(composing); @@ -1031,81 +942,30 @@ namespace Umbraco.Tests.Services CreateFrenchAndEnglishLangs(); - var composing = new ContentType(-1) - { - Alias = "composing", - Name = "composing", - Variations = ContentVariation.Culture - }; + var composing = CreateContentType(ContentVariation.Culture, "composing"); - var properties1 = new PropertyTypeCollection(true) - { - new PropertyType("value11", ValueStorageType.Ntext) - { - Alias = "value11", - DataTypeId = -88, - Variations = ContentVariation.Culture - }, - new PropertyType("value12", ValueStorageType.Ntext) - { - Alias = "value12", - DataTypeId = -88, - Variations = ContentVariation.Nothing - } - }; + var properties1 = CreatePropertyCollection( + ("value11", ContentVariation.Culture), + ("value12", ContentVariation.Nothing)); composing.PropertyGroups.Add(new PropertyGroup(properties1) { Name = "Content" }); ServiceContext.ContentTypeService.Save(composing); - var composed1 = new ContentType(-1) - { - Alias = "composed1", - Name = "composed1", - Variations = ContentVariation.Culture - }; + var composed1 = CreateContentType(ContentVariation.Culture, "composed1"); - var properties2 = new PropertyTypeCollection(true) - { - new PropertyType("value21", ValueStorageType.Ntext) - { - Alias = "value21", - DataTypeId = -88, - Variations = ContentVariation.Culture - }, - new PropertyType("value22", ValueStorageType.Ntext) - { - Alias = "value22", - DataTypeId = -88, - Variations = ContentVariation.Nothing - } - }; + var properties2 = CreatePropertyCollection( + ("value21", ContentVariation.Culture), + ("value22", ContentVariation.Nothing)); composed1.PropertyGroups.Add(new PropertyGroup(properties2) { Name = "Content" }); composed1.AddContentType(composing); ServiceContext.ContentTypeService.Save(composed1); - var composed2 = new ContentType(-1) - { - Alias = "composed2", - Name = "composed2", - Variations = ContentVariation.Nothing - }; + var composed2 = CreateContentType(ContentVariation.Nothing, "composed2"); - var properties3 = new PropertyTypeCollection(true) - { - new PropertyType("value31", ValueStorageType.Ntext) - { - Alias = "value31", - DataTypeId = -88, - Variations = ContentVariation.Nothing - }, - new PropertyType("value32", ValueStorageType.Ntext) - { - Alias = "value32", - DataTypeId = -88, - Variations = ContentVariation.Nothing - } - }; + var properties3 = CreatePropertyCollection( + ("value31", ContentVariation.Nothing), + ("value32", ContentVariation.Nothing)); composed2.PropertyGroups.Add(new PropertyGroup(properties3) { Name = "Content" }); composed2.AddContentType(composing); @@ -1219,5 +1079,27 @@ namespace Umbraco.Tests.Services var languageFr = new Language("fr"); ServiceContext.LocalizationService.Save(languageFr); } + + private IContentType CreateContentType(ContentVariation variance, string alias = "contentType") => new ContentType(-1) + { + Alias = alias, + Name = alias, + Variations = variance + }; + + private PropertyTypeCollection CreatePropertyCollection(params (string alias, ContentVariation variance)[] props) + { + var propertyCollection = new PropertyTypeCollection(true); + + foreach (var (alias, variance) in props) + propertyCollection.Add(new PropertyType(alias, ValueStorageType.Ntext) + { + Alias = alias, + DataTypeId = -88, + Variations = variance + }); + + return propertyCollection; + } } } From 2e83ac9282384dbcd621582d8316b61a2af19880 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 1 Aug 2019 18:54:19 +1000 Subject: [PATCH 0073/1001] Cleans up some tests --- .../ContentTypeServiceVariantsTests.cs | 84 +++---------------- 1 file changed, 12 insertions(+), 72 deletions(-) diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs index 4b8262d4a5..39d71feee9 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs @@ -112,18 +112,8 @@ namespace Umbraco.Tests.Services { var contentType = MockedContentTypes.CreateBasicContentType(); contentType.Variations = ContentVariation.Nothing; - var contentCollection = new PropertyTypeCollection(true); - contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) - { - Alias = "title", - Name = "Title", - Description = "", - Mandatory = false, - SortOrder = 1, - DataTypeId = -88, - Variations = ContentVariation.Nothing - }); - contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); + var properties = CreatePropertyCollection(("title", ContentVariation.Nothing)); + contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); ServiceContext.ContentTypeService.Save(contentType); var contentType2 = MockedContentTypes.CreateBasicContentType("test"); ServiceContext.ContentTypeService.Save(contentType2); @@ -156,18 +146,8 @@ namespace Umbraco.Tests.Services { var contentType = MockedContentTypes.CreateBasicContentType(); contentType.Variations = ContentVariation.Nothing; - var contentCollection = new PropertyTypeCollection(true); - contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) - { - Alias = "title", - Name = "Title", - Description = "", - Mandatory = false, - SortOrder = 1, - DataTypeId = -88, - Variations = ContentVariation.Nothing - }); - contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); + var properties = CreatePropertyCollection(("title", ContentVariation.Nothing)); + contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); ServiceContext.ContentTypeService.Save(contentType); //create some content of this content type @@ -205,18 +185,8 @@ namespace Umbraco.Tests.Services { var contentType = MockedContentTypes.CreateBasicContentType(); contentType.Variations = ContentVariation.Culture; - var contentCollection = new PropertyTypeCollection(true); - contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) - { - Alias = "title", - Name = "Title", - Description = "", - Mandatory = false, - SortOrder = 1, - DataTypeId = -88, - Variations = ContentVariation.Culture - }); - contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); + var properties = CreatePropertyCollection(("title", ContentVariation.Culture)); + contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); ServiceContext.ContentTypeService.Save(contentType); //create some content of this content type @@ -321,18 +291,8 @@ namespace Umbraco.Tests.Services //create content type with a property type that varies by culture var contentType = MockedContentTypes.CreateBasicContentType(); contentType.Variations = ContentVariation.Culture; - var contentCollection = new PropertyTypeCollection(true); - contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) - { - Alias = "title", - Name = "Title", - Description = "", - Mandatory = false, - SortOrder = 1, - DataTypeId = -88, - Variations = ContentVariation.Culture - }); - contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); + var properties = CreatePropertyCollection(("title", ContentVariation.Culture)); + contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); ServiceContext.ContentTypeService.Save(contentType); //create some content of this content type @@ -364,18 +324,8 @@ namespace Umbraco.Tests.Services //create content type with a property type that varies by culture var contentType = MockedContentTypes.CreateBasicContentType(); contentType.Variations = ContentVariation.Culture; - var contentCollection = new PropertyTypeCollection(true); - contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) - { - Alias = "title", - Name = "Title", - Description = "", - Mandatory = false, - SortOrder = 1, - DataTypeId = -88, - Variations = ContentVariation.Culture - }); - contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); + var properties = CreatePropertyCollection(("title", ContentVariation.Culture)); + contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); ServiceContext.ContentTypeService.Save(contentType); //compose this from the other one @@ -420,18 +370,8 @@ namespace Umbraco.Tests.Services //create content type with a property type that varies by culture var contentType = MockedContentTypes.CreateBasicContentType(); contentType.Variations = ContentVariation.Culture; - var contentCollection = new PropertyTypeCollection(true); - contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) - { - Alias = "title", - Name = "Title", - Description = "", - Mandatory = false, - SortOrder = 1, - DataTypeId = -88, - Variations = ContentVariation.Culture - }); - contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); + var properties = CreatePropertyCollection(("title", ContentVariation.Culture)); + contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); ServiceContext.ContentTypeService.Save(contentType); //compose this from the other one From ded1a22e45e2d2e168f76f388358382d032bc3a6 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 1 Aug 2019 19:01:41 +1000 Subject: [PATCH 0074/1001] fixes validation check --- .../Repositories/Implement/ContentTypeRepositoryBase.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 8f88c71bf0..2705bab39b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -532,9 +532,15 @@ AND umbracoNode.id <> @id", { //if the entity does not vary at all, then the property cannot have a variance value greater than it if (entity.Variations == ContentVariation.Nothing) + { foreach (var prop in entity.PropertyTypes) - if (prop.Variations > entity.Variations) + { + if (prop.IsPropertyDirty(nameof(prop.Variations)) && prop.Variations > entity.Variations) throw new InvalidOperationException($"The property {prop.Alias} cannot have variations of {prop.Variations} with the content type variations of {entity.Variations}"); + } + + } + } private IEnumerable GetImpactedContentTypes(IContentTypeComposition contentType, IEnumerable all) From 9efec9244b51250f85a9ae3eaeb7192356ea5707 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 1 Aug 2019 19:09:19 +1000 Subject: [PATCH 0075/1001] removes commented out code --- .../Implement/ContentTypeRepositoryBase.cs | 87 +------------------ 1 file changed, 2 insertions(+), 85 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 2705bab39b..f2efb03ba4 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -671,16 +671,12 @@ AND umbracoNode.id <> @id", case ContentVariation.Culture: CopyPropertyData(null, defaultLanguageId, propertyTypeIds, impactedL); CopyTagData(null, defaultLanguageId, propertyTypeIds, impactedL); - RenormalizeDocumentEditedFlags(propertyTypeIds, impactedL); - //TODO: Here we need to normalize the umbracoDocumentCultureVariation table for it's edited flags which are calculated based - //on changed property or name values + RenormalizeDocumentEditedFlags(propertyTypeIds, impactedL); break; case ContentVariation.Nothing: CopyPropertyData(defaultLanguageId, null, propertyTypeIds, impactedL); CopyTagData(defaultLanguageId, null, propertyTypeIds, impactedL); - RenormalizeDocumentEditedFlags(propertyTypeIds, impactedL); - //TODO: Here we need to normalize the umbracoDocumentCultureVariation table for it's edited flags which are calculated based - //on changed property or name values + RenormalizeDocumentEditedFlags(propertyTypeIds, impactedL); break; case ContentVariation.CultureAndSegment: case ContentVariation.Segment: @@ -690,55 +686,6 @@ AND umbracoNode.id <> @id", } } - //private HashSet GetEditedCultures(ContentVariation contentVariation, int currentVersionId, int publishedVersionId, IEnumerable properties) - //{ - // HashSet editedCultures = null; // don't allocate unless necessary - // string defaultCulture = null; //don't allocate unless necessary - - // var entityVariesByCulture = contentVariation.VariesByCulture(); - - // // create dtos for each property values, but only for values that do actually exist - // // ie have a non-null value, everything else is just ignored and won't have a db row - - // foreach (var property in properties) - // { - // if (property.PropertyType.SupportsPublishing) - // { - // //create the resulting hashset if it's not created and the entity varies by culture - // if (entityVariesByCulture && editedCultures == null) - // editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase); - - // // publishing = deal with edit and published values - // foreach (var propertyValue in property.Values) - // { - // var isInvariantValue = propertyValue.Culture == null; - // var isCultureValue = propertyValue.Culture != null && propertyValue.Segment == null; - - // // use explicit equals here, else object comparison fails at comparing eg strings - // var sameValues = propertyValue.PublishedValue == null ? propertyValue.EditedValue == null : propertyValue.PublishedValue.Equals(propertyValue.EditedValue); - - // if (entityVariesByCulture && !sameValues) - // { - // if (isCultureValue) - // { - // editedCultures.Add(propertyValue.Culture); // report culture as edited - // } - // else if (isInvariantValue) - // { - // // flag culture as edited if it contains an edited invariant property - // if (defaultCulture == null) - // defaultCulture = languageRepository.GetDefaultIsoCode(); - - // editedCultures.Add(defaultCulture); - // } - // } - // } - // } - // } - - // return editedCultures; - //} - /// /// Moves variant data for a content type variation change. /// @@ -1153,36 +1100,6 @@ AND umbracoNode.id <> @id", } } - ////Generate SQL to lookup the current name vs the publish name for each language - //var nameSql = Sql() - // .Select("cv1", x => x.NodeId, x => Alias(x.Id, "currentVersion")) - // .AndSelect("cvcv1", x => x.LanguageId, x => Alias(x.Name, "currentName")) - // .AndSelect("cvcv2", x => Alias(x.Name, "publishedName")) - // .AndSelect("dv", x => Alias(x.Id, "publishedVersion")) - // .AndSelect("dcv", x => x.Id, x => x.Edited) - // .From("cvcv1") - // .InnerJoin("cv1") - // .On((left, right) => left.Id == right.VersionId, "cv1", "cvcv1") - // .InnerJoin("dcv") - // .On((left, right, other) => left.NodeId == right.NodeId && left.LanguageId == other.LanguageId, "dcv", "cv1", "cvcv1") - // .LeftJoin(nested => - // nested.InnerJoin("dv") - // .On((left, right) => left.Id == right.Id && right.Published, "cv2", "dv"), "cv2") - // .On((left, right) => left.NodeId == right.NodeId, "cv1", "cv2") - // .LeftJoin("cvcv2") - // .On((left, right, other) => left.VersionId == right.Id && left.LanguageId == other.LanguageId, "cvcv2", "cv2", "cvcv1") - // .Where(x => x.Current, "cv1") - // .OrderBy("cv1.nodeId, cvcv1.versionId, cvcv1.languageId"); - - ////This is a reader (Query), we are not fetching this all into memory so we cannot make any changes during this iteration, we are just collecting data. - //foreach (var name in Database.Query(nameSql)) - //{ - // if (name.CurrentName != name.PublishedName) - // { - - // } - //} - //lookup all matching rows in umbracoDocumentCultureVariation var docCultureVariationsToUpdate = editedLanguageVersions.InGroupsOf(2000) .SelectMany(_ => Database.Fetch( From 600a29514f17d8917e751ec87f8540da1803d89a Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 1 Aug 2019 21:27:31 +1000 Subject: [PATCH 0076/1001] more asserts --- .../ContentTypeServiceVariantsTests.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs index 39d71feee9..381261173b 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs @@ -156,8 +156,12 @@ namespace Umbraco.Tests.Services doc.SetValue("title", "hello world"); ServiceContext.ContentService.Save(doc); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get + Assert.AreEqual("Hello1", doc.Name); Assert.AreEqual("hello world", doc.GetValue("title")); + Assert.IsTrue(doc.Edited); + Assert.IsFalse (doc.IsCultureEdited("en-US")); //change the content type to be variant, we will also update the name here to detect the copy changes doc.Name = "Hello2"; @@ -168,6 +172,8 @@ namespace Umbraco.Tests.Services Assert.AreEqual("Hello2", doc.GetCultureName("en-US")); Assert.AreEqual("hello world", doc.GetValue("title")); //We are not checking against en-US here because properties will remain invariant + Assert.IsTrue(doc.Edited); + Assert.IsTrue(doc.IsCultureEdited("en-US")); //change back property type to be invariant, we will also update the name here to detect the copy changes doc.SetCultureName("Hello3", "en-US"); @@ -178,6 +184,8 @@ namespace Umbraco.Tests.Services Assert.AreEqual("Hello3", doc.Name); Assert.AreEqual("hello world", doc.GetValue("title")); + Assert.IsTrue(doc.Edited); + Assert.IsFalse(doc.IsCultureEdited("en-US")); } [Test] @@ -195,8 +203,11 @@ namespace Umbraco.Tests.Services doc.SetValue("title", "hello world", "en-US"); ServiceContext.ContentService.Save(doc); + doc = ServiceContext.ContentService.GetById(doc.Id); //re-get Assert.AreEqual("Hello1", doc.GetCultureName("en-US")); Assert.AreEqual("hello world", doc.GetValue("title", "en-US")); + Assert.IsTrue(doc.Edited); + Assert.IsTrue(doc.IsCultureEdited("en-US")); //change the content type to be invariant, we will also update the name here to detect the copy changes doc.SetCultureName("Hello2", "en-US"); @@ -207,6 +218,8 @@ namespace Umbraco.Tests.Services Assert.AreEqual("Hello2", doc.Name); Assert.AreEqual("hello world", doc.GetValue("title")); + Assert.IsTrue(doc.Edited); + Assert.IsFalse(doc.IsCultureEdited("en-US")); //change back property type to be variant, we will also update the name here to detect the copy changes doc.Name = "Hello3"; @@ -219,6 +232,8 @@ namespace Umbraco.Tests.Services //exists it will not be returned because the property type is invariant, //so this check proves that null will be returned Assert.IsNull(doc.GetValue("title", "en-US")); + Assert.IsTrue(doc.Edited); + Assert.IsTrue(doc.IsCultureEdited("en-US")); // this is true because the name change is copied to the default language //we can now switch the property type to be variant and the value can be returned again contentType.PropertyTypes.First().Variations = ContentVariation.Culture; @@ -227,9 +242,12 @@ namespace Umbraco.Tests.Services Assert.AreEqual("Hello3", doc.GetCultureName("en-US")); Assert.AreEqual("hello world", doc.GetValue("title", "en-US")); + Assert.IsTrue(doc.Edited); + Assert.IsTrue(doc.IsCultureEdited("en-US")); } + [Test] public void Change_Property_Type_From_To_Variant_On_Invariant_Content_Type() { From be30bb0ad0706d9a146afd85498d22aa01b055a0 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 1 Aug 2019 21:41:43 +1000 Subject: [PATCH 0077/1001] adds test --- .../ContentTypeServiceVariantsTests.cs | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs index 381261173b..956de186be 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs @@ -795,6 +795,85 @@ namespace Umbraco.Tests.Services Assert.IsFalse(document.Edited); } + [Test] + public void Change_Property_Variations_From_Invariant_To_Variant_And_Ensure_Edited_Values_Are_Renormalized() + { + // one simple content type, variant, with both variant and invariant properties + // can change an invariant property to variant and back + + CreateFrenchAndEnglishLangs(); + + var contentType = CreateContentType(ContentVariation.Culture); + + var properties = CreatePropertyCollection(("value1", ContentVariation.Nothing)); + + contentType.PropertyGroups.Add(new PropertyGroup(properties) { Name = "Content" }); + ServiceContext.ContentTypeService.Save(contentType); + + var document = (IContent)new Content("document", -1, contentType); + document.SetCultureName("doc1en", "en"); + document.SetCultureName("doc1fr", "fr"); + document.SetValue("value1", "v1en-init"); + ServiceContext.ContentService.SaveAndPublish(document); //all values are published which means the document is not 'edited' + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.IsFalse(document.IsCultureEdited("en")); + Assert.IsFalse(document.IsCultureEdited("fr")); + Assert.IsFalse(document.Edited); + + document.SetValue("value1", "v1en"); //change the property value, so now the invariant (default) culture will be edited + ServiceContext.ContentService.Save(document); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.AreEqual("doc1en", document.GetCultureName("en")); + Assert.AreEqual("doc1fr", document.GetCultureName("fr")); + Assert.AreEqual("v1en", document.GetValue("value1")); + Assert.AreEqual("v1en-init", document.GetValue("value1", published: true)); + Assert.IsTrue(document.IsCultureEdited("en")); //This is true because the invariant property reflects changes on the default lang + Assert.IsFalse(document.IsCultureEdited("fr")); + Assert.IsTrue(document.Edited); + + // switch property type to Culture + contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Culture; + ServiceContext.ContentTypeService.Save(contentType); //This is going to have to re-normalize the "Edited" flag + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.IsTrue(document.IsCultureEdited("en")); //Remains true + Assert.IsFalse(document.IsCultureEdited("fr")); //False because no french property has ever been edited + Assert.IsTrue(document.Edited); + + //update the culture value and publish + document.SetValue("value1", "v1en2", "en"); + ServiceContext.ContentService.SaveAndPublish(document); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("doc1en", document.Name); + Assert.AreEqual("doc1en", document.GetCultureName("en")); + Assert.AreEqual("doc1fr", document.GetCultureName("fr")); + Assert.IsNull(document.GetValue("value1")); //The values are there but the business logic returns null + Assert.IsNull(document.GetValue("value1", published: true)); //The values are there but the business logic returns null + Assert.AreEqual("v1en2", document.GetValue("value1", "en")); + Assert.AreEqual("v1en2", document.GetValue("value1", "en", published: true)); + Assert.IsFalse(document.IsCultureEdited("en")); //This returns false, everything is published + Assert.IsFalse(document.IsCultureEdited("fr")); //False because no french property has ever been edited + Assert.IsFalse(document.Edited); + + // switch property back to Invariant + contentType.PropertyTypes.First(x => x.Alias == "value1").Variations = ContentVariation.Nothing; + ServiceContext.ContentTypeService.Save(contentType); + + document = ServiceContext.ContentService.GetById(document.Id); + Assert.AreEqual("v1en2", document.GetValue("value1")); //The variant property value gets copied over to the invariant + Assert.AreEqual("v1en2", document.GetValue("value1", published: true)); + Assert.IsNull(document.GetValue("value1", "fr")); //The values are there but the business logic returns null + Assert.IsNull(document.GetValue("value1", "fr", published: true)); //The values are there but the business logic returns null + Assert.IsFalse(document.IsCultureEdited("en")); //The variant published AND edited values are copied over to the invariant + Assert.IsFalse(document.IsCultureEdited("fr")); + Assert.IsFalse(document.Edited); + + } + [Test] public void Change_Variations_ComposedContentType_1() { From 9f6a7dec2e474854adc52151078ac4f8de770835 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 2 Aug 2019 00:28:48 +1000 Subject: [PATCH 0078/1001] Fixes how examine starts up to make it easier for custom index developers --- src/Umbraco.Web/Search/ExamineComponent.cs | 134 ++-------------- src/Umbraco.Web/Search/ExamineComposer.cs | 1 + .../Search/ExamineFinalComponent.cs | 151 ++++++++++++++++++ .../Search/ExamineFinalComposer.cs | 11 ++ .../Search/ExamineUserComponent.cs | 29 ++++ src/Umbraco.Web/Umbraco.Web.csproj | 3 + 6 files changed, 210 insertions(+), 119 deletions(-) create mode 100644 src/Umbraco.Web/Search/ExamineFinalComponent.cs create mode 100644 src/Umbraco.Web/Search/ExamineFinalComposer.cs create mode 100644 src/Umbraco.Web/Search/ExamineUserComponent.cs diff --git a/src/Umbraco.Web/Search/ExamineComponent.cs b/src/Umbraco.Web/Search/ExamineComponent.cs index 3583e5b7f9..b468963380 100644 --- a/src/Umbraco.Web/Search/ExamineComponent.cs +++ b/src/Umbraco.Web/Search/ExamineComponent.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Threading; using Examine; using Umbraco.Core; using Umbraco.Core.Cache; @@ -15,13 +14,12 @@ using Umbraco.Core.Sync; using Umbraco.Web.Cache; using Umbraco.Examine; using Umbraco.Core.Persistence.DatabaseModelDefinitions; -using Umbraco.Web.Scheduling; -using System.Threading.Tasks; using Examine.LuceneEngine.Directories; using Umbraco.Core.Composing; namespace Umbraco.Web.Search { + public sealed class ExamineComponent : IComponent { private readonly IExamineManager _examineManager; @@ -30,26 +28,31 @@ namespace Umbraco.Web.Search private readonly IValueSetBuilder _mediaValueSetBuilder; private readonly IValueSetBuilder _memberValueSetBuilder; private static bool _disableExamineIndexing = false; - private static volatile bool _isConfigured = false; - private static readonly object IsConfiguredLocker = new object(); private readonly IScopeProvider _scopeProvider; - private readonly ServiceContext _services; - private static BackgroundTaskRunner _rebuildOnStartupRunner; - private static readonly object RebuildLocker = new object(); + private readonly ServiceContext _services; private readonly IMainDom _mainDom; private readonly IProfilingLogger _logger; private readonly IUmbracoIndexesCreator _indexCreator; - private readonly IndexRebuilder _indexRebuilder; + // the default enlist priority is 100 // enlist with a lower priority to ensure that anything "default" runs after us // but greater that SafeXmlReaderWriter priority which is 60 private const int EnlistPriority = 80; + /// + /// Returns true if Examine is enabled for this AppDomain + /// + /// + /// This flag should be used by custom Examine index implementors to determine if they should + /// execute. This value is true if this component successfully registered with + /// + public static bool ExamineEnabled => !_disableExamineIndexing; + public ExamineComponent(IMainDom mainDom, IExamineManager examineManager, IProfilingLogger profilingLogger, IScopeProvider scopeProvider, IUmbracoIndexesCreator indexCreator, - IndexRebuilder indexRebuilder, ServiceContext services, + ServiceContext services, IContentValueSetBuilder contentValueSetBuilder, IPublishedContentValueSetBuilder publishedContentValueSetBuilder, IValueSetBuilder mediaValueSetBuilder, @@ -66,7 +69,6 @@ namespace Umbraco.Web.Search _mainDom = mainDom; _logger = profilingLogger; _indexCreator = indexCreator; - _indexRebuilder = indexRebuilder; } public void Initialize() @@ -119,11 +121,6 @@ namespace Umbraco.Web.Search ContentTypeCacheRefresher.CacheUpdated += ContentTypeCacheRefresherUpdated; ; MediaCacheRefresher.CacheUpdated += MediaCacheRefresherUpdated; MemberCacheRefresher.CacheUpdated += MemberCacheRefresherUpdated; - - EnsureUnlocked(_logger, _examineManager); - - // TODO: Instead of waiting 5000 ms, we could add an event handler on to fulfilling the first request, then start? - RebuildIndexes(_indexRebuilder, _logger, true, 5000); } public void Terminate() @@ -136,51 +133,7 @@ namespace Umbraco.Web.Search /// /// /// - public static void RebuildIndexes(IndexRebuilder indexRebuilder, ILogger logger, bool onlyEmptyIndexes, int waitMilliseconds = 0) - { - // TODO: need a way to disable rebuilding on startup - - lock(RebuildLocker) - { - if (_rebuildOnStartupRunner != null && _rebuildOnStartupRunner.IsRunning) - { - logger.Warn("Call was made to RebuildIndexes but the task runner for rebuilding is already running"); - return; - } - - logger.Info("Starting initialize async background thread."); - //do the rebuild on a managed background thread - var task = new RebuildOnStartupTask(indexRebuilder, logger, onlyEmptyIndexes, waitMilliseconds); - - _rebuildOnStartupRunner = new BackgroundTaskRunner( - "RebuildIndexesOnStartup", - logger); - - _rebuildOnStartupRunner.TryAdd(task); - } - } - - /// - /// Must be called to each index is unlocked before any indexing occurs - /// - /// - /// Indexing rebuilding can occur on a normal boot if the indexes are empty or on a cold boot by the database server messenger. Before - /// either of these happens, we need to configure the indexes. - /// - private static void EnsureUnlocked(ILogger logger, IExamineManager examineManager) - { - if (_disableExamineIndexing) return; - if (_isConfigured) return; - - lock (IsConfiguredLocker) - { - //double check - if (_isConfigured) return; - - _isConfigured = true; - examineManager.UnlockLuceneIndexes(logger); - } - } + public static void RebuildIndexes(IndexRebuilder indexRebuilder, ILogger logger, bool onlyEmptyIndexes, int waitMilliseconds = 0) => ExamineFinalComponent.RebuildIndexes(indexRebuilder, logger, onlyEmptyIndexes, waitMilliseconds); #region Cache refresher updated event handlers @@ -746,63 +699,6 @@ namespace Umbraco.Web.Search } #endregion - /// - /// Background task used to rebuild empty indexes on startup - /// - private class RebuildOnStartupTask : IBackgroundTask - { - private readonly IndexRebuilder _indexRebuilder; - private readonly ILogger _logger; - private readonly bool _onlyEmptyIndexes; - private readonly int _waitMilliseconds; - - public RebuildOnStartupTask(IndexRebuilder indexRebuilder, ILogger logger, bool onlyEmptyIndexes, int waitMilliseconds = 0) - { - _indexRebuilder = indexRebuilder ?? throw new ArgumentNullException(nameof(indexRebuilder)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _onlyEmptyIndexes = onlyEmptyIndexes; - _waitMilliseconds = waitMilliseconds; - } - - public bool IsAsync => false; - - public void Dispose() - { - } - - public void Run() - { - try - { - // rebuilds indexes - RebuildIndexes(); - } - catch (Exception ex) - { - _logger.Error(ex, "Failed to rebuild empty indexes."); - } - } - - public Task RunAsync(CancellationToken token) - { - throw new NotImplementedException(); - } - - /// - /// Used to rebuild indexes on startup or cold boot - /// - private void RebuildIndexes() - { - //do not attempt to do this if this has been disabled since we are not the main dom. - //this can be called during a cold boot - if (_disableExamineIndexing) return; - - if (_waitMilliseconds > 0) - Thread.Sleep(_waitMilliseconds); - - EnsureUnlocked(_logger, _indexRebuilder.ExamineManager); - _indexRebuilder.RebuildIndexes(_onlyEmptyIndexes); - } - } + } } diff --git a/src/Umbraco.Web/Search/ExamineComposer.cs b/src/Umbraco.Web/Search/ExamineComposer.cs index 6e74f0e89d..de33afe1cb 100644 --- a/src/Umbraco.Web/Search/ExamineComposer.cs +++ b/src/Umbraco.Web/Search/ExamineComposer.cs @@ -10,6 +10,7 @@ using Umbraco.Examine; namespace Umbraco.Web.Search { + /// /// Configures and installs Examine. /// diff --git a/src/Umbraco.Web/Search/ExamineFinalComponent.cs b/src/Umbraco.Web/Search/ExamineFinalComponent.cs new file mode 100644 index 0000000000..5a13185cdc --- /dev/null +++ b/src/Umbraco.Web/Search/ExamineFinalComponent.cs @@ -0,0 +1,151 @@ +using System; +using System.Threading; +using Examine; +using Umbraco.Core.Logging; +using Umbraco.Examine; +using Umbraco.Web.Scheduling; +using System.Threading.Tasks; +using Umbraco.Core.Composing; + +namespace Umbraco.Web.Search +{ + /// + /// Executes after all other examine components have executed + /// + public sealed class ExamineFinalComponent : IComponent + { + private static volatile bool _isConfigured = false; + private static readonly object IsConfiguredLocker = new object(); + private readonly IProfilingLogger _logger; + private readonly IExamineManager _examineManager; + private static readonly object RebuildLocker = new object(); + private readonly IndexRebuilder _indexRebuilder; + private static BackgroundTaskRunner _rebuildOnStartupRunner; + + public void Initialize() + { + if (!ExamineComponent.ExamineEnabled) return; + + EnsureUnlocked(_logger, _examineManager); + + // TODO: Instead of waiting 5000 ms, we could add an event handler on to fulfilling the first request, then start? + ExamineComponent.RebuildIndexes(_indexRebuilder, _logger, true, 5000); + } + + public void Terminate() + { + } + + /// + /// Called to rebuild empty indexes on startup + /// + /// + /// + /// + /// + internal static void RebuildIndexes(IndexRebuilder indexRebuilder, ILogger logger, bool onlyEmptyIndexes, int waitMilliseconds = 0) + { + // TODO: need a way to disable rebuilding on startup + + lock (RebuildLocker) + { + if (_rebuildOnStartupRunner != null && _rebuildOnStartupRunner.IsRunning) + { + logger.Warn("Call was made to RebuildIndexes but the task runner for rebuilding is already running"); + return; + } + + logger.Info("Starting initialize async background thread."); + //do the rebuild on a managed background thread + var task = new RebuildOnStartupTask(indexRebuilder, logger, onlyEmptyIndexes, waitMilliseconds); + + _rebuildOnStartupRunner = new BackgroundTaskRunner( + "RebuildIndexesOnStartup", + logger); + + _rebuildOnStartupRunner.TryAdd(task); + } + } + + /// + /// Must be called to each index is unlocked before any indexing occurs + /// + /// + /// Indexing rebuilding can occur on a normal boot if the indexes are empty or on a cold boot by the database server messenger. Before + /// either of these happens, we need to configure the indexes. + /// + internal static void EnsureUnlocked(ILogger logger, IExamineManager examineManager) + { + if (!ExamineComponent.ExamineEnabled) return; + if (_isConfigured) return; + + lock (IsConfiguredLocker) + { + //double check + if (_isConfigured) return; + + _isConfigured = true; + examineManager.UnlockLuceneIndexes(logger); + } + } + + /// + /// Background task used to rebuild empty indexes on startup + /// + private class RebuildOnStartupTask : IBackgroundTask + { + private readonly IndexRebuilder _indexRebuilder; + private readonly ILogger _logger; + private readonly bool _onlyEmptyIndexes; + private readonly int _waitMilliseconds; + + public RebuildOnStartupTask(IndexRebuilder indexRebuilder, ILogger logger, bool onlyEmptyIndexes, int waitMilliseconds = 0) + { + _indexRebuilder = indexRebuilder ?? throw new ArgumentNullException(nameof(indexRebuilder)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _onlyEmptyIndexes = onlyEmptyIndexes; + _waitMilliseconds = waitMilliseconds; + } + + public bool IsAsync => false; + + public void Dispose() + { + } + + public void Run() + { + try + { + // rebuilds indexes + RebuildIndexes(); + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to rebuild empty indexes."); + } + } + + public Task RunAsync(CancellationToken token) + { + throw new NotImplementedException(); + } + + /// + /// Used to rebuild indexes on startup or cold boot + /// + private void RebuildIndexes() + { + //do not attempt to do this if this has been disabled since we are not the main dom. + //this can be called during a cold boot + if (!ExamineComponent.ExamineEnabled) return; + + if (_waitMilliseconds > 0) + Thread.Sleep(_waitMilliseconds); + + EnsureUnlocked(_logger, _indexRebuilder.ExamineManager); + _indexRebuilder.RebuildIndexes(_onlyEmptyIndexes); + } + } + } +} diff --git a/src/Umbraco.Web/Search/ExamineFinalComposer.cs b/src/Umbraco.Web/Search/ExamineFinalComposer.cs new file mode 100644 index 0000000000..6b33459159 --- /dev/null +++ b/src/Umbraco.Web/Search/ExamineFinalComposer.cs @@ -0,0 +1,11 @@ +using Umbraco.Core.Composing; + +namespace Umbraco.Web.Search +{ + // examine's final composer composes after all user composers + // and *also* after ICoreComposer (in case IUserComposer is disabled) + [ComposeAfter(typeof(IUserComposer))] + [ComposeAfter(typeof(ICoreComposer))] + public class ExamineFinalComposer : ComponentComposer + { } +} diff --git a/src/Umbraco.Web/Search/ExamineUserComponent.cs b/src/Umbraco.Web/Search/ExamineUserComponent.cs new file mode 100644 index 0000000000..ddee07543c --- /dev/null +++ b/src/Umbraco.Web/Search/ExamineUserComponent.cs @@ -0,0 +1,29 @@ +using Umbraco.Core.Composing; + +namespace Umbraco.Web.Search +{ + /// + /// An abstract class for custom index authors to inherit from + /// + public abstract class ExamineUserComponent : IComponent + { + /// + /// Initialize the component, eagerly exits if ExamineComponent.ExamineEnabled == false + /// + public void Initialize() + { + if (!ExamineComponent.ExamineEnabled) return; + + InitializeComponent(); + } + + /// + /// Abstract method which executes to initialize this component if ExamineComponent.ExamineEnabled == true + /// + protected abstract void InitializeComponent(); + + public virtual void Terminate() + { + } + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index dcaa3494fd..c9c9a4412a 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -233,6 +233,9 @@ + + + From 366058581a8fc11697f92636a3961fa7d4cbb2d5 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 2 Aug 2019 01:15:08 +1000 Subject: [PATCH 0079/1001] Adds LuceneIndexDiagnostics to make it easier for devs and custom indexes --- src/Umbraco.Examine/LuceneIndexDiagnostics.cs | 80 +++++++++++++++++++ src/Umbraco.Examine/Umbraco.Examine.csproj | 1 + .../UmbracoExamineIndexDiagnostics.cs | 62 ++------------ src/Umbraco.Web/Search/ExamineComponent.cs | 2 +- .../Search/GenericIndexDiagnostics.cs | 1 + src/Umbraco.Web/Suspendable.cs | 2 + 6 files changed, 91 insertions(+), 57 deletions(-) create mode 100644 src/Umbraco.Examine/LuceneIndexDiagnostics.cs diff --git a/src/Umbraco.Examine/LuceneIndexDiagnostics.cs b/src/Umbraco.Examine/LuceneIndexDiagnostics.cs new file mode 100644 index 0000000000..03e5018a79 --- /dev/null +++ b/src/Umbraco.Examine/LuceneIndexDiagnostics.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using Examine.LuceneEngine.Providers; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Lucene.Net.Store; +using Umbraco.Core.IO; + +namespace Umbraco.Examine +{ + public class LuceneIndexDiagnostics : IIndexDiagnostics + { + public LuceneIndexDiagnostics(LuceneIndex index, ILogger logger) + { + Index = index; + Logger = logger; + } + + public LuceneIndex Index { get; } + public ILogger Logger { get; } + + public int DocumentCount + { + get + { + try + { + return Index.GetIndexDocumentCount(); + } + catch (AlreadyClosedException) + { + Logger.Warn(typeof(UmbracoContentIndex), "Cannot get GetIndexDocumentCount, the writer is already closed"); + return 0; + } + } + } + + public int FieldCount + { + get + { + try + { + return Index.GetIndexFieldCount(); + } + catch (AlreadyClosedException) + { + Logger.Warn(typeof(UmbracoContentIndex), "Cannot get GetIndexFieldCount, the writer is already closed"); + return 0; + } + } + } + + public Attempt IsHealthy() + { + var isHealthy = Index.IsHealthy(out var indexError); + return isHealthy ? Attempt.Succeed() : Attempt.Fail(indexError.Message); + } + + public virtual IReadOnlyDictionary Metadata + { + get + { + var d = new Dictionary + { + [nameof(UmbracoExamineIndex.CommitCount)] = Index.CommitCount, + [nameof(UmbracoExamineIndex.DefaultAnalyzer)] = Index.DefaultAnalyzer.GetType().Name, + ["LuceneDirectory"] = Index.GetLuceneDirectory().GetType().Name, + [nameof(UmbracoExamineIndex.LuceneIndexFolder)] = + Index.LuceneIndexFolder == null + ? string.Empty + : Index.LuceneIndexFolder.ToString().ToLowerInvariant().TrimStart(IOHelper.MapPath(SystemDirectories.Root).ToLowerInvariant()).Replace("\\", "/").EnsureStartsWith('/'), + }; + + return d; + } + } + + + } +} diff --git a/src/Umbraco.Examine/Umbraco.Examine.csproj b/src/Umbraco.Examine/Umbraco.Examine.csproj index 95690c17e4..e28a8e674e 100644 --- a/src/Umbraco.Examine/Umbraco.Examine.csproj +++ b/src/Umbraco.Examine/Umbraco.Examine.csproj @@ -72,6 +72,7 @@ + diff --git a/src/Umbraco.Examine/UmbracoExamineIndexDiagnostics.cs b/src/Umbraco.Examine/UmbracoExamineIndexDiagnostics.cs index fed5b9bae7..8be46869ac 100644 --- a/src/Umbraco.Examine/UmbracoExamineIndexDiagnostics.cs +++ b/src/Umbraco.Examine/UmbracoExamineIndexDiagnostics.cs @@ -7,73 +7,23 @@ using Umbraco.Core.Logging; namespace Umbraco.Examine { - public class UmbracoExamineIndexDiagnostics : IIndexDiagnostics + public class UmbracoExamineIndexDiagnostics : LuceneIndexDiagnostics { private readonly UmbracoExamineIndex _index; - private readonly ILogger _logger; public UmbracoExamineIndexDiagnostics(UmbracoExamineIndex index, ILogger logger) + : base(index, logger) { - _index = index; - _logger = logger; } - public int DocumentCount + public override IReadOnlyDictionary Metadata { get { - try - { - return _index.GetIndexDocumentCount(); - } - catch (AlreadyClosedException) - { - _logger.Warn(typeof(UmbracoContentIndex), "Cannot get GetIndexDocumentCount, the writer is already closed"); - return 0; - } - } - } + var d = base.Metadata.ToDictionary(x => x.Key, x => x.Value); - public int FieldCount - { - get - { - try - { - return _index.GetIndexFieldCount(); - } - catch (AlreadyClosedException) - { - _logger.Warn(typeof(UmbracoContentIndex), "Cannot get GetIndexFieldCount, the writer is already closed"); - return 0; - } - } - } - - public Attempt IsHealthy() - { - var isHealthy = _index.IsHealthy(out var indexError); - return isHealthy ? Attempt.Succeed() : Attempt.Fail(indexError.Message); - } - - public virtual IReadOnlyDictionary Metadata - { - get - { - var d = new Dictionary - { - [nameof(UmbracoExamineIndex.CommitCount)] = _index.CommitCount, - [nameof(UmbracoExamineIndex.DefaultAnalyzer)] = _index.DefaultAnalyzer.GetType().Name, - ["LuceneDirectory"] = _index.GetLuceneDirectory().GetType().Name, - [nameof(UmbracoExamineIndex.EnableDefaultEventHandler)] = _index.EnableDefaultEventHandler, - [nameof(UmbracoExamineIndex.LuceneIndexFolder)] = - _index.LuceneIndexFolder == null - ? string.Empty - : _index.LuceneIndexFolder.ToString().ToLowerInvariant().TrimStart(IOHelper.MapPath(SystemDirectories.Root).ToLowerInvariant()).Replace("\\", "/").EnsureStartsWith('/'), - [nameof(UmbracoExamineIndex.PublishedValuesOnly)] = _index.PublishedValuesOnly, - //There's too much info here - //[nameof(UmbracoExamineIndexer.FieldDefinitionCollection)] = _index.FieldDefinitionCollection, - }; + d[nameof(UmbracoExamineIndex.EnableDefaultEventHandler)] = _index.EnableDefaultEventHandler; + d[nameof(UmbracoExamineIndex.PublishedValuesOnly)] = _index.PublishedValuesOnly; if (_index.ValueSetValidator is ValueSetValidator vsv) { diff --git a/src/Umbraco.Web/Search/ExamineComponent.cs b/src/Umbraco.Web/Search/ExamineComponent.cs index b468963380..5d33bfb169 100644 --- a/src/Umbraco.Web/Search/ExamineComponent.cs +++ b/src/Umbraco.Web/Search/ExamineComponent.cs @@ -118,7 +118,7 @@ namespace Umbraco.Web.Search // bind to distributed cache events - this ensures that this logic occurs on ALL servers // that are taking part in a load balanced environment. ContentCacheRefresher.CacheUpdated += ContentCacheRefresherUpdated; - ContentTypeCacheRefresher.CacheUpdated += ContentTypeCacheRefresherUpdated; ; + ContentTypeCacheRefresher.CacheUpdated += ContentTypeCacheRefresherUpdated; MediaCacheRefresher.CacheUpdated += MediaCacheRefresherUpdated; MemberCacheRefresher.CacheUpdated += MemberCacheRefresherUpdated; } diff --git a/src/Umbraco.Web/Search/GenericIndexDiagnostics.cs b/src/Umbraco.Web/Search/GenericIndexDiagnostics.cs index 560fb19f09..cb25e1242a 100644 --- a/src/Umbraco.Web/Search/GenericIndexDiagnostics.cs +++ b/src/Umbraco.Web/Search/GenericIndexDiagnostics.cs @@ -8,6 +8,7 @@ using Umbraco.Examine; namespace Umbraco.Web.Search { + /// /// Used to return diagnostic data for any index /// diff --git a/src/Umbraco.Web/Suspendable.cs b/src/Umbraco.Web/Suspendable.cs index 86c83120ea..cfe4d28e8b 100644 --- a/src/Umbraco.Web/Suspendable.cs +++ b/src/Umbraco.Web/Suspendable.cs @@ -50,6 +50,8 @@ namespace Umbraco.Web } } + //This is really needed at all since the only place this is used is in ExamineComponent and that already maintains a flag of whether it suspsended or not + // AHH... but Deploy probably uses this? public static class ExamineEvents { private static bool _tried, _suspended; From 14915e9da788570a1626a0fe8c1a2de7b29c49de Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 2 Aug 2019 01:16:37 +1000 Subject: [PATCH 0080/1001] oops --- src/Umbraco.Examine/UmbracoExamineIndexDiagnostics.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Examine/UmbracoExamineIndexDiagnostics.cs b/src/Umbraco.Examine/UmbracoExamineIndexDiagnostics.cs index 8be46869ac..4a926deebe 100644 --- a/src/Umbraco.Examine/UmbracoExamineIndexDiagnostics.cs +++ b/src/Umbraco.Examine/UmbracoExamineIndexDiagnostics.cs @@ -14,6 +14,7 @@ namespace Umbraco.Examine public UmbracoExamineIndexDiagnostics(UmbracoExamineIndex index, ILogger logger) : base(index, logger) { + _index = index; } public override IReadOnlyDictionary Metadata From 86a97c30d193d92c67c9fe917c17e2a783449d51 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 2 Aug 2019 12:40:45 +1000 Subject: [PATCH 0081/1001] fixes up LuceneIndexDiagnostics --- src/Umbraco.Examine/LuceneIndexDiagnostics.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Examine/LuceneIndexDiagnostics.cs b/src/Umbraco.Examine/LuceneIndexDiagnostics.cs index 03e5018a79..96363904b4 100644 --- a/src/Umbraco.Examine/LuceneIndexDiagnostics.cs +++ b/src/Umbraco.Examine/LuceneIndexDiagnostics.cs @@ -4,6 +4,7 @@ using Umbraco.Core; using Umbraco.Core.Logging; using Lucene.Net.Store; using Umbraco.Core.IO; +using System.Linq; namespace Umbraco.Examine { @@ -60,17 +61,19 @@ namespace Umbraco.Examine { get { + var luceneDir = Index.GetLuceneDirectory(); var d = new Dictionary { [nameof(UmbracoExamineIndex.CommitCount)] = Index.CommitCount, [nameof(UmbracoExamineIndex.DefaultAnalyzer)] = Index.DefaultAnalyzer.GetType().Name, - ["LuceneDirectory"] = Index.GetLuceneDirectory().GetType().Name, - [nameof(UmbracoExamineIndex.LuceneIndexFolder)] = - Index.LuceneIndexFolder == null - ? string.Empty - : Index.LuceneIndexFolder.ToString().ToLowerInvariant().TrimStart(IOHelper.MapPath(SystemDirectories.Root).ToLowerInvariant()).Replace("\\", "/").EnsureStartsWith('/'), + ["LuceneDirectory"] = luceneDir.GetType().Name }; + if (luceneDir is FSDirectory fsDir) + { + d[nameof(UmbracoExamineIndex.LuceneIndexFolder)] = fsDir.Directory.ToString().ToLowerInvariant().TrimStart(IOHelper.MapPath(SystemDirectories.Root).ToLowerInvariant()).Replace("\\", "/").EnsureStartsWith('/'); + } + return d; } } From 345e6091d2d09f3c83d9d26044e0ace9f22cd5d4 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 2 Aug 2019 13:01:02 +1000 Subject: [PATCH 0082/1001] fixes null check, was on the wrong element --- src/Umbraco.Examine/ContentValueSetBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Examine/ContentValueSetBuilder.cs b/src/Umbraco.Examine/ContentValueSetBuilder.cs index 44cef08813..9cbc311639 100644 --- a/src/Umbraco.Examine/ContentValueSetBuilder.cs +++ b/src/Umbraco.Examine/ContentValueSetBuilder.cs @@ -54,7 +54,7 @@ namespace Umbraco.Examine {"updateDate", new object[] {c.UpdateDate}}, //Always add invariant updateDate {"nodeName", (PublishedValuesOnly //Always add invariant nodeName ? c.PublishName?.Yield() - : c?.Name.Yield()) ?? Enumerable.Empty()}, + : c.Name?.Yield()) ?? Enumerable.Empty()}, {"urlName", urlValue?.Yield() ?? Enumerable.Empty()}, //Always add invariant urlName {"path", c.Path?.Yield() ?? Enumerable.Empty()}, {"nodeType", c.ContentType.Id.ToString().Yield() ?? Enumerable.Empty()}, From ddf500b169f80b3149a5738582473a95a877851f Mon Sep 17 00:00:00 2001 From: Brian Sokolnicki Date: Fri, 2 Aug 2019 12:53:50 -0500 Subject: [PATCH 0083/1001] bug fix for array iteration --- .../src/common/services/tree.service.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js index 3e60b09ad9..3c9846fc43 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js @@ -69,12 +69,14 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS if (childPath.startsWith(p + ",")) { //this means that the node's path supercedes this path stored so we can remove the current 'p' and replace it with node.path expandedPaths.splice(expandedPaths.indexOf(p), 1); //remove it - expandedPaths.push(childPath); //replace it + if (expandedPaths.indexOf(childPath) === -1) { + expandedPaths.push(childPath); //replace it + } } else if (p.startsWith(childPath + ",")) { //this means we've already tracked a deeper node so we shouldn't track this one } - else { + else if (expandedPaths.indexOf(childPath) === -1) { expandedPaths.push(childPath); //track it } }); From 6d3faf91d8bc06f0378c816f92b2b2fc517ee168 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 5 Aug 2019 13:50:50 +1000 Subject: [PATCH 0084/1001] fixes spacing with ext login providers --- .../src/views/common/overlays/user/user.html | 16 ++++++++-------- .../views/components/application/umb-login.html | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html index 4af8c83983..60477ae9b8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html @@ -53,14 +53,14 @@
- + + Link your {{login.caption}} account +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html index e7513617b7..bc27196466 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html @@ -138,7 +138,7 @@ id="{{login.authType}}" name="provider" value="{{login.authType}}" title="Log in using your {{login.caption}} account"> - Sign in with {{login.caption}} + Sign in with {{login.caption}} From 89429bd65a3377b8b86ffd284b44ac2483693a64 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 5 Aug 2019 16:28:23 +1000 Subject: [PATCH 0085/1001] fixes ctor --- src/Umbraco.Web/Search/ExamineFinalComponent.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Umbraco.Web/Search/ExamineFinalComponent.cs b/src/Umbraco.Web/Search/ExamineFinalComponent.cs index 5a13185cdc..4213efac34 100644 --- a/src/Umbraco.Web/Search/ExamineFinalComponent.cs +++ b/src/Umbraco.Web/Search/ExamineFinalComponent.cs @@ -14,6 +14,7 @@ namespace Umbraco.Web.Search ///
public sealed class ExamineFinalComponent : IComponent { + private static volatile bool _isConfigured = false; private static readonly object IsConfiguredLocker = new object(); private readonly IProfilingLogger _logger; @@ -22,6 +23,13 @@ namespace Umbraco.Web.Search private readonly IndexRebuilder _indexRebuilder; private static BackgroundTaskRunner _rebuildOnStartupRunner; + public ExamineFinalComponent(IProfilingLogger logger, IExamineManager examineManager, IndexRebuilder indexRebuilder) + { + _logger = logger; + _examineManager = examineManager; + _indexRebuilder = indexRebuilder; + } + public void Initialize() { if (!ExamineComponent.ExamineEnabled) return; From be4536cf164dd5a27c2046603319409b8a7b9eb3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 5 Aug 2019 17:14:29 +1000 Subject: [PATCH 0086/1001] Refactors ugly statics and nested classes --- src/Umbraco.Examine/ExamineExtensions.cs | 25 ++++ src/Umbraco.Examine/IndexRebuilder.cs | 3 +- .../Search/BackgroundIndexRebuilder.cs | 123 +++++++++++++++ src/Umbraco.Web/Search/ExamineComponent.cs | 28 +--- src/Umbraco.Web/Search/ExamineComposer.cs | 1 + .../Search/ExamineFinalComponent.cs | 141 ++---------------- .../Search/ExamineUserComponent.cs | 12 +- src/Umbraco.Web/Umbraco.Web.csproj | 1 + 8 files changed, 180 insertions(+), 154 deletions(-) create mode 100644 src/Umbraco.Web/Search/BackgroundIndexRebuilder.cs diff --git a/src/Umbraco.Examine/ExamineExtensions.cs b/src/Umbraco.Examine/ExamineExtensions.cs index 376950c95e..ae6913f4ee 100644 --- a/src/Umbraco.Examine/ExamineExtensions.cs +++ b/src/Umbraco.Examine/ExamineExtensions.cs @@ -28,6 +28,31 @@ namespace Umbraco.Examine /// internal static readonly Regex CultureIsoCodeFieldNameMatchExpression = new Regex("^([_\\w]+)_([a-z]{2}-[a-z0-9]{2,4})$", RegexOptions.Compiled); + private static volatile bool _isUnlocked = false; + private static readonly object IsUnlockedLocker = new object(); + + /// + /// Unlocks all Lucene based indexes registered with the + /// + /// + /// Indexing rebuilding can occur on a normal boot if the indexes are empty or on a cold boot by the database server messenger. Before + /// either of these happens, we need to configure the indexes. + /// + internal static void EnsureUnlocked(this IExamineManager examineManager, IMainDom mainDom, ILogger logger) + { + if (!mainDom.IsMainDom) return; + if (_isUnlocked) return; + + lock (IsUnlockedLocker) + { + //double check + if (_isUnlocked) return; + + _isUnlocked = true; + examineManager.UnlockLuceneIndexes(logger); + } + } + //TODO: We need a public method here to just match a field name against CultureIsoCodeFieldNameMatchExpression /// diff --git a/src/Umbraco.Examine/IndexRebuilder.cs b/src/Umbraco.Examine/IndexRebuilder.cs index 43c309b9c5..786aecac71 100644 --- a/src/Umbraco.Examine/IndexRebuilder.cs +++ b/src/Umbraco.Examine/IndexRebuilder.cs @@ -5,7 +5,8 @@ using System.Threading.Tasks; using Examine; namespace Umbraco.Examine -{ +{ + /// /// Utility to rebuild all indexes ensuring minimal data queries /// diff --git a/src/Umbraco.Web/Search/BackgroundIndexRebuilder.cs b/src/Umbraco.Web/Search/BackgroundIndexRebuilder.cs new file mode 100644 index 0000000000..d747d8d9e4 --- /dev/null +++ b/src/Umbraco.Web/Search/BackgroundIndexRebuilder.cs @@ -0,0 +1,123 @@ +using System; +using System.Threading; +using Umbraco.Core.Logging; +using Umbraco.Examine; +using System.Threading.Tasks; +using Umbraco.Core; +using Umbraco.Web.Scheduling; + +namespace Umbraco.Web.Search +{ + /// + /// Utility to rebuild all indexes on a background thread + /// + public sealed class BackgroundIndexRebuilder + { + private static readonly object RebuildLocker = new object(); + private readonly IndexRebuilder _indexRebuilder; + private readonly IMainDom _mainDom; + private readonly IProfilingLogger _logger; + private static BackgroundTaskRunner _rebuildOnStartupRunner; + + public BackgroundIndexRebuilder(IMainDom mainDom, IProfilingLogger logger, IndexRebuilder indexRebuilder) + { + _mainDom = mainDom; + _logger = logger; + _indexRebuilder = indexRebuilder; + } + + /// + /// Called to rebuild empty indexes on startup + /// + /// + /// + /// + /// + public void RebuildIndexes(bool onlyEmptyIndexes, int waitMilliseconds = 0) + { + // TODO: need a way to disable rebuilding on startup + + lock (RebuildLocker) + { + if (_rebuildOnStartupRunner != null && _rebuildOnStartupRunner.IsRunning) + { + _logger.Warn("Call was made to RebuildIndexes but the task runner for rebuilding is already running"); + return; + } + + _logger.Info("Starting initialize async background thread."); + //do the rebuild on a managed background thread + var task = new RebuildOnStartupTask(_mainDom, _indexRebuilder, _logger, onlyEmptyIndexes, waitMilliseconds); + + _rebuildOnStartupRunner = new BackgroundTaskRunner( + "RebuildIndexesOnStartup", + _logger); + + _rebuildOnStartupRunner.TryAdd(task); + } + } + + /// + /// Background task used to rebuild empty indexes on startup + /// + private class RebuildOnStartupTask : IBackgroundTask + { + private readonly IMainDom _mainDom; + + private readonly IndexRebuilder _indexRebuilder; + private readonly ILogger _logger; + private readonly bool _onlyEmptyIndexes; + private readonly int _waitMilliseconds; + + public RebuildOnStartupTask(IMainDom mainDom, + IndexRebuilder indexRebuilder, ILogger logger, bool onlyEmptyIndexes, int waitMilliseconds = 0) + { + _mainDom = mainDom; + _indexRebuilder = indexRebuilder ?? throw new ArgumentNullException(nameof(indexRebuilder)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _onlyEmptyIndexes = onlyEmptyIndexes; + _waitMilliseconds = waitMilliseconds; + } + + public bool IsAsync => false; + + public void Dispose() + { + } + + public void Run() + { + try + { + // rebuilds indexes + RebuildIndexes(); + } + catch (Exception ex) + { + _logger.Error(ex, "Failed to rebuild empty indexes."); + } + } + + public Task RunAsync(CancellationToken token) + { + throw new NotImplementedException(); + } + + /// + /// Used to rebuild indexes on startup or cold boot + /// + private void RebuildIndexes() + { + //do not attempt to do this if this has been disabled since we are not the main dom. + //this can be called during a cold boot + if (!_mainDom.IsMainDom) return; + + if (_waitMilliseconds > 0) + Thread.Sleep(_waitMilliseconds); + + _indexRebuilder.ExamineManager.EnsureUnlocked(_mainDom, _logger); + _indexRebuilder.RebuildIndexes(_onlyEmptyIndexes); + } + } + } +} diff --git a/src/Umbraco.Web/Search/ExamineComponent.cs b/src/Umbraco.Web/Search/ExamineComponent.cs index 5d33bfb169..fd1eee8f96 100644 --- a/src/Umbraco.Web/Search/ExamineComponent.cs +++ b/src/Umbraco.Web/Search/ExamineComponent.cs @@ -16,18 +16,17 @@ using Umbraco.Examine; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Examine.LuceneEngine.Directories; using Umbraco.Core.Composing; +using System.ComponentModel; namespace Umbraco.Web.Search { - - public sealed class ExamineComponent : IComponent + public sealed class ExamineComponent : Umbraco.Core.Composing.IComponent { private readonly IExamineManager _examineManager; private readonly IContentValueSetBuilder _contentValueSetBuilder; private readonly IPublishedContentValueSetBuilder _publishedContentValueSetBuilder; private readonly IValueSetBuilder _mediaValueSetBuilder; private readonly IValueSetBuilder _memberValueSetBuilder; - private static bool _disableExamineIndexing = false; private readonly IScopeProvider _scopeProvider; private readonly ServiceContext _services; private readonly IMainDom _mainDom; @@ -39,16 +38,7 @@ namespace Umbraco.Web.Search // enlist with a lower priority to ensure that anything "default" runs after us // but greater that SafeXmlReaderWriter priority which is 60 private const int EnlistPriority = 80; - - /// - /// Returns true if Examine is enabled for this AppDomain - /// - /// - /// This flag should be used by custom Examine index implementors to determine if they should - /// execute. This value is true if this component successfully registered with - /// - public static bool ExamineEnabled => !_disableExamineIndexing; - + public ExamineComponent(IMainDom mainDom, IExamineManager examineManager, IProfilingLogger profilingLogger, IScopeProvider scopeProvider, IUmbracoIndexesCreator indexCreator, @@ -97,7 +87,6 @@ namespace Umbraco.Web.Search //if we could not register the shutdown examine ourselves, it means we are not maindom! in this case all of examine should be disabled! Suspendable.ExamineEvents.SuspendIndexers(_logger); - _disableExamineIndexing = true; return; //exit, do not continue } @@ -126,14 +115,9 @@ namespace Umbraco.Web.Search public void Terminate() { } - /// - /// Called to rebuild empty indexes on startup - /// - /// - /// - /// - /// - public static void RebuildIndexes(IndexRebuilder indexRebuilder, ILogger logger, bool onlyEmptyIndexes, int waitMilliseconds = 0) => ExamineFinalComponent.RebuildIndexes(indexRebuilder, logger, onlyEmptyIndexes, waitMilliseconds); + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This method should not be used and will be removed in future versions, rebuilding indexes can be done with the IndexRebuilder or the BackgroundIndexRebuilder")] + public static void RebuildIndexes(IndexRebuilder indexRebuilder, ILogger logger, bool onlyEmptyIndexes, int waitMilliseconds = 0) => Current.Factory.GetInstance().RebuildIndexes(onlyEmptyIndexes, waitMilliseconds); #region Cache refresher updated event handlers diff --git a/src/Umbraco.Web/Search/ExamineComposer.cs b/src/Umbraco.Web/Search/ExamineComposer.cs index de33afe1cb..0ade432d70 100644 --- a/src/Umbraco.Web/Search/ExamineComposer.cs +++ b/src/Umbraco.Web/Search/ExamineComposer.cs @@ -44,6 +44,7 @@ namespace Umbraco.Web.Search false)); composition.RegisterUnique, MediaValueSetBuilder>(); composition.RegisterUnique, MemberValueSetBuilder>(); + composition.RegisterUnique(); //We want to manage Examine's AppDomain shutdown sequence ourselves so first we'll disable Examine's default behavior //and then we'll use MainDom to control Examine's shutdown - this MUST be done in Compose ie before ExamineManager diff --git a/src/Umbraco.Web/Search/ExamineFinalComponent.cs b/src/Umbraco.Web/Search/ExamineFinalComponent.cs index 4213efac34..4514a78faa 100644 --- a/src/Umbraco.Web/Search/ExamineFinalComponent.cs +++ b/src/Umbraco.Web/Search/ExamineFinalComponent.cs @@ -1,159 +1,42 @@ -using System; -using System.Threading; -using Examine; +using Examine; using Umbraco.Core.Logging; using Umbraco.Examine; -using Umbraco.Web.Scheduling; -using System.Threading.Tasks; using Umbraco.Core.Composing; +using Umbraco.Core; namespace Umbraco.Web.Search { + /// /// Executes after all other examine components have executed /// public sealed class ExamineFinalComponent : IComponent - { - - private static volatile bool _isConfigured = false; - private static readonly object IsConfiguredLocker = new object(); + { private readonly IProfilingLogger _logger; private readonly IExamineManager _examineManager; - private static readonly object RebuildLocker = new object(); - private readonly IndexRebuilder _indexRebuilder; - private static BackgroundTaskRunner _rebuildOnStartupRunner; - - public ExamineFinalComponent(IProfilingLogger logger, IExamineManager examineManager, IndexRebuilder indexRebuilder) + BackgroundIndexRebuilder _indexRebuilder; + private readonly IMainDom _mainDom; + + public ExamineFinalComponent(IProfilingLogger logger, IExamineManager examineManager, BackgroundIndexRebuilder indexRebuilder, IMainDom mainDom) { _logger = logger; _examineManager = examineManager; _indexRebuilder = indexRebuilder; + _mainDom = mainDom; } public void Initialize() { - if (!ExamineComponent.ExamineEnabled) return; + if (!_mainDom.IsMainDom) return; - EnsureUnlocked(_logger, _examineManager); + _examineManager.EnsureUnlocked(_mainDom, _logger); // TODO: Instead of waiting 5000 ms, we could add an event handler on to fulfilling the first request, then start? - ExamineComponent.RebuildIndexes(_indexRebuilder, _logger, true, 5000); + _indexRebuilder.RebuildIndexes(true, 5000); } public void Terminate() { } - - /// - /// Called to rebuild empty indexes on startup - /// - /// - /// - /// - /// - internal static void RebuildIndexes(IndexRebuilder indexRebuilder, ILogger logger, bool onlyEmptyIndexes, int waitMilliseconds = 0) - { - // TODO: need a way to disable rebuilding on startup - - lock (RebuildLocker) - { - if (_rebuildOnStartupRunner != null && _rebuildOnStartupRunner.IsRunning) - { - logger.Warn("Call was made to RebuildIndexes but the task runner for rebuilding is already running"); - return; - } - - logger.Info("Starting initialize async background thread."); - //do the rebuild on a managed background thread - var task = new RebuildOnStartupTask(indexRebuilder, logger, onlyEmptyIndexes, waitMilliseconds); - - _rebuildOnStartupRunner = new BackgroundTaskRunner( - "RebuildIndexesOnStartup", - logger); - - _rebuildOnStartupRunner.TryAdd(task); - } - } - - /// - /// Must be called to each index is unlocked before any indexing occurs - /// - /// - /// Indexing rebuilding can occur on a normal boot if the indexes are empty or on a cold boot by the database server messenger. Before - /// either of these happens, we need to configure the indexes. - /// - internal static void EnsureUnlocked(ILogger logger, IExamineManager examineManager) - { - if (!ExamineComponent.ExamineEnabled) return; - if (_isConfigured) return; - - lock (IsConfiguredLocker) - { - //double check - if (_isConfigured) return; - - _isConfigured = true; - examineManager.UnlockLuceneIndexes(logger); - } - } - - /// - /// Background task used to rebuild empty indexes on startup - /// - private class RebuildOnStartupTask : IBackgroundTask - { - private readonly IndexRebuilder _indexRebuilder; - private readonly ILogger _logger; - private readonly bool _onlyEmptyIndexes; - private readonly int _waitMilliseconds; - - public RebuildOnStartupTask(IndexRebuilder indexRebuilder, ILogger logger, bool onlyEmptyIndexes, int waitMilliseconds = 0) - { - _indexRebuilder = indexRebuilder ?? throw new ArgumentNullException(nameof(indexRebuilder)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _onlyEmptyIndexes = onlyEmptyIndexes; - _waitMilliseconds = waitMilliseconds; - } - - public bool IsAsync => false; - - public void Dispose() - { - } - - public void Run() - { - try - { - // rebuilds indexes - RebuildIndexes(); - } - catch (Exception ex) - { - _logger.Error(ex, "Failed to rebuild empty indexes."); - } - } - - public Task RunAsync(CancellationToken token) - { - throw new NotImplementedException(); - } - - /// - /// Used to rebuild indexes on startup or cold boot - /// - private void RebuildIndexes() - { - //do not attempt to do this if this has been disabled since we are not the main dom. - //this can be called during a cold boot - if (!ExamineComponent.ExamineEnabled) return; - - if (_waitMilliseconds > 0) - Thread.Sleep(_waitMilliseconds); - - EnsureUnlocked(_logger, _indexRebuilder.ExamineManager); - _indexRebuilder.RebuildIndexes(_onlyEmptyIndexes); - } - } } } diff --git a/src/Umbraco.Web/Search/ExamineUserComponent.cs b/src/Umbraco.Web/Search/ExamineUserComponent.cs index ddee07543c..35bc3f59ea 100644 --- a/src/Umbraco.Web/Search/ExamineUserComponent.cs +++ b/src/Umbraco.Web/Search/ExamineUserComponent.cs @@ -1,4 +1,5 @@ -using Umbraco.Core.Composing; +using Umbraco.Core; +using Umbraco.Core.Composing; namespace Umbraco.Web.Search { @@ -7,12 +8,19 @@ namespace Umbraco.Web.Search /// public abstract class ExamineUserComponent : IComponent { + private readonly IMainDom _mainDom; + + public ExamineUserComponent(IMainDom mainDom) + { + _mainDom = mainDom; + } + /// /// Initialize the component, eagerly exits if ExamineComponent.ExamineEnabled == false /// public void Initialize() { - if (!ExamineComponent.ExamineEnabled) return; + if (!_mainDom.IsMainDom) return; InitializeComponent(); } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index c9c9a4412a..2365017504 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -233,6 +233,7 @@ + From e4f346620ee051a17357da7ece6ba65500620505 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 5 Aug 2019 10:38:35 +0200 Subject: [PATCH 0087/1001] Added runtime level to ExamineFinalComposer --- src/Umbraco.Web/Search/ExamineFinalComposer.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Search/ExamineFinalComposer.cs b/src/Umbraco.Web/Search/ExamineFinalComposer.cs index 6b33459159..5b6334f1f6 100644 --- a/src/Umbraco.Web/Search/ExamineFinalComposer.cs +++ b/src/Umbraco.Web/Search/ExamineFinalComposer.cs @@ -1,4 +1,5 @@ -using Umbraco.Core.Composing; +using Umbraco.Core; +using Umbraco.Core.Composing; namespace Umbraco.Web.Search { @@ -6,6 +7,7 @@ namespace Umbraco.Web.Search // and *also* after ICoreComposer (in case IUserComposer is disabled) [ComposeAfter(typeof(IUserComposer))] [ComposeAfter(typeof(ICoreComposer))] + [RuntimeLevel(MinLevel = RuntimeLevel.Run)] public class ExamineFinalComposer : ComponentComposer { } } From 467cf8d76235fb421876db66086a33fd7c3cf4e6 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 7 Aug 2019 15:40:27 +1000 Subject: [PATCH 0088/1001] Adds comments/notes --- src/Umbraco.Core/Composing/Lifetime.cs | 67 +++++++++++++++++++------- 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/src/Umbraco.Core/Composing/Lifetime.cs b/src/Umbraco.Core/Composing/Lifetime.cs index 1a2cc3119a..012555be5e 100644 --- a/src/Umbraco.Core/Composing/Lifetime.cs +++ b/src/Umbraco.Core/Composing/Lifetime.cs @@ -12,31 +12,62 @@ /// or MS.DI, PerDependency in Autofac. Transient, + // TODO: We need to fix this up, currently LightInject is the only one that behaves differently from all other containers. + // ... the simple fix would be to map this to PerScopeLifetime in LI but need to wait on a response here https://github.com/seesharper/LightInject/issues/494#issuecomment-518942625 + // + // we use it for controllers, httpContextBase and other request scoped objects: MembershpHelper, TagQuery, UmbracoTreeSearcher and ISearchableTree + // - so that they are automatically disposed at the end of the scope (ie request) + // - not sure they should not be simply 'scoped'? + /// /// One unique instance per request. /// - // TODO: review lifetimes for LightInject vs other containers - // currently, corresponds to 'Request' in LightInject which is 'Transient + disposed by Scope' - // but NOT (in LightInject) a per-web-request lifetime, more a TransientScoped - // - // we use it for controllers, httpContextBase and umbracoContext - // - so that they are automatically disposed at the end of the scope (ie request) - // - not sure they should not be simply 'scoped'? - // - // Castle has an extra PerWebRequest something, and others use scope - // what about Request before first request ie during application startup? - // see http://blog.ploeh.dk/2009/11/17/UsingCastleWindsor'sPerWebRequestlifestylewithASP.NETMVConIIS7/ - // Castle ends up requiring a special scope manager too - // see https://groups.google.com/forum/#!topic/castle-project-users/1E2W9LVIYR4 - // - // but maybe also - why are we requiring scoped services at startup? + /// + /// + /// Any instance created with this lifetime will be disposed at the end of a request. + /// + /// Corresponds to + /// + /// PerRequestLifeTime in LightInject - means transient but disposed at the end of the current web request. + /// see: https://github.com/seesharper/LightInject/issues/494#issuecomment-518493262 + /// + /// + /// Scoped in MS.DI - means one per web request. + /// see https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.2#service-lifetimes + /// + /// InstancePerRequest in Autofac - means one per web request. + /// see https://autofaccn.readthedocs.io/en/latest/lifetime/instance-scope.html#instance-per-request + /// But "Behind the scenes, though, it’s still just instance per matching lifetime scope." + /// + /// + /// LifestylePerWebRequest in Castle Windsor - means one per web request. + /// see https://github.com/castleproject/Windsor/blob/master/docs/mvc-tutorial-part-7-lifestyles.md#the-perwebrequest-lifestyle + /// + /// Request, /// - /// One unique instance per container scope. + /// One unique instance per scope. /// - /// Corresponds to Scope in LightInject, Scoped in MS.DI - /// or Castle Windsor, PerLifetimeScope in Autofac. + /// + /// + /// Any instance created with this lifetime will be disposed at the end of the current scope. + /// + /// Corresponds to + /// PerScopeLifetime in LightInject (when in a request, means one per web request) + /// + /// Scoped in MS.DI (when in a request, means one per web request) + /// see https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.2#service-lifetimes + /// + /// InstancePerLifetimeScope in Autofac (when in a request, means one per web request) + /// see https://autofaccn.readthedocs.io/en/latest/lifetime/instance-scope.html#instance-per-lifetime-scope + /// Also note that Autofac's InstancePerRequest is the same as this, see https://autofaccn.readthedocs.io/en/latest/lifetime/instance-scope.html#instance-per-request + /// it says "Behind the scenes, though, it’s still just instance per matching lifetime scope." + /// + /// + /// LifestyleScoped in Castle Windsor + /// + /// Scope, /// From 0517f2913438638ace044e063facc74d12fb670f Mon Sep 17 00:00:00 2001 From: Linus Elander Date: Wed, 19 Jun 2019 21:41:47 +0200 Subject: [PATCH 0089/1001] Adjusts conditions for which indexes should be affected when a node is unpublished (cherry picked from commit 7557c584d8331482ffb7157dcf2c4d083c5e226d) --- src/Umbraco.Web/Search/ExamineComponent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Search/ExamineComponent.cs b/src/Umbraco.Web/Search/ExamineComponent.cs index fd1eee8f96..d2eaa7aa0c 100644 --- a/src/Umbraco.Web/Search/ExamineComponent.cs +++ b/src/Umbraco.Web/Search/ExamineComponent.cs @@ -674,7 +674,7 @@ namespace Umbraco.Web.Search { var strId = id.ToString(CultureInfo.InvariantCulture); foreach (var index in examineComponent._examineManager.Indexes.OfType() - .Where(x => (keepIfUnpublished && !x.PublishedValuesOnly) || !keepIfUnpublished) + .Where(x => x.PublishedValuesOnly || !keepIfUnpublished) .Where(x => x.EnableDefaultEventHandler)) { index.DeleteFromIndex(strId); From e495faf83c2e927578092f64560d0f6179474bae Mon Sep 17 00:00:00 2001 From: Benjamin Carleski Date: Wed, 7 Aug 2019 10:31:08 -0700 Subject: [PATCH 0090/1001] Fix FIPS compliance, add FIPS tests (#5978) --- .../ControllerTesting/TestRunner.cs | 10 +- src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + .../AuthenticationControllerTests.cs | 139 ++++++++++++++++++ .../Web/Controllers/UsersControllerTests.cs | 97 +++++++++++- .../Models/Mapping/UserMapDefinition.cs | 2 +- 5 files changed, 243 insertions(+), 6 deletions(-) create mode 100644 src/Umbraco.Tests/Web/Controllers/AuthenticationControllerTests.cs diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestRunner.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestRunner.cs index 8c598281dd..9f9f933d72 100644 --- a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestRunner.cs +++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestRunner.cs @@ -25,19 +25,23 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting public async Task> Execute(string controllerName, string actionName, HttpMethod method, HttpContent content = null, MediaTypeWithQualityHeaderValue mediaTypeHeader = null, - bool assertOkResponse = true) + bool assertOkResponse = true, object routeDefaults = null, string url = null) { if (mediaTypeHeader == null) { mediaTypeHeader = new MediaTypeWithQualityHeaderValue("application/json"); } + if (routeDefaults == null) + { + routeDefaults = new { controller = controllerName, action = actionName, id = RouteParameter.Optional }; + } var startup = new TestStartup( configuration => { configuration.Routes.MapHttpRoute("Default", routeTemplate: "{controller}/{action}/{id}", - defaults: new { controller = controllerName, action = actionName, id = RouteParameter.Optional }); + defaults: routeDefaults); }, _controllerFactory); @@ -45,7 +49,7 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting { var request = new HttpRequestMessage { - RequestUri = new Uri("https://testserver/"), + RequestUri = new Uri("https://testserver/" + (url ?? "")), Method = method }; diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 717006b702..f788168ddc 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -243,6 +243,7 @@ + diff --git a/src/Umbraco.Tests/Web/Controllers/AuthenticationControllerTests.cs b/src/Umbraco.Tests/Web/Controllers/AuthenticationControllerTests.cs new file mode 100644 index 0000000000..3d264663b5 --- /dev/null +++ b/src/Umbraco.Tests/Web/Controllers/AuthenticationControllerTests.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Security.Cryptography; +using System.Threading; +using System.Web; +using System.Web.Hosting; +using System.Web.Http; +using Moq; +using Newtonsoft.Json; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Composing; +using Umbraco.Core.Configuration; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Mappers; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.Services; +using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.TestHelpers.ControllerTesting; +using Umbraco.Tests.Testing; +using Umbraco.Web; +using Umbraco.Web.Editors; +using Umbraco.Web.Features; +using Umbraco.Web.Models.ContentEditing; +using IUser = Umbraco.Core.Models.Membership.IUser; + +namespace Umbraco.Tests.Web.Controllers +{ + [TestFixture] + [UmbracoTest(Database = UmbracoTestOptions.Database.None)] + public class AuthenticationControllerTests : TestWithDatabaseBase + { + protected override void ComposeApplication(bool withApplication) + { + base.ComposeApplication(withApplication); + //if (!withApplication) return; + + // replace the true IUserService implementation with a mock + // so that each test can configure the service to their liking + Composition.RegisterUnique(f => Mock.Of()); + + // kill the true IEntityService too + Composition.RegisterUnique(f => Mock.Of()); + + Composition.RegisterUnique(); + } + + + [Test] + public async System.Threading.Tasks.Task GetCurrentUser_Fips() + { + ApiController CtrlFactory(HttpRequestMessage message, IUmbracoContextAccessor umbracoContextAccessor, UmbracoHelper helper) + { + //setup some mocks + var userServiceMock = Mock.Get(Current.Services.UserService); + userServiceMock.Setup(service => service.GetUserById(It.IsAny())) + .Returns(() => null); + + if (Thread.GetDomain().GetData(".appPath") != null) + { + HttpContext.Current = new HttpContext(new SimpleWorkerRequest("", "", new StringWriter())); + } + else + { + var baseDir = IOHelper.MapPath("", false).TrimEnd(IOHelper.DirSepChar); + HttpContext.Current = new HttpContext(new SimpleWorkerRequest("/", baseDir, "", "", new StringWriter())); + } + IOHelper.ForceNotHosted = true; + var usersController = new AuthenticationController( + Factory.GetInstance(), + umbracoContextAccessor, + Factory.GetInstance(), + Factory.GetInstance(), + Factory.GetInstance(), + Factory.GetInstance(), + Factory.GetInstance(), + helper); + return usersController; + } + + Mock.Get(Current.SqlContext) + .Setup(x => x.Query()) + .Returns(new Query(Current.SqlContext)); + + var syntax = new SqlCeSyntaxProvider(); + + Mock.Get(Current.SqlContext) + .Setup(x => x.SqlSyntax) + .Returns(syntax); + + var mappers = new MapperCollection(new[] + { + new UserMapper(new Lazy(() => Current.SqlContext), new ConcurrentDictionary>()) + }); + + Mock.Get(Current.SqlContext) + .Setup(x => x.Mappers) + .Returns(mappers); + + // Testing what happens if the system were configured to only use FIPS-compliant algorithms + var typ = typeof(CryptoConfig); + var flds = typ.GetFields(BindingFlags.Static | BindingFlags.NonPublic); + var haveFld = flds.FirstOrDefault(f => f.Name == "s_haveFipsAlgorithmPolicy"); + var isFld = flds.FirstOrDefault(f => f.Name == "s_fipsAlgorithmPolicy"); + var originalFipsValue = CryptoConfig.AllowOnlyFipsAlgorithms; + + try + { + if (!originalFipsValue) + { + haveFld.SetValue(null, true); + isFld.SetValue(null, true); + } + + var runner = new TestRunner(CtrlFactory); + var response = await runner.Execute("Authentication", "GetCurrentUser", HttpMethod.Get); + + var obj = JsonConvert.DeserializeObject(response.Item2); + Assert.AreEqual(-1, obj.UserId); + } + finally + { + if (!originalFipsValue) + { + haveFld.SetValue(null, false); + isFld.SetValue(null, false); + } + } + } + } +} diff --git a/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs b/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs index a4c3078b8f..85dd303432 100644 --- a/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs @@ -4,6 +4,8 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Net.Http.Formatting; +using System.Reflection; +using System.Security.Cryptography; using System.Web.Http; using Moq; using Newtonsoft.Json; @@ -155,7 +157,7 @@ namespace Umbraco.Tests.Web.Controllers var runner = new TestRunner(CtrlFactory); var response = await runner.Execute("Users", "GetPagedUsers", HttpMethod.Get); - var obj = JsonConvert.DeserializeObject>(response.Item2); + var obj = JsonConvert.DeserializeObject>(response.Item2); Assert.AreEqual(0, obj.TotalItems); } @@ -190,9 +192,100 @@ namespace Umbraco.Tests.Web.Controllers var runner = new TestRunner(CtrlFactory); var response = await runner.Execute("Users", "GetPagedUsers", HttpMethod.Get); - var obj = JsonConvert.DeserializeObject>(response.Item2); + var obj = JsonConvert.DeserializeObject>(response.Item2); Assert.AreEqual(10, obj.TotalItems); Assert.AreEqual(10, obj.Items.Count()); } + + [Test] + public async System.Threading.Tasks.Task GetPagedUsers_Fips() + { + await RunFipsTest("GetPagedUsers", mock => + { + var users = MockedUser.CreateMulipleUsers(10); + long outVal = 10; + mock.Setup(service => service.GetAll( + It.IsAny(), It.IsAny(), out outVal, It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())) + .Returns(() => users); + }, response => + { + var obj = JsonConvert.DeserializeObject>(response.Item2); + Assert.AreEqual(10, obj.TotalItems); + Assert.AreEqual(10, obj.Items.Count()); + }); + } + + [Test] + public async System.Threading.Tasks.Task GetById_Fips() + { + const int mockUserId = 1234; + var user = MockedUser.CreateUser(); + + await RunFipsTest("GetById", mock => + { + mock.Setup(service => service.GetUserById(1234)) + .Returns((int i) => i == mockUserId ? user : null); + }, response => + { + var obj = JsonConvert.DeserializeObject(response.Item2); + Assert.AreEqual(user.Username, obj.Username); + Assert.AreEqual(user.Email, obj.Email); + }, new { controller = "Users", action = "GetById" }, $"Users/GetById/{mockUserId}"); + } + + + private async System.Threading.Tasks.Task RunFipsTest(string action, Action> userServiceSetup, + Action> verification, + object routeDefaults = null, string url = null) + { + ApiController CtrlFactory(HttpRequestMessage message, IUmbracoContextAccessor umbracoContextAccessor, UmbracoHelper helper) + { + //setup some mocks + var userServiceMock = Mock.Get(Current.Services.UserService); + userServiceSetup(userServiceMock); + + var usersController = new UsersController( + Factory.GetInstance(), + umbracoContextAccessor, + Factory.GetInstance(), + Factory.GetInstance(), + Factory.GetInstance(), + Factory.GetInstance(), + Factory.GetInstance(), + helper); + return usersController; + } + + // Testing what happens if the system were configured to only use FIPS-compliant algorithms + var typ = typeof(CryptoConfig); + var flds = typ.GetFields(BindingFlags.Static | BindingFlags.NonPublic); + var haveFld = flds.FirstOrDefault(f => f.Name == "s_haveFipsAlgorithmPolicy"); + var isFld = flds.FirstOrDefault(f => f.Name == "s_fipsAlgorithmPolicy"); + var originalFipsValue = CryptoConfig.AllowOnlyFipsAlgorithms; + + try + { + if (!originalFipsValue) + { + haveFld.SetValue(null, true); + isFld.SetValue(null, true); + } + + MockForGetPagedUsers(); + + var runner = new TestRunner(CtrlFactory); + var response = await runner.Execute("Users", action, HttpMethod.Get, routeDefaults: routeDefaults, url: url); + verification(response); + } + finally + { + if (!originalFipsValue) + { + haveFld.SetValue(null, false); + isFld.SetValue(null, false); + } + } + } } } diff --git a/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs index 3860d5d525..4acda1e552 100644 --- a/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs @@ -301,7 +301,7 @@ namespace Umbraco.Web.Models.Mapping target.Avatars = source.GetUserAvatarUrls(_appCaches.RuntimeCache); target.Culture = source.GetUserCulture(_textService, _globalSettings).ToString(); target.Email = source.Email; - target.EmailHash = source.Email.ToLowerInvariant().Trim().ToMd5(); + target.EmailHash = source.Email.ToLowerInvariant().Trim().GenerateHash(); target.Id = source.Id; target.Key = source.Key; target.LastLoginDate = source.LastLoginDate == default ? null : (DateTime?) source.LastLoginDate; From 289d50d170e90e7c2ba7193bae12e08790e86f3f Mon Sep 17 00:00:00 2001 From: Benjamin Carleski Date: Wed, 7 Aug 2019 10:31:08 -0700 Subject: [PATCH 0091/1001] Fix FIPS compliance, add FIPS tests (#5978) (cherry picked from commit e495faf83c2e927578092f64560d0f6179474bae) --- .../ControllerTesting/TestRunner.cs | 10 +- src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + .../AuthenticationControllerTests.cs | 139 ++++++++++++++++++ .../Web/Controllers/UsersControllerTests.cs | 97 +++++++++++- .../Models/Mapping/UserMapDefinition.cs | 2 +- 5 files changed, 243 insertions(+), 6 deletions(-) create mode 100644 src/Umbraco.Tests/Web/Controllers/AuthenticationControllerTests.cs diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestRunner.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestRunner.cs index 8c598281dd..9f9f933d72 100644 --- a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestRunner.cs +++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestRunner.cs @@ -25,19 +25,23 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting public async Task> Execute(string controllerName, string actionName, HttpMethod method, HttpContent content = null, MediaTypeWithQualityHeaderValue mediaTypeHeader = null, - bool assertOkResponse = true) + bool assertOkResponse = true, object routeDefaults = null, string url = null) { if (mediaTypeHeader == null) { mediaTypeHeader = new MediaTypeWithQualityHeaderValue("application/json"); } + if (routeDefaults == null) + { + routeDefaults = new { controller = controllerName, action = actionName, id = RouteParameter.Optional }; + } var startup = new TestStartup( configuration => { configuration.Routes.MapHttpRoute("Default", routeTemplate: "{controller}/{action}/{id}", - defaults: new { controller = controllerName, action = actionName, id = RouteParameter.Optional }); + defaults: routeDefaults); }, _controllerFactory); @@ -45,7 +49,7 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting { var request = new HttpRequestMessage { - RequestUri = new Uri("https://testserver/"), + RequestUri = new Uri("https://testserver/" + (url ?? "")), Method = method }; diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 717006b702..f788168ddc 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -243,6 +243,7 @@ + diff --git a/src/Umbraco.Tests/Web/Controllers/AuthenticationControllerTests.cs b/src/Umbraco.Tests/Web/Controllers/AuthenticationControllerTests.cs new file mode 100644 index 0000000000..3d264663b5 --- /dev/null +++ b/src/Umbraco.Tests/Web/Controllers/AuthenticationControllerTests.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Security.Cryptography; +using System.Threading; +using System.Web; +using System.Web.Hosting; +using System.Web.Http; +using Moq; +using Newtonsoft.Json; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Composing; +using Umbraco.Core.Configuration; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Mappers; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.Services; +using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.TestHelpers.ControllerTesting; +using Umbraco.Tests.Testing; +using Umbraco.Web; +using Umbraco.Web.Editors; +using Umbraco.Web.Features; +using Umbraco.Web.Models.ContentEditing; +using IUser = Umbraco.Core.Models.Membership.IUser; + +namespace Umbraco.Tests.Web.Controllers +{ + [TestFixture] + [UmbracoTest(Database = UmbracoTestOptions.Database.None)] + public class AuthenticationControllerTests : TestWithDatabaseBase + { + protected override void ComposeApplication(bool withApplication) + { + base.ComposeApplication(withApplication); + //if (!withApplication) return; + + // replace the true IUserService implementation with a mock + // so that each test can configure the service to their liking + Composition.RegisterUnique(f => Mock.Of()); + + // kill the true IEntityService too + Composition.RegisterUnique(f => Mock.Of()); + + Composition.RegisterUnique(); + } + + + [Test] + public async System.Threading.Tasks.Task GetCurrentUser_Fips() + { + ApiController CtrlFactory(HttpRequestMessage message, IUmbracoContextAccessor umbracoContextAccessor, UmbracoHelper helper) + { + //setup some mocks + var userServiceMock = Mock.Get(Current.Services.UserService); + userServiceMock.Setup(service => service.GetUserById(It.IsAny())) + .Returns(() => null); + + if (Thread.GetDomain().GetData(".appPath") != null) + { + HttpContext.Current = new HttpContext(new SimpleWorkerRequest("", "", new StringWriter())); + } + else + { + var baseDir = IOHelper.MapPath("", false).TrimEnd(IOHelper.DirSepChar); + HttpContext.Current = new HttpContext(new SimpleWorkerRequest("/", baseDir, "", "", new StringWriter())); + } + IOHelper.ForceNotHosted = true; + var usersController = new AuthenticationController( + Factory.GetInstance(), + umbracoContextAccessor, + Factory.GetInstance(), + Factory.GetInstance(), + Factory.GetInstance(), + Factory.GetInstance(), + Factory.GetInstance(), + helper); + return usersController; + } + + Mock.Get(Current.SqlContext) + .Setup(x => x.Query()) + .Returns(new Query(Current.SqlContext)); + + var syntax = new SqlCeSyntaxProvider(); + + Mock.Get(Current.SqlContext) + .Setup(x => x.SqlSyntax) + .Returns(syntax); + + var mappers = new MapperCollection(new[] + { + new UserMapper(new Lazy(() => Current.SqlContext), new ConcurrentDictionary>()) + }); + + Mock.Get(Current.SqlContext) + .Setup(x => x.Mappers) + .Returns(mappers); + + // Testing what happens if the system were configured to only use FIPS-compliant algorithms + var typ = typeof(CryptoConfig); + var flds = typ.GetFields(BindingFlags.Static | BindingFlags.NonPublic); + var haveFld = flds.FirstOrDefault(f => f.Name == "s_haveFipsAlgorithmPolicy"); + var isFld = flds.FirstOrDefault(f => f.Name == "s_fipsAlgorithmPolicy"); + var originalFipsValue = CryptoConfig.AllowOnlyFipsAlgorithms; + + try + { + if (!originalFipsValue) + { + haveFld.SetValue(null, true); + isFld.SetValue(null, true); + } + + var runner = new TestRunner(CtrlFactory); + var response = await runner.Execute("Authentication", "GetCurrentUser", HttpMethod.Get); + + var obj = JsonConvert.DeserializeObject(response.Item2); + Assert.AreEqual(-1, obj.UserId); + } + finally + { + if (!originalFipsValue) + { + haveFld.SetValue(null, false); + isFld.SetValue(null, false); + } + } + } + } +} diff --git a/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs b/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs index a4c3078b8f..85dd303432 100644 --- a/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/UsersControllerTests.cs @@ -4,6 +4,8 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Net.Http.Formatting; +using System.Reflection; +using System.Security.Cryptography; using System.Web.Http; using Moq; using Newtonsoft.Json; @@ -155,7 +157,7 @@ namespace Umbraco.Tests.Web.Controllers var runner = new TestRunner(CtrlFactory); var response = await runner.Execute("Users", "GetPagedUsers", HttpMethod.Get); - var obj = JsonConvert.DeserializeObject>(response.Item2); + var obj = JsonConvert.DeserializeObject>(response.Item2); Assert.AreEqual(0, obj.TotalItems); } @@ -190,9 +192,100 @@ namespace Umbraco.Tests.Web.Controllers var runner = new TestRunner(CtrlFactory); var response = await runner.Execute("Users", "GetPagedUsers", HttpMethod.Get); - var obj = JsonConvert.DeserializeObject>(response.Item2); + var obj = JsonConvert.DeserializeObject>(response.Item2); Assert.AreEqual(10, obj.TotalItems); Assert.AreEqual(10, obj.Items.Count()); } + + [Test] + public async System.Threading.Tasks.Task GetPagedUsers_Fips() + { + await RunFipsTest("GetPagedUsers", mock => + { + var users = MockedUser.CreateMulipleUsers(10); + long outVal = 10; + mock.Setup(service => service.GetAll( + It.IsAny(), It.IsAny(), out outVal, It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())) + .Returns(() => users); + }, response => + { + var obj = JsonConvert.DeserializeObject>(response.Item2); + Assert.AreEqual(10, obj.TotalItems); + Assert.AreEqual(10, obj.Items.Count()); + }); + } + + [Test] + public async System.Threading.Tasks.Task GetById_Fips() + { + const int mockUserId = 1234; + var user = MockedUser.CreateUser(); + + await RunFipsTest("GetById", mock => + { + mock.Setup(service => service.GetUserById(1234)) + .Returns((int i) => i == mockUserId ? user : null); + }, response => + { + var obj = JsonConvert.DeserializeObject(response.Item2); + Assert.AreEqual(user.Username, obj.Username); + Assert.AreEqual(user.Email, obj.Email); + }, new { controller = "Users", action = "GetById" }, $"Users/GetById/{mockUserId}"); + } + + + private async System.Threading.Tasks.Task RunFipsTest(string action, Action> userServiceSetup, + Action> verification, + object routeDefaults = null, string url = null) + { + ApiController CtrlFactory(HttpRequestMessage message, IUmbracoContextAccessor umbracoContextAccessor, UmbracoHelper helper) + { + //setup some mocks + var userServiceMock = Mock.Get(Current.Services.UserService); + userServiceSetup(userServiceMock); + + var usersController = new UsersController( + Factory.GetInstance(), + umbracoContextAccessor, + Factory.GetInstance(), + Factory.GetInstance(), + Factory.GetInstance(), + Factory.GetInstance(), + Factory.GetInstance(), + helper); + return usersController; + } + + // Testing what happens if the system were configured to only use FIPS-compliant algorithms + var typ = typeof(CryptoConfig); + var flds = typ.GetFields(BindingFlags.Static | BindingFlags.NonPublic); + var haveFld = flds.FirstOrDefault(f => f.Name == "s_haveFipsAlgorithmPolicy"); + var isFld = flds.FirstOrDefault(f => f.Name == "s_fipsAlgorithmPolicy"); + var originalFipsValue = CryptoConfig.AllowOnlyFipsAlgorithms; + + try + { + if (!originalFipsValue) + { + haveFld.SetValue(null, true); + isFld.SetValue(null, true); + } + + MockForGetPagedUsers(); + + var runner = new TestRunner(CtrlFactory); + var response = await runner.Execute("Users", action, HttpMethod.Get, routeDefaults: routeDefaults, url: url); + verification(response); + } + finally + { + if (!originalFipsValue) + { + haveFld.SetValue(null, false); + isFld.SetValue(null, false); + } + } + } } } diff --git a/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs index 3860d5d525..4acda1e552 100644 --- a/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/UserMapDefinition.cs @@ -301,7 +301,7 @@ namespace Umbraco.Web.Models.Mapping target.Avatars = source.GetUserAvatarUrls(_appCaches.RuntimeCache); target.Culture = source.GetUserCulture(_textService, _globalSettings).ToString(); target.Email = source.Email; - target.EmailHash = source.Email.ToLowerInvariant().Trim().ToMd5(); + target.EmailHash = source.Email.ToLowerInvariant().Trim().GenerateHash(); target.Id = source.Id; target.Key = source.Key; target.LastLoginDate = source.LastLoginDate == default ? null : (DateTime?) source.LastLoginDate; From 6c0fb2f849fa9287c4bc1ff218a6aeb9172e8a62 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 7 Aug 2019 19:56:30 +0200 Subject: [PATCH 0092/1001] Fix the create context menu overflows introduced with 405e9a79 (#5754) --- .../src/views/contentblueprints/create.html | 4 ++-- .../src/views/datatypes/create.html | 8 ++++---- .../src/views/documenttypes/create.html | 16 +++++++-------- .../src/views/media/create.html | 4 ++-- .../src/views/mediatypes/create.html | 8 ++++---- .../src/views/member/create.html | 4 ++-- .../src/views/membertypes/create.html | 8 ++++---- .../src/views/partialviewmacros/create.html | 20 +++++++++---------- .../src/views/partialviews/create.html | 16 +++++++-------- .../src/views/scripts/create.html | 8 ++++---- .../src/views/stylesheets/create.html | 12 +++++------ 11 files changed, 54 insertions(+), 54 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/contentblueprints/create.html b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/create.html index 24cfe5d0d1..6146c007b1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/contentblueprints/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/create.html @@ -11,7 +11,7 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/datatypes/create.html b/src/Umbraco.Web.UI.Client/src/views/datatypes/create.html index c359e7bfd1..55039831ac 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatypes/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/datatypes/create.html @@ -6,20 +6,20 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.html index 4085e84ccd..2241f852b5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.html @@ -7,36 +7,36 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/media/create.html b/src/Umbraco.Web.UI.Client/src/views/media/create.html index d93d4f0e30..57c8f48eb4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/media/create.html @@ -17,13 +17,13 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/media/create.html b/src/Umbraco.Web.UI.Client/src/views/media/create.html index d93d4f0e30..57c8f48eb4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/media/create.html @@ -17,13 +17,13 @@ /// The text. - /// The text with text line breaks replaced with HTML line breaks (
)
+ /// The text with text line breaks replaced with HTML line breaks (<br />). + [Obsolete("This method doesn't HTML encode the text. Use ReplaceLineBreaks instead.")] public HtmlString ReplaceLineBreaksForHtml(string text) { - return new HtmlString(text.Replace("\r\n", @"
").Replace("\n", @"
").Replace("\r", @"
")); + return new HtmlString(text.Replace("\r\n", @"
").Replace("\n", @"
").Replace("\r", @"
")); + } + + /// + /// HTML encodes the text and replaces text line breaks with HTML line breaks. + /// + /// The text. + /// The HTML encoded text with text line breaks replaced with HTML line breaks (<br />). + public IHtmlString ReplaceLineBreaks(string text) + { + var value = HttpUtility.HtmlEncode(text)? + .Replace("\r\n", "
") + .Replace("\r", "
") + .Replace("\n", "
"); + + return new HtmlString(value); } public HtmlString StripHtmlTags(string html, params string[] tags) From 24efa54481f98078740e20a5e84f70f587df2ab6 Mon Sep 17 00:00:00 2001 From: Kasper Christensen Date: Mon, 2 Sep 2019 10:00:44 +0200 Subject: [PATCH 0310/1001] =?UTF-8?q?Loginpage:=20Added=20an=20id=20to=20i?= =?UTF-8?q?nput=20fields=20and=20a=20for=20attr=20on=20the=20la=E2=80=A6?= =?UTF-8?q?=20(#6247)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/application/umb-login.html | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html index bc27196466..676b2efc38 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html @@ -18,11 +18,11 @@

-
- - + + Required The confirmed password doesn't match the new password! @@ -152,13 +152,13 @@
- - + +
- - + +
Show password @@ -189,8 +189,8 @@
- - + +
@@ -220,13 +220,13 @@
- - + +
- - + +
From da0ace18e69879471a55472e391aa7b98c96cb4d Mon Sep 17 00:00:00 2001 From: Kasper Christensen Date: Mon, 2 Sep 2019 10:00:44 +0200 Subject: [PATCH 0311/1001] =?UTF-8?q?Loginpage:=20Added=20an=20id=20to=20i?= =?UTF-8?q?nput=20fields=20and=20a=20for=20attr=20on=20the=20la=E2=80=A6?= =?UTF-8?q?=20(#6247)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit 24efa54481f98078740e20a5e84f70f587df2ab6) --- .../components/application/umb-login.html | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html index bc27196466..676b2efc38 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-login.html @@ -18,11 +18,11 @@

-
- - + + Required The confirmed password doesn't match the new password! @@ -152,13 +152,13 @@
- - + +
- - + +
Show password @@ -189,8 +189,8 @@
- - + +
@@ -220,13 +220,13 @@
- - + +
- - + +
From 3d3a836c5281de142d36febe49ced5b845b6b251 Mon Sep 17 00:00:00 2001 From: Poornima Nayar Date: Fri, 30 Aug 2019 23:19:54 +0100 Subject: [PATCH 0312/1001] fix to the documentation link in create packages opening in same window --- src/Umbraco.Web.UI.Client/src/views/packages/edit.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/packages/edit.html b/src/Umbraco.Web.UI.Client/src/views/packages/edit.html index 2df95cec0d..7a95fd9214 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packages/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/packages/edit.html @@ -298,7 +298,7 @@ description="Here you can add custom installer / uninstaller events to perform certain tasks during installation and uninstallation. All actions are formed as a xml node, containing data for the action to be performed.">
- Documentation + Documentation
From 1963dbe6394f3f56a329f3b0288936642fec8897 Mon Sep 17 00:00:00 2001 From: Poornima Nayar Date: Fri, 30 Aug 2019 23:19:54 +0100 Subject: [PATCH 0313/1001] fix to the documentation link in create packages opening in same window (cherry picked from commit 3d3a836c5281de142d36febe49ced5b845b6b251) --- src/Umbraco.Web.UI.Client/src/views/packages/edit.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/packages/edit.html b/src/Umbraco.Web.UI.Client/src/views/packages/edit.html index 2df95cec0d..7a95fd9214 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packages/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/packages/edit.html @@ -298,7 +298,7 @@ description="Here you can add custom installer / uninstaller events to perform certain tasks during installation and uninstallation. All actions are formed as a xml node, containing data for the action to be performed.">
- Documentation + Documentation
From 157c0a2479b4566901b9bfa2d80f92dab8354fa1 Mon Sep 17 00:00:00 2001 From: Poornima Nayar Date: Sat, 31 Aug 2019 21:22:12 +0100 Subject: [PATCH 0314/1001] Added Review process file --- .github/REVIEW_PROCESS.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/REVIEW_PROCESS.md diff --git a/.github/REVIEW_PROCESS.md b/.github/REVIEW_PROCESS.md new file mode 100644 index 0000000000..917d25b090 --- /dev/null +++ b/.github/REVIEW_PROCESS.md @@ -0,0 +1,25 @@ +# Review process + +You're an awesome person and have sent us your contribution in the form of a pull request! It's now time to relax for a bit and wait for our response. + +In order to set some expectations, here's what happens next. + +## Review process + +You will get an initial reply within 48 hours (workdays) to acknowledge that we’ve seen your PR and we’ll pick it up as soon as we can. + +You will get feedback within at most 14 days after opening the PR. You’ll most likely get feedback sooner though. Then there are a few possible outcomes: + +- Your proposed change is awesome! We merge it in and it will be included in the next minor release of Umbraco +- If the change is a high priority bug fix, we will cherry-pick it into the next patch release as well so that we can release it as soon as possible +- Your proposed change is awesome but needs a bit more work, we’ll give you feedback on the changes we’d like to see +- Your proposed change is awesome but.. not something we’re looking to include at this point. We’ll close your PR and the related issue (we’ll be nice about it!) + +## Are you still available? + +We understand you have other things to do and can't just drop everything to help us out. +So if we’re asking for your help to improve the PR we’ll wait for two weeks to give you a fair chance to make changes. We’ll ask for an update if we don’t hear back from you after that time. + +If we don’t hear back from you for 4 weeks, we’ll close the PR so that it doesn’t just hang around forever. You’re very welcome to re-open it once you have some more time to spend on it. + +There will be times that we really like your proposed changes and we’ll finish the final improvements we’d like to see ourselves. You still get the credits and your commits will live on in the git repository. \ No newline at end of file From 087122868557f28d1f5d0b6f2a372eaf52e3ab22 Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Fri, 16 Aug 2019 11:52:34 +0200 Subject: [PATCH 0315/1001] v8: Add umbLoader directive (#4987) (cherry picked from commit e4f19f71926ff1c0db427d6604798795fbe88f8c) --- .../components/umbloader.directive.js | 75 +++++++++++++++++++ .../components/umbloadindicator.directive.js | 2 +- src/Umbraco.Web.UI.Client/src/less/belle.less | 1 + .../less/components/application/umb-tour.less | 12 ++- .../src/less/components/umb-loader.less | 42 +++++++++++ src/Umbraco.Web.UI.Client/src/less/main.less | 36 --------- .../components/application/umb-tour.html | 3 +- .../src/views/components/umb-loader.html | 3 + .../src/views/content/copy.html | 4 +- .../src/views/content/emptyrecyclebin.html | 5 +- .../src/views/content/move.html | 4 +- .../src/views/datatypes/move.html | 4 +- .../src/views/documenttypes/copy.html | 4 +- .../src/views/documenttypes/move.html | 4 +- .../src/views/media/emptyrecyclebin.html | 5 +- .../src/views/mediatypes/copy.html | 4 +- .../src/views/mediatypes/move.html | 4 +- .../propertyeditors/listview/listview.html | 5 +- 18 files changed, 145 insertions(+), 72 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/common/directives/components/umbloader.directive.js create mode 100644 src/Umbraco.Web.UI.Client/src/less/components/umb-loader.less create mode 100644 src/Umbraco.Web.UI.Client/src/views/components/umb-loader.html diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbloader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbloader.directive.js new file mode 100644 index 0000000000..e70f7b3cac --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbloader.directive.js @@ -0,0 +1,75 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbLoader +@restrict E + +@description +Use this directive to generate a loading indicator. + +

Markup example

+
+    
+ + + + +
+

{{content}}

+
+ +
+
+ +

Controller example

+
+    (function () {
+        "use strict";
+
+        function Controller(myService) {
+
+            var vm = this;
+
+            vm.content = "";
+            vm.loading = true;
+
+            myService.getContent().then(function(content){
+                vm.content = content;
+                vm.loading = false;
+            });
+
+        }
+
+        angular.module("umbraco").controller("My.Controller", Controller);
+    })();
+
+ +@param {string=} position The loader position ("top", "bottom"). + +**/ + +(function() { + 'use strict'; + + function UmbLoaderDirective() { + + function link(scope, el, attr, ctrl) { + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-loader.html', + scope: { + position: "@?" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbLoader', UmbLoaderDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbloadindicator.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbloadindicator.directive.js index 0671770796..c45b8f3f47 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbloadindicator.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbloadindicator.directive.js @@ -39,7 +39,7 @@ Use this directive to generate a loading indicator. }); } -½ + angular.module("umbraco").controller("My.Controller", Controller); })(); diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index bd1cdd5b4f..cf5ccb3d00 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -124,6 +124,7 @@ @import "components/umb-form-check.less"; @import "components/umb-locked-field.less"; @import "components/umb-tabs.less"; +@import "components/umb-loader.less"; @import "components/umb-load-indicator.less"; @import "components/umb-breadcrumbs.less"; @import "components/umb-media-grid.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less index 315cd91dbd..33a723a3f7 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less @@ -1,8 +1,12 @@ -.umb-tour__loader { - background: @white; - z-index: @zindexTourModal; +.umb-loader-wrapper.umb-tour__loader { + margin: 0; position: fixed; - height: 5px; + z-index: @zindexTourModal; + + .umb-loader { + background-color: @white; + height: 5px; + } } .umb-tour__pulse { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-loader.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-loader.less new file mode 100644 index 0000000000..260710ce72 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-loader.less @@ -0,0 +1,42 @@ +// Loading Animation +// ------------------------ + +.umb-loader { + background-color: @blue; + margin-top: 0; + margin-left: -100%; + animation-name: bounce_loadingProgressG; + animation-duration: 1s; + animation-iteration-count: infinite; + animation-timing-function: linear; + width: 100%; + height: 2px; +} + +@keyframes bounce_loadingProgressG { + 0% { + margin-left: -100%; + } + + 100% { + margin-left: 100%; + } +} + +.umb-loader-wrapper { + position: absolute; + right: 0; + left: 0; + margin: 10px 0; + overflow: hidden; +} + +.umb-loader-wrapper.-top { + top: 0; + bottom: auto; +} + +.umb-loader-wrapper.-bottom { + top: auto; + bottom: 0; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/main.less b/src/Umbraco.Web.UI.Client/src/less/main.less index 9f9bbce310..2134514b4d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/main.less +++ b/src/Umbraco.Web.UI.Client/src/less/main.less @@ -486,42 +486,6 @@ table thead a { color:@green; } -// Loading Animation -// ------------------------ - -.umb-loader{ - background-color: @blue; - margin-top:0; - margin-left:-100%; - animation-name:bounce_loadingProgressG; - animation-duration:1s; - animation-iteration-count:infinite; - animation-timing-function:linear; - width:100%; - height:2px; -} - -@keyframes bounce_loadingProgressG{ - 0%{ - margin-left:-100%; - } - 100%{ - margin-left:100%; - } -} - -.umb-loader-wrapper { - position: absolute; - right: 0; - left: 0; - margin: 10px 0; - overflow: hidden; -} - -.umb-loader-wrapper.-bottom { - bottom: 0; -} - // Helpers .strong { diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html index b756d23b50..2910986048 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html @@ -1,6 +1,7 @@
-
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-loader.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-loader.html new file mode 100644 index 0000000000..07aeb7fdfa --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-loader.html @@ -0,0 +1,3 @@ +
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/copy.html b/src/Umbraco.Web.UI.Client/src/views/content/copy.html index 0ebe577ed8..111dacd7cb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/copy.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/copy.html @@ -24,9 +24,7 @@ to in the tree structure below

-
-
-
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/emptyrecyclebin.html b/src/Umbraco.Web.UI.Client/src/views/content/emptyrecyclebin.html index 524bb06354..9ac7ef10fc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/emptyrecyclebin.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/emptyrecyclebin.html @@ -1,9 +1,8 @@
-
-
-
+ +

When items are deleted from the recycle bin, they will be gone forever. diff --git a/src/Umbraco.Web.UI.Client/src/views/content/move.html b/src/Umbraco.Web.UI.Client/src/views/content/move.html index fb72c38974..0bc2012ad3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/move.html @@ -24,9 +24,7 @@ to in the tree structure below

-
-
-
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/datatypes/move.html b/src/Umbraco.Web.UI.Client/src/views/datatypes/move.html index 2723dd305d..dd943bf832 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatypes/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/datatypes/move.html @@ -7,9 +7,7 @@ Select the folder to move {{source.name}} to in the tree structure below

-
-
-
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.html index 4b90c244e8..7ea990e0bf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.html +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.html @@ -7,9 +7,7 @@ Select the folder to copy {{source.name}} to in the tree structure below

-
-
-
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.html index 6b81c15bc5..49fdae9c92 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.html @@ -7,9 +7,7 @@ Select the folder to move {{source.name}} to in the tree structure below

-
-
-
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/media/emptyrecyclebin.html b/src/Umbraco.Web.UI.Client/src/views/media/emptyrecyclebin.html index 9d1b28edc9..33681f9269 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/emptyrecyclebin.html +++ b/src/Umbraco.Web.UI.Client/src/views/media/emptyrecyclebin.html @@ -2,9 +2,8 @@
-
-
-
+ +

When items are deleted from the recycle bin, they will be gone forever. diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/copy.html b/src/Umbraco.Web.UI.Client/src/views/mediatypes/copy.html index 05075ecbec..bd54dfe4a1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediatypes/copy.html +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/copy.html @@ -7,9 +7,7 @@ Select the folder to copy {{source.name}} to in the tree structure below

-
-
-
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/move.html b/src/Umbraco.Web.UI.Client/src/views/mediatypes/move.html index 01d5ade7b1..4d4a840850 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediatypes/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/move.html @@ -7,9 +7,7 @@ Select the folder to move {{source.name}} to in the tree structure below

-
-
-
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html index e971897f2a..0770081aba 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html @@ -95,9 +95,8 @@ {{ selectedItemsCount() }} of {{ listViewResultSet.items.length }} selected -
-
-
+ + From 6588fa2e379bf7be38c63595540729cc084cb9d3 Mon Sep 17 00:00:00 2001 From: Liam Laverty Date: Tue, 3 Sep 2019 15:28:08 +0100 Subject: [PATCH 0316/1001] updated to 700 people --- src/Umbraco.Web.UI.Client/src/installer/installer.service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/installer/installer.service.js b/src/Umbraco.Web.UI.Client/src/installer/installer.service.js index 50b9f2f3c0..f6f162f04f 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/installer.service.js +++ b/src/Umbraco.Web.UI.Client/src/installer/installer.service.js @@ -31,7 +31,7 @@ angular.module("umbraco.install").factory('installerService', function ($rootSco "At least 4 people have the Umbraco logo tattooed on them", "'Umbraco' is the Danish name for an allen key", "Umbraco has been around since 2005, that's a looong time in IT", - "More than 600 people from all over the world meet each year in Denmark in May for our annual conference CodeGarden", + "More than 700 people from all over the world meet each year in Denmark in May for our annual conference CodeGarden", "While you are installing Umbraco someone else on the other side of the planet is probably doing it too", "You can extend Umbraco without modifying the source code using either JavaScript or C#", "Umbraco has been installed in more than 198 countries" From 8b0a64f8cd37d4268de3aef098a38e0127cda1e7 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 4 Sep 2019 14:22:57 +0100 Subject: [PATCH 0317/1001] Use the angular 3rd party lib angular-local-storage (which falls back to cookies) instead of native localStorage --- .../src/common/services/tinymce.service.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index b8a1191175..23104b99f5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -6,7 +6,8 @@ * @description * A service containing all logic for all of the Umbraco TinyMCE plugins */ -function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, stylesheetResource, macroResource, macroService, $routeParams, umbRequestHelper, angularHelper, userService, editorService, entityResource) { +function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, stylesheetResource, macroResource, macroService, + $routeParams, umbRequestHelper, angularHelper, userService, editorService, entityResource, localStorageService) { //These are absolutely required in order for the macros to render inline //we put these as extended elements because they get merged on top of the normal allowed elements by tiny mce @@ -190,7 +191,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s } // Put UDI into localstorage (used to update the img with data-udi later on) - localStorage.setItem(`tinymce__${json.location}`, json.udi); + localStorageService.set(`tinymce__${json.location}`, json.udi); success(json.location); }; @@ -222,13 +223,13 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s var imgSrc = img.getAttribute("src"); //Try & find in localstorage - var udi = localStorage.getItem(`tinymce__${imgSrc}`); + var udi = localStorageService.get(`tinymce__${imgSrc}`); //Select the img & update is attr tinymce.activeEditor.$(img).attr({ "data-udi": udi }); //Remove key - localStorage.removeItem(`tinymce__${imgSrc}`); + localStorageService.remove(`tinymce__${imgSrc}`); }); }); } From 5bbe1faddabec021d599742744adf3c6ad4c0ebf Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Thu, 5 Sep 2019 10:31:08 +0200 Subject: [PATCH 0318/1001] Fixes #6021 - Error on save and publish for existing node after content creation in eventhandler --- src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 8e2cf7bc3c..1bd58c3878 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -712,9 +712,9 @@ namespace Umbraco.Web.PublishedCache.NuCache var id = content.FirstChildContentId; while (id > 0) { - var link = GetLinkedNode(id, "child"); - ClearBranchLocked(link.Value); - id = link.Value.NextSiblingContentId; + var child = GetLinkedNode(id, "child").Value; + ClearBranchLocked(child); + id = child.NextSiblingContentId; } } From d3f3c74e10770d839c4c025e329219bf1d3a89e3 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Thu, 5 Sep 2019 11:47:13 +0200 Subject: [PATCH 0319/1001] Fix #6283 Save Member resets password --- .../src/views/member/member.edit.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js index eb99e46a1f..8d54597791 100644 --- a/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js @@ -133,7 +133,7 @@ function MemberEditController($scope, $routeParams, $location, $q, $window, appS //anytime a user is changing a member's password without the oldPassword, we are in effect resetting it so we need to set that flag here var passwordProp = _.find(contentEditingHelper.getAllProps($scope.content), function (e) { return e.alias === '_umb_password' }); - if (passwordProp && passwordProp.value && !passwordProp.value.reset) { + if (passwordProp && passwordProp.value && (typeof passwordProp.value.reset !== 'undefined') && !passwordProp.value.reset) { //so if the admin is not explicitly resetting the password, flag it for resetting if a new password is being entered passwordProp.value.reset = !passwordProp.value.oldPassword && passwordProp.config.allowManuallyChangingPassword; } From 4b344ac3fdde5801074a1539f83b5e6adfcc51cf Mon Sep 17 00:00:00 2001 From: Jan Skovgaard Date: Thu, 5 Sep 2019 12:29:25 +0200 Subject: [PATCH 0320/1001] Accessibility: Grid filter drop down can't be accessed via keyboard (#5963) --- .../components/events/events.directive.js | 37 +++++++++++++++++++ .../components/umblayoutselector.directive.js | 5 +++ .../less/components/umb-layout-selector.less | 2 + .../views/components/umb-layout-selector.html | 18 ++++++--- src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 3 +- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 3 +- .../Umbraco/config/lang/en_us.xml | 3 +- 7 files changed, 62 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js index 15e74bbd90..53aa7475c4 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js @@ -233,4 +233,41 @@ angular.module('umbraco.directives') }); } }; + }) + + // A slightly modified version of https://github.com/myplanet/angular-deep-blur/blob/master/angular-deep-blur.js - Kudos to Ufuk Kayserilioglu (paracycle) + .directive('deepBlur', function ($timeout) { + return { + + restrict: 'A', + + controller: function ($scope, $element, $attrs) { + var leaveExpr = $attrs.deepBlur, + dom = $element[0]; + + function containsDom(parent, dom) { + while (dom) { + if (dom === parent) { + return true; + } + dom = dom.parentNode; + } + return false; + } + + function onBlur(e) { + var targetElement = e.relatedTarget; + + if (!containsDom(dom, targetElement)) { + $timeout(function () { + $scope.$apply(leaveExpr); + }, 10); + } + } + + dom.addEventListener('blur', onBlur, true); + } + }; }); + + diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblayoutselector.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblayoutselector.directive.js index 58a5e1be0e..7453353018 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblayoutselector.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblayoutselector.directive.js @@ -24,6 +24,7 @@ vm.showLayoutSelector = true; vm.pickLayout = pickLayout; vm.toggleLayoutDropdown = toggleLayoutDropdown; + vm.leaveLayoutDropdown = leaveLayoutDropdown; vm.closeLayoutDropdown = closeLayoutDropdown; function onInit() { @@ -38,6 +39,10 @@ vm.layoutDropDownIsOpen = !vm.layoutDropDownIsOpen; } + function leaveLayoutDropdown() { + vm.layoutDropDownIsOpen = false; + } + function pickLayout(selectedLayout) { if (vm.onLayoutSelect) { vm.onLayoutSelect({ layout: selectedLayout }); diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-layout-selector.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-layout-selector.less index cf407b667f..cdc6cfcb63 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-layout-selector.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-layout-selector.less @@ -4,6 +4,7 @@ } .umb-layout-selector__active-layout { + background: transparent; box-sizing: border-box; border: 1px solid @inputBorder; cursor: pointer; @@ -33,6 +34,7 @@ } .umb-layout-selector__dropdown-item { + background: transparent; padding: 5px; margin: 3px 5px; display: flex; diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-layout-selector.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-layout-selector.html index c84e63a359..c6c841f8b1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-layout-selector.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-layout-selector.html @@ -1,20 +1,26 @@
-
- -
+
+ on-outside-click="vm.closeLayoutDropdown()" + deep-blur="vm.leaveLayoutDropdown()"> -
- + + {{layout.name}}
diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 8d4226fe36..0e43d4217e 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -1613,6 +1613,7 @@ Mange hilsner fra Umbraco robotten Installer Umbraco Forms - Gå tilbage + Gå tilbage + Aktivt layout: diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index a8114faf93..41c77d0c39 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -2131,6 +2131,7 @@ To manage your website, simply open the Umbraco back office and start adding con Install Umbraco Forms - Go back + Go back + Active layout: 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 a933b8507d..d9cea4f14c 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -2145,6 +2145,7 @@ To manage your website, simply open the Umbraco back office and start adding con Install Umbraco Forms - Go back + Go back + Active layout: From e1a438dfc9183737d502d9d53a9c3979220807ad Mon Sep 17 00:00:00 2001 From: Jan Skovgaard Date: Thu, 5 Sep 2019 12:29:25 +0200 Subject: [PATCH 0321/1001] Accessibility: Grid filter drop down can't be accessed via keyboard (#5963) (cherry picked from commit 4b344ac3fdde5801074a1539f83b5e6adfcc51cf) # Conflicts: # src/Umbraco.Web.UI/Umbraco/config/lang/da.xml # src/Umbraco.Web.UI/Umbraco/config/lang/en.xml # src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml --- .../components/events/events.directive.js | 37 +++++++++++++++++++ .../components/umblayoutselector.directive.js | 5 +++ .../less/components/umb-layout-selector.less | 2 + .../views/components/umb-layout-selector.html | 18 ++++++--- src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 6 ++- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 6 ++- .../Umbraco/config/lang/en_us.xml | 6 ++- 7 files changed, 71 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js index 15e74bbd90..53aa7475c4 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js @@ -233,4 +233,41 @@ angular.module('umbraco.directives') }); } }; + }) + + // A slightly modified version of https://github.com/myplanet/angular-deep-blur/blob/master/angular-deep-blur.js - Kudos to Ufuk Kayserilioglu (paracycle) + .directive('deepBlur', function ($timeout) { + return { + + restrict: 'A', + + controller: function ($scope, $element, $attrs) { + var leaveExpr = $attrs.deepBlur, + dom = $element[0]; + + function containsDom(parent, dom) { + while (dom) { + if (dom === parent) { + return true; + } + dom = dom.parentNode; + } + return false; + } + + function onBlur(e) { + var targetElement = e.relatedTarget; + + if (!containsDom(dom, targetElement)) { + $timeout(function () { + $scope.$apply(leaveExpr); + }, 10); + } + } + + dom.addEventListener('blur', onBlur, true); + } + }; }); + + diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblayoutselector.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblayoutselector.directive.js index 58a5e1be0e..7453353018 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblayoutselector.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblayoutselector.directive.js @@ -24,6 +24,7 @@ vm.showLayoutSelector = true; vm.pickLayout = pickLayout; vm.toggleLayoutDropdown = toggleLayoutDropdown; + vm.leaveLayoutDropdown = leaveLayoutDropdown; vm.closeLayoutDropdown = closeLayoutDropdown; function onInit() { @@ -38,6 +39,10 @@ vm.layoutDropDownIsOpen = !vm.layoutDropDownIsOpen; } + function leaveLayoutDropdown() { + vm.layoutDropDownIsOpen = false; + } + function pickLayout(selectedLayout) { if (vm.onLayoutSelect) { vm.onLayoutSelect({ layout: selectedLayout }); diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-layout-selector.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-layout-selector.less index cf407b667f..cdc6cfcb63 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-layout-selector.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-layout-selector.less @@ -4,6 +4,7 @@ } .umb-layout-selector__active-layout { + background: transparent; box-sizing: border-box; border: 1px solid @inputBorder; cursor: pointer; @@ -33,6 +34,7 @@ } .umb-layout-selector__dropdown-item { + background: transparent; padding: 5px; margin: 3px 5px; display: flex; diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-layout-selector.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-layout-selector.html index c84e63a359..c6c841f8b1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-layout-selector.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-layout-selector.html @@ -1,20 +1,26 @@
-
- -
+
+ on-outside-click="vm.closeLayoutDropdown()" + deep-blur="vm.leaveLayoutDropdown()"> -
- + + {{layout.name}}
diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 5ae084bc06..b16fea3301 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -1610,5 +1610,9 @@ Mange hilsner fra Umbraco robotten Profiling Kom godt i gang Installer Umbraco Forms - + + + Gå tilbage + Aktivt layout: + diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index fc8a6cf213..df23ce9204 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -2128,5 +2128,9 @@ To manage your website, simply open the Umbraco back office and start adding con Profiling Getting Started Install Umbraco Forms - > + + + Go back + Active layout: + 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 13ec6f8135..c4e07f18ca 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -2142,5 +2142,9 @@ To manage your website, simply open the Umbraco back office and start adding con Profiling Getting Started Install Umbraco Forms - + + + Go back + Active layout: + From b94d78e01a88435a1f5b0ecb49f9bf50c05e0d85 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Thu, 5 Sep 2019 13:01:05 +0200 Subject: [PATCH 0322/1001] Revert "Accessibility: Grid filter drop down can't be accessed via keyboard (#5963)" This reverts commit e1a438dfc9183737d502d9d53a9c3979220807ad. Should not have been picked into a patch release :) --- .../components/events/events.directive.js | 37 ------------------- .../components/umblayoutselector.directive.js | 5 --- .../less/components/umb-layout-selector.less | 2 - .../views/components/umb-layout-selector.html | 18 +++------ src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 6 +-- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 6 +-- .../Umbraco/config/lang/en_us.xml | 6 +-- 7 files changed, 9 insertions(+), 71 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js index 53aa7475c4..15e74bbd90 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js @@ -233,41 +233,4 @@ angular.module('umbraco.directives') }); } }; - }) - - // A slightly modified version of https://github.com/myplanet/angular-deep-blur/blob/master/angular-deep-blur.js - Kudos to Ufuk Kayserilioglu (paracycle) - .directive('deepBlur', function ($timeout) { - return { - - restrict: 'A', - - controller: function ($scope, $element, $attrs) { - var leaveExpr = $attrs.deepBlur, - dom = $element[0]; - - function containsDom(parent, dom) { - while (dom) { - if (dom === parent) { - return true; - } - dom = dom.parentNode; - } - return false; - } - - function onBlur(e) { - var targetElement = e.relatedTarget; - - if (!containsDom(dom, targetElement)) { - $timeout(function () { - $scope.$apply(leaveExpr); - }, 10); - } - } - - dom.addEventListener('blur', onBlur, true); - } - }; }); - - diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblayoutselector.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblayoutselector.directive.js index 7453353018..58a5e1be0e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblayoutselector.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblayoutselector.directive.js @@ -24,7 +24,6 @@ vm.showLayoutSelector = true; vm.pickLayout = pickLayout; vm.toggleLayoutDropdown = toggleLayoutDropdown; - vm.leaveLayoutDropdown = leaveLayoutDropdown; vm.closeLayoutDropdown = closeLayoutDropdown; function onInit() { @@ -39,10 +38,6 @@ vm.layoutDropDownIsOpen = !vm.layoutDropDownIsOpen; } - function leaveLayoutDropdown() { - vm.layoutDropDownIsOpen = false; - } - function pickLayout(selectedLayout) { if (vm.onLayoutSelect) { vm.onLayoutSelect({ layout: selectedLayout }); diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-layout-selector.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-layout-selector.less index cdc6cfcb63..cf407b667f 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-layout-selector.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-layout-selector.less @@ -4,7 +4,6 @@ } .umb-layout-selector__active-layout { - background: transparent; box-sizing: border-box; border: 1px solid @inputBorder; cursor: pointer; @@ -34,7 +33,6 @@ } .umb-layout-selector__dropdown-item { - background: transparent; padding: 5px; margin: 3px 5px; display: flex; diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-layout-selector.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-layout-selector.html index c6c841f8b1..c84e63a359 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-layout-selector.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-layout-selector.html @@ -1,26 +1,20 @@
- +
+ +
+ on-outside-click="vm.closeLayoutDropdown()"> -
diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index b16fea3301..5ae084bc06 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -1610,9 +1610,5 @@ Mange hilsner fra Umbraco robotten Profiling Kom godt i gang Installer Umbraco Forms - - - Gå tilbage - Aktivt layout: - + diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index df23ce9204..fc8a6cf213 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -2128,9 +2128,5 @@ To manage your website, simply open the Umbraco back office and start adding con Profiling Getting Started Install Umbraco Forms - - - Go back - Active layout: - + > 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 c4e07f18ca..13ec6f8135 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -2142,9 +2142,5 @@ To manage your website, simply open the Umbraco back office and start adding con Profiling Getting Started Install Umbraco Forms - - - Go back - Active layout: - + From 428da6ce1331af62353613366a956335d32222a2 Mon Sep 17 00:00:00 2001 From: Jan Skovgaard Date: Thu, 5 Sep 2019 13:41:21 +0200 Subject: [PATCH 0323/1001] Accessibility: Create drop down can't be accessed via keyboard (#5960) --- .../lib/bootstrap/less/dropdowns.less | 2 + .../lib/bootstrap/less/sprites.less | 4 ++ .../src/less/canvas-designer.less | 10 ++- .../components/buttons/umb-button-group.less | 3 +- src/Umbraco.Web.UI.Client/src/less/navs.less | 22 +++++- .../listview/listview.controller.js | 14 +++- .../propertyeditors/listview/listview.html | 72 +++++++++++-------- 7 files changed, 91 insertions(+), 36 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/lib/bootstrap/less/dropdowns.less b/src/Umbraco.Web.UI.Client/lib/bootstrap/less/dropdowns.less index 7707e04feb..94f229a191 100644 --- a/src/Umbraco.Web.UI.Client/lib/bootstrap/less/dropdowns.less +++ b/src/Umbraco.Web.UI.Client/lib/bootstrap/less/dropdowns.less @@ -80,6 +80,8 @@ // ----------- .dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus, +.dropdown-menu > li > button:hover, +.dropdown-menu > li > button:focus, .dropdown-submenu:hover > a, .dropdown-submenu:focus > a { text-decoration: none; diff --git a/src/Umbraco.Web.UI.Client/lib/bootstrap/less/sprites.less b/src/Umbraco.Web.UI.Client/lib/bootstrap/less/sprites.less index d73e23f5ea..dd60f10cee 100644 --- a/src/Umbraco.Web.UI.Client/lib/bootstrap/less/sprites.less +++ b/src/Umbraco.Web.UI.Client/lib/bootstrap/less/sprites.less @@ -39,6 +39,10 @@ .dropdown-menu > li > a:focus > [class^="icon-"], .dropdown-menu > li > a:hover > [class*=" icon-"], .dropdown-menu > li > a:focus > [class*=" icon-"], +.dropdown-menu > li > button:hover > [class^="icon-"], +.dropdown-menu > li > button:focus > [class^="icon-"], +.dropdown-menu > li > button:hover > [class*=" icon-"], +.dropdown-menu > li > button:focus > [class*=" icon-"], .dropdown-menu > .active > a > [class^="icon-"], .dropdown-menu > .active > a > [class*=" icon-"], .dropdown-submenu:hover > a > [class^="icon-"], diff --git a/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less b/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less index 7440a5723a..4db2e434d2 100644 --- a/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less +++ b/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less @@ -163,7 +163,8 @@ a, a:hover{ background-clip: padding-box; } -.dropdown-menu > li > a { +.dropdown-menu > li > a, +.dropdown-menu > li > button { display: block; padding: 3px 20px; clear: both; @@ -174,7 +175,12 @@ a, a:hover{ cursor:pointer; } -.dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus, .dropdown-submenu:hover > a, .dropdown-submenu:focus > a { +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus, +.dropdown-menu > li > button:hover, +.dropdown-menu > li > button:focus, +.dropdown-submenu:hover > a, +.dropdown-submenu:focus > a { color: #000000; background: #e4e0dd; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button-group.less b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button-group.less index e40282cb58..0465881387 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button-group.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button-group.less @@ -9,6 +9,7 @@ left: auto; } +.umb-button-group__sub-buttons>li>button, .umb-button-group__sub-buttons>li>a { display: flex; } @@ -20,7 +21,7 @@ } .umb-button-group__toggle { - border-radius: 0px @baseBorderRadius @baseBorderRadius 0; + border-radius: 0 @baseBorderRadius @baseBorderRadius 0; border-left: 1px solid rgba(0,0,0,0.09); margin-left: -2px; padding-left: 10px; diff --git a/src/Umbraco.Web.UI.Client/src/less/navs.less b/src/Umbraco.Web.UI.Client/src/less/navs.less index a2710fab6c..5b97464e31 100644 --- a/src/Umbraco.Web.UI.Client/src/less/navs.less +++ b/src/Umbraco.Web.UI.Client/src/less/navs.less @@ -237,7 +237,27 @@ color: @ui-option-type; } -.dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus, .dropdown-submenu:hover > a, .dropdown-submenu:focus > a { +.dropdown-menu > li > button { + background: transparent; + border: 0; + padding: 8px 20px; + color: @ui-option-type; + display: block; + clear: both; + font-weight: normal; + line-height: 20px; + white-space: nowrap; + cursor:pointer; + width: 100%; + text-align: left; +} + +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus, +.dropdown-menu > li > button:hover, +.dropdown-menu > li > button:focus, +.dropdown-submenu:hover > a, +.dropdown-submenu:focus > a { color: @ui-option-type-hover; background: @ui-option-hover; } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js index 2fbd2b0a32..ce0815c640 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js @@ -53,7 +53,9 @@ function listViewController($scope, $routeParams, $injector, $timeout, currentUs $scope.actionInProgress = false; $scope.selection = []; $scope.folders = []; - $scope.page = {}; + $scope.page = { + createDropdownOpen: false + }; $scope.listViewResultSet = { totalPages: 0, items: [] @@ -795,8 +797,18 @@ function listViewController($scope, $routeParams, $injector, $timeout, currentUs .search("blueprintId", blueprintId); } + function toggleDropdown () { + $scope.page.createDropdownOpen = !$scope.page.createDropdownOpen; + } + + function leaveDropdown () { + $scope.page.createDropdownOpen = false; + } + $scope.createBlank = createBlank; $scope.createFromBlueprint = createFromBlueprint; + $scope.toggleDropdown = toggleDropdown; + $scope.leaveDropdown = leaveDropdown; //GO! initView(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html index 0770081aba..304f3f3fbc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html @@ -12,57 +12,68 @@ + -
- - + + - - + + +