From 463dfc45a60593bc3dd40a924b72fbf1b3ec8ce9 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 6 Jun 2019 09:34:27 +0200 Subject: [PATCH 0001/1545] 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/1545] 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/1545] 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/1545] 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/1545] 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/1545] 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 a90f0c407027e7a93aa3923cdbfc45fbad368c5a Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 26 Jun 2019 14:37:02 +0200 Subject: [PATCH 0007/1545] Added custom validation messages to property types: model, dto and migration, update via property type editing, display when content editing. --- .../Migrations/Upgrade/UmbracoPlan.cs | 1 + ...AddPropertyTypeValidationMessageColumns.cs | 20 +++++++ src/Umbraco.Core/Models/PropertyType.cs | 24 ++++++++- .../Packaging/PackageDataInstallation.cs | 2 + .../Persistence/Dtos/PropertyTypeDto.cs | 10 ++++ .../Dtos/PropertyTypeReadOnlyDto.cs | 6 +++ .../Factories/PropertyGroupFactory.cs | 4 ++ .../Persistence/Mappers/PropertyTypeMapper.cs | 2 + .../Implement/ContentTypeCommonRepository.cs | 2 + .../PropertyEditors/DataValueEditor.cs | 6 +-- .../PropertyEditors/IDataValueEditor.cs | 6 ++- .../PropertyEditors/IValueFormatValidator.cs | 5 +- .../IValueRequiredValidator.cs | 5 +- .../Validators/RegexValidator.cs | 19 +++++-- .../Validators/RequiredValidator.cs | 22 ++++++-- .../Services/Implement/EntityXmlSerializer.cs | 4 ++ .../Services/PropertyValidationService.cs | 2 +- src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../Mapping/ContentTypeModelMappingTests.cs | 32 ++++++----- .../components/umbgroupsbuilder.directive.js | 6 ++- .../less/components/umb-group-builder.less | 16 ++++-- src/Umbraco.Web.UI.Client/src/less/forms.less | 3 +- .../propertysettings/propertysettings.html | 53 ++++++++++++------- .../textbox/textbox.controller.js | 11 +++- .../propertyeditors/textbox/textbox.html | 4 +- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 2 + .../Umbraco/config/lang/en_us.xml | 2 + .../Editors/Filters/ContentModelValidator.cs | 2 +- .../ContentEditing/ContentPropertyDto.cs | 7 +++ .../ContentEditing/PropertyTypeValidation.cs | 6 +++ .../Mapping/ContentPropertyDisplayMapper.cs | 2 + .../Mapping/ContentPropertyDtoMapper.cs | 2 + .../Mapping/ContentTypeMapDefinition.cs | 2 + .../Models/Mapping/PropertyTypeGroupMapper.cs | 8 ++- .../NestedContentPropertyEditor.cs | 19 +++++-- .../PropertyEditors/TagsPropertyEditor.cs | 2 +- 36 files changed, 254 insertions(+), 66 deletions(-) create mode 100644 src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/AddPropertyTypeValidationMessageColumns.cs diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index 834eade986..3a3c9cffc5 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -176,6 +176,7 @@ namespace Umbraco.Core.Migrations.Upgrade To("{B69B6E8C-A769-4044-A27E-4A4E18D1645A}"); To("{0372A42B-DECF-498D-B4D1-6379E907EB94}"); To("{5B1E0D93-F5A3-449B-84BA-65366B84E2D4}"); + To("{3D67D2C8-5E65-47D0-A9E1-DC2EE0779D6B}"); //FINAL } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/AddPropertyTypeValidationMessageColumns.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/AddPropertyTypeValidationMessageColumns.cs new file mode 100644 index 0000000000..07514faf75 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/AddPropertyTypeValidationMessageColumns.cs @@ -0,0 +1,20 @@ +using System.Linq; +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_1_0 +{ + public class AddPropertyTypeValidationMessageColumns : MigrationBase + { + public AddPropertyTypeValidationMessageColumns(IMigrationContext context) + : base(context) + { } + + public override void Migrate() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); + + AddColumnIfNotExists(columns, "mandatoryMessage"); + AddColumnIfNotExists(columns, "validationRegExpMessage"); + } + } +} diff --git a/src/Umbraco.Core/Models/PropertyType.cs b/src/Umbraco.Core/Models/PropertyType.cs index 1e950a876c..1ae5133f8d 100644 --- a/src/Umbraco.Core/Models/PropertyType.cs +++ b/src/Umbraco.Core/Models/PropertyType.cs @@ -25,8 +25,10 @@ namespace Umbraco.Core.Models private string _propertyEditorAlias; private ValueStorageType _valueStorageType; private bool _mandatory; + private string _mandatoryMessage; private int _sortOrder; private string _validationRegExp; + private string _validationRegExpMessage; private ContentVariation _variations; /// @@ -175,7 +177,7 @@ namespace Umbraco.Core.Models } /// - /// Gets of sets a value indicating whether a value for this property type is required. + /// Gets or sets a value indicating whether a value for this property type is required. /// [DataMember] public bool Mandatory @@ -184,6 +186,16 @@ namespace Umbraco.Core.Models set => SetPropertyValueAndDetectChanges(value, ref _mandatory, nameof(Mandatory)); } + /// + /// Gets or sets the custom validation message used when a value for this PropertyType is required + /// + [DataMember] + public string MandatoryMessage + { + get => _mandatoryMessage; + set => SetPropertyValueAndDetectChanges(value, ref _mandatoryMessage, nameof(MandatoryMessage)); + } + /// /// Gets of sets the sort order of the property type. /// @@ -204,6 +216,16 @@ namespace Umbraco.Core.Models set => SetPropertyValueAndDetectChanges(value, ref _validationRegExp, nameof(ValidationRegExp)); } + /// + /// Gets or sets the custom validation message used when a pattern for this PropertyType must be matched + /// + [DataMember] + public string ValidationRegExpMessage + { + get => _validationRegExpMessage; + set => SetPropertyValueAndDetectChanges(value, ref _validationRegExpMessage, nameof(ValidationRegExpMessage)); + } + /// /// Gets or sets the content variation of the property type. /// diff --git a/src/Umbraco.Core/Packaging/PackageDataInstallation.cs b/src/Umbraco.Core/Packaging/PackageDataInstallation.cs index c811f484bc..d5738b5e3f 100644 --- a/src/Umbraco.Core/Packaging/PackageDataInstallation.cs +++ b/src/Umbraco.Core/Packaging/PackageDataInstallation.cs @@ -749,7 +749,9 @@ namespace Umbraco.Core.Packaging Name = property.Element("Name").Value, Description = (string)property.Element("Description"), Mandatory = property.Element("Mandatory") != null ? property.Element("Mandatory").Value.ToLowerInvariant().Equals("true") : false, + MandatoryMessage = property.Element("MandatoryMessage") != null ? (string)property.Element("MandatoryMessage") : string.Empty, ValidationRegExp = (string)property.Element("Validation"), + ValidationRegExpMessage = property.Element("ValidationRegExpMessage") != null ? (string)property.Element("ValidationRegExpMessage") : string.Empty, SortOrder = sortOrder }; diff --git a/src/Umbraco.Core/Persistence/Dtos/PropertyTypeDto.cs b/src/Umbraco.Core/Persistence/Dtos/PropertyTypeDto.cs index 8c52aa1e15..3e8d6e7496 100644 --- a/src/Umbraco.Core/Persistence/Dtos/PropertyTypeDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/PropertyTypeDto.cs @@ -43,10 +43,20 @@ namespace Umbraco.Core.Persistence.Dtos [Constraint(Default = "0")] public bool Mandatory { get; set; } + [Column("mandatoryMessage")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(500)] + public string MandatoryMessage { get; set; } + [Column("validationRegExp")] [NullSetting(NullSetting = NullSettings.Null)] public string ValidationRegExp { get; set; } + [Column("validationRegExpMessage")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(500)] + public string ValidationRegExpMessage { get; set; } + [Column("Description")] [NullSetting(NullSetting = NullSettings.Null)] [Length(2000)] diff --git a/src/Umbraco.Core/Persistence/Dtos/PropertyTypeReadOnlyDto.cs b/src/Umbraco.Core/Persistence/Dtos/PropertyTypeReadOnlyDto.cs index c68dee42b5..4c352a0134 100644 --- a/src/Umbraco.Core/Persistence/Dtos/PropertyTypeReadOnlyDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/PropertyTypeReadOnlyDto.cs @@ -32,9 +32,15 @@ namespace Umbraco.Core.Persistence.Dtos [Column("mandatory")] public bool Mandatory { get; set; } + [Column("mandatoryMessage")] + public string MandatoryMessage { get; set; } + [Column("validationRegExp")] public string ValidationRegExp { get; set; } + [Column("validationRegExpMessage")] + public string ValidationRegExpMessage { get; set; } + [Column("Description")] public string Description { get; set; } diff --git a/src/Umbraco.Core/Persistence/Factories/PropertyGroupFactory.cs b/src/Umbraco.Core/Persistence/Factories/PropertyGroupFactory.cs index c134748047..0846d6d1f6 100644 --- a/src/Umbraco.Core/Persistence/Factories/PropertyGroupFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/PropertyGroupFactory.cs @@ -59,8 +59,10 @@ namespace Umbraco.Core.Persistence.Factories propertyType.Key = typeDto.UniqueId; propertyType.Name = typeDto.Name; propertyType.Mandatory = typeDto.Mandatory; + propertyType.MandatoryMessage = typeDto.MandatoryMessage; propertyType.SortOrder = typeDto.SortOrder; propertyType.ValidationRegExp = typeDto.ValidationRegExp; + propertyType.ValidationRegExpMessage = typeDto.ValidationRegExpMessage; propertyType.PropertyGroupId = new Lazy(() => tempGroupDto.Id); propertyType.CreateDate = createDate; propertyType.UpdateDate = updateDate; @@ -123,9 +125,11 @@ namespace Umbraco.Core.Persistence.Factories DataTypeId = propertyType.DataTypeId, Description = propertyType.Description, Mandatory = propertyType.Mandatory, + MandatoryMessage = propertyType.MandatoryMessage, Name = propertyType.Name, SortOrder = propertyType.SortOrder, ValidationRegExp = propertyType.ValidationRegExp, + ValidationRegExpMessage = propertyType.ValidationRegExpMessage, UniqueId = propertyType.Key, Variations = (byte)propertyType.Variations }; diff --git a/src/Umbraco.Core/Persistence/Mappers/PropertyTypeMapper.cs b/src/Umbraco.Core/Persistence/Mappers/PropertyTypeMapper.cs index ab1869a7f5..6f22b61f9a 100644 --- a/src/Umbraco.Core/Persistence/Mappers/PropertyTypeMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/PropertyTypeMapper.cs @@ -24,9 +24,11 @@ namespace Umbraco.Core.Persistence.Mappers DefineMap(nameof(PropertyType.DataTypeId), nameof(PropertyTypeDto.DataTypeId)); DefineMap(nameof(PropertyType.Description), nameof(PropertyTypeDto.Description)); DefineMap(nameof(PropertyType.Mandatory), nameof(PropertyTypeDto.Mandatory)); + DefineMap(nameof(PropertyType.MandatoryMessage), nameof(PropertyTypeDto.MandatoryMessage)); DefineMap(nameof(PropertyType.Name), nameof(PropertyTypeDto.Name)); DefineMap(nameof(PropertyType.SortOrder), nameof(PropertyTypeDto.SortOrder)); DefineMap(nameof(PropertyType.ValidationRegExp), nameof(PropertyTypeDto.ValidationRegExp)); + DefineMap(nameof(PropertyType.ValidationRegExpMessage), nameof(PropertyTypeDto.ValidationRegExpMessage)); DefineMap(nameof(PropertyType.PropertyEditorAlias), nameof(DataTypeDto.EditorAlias)); DefineMap(nameof(PropertyType.ValueStorageType), nameof(DataTypeDto.DbType)); } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs index ccafb9f771..8cbcf4a523 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs @@ -293,10 +293,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Id = dto.Id, Key = dto.UniqueId, Mandatory = dto.Mandatory, + MandatoryMessage = dto.MandatoryMessage, Name = dto.Name, PropertyGroupId = groupId.HasValue ? new Lazy(() => groupId.Value) : null, SortOrder = dto.SortOrder, ValidationRegExp = dto.ValidationRegExp, + ValidationRegExpMessage = dto.ValidationRegExpMessage, Variations = (ContentVariation)dto.Variations }; } diff --git a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs index c4380f032c..b88c49583f 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs @@ -85,7 +85,7 @@ namespace Umbraco.Core.PropertyEditors public string ValueType { get; set; } /// - public IEnumerable Validate(object value, bool required, string format) + public IEnumerable Validate(object value, bool required, string requiredMessage, string format, string formatMessage) { List results = null; var r = Validators.SelectMany(v => v.Validate(value, ValueType, Configuration)).ToList(); @@ -97,14 +97,14 @@ namespace Umbraco.Core.PropertyEditors if (required) { - r = RequiredValidator.ValidateRequired(value, ValueType).ToList(); + r = RequiredValidator.ValidateRequired(value, ValueType, requiredMessage).ToList(); if (r.Any()) { if (results == null) results = r; else results.AddRange(r); } } var stringValue = value?.ToString(); if (!string.IsNullOrWhiteSpace(format) && !string.IsNullOrWhiteSpace(stringValue)) { - r = FormatValidator.ValidateFormat(value, ValueType, format).ToList(); + r = FormatValidator.ValidateFormat(value, ValueType, format, formatMessage).ToList(); if (r.Any()) { if (results == null) results = r; else results.AddRange(r); } } diff --git a/src/Umbraco.Core/PropertyEditors/IDataValueEditor.cs b/src/Umbraco.Core/PropertyEditors/IDataValueEditor.cs index cb68531cc7..0db4881da4 100644 --- a/src/Umbraco.Core/PropertyEditors/IDataValueEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/IDataValueEditor.cs @@ -35,12 +35,14 @@ namespace Umbraco.Core.PropertyEditors bool HideLabel { get; } /// - /// Validates a property value. + /// Validates a property value using custom messages. /// /// The property value. /// A value indicating whether the property value is required. + /// A custom validation message to use when the property value is required. /// A specific format (regex) that the property value must respect. - IEnumerable Validate(object value, bool required, string format); + /// A custom validation message to use when the property value is does not match the specific format (regex). + IEnumerable Validate(object value, bool required, string requiredMessage, string format, string formatMessage); /// /// Gets the validators to use to validate the edited value. diff --git a/src/Umbraco.Core/PropertyEditors/IValueFormatValidator.cs b/src/Umbraco.Core/PropertyEditors/IValueFormatValidator.cs index 3e4aea4385..a361f387e3 100644 --- a/src/Umbraco.Core/PropertyEditors/IValueFormatValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/IValueFormatValidator.cs @@ -14,11 +14,12 @@ namespace Umbraco.Core.PropertyEditors /// The value to validate. /// The value type. /// A format definition. + /// A custom validation message to use when the property value is does not match the specific format (regex). /// Validation results. /// /// The is expected to be a valid regular expression. /// This is used to validate values against the property type validation regular expression. /// - IEnumerable ValidateFormat(object value, string valueType, string format); + IEnumerable ValidateFormat(object value, string valueType, string format, string formatMessage); } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/PropertyEditors/IValueRequiredValidator.cs b/src/Umbraco.Core/PropertyEditors/IValueRequiredValidator.cs index f8e62788c8..7055e1287b 100644 --- a/src/Umbraco.Core/PropertyEditors/IValueRequiredValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/IValueRequiredValidator.cs @@ -13,10 +13,11 @@ namespace Umbraco.Core.PropertyEditors /// /// The value to validate. /// The value type. + /// A custom validation message to use when the property value is required. /// Validation results. /// /// This is used to validate values when the property type specifies that a value is required. /// - IEnumerable ValidateRequired(object value, string valueType); + IEnumerable ValidateRequired(object value, string valueType, string requiredMessage); } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/PropertyEditors/Validators/RegexValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/RegexValidator.cs index e405fa3a3e..53686d745c 100644 --- a/src/Umbraco.Core/PropertyEditors/Validators/RegexValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/Validators/RegexValidator.cs @@ -58,17 +58,28 @@ namespace Umbraco.Core.PropertyEditors.Validators public IEnumerable Validate(object value, string valueType, object dataTypeConfiguration) { if (_regex == null) + { throw new InvalidOperationException("The validator has not been configured."); + } - return ValidateFormat(value, valueType, _regex); + return ValidateFormat(value, valueType, _regex, string.Empty); } /// - public IEnumerable ValidateFormat(object value, string valueType, string format) + public IEnumerable ValidateFormat(object value, string valueType, string format, string formatMessage) { - if (string.IsNullOrWhiteSpace(format)) throw new ArgumentNullOrEmptyException(nameof(format)); + if (string.IsNullOrWhiteSpace(format)) + { + throw new ArgumentNullOrEmptyException(nameof(format)); + } + if (value == null || !new Regex(format).IsMatch(value.ToString())) - yield return new ValidationResult(_textService.Localize("validation", "invalidPattern"), new[] { "value" }); + { + var message = string.IsNullOrWhiteSpace(formatMessage) + ? _textService.Localize("validation", "invalidPattern") + : formatMessage; + yield return new ValidationResult(message, new[] { "value" }); + } } } } diff --git a/src/Umbraco.Core/PropertyEditors/Validators/RequiredValidator.cs b/src/Umbraco.Core/PropertyEditors/Validators/RequiredValidator.cs index c51f572817..d752c4ca47 100644 --- a/src/Umbraco.Core/PropertyEditors/Validators/RequiredValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/Validators/RequiredValidator.cs @@ -27,28 +27,40 @@ namespace Umbraco.Core.PropertyEditors.Validators /// public IEnumerable Validate(object value, string valueType, object dataTypeConfiguration) { - return ValidateRequired(value, valueType); + return ValidateRequired(value, valueType, string.Empty); } /// - public IEnumerable ValidateRequired(object value, string valueType) + public IEnumerable ValidateRequired(object value, string valueType, string requiredMessage) { if (value == null) { - yield return new ValidationResult(_textService.Localize("validation", "invalidNull"), new[] {"value"}); + var message = string.IsNullOrWhiteSpace(requiredMessage) + ? _textService.Localize("validation", "invalidNull") + : requiredMessage; + yield return new ValidationResult(message, new[] {"value"}); yield break; } if (valueType.InvariantEquals(ValueTypes.Json)) { + var message = string.IsNullOrWhiteSpace(requiredMessage) + ? _textService.Localize("validation", "invalidEmpty") + : requiredMessage; if (value.ToString().DetectIsEmptyJson()) - yield return new ValidationResult(_textService.Localize("validation", "invalidEmpty"), new[] { "value" }); + { + yield return new ValidationResult(message, new[] { "value" }); + } + yield break; } if (value.ToString().IsNullOrWhiteSpace()) { - yield return new ValidationResult(_textService.Localize("validation", "invalidEmpty"), new[] { "value" }); + var message = string.IsNullOrWhiteSpace(requiredMessage) + ? _textService.Localize("validation", "invalidEmpty") + : requiredMessage; + yield return new ValidationResult(message, new[] { "value" }); } } } diff --git a/src/Umbraco.Core/Services/Implement/EntityXmlSerializer.cs b/src/Umbraco.Core/Services/Implement/EntityXmlSerializer.cs index 863ecc0b1b..d9be4b01b7 100644 --- a/src/Umbraco.Core/Services/Implement/EntityXmlSerializer.cs +++ b/src/Umbraco.Core/Services/Implement/EntityXmlSerializer.cs @@ -360,7 +360,9 @@ namespace Umbraco.Core.Services.Implement new XElement("Definition", definition.Key), new XElement("Tab", propertyGroup == null ? "" : propertyGroup.Name), new XElement("Mandatory", propertyType.Mandatory.ToString()), + new XElement("MandatoryMessage", propertyType.MandatoryMessage), new XElement("Validation", propertyType.ValidationRegExp), + new XElement("ValidationRegExpMessage", propertyType.ValidationRegExpMessage), new XElement("Description", new XCData(propertyType.Description))); genericProperties.Add(genericProperty); } @@ -486,7 +488,9 @@ namespace Umbraco.Core.Services.Implement new XElement("Tab", propertyGroup == null ? "" : propertyGroup.Name), new XElement("SortOrder", propertyType.SortOrder), new XElement("Mandatory", propertyType.Mandatory.ToString()), + propertyType.MandatoryMessage != null ? new XElement("MandatoryMessage", propertyType.MandatoryMessage) : null, propertyType.ValidationRegExp != null ? new XElement("Validation", propertyType.ValidationRegExp) : null, + propertyType.ValidationRegExpMessage != null ? new XElement("ValidationRegExpMessage", propertyType.ValidationRegExpMessage) : null, propertyType.Description != null ? new XElement("Description", new XCData(propertyType.Description)) : null); genericProperties.Add(genericProperty); diff --git a/src/Umbraco.Core/Services/PropertyValidationService.cs b/src/Umbraco.Core/Services/PropertyValidationService.cs index b846095bd1..2e1ba0b8eb 100644 --- a/src/Umbraco.Core/Services/PropertyValidationService.cs +++ b/src/Umbraco.Core/Services/PropertyValidationService.cs @@ -133,7 +133,7 @@ namespace Umbraco.Core.Services var editor = _propertyEditors[propertyType.PropertyEditorAlias]; var configuration = _dataTypeService.GetDataType(propertyType.DataTypeId).Configuration; var valueEditor = editor.GetValueEditor(configuration); - return !valueEditor.Validate(value, propertyType.Mandatory, propertyType.ValidationRegExp).Any(); + return !valueEditor.Validate(value, propertyType.Mandatory, propertyType.MandatoryMessage, propertyType.ValidationRegExp, propertyType.ValidationRegExpMessage).Any(); } } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 7c0e41348b..cbd372cfb6 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -240,6 +240,7 @@ + diff --git a/src/Umbraco.Tests/Models/Mapping/ContentTypeModelMappingTests.cs b/src/Umbraco.Tests/Models/Mapping/ContentTypeModelMappingTests.cs index 21180ce51b..eee7446823 100644 --- a/src/Umbraco.Tests/Models/Mapping/ContentTypeModelMappingTests.cs +++ b/src/Umbraco.Tests/Models/Mapping/ContentTypeModelMappingTests.cs @@ -621,7 +621,9 @@ namespace Umbraco.Tests.Models.Mapping Validation = new PropertyTypeValidation() { Mandatory = true, - Pattern = "xyz" + MandatoryMessage = "Please enter a value", + Pattern = "xyz", + PatternMessage = "Please match the pattern", } }; @@ -634,7 +636,9 @@ namespace Umbraco.Tests.Models.Mapping Assert.AreEqual(basic.DataTypeId, result.DataTypeId); Assert.AreEqual(basic.Label, result.Name); Assert.AreEqual(basic.Validation.Mandatory, result.Mandatory); + Assert.AreEqual(basic.Validation.MandatoryMessage, result.MandatoryMessage); Assert.AreEqual(basic.Validation.Pattern, result.ValidationRegExp); + Assert.AreEqual(basic.Validation.PatternMessage, result.ValidationRegExpMessage); } [Test] @@ -655,7 +659,9 @@ namespace Umbraco.Tests.Models.Mapping Validation = new PropertyTypeValidation() { Mandatory = true, - Pattern = "xyz" + MandatoryMessage = "Please enter a value", + Pattern = "xyz", + PatternMessage = "Please match the pattern", } }; @@ -668,7 +674,9 @@ namespace Umbraco.Tests.Models.Mapping Assert.AreEqual(basic.DataTypeId, result.DataTypeId); Assert.AreEqual(basic.Label, result.Name); Assert.AreEqual(basic.Validation.Mandatory, result.Mandatory); + Assert.AreEqual(basic.Validation.MandatoryMessage, result.MandatoryMessage); Assert.AreEqual(basic.Validation.Pattern, result.ValidationRegExp); + Assert.AreEqual(basic.Validation.PatternMessage, result.ValidationRegExpMessage); } [Test] @@ -951,7 +959,7 @@ namespace Umbraco.Tests.Models.Mapping Name = "Tab 1", SortOrder = 0, Inherited = false, - Properties = new [] + Properties = new[] { new MemberPropertyTypeBasic { @@ -965,7 +973,7 @@ namespace Umbraco.Tests.Models.Mapping Validation = new PropertyTypeValidation { Mandatory = false, - Pattern = "" + Pattern = string.Empty }, SortOrder = 0, DataTypeId = 555 @@ -1000,7 +1008,7 @@ namespace Umbraco.Tests.Models.Mapping Name = "Tab 1", SortOrder = 0, Inherited = false, - Properties = new [] + Properties = new[] { new PropertyTypeBasic { @@ -1011,7 +1019,7 @@ namespace Umbraco.Tests.Models.Mapping Validation = new PropertyTypeValidation { Mandatory = false, - Pattern = "" + Pattern = string.Empty }, SortOrder = 0, DataTypeId = 555 @@ -1052,7 +1060,7 @@ namespace Umbraco.Tests.Models.Mapping Name = "Tab 1", SortOrder = 0, Inherited = false, - Properties = new [] + Properties = new[] { new PropertyTypeBasic { @@ -1063,7 +1071,7 @@ namespace Umbraco.Tests.Models.Mapping Validation = new PropertyTypeValidation { Mandatory = false, - Pattern = "" + Pattern = string.Empty }, SortOrder = 0, DataTypeId = 555 @@ -1109,7 +1117,7 @@ namespace Umbraco.Tests.Models.Mapping Validation = new PropertyTypeValidation { Mandatory = false, - Pattern = "" + Pattern = string.Empty }, SortOrder = 0, DataTypeId = 555 @@ -1133,7 +1141,7 @@ namespace Umbraco.Tests.Models.Mapping Validation = new PropertyTypeValidation { Mandatory = false, - Pattern = "" + Pattern = string.Empty }, SortOrder = 0, DataTypeId = 555 @@ -1187,7 +1195,7 @@ namespace Umbraco.Tests.Models.Mapping Validation = new PropertyTypeValidation { Mandatory = false, - Pattern = "" + Pattern = string.Empty }, SortOrder = 0, DataTypeId = 555 @@ -1211,7 +1219,7 @@ namespace Umbraco.Tests.Models.Mapping Validation = new PropertyTypeValidation { Mandatory = false, - Pattern = "" + Pattern = string.Empty }, SortOrder = 0, DataTypeId = 555 diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js index 396699866c..1d04418060 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js @@ -501,7 +501,9 @@ property.dataTypeIcon = propertyModel.dataTypeIcon; property.dataTypeName = propertyModel.dataTypeName; property.validation.mandatory = propertyModel.validation.mandatory; + property.validation.mandatoryMessage = propertyModel.validation.mandatoryMessage; property.validation.pattern = propertyModel.validation.pattern; + property.validation.patternMessage = propertyModel.validation.patternMessage; property.showOnMemberProfile = propertyModel.showOnMemberProfile; property.memberCanEdit = propertyModel.memberCanEdit; property.isSensitiveValue = propertyModel.isSensitiveValue; @@ -592,7 +594,9 @@ propertyState: "init", validation: { mandatory: false, - pattern: null + mandatoryMessage: null, + pattern: null, + patternMessage: null } }; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less index 4c23aef5f0..36bd00c8cb 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less @@ -494,11 +494,11 @@ input.umb-group-builder__group-sort-value { font-weight: bold; font-size: 14px; color: @ui-action-type; - - &:hover{ + + &:hover { text-decoration: none; - color:@ui-action-type-hover; - border-color:@ui-action-border-hover; + color: @ui-action-type-hover; + border-color: @ui-action-border-hover; } } @@ -555,7 +555,13 @@ input.umb-group-builder__group-sort-value { overflow: hidden; } - .editor-validation-pattern{ + .editor-validation-message { + min-width: 100%; + min-height: 25px; + margin-top: 4px; + } + + .editor-validation-pattern { border: 1px solid @gray-7; margin: 10px 0 0; padding: 6px; diff --git a/src/Umbraco.Web.UI.Client/src/less/forms.less b/src/Umbraco.Web.UI.Client/src/less/forms.less index cfbb8b78ab..72abb3ba00 100644 --- a/src/Umbraco.Web.UI.Client/src/less/forms.less +++ b/src/Umbraco.Web.UI.Client/src/less/forms.less @@ -528,7 +528,8 @@ input[type="checkbox"][readonly] { .help-inline { display: inline-block; vertical-align: middle; - padding-left: 5px; + padding-top: 4px; + padding-left: 2px; } div.help { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html index 93d7936326..56c2b94bd7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html @@ -81,38 +81,55 @@
- +
- -
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js index 32e9891eb4..2bbd7404f7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js @@ -1,4 +1,4 @@ -function textboxController($scope) { +function textboxController($scope, localizationService) { // macro parameter editor doesn't contains a config object, // so we create a new one to hold any properties if (!$scope.model.config) { @@ -18,6 +18,15 @@ function textboxController($scope) { } } $scope.model.change(); + + $scope.mandatoryMessage = ""; + if ($scope.model.validation.mandatoryMessage) { + $scope.mandatoryMessage = $scope.model.validation.mandatoryMessage; + } else { + localizationService.localize("general_required").then(function (value) { + $scope.mandatoryMessage = value; + }); + } } angular.module('umbraco').controller("Umbraco.PropertyEditors.textboxController", textboxController); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html index 92f02b9f5b..238d70e5e3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html @@ -7,9 +7,9 @@ ng-trim="false" ng-keyup="model.change()" /> - + {{textboxFieldForm.textbox.errorMsg}} - Required + {{mandatoryMessage}}
diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 175146aafd..7541297815 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -1951,7 +1951,9 @@ To manage your website, simply open the Umbraco back office and start adding con Validate as a URL ...or enter a custom validation Field is mandatory + Enter a custom validation error message (optional) Enter a regular expression + Enter a custom validation error message (optional) You need to add at least You can only have items 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 6aa6054992..9357d4720a 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -1961,7 +1961,9 @@ To manage your website, simply open the Umbraco back office and start adding con Validate as a URL ...or enter a custom validation Field is mandatory + Enter a custom validation error message (optional) Enter a regular expression + Enter a custom validation error message (optional) You need to add at least You can only have items diff --git a/src/Umbraco.Web/Editors/Filters/ContentModelValidator.cs b/src/Umbraco.Web/Editors/Filters/ContentModelValidator.cs index 4acf0c948e..67f2e2d090 100644 --- a/src/Umbraco.Web/Editors/Filters/ContentModelValidator.cs +++ b/src/Umbraco.Web/Editors/Filters/ContentModelValidator.cs @@ -167,7 +167,7 @@ namespace Umbraco.Web.Editors.Filters { // validate var valueEditor = editor.GetValueEditor(property.DataType.Configuration); - foreach (var r in valueEditor.Validate(postedValue, property.IsRequired, property.ValidationRegExp)) + foreach (var r in valueEditor.Validate(postedValue, property.IsRequired, property.IsRequiredMessage, property.ValidationRegExp, property.ValidationRegExpMessage)) { modelState.AddPropertyError(r, property.Alias, property.Culture); } diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentPropertyDto.cs b/src/Umbraco.Web/Models/ContentEditing/ContentPropertyDto.cs index 1f9d0e9b77..83e5e2a9b2 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentPropertyDto.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentPropertyDto.cs @@ -11,10 +11,17 @@ namespace Umbraco.Web.Models.ContentEditing internal class ContentPropertyDto : ContentPropertyBasic { public IDataType DataType { get; set; } + public string Label { get; set; } + public string Description { get; set; } + public bool IsRequired { get; set; } + + public string IsRequiredMessage { get; set; } + public string ValidationRegExp { get; set; } + public string ValidationRegExpMessage { get; set; } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/PropertyTypeValidation.cs b/src/Umbraco.Web/Models/ContentEditing/PropertyTypeValidation.cs index 929c7ce324..a45af12341 100644 --- a/src/Umbraco.Web/Models/ContentEditing/PropertyTypeValidation.cs +++ b/src/Umbraco.Web/Models/ContentEditing/PropertyTypeValidation.cs @@ -11,7 +11,13 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "mandatory")] public bool Mandatory { get; set; } + [DataMember(Name = "mandatoryMessage")] + public string MandatoryMessage { get; set; } + [DataMember(Name = "pattern")] public string Pattern { get; set; } + + [DataMember(Name = "patternMessage")] + public string PatternMessage { get; set; } } } diff --git a/src/Umbraco.Web/Models/Mapping/ContentPropertyDisplayMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentPropertyDisplayMapper.cs index 8a45548e9c..535c67bee1 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentPropertyDisplayMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentPropertyDisplayMapper.cs @@ -42,7 +42,9 @@ namespace Umbraco.Web.Models.Mapping //add the validation information dest.Validation.Mandatory = originalProp.PropertyType.Mandatory; + dest.Validation.MandatoryMessage = originalProp.PropertyType.MandatoryMessage; dest.Validation.Pattern = originalProp.PropertyType.ValidationRegExp; + dest.Validation.PatternMessage = originalProp.PropertyType.ValidationRegExpMessage; if (dest.PropertyEditor == null) { diff --git a/src/Umbraco.Web/Models/Mapping/ContentPropertyDtoMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentPropertyDtoMapper.cs index f192cd32ce..472a32f0b1 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentPropertyDtoMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentPropertyDtoMapper.cs @@ -21,7 +21,9 @@ namespace Umbraco.Web.Models.Mapping base.Map(property, dest, context); dest.IsRequired = property.PropertyType.Mandatory; + dest.IsRequiredMessage = property.PropertyType.MandatoryMessage; dest.ValidationRegExp = property.PropertyType.ValidationRegExp; + dest.ValidationRegExpMessage = property.PropertyType.ValidationRegExpMessage; dest.Description = property.PropertyType.Description; dest.Label = property.PropertyType.Name; dest.DataType = DataTypeService.GetDataType(property.PropertyType.DataTypeId); diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs index a438f04781..f89e0604b4 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs @@ -220,7 +220,9 @@ namespace Umbraco.Web.Models.Mapping target.Name = source.Label; target.DataTypeId = source.DataTypeId; target.Mandatory = source.Validation.Mandatory; + target.MandatoryMessage = source.Validation.MandatoryMessage; target.ValidationRegExp = source.Validation.Pattern; + target.ValidationRegExpMessage = source.Validation.PatternMessage; target.Variations = source.AllowCultureVariant ? ContentVariation.Culture : ContentVariation.Nothing; if (source.Id > 0) diff --git a/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupMapper.cs b/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupMapper.cs index 8c5f347799..70074d22a9 100644 --- a/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupMapper.cs @@ -223,7 +223,13 @@ namespace Umbraco.Web.Models.Mapping Alias = p.Alias, Description = p.Description, Editor = p.PropertyEditorAlias, - Validation = new PropertyTypeValidation {Mandatory = p.Mandatory, Pattern = p.ValidationRegExp}, + Validation = new PropertyTypeValidation + { + Mandatory = p.Mandatory, + MandatoryMessage = p.MandatoryMessage, + Pattern = p.ValidationRegExp, + PatternMessage = p.ValidationRegExpMessage, + }, Label = p.Name, View = propertyEditor.GetValueEditor().View, Config = config, diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs index 6dee2f78b5..f85171304b 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs @@ -313,9 +313,19 @@ namespace Umbraco.Web.PropertyEditors if (propType.Mandatory) { if (propValues[propKey] == null) - yield return new ValidationResult("Item " + (i + 1) + " '" + propType.Name + "' cannot be null", new[] { propKey }); + { + var message = string.IsNullOrWhiteSpace(propType.MandatoryMessage) + ? $"'{propType.Name}' cannot be null" + : propType.MandatoryMessage; + yield return new ValidationResult($"Item {(i + 1)}: {message}", new[] { propKey }); + } else if (propValues[propKey].ToString().IsNullOrWhiteSpace() || (propValues[propKey].Type == JTokenType.Array && !propValues[propKey].HasValues)) - yield return new ValidationResult("Item " + (i + 1) + " '" + propType.Name + "' cannot be empty", new[] { propKey }); + { + var message = string.IsNullOrWhiteSpace(propType.MandatoryMessage) + ? $"'{propType.Name}' cannot be empty" + : propType.MandatoryMessage; + yield return new ValidationResult($"Item{(i + 1)}: {message}", new[] { propKey }); + } } // Check regex @@ -325,7 +335,10 @@ namespace Umbraco.Web.PropertyEditors var regex = new Regex(propType.ValidationRegExp); if (!regex.IsMatch(propValues[propKey].ToString())) { - yield return new ValidationResult("Item " + (i + 1) + " '" + propType.Name + "' is invalid, it does not match the correct pattern", new[] { propKey }); + var message = string.IsNullOrWhiteSpace(propType.ValidationRegExpMessage) + ? $"'{propType.Name}' is invalid, it does not match the correct pattern" + : propType.ValidationRegExpMessage; + yield return new ValidationResult($"Item {(i + 1)}: {message}", new[] { propKey }); } } } diff --git a/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs index 90527a8b8d..508f4d03e8 100644 --- a/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs @@ -57,7 +57,7 @@ namespace Umbraco.Web.PropertyEditors private class RequiredJsonValueValidator : IValueRequiredValidator { /// - public IEnumerable ValidateRequired(object value, string valueType) + public IEnumerable ValidateRequired(object value, string valueType, string requiredMessage) { if (value == null) { From 355a5e0eb490173a10c72681bbbbe2cf565bdaa2 Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 24 Jun 2019 10:32:52 +0200 Subject: [PATCH 0008/1545] Remove ModelsBuilder NuGet dependency, add project --- .../Api/ApiBasicAuthFilter.cs | 86 --- src/Umbraco.ModelsBuilder/Api/ApiClient.cs | 159 ----- src/Umbraco.ModelsBuilder/Api/ApiHelper.cs | 27 - src/Umbraco.ModelsBuilder/Api/ApiVersion.cs | 88 --- .../Api/GetModelsData.cs | 17 - .../Api/ModelsBuilderApiController.cs | 85 --- src/Umbraco.ModelsBuilder/Api/TokenData.cs | 17 - .../Api/ValidateClientVersionData.cs | 57 -- src/Umbraco.ModelsBuilder/Building/Builder.cs | 322 ---------- .../Building/CodeDomBuilder.cs | 113 ---- .../Building/CodeParser.cs | 238 ------- .../Building/Compiler.cs | 171 ----- .../Building/CompilerException.cs | 25 - .../Building/ParseResult.cs | 275 -------- .../Building/PropertyModel.cs | 66 -- .../Building/TextBuilder.cs | 554 ---------------- .../Building/TextHeaderWriter.cs | 26 - .../Building/TypeModel.cs | 208 ------ .../Configuration/ClrNameSource.cs | 28 - .../Configuration/Config.cs | 357 ----------- .../Configuration/ModelsMode.cs | 52 -- .../Configuration/ModelsModeExtensions.cs | 51 -- .../Configuration/UmbracoConfigExtensions.cs | 34 - .../Dashboard/BuilderDashboardHelper.cs | 91 --- .../EnumerableExtensions.cs | 33 - .../IgnoreContentTypeAttribute.cs | 46 -- .../IgnorePropertyTypeAttribute.cs | 19 - .../ImplementContentTypeAttribute.cs | 20 - .../ImplementPropertyTypeAttribute.cs | 23 - .../ModelsBaseClassAttribute.cs | 16 - .../ModelsBuilderAssemblyAttribute.cs | 23 - .../ModelsNamespaceAttribute.cs | 16 - .../ModelsUsingAttribute.cs | 21 - .../Properties/AssemblyInfo.cs | 34 +- .../PublishedElementExtensions.cs | 48 -- .../PublishedPropertyTypeExtensions.cs | 20 - .../PureLiveAssemblyAttribute.cs | 15 - .../ReferencedAssemblies.cs | 137 ---- .../RenameContentTypeAttribute.cs | 18 - .../RenamePropertyTypeAttribute.cs | 18 - src/Umbraco.ModelsBuilder/TypeExtensions.cs | 22 - .../Umbraco.ModelsBuilder.csproj | 75 +-- .../Umbraco/HashCombiner.cs | 38 -- .../Umbraco/HashHelper.cs | 45 -- .../Umbraco/LiveModelsProvider.cs | 142 ----- .../ModelsBuilderBackOfficeController.cs | 194 ------ .../Umbraco/ModelsBuilderComponent.cs | 178 ------ .../Umbraco/ModelsGenerationError.cs | 60 -- .../Umbraco/OutOfDateModelsStatus.cs | 58 -- .../Umbraco/PublishedModelUtility.cs | 67 -- .../Umbraco/PureLiveModelFactory.cs | 602 ------------------ .../Umbraco/UmbracoServices.cs | 236 ------- .../Validation/ContentTypeModelValidator.cs | 95 --- src/umbraco.sln | 10 +- 54 files changed, 38 insertions(+), 5438 deletions(-) delete mode 100644 src/Umbraco.ModelsBuilder/Api/ApiBasicAuthFilter.cs delete mode 100644 src/Umbraco.ModelsBuilder/Api/ApiClient.cs delete mode 100644 src/Umbraco.ModelsBuilder/Api/ApiHelper.cs delete mode 100644 src/Umbraco.ModelsBuilder/Api/ApiVersion.cs delete mode 100644 src/Umbraco.ModelsBuilder/Api/GetModelsData.cs delete mode 100644 src/Umbraco.ModelsBuilder/Api/ModelsBuilderApiController.cs delete mode 100644 src/Umbraco.ModelsBuilder/Api/TokenData.cs delete mode 100644 src/Umbraco.ModelsBuilder/Api/ValidateClientVersionData.cs delete mode 100644 src/Umbraco.ModelsBuilder/Building/Builder.cs delete mode 100644 src/Umbraco.ModelsBuilder/Building/CodeDomBuilder.cs delete mode 100644 src/Umbraco.ModelsBuilder/Building/CodeParser.cs delete mode 100644 src/Umbraco.ModelsBuilder/Building/Compiler.cs delete mode 100644 src/Umbraco.ModelsBuilder/Building/CompilerException.cs delete mode 100644 src/Umbraco.ModelsBuilder/Building/ParseResult.cs delete mode 100644 src/Umbraco.ModelsBuilder/Building/PropertyModel.cs delete mode 100644 src/Umbraco.ModelsBuilder/Building/TextBuilder.cs delete mode 100644 src/Umbraco.ModelsBuilder/Building/TextHeaderWriter.cs delete mode 100644 src/Umbraco.ModelsBuilder/Building/TypeModel.cs delete mode 100644 src/Umbraco.ModelsBuilder/Configuration/ClrNameSource.cs delete mode 100644 src/Umbraco.ModelsBuilder/Configuration/Config.cs delete mode 100644 src/Umbraco.ModelsBuilder/Configuration/ModelsMode.cs delete mode 100644 src/Umbraco.ModelsBuilder/Configuration/ModelsModeExtensions.cs delete mode 100644 src/Umbraco.ModelsBuilder/Configuration/UmbracoConfigExtensions.cs delete mode 100644 src/Umbraco.ModelsBuilder/Dashboard/BuilderDashboardHelper.cs delete mode 100644 src/Umbraco.ModelsBuilder/EnumerableExtensions.cs delete mode 100644 src/Umbraco.ModelsBuilder/IgnoreContentTypeAttribute.cs delete mode 100644 src/Umbraco.ModelsBuilder/IgnorePropertyTypeAttribute.cs delete mode 100644 src/Umbraco.ModelsBuilder/ImplementContentTypeAttribute.cs delete mode 100644 src/Umbraco.ModelsBuilder/ImplementPropertyTypeAttribute.cs delete mode 100644 src/Umbraco.ModelsBuilder/ModelsBaseClassAttribute.cs delete mode 100644 src/Umbraco.ModelsBuilder/ModelsBuilderAssemblyAttribute.cs delete mode 100644 src/Umbraco.ModelsBuilder/ModelsNamespaceAttribute.cs delete mode 100644 src/Umbraco.ModelsBuilder/ModelsUsingAttribute.cs delete mode 100644 src/Umbraco.ModelsBuilder/PublishedElementExtensions.cs delete mode 100644 src/Umbraco.ModelsBuilder/PublishedPropertyTypeExtensions.cs delete mode 100644 src/Umbraco.ModelsBuilder/PureLiveAssemblyAttribute.cs delete mode 100644 src/Umbraco.ModelsBuilder/ReferencedAssemblies.cs delete mode 100644 src/Umbraco.ModelsBuilder/RenameContentTypeAttribute.cs delete mode 100644 src/Umbraco.ModelsBuilder/RenamePropertyTypeAttribute.cs delete mode 100644 src/Umbraco.ModelsBuilder/TypeExtensions.cs delete mode 100644 src/Umbraco.ModelsBuilder/Umbraco/HashCombiner.cs delete mode 100644 src/Umbraco.ModelsBuilder/Umbraco/HashHelper.cs delete mode 100644 src/Umbraco.ModelsBuilder/Umbraco/LiveModelsProvider.cs delete mode 100644 src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderBackOfficeController.cs delete mode 100644 src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderComponent.cs delete mode 100644 src/Umbraco.ModelsBuilder/Umbraco/ModelsGenerationError.cs delete mode 100644 src/Umbraco.ModelsBuilder/Umbraco/OutOfDateModelsStatus.cs delete mode 100644 src/Umbraco.ModelsBuilder/Umbraco/PublishedModelUtility.cs delete mode 100644 src/Umbraco.ModelsBuilder/Umbraco/PureLiveModelFactory.cs delete mode 100644 src/Umbraco.ModelsBuilder/Umbraco/UmbracoServices.cs delete mode 100644 src/Umbraco.ModelsBuilder/Validation/ContentTypeModelValidator.cs diff --git a/src/Umbraco.ModelsBuilder/Api/ApiBasicAuthFilter.cs b/src/Umbraco.ModelsBuilder/Api/ApiBasicAuthFilter.cs deleted file mode 100644 index cc862ff207..0000000000 --- a/src/Umbraco.ModelsBuilder/Api/ApiBasicAuthFilter.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Web.Http.Controllers; -using System.Web.Security; -using Umbraco.Core; -using Umbraco.Core.Composing; -using Umbraco.Core.Configuration; -using Umbraco.Core.Models.Membership; - -namespace Umbraco.ModelsBuilder.Api -{ - - //TODO: This needs to be changed: - // * Authentication cannot happen in a filter, only Authorization - // * The filter must be an AuthorizationFilter, not an ActionFilter - // * Authorization must be done using the Umbraco logic - it is very specific for claim checking for ASP.Net Identity - // * Theoretically this shouldn't be required whatsoever because when we authenticate a request that has Basic Auth (i.e. for - // VS to work, it will add the correct Claims to the Identity and it will automatically be authorized. - // - // we *do* have POC supporting ASP.NET identity, however they require some config on the server - // we'll keep using this quick-and-dirty method for the time being - - public class ApiBasicAuthFilter : System.Web.Http.Filters.ActionFilterAttribute // use the http one, not mvc, with api controllers! - { - private static readonly char[] Separator = ":".ToCharArray(); - private readonly string _section; - - public ApiBasicAuthFilter(string section) - { - _section = section; - } - - public override void OnActionExecuting(HttpActionContext actionContext) - { - try - { - var user = Authenticate(actionContext.Request); - if (user == null || !user.AllowedSections.Contains(_section)) - { - actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized); - } - //else - //{ - // // note - would that be a proper way to pass data to the controller? - // // see http://stevescodingblog.co.uk/basic-authentication-with-asp-net-webapi/ - // actionContext.ControllerContext.RouteData.Values["umbraco-user"] = user; - //} - - base.OnActionExecuting(actionContext); - } - catch - { - actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized); - } - } - - private static IUser Authenticate(HttpRequestMessage request) - { - var ah = request.Headers.Authorization; - if (ah == null || ah.Scheme != "Basic") - return null; - - var token = ah.Parameter; - var credentials = Encoding.ASCII - .GetString(Convert.FromBase64String(token)) - .Split(Separator); - if (credentials.Length != 2) - return null; - - var username = ApiClient.DecodeTokenElement(credentials[0]); - var password = ApiClient.DecodeTokenElement(credentials[1]); - - var providerKey = UmbracoConfig.For.UmbracoSettings().Providers.DefaultBackOfficeUserProvider; - var provider = Membership.Providers[providerKey]; - if (provider == null || !provider.ValidateUser(username, password)) - return null; - var user = Current.Services.UserService.GetByUsername(username); - if (!user.IsApproved || user.IsLockedOut) - return null; - return user; - } - } -} \ No newline at end of file diff --git a/src/Umbraco.ModelsBuilder/Api/ApiClient.cs b/src/Umbraco.ModelsBuilder/Api/ApiClient.cs deleted file mode 100644 index dde3641b97..0000000000 --- a/src/Umbraco.ModelsBuilder/Api/ApiClient.cs +++ /dev/null @@ -1,159 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Net.Http.Formatting; -using System.Net.Http.Headers; -using System.Text; - -namespace Umbraco.ModelsBuilder.Api -{ - public class ApiClient - { - private readonly string _url; - private readonly string _user; - private readonly string _password; - - private readonly JsonMediaTypeFormatter _formatter; - private readonly MediaTypeFormatter[] _formatters; - - // fixme hardcoded? - // could be options - but we cannot "discover" them as the API client runs outside of the web app - // in addition, anything that references the controller forces API clients to reference Umbraco.Core - private const string ApiControllerUrl = "/Umbraco/BackOffice/ModelsBuilder/ModelsBuilderApi/"; - - public ApiClient(string url, string user, string password) - { - _url = url.TrimEnd('/'); - _user = user; - _password = password; - - _formatter = new JsonMediaTypeFormatter(); - _formatters = new MediaTypeFormatter[] { _formatter }; - } - - private void SetBaseAddress(HttpClient client, string url) - { - try - { - client.BaseAddress = new Uri(url); - } - catch - { - throw new UriFormatException($"Invalid URI: the format of the URI \"{url}\" could not be determined."); - } - } - - public void ValidateClientVersion() - { - // FIXME - add proxys support - - var hch = new HttpClientHandler(); - - using (var client = new HttpClient(hch)) - { - SetBaseAddress(client, _url); - Authorize(client); - - var data = new ValidateClientVersionData - { - ClientVersion = ApiVersion.Current.Version, - MinServerVersionSupportingClient = ApiVersion.Current.MinServerVersionSupportingClient, - }; - - var result = client.PostAsync(_url + ApiControllerUrl + nameof(ModelsBuilderApiController.ValidateClientVersion), - data, _formatter).Result; - - // this is not providing enough details in case of an error - do our own reporting - //result.EnsureSuccessStatusCode(); - EnsureSuccess(result); - } - } - - public IDictionary GetModels(Dictionary ourFiles, string modelsNamespace) - { - // FIXME - add proxys support - - var hch = new HttpClientHandler(); - - //hch.Proxy = new WebProxy("path.to.proxy", 8888); - //hch.UseProxy = true; - - using (var client = new HttpClient(hch)) - { - SetBaseAddress(client, _url); - Authorize(client); - - var data = new GetModelsData - { - Namespace = modelsNamespace, - ClientVersion = ApiVersion.Current.Version, - MinServerVersionSupportingClient = ApiVersion.Current.MinServerVersionSupportingClient, - Files = ourFiles - }; - - var result = client.PostAsync(_url + ApiControllerUrl + nameof(ModelsBuilderApiController.GetModels), - data, _formatter).Result; - - // this is not providing enough details in case of an error - do our own reporting - //result.EnsureSuccessStatusCode(); - EnsureSuccess(result); - - var genFiles = result.Content.ReadAsAsync>(_formatters).Result; - return genFiles; - } - } - - private static void EnsureSuccess(HttpResponseMessage result) - { - if (result.IsSuccessStatusCode) return; - - var text = result.Content.ReadAsStringAsync().Result; - throw new Exception($"Response status code does not indicate success ({result.StatusCode})\n{text}"); - } - - private void Authorize(HttpClient client) - { - AuthorizeBasic(client); - } - - // fixme - for the time being, we don't cache the token and we auth on each API call - // not used at the moment - /* - private void AuthorizeIdentity(HttpClient client) - { - var formData = new FormUrlEncodedContent(new[] - { - new KeyValuePair("grant_type", "password"), - new KeyValuePair("userName", _user), - new KeyValuePair("password", _password), - }); - - var result = client.PostAsync(_url + UmbracoOAuthTokenUrl, formData).Result; - - EnsureSuccess(result); - - var token = result.Content.ReadAsAsync(_formatters).Result; - if (token.TokenType != "bearer") - throw new Exception($"Received invalid token type \"{token.TokenType}\", expected \"bearer\"."); - - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken); - } - */ - - private void AuthorizeBasic(HttpClient client) - { - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", - Convert.ToBase64String(Encoding.UTF8.GetBytes(EncodeTokenElement(_user) + ':' + EncodeTokenElement(_password)))); - } - - public static string EncodeTokenElement(string s) - { - return s.Replace("%", "%a").Replace(":", "%b"); - } - - public static string DecodeTokenElement(string s) - { - return s.Replace("%b", ":").Replace("%a", "%"); - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Api/ApiHelper.cs b/src/Umbraco.ModelsBuilder/Api/ApiHelper.cs deleted file mode 100644 index fa6492fe3f..0000000000 --- a/src/Umbraco.ModelsBuilder/Api/ApiHelper.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using System.Text; -using Umbraco.ModelsBuilder.Building; -using Umbraco.ModelsBuilder.Umbraco; - -namespace Umbraco.ModelsBuilder.Api -{ - internal static class ApiHelper - { - public static Dictionary GetModels(UmbracoServices umbracoServices, string modelsNamespace, IDictionary files) - { - var typeModels = umbracoServices.GetAllTypes(); - - var parseResult = new CodeParser().ParseWithReferencedAssemblies(files); - var builder = new TextBuilder(typeModels, parseResult, modelsNamespace); - - var models = new Dictionary(); - foreach (var typeModel in builder.GetModelsToGenerate()) - { - var sb = new StringBuilder(); - builder.Generate(sb, typeModel); - models[typeModel.ClrName] = sb.ToString(); - } - return models; - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Api/ApiVersion.cs b/src/Umbraco.ModelsBuilder/Api/ApiVersion.cs deleted file mode 100644 index 2ee64b8c54..0000000000 --- a/src/Umbraco.ModelsBuilder/Api/ApiVersion.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Reflection; - -namespace Umbraco.ModelsBuilder.Api -{ - /// - /// Manages API version handshake between client and server. - /// - public class ApiVersion - { - #region Configure - - // indicate the minimum version of the client API that is supported by this server's API. - // eg our Version = 4.8 but we support connections from VSIX down to version 3.2 - // => as a server, we accept connections from client down to version ... - private static readonly Version MinClientVersionSupportedByServerConst = new Version(3, 0, 0, 0); - - // indicate the minimum version of the server that can support the client API - // eg our Version = 4.8 and we know we're compatible with website server down to version 3.2 - // => as a client, we tell the server down to version ... that it should accept us - private static readonly Version MinServerVersionSupportingClientConst = new Version(3, 0, 0, 0); - - #endregion - - /// - /// Initializes a new instance of the class. - /// - /// The currently executing version. - /// The min client version supported by the server. - /// An opt min server version supporting the client. - internal ApiVersion(Version executingVersion, Version minClientVersionSupportedByServer, Version minServerVersionSupportingClient = null) - { - if (executingVersion == null) throw new ArgumentNullException(nameof(executingVersion)); - if (minClientVersionSupportedByServer == null) throw new ArgumentNullException(nameof(minClientVersionSupportedByServer)); - - Version = executingVersion; - MinClientVersionSupportedByServer = minClientVersionSupportedByServer; - MinServerVersionSupportingClient = minServerVersionSupportingClient; - } - - /// - /// Gets the currently executing API version. - /// - public static ApiVersion Current { get; } - = new ApiVersion(Assembly.GetExecutingAssembly().GetName().Version, - MinClientVersionSupportedByServerConst, MinServerVersionSupportingClientConst); - - /// - /// Gets the executing version of the API. - /// - public Version Version { get; } - - /// - /// Gets the min client version supported by the server. - /// - public Version MinClientVersionSupportedByServer { get; } - - /// - /// Gets the min server version supporting the client. - /// - public Version MinServerVersionSupportingClient { get; } - - /// - /// Gets a value indicating whether the API server is compatible with a client. - /// - /// The client version. - /// An opt min server version supporting the client. - /// - /// A client is compatible with a server if the client version is greater-or-equal _minClientVersionSupportedByServer - /// (ie client can be older than server, up to a point) AND the client version is lower-or-equal the server version - /// (ie client cannot be more recent than server) UNLESS the server . - /// - public bool IsCompatibleWith(Version clientVersion, Version minServerVersionSupportingClient = null) - { - // client cannot be older than server's min supported version - if (clientVersion < MinClientVersionSupportedByServer) - return false; - - // if we know about this client (client is older than server), it is supported - if (clientVersion <= Version) // if we know about this client (client older than server) - return true; - - // if we don't know about this client (client is newer than server), - // give server a chance to tell client it is, indeed, ok to support it - return minServerVersionSupportingClient != null && minServerVersionSupportingClient <= Version; - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Api/GetModelsData.cs b/src/Umbraco.ModelsBuilder/Api/GetModelsData.cs deleted file mode 100644 index 9a5c55afc2..0000000000 --- a/src/Umbraco.ModelsBuilder/Api/GetModelsData.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; -using System.Runtime.Serialization; - -namespace Umbraco.ModelsBuilder.Api -{ - [DataContract] - public class GetModelsData : ValidateClientVersionData - { - [DataMember] - public string Namespace { get; set; } - - [DataMember] - public IDictionary Files { get; set; } - - public override bool IsValid => base.IsValid && Files != null; - } -} \ No newline at end of file diff --git a/src/Umbraco.ModelsBuilder/Api/ModelsBuilderApiController.cs b/src/Umbraco.ModelsBuilder/Api/ModelsBuilderApiController.cs deleted file mode 100644 index 444910b069..0000000000 --- a/src/Umbraco.ModelsBuilder/Api/ModelsBuilderApiController.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Net; -using System.Net.Http; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.ModelsBuilder.Configuration; -using Umbraco.ModelsBuilder.Umbraco; -using Umbraco.Web.Mvc; -using Umbraco.Web.WebApi; - -namespace Umbraco.ModelsBuilder.Api -{ - // read http://umbraco.com/follow-us/blog-archive/2014/1/17/heads-up,-breaking-change-coming-in-702-and-62.aspx - // read http://our.umbraco.org/forum/developers/api-questions/43025-Web-API-authentication - // UmbracoAuthorizedApiController :: /Umbraco/BackOffice/Zbu/ModelsBuilderApi/GetTypeModels - // UmbracoApiController :: /Umbraco/Zbu/ModelsBuilderApi/GetTypeModels ?? UNLESS marked with isbackoffice - // - // BEWARE! the controller url is hard-coded in ModelsBuilderApi and needs to be in sync! - - [PluginController(ControllerArea)] - [IsBackOffice] - //[UmbracoApplicationAuthorize(Constants.Applications.Developer)] // see ApiBasicAuthFilter - that one would be for ASP.NET identity - public class ModelsBuilderApiController : UmbracoApiController // UmbracoAuthorizedApiController - for ASP.NET identity - { - public const string ControllerArea = "ModelsBuilder"; - - private readonly UmbracoServices _umbracoServices; - - public ModelsBuilderApiController(UmbracoServices umbracoServices) - { - _umbracoServices = umbracoServices; - } - - // invoked by the API - [System.Web.Http.HttpPost] // use the http one, not mvc, with api controllers! - [ApiBasicAuthFilter("developer")] // have to use our own, non-cookie-based, auth - public HttpResponseMessage ValidateClientVersion(ValidateClientVersionData data) - { - if (!UmbracoConfig.For.ModelsBuilder().ApiServer) - return Request.CreateResponse(HttpStatusCode.Forbidden, "API server does not want to talk to you."); - - if (!ModelState.IsValid || data == null || !data.IsValid) - return Request.CreateResponse(HttpStatusCode.BadRequest, "Invalid data."); - - var checkResult = CheckVersion(data.ClientVersion, data.MinServerVersionSupportingClient); - return (checkResult.Success - ? Request.CreateResponse(HttpStatusCode.OK, "OK", Configuration.Formatters.JsonFormatter) - : checkResult.Result); - } - - // invoked by the API - [System.Web.Http.HttpPost] // use the http one, not mvc, with api controllers! - [ApiBasicAuthFilter("developer")] // have to use our own, non-cookie-based, auth - public HttpResponseMessage GetModels(GetModelsData data) - { - if (!UmbracoConfig.For.ModelsBuilder().ApiServer) - return Request.CreateResponse(HttpStatusCode.Forbidden, "API server does not want to talk to you."); - - if (!ModelState.IsValid || data == null || !data.IsValid) - return Request.CreateResponse(HttpStatusCode.BadRequest, "Invalid data."); - - var checkResult = CheckVersion(data.ClientVersion, data.MinServerVersionSupportingClient); - if (!checkResult.Success) - return checkResult.Result; - - var models = ApiHelper.GetModels(_umbracoServices, data.Namespace, data.Files); - - return Request.CreateResponse(HttpStatusCode.OK, models, Configuration.Formatters.JsonFormatter); - } - - private Attempt CheckVersion(Version clientVersion, Version minServerVersionSupportingClient) - { - if (clientVersion == null) - return Attempt.Fail(Request.CreateResponse(HttpStatusCode.Forbidden, - $"API version conflict: client version () is not compatible with server version({ApiVersion.Current.Version}).")); - - // minServerVersionSupportingClient can be null - var isOk = ApiVersion.Current.IsCompatibleWith(clientVersion, minServerVersionSupportingClient); - var response = isOk ? null : Request.CreateResponse(HttpStatusCode.Forbidden, - $"API version conflict: client version ({clientVersion}) is not compatible with server version({ApiVersion.Current.Version})."); - - return Attempt.If(isOk, response); - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Api/TokenData.cs b/src/Umbraco.ModelsBuilder/Api/TokenData.cs deleted file mode 100644 index c34a6c75c5..0000000000 --- a/src/Umbraco.ModelsBuilder/Api/TokenData.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Runtime.Serialization; - -namespace Umbraco.ModelsBuilder.Api -{ - [DataContract] - class TokenData - { - [DataMember(Name = "access_token")] - public string AccessToken { get; set; } - - [DataMember(Name = "token_type")] - public string TokenType { get; set; } - - [DataMember(Name = "expires_in")] - public int ExpiresIn { get; set; } - } -} diff --git a/src/Umbraco.ModelsBuilder/Api/ValidateClientVersionData.cs b/src/Umbraco.ModelsBuilder/Api/ValidateClientVersionData.cs deleted file mode 100644 index 39ef08d816..0000000000 --- a/src/Umbraco.ModelsBuilder/Api/ValidateClientVersionData.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Runtime.Serialization; - -namespace Umbraco.ModelsBuilder.Api -{ - [DataContract] - public class ValidateClientVersionData - { - // issues 32, 34... problems when serializing versions - // - // make sure System.Version objects are transfered as strings - // depending on the JSON serializer version, it looks like versions are causing issues - // see - // http://stackoverflow.com/questions/13170386/why-system-version-in-json-string-does-not-deserialize-correctly - // - // if the class is marked with [DataContract] then only properties marked with [DataMember] - // are serialized and the rest is ignored, see - // http://www.asp.net/web-api/overview/formats-and-model-binding/json-and-xml-serialization - - [DataMember] - public string ClientVersionString - { - get { return VersionToString(ClientVersion); } - set { ClientVersion = ParseVersion(value, false, "client"); } - } - - [DataMember] - public string MinServerVersionSupportingClientString - { - get { return VersionToString(MinServerVersionSupportingClient); } - set { MinServerVersionSupportingClient = ParseVersion(value, true, "minServer"); } - } - - // not serialized - public Version ClientVersion { get; set; } - public Version MinServerVersionSupportingClient { get; set; } - - private static string VersionToString(Version version) - { - return version?.ToString() ?? "0.0.0.0"; - } - - private static Version ParseVersion(string value, bool canBeNull, string name) - { - if (string.IsNullOrWhiteSpace(value) && canBeNull) - return null; - - Version version; - if (Version.TryParse(value, out version)) - return version; - - throw new ArgumentException($"Failed to parse \"{value}\" as {name} version."); - } - - public virtual bool IsValid => ClientVersion != null; - } -} \ No newline at end of file diff --git a/src/Umbraco.ModelsBuilder/Building/Builder.cs b/src/Umbraco.ModelsBuilder/Building/Builder.cs deleted file mode 100644 index acfa402355..0000000000 --- a/src/Umbraco.ModelsBuilder/Building/Builder.cs +++ /dev/null @@ -1,322 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Umbraco.Core.Configuration; -using Umbraco.ModelsBuilder.Configuration; - -namespace Umbraco.ModelsBuilder.Building -{ - // NOTE - // The idea was to have different types of builder, because I wanted to experiment with - // building code with CodeDom. Turns out more complicated than I thought and maybe not - // worth it at the moment, to we're using TextBuilder and its Generate method is specific. - // - // Keeping the code as-is for the time being... - - /// - /// Provides a base class for all builders. - /// - internal abstract class Builder - { - private readonly IList _typeModels; - - protected Dictionary ModelsMap { get; } = new Dictionary(); - protected ParseResult ParseResult { get; } - - // the list of assemblies that will be 'using' by default - protected readonly IList TypesUsing = new List - { - "System", - "System.Collections.Generic", - "System.Linq.Expressions", - "System.Web", - "Umbraco.Core.Models", - "Umbraco.Core.Models.PublishedContent", - "Umbraco.Web", - "Umbraco.ModelsBuilder", - "Umbraco.ModelsBuilder.Umbraco", - }; - - /// - /// Gets or sets a value indicating the namespace to use for the models. - /// - /// May be overriden by code attributes. - public string ModelsNamespace { get; set; } - - /// - /// Gets the list of assemblies to add to the set of 'using' assemblies in each model file. - /// - public IList Using => TypesUsing; - - /// - /// Gets the list of models to generate. - /// - /// The models to generate, ie those that are not ignored. - public IEnumerable GetModelsToGenerate() - { - return _typeModels.Where(x => !x.IsContentIgnored); - } - - /// - /// Gets the list of all models. - /// - /// Includes those that are ignored. - internal IList TypeModels => _typeModels; - - /// - /// Initializes a new instance of the class with a list of models to generate - /// and the result of code parsing. - /// - /// The list of models to generate. - /// The result of code parsing. - protected Builder(IList typeModels, ParseResult parseResult) - { - _typeModels = typeModels ?? throw new ArgumentNullException(nameof(typeModels)); - ParseResult = parseResult ?? throw new ArgumentNullException(nameof(parseResult)); - - Prepare(); - } - - /// - /// Initializes a new instance of the class with a list of models to generate, - /// the result of code parsing, and a models namespace. - /// - /// The list of models to generate. - /// The result of code parsing. - /// The models namespace. - protected Builder(IList typeModels, ParseResult parseResult, string modelsNamespace) - : this(typeModels, parseResult) - { - // can be null or empty, we'll manage - ModelsNamespace = modelsNamespace; - } - - // for unit tests only - protected Builder() - { } - - /// - /// Prepares generation by processing the result of code parsing. - /// - /// - /// Preparation includes figuring out from the existing code which models or properties should - /// be ignored or renamed, etc. -- anything that comes from the attributes in the existing code. - /// - private void Prepare() - { - var pureLive = UmbracoConfig.For.ModelsBuilder().ModelsMode == ModelsMode.PureLive; - - // mark IsContentIgnored models that we discovered should be ignored - // then propagate / ignore children of ignored contents - // ignore content = don't generate a class for it, don't generate children - foreach (var typeModel in _typeModels.Where(x => ParseResult.IsIgnored(x.Alias))) - typeModel.IsContentIgnored = true; - foreach (var typeModel in _typeModels.Where(x => !x.IsContentIgnored && x.EnumerateBaseTypes().Any(xx => xx.IsContentIgnored))) - typeModel.IsContentIgnored = true; - - // handle model renames - foreach (var typeModel in _typeModels.Where(x => ParseResult.IsContentRenamed(x.Alias))) - { - typeModel.ClrName = ParseResult.ContentClrName(typeModel.Alias); - typeModel.IsRenamed = true; - ModelsMap[typeModel.Alias] = typeModel.ClrName; - } - - // handle implement - foreach (var typeModel in _typeModels.Where(x => ParseResult.HasContentImplement(x.Alias))) - { - typeModel.HasImplement = true; - } - - // mark OmitBase models that we discovered already have a base class - foreach (var typeModel in _typeModels.Where(x => ParseResult.HasContentBase(ParseResult.ContentClrName(x.Alias) ?? x.ClrName))) - typeModel.HasBase = true; - - foreach (var typeModel in _typeModels) - { - // mark IsRemoved properties that we discovered should be ignored - // ie is marked as ignored on type, or on any parent type - var tm = typeModel; - foreach (var property in typeModel.Properties - .Where(property => tm.EnumerateBaseTypes(true).Any(x => ParseResult.IsPropertyIgnored(ParseResult.ContentClrName(x.Alias) ?? x.ClrName, property.Alias)))) - { - property.IsIgnored = true; - } - - // handle property renames - foreach (var property in typeModel.Properties) - property.ClrName = ParseResult.PropertyClrName(ParseResult.ContentClrName(typeModel.Alias) ?? typeModel.ClrName, property.Alias) ?? property.ClrName; - } - - // for the first two of these two tests, - // always throw, even in purelive: cannot happen unless ppl start fidling with attributes to rename - // things, and then they should pay attention to the generation error log - there's no magic here - // for the last one, don't throw in purelive, see comment - - // ensure we have no duplicates type names - foreach (var xx in _typeModels.Where(x => !x.IsContentIgnored).GroupBy(x => x.ClrName).Where(x => x.Count() > 1)) - throw new InvalidOperationException($"Type name \"{xx.Key}\" is used" - + $" for types with alias {string.Join(", ", xx.Select(x => x.ItemType + ":\"" + x.Alias + "\""))}. Names have to be unique." - + " Consider using an attribute to assign different names to conflicting types."); - - // ensure we have no duplicates property names - foreach (var typeModel in _typeModels.Where(x => !x.IsContentIgnored)) - foreach (var xx in typeModel.Properties.Where(x => !x.IsIgnored).GroupBy(x => x.ClrName).Where(x => x.Count() > 1)) - throw new InvalidOperationException($"Property name \"{xx.Key}\" in type {typeModel.ItemType}:\"{typeModel.Alias}\"" - + $" is used for properties with alias {string.Join(", ", xx.Select(x => "\"" + x.Alias + "\""))}. Names have to be unique." - + " Consider using an attribute to assign different names to conflicting properties."); - - // ensure content & property type don't have identical name (csharp hates it) - foreach (var typeModel in _typeModels.Where(x => !x.IsContentIgnored)) - { - foreach (var xx in typeModel.Properties.Where(x => !x.IsIgnored && x.ClrName == typeModel.ClrName)) - { - if (!pureLive) - throw new InvalidOperationException($"The model class for content type with alias \"{typeModel.Alias}\" is named \"{xx.ClrName}\"." - + $" CSharp does not support using the same name for the property with alias \"{xx.Alias}\"." - + " Consider using an attribute to assign a different name to the property."); - - // for purelive, will we generate a commented out properties with an error message, - // instead of throwing, because then it kills the sites and ppl don't understand why - xx.AddError($"The class {typeModel.ClrName} cannot implement this property, because" - + $" CSharp does not support naming the property with alias \"{xx.Alias}\" with the same name as content type with alias \"{typeModel.Alias}\"." - + " Consider using an attribute to assign a different name to the property."); - - // will not be implemented on interface nor class - // note: we will still create the static getter, and implement the property on other classes... - } - } - - // ensure we have no collision between base types - // NO: we may want to define a base class in a partial, on a model that has a parent - // we are NOT checking that the defined base type does maintain the inheritance chain - //foreach (var xx in _typeModels.Where(x => !x.IsContentIgnored).Where(x => x.BaseType != null && x.HasBase)) - // throw new InvalidOperationException(string.Format("Type alias \"{0}\" has more than one parent class.", - // xx.Alias)); - - // discover interfaces that need to be declared / implemented - foreach (var typeModel in _typeModels) - { - // collect all the (non-removed) types implemented at parent level - // ie the parent content types and the mixins content types, recursively - var parentImplems = new List(); - if (typeModel.BaseType != null && !typeModel.BaseType.IsContentIgnored) - TypeModel.CollectImplems(parentImplems, typeModel.BaseType); - - // interfaces we must declare we implement (initially empty) - // ie this type's mixins, except those that have been removed, - // and except those that are already declared at the parent level - // in other words, DeclaringInterfaces is "local mixins" - var declaring = typeModel.MixinTypes - .Where(x => !x.IsContentIgnored) - .Except(parentImplems); - typeModel.DeclaringInterfaces.AddRange(declaring); - - // interfaces we must actually implement (initially empty) - // if we declare we implement a mixin interface, we must actually implement - // its properties, all recursively (ie if the mixin interface implements...) - // so, starting with local mixins, we collect all the (non-removed) types above them - var mixinImplems = new List(); - foreach (var i in typeModel.DeclaringInterfaces) - TypeModel.CollectImplems(mixinImplems, i); - // and then we remove from that list anything that is already declared at the parent level - typeModel.ImplementingInterfaces.AddRange(mixinImplems.Except(parentImplems)); - } - - // register using types - foreach (var usingNamespace in ParseResult.UsingNamespaces) - { - if (!TypesUsing.Contains(usingNamespace)) - TypesUsing.Add(usingNamespace); - } - - // discover static mixin methods - foreach (var typeModel in _typeModels) - typeModel.StaticMixinMethods.AddRange(ParseResult.StaticMixinMethods(typeModel.ClrName)); - - // handle ctor - foreach (var typeModel in _typeModels.Where(x => ParseResult.HasCtor(x.ClrName))) - typeModel.HasCtor = true; - } - - private SemanticModel _ambiguousSymbolsModel; - private int _ambiguousSymbolsPos; - - // internal for tests - internal void PrepareAmbiguousSymbols() - { - var codeBuilder = new StringBuilder(); - foreach (var t in TypesUsing) - codeBuilder.AppendFormat("using {0};\n", t); - - codeBuilder.AppendFormat("namespace {0}\n{{ }}\n", GetModelsNamespace()); - - var compiler = new Compiler(); - SyntaxTree[] trees; - var compilation = compiler.GetCompilation("MyCompilation", new Dictionary { { "code", codeBuilder.ToString() } }, out trees); - var tree = trees[0]; - _ambiguousSymbolsModel = compilation.GetSemanticModel(tree); - - var namespaceSyntax = tree.GetRoot().DescendantNodes().OfType().First(); - //var namespaceSymbol = model.GetDeclaredSymbol(namespaceSyntax); - _ambiguousSymbolsPos = namespaceSyntax.OpenBraceToken.SpanStart; - } - - // looking for a simple symbol eg 'Umbraco' or 'String' - // expecting to match eg 'Umbraco' or 'System.String' - // returns true if either - // - more than 1 symbol is found (explicitely ambiguous) - // - 1 symbol is found BUT not matching (implicitely ambiguous) - protected bool IsAmbiguousSymbol(string symbol, string match) - { - if (_ambiguousSymbolsModel == null) - PrepareAmbiguousSymbols(); - if (_ambiguousSymbolsModel == null) - throw new Exception("Could not prepare ambiguous symbols."); - var symbols = _ambiguousSymbolsModel.LookupNamespacesAndTypes(_ambiguousSymbolsPos, null, symbol); - - if (symbols.Length > 1) return true; - if (symbols.Length == 0) return false; // what else? - - // only 1 - ensure it matches - var found = symbols[0].ToDisplayString(); - var pos = found.IndexOf('<'); // generic? - if (pos > 0) found = found.Substring(0, pos); // strip - return found != match; // and compare - } - - internal string ModelsNamespaceForTests; - - public string GetModelsNamespace() - { - if (ModelsNamespaceForTests != null) - return ModelsNamespaceForTests; - - // code attribute overrides everything - if (ParseResult.HasModelsNamespace) - return ParseResult.ModelsNamespace; - - // if builder was initialized with a namespace, use that one - if (!string.IsNullOrWhiteSpace(ModelsNamespace)) - return ModelsNamespace; - - // default - // fixme - should NOT reference config here, should make ModelsNamespace mandatory - return UmbracoConfig.For.ModelsBuilder().ModelsNamespace; - } - - protected string GetModelsBaseClassName(TypeModel type) - { - // code attribute overrides everything - if (ParseResult.HasModelsBaseClassName) - return ParseResult.ModelsBaseClassName; - - // default - return type.IsElement ? "PublishedElementModel" : "PublishedContentModel"; - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Building/CodeDomBuilder.cs b/src/Umbraco.ModelsBuilder/Building/CodeDomBuilder.cs deleted file mode 100644 index 925337bd1e..0000000000 --- a/src/Umbraco.ModelsBuilder/Building/CodeDomBuilder.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System.CodeDom; -using System.Collections.Generic; - -namespace Umbraco.ModelsBuilder.Building -{ - // NOTE - // See nodes in Builder.cs class - that one does not work, is not complete, - // and was just some sort of experiment... - - /// - /// Implements a builder that works by using CodeDom - /// - internal class CodeDomBuilder : Builder - { - /// - /// Initializes a new instance of the class with a list of models to generate. - /// - /// The list of models to generate. - public CodeDomBuilder(IList typeModels) - : base(typeModels, null) - { } - - /// - /// Outputs a generated model to a code namespace. - /// - /// The code namespace. - /// The model to generate. - public void Generate(CodeNamespace ns, TypeModel typeModel) - { - // what about USING? - // what about references? - - if (typeModel.IsMixin) - { - var i = new CodeTypeDeclaration("I" + typeModel.ClrName) - { - IsInterface = true, - IsPartial = true, - Attributes = MemberAttributes.Public - }; - i.BaseTypes.Add(typeModel.BaseType == null ? "IPublishedContent" : "I" + typeModel.BaseType.ClrName); - - foreach (var mixinType in typeModel.DeclaringInterfaces) - i.BaseTypes.Add(mixinType.ClrName); - - i.Comments.Add(new CodeCommentStatement($"Mixin content Type {typeModel.Id} with alias \"{typeModel.Alias}\"")); - - foreach (var propertyModel in typeModel.Properties) - { - var p = new CodeMemberProperty - { - Name = propertyModel.ClrName, - Type = new CodeTypeReference(propertyModel.ModelClrType), - Attributes = MemberAttributes.Public, - HasGet = true, - HasSet = false - }; - i.Members.Add(p); - } - } - - var c = new CodeTypeDeclaration(typeModel.ClrName) - { - IsClass = true, - IsPartial = true, - Attributes = MemberAttributes.Public - }; - - c.BaseTypes.Add(typeModel.BaseType == null ? "PublishedContentModel" : typeModel.BaseType.ClrName); - - // if it's a missing it implements its own interface - if (typeModel.IsMixin) - c.BaseTypes.Add("I" + typeModel.ClrName); - - // write the mixins, if any, as interfaces - // only if not a mixin because otherwise the interface already has them - if (typeModel.IsMixin == false) - foreach (var mixinType in typeModel.DeclaringInterfaces) - c.BaseTypes.Add("I" + mixinType.ClrName); - - foreach (var mixin in typeModel.MixinTypes) - c.BaseTypes.Add("I" + mixin.ClrName); - - c.Comments.Add(new CodeCommentStatement($"Content Type {typeModel.Id} with alias \"{typeModel.Alias}\"")); - - foreach (var propertyModel in typeModel.Properties) - { - var p = new CodeMemberProperty - { - Name = propertyModel.ClrName, - Type = new CodeTypeReference(propertyModel.ModelClrType), - Attributes = MemberAttributes.Public, - HasGet = true, - HasSet = false - }; - p.GetStatements.Add(new CodeMethodReturnStatement( // return - new CodeMethodInvokeExpression( - new CodeMethodReferenceExpression( - new CodeThisReferenceExpression(), // this - "Value", // .Value - new[] // - { - new CodeTypeReference(propertyModel.ModelClrType) - }), - new CodeExpression[] // ("alias") - { - new CodePrimitiveExpression(propertyModel.Alias) - }))); - c.Members.Add(p); - } - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Building/CodeParser.cs b/src/Umbraco.ModelsBuilder/Building/CodeParser.cs deleted file mode 100644 index 30fcbf1f91..0000000000 --- a/src/Umbraco.ModelsBuilder/Building/CodeParser.cs +++ /dev/null @@ -1,238 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Umbraco.Core.Models.PublishedContent; - -namespace Umbraco.ModelsBuilder.Building -{ - /// - /// Implements code parsing. - /// - /// Parses user's code and look for generator's instructions. - internal class CodeParser - { - /// - /// Parses a set of file. - /// - /// A set of (filename,content) representing content to parse. - /// The result of the code parsing. - /// The set of files is a dictionary of name, content. - public ParseResult Parse(IDictionary files) - { - return Parse(files, Enumerable.Empty()); - } - - /// - /// Parses a set of file. - /// - /// A set of (filename,content) representing content to parse. - /// Assemblies to reference in compilations. - /// The result of the code parsing. - /// The set of files is a dictionary of name, content. - public ParseResult Parse(IDictionary files, IEnumerable references) - { - SyntaxTree[] trees; - var compiler = new Compiler { References = references }; - var compilation = compiler.GetCompilation("Umbraco.ModelsBuilder.Generated", files, out trees); - - var disco = new ParseResult(); - foreach (var tree in trees) - Parse(disco, compilation, tree); - - return disco; - } - - public ParseResult ParseWithReferencedAssemblies(IDictionary files) - { - return Parse(files, ReferencedAssemblies.References); - } - - private static void Parse(ParseResult disco, CSharpCompilation compilation, SyntaxTree tree) - { - var model = compilation.GetSemanticModel(tree); - - //we quite probably have errors but that is normal - //var diags = model.GetDiagnostics(); - - var classDecls = tree.GetRoot().DescendantNodes().OfType(); - foreach (var classSymbol in classDecls.Select(x => model.GetDeclaredSymbol(x))) - { - ParseClassSymbols(disco, classSymbol); - - var baseClassSymbol = classSymbol.BaseType; - if (baseClassSymbol != null) - //disco.SetContentBaseClass(SymbolDisplay.ToDisplayString(classSymbol), SymbolDisplay.ToDisplayString(baseClassSymbol)); - disco.SetContentBaseClass(classSymbol.Name, baseClassSymbol.Name); - - var interfaceSymbols = classSymbol.Interfaces; - disco.SetContentInterfaces(classSymbol.Name, //SymbolDisplay.ToDisplayString(classSymbol), - interfaceSymbols.Select(x => x.Name)); //SymbolDisplay.ToDisplayString(x))); - - var hasCtor = classSymbol.Constructors - .Any(x => - { - if (x.IsStatic) return false; - if (x.Parameters.Length != 1) return false; - var type1 = x.Parameters[0].Type; - var type2 = typeof (IPublishedContent); - return type1.ToDisplayString() == type2.FullName; - }); - - if (hasCtor) - disco.SetHasCtor(classSymbol.Name); - - foreach (var propertySymbol in classSymbol.GetMembers().Where(x => x is IPropertySymbol)) - ParsePropertySymbols(disco, classSymbol, propertySymbol); - - foreach (var staticMethodSymbol in classSymbol.GetMembers().Where(x => x is IMethodSymbol)) - ParseMethodSymbol(disco, classSymbol, staticMethodSymbol); - } - - var interfaceDecls = tree.GetRoot().DescendantNodes().OfType(); - foreach (var interfaceSymbol in interfaceDecls.Select(x => model.GetDeclaredSymbol(x))) - { - ParseClassSymbols(disco, interfaceSymbol); - - var interfaceSymbols = interfaceSymbol.Interfaces; - disco.SetContentInterfaces(interfaceSymbol.Name, //SymbolDisplay.ToDisplayString(interfaceSymbol), - interfaceSymbols.Select(x => x.Name)); // SymbolDisplay.ToDisplayString(x))); - } - - ParseAssemblySymbols(disco, compilation.Assembly); - } - - private static void ParseClassSymbols(ParseResult disco, ISymbol symbol) - { - foreach (var attrData in symbol.GetAttributes()) - { - var attrClassSymbol = attrData.AttributeClass; - - // handle errors - if (attrClassSymbol is IErrorTypeSymbol) continue; - if (attrData.AttributeConstructor == null) continue; - - var attrClassName = SymbolDisplay.ToDisplayString(attrClassSymbol); - switch (attrClassName) - { - case "Umbraco.ModelsBuilder.IgnorePropertyTypeAttribute": - var propertyAliasToIgnore = (string)attrData.ConstructorArguments[0].Value; - disco.SetIgnoredProperty(symbol.Name /*SymbolDisplay.ToDisplayString(symbol)*/, propertyAliasToIgnore); - break; - case "Umbraco.ModelsBuilder.RenamePropertyTypeAttribute": - var propertyAliasToRename = (string)attrData.ConstructorArguments[0].Value; - var propertyRenamed = (string)attrData.ConstructorArguments[1].Value; - disco.SetRenamedProperty(symbol.Name /*SymbolDisplay.ToDisplayString(symbol)*/, propertyAliasToRename, propertyRenamed); - break; - // that one causes all sorts of issues with references to Umbraco.Core in Roslyn - //case "Umbraco.Core.Models.PublishedContent.PublishedContentModelAttribute": - // var contentAliasToRename = (string)attrData.ConstructorArguments[0].Value; - // disco.SetRenamedContent(contentAliasToRename, symbol.Name /*SymbolDisplay.ToDisplayString(symbol)*/); - // break; - case "Umbraco.ModelsBuilder.ImplementContentTypeAttribute": - var contentAliasToRename = (string)attrData.ConstructorArguments[0].Value; - disco.SetRenamedContent(contentAliasToRename, symbol.Name, true /*SymbolDisplay.ToDisplayString(symbol)*/); - break; - } - } - } - - private static void ParsePropertySymbols(ParseResult disco, ISymbol classSymbol, ISymbol symbol) - { - foreach (var attrData in symbol.GetAttributes()) - { - var attrClassSymbol = attrData.AttributeClass; - - // handle errors - if (attrClassSymbol is IErrorTypeSymbol) continue; - if (attrData.AttributeConstructor == null) continue; - - var attrClassName = SymbolDisplay.ToDisplayString(attrClassSymbol); - // ReSharper disable once SwitchStatementMissingSomeCases - switch (attrClassName) - { - case "Umbraco.ModelsBuilder.ImplementPropertyTypeAttribute": - var propertyAliasToIgnore = (string)attrData.ConstructorArguments[0].Value; - disco.SetIgnoredProperty(classSymbol.Name /*SymbolDisplay.ToDisplayString(classSymbol)*/, propertyAliasToIgnore); - break; - } - } - } - - private static void ParseAssemblySymbols(ParseResult disco, ISymbol symbol) - { - foreach (var attrData in symbol.GetAttributes()) - { - var attrClassSymbol = attrData.AttributeClass; - - // handle errors - if (attrClassSymbol is IErrorTypeSymbol) continue; - if (attrData.AttributeConstructor == null) continue; - - var attrClassName = SymbolDisplay.ToDisplayString(attrClassSymbol); - switch (attrClassName) - { - case "Umbraco.ModelsBuilder.IgnoreContentTypeAttribute": - var contentAliasToIgnore = (string)attrData.ConstructorArguments[0].Value; - // see notes in IgnoreContentTypeAttribute - //var ignoreContent = (bool)attrData.ConstructorArguments[1].Value; - //var ignoreMixin = (bool)attrData.ConstructorArguments[1].Value; - //var ignoreMixinProperties = (bool)attrData.ConstructorArguments[1].Value; - disco.SetIgnoredContent(contentAliasToIgnore /*, ignoreContent, ignoreMixin, ignoreMixinProperties*/); - break; - - case "Umbraco.ModelsBuilder.RenameContentTypeAttribute": - var contentAliasToRename = (string) attrData.ConstructorArguments[0].Value; - var contentRenamed = (string)attrData.ConstructorArguments[1].Value; - disco.SetRenamedContent(contentAliasToRename, contentRenamed, false); - break; - - case "Umbraco.ModelsBuilder.ModelsBaseClassAttribute": - var modelsBaseClass = (INamedTypeSymbol) attrData.ConstructorArguments[0].Value; - if (modelsBaseClass is IErrorTypeSymbol) - throw new Exception($"Invalid base class type \"{modelsBaseClass.Name}\"."); - disco.SetModelsBaseClassName(SymbolDisplay.ToDisplayString(modelsBaseClass)); - break; - - case "Umbraco.ModelsBuilder.ModelsNamespaceAttribute": - var modelsNamespace= (string) attrData.ConstructorArguments[0].Value; - disco.SetModelsNamespace(modelsNamespace); - break; - - case "Umbraco.ModelsBuilder.ModelsUsingAttribute": - var usingNamespace = (string)attrData.ConstructorArguments[0].Value; - disco.SetUsingNamespace(usingNamespace); - break; - } - } - } - - private static void ParseMethodSymbol(ParseResult disco, ISymbol classSymbol, ISymbol symbol) - { - var methodSymbol = symbol as IMethodSymbol; - - if (methodSymbol == null - || !methodSymbol.IsStatic - || methodSymbol.IsGenericMethod - || methodSymbol.ReturnsVoid - || methodSymbol.IsExtensionMethod - || methodSymbol.Parameters.Length != 1) - return; - - var returnType = methodSymbol.ReturnType; - var paramSymbol = methodSymbol.Parameters[0]; - var paramType = paramSymbol.Type; - - // cannot do this because maybe the param type is ISomething and we don't have - // that type yet - will be generated - so cannot put any condition on it really - //const string iPublishedContent = "Umbraco.Core.Models.IPublishedContent"; - //var implements = paramType.AllInterfaces.Any(x => x.ToDisplayString() == iPublishedContent); - //if (!implements) - // return; - - disco.SetStaticMixinMethod(classSymbol.Name, methodSymbol.Name, returnType.Name, paramType.Name); - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Building/Compiler.cs b/src/Umbraco.ModelsBuilder/Building/Compiler.cs deleted file mode 100644 index 66064bef0b..0000000000 --- a/src/Umbraco.ModelsBuilder/Building/Compiler.cs +++ /dev/null @@ -1,171 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Web; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Umbraco.Core.Configuration; -using Umbraco.ModelsBuilder.Configuration; - -namespace Umbraco.ModelsBuilder.Building -{ - // main Roslyn compiler - internal class Compiler - { - private readonly LanguageVersion _languageVersion; - - public Compiler() - : this(UmbracoConfig.For.ModelsBuilder().LanguageVersion) - { } - - public Compiler(LanguageVersion languageVersion) - { - _languageVersion = languageVersion; - References = ReferencedAssemblies.References; - Debug = HttpContext.Current != null && HttpContext.Current.IsDebuggingEnabled; - } - - // gets or sets the references - public IEnumerable References { get; set; } - - public bool Debug { get; set; } - - // gets a compilation - public CSharpCompilation GetCompilation(string assemblyName, IDictionary files) - { - SyntaxTree[] trees; - return GetCompilation(assemblyName, files, out trees); - } - - // gets a compilation - // used by CodeParser to get a "compilation" of the existing files - public CSharpCompilation GetCompilation(string assemblyName, IDictionary files, out SyntaxTree[] trees) - { - var options = new CSharpParseOptions(_languageVersion); - trees = files.Select(x => - { - var text = x.Value; - var tree = CSharpSyntaxTree.ParseText(text, /*options:*/ options); - var diagnostic = tree.GetDiagnostics().FirstOrDefault(y => y.Severity == DiagnosticSeverity.Error); - if (diagnostic != null) - ThrowExceptionFromDiagnostic(x.Key, x.Value, diagnostic); - return tree; - }).ToArray(); - - var refs = References; - - var compilationOptions = new CSharpCompilationOptions( - OutputKind.DynamicallyLinkedLibrary, - assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default, - optimizationLevel: Debug ? OptimizationLevel.Debug : OptimizationLevel.Release - ); - var compilation = CSharpCompilation.Create( - assemblyName, - /*syntaxTrees:*/ trees, - /*references:*/ refs, - compilationOptions); - - return compilation; - } - - // compile files into a Dll - // used by ModelsBuilderBackOfficeController in [Live]Dll mode, to compile the models to disk - public void Compile(string assemblyName, IDictionary files, string binPath) - { - var assemblyPath = Path.Combine(binPath, assemblyName + ".dll"); - using (var stream = new FileStream(assemblyPath, FileMode.Create)) - { - Compile(assemblyName, files, stream); - } - - // this is how we'd create the pdb: - /* - var pdbPath = Path.Combine(binPath, assemblyName + ".pdb"); - - // create the compilation - var compilation = GetCompilation(assemblyName, files); - - // check diagnostics for errors (not warnings) - foreach (var diag in compilation.GetDiagnostics().Where(x => x.Severity == DiagnosticSeverity.Error)) - ThrowExceptionFromDiagnostic(files, diag); - - // emit - var result = compilation.Emit(assemblyPath, pdbPath); - if (result.Success) return; - - // deal with errors - var diagnostic = result.Diagnostics.First(x => x.Severity == DiagnosticSeverity.Error); - ThrowExceptionFromDiagnostic(files, diagnostic); - */ - } - - // compile files into an assembly - public Assembly Compile(string assemblyName, IDictionary files) - { - using (var stream = new MemoryStream()) - { - Compile(assemblyName, files, stream); - return Assembly.Load(stream.GetBuffer()); - } - } - - // compile one file into an assembly - public Assembly Compile(string assemblyName, string path, string code) - { - using (var stream = new MemoryStream()) - { - Compile(assemblyName, new Dictionary { { path, code } }, stream); - return Assembly.Load(stream.GetBuffer()); - } - } - - // compiles files into a stream - public void Compile(string assemblyName, IDictionary files, Stream stream) - { - // create the compilation - var compilation = GetCompilation(assemblyName, files); - - // check diagnostics for errors (not warnings) - foreach (var diag in compilation.GetDiagnostics().Where(x => x.Severity == DiagnosticSeverity.Error)) - ThrowExceptionFromDiagnostic(files, diag); - - // emit - var result = compilation.Emit(stream); - if (result.Success) return; - - // deal with errors - var diagnostic = result.Diagnostics.First(x => x.Severity == DiagnosticSeverity.Error); - ThrowExceptionFromDiagnostic(files, diagnostic); - } - - // compiles one file into a stream - public void Compile(string assemblyName, string path, string code, Stream stream) - { - Compile(assemblyName, new Dictionary { { path, code } }, stream); - } - - private static void ThrowExceptionFromDiagnostic(IDictionary files, Diagnostic diagnostic) - { - var message = diagnostic.GetMessage(); - if (diagnostic.Location == Location.None) - throw new CompilerException(message); - - var position = diagnostic.Location.GetLineSpan().StartLinePosition.Line + 1; - var path = diagnostic.Location.SourceTree.FilePath; - var code = files.ContainsKey(path) ? files[path] : string.Empty; - throw new CompilerException(message, path, code, position); - } - - private static void ThrowExceptionFromDiagnostic(string path, string code, Diagnostic diagnostic) - { - var message = diagnostic.GetMessage(); - if (diagnostic.Location == Location.None) - throw new CompilerException(message); - - var position = diagnostic.Location.GetLineSpan().StartLinePosition.Line + 1; - throw new CompilerException(message, path, code, position); - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Building/CompilerException.cs b/src/Umbraco.ModelsBuilder/Building/CompilerException.cs deleted file mode 100644 index e978f67ae5..0000000000 --- a/src/Umbraco.ModelsBuilder/Building/CompilerException.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; - -namespace Umbraco.ModelsBuilder.Building -{ - public class CompilerException : Exception - { - public CompilerException(string message) - : base(message) - { } - - public CompilerException(string message, string path, string sourceCode, int line) - : base(message) - { - Path = path; - SourceCode = sourceCode; - Line = line; - } - - public string Path { get; } = string.Empty; - - public string SourceCode { get; } = string.Empty; - - public int Line { get; } = -1; - } -} diff --git a/src/Umbraco.ModelsBuilder/Building/ParseResult.cs b/src/Umbraco.ModelsBuilder/Building/ParseResult.cs deleted file mode 100644 index d1f61363ff..0000000000 --- a/src/Umbraco.ModelsBuilder/Building/ParseResult.cs +++ /dev/null @@ -1,275 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Umbraco.ModelsBuilder.Building -{ - /// - /// Contains the result of a code parsing. - /// - internal class ParseResult - { - // "alias" is the umbraco alias - // content "name" is the complete name eg Foo.Bar.Name - // property "name" is just the local name - - // see notes in IgnoreContentTypeAttribute - - private readonly HashSet _ignoredContent - = new HashSet(StringComparer.InvariantCultureIgnoreCase); - //private readonly HashSet _ignoredMixin - // = new HashSet(StringComparer.InvariantCultureIgnoreCase); - //private readonly HashSet _ignoredMixinProperties - // = new HashSet(StringComparer.InvariantCultureIgnoreCase); - private readonly Dictionary _renamedContent - = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - private readonly HashSet _withImplementContent - = new HashSet(StringComparer.InvariantCultureIgnoreCase); - private readonly Dictionary> _ignoredProperty - = new Dictionary>(); - private readonly Dictionary> _renamedProperty - = new Dictionary>(); - private readonly Dictionary _contentBase - = new Dictionary(); - private readonly Dictionary _contentInterfaces - = new Dictionary(); - private readonly List _usingNamespaces - = new List(); - private readonly Dictionary> _staticMixins - = new Dictionary>(); - private readonly HashSet _withCtor - = new HashSet(StringComparer.InvariantCultureIgnoreCase); - - public static readonly ParseResult Empty = new ParseResult(); - - private class StaticMixinMethodInfo - { - public StaticMixinMethodInfo(string contentName, string methodName, string returnType, string paramType) - { - ContentName = contentName; - MethodName = methodName; - //ReturnType = returnType; - //ParamType = paramType; - } - - // short name eg Type1 - public string ContentName { get; private set; } - - // short name eg GetProp1 - public string MethodName { get; private set; } - - // those types cannot be FQ because when parsing, some of them - // might not exist since we're generating them... and so prob. - // that info is worthless - not using it anyway at the moment... - - //public string ReturnType { get; private set; } - //public string ParamType { get; private set; } - } - - #region Declare - - // content with that alias should not be generated - // alias can end with a * (wildcard) - public void SetIgnoredContent(string contentAlias /*, bool ignoreContent, bool ignoreMixin, bool ignoreMixinProperties*/) - { - //if (ignoreContent) - _ignoredContent.Add(contentAlias); - //if (ignoreMixin) - // _ignoredMixin.Add(contentAlias); - //if (ignoreMixinProperties) - // _ignoredMixinProperties.Add(contentAlias); - } - - // content with that alias should be generated with a different name - public void SetRenamedContent(string contentAlias, string contentName, bool withImplement) - { - _renamedContent[contentAlias] = contentName; - if (withImplement) - _withImplementContent.Add(contentAlias); - } - - // property with that alias should not be generated - // applies to content name and any content that implements it - // here, contentName may be an interface - // alias can end with a * (wildcard) - public void SetIgnoredProperty(string contentName, string propertyAlias) - { - HashSet ignores; - if (!_ignoredProperty.TryGetValue(contentName, out ignores)) - ignores = _ignoredProperty[contentName] = new HashSet(StringComparer.InvariantCultureIgnoreCase); - ignores.Add(propertyAlias); - } - - // property with that alias should be generated with a different name - // applies to content name and any content that implements it - // here, contentName may be an interface - public void SetRenamedProperty(string contentName, string propertyAlias, string propertyName) - { - Dictionary renames; - if (!_renamedProperty.TryGetValue(contentName, out renames)) - renames = _renamedProperty[contentName] = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - renames[propertyAlias] = propertyName; - } - - // content with that name has a base class so no need to generate one - public void SetContentBaseClass(string contentName, string baseName) - { - if (baseName.ToLowerInvariant() != "object") - _contentBase[contentName] = baseName; - } - - // content with that name implements the interfaces - public void SetContentInterfaces(string contentName, IEnumerable interfaceNames) - { - _contentInterfaces[contentName] = interfaceNames.ToArray(); - } - - public void SetModelsBaseClassName(string modelsBaseClassName) - { - ModelsBaseClassName = modelsBaseClassName; - } - - public void SetModelsNamespace(string modelsNamespace) - { - ModelsNamespace = modelsNamespace; - } - - public void SetUsingNamespace(string usingNamespace) - { - _usingNamespaces.Add(usingNamespace); - } - - public void SetStaticMixinMethod(string contentName, string methodName, string returnType, string paramType) - { - if (!_staticMixins.ContainsKey(contentName)) - _staticMixins[contentName] = new List(); - - _staticMixins[contentName].Add(new StaticMixinMethodInfo(contentName, methodName, returnType, paramType)); - } - - public void SetHasCtor(string contentName) - { - _withCtor.Add(contentName); - } - - #endregion - - #region Query - - public bool IsIgnored(string contentAlias) - { - return IsContentOrMixinIgnored(contentAlias, _ignoredContent); - } - - //public bool IsMixinIgnored(string contentAlias) - //{ - // return IsContentOrMixinIgnored(contentAlias, _ignoredMixin); - //} - - //public bool IsMixinPropertiesIgnored(string contentAlias) - //{ - // return IsContentOrMixinIgnored(contentAlias, _ignoredMixinProperties); - //} - - private static bool IsContentOrMixinIgnored(string contentAlias, HashSet ignored) - { - if (ignored.Contains(contentAlias)) return true; - return ignored - .Where(x => x.EndsWith("*")) - .Select(x => x.Substring(0, x.Length - 1)) - .Any(x => contentAlias.StartsWith(x, StringComparison.InvariantCultureIgnoreCase)); - } - - public bool HasContentBase(string contentName) - { - return _contentBase.ContainsKey(contentName); - } - - public bool IsContentRenamed(string contentAlias) - { - return _renamedContent.ContainsKey(contentAlias); - } - - public bool HasContentImplement(string contentAlias) - { - return _withImplementContent.Contains(contentAlias); - } - - public string ContentClrName(string contentAlias) - { - string name; - return (_renamedContent.TryGetValue(contentAlias, out name)) ? name : null; - } - - public bool IsPropertyIgnored(string contentName, string propertyAlias) - { - HashSet ignores; - if (_ignoredProperty.TryGetValue(contentName, out ignores)) - { - if (ignores.Contains(propertyAlias)) return true; - if (ignores - .Where(x => x.EndsWith("*")) - .Select(x => x.Substring(0, x.Length - 1)) - .Any(x => propertyAlias.StartsWith(x, StringComparison.InvariantCultureIgnoreCase))) - return true; - } - string baseName; - if (_contentBase.TryGetValue(contentName, out baseName) - && IsPropertyIgnored(baseName, propertyAlias)) return true; - string[] interfaceNames; - if (_contentInterfaces.TryGetValue(contentName, out interfaceNames) - && interfaceNames.Any(interfaceName => IsPropertyIgnored(interfaceName, propertyAlias))) return true; - return false; - } - - public string PropertyClrName(string contentName, string propertyAlias) - { - Dictionary renames; - string name; - if (_renamedProperty.TryGetValue(contentName, out renames) - && renames.TryGetValue(propertyAlias, out name)) return name; - string baseName; - if (_contentBase.TryGetValue(contentName, out baseName) - && null != (name = PropertyClrName(baseName, propertyAlias))) return name; - string[] interfaceNames; - if (_contentInterfaces.TryGetValue(contentName, out interfaceNames) - && null != (name = interfaceNames - .Select(interfaceName => PropertyClrName(interfaceName, propertyAlias)) - .FirstOrDefault(x => x != null))) return name; - return null; - } - - public bool HasModelsBaseClassName - { - get { return !string.IsNullOrWhiteSpace(ModelsBaseClassName); } - } - - public string ModelsBaseClassName { get; private set; } - - public bool HasModelsNamespace - { - get { return !string.IsNullOrWhiteSpace(ModelsNamespace); } - } - - public string ModelsNamespace { get; private set; } - - public IEnumerable UsingNamespaces - { - get { return _usingNamespaces; } - } - - public IEnumerable StaticMixinMethods(string contentName) - { - return _staticMixins.ContainsKey(contentName) - ? _staticMixins[contentName].Select(x => x.MethodName) - : Enumerable.Empty() ; - } - - public bool HasCtor(string contentName) - { - return _withCtor.Contains(contentName); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Umbraco.ModelsBuilder/Building/PropertyModel.cs b/src/Umbraco.ModelsBuilder/Building/PropertyModel.cs deleted file mode 100644 index 1595b3f888..0000000000 --- a/src/Umbraco.ModelsBuilder/Building/PropertyModel.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Umbraco.ModelsBuilder.Building -{ - /// - /// Represents a model property. - /// - public class PropertyModel - { - /// - /// Gets the alias of the property. - /// - public string Alias; - - /// - /// Gets the name of the property. - /// - public string Name; - - /// - /// Gets the description of the property. - /// - public string Description; - - /// - /// Gets the clr name of the property. - /// - /// This is just the local name eg "Price". - public string ClrName; - - /// - /// Gets the Model Clr type of the property values. - /// - /// As indicated by the PublishedPropertyType, ie by the IPropertyValueConverter - /// if any, else object. May include some ModelType that will need to be mapped. - public Type ModelClrType; - - /// - /// Gets the CLR type name of the property values. - /// - public string ClrTypeName; - - /// - /// Gets a value indicating whether this property should be excluded from generation. - /// - public bool IsIgnored; - - /// - /// Gets the generation errors for the property. - /// - /// This should be null, unless something prevents the property from being - /// generated, and then the value should explain what. This can be used to generate - /// commented out code eg in PureLive. - public List Errors; - - /// - /// Adds an error. - /// - public void AddError(string error) - { - if (Errors == null) Errors = new List(); - Errors.Add(error); - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Building/TextBuilder.cs b/src/Umbraco.ModelsBuilder/Building/TextBuilder.cs deleted file mode 100644 index 85ccd541b7..0000000000 --- a/src/Umbraco.ModelsBuilder/Building/TextBuilder.cs +++ /dev/null @@ -1,554 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using Umbraco.Core.Configuration; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.ModelsBuilder.Configuration; - -namespace Umbraco.ModelsBuilder.Building -{ - /// - /// Implements a builder that works by writing text. - /// - internal class TextBuilder : Builder - { - /// - /// Initializes a new instance of the class with a list of models to generate - /// and the result of code parsing. - /// - /// The list of models to generate. - /// The result of code parsing. - public TextBuilder(IList typeModels, ParseResult parseResult) - : base(typeModels, parseResult) - { } - - /// - /// Initializes a new instance of the class with a list of models to generate, - /// the result of code parsing, and a models namespace. - /// - /// The list of models to generate. - /// The result of code parsing. - /// The models namespace. - public TextBuilder(IList typeModels, ParseResult parseResult, string modelsNamespace) - : base(typeModels, parseResult, modelsNamespace) - { } - - // internal for unit tests only - internal TextBuilder() - { } - - /// - /// Outputs a generated model to a string builder. - /// - /// The string builder. - /// The model to generate. - public void Generate(StringBuilder sb, TypeModel typeModel) - { - WriteHeader(sb); - - foreach (var t in TypesUsing) - sb.AppendFormat("using {0};\n", t); - - sb.Append("\n"); - sb.AppendFormat("namespace {0}\n", GetModelsNamespace()); - sb.Append("{\n"); - - WriteContentType(sb, typeModel); - - sb.Append("}\n"); - } - - /// - /// Outputs generated models to a string builder. - /// - /// The string builder. - /// The models to generate. - public void Generate(StringBuilder sb, IEnumerable typeModels) - { - WriteHeader(sb); - - foreach (var t in TypesUsing) - sb.AppendFormat("using {0};\n", t); - - // assembly attributes marker - sb.Append("\n//ASSATTR\n"); - - sb.Append("\n"); - sb.AppendFormat("namespace {0}\n", GetModelsNamespace()); - sb.Append("{\n"); - - foreach (var typeModel in typeModels) - { - WriteContentType(sb, typeModel); - sb.Append("\n"); - } - - sb.Append("}\n"); - } - - /// - /// Outputs an "auto-generated" header to a string builder. - /// - /// The string builder. - public static void WriteHeader(StringBuilder sb) - { - TextHeaderWriter.WriteHeader(sb); - } - - private void WriteContentType(StringBuilder sb, TypeModel type) - { - string sep; - - if (type.IsMixin) - { - // write the interface declaration - sb.AppendFormat("\t// Mixin content Type {0} with alias \"{1}\"\n", type.Id, type.Alias); - if (!string.IsNullOrWhiteSpace(type.Name)) - sb.AppendFormat("\t/// {0}\n", XmlCommentString(type.Name)); - sb.AppendFormat("\tpublic partial interface I{0}", type.ClrName); - var implements = type.BaseType == null || type.BaseType.IsContentIgnored - ? (type.HasBase ? null : (type.IsElement ? "PublishedElement" : "PublishedContent")) - : type.BaseType.ClrName; - if (implements != null) - sb.AppendFormat(" : I{0}", implements); - - // write the mixins - sep = implements == null ? ":" : ","; - foreach (var mixinType in type.DeclaringInterfaces.OrderBy(x => x.ClrName)) - { - sb.AppendFormat("{0} I{1}", sep, mixinType.ClrName); - sep = ","; - } - - sb.Append("\n\t{\n"); - - // write the properties - only the local (non-ignored) ones, we're an interface - var more = false; - foreach (var prop in type.Properties.Where(x => !x.IsIgnored).OrderBy(x => x.ClrName)) - { - if (more) sb.Append("\n"); - more = true; - WriteInterfaceProperty(sb, prop); - } - - sb.Append("\t}\n\n"); - } - - // write the class declaration - if (type.IsRenamed) - sb.AppendFormat("\t// Content Type {0} with alias \"{1}\"\n", type.Id, type.Alias); - if (!string.IsNullOrWhiteSpace(type.Name)) - sb.AppendFormat("\t/// {0}\n", XmlCommentString(type.Name)); - // cannot do it now. see note in ImplementContentTypeAttribute - //if (!type.HasImplement) - // sb.AppendFormat("\t[ImplementContentType(\"{0}\")]\n", type.Alias); - sb.AppendFormat("\t[PublishedModel(\"{0}\")]\n", type.Alias); - sb.AppendFormat("\tpublic partial class {0}", type.ClrName); - var inherits = type.HasBase - ? null // has its own base already - : (type.BaseType == null || type.BaseType.IsContentIgnored - ? GetModelsBaseClassName(type) - : type.BaseType.ClrName); - if (inherits != null) - sb.AppendFormat(" : {0}", inherits); - - sep = inherits == null ? ":" : ","; - if (type.IsMixin) - { - // if it's a mixin it implements its own interface - sb.AppendFormat("{0} I{1}", sep, type.ClrName); - } - else - { - // write the mixins, if any, as interfaces - // only if not a mixin because otherwise the interface already has them already - foreach (var mixinType in type.DeclaringInterfaces.OrderBy(x => x.ClrName)) - { - sb.AppendFormat("{0} I{1}", sep, mixinType.ClrName); - sep = ","; - } - } - - // begin class body - sb.Append("\n\t{\n"); - - // write the constants & static methods - // as 'new' since parent has its own - or maybe not - disable warning - sb.Append("\t\t// helpers\n"); - sb.Append("#pragma warning disable 0109 // new is redundant\n"); - sb.AppendFormat("\t\tpublic new const string ModelTypeAlias = \"{0}\";\n", - type.Alias); - var itemType = type.IsElement ? TypeModel.ItemTypes.Content : type.ItemType; // fixme - sb.AppendFormat("\t\tpublic new const PublishedItemType ModelItemType = PublishedItemType.{0};\n", - itemType); - sb.Append("\t\tpublic new static PublishedContentType GetModelContentType()\n"); - sb.Append("\t\t\t=> PublishedModelUtility.GetModelContentType(ModelItemType, ModelTypeAlias);\n"); - sb.AppendFormat("\t\tpublic static PublishedPropertyType GetModelPropertyType(Expression> selector)\n", - type.ClrName); - sb.Append("\t\t\t=> PublishedModelUtility.GetModelPropertyType(GetModelContentType(), selector);\n"); - sb.Append("#pragma warning restore 0109\n\n"); - - // write the ctor - if (!type.HasCtor) - sb.AppendFormat("\t\t// ctor\n\t\tpublic {0}(IPublished{1} content)\n\t\t\t: base(content)\n\t\t{{ }}\n\n", - type.ClrName, type.IsElement ? "Element" : "Content"); - - // write the properties - sb.Append("\t\t// properties\n"); - WriteContentTypeProperties(sb, type); - - // close the class declaration - sb.Append("\t}\n"); - } - - private void WriteContentTypeProperties(StringBuilder sb, TypeModel type) - { - var staticMixinGetters = UmbracoConfig.For.ModelsBuilder().StaticMixinGetters; - - // write the properties - foreach (var prop in type.Properties.Where(x => !x.IsIgnored).OrderBy(x => x.ClrName)) - WriteProperty(sb, type, prop, staticMixinGetters && type.IsMixin ? type.ClrName : null); - - // no need to write the parent properties since we inherit from the parent - // and the parent defines its own properties. need to write the mixins properties - // since the mixins are only interfaces and we have to provide an implementation. - - // write the mixins properties - foreach (var mixinType in type.ImplementingInterfaces.OrderBy(x => x.ClrName)) - foreach (var prop in mixinType.Properties.Where(x => !x.IsIgnored).OrderBy(x => x.ClrName)) - if (staticMixinGetters) - WriteMixinProperty(sb, prop, mixinType.ClrName); - else - WriteProperty(sb, mixinType, prop); - } - - private void WriteMixinProperty(StringBuilder sb, PropertyModel property, string mixinClrName) - { - sb.Append("\n"); - - // Adds xml summary to each property containing - // property name and property description - if (!string.IsNullOrWhiteSpace(property.Name) || !string.IsNullOrWhiteSpace(property.Description)) - { - sb.Append("\t\t///\n"); - - if (!string.IsNullOrWhiteSpace(property.Description)) - sb.AppendFormat("\t\t/// {0}: {1}\n", XmlCommentString(property.Name), XmlCommentString(property.Description)); - else - sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name)); - - sb.Append("\t\t///\n"); - } - - sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias); - - sb.Append("\t\tpublic "); - WriteClrType(sb, property.ClrTypeName); - - sb.AppendFormat(" {0} => ", - property.ClrName); - WriteNonGenericClrType(sb, GetModelsNamespace() + "." + mixinClrName); - sb.AppendFormat(".{0}(this);\n", - MixinStaticGetterName(property.ClrName)); - } - - private static string MixinStaticGetterName(string clrName) - { - return string.Format(UmbracoConfig.For.ModelsBuilder().StaticMixinGetterPattern, clrName); - } - - private void WriteProperty(StringBuilder sb, TypeModel type, PropertyModel property, string mixinClrName = null) - { - var mixinStatic = mixinClrName != null; - - sb.Append("\n"); - - if (property.Errors != null) - { - sb.Append("\t\t/*\n"); - sb.Append("\t\t * THIS PROPERTY CANNOT BE IMPLEMENTED, BECAUSE:\n"); - sb.Append("\t\t *\n"); - var first = true; - foreach (var error in property.Errors) - { - if (first) first = false; - else sb.Append("\t\t *\n"); - foreach (var s in SplitError(error)) - { - sb.Append("\t\t * "); - sb.Append(s); - sb.Append("\n"); - } - } - sb.Append("\t\t *\n"); - sb.Append("\n"); - } - - // Adds xml summary to each property containing - // property name and property description - if (!string.IsNullOrWhiteSpace(property.Name) || !string.IsNullOrWhiteSpace(property.Description)) - { - sb.Append("\t\t///\n"); - - if (!string.IsNullOrWhiteSpace(property.Description)) - sb.AppendFormat("\t\t/// {0}: {1}\n", XmlCommentString(property.Name), XmlCommentString(property.Description)); - else - sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name)); - - sb.Append("\t\t///\n"); - } - - sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias); - - if (mixinStatic) - { - sb.Append("\t\tpublic "); - WriteClrType(sb, property.ClrTypeName); - sb.AppendFormat(" {0} => {1}(this);\n", - property.ClrName, MixinStaticGetterName(property.ClrName)); - } - else - { - sb.Append("\t\tpublic "); - WriteClrType(sb, property.ClrTypeName); - sb.AppendFormat(" {0} => this.Value", - property.ClrName); - if (property.ModelClrType != typeof(object)) - { - sb.Append("<"); - WriteClrType(sb, property.ClrTypeName); - sb.Append(">"); - } - sb.AppendFormat("(\"{0}\");\n", - property.Alias); - } - - if (property.Errors != null) - { - sb.Append("\n"); - sb.Append("\t\t *\n"); - sb.Append("\t\t */\n"); - } - - if (!mixinStatic) return; - - var mixinStaticGetterName = MixinStaticGetterName(property.ClrName); - - if (type.StaticMixinMethods.Contains(mixinStaticGetterName)) return; - - sb.Append("\n"); - - if (!string.IsNullOrWhiteSpace(property.Name)) - sb.AppendFormat("\t\t/// Static getter for {0}\n", XmlCommentString(property.Name)); - - sb.Append("\t\tpublic static "); - WriteClrType(sb, property.ClrTypeName); - sb.AppendFormat(" {0}(I{1} that) => that.Value", - mixinStaticGetterName, mixinClrName); - if (property.ModelClrType != typeof(object)) - { - sb.Append("<"); - WriteClrType(sb, property.ClrTypeName); - sb.Append(">"); - } - sb.AppendFormat("(\"{0}\");\n", - property.Alias); - } - - private static IEnumerable SplitError(string error) - { - var p = 0; - while (p < error.Length) - { - var n = p + 50; - while (n < error.Length && error[n] != ' ') n++; - if (n >= error.Length) break; - yield return error.Substring(p, n - p); - p = n + 1; - } - if (p < error.Length) - yield return error.Substring(p); - } - - private void WriteInterfaceProperty(StringBuilder sb, PropertyModel property) - { - if (property.Errors != null) - { - sb.Append("\t\t/*\n"); - sb.Append("\t\t * THIS PROPERTY CANNOT BE IMPLEMENTED, BECAUSE:\n"); - sb.Append("\t\t *\n"); - var first = true; - foreach (var error in property.Errors) - { - if (first) first = false; - else sb.Append("\t\t *\n"); - foreach (var s in SplitError(error)) - { - sb.Append("\t\t * "); - sb.Append(s); - sb.Append("\n"); - } - } - sb.Append("\t\t *\n"); - sb.Append("\n"); - } - - if (!string.IsNullOrWhiteSpace(property.Name)) - sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name)); - sb.Append("\t\t"); - WriteClrType(sb, property.ClrTypeName); - sb.AppendFormat(" {0} {{ get; }}\n", - property.ClrName); - - if (property.Errors != null) - { - sb.Append("\n"); - sb.Append("\t\t *\n"); - sb.Append("\t\t */\n"); - } - } - - // internal for unit tests - internal void WriteClrType(StringBuilder sb, Type type) - { - var s = type.ToString(); - - if (type.IsGenericType) - { - var p = s.IndexOf('`'); - WriteNonGenericClrType(sb, s.Substring(0, p)); - sb.Append("<"); - var args = type.GetGenericArguments(); - for (var i = 0; i < args.Length; i++) - { - if (i > 0) sb.Append(", "); - WriteClrType(sb, args[i]); - } - sb.Append(">"); - } - else - { - WriteNonGenericClrType(sb, s); - } - } - - internal void WriteClrType(StringBuilder sb, string type) - { - var p = type.IndexOf('<'); - if (type.Contains('<')) - { - WriteNonGenericClrType(sb, type.Substring(0, p)); - sb.Append("<"); - var args = type.Substring(p + 1).TrimEnd('>').Split(','); // fixme will NOT work with nested generic types - for (var i = 0; i < args.Length; i++) - { - if (i > 0) sb.Append(", "); - WriteClrType(sb, args[i]); - } - sb.Append(">"); - } - else - { - WriteNonGenericClrType(sb, type); - } - } - - private void WriteNonGenericClrType(StringBuilder sb, string s) - { - // map model types - s = Regex.Replace(s, @"\{(.*)\}\[\*\]", m => ModelsMap[m.Groups[1].Value + "[]"]); - - // takes care eg of "System.Int32" vs. "int" - if (TypesMap.TryGetValue(s.ToLowerInvariant(), out string typeName)) - { - sb.Append(typeName); - return; - } - - // if full type name matches a using clause, strip - // so if we want Umbraco.Core.Models.IPublishedContent - // and using Umbraco.Core.Models, then we just need IPublishedContent - typeName = s; - string typeUsing = null; - var p = typeName.LastIndexOf('.'); - if (p > 0) - { - var x = typeName.Substring(0, p); - if (Using.Contains(x)) - { - typeName = typeName.Substring(p + 1); - typeUsing = x; - } - } - - // nested types *after* using - typeName = typeName.Replace("+", "."); - - // symbol to test is the first part of the name - // so if type name is Foo.Bar.Nil we want to ensure that Foo is not ambiguous - p = typeName.IndexOf('.'); - var symbol = p > 0 ? typeName.Substring(0, p) : typeName; - - // what we should find - WITHOUT any generic thing - just the type - // no 'using' = the exact symbol - // a 'using' = using.symbol - var match = typeUsing == null ? symbol : (typeUsing + "." + symbol); - - // if not ambiguous, be happy - if (!IsAmbiguousSymbol(symbol, match)) - { - sb.Append(typeName); - return; - } - - // symbol is ambiguous - // if no 'using', must prepend global:: - if (typeUsing == null) - { - sb.Append("global::"); - sb.Append(s.Replace("+", ".")); - return; - } - - // could fullname be non-ambiguous? - // note: all-or-nothing, not trying to segment the using clause - typeName = s.Replace("+", "."); - p = typeName.IndexOf('.'); - symbol = typeName.Substring(0, p); - match = symbol; - - // still ambiguous, must prepend global:: - if (IsAmbiguousSymbol(symbol, match)) - sb.Append("global::"); - - sb.Append(typeName); - } - - private static string XmlCommentString(string s) - { - return s.Replace('<', '{').Replace('>', '}').Replace('\r', ' ').Replace('\n', ' '); - } - - private static readonly IDictionary TypesMap = new Dictionary - { - { "system.int16", "short" }, - { "system.int32", "int" }, - { "system.int64", "long" }, - { "system.string", "string" }, - { "system.object", "object" }, - { "system.boolean", "bool" }, - { "system.void", "void" }, - { "system.char", "char" }, - { "system.byte", "byte" }, - { "system.uint16", "ushort" }, - { "system.uint32", "uint" }, - { "system.uint64", "ulong" }, - { "system.sbyte", "sbyte" }, - { "system.single", "float" }, - { "system.double", "double" }, - { "system.decimal", "decimal" } - }; - } -} diff --git a/src/Umbraco.ModelsBuilder/Building/TextHeaderWriter.cs b/src/Umbraco.ModelsBuilder/Building/TextHeaderWriter.cs deleted file mode 100644 index d165f03907..0000000000 --- a/src/Umbraco.ModelsBuilder/Building/TextHeaderWriter.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Text; -using Umbraco.ModelsBuilder.Api; - -namespace Umbraco.ModelsBuilder.Building -{ - public static class TextHeaderWriter - { - /// - /// Outputs an "auto-generated" header to a string builder. - /// - /// The string builder. - public static void WriteHeader(StringBuilder sb) - { - sb.Append("//------------------------------------------------------------------------------\n"); - sb.Append("// \n"); - sb.Append("// This code was generated by a tool.\n"); - sb.Append("//\n"); - sb.AppendFormat("// Umbraco.ModelsBuilder v{0}\n", ApiVersion.Current.Version); - sb.Append("//\n"); - sb.Append("// Changes to this file will be lost if the code is regenerated.\n"); - sb.Append("// \n"); - sb.Append("//------------------------------------------------------------------------------\n"); - sb.Append("\n"); - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Building/TypeModel.cs b/src/Umbraco.ModelsBuilder/Building/TypeModel.cs deleted file mode 100644 index 5ada8e881c..0000000000 --- a/src/Umbraco.ModelsBuilder/Building/TypeModel.cs +++ /dev/null @@ -1,208 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Umbraco.ModelsBuilder.Building -{ - /// - /// Represents a model. - /// - public class TypeModel - { - /// - /// Gets the unique identifier of the corresponding content type. - /// - public int Id; - - /// - /// Gets the alias of the model. - /// - public string Alias; - - /// - /// Gets the name of the content type. - /// - public string Name; - - /// - /// Gets the description of the content type. - /// - public string Description; - - /// - /// Gets the clr name of the model. - /// - /// This is the complete name eg "Foo.Bar.MyContent". - public string ClrName; - - /// - /// Gets the unique identifier of the parent. - /// - /// The parent can either be a base content type, or a content types container. If the content - /// type does not have a base content type, then returns -1. - public int ParentId; - - /// - /// Gets the base model. - /// - /// - /// If the content type does not have a base content type, then returns null. - /// The current model inherits from its base model. - /// - public TypeModel BaseType; // the parent type in Umbraco (type inherits its properties) - - /// - /// Gets the list of properties that are defined by this model. - /// - /// These are only those property that are defined locally by this model, - /// and the list does not contain properties inherited from base models or from mixins. - public readonly List Properties = new List(); - - /// - /// Gets the mixin models. - /// - /// The current model implements mixins. - public readonly List MixinTypes = new List(); - - /// - /// Gets the list of interfaces that this model needs to declare it implements. - /// - /// Some of these interfaces may actually be implemented by a base model - /// that this model inherits from. - public readonly List DeclaringInterfaces = new List(); - - /// - /// Gets the list of interfaces that this model needs to actually implement. - /// - public readonly List ImplementingInterfaces = new List(); - - /// - /// Gets the list of existing static mixin method candidates. - /// - public readonly List StaticMixinMethods = new List(); - - /// - /// Gets a value indicating whether this model has a base class. - /// - /// Can be either because the content type has a base content type declared in Umbraco, - /// or because the existing user's code declares a base class for this model. - public bool HasBase; - - /// - /// Gets a value indicating whether this model has been renamed. - /// - public bool IsRenamed; - - /// - /// Gets a value indicating whether this model has [ImplementContentType] already. - /// - public bool HasImplement; - - /// - /// Gets a value indicating whether this model is used as a mixin by another model. - /// - public bool IsMixin; - - /// - /// Gets a value indicating whether this model is the base model of another model. - /// - public bool IsParent; - - /// - /// Gets a value indicating whether this model should be excluded from generation. - /// - public bool IsContentIgnored; - - /// - /// Gets a value indicating whether the ctor is already defined in a partial. - /// - public bool HasCtor; - - /// - /// Gets a value indicating whether the type is an element. - /// - public bool IsElement => ItemType == ItemTypes.Element; - - /// - /// Represents the different model item types. - /// - public enum ItemTypes - { - /// - /// Element. - /// - Element, - - /// - /// Content. - /// - Content, - - /// - /// Media. - /// - Media, - - /// - /// Member. - /// - Member - } - - private ItemTypes _itemType; - - /// - /// Gets or sets the model item type. - /// - public ItemTypes ItemType - { - get { return _itemType; } - set - { - switch (value) - { - case ItemTypes.Element: - case ItemTypes.Content: - case ItemTypes.Media: - case ItemTypes.Member: - _itemType = value; - break; - default: - throw new ArgumentException("value"); - } - } - } - - /// - /// Recursively collects all types inherited, or implemented as interfaces, by a specified type. - /// - /// The collection. - /// The type. - /// Includes the specified type. - internal static void CollectImplems(ICollection types, TypeModel type) - { - if (!type.IsContentIgnored && types.Contains(type) == false) - types.Add(type); - if (type.BaseType != null && !type.BaseType.IsContentIgnored) - CollectImplems(types, type.BaseType); - foreach (var mixin in type.MixinTypes.Where(x => !x.IsContentIgnored)) - CollectImplems(types, mixin); - } - - /// - /// Enumerates the base models starting from the current model up. - /// - /// Indicates whether the enumeration should start with the current model - /// or from its base model. - /// The base models. - public IEnumerable EnumerateBaseTypes(bool andSelf = false) - { - var typeModel = andSelf ? this : BaseType; - while (typeModel != null) - { - yield return typeModel; - typeModel = typeModel.BaseType; - } - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Configuration/ClrNameSource.cs b/src/Umbraco.ModelsBuilder/Configuration/ClrNameSource.cs deleted file mode 100644 index d195846411..0000000000 --- a/src/Umbraco.ModelsBuilder/Configuration/ClrNameSource.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace Umbraco.ModelsBuilder.Configuration -{ - /// - /// Defines the CLR name sources. - /// - public enum ClrNameSource - { - /// - /// No source. - /// - Nothing = 0, - - /// - /// Use the name as source. - /// - Name, - - /// - /// Use the alias as source. - /// - Alias, - - /// - /// Use the alias directly. - /// - RawAlias - } -} diff --git a/src/Umbraco.ModelsBuilder/Configuration/Config.cs b/src/Umbraco.ModelsBuilder/Configuration/Config.cs deleted file mode 100644 index ebfe4be709..0000000000 --- a/src/Umbraco.ModelsBuilder/Configuration/Config.cs +++ /dev/null @@ -1,357 +0,0 @@ -using System; -using System.Configuration; -using System.IO; -using System.Reflection; -using System.Web.Configuration; -using System.Web.Hosting; -using Microsoft.CodeAnalysis.CSharp; -using Umbraco.Core; - -namespace Umbraco.ModelsBuilder.Configuration -{ - /// - /// Represents the models builder configuration. - /// - public class Config - { - private static Config _value; - - /// - /// Gets the configuration - internal so that the UmbracoConfig extension - /// can get the value to initialize its own value. Either a value has - /// been provided via the Setup method, or a new instance is created, which - /// will load settings from the config file. - /// - internal static Config Value => _value ?? new Config(); - - /// - /// Sets the configuration programmatically. - /// - /// The configuration. - /// - /// Once the configuration has been accessed via the UmbracoConfig extension, - /// it cannot be changed anymore, and using this method will achieve nothing. - /// For tests, see UmbracoConfigExtensions.ResetConfig(). - /// - public static void Setup(Config config) - { - _value = config; - } - - internal const string DefaultStaticMixinGetterPattern = "Get{0}"; - internal const LanguageVersion DefaultLanguageVersion = LanguageVersion.CSharp6; - internal const string DefaultModelsNamespace = "Umbraco.Web.PublishedModels"; - internal const ClrNameSource DefaultClrNameSource = ClrNameSource.Alias; // for legacy reasons - internal const string DefaultModelsDirectory = "~/App_Data/Models"; - - /// - /// Initializes a new instance of the class. - /// - private Config() - { - const string prefix = "Umbraco.ModelsBuilder."; - - // giant kill switch, default: false - // must be explicitely set to true for anything else to happen - Enable = ConfigurationManager.AppSettings[prefix + "Enable"] == "true"; - - // ensure defaults are initialized for tests - StaticMixinGetterPattern = DefaultStaticMixinGetterPattern; - LanguageVersion = DefaultLanguageVersion; - ModelsNamespace = DefaultModelsNamespace; - ClrNameSource = DefaultClrNameSource; - ModelsDirectory = HostingEnvironment.IsHosted - ? HostingEnvironment.MapPath(DefaultModelsDirectory) - : DefaultModelsDirectory.TrimStart("~/"); - DebugLevel = 0; - - // stop here, everything is false - if (!Enable) return; - - // mode - var modelsMode = ConfigurationManager.AppSettings[prefix + "ModelsMode"]; - if (!string.IsNullOrWhiteSpace(modelsMode)) - { - switch (modelsMode) - { - case nameof(ModelsMode.Nothing): - ModelsMode = ModelsMode.Nothing; - break; - case nameof(ModelsMode.PureLive): - ModelsMode = ModelsMode.PureLive; - break; - case nameof(ModelsMode.Dll): - ModelsMode = ModelsMode.Dll; - break; - case nameof(ModelsMode.LiveDll): - ModelsMode = ModelsMode.LiveDll; - break; - case nameof(ModelsMode.AppData): - ModelsMode = ModelsMode.AppData; - break; - case nameof(ModelsMode.LiveAppData): - ModelsMode = ModelsMode.LiveAppData; - break; - default: - throw new ConfigurationErrorsException($"ModelsMode \"{modelsMode}\" is not a valid mode." - + " Note that modes are case-sensitive."); - } - } - - // default: false - EnableApi = ConfigurationManager.AppSettings[prefix + "EnableApi"].InvariantEquals("true"); - AcceptUnsafeModelsDirectory = ConfigurationManager.AppSettings[prefix + "AcceptUnsafeModelsDirectory"].InvariantEquals("true"); - - // default: true - EnableFactory = !ConfigurationManager.AppSettings[prefix + "EnableFactory"].InvariantEquals("false"); - StaticMixinGetters = !ConfigurationManager.AppSettings[prefix + "StaticMixinGetters"].InvariantEquals("false"); - FlagOutOfDateModels = !ConfigurationManager.AppSettings[prefix + "FlagOutOfDateModels"].InvariantEquals("false"); - - // default: initialized above with DefaultModelsNamespace const - var value = ConfigurationManager.AppSettings[prefix + "ModelsNamespace"]; - if (!string.IsNullOrWhiteSpace(value)) - ModelsNamespace = value; - - // default: initialized above with DefaultStaticMixinGetterPattern const - value = ConfigurationManager.AppSettings[prefix + "StaticMixinGetterPattern"]; - if (!string.IsNullOrWhiteSpace(value)) - StaticMixinGetterPattern = value; - - // default: initialized above with DefaultLanguageVersion const - value = ConfigurationManager.AppSettings[prefix + "LanguageVersion"]; - if (!string.IsNullOrWhiteSpace(value)) - { - LanguageVersion lv; - if (!Enum.TryParse(value, true, out lv)) - throw new ConfigurationErrorsException($"Invalid language version \"{value}\"."); - LanguageVersion = lv; - } - - // default: initialized above with DefaultClrNameSource const - value = ConfigurationManager.AppSettings[prefix + "ClrNameSource"]; - if (!string.IsNullOrWhiteSpace(value)) - { - switch (value) - { - case nameof(ClrNameSource.Nothing): - ClrNameSource = ClrNameSource.Nothing; - break; - case nameof(ClrNameSource.Alias): - ClrNameSource = ClrNameSource.Alias; - break; - case nameof(ClrNameSource.RawAlias): - ClrNameSource = ClrNameSource.RawAlias; - break; - case nameof(ClrNameSource.Name): - ClrNameSource = ClrNameSource.Name; - break; - default: - throw new ConfigurationErrorsException($"ClrNameSource \"{value}\" is not a valid source." - + " Note that sources are case-sensitive."); - } - } - - // default: initialized above with DefaultModelsDirectory const - value = ConfigurationManager.AppSettings[prefix + "ModelsDirectory"]; - if (!string.IsNullOrWhiteSpace(value)) - { - var root = HostingEnvironment.IsHosted - ? HostingEnvironment.MapPath("~/") - : Directory.GetCurrentDirectory(); - if (root == null) - throw new ConfigurationErrorsException("Could not determine root directory."); - - // GetModelsDirectory will ensure that the path is safe - ModelsDirectory = GetModelsDirectory(root, value, AcceptUnsafeModelsDirectory); - } - - // default: 0 - value = ConfigurationManager.AppSettings[prefix + "DebugLevel"]; - if (!string.IsNullOrWhiteSpace(value)) - { - int debugLevel; - if (!int.TryParse(value, out debugLevel)) - throw new ConfigurationErrorsException($"Invalid debug level \"{value}\"."); - DebugLevel = debugLevel; - } - - // not flagging if not generating, or live (incl. pure) - if (ModelsMode == ModelsMode.Nothing || ModelsMode.IsLive()) - FlagOutOfDateModels = false; - } - - /// - /// Initializes a new instance of the class. - /// - public Config( - bool enable = false, - ModelsMode modelsMode = ModelsMode.Nothing, - bool enableApi = true, - string modelsNamespace = null, - bool enableFactory = true, - LanguageVersion languageVersion = DefaultLanguageVersion, - bool staticMixinGetters = true, - string staticMixinGetterPattern = null, - bool flagOutOfDateModels = true, - ClrNameSource clrNameSource = DefaultClrNameSource, - string modelsDirectory = null, - bool acceptUnsafeModelsDirectory = false, - int debugLevel = 0) - { - Enable = enable; - ModelsMode = modelsMode; - - EnableApi = enableApi; - ModelsNamespace = string.IsNullOrWhiteSpace(modelsNamespace) ? DefaultModelsNamespace : modelsNamespace; - EnableFactory = enableFactory; - LanguageVersion = languageVersion; - StaticMixinGetters = staticMixinGetters; - StaticMixinGetterPattern = string.IsNullOrWhiteSpace(staticMixinGetterPattern) ? DefaultStaticMixinGetterPattern : staticMixinGetterPattern; - FlagOutOfDateModels = flagOutOfDateModels; - ClrNameSource = clrNameSource; - ModelsDirectory = string.IsNullOrWhiteSpace(modelsDirectory) ? DefaultModelsDirectory : modelsDirectory; - AcceptUnsafeModelsDirectory = acceptUnsafeModelsDirectory; - DebugLevel = debugLevel; - } - - // internal for tests - internal static string GetModelsDirectory(string root, string config, bool acceptUnsafe) - { - // making sure it is safe, ie under the website root, - // unless AcceptUnsafeModelsDirectory and then everything is OK. - - if (!Path.IsPathRooted(root)) - throw new ConfigurationErrorsException($"Root is not rooted \"{root}\"."); - - if (config.StartsWith("~/")) - { - var dir = Path.Combine(root, config.TrimStart("~/")); - - // sanitize - GetFullPath will take care of any relative - // segments in path, eg '../../foo.tmp' - it may throw a SecurityException - // if the combined path reaches illegal parts of the filesystem - dir = Path.GetFullPath(dir); - root = Path.GetFullPath(root); - - if (!dir.StartsWith(root) && !acceptUnsafe) - throw new ConfigurationErrorsException($"Invalid models directory \"{config}\"."); - - return dir; - } - - if (acceptUnsafe) - return Path.GetFullPath(config); - - throw new ConfigurationErrorsException($"Invalid models directory \"{config}\"."); - } - - /// - /// Gets a value indicating whether the whole models experience is enabled. - /// - /// - /// If this is false then absolutely nothing happens. - /// Default value is false which means that unless we have this setting, nothing happens. - /// - public bool Enable { get; } - - /// - /// Gets the models mode. - /// - public ModelsMode ModelsMode { get; } - - /// - /// Gets a value indicating whether to serve the API. - /// - public bool ApiServer => EnableApi && ApiInstalled && IsDebug; - - /// - /// Gets a value indicating whether to enable the API. - /// - /// - /// Default value is true. - /// The API is used by the Visual Studio extension and the console tool to talk to Umbraco - /// and retrieve the content types. It needs to be enabled so the extension & tool can work. - /// - public bool EnableApi { get; } - - /// - /// Gets a value indicating whether the API is installed. - /// - // fixme - this is now always true as the API is part of Core - public bool ApiInstalled => true; - - /// - /// Gets a value indicating whether system.web/compilation/@debug is true. - /// - public bool IsDebug - { - get - { - var section = (CompilationSection) ConfigurationManager.GetSection("system.web/compilation"); - return section != null && section.Debug; - } - } - - /// - /// Gets the models namespace. - /// - /// That value could be overriden by other (attribute in user's code...). Return default if no value was supplied. - public string ModelsNamespace { get; } - - /// - /// Gets a value indicating whether we should enable the models factory. - /// - /// Default value is true because no factory is enabled by default in Umbraco. - public bool EnableFactory { get; } - - /// - /// Gets the Roslyn parser language version. - /// - /// Default value is CSharp6. - public LanguageVersion LanguageVersion { get; } - - /// - /// Gets a value indicating whether to generate static mixin getters. - /// - /// Default value is false for backward compat reaons. - public bool StaticMixinGetters { get; } - - /// - /// Gets the string pattern for mixin properties static getter name. - /// - /// Default value is "GetXxx". Standard string format. - public string StaticMixinGetterPattern { get; } - - /// - /// Gets a value indicating whether we should flag out-of-date models. - /// - /// Models become out-of-date when data types or content types are updated. When this - /// setting is activated the ~/App_Data/Models/ood.txt file is then created. When models are - /// generated through the dashboard, the files is cleared. Default value is false. - public bool FlagOutOfDateModels { get; } - - /// - /// Gets the CLR name source. - /// - public ClrNameSource ClrNameSource { get; } - - /// - /// Gets the models directory. - /// - /// Default is ~/App_Data/Models but that can be changed. - public string ModelsDirectory { get; } - - /// - /// Gets a value indicating whether to accept an unsafe value for ModelsDirectory. - /// - /// An unsafe value is an absolute path, or a relative path pointing outside - /// of the website root. - public bool AcceptUnsafeModelsDirectory { get; } - - /// - /// Gets a value indicating the debug log level. - /// - /// 0 means minimal (safe on live site), anything else means more and more details (maybe not safe). - public int DebugLevel { get; } - } -} diff --git a/src/Umbraco.ModelsBuilder/Configuration/ModelsMode.cs b/src/Umbraco.ModelsBuilder/Configuration/ModelsMode.cs deleted file mode 100644 index e04c4dee90..0000000000 --- a/src/Umbraco.ModelsBuilder/Configuration/ModelsMode.cs +++ /dev/null @@ -1,52 +0,0 @@ -namespace Umbraco.ModelsBuilder.Configuration -{ - /// - /// Defines the models generation modes. - /// - public enum ModelsMode - { - /// - /// Do not generate models. - /// - Nothing = 0, // default value - - /// - /// Generate models in memory. - /// When: a content type change occurs. - /// - /// The app does not restart. Models are available in views exclusively. - PureLive, - - /// - /// Generate models in AppData. - /// When: generation is triggered. - /// - /// Generation can be triggered from the dashboard. The app does not restart. - /// Models are not compiled and thus are not available to the project. - AppData, - - /// - /// Generate models in AppData. - /// When: a content type change occurs, or generation is triggered. - /// - /// Generation can be triggered from the dashboard. The app does not restart. - /// Models are not compiled and thus are not available to the project. - LiveAppData, - - /// - /// Generates models in AppData and compiles them into a Dll into ~/bin (the app restarts). - /// When: generation is triggered. - /// - /// Generation can be triggered from the dashboard. The app does restart. Models - /// are available to the entire project. - Dll, - - /// - /// Generates models in AppData and compiles them into a Dll into ~/bin (the app restarts). - /// When: a content type change occurs, or generation is triggered. - /// - /// Generation can be triggered from the dashboard. The app does restart. Models - /// are available to the entire project. - LiveDll - } -} diff --git a/src/Umbraco.ModelsBuilder/Configuration/ModelsModeExtensions.cs b/src/Umbraco.ModelsBuilder/Configuration/ModelsModeExtensions.cs deleted file mode 100644 index be609c0548..0000000000 --- a/src/Umbraco.ModelsBuilder/Configuration/ModelsModeExtensions.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace Umbraco.ModelsBuilder.Configuration -{ - /// - /// Provides extensions for the enumeration. - /// - public static class ModelsModeExtensions - { - /// - /// Gets a value indicating whether the mode is LiveAnything or PureLive. - /// - public static bool IsLive(this ModelsMode modelsMode) - { - return - modelsMode == ModelsMode.PureLive - || modelsMode == ModelsMode.LiveDll - || modelsMode == ModelsMode.LiveAppData; - } - - /// - /// Gets a value indicating whether the mode is LiveAnything but not PureLive. - /// - public static bool IsLiveNotPure(this ModelsMode modelsMode) - { - return - modelsMode == ModelsMode.LiveDll - || modelsMode == ModelsMode.LiveAppData; - } - - /// - /// Gets a value indicating whether the mode is [Live]Dll. - /// - public static bool IsAnyDll(this ModelsMode modelsMode) - { - return - modelsMode == ModelsMode.Dll - || modelsMode == ModelsMode.LiveDll; - } - - /// - /// Gets a value indicating whether the mode supports explicit generation (as opposed to pure live). - /// - public static bool SupportsExplicitGeneration(this ModelsMode modelsMode) - { - return - modelsMode == ModelsMode.Dll - || modelsMode == ModelsMode.LiveDll - || modelsMode == ModelsMode.AppData - || modelsMode == ModelsMode.LiveAppData; - } - } -} \ No newline at end of file diff --git a/src/Umbraco.ModelsBuilder/Configuration/UmbracoConfigExtensions.cs b/src/Umbraco.ModelsBuilder/Configuration/UmbracoConfigExtensions.cs deleted file mode 100644 index acc587e779..0000000000 --- a/src/Umbraco.ModelsBuilder/Configuration/UmbracoConfigExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Threading; -using Umbraco.Core.Configuration; - -namespace Umbraco.ModelsBuilder.Configuration -{ - /// - /// Provides extension methods for the class. - /// - public static class UmbracoConfigExtensions - { - private static Config _config; - - /// - /// Gets the models builder configuration. - /// - /// The umbraco configuration. - /// The models builder configuration. - /// Getting the models builder configuration freezes its state, - /// and any attempt at modifying the configuration using the Setup method - /// will be ignored. - public static Config ModelsBuilder(this UmbracoConfig umbracoConfig) - { - // capture the current Config2.Default value, cannot change anymore - LazyInitializer.EnsureInitialized(ref _config, () => Config.Value); - return _config; - } - - // internal for tests - internal static void ResetConfig() - { - _config = null; - } - } -} \ No newline at end of file diff --git a/src/Umbraco.ModelsBuilder/Dashboard/BuilderDashboardHelper.cs b/src/Umbraco.ModelsBuilder/Dashboard/BuilderDashboardHelper.cs deleted file mode 100644 index 9e5741805e..0000000000 --- a/src/Umbraco.ModelsBuilder/Dashboard/BuilderDashboardHelper.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Text; -using Umbraco.Core.Configuration; -using Umbraco.ModelsBuilder.Configuration; -using Umbraco.ModelsBuilder.Umbraco; - -namespace Umbraco.ModelsBuilder.Dashboard -{ - internal static class BuilderDashboardHelper - { - public static bool CanGenerate() - { - return UmbracoConfig.For.ModelsBuilder().ModelsMode.SupportsExplicitGeneration(); - } - - public static bool GenerateCausesRestart() - { - return UmbracoConfig.For.ModelsBuilder().ModelsMode.IsAnyDll(); - } - - public static bool AreModelsOutOfDate() - { - return OutOfDateModelsStatus.IsOutOfDate; - } - - public static string LastError() - { - return ModelsGenerationError.GetLastError(); - } - - public static string Text() - { - var config = UmbracoConfig.For.ModelsBuilder(); - if (!config.Enable) - return "ModelsBuilder is disabled
(the .Enable key is missing, or its value is not 'true')."; - - var sb = new StringBuilder(); - - sb.Append("ModelsBuilder is enabled, with the following configuration:"); - - sb.Append("
    "); - - sb.Append("
  • The models factory is "); - sb.Append(config.EnableFactory || config.ModelsMode == ModelsMode.PureLive - ? "enabled" - : "not enabled. Umbraco will not use models"); - sb.Append(".
  • "); - - sb.Append("
  • The API is "); - if (config.ApiInstalled && config.EnableApi) - { - sb.Append("installed and enabled"); - if (!config.IsDebug) sb.Append(".
    However, the API runs only with debug compilation mode"); - } - else if (config.ApiInstalled || config.EnableApi) - sb.Append(config.ApiInstalled ? "installed but not enabled" : "enabled but not installed"); - else sb.Append("neither installed nor enabled"); - sb.Append(".
    "); - if (!config.ApiServer) - sb.Append("External tools such as Visual Studio cannot use the API"); - else - sb.Append("The API endpoint is open on this server"); - sb.Append(".
  • "); - - sb.Append(config.ModelsMode != ModelsMode.Nothing - ? $"
  • {config.ModelsMode} models are enabled.
  • " - : "
  • No models mode is specified: models will not be generated.
  • "); - - sb.Append($"
  • Models namespace is {config.ModelsNamespace}.
  • "); - - sb.Append("
  • Static mixin getters are "); - sb.Append(config.StaticMixinGetters ? "enabled" : "disabled"); - if (config.StaticMixinGetters) - { - sb.Append(". The pattern for getters is "); - sb.Append(string.IsNullOrWhiteSpace(config.StaticMixinGetterPattern) - ? "not configured (will use default)" - : $"\"{config.StaticMixinGetterPattern}\""); - } - sb.Append(".
  • "); - - sb.Append("
  • Tracking of out-of-date models is "); - sb.Append(config.FlagOutOfDateModels ? "enabled" : "not enabled"); - sb.Append(".
  • "); - - sb.Append("
"); - - return sb.ToString(); - } - } -} diff --git a/src/Umbraco.ModelsBuilder/EnumerableExtensions.cs b/src/Umbraco.ModelsBuilder/EnumerableExtensions.cs deleted file mode 100644 index da77bfa958..0000000000 --- a/src/Umbraco.ModelsBuilder/EnumerableExtensions.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Umbraco.ModelsBuilder -{ - public static class EnumerableExtensions - { - public static void RemoveAll(this IList list, Func predicate) - { - for (var i = 0; i < list.Count; i++) - { - if (predicate(list[i])) - { - list.RemoveAt(i--); // i-- is important here! - } - } - } - - public static IEnumerable And(this IEnumerable enumerable, T item) - { - foreach (var x in enumerable) yield return x; - yield return item; - } - - public static IEnumerable AndIfNotNull(this IEnumerable enumerable, T item) - where T : class - { - foreach (var x in enumerable) yield return x; - if (item != null) - yield return item; - } - } -} diff --git a/src/Umbraco.ModelsBuilder/IgnoreContentTypeAttribute.cs b/src/Umbraco.ModelsBuilder/IgnoreContentTypeAttribute.cs deleted file mode 100644 index e5ab3a2e35..0000000000 --- a/src/Umbraco.ModelsBuilder/IgnoreContentTypeAttribute.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Policy; -using System.Text; -using System.Threading.Tasks; -using Umbraco.ModelsBuilder; - -namespace Umbraco.ModelsBuilder -{ - // for the time being it's all-or-nothing - // when the content type is ignored then - // - no class is generated for that content type - // - no class is generated for any child of that class - // - no interface is generated for that content type as a mixin - // - and it is ignored as a mixin ie its properties are not generated - // in the future we may way to do - // [assembly:IgnoreContentType("foo", ContentTypeIgnorable.ContentType|ContentTypeIgnorable.Mixin|ContentTypeIgnorable.MixinProperties)] - // so that we can - // - generate a class for that content type, or not - // - if not generated, generate children or not - // - if generate children, include properties or not - // - generate an interface for that content type as a mixin - // - if not generated, still generate properties in content types implementing the mixin or not - // but... I'm not even sure it makes sense - // if we don't want it... we don't want it. - - // about ignoring - // - content (don't generate the content, use as mixin) - // - mixin (don't generate the interface, use the properties) - // - mixin properties (generate the interface, not the properties) - // - mixin: local only or children too... - - /// - /// Indicates that no model should be generated for a specified content type alias. - /// - /// When a content type is ignored, its descendants are also ignored. - /// Supports trailing wildcard eg "foo*". - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)] - public sealed class IgnoreContentTypeAttribute : Attribute - { - public IgnoreContentTypeAttribute(string alias /*, bool ignoreContent = true, bool ignoreMixin = true, bool ignoreMixinProperties = true*/) - {} - } -} - diff --git a/src/Umbraco.ModelsBuilder/IgnorePropertyTypeAttribute.cs b/src/Umbraco.ModelsBuilder/IgnorePropertyTypeAttribute.cs deleted file mode 100644 index 4dce0f9b7f..0000000000 --- a/src/Umbraco.ModelsBuilder/IgnorePropertyTypeAttribute.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Umbraco.ModelsBuilder -{ - /// - /// Indicates that no model should be generated for a specified property type alias. - /// - /// Supports trailing wildcard eg "foo*". - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] - public sealed class IgnorePropertyTypeAttribute : Attribute - { - public IgnorePropertyTypeAttribute(string alias) - {} - } -} diff --git a/src/Umbraco.ModelsBuilder/ImplementContentTypeAttribute.cs b/src/Umbraco.ModelsBuilder/ImplementContentTypeAttribute.cs deleted file mode 100644 index 142f115b07..0000000000 --- a/src/Umbraco.ModelsBuilder/ImplementContentTypeAttribute.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace Umbraco.ModelsBuilder -{ - // NOTE - // that attribute should inherit from PublishedModelAttribute - // so we do not have different syntaxes - // but... is sealed at the moment. - - /// - /// Indicates that a (partial) class defines the model type for a specific alias. - /// - /// Though a model will be generated - so that is the way to register a rename. - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] - public sealed class ImplementContentTypeAttribute : Attribute - { - public ImplementContentTypeAttribute(string alias) - { } - } -} diff --git a/src/Umbraco.ModelsBuilder/ImplementPropertyTypeAttribute.cs b/src/Umbraco.ModelsBuilder/ImplementPropertyTypeAttribute.cs deleted file mode 100644 index c5d8f8cad4..0000000000 --- a/src/Umbraco.ModelsBuilder/ImplementPropertyTypeAttribute.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Umbraco.ModelsBuilder -{ - /// - /// Indicates that a property implements a given property alias. - /// - /// And therefore it should not be generated. - [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] - public sealed class ImplementPropertyTypeAttribute : Attribute - { - public ImplementPropertyTypeAttribute(string alias) - { - Alias = alias; - } - - public string Alias { get; private set; } - } -} diff --git a/src/Umbraco.ModelsBuilder/ModelsBaseClassAttribute.cs b/src/Umbraco.ModelsBuilder/ModelsBaseClassAttribute.cs deleted file mode 100644 index 3c401b7fdb..0000000000 --- a/src/Umbraco.ModelsBuilder/ModelsBaseClassAttribute.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace Umbraco.ModelsBuilder -{ - /// - /// Indicates the default base class for models. - /// - /// Otherwise it is PublishedContentModel. Would make sense to inherit from PublishedContentModel. - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)] - public sealed class ModelsBaseClassAttribute : Attribute - { - public ModelsBaseClassAttribute(Type type) - {} - } -} - diff --git a/src/Umbraco.ModelsBuilder/ModelsBuilderAssemblyAttribute.cs b/src/Umbraco.ModelsBuilder/ModelsBuilderAssemblyAttribute.cs deleted file mode 100644 index ed956852f8..0000000000 --- a/src/Umbraco.ModelsBuilder/ModelsBuilderAssemblyAttribute.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; - -namespace Umbraco.ModelsBuilder -{ - /// - /// Indicates that an Assembly is a Models Builder assembly. - /// - [AttributeUsage(AttributeTargets.Assembly /*, AllowMultiple = false, Inherited = false*/)] - public sealed class ModelsBuilderAssemblyAttribute : Attribute - { - /// - /// Gets or sets a value indicating whether the assembly is a PureLive assembly. - /// - /// A Models Builder assembly can be either PureLive or normal Dll. - public bool PureLive { get; set; } - - /// - /// Gets or sets a hash value representing the state of the custom source code files - /// and the Umbraco content types that were used to generate and compile the assembly. - /// - public string SourceHash { get; set; } - } -} diff --git a/src/Umbraco.ModelsBuilder/ModelsNamespaceAttribute.cs b/src/Umbraco.ModelsBuilder/ModelsNamespaceAttribute.cs deleted file mode 100644 index 1b1d62d9bc..0000000000 --- a/src/Umbraco.ModelsBuilder/ModelsNamespaceAttribute.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace Umbraco.ModelsBuilder -{ - /// - /// Indicates the models namespace. - /// - /// Will override anything else that might come from settings. - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)] - public sealed class ModelsNamespaceAttribute : Attribute - { - public ModelsNamespaceAttribute(string modelsNamespace) - {} - } -} - diff --git a/src/Umbraco.ModelsBuilder/ModelsUsingAttribute.cs b/src/Umbraco.ModelsBuilder/ModelsUsingAttribute.cs deleted file mode 100644 index 8fe1335631..0000000000 --- a/src/Umbraco.ModelsBuilder/ModelsUsingAttribute.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Policy; -using System.Text; -using System.Threading.Tasks; -using Umbraco.ModelsBuilder; - -namespace Umbraco.ModelsBuilder -{ - /// - /// Indicates namespaces that should be used in generated models (in using clauses). - /// - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)] - public sealed class ModelsUsingAttribute : Attribute - { - public ModelsUsingAttribute(string usingNamespace) - {} - } -} - diff --git a/src/Umbraco.ModelsBuilder/Properties/AssemblyInfo.cs b/src/Umbraco.ModelsBuilder/Properties/AssemblyInfo.cs index a2f8d1ae1e..8ec221bffb 100644 --- a/src/Umbraco.ModelsBuilder/Properties/AssemblyInfo.cs +++ b/src/Umbraco.ModelsBuilder/Properties/AssemblyInfo.cs @@ -1,14 +1,36 @@ using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. [assembly: AssemblyTitle("Umbraco.ModelsBuilder")] -[assembly: AssemblyDescription("Umbraco ModelsBuilder")] +[assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] -[assembly: AssemblyProduct("Umbraco CMS")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Umbraco.ModelsBuilder")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] -[assembly: Guid("7020a059-c0d1-43a0-8efd-23591a0c9af6")] -// code analysis -// IDE1006 is broken, wants _value syntax for consts, etc - and it's even confusing ppl at MS, kill it -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "~_~")] +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("52ac0ba8-a60e-4e36-897b-e8b97a54ed1c")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Umbraco.ModelsBuilder/PublishedElementExtensions.cs b/src/Umbraco.ModelsBuilder/PublishedElementExtensions.cs deleted file mode 100644 index f3320b5dfb..0000000000 --- a/src/Umbraco.ModelsBuilder/PublishedElementExtensions.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Linq.Expressions; -using System.Reflection; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.Web; - -namespace Umbraco.ModelsBuilder -{ - /// - /// Provides extension methods to models. - /// - public static class PublishedElementExtensions - { - /// - /// Gets the value of a property. - /// - public static TValue Value(this TModel model, Expression> property, string culture = ".", string segment = ".") - where TModel : IPublishedElement - { - var alias = GetAlias(model, property); - return model.Value(alias, culture, segment); - } - - private static string GetAlias(TModel model, Expression> property) - { - if (property.NodeType != ExpressionType.Lambda) - throw new ArgumentException("Not a proper lambda expression (lambda).", nameof(property)); - - var lambda = (LambdaExpression) property; - var lambdaBody = lambda.Body; - - if (lambdaBody.NodeType != ExpressionType.MemberAccess) - throw new ArgumentException("Not a proper lambda expression (body).", nameof(property)); - - var memberExpression = (MemberExpression) lambdaBody; - if (memberExpression.Expression.NodeType != ExpressionType.Parameter) - throw new ArgumentException("Not a proper lambda expression (member).", nameof(property)); - - var member = memberExpression.Member; - - var attribute = member.GetCustomAttribute(); - if (attribute == null) - throw new InvalidOperationException("Property is not marked with ImplementPropertyType attribute."); - - return attribute.Alias; - } - } -} diff --git a/src/Umbraco.ModelsBuilder/PublishedPropertyTypeExtensions.cs b/src/Umbraco.ModelsBuilder/PublishedPropertyTypeExtensions.cs deleted file mode 100644 index b67ba54432..0000000000 --- a/src/Umbraco.ModelsBuilder/PublishedPropertyTypeExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Umbraco.Core; -using Umbraco.Core.Models.PublishedContent; - -namespace Umbraco.ModelsBuilder -{ - public static class PublishedPropertyTypeExtensions - { - // fixme - need to rewrite that one - we don't have prevalues anymore - //public static KeyValuePair[] PreValues(this PublishedPropertyType propertyType) - //{ - // return ApplicationContext.Current.Services.DataTypeService - // .GetPreValuesCollectionByDataTypeId(propertyType.DataType.Id) - // .PreValuesAsArray - // .Select(x => new KeyValuePair(x.Id, x.Value)) - // .ToArray(); - //} - } -} diff --git a/src/Umbraco.ModelsBuilder/PureLiveAssemblyAttribute.cs b/src/Umbraco.ModelsBuilder/PureLiveAssemblyAttribute.cs deleted file mode 100644 index dfe369dc21..0000000000 --- a/src/Umbraco.ModelsBuilder/PureLiveAssemblyAttribute.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Umbraco.ModelsBuilder -{ - /// - /// Indicates that an Assembly is a PureLive models assembly. - /// - /// Though technically not required, ie models will work without it, the attribute - /// can be used by Umbraco view models binder to figure out whether the model type comes - /// from a PureLive Assembly. - [Obsolete("Should use ModelsBuilderAssemblyAttribute but that requires a change in Umbraco Core.")] - [AttributeUsage(AttributeTargets.Assembly /*, AllowMultiple = false, Inherited = false*/)] - public sealed class PureLiveAssemblyAttribute : Attribute - { } -} diff --git a/src/Umbraco.ModelsBuilder/ReferencedAssemblies.cs b/src/Umbraco.ModelsBuilder/ReferencedAssemblies.cs deleted file mode 100644 index 42e8b3b9c9..0000000000 --- a/src/Umbraco.ModelsBuilder/ReferencedAssemblies.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Web.Compilation; -using System.Web.Hosting; -using Microsoft.CodeAnalysis; -using Umbraco.Core; - -namespace Umbraco.ModelsBuilder -{ - internal static class ReferencedAssemblies - { - private static readonly Lazy> LazyLocations; - private static readonly Lazy> LazyReferences; - - static ReferencedAssemblies() - { - LazyLocations = new Lazy>(() => HostingEnvironment.IsHosted - ? GetAllReferencedAssembliesLocationFromBuildManager() - : GetAllReferencedAssembliesFromDomain()); - - LazyReferences = new Lazy>(() => Locations - .Select(x => MetadataReference.CreateFromFile(x)) - .ToArray()); - } - - /// - /// Gets the assembly locations of all the referenced assemblies, that - /// are not dynamic, and have a non-null nor empty location. - /// - public static IEnumerable Locations => LazyLocations.Value; - - /// - /// Gets the metadata reference of all the referenced assemblies. - /// - public static IEnumerable References => LazyReferences.Value; - - // hosted, get referenced assemblies from the BuildManader and filter - private static IEnumerable GetAllReferencedAssembliesLocationFromBuildManager() - { - return BuildManager.GetReferencedAssemblies() - .Cast() - .Where(x => !x.IsDynamic && !x.Location.IsNullOrWhiteSpace()) - .Select(x => x.Location) - .And(typeof(ReferencedAssemblies).Assembly.Location) // always include ourselves - .Distinct() - .ToList(); - } - - // non-hosted, do our best - private static IEnumerable GetAllReferencedAssembliesFromDomain() - { - //TODO: This method has bugs since I've been stuck in an infinite loop with it, though this shouldn't - // execute while in the web application anyways. - - var assemblies = new List(); - var tmp1 = new List(); - var failed = new List(); - foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies() - .Where(x => x.IsDynamic == false) - .Where(x => !string.IsNullOrWhiteSpace(x.Location))) // though... IsDynamic should be enough? - { - assemblies.Add(assembly); - tmp1.Add(assembly); - } - - // fixme - AssemblyUtility questions - // - should we also load everything that's in the same directory? - // - do we want to load in the current app domain? - // - if this runs within Umbraco then we have already loaded them all? - - while (tmp1.Count > 0) - { - var tmp2 = tmp1 - .SelectMany(x => x.GetReferencedAssemblies()) - .Distinct() - .Where(x => assemblies.All(xx => x.FullName != xx.FullName)) // we don't have it already - .Where(x => failed.All(xx => x.FullName != xx.FullName)) // it hasn't failed already - .ToArray(); - tmp1.Clear(); - foreach (var assemblyName in tmp2) - { - try - { - var assembly = AppDomain.CurrentDomain.Load(assemblyName); - assemblies.Add(assembly); - tmp1.Add(assembly); - } - catch - { - failed.Add(assemblyName); - } - } - } - return assemblies.Select(x => x.Location).Distinct(); - } - - - // ---- - - private static IEnumerable GetDeepReferencedAssemblies(Assembly assembly) - { - var visiting = new Stack(); - var visited = new HashSet(); - - visiting.Push(assembly); - visited.Add(assembly); - while (visiting.Count > 0) - { - var visAsm = visiting.Pop(); - foreach (var refAsm in visAsm.GetReferencedAssemblies() - .Select(TryLoad) - .Where(x => x != null && visited.Contains(x) == false)) - { - yield return refAsm; - visiting.Push(refAsm); - visited.Add(refAsm); - } - } - } - - private static Assembly TryLoad(AssemblyName name) - { - try - { - return AppDomain.CurrentDomain.Load(name); - } - catch (Exception) - { - //Console.WriteLine(name); - return null; - } - } - - } -} diff --git a/src/Umbraco.ModelsBuilder/RenameContentTypeAttribute.cs b/src/Umbraco.ModelsBuilder/RenameContentTypeAttribute.cs deleted file mode 100644 index 0f985e70b3..0000000000 --- a/src/Umbraco.ModelsBuilder/RenameContentTypeAttribute.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Umbraco.ModelsBuilder -{ - /// - /// Indicates a model name for a specified content alias. - /// - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)] - public sealed class RenameContentTypeAttribute : Attribute - { - public RenameContentTypeAttribute(string alias, string name) - {} - } -} diff --git a/src/Umbraco.ModelsBuilder/RenamePropertyTypeAttribute.cs b/src/Umbraco.ModelsBuilder/RenamePropertyTypeAttribute.cs deleted file mode 100644 index 0d8fd31b63..0000000000 --- a/src/Umbraco.ModelsBuilder/RenamePropertyTypeAttribute.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Umbraco.ModelsBuilder -{ - /// - /// Indicates a model name for a specified property alias. - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] - public sealed class RenamePropertyTypeAttribute : Attribute - { - public RenamePropertyTypeAttribute(string alias, string name) - {} - } -} diff --git a/src/Umbraco.ModelsBuilder/TypeExtensions.cs b/src/Umbraco.ModelsBuilder/TypeExtensions.cs deleted file mode 100644 index d3b3ff6b4e..0000000000 --- a/src/Umbraco.ModelsBuilder/TypeExtensions.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace Umbraco.ModelsBuilder -{ - internal static class TypeExtensions - { - /// - /// Creates a generic instance of a generic type with the proper actual type of an object. - /// - /// A generic type such as Something{} - /// An object whose type is used as generic type param. - /// Arguments for the constructor. - /// A generic instance of the generic type with the proper type. - /// Usage... typeof (Something{}).CreateGenericInstance(object1, object2, object3) will return - /// a Something{Type1} if object1.GetType() is Type1. - public static object CreateGenericInstance(this Type genericType, object typeParmObj, params object[] ctorArgs) - { - var type = genericType.MakeGenericType(typeParmObj.GetType()); - return Activator.CreateInstance(type, ctorArgs); - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Umbraco.ModelsBuilder.csproj b/src/Umbraco.ModelsBuilder/Umbraco.ModelsBuilder.csproj index b9a5890d57..d1d3ca205d 100644 --- a/src/Umbraco.ModelsBuilder/Umbraco.ModelsBuilder.csproj +++ b/src/Umbraco.ModelsBuilder/Umbraco.ModelsBuilder.csproj @@ -4,13 +4,14 @@ Debug AnyCPU - {7020A059-C0D1-43A0-8EFD-23591A0C9AF6} + {52AC0BA8-A60E-4E36-897B-E8B97A54ED1C} Library Properties Umbraco.ModelsBuilder Umbraco.ModelsBuilder v4.7.2 512 + true true @@ -28,13 +29,10 @@ TRACE prompt 4 - bin\Release\Umbraco.ModelsBuilder.xml - - @@ -43,76 +41,7 @@ - - Properties\SolutionInfo.cs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 2.8.0 - - - - - {31785bc3-256c-4613-b2f5-a1b0bdded8c1} - Umbraco.Core - - - {651e1350-91b6-44b7-bd60-7207006d7003} - Umbraco.Web - \ No newline at end of file diff --git a/src/Umbraco.ModelsBuilder/Umbraco/HashCombiner.cs b/src/Umbraco.ModelsBuilder/Umbraco/HashCombiner.cs deleted file mode 100644 index e11662eb24..0000000000 --- a/src/Umbraco.ModelsBuilder/Umbraco/HashCombiner.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Globalization; - -namespace Umbraco.ModelsBuilder.Umbraco -{ - // because, of course, it's internal in Umbraco - // see also System.Web.Util.HashCodeCombiner - class HashCombiner - { - private long _combinedHash = 5381L; - - public void Add(int i) - { - _combinedHash = ((_combinedHash << 5) + _combinedHash) ^ i; - } - - public void Add(object o) - { - Add(o.GetHashCode()); - } - - public void Add(DateTime d) - { - Add(d.GetHashCode()); - } - - public void Add(string s) - { - if (s == null) return; - Add((StringComparer.InvariantCulture).GetHashCode(s)); - } - - public string GetCombinedHashCode() - { - return _combinedHash.ToString("x", CultureInfo.InvariantCulture); - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Umbraco/HashHelper.cs b/src/Umbraco.ModelsBuilder/Umbraco/HashHelper.cs deleted file mode 100644 index c530cbbd6b..0000000000 --- a/src/Umbraco.ModelsBuilder/Umbraco/HashHelper.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Umbraco.ModelsBuilder.Building; - -namespace Umbraco.ModelsBuilder.Umbraco -{ - class HashHelper - { - public static string Hash(IDictionary ourFiles, IEnumerable typeModels) - { - var hash = new HashCombiner(); - - foreach (var kvp in ourFiles) - hash.Add(kvp.Key + "::" + kvp.Value); - - // see Umbraco.ModelsBuilder.Umbraco.Application for what's important to hash - // ie what comes from Umbraco (not computed by ModelsBuilder) and makes a difference - - foreach (var typeModel in typeModels.OrderBy(x => x.Alias)) - { - hash.Add("--- CONTENT TYPE MODEL ---"); - hash.Add(typeModel.Id); - hash.Add(typeModel.Alias); - hash.Add(typeModel.ClrName); - hash.Add(typeModel.ParentId); - hash.Add(typeModel.Name); - hash.Add(typeModel.Description); - hash.Add(typeModel.ItemType.ToString()); - hash.Add("MIXINS:" + string.Join(",", typeModel.MixinTypes.OrderBy(x => x.Id).Select(x => x.Id))); - - foreach (var prop in typeModel.Properties.OrderBy(x => x.Alias)) - { - hash.Add("--- PROPERTY ---"); - hash.Add(prop.Alias); - hash.Add(prop.ClrName); - hash.Add(prop.Name); - hash.Add(prop.Description); - hash.Add(prop.ModelClrType.ToString()); // see ModelType tests, want ToString() not FullName - } - } - - return hash.GetCombinedHashCode(); - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Umbraco/LiveModelsProvider.cs b/src/Umbraco.ModelsBuilder/Umbraco/LiveModelsProvider.cs deleted file mode 100644 index b6c37a3558..0000000000 --- a/src/Umbraco.ModelsBuilder/Umbraco/LiveModelsProvider.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using System.Threading; -using System.Web; -using System.Web.Hosting; -using Umbraco.Core.Composing; -using Umbraco.Core.Configuration; -using Umbraco.Core.Logging; -using Umbraco.ModelsBuilder.Configuration; -using Umbraco.ModelsBuilder.Umbraco; -using Umbraco.Web.Cache; - -// will install only if configuration says it needs to be installed -[assembly: PreApplicationStartMethod(typeof(LiveModelsProviderModule), "Install")] - -namespace Umbraco.ModelsBuilder.Umbraco -{ - // supports LiveDll and LiveAppData - but not PureLive - public sealed class LiveModelsProvider - { - private static UmbracoServices _umbracoServices; - private static Mutex _mutex; - private static int _req; - - internal static bool IsEnabled - { - get - { - var config = UmbracoConfig.For.ModelsBuilder(); - return config.ModelsMode.IsLiveNotPure(); - // we do not manage pure live here - } - } - - internal static void Install(UmbracoServices umbracoServices) - { - // just be sure - if (!IsEnabled) - return; - - _umbracoServices = umbracoServices; - - // initialize mutex - // ApplicationId will look like "/LM/W3SVC/1/Root/AppName" - // name is system-wide and must be less than 260 chars - var name = HostingEnvironment.ApplicationID + "/UmbracoLiveModelsProvider"; - _mutex = new Mutex(false, name); - - // anything changes, and we want to re-generate models. - ContentTypeCacheRefresher.CacheUpdated += RequestModelsGeneration; - DataTypeCacheRefresher.CacheUpdated += RequestModelsGeneration; - - // at the end of a request since we're restarting the pool - // NOTE - this does NOT trigger - see module below - //umbracoApplication.EndRequest += GenerateModelsIfRequested; - } - - // NOTE - // Using HttpContext Items fails because CacheUpdated triggers within - // some asynchronous backend task where we seem to have no HttpContext. - - // So we use a static (non request-bound) var to register that models - // need to be generated. Could be by another request. Anyway. We could - // have collisions but... you know the risk. - - private static void RequestModelsGeneration(object sender, EventArgs args) - { - //HttpContext.Current.Items[this] = true; - Current.Logger.Debug("Requested to generate models."); - Interlocked.Exchange(ref _req, 1); - } - - public static void GenerateModelsIfRequested(object sender, EventArgs args) - { - //if (HttpContext.Current.Items[this] == null) return; - if (Interlocked.Exchange(ref _req, 0) == 0) return; - - // cannot use a simple lock here because we don't want another AppDomain - // to generate while we do... and there could be 2 AppDomains if the app restarts. - - try - { - Current.Logger.Debug("Generate models..."); - const int timeout = 2*60*1000; // 2 mins - _mutex.WaitOne(timeout); // wait until it is safe, and acquire - Current.Logger.Info("Generate models now."); - GenerateModels(); - ModelsGenerationError.Clear(); - Current.Logger.Info("Generated."); - } - catch (TimeoutException) - { - Current.Logger.Warn("Timeout, models were NOT generated."); - } - catch (Exception e) - { - ModelsGenerationError.Report("Failed to build Live models.", e); - Current.Logger.Error("Failed to generate models.", e); - } - finally - { - _mutex.ReleaseMutex(); // release - } - } - - private static void GenerateModels() - { - var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory; - - var bin = HostingEnvironment.MapPath("~/bin"); - if (bin == null) - throw new Exception("Panic: bin is null."); - - var config = UmbracoConfig.For.ModelsBuilder(); - - // EnableDllModels will recycle the app domain - but this request will end properly - ModelsBuilderBackOfficeController.GenerateModels(_umbracoServices, modelsDirectory, config.ModelsMode.IsAnyDll() ? bin : null); - } - } - - // have to do this because it's the only way to subscribe to EndRequest, - // module is installed by assembly attribute at the top of this file - public class LiveModelsProviderModule : IHttpModule - { - public void Init(HttpApplication app) - { - app.EndRequest += LiveModelsProvider.GenerateModelsIfRequested; - } - - public void Dispose() - { - // nothing - } - - public static void Install() - { - if (!LiveModelsProvider.IsEnabled) - return; - - HttpApplication.RegisterModule(typeof(LiveModelsProviderModule)); - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderBackOfficeController.cs b/src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderBackOfficeController.cs deleted file mode 100644 index 19c9bda5da..0000000000 --- a/src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderBackOfficeController.cs +++ /dev/null @@ -1,194 +0,0 @@ -using System; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Runtime.Serialization; -using System.Text; -using System.Web.Hosting; -using Umbraco.Core.Configuration; -using Umbraco.ModelsBuilder.Building; -using Umbraco.ModelsBuilder.Configuration; -using Umbraco.ModelsBuilder.Dashboard; -using Umbraco.Web.Editors; -using Umbraco.Web.WebApi.Filters; - -namespace Umbraco.ModelsBuilder.Umbraco -{ - /// - /// API controller for use in the Umbraco back office with Angular resources - /// - /// - /// We've created a different controller for the backoffice/angular specifically this is to ensure that the - /// correct CSRF security is adhered to for angular and it also ensures that this controller is not subseptipal to - /// global WebApi formatters being changed since this is always forced to only return Angular JSON Specific formats. - /// - [UmbracoApplicationAuthorize(Core.Constants.Applications.Developer)] - public class ModelsBuilderBackOfficeController : UmbracoAuthorizedJsonController - { - private readonly UmbracoServices _umbracoServices; - - public ModelsBuilderBackOfficeController(UmbracoServices umbracoServices) - { - _umbracoServices = umbracoServices; - } - - // invoked by the dashboard - // requires that the user is logged into the backoffice and has access to the developer section - // beware! the name of the method appears in modelsbuilder.controller.js - [System.Web.Http.HttpPost] // use the http one, not mvc, with api controllers! - public HttpResponseMessage BuildModels() - { - try - { - if (!UmbracoConfig.For.ModelsBuilder().ModelsMode.SupportsExplicitGeneration()) - { - var result2 = new BuildResult { Success = false, Message = "Models generation is not enabled." }; - return Request.CreateResponse(HttpStatusCode.OK, result2, Configuration.Formatters.JsonFormatter); - } - - var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory; - - var bin = HostingEnvironment.MapPath("~/bin"); - if (bin == null) - throw new Exception("Panic: bin is null."); - - // EnableDllModels will recycle the app domain - but this request will end properly - GenerateModels(modelsDirectory, UmbracoConfig.For.ModelsBuilder().ModelsMode.IsAnyDll() ? bin : null); - - ModelsGenerationError.Clear(); - } - catch (Exception e) - { - ModelsGenerationError.Report("Failed to build models.", e); - } - - return Request.CreateResponse(HttpStatusCode.OK, GetDashboardResult(), Configuration.Formatters.JsonFormatter); - } - - // invoked by the back-office - // requires that the user is logged into the backoffice and has access to the developer section - [System.Web.Http.HttpGet] // use the http one, not mvc, with api controllers! - public HttpResponseMessage GetModelsOutOfDateStatus() - { - var status = OutOfDateModelsStatus.IsEnabled - ? (OutOfDateModelsStatus.IsOutOfDate - ? new OutOfDateStatus { Status = OutOfDateType.OutOfDate } - : new OutOfDateStatus { Status = OutOfDateType.Current }) - : new OutOfDateStatus { Status = OutOfDateType.Unknown }; - - return Request.CreateResponse(HttpStatusCode.OK, status, Configuration.Formatters.JsonFormatter); - } - - // invoked by the back-office - // requires that the user is logged into the backoffice and has access to the developer section - // beware! the name of the method appears in modelsbuilder.controller.js - [System.Web.Http.HttpGet] // use the http one, not mvc, with api controllers! - public HttpResponseMessage GetDashboard() - { - return Request.CreateResponse(HttpStatusCode.OK, GetDashboardResult(), Configuration.Formatters.JsonFormatter); - } - - private Dashboard GetDashboardResult() - { - return new Dashboard - { - Enable = UmbracoConfig.For.ModelsBuilder().Enable, - Text = BuilderDashboardHelper.Text(), - CanGenerate = BuilderDashboardHelper.CanGenerate(), - GenerateCausesRestart = BuilderDashboardHelper.GenerateCausesRestart(), - OutOfDateModels = BuilderDashboardHelper.AreModelsOutOfDate(), - LastError = BuilderDashboardHelper.LastError(), - }; - } - - private void GenerateModels(string modelsDirectory, string bin) - { - GenerateModels(_umbracoServices, modelsDirectory, bin); - } - - internal static void GenerateModels(UmbracoServices umbracoServices, string modelsDirectory, string bin) - { - if (!Directory.Exists(modelsDirectory)) - Directory.CreateDirectory(modelsDirectory); - - foreach (var file in Directory.GetFiles(modelsDirectory, "*.generated.cs")) - File.Delete(file); - - var typeModels = umbracoServices.GetAllTypes(); - - var ourFiles = Directory.GetFiles(modelsDirectory, "*.cs").ToDictionary(x => x, File.ReadAllText); - var parseResult = new CodeParser().ParseWithReferencedAssemblies(ourFiles); - var builder = new TextBuilder(typeModels, parseResult, UmbracoConfig.For.ModelsBuilder().ModelsNamespace); - - foreach (var typeModel in builder.GetModelsToGenerate()) - { - var sb = new StringBuilder(); - builder.Generate(sb, typeModel); - var filename = Path.Combine(modelsDirectory, typeModel.ClrName + ".generated.cs"); - File.WriteAllText(filename, sb.ToString()); - } - - // the idea was to calculate the current hash and to add it as an extra file to the compilation, - // in order to be able to detect whether a DLL is consistent with an environment - however the - // environment *might not* contain the local partial files, and thus it could be impossible to - // calculate the hash. So... maybe that's not a good idea after all? - /* - var currentHash = HashHelper.Hash(ourFiles, typeModels); - ourFiles["models.hash.cs"] = $@"using Umbraco.ModelsBuilder; -[assembly:ModelsBuilderAssembly(SourceHash = ""{currentHash}"")] -"; - */ - - if (bin != null) - { - foreach (var file in Directory.GetFiles(modelsDirectory, "*.generated.cs")) - ourFiles[file] = File.ReadAllText(file); - var compiler = new Compiler(); - compiler.Compile(builder.GetModelsNamespace(), ourFiles, bin); - } - - OutOfDateModelsStatus.Clear(); - } - - [DataContract] - internal class BuildResult - { - [DataMember(Name = "success")] - public bool Success; - [DataMember(Name = "message")] - public string Message; - } - - [DataContract] - internal class Dashboard - { - [DataMember(Name = "enable")] - public bool Enable; - [DataMember(Name = "text")] - public string Text; - [DataMember(Name = "canGenerate")] - public bool CanGenerate; - [DataMember(Name = "generateCausesRestart")] - public bool GenerateCausesRestart; - [DataMember(Name = "outOfDateModels")] - public bool OutOfDateModels; - [DataMember(Name = "lastError")] - public string LastError; - } - - internal enum OutOfDateType - { - OutOfDate, - Current, - Unknown = 100 - } - - [DataContract] - internal class OutOfDateStatus - { - [DataMember(Name = "status")] - public OutOfDateType Status { get; set; } - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderComponent.cs b/src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderComponent.cs deleted file mode 100644 index a581319ba5..0000000000 --- a/src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderComponent.cs +++ /dev/null @@ -1,178 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Web; -using System.Web.Mvc; -using System.Web.Routing; -using LightInject; -using Umbraco.Core; -using Umbraco.Core.Components; -using Umbraco.Core.Composing; -using Umbraco.Core.Configuration; -using Umbraco.Core.IO; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.Services; -using Umbraco.Core.Services.Implement; -using Umbraco.ModelsBuilder.Api; -using Umbraco.ModelsBuilder.Configuration; -using Umbraco.Web; -using Umbraco.Web.PublishedCache.NuCache; -using Umbraco.Web.UI.JavaScript; - -namespace Umbraco.ModelsBuilder.Umbraco -{ - [RequiredComponent(typeof(NuCacheComponent))] - [RuntimeLevel(MinLevel = RuntimeLevel.Run)] - public class ModelsBuilderComponent : UmbracoComponentBase, IUmbracoCoreComponent - { - public override void Compose(Composition composition) - { - base.Compose(composition); - - composition.Container.Register(new PerContainerLifetime()); - - var config = UmbracoConfig.For.ModelsBuilder(); - - if (config.ModelsMode == ModelsMode.PureLive) - ComposeForLiveModels(composition.Container); - else if (config.EnableFactory) - ComposeForDefaultModelsFactory(composition.Container); - - // always setup the dashboard - InstallServerVars(composition.Container.GetInstance().Level); - composition.Container.Register(typeof(ModelsBuilderBackOfficeController), new PerRequestLifeTime()); - - // setup the API if enabled (and in debug mode) - if (config.ApiServer) - composition.Container.Register(typeof(ModelsBuilderApiController), new PerRequestLifeTime()); - } - - public void Initialize(UmbracoServices umbracoServices) - { - var config = UmbracoConfig.For.ModelsBuilder(); - - if (config.Enable) - FileService.SavingTemplate += FileService_SavingTemplate; - - // fixme LiveModelsProvider should not be static - if (config.ModelsMode.IsLiveNotPure()) - LiveModelsProvider.Install(umbracoServices); - - // fixme OutOfDateModelsStatus should not be static - if (config.FlagOutOfDateModels) - OutOfDateModelsStatus.Install(); - } - - private void ComposeForDefaultModelsFactory(IServiceContainer container) - { - container.RegisterSingleton(factory - => new PublishedModelFactory(factory.GetInstance().GetTypes())); - } - - private void ComposeForLiveModels(IServiceContainer container) - { - container.RegisterSingleton(); - - // the following would add @using statement in every view so user's don't - // have to do it - however, then noone understands where the @using statement - // comes from, and it cannot be avoided / removed --- DISABLED - // - /* - // no need for @using in views - // note: - // we are NOT using the in-code attribute here, config is required - // because that would require parsing the code... and what if it changes? - // we can AddGlobalImport not sure we can remove one anyways - var modelsNamespace = Configuration.Config.ModelsNamespace; - if (string.IsNullOrWhiteSpace(modelsNamespace)) - modelsNamespace = Configuration.Config.DefaultModelsNamespace; - System.Web.WebPages.Razor.WebPageRazorHost.AddGlobalImport(modelsNamespace); - */ - } - - private void InstallServerVars(RuntimeLevel level) - { - // register our url - for the backoffice api - ServerVariablesParser.Parsing += (sender, serverVars) => - { - if (!serverVars.ContainsKey("umbracoUrls")) - throw new Exception("Missing umbracoUrls."); - var umbracoUrlsObject = serverVars["umbracoUrls"]; - if (umbracoUrlsObject == null) - throw new Exception("Null umbracoUrls"); - if (!(umbracoUrlsObject is Dictionary umbracoUrls)) - throw new Exception("Invalid umbracoUrls"); - - if (!serverVars.ContainsKey("umbracoPlugins")) - throw new Exception("Missing umbracoPlugins."); - if (!(serverVars["umbracoPlugins"] is Dictionary umbracoPlugins)) - throw new Exception("Invalid umbracoPlugins"); - - if (HttpContext.Current == null) throw new InvalidOperationException("HttpContext is null"); - var urlHelper = new UrlHelper(new RequestContext(new HttpContextWrapper(HttpContext.Current), new RouteData())); - - umbracoUrls["modelsBuilderBaseUrl"] = urlHelper.GetUmbracoApiServiceBaseUrl(controller => controller.BuildModels()); - umbracoPlugins["modelsBuilder"] = GetModelsBuilderSettings(level); - }; - } - - private Dictionary GetModelsBuilderSettings(RuntimeLevel level) - { - if (level != RuntimeLevel.Run) - return null; - - var settings = new Dictionary - { - {"enabled", UmbracoConfig.For.ModelsBuilder().Enable} - }; - - return settings; - } - - /// - /// Used to check if a template is being created based on a document type, in this case we need to - /// ensure the template markup is correct based on the model name of the document type - /// - /// - /// - private void FileService_SavingTemplate(IFileService sender, Core.Events.SaveEventArgs e) - { - // don't do anything if the factory is not enabled - // because, no factory = no models (even if generation is enabled) - if (!UmbracoConfig.For.ModelsBuilder().EnableFactory) return; - - // don't do anything if this special key is not found - if (!e.AdditionalData.ContainsKey("CreateTemplateForContentType")) return; - - // ensure we have the content type alias - if (!e.AdditionalData.ContainsKey("ContentTypeAlias")) - throw new InvalidOperationException("The additionalData key: ContentTypeAlias was not found"); - - foreach (var template in e.SavedEntities) - { - // if it is in fact a new entity (not been saved yet) and the "CreateTemplateForContentType" key - // is found, then it means a new template is being created based on the creation of a document type - if (!template.HasIdentity && string.IsNullOrWhiteSpace(template.Content)) - { - // ensure is safe and always pascal cased, per razor standard - // + this is how we get the default model name in Umbraco.ModelsBuilder.Umbraco.Application - var alias = e.AdditionalData["ContentTypeAlias"].ToString(); - var name = template.Name; // will be the name of the content type since we are creating - var className = UmbracoServices.GetClrName(name, alias); - - var modelNamespace = UmbracoConfig.For.ModelsBuilder().ModelsNamespace; - - // we do not support configuring this at the moment, so just let Umbraco use its default value - //var modelNamespaceAlias = ...; - - var markup = ViewHelper.GetDefaultFileContent( - modelClassName: className, - modelNamespace: modelNamespace/*, - modelNamespaceAlias: modelNamespaceAlias*/); - - //set the template content to the new markup - template.Content = markup; - } - } - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Umbraco/ModelsGenerationError.cs b/src/Umbraco.ModelsBuilder/Umbraco/ModelsGenerationError.cs deleted file mode 100644 index 7102190b5e..0000000000 --- a/src/Umbraco.ModelsBuilder/Umbraco/ModelsGenerationError.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.IO; -using System.Text; -using Umbraco.Core.Configuration; -using Umbraco.ModelsBuilder.Configuration; - -namespace Umbraco.ModelsBuilder.Umbraco -{ - internal static class ModelsGenerationError - { - public static void Clear() - { - var errFile = GetErrFile(); - if (errFile == null) return; - - // "If the file to be deleted does not exist, no exception is thrown." - File.Delete(errFile); - } - - public static void Report(string message, Exception e) - { - var errFile = GetErrFile(); - if (errFile == null) return; - - var sb = new StringBuilder(); - sb.Append(message); - sb.Append("\r\n"); - sb.Append(e.Message); - sb.Append("\r\n\r\n"); - sb.Append(e.StackTrace); - sb.Append("\r\n"); - - File.WriteAllText(errFile, sb.ToString()); - } - - public static string GetLastError() - { - var errFile = GetErrFile(); - if (errFile == null) return null; - - try - { - return File.ReadAllText(errFile); - } - catch // accepted - { - return null; - } - } - - private static string GetErrFile() - { - var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory; - if (!Directory.Exists(modelsDirectory)) - return null; - - return Path.Combine(modelsDirectory, "models.err"); - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Umbraco/OutOfDateModelsStatus.cs b/src/Umbraco.ModelsBuilder/Umbraco/OutOfDateModelsStatus.cs deleted file mode 100644 index a047f21edb..0000000000 --- a/src/Umbraco.ModelsBuilder/Umbraco/OutOfDateModelsStatus.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.IO; -using System.Web.Hosting; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.ModelsBuilder.Configuration; -using Umbraco.Web.Cache; - -namespace Umbraco.ModelsBuilder.Umbraco -{ - public sealed class OutOfDateModelsStatus - { - internal static void Install() - { - // just be sure - if (UmbracoConfig.For.ModelsBuilder().FlagOutOfDateModels == false) - return; - - ContentTypeCacheRefresher.CacheUpdated += (sender, args) => Write(); - DataTypeCacheRefresher.CacheUpdated += (sender, args) => Write(); - } - - private static string GetFlagPath() - { - var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory; - if (!Directory.Exists(modelsDirectory)) - Directory.CreateDirectory(modelsDirectory); - return Path.Combine(modelsDirectory, "ood.flag"); - } - - private static void Write() - { - var path = GetFlagPath(); - if (path == null || File.Exists(path)) return; - File.WriteAllText(path, "THIS FILE INDICATES THAT MODELS ARE OUT-OF-DATE\n\n"); - } - - public static void Clear() - { - if (UmbracoConfig.For.ModelsBuilder().FlagOutOfDateModels == false) return; - var path = GetFlagPath(); - if (path == null || !File.Exists(path)) return; - File.Delete(path); - } - - public static bool IsEnabled => UmbracoConfig.For.ModelsBuilder().FlagOutOfDateModels; - - public static bool IsOutOfDate - { - get - { - if (UmbracoConfig.For.ModelsBuilder().FlagOutOfDateModels == false) return false; - var path = GetFlagPath(); - return path != null && File.Exists(path); - } - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Umbraco/PublishedModelUtility.cs b/src/Umbraco.ModelsBuilder/Umbraco/PublishedModelUtility.cs deleted file mode 100644 index c70e8a3b65..0000000000 --- a/src/Umbraco.ModelsBuilder/Umbraco/PublishedModelUtility.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Linq; -using System.Linq.Expressions; -using Umbraco.Web.Composing; -using Umbraco.Core.Models.PublishedContent; - -namespace Umbraco.ModelsBuilder.Umbraco -{ - public static class PublishedModelUtility - { - // looks safer but probably useless... ppl should not call these methods directly - // and if they do... they have to take care about not doing stupid things - - //public static PublishedPropertyType GetModelPropertyType2(Expression> selector) - // where T : PublishedContentModel - //{ - // var type = typeof (T); - // var s1 = type.GetField("ModelTypeAlias", BindingFlags.Public | BindingFlags.Static); - // var alias = (s1.IsLiteral && s1.IsInitOnly && s1.FieldType == typeof(string)) ? (string)s1.GetValue(null) : null; - // var s2 = type.GetField("ModelItemType", BindingFlags.Public | BindingFlags.Static); - // var itemType = (s2.IsLiteral && s2.IsInitOnly && s2.FieldType == typeof(PublishedItemType)) ? (PublishedItemType)s2.GetValue(null) : 0; - - // var contentType = PublishedContentType.Get(itemType, alias); - // // etc... - //} - - public static PublishedContentType GetModelContentType(PublishedItemType itemType, string alias) - { - var facade = Current.UmbracoContext.PublishedSnapshot; // fixme inject! - switch (itemType) - { - case PublishedItemType.Content: - return facade.Content.GetContentType(alias); - case PublishedItemType.Media: - return facade.Media.GetContentType(alias); - case PublishedItemType.Member: - return facade.Members.GetContentType(alias); - default: - throw new ArgumentOutOfRangeException(nameof(itemType)); - } - } - - public static PublishedPropertyType GetModelPropertyType(PublishedContentType contentType, Expression> selector) - //where TModel : PublishedContentModel // fixme PublishedContentModel _or_ PublishedElementModel - { - // fixme therefore, missing a check on TModel here - - var expr = selector.Body as MemberExpression; - - if (expr == null) - throw new ArgumentException("Not a property expression.", nameof(selector)); - - // there _is_ a risk that contentType and T do not match - // see note above : accepted risk... - - var attr = expr.Member - .GetCustomAttributes(typeof (ImplementPropertyTypeAttribute), false) - .OfType() - .SingleOrDefault(); - - if (string.IsNullOrWhiteSpace(attr?.Alias)) - throw new InvalidOperationException($"Could not figure out property alias for property \"{expr.Member.Name}\"."); - - return contentType.GetPropertyType(attr.Alias); - } - } -} diff --git a/src/Umbraco.ModelsBuilder/Umbraco/PureLiveModelFactory.cs b/src/Umbraco.ModelsBuilder/Umbraco/PureLiveModelFactory.cs deleted file mode 100644 index 9558c0140e..0000000000 --- a/src/Umbraco.ModelsBuilder/Umbraco/PureLiveModelFactory.cs +++ /dev/null @@ -1,602 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Reflection.Emit; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading; -using System.Web.Compilation; -using System.Web.Hosting; -using System.Web.WebPages.Razor; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.Web.Cache; -using Umbraco.ModelsBuilder.Building; -using Umbraco.ModelsBuilder.Configuration; -using File = System.IO.File; - -namespace Umbraco.ModelsBuilder.Umbraco -{ - internal class PureLiveModelFactory : IPublishedModelFactory, IRegisteredObject - { - private Assembly _modelsAssembly; - private Infos _infos = new Infos { ModelInfos = null, ModelTypeMap = new Dictionary() }; - private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(); - private volatile bool _hasModels; // volatile 'cos reading outside lock - private bool _pendingRebuild; - private readonly ProfilingLogger _logger; - private readonly FileSystemWatcher _watcher; - private int _ver, _skipver; - private readonly int _debugLevel; - private BuildManager _theBuildManager; - private readonly Lazy _umbracoServices; - private UmbracoServices UmbracoServices => _umbracoServices.Value; - - private static readonly Regex AssemblyVersionRegex = new Regex("AssemblyVersion\\(\"[0-9]+.[0-9]+.[0-9]+.[0-9]+\"\\)", RegexOptions.Compiled); - private const string ProjVirt = "~/App_Data/Models/all.generated.cs"; - private static readonly string[] OurFiles = { "models.hash", "models.generated.cs", "all.generated.cs", "all.dll.path", "models.err" }; - - public PureLiveModelFactory(Lazy umbracoServices, ProfilingLogger logger) - { - _umbracoServices = umbracoServices; - _logger = logger; - _ver = 1; // zero is for when we had no version - _skipver = -1; // nothing to skip - ContentTypeCacheRefresher.CacheUpdated += (sender, args) => ResetModels(); - DataTypeCacheRefresher.CacheUpdated += (sender, args) => ResetModels(); - RazorBuildProvider.CodeGenerationStarted += RazorBuildProvider_CodeGenerationStarted; - - if (!HostingEnvironment.IsHosted) return; - - var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory; - if (!Directory.Exists(modelsDirectory)) - Directory.CreateDirectory(modelsDirectory); - - // BEWARE! if the watcher is not properly released then for some reason the - // BuildManager will start confusing types - using a 'registered object' here - // though we should probably plug into Umbraco's MainDom - which is internal - HostingEnvironment.RegisterObject(this); - _watcher = new FileSystemWatcher(modelsDirectory); - _watcher.Changed += WatcherOnChanged; - _watcher.EnableRaisingEvents = true; - - // get it here, this need to be fast - _debugLevel = UmbracoConfig.For.ModelsBuilder().DebugLevel; - } - - #region IPublishedModelFactory - - public IPublishedElement CreateModel(IPublishedElement element) - { - // get models, rebuilding them if needed - var infos = EnsureModels()?.ModelInfos; - if (infos == null) - return element; - - // be case-insensitive - var contentTypeAlias = element.ContentType.Alias; - - // lookup model constructor (else null) - infos.TryGetValue(contentTypeAlias, out ModelInfo info); - - // create model - return info == null ? element : info.Ctor(element); - } - - // this runs only once the factory is ready - // NOT when building models - public Type MapModelType(Type type) - { - var infos = EnsureModels(); - return ModelType.Map(type, infos.ModelTypeMap); - } - - // this runs only once the factory is ready - // NOT when building models - public IList CreateModelList(string alias) - { - var infos = EnsureModels(); - - // fail fast - if (infos == null) - return new List(); - - if (!infos.ModelInfos.TryGetValue(alias, out var modelInfo)) - return new List(); - - var ctor = modelInfo.ListCtor; - if (ctor != null) return ctor(); - - var listType = typeof(List<>).MakeGenericType(modelInfo.ModelType); - ctor = modelInfo.ListCtor = ReflectionUtilities.EmitCtor>(declaring: listType); - return ctor(); - } - - #endregion - - #region Compilation - - // deadlock note - // - // when RazorBuildProvider_CodeGenerationStarted runs, the thread has Monitor.Enter-ed the BuildManager - // singleton instance, through a call to CompilationLock.GetLock in BuildManager.GetVPathBuildResultInternal, - // and now wants to lock _locker. - // when EnsureModels runs, the thread locks _locker and then wants BuildManager to compile, which in turns - // requires that the BuildManager can Monitor.Enter-ed itself. - // so: - // - // T1 - needs to ensure models, locks _locker - // T2 - needs to compile a view, locks BuildManager - // hits RazorBuildProvider_CodeGenerationStarted - // wants to lock _locker, wait - // T1 - needs to compile models, using BuildManager - // wants to lock itself, wait - // - // - // until ASP.NET kills the long-running request (thread abort) - // - // problem is, we *want* to suspend views compilation while the models assembly is being changed else we - // end up with views compiled and cached with the old assembly, while models come from the new assembly, - // which gives more YSOD. so we *have* to lock _locker in RazorBuildProvider_CodeGenerationStarted. - // - // one "easy" solution consists in locking the BuildManager *before* _locker in EnsureModels, thus ensuring - // we always lock in the same order, and getting rid of deadlocks - but that requires having access to the - // current BuildManager instance, which is BuildManager.TheBuildManager, which is an internal property. - // - // well, that's what we are doing in this class' TheBuildManager property, using reflection. - - private void RazorBuildProvider_CodeGenerationStarted(object sender, EventArgs e) - { - try - { - _locker.EnterReadLock(); - - // just be safe - can happen if the first view is not an Umbraco view, - // or if something went wrong and we don't have an assembly at all - if (_modelsAssembly == null) return; - - if (_debugLevel > 0) - _logger.Logger.Debug("RazorBuildProvider.CodeGenerationStarted"); - if (!(sender is RazorBuildProvider provider)) return; - - // add the assembly, and add a dependency to a text file that will change on each - // compilation as in some environments (could not figure which/why) the BuildManager - // would not re-compile the views when the models assembly is rebuilt. - provider.AssemblyBuilder.AddAssemblyReference(_modelsAssembly); - provider.AddVirtualPathDependency(ProjVirt); - } - finally - { - if (_locker.IsReadLockHeld) - _locker.ExitReadLock(); - } - } - - // tells the factory that it should build a new generation of models - private void ResetModels() - { - _logger.Logger.Debug("Resetting models."); - - try - { - _locker.EnterWriteLock(); - - _hasModels = false; - _pendingRebuild = true; - } - finally - { - if (_locker.IsWriteLockHeld) - _locker.ExitWriteLock(); - } - } - - // gets "the" build manager - private BuildManager TheBuildManager - { - get - { - if (_theBuildManager != null) return _theBuildManager; - var prop = typeof (BuildManager).GetProperty("TheBuildManager", BindingFlags.NonPublic | BindingFlags.Static); - if (prop == null) - throw new InvalidOperationException("Could not get BuildManager.TheBuildManager property."); - _theBuildManager = (BuildManager) prop.GetValue(null); - return _theBuildManager; - } - } - - // ensure that the factory is running with the lastest generation of models - internal Infos EnsureModels() - { - if (_debugLevel > 0) - _logger.Logger.Debug("Ensuring models."); - - // don't use an upgradeable lock here because only 1 thread at a time could enter it - try - { - _locker.EnterReadLock(); - if (_hasModels) - return _infos; - } - finally - { - if (_locker.IsReadLockHeld) - _locker.ExitReadLock(); - } - - var buildManagerLocked = false; - try - { - // always take the BuildManager lock *before* taking the _locker lock - // to avoid possible deadlock situations (see notes above) - Monitor.Enter(TheBuildManager, ref buildManagerLocked); - - _locker.EnterUpgradeableReadLock(); - - if (_hasModels) return _infos; - - _locker.EnterWriteLock(); - - // we don't have models, - // either they haven't been loaded from the cache yet - // or they have been reseted and are pending a rebuild - - using (_logger.DebugDuration("Get models.", "Got models.")) - { - try - { - var assembly = GetModelsAssembly(_pendingRebuild); - - // the one below can be used to simulate an issue with BuildManager, ie it will register - // the models with the factory but NOT with the BuildManager, which will not recompile views. - // this is for U4-8043 which is an obvious issue but I cannot replicate - //_modelsAssembly = _modelsAssembly ?? assembly; - - // the one below is the normal one - _modelsAssembly = assembly; - - var types = assembly.ExportedTypes.Where(x => x.Inherits() || x.Inherits()); - _infos = RegisterModels(types); - ModelsGenerationError.Clear(); - } - catch (Exception e) - { - try - { - _logger.Logger.Error("Failed to build models.", e); - _logger.Logger.Warn("Running without models."); // be explicit - ModelsGenerationError.Report("Failed to build PureLive models.", e); - } - finally - { - _modelsAssembly = null; - _infos = new Infos { ModelInfos = null, ModelTypeMap = new Dictionary() }; - } - } - - // don't even try again - _hasModels = true; - } - - return _infos; - } - finally - { - if (_locker.IsWriteLockHeld) - _locker.ExitWriteLock(); - if (_locker.IsUpgradeableReadLockHeld) - _locker.ExitUpgradeableReadLock(); - if (buildManagerLocked) - Monitor.Exit(TheBuildManager); - } - } - - private Assembly GetModelsAssembly(bool forceRebuild) - { - var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory; - if (!Directory.Exists(modelsDirectory)) - Directory.CreateDirectory(modelsDirectory); - - // must filter out *.generated.cs because we haven't deleted them yet! - var ourFiles = Directory.Exists(modelsDirectory) - ? Directory.GetFiles(modelsDirectory, "*.cs") - .Where(x => !x.EndsWith(".generated.cs")) - .ToDictionary(x => x, File.ReadAllText) - : new Dictionary(); - - var typeModels = UmbracoServices.GetAllTypes(); - var currentHash = HashHelper.Hash(ourFiles, typeModels); - var modelsHashFile = Path.Combine(modelsDirectory, "models.hash"); - var modelsSrcFile = Path.Combine(modelsDirectory, "models.generated.cs"); - var projFile = Path.Combine(modelsDirectory, "all.generated.cs"); - var dllPathFile = Path.Combine(modelsDirectory, "all.dll.path"); - - // caching the generated models speeds up booting - // currentHash hashes both the types & the user's partials - - if (!forceRebuild) - { - _logger.Logger.Debug("Looking for cached models."); - if (File.Exists(modelsHashFile) && File.Exists(projFile)) - { - var cachedHash = File.ReadAllText(modelsHashFile); - if (currentHash != cachedHash) - { - _logger.Logger.Debug("Found obsolete cached models."); - forceRebuild = true; - } - } - else - { - _logger.Logger.Debug("Could not find cached models."); - forceRebuild = true; - } - } - - Assembly assembly; - if (forceRebuild == false) - { - // try to load the dll directly (avoid rebuilding) - if (File.Exists(dllPathFile)) - { - var dllPath = File.ReadAllText(dllPathFile); - if (File.Exists(dllPath)) - { - assembly = Assembly.LoadFile(dllPath); - var attr = assembly.GetCustomAttribute(); - if (attr != null && attr.PureLive && attr.SourceHash == currentHash) - { - // if we were to resume at that revision, then _ver would keep increasing - // and that is probably a bad idea - so, we'll always rebuild starting at - // ver 1, but we remember we want to skip that one - so we never end up - // with the "same but different" version of the assembly in memory - _skipver = assembly.GetName().Version.Revision; - - _logger.Logger.Debug("Loading cached models (dll)."); - return assembly; - } - } - } - - // mmust reset the version in the file else it would keep growing - // loading cached modules only happens when the app restarts - var text = File.ReadAllText(projFile); - var match = AssemblyVersionRegex.Match(text); - if (match.Success) - { - text = text.Replace(match.Value, "AssemblyVersion(\"0.0.0." + _ver + "\")"); - File.WriteAllText(projFile, text); - } - - // generate a marker file that will be a dependency - // see note in RazorBuildProvider_CodeGenerationStarted - // NO: using all.generated.cs as a dependency - //File.WriteAllText(Path.Combine(modelsDirectory, "models.dep"), "VER:" + _ver); - - _ver++; - assembly = BuildManager.GetCompiledAssembly(ProjVirt); - File.WriteAllText(dllPathFile, assembly.Location); - - _logger.Logger.Debug("Loading cached models (source)."); - return assembly; - } - - // need to rebuild - _logger.Logger.Debug("Rebuilding models."); - - // generate code, save - var code = GenerateModelsCode(ourFiles, typeModels); - // add extra attributes, - // PureLiveAssembly helps identifying Assemblies that contain PureLive models - // AssemblyVersion is so that we have a different version for each rebuild - var ver = _ver == _skipver ? ++_ver : _ver; - _ver++; - code = code.Replace("//ASSATTR", $@"[assembly: PureLiveAssembly] -[assembly:ModelsBuilderAssembly(PureLive = true, SourceHash = ""{currentHash}"")] -[assembly:System.Reflection.AssemblyVersion(""0.0.0.{ver}"")]"); - File.WriteAllText(modelsSrcFile, code); - - // generate proj, save - ourFiles["models.generated.cs"] = code; - var proj = GenerateModelsProj(ourFiles); - File.WriteAllText(projFile, proj); - - // compile and register - assembly = BuildManager.GetCompiledAssembly(ProjVirt); - File.WriteAllText(dllPathFile, assembly.Location); - - // assuming we can write and it's not going to cause exceptions... - File.WriteAllText(modelsHashFile, currentHash); - - _logger.Logger.Debug("Done rebuilding."); - return assembly; - } - - private static Infos RegisterModels(IEnumerable types) - { - var ctorArgTypes = new[] { typeof (IPublishedElement) }; - var modelInfos = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - var map = new Dictionary(); - - foreach (var type in types) - { - ConstructorInfo constructor = null; - Type parameterType = null; - - foreach (var ctor in type.GetConstructors()) - { - var parms = ctor.GetParameters(); - if (parms.Length == 1 && typeof (IPublishedElement).IsAssignableFrom(parms[0].ParameterType)) - { - if (constructor != null) - throw new InvalidOperationException($"Type {type.FullName} has more than one public constructor with one argument of type, or implementing, IPropertySet."); - constructor = ctor; - parameterType = parms[0].ParameterType; - } - } - - if (constructor == null) - throw new InvalidOperationException($"Type {type.FullName} is missing a public constructor with one argument of type, or implementing, IPropertySet."); - - var attribute = type.GetCustomAttribute(false); - var typeName = attribute == null ? type.Name : attribute.ContentTypeAlias; - - if (modelInfos.TryGetValue(typeName, out ModelInfo modelInfo)) - throw new InvalidOperationException($"Both types {type.FullName} and {modelInfo.ModelType.FullName} want to be a model type for content type with alias \"{typeName}\"."); - - // fixme use Core's ReflectionUtilities.EmitCtor !! - var meth = new DynamicMethod(string.Empty, typeof (IPublishedElement), ctorArgTypes, type.Module, true); - var gen = meth.GetILGenerator(); - gen.Emit(OpCodes.Ldarg_0); - gen.Emit(OpCodes.Newobj, constructor); - gen.Emit(OpCodes.Ret); - var func = (Func) meth.CreateDelegate(typeof (Func)); - - modelInfos[typeName] = new ModelInfo { ParameterType = parameterType, Ctor = func, ModelType = type }; - map[typeName] = type; - } - - return new Infos { ModelInfos = modelInfos.Count > 0 ? modelInfos : null, ModelTypeMap = map }; - } - - private static string GenerateModelsCode(IDictionary ourFiles, IList typeModels) - { - var modelsDirectory = UmbracoConfig.For.ModelsBuilder().ModelsDirectory; - if (!Directory.Exists(modelsDirectory)) - Directory.CreateDirectory(modelsDirectory); - - foreach (var file in Directory.GetFiles(modelsDirectory, "*.generated.cs")) - File.Delete(file); - - var map = typeModels.ToDictionary(x => x.Alias, x => x.ClrName); - foreach (var typeModel in typeModels) - { - foreach (var propertyModel in typeModel.Properties) - { - propertyModel.ClrTypeName = ModelType.MapToName(propertyModel.ModelClrType, map); - } - } - - var parseResult = new CodeParser().ParseWithReferencedAssemblies(ourFiles); - var builder = new TextBuilder(typeModels, parseResult, UmbracoConfig.For.ModelsBuilder().ModelsNamespace); - - var codeBuilder = new StringBuilder(); - builder.Generate(codeBuilder, builder.GetModelsToGenerate()); - var code = codeBuilder.ToString(); - - return code; - } - - private static readonly Regex UsingRegex = new Regex("^using(.*);", RegexOptions.Compiled | RegexOptions.Multiline); - private static readonly Regex AattrRegex = new Regex("^\\[assembly:(.*)\\]", RegexOptions.Compiled | RegexOptions.Multiline); - - private static string GenerateModelsProj(IDictionary files) - { - // ideally we would generate a CSPROJ file but then we'd need a BuildProvider for csproj - // trying to keep things simple for the time being, just write everything to one big file - - // group all 'using' at the top of the file (else fails) - var usings = new List(); - foreach (var k in files.Keys.ToList()) - files[k] = UsingRegex.Replace(files[k], m => - { - usings.Add(m.Groups[1].Value); - return string.Empty; - }); - - // group all '[assembly:...]' at the top of the file (else fails) - var aattrs = new List(); - foreach (var k in files.Keys.ToList()) - files[k] = AattrRegex.Replace(files[k], m => - { - aattrs.Add(m.Groups[1].Value); - return string.Empty; - }); - - var text = new StringBuilder(); - foreach (var u in usings.Distinct()) - { - text.Append("using "); - text.Append(u); - text.Append(";\r\n"); - } - foreach (var a in aattrs) - { - text.Append("[assembly:"); - text.Append(a); - text.Append("]\r\n"); - } - text.Append("\r\n\r\n"); - foreach (var f in files) - { - text.Append("// FILE: "); - text.Append(f.Key); - text.Append("\r\n\r\n"); - text.Append(f.Value); - text.Append("\r\n\r\n\r\n"); - } - text.Append("// EOF\r\n"); - - return text.ToString(); - } - - internal class Infos - { - public Dictionary ModelTypeMap { get; set; } - public Dictionary ModelInfos { get; set; } - } - - internal class ModelInfo - { - public Type ParameterType { get; set; } - public Func Ctor { get; set; } - public Type ModelType { get; set; } - public Func ListCtor { get; set; } - } - - #endregion - - #region Watching - - private void WatcherOnChanged(object sender, FileSystemEventArgs args) - { - var changed = args.Name; - - // don't reset when our files change because we are building! - // - // comment it out, and always ignore our files, because it seems that some - // race conditions can occur on slow Cloud filesystems and then we keep - // rebuilding - - //if (_building && OurFiles.Contains(changed)) - //{ - // //_logger.Logger.Info("Ignoring files self-changes."); - // return; - //} - - // always ignore our own file changes - if (OurFiles.Contains(changed)) - return; - - _logger.Logger.Info("Detected files changes."); - - ResetModels(); - } - - public void Stop(bool immediate) - { - _watcher.EnableRaisingEvents = false; - _watcher.Dispose(); - HostingEnvironment.UnregisterObject(this); - } - - #endregion - } -} diff --git a/src/Umbraco.ModelsBuilder/Umbraco/UmbracoServices.cs b/src/Umbraco.ModelsBuilder/Umbraco/UmbracoServices.cs deleted file mode 100644 index f0347d9194..0000000000 --- a/src/Umbraco.ModelsBuilder/Umbraco/UmbracoServices.cs +++ /dev/null @@ -1,236 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Models; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.Services; -using Umbraco.Core.Strings; -using Umbraco.ModelsBuilder.Building; -using Umbraco.ModelsBuilder.Configuration; - -namespace Umbraco.ModelsBuilder.Umbraco -{ - public class UmbracoServices - { - private readonly IContentTypeService _contentTypeService; - private readonly IMediaTypeService _mediaTypeService; - private readonly IMemberTypeService _memberTypeService; - private readonly IPublishedContentTypeFactory _publishedContentTypeFactory; - - public UmbracoServices(IContentTypeService contentTypeService, IMediaTypeService mediaTypeService, IMemberTypeService memberTypeService, IPublishedContentTypeFactory publishedContentTypeFactory) - { - _contentTypeService = contentTypeService; - _mediaTypeService = mediaTypeService; - _memberTypeService = memberTypeService; - _publishedContentTypeFactory = publishedContentTypeFactory; - } - - #region Services - - public IList GetAllTypes() - { - var types = new List(); - - types.AddRange(GetTypes(PublishedItemType.Content, _contentTypeService.GetAll().Cast().ToArray())); - types.AddRange(GetTypes(PublishedItemType.Media, _mediaTypeService.GetAll().Cast().ToArray())); - types.AddRange(GetTypes(PublishedItemType.Member, _memberTypeService.GetAll().Cast().ToArray())); - - return EnsureDistinctAliases(types); - } - - public IList GetContentTypes() - { - var contentTypes = _contentTypeService.GetAll().Cast().ToArray(); - return GetTypes(PublishedItemType.Content, contentTypes); // aliases have to be unique here - } - - public IList GetMediaTypes() - { - var contentTypes = _mediaTypeService.GetAll().Cast().ToArray(); - return GetTypes(PublishedItemType.Media, contentTypes); // aliases have to be unique here - } - - public IList GetMemberTypes() - { - var memberTypes = _memberTypeService.GetAll().Cast().ToArray(); - return GetTypes(PublishedItemType.Member, memberTypes); // aliases have to be unique here - } - - public static string GetClrName(string name, string alias) - { - // ideally we should just be able to re-use Umbraco's alias, - // just upper-casing the first letter, however in v7 for backward - // compatibility reasons aliases derive from names via ToSafeAlias which is - // PreFilter = ApplyUrlReplaceCharacters, - // IsTerm = (c, leading) => leading - // ? char.IsLetter(c) // only letters - // : (char.IsLetterOrDigit(c) || c == '_'), // letter, digit or underscore - // StringType = CleanStringType.Ascii | CleanStringType.UmbracoCase, - // BreakTermsOnUpper = false - // - // but that is not ideal with acronyms and casing - // however we CANNOT change Umbraco - // so, adding a way to "do it right" deriving from name, here - - switch (UmbracoConfig.For.ModelsBuilder().ClrNameSource) - { - case ClrNameSource.RawAlias: - // use Umbraco's alias - return alias; - - case ClrNameSource.Alias: - // ModelsBuilder's legacy - but not ideal - return alias.ToCleanString(CleanStringType.ConvertCase | CleanStringType.PascalCase); - - case ClrNameSource.Name: - // derive from name - var source = name.TrimStart('_'); // because CleanStringType.ConvertCase accepts them - return source.ToCleanString(CleanStringType.ConvertCase | CleanStringType.PascalCase | CleanStringType.Ascii); - - default: - throw new Exception("Invalid ClrNameSource."); - } - } - - private IList GetTypes(PublishedItemType itemType, IContentTypeComposition[] contentTypes) - { - var typeModels = new List(); - var uniqueTypes = new HashSet(); - - // get the types and the properties - foreach (var contentType in contentTypes) - { - var typeModel = new TypeModel - { - Id = contentType.Id, - Alias = contentType.Alias, - ClrName = GetClrName(contentType.Name, contentType.Alias), - ParentId = contentType.ParentId, - - Name = contentType.Name, - Description = contentType.Description - }; - - // of course this should never happen, but when it happens, better detect it - // else we end up with weird nullrefs everywhere - if (uniqueTypes.Contains(typeModel.ClrName)) - throw new Exception($"Panic: duplicate type ClrName \"{typeModel.ClrName}\"."); - uniqueTypes.Add(typeModel.ClrName); - - // fixme - we need a better way at figuring out what's an element type! - // and then we should not do the alias filtering below - bool IsElement(PublishedContentType x) - { - return x.Alias.InvariantEndsWith("Element"); - } - - var publishedContentType = _publishedContentTypeFactory.CreateContentType(contentType); - switch (itemType) - { - case PublishedItemType.Content: - if (IsElement(publishedContentType)) - { - typeModel.ItemType = TypeModel.ItemTypes.Element; - if (typeModel.ClrName.InvariantEndsWith("Element")) - typeModel.ClrName = typeModel.ClrName.Substring(0, typeModel.ClrName.Length - "Element".Length); - } - else - { - typeModel.ItemType = TypeModel.ItemTypes.Content; - } - break; - case PublishedItemType.Media: - typeModel.ItemType = TypeModel.ItemTypes.Media; - break; - case PublishedItemType.Member: - typeModel.ItemType = TypeModel.ItemTypes.Member; - break; - default: - throw new InvalidOperationException(string.Format("Unsupported PublishedItemType \"{0}\".", itemType)); - } - - typeModels.Add(typeModel); - - foreach (var propertyType in contentType.PropertyTypes) - { - var propertyModel = new PropertyModel - { - Alias = propertyType.Alias, - ClrName = GetClrName(propertyType.Name, propertyType.Alias), - - Name = propertyType.Name, - Description = propertyType.Description - }; - - var publishedPropertyType = publishedContentType.GetPropertyType(propertyType.Alias); - if (publishedPropertyType == null) - throw new Exception($"Panic: could not get published property type {contentType.Alias}.{propertyType.Alias}."); - - propertyModel.ModelClrType = publishedPropertyType.ModelClrType; - - typeModel.Properties.Add(propertyModel); - } - } - - // wire the base types - foreach (var typeModel in typeModels.Where(x => x.ParentId > 0)) - { - typeModel.BaseType = typeModels.SingleOrDefault(x => x.Id == typeModel.ParentId); - // Umbraco 7.4 introduces content types containers, so even though ParentId > 0, the parent might - // not be a content type - here we assume that BaseType being null while ParentId > 0 means that - // the parent is a container (and we don't check). - typeModel.IsParent = typeModel.BaseType != null; - } - - // discover mixins - foreach (var contentType in contentTypes) - { - var typeModel = typeModels.SingleOrDefault(x => x.Id == contentType.Id); - if (typeModel == null) throw new Exception("Panic: no type model matching content type."); - - IEnumerable compositionTypes; - var contentTypeAsMedia = contentType as IMediaType; - var contentTypeAsContent = contentType as IContentType; - var contentTypeAsMember = contentType as IMemberType; - if (contentTypeAsMedia != null) compositionTypes = contentTypeAsMedia.ContentTypeComposition; - else if (contentTypeAsContent != null) compositionTypes = contentTypeAsContent.ContentTypeComposition; - else if (contentTypeAsMember != null) compositionTypes = contentTypeAsMember.ContentTypeComposition; - else throw new Exception(string.Format("Panic: unsupported type \"{0}\".", contentType.GetType().FullName)); - - foreach (var compositionType in compositionTypes) - { - var compositionModel = typeModels.SingleOrDefault(x => x.Id == compositionType.Id); - if (compositionModel == null) throw new Exception("Panic: composition type does not exist."); - - if (compositionType.Id == contentType.ParentId) continue; - - // add to mixins - typeModel.MixinTypes.Add(compositionModel); - - // mark as mixin - as well as parents - compositionModel.IsMixin = true; - while ((compositionModel = compositionModel.BaseType) != null) - compositionModel.IsMixin = true; - } - } - - return typeModels; - } - - internal static IList EnsureDistinctAliases(IList typeModels) - { - var groups = typeModels.GroupBy(x => x.Alias.ToLowerInvariant()); - foreach (var group in groups.Where(x => x.Count() > 1)) - { - throw new NotSupportedException($"Alias \"{group.Key}\" is used by types" - + $" {string.Join(", ", group.Select(x => x.ItemType + ":\"" + x.Alias + "\""))}. Aliases have to be unique." - + " One of the aliases must be modified in order to use the ModelsBuilder."); - } - return typeModels; - } - - #endregion - } -} diff --git a/src/Umbraco.ModelsBuilder/Validation/ContentTypeModelValidator.cs b/src/Umbraco.ModelsBuilder/Validation/ContentTypeModelValidator.cs deleted file mode 100644 index 20f5e94b64..0000000000 --- a/src/Umbraco.ModelsBuilder/Validation/ContentTypeModelValidator.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Models; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.ModelsBuilder.Configuration; -using Umbraco.Web.Editors; -using Umbraco.Web.Models.ContentEditing; - -namespace Umbraco.ModelsBuilder.Validation -{ - /// - /// Used to validate the aliases for the content type when MB is enabled to ensure that - /// no illegal aliases are used - /// - internal class ContentTypeModelValidator : ContentTypeModelValidatorBase - { - } - - /// - /// Used to validate the aliases for the content type when MB is enabled to ensure that - /// no illegal aliases are used - /// - internal class MediaTypeModelValidator : ContentTypeModelValidatorBase - { - } - - /// - /// Used to validate the aliases for the content type when MB is enabled to ensure that - /// no illegal aliases are used - /// - internal class MemberTypeModelValidator : ContentTypeModelValidatorBase - { - } - - internal abstract class ContentTypeModelValidatorBase : EditorValidator - where TModel: ContentTypeSave - where TProperty: PropertyTypeBasic - { - protected override IEnumerable Validate(TModel model) - { - //don't do anything if we're not enabled - if (UmbracoConfig.For.ModelsBuilder().Enable) - { - var properties = model.Groups.SelectMany(x => x.Properties) - .Where(x => x.Inherited == false) - .ToArray(); - - foreach (var prop in properties) - { - var propertyGroup = model.Groups.Single(x => x.Properties.Contains(prop)); - - if (model.Alias.ToLowerInvariant() == prop.Alias.ToLowerInvariant()) - yield return new ValidationResult(string.Format("With Models Builder enabled, you can't have a property with a the alias \"{0}\" when the content type alias is also \"{0}\".", prop.Alias), new[] - { - string.Format("Groups[{0}].Properties[{1}].Alias", model.Groups.IndexOf(propertyGroup), propertyGroup.Properties.IndexOf(prop)) - }); - - //we need to return the field name with an index so it's wired up correctly - var groupIndex = model.Groups.IndexOf(propertyGroup); - var propertyIndex = propertyGroup.Properties.IndexOf(prop); - - var validationResult = ValidateProperty(prop, groupIndex, propertyIndex); - if (validationResult != null) - { - yield return validationResult; - } - } - } - } - - private ValidationResult ValidateProperty(PropertyTypeBasic property, int groupIndex, int propertyIndex) - { - //don't let them match any properties or methods in IPublishedContent - //TODO: There are probably more! - var reservedProperties = typeof(IPublishedContent).GetProperties().Select(x => x.Name).ToArray(); - var reservedMethods = typeof(IPublishedContent).GetMethods().Select(x => x.Name).ToArray(); - - var alias = property.Alias; - - if (reservedProperties.InvariantContains(alias) || reservedMethods.InvariantContains(alias)) - { - return new ValidationResult( - string.Format("The alias {0} is a reserved term and cannot be used", alias), new[] - { - string.Format("Groups[{0}].Properties[{1}].Alias", groupIndex, propertyIndex) - }); - } - - return null; - } - } -} diff --git a/src/umbraco.sln b/src/umbraco.sln index 7313e06e49..e603960782 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27004.2005 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29009.5 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Web.UI", "Umbraco.Web.UI\Umbraco.Web.UI.csproj", "{4C4C194C-B5E4-4991-8F87-4373E24CC19F}" EndProject @@ -103,6 +103,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IssueTemplates", "IssueTemp ..\.github\ISSUE_TEMPLATE\5_Security_issue.md = ..\.github\ISSUE_TEMPLATE\5_Security_issue.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.ModelsBuilder", "Umbraco.ModelsBuilder\Umbraco.ModelsBuilder.csproj", "{52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -135,6 +137,10 @@ Global {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Debug|Any CPU.Build.0 = Debug|Any CPU {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Release|Any CPU.ActiveCfg = Release|Any CPU {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Release|Any CPU.Build.0 = Release|Any CPU + {52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {52AC0BA8-A60E-4E36-897B-E8B97A54ED1C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From ee83c02424492d60079e0f364b303f829936c04c Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 24 Jun 2019 11:58:36 +0200 Subject: [PATCH 0009/1545] Import slimmed down ModelsBuilder --- src/Umbraco.ModelsBuilder/Api/ApiVersion.cs | 36 + src/Umbraco.ModelsBuilder/Building/Builder.cs | 236 ++++++ .../Building/PropertyModel.cs | 61 ++ .../Building/TextBuilder.cs | 579 +++++++++++++++ .../Building/TextHeaderWriter.cs | 26 + .../Building/TypeModel.cs | 205 ++++++ .../ConfigsExtensions.cs | 20 + .../Configuration/ClrNameSource.cs | 28 + .../Configuration/Config.cs | 224 ++++++ .../Configuration/ModelsMode.cs | 36 + .../Configuration/ModelsModeExtensions.cs | 37 + .../Dashboard/BuilderDashboardHelper.cs | 65 ++ .../EnumerableExtensions.cs | 33 + .../ImplementPropertyTypeAttribute.cs | 19 + .../ModelsBuilderAssemblyAttribute.cs | 23 + .../Properties/AssemblyInfo.cs | 30 +- .../PublishedElementExtensions.cs | 51 ++ .../PureLiveAssemblyAttribute.cs | 15 + .../ReferencedAssemblies.cs | 179 +++++ src/Umbraco.ModelsBuilder/TypeExtensions.cs | 22 + .../Umbraco.ModelsBuilder.csproj | 53 ++ .../Umbraco/HashCombiner.cs | 38 + .../Umbraco/HashHelper.cs | 42 ++ .../Umbraco/LiveModelsProvider.cs | 133 ++++ .../ModelsBuilderBackOfficeController.cs | 184 +++++ .../Umbraco/ModelsBuilderComponent.cs | 186 +++++ .../Umbraco/ModelsBuilderComposer.cs | 60 ++ .../Umbraco/ModelsBuilderInitializer.cs | 28 + .../Umbraco/ModelsGenerationError.cs | 62 ++ .../Umbraco/OutOfDateModelsStatus.cs | 57 ++ .../Umbraco/PublishedModelUtility.cs | 67 ++ .../Umbraco/PureLiveModelFactory.cs | 677 ++++++++++++++++++ .../Umbraco/UmbracoServices.cs | 198 +++++ .../Validation/ContentTypeModelValidator.cs | 93 +++ src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 + 35 files changed, 3779 insertions(+), 28 deletions(-) create mode 100644 src/Umbraco.ModelsBuilder/Api/ApiVersion.cs create mode 100644 src/Umbraco.ModelsBuilder/Building/Builder.cs create mode 100644 src/Umbraco.ModelsBuilder/Building/PropertyModel.cs create mode 100644 src/Umbraco.ModelsBuilder/Building/TextBuilder.cs create mode 100644 src/Umbraco.ModelsBuilder/Building/TextHeaderWriter.cs create mode 100644 src/Umbraco.ModelsBuilder/Building/TypeModel.cs create mode 100644 src/Umbraco.ModelsBuilder/ConfigsExtensions.cs create mode 100644 src/Umbraco.ModelsBuilder/Configuration/ClrNameSource.cs create mode 100644 src/Umbraco.ModelsBuilder/Configuration/Config.cs create mode 100644 src/Umbraco.ModelsBuilder/Configuration/ModelsMode.cs create mode 100644 src/Umbraco.ModelsBuilder/Configuration/ModelsModeExtensions.cs create mode 100644 src/Umbraco.ModelsBuilder/Dashboard/BuilderDashboardHelper.cs create mode 100644 src/Umbraco.ModelsBuilder/EnumerableExtensions.cs create mode 100644 src/Umbraco.ModelsBuilder/ImplementPropertyTypeAttribute.cs create mode 100644 src/Umbraco.ModelsBuilder/ModelsBuilderAssemblyAttribute.cs create mode 100644 src/Umbraco.ModelsBuilder/PublishedElementExtensions.cs create mode 100644 src/Umbraco.ModelsBuilder/PureLiveAssemblyAttribute.cs create mode 100644 src/Umbraco.ModelsBuilder/ReferencedAssemblies.cs create mode 100644 src/Umbraco.ModelsBuilder/TypeExtensions.cs create mode 100644 src/Umbraco.ModelsBuilder/Umbraco/HashCombiner.cs create mode 100644 src/Umbraco.ModelsBuilder/Umbraco/HashHelper.cs create mode 100644 src/Umbraco.ModelsBuilder/Umbraco/LiveModelsProvider.cs create mode 100644 src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderBackOfficeController.cs create mode 100644 src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderComponent.cs create mode 100644 src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderComposer.cs create mode 100644 src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderInitializer.cs create mode 100644 src/Umbraco.ModelsBuilder/Umbraco/ModelsGenerationError.cs create mode 100644 src/Umbraco.ModelsBuilder/Umbraco/OutOfDateModelsStatus.cs create mode 100644 src/Umbraco.ModelsBuilder/Umbraco/PublishedModelUtility.cs create mode 100644 src/Umbraco.ModelsBuilder/Umbraco/PureLiveModelFactory.cs create mode 100644 src/Umbraco.ModelsBuilder/Umbraco/UmbracoServices.cs create mode 100644 src/Umbraco.ModelsBuilder/Validation/ContentTypeModelValidator.cs diff --git a/src/Umbraco.ModelsBuilder/Api/ApiVersion.cs b/src/Umbraco.ModelsBuilder/Api/ApiVersion.cs new file mode 100644 index 0000000000..a7f2db397e --- /dev/null +++ b/src/Umbraco.ModelsBuilder/Api/ApiVersion.cs @@ -0,0 +1,36 @@ +using System; +using System.Reflection; +using Semver; + +namespace Umbraco.ModelsBuilder.Api +{ + /// + /// Manages API version handshake between client and server. + /// + public class ApiVersion + { + /// + /// Initializes a new instance of the class. + /// + /// The currently executing version. + /// + internal ApiVersion(SemVersion executingVersion) + { + Version = executingVersion ?? throw new ArgumentNullException(nameof(executingVersion)); + } + + private static SemVersion CurrentAssemblyVersion + => SemVersion.Parse(Assembly.GetExecutingAssembly().GetCustomAttribute().InformationalVersion); + + /// + /// Gets the currently executing API version. + /// + public static ApiVersion Current { get; } + = new ApiVersion(CurrentAssemblyVersion); + + /// + /// Gets the executing version of the API. + /// + public SemVersion Version { get; } + } +} diff --git a/src/Umbraco.ModelsBuilder/Building/Builder.cs b/src/Umbraco.ModelsBuilder/Building/Builder.cs new file mode 100644 index 0000000000..fd5eccde5b --- /dev/null +++ b/src/Umbraco.ModelsBuilder/Building/Builder.cs @@ -0,0 +1,236 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Umbraco.Core.Composing; +using Umbraco.Core.Configuration; +using Umbraco.ModelsBuilder.Configuration; + +namespace Umbraco.ModelsBuilder.Building +{ + // NOTE + // The idea was to have different types of builder, because I wanted to experiment with + // building code with CodeDom. Turns out more complicated than I thought and maybe not + // worth it at the moment, to we're using TextBuilder and its Generate method is specific. + // + // Keeping the code as-is for the time being... + + /// + /// Provides a base class for all builders. + /// + internal abstract class Builder + { + private readonly IList _typeModels; + + protected Dictionary ModelsMap { get; } = new Dictionary(); + + private static Config Config => Current.Configs.ModelsBuilder(); + + // the list of assemblies that will be 'using' by default + protected readonly IList TypesUsing = new List + { + "System", + "System.Collections.Generic", + "System.Linq.Expressions", + "System.Web", + "Umbraco.Core.Models", + "Umbraco.Core.Models.PublishedContent", + "Umbraco.Web", + "Umbraco.ModelsBuilder", + "Umbraco.ModelsBuilder.Umbraco", + }; + + /// + /// Gets or sets a value indicating the namespace to use for the models. + /// + /// May be overriden by code attributes. + public string ModelsNamespace { get; set; } + + /// + /// Gets the list of assemblies to add to the set of 'using' assemblies in each model file. + /// + public IList Using => TypesUsing; + + /// + /// Gets the list of models to generate. + /// + /// The models to generate, ie those that are not ignored. + public IEnumerable GetModelsToGenerate() + { + return _typeModels; + } + + /// + /// Gets the list of all models. + /// + /// Includes those that are ignored. + internal IList TypeModels => _typeModels; + + /// + /// Initializes a new instance of the class with a list of models to generate + /// and the result of code parsing. + /// + /// The list of models to generate. + protected Builder(IList typeModels) + : this(typeModels, null) + { } + + /// + /// Initializes a new instance of the class with a list of models to generate, + /// the result of code parsing, and a models namespace. + /// + /// The list of models to generate. + /// The models namespace. + protected Builder(IList typeModels, string modelsNamespace) + { + _typeModels = typeModels ?? throw new ArgumentNullException(nameof(typeModels)); + + // can be null or empty, we'll manage + ModelsNamespace = modelsNamespace; + + // but we want it to prepare + Prepare(); + } + + // for unit tests only + protected Builder() + { } + + /// + /// Prepares generation by processing the result of code parsing. + /// + /// + /// Preparation includes figuring out from the existing code which models or properties should + /// be ignored or renamed, etc. -- anything that comes from the attributes in the existing code. + /// + private void Prepare() + { + TypeModel.MapModelTypes(_typeModels, ModelsNamespace); + + var pureLive = Config.ModelsMode == ModelsMode.PureLive; + + // for the first two of these two tests, + // always throw, even in purelive: cannot happen unless ppl start fidling with attributes to rename + // things, and then they should pay attention to the generation error log - there's no magic here + // for the last one, don't throw in purelive, see comment + + // ensure we have no duplicates type names + foreach (var xx in _typeModels.GroupBy(x => x.ClrName).Where(x => x.Count() > 1)) + throw new InvalidOperationException($"Type name \"{xx.Key}\" is used" + + $" for types with alias {string.Join(", ", xx.Select(x => x.ItemType + ":\"" + x.Alias + "\""))}. Names have to be unique." + + " Consider using an attribute to assign different names to conflicting types."); + + // ensure we have no duplicates property names + foreach (var typeModel in _typeModels) + foreach (var xx in typeModel.Properties.GroupBy(x => x.ClrName).Where(x => x.Count() > 1)) + throw new InvalidOperationException($"Property name \"{xx.Key}\" in type {typeModel.ItemType}:\"{typeModel.Alias}\"" + + $" is used for properties with alias {string.Join(", ", xx.Select(x => "\"" + x.Alias + "\""))}. Names have to be unique." + + " Consider using an attribute to assign different names to conflicting properties."); + + // ensure content & property type don't have identical name (csharp hates it) + foreach (var typeModel in _typeModels) + { + foreach (var xx in typeModel.Properties.Where(x => x.ClrName == typeModel.ClrName)) + { + if (!pureLive) + throw new InvalidOperationException($"The model class for content type with alias \"{typeModel.Alias}\" is named \"{xx.ClrName}\"." + + $" CSharp does not support using the same name for the property with alias \"{xx.Alias}\"." + + " Consider using an attribute to assign a different name to the property."); + + // for purelive, will we generate a commented out properties with an error message, + // instead of throwing, because then it kills the sites and ppl don't understand why + xx.AddError($"The class {typeModel.ClrName} cannot implement this property, because" + + $" CSharp does not support naming the property with alias \"{xx.Alias}\" with the same name as content type with alias \"{typeModel.Alias}\"." + + " Consider using an attribute to assign a different name to the property."); + + // will not be implemented on interface nor class + // note: we will still create the static getter, and implement the property on other classes... + } + } + + // ensure we have no collision between base types + // NO: we may want to define a base class in a partial, on a model that has a parent + // we are NOT checking that the defined base type does maintain the inheritance chain + //foreach (var xx in _typeModels.Where(x => !x.IsContentIgnored).Where(x => x.BaseType != null && x.HasBase)) + // throw new InvalidOperationException(string.Format("Type alias \"{0}\" has more than one parent class.", + // xx.Alias)); + + // discover interfaces that need to be declared / implemented + foreach (var typeModel in _typeModels) + { + // collect all the (non-removed) types implemented at parent level + // ie the parent content types and the mixins content types, recursively + var parentImplems = new List(); + if (typeModel.BaseType != null) + TypeModel.CollectImplems(parentImplems, typeModel.BaseType); + + // interfaces we must declare we implement (initially empty) + // ie this type's mixins, except those that have been removed, + // and except those that are already declared at the parent level + // in other words, DeclaringInterfaces is "local mixins" + var declaring = typeModel.MixinTypes + .Except(parentImplems); + typeModel.DeclaringInterfaces.AddRange(declaring); + + // interfaces we must actually implement (initially empty) + // if we declare we implement a mixin interface, we must actually implement + // its properties, all recursively (ie if the mixin interface implements...) + // so, starting with local mixins, we collect all the (non-removed) types above them + var mixinImplems = new List(); + foreach (var i in typeModel.DeclaringInterfaces) + TypeModel.CollectImplems(mixinImplems, i); + // and then we remove from that list anything that is already declared at the parent level + typeModel.ImplementingInterfaces.AddRange(mixinImplems.Except(parentImplems)); + } + + // ensure elements don't inherit from non-elements + foreach (var typeModel in _typeModels.Where(x => x.IsElement)) + { + if (typeModel.BaseType != null && !typeModel.BaseType.IsElement) + throw new Exception($"Cannot generate model for type '{typeModel.Alias}' because it is an element type, but its parent type '{typeModel.BaseType.Alias}' is not."); + + var errs = typeModel.MixinTypes.Where(x => !x.IsElement).ToList(); + if (errs.Count > 0) + throw new Exception($"Cannot generate model for type '{typeModel.Alias}' because it is an element type, but it is composed of {string.Join(", ", errs.Select(x => "'" + x.Alias + "'"))} which {(errs.Count == 1 ? "is" : "are")} not."); + } + } + + // looking for a simple symbol eg 'Umbraco' or 'String' + // expecting to match eg 'Umbraco' or 'System.String' + // returns true if either + // - more than 1 symbol is found (explicitely ambiguous) + // - 1 symbol is found BUT not matching (implicitely ambiguous) + protected bool IsAmbiguousSymbol(string symbol, string match) + { + // cannot figure out is a symbol is ambiguous without Roslyn + // so... let's say everything is ambiguous - code won't be + // pretty but it'll work + return true; + } + + internal string ModelsNamespaceForTests; + + public string GetModelsNamespace() + { + if (ModelsNamespaceForTests != null) + return ModelsNamespaceForTests; + + // if builder was initialized with a namespace, use that one + if (!string.IsNullOrWhiteSpace(ModelsNamespace)) + return ModelsNamespace; + + // default + // fixme - should NOT reference config here, should make ModelsNamespace mandatory + return Config.ModelsNamespace; + } + + protected string GetModelsBaseClassName(TypeModel type) + { + // default + return type.IsElement ? "PublishedElementModel" : "PublishedContentModel"; + } + } +} diff --git a/src/Umbraco.ModelsBuilder/Building/PropertyModel.cs b/src/Umbraco.ModelsBuilder/Building/PropertyModel.cs new file mode 100644 index 0000000000..5f2545ec2a --- /dev/null +++ b/src/Umbraco.ModelsBuilder/Building/PropertyModel.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; + +namespace Umbraco.ModelsBuilder.Building +{ + /// + /// Represents a model property. + /// + public class PropertyModel + { + /// + /// Gets the alias of the property. + /// + public string Alias; + + /// + /// Gets the name of the property. + /// + public string Name; + + /// + /// Gets the description of the property. + /// + public string Description; + + /// + /// Gets the clr name of the property. + /// + /// This is just the local name eg "Price". + public string ClrName; + + /// + /// Gets the Model Clr type of the property values. + /// + /// As indicated by the PublishedPropertyType, ie by the IPropertyValueConverter + /// if any, else object. May include some ModelType that will need to be mapped. + public Type ModelClrType; + + /// + /// Gets the CLR type name of the property values. + /// + public string ClrTypeName; + + /// + /// Gets the generation errors for the property. + /// + /// This should be null, unless something prevents the property from being + /// generated, and then the value should explain what. This can be used to generate + /// commented out code eg in PureLive. + public List Errors; + + /// + /// Adds an error. + /// + public void AddError(string error) + { + if (Errors == null) Errors = new List(); + Errors.Add(error); + } + } +} diff --git a/src/Umbraco.ModelsBuilder/Building/TextBuilder.cs b/src/Umbraco.ModelsBuilder/Building/TextBuilder.cs new file mode 100644 index 0000000000..e15fdfca6e --- /dev/null +++ b/src/Umbraco.ModelsBuilder/Building/TextBuilder.cs @@ -0,0 +1,579 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using Umbraco.Core.Composing; +using Umbraco.Core.Configuration; +using Umbraco.ModelsBuilder.Api; +using Umbraco.ModelsBuilder.Configuration; + +namespace Umbraco.ModelsBuilder.Building +{ + /// + /// Implements a builder that works by writing text. + /// + internal class TextBuilder : Builder + { + /// + /// Initializes a new instance of the class with a list of models to generate + /// and the result of code parsing. + /// + /// The list of models to generate. + public TextBuilder(IList typeModels) + : base(typeModels) + { } + + /// + /// Initializes a new instance of the class with a list of models to generate, + /// the result of code parsing, and a models namespace. + /// + /// The list of models to generate. + /// The models namespace. + public TextBuilder(IList typeModels, string modelsNamespace) + : base(typeModels, modelsNamespace) + { } + + // internal for unit tests only + internal TextBuilder() + { } + + private static Config Config => Current.Configs.ModelsBuilder(); + + /// + /// Outputs a generated model to a string builder. + /// + /// The string builder. + /// The model to generate. + public void Generate(StringBuilder sb, TypeModel typeModel) + { + WriteHeader(sb); + + foreach (var t in TypesUsing) + sb.AppendFormat("using {0};\n", t); + + sb.Append("\n"); + sb.AppendFormat("namespace {0}\n", GetModelsNamespace()); + sb.Append("{\n"); + + WriteContentType(sb, typeModel); + + sb.Append("}\n"); + } + + /// + /// Outputs generated models to a string builder. + /// + /// The string builder. + /// The models to generate. + public void Generate(StringBuilder sb, IEnumerable typeModels) + { + WriteHeader(sb); + + foreach (var t in TypesUsing) + sb.AppendFormat("using {0};\n", t); + + // assembly attributes marker + sb.Append("\n//ASSATTR\n"); + + sb.Append("\n"); + sb.AppendFormat("namespace {0}\n", GetModelsNamespace()); + sb.Append("{\n"); + + foreach (var typeModel in typeModels) + { + WriteContentType(sb, typeModel); + sb.Append("\n"); + } + + sb.Append("}\n"); + } + + /// + /// Outputs an "auto-generated" header to a string builder. + /// + /// The string builder. + public static void WriteHeader(StringBuilder sb) + { + TextHeaderWriter.WriteHeader(sb); + } + + // writes an attribute that identifies code generated by a tool + // (helps reduce warnings, tools such as FxCop use it) + // see https://github.com/zpqrtbnk/Zbu.ModelsBuilder/issues/107 + // see https://docs.microsoft.com/en-us/dotnet/api/system.codedom.compiler.generatedcodeattribute + // see https://blogs.msdn.microsoft.com/codeanalysis/2007/04/27/correct-usage-of-the-compilergeneratedattribute-and-the-generatedcodeattribute/ + // + // note that the blog post above clearly states that "Nor should it be applied at the type level if the type being generated is a partial class." + // and since our models are partial classes, we have to apply the attribute against the individual members, not the class itself. + // + private static void WriteGeneratedCodeAttribute(StringBuilder sb, string tabs) + { + sb.AppendFormat("{0}[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"Umbraco.ModelsBuilder\", \"{1}\")]\n", tabs, ApiVersion.Current.Version); + } + + private void WriteContentType(StringBuilder sb, TypeModel type) + { + string sep; + + if (type.IsMixin) + { + // write the interface declaration + sb.AppendFormat("\t// Mixin Content Type with alias \"{0}\"\n", type.Alias); + if (!string.IsNullOrWhiteSpace(type.Name)) + sb.AppendFormat("\t/// {0}\n", XmlCommentString(type.Name)); + sb.AppendFormat("\tpublic partial interface I{0}", type.ClrName); + var implements = type.BaseType == null + ? (type.HasBase ? null : (type.IsElement ? "PublishedElement" : "PublishedContent")) + : type.BaseType.ClrName; + if (implements != null) + sb.AppendFormat(" : I{0}", implements); + + // write the mixins + sep = implements == null ? ":" : ","; + foreach (var mixinType in type.DeclaringInterfaces.OrderBy(x => x.ClrName)) + { + sb.AppendFormat("{0} I{1}", sep, mixinType.ClrName); + sep = ","; + } + + sb.Append("\n\t{\n"); + + // write the properties - only the local (non-ignored) ones, we're an interface + var more = false; + foreach (var prop in type.Properties.OrderBy(x => x.ClrName)) + { + if (more) sb.Append("\n"); + more = true; + WriteInterfaceProperty(sb, prop); + } + + sb.Append("\t}\n\n"); + } + + // write the class declaration + if (!string.IsNullOrWhiteSpace(type.Name)) + sb.AppendFormat("\t/// {0}\n", XmlCommentString(type.Name)); + // cannot do it now. see note in ImplementContentTypeAttribute + //if (!type.HasImplement) + // sb.AppendFormat("\t[ImplementContentType(\"{0}\")]\n", type.Alias); + sb.AppendFormat("\t[PublishedModel(\"{0}\")]\n", type.Alias); + sb.AppendFormat("\tpublic partial class {0}", type.ClrName); + var inherits = type.HasBase + ? null // has its own base already + : (type.BaseType == null + ? GetModelsBaseClassName(type) + : type.BaseType.ClrName); + if (inherits != null) + sb.AppendFormat(" : {0}", inherits); + + sep = inherits == null ? ":" : ","; + if (type.IsMixin) + { + // if it's a mixin it implements its own interface + sb.AppendFormat("{0} I{1}", sep, type.ClrName); + } + else + { + // write the mixins, if any, as interfaces + // only if not a mixin because otherwise the interface already has them already + foreach (var mixinType in type.DeclaringInterfaces.OrderBy(x => x.ClrName)) + { + sb.AppendFormat("{0} I{1}", sep, mixinType.ClrName); + sep = ","; + } + } + + // begin class body + sb.Append("\n\t{\n"); + + // write the constants & static methods + // as 'new' since parent has its own - or maybe not - disable warning + sb.Append("\t\t// helpers\n"); + sb.Append("#pragma warning disable 0109 // new is redundant\n"); + WriteGeneratedCodeAttribute(sb, "\t\t"); + sb.AppendFormat("\t\tpublic new const string ModelTypeAlias = \"{0}\";\n", + type.Alias); + var itemType = type.IsElement ? TypeModel.ItemTypes.Content : type.ItemType; // fixme + WriteGeneratedCodeAttribute(sb, "\t\t"); + sb.AppendFormat("\t\tpublic new const PublishedItemType ModelItemType = PublishedItemType.{0};\n", + itemType); + WriteGeneratedCodeAttribute(sb, "\t\t"); + sb.Append("\t\tpublic new static PublishedContentType GetModelContentType()\n"); + sb.Append("\t\t\t=> PublishedModelUtility.GetModelContentType(ModelItemType, ModelTypeAlias);\n"); + WriteGeneratedCodeAttribute(sb, "\t\t"); + sb.AppendFormat("\t\tpublic static PublishedPropertyType GetModelPropertyType(Expression> selector)\n", + type.ClrName); + sb.Append("\t\t\t=> PublishedModelUtility.GetModelPropertyType(GetModelContentType(), selector);\n"); + sb.Append("#pragma warning restore 0109\n\n"); + + // write the ctor + sb.AppendFormat("\t\t// ctor\n\t\tpublic {0}(IPublished{1} content)\n\t\t\t: base(content)\n\t\t{{ }}\n\n", + type.ClrName, type.IsElement ? "Element" : "Content"); + + // write the properties + sb.Append("\t\t// properties\n"); + WriteContentTypeProperties(sb, type); + + // close the class declaration + sb.Append("\t}\n"); + } + + private void WriteContentTypeProperties(StringBuilder sb, TypeModel type) + { + var staticMixinGetters = true; + + // write the properties + foreach (var prop in type.Properties.OrderBy(x => x.ClrName)) + WriteProperty(sb, type, prop, staticMixinGetters && type.IsMixin ? type.ClrName : null); + + // no need to write the parent properties since we inherit from the parent + // and the parent defines its own properties. need to write the mixins properties + // since the mixins are only interfaces and we have to provide an implementation. + + // write the mixins properties + foreach (var mixinType in type.ImplementingInterfaces.OrderBy(x => x.ClrName)) + foreach (var prop in mixinType.Properties.OrderBy(x => x.ClrName)) + if (staticMixinGetters) + WriteMixinProperty(sb, prop, mixinType.ClrName); + else + WriteProperty(sb, mixinType, prop); + } + + private void WriteMixinProperty(StringBuilder sb, PropertyModel property, string mixinClrName) + { + sb.Append("\n"); + + // Adds xml summary to each property containing + // property name and property description + if (!string.IsNullOrWhiteSpace(property.Name) || !string.IsNullOrWhiteSpace(property.Description)) + { + sb.Append("\t\t///\n"); + + if (!string.IsNullOrWhiteSpace(property.Description)) + sb.AppendFormat("\t\t/// {0}: {1}\n", XmlCommentString(property.Name), XmlCommentString(property.Description)); + else + sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name)); + + sb.Append("\t\t///\n"); + } + + WriteGeneratedCodeAttribute(sb, "\t\t"); + sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias); + + sb.Append("\t\tpublic "); + WriteClrType(sb, property.ClrTypeName); + + sb.AppendFormat(" {0} => ", + property.ClrName); + WriteNonGenericClrType(sb, GetModelsNamespace() + "." + mixinClrName); + sb.AppendFormat(".{0}(this);\n", + MixinStaticGetterName(property.ClrName)); + } + + private static string MixinStaticGetterName(string clrName) + { + return string.Format("Get{0}", clrName); + } + + private void WriteProperty(StringBuilder sb, TypeModel type, PropertyModel property, string mixinClrName = null) + { + var mixinStatic = mixinClrName != null; + + sb.Append("\n"); + + if (property.Errors != null) + { + sb.Append("\t\t/*\n"); + sb.Append("\t\t * THIS PROPERTY CANNOT BE IMPLEMENTED, BECAUSE:\n"); + sb.Append("\t\t *\n"); + var first = true; + foreach (var error in property.Errors) + { + if (first) first = false; + else sb.Append("\t\t *\n"); + foreach (var s in SplitError(error)) + { + sb.Append("\t\t * "); + sb.Append(s); + sb.Append("\n"); + } + } + sb.Append("\t\t *\n"); + sb.Append("\n"); + } + + // Adds xml summary to each property containing + // property name and property description + if (!string.IsNullOrWhiteSpace(property.Name) || !string.IsNullOrWhiteSpace(property.Description)) + { + sb.Append("\t\t///\n"); + + if (!string.IsNullOrWhiteSpace(property.Description)) + sb.AppendFormat("\t\t/// {0}: {1}\n", XmlCommentString(property.Name), XmlCommentString(property.Description)); + else + sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name)); + + sb.Append("\t\t///\n"); + } + + WriteGeneratedCodeAttribute(sb, "\t\t"); + sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias); + + if (mixinStatic) + { + sb.Append("\t\tpublic "); + WriteClrType(sb, property.ClrTypeName); + sb.AppendFormat(" {0} => {1}(this);\n", + property.ClrName, MixinStaticGetterName(property.ClrName)); + } + else + { + sb.Append("\t\tpublic "); + WriteClrType(sb, property.ClrTypeName); + sb.AppendFormat(" {0} => this.Value", + property.ClrName); + if (property.ModelClrType != typeof(object)) + { + sb.Append("<"); + WriteClrType(sb, property.ClrTypeName); + sb.Append(">"); + } + sb.AppendFormat("(\"{0}\");\n", + property.Alias); + } + + if (property.Errors != null) + { + sb.Append("\n"); + sb.Append("\t\t *\n"); + sb.Append("\t\t */\n"); + } + + if (!mixinStatic) return; + + var mixinStaticGetterName = MixinStaticGetterName(property.ClrName); + + if (type.StaticMixinMethods.Contains(mixinStaticGetterName)) return; + + sb.Append("\n"); + + if (!string.IsNullOrWhiteSpace(property.Name)) + sb.AppendFormat("\t\t/// Static getter for {0}\n", XmlCommentString(property.Name)); + + WriteGeneratedCodeAttribute(sb, "\t\t"); + sb.Append("\t\tpublic static "); + WriteClrType(sb, property.ClrTypeName); + sb.AppendFormat(" {0}(I{1} that) => that.Value", + mixinStaticGetterName, mixinClrName); + if (property.ModelClrType != typeof(object)) + { + sb.Append("<"); + WriteClrType(sb, property.ClrTypeName); + sb.Append(">"); + } + sb.AppendFormat("(\"{0}\");\n", + property.Alias); + } + + private static IEnumerable SplitError(string error) + { + var p = 0; + while (p < error.Length) + { + var n = p + 50; + while (n < error.Length && error[n] != ' ') n++; + if (n >= error.Length) break; + yield return error.Substring(p, n - p); + p = n + 1; + } + if (p < error.Length) + yield return error.Substring(p); + } + + private void WriteInterfaceProperty(StringBuilder sb, PropertyModel property) + { + if (property.Errors != null) + { + sb.Append("\t\t/*\n"); + sb.Append("\t\t * THIS PROPERTY CANNOT BE IMPLEMENTED, BECAUSE:\n"); + sb.Append("\t\t *\n"); + var first = true; + foreach (var error in property.Errors) + { + if (first) first = false; + else sb.Append("\t\t *\n"); + foreach (var s in SplitError(error)) + { + sb.Append("\t\t * "); + sb.Append(s); + sb.Append("\n"); + } + } + sb.Append("\t\t *\n"); + sb.Append("\n"); + } + + if (!string.IsNullOrWhiteSpace(property.Name)) + sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name)); + WriteGeneratedCodeAttribute(sb, "\t\t"); + sb.Append("\t\t"); + WriteClrType(sb, property.ClrTypeName); + sb.AppendFormat(" {0} {{ get; }}\n", + property.ClrName); + + if (property.Errors != null) + { + sb.Append("\n"); + sb.Append("\t\t *\n"); + sb.Append("\t\t */\n"); + } + } + + // internal for unit tests + internal void WriteClrType(StringBuilder sb, Type type) + { + var s = type.ToString(); + + if (type.IsGenericType) + { + var p = s.IndexOf('`'); + WriteNonGenericClrType(sb, s.Substring(0, p)); + sb.Append("<"); + var args = type.GetGenericArguments(); + for (var i = 0; i < args.Length; i++) + { + if (i > 0) sb.Append(", "); + WriteClrType(sb, args[i]); + } + sb.Append(">"); + } + else + { + WriteNonGenericClrType(sb, s); + } + } + + internal void WriteClrType(StringBuilder sb, string type) + { + var p = type.IndexOf('<'); + if (type.Contains('<')) + { + WriteNonGenericClrType(sb, type.Substring(0, p)); + sb.Append("<"); + var args = type.Substring(p + 1).TrimEnd('>').Split(','); // fixme will NOT work with nested generic types + for (var i = 0; i < args.Length; i++) + { + if (i > 0) sb.Append(", "); + WriteClrType(sb, args[i]); + } + sb.Append(">"); + } + else + { + WriteNonGenericClrType(sb, type); + } + } + + private void WriteNonGenericClrType(StringBuilder sb, string s) + { + // map model types + s = Regex.Replace(s, @"\{(.*)\}\[\*\]", m => ModelsMap[m.Groups[1].Value + "[]"]); + + // takes care eg of "System.Int32" vs. "int" + if (TypesMap.TryGetValue(s, out string typeName)) + { + sb.Append(typeName); + return; + } + + // if full type name matches a using clause, strip + // so if we want Umbraco.Core.Models.IPublishedContent + // and using Umbraco.Core.Models, then we just need IPublishedContent + typeName = s; + string typeUsing = null; + var p = typeName.LastIndexOf('.'); + if (p > 0) + { + var x = typeName.Substring(0, p); + if (Using.Contains(x)) + { + typeName = typeName.Substring(p + 1); + typeUsing = x; + } + else if (x == ModelsNamespace) // that one is used by default + { + typeName = typeName.Substring(p + 1); + typeUsing = ModelsNamespace; + } + } + + // nested types *after* using + typeName = typeName.Replace("+", "."); + + // symbol to test is the first part of the name + // so if type name is Foo.Bar.Nil we want to ensure that Foo is not ambiguous + p = typeName.IndexOf('.'); + var symbol = p > 0 ? typeName.Substring(0, p) : typeName; + + // what we should find - WITHOUT any generic thing - just the type + // no 'using' = the exact symbol + // a 'using' = using.symbol + var match = typeUsing == null ? symbol : (typeUsing + "." + symbol); + + // if not ambiguous, be happy + if (!IsAmbiguousSymbol(symbol, match)) + { + sb.Append(typeName); + return; + } + + // symbol is ambiguous + // if no 'using', must prepend global:: + if (typeUsing == null) + { + sb.Append("global::"); + sb.Append(s.Replace("+", ".")); + return; + } + + // could fullname be non-ambiguous? + // note: all-or-nothing, not trying to segment the using clause + typeName = s.Replace("+", "."); + p = typeName.IndexOf('.'); + symbol = typeName.Substring(0, p); + match = symbol; + + // still ambiguous, must prepend global:: + if (IsAmbiguousSymbol(symbol, match)) + sb.Append("global::"); + + sb.Append(typeName); + } + + private static string XmlCommentString(string s) + { + return s.Replace('<', '{').Replace('>', '}').Replace('\r', ' ').Replace('\n', ' '); + } + + private static readonly IDictionary TypesMap = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "System.Int16", "short" }, + { "System.Int32", "int" }, + { "System.Int64", "long" }, + { "System.String", "string" }, + { "System.Object", "object" }, + { "System.Boolean", "bool" }, + { "System.Void", "void" }, + { "System.Char", "char" }, + { "System.Byte", "byte" }, + { "System.UInt16", "ushort" }, + { "System.UInt32", "uint" }, + { "System.UInt64", "ulong" }, + { "System.SByte", "sbyte" }, + { "System.Single", "float" }, + { "System.Double", "double" }, + { "System.Decimal", "decimal" } + }; + } +} diff --git a/src/Umbraco.ModelsBuilder/Building/TextHeaderWriter.cs b/src/Umbraco.ModelsBuilder/Building/TextHeaderWriter.cs new file mode 100644 index 0000000000..d165f03907 --- /dev/null +++ b/src/Umbraco.ModelsBuilder/Building/TextHeaderWriter.cs @@ -0,0 +1,26 @@ +using System.Text; +using Umbraco.ModelsBuilder.Api; + +namespace Umbraco.ModelsBuilder.Building +{ + public static class TextHeaderWriter + { + /// + /// Outputs an "auto-generated" header to a string builder. + /// + /// The string builder. + public static void WriteHeader(StringBuilder sb) + { + sb.Append("//------------------------------------------------------------------------------\n"); + sb.Append("// \n"); + sb.Append("// This code was generated by a tool.\n"); + sb.Append("//\n"); + sb.AppendFormat("// Umbraco.ModelsBuilder v{0}\n", ApiVersion.Current.Version); + sb.Append("//\n"); + sb.Append("// Changes to this file will be lost if the code is regenerated.\n"); + sb.Append("// \n"); + sb.Append("//------------------------------------------------------------------------------\n"); + sb.Append("\n"); + } + } +} diff --git a/src/Umbraco.ModelsBuilder/Building/TypeModel.cs b/src/Umbraco.ModelsBuilder/Building/TypeModel.cs new file mode 100644 index 0000000000..941894774e --- /dev/null +++ b/src/Umbraco.ModelsBuilder/Building/TypeModel.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.ModelsBuilder.Building +{ + /// + /// Represents a model. + /// + public class TypeModel + { + /// + /// Gets the unique identifier of the corresponding content type. + /// + public int Id; + + /// + /// Gets the alias of the model. + /// + public string Alias; + + /// + /// Gets the name of the content type. + /// + public string Name; + + /// + /// Gets the description of the content type. + /// + public string Description; + + /// + /// Gets the clr name of the model. + /// + /// This is the complete name eg "Foo.Bar.MyContent". + public string ClrName; + + /// + /// Gets the unique identifier of the parent. + /// + /// The parent can either be a base content type, or a content types container. If the content + /// type does not have a base content type, then returns -1. + public int ParentId; + + /// + /// Gets the base model. + /// + /// + /// If the content type does not have a base content type, then returns null. + /// The current model inherits from its base model. + /// + public TypeModel BaseType; // the parent type in Umbraco (type inherits its properties) + + /// + /// Gets the list of properties that are defined by this model. + /// + /// These are only those property that are defined locally by this model, + /// and the list does not contain properties inherited from base models or from mixins. + public readonly List Properties = new List(); + + /// + /// Gets the mixin models. + /// + /// The current model implements mixins. + public readonly List MixinTypes = new List(); + + /// + /// Gets the list of interfaces that this model needs to declare it implements. + /// + /// Some of these interfaces may actually be implemented by a base model + /// that this model inherits from. + public readonly List DeclaringInterfaces = new List(); + + /// + /// Gets the list of interfaces that this model needs to actually implement. + /// + public readonly List ImplementingInterfaces = new List(); + + /// + /// Gets the list of existing static mixin method candidates. + /// + public readonly List StaticMixinMethods = new List(); + + /// + /// Gets a value indicating whether this model has a base class. + /// + /// Can be either because the content type has a base content type declared in Umbraco, + /// or because the existing user's code declares a base class for this model. + public bool HasBase; + + /// + /// Gets a value indicating whether this model is used as a mixin by another model. + /// + public bool IsMixin; + + /// + /// Gets a value indicating whether this model is the base model of another model. + /// + public bool IsParent; + + /// + /// Gets a value indicating whether the type is an element. + /// + public bool IsElement => ItemType == ItemTypes.Element; + + /// + /// Represents the different model item types. + /// + public enum ItemTypes + { + /// + /// Element. + /// + Element, + + /// + /// Content. + /// + Content, + + /// + /// Media. + /// + Media, + + /// + /// Member. + /// + Member + } + + private ItemTypes _itemType; + + /// + /// Gets or sets the model item type. + /// + public ItemTypes ItemType + { + get { return _itemType; } + set + { + switch (value) + { + case ItemTypes.Element: + case ItemTypes.Content: + case ItemTypes.Media: + case ItemTypes.Member: + _itemType = value; + break; + default: + throw new ArgumentException("value"); + } + } + } + + /// + /// Recursively collects all types inherited, or implemented as interfaces, by a specified type. + /// + /// The collection. + /// The type. + /// Includes the specified type. + internal static void CollectImplems(ICollection types, TypeModel type) + { + if (types.Contains(type) == false) + types.Add(type); + if (type.BaseType != null) + CollectImplems(types, type.BaseType); + foreach (var mixin in type.MixinTypes) + CollectImplems(types, mixin); + } + + /// + /// Enumerates the base models starting from the current model up. + /// + /// Indicates whether the enumeration should start with the current model + /// or from its base model. + /// The base models. + public IEnumerable EnumerateBaseTypes(bool andSelf = false) + { + var typeModel = andSelf ? this : BaseType; + while (typeModel != null) + { + yield return typeModel; + typeModel = typeModel.BaseType; + } + } + + /// + /// Maps ModelType. + /// + public static void MapModelTypes(IList typeModels, string ns) + { + var hasNs = !string.IsNullOrWhiteSpace(ns); + var map = typeModels.ToDictionary(x => x.Alias, x => hasNs ? (ns + "." + x.ClrName) : x.ClrName); + foreach (var typeModel in typeModels) + { + foreach (var propertyModel in typeModel.Properties) + { + propertyModel.ClrTypeName = ModelType.MapToName(propertyModel.ModelClrType, map); + } + } + } + } +} diff --git a/src/Umbraco.ModelsBuilder/ConfigsExtensions.cs b/src/Umbraco.ModelsBuilder/ConfigsExtensions.cs new file mode 100644 index 0000000000..dc0b136422 --- /dev/null +++ b/src/Umbraco.ModelsBuilder/ConfigsExtensions.cs @@ -0,0 +1,20 @@ +using Umbraco.Core.Configuration; +using Umbraco.ModelsBuilder.Configuration; + +namespace Umbraco.ModelsBuilder +{ + /// + /// Provides extension methods for the class. + /// + public static class ConfigsExtensions + { + /// + /// Gets the models builder configuration. + /// + /// Getting the models builder configuration freezes its state, + /// and any attempt at modifying the configuration using the Setup method + /// will be ignored. + public static Config ModelsBuilder(this Configs configs) + => configs.GetConfig(); + } +} \ No newline at end of file diff --git a/src/Umbraco.ModelsBuilder/Configuration/ClrNameSource.cs b/src/Umbraco.ModelsBuilder/Configuration/ClrNameSource.cs new file mode 100644 index 0000000000..d195846411 --- /dev/null +++ b/src/Umbraco.ModelsBuilder/Configuration/ClrNameSource.cs @@ -0,0 +1,28 @@ +namespace Umbraco.ModelsBuilder.Configuration +{ + /// + /// Defines the CLR name sources. + /// + public enum ClrNameSource + { + /// + /// No source. + /// + Nothing = 0, + + /// + /// Use the name as source. + /// + Name, + + /// + /// Use the alias as source. + /// + Alias, + + /// + /// Use the alias directly. + /// + RawAlias + } +} diff --git a/src/Umbraco.ModelsBuilder/Configuration/Config.cs b/src/Umbraco.ModelsBuilder/Configuration/Config.cs new file mode 100644 index 0000000000..8fdc10d5dc --- /dev/null +++ b/src/Umbraco.ModelsBuilder/Configuration/Config.cs @@ -0,0 +1,224 @@ +using System; +using System.Configuration; +using System.IO; +using System.Web.Configuration; +using System.Web.Hosting; +using Umbraco.Core; + +namespace Umbraco.ModelsBuilder.Configuration +{ + /// + /// Represents the models builder configuration. + /// + public class Config + { + internal const string DefaultModelsNamespace = "Umbraco.Web.PublishedModels"; + internal const string DefaultModelsDirectory = "~/App_Data/Models"; + + /// + /// Initializes a new instance of the class. + /// + public Config() + { + const string prefix = "Umbraco.ModelsBuilder."; + + // giant kill switch, default: false + // must be explicitely set to true for anything else to happen + Enable = ConfigurationManager.AppSettings[prefix + "Enable"] == "true"; + + // ensure defaults are initialized for tests + ModelsNamespace = DefaultModelsNamespace; + ModelsDirectory = HostingEnvironment.IsHosted + ? HostingEnvironment.MapPath(DefaultModelsDirectory) + : DefaultModelsDirectory.TrimStart("~/"); + DebugLevel = 0; + + // stop here, everything is false + if (!Enable) return; + + // mode + var modelsMode = ConfigurationManager.AppSettings[prefix + "ModelsMode"]; + if (!string.IsNullOrWhiteSpace(modelsMode)) + { + switch (modelsMode) + { + case nameof(ModelsMode.Nothing): + ModelsMode = ModelsMode.Nothing; + break; + case nameof(ModelsMode.PureLive): + ModelsMode = ModelsMode.PureLive; + break; + case nameof(ModelsMode.AppData): + ModelsMode = ModelsMode.AppData; + break; + case nameof(ModelsMode.LiveAppData): + ModelsMode = ModelsMode.LiveAppData; + break; + default: + throw new ConfigurationErrorsException($"ModelsMode \"{modelsMode}\" is not a valid mode." + + " Note that modes are case-sensitive. Possible values are: " + string.Join(", ", Enum.GetNames(typeof(ModelsMode)))); + } + } + + // default: false + AcceptUnsafeModelsDirectory = ConfigurationManager.AppSettings[prefix + "AcceptUnsafeModelsDirectory"].InvariantEquals("true"); + + // default: true + EnableFactory = !ConfigurationManager.AppSettings[prefix + "EnableFactory"].InvariantEquals("false"); + FlagOutOfDateModels = !ConfigurationManager.AppSettings[prefix + "FlagOutOfDateModels"].InvariantEquals("false"); + + // default: initialized above with DefaultModelsNamespace const + var value = ConfigurationManager.AppSettings[prefix + "ModelsNamespace"]; + if (!string.IsNullOrWhiteSpace(value)) + ModelsNamespace = value; + + // default: initialized above with DefaultModelsDirectory const + value = ConfigurationManager.AppSettings[prefix + "ModelsDirectory"]; + if (!string.IsNullOrWhiteSpace(value)) + { + var root = HostingEnvironment.IsHosted + ? HostingEnvironment.MapPath("~/") + : Directory.GetCurrentDirectory(); + if (root == null) + throw new ConfigurationErrorsException("Could not determine root directory."); + + // GetModelsDirectory will ensure that the path is safe + ModelsDirectory = GetModelsDirectory(root, value, AcceptUnsafeModelsDirectory); + } + + // default: 0 + value = ConfigurationManager.AppSettings[prefix + "DebugLevel"]; + if (!string.IsNullOrWhiteSpace(value)) + { + int debugLevel; + if (!int.TryParse(value, out debugLevel)) + throw new ConfigurationErrorsException($"Invalid debug level \"{value}\"."); + DebugLevel = debugLevel; + } + + // not flagging if not generating, or live (incl. pure) + if (ModelsMode == ModelsMode.Nothing || ModelsMode.IsLive()) + FlagOutOfDateModels = false; + } + + /// + /// Initializes a new instance of the class. + /// + public Config( + bool enable = false, + ModelsMode modelsMode = ModelsMode.Nothing, + string modelsNamespace = null, + bool enableFactory = true, + bool flagOutOfDateModels = true, + string modelsDirectory = null, + bool acceptUnsafeModelsDirectory = false, + int debugLevel = 0) + { + Enable = enable; + ModelsMode = modelsMode; + + ModelsNamespace = string.IsNullOrWhiteSpace(modelsNamespace) ? DefaultModelsNamespace : modelsNamespace; + EnableFactory = enableFactory; + FlagOutOfDateModels = flagOutOfDateModels; + ModelsDirectory = string.IsNullOrWhiteSpace(modelsDirectory) ? DefaultModelsDirectory : modelsDirectory; + AcceptUnsafeModelsDirectory = acceptUnsafeModelsDirectory; + DebugLevel = debugLevel; + } + + // internal for tests + internal static string GetModelsDirectory(string root, string config, bool acceptUnsafe) + { + // making sure it is safe, ie under the website root, + // unless AcceptUnsafeModelsDirectory and then everything is OK. + + if (!Path.IsPathRooted(root)) + throw new ConfigurationErrorsException($"Root is not rooted \"{root}\"."); + + if (config.StartsWith("~/")) + { + var dir = Path.Combine(root, config.TrimStart("~/")); + + // sanitize - GetFullPath will take care of any relative + // segments in path, eg '../../foo.tmp' - it may throw a SecurityException + // if the combined path reaches illegal parts of the filesystem + dir = Path.GetFullPath(dir); + root = Path.GetFullPath(root); + + if (!dir.StartsWith(root) && !acceptUnsafe) + throw new ConfigurationErrorsException($"Invalid models directory \"{config}\"."); + + return dir; + } + + if (acceptUnsafe) + return Path.GetFullPath(config); + + throw new ConfigurationErrorsException($"Invalid models directory \"{config}\"."); + } + + /// + /// Gets a value indicating whether the whole models experience is enabled. + /// + /// + /// If this is false then absolutely nothing happens. + /// Default value is false which means that unless we have this setting, nothing happens. + /// + public bool Enable { get; } + + /// + /// Gets the models mode. + /// + public ModelsMode ModelsMode { get; } + + /// + /// Gets a value indicating whether system.web/compilation/@debug is true. + /// + public bool IsDebug + { + get + { + var section = (CompilationSection) ConfigurationManager.GetSection("system.web/compilation"); + return section != null && section.Debug; + } + } + + /// + /// Gets the models namespace. + /// + /// That value could be overriden by other (attribute in user's code...). Return default if no value was supplied. + public string ModelsNamespace { get; } + + /// + /// Gets a value indicating whether we should enable the models factory. + /// + /// Default value is true because no factory is enabled by default in Umbraco. + public bool EnableFactory { get; } + + /// + /// Gets a value indicating whether we should flag out-of-date models. + /// + /// Models become out-of-date when data types or content types are updated. When this + /// setting is activated the ~/App_Data/Models/ood.txt file is then created. When models are + /// generated through the dashboard, the files is cleared. Default value is false. + public bool FlagOutOfDateModels { get; } + + /// + /// Gets the models directory. + /// + /// Default is ~/App_Data/Models but that can be changed. + public string ModelsDirectory { get; } + + /// + /// Gets a value indicating whether to accept an unsafe value for ModelsDirectory. + /// + /// An unsafe value is an absolute path, or a relative path pointing outside + /// of the website root. + public bool AcceptUnsafeModelsDirectory { get; } + + /// + /// Gets a value indicating the debug log level. + /// + /// 0 means minimal (safe on live site), anything else means more and more details (maybe not safe). + public int DebugLevel { get; } + } +} diff --git a/src/Umbraco.ModelsBuilder/Configuration/ModelsMode.cs b/src/Umbraco.ModelsBuilder/Configuration/ModelsMode.cs new file mode 100644 index 0000000000..cc36099bc5 --- /dev/null +++ b/src/Umbraco.ModelsBuilder/Configuration/ModelsMode.cs @@ -0,0 +1,36 @@ +namespace Umbraco.ModelsBuilder.Configuration +{ + /// + /// Defines the models generation modes. + /// + public enum ModelsMode + { + /// + /// Do not generate models. + /// + Nothing = 0, // default value + + /// + /// Generate models in memory. + /// When: a content type change occurs. + /// + /// The app does not restart. Models are available in views exclusively. + PureLive, + + /// + /// Generate models in AppData. + /// When: generation is triggered. + /// + /// Generation can be triggered from the dashboard. The app does not restart. + /// Models are not compiled and thus are not available to the project. + AppData, + + /// + /// Generate models in AppData. + /// When: a content type change occurs, or generation is triggered. + /// + /// Generation can be triggered from the dashboard. The app does not restart. + /// Models are not compiled and thus are not available to the project. + LiveAppData + } +} diff --git a/src/Umbraco.ModelsBuilder/Configuration/ModelsModeExtensions.cs b/src/Umbraco.ModelsBuilder/Configuration/ModelsModeExtensions.cs new file mode 100644 index 0000000000..f40d7973b8 --- /dev/null +++ b/src/Umbraco.ModelsBuilder/Configuration/ModelsModeExtensions.cs @@ -0,0 +1,37 @@ +namespace Umbraco.ModelsBuilder.Configuration +{ + /// + /// Provides extensions for the enumeration. + /// + public static class ModelsModeExtensions + { + /// + /// Gets a value indicating whether the mode is LiveAnything or PureLive. + /// + public static bool IsLive(this ModelsMode modelsMode) + { + return + modelsMode == ModelsMode.PureLive + || modelsMode == ModelsMode.LiveAppData; + } + + /// + /// Gets a value indicating whether the mode is LiveAnything but not PureLive. + /// + public static bool IsLiveNotPure(this ModelsMode modelsMode) + { + return + modelsMode == ModelsMode.LiveAppData; + } + + /// + /// Gets a value indicating whether the mode supports explicit generation (as opposed to pure live). + /// + public static bool SupportsExplicitGeneration(this ModelsMode modelsMode) + { + return + modelsMode == ModelsMode.AppData + || modelsMode == ModelsMode.LiveAppData; + } + } +} diff --git a/src/Umbraco.ModelsBuilder/Dashboard/BuilderDashboardHelper.cs b/src/Umbraco.ModelsBuilder/Dashboard/BuilderDashboardHelper.cs new file mode 100644 index 0000000000..75c318faab --- /dev/null +++ b/src/Umbraco.ModelsBuilder/Dashboard/BuilderDashboardHelper.cs @@ -0,0 +1,65 @@ +using System.Text; +using Umbraco.Core.Composing; +using Umbraco.ModelsBuilder.Configuration; +using Umbraco.ModelsBuilder.Umbraco; + +namespace Umbraco.ModelsBuilder.Dashboard +{ + internal static class BuilderDashboardHelper + { + private static Config Config => Current.Configs.ModelsBuilder(); + + public static bool CanGenerate() + { + return Config.ModelsMode.SupportsExplicitGeneration(); + } + + public static bool AreModelsOutOfDate() + { + return OutOfDateModelsStatus.IsOutOfDate; + } + + public static string LastError() + { + return ModelsGenerationError.GetLastError(); + } + + public static string Text() + { + var config = Config; + + if (!config.Enable) + return "Version: " + Api.ApiVersion.Current.Version + "
 
ModelsBuilder is disabled
(the .Enable key is missing, or its value is not 'true')."; + + var sb = new StringBuilder(); + + sb.Append("Version: "); + sb.Append(Api.ApiVersion.Current.Version); + sb.Append("
 
"); + + sb.Append("ModelsBuilder is enabled, with the following configuration:"); + + sb.Append("
    "); + + sb.Append("
  • The models factory is "); + sb.Append(config.EnableFactory || config.ModelsMode == ModelsMode.PureLive + ? "enabled" + : "not enabled. Umbraco will not use models"); + sb.Append(".
  • "); + + sb.Append(config.ModelsMode != ModelsMode.Nothing + ? $"
  • {config.ModelsMode} models are enabled.
  • " + : "
  • No models mode is specified: models will not be generated.
  • "); + + sb.Append($"
  • Models namespace is {config.ModelsNamespace}.
  • "); + + sb.Append("
  • Tracking of out-of-date models is "); + sb.Append(config.FlagOutOfDateModels ? "enabled" : "not enabled"); + sb.Append(".
  • "); + + sb.Append("
"); + + return sb.ToString(); + } + } +} diff --git a/src/Umbraco.ModelsBuilder/EnumerableExtensions.cs b/src/Umbraco.ModelsBuilder/EnumerableExtensions.cs new file mode 100644 index 0000000000..da77bfa958 --- /dev/null +++ b/src/Umbraco.ModelsBuilder/EnumerableExtensions.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; + +namespace Umbraco.ModelsBuilder +{ + public static class EnumerableExtensions + { + public static void RemoveAll(this IList list, Func predicate) + { + for (var i = 0; i < list.Count; i++) + { + if (predicate(list[i])) + { + list.RemoveAt(i--); // i-- is important here! + } + } + } + + public static IEnumerable And(this IEnumerable enumerable, T item) + { + foreach (var x in enumerable) yield return x; + yield return item; + } + + public static IEnumerable AndIfNotNull(this IEnumerable enumerable, T item) + where T : class + { + foreach (var x in enumerable) yield return x; + if (item != null) + yield return item; + } + } +} diff --git a/src/Umbraco.ModelsBuilder/ImplementPropertyTypeAttribute.cs b/src/Umbraco.ModelsBuilder/ImplementPropertyTypeAttribute.cs new file mode 100644 index 0000000000..a3dba740c1 --- /dev/null +++ b/src/Umbraco.ModelsBuilder/ImplementPropertyTypeAttribute.cs @@ -0,0 +1,19 @@ +using System; + +namespace Umbraco.ModelsBuilder +{ + /// + /// Indicates that a property implements a given property alias. + /// + /// And therefore it should not be generated. + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + public sealed class ImplementPropertyTypeAttribute : Attribute + { + public ImplementPropertyTypeAttribute(string alias) + { + Alias = alias; + } + + public string Alias { get; private set; } + } +} diff --git a/src/Umbraco.ModelsBuilder/ModelsBuilderAssemblyAttribute.cs b/src/Umbraco.ModelsBuilder/ModelsBuilderAssemblyAttribute.cs new file mode 100644 index 0000000000..ed956852f8 --- /dev/null +++ b/src/Umbraco.ModelsBuilder/ModelsBuilderAssemblyAttribute.cs @@ -0,0 +1,23 @@ +using System; + +namespace Umbraco.ModelsBuilder +{ + /// + /// Indicates that an Assembly is a Models Builder assembly. + /// + [AttributeUsage(AttributeTargets.Assembly /*, AllowMultiple = false, Inherited = false*/)] + public sealed class ModelsBuilderAssemblyAttribute : Attribute + { + /// + /// Gets or sets a value indicating whether the assembly is a PureLive assembly. + /// + /// A Models Builder assembly can be either PureLive or normal Dll. + public bool PureLive { get; set; } + + /// + /// Gets or sets a hash value representing the state of the custom source code files + /// and the Umbraco content types that were used to generate and compile the assembly. + /// + public string SourceHash { get; set; } + } +} diff --git a/src/Umbraco.ModelsBuilder/Properties/AssemblyInfo.cs b/src/Umbraco.ModelsBuilder/Properties/AssemblyInfo.cs index 8ec221bffb..b576807bb7 100644 --- a/src/Umbraco.ModelsBuilder/Properties/AssemblyInfo.cs +++ b/src/Umbraco.ModelsBuilder/Properties/AssemblyInfo.cs @@ -1,36 +1,10 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. [assembly: AssemblyTitle("Umbraco.ModelsBuilder")] -[assembly: AssemblyDescription("")] +[assembly: AssemblyDescription("Umbraco ModelsBuilder")] [assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Umbraco.ModelsBuilder")] -[assembly: AssemblyCopyright("Copyright © 2019")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] +[assembly: AssemblyProduct("Umbraco CMS")] -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("52ac0ba8-a60e-4e36-897b-e8b97a54ed1c")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Umbraco.ModelsBuilder/PublishedElementExtensions.cs b/src/Umbraco.ModelsBuilder/PublishedElementExtensions.cs new file mode 100644 index 0000000000..1211c4fdad --- /dev/null +++ b/src/Umbraco.ModelsBuilder/PublishedElementExtensions.cs @@ -0,0 +1,51 @@ +using System; +using System.Linq.Expressions; +using System.Reflection; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.ModelsBuilder; + +// same namespace as original Umbraco.Web PublishedElementExtensions +// ReSharper disable once CheckNamespace +namespace Umbraco.Web +{ + /// + /// Provides extension methods to models. + /// + public static class PublishedElementExtensions + { + /// + /// Gets the value of a property. + /// + public static TValue Value(this TModel model, Expression> property, string culture = null, string segment = null, Fallback fallback = default, TValue defaultValue = default) + where TModel : IPublishedElement + { + var alias = GetAlias(model, property); + return model.Value(alias, culture, segment, fallback, defaultValue); + } + + // fixme that one should be public so ppl can use it + private static string GetAlias(TModel model, Expression> property) + { + if (property.NodeType != ExpressionType.Lambda) + throw new ArgumentException("Not a proper lambda expression (lambda).", nameof(property)); + + var lambda = (LambdaExpression) property; + var lambdaBody = lambda.Body; + + if (lambdaBody.NodeType != ExpressionType.MemberAccess) + throw new ArgumentException("Not a proper lambda expression (body).", nameof(property)); + + var memberExpression = (MemberExpression) lambdaBody; + if (memberExpression.Expression.NodeType != ExpressionType.Parameter) + throw new ArgumentException("Not a proper lambda expression (member).", nameof(property)); + + var member = memberExpression.Member; + + var attribute = member.GetCustomAttribute(); + if (attribute == null) + throw new InvalidOperationException("Property is not marked with ImplementPropertyType attribute."); + + return attribute.Alias; + } + } +} diff --git a/src/Umbraco.ModelsBuilder/PureLiveAssemblyAttribute.cs b/src/Umbraco.ModelsBuilder/PureLiveAssemblyAttribute.cs new file mode 100644 index 0000000000..dfe369dc21 --- /dev/null +++ b/src/Umbraco.ModelsBuilder/PureLiveAssemblyAttribute.cs @@ -0,0 +1,15 @@ +using System; + +namespace Umbraco.ModelsBuilder +{ + /// + /// Indicates that an Assembly is a PureLive models assembly. + /// + /// Though technically not required, ie models will work without it, the attribute + /// can be used by Umbraco view models binder to figure out whether the model type comes + /// from a PureLive Assembly. + [Obsolete("Should use ModelsBuilderAssemblyAttribute but that requires a change in Umbraco Core.")] + [AttributeUsage(AttributeTargets.Assembly /*, AllowMultiple = false, Inherited = false*/)] + public sealed class PureLiveAssemblyAttribute : Attribute + { } +} diff --git a/src/Umbraco.ModelsBuilder/ReferencedAssemblies.cs b/src/Umbraco.ModelsBuilder/ReferencedAssemblies.cs new file mode 100644 index 0000000000..e0a20eb9d6 --- /dev/null +++ b/src/Umbraco.ModelsBuilder/ReferencedAssemblies.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Web.Compilation; +using System.Web.Hosting; +using Umbraco.Core; + +namespace Umbraco.ModelsBuilder +{ + internal static class ReferencedAssemblies + { + private static readonly Lazy> LazyLocations; + + static ReferencedAssemblies() + { + LazyLocations = new Lazy>(() => HostingEnvironment.IsHosted + ? GetAllReferencedAssembliesLocationFromBuildManager() + : GetAllReferencedAssembliesFromDomain()); + } + + /// + /// Gets the assembly locations of all the referenced assemblies, that + /// are not dynamic, and have a non-null nor empty location. + /// + public static IEnumerable Locations => LazyLocations.Value; + + public static Assembly GetNetStandardAssembly(List assemblies) + { + if (assemblies == null) + assemblies = BuildManager.GetReferencedAssemblies().Cast().ToList(); + + // for some reason, netstandard is also missing from BuildManager.ReferencedAssemblies and yet, is part of + // the references that CSharpCompiler (above) receives - where is it coming from? cannot figure it out + try + { + // so, resorting to an ugly trick + // we should have System.Reflection.Metadata around, and it should reference netstandard + var someAssembly = assemblies.First(x => x.FullName.StartsWith("System.Reflection.Metadata,")); + var netStandardAssemblyName = someAssembly.GetReferencedAssemblies().First(x => x.FullName.StartsWith("netstandard,")); + var netStandard = Assembly.Load(netStandardAssemblyName.FullName); + return netStandard; + } + catch { /* never mind */ } + + return null; + } + + public static Assembly GetNetStandardAssembly() + { + // in PreApplicationStartMethod we cannot get BuildManager.Referenced assemblies, do it differently + try + { + var someAssembly = Assembly.Load("System.Reflection.Metadata"); + var netStandardAssemblyName = someAssembly.GetReferencedAssemblies().First(x => x.FullName.StartsWith("netstandard,")); + var netStandard = Assembly.Load(netStandardAssemblyName.FullName); + return netStandard; + } + catch { /* never mind */ } + + return null; + } + + // hosted, get referenced assemblies from the BuildManager and filter + private static IEnumerable GetAllReferencedAssembliesLocationFromBuildManager() + { + var assemblies = BuildManager.GetReferencedAssemblies().Cast().ToList(); + + assemblies.Add(typeof(ReferencedAssemblies).Assembly); // always include ourselves + + // see https://github.com/aspnet/RoslynCodeDomProvider/blob/master/src/Microsoft.CodeDom.Providers.DotNetCompilerPlatform/CSharpCompiler.cs: + // mentions "Bug 913691: Explicitly add System.Runtime as a reference." + // and explicitly adds System.Runtime to references before invoking csc.exe + // so, doing the same here + try + { + var systemRuntime = Assembly.Load("System.Runtime, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + assemblies.Add(systemRuntime); + } + catch { /* never mind */ } + + // for some reason, netstandard is also missing from BuildManager.ReferencedAssemblies and yet, is part of + // the references that CSharpCompiler (above) receives - where is it coming from? cannot figure it out + var netStandard = GetNetStandardAssembly(assemblies); + if (netStandard != null) assemblies.Add(netStandard); + + return assemblies + .Where(x => !x.IsDynamic && !x.Location.IsNullOrWhiteSpace()) + .Select(x => x.Location) + .Distinct() + .ToList(); + } + + // non-hosted, do our best + private static IEnumerable GetAllReferencedAssembliesFromDomain() + { + //TODO: This method has bugs since I've been stuck in an infinite loop with it, though this shouldn't + // execute while in the web application anyways. + + var assemblies = new List(); + var tmp1 = new List(); + var failed = new List(); + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies() + .Where(x => x.IsDynamic == false) + .Where(x => !string.IsNullOrWhiteSpace(x.Location))) // though... IsDynamic should be enough? + { + assemblies.Add(assembly); + tmp1.Add(assembly); + } + + // fixme - AssemblyUtility questions + // - should we also load everything that's in the same directory? + // - do we want to load in the current app domain? + // - if this runs within Umbraco then we have already loaded them all? + + while (tmp1.Count > 0) + { + var tmp2 = tmp1 + .SelectMany(x => x.GetReferencedAssemblies()) + .Distinct() + .Where(x => assemblies.All(xx => x.FullName != xx.FullName)) // we don't have it already + .Where(x => failed.All(xx => x.FullName != xx.FullName)) // it hasn't failed already + .ToArray(); + tmp1.Clear(); + foreach (var assemblyName in tmp2) + { + try + { + var assembly = AppDomain.CurrentDomain.Load(assemblyName); + assemblies.Add(assembly); + tmp1.Add(assembly); + } + catch + { + failed.Add(assemblyName); + } + } + } + return assemblies.Select(x => x.Location).Distinct(); + } + + + // ---- + + private static IEnumerable GetDeepReferencedAssemblies(Assembly assembly) + { + var visiting = new Stack(); + var visited = new HashSet(); + + visiting.Push(assembly); + visited.Add(assembly); + while (visiting.Count > 0) + { + var visAsm = visiting.Pop(); + foreach (var refAsm in visAsm.GetReferencedAssemblies() + .Select(TryLoad) + .Where(x => x != null && visited.Contains(x) == false)) + { + yield return refAsm; + visiting.Push(refAsm); + visited.Add(refAsm); + } + } + } + + private static Assembly TryLoad(AssemblyName name) + { + try + { + return AppDomain.CurrentDomain.Load(name); + } + catch (Exception) + { + //Console.WriteLine(name); + return null; + } + } + } +} diff --git a/src/Umbraco.ModelsBuilder/TypeExtensions.cs b/src/Umbraco.ModelsBuilder/TypeExtensions.cs new file mode 100644 index 0000000000..d3b3ff6b4e --- /dev/null +++ b/src/Umbraco.ModelsBuilder/TypeExtensions.cs @@ -0,0 +1,22 @@ +using System; + +namespace Umbraco.ModelsBuilder +{ + internal static class TypeExtensions + { + /// + /// Creates a generic instance of a generic type with the proper actual type of an object. + /// + /// A generic type such as Something{} + /// An object whose type is used as generic type param. + /// Arguments for the constructor. + /// A generic instance of the generic type with the proper type. + /// Usage... typeof (Something{}).CreateGenericInstance(object1, object2, object3) will return + /// a Something{Type1} if object1.GetType() is Type1. + public static object CreateGenericInstance(this Type genericType, object typeParmObj, params object[] ctorArgs) + { + var type = genericType.MakeGenericType(typeParmObj.GetType()); + return Activator.CreateInstance(type, ctorArgs); + } + } +} diff --git a/src/Umbraco.ModelsBuilder/Umbraco.ModelsBuilder.csproj b/src/Umbraco.ModelsBuilder/Umbraco.ModelsBuilder.csproj index d1d3ca205d..5b0272a526 100644 --- a/src/Umbraco.ModelsBuilder/Umbraco.ModelsBuilder.csproj +++ b/src/Umbraco.ModelsBuilder/Umbraco.ModelsBuilder.csproj @@ -32,7 +32,10 @@ + + + @@ -41,7 +44,57 @@ + + Properties\SolutionInfo.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {31785BC3-256C-4613-B2F5-A1B0BDDED8C1} + Umbraco.Core + + + {651e1350-91b6-44b7-bd60-7207006d7003} + Umbraco.Web + + + + + 5.2.7 + \ No newline at end of file diff --git a/src/Umbraco.ModelsBuilder/Umbraco/HashCombiner.cs b/src/Umbraco.ModelsBuilder/Umbraco/HashCombiner.cs new file mode 100644 index 0000000000..e11662eb24 --- /dev/null +++ b/src/Umbraco.ModelsBuilder/Umbraco/HashCombiner.cs @@ -0,0 +1,38 @@ +using System; +using System.Globalization; + +namespace Umbraco.ModelsBuilder.Umbraco +{ + // because, of course, it's internal in Umbraco + // see also System.Web.Util.HashCodeCombiner + class HashCombiner + { + private long _combinedHash = 5381L; + + public void Add(int i) + { + _combinedHash = ((_combinedHash << 5) + _combinedHash) ^ i; + } + + public void Add(object o) + { + Add(o.GetHashCode()); + } + + public void Add(DateTime d) + { + Add(d.GetHashCode()); + } + + public void Add(string s) + { + if (s == null) return; + Add((StringComparer.InvariantCulture).GetHashCode(s)); + } + + public string GetCombinedHashCode() + { + return _combinedHash.ToString("x", CultureInfo.InvariantCulture); + } + } +} diff --git a/src/Umbraco.ModelsBuilder/Umbraco/HashHelper.cs b/src/Umbraco.ModelsBuilder/Umbraco/HashHelper.cs new file mode 100644 index 0000000000..e4a0705ec0 --- /dev/null +++ b/src/Umbraco.ModelsBuilder/Umbraco/HashHelper.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Linq; +using Umbraco.ModelsBuilder.Building; + +namespace Umbraco.ModelsBuilder.Umbraco +{ + class HashHelper + { + public static string Hash(IEnumerable typeModels) + { + var hash = new HashCombiner(); + + // see Umbraco.ModelsBuilder.Umbraco.Application for what's important to hash + // ie what comes from Umbraco (not computed by ModelsBuilder) and makes a difference + + foreach (var typeModel in typeModels.OrderBy(x => x.Alias)) + { + hash.Add("--- CONTENT TYPE MODEL ---"); + hash.Add(typeModel.Id); + hash.Add(typeModel.Alias); + hash.Add(typeModel.ClrName); + hash.Add(typeModel.ParentId); + hash.Add(typeModel.Name); + hash.Add(typeModel.Description); + hash.Add(typeModel.ItemType.ToString()); + hash.Add("MIXINS:" + string.Join(",", typeModel.MixinTypes.OrderBy(x => x.Id).Select(x => x.Id))); + + foreach (var prop in typeModel.Properties.OrderBy(x => x.Alias)) + { + hash.Add("--- PROPERTY ---"); + hash.Add(prop.Alias); + hash.Add(prop.ClrName); + hash.Add(prop.Name); + hash.Add(prop.Description); + hash.Add(prop.ModelClrType.ToString()); // see ModelType tests, want ToString() not FullName + } + } + + return hash.GetCombinedHashCode(); + } + } +} diff --git a/src/Umbraco.ModelsBuilder/Umbraco/LiveModelsProvider.cs b/src/Umbraco.ModelsBuilder/Umbraco/LiveModelsProvider.cs new file mode 100644 index 0000000000..194e7de695 --- /dev/null +++ b/src/Umbraco.ModelsBuilder/Umbraco/LiveModelsProvider.cs @@ -0,0 +1,133 @@ +using System; +using System.Threading; +using System.Web; +using System.Web.Hosting; +using Umbraco.Core.Composing; +using Umbraco.Core.Logging; +using Umbraco.ModelsBuilder.Configuration; +using Umbraco.ModelsBuilder.Umbraco; +using Umbraco.Web.Cache; + +// will install only if configuration says it needs to be installed +[assembly: PreApplicationStartMethod(typeof(LiveModelsProviderModule), "Install")] + +namespace Umbraco.ModelsBuilder.Umbraco +{ + // supports LiveDll and LiveAppData - but not PureLive + public sealed class LiveModelsProvider + { + private static UmbracoServices _umbracoServices; + private static Mutex _mutex; + private static int _req; + + private static Config Config => Current.Configs.ModelsBuilder(); + + // we do not manage pure live here + internal static bool IsEnabled => Config.ModelsMode.IsLiveNotPure(); + + internal static void Install(UmbracoServices umbracoServices) + { + // just be sure + if (!IsEnabled) + return; + + _umbracoServices = umbracoServices; + + // initialize mutex + // ApplicationId will look like "/LM/W3SVC/1/Root/AppName" + // name is system-wide and must be less than 260 chars + var name = HostingEnvironment.ApplicationID + "/UmbracoLiveModelsProvider"; + _mutex = new Mutex(false, name); + + // anything changes, and we want to re-generate models. + ContentTypeCacheRefresher.CacheUpdated += RequestModelsGeneration; + DataTypeCacheRefresher.CacheUpdated += RequestModelsGeneration; + + // at the end of a request since we're restarting the pool + // NOTE - this does NOT trigger - see module below + //umbracoApplication.EndRequest += GenerateModelsIfRequested; + } + + // NOTE + // Using HttpContext Items fails because CacheUpdated triggers within + // some asynchronous backend task where we seem to have no HttpContext. + + // So we use a static (non request-bound) var to register that models + // need to be generated. Could be by another request. Anyway. We could + // have collisions but... you know the risk. + + private static void RequestModelsGeneration(object sender, EventArgs args) + { + //HttpContext.Current.Items[this] = true; + Current.Logger.Debug("Requested to generate models."); + Interlocked.Exchange(ref _req, 1); + } + + public static void GenerateModelsIfRequested(object sender, EventArgs args) + { + //if (HttpContext.Current.Items[this] == null) return; + if (Interlocked.Exchange(ref _req, 0) == 0) return; + + // cannot use a simple lock here because we don't want another AppDomain + // to generate while we do... and there could be 2 AppDomains if the app restarts. + + try + { + Current.Logger.Debug("Generate models..."); + const int timeout = 2*60*1000; // 2 mins + _mutex.WaitOne(timeout); // wait until it is safe, and acquire + Current.Logger.Info("Generate models now."); + GenerateModels(); + ModelsGenerationError.Clear(); + Current.Logger.Info("Generated."); + } + catch (TimeoutException) + { + Current.Logger.Warn("Timeout, models were NOT generated."); + } + catch (Exception e) + { + ModelsGenerationError.Report("Failed to build Live models.", e); + Current.Logger.Error("Failed to generate models.", e); + } + finally + { + _mutex.ReleaseMutex(); // release + } + } + + private static void GenerateModels() + { + var modelsDirectory = Config.ModelsDirectory; + var modelsNamespace = Config.ModelsNamespace; + + var bin = HostingEnvironment.MapPath("~/bin"); + if (bin == null) + throw new Exception("Panic: bin is null."); + + // EnableDllModels will recycle the app domain - but this request will end properly + ModelsBuilderBackOfficeController.GenerateModels(_umbracoServices, modelsDirectory, modelsNamespace); + } + } + + // have to do this because it's the only way to subscribe to EndRequest, + // module is installed by assembly attribute at the top of this file + public class LiveModelsProviderModule : IHttpModule + { + public void Init(HttpApplication app) + { + app.EndRequest += LiveModelsProvider.GenerateModelsIfRequested; + } + + public void Dispose() + { + // nothing + } + + public static void Install() + { + // always - don't read config in PreApplicationStartMethod + HttpApplication.RegisterModule(typeof(LiveModelsProviderModule)); + } + } +} diff --git a/src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderBackOfficeController.cs b/src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderBackOfficeController.cs new file mode 100644 index 0000000000..b1e6fbb1fb --- /dev/null +++ b/src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderBackOfficeController.cs @@ -0,0 +1,184 @@ +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Runtime.Serialization; +using System.Text; +using System.Web.Hosting; +using Umbraco.ModelsBuilder.Building; +using Umbraco.ModelsBuilder.Configuration; +using Umbraco.ModelsBuilder.Dashboard; +using Umbraco.Web.Editors; +using Umbraco.Web.WebApi.Filters; + +namespace Umbraco.ModelsBuilder.Umbraco +{ + /// + /// API controller for use in the Umbraco back office with Angular resources + /// + /// + /// We've created a different controller for the backoffice/angular specifically this is to ensure that the + /// correct CSRF security is adhered to for angular and it also ensures that this controller is not subseptipal to + /// global WebApi formatters being changed since this is always forced to only return Angular JSON Specific formats. + /// + [UmbracoApplicationAuthorize(Core.Constants.Applications.Settings)] + public class ModelsBuilderBackOfficeController : UmbracoAuthorizedJsonController + { + private readonly UmbracoServices _umbracoServices; + private readonly Config _config; + + public ModelsBuilderBackOfficeController(UmbracoServices umbracoServices, Config config) + { + _umbracoServices = umbracoServices; + _config = config; + } + + // invoked by the dashboard + // requires that the user is logged into the backoffice and has access to the settings section + // beware! the name of the method appears in modelsbuilder.controller.js + [System.Web.Http.HttpPost] // use the http one, not mvc, with api controllers! + public HttpResponseMessage BuildModels() + { + try + { + var config = _config; + + if (!config.ModelsMode.SupportsExplicitGeneration()) + { + var result2 = new BuildResult { Success = false, Message = "Models generation is not enabled." }; + return Request.CreateResponse(HttpStatusCode.OK, result2, Configuration.Formatters.JsonFormatter); + } + + var modelsDirectory = config.ModelsDirectory; + + var bin = HostingEnvironment.MapPath("~/bin"); + if (bin == null) + throw new Exception("Panic: bin is null."); + + // EnableDllModels will recycle the app domain - but this request will end properly + GenerateModels(modelsDirectory); + + ModelsGenerationError.Clear(); + } + catch (Exception e) + { + ModelsGenerationError.Report("Failed to build models.", e); + } + + return Request.CreateResponse(HttpStatusCode.OK, GetDashboardResult(), Configuration.Formatters.JsonFormatter); + } + + // invoked by the back-office + // requires that the user is logged into the backoffice and has access to the settings section + [System.Web.Http.HttpGet] // use the http one, not mvc, with api controllers! + public HttpResponseMessage GetModelsOutOfDateStatus() + { + var status = OutOfDateModelsStatus.IsEnabled + ? (OutOfDateModelsStatus.IsOutOfDate + ? new OutOfDateStatus { Status = OutOfDateType.OutOfDate } + : new OutOfDateStatus { Status = OutOfDateType.Current }) + : new OutOfDateStatus { Status = OutOfDateType.Unknown }; + + return Request.CreateResponse(HttpStatusCode.OK, status, Configuration.Formatters.JsonFormatter); + } + + // invoked by the back-office + // requires that the user is logged into the backoffice and has access to the settings section + // beware! the name of the method appears in modelsbuilder.controller.js + [System.Web.Http.HttpGet] // use the http one, not mvc, with api controllers! + public HttpResponseMessage GetDashboard() + { + return Request.CreateResponse(HttpStatusCode.OK, GetDashboardResult(), Configuration.Formatters.JsonFormatter); + } + + private Dashboard GetDashboardResult() + { + return new Dashboard + { + Enable = _config.Enable, + Text = BuilderDashboardHelper.Text(), + CanGenerate = BuilderDashboardHelper.CanGenerate(), + OutOfDateModels = BuilderDashboardHelper.AreModelsOutOfDate(), + LastError = BuilderDashboardHelper.LastError(), + }; + } + + private void GenerateModels(string modelsDirectory) + { + GenerateModels(_umbracoServices, modelsDirectory, _config.ModelsNamespace); + } + + internal static void GenerateModels(UmbracoServices umbracoServices, string modelsDirectory, string modelsNamespace) + { + if (!Directory.Exists(modelsDirectory)) + Directory.CreateDirectory(modelsDirectory); + + foreach (var file in Directory.GetFiles(modelsDirectory, "*.generated.cs")) + File.Delete(file); + + var typeModels = umbracoServices.GetAllTypes(); + + var builder = new TextBuilder(typeModels, modelsNamespace); + + foreach (var typeModel in builder.GetModelsToGenerate()) + { + var sb = new StringBuilder(); + builder.Generate(sb, typeModel); + var filename = Path.Combine(modelsDirectory, typeModel.ClrName + ".generated.cs"); + File.WriteAllText(filename, sb.ToString()); + } + + // the idea was to calculate the current hash and to add it as an extra file to the compilation, + // in order to be able to detect whether a DLL is consistent with an environment - however the + // environment *might not* contain the local partial files, and thus it could be impossible to + // calculate the hash. So... maybe that's not a good idea after all? + /* + var currentHash = HashHelper.Hash(ourFiles, typeModels); + ourFiles["models.hash.cs"] = $@"using Umbraco.ModelsBuilder; +[assembly:ModelsBuilderAssembly(SourceHash = ""{currentHash}"")] +"; + */ + + OutOfDateModelsStatus.Clear(); + } + + [DataContract] + internal class BuildResult + { + [DataMember(Name = "success")] + public bool Success; + [DataMember(Name = "message")] + public string Message; + } + + [DataContract] + internal class Dashboard + { + [DataMember(Name = "enable")] + public bool Enable; + [DataMember(Name = "text")] + public string Text; + [DataMember(Name = "canGenerate")] + public bool CanGenerate; + [DataMember(Name = "outOfDateModels")] + public bool OutOfDateModels; + [DataMember(Name = "lastError")] + public string LastError; + } + + internal enum OutOfDateType + { + OutOfDate, + Current, + Unknown = 100 + } + + [DataContract] + internal class OutOfDateStatus + { + [DataMember(Name = "status")] + public OutOfDateType Status { get; set; } + } + } +} diff --git a/src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderComponent.cs b/src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderComponent.cs new file mode 100644 index 0000000000..70d55368ac --- /dev/null +++ b/src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderComponent.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Web; +using System.Web.Mvc; +using System.Web.Routing; +using Umbraco.Core.Composing; +using Umbraco.Core.IO; +using Umbraco.Core.Services; +using Umbraco.Core.Services.Implement; +using Umbraco.ModelsBuilder.Configuration; +using Umbraco.Web; +using Umbraco.Web.JavaScript; +using Umbraco.Web.Mvc; + +namespace Umbraco.ModelsBuilder.Umbraco +{ + public class ModelsBuilderComponent : IComponent + { + private readonly UmbracoServices _umbracoServices; + + private readonly Config _config; + + public ModelsBuilderComponent(UmbracoServices umbracoServices, Config config) + { + _umbracoServices = umbracoServices; + _config = config; + } + + public void Initialize() + { + // always setup the dashboard + // note: UmbracoApiController instances are automatically registered + InstallServerVars(); + + ContentModelBinder.ModelBindingException += ContentModelBinder_ModelBindingException; + + if (_config.Enable) + FileService.SavingTemplate += FileService_SavingTemplate; + + // fixme LiveModelsProvider should not be static + if (_config.ModelsMode.IsLiveNotPure()) + LiveModelsProvider.Install(_umbracoServices); + + // fixme OutOfDateModelsStatus should not be static + if (_config.FlagOutOfDateModels) + OutOfDateModelsStatus.Install(); + } + + public void Terminate() + { } + + private void InstallServerVars() + { + // register our url - for the backoffice api + ServerVariablesParser.Parsing += (sender, serverVars) => + { + if (!serverVars.ContainsKey("umbracoUrls")) + throw new Exception("Missing umbracoUrls."); + var umbracoUrlsObject = serverVars["umbracoUrls"]; + if (umbracoUrlsObject == null) + throw new Exception("Null umbracoUrls"); + if (!(umbracoUrlsObject is Dictionary umbracoUrls)) + throw new Exception("Invalid umbracoUrls"); + + if (!serverVars.ContainsKey("umbracoPlugins")) + throw new Exception("Missing umbracoPlugins."); + if (!(serverVars["umbracoPlugins"] is Dictionary umbracoPlugins)) + throw new Exception("Invalid umbracoPlugins"); + + if (HttpContext.Current == null) throw new InvalidOperationException("HttpContext is null"); + var urlHelper = new UrlHelper(new RequestContext(new HttpContextWrapper(HttpContext.Current), new RouteData())); + + umbracoUrls["modelsBuilderBaseUrl"] = urlHelper.GetUmbracoApiServiceBaseUrl(controller => controller.BuildModels()); + umbracoPlugins["modelsBuilder"] = GetModelsBuilderSettings(); + }; + } + + private Dictionary GetModelsBuilderSettings() + { + var settings = new Dictionary + { + {"enabled", _config.Enable} + }; + + return settings; + } + + /// + /// Used to check if a template is being created based on a document type, in this case we need to + /// ensure the template markup is correct based on the model name of the document type + /// + /// + /// + private void FileService_SavingTemplate(IFileService sender, Core.Events.SaveEventArgs e) + { + // don't do anything if the factory is not enabled + // because, no factory = no models (even if generation is enabled) + if (!_config.EnableFactory) return; + + // don't do anything if this special key is not found + if (!e.AdditionalData.ContainsKey("CreateTemplateForContentType")) return; + + // ensure we have the content type alias + if (!e.AdditionalData.ContainsKey("ContentTypeAlias")) + throw new InvalidOperationException("The additionalData key: ContentTypeAlias was not found"); + + foreach (var template in e.SavedEntities) + { + // if it is in fact a new entity (not been saved yet) and the "CreateTemplateForContentType" key + // is found, then it means a new template is being created based on the creation of a document type + if (!template.HasIdentity && string.IsNullOrWhiteSpace(template.Content)) + { + // ensure is safe and always pascal cased, per razor standard + // + this is how we get the default model name in Umbraco.ModelsBuilder.Umbraco.Application + var alias = e.AdditionalData["ContentTypeAlias"].ToString(); + var name = template.Name; // will be the name of the content type since we are creating + var className = UmbracoServices.GetClrName(name, alias); + + var modelNamespace = _config.ModelsNamespace; + + // we do not support configuring this at the moment, so just let Umbraco use its default value + //var modelNamespaceAlias = ...; + + var markup = ViewHelper.GetDefaultFileContent( + modelClassName: className, + modelNamespace: modelNamespace/*, + modelNamespaceAlias: modelNamespaceAlias*/); + + //set the template content to the new markup + template.Content = markup; + } + } + } + + private void ContentModelBinder_ModelBindingException(object sender, ContentModelBinder.ModelBindingArgs args) + { + var sourceAttr = args.SourceType.Assembly.GetCustomAttribute(); + var modelAttr = args.ModelType.Assembly.GetCustomAttribute(); + + // if source or model is not a ModelsBuider type... + if (sourceAttr == null || modelAttr == null) + { + // if neither are ModelsBuilder types, give up entirely + if (sourceAttr == null && modelAttr == null) + return; + + // else report, but better not restart (loops?) + args.Message.Append(" The "); + args.Message.Append(sourceAttr == null ? "view model" : "source"); + args.Message.Append(" is a ModelsBuilder type, but the "); + args.Message.Append(sourceAttr != null ? "view model" : "source"); + args.Message.Append(" is not. The application is in an unstable state and should be restarted."); + return; + } + + // both are ModelsBuilder types + var pureSource = sourceAttr.PureLive; + var pureModel = modelAttr.PureLive; + + if (sourceAttr.PureLive || modelAttr.PureLive) + { + if (pureSource == false || pureModel == false) + { + // only one is pure - report, but better not restart (loops?) + args.Message.Append(pureSource + ? " The content model is PureLive, but the view model is not." + : " The view model is PureLive, but the content model is not."); + args.Message.Append(" The application is in an unstable state and should be restarted."); + } + else + { + // both are pure - report, and if different versions, restart + // if same version... makes no sense... and better not restart (loops?) + var sourceVersion = args.SourceType.Assembly.GetName().Version; + var modelVersion = args.ModelType.Assembly.GetName().Version; + args.Message.Append(" Both view and content models are PureLive, with "); + args.Message.Append(sourceVersion == modelVersion + ? "same version. The application is in an unstable state and should be restarted." + : "different versions. The application is in an unstable state and is going to be restarted."); + args.Restart = sourceVersion != modelVersion; + } + } + } + } +} diff --git a/src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderComposer.cs b/src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderComposer.cs new file mode 100644 index 0000000000..3dae2f86ab --- /dev/null +++ b/src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderComposer.cs @@ -0,0 +1,60 @@ +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.ModelsBuilder.Configuration; +using Umbraco.Web.PublishedCache.NuCache; + +namespace Umbraco.ModelsBuilder.Umbraco +{ + [ComposeBefore(typeof(NuCacheComposer))] + [RuntimeLevel(MinLevel = RuntimeLevel.Run)] + public sealed class ModelsBuilderComposer : ComponentComposer, ICoreComposer + { + public override void Compose(Composition composition) + { + base.Compose(composition); + + composition.Register(Lifetime.Singleton); + composition.Configs.Add(() => new Config()); + + if (composition.Configs.ModelsBuilder().ModelsMode == ModelsMode.PureLive) + ComposeForLiveModels(composition); + else if (composition.Configs.ModelsBuilder().EnableFactory) + ComposeForDefaultModelsFactory(composition); + } + + private void ComposeForDefaultModelsFactory(Composition composition) + { + composition.RegisterUnique(factory => + { + var typeLoader = factory.GetInstance(); + var types = typeLoader + .GetTypes() // element models + .Concat(typeLoader.GetTypes()); // content models + return new PublishedModelFactory(types); + }); + } + + private void ComposeForLiveModels(Composition composition) + { + composition.RegisterUnique(); + + // the following would add @using statement in every view so user's don't + // have to do it - however, then noone understands where the @using statement + // comes from, and it cannot be avoided / removed --- DISABLED + // + /* + // no need for @using in views + // note: + // we are NOT using the in-code attribute here, config is required + // because that would require parsing the code... and what if it changes? + // we can AddGlobalImport not sure we can remove one anyways + var modelsNamespace = Configuration.Config.ModelsNamespace; + if (string.IsNullOrWhiteSpace(modelsNamespace)) + modelsNamespace = Configuration.Config.DefaultModelsNamespace; + System.Web.WebPages.Razor.WebPageRazorHost.AddGlobalImport(modelsNamespace); + */ + } + } +} diff --git a/src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderInitializer.cs b/src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderInitializer.cs new file mode 100644 index 0000000000..9bd662da37 --- /dev/null +++ b/src/Umbraco.ModelsBuilder/Umbraco/ModelsBuilderInitializer.cs @@ -0,0 +1,28 @@ +using System.Web; +using System.Web.Compilation; +using Umbraco.ModelsBuilder.Umbraco; + +[assembly: PreApplicationStartMethod(typeof(ModelsBuilderInitializer), "Initialize")] + +namespace Umbraco.ModelsBuilder.Umbraco +{ + public static class ModelsBuilderInitializer + { + public static void Initialize() + { + // for some reason, netstandard is missing from BuildManager.ReferencedAssemblies and yet, is part of + // the references that CSharpCompiler receives - in some cases eg when building views - but not when + // using BuildManager to build the PureLive models - where is it coming from? cannot figure it out + + // so... cheating here + + // this is equivalent to adding + // + // to web.config system.web/compilation/assemblies + + var netStandard = ReferencedAssemblies.GetNetStandardAssembly(); + if (netStandard != null) + BuildManager.AddReferencedAssembly(netStandard); + } + } +} diff --git a/src/Umbraco.ModelsBuilder/Umbraco/ModelsGenerationError.cs b/src/Umbraco.ModelsBuilder/Umbraco/ModelsGenerationError.cs new file mode 100644 index 0000000000..a7b437df57 --- /dev/null +++ b/src/Umbraco.ModelsBuilder/Umbraco/ModelsGenerationError.cs @@ -0,0 +1,62 @@ +using System; +using System.IO; +using System.Text; +using Umbraco.Core.Composing; +using Umbraco.ModelsBuilder.Configuration; + +namespace Umbraco.ModelsBuilder.Umbraco +{ + internal static class ModelsGenerationError + { + private static Config Config => Current.Configs.ModelsBuilder(); + + public static void Clear() + { + var errFile = GetErrFile(); + if (errFile == null) return; + + // "If the file to be deleted does not exist, no exception is thrown." + File.Delete(errFile); + } + + public static void Report(string message, Exception e) + { + var errFile = GetErrFile(); + if (errFile == null) return; + + var sb = new StringBuilder(); + sb.Append(message); + sb.Append("\r\n"); + sb.Append(e.Message); + sb.Append("\r\n\r\n"); + sb.Append(e.StackTrace); + sb.Append("\r\n"); + + File.WriteAllText(errFile, sb.ToString()); + } + + public static string GetLastError() + { + var errFile = GetErrFile(); + if (errFile == null) return null; + + try + { + return File.ReadAllText(errFile); + } + catch // accepted + { + return null; + } + } + + private static string GetErrFile() + { + var modelsDirectory = Config.ModelsDirectory; + if (!Directory.Exists(modelsDirectory)) + return null; + + return Path.Combine(modelsDirectory, "models.err"); + } + } +} diff --git a/src/Umbraco.ModelsBuilder/Umbraco/OutOfDateModelsStatus.cs b/src/Umbraco.ModelsBuilder/Umbraco/OutOfDateModelsStatus.cs new file mode 100644 index 0000000000..142cc7578a --- /dev/null +++ b/src/Umbraco.ModelsBuilder/Umbraco/OutOfDateModelsStatus.cs @@ -0,0 +1,57 @@ +using System.IO; +using Umbraco.Core.Composing; +using Umbraco.ModelsBuilder.Configuration; +using Umbraco.Web.Cache; + +namespace Umbraco.ModelsBuilder.Umbraco +{ + public sealed class OutOfDateModelsStatus + { + private static Config Config => Current.Configs.ModelsBuilder(); + + internal static void Install() + { + // just be sure + if (Config.FlagOutOfDateModels == false) + return; + + ContentTypeCacheRefresher.CacheUpdated += (sender, args) => Write(); + DataTypeCacheRefresher.CacheUpdated += (sender, args) => Write(); + } + + private static string GetFlagPath() + { + var modelsDirectory = Config.ModelsDirectory; + if (!Directory.Exists(modelsDirectory)) + Directory.CreateDirectory(modelsDirectory); + return Path.Combine(modelsDirectory, "ood.flag"); + } + + private static void Write() + { + var path = GetFlagPath(); + if (path == null || File.Exists(path)) return; + File.WriteAllText(path, "THIS FILE INDICATES THAT MODELS ARE OUT-OF-DATE\n\n"); + } + + public static void Clear() + { + if (Config.FlagOutOfDateModels == false) return; + var path = GetFlagPath(); + if (path == null || !File.Exists(path)) return; + File.Delete(path); + } + + public static bool IsEnabled => Config.FlagOutOfDateModels; + + public static bool IsOutOfDate + { + get + { + if (Config.FlagOutOfDateModels == false) return false; + var path = GetFlagPath(); + return path != null && File.Exists(path); + } + } + } +} diff --git a/src/Umbraco.ModelsBuilder/Umbraco/PublishedModelUtility.cs b/src/Umbraco.ModelsBuilder/Umbraco/PublishedModelUtility.cs new file mode 100644 index 0000000000..c70e8a3b65 --- /dev/null +++ b/src/Umbraco.ModelsBuilder/Umbraco/PublishedModelUtility.cs @@ -0,0 +1,67 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using Umbraco.Web.Composing; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.ModelsBuilder.Umbraco +{ + public static class PublishedModelUtility + { + // looks safer but probably useless... ppl should not call these methods directly + // and if they do... they have to take care about not doing stupid things + + //public static PublishedPropertyType GetModelPropertyType2(Expression> selector) + // where T : PublishedContentModel + //{ + // var type = typeof (T); + // var s1 = type.GetField("ModelTypeAlias", BindingFlags.Public | BindingFlags.Static); + // var alias = (s1.IsLiteral && s1.IsInitOnly && s1.FieldType == typeof(string)) ? (string)s1.GetValue(null) : null; + // var s2 = type.GetField("ModelItemType", BindingFlags.Public | BindingFlags.Static); + // var itemType = (s2.IsLiteral && s2.IsInitOnly && s2.FieldType == typeof(PublishedItemType)) ? (PublishedItemType)s2.GetValue(null) : 0; + + // var contentType = PublishedContentType.Get(itemType, alias); + // // etc... + //} + + public static PublishedContentType GetModelContentType(PublishedItemType itemType, string alias) + { + var facade = Current.UmbracoContext.PublishedSnapshot; // fixme inject! + switch (itemType) + { + case PublishedItemType.Content: + return facade.Content.GetContentType(alias); + case PublishedItemType.Media: + return facade.Media.GetContentType(alias); + case PublishedItemType.Member: + return facade.Members.GetContentType(alias); + default: + throw new ArgumentOutOfRangeException(nameof(itemType)); + } + } + + public static PublishedPropertyType GetModelPropertyType(PublishedContentType contentType, Expression> selector) + //where TModel : PublishedContentModel // fixme PublishedContentModel _or_ PublishedElementModel + { + // fixme therefore, missing a check on TModel here + + var expr = selector.Body as MemberExpression; + + if (expr == null) + throw new ArgumentException("Not a property expression.", nameof(selector)); + + // there _is_ a risk that contentType and T do not match + // see note above : accepted risk... + + var attr = expr.Member + .GetCustomAttributes(typeof (ImplementPropertyTypeAttribute), false) + .OfType() + .SingleOrDefault(); + + if (string.IsNullOrWhiteSpace(attr?.Alias)) + throw new InvalidOperationException($"Could not figure out property alias for property \"{expr.Member.Name}\"."); + + return contentType.GetPropertyType(attr.Alias); + } + } +} diff --git a/src/Umbraco.ModelsBuilder/Umbraco/PureLiveModelFactory.cs b/src/Umbraco.ModelsBuilder/Umbraco/PureLiveModelFactory.cs new file mode 100644 index 0000000000..05f748a588 --- /dev/null +++ b/src/Umbraco.ModelsBuilder/Umbraco/PureLiveModelFactory.cs @@ -0,0 +1,677 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Web; +using System.Web.Compilation; +using System.Web.Hosting; +using System.Web.WebPages.Razor; +using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Web.Cache; +using Umbraco.ModelsBuilder.Building; +using Umbraco.ModelsBuilder.Configuration; +using File = System.IO.File; + +namespace Umbraco.ModelsBuilder.Umbraco +{ + internal class PureLiveModelFactory : ILivePublishedModelFactory, IRegisteredObject + { + private Assembly _modelsAssembly; + private Infos _infos = new Infos { ModelInfos = null, ModelTypeMap = new Dictionary() }; + private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(); + private volatile bool _hasModels; // volatile 'cos reading outside lock + private bool _pendingRebuild; + private readonly IProfilingLogger _logger; + private readonly FileSystemWatcher _watcher; + private int _ver, _skipver; + private readonly int _debugLevel; + private BuildManager _theBuildManager; + private readonly Lazy _umbracoServices; + private UmbracoServices UmbracoServices => _umbracoServices.Value; + + private static readonly Regex AssemblyVersionRegex = new Regex("AssemblyVersion\\(\"[0-9]+.[0-9]+.[0-9]+.[0-9]+\"\\)", RegexOptions.Compiled); + private const string ProjVirt = "~/App_Data/Models/all.generated.cs"; + private static readonly string[] OurFiles = { "models.hash", "models.generated.cs", "all.generated.cs", "all.dll.path", "models.err" }; + + private readonly Config _config; + + public PureLiveModelFactory(Lazy umbracoServices, IProfilingLogger logger, Config config) + { + _umbracoServices = umbracoServices; + _logger = logger; + _config = config; + _ver = 1; // zero is for when we had no version + _skipver = -1; // nothing to skip + + RazorBuildProvider.CodeGenerationStarted += RazorBuildProvider_CodeGenerationStarted; + + if (!HostingEnvironment.IsHosted) return; + + var modelsDirectory = _config.ModelsDirectory; + if (!Directory.Exists(modelsDirectory)) + Directory.CreateDirectory(modelsDirectory); + + // BEWARE! if the watcher is not properly released then for some reason the + // BuildManager will start confusing types - using a 'registered object' here + // though we should probably plug into Umbraco's MainDom - which is internal + HostingEnvironment.RegisterObject(this); + _watcher = new FileSystemWatcher(modelsDirectory); + _watcher.Changed += WatcherOnChanged; + _watcher.EnableRaisingEvents = true; + + // get it here, this need to be fast + _debugLevel = _config.DebugLevel; + } + + #region ILivePublishedModelFactory + + /// + public object SyncRoot { get; } = new object(); + + /// + public void Refresh() + { + ResetModels(); + EnsureModels(); + } + + #endregion + + #region IPublishedModelFactory + + public IPublishedElement CreateModel(IPublishedElement element) + { + // get models, rebuilding them if needed + var infos = EnsureModels()?.ModelInfos; + if (infos == null) + return element; + + // be case-insensitive + var contentTypeAlias = element.ContentType.Alias; + + // lookup model constructor (else null) + infos.TryGetValue(contentTypeAlias, out ModelInfo info); + + // create model + return info == null ? element : info.Ctor(element); + } + + // this runs only once the factory is ready + // NOT when building models + public Type MapModelType(Type type) + { + var infos = EnsureModels(); + return ModelType.Map(type, infos.ModelTypeMap); + } + + // this runs only once the factory is ready + // NOT when building models + public IList CreateModelList(string alias) + { + var infos = EnsureModels(); + + // fail fast + if (infos == null) + return new List(); + + if (!infos.ModelInfos.TryGetValue(alias, out var modelInfo)) + return new List(); + + var ctor = modelInfo.ListCtor; + if (ctor != null) return ctor(); + + var listType = typeof(List<>).MakeGenericType(modelInfo.ModelType); + ctor = modelInfo.ListCtor = ReflectionUtilities.EmitConstructor>(declaring: listType); + return ctor(); + } + + #endregion + + #region Compilation + + // deadlock note + // + // when RazorBuildProvider_CodeGenerationStarted runs, the thread has Monitor.Enter-ed the BuildManager + // singleton instance, through a call to CompilationLock.GetLock in BuildManager.GetVPathBuildResultInternal, + // and now wants to lock _locker. + // when EnsureModels runs, the thread locks _locker and then wants BuildManager to compile, which in turns + // requires that the BuildManager can Monitor.Enter-ed itself. + // so: + // + // T1 - needs to ensure models, locks _locker + // T2 - needs to compile a view, locks BuildManager + // hits RazorBuildProvider_CodeGenerationStarted + // wants to lock _locker, wait + // T1 - needs to compile models, using BuildManager + // wants to lock itself, wait + // + // + // until ASP.NET kills the long-running request (thread abort) + // + // problem is, we *want* to suspend views compilation while the models assembly is being changed else we + // end up with views compiled and cached with the old assembly, while models come from the new assembly, + // which gives more YSOD. so we *have* to lock _locker in RazorBuildProvider_CodeGenerationStarted. + // + // one "easy" solution consists in locking the BuildManager *before* _locker in EnsureModels, thus ensuring + // we always lock in the same order, and getting rid of deadlocks - but that requires having access to the + // current BuildManager instance, which is BuildManager.TheBuildManager, which is an internal property. + // + // well, that's what we are doing in this class' TheBuildManager property, using reflection. + + private void RazorBuildProvider_CodeGenerationStarted(object sender, EventArgs e) + { + try + { + _locker.EnterReadLock(); + + // just be safe - can happen if the first view is not an Umbraco view, + // or if something went wrong and we don't have an assembly at all + if (_modelsAssembly == null) return; + + if (_debugLevel > 0) + _logger.Debug("RazorBuildProvider.CodeGenerationStarted"); + if (!(sender is RazorBuildProvider provider)) return; + + // add the assembly, and add a dependency to a text file that will change on each + // compilation as in some environments (could not figure which/why) the BuildManager + // would not re-compile the views when the models assembly is rebuilt. + provider.AssemblyBuilder.AddAssemblyReference(_modelsAssembly); + provider.AddVirtualPathDependency(ProjVirt); + } + finally + { + if (_locker.IsReadLockHeld) + _locker.ExitReadLock(); + } + } + + // tells the factory that it should build a new generation of models + private void ResetModels() + { + _logger.Debug("Resetting models."); + + try + { + _locker.EnterWriteLock(); + + _hasModels = false; + _pendingRebuild = true; + + var modelsDirectory = _config.ModelsDirectory; + if (!Directory.Exists(modelsDirectory)) + Directory.CreateDirectory(modelsDirectory); + + // clear stuff + var modelsHashFile = Path.Combine(modelsDirectory, "models.hash"); + //var modelsSrcFile = Path.Combine(modelsDirectory, "models.generated.cs"); + //var projFile = Path.Combine(modelsDirectory, "all.generated.cs"); + var dllPathFile = Path.Combine(modelsDirectory, "all.dll.path"); + + if (File.Exists(dllPathFile)) File.Delete(dllPathFile); + if (File.Exists(modelsHashFile)) File.Delete(modelsHashFile); + } + finally + { + if (_locker.IsWriteLockHeld) + _locker.ExitWriteLock(); + } + } + + // gets "the" build manager + private BuildManager TheBuildManager + { + get + { + if (_theBuildManager != null) return _theBuildManager; + var prop = typeof (BuildManager).GetProperty("TheBuildManager", BindingFlags.NonPublic | BindingFlags.Static); + if (prop == null) + throw new InvalidOperationException("Could not get BuildManager.TheBuildManager property."); + _theBuildManager = (BuildManager) prop.GetValue(null); + return _theBuildManager; + } + } + + // ensure that the factory is running with the lastest generation of models + internal Infos EnsureModels() + { + if (_debugLevel > 0) + _logger.Debug("Ensuring models."); + + // don't use an upgradeable lock here because only 1 thread at a time could enter it + try + { + _locker.EnterReadLock(); + if (_hasModels) + return _infos; + } + finally + { + if (_locker.IsReadLockHeld) + _locker.ExitReadLock(); + } + + var buildManagerLocked = false; + try + { + // always take the BuildManager lock *before* taking the _locker lock + // to avoid possible deadlock situations (see notes above) + Monitor.Enter(TheBuildManager, ref buildManagerLocked); + + _locker.EnterUpgradeableReadLock(); + + if (_hasModels) return _infos; + + _locker.EnterWriteLock(); + + // we don't have models, + // either they haven't been loaded from the cache yet + // or they have been reseted and are pending a rebuild + + using (_logger.DebugDuration("Get models.", "Got models.")) + { + try + { + var assembly = GetModelsAssembly(_pendingRebuild); + + // the one below can be used to simulate an issue with BuildManager, ie it will register + // the models with the factory but NOT with the BuildManager, which will not recompile views. + // this is for U4-8043 which is an obvious issue but I cannot replicate + //_modelsAssembly = _modelsAssembly ?? assembly; + + // the one below is the normal one + _modelsAssembly = assembly; + + var types = assembly.ExportedTypes.Where(x => x.Inherits() || x.Inherits()); + _infos = RegisterModels(types); + ModelsGenerationError.Clear(); + } + catch (Exception e) + { + try + { + _logger.Error("Failed to build models.", e); + _logger.Warn("Running without models."); // be explicit + ModelsGenerationError.Report("Failed to build PureLive models.", e); + } + finally + { + _modelsAssembly = null; + _infos = new Infos { ModelInfos = null, ModelTypeMap = new Dictionary() }; + } + } + + // don't even try again + _hasModels = true; + } + + return _infos; + } + finally + { + if (_locker.IsWriteLockHeld) + _locker.ExitWriteLock(); + if (_locker.IsUpgradeableReadLockHeld) + _locker.ExitUpgradeableReadLock(); + if (buildManagerLocked) + Monitor.Exit(TheBuildManager); + } + } + + private Assembly GetModelsAssembly(bool forceRebuild) + { + var modelsDirectory = _config.ModelsDirectory; + if (!Directory.Exists(modelsDirectory)) + Directory.CreateDirectory(modelsDirectory); + + var typeModels = UmbracoServices.GetAllTypes(); + var currentHash = HashHelper.Hash(typeModels); + var modelsHashFile = Path.Combine(modelsDirectory, "models.hash"); + var modelsSrcFile = Path.Combine(modelsDirectory, "models.generated.cs"); + var projFile = Path.Combine(modelsDirectory, "all.generated.cs"); + var dllPathFile = Path.Combine(modelsDirectory, "all.dll.path"); + + // caching the generated models speeds up booting + // currentHash hashes both the types & the user's partials + + if (!forceRebuild) + { + _logger.Debug("Looking for cached models."); + if (File.Exists(modelsHashFile) && File.Exists(projFile)) + { + var cachedHash = File.ReadAllText(modelsHashFile); + if (currentHash != cachedHash) + { + _logger.Debug("Found obsolete cached models."); + forceRebuild = true; + } + + // else cachedHash matches currentHash, we can try to load an existing dll + } + else + { + _logger.Debug("Could not find cached models."); + forceRebuild = true; + } + } + + Assembly assembly; + if (!forceRebuild) + { + // try to load the dll directly (avoid rebuilding) + // + // ensure that the .dll file does not have a corresponding .dll.delete file + // as that would mean the the .dll file is going to be deleted and should not + // be re-used - that should not happen in theory, but better be safe + // + // ensure that the .dll file is in the current codegen directory - when IIS + // or Express does a full restart, it can switch to an entirely new codegen + // directory, and then we end up referencing a dll which is *not* in that + // directory, and BuildManager fails to instantiate views ("the view found + // at ... was not created"). + // + if (File.Exists(dllPathFile)) + { + var dllPath = File.ReadAllText(dllPathFile); + var codegen = HttpRuntime.CodegenDir; + + _logger.Debug($"Cached models dll at {dllPath}."); + + if (File.Exists(dllPath) && !File.Exists(dllPath + ".delete") && dllPath.StartsWith(codegen)) + { + assembly = Assembly.LoadFile(dllPath); + var attr = assembly.GetCustomAttribute(); + if (attr != null && attr.PureLive && attr.SourceHash == currentHash) + { + // if we were to resume at that revision, then _ver would keep increasing + // and that is probably a bad idea - so, we'll always rebuild starting at + // ver 1, but we remember we want to skip that one - so we never end up + // with the "same but different" version of the assembly in memory + _skipver = assembly.GetName().Version.Revision; + + _logger.Debug("Loading cached models (dll)."); + return assembly; + } + + _logger.Debug("Cached models dll cannot be loaded (invalid assembly)."); + } + else if (!File.Exists(dllPath)) + _logger.Debug("Cached models dll does not exist."); + else if (File.Exists(dllPath + ".delete")) + _logger.Debug("Cached models dll is marked for deletion."); + else if (!dllPath.StartsWith(codegen)) + _logger.Debug("Cached models dll is in a different codegen directory."); + else + _logger.Debug("Cached models dll cannot be loaded (why?)."); + } + + // must reset the version in the file else it would keep growing + // loading cached modules only happens when the app restarts + var text = File.ReadAllText(projFile); + var match = AssemblyVersionRegex.Match(text); + if (match.Success) + { + text = text.Replace(match.Value, "AssemblyVersion(\"0.0.0." + _ver + "\")"); + File.WriteAllText(projFile, text); + } + + // generate a marker file that will be a dependency + // see note in RazorBuildProvider_CodeGenerationStarted + // NO: using all.generated.cs as a dependency + //File.WriteAllText(Path.Combine(modelsDirectory, "models.dep"), "VER:" + _ver); + + _ver++; + try + { + assembly = BuildManager.GetCompiledAssembly(ProjVirt); + File.WriteAllText(dllPathFile, assembly.Location); + } + catch + { + ClearOnFailingToCompile(dllPathFile, modelsHashFile, projFile); + throw; + } + + _logger.Debug("Loading cached models (source)."); + return assembly; + } + + // need to rebuild + _logger.Debug("Rebuilding models."); + + // generate code, save + var code = GenerateModelsCode(typeModels); + // add extra attributes, + // PureLiveAssembly helps identifying Assemblies that contain PureLive models + // AssemblyVersion is so that we have a different version for each rebuild + var ver = _ver == _skipver ? ++_ver : _ver; + _ver++; + code = code.Replace("//ASSATTR", $@"[assembly: PureLiveAssembly] +[assembly:ModelsBuilderAssembly(PureLive = true, SourceHash = ""{currentHash}"")] +[assembly:System.Reflection.AssemblyVersion(""0.0.0.{ver}"")]"); + File.WriteAllText(modelsSrcFile, code); + + // generate proj, save + var projFiles = new Dictionary + { + { "models.generated.cs", code } + }; + var proj = GenerateModelsProj(projFiles); + File.WriteAllText(projFile, proj); + + // compile and register + try + { + assembly = BuildManager.GetCompiledAssembly(ProjVirt); + File.WriteAllText(dllPathFile, assembly.Location); + File.WriteAllText(modelsHashFile, currentHash); + } + catch + { + ClearOnFailingToCompile(dllPathFile, modelsHashFile, projFile); + throw; + } + + _logger.Debug("Done rebuilding."); + return assembly; + } + + private void ClearOnFailingToCompile(string dllPathFile, string modelsHashFile, string projFile) + { + _logger.Debug("Failed to compile."); + + // the dll file reference still points to the previous dll, which is obsolete + // now and will be deleted by ASP.NET eventually, so better clear that reference. + // also touch the proj file to force views to recompile - don't delete as it's + // useful to have the source around for debugging. + try + { + if (File.Exists(dllPathFile)) File.Delete(dllPathFile); + if (File.Exists(modelsHashFile)) File.Delete(modelsHashFile); + if (File.Exists(projFile)) File.SetLastWriteTime(projFile, DateTime.Now); + } + catch { /* enough */ } + } + + private static Infos RegisterModels(IEnumerable types) + { + var ctorArgTypes = new[] { typeof (IPublishedElement) }; + var modelInfos = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + var map = new Dictionary(); + + foreach (var type in types) + { + ConstructorInfo constructor = null; + Type parameterType = null; + + foreach (var ctor in type.GetConstructors()) + { + var parms = ctor.GetParameters(); + if (parms.Length == 1 && typeof (IPublishedElement).IsAssignableFrom(parms[0].ParameterType)) + { + if (constructor != null) + throw new InvalidOperationException($"Type {type.FullName} has more than one public constructor with one argument of type, or implementing, IPropertySet."); + constructor = ctor; + parameterType = parms[0].ParameterType; + } + } + + if (constructor == null) + throw new InvalidOperationException($"Type {type.FullName} is missing a public constructor with one argument of type, or implementing, IPropertySet."); + + var attribute = type.GetCustomAttribute(false); + var typeName = attribute == null ? type.Name : attribute.ContentTypeAlias; + + if (modelInfos.TryGetValue(typeName, out ModelInfo modelInfo)) + throw new InvalidOperationException($"Both types {type.FullName} and {modelInfo.ModelType.FullName} want to be a model type for content type with alias \"{typeName}\"."); + + // fixme use Core's ReflectionUtilities.EmitCtor !! + var meth = new DynamicMethod(string.Empty, typeof (IPublishedElement), ctorArgTypes, type.Module, true); + var gen = meth.GetILGenerator(); + gen.Emit(OpCodes.Ldarg_0); + gen.Emit(OpCodes.Newobj, constructor); + gen.Emit(OpCodes.Ret); + var func = (Func) meth.CreateDelegate(typeof (Func)); + + modelInfos[typeName] = new ModelInfo { ParameterType = parameterType, Ctor = func, ModelType = type }; + map[typeName] = type; + } + + return new Infos { ModelInfos = modelInfos.Count > 0 ? modelInfos : null, ModelTypeMap = map }; + } + + private string GenerateModelsCode(IList typeModels) + { + var modelsDirectory = _config.ModelsDirectory; + if (!Directory.Exists(modelsDirectory)) + Directory.CreateDirectory(modelsDirectory); + + foreach (var file in Directory.GetFiles(modelsDirectory, "*.generated.cs")) + File.Delete(file); + + var builder = new TextBuilder(typeModels, _config.ModelsNamespace); + + var codeBuilder = new StringBuilder(); + builder.Generate(codeBuilder, builder.GetModelsToGenerate()); + var code = codeBuilder.ToString(); + + return code; + } + + private static readonly Regex UsingRegex = new Regex("^using(.*);", RegexOptions.Compiled | RegexOptions.Multiline); + private static readonly Regex AattrRegex = new Regex("^\\[assembly:(.*)\\]", RegexOptions.Compiled | RegexOptions.Multiline); + + private static string GenerateModelsProj(IDictionary files) + { + // ideally we would generate a CSPROJ file but then we'd need a BuildProvider for csproj + // trying to keep things simple for the time being, just write everything to one big file + + // group all 'using' at the top of the file (else fails) + var usings = new List(); + foreach (var k in files.Keys.ToList()) + files[k] = UsingRegex.Replace(files[k], m => + { + usings.Add(m.Groups[1].Value); + return string.Empty; + }); + + // group all '[assembly:...]' at the top of the file (else fails) + var aattrs = new List(); + foreach (var k in files.Keys.ToList()) + files[k] = AattrRegex.Replace(files[k], m => + { + aattrs.Add(m.Groups[1].Value); + return string.Empty; + }); + + var text = new StringBuilder(); + foreach (var u in usings.Distinct()) + { + text.Append("using "); + text.Append(u); + text.Append(";\r\n"); + } + foreach (var a in aattrs) + { + text.Append("[assembly:"); + text.Append(a); + text.Append("]\r\n"); + } + text.Append("\r\n\r\n"); + foreach (var f in files) + { + text.Append("// FILE: "); + text.Append(f.Key); + text.Append("\r\n\r\n"); + text.Append(f.Value); + text.Append("\r\n\r\n\r\n"); + } + text.Append("// EOF\r\n"); + + return text.ToString(); + } + + internal class Infos + { + public Dictionary ModelTypeMap { get; set; } + public Dictionary ModelInfos { get; set; } + } + + internal class ModelInfo + { + public Type ParameterType { get; set; } + public Func Ctor { get; set; } + public Type ModelType { get; set; } + public Func ListCtor { get; set; } + } + + #endregion + + #region Watching + + private void WatcherOnChanged(object sender, FileSystemEventArgs args) + { + var changed = args.Name; + + // don't reset when our files change because we are building! + // + // comment it out, and always ignore our files, because it seems that some + // race conditions can occur on slow Cloud filesystems and then we keep + // rebuilding + + //if (_building && OurFiles.Contains(changed)) + //{ + // //_logger.Info("Ignoring files self-changes."); + // return; + //} + + // always ignore our own file changes + if (OurFiles.Contains(changed)) + return; + + _logger.Info("Detected files changes."); + + lock (SyncRoot) // don't reset while being locked + { + ResetModels(); + } + } + + public void Stop(bool immediate) + { + _watcher.EnableRaisingEvents = false; + _watcher.Dispose(); + HostingEnvironment.UnregisterObject(this); + } + + #endregion + } +} diff --git a/src/Umbraco.ModelsBuilder/Umbraco/UmbracoServices.cs b/src/Umbraco.ModelsBuilder/Umbraco/UmbracoServices.cs new file mode 100644 index 0000000000..67ef10be1a --- /dev/null +++ b/src/Umbraco.ModelsBuilder/Umbraco/UmbracoServices.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Services; +using Umbraco.Core.Strings; +using Umbraco.ModelsBuilder.Building; +using Umbraco.ModelsBuilder.Configuration; + +namespace Umbraco.ModelsBuilder.Umbraco +{ + public class UmbracoServices + { + private readonly IContentTypeService _contentTypeService; + private readonly IMediaTypeService _mediaTypeService; + private readonly IMemberTypeService _memberTypeService; + private readonly IPublishedContentTypeFactory _publishedContentTypeFactory; + + public UmbracoServices(IContentTypeService contentTypeService, IMediaTypeService mediaTypeService, IMemberTypeService memberTypeService, IPublishedContentTypeFactory publishedContentTypeFactory) + { + _contentTypeService = contentTypeService; + _mediaTypeService = mediaTypeService; + _memberTypeService = memberTypeService; + _publishedContentTypeFactory = publishedContentTypeFactory; + } + + private static Config Config => Current.Configs.ModelsBuilder(); + + #region Services + + public IList GetAllTypes() + { + var types = new List(); + + types.AddRange(GetTypes(PublishedItemType.Content, _contentTypeService.GetAll().Cast().ToArray())); + types.AddRange(GetTypes(PublishedItemType.Media, _mediaTypeService.GetAll().Cast().ToArray())); + types.AddRange(GetTypes(PublishedItemType.Member, _memberTypeService.GetAll().Cast().ToArray())); + + return EnsureDistinctAliases(types); + } + + public IList GetContentTypes() + { + var contentTypes = _contentTypeService.GetAll().Cast().ToArray(); + return GetTypes(PublishedItemType.Content, contentTypes); // aliases have to be unique here + } + + public IList GetMediaTypes() + { + var contentTypes = _mediaTypeService.GetAll().Cast().ToArray(); + return GetTypes(PublishedItemType.Media, contentTypes); // aliases have to be unique here + } + + public IList GetMemberTypes() + { + var memberTypes = _memberTypeService.GetAll().Cast().ToArray(); + return GetTypes(PublishedItemType.Member, memberTypes); // aliases have to be unique here + } + + public static string GetClrName(string name, string alias) + { + // ModelsBuilder's legacy - but not ideal + return alias.ToCleanString(CleanStringType.ConvertCase | CleanStringType.PascalCase); + } + + private IList GetTypes(PublishedItemType itemType, IContentTypeComposition[] contentTypes) + { + var typeModels = new List(); + var uniqueTypes = new HashSet(); + + // get the types and the properties + foreach (var contentType in contentTypes) + { + var typeModel = new TypeModel + { + Id = contentType.Id, + Alias = contentType.Alias, + ClrName = GetClrName(contentType.Name, contentType.Alias), + ParentId = contentType.ParentId, + + Name = contentType.Name, + Description = contentType.Description + }; + + // of course this should never happen, but when it happens, better detect it + // else we end up with weird nullrefs everywhere + if (uniqueTypes.Contains(typeModel.ClrName)) + throw new Exception($"Panic: duplicate type ClrName \"{typeModel.ClrName}\"."); + uniqueTypes.Add(typeModel.ClrName); + + var publishedContentType = _publishedContentTypeFactory.CreateContentType(contentType); + switch (itemType) + { + case PublishedItemType.Content: + typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element + ? TypeModel.ItemTypes.Element + : TypeModel.ItemTypes.Content; + break; + case PublishedItemType.Media: + typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element + ? TypeModel.ItemTypes.Element + : TypeModel.ItemTypes.Media; + break; + case PublishedItemType.Member: + typeModel.ItemType = publishedContentType.ItemType == PublishedItemType.Element + ? TypeModel.ItemTypes.Element + : TypeModel.ItemTypes.Member; + break; + default: + throw new InvalidOperationException(string.Format("Unsupported PublishedItemType \"{0}\".", itemType)); + } + + typeModels.Add(typeModel); + + foreach (var propertyType in contentType.PropertyTypes) + { + var propertyModel = new PropertyModel + { + Alias = propertyType.Alias, + ClrName = GetClrName(propertyType.Name, propertyType.Alias), + + Name = propertyType.Name, + Description = propertyType.Description + }; + + var publishedPropertyType = publishedContentType.GetPropertyType(propertyType.Alias); + if (publishedPropertyType == null) + throw new Exception($"Panic: could not get published property type {contentType.Alias}.{propertyType.Alias}."); + + propertyModel.ModelClrType = publishedPropertyType.ModelClrType; + + typeModel.Properties.Add(propertyModel); + } + } + + // wire the base types + foreach (var typeModel in typeModels.Where(x => x.ParentId > 0)) + { + typeModel.BaseType = typeModels.SingleOrDefault(x => x.Id == typeModel.ParentId); + // Umbraco 7.4 introduces content types containers, so even though ParentId > 0, the parent might + // not be a content type - here we assume that BaseType being null while ParentId > 0 means that + // the parent is a container (and we don't check). + typeModel.IsParent = typeModel.BaseType != null; + } + + // discover mixins + foreach (var contentType in contentTypes) + { + var typeModel = typeModels.SingleOrDefault(x => x.Id == contentType.Id); + if (typeModel == null) throw new Exception("Panic: no type model matching content type."); + + IEnumerable compositionTypes; + var contentTypeAsMedia = contentType as IMediaType; + var contentTypeAsContent = contentType as IContentType; + var contentTypeAsMember = contentType as IMemberType; + if (contentTypeAsMedia != null) compositionTypes = contentTypeAsMedia.ContentTypeComposition; + else if (contentTypeAsContent != null) compositionTypes = contentTypeAsContent.ContentTypeComposition; + else if (contentTypeAsMember != null) compositionTypes = contentTypeAsMember.ContentTypeComposition; + else throw new Exception(string.Format("Panic: unsupported type \"{0}\".", contentType.GetType().FullName)); + + foreach (var compositionType in compositionTypes) + { + var compositionModel = typeModels.SingleOrDefault(x => x.Id == compositionType.Id); + if (compositionModel == null) throw new Exception("Panic: composition type does not exist."); + + if (compositionType.Id == contentType.ParentId) continue; + + // add to mixins + typeModel.MixinTypes.Add(compositionModel); + + // mark as mixin - as well as parents + compositionModel.IsMixin = true; + while ((compositionModel = compositionModel.BaseType) != null) + compositionModel.IsMixin = true; + } + } + + return typeModels; + } + + internal static IList EnsureDistinctAliases(IList typeModels) + { + var groups = typeModels.GroupBy(x => x.Alias.ToLowerInvariant()); + foreach (var group in groups.Where(x => x.Count() > 1)) + { + throw new NotSupportedException($"Alias \"{group.Key}\" is used by types" + + $" {string.Join(", ", group.Select(x => x.ItemType + ":\"" + x.Alias + "\""))}. Aliases have to be unique." + + " One of the aliases must be modified in order to use the ModelsBuilder."); + } + return typeModels; + } + + #endregion + } +} diff --git a/src/Umbraco.ModelsBuilder/Validation/ContentTypeModelValidator.cs b/src/Umbraco.ModelsBuilder/Validation/ContentTypeModelValidator.cs new file mode 100644 index 0000000000..3d2ce600c2 --- /dev/null +++ b/src/Umbraco.ModelsBuilder/Validation/ContentTypeModelValidator.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.ModelsBuilder.Configuration; +using Umbraco.Web.Editors; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.ModelsBuilder.Validation +{ + /// + /// Used to validate the aliases for the content type when MB is enabled to ensure that + /// no illegal aliases are used + /// + internal class ContentTypeModelValidator : ContentTypeModelValidatorBase + { } + + /// + /// Used to validate the aliases for the content type when MB is enabled to ensure that + /// no illegal aliases are used + /// + internal class MediaTypeModelValidator : ContentTypeModelValidatorBase + { } + + /// + /// Used to validate the aliases for the content type when MB is enabled to ensure that + /// no illegal aliases are used + /// + internal class MemberTypeModelValidator : ContentTypeModelValidatorBase + { } + + internal abstract class ContentTypeModelValidatorBase : EditorValidator + where TModel: ContentTypeSave + where TProperty: PropertyTypeBasic + { + private static Config Config => Current.Configs.ModelsBuilder(); + + protected override IEnumerable Validate(TModel model) + { + //don't do anything if we're not enabled + if (Config.Enable) + { + var properties = model.Groups.SelectMany(x => x.Properties) + .Where(x => x.Inherited == false) + .ToArray(); + + foreach (var prop in properties) + { + var propertyGroup = model.Groups.Single(x => x.Properties.Contains(prop)); + + if (model.Alias.ToLowerInvariant() == prop.Alias.ToLowerInvariant()) + yield return new ValidationResult(string.Format("With Models Builder enabled, you can't have a property with a the alias \"{0}\" when the content type alias is also \"{0}\".", prop.Alias), new[] + { + string.Format("Groups[{0}].Properties[{1}].Alias", model.Groups.IndexOf(propertyGroup), propertyGroup.Properties.IndexOf(prop)) + }); + + //we need to return the field name with an index so it's wired up correctly + var groupIndex = model.Groups.IndexOf(propertyGroup); + var propertyIndex = propertyGroup.Properties.IndexOf(prop); + + var validationResult = ValidateProperty(prop, groupIndex, propertyIndex); + if (validationResult != null) + { + yield return validationResult; + } + } + } + } + + private ValidationResult ValidateProperty(PropertyTypeBasic property, int groupIndex, int propertyIndex) + { + //don't let them match any properties or methods in IPublishedContent + //TODO: There are probably more! + var reservedProperties = typeof(IPublishedContent).GetProperties().Select(x => x.Name).ToArray(); + var reservedMethods = typeof(IPublishedContent).GetMethods().Select(x => x.Name).ToArray(); + + var alias = property.Alias; + + if (reservedProperties.InvariantContains(alias) || reservedMethods.InvariantContains(alias)) + { + return new ValidationResult( + string.Format("The alias {0} is a reserved term and cannot be used", alias), new[] + { + string.Format("Groups[{0}].Properties[{1}].Alias", groupIndex, propertyIndex) + }); + } + + return null; + } + } +} diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 1a1946b01a..e52cb253cf 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -117,6 +117,10 @@ Umbraco.Examine {07FBC26B-2927-4A22-8D96-D644C667FECC} + + {52ac0ba8-a60e-4e36-897b-e8b97a54ed1c} + Umbraco.ModelsBuilder + {651e1350-91b6-44b7-bd60-7207006d7003} Umbraco.Web From f3643747b37335a7804cceb89a4407012a3a4915 Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 24 Jun 2019 14:33:15 +0200 Subject: [PATCH 0010/1545] Publicize IEditorValidator --- src/Umbraco.Web/Editors/EditorValidatorOfT.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/Editors/EditorValidatorOfT.cs b/src/Umbraco.Web/Editors/EditorValidatorOfT.cs index 4ca008cf0d..715a49179e 100644 --- a/src/Umbraco.Web/Editors/EditorValidatorOfT.cs +++ b/src/Umbraco.Web/Editors/EditorValidatorOfT.cs @@ -8,7 +8,7 @@ namespace Umbraco.Web.Editors /// Provides a base class for implementations. ///
/// The validated object type. - internal abstract class EditorValidator : IEditorValidator + public abstract class EditorValidator : IEditorValidator { public Type ModelType => typeof (T); @@ -16,4 +16,4 @@ namespace Umbraco.Web.Editors protected abstract IEnumerable Validate(T model); } -} \ No newline at end of file +} From 834aeee107ff48e7cdfcb5bb6edb778ab8711f4e Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 26 Jun 2019 22:38:32 +0200 Subject: [PATCH 0011/1545] Fix slimmed down ModelsBuilder --- build/NuSpecs/UmbracoCms.Web.nuspec | 3 +++ build/NuSpecs/UmbracoCms.nuspec | 1 - src/Umbraco.ModelsBuilder/Umbraco.ModelsBuilder.csproj | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/build/NuSpecs/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec index 614a816f3f..89ff42f853 100644 --- a/build/NuSpecs/UmbracoCms.Web.nuspec +++ b/build/NuSpecs/UmbracoCms.Web.nuspec @@ -51,14 +51,17 @@ + + + diff --git a/build/NuSpecs/UmbracoCms.nuspec b/build/NuSpecs/UmbracoCms.nuspec index 0d18ac59fa..62fd620faa 100644 --- a/build/NuSpecs/UmbracoCms.nuspec +++ b/build/NuSpecs/UmbracoCms.nuspec @@ -25,7 +25,6 @@ not want this to happen as the alpha of the next major is, really, the next major already. --> - diff --git a/src/Umbraco.ModelsBuilder/Umbraco.ModelsBuilder.csproj b/src/Umbraco.ModelsBuilder/Umbraco.ModelsBuilder.csproj index 5b0272a526..207d822fbb 100644 --- a/src/Umbraco.ModelsBuilder/Umbraco.ModelsBuilder.csproj +++ b/src/Umbraco.ModelsBuilder/Umbraco.ModelsBuilder.csproj @@ -29,6 +29,7 @@ TRACE prompt 4 + bin\Release\Umbraco.ModelsBuilder.xml From b3867d9d550b07f350c02b202730c72ade1195a6 Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 27 Jun 2019 09:16:21 +0200 Subject: [PATCH 0012/1545] Post-merge fixes --- src/Umbraco.ModelsBuilder/Umbraco/PublishedModelUtility.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.ModelsBuilder/Umbraco/PublishedModelUtility.cs b/src/Umbraco.ModelsBuilder/Umbraco/PublishedModelUtility.cs index c70e8a3b65..7bbae927d5 100644 --- a/src/Umbraco.ModelsBuilder/Umbraco/PublishedModelUtility.cs +++ b/src/Umbraco.ModelsBuilder/Umbraco/PublishedModelUtility.cs @@ -24,7 +24,7 @@ namespace Umbraco.ModelsBuilder.Umbraco // // etc... //} - public static PublishedContentType GetModelContentType(PublishedItemType itemType, string alias) + public static IPublishedContentType GetModelContentType(PublishedItemType itemType, string alias) { var facade = Current.UmbracoContext.PublishedSnapshot; // fixme inject! switch (itemType) @@ -40,7 +40,7 @@ namespace Umbraco.ModelsBuilder.Umbraco } } - public static PublishedPropertyType GetModelPropertyType(PublishedContentType contentType, Expression> selector) + public static IPublishedPropertyType GetModelPropertyType(PublishedContentType contentType, Expression> selector) //where TModel : PublishedContentModel // fixme PublishedContentModel _or_ PublishedElementModel { // fixme therefore, missing a check on TModel here From 86cf3b275f2385dc398cc50bdb1e4e1ded0a38a2 Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 27 Jun 2019 13:40:40 +0200 Subject: [PATCH 0013/1545] Some more things need to be public --- .../Validation/ContentTypeModelValidator.cs | 8 ++++---- src/Umbraco.Web/Editors/EditorValidatorCollection.cs | 2 +- .../Editors/EditorValidatorCollectionBuilder.cs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.ModelsBuilder/Validation/ContentTypeModelValidator.cs b/src/Umbraco.ModelsBuilder/Validation/ContentTypeModelValidator.cs index 3d2ce600c2..dc4649f364 100644 --- a/src/Umbraco.ModelsBuilder/Validation/ContentTypeModelValidator.cs +++ b/src/Umbraco.ModelsBuilder/Validation/ContentTypeModelValidator.cs @@ -14,24 +14,24 @@ namespace Umbraco.ModelsBuilder.Validation /// Used to validate the aliases for the content type when MB is enabled to ensure that /// no illegal aliases are used /// - internal class ContentTypeModelValidator : ContentTypeModelValidatorBase + public class ContentTypeModelValidator : ContentTypeModelValidatorBase { } /// /// Used to validate the aliases for the content type when MB is enabled to ensure that /// no illegal aliases are used /// - internal class MediaTypeModelValidator : ContentTypeModelValidatorBase + public class MediaTypeModelValidator : ContentTypeModelValidatorBase { } /// /// Used to validate the aliases for the content type when MB is enabled to ensure that /// no illegal aliases are used /// - internal class MemberTypeModelValidator : ContentTypeModelValidatorBase + public class MemberTypeModelValidator : ContentTypeModelValidatorBase { } - internal abstract class ContentTypeModelValidatorBase : EditorValidator + public abstract class ContentTypeModelValidatorBase : EditorValidator where TModel: ContentTypeSave where TProperty: PropertyTypeBasic { diff --git a/src/Umbraco.Web/Editors/EditorValidatorCollection.cs b/src/Umbraco.Web/Editors/EditorValidatorCollection.cs index 6fc6bb5de2..0e42b0027c 100644 --- a/src/Umbraco.Web/Editors/EditorValidatorCollection.cs +++ b/src/Umbraco.Web/Editors/EditorValidatorCollection.cs @@ -6,7 +6,7 @@ using Umbraco.Core.Composing; namespace Umbraco.Web.Editors { - internal class EditorValidatorCollection : BuilderCollectionBase + public class EditorValidatorCollection : BuilderCollectionBase { public EditorValidatorCollection(IEnumerable items) : base(items) diff --git a/src/Umbraco.Web/Editors/EditorValidatorCollectionBuilder.cs b/src/Umbraco.Web/Editors/EditorValidatorCollectionBuilder.cs index b3b7bab1a5..994a813cf4 100644 --- a/src/Umbraco.Web/Editors/EditorValidatorCollectionBuilder.cs +++ b/src/Umbraco.Web/Editors/EditorValidatorCollectionBuilder.cs @@ -2,7 +2,7 @@ namespace Umbraco.Web.Editors { - internal class EditorValidatorCollectionBuilder : LazyCollectionBuilderBase + public class EditorValidatorCollectionBuilder : LazyCollectionBuilderBase { protected override EditorValidatorCollectionBuilder This => this; } From bb3d2cd948703196f2df883049a1dffba107d938 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Thu, 27 Jun 2019 15:10:27 +0200 Subject: [PATCH 0014/1545] Applied custom mandatory validation message to existing core property editors that support the mandatory setting. --- .../services/validationhelper.service.js | 30 +++++++++++++ .../colorpicker/colorpicker.controller.js | 2 +- .../datepicker/datepicker.controller.js | 7 +++- .../datepicker/datepicker.html | 2 +- .../dropdownFlexible.controller.js | 8 +++- .../dropdownFlexible/dropdownFlexible.html | 42 +++++++++++-------- .../propertyeditors/email/email.controller.js | 10 +++++ .../views/propertyeditors/email/email.html | 4 +- .../radiobuttons/radiobuttons.controller.js | 8 +++- .../radiobuttons/radiobuttons.html | 18 +++++--- .../textbox/textbox.controller.js | 16 +++---- 11 files changed, 108 insertions(+), 39 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/common/services/validationhelper.service.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.controller.js diff --git a/src/Umbraco.Web.UI.Client/src/common/services/validationhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/validationhelper.service.js new file mode 100644 index 0000000000..0ab8e9fdb9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/services/validationhelper.service.js @@ -0,0 +1,30 @@ +(function () { + 'use strict'; + + function validationHelperService($q, localizationService) { + + // Gets the message to use for when a mandatory field isn't completed. + // Will either use the one provided on the property type's validation object + // or a localised default. + function getMandatoryMessage(validation) { + if (validation.mandatoryMessage) { + return $q.when(validation.mandatoryMessage); + } else { + return localizationService.localize("general_required").then(function (value) { + return $q.when(value); + }); + } + } + + var service = { + getMandatoryMessage: getMandatoryMessage + }; + + return service; + + } + + angular.module('umbraco.services').factory('validationHelper', validationHelperService); + + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.controller.js index d6fe709962..937e4e787b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.controller.js @@ -110,7 +110,7 @@ function ColorPickerController($scope) { ); return { isValid: isValid, - errorMsg: "Value cannot be empty", + errorMsg: $scope.model.validation.mandatoryMessage || "Value cannot be empty", errorKey: "required" }; } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js index 62099734fb..092f6411e8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js @@ -1,4 +1,4 @@ -function dateTimePickerController($scope, notificationsService, assetsService, angularHelper, userService, $element, dateHelper) { +function dateTimePickerController($scope, angularHelper, dateHelper, validationHelper) { let flatPickr = null; @@ -56,6 +56,11 @@ function dateTimePickerController($scope, notificationsService, assetsService, a setDatePickerVal(); + // Set the message to use for when a mandatory field isn't completed. + // Will either use the one provided on the property type or a localised default. + validationHelper.getMandatoryMessage($scope.model.validation).then(function (value) { + $scope.mandatoryMessage = value; + }); } $scope.clearDate = function() { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html index 0d3fad580e..77719e4421 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html @@ -32,7 +32,7 @@ - Required + {{mandatoryMessage}} {{datePickerForm.datepicker.errorMsg}} Invalid date diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/dropdownFlexible/dropdownFlexible.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/dropdownFlexible/dropdownFlexible.controller.js index 3b341f7ac0..d842b3b006 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/dropdownFlexible/dropdownFlexible.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/dropdownFlexible/dropdownFlexible.controller.js @@ -1,5 +1,5 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.DropdownFlexibleController", - function($scope) { + function ($scope, validationHelper) { //setup the default config var config = { @@ -89,4 +89,10 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.DropdownFlexibleCo $scope.model.value = null; } } + + // Set the message to use for when a mandatory field isn't completed. + // Will either use the one provided on the property type or a localised default. + validationHelper.getMandatoryMessage($scope.model.validation).then(function (value) { + $scope.mandatoryMessage = value; + }); }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/dropdownFlexible/dropdownFlexible.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/dropdownFlexible/dropdownFlexible.html index 5f873e9e43..617d841139 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/dropdownFlexible/dropdownFlexible.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/dropdownFlexible/dropdownFlexible.html @@ -1,21 +1,29 @@
- + + + - - + + + + + {{mandatoryMessage}} + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.controller.js new file mode 100644 index 0000000000..9c8a581dd4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.controller.js @@ -0,0 +1,10 @@ +function emailController($scope, validationHelper) { + + // Set the message to use for when a mandatory field isn't completed. + // Will either use the one provided on the property type or a localised default. + validationHelper.getMandatoryMessage($scope.model.validation).then(function(value) { + $scope.mandatoryMessage = value; + }); + +} +angular.module('umbraco').controller("Umbraco.PropertyEditors.EmailController", emailController); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html index 881ad37d7c..8576d179f2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html @@ -1,4 +1,4 @@ -
+
- Required + {{mandatoryMessage}} Invalid email {{emailFieldForm.textbox.errorMsg}} diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/radiobuttons/radiobuttons.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/radiobuttons/radiobuttons.controller.js index 3a96b6573a..13baf27f8a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/radiobuttons/radiobuttons.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/radiobuttons/radiobuttons.controller.js @@ -1,5 +1,5 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.RadioButtonsController", - function($scope) { + function ($scope, validationHelper) { function init() { @@ -19,6 +19,12 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.RadioButtonsContro $scope.configItems = configItems; } + + // Set the message to use for when a mandatory field isn't completed. + // Will either use the one provided on the property type or a localised default. + validationHelper.getMandatoryMessage($scope.model.validation).then(function (value) { + $scope.mandatoryMessage = value; + }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/radiobuttons/radiobuttons.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/radiobuttons/radiobuttons.html index 8c5f2a22d3..b5e55adebd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/radiobuttons/radiobuttons.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/radiobuttons/radiobuttons.html @@ -1,7 +1,15 @@ 
-
    -
  • - -
  • -
+ + +
    +
  • + +
  • +
+ + + {{mandatoryMessage}} + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js index 2bbd7404f7..feb0148849 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js @@ -1,4 +1,4 @@ -function textboxController($scope, localizationService) { +function textboxController($scope, validationHelper) { // macro parameter editor doesn't contains a config object, // so we create a new one to hold any properties if (!$scope.model.config) { @@ -19,14 +19,10 @@ function textboxController($scope, localizationService) { } $scope.model.change(); - $scope.mandatoryMessage = ""; - if ($scope.model.validation.mandatoryMessage) { - $scope.mandatoryMessage = $scope.model.validation.mandatoryMessage; - } else { - localizationService.localize("general_required").then(function (value) { - $scope.mandatoryMessage = value; - }); - } - + // Set the message to use for when a mandatory field isn't completed. + // Will either use the one provided on the property type or a localised default. + validationHelper.getMandatoryMessage($scope.model.validation).then(function(value) { + $scope.mandatoryMessage = value; + }); } angular.module('umbraco').controller("Umbraco.PropertyEditors.textboxController", textboxController); From 011ad47ac0709227aa3e7313966fb7c022b6d3ce Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Thu, 27 Jun 2019 20:00:52 +0200 Subject: [PATCH 0015/1545] Updated new ng-messsages elements to use tags introduced in main branch --- .../views/propertyeditors/datepicker/datepicker.html | 10 +++++----- .../dropdownFlexible/dropdownFlexible.html | 6 +++--- .../src/views/propertyeditors/email/email.html | 10 +++++----- .../propertyeditors/radiobuttons/radiobuttons.html | 6 +++--- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html index 3142f078ec..0f3ae8f510 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html @@ -32,11 +32,11 @@
- - {{mandatoryMessage}} - {{datePickerForm.datepicker.errorMsg}} - Invalid date - +
+

{{mandatoryMessage}}

+

{{datePickerForm.datepicker.errorMsg}}

+

Invalid date

+

This translates to the following time on the server: {{serverTime}}
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/dropdownFlexible/dropdownFlexible.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/dropdownFlexible/dropdownFlexible.html index 617d841139..1059df2994 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/dropdownFlexible/dropdownFlexible.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/dropdownFlexible/dropdownFlexible.html @@ -21,9 +21,9 @@ ng-options="item.value as item.value for item in model.config.items" ng-required="model.validation.mandatory"> - - {{mandatoryMessage}} - +

+

{{mandatoryMessage}}

+
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html index 8576d179f2..26ec22df8d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html @@ -8,10 +8,10 @@ ng-required="model.config.IsRequired || model.validation.mandatory" val-server="value" /> - - {{mandatoryMessage}} - Invalid email - {{emailFieldForm.textbox.errorMsg}} - +
+

{{mandatoryMessage}}

+

Invalid email

+

{{emailFieldForm.textbox.errorMsg}}

+
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/radiobuttons/radiobuttons.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/radiobuttons/radiobuttons.html index b5e55adebd..c4c76e0b40 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/radiobuttons/radiobuttons.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/radiobuttons/radiobuttons.html @@ -7,9 +7,9 @@ - - {{mandatoryMessage}} - +
+

{{mandatoryMessage}}

+
From 1168197e6652eea6cbbd11a488740d29a27e2a6f Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Thu, 27 Jun 2019 23:20:40 +0200 Subject: [PATCH 0016/1545] Fixed formatting of nested content validation --- src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs index f85171304b..c4b0333c2d 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs @@ -324,7 +324,7 @@ namespace Umbraco.Web.PropertyEditors var message = string.IsNullOrWhiteSpace(propType.MandatoryMessage) ? $"'{propType.Name}' cannot be empty" : propType.MandatoryMessage; - yield return new ValidationResult($"Item{(i + 1)}: {message}", new[] { propKey }); + yield return new ValidationResult($"Item {(i + 1)}: {message}", new[] { propKey }); } } From 8fe25ff781e2b0301f29054a4f67fde741698ce5 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Fri, 28 Jun 2019 14:46:28 +0200 Subject: [PATCH 0017/1545] Added null check to resolve failing front-end unit test --- .../src/common/services/validationhelper.service.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/validationhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/validationhelper.service.js index 0ab8e9fdb9..16ea5a60a5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/validationhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/validationhelper.service.js @@ -7,6 +7,10 @@ // Will either use the one provided on the property type's validation object // or a localised default. function getMandatoryMessage(validation) { + if (!validation) { + return $q.when(""); + } + if (validation.mandatoryMessage) { return $q.when(validation.mandatoryMessage); } else { From d31e71a838abbbe5391049ad9cfc331ec31e895b Mon Sep 17 00:00:00 2001 From: skttl Date: Fri, 12 Jul 2019 07:33:17 +0200 Subject: [PATCH 0018/1545] 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 5d5bd54aee50a92fef3e096c1c97524c958b1c4f Mon Sep 17 00:00:00 2001 From: skttl Date: Fri, 12 Jul 2019 21:07:55 +0200 Subject: [PATCH 0019/1545] 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: Sun, 21 Jul 2019 15:47:37 +0200 Subject: [PATCH 0020/1545] 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 342b2087e999f508adcfdcbf4000504dbdf0f0f4 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Fri, 12 Jul 2019 19:20:15 +0200 Subject: [PATCH 0021/1545] 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 186460ac08de66428a54357857c630093b8c9674 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 24 Jul 2019 19:23:05 +1000 Subject: [PATCH 0022/1545] 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 361557dc39969299edb0f57d89b47d0e76d31369 Mon Sep 17 00:00:00 2001 From: Steve Megson Date: Sun, 21 Jul 2019 15:19:17 +0100 Subject: [PATCH 0023/1545] 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 0024/1545] 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 0025/1545] 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 0ab84fc44357928e580329f19a6ede5da9ce365a Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 25 Jul 2019 10:28:16 +0200 Subject: [PATCH 0026/1545] 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 41093e5b8b59ecd7a71dae7ec3fddb5e672e9a2d Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 29 Jul 2019 15:38:08 +1000 Subject: [PATCH 0027/1545] 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 0028/1545] 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 ca56655d07f86d5b21abb2f6d5560e98be2d05e5 Mon Sep 17 00:00:00 2001 From: Rasmus John Pedersen Date: Mon, 29 Jul 2019 12:59:29 +0200 Subject: [PATCH 0029/1545] 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 0030/1545] 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 0031/1545] 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 0032/1545] 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 0033/1545] 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 0034/1545] 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 0035/1545] 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 0036/1545] 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 0037/1545] 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 0038/1545] 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 0039/1545] 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 0040/1545] 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 0041/1545] 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 0042/1545] 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 0043/1545] 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 0044/1545] 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 0045/1545] 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 0046/1545] 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 0047/1545] 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 0048/1545] 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 0049/1545] 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 0050/1545] 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 0051/1545] 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 0052/1545] 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 0053/1545] 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 0054/1545] 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 0055/1545] 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 0056/1545] 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 0057/1545] 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 0058/1545] 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 0059/1545] 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 0060/1545] 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 0061/1545] 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 0062/1545] 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 0063/1545] 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 0064/1545] 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 0065/1545] 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 0066/1545] 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 0067/1545] 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 0068/1545] 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 0286/1545] =?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 0287/1545] =?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 0288/1545] 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 0289/1545] 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 0290/1545] 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 0291/1545] 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 0292/1545] 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 0293/1545] 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 0294/1545] 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 0295/1545] 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 0296/1545] 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 0297/1545] 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 0298/1545] 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 0299/1545] 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 @@ + -
- - + + - - + + +