From c4b5483d8c1c5c0b44e356ee856319ec1821fa76 Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Thu, 4 Jul 2019 11:19:10 +1000 Subject: [PATCH 01/67] make log naming and colors consistent between views --- .../views/logviewer/overview.controller.js | 12 +++---- .../src/views/logviewer/overview.html | 2 +- .../src/views/logviewer/search.controller.js | 35 ++++++------------- 3 files changed, 17 insertions(+), 32 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.controller.js b/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.controller.js index fc24fbe1bc..5cee21668e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.controller.js @@ -12,9 +12,9 @@ vm.commonLogMessagesCount = 10; // ChartJS Options - for count/overview of log distribution - vm.logTypeLabels = ["Info", "Debug", "Warning", "Error", "Critical"]; + vm.logTypeLabels = ["Debug", "Info", "Warning", "Error", "Fatal"]; vm.logTypeData = [0, 0, 0, 0, 0]; - vm.logTypeColors = [ '#dcdcdc', '#97bbcd', '#46bfbd', '#fdb45c', '#f7464a']; + vm.logTypeColors = ['#eaddd5', '#2bc37c', '#3544b1', '#ff9412', '#d42054']; vm.chartOptions = { legend: { display: true, @@ -74,7 +74,7 @@ "query": "Not(@Level='Verbose') and Not(@Level='Debug')" }, { - "name": "Find all logs that has an exception property (Warning, Error & Critical with Exceptions)", + "name": "Find all logs that has an exception property (Warning, Error & Fatal with Exceptions)", "query": "Has(@Exception)" }, { @@ -113,8 +113,8 @@ vm.commonLogMessages = data; }); - //Set loading indicatior to false when these 3 queries complete - $q.all([savedSearches, numOfErrors, logCounts, commonMsgs]).then(function(data) { + //Set loading indicator to false when these 3 queries complete + $q.all([savedSearches, numOfErrors, logCounts, commonMsgs]).then(function() { vm.loading = false; }); @@ -148,7 +148,7 @@ conjunction: " to " }; - vm.dateRangeChange = function(selectedDates, dateStr, instance) { + vm.dateRangeChange = function(selectedDates) { if(selectedDates.length > 0){ vm.startDate = selectedDates[0].toIsoDateString(); diff --git a/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.html b/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.html index 854bed755f..896cb17da7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.html +++ b/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.html @@ -78,7 +78,7 @@ class="datepicker" ng-model="vm.period" options="vm.config" - on-close="vm.dateRangeChange(selectedDates, dateStr, instance)"> + on-close="vm.dateRangeChange(selectedDates)"> diff --git a/src/Umbraco.Web.UI.Client/src/views/logviewer/search.controller.js b/src/Umbraco.Web.UI.Client/src/views/logviewer/search.controller.js index d4b0ea8f8e..fb627855b6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/logviewer/search.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/logviewer/search.controller.js @@ -11,26 +11,28 @@ vm.showBackButton = true; vm.page = {}; + // this array is also used to map the logTypeColor param onto the log items + // in setLogTypeColors() vm.logLevels = [ { name: 'Verbose', - logTypeColor: 'gray' + logTypeColor: '' }, { name: 'Debug', - logTypeColor: 'secondary' + logTypeColor: 'gray' }, { name: 'Information', - logTypeColor: 'primary' + logTypeColor: 'success' }, { name: 'Warning', - logTypeColor: 'warning' + logTypeColor: 'primary' }, { name: 'Error', - logTypeColor: 'danger' + logTypeColor: 'warning' }, { name: 'Fatal', @@ -118,7 +120,7 @@ "query": "Not(@Level='Verbose') and Not(@Level='Debug')" }, { - "name": "Find all logs that has an exception property (Warning, Error & Critical with Exceptions)", + "name": "Find all logs that has an exception property (Warning, Error & Fatal with Exceptions)", "query": "Has(@Exception)" }, { @@ -173,25 +175,8 @@ } function setLogTypeColor(logItems) { - angular.forEach(logItems, function (log) { - switch (log.Level) { - case "Information": - log.logTypeColor = "primary"; - break; - case "Debug": - log.logTypeColor = "secondary"; - break; - case "Warning": - log.logTypeColor = "warning"; - break; - case "Fatal": - case "Error": - log.logTypeColor = "danger"; - break; - default: - log.logTypeColor = "gray"; - } - }); + logItems.forEach(logItem => + logItem.logTypeColor = vm.logLevels.find(x => x.name === logItem.Level).logTypeColor); } function getFilterName(array) { From a0bcfeddcc6631a48d73dfb6fd3e88a96fa7d146 Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Thu, 4 Jul 2019 11:48:02 +1000 Subject: [PATCH 02/67] fix date range formatting on flatpickr - 'to' wasnt displayed after choosing a range --- .../views/logviewer/overview.controller.js | 24 ++++++++++++------- .../src/views/logviewer/overview.html | 2 +- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.controller.js b/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.controller.js index 5cee21668e..79ac8af5f8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.controller.js @@ -43,14 +43,20 @@ vm.searchLogQuery = searchLogQuery; vm.findMessageTemplate = findMessageTemplate; - function preFlightCheck(){ - vm.loading = true; + function preFlightCheck(instance){ + vm.loading = true; + + //Do our pre-flight check (to see if we can view logs) //IE the log file is NOT too big such as 1GB & crash the site logViewerResource.canViewLogs(vm.startDate, vm.endDate).then(function(result){ vm.loading = false; vm.canLoadLogs = result; + if (instance) { + instance.setDate(vm.period); + } + if(result){ //Can view logs - so initalise init(); @@ -117,7 +123,7 @@ $q.all([savedSearches, numOfErrors, logCounts, commonMsgs]).then(function() { vm.loading = false; }); - + $timeout(function () { navigationService.syncTree({ tree: "logViewer", path: "-1" }); }); @@ -146,9 +152,9 @@ mode: "range", maxDate: "today", conjunction: " to " - }; - - vm.dateRangeChange = function(selectedDates) { + }; + + vm.dateRangeChange = function(selectedDates, dateStr, instance) { if(selectedDates.length > 0){ vm.startDate = selectedDates[0].toIsoDateString(); @@ -158,9 +164,9 @@ vm.period = [vm.startDate]; }else{ vm.period = [vm.startDate, vm.endDate]; - } - - preFlightCheck(); + } + + preFlightCheck(instance); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.html b/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.html index 896cb17da7..854bed755f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.html +++ b/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.html @@ -78,7 +78,7 @@ class="datepicker" ng-model="vm.period" options="vm.config" - on-close="vm.dateRangeChange(selectedDates)"> + on-close="vm.dateRangeChange(selectedDates, dateStr, instance)"> From 2cd01735343e49e853555d662ad6e9493ee69a45 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Thu, 4 Jul 2019 14:35:06 +0200 Subject: [PATCH 03/67] Fixed incorrect label for "Fatal" errors on the log viewer pie chart. Made the log viewer title variable so it either says "Log Overview For Today" or "Log Overview For Selected Time Period" (previously it said the former even if you'd adjusted the dates you were looking at. Fixed up the selected date period display so the conjunction "to" is used between the dates when the selected time period is changed and querystring is updated. Minor code tidy-up. --- .../views/logviewer/overview.controller.js | 134 +++++++++--------- .../src/views/logviewer/overview.html | 11 +- .../Editors/LogViewerController.cs | 6 +- 3 files changed, 77 insertions(+), 74 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.controller.js b/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.controller.js index fc24fbe1bc..0afad09b81 100644 --- a/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.controller.js @@ -10,11 +10,12 @@ vm.numberOfErrors = 0; vm.commonLogMessages = []; vm.commonLogMessagesCount = 10; + vm.dateRangeLabel = ""; // ChartJS Options - for count/overview of log distribution - vm.logTypeLabels = ["Info", "Debug", "Warning", "Error", "Critical"]; + vm.logTypeLabels = ["Info", "Debug", "Warning", "Error", "Fatal"]; vm.logTypeData = [0, 0, 0, 0, 0]; - vm.logTypeColors = [ '#dcdcdc', '#97bbcd', '#46bfbd', '#fdb45c', '#f7464a']; + vm.logTypeColors = ["#dcdcdc", "#97bbcd", "#46bfbd", "#fdb45c", "#f7464a"]; vm.chartOptions = { legend: { display: true, @@ -23,35 +24,41 @@ }; let querystring = $location.search(); - if(querystring.startDate){ + if (querystring.startDate) { vm.startDate = querystring.startDate; - }else{ + vm.dateRangeLabel = getDateRangeLabel("Selected Time Period"); + } else { vm.startDate = new Date(Date.now()); - vm.startDate.setDate(vm.startDate.getDate()-1); + vm.startDate.setDate(vm.startDate.getDate() - 1); vm.startDate = vm.startDate.toIsoDateString(); + vm.dateRangeLabel = getDateRangeLabel("Today"); } - if(querystring.endDate){ + if (querystring.endDate) { vm.endDate = querystring.endDate; - }else{ + + if (querystring.endDate === querystring.startDate) { + vm.dateRangeLabel = getDateRangeLabel("Selected Date"); + } + } else { vm.endDate = new Date(Date.now()).toIsoDateString(); } - vm.period = [vm.startDate, vm.endDate]; + vm.period = [vm.startDate, vm.endDate]; //functions vm.searchLogQuery = searchLogQuery; vm.findMessageTemplate = findMessageTemplate; - - function preFlightCheck(){ + + function preFlightCheck() { vm.loading = true; //Do our pre-flight check (to see if we can view logs) //IE the log file is NOT too big such as 1GB & crash the site - logViewerResource.canViewLogs(vm.startDate, vm.endDate).then(function(result){ + logViewerResource.canViewLogs(vm.startDate, vm.endDate).then(function (result) { vm.loading = false; vm.canLoadLogs = result; - if(result){ + if (result) { //Can view logs - so initalise init(); } @@ -62,39 +69,39 @@ function init() { vm.loading = true; - + var savedSearches = logViewerResource.getSavedSearches().then(function (data) { vm.searches = data; }, - // fallback to some defaults if error from API response - function () { - vm.searches = [ - { - "name": "Find all logs where the Level is NOT Verbose and NOT Debug", - "query": "Not(@Level='Verbose') and Not(@Level='Debug')" - }, - { - "name": "Find all logs that has an exception property (Warning, Error & Critical with Exceptions)", - "query": "Has(@Exception)" - }, - { - "name": "Find all logs that have the property 'Duration'", - "query": "Has(Duration)" - }, - { - "name": "Find all logs that have the property 'Duration' and the duration is greater than 1000ms", - "query": "Has(Duration) and Duration > 1000" - }, - { - "name": "Find all logs that are from the namespace 'Umbraco.Core'", - "query": "StartsWith(SourceContext, 'Umbraco.Core')" - }, - { - "name": "Find all logs that use a specific log message template", - "query": "@MessageTemplate = '[Timing {TimingId}] {EndMessage} ({TimingDuration}ms)'" - } - ] - }); + // fallback to some defaults if error from API response + function () { + vm.searches = [ + { + "name": "Find all logs where the Level is NOT Verbose and NOT Debug", + "query": "Not(@Level='Verbose') and Not(@Level='Debug')" + }, + { + "name": "Find all logs that has an exception property (Warning, Error & Critical with Exceptions)", + "query": "Has(@Exception)" + }, + { + "name": "Find all logs that have the property 'Duration'", + "query": "Has(Duration)" + }, + { + "name": "Find all logs that have the property 'Duration' and the duration is greater than 1000ms", + "query": "Has(Duration) and Duration > 1000" + }, + { + "name": "Find all logs that are from the namespace 'Umbraco.Core'", + "query": "StartsWith(SourceContext, 'Umbraco.Core')" + }, + { + "name": "Find all logs that use a specific log message template", + "query": "@MessageTemplate = '[Timing {TimingId}] {EndMessage} ({TimingDuration}ms)'" + } + ]; + }); var numOfErrors = logViewerResource.getNumberOfErrors(vm.startDate, vm.endDate).then(function (data) { vm.numberOfErrors = data; @@ -109,12 +116,12 @@ vm.logTypeData.push(data.Fatal); }); - var commonMsgs = logViewerResource.getMessageTemplates(vm.startDate, vm.endDate).then(function(data){ + var commonMsgs = logViewerResource.getMessageTemplates(vm.startDate, vm.endDate).then(function (data) { vm.commonLogMessages = data; }); //Set loading indicatior to false when these 3 queries complete - $q.all([savedSearches, numOfErrors, logCounts, commonMsgs]).then(function(data) { + $q.all([savedSearches, numOfErrors, logCounts, commonMsgs]).then(function (data) { vm.loading = false; }); @@ -123,20 +130,21 @@ }); } - function searchLogQuery(logQuery){ - $location.path("/settings/logViewer/search").search({lq: logQuery, startDate: vm.startDate, endDate: vm.endDate}); + function searchLogQuery(logQuery) { + $location.path("/settings/logViewer/search").search({ lq: logQuery, startDate: vm.startDate, endDate: vm.endDate }); } - function findMessageTemplate(template){ + function findMessageTemplate(template) { var logQuery = "@MessageTemplate='" + template.MessageTemplate + "'"; searchLogQuery(logQuery); } - - - - preFlightCheck(); + function getDateRangeLabel(suffix) { + return "Log Overview for " + suffix; + } + preFlightCheck(); + ///////////////////// vm.config = { @@ -147,20 +155,18 @@ maxDate: "today", conjunction: " to " }; - - vm.dateRangeChange = function(selectedDates, dateStr, instance) { - - if(selectedDates.length > 0){ - vm.startDate = selectedDates[0].toIsoDateString(); - vm.endDate = selectedDates[selectedDates.length-1].toIsoDateString(); // Take the last date as end - if(vm.startDate === vm.endDate){ - vm.period = [vm.startDate]; - }else{ - vm.period = [vm.startDate, vm.endDate]; - } - - preFlightCheck(); + vm.dateRangeChange = function (selectedDates, dateStr, instance) { + + if (selectedDates.length > 0) { + + // Update view by re-requesting route with updated querystring. + // By doing this we make sure the URL matches the selected time period, aiding sharing the link. + // Also resolves a minor layout issue where the " to " conjunction between the selected dates + // is collapsed to a comma. + const startDate = selectedDates[0].toIsoDateString(); + const endDate = selectedDates[selectedDates.length - 1].toIsoDateString(); // Take the last date as end + $location.path("/settings/logViewer/overview").search({ startDate: startDate, endDate: endDate }); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.html b/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.html index 854bed755f..7648bbf162 100644 --- a/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.html +++ b/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.html @@ -3,7 +3,7 @@ - + diff --git a/src/Umbraco.Web/Editors/LogViewerController.cs b/src/Umbraco.Web/Editors/LogViewerController.cs index 79eb3bb312..d9fcfd108a 100644 --- a/src/Umbraco.Web/Editors/LogViewerController.cs +++ b/src/Umbraco.Web/Editors/LogViewerController.cs @@ -15,11 +15,11 @@ namespace Umbraco.Web.Editors [PluginController("UmbracoApi")] public class LogViewerController : UmbracoAuthorizedJsonController { - private ILogViewer _logViewer; + private readonly ILogViewer _logViewer; public LogViewerController(ILogViewer logViewer) { - _logViewer = logViewer; + _logViewer = logViewer ?? throw new ArgumentNullException(nameof(logViewer)); } private bool CanViewLogs(LogTimePeriod logTimePeriod) @@ -91,8 +91,6 @@ namespace Umbraco.Web.Editors var direction = orderDirection == "Descending" ? Direction.Descending : Direction.Ascending; - - return _logViewer.GetLogs(logTimePeriod, filterExpression: filterExpression, pageNumber: pageNumber, orderDirection: direction, logLevels: logLevels); } From 5ada85df29dee4793eff496d70e674b881c07e85 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Fri, 5 Jul 2019 12:11:03 +0200 Subject: [PATCH 04/67] Provide the correct assembly redirects --- build/NuSpecs/tools/Web.config.install.xdt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/build/NuSpecs/tools/Web.config.install.xdt b/build/NuSpecs/tools/Web.config.install.xdt index 4f8a1927a8..f0d194d341 100644 --- a/build/NuSpecs/tools/Web.config.install.xdt +++ b/build/NuSpecs/tools/Web.config.install.xdt @@ -346,7 +346,7 @@ - + @@ -358,11 +358,11 @@ - + - + @@ -370,27 +370,27 @@ - + - + - + - + - + - + From 80d7f1b2c99eb0332446e70170ebe78bbb44e160 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sun, 7 Jul 2019 11:36:26 +0200 Subject: [PATCH 05/67] Make it possible to save a member without resetting the 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 339358dbf2..eb99e46a1f 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.value.reset) { + if (passwordProp && passwordProp.value && !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 df896af47672b37b70526492e197c98077ce7d1e Mon Sep 17 00:00:00 2001 From: Claus Date: Wed, 10 Jul 2019 12:58:15 +0200 Subject: [PATCH 06/67] adding build targets package. --- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 2 ++ src/Umbraco.Web.UI/packages.config | 1 + 2 files changed, 3 insertions(+) diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index a0624a2dd7..646b86f665 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -1,5 +1,6 @@  + @@ -1088,5 +1089,6 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.1\x86\*.* "$(TargetDir)x86\" + \ No newline at end of file diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config index 18cbfdb042..a8271c1370 100644 --- a/src/Umbraco.Web.UI/packages.config +++ b/src/Umbraco.Web.UI/packages.config @@ -32,6 +32,7 @@ + From 89bb94aa1ae542b17bf4f3672f0f00e69b37e5bc Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 12 Jul 2019 10:57:26 +1000 Subject: [PATCH 07/67] Fixes issue during upgrade where umbraco can't clear cdf log files --- .../JavaScript/ClientDependencyConfiguration.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web/JavaScript/ClientDependencyConfiguration.cs b/src/Umbraco.Web/JavaScript/ClientDependencyConfiguration.cs index 777786675f..2bf069b06d 100644 --- a/src/Umbraco.Web/JavaScript/ClientDependencyConfiguration.cs +++ b/src/Umbraco.Web/JavaScript/ClientDependencyConfiguration.cs @@ -106,8 +106,10 @@ namespace Umbraco.Web.JavaScript } try - { - var fullPath = currentHttpContext.Server.MapPath(XmlFileMapper.FileMapDefaultFolder); + { + var fullPath = XmlFileMapper.FileMapDefaultFolder.StartsWith("~/") + ? currentHttpContext.Server.MapPath(XmlFileMapper.FileMapDefaultFolder) + : XmlFileMapper.FileMapDefaultFolder; if (fullPath != null) { cdfTempDirectories.Add(fullPath); @@ -122,13 +124,12 @@ namespace Umbraco.Web.JavaScript var success = true; foreach (var directory in cdfTempDirectories) { - var directoryInfo = new DirectoryInfo(directory); - if (directoryInfo.Exists == false) - continue; - try { - directoryInfo.Delete(true); + if (!Directory.Exists(directory)) + continue; + + Directory.Delete(directory, true); } catch (Exception ex) { From 2252db0d55ae0c7f4ddba19bde6f408dbfb9eb93 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 12 Jul 2019 13:40:21 +1000 Subject: [PATCH 08/67] Fixes 5822 by reinstating optional method overloads of loadBaseType --- src/Umbraco.Core/Services/EntityService.cs | 28 +++++++++++++++++++ src/Umbraco.Core/Services/IEntityService.cs | 26 ++++++++++++++++- src/Umbraco.Core/Services/IRelationService.cs | 27 ++++++++++++++++++ src/Umbraco.Core/Services/RelationService.cs | 27 ++++++++++++++++++ 4 files changed, 107 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs index f4b1b71732..84f5e2d9fd 100644 --- a/src/Umbraco.Core/Services/EntityService.cs +++ b/src/Umbraco.Core/Services/EntityService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Linq.Expressions; using Umbraco.Core.CodeAnnotations; @@ -717,5 +718,32 @@ namespace Umbraco.Core.Services } return node.NodeId; } + + #region Obsolete - only here for compat + + [Obsolete("Use the overload that doesn't specify loadBaseType instead, loadBaseType will not affect any results")] + [EditorBrowsable(EditorBrowsableState.Never)] + public IUmbracoEntity GetByKey(Guid key, bool loadBaseType = true) => GetByKey(key); + + [Obsolete("Use the overload that doesn't specify loadBaseType instead, loadBaseType will not affect any results")] + [EditorBrowsable(EditorBrowsableState.Never)] + public IUmbracoEntity Get(int id, bool loadBaseType = true) => Get(id); + + [Obsolete("Use the overload that doesn't specify loadBaseType instead, loadBaseType will not affect any results")] + [EditorBrowsable(EditorBrowsableState.Never)] + public IUmbracoEntity GetByKey(Guid key, UmbracoObjectTypes umbracoObjectType, bool loadBaseType = true) => GetByKey(key, umbracoObjectType); + + [Obsolete("Use the overload that doesn't specify loadBaseType instead, loadBaseType will not affect any results")] + [EditorBrowsable(EditorBrowsableState.Never)] + public IUmbracoEntity GetByKey(Guid key, bool loadBaseType = true) where T : IUmbracoEntity => GetByKey(key); + + [Obsolete("Use the overload that doesn't specify loadBaseType instead, loadBaseType will not affect any results")] + [EditorBrowsable(EditorBrowsableState.Never)] + public IUmbracoEntity Get(int id, UmbracoObjectTypes umbracoObjectType, bool loadBaseType = true) => Get(id, umbracoObjectType); + + [Obsolete("Use the overload that doesn't specify loadBaseType instead, loadBaseType will not affect any results")] + [EditorBrowsable(EditorBrowsableState.Never)] + public IUmbracoEntity Get(int id, bool loadBaseType = true) where T : IUmbracoEntity => Get(id); + #endregion } } diff --git a/src/Umbraco.Core/Services/IEntityService.cs b/src/Umbraco.Core/Services/IEntityService.cs index a6a2254138..a0c7363c60 100644 --- a/src/Umbraco.Core/Services/IEntityService.cs +++ b/src/Umbraco.Core/Services/IEntityService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Persistence.DatabaseModelDefinitions; @@ -55,6 +56,10 @@ namespace Umbraco.Core.Services /// An IUmbracoEntity GetByKey(Guid key); + [Obsolete("Use the overload that doesn't specify loadBaseType instead, loadBaseType will not affect any results")] + [EditorBrowsable(EditorBrowsableState.Never)] + IUmbracoEntity GetByKey(Guid key, bool loadBaseType = true); + /// /// Gets an UmbracoEntity by its Id, and optionally loads the complete object graph. /// @@ -62,9 +67,13 @@ namespace Umbraco.Core.Services /// By default this will load the base type with a minimum set of properties. /// /// Id of the object to retrieve - /// An + /// An IUmbracoEntity Get(int id); + [Obsolete("Use the overload that doesn't specify loadBaseType instead, loadBaseType will not affect any results")] + [EditorBrowsable(EditorBrowsableState.Never)] + IUmbracoEntity Get(int id, bool loadBaseType = true); + /// /// Gets an UmbracoEntity by its Id and UmbracoObjectType, and optionally loads the complete object graph. /// @@ -76,6 +85,14 @@ namespace Umbraco.Core.Services /// An IUmbracoEntity GetByKey(Guid key, UmbracoObjectTypes umbracoObjectType); + [Obsolete("Use the overload that doesn't specify loadBaseType instead, loadBaseType will not affect any results")] + [EditorBrowsable(EditorBrowsableState.Never)] + IUmbracoEntity GetByKey(Guid key, UmbracoObjectTypes umbracoObjectType, bool loadBaseType = true); + + [Obsolete("Use the overload that doesn't specify loadBaseType instead, loadBaseType will not affect any results")] + [EditorBrowsable(EditorBrowsableState.Never)] + IUmbracoEntity GetByKey(Guid key, bool loadBaseType = true) where T : IUmbracoEntity; + /// /// Gets an UmbracoEntity by its Id and UmbracoObjectType, and optionally loads the complete object graph. /// @@ -87,6 +104,9 @@ namespace Umbraco.Core.Services /// An IUmbracoEntity Get(int id, UmbracoObjectTypes umbracoObjectType); + [Obsolete("Use the overload that doesn't specify loadBaseType instead, loadBaseType will not affect any results")] + [EditorBrowsable(EditorBrowsableState.Never)] + IUmbracoEntity Get(int id, UmbracoObjectTypes umbracoObjectType, bool loadBaseType = true); /// /// Gets an UmbracoEntity by its Id and specified Type. Optionally loads the complete object graph. @@ -99,6 +119,10 @@ namespace Umbraco.Core.Services /// An IUmbracoEntity Get(int id) where T : IUmbracoEntity; + [Obsolete("Use the overload that doesn't specify loadBaseType instead, loadBaseType will not affect any results")] + [EditorBrowsable(EditorBrowsableState.Never)] + IUmbracoEntity Get(int id, bool loadBaseType = true) where T : IUmbracoEntity; + /// /// Gets the parent of entity by its id /// diff --git a/src/Umbraco.Core/Services/IRelationService.cs b/src/Umbraco.Core/Services/IRelationService.cs index b5ca9ee019..819952b697 100644 --- a/src/Umbraco.Core/Services/IRelationService.cs +++ b/src/Umbraco.Core/Services/IRelationService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; @@ -145,6 +146,10 @@ namespace Umbraco.Core.Services /// An IUmbracoEntity GetChildEntityFromRelation(IRelation relation); + [Obsolete("Use the overload that doesn't specify loadBaseType instead, loadBaseType will not affect any results")] + [EditorBrowsable(EditorBrowsableState.Never)] + IUmbracoEntity GetChildEntityFromRelation(IRelation relation, bool loadBaseType = false); + /// /// Gets the Parent object from a Relation as an /// @@ -152,6 +157,10 @@ namespace Umbraco.Core.Services /// An IUmbracoEntity GetParentEntityFromRelation(IRelation relation); + [Obsolete("Use the overload that doesn't specify loadBaseType instead, loadBaseType will not affect any results")] + [EditorBrowsable(EditorBrowsableState.Never)] + IUmbracoEntity GetParentEntityFromRelation(IRelation relation, bool loadBaseType = false); + /// /// Gets the Parent and Child objects from a Relation as a "/> with . /// @@ -159,6 +168,10 @@ namespace Umbraco.Core.Services /// Returns a Tuple with Parent (item1) and Child (item2) Tuple GetEntitiesFromRelation(IRelation relation); + [Obsolete("Use the overload that doesn't specify loadBaseType instead, loadBaseType will not affect any results")] + [EditorBrowsable(EditorBrowsableState.Never)] + Tuple GetEntitiesFromRelation(IRelation relation, bool loadBaseType = false); + /// /// Gets the Child objects from a list of Relations as a list of objects. /// @@ -166,6 +179,10 @@ namespace Umbraco.Core.Services /// An enumerable list of IEnumerable GetChildEntitiesFromRelations(IEnumerable relations); + [Obsolete("Use the overload that doesn't specify loadBaseType instead, loadBaseType will not affect any results")] + [EditorBrowsable(EditorBrowsableState.Never)] + IEnumerable GetChildEntitiesFromRelations(IEnumerable relations, bool loadBaseType = false); + /// /// Gets the Parent objects from a list of Relations as a list of objects. /// @@ -173,6 +190,10 @@ namespace Umbraco.Core.Services /// An enumerable list of IEnumerable GetParentEntitiesFromRelations(IEnumerable relations); + [Obsolete("Use the overload that doesn't specify loadBaseType instead, loadBaseType will not affect any results")] + [EditorBrowsable(EditorBrowsableState.Never)] + IEnumerable GetParentEntitiesFromRelations(IEnumerable relations, bool loadBaseType = false); + /// /// Gets the Parent and Child objects from a list of Relations as a list of objects. /// @@ -181,6 +202,12 @@ namespace Umbraco.Core.Services IEnumerable> GetEntitiesFromRelations( IEnumerable relations); + [Obsolete("Use the overload that doesn't specify loadBaseType instead, loadBaseType will not affect any results")] + [EditorBrowsable(EditorBrowsableState.Never)] + IEnumerable> GetEntitiesFromRelations( + IEnumerable relations, + bool loadBaseType = false); + /// /// Relates two objects by their entity Ids. /// diff --git a/src/Umbraco.Core/Services/RelationService.cs b/src/Umbraco.Core/Services/RelationService.cs index a940096ded..7ddd2ecf12 100644 --- a/src/Umbraco.Core/Services/RelationService.cs +++ b/src/Umbraco.Core/Services/RelationService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using Umbraco.Core.Events; using Umbraco.Core.Logging; @@ -736,5 +737,31 @@ namespace Umbraco.Core.Services /// public static event TypedEventHandler> SavedRelationType; #endregion + + #region Obsolete - only here for compat + [Obsolete("Use the overload that doesn't specify loadBaseType instead, loadBaseType will not affect any results")] + [EditorBrowsable(EditorBrowsableState.Never)] + public IUmbracoEntity GetChildEntityFromRelation(IRelation relation, bool loadBaseType = false) => GetChildEntityFromRelation(relation); + + [Obsolete("Use the overload that doesn't specify loadBaseType instead, loadBaseType will not affect any results")] + [EditorBrowsable(EditorBrowsableState.Never)] + public IUmbracoEntity GetParentEntityFromRelation(IRelation relation, bool loadBaseType = false) => GetParentEntityFromRelation(relation); + + [Obsolete("Use the overload that doesn't specify loadBaseType instead, loadBaseType will not affect any results")] + [EditorBrowsable(EditorBrowsableState.Never)] + public Tuple GetEntitiesFromRelation(IRelation relation, bool loadBaseType = false) => GetEntitiesFromRelation(relation); + + [Obsolete("Use the overload that doesn't specify loadBaseType instead, loadBaseType will not affect any results")] + [EditorBrowsable(EditorBrowsableState.Never)] + public IEnumerable GetChildEntitiesFromRelations(IEnumerable relations, bool loadBaseType = false) => GetChildEntitiesFromRelations(relations); + + [Obsolete("Use the overload that doesn't specify loadBaseType instead, loadBaseType will not affect any results")] + [EditorBrowsable(EditorBrowsableState.Never)] + public IEnumerable GetParentEntitiesFromRelations(IEnumerable relations, bool loadBaseType = false) => GetParentEntitiesFromRelations(relations); + + [Obsolete("Use the overload that doesn't specify loadBaseType instead, loadBaseType will not affect any results")] + [EditorBrowsable(EditorBrowsableState.Never)] + public IEnumerable> GetEntitiesFromRelations(IEnumerable relations, bool loadBaseType = false) => GetEntitiesFromRelations(relations); + #endregion } } From cf49e6160a691e1646cbebc139950f03b554dffe Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Fri, 12 Jul 2019 14:20:28 +1000 Subject: [PATCH 09/67] critical => fatal --- src/Umbraco.Web.UI/config/logviewer.searches.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI/config/logviewer.searches.config.js b/src/Umbraco.Web.UI/config/logviewer.searches.config.js index 25ee9b2242..345fe23764 100644 --- a/src/Umbraco.Web.UI/config/logviewer.searches.config.js +++ b/src/Umbraco.Web.UI/config/logviewer.searches.config.js @@ -4,7 +4,7 @@ "query": "Not(@Level='Verbose') and Not(@Level='Debug')" }, { - "name": "Find all logs that has an exception property (Warning, Error & Critical with Exceptions)", + "name": "Find all logs that has an exception property (Warning, Error & Fatal with Exceptions)", "query": "Has(@Exception)" }, { From 3c2a92d3b630bdaf23ce20331b2501c21820adb0 Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Fri, 12 Jul 2019 14:21:28 +1000 Subject: [PATCH 10/67] map log values into logtypedata array - removes need to add these in the correct order. --- .../views/logviewer/overview.controller.js | 82 +++++++++++-------- 1 file changed, 47 insertions(+), 35 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.controller.js b/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.controller.js index 830a9f5c39..86d72feffd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.controller.js @@ -71,37 +71,37 @@ vm.loading = true; var savedSearches = logViewerResource.getSavedSearches().then(function (data) { - vm.searches = data; - }, - // fallback to some defaults if error from API response - function () { - vm.searches = [ - { - "name": "Find all logs where the Level is NOT Verbose and NOT Debug", - "query": "Not(@Level='Verbose') and Not(@Level='Debug')" + vm.searches = data; + }, + // fallback to some defaults if error from API response + function () { + vm.searches = [ + { + "name": "Find all logs where the Level is NOT Verbose and NOT Debug", + "query": "Not(@Level='Verbose') and Not(@Level='Debug')" }, - { - "name": "Find all logs that has an exception property (Warning, Error & Fatal with Exceptions)", - "query": "Has(@Exception)" + { + "name": "Find all logs that has an exception property (Warning, Error & Fatal with Exceptions)", + "query": "Has(@Exception)" }, - { - "name": "Find all logs that have the property 'Duration'", - "query": "Has(Duration)" + { + "name": "Find all logs that have the property 'Duration'", + "query": "Has(Duration)" }, - { - "name": "Find all logs that have the property 'Duration' and the duration is greater than 1000ms", - "query": "Has(Duration) and Duration > 1000" + { + "name": "Find all logs that have the property 'Duration' and the duration is greater than 1000ms", + "query": "Has(Duration) and Duration > 1000" }, - { - "name": "Find all logs that are from the namespace 'Umbraco.Core'", - "query": "StartsWith(SourceContext, 'Umbraco.Core')" + { + "name": "Find all logs that are from the namespace 'Umbraco.Core'", + "query": "StartsWith(SourceContext, 'Umbraco.Core')" }, - { - "name": "Find all logs that use a specific log message template", - "query": "@MessageTemplate = '[Timing {TimingId}] {EndMessage} ({TimingDuration}ms)'" + { + "name": "Find all logs that use a specific log message template", + "query": "@MessageTemplate = '[Timing {TimingId}] {EndMessage} ({TimingDuration}ms)'" } ] - }); + }); var numOfErrors = logViewerResource.getNumberOfErrors(vm.startDate, vm.endDate).then(function (data) { vm.numberOfErrors = data; @@ -109,11 +109,13 @@ var logCounts = logViewerResource.getLogLevelCounts(vm.startDate, vm.endDate).then(function (data) { vm.logTypeData = []; - vm.logTypeData.push(data.Information); - vm.logTypeData.push(data.Debug); - vm.logTypeData.push(data.Warning); - vm.logTypeData.push(data.Error); - vm.logTypeData.push(data.Fatal); + + for (let [key, value] of Object.entries(data)) { + const index = vm.logTypeLabels.findIndex(x => key.startsWith(x)); + if (index > -1 && index < vm.logTypeData.length) { + vm.logTypeData[index] = value; + } + } }); var commonMsgs = logViewerResource.getMessageTemplates(vm.startDate, vm.endDate).then(function (data) { @@ -121,17 +123,24 @@ }); //Set loading indicator to false when these 3 queries complete - $q.all([savedSearches, numOfErrors, logCounts, commonMsgs]).then(function() { + $q.all([savedSearches, numOfErrors, logCounts, commonMsgs]).then(function () { vm.loading = false; }); - + $timeout(function () { - navigationService.syncTree({ tree: "logViewer", path: "-1" }); + navigationService.syncTree({ + tree: "logViewer", + path: "-1" + }); }); } function searchLogQuery(logQuery) { - $location.path("/settings/logViewer/search").search({ lq: logQuery, startDate: vm.startDate, endDate: vm.endDate }); + $location.path("/settings/logViewer/search").search({ + lq: logQuery, + startDate: vm.startDate, + endDate: vm.endDate + }); } function findMessageTemplate(template) { @@ -142,7 +151,7 @@ function getDateRangeLabel(suffix) { return "Log Overview for " + suffix; } - + preFlightCheck(); ///////////////////// @@ -166,7 +175,10 @@ // is collapsed to a comma. const startDate = selectedDates[0].toIsoDateString(); const endDate = selectedDates[selectedDates.length - 1].toIsoDateString(); // Take the last date as end - $location.path("/settings/logViewer/overview").search({ startDate: startDate, endDate: endDate }); + $location.path("/settings/logViewer/overview").search({ + startDate: startDate, + endDate: endDate + }); } } From 59bf24c46110730ff59b070e47545316f9fb48a0 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 12 Jul 2019 11:22:31 +0200 Subject: [PATCH 11/67] Added null checks when *.dataTypeId is used --- .../src/common/resources/entity.resource.js | 4 +++- .../common/dialogs/linkpicker.controller.js | 6 +++++- .../linkpicker/linkpicker.controller.js | 15 ++++++++++++-- .../mediaPicker/mediapicker.controller.js | 20 ++++++++++++++++--- .../treepicker/treepicker.controller.js | 7 ++++++- .../contentpicker/contentpicker.controller.js | 7 ++++++- .../grid/editors/media.controller.js | 7 ++++++- .../grid/editors/rte.controller.js | 14 +++++++++++-- .../mediapicker/mediapicker.controller.js | 6 +++++- .../multiurlpicker.controller.js | 7 ++++++- .../relatedlinks/relatedlinks.controller.js | 14 +++++++++++-- .../propertyeditors/rte/rte.controller.js | 13 ++++++++++-- 12 files changed, 102 insertions(+), 18 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js index c85a85bb57..4e164cacf6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js @@ -409,7 +409,8 @@ function entityResource($q, $http, umbRequestHelper) { pageNumber: 100, filter: '', orderDirection: "Ascending", - orderBy: "SortOrder" + orderBy: "SortOrder", + dataTypeId: null }; if (options === undefined) { options = {}; @@ -426,6 +427,7 @@ function entityResource($q, $http, umbRequestHelper) { options.orderDirection = "Descending"; } + return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/linkpicker.controller.js index e08178ec93..876a9f9426 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/linkpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/linkpicker.controller.js @@ -8,13 +8,17 @@ angular.module("umbraco").controller("Umbraco.Dialogs.LinkPickerController", searchText = value + "..."; }); + var dataTypeId = null; + if($scope.model && $scope.model.dataTypeId) { + dataTypeId = $scope.model.dataTypeId; + } $scope.dialogTreeEventHandler = $({}); $scope.target = {}; $scope.searchInfo = { searchFromId: null, searchFromName: null, showSearch: false, - dataTypeId: $scope.model.dataTypeId, + dataTypeId: dataTypeId, results: [], selectedSearchResults: [] } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js index 2b6b77ed41..0c5641ba0a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js @@ -12,13 +12,19 @@ angular.module("umbraco").controller("Umbraco.Overlays.LinkPickerController", $scope.model.title = localizationService.localize("defaultdialogs_selectLink"); } + var dataTypeId = null; + if(dialogOptions && dialogOptions.dataTypeId){ + dataTypeId = dialogOptions.dataTypeId; + } + + $scope.dialogTreeEventHandler = $({}); $scope.model.target = {}; $scope.searchInfo = { searchFromId: null, searchFromName: null, showSearch: false, - dataTypeId: dialogOptions.dataTypeId, + dataTypeId: dataTypeId, results: [], selectedSearchResults: [] }; @@ -115,12 +121,17 @@ angular.module("umbraco").controller("Umbraco.Overlays.LinkPickerController", startNodeId = -1; startNodeIsVirtual = true; } + var dataTypeId = null; + if(dialogOptions && dialogOptions.dataTypeId){ + dataTypeId = dialogOptions.dataTypeId; + } + $scope.mediaPickerOverlay = { view: "mediapicker", startNodeId: startNodeId, startNodeIsVirtual: startNodeIsVirtual, show: true, - dataTypeId: dialogOptions.dataTypeId, + dataTypeId: dataTypeId, submit: function (model) { var media = model.selectedImages[0]; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js index bb34ba379c..61f9619a52 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js @@ -44,13 +44,17 @@ angular.module("umbraco") $scope.acceptedMediatypes = types; }); + var dataTypeId = null; + if($scope.model && $scope.model.dataTypeId) { + dataTypeId = $scope.model.dataTypeId; + } $scope.searchOptions = { pageNumber: 1, pageSize: 100, totalItems: 0, totalPages: 0, filter: '', - dataTypeId: $scope.model.dataTypeId + dataTypeId: dataTypeId }; //preload selected item @@ -157,7 +161,12 @@ angular.module("umbraco") } if (folder.id > 0) { - entityResource.getAncestors(folder.id, "media", { dataTypeId: $scope.model.dataTypeId }) + var dataTypeId = null; + if($scope.model && $scope.model.dataTypeId) { + dataTypeId = $scope.model.dataTypeId; + } + + entityResource.getAncestors(folder.id, "media", { dataTypeId: dataTypeId }) .then(function (anc) { $scope.path = _.filter(anc, function (f) { @@ -309,6 +318,11 @@ angular.module("umbraco") if ($scope.searchOptions.filter) { searchMedia(); } else { + var dataTypeId = null; + if($scope.model && $scope.model.dataTypeId) { + dataTypeId = $scope.model.dataTypeId; + } + // reset pagination $scope.searchOptions = { pageNumber: 1, @@ -316,7 +330,7 @@ angular.module("umbraco") totalItems: 0, totalPages: 0, filter: '', - dataTypeId: $scope.model.dataTypeId + dataTypeId: dataTypeId }; getChildren($scope.currentFolder.id); } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js index b4dd34dfff..851d270789 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js @@ -4,6 +4,11 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", var tree = null; var dialogOptions = $scope.model; + + var dataTypeId = null; + if(dialogOptions && dialogOptions.dataTypeId){ + dataTypeId = dialogOptions.dataTypeId; + } $scope.treeReady = false; $scope.dialogTreeEventHandler = $({}); $scope.section = dialogOptions.section; @@ -16,7 +21,7 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", searchFromId: dialogOptions.startNodeId, searchFromName: null, showSearch: false, - dataTypeId: dialogOptions.dataTypeId, + dataTypeId: dataTypeId, results: [], selectedSearchResults: [] } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js index 187b86a811..37a372a5b1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js @@ -180,10 +180,15 @@ function contentPickerController($scope, entityResource, editorState, iconHelper //dialog $scope.openContentPicker = function() { + var dataTypeId = null; + if($scope.model && $scope.model.dataTypeId) { + dataTypeId = $scope.model.dataTypeId; + } + $scope.contentPickerOverlay = dialogOptions; $scope.contentPickerOverlay.view = "treepicker"; $scope.contentPickerOverlay.show = true; - $scope.contentPickerOverlay.dataTypeId = $scope.model.dataTypeId; + $scope.contentPickerOverlay.dataTypeId = dataTypeId; $scope.contentPickerOverlay.submit = function(model) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js index c61f5419ca..e9c095c1b9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js @@ -16,11 +16,16 @@ angular.module("umbraco") } $scope.setImage = function(){ + var dataTypeId = null; + if($scope.model && $scope.model.dataTypeId) { + dataTypeId = $scope.model.dataTypeId; + } + $scope.mediaPickerOverlay = {}; $scope.mediaPickerOverlay.view = "mediapicker"; $scope.mediaPickerOverlay.startNodeId = $scope.model.config && $scope.model.config.startNodeId ? $scope.model.config.startNodeId : undefined; $scope.mediaPickerOverlay.startNodeIsVirtual = $scope.mediaPickerOverlay.startNodeId ? $scope.model.config.startNodeIsVirtual : undefined; - $scope.mediaPickerOverlay.dataTypeId = $scope.model.dataTypeId; + $scope.mediaPickerOverlay.dataTypeId = dataTypeId; $scope.mediaPickerOverlay.cropSize = $scope.control.editor.config && $scope.control.editor.config.size ? $scope.control.editor.config.size : undefined; $scope.mediaPickerOverlay.showDetails = true; $scope.mediaPickerOverlay.disableFolderSelect = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js index da869dbf01..580d231de8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js @@ -13,11 +13,16 @@ function openLinkPicker(editor, currentTarget, anchorElement) { entityResource.getAnchors(JSON.stringify($scope.model.value)).then(function(anchorValues) { + var dataTypeId = null; + if($scope.model && $scope.model.dataTypeId) { + dataTypeId = $scope.model.dataTypeId; + } + vm.linkPickerOverlay = { view: "linkpicker", currentTarget: currentTarget, anchors: anchorValues, - dataTypeId: $scope.model.dataTypeId, + dataTypeId: dataTypeId, ignoreUserStartNodes : $scope.model.config.ignoreUserStartNodes, show: true, submit: function(model) { @@ -40,13 +45,18 @@ startNodeIsVirtual = true; } + var dataTypeId = null; + if($scope.model && $scope.model.dataTypeId) { + dataTypeId = $scope.model.dataTypeId; + } + vm.mediaPickerOverlay = { currentTarget: currentTarget, onlyImages: true, showDetails: true, startNodeId: startNodeId, startNodeIsVirtual: startNodeIsVirtual, - dataTypeId: $scope.model.dataTypeId, + dataTypeId: dataTypeId, view: "mediapicker", show: true, submit: function(model) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js index 138bb386be..63beddb49c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js @@ -107,13 +107,17 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl }; $scope.add = function() { + var dataTypeId = null; + if($scope.model && $scope.model.dataTypeId) { + dataTypeId = $scope.model.dataTypeId; + } $scope.mediaPickerOverlay = { view: "mediapicker", title: "Select media", startNodeId: $scope.model.config.startNodeId, startNodeIsVirtual: $scope.model.config.startNodeIsVirtual, - dataTypeId: $scope.model.dataTypeId, + dataTypeId: dataTypeId, multiPicker: multiPicker, onlyImages: onlyImages, disableFolderSelect: disableFolderSelect, diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js index 124f342741..cab124ad23 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js @@ -68,10 +68,15 @@ function multiUrlPickerController($scope, angularHelper, localizationService, en target: link.target } : null; + var dataTypeId = null; + if($scope.model && $scope.model.dataTypeId) { + dataTypeId = $scope.model.dataTypeId; + } + $scope.linkPickerOverlay = { view: "linkpicker", currentTarget: target, - dataTypeId: $scope.model.dataTypeId, + dataTypeId: dataTypeId, ignoreUserStartNodes : $scope.model.config.ignoreUserStartNodes, show: true, submit: function (model) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js index 26c6a0c051..fa92442eac 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js @@ -19,13 +19,18 @@ $scope.hasError = false; $scope.internal = function($event) { + var dataTypeId = null; + if($scope.model && $scope.model.dataTypeId) { + dataTypeId = $scope.model.dataTypeId; + } + $scope.currentEditLink = null; $scope.contentPickerOverlay = {}; $scope.contentPickerOverlay.view = "contentpicker"; $scope.contentPickerOverlay.multiPicker = false; $scope.contentPickerOverlay.show = true; - $scope.contentPickerOverlay.dataTypeId = $scope.model.dataTypeId; + $scope.contentPickerOverlay.dataTypeId = dataTypeId; $scope.contentPickerOverlay.idType = $scope.model.config.idType ? $scope.model.config.idType : "int"; $scope.contentPickerOverlay.submit = function(model) { @@ -45,13 +50,18 @@ }; $scope.selectInternal = function ($event, link) { + var dataTypeId = null; + if($scope.model && $scope.model.dataTypeId) { + dataTypeId = $scope.model.dataTypeId; + } + $scope.currentEditLink = link; $scope.contentPickerOverlay = {}; $scope.contentPickerOverlay.view = "contentpicker"; $scope.contentPickerOverlay.multiPicker = false; $scope.contentPickerOverlay.show = true; - $scope.contentPickerOverlay.dataTypeId = $scope.model.dataTypeId; + $scope.contentPickerOverlay.dataTypeId = dataTypeId; $scope.contentPickerOverlay.idType = $scope.model.config.idType ? $scope.model.config.idType : "int"; $scope.contentPickerOverlay.submit = function(model) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js index dd9fb404c1..2b3ee930d9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js @@ -272,11 +272,16 @@ angular.module("umbraco") tinyMceService.createLinkPicker(editor, $scope, function(currentTarget, anchorElement) { entityResource.getAnchors($scope.model.value).then(function(anchorValues){ + var dataTypeId = null; + if($scope.model && $scope.model.dataTypeId) { + dataTypeId = $scope.model.dataTypeId; + } + $scope.linkPickerOverlay = { view: "linkpicker", currentTarget: currentTarget, anchors: anchorValues, - dataTypeId: $scope.model.dataTypeId, + dataTypeId: dataTypeId, ignoreUserStartNodes: $scope.model.config.ignoreUserStartNodes, show: true, submit: function(model) { @@ -300,6 +305,10 @@ angular.module("umbraco") startNodeId = -1; startNodeIsVirtual = true; } + var dataTypeId = null; + if($scope.model && $scope.model.dataTypeId) { + dataTypeId = $scope.model.dataTypeId; + } $scope.mediaPickerOverlay = { currentTarget: currentTarget, @@ -308,7 +317,7 @@ angular.module("umbraco") disableFolderSelect: true, startNodeId: startNodeId, startNodeIsVirtual: startNodeIsVirtual, - dataTypeId: $scope.model.dataTypeId, + dataTypeId: dataTypeId, view: "mediapicker", show: true, submit: function(model) { From 61a18297d96bd12ca7982863b06c207c93921912 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Fri, 12 Jul 2019 13:03:20 +0100 Subject: [PATCH 12/67] Make v8/dev now 8.2.0 --- src/SolutionInfo.cs | 4 ++-- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 841e054aee..a4e859988e 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -18,5 +18,5 @@ using System.Resources; [assembly: AssemblyVersion("8.0.0")] // these are FYI and changed automatically -[assembly: AssemblyFileVersion("8.1.1")] -[assembly: AssemblyInformationalVersion("8.1.1")] +[assembly: AssemblyFileVersion("8.2.0")] +[assembly: AssemblyInformationalVersion("8.2.0")] diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index e58f44e1ae..147912ed41 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -345,9 +345,9 @@ False True - 8110 + 8200 / - http://localhost:8110 + http://localhost:8200 False False From 1a7251909cbd6d56d6fb27335cfdfd164d3d468b Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Sat, 13 Jul 2019 13:45:13 +1000 Subject: [PATCH 13/67] fix empty array - was checking index against empty array, so condition was never met --- .../src/views/logviewer/overview.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.controller.js b/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.controller.js index 86d72feffd..b622d7a517 100644 --- a/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.controller.js @@ -112,7 +112,7 @@ for (let [key, value] of Object.entries(data)) { const index = vm.logTypeLabels.findIndex(x => key.startsWith(x)); - if (index > -1 && index < vm.logTypeData.length) { + if (index > -1) { vm.logTypeData[index] = value; } } From 1073e6257c56f8cd118e09695468d43c4616be2a Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Sat, 13 Jul 2019 14:48:35 +0200 Subject: [PATCH 14/67] Adjust comments --- .../common/overlays/linkpicker/linkpicker.controller.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js index 2b6b77ed41..d9f907eacc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js @@ -28,24 +28,23 @@ angular.module("umbraco").controller("Umbraco.Overlays.LinkPickerController", if (dialogOptions.currentTarget) { // clone the current target so we don't accidentally update the caller's model while manipulating $scope.model.target $scope.model.target = angular.copy(dialogOptions.currentTarget); - //if we have a node ID, we fetch the current node to build the form data + // if we have a node ID, we fetch the current node to build the form data if ($scope.model.target.id || $scope.model.target.udi) { - //will be either a udi or an int + // will be either a udi or an int var id = $scope.model.target.udi ? $scope.model.target.udi : $scope.model.target.id; // is it a content link? if (!$scope.model.target.isMedia) { // get the content path entityResource.getPath(id, "Document").then(function (path) { - //now sync the tree to this path + // now sync the tree to this path $scope.dialogTreeEventHandler.syncTree({ path: path, tree: "content" }); }); - entityResource.getUrlAndAnchors(id).then(function(resp){ $scope.anchorValues = resp.anchorValues; $scope.model.target.url = resp.url; From 82eb241119a39f4ee49283538c562241dca6e326 Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Sat, 13 Jul 2019 14:49:05 +0200 Subject: [PATCH 15/67] Add overload method for Udi --- src/Umbraco.Web/Editors/EntityController.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index 85d5b607b6..ada0206545 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -56,6 +56,7 @@ namespace Umbraco.Web.Editors //id is passed in eventually we'll probably want to support GUID + Udi too new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetPagedChildren", "id", typeof(int), typeof(string)), new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetPath", "id", typeof(int), typeof(Guid), typeof(Udi)), + new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetUrlAndAnchors", "id", typeof(int), typeof(Guid), typeof(Udi)), new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetById", "id", typeof(int), typeof(Guid), typeof(Udi)), new ParameterSwapControllerActionSelector.ParameterSwapInfo("GetByIds", "ids", typeof(int[]), typeof(Guid[]), typeof(Udi[])))); } @@ -281,9 +282,17 @@ namespace Umbraco.Web.Editors publishedContentExists: i => Umbraco.TypedContent(i) != null); } + [HttpGet] + public UrlAndAnchors GetUrlAndAnchors(Udi id) + { + var nodeId = Umbraco.GetIdForUdi(id); + var url = Umbraco.Url(nodeId); + var anchorValues = Services.ContentService.GetAnchorValuesFromRTEs(nodeId); + return new UrlAndAnchors(url, anchorValues); + } [HttpGet] - public UrlAndAnchors GetUrlAndAnchors([FromUri]int id) + public UrlAndAnchors GetUrlAndAnchors(int id) { var url = Umbraco.Url(id); var anchorValues = Services.ContentService.GetAnchorValuesFromRTEs(id); From 7c03fe3c1f4e963f1e68e32968aef39ca577beeb Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Sat, 13 Jul 2019 14:49:35 +0200 Subject: [PATCH 16/67] Adjust parameter --- .../src/common/resources/entity.resource.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js index c85a85bb57..9453ca8c41 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js @@ -172,7 +172,7 @@ function entityResource($q, $http, umbRequestHelper) { umbRequestHelper.getApiUrl( "entityApiBaseUrl", "GetUrlAndAnchors", - { id: id })), + [{ id: id }])), 'Failed to retrieve url and anchors data for id ' + id); }, From de9241bcf592ca9994810d43232545844fc06848 Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Sat, 13 Jul 2019 14:50:25 +0200 Subject: [PATCH 17/67] Add UmbracoHelper overload methods to get url by guid --- src/Umbraco.Web/UmbracoHelper.cs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/Umbraco.Web/UmbracoHelper.cs b/src/Umbraco.Web/UmbracoHelper.cs index c00d37f216..2c7cafb298 100644 --- a/src/Umbraco.Web/UmbracoHelper.cs +++ b/src/Umbraco.Web/UmbracoHelper.cs @@ -502,6 +502,16 @@ namespace Umbraco.Web return UrlProvider.GetUrl(contentId); } + /// + /// Gets the url of a content identified by its identifier. + /// + /// The content identifier. + /// The url for the content. + public string Url(Guid contentGuid) + { + return UrlProvider.GetUrl(contentGuid); + } + /// /// Gets the url of a content identified by its identifier, in a specified mode. /// @@ -513,6 +523,17 @@ namespace Umbraco.Web return UrlProvider.GetUrl(contentId, mode); } + /// + /// Gets the url of a content identified by its identifier, in a specified mode. + /// + /// The content identifier. + /// The mode. + /// The url for the content. + public string Url(Guid contentGuid, UrlProviderMode mode) + { + return UrlProvider.GetUrl(contentGuid, mode); + } + /// /// This method will always add the domain to the path if the hostnames are set up correctly. /// From c47a08b5532aa3b858ec2cfbe3daa2f09b7bf4f6 Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Sat, 13 Jul 2019 16:40:01 +0200 Subject: [PATCH 18/67] Add overload for NiceUrl --- src/Umbraco.Web/UmbracoHelper.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Umbraco.Web/UmbracoHelper.cs b/src/Umbraco.Web/UmbracoHelper.cs index 2c7cafb298..46ae098e2e 100644 --- a/src/Umbraco.Web/UmbracoHelper.cs +++ b/src/Umbraco.Web/UmbracoHelper.cs @@ -492,6 +492,18 @@ namespace Umbraco.Web return Url(nodeId); } + /// + /// Returns a string with a friendly url from a node. + /// IE.: Instead of having /482 (id) as an url, you can have + /// /screenshots/developer/macros (spoken url) + /// + /// Identifier for the node that should be returned + /// String with a friendly url from a node + public string NiceUrl(Guid guid) + { + return Url(guid); + } + /// /// Gets the url of a content identified by its identifier. /// From 62f924a7572b92c84398b994c83ff1a5da961b07 Mon Sep 17 00:00:00 2001 From: hifi-phil Date: Sun, 14 Jul 2019 09:45:12 +0100 Subject: [PATCH 19/67] Change PropertyType.Alias to virtual When unit testing PropertyType.Alias needs to be virtual to make it mockable. The underlying code on the set triggers IOC logic and makes it hard to test anything that uses Property Types. --- src/Umbraco.Core/Models/PropertyType.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Models/PropertyType.cs b/src/Umbraco.Core/Models/PropertyType.cs index 34775ccf89..61736607c6 100644 --- a/src/Umbraco.Core/Models/PropertyType.cs +++ b/src/Umbraco.Core/Models/PropertyType.cs @@ -114,7 +114,7 @@ namespace Umbraco.Core.Models /// Gets of sets the alias of the property type. /// [DataMember] - public string Alias + public virtual string Alias { get => _alias; set => SetPropertyValueAndDetectChanges(SanitizeAlias(value), ref _alias, nameof(Alias)); From 320ccf810556f6381b35eba6943ffa824f5a7985 Mon Sep 17 00:00:00 2001 From: Steve Megson Date: Sun, 14 Jul 2019 16:46:01 +0100 Subject: [PATCH 20/67] Fixes #5852 - TablesForScheduledPublishing --- .../V_8_0_0/TablesForScheduledPublishing.cs | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs index 70dbe3d29e..2f7ffe8679 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs @@ -14,11 +14,18 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 public override void Migrate() { //Get anything currently scheduled - var scheduleSql = new Sql() - .Select("nodeId", "releaseDate", "expireDate") + var releaseSql = new Sql() + .Select("nodeId", "releaseDate") .From("umbracoDocument") - .Where("releaseDate IS NOT NULL OR expireDate IS NOT NULL"); - var schedules = Database.Dictionary (scheduleSql); + .Where("releaseDate IS NOT NULL"); + var releases = Database.Dictionary (releaseSql); + + var expireSql = new Sql() + .Select("nodeId", "expireDate") + .From("umbracoDocument") + .Where("expireDate IS NOT NULL"); + var expires = Database.Dictionary(expireSql); + //drop old cols Delete.Column("releaseDate").FromTable("umbracoDocument").Do(); @@ -27,20 +34,25 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 Create.Table(true).Do(); //migrate the schedule - foreach(var s in schedules) + foreach(var s in releases) { - var date = s.Value.releaseDate; + var date = s.Value; var action = ContentScheduleAction.Release.ToString(); - if (!date.HasValue) - { - date = s.Value.expireDate; - action = ContentScheduleAction.Expire.ToString(); - } Insert.IntoTable(ContentScheduleDto.TableName) - .Row(new { nodeId = s.Key, date = date.Value, action = action }) + .Row(new { id = Guid.NewGuid(), nodeId = s.Key, date = date, action = action }) .Do(); } + + foreach (var s in expires) + { + var date = s.Value; + var action = ContentScheduleAction.Expire.ToString(); + + Insert.IntoTable(ContentScheduleDto.TableName) + .Row(new { id = Guid.NewGuid(), nodeId = s.Key, date = date, action = action }) + .Do(); + } } } } From 4e0dd728a82a20a25b6500a6e450d0e4ecac376d Mon Sep 17 00:00:00 2001 From: Steve Megson Date: Sun, 14 Jul 2019 16:48:33 +0100 Subject: [PATCH 21/67] Fixes #5864 - migrate prevalues for Umbraco.DropDown.Flexible --- .../DropDownFlexiblePreValueMigrator.cs | 32 +++++++++++++++++++ .../DataTypes/PreValueMigratorComposer.cs | 1 + src/Umbraco.Core/Umbraco.Core.csproj | 1 + 3 files changed, 34 insertions(+) create mode 100644 src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/DropDownFlexiblePreValueMigrator.cs diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/DropDownFlexiblePreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/DropDownFlexiblePreValueMigrator.cs new file mode 100644 index 0000000000..35ca574bab --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/DropDownFlexiblePreValueMigrator.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes +{ + class DropDownFlexiblePreValueMigrator : IPreValueMigrator + { + public bool CanMigrate(string editorAlias) + => editorAlias == "Umbraco.DropDown.Flexible"; + + public virtual string GetNewAlias(string editorAlias) + => null; + + public object GetConfiguration(int dataTypeId, string editorAlias, Dictionary preValues) + { + var config = new DropDownFlexibleConfiguration(); + foreach (var preValue in preValues.Values) + { + if (preValue.Alias == "multiple") + { + config.Multiple = (preValue.Value == "1"); + } + else + { + config.Items.Add(new ValueListConfiguration.ValueListItem { Id = preValue.Id, Value = preValue.Value }); + } + } + return config; + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorComposer.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorComposer.cs index 4c3f8534e7..8dfa464508 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorComposer.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorComposer.cs @@ -19,6 +19,7 @@ public class PreValueMigratorComposer : ICoreComposer .Append() .Append() .Append() + .Append() .Append(); } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index ca9b7d4034..6adce1944f 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -241,6 +241,7 @@ + From f7a10e3a15d89ac57e457b8c936597f31d72b7c7 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Sat, 13 Jul 2019 23:55:50 +0200 Subject: [PATCH 22/67] Added attribute to validate Umbraco form route strings (ufprt) --- .../HttpUmbracoFormRouteStringException.cs | 46 +++++++++++++++++ ...ValidateUmbracoFormRouteStringAttribute.cs | 51 +++++++++++++++++++ src/Umbraco.Web/Umbraco.Web.csproj | 2 + 3 files changed, 99 insertions(+) create mode 100644 src/Umbraco.Web/Mvc/HttpUmbracoFormRouteStringException.cs create mode 100644 src/Umbraco.Web/Mvc/ValidateUmbracoFormRouteStringAttribute.cs diff --git a/src/Umbraco.Web/Mvc/HttpUmbracoFormRouteStringException.cs b/src/Umbraco.Web/Mvc/HttpUmbracoFormRouteStringException.cs new file mode 100644 index 0000000000..8e0b50b11c --- /dev/null +++ b/src/Umbraco.Web/Mvc/HttpUmbracoFormRouteStringException.cs @@ -0,0 +1,46 @@ +using System; +using System.Runtime.Serialization; +using System.Web; + +namespace Umbraco.Web.Mvc +{ + /// + /// Describes an exception that occurred during the processing of an Umbraco form route string. + /// + /// + [Serializable] + public sealed class HttpUmbracoFormRouteStringException : HttpException + { + /// + /// Initializes a new instance of the class. + /// + public HttpUmbracoFormRouteStringException() + { } + + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that holds the contextual information about the source or destination. + private HttpUmbracoFormRouteStringException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } + + /// + /// Initializes a new instance of the class. + /// + /// The error message displayed to the client when the exception is thrown. + public HttpUmbracoFormRouteStringException(string message) + : base(message) + { } + + /// + /// Initializes a new instance of the class. + /// + /// The error message displayed to the client when the exception is thrown. + /// The , if any, that threw the current exception. + public HttpUmbracoFormRouteStringException(string message, Exception innerException) + : base(message, innerException) + { } + } +} diff --git a/src/Umbraco.Web/Mvc/ValidateUmbracoFormRouteStringAttribute.cs b/src/Umbraco.Web/Mvc/ValidateUmbracoFormRouteStringAttribute.cs new file mode 100644 index 0000000000..2be89f05a6 --- /dev/null +++ b/src/Umbraco.Web/Mvc/ValidateUmbracoFormRouteStringAttribute.cs @@ -0,0 +1,51 @@ +using System; +using System.Web.Mvc; +using Umbraco.Core; + +namespace Umbraco.Web.Mvc +{ + /// + /// Represents an attribute that is used to prevent an invalid Umbraco form request route string on a request. + /// + /// + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public sealed class ValidateUmbracoFormRouteStringAttribute : FilterAttribute, IAuthorizationFilter + { + /// + /// Called when authorization is required. + /// + /// The filter context. + /// filterContext + /// The required request field \"ufprt\" is not present. + /// or + /// The Umbraco form request route string could not be decrypted. + /// or + /// The provided Umbraco form request route string was meant for a different controller and action. + public void OnAuthorization(AuthorizationContext filterContext) + { + if (filterContext == null) + { + throw new ArgumentNullException(nameof(filterContext)); + } + + var ufprt = filterContext.HttpContext.Request["ufprt"]; + if (ufprt.IsNullOrWhiteSpace()) + { + throw new HttpUmbracoFormRouteStringException("The required request field \"ufprt\" is not present."); + } + + if (!UmbracoHelper.DecryptAndValidateEncryptedRouteString(ufprt, out var additionalDataParts)) + { + throw new HttpUmbracoFormRouteStringException("The Umbraco form request route string could not be decrypted."); + } + + if (additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Controller] != filterContext.ActionDescriptor.ControllerDescriptor.ControllerName || + additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Action] != filterContext.ActionDescriptor.ActionName || + additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Area].NullOrWhiteSpaceAsNull() != filterContext.RouteData.DataTokens["area"]?.ToString().NullOrWhiteSpaceAsNull()) + { + throw new HttpUmbracoFormRouteStringException("The provided Umbraco form request route string was meant for a different controller and action."); + } + } + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 105a40b4a7..39a05ddbc4 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -219,8 +219,10 @@ + + From 101a8b6c782db20c6018ad8f3120a1a0055d7a75 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Sat, 13 Jul 2019 23:56:29 +0200 Subject: [PATCH 23/67] Added ValidateUmbracoFormRouteStringAttribute to public controllers --- src/Umbraco.Web/Controllers/UmbLoginController.cs | 1 + src/Umbraco.Web/Controllers/UmbLoginStatusController.cs | 1 + src/Umbraco.Web/Controllers/UmbProfileController.cs | 1 + src/Umbraco.Web/Controllers/UmbRegisterController.cs | 1 + 4 files changed, 4 insertions(+) diff --git a/src/Umbraco.Web/Controllers/UmbLoginController.cs b/src/Umbraco.Web/Controllers/UmbLoginController.cs index 2980b4a4c0..2ff80e2668 100644 --- a/src/Umbraco.Web/Controllers/UmbLoginController.cs +++ b/src/Umbraco.Web/Controllers/UmbLoginController.cs @@ -22,6 +22,7 @@ namespace Umbraco.Web.Controllers [HttpPost] [ValidateAntiForgeryToken] + [ValidateUmbracoFormRouteString] public ActionResult HandleLogin([Bind(Prefix = "loginModel")]LoginModel model) { if (ModelState.IsValid == false) diff --git a/src/Umbraco.Web/Controllers/UmbLoginStatusController.cs b/src/Umbraco.Web/Controllers/UmbLoginStatusController.cs index fdc2de8c5f..8f572404fc 100644 --- a/src/Umbraco.Web/Controllers/UmbLoginStatusController.cs +++ b/src/Umbraco.Web/Controllers/UmbLoginStatusController.cs @@ -24,6 +24,7 @@ namespace Umbraco.Web.Controllers [HttpPost] [ValidateAntiForgeryToken] + [ValidateUmbracoFormRouteString] public ActionResult HandleLogout([Bind(Prefix = "logoutModel")]PostRedirectModel model) { if (ModelState.IsValid == false) diff --git a/src/Umbraco.Web/Controllers/UmbProfileController.cs b/src/Umbraco.Web/Controllers/UmbProfileController.cs index b14652ad2e..d9333e8e65 100644 --- a/src/Umbraco.Web/Controllers/UmbProfileController.cs +++ b/src/Umbraco.Web/Controllers/UmbProfileController.cs @@ -23,6 +23,7 @@ namespace Umbraco.Web.Controllers [HttpPost] [ValidateAntiForgeryToken] + [ValidateUmbracoFormRouteString] public ActionResult HandleUpdateProfile([Bind(Prefix = "profileModel")] ProfileModel model) { var provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); diff --git a/src/Umbraco.Web/Controllers/UmbRegisterController.cs b/src/Umbraco.Web/Controllers/UmbRegisterController.cs index b0187d6127..4f4173a67d 100644 --- a/src/Umbraco.Web/Controllers/UmbRegisterController.cs +++ b/src/Umbraco.Web/Controllers/UmbRegisterController.cs @@ -24,6 +24,7 @@ namespace Umbraco.Web.Controllers [HttpPost] [ValidateAntiForgeryToken] + [ValidateUmbracoFormRouteString] public ActionResult HandleRegisterMember([Bind(Prefix = "registerModel")]RegisterModel model) { if (ModelState.IsValid == false) From b0c4042e8748baff647e9e12d0d6b9aeaf3f1636 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Mon, 15 Jul 2019 10:16:09 +0200 Subject: [PATCH 24/67] Removed UmbracoAntiForgeryAdditionalDataProvider --- ...oAntiForgeryAdditionalDataProviderTests.cs | 157 ------------------ src/Umbraco.Tests/Umbraco.Tests.csproj | 1 - src/Umbraco.Web/HtmlHelperRenderExtensions.cs | 8 - src/Umbraco.Web/Runtime/WebFinalComponent.cs | 2 - ...mbracoAntiForgeryAdditionalDataProvider.cs | 92 ---------- src/Umbraco.Web/Umbraco.Web.csproj | 1 - 6 files changed, 261 deletions(-) delete mode 100644 src/Umbraco.Tests/Security/UmbracoAntiForgeryAdditionalDataProviderTests.cs delete mode 100644 src/Umbraco.Web/Security/UmbracoAntiForgeryAdditionalDataProvider.cs diff --git a/src/Umbraco.Tests/Security/UmbracoAntiForgeryAdditionalDataProviderTests.cs b/src/Umbraco.Tests/Security/UmbracoAntiForgeryAdditionalDataProviderTests.cs deleted file mode 100644 index c81c108e0d..0000000000 --- a/src/Umbraco.Tests/Security/UmbracoAntiForgeryAdditionalDataProviderTests.cs +++ /dev/null @@ -1,157 +0,0 @@ -using System.Collections.Specialized; -using System.Web; -using System.Web.Helpers; -using Moq; -using Newtonsoft.Json; -using NUnit.Framework; -using Umbraco.Core; -using Umbraco.Tests.TestHelpers; -using Umbraco.Web.Mvc; -using Umbraco.Web.Security; - -namespace Umbraco.Tests.Security -{ - [TestFixture] - public class UmbracoAntiForgeryAdditionalDataProviderTests - { - [Test] - public void Test_Wrapped_Non_BeginUmbracoForm() - { - var wrapped = Mock.Of(x => x.GetAdditionalData(It.IsAny()) == "custom"); - var provider = new UmbracoAntiForgeryAdditionalDataProvider(wrapped); - - var httpContextFactory = new FakeHttpContextFactory("/hello/world"); - var data = provider.GetAdditionalData(httpContextFactory.HttpContext); - - Assert.IsTrue(data.DetectIsJson()); - var json = JsonConvert.DeserializeObject(data); - Assert.AreEqual(null, json.Ufprt); - Assert.IsTrue(json.Stamp != default); - Assert.AreEqual("custom", json.WrappedValue); - } - - [Test] - public void Null_Wrapped_Non_BeginUmbracoForm() - { - var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); - - var httpContextFactory = new FakeHttpContextFactory("/hello/world"); - var data = provider.GetAdditionalData(httpContextFactory.HttpContext); - - Assert.IsTrue(data.DetectIsJson()); - var json = JsonConvert.DeserializeObject(data); - Assert.AreEqual(null, json.Ufprt); - Assert.IsTrue(json.Stamp != default); - Assert.AreEqual("default", json.WrappedValue); - } - - [Test] - public void Validate_Non_Json() - { - var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); - - var httpContextFactory = new FakeHttpContextFactory("/hello/world"); - var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "hello"); - - Assert.IsFalse(isValid); - } - - [Test] - public void Validate_Invalid_Json() - { - var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); - - var httpContextFactory = new FakeHttpContextFactory("/hello/world"); - var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '0'}"); - Assert.IsFalse(isValid); - - isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': ''}"); - Assert.IsFalse(isValid); - - isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'hello': 'world'}"); - Assert.IsFalse(isValid); - - } - - [Test] - public void Validate_No_Request_Ufprt() - { - var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); - - var httpContextFactory = new FakeHttpContextFactory("/hello/world"); - //there is a ufprt in the additional data, but not in the request - var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': 'ASBVDFDFDFDF'}"); - Assert.IsFalse(isValid); - } - - [Test] - public void Validate_No_AdditionalData_Ufprt() - { - var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); - - var httpContextFactory = new FakeHttpContextFactory("/hello/world"); - var requestMock = Mock.Get(httpContextFactory.HttpContext.Request); - requestMock.SetupGet(x => x["ufprt"]).Returns("ABCDEFG"); - - //there is a ufprt in the additional data, but not in the request - var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': ''}"); - Assert.IsFalse(isValid); - } - - [Test] - public void Validate_No_AdditionalData_Or_Request_Ufprt() - { - var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); - - var httpContextFactory = new FakeHttpContextFactory("/hello/world"); - - //there is a ufprt in the additional data, but not in the request - var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': ''}"); - Assert.IsTrue(isValid); - } - - [Test] - public void Validate_Request_And_AdditionalData_Ufprt() - { - var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); - - var routeParams1 = $"{RenderRouteHandler.ReservedAdditionalKeys.Controller}={HttpUtility.UrlEncode("Test")}&{RenderRouteHandler.ReservedAdditionalKeys.Action}={HttpUtility.UrlEncode("Index")}&{RenderRouteHandler.ReservedAdditionalKeys.Area}=Umbraco"; - var routeParams2 = $"{RenderRouteHandler.ReservedAdditionalKeys.Controller}={HttpUtility.UrlEncode("Test")}&{RenderRouteHandler.ReservedAdditionalKeys.Action}={HttpUtility.UrlEncode("Index")}&{RenderRouteHandler.ReservedAdditionalKeys.Area}=Umbraco"; - - var httpContextFactory = new FakeHttpContextFactory("/hello/world"); - var requestMock = Mock.Get(httpContextFactory.HttpContext.Request); - requestMock.SetupGet(x => x["ufprt"]).Returns(routeParams1.EncryptWithMachineKey()); - - var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': '" + routeParams2.EncryptWithMachineKey() + "'}"); - Assert.IsTrue(isValid); - - routeParams2 = $"{RenderRouteHandler.ReservedAdditionalKeys.Controller}={HttpUtility.UrlEncode("Invalid")}&{RenderRouteHandler.ReservedAdditionalKeys.Action}={HttpUtility.UrlEncode("Index")}&{RenderRouteHandler.ReservedAdditionalKeys.Area}=Umbraco"; - isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': '" + routeParams2.EncryptWithMachineKey() + "'}"); - Assert.IsFalse(isValid); - } - - [Test] - public void Validate_Wrapped_Request_And_AdditionalData_Ufprt() - { - var wrapped = Mock.Of(x => x.ValidateAdditionalData(It.IsAny(), "custom") == true); - var provider = new UmbracoAntiForgeryAdditionalDataProvider(wrapped); - - var routeParams1 = $"{RenderRouteHandler.ReservedAdditionalKeys.Controller}={HttpUtility.UrlEncode("Test")}&{RenderRouteHandler.ReservedAdditionalKeys.Action}={HttpUtility.UrlEncode("Index")}&{RenderRouteHandler.ReservedAdditionalKeys.Area}=Umbraco"; - var routeParams2 = $"{RenderRouteHandler.ReservedAdditionalKeys.Controller}={HttpUtility.UrlEncode("Test")}&{RenderRouteHandler.ReservedAdditionalKeys.Action}={HttpUtility.UrlEncode("Index")}&{RenderRouteHandler.ReservedAdditionalKeys.Area}=Umbraco"; - - var httpContextFactory = new FakeHttpContextFactory("/hello/world"); - var requestMock = Mock.Get(httpContextFactory.HttpContext.Request); - requestMock.SetupGet(x => x["ufprt"]).Returns(routeParams1.EncryptWithMachineKey()); - - var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': '" + routeParams2.EncryptWithMachineKey() + "'}"); - Assert.IsFalse(isValid); - - isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'custom', 'Ufprt': '" + routeParams2.EncryptWithMachineKey() + "'}"); - Assert.IsTrue(isValid); - - routeParams2 = $"{RenderRouteHandler.ReservedAdditionalKeys.Controller}={HttpUtility.UrlEncode("Invalid")}&{RenderRouteHandler.ReservedAdditionalKeys.Action}={HttpUtility.UrlEncode("Index")}&{RenderRouteHandler.ReservedAdditionalKeys.Area}=Umbraco"; - isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': '" + routeParams2.EncryptWithMachineKey() + "'}"); - Assert.IsFalse(isValid); - } - } -} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index cc569d6989..4f4a83dc26 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -147,7 +147,6 @@ - diff --git a/src/Umbraco.Web/HtmlHelperRenderExtensions.cs b/src/Umbraco.Web/HtmlHelperRenderExtensions.cs index e9b731348d..7249500441 100644 --- a/src/Umbraco.Web/HtmlHelperRenderExtensions.cs +++ b/src/Umbraco.Web/HtmlHelperRenderExtensions.cs @@ -223,14 +223,6 @@ namespace Umbraco.Web _method = method; _controllerName = controllerName; _encryptedString = UrlHelperRenderExtensions.CreateEncryptedRouteString(controllerName, controllerAction, area, additionalRouteVals); - - //For UmbracoForm's we want to add our routing string to the httpcontext items in the case where anti-forgery tokens are used. - //In which case our custom UmbracoAntiForgeryAdditionalDataProvider will kick in and validate the values in the request against - //the values that will be appended to the token. This essentially means that when anti-forgery tokens are used with UmbracoForm's forms, - //that each token is unique to the controller/action/area instead of the default ASP.Net implementation which is that the token is unique - //per user. - _viewContext.HttpContext.Items["ufprt"] = _encryptedString; - } diff --git a/src/Umbraco.Web/Runtime/WebFinalComponent.cs b/src/Umbraco.Web/Runtime/WebFinalComponent.cs index ba606e8d5e..6177d9b868 100644 --- a/src/Umbraco.Web/Runtime/WebFinalComponent.cs +++ b/src/Umbraco.Web/Runtime/WebFinalComponent.cs @@ -36,8 +36,6 @@ namespace Umbraco.Web.Runtime // ensure WebAPI is initialized, after everything GlobalConfiguration.Configuration.EnsureInitialized(); - - AntiForgeryConfig.AdditionalDataProvider = new UmbracoAntiForgeryAdditionalDataProvider(AntiForgeryConfig.AdditionalDataProvider); } public void Terminate() diff --git a/src/Umbraco.Web/Security/UmbracoAntiForgeryAdditionalDataProvider.cs b/src/Umbraco.Web/Security/UmbracoAntiForgeryAdditionalDataProvider.cs deleted file mode 100644 index c6ad4c6901..0000000000 --- a/src/Umbraco.Web/Security/UmbracoAntiForgeryAdditionalDataProvider.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using Umbraco.Web.Mvc; -using Umbraco.Core; -using System.Web.Helpers; -using System.Web; -using Newtonsoft.Json; -using Umbraco.Web.Composing; - -namespace Umbraco.Web.Security -{ - /// - /// A custom to create a unique antiforgery token/validator per form created with BeginUmbracoForm - /// - public class UmbracoAntiForgeryAdditionalDataProvider : IAntiForgeryAdditionalDataProvider - { - private readonly IAntiForgeryAdditionalDataProvider _defaultProvider; - - /// - /// Constructor, allows wrapping a default provider - /// - /// - public UmbracoAntiForgeryAdditionalDataProvider(IAntiForgeryAdditionalDataProvider defaultProvider) - { - _defaultProvider = defaultProvider; - } - - public string GetAdditionalData(HttpContextBase context) - { - return JsonConvert.SerializeObject(new AdditionalData - { - Stamp = DateTime.UtcNow.Ticks, - //this value will be here if this is a BeginUmbracoForms form - Ufprt = context.Items["ufprt"]?.ToString(), - //if there was a wrapped provider, add it's value to the json, else just a static value - WrappedValue = _defaultProvider?.GetAdditionalData(context) ?? "default" - }); - } - - public bool ValidateAdditionalData(HttpContextBase context, string additionalData) - { - if (!additionalData.DetectIsJson()) - return false; //must be json - - AdditionalData json; - try - { - json = JsonConvert.DeserializeObject(additionalData); - } - catch - { - return false; //couldn't parse - } - - if (json.Stamp == default) return false; - - //if there was a wrapped provider, validate it, else validate the static value - var validateWrapped = _defaultProvider?.ValidateAdditionalData(context, json.WrappedValue) ?? json.WrappedValue == "default"; - if (!validateWrapped) - return false; - - var ufprtRequest = context.Request["ufprt"]?.ToString(); - - //if the custom BeginUmbracoForms route value is not there, then it's nothing more to validate - if (ufprtRequest.IsNullOrWhiteSpace() && json.Ufprt.IsNullOrWhiteSpace()) - return true; - - //if one or the other is null then something is wrong - if (!ufprtRequest.IsNullOrWhiteSpace() && json.Ufprt.IsNullOrWhiteSpace()) return false; - if (ufprtRequest.IsNullOrWhiteSpace() && !json.Ufprt.IsNullOrWhiteSpace()) return false; - - if (!UmbracoHelper.DecryptAndValidateEncryptedRouteString(json.Ufprt, out var additionalDataParts)) - return false; - - if (!UmbracoHelper.DecryptAndValidateEncryptedRouteString(ufprtRequest, out var requestParts)) - return false; - - //ensure they all match - return additionalDataParts.Count == requestParts.Count - && additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Controller] == requestParts[RenderRouteHandler.ReservedAdditionalKeys.Controller] - && additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Action] == requestParts[RenderRouteHandler.ReservedAdditionalKeys.Action] - && additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Area] == requestParts[RenderRouteHandler.ReservedAdditionalKeys.Area]; - } - - internal class AdditionalData - { - public string Ufprt { get; set; } - public long Stamp { get; set; } - public string WrappedValue { get; set; } - } - - } -} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 39a05ddbc4..41a3393fa4 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -232,7 +232,6 @@ - From d60fc63e789f01202d5c86544b2ed994666fb475 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 15 Jul 2019 12:23:09 +0200 Subject: [PATCH 25/67] Fix YSOD when editing media types --- .../PublishedCache/NuCache/PublishedSnapshotService.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index dad9811af8..f26122d4d0 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -1057,7 +1057,10 @@ namespace Umbraco.Web.PublishedCache.NuCache _mediaStore.UpdateContentTypes(removedIds, typesA, kits); _mediaStore.UpdateContentTypes(CreateContentTypes(PublishedItemType.Media, otherIds.ToArray()).ToArray()); - _mediaStore.NewContentTypes(CreateContentTypes(PublishedItemType.Media, newIds.ToArray()).ToArray()); + if(newIds != null && newIds.Any()) + { + _mediaStore.NewContentTypes(CreateContentTypes(PublishedItemType.Media, newIds.ToArray()).ToArray()); + } scope.Complete(); } } From f6a8d487ccd6a29487f6ef5f8d9e9aad0e6a54f6 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 15 Jul 2019 13:06:47 +0200 Subject: [PATCH 26/67] AB#1695 - https://github.com/umbraco/Umbraco-CMS/issues/5846 Give better exception, if a json exception happens doing migration of grid properties in ConvertTinyMceAndGridMediaUrlsToLocalLink --- ...nvertTinyMceAndGridMediaUrlsToLocalLink.cs | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs index bf048bf2bd..5f49680d89 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using Newtonsoft.Json; @@ -36,6 +37,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_1_0 var properties = Database.Fetch(sqlPropertyData); + var exceptions = new List(); foreach (var property in properties) { var value = property.TextValue; @@ -43,19 +45,34 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_1_0 if (property.PropertyTypeDto.DataTypeDto.EditorAlias == Constants.PropertyEditors.Aliases.Grid) { - var obj = JsonConvert.DeserializeObject(value); - var allControls = obj.SelectTokens("$.sections..rows..areas..controls"); - - foreach (var control in allControls.SelectMany(c => c)) + try { - var controlValue = control["value"]; - if (controlValue.Type == JTokenType.String) + var obj = JsonConvert.DeserializeObject(value); + var allControls = obj.SelectTokens("$.sections..rows..areas..controls"); + + foreach (var control in allControls.SelectMany(c => c)) { - control["value"] = UpdateMediaUrls(mediaLinkPattern, controlValue.Value()); + var controlValue = control["value"]; + if (controlValue.Type == JTokenType.String) + { + control["value"] = UpdateMediaUrls(mediaLinkPattern, controlValue.Value()); + } } + + property.TextValue = JsonConvert.SerializeObject(obj); + } + catch (JsonException e) + { + exceptions.Add(new InvalidOperationException( + "Cannot deserialize the value as json. This can be because the property editor " + + "type is changed from another type into a grid. Old versions of the value in this " + + "property can have the structure from the old property editor type. This needs to be " + + "changed manually before updating the database.\n" + + $"Property info: Id = {property.Id}, LanguageId = {property.LanguageId}, VersionId = {property.VersionId}, Value = {property.Value}" + , e)); + continue; } - property.TextValue = JsonConvert.SerializeObject(obj); } else { @@ -65,6 +82,12 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_1_0 Database.Update(property); } + + if (exceptions.Any()) + { + throw new AggregateException(exceptions); + } + Context.AddPostMigration(); } From 77dda5a70f139b272a0e442980f04d9978bf96d4 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 12 Jul 2019 02:57:26 +0200 Subject: [PATCH 27/67] Fixes issue during upgrade where umbraco can't clear cdf log files --- .../JavaScript/ClientDependencyConfiguration.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web/JavaScript/ClientDependencyConfiguration.cs b/src/Umbraco.Web/JavaScript/ClientDependencyConfiguration.cs index 777786675f..2bf069b06d 100644 --- a/src/Umbraco.Web/JavaScript/ClientDependencyConfiguration.cs +++ b/src/Umbraco.Web/JavaScript/ClientDependencyConfiguration.cs @@ -106,8 +106,10 @@ namespace Umbraco.Web.JavaScript } try - { - var fullPath = currentHttpContext.Server.MapPath(XmlFileMapper.FileMapDefaultFolder); + { + var fullPath = XmlFileMapper.FileMapDefaultFolder.StartsWith("~/") + ? currentHttpContext.Server.MapPath(XmlFileMapper.FileMapDefaultFolder) + : XmlFileMapper.FileMapDefaultFolder; if (fullPath != null) { cdfTempDirectories.Add(fullPath); @@ -122,13 +124,12 @@ namespace Umbraco.Web.JavaScript var success = true; foreach (var directory in cdfTempDirectories) { - var directoryInfo = new DirectoryInfo(directory); - if (directoryInfo.Exists == false) - continue; - try { - directoryInfo.Delete(true); + if (!Directory.Exists(directory)) + continue; + + Directory.Delete(directory, true); } catch (Exception ex) { From 2aaca865e76ba45720b5b9954d81a83af6233dbb Mon Sep 17 00:00:00 2001 From: skttl Date: Fri, 12 Jul 2019 07:33:17 +0200 Subject: [PATCH 28/67] 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 da71a94c39fbd5403c114344a5d5627adacc88b0 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 16 Jul 2019 12:45:27 +0200 Subject: [PATCH 29/67] Use IsCollectionEmpty() as per review --- .../PublishedCache/NuCache/PublishedSnapshotService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index f26122d4d0..b78c806824 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -1057,7 +1057,7 @@ namespace Umbraco.Web.PublishedCache.NuCache _mediaStore.UpdateContentTypes(removedIds, typesA, kits); _mediaStore.UpdateContentTypes(CreateContentTypes(PublishedItemType.Media, otherIds.ToArray()).ToArray()); - if(newIds != null && newIds.Any()) + if(newIds.IsCollectionEmpty() == false) { _mediaStore.NewContentTypes(CreateContentTypes(PublishedItemType.Media, newIds.ToArray()).ToArray()); } From 09c8ae4e7ec9a3c8e321d8bdaa1f988a9ed62780 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 16 Jul 2019 13:36:16 +0200 Subject: [PATCH 30/67] Fixed null ref exception in related links migrations --- .../V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs index ed1c08f0f8..44f7affe8e 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs @@ -50,10 +50,10 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 var properties = Database.Fetch(sqlPropertyData); // Create a Multi URL Picker datatype for the converted RelatedLinks data - + foreach (var property in properties) { - var value = property.Value.ToString(); + var value = property.Value?.ToString(); if (string.IsNullOrWhiteSpace(value)) continue; @@ -89,7 +89,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 Name = relatedLink.Caption, Target = relatedLink.NewWindow ? "_blank" : null, Udi = udi, - // Should only have a URL if it's an external link otherwise it wil be a UDI + // Should only have a URL if it's an external link otherwise it wil be a UDI Url = relatedLink.IsInternal == false ? relatedLink.Link : null }; @@ -103,7 +103,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 Database.Update(property); } - + } } From 0feb053e517047bd8941f7234911e8284bd229cc Mon Sep 17 00:00:00 2001 From: Steve Megson Date: Sun, 14 Jul 2019 17:46:01 +0200 Subject: [PATCH 31/67] Fixes #5852 - TablesForScheduledPublishing --- .../V_8_0_0/TablesForScheduledPublishing.cs | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs index 70dbe3d29e..2f7ffe8679 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs @@ -14,11 +14,18 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 public override void Migrate() { //Get anything currently scheduled - var scheduleSql = new Sql() - .Select("nodeId", "releaseDate", "expireDate") + var releaseSql = new Sql() + .Select("nodeId", "releaseDate") .From("umbracoDocument") - .Where("releaseDate IS NOT NULL OR expireDate IS NOT NULL"); - var schedules = Database.Dictionary (scheduleSql); + .Where("releaseDate IS NOT NULL"); + var releases = Database.Dictionary (releaseSql); + + var expireSql = new Sql() + .Select("nodeId", "expireDate") + .From("umbracoDocument") + .Where("expireDate IS NOT NULL"); + var expires = Database.Dictionary(expireSql); + //drop old cols Delete.Column("releaseDate").FromTable("umbracoDocument").Do(); @@ -27,20 +34,25 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 Create.Table(true).Do(); //migrate the schedule - foreach(var s in schedules) + foreach(var s in releases) { - var date = s.Value.releaseDate; + var date = s.Value; var action = ContentScheduleAction.Release.ToString(); - if (!date.HasValue) - { - date = s.Value.expireDate; - action = ContentScheduleAction.Expire.ToString(); - } Insert.IntoTable(ContentScheduleDto.TableName) - .Row(new { nodeId = s.Key, date = date.Value, action = action }) + .Row(new { id = Guid.NewGuid(), nodeId = s.Key, date = date, action = action }) .Do(); } + + foreach (var s in expires) + { + var date = s.Value; + var action = ContentScheduleAction.Expire.ToString(); + + Insert.IntoTable(ContentScheduleDto.TableName) + .Row(new { id = Guid.NewGuid(), nodeId = s.Key, date = date, action = action }) + .Do(); + } } } } From 3062f40ef5dd8ccc50dfee7c6edd5401690da71a Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 16 Jul 2019 13:43:10 +0200 Subject: [PATCH 32/67] Missing file in project file --- src/Umbraco.Core/Umbraco.Core.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index ca9b7d4034..6adce1944f 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -241,6 +241,7 @@ + From 0cbe0b47967f7cc70f8d6291e983a1b706e37405 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 16 Jul 2019 22:34:34 +1000 Subject: [PATCH 33/67] Cherry picks ValidateUmbracoFormRouteStringAttribute implementation --- .../Controllers/UmbLoginController.cs | 1 + .../Controllers/UmbLoginStatusController.cs | 1 + .../Controllers/UmbProfileController.cs | 1 + .../Controllers/UmbRegisterController.cs | 1 + .../HttpUmbracoFormRouteStringException.cs | 23 ++++++++ ...ValidateUmbracoFormRouteStringAttribute.cs | 52 +++++++++++++++++++ src/Umbraco.Web/Umbraco.Web.csproj | 2 + 7 files changed, 81 insertions(+) create mode 100644 src/Umbraco.Web/Mvc/HttpUmbracoFormRouteStringException.cs create mode 100644 src/Umbraco.Web/Mvc/ValidateUmbracoFormRouteStringAttribute.cs diff --git a/src/Umbraco.Web/Controllers/UmbLoginController.cs b/src/Umbraco.Web/Controllers/UmbLoginController.cs index 2980b4a4c0..2ff80e2668 100644 --- a/src/Umbraco.Web/Controllers/UmbLoginController.cs +++ b/src/Umbraco.Web/Controllers/UmbLoginController.cs @@ -22,6 +22,7 @@ namespace Umbraco.Web.Controllers [HttpPost] [ValidateAntiForgeryToken] + [ValidateUmbracoFormRouteString] public ActionResult HandleLogin([Bind(Prefix = "loginModel")]LoginModel model) { if (ModelState.IsValid == false) diff --git a/src/Umbraco.Web/Controllers/UmbLoginStatusController.cs b/src/Umbraco.Web/Controllers/UmbLoginStatusController.cs index fdc2de8c5f..8f572404fc 100644 --- a/src/Umbraco.Web/Controllers/UmbLoginStatusController.cs +++ b/src/Umbraco.Web/Controllers/UmbLoginStatusController.cs @@ -24,6 +24,7 @@ namespace Umbraco.Web.Controllers [HttpPost] [ValidateAntiForgeryToken] + [ValidateUmbracoFormRouteString] public ActionResult HandleLogout([Bind(Prefix = "logoutModel")]PostRedirectModel model) { if (ModelState.IsValid == false) diff --git a/src/Umbraco.Web/Controllers/UmbProfileController.cs b/src/Umbraco.Web/Controllers/UmbProfileController.cs index b14652ad2e..d9333e8e65 100644 --- a/src/Umbraco.Web/Controllers/UmbProfileController.cs +++ b/src/Umbraco.Web/Controllers/UmbProfileController.cs @@ -23,6 +23,7 @@ namespace Umbraco.Web.Controllers [HttpPost] [ValidateAntiForgeryToken] + [ValidateUmbracoFormRouteString] public ActionResult HandleUpdateProfile([Bind(Prefix = "profileModel")] ProfileModel model) { var provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); diff --git a/src/Umbraco.Web/Controllers/UmbRegisterController.cs b/src/Umbraco.Web/Controllers/UmbRegisterController.cs index b0187d6127..4f4173a67d 100644 --- a/src/Umbraco.Web/Controllers/UmbRegisterController.cs +++ b/src/Umbraco.Web/Controllers/UmbRegisterController.cs @@ -24,6 +24,7 @@ namespace Umbraco.Web.Controllers [HttpPost] [ValidateAntiForgeryToken] + [ValidateUmbracoFormRouteString] public ActionResult HandleRegisterMember([Bind(Prefix = "registerModel")]RegisterModel model) { if (ModelState.IsValid == false) diff --git a/src/Umbraco.Web/Mvc/HttpUmbracoFormRouteStringException.cs b/src/Umbraco.Web/Mvc/HttpUmbracoFormRouteStringException.cs new file mode 100644 index 0000000000..d4734e5d24 --- /dev/null +++ b/src/Umbraco.Web/Mvc/HttpUmbracoFormRouteStringException.cs @@ -0,0 +1,23 @@ +using System; +using System.Net; +using System.Web; + +namespace Umbraco.Web.Mvc +{ + /// + /// Exception that occurs when an Umbraco form route string is invalid + /// + /// + [Serializable] + public sealed class HttpUmbracoFormRouteStringException : HttpException + { + /// + /// Initializes a new instance of the class. + /// + /// The error message displayed to the client when the exception is thrown. + public HttpUmbracoFormRouteStringException(string message) + : base(message) + { } + + } +} diff --git a/src/Umbraco.Web/Mvc/ValidateUmbracoFormRouteStringAttribute.cs b/src/Umbraco.Web/Mvc/ValidateUmbracoFormRouteStringAttribute.cs new file mode 100644 index 0000000000..a57ce7e7f7 --- /dev/null +++ b/src/Umbraco.Web/Mvc/ValidateUmbracoFormRouteStringAttribute.cs @@ -0,0 +1,52 @@ +using System; +using System.Net; +using System.Web.Mvc; +using Umbraco.Core; + +namespace Umbraco.Web.Mvc +{ + /// + /// Represents an attribute that is used to prevent an invalid Umbraco form request route string on a request. + /// + /// + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public sealed class ValidateUmbracoFormRouteStringAttribute : FilterAttribute, IAuthorizationFilter + { + /// + /// Called when authorization is required. + /// + /// The filter context. + /// filterContext + /// The required request field \"ufprt\" is not present. + /// or + /// The Umbraco form request route string could not be decrypted. + /// or + /// The provided Umbraco form request route string was meant for a different controller and action. + public void OnAuthorization(AuthorizationContext filterContext) + { + if (filterContext == null) + { + throw new ArgumentNullException(nameof(filterContext)); + } + + var ufprt = filterContext.HttpContext.Request["ufprt"]; + if (ufprt.IsNullOrWhiteSpace()) + { + throw new HttpUmbracoFormRouteStringException("The required Umbraco request data is invalid."); + } + + if (!UmbracoHelper.DecryptAndValidateEncryptedRouteString(ufprt, out var additionalDataParts)) + { + throw new HttpUmbracoFormRouteStringException("The required Umbraco request data is invalid."); + } + + if (additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Controller] != filterContext.ActionDescriptor.ControllerDescriptor.ControllerName || + additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Action] != filterContext.ActionDescriptor.ActionName || + additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Area].NullOrWhiteSpaceAsNull() != filterContext.RouteData.DataTokens["area"]?.ToString().NullOrWhiteSpaceAsNull()) + { + throw new HttpUmbracoFormRouteStringException("The required Umbraco request data is invalid."); + } + } + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 105a40b4a7..39a05ddbc4 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -219,8 +219,10 @@ + + From d52420183e85e8a94b70b2cb0c68ab46e6f4a681 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 16 Jul 2019 23:03:26 +1000 Subject: [PATCH 34/67] Cherry picks ValidateUmbracoFormRouteStringAttribute implementation and fixes up some logic --- ...oAntiForgeryAdditionalDataProviderTests.cs | 157 ------------------ src/Umbraco.Tests/Umbraco.Tests.csproj | 1 - .../Controllers/UmbLoginController.cs | 1 + .../Controllers/UmbLoginStatusController.cs | 1 + .../Controllers/UmbProfileController.cs | 1 + .../Controllers/UmbRegisterController.cs | 1 + src/Umbraco.Web/HtmlHelperRenderExtensions.cs | 7 - .../HttpUmbracoFormRouteStringException.cs | 23 +++ ...ValidateUmbracoFormRouteStringAttribute.cs | 52 ++++++ ...mbracoAntiForgeryAdditionalDataProvider.cs | 91 ---------- src/Umbraco.Web/Umbraco.Web.csproj | 3 +- src/Umbraco.Web/WebBootManager.cs | 2 - 12 files changed, 81 insertions(+), 259 deletions(-) delete mode 100644 src/Umbraco.Tests/Security/UmbracoAntiForgeryAdditionalDataProviderTests.cs create mode 100644 src/Umbraco.Web/Mvc/HttpUmbracoFormRouteStringException.cs create mode 100644 src/Umbraco.Web/Mvc/ValidateUmbracoFormRouteStringAttribute.cs delete mode 100644 src/Umbraco.Web/Security/UmbracoAntiForgeryAdditionalDataProvider.cs diff --git a/src/Umbraco.Tests/Security/UmbracoAntiForgeryAdditionalDataProviderTests.cs b/src/Umbraco.Tests/Security/UmbracoAntiForgeryAdditionalDataProviderTests.cs deleted file mode 100644 index c81c108e0d..0000000000 --- a/src/Umbraco.Tests/Security/UmbracoAntiForgeryAdditionalDataProviderTests.cs +++ /dev/null @@ -1,157 +0,0 @@ -using System.Collections.Specialized; -using System.Web; -using System.Web.Helpers; -using Moq; -using Newtonsoft.Json; -using NUnit.Framework; -using Umbraco.Core; -using Umbraco.Tests.TestHelpers; -using Umbraco.Web.Mvc; -using Umbraco.Web.Security; - -namespace Umbraco.Tests.Security -{ - [TestFixture] - public class UmbracoAntiForgeryAdditionalDataProviderTests - { - [Test] - public void Test_Wrapped_Non_BeginUmbracoForm() - { - var wrapped = Mock.Of(x => x.GetAdditionalData(It.IsAny()) == "custom"); - var provider = new UmbracoAntiForgeryAdditionalDataProvider(wrapped); - - var httpContextFactory = new FakeHttpContextFactory("/hello/world"); - var data = provider.GetAdditionalData(httpContextFactory.HttpContext); - - Assert.IsTrue(data.DetectIsJson()); - var json = JsonConvert.DeserializeObject(data); - Assert.AreEqual(null, json.Ufprt); - Assert.IsTrue(json.Stamp != default); - Assert.AreEqual("custom", json.WrappedValue); - } - - [Test] - public void Null_Wrapped_Non_BeginUmbracoForm() - { - var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); - - var httpContextFactory = new FakeHttpContextFactory("/hello/world"); - var data = provider.GetAdditionalData(httpContextFactory.HttpContext); - - Assert.IsTrue(data.DetectIsJson()); - var json = JsonConvert.DeserializeObject(data); - Assert.AreEqual(null, json.Ufprt); - Assert.IsTrue(json.Stamp != default); - Assert.AreEqual("default", json.WrappedValue); - } - - [Test] - public void Validate_Non_Json() - { - var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); - - var httpContextFactory = new FakeHttpContextFactory("/hello/world"); - var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "hello"); - - Assert.IsFalse(isValid); - } - - [Test] - public void Validate_Invalid_Json() - { - var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); - - var httpContextFactory = new FakeHttpContextFactory("/hello/world"); - var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '0'}"); - Assert.IsFalse(isValid); - - isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': ''}"); - Assert.IsFalse(isValid); - - isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'hello': 'world'}"); - Assert.IsFalse(isValid); - - } - - [Test] - public void Validate_No_Request_Ufprt() - { - var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); - - var httpContextFactory = new FakeHttpContextFactory("/hello/world"); - //there is a ufprt in the additional data, but not in the request - var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': 'ASBVDFDFDFDF'}"); - Assert.IsFalse(isValid); - } - - [Test] - public void Validate_No_AdditionalData_Ufprt() - { - var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); - - var httpContextFactory = new FakeHttpContextFactory("/hello/world"); - var requestMock = Mock.Get(httpContextFactory.HttpContext.Request); - requestMock.SetupGet(x => x["ufprt"]).Returns("ABCDEFG"); - - //there is a ufprt in the additional data, but not in the request - var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': ''}"); - Assert.IsFalse(isValid); - } - - [Test] - public void Validate_No_AdditionalData_Or_Request_Ufprt() - { - var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); - - var httpContextFactory = new FakeHttpContextFactory("/hello/world"); - - //there is a ufprt in the additional data, but not in the request - var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': ''}"); - Assert.IsTrue(isValid); - } - - [Test] - public void Validate_Request_And_AdditionalData_Ufprt() - { - var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); - - var routeParams1 = $"{RenderRouteHandler.ReservedAdditionalKeys.Controller}={HttpUtility.UrlEncode("Test")}&{RenderRouteHandler.ReservedAdditionalKeys.Action}={HttpUtility.UrlEncode("Index")}&{RenderRouteHandler.ReservedAdditionalKeys.Area}=Umbraco"; - var routeParams2 = $"{RenderRouteHandler.ReservedAdditionalKeys.Controller}={HttpUtility.UrlEncode("Test")}&{RenderRouteHandler.ReservedAdditionalKeys.Action}={HttpUtility.UrlEncode("Index")}&{RenderRouteHandler.ReservedAdditionalKeys.Area}=Umbraco"; - - var httpContextFactory = new FakeHttpContextFactory("/hello/world"); - var requestMock = Mock.Get(httpContextFactory.HttpContext.Request); - requestMock.SetupGet(x => x["ufprt"]).Returns(routeParams1.EncryptWithMachineKey()); - - var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': '" + routeParams2.EncryptWithMachineKey() + "'}"); - Assert.IsTrue(isValid); - - routeParams2 = $"{RenderRouteHandler.ReservedAdditionalKeys.Controller}={HttpUtility.UrlEncode("Invalid")}&{RenderRouteHandler.ReservedAdditionalKeys.Action}={HttpUtility.UrlEncode("Index")}&{RenderRouteHandler.ReservedAdditionalKeys.Area}=Umbraco"; - isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': '" + routeParams2.EncryptWithMachineKey() + "'}"); - Assert.IsFalse(isValid); - } - - [Test] - public void Validate_Wrapped_Request_And_AdditionalData_Ufprt() - { - var wrapped = Mock.Of(x => x.ValidateAdditionalData(It.IsAny(), "custom") == true); - var provider = new UmbracoAntiForgeryAdditionalDataProvider(wrapped); - - var routeParams1 = $"{RenderRouteHandler.ReservedAdditionalKeys.Controller}={HttpUtility.UrlEncode("Test")}&{RenderRouteHandler.ReservedAdditionalKeys.Action}={HttpUtility.UrlEncode("Index")}&{RenderRouteHandler.ReservedAdditionalKeys.Area}=Umbraco"; - var routeParams2 = $"{RenderRouteHandler.ReservedAdditionalKeys.Controller}={HttpUtility.UrlEncode("Test")}&{RenderRouteHandler.ReservedAdditionalKeys.Action}={HttpUtility.UrlEncode("Index")}&{RenderRouteHandler.ReservedAdditionalKeys.Area}=Umbraco"; - - var httpContextFactory = new FakeHttpContextFactory("/hello/world"); - var requestMock = Mock.Get(httpContextFactory.HttpContext.Request); - requestMock.SetupGet(x => x["ufprt"]).Returns(routeParams1.EncryptWithMachineKey()); - - var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': '" + routeParams2.EncryptWithMachineKey() + "'}"); - Assert.IsFalse(isValid); - - isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'custom', 'Ufprt': '" + routeParams2.EncryptWithMachineKey() + "'}"); - Assert.IsTrue(isValid); - - routeParams2 = $"{RenderRouteHandler.ReservedAdditionalKeys.Controller}={HttpUtility.UrlEncode("Invalid")}&{RenderRouteHandler.ReservedAdditionalKeys.Action}={HttpUtility.UrlEncode("Index")}&{RenderRouteHandler.ReservedAdditionalKeys.Area}=Umbraco"; - isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': '" + routeParams2.EncryptWithMachineKey() + "'}"); - Assert.IsFalse(isValid); - } - } -} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 7e6fdf4823..72939c12d9 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -197,7 +197,6 @@ - diff --git a/src/Umbraco.Web/Controllers/UmbLoginController.cs b/src/Umbraco.Web/Controllers/UmbLoginController.cs index ba46d0a17e..a952a944bc 100644 --- a/src/Umbraco.Web/Controllers/UmbLoginController.cs +++ b/src/Umbraco.Web/Controllers/UmbLoginController.cs @@ -12,6 +12,7 @@ namespace Umbraco.Web.Controllers { [HttpPost] [ValidateAntiForgeryToken] + [ValidateUmbracoFormRouteString] public ActionResult HandleLogin([Bind(Prefix = "loginModel")]LoginModel model) { if (ModelState.IsValid == false) diff --git a/src/Umbraco.Web/Controllers/UmbLoginStatusController.cs b/src/Umbraco.Web/Controllers/UmbLoginStatusController.cs index 8e063bf2a3..e1c334bf0b 100644 --- a/src/Umbraco.Web/Controllers/UmbLoginStatusController.cs +++ b/src/Umbraco.Web/Controllers/UmbLoginStatusController.cs @@ -13,6 +13,7 @@ namespace Umbraco.Web.Controllers { [HttpPost] [ValidateAntiForgeryToken] + [ValidateUmbracoFormRouteString] public ActionResult HandleLogout([Bind(Prefix = "logoutModel")]PostRedirectModel model) { if (ModelState.IsValid == false) diff --git a/src/Umbraco.Web/Controllers/UmbProfileController.cs b/src/Umbraco.Web/Controllers/UmbProfileController.cs index 7def7af826..19d2905da1 100644 --- a/src/Umbraco.Web/Controllers/UmbProfileController.cs +++ b/src/Umbraco.Web/Controllers/UmbProfileController.cs @@ -16,6 +16,7 @@ namespace Umbraco.Web.Controllers { [HttpPost] [ValidateAntiForgeryToken] + [ValidateUmbracoFormRouteString] public ActionResult HandleUpdateProfile([Bind(Prefix = "profileModel")] ProfileModel model) { var provider = global::Umbraco.Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); diff --git a/src/Umbraco.Web/Controllers/UmbRegisterController.cs b/src/Umbraco.Web/Controllers/UmbRegisterController.cs index 7931565c47..3e20277537 100644 --- a/src/Umbraco.Web/Controllers/UmbRegisterController.cs +++ b/src/Umbraco.Web/Controllers/UmbRegisterController.cs @@ -11,6 +11,7 @@ namespace Umbraco.Web.Controllers { [HttpPost] [ValidateAntiForgeryToken] + [ValidateUmbracoFormRouteString] public ActionResult HandleRegisterMember([Bind(Prefix = "registerModel")]RegisterModel model) { if (ModelState.IsValid == false) diff --git a/src/Umbraco.Web/HtmlHelperRenderExtensions.cs b/src/Umbraco.Web/HtmlHelperRenderExtensions.cs index 0e2a2a6450..180cf195aa 100644 --- a/src/Umbraco.Web/HtmlHelperRenderExtensions.cs +++ b/src/Umbraco.Web/HtmlHelperRenderExtensions.cs @@ -293,13 +293,6 @@ namespace Umbraco.Web _controllerName = controllerName; _encryptedString = UmbracoHelper.CreateEncryptedRouteString(controllerName, controllerAction, area, additionalRouteVals); - //For UmbracoForm's we want to add our routing string to the httpcontext items in the case where anti-forgery tokens are used. - //In which case our custom UmbracoAntiForgeryAdditionalDataProvider will kick in and validate the values in the request against - //the values that will be appended to the token. This essentially means that when anti-forgery tokens are used with UmbracoForm's forms, - //that each token is unique to the controller/action/area instead of the default ASP.Net implementation which is that the token is unique - //per user. - _viewContext.HttpContext.Items["ufprt"] = _encryptedString; - } diff --git a/src/Umbraco.Web/Mvc/HttpUmbracoFormRouteStringException.cs b/src/Umbraco.Web/Mvc/HttpUmbracoFormRouteStringException.cs new file mode 100644 index 0000000000..d4734e5d24 --- /dev/null +++ b/src/Umbraco.Web/Mvc/HttpUmbracoFormRouteStringException.cs @@ -0,0 +1,23 @@ +using System; +using System.Net; +using System.Web; + +namespace Umbraco.Web.Mvc +{ + /// + /// Exception that occurs when an Umbraco form route string is invalid + /// + /// + [Serializable] + public sealed class HttpUmbracoFormRouteStringException : HttpException + { + /// + /// Initializes a new instance of the class. + /// + /// The error message displayed to the client when the exception is thrown. + public HttpUmbracoFormRouteStringException(string message) + : base(message) + { } + + } +} diff --git a/src/Umbraco.Web/Mvc/ValidateUmbracoFormRouteStringAttribute.cs b/src/Umbraco.Web/Mvc/ValidateUmbracoFormRouteStringAttribute.cs new file mode 100644 index 0000000000..f3d0cc0e27 --- /dev/null +++ b/src/Umbraco.Web/Mvc/ValidateUmbracoFormRouteStringAttribute.cs @@ -0,0 +1,52 @@ +using System; +using System.Net; +using System.Web.Mvc; +using Umbraco.Core; + +namespace Umbraco.Web.Mvc +{ + /// + /// Represents an attribute that is used to prevent an invalid Umbraco form request route string on a request. + /// + /// + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public sealed class ValidateUmbracoFormRouteStringAttribute : FilterAttribute, IAuthorizationFilter + { + /// + /// Called when authorization is required. + /// + /// The filter context. + /// filterContext + /// The required request field \"ufprt\" is not present. + /// or + /// The Umbraco form request route string could not be decrypted. + /// or + /// The provided Umbraco form request route string was meant for a different controller and action. + public void OnAuthorization(AuthorizationContext filterContext) + { + if (filterContext == null) + { + throw new ArgumentNullException(nameof(filterContext)); + } + + var ufprt = filterContext.HttpContext.Request["ufprt"]; + if (ufprt.IsNullOrWhiteSpace()) + { + throw new HttpUmbracoFormRouteStringException("The required Umbraco request data is invalid."); + } + + if (!UmbracoHelper.DecryptAndValidateEncryptedRouteString(ufprt, out var additionalDataParts)) + { + throw new HttpUmbracoFormRouteStringException("The required Umbraco request data is invalid."); + } + + if (!additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Controller].InvariantEquals(filterContext.ActionDescriptor.ControllerDescriptor.ControllerName) || + !additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Action].InvariantEquals(filterContext.ActionDescriptor.ActionName) || + (!additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Area].IsNullOrWhiteSpace() && !additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Area].InvariantEquals(filterContext.RouteData.DataTokens["area"]?.ToString()))) + { + throw new HttpUmbracoFormRouteStringException("The required Umbraco request data is invalid."); + } + } + } +} diff --git a/src/Umbraco.Web/Security/UmbracoAntiForgeryAdditionalDataProvider.cs b/src/Umbraco.Web/Security/UmbracoAntiForgeryAdditionalDataProvider.cs deleted file mode 100644 index 660f7e58fc..0000000000 --- a/src/Umbraco.Web/Security/UmbracoAntiForgeryAdditionalDataProvider.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using Umbraco.Web.Mvc; -using Umbraco.Core; -using System.Web.Helpers; -using System.Web; -using Newtonsoft.Json; - -namespace Umbraco.Web.Security -{ - /// - /// A custom to create a unique antiforgery token/validator per form created with BeginUmbracoForm - /// - public class UmbracoAntiForgeryAdditionalDataProvider : IAntiForgeryAdditionalDataProvider - { - private readonly IAntiForgeryAdditionalDataProvider _defaultProvider; - - /// - /// Constructor, allows wrapping a default provider - /// - /// - public UmbracoAntiForgeryAdditionalDataProvider(IAntiForgeryAdditionalDataProvider defaultProvider) - { - _defaultProvider = defaultProvider; - } - - public string GetAdditionalData(HttpContextBase context) - { - return JsonConvert.SerializeObject(new AdditionalData - { - Stamp = DateTime.UtcNow.Ticks, - //this value will be here if this is a BeginUmbracoForms form - Ufprt = context.Items["ufprt"]?.ToString(), - //if there was a wrapped provider, add it's value to the json, else just a static value - WrappedValue = _defaultProvider?.GetAdditionalData(context) ?? "default" - }); - } - - public bool ValidateAdditionalData(HttpContextBase context, string additionalData) - { - if (!additionalData.DetectIsJson()) - return false; //must be json - - AdditionalData json; - try - { - json = JsonConvert.DeserializeObject(additionalData); - } - catch - { - return false; //couldn't parse - } - - if (json.Stamp == default) return false; - - //if there was a wrapped provider, validate it, else validate the static value - var validateWrapped = _defaultProvider?.ValidateAdditionalData(context, json.WrappedValue) ?? json.WrappedValue == "default"; - if (!validateWrapped) - return false; - - var ufprtRequest = context.Request["ufprt"]?.ToString(); - - //if the custom BeginUmbracoForms route value is not there, then it's nothing more to validate - if (ufprtRequest.IsNullOrWhiteSpace() && json.Ufprt.IsNullOrWhiteSpace()) - return true; - - //if one or the other is null then something is wrong - if (!ufprtRequest.IsNullOrWhiteSpace() && json.Ufprt.IsNullOrWhiteSpace()) return false; - if (ufprtRequest.IsNullOrWhiteSpace() && !json.Ufprt.IsNullOrWhiteSpace()) return false; - - if (!UmbracoHelper.DecryptAndValidateEncryptedRouteString(json.Ufprt, out var additionalDataParts)) - return false; - - if (!UmbracoHelper.DecryptAndValidateEncryptedRouteString(ufprtRequest, out var requestParts)) - return false; - - //ensure they all match - return additionalDataParts.Count == requestParts.Count - && additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Controller] == requestParts[RenderRouteHandler.ReservedAdditionalKeys.Controller] - && additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Action] == requestParts[RenderRouteHandler.ReservedAdditionalKeys.Action] - && additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Area] == requestParts[RenderRouteHandler.ReservedAdditionalKeys.Area]; - } - - internal class AdditionalData - { - public string Ufprt { get; set; } - public long Stamp { get; set; } - public string WrappedValue { get; set; } - } - - } -} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 167c99b9b9..2fb54a2c80 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -307,7 +307,8 @@ - + + diff --git a/src/Umbraco.Web/WebBootManager.cs b/src/Umbraco.Web/WebBootManager.cs index ff6fcff164..46b67a60c7 100644 --- a/src/Umbraco.Web/WebBootManager.cs +++ b/src/Umbraco.Web/WebBootManager.cs @@ -190,8 +190,6 @@ namespace Umbraco.Web base.Complete(afterComplete); - AntiForgeryConfig.AdditionalDataProvider = new UmbracoAntiForgeryAdditionalDataProvider(AntiForgeryConfig.AdditionalDataProvider); - //Now, startup all of our legacy startup handler ApplicationEventsResolver.Current.InstantiateLegacyStartupHandlers(); From b675f6252c1bb471e5a76c01329844905c379e4a Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 16 Jul 2019 23:07:31 +1000 Subject: [PATCH 35/67] removes files (should have been part of cherry pick?), fixes logic --- ...oAntiForgeryAdditionalDataProviderTests.cs | 157 ------------------ src/Umbraco.Tests/Umbraco.Tests.csproj | 1 - ...ValidateUmbracoFormRouteStringAttribute.cs | 6 +- src/Umbraco.Web/Runtime/WebFinalComponent.cs | 2 - ...mbracoAntiForgeryAdditionalDataProvider.cs | 92 ---------- src/Umbraco.Web/Umbraco.Web.csproj | 1 - 6 files changed, 3 insertions(+), 256 deletions(-) delete mode 100644 src/Umbraco.Tests/Security/UmbracoAntiForgeryAdditionalDataProviderTests.cs delete mode 100644 src/Umbraco.Web/Security/UmbracoAntiForgeryAdditionalDataProvider.cs diff --git a/src/Umbraco.Tests/Security/UmbracoAntiForgeryAdditionalDataProviderTests.cs b/src/Umbraco.Tests/Security/UmbracoAntiForgeryAdditionalDataProviderTests.cs deleted file mode 100644 index c81c108e0d..0000000000 --- a/src/Umbraco.Tests/Security/UmbracoAntiForgeryAdditionalDataProviderTests.cs +++ /dev/null @@ -1,157 +0,0 @@ -using System.Collections.Specialized; -using System.Web; -using System.Web.Helpers; -using Moq; -using Newtonsoft.Json; -using NUnit.Framework; -using Umbraco.Core; -using Umbraco.Tests.TestHelpers; -using Umbraco.Web.Mvc; -using Umbraco.Web.Security; - -namespace Umbraco.Tests.Security -{ - [TestFixture] - public class UmbracoAntiForgeryAdditionalDataProviderTests - { - [Test] - public void Test_Wrapped_Non_BeginUmbracoForm() - { - var wrapped = Mock.Of(x => x.GetAdditionalData(It.IsAny()) == "custom"); - var provider = new UmbracoAntiForgeryAdditionalDataProvider(wrapped); - - var httpContextFactory = new FakeHttpContextFactory("/hello/world"); - var data = provider.GetAdditionalData(httpContextFactory.HttpContext); - - Assert.IsTrue(data.DetectIsJson()); - var json = JsonConvert.DeserializeObject(data); - Assert.AreEqual(null, json.Ufprt); - Assert.IsTrue(json.Stamp != default); - Assert.AreEqual("custom", json.WrappedValue); - } - - [Test] - public void Null_Wrapped_Non_BeginUmbracoForm() - { - var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); - - var httpContextFactory = new FakeHttpContextFactory("/hello/world"); - var data = provider.GetAdditionalData(httpContextFactory.HttpContext); - - Assert.IsTrue(data.DetectIsJson()); - var json = JsonConvert.DeserializeObject(data); - Assert.AreEqual(null, json.Ufprt); - Assert.IsTrue(json.Stamp != default); - Assert.AreEqual("default", json.WrappedValue); - } - - [Test] - public void Validate_Non_Json() - { - var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); - - var httpContextFactory = new FakeHttpContextFactory("/hello/world"); - var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "hello"); - - Assert.IsFalse(isValid); - } - - [Test] - public void Validate_Invalid_Json() - { - var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); - - var httpContextFactory = new FakeHttpContextFactory("/hello/world"); - var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '0'}"); - Assert.IsFalse(isValid); - - isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': ''}"); - Assert.IsFalse(isValid); - - isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'hello': 'world'}"); - Assert.IsFalse(isValid); - - } - - [Test] - public void Validate_No_Request_Ufprt() - { - var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); - - var httpContextFactory = new FakeHttpContextFactory("/hello/world"); - //there is a ufprt in the additional data, but not in the request - var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': 'ASBVDFDFDFDF'}"); - Assert.IsFalse(isValid); - } - - [Test] - public void Validate_No_AdditionalData_Ufprt() - { - var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); - - var httpContextFactory = new FakeHttpContextFactory("/hello/world"); - var requestMock = Mock.Get(httpContextFactory.HttpContext.Request); - requestMock.SetupGet(x => x["ufprt"]).Returns("ABCDEFG"); - - //there is a ufprt in the additional data, but not in the request - var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': ''}"); - Assert.IsFalse(isValid); - } - - [Test] - public void Validate_No_AdditionalData_Or_Request_Ufprt() - { - var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); - - var httpContextFactory = new FakeHttpContextFactory("/hello/world"); - - //there is a ufprt in the additional data, but not in the request - var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': ''}"); - Assert.IsTrue(isValid); - } - - [Test] - public void Validate_Request_And_AdditionalData_Ufprt() - { - var provider = new UmbracoAntiForgeryAdditionalDataProvider(null); - - var routeParams1 = $"{RenderRouteHandler.ReservedAdditionalKeys.Controller}={HttpUtility.UrlEncode("Test")}&{RenderRouteHandler.ReservedAdditionalKeys.Action}={HttpUtility.UrlEncode("Index")}&{RenderRouteHandler.ReservedAdditionalKeys.Area}=Umbraco"; - var routeParams2 = $"{RenderRouteHandler.ReservedAdditionalKeys.Controller}={HttpUtility.UrlEncode("Test")}&{RenderRouteHandler.ReservedAdditionalKeys.Action}={HttpUtility.UrlEncode("Index")}&{RenderRouteHandler.ReservedAdditionalKeys.Area}=Umbraco"; - - var httpContextFactory = new FakeHttpContextFactory("/hello/world"); - var requestMock = Mock.Get(httpContextFactory.HttpContext.Request); - requestMock.SetupGet(x => x["ufprt"]).Returns(routeParams1.EncryptWithMachineKey()); - - var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': '" + routeParams2.EncryptWithMachineKey() + "'}"); - Assert.IsTrue(isValid); - - routeParams2 = $"{RenderRouteHandler.ReservedAdditionalKeys.Controller}={HttpUtility.UrlEncode("Invalid")}&{RenderRouteHandler.ReservedAdditionalKeys.Action}={HttpUtility.UrlEncode("Index")}&{RenderRouteHandler.ReservedAdditionalKeys.Area}=Umbraco"; - isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': '" + routeParams2.EncryptWithMachineKey() + "'}"); - Assert.IsFalse(isValid); - } - - [Test] - public void Validate_Wrapped_Request_And_AdditionalData_Ufprt() - { - var wrapped = Mock.Of(x => x.ValidateAdditionalData(It.IsAny(), "custom") == true); - var provider = new UmbracoAntiForgeryAdditionalDataProvider(wrapped); - - var routeParams1 = $"{RenderRouteHandler.ReservedAdditionalKeys.Controller}={HttpUtility.UrlEncode("Test")}&{RenderRouteHandler.ReservedAdditionalKeys.Action}={HttpUtility.UrlEncode("Index")}&{RenderRouteHandler.ReservedAdditionalKeys.Area}=Umbraco"; - var routeParams2 = $"{RenderRouteHandler.ReservedAdditionalKeys.Controller}={HttpUtility.UrlEncode("Test")}&{RenderRouteHandler.ReservedAdditionalKeys.Action}={HttpUtility.UrlEncode("Index")}&{RenderRouteHandler.ReservedAdditionalKeys.Area}=Umbraco"; - - var httpContextFactory = new FakeHttpContextFactory("/hello/world"); - var requestMock = Mock.Get(httpContextFactory.HttpContext.Request); - requestMock.SetupGet(x => x["ufprt"]).Returns(routeParams1.EncryptWithMachineKey()); - - var isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': '" + routeParams2.EncryptWithMachineKey() + "'}"); - Assert.IsFalse(isValid); - - isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'custom', 'Ufprt': '" + routeParams2.EncryptWithMachineKey() + "'}"); - Assert.IsTrue(isValid); - - routeParams2 = $"{RenderRouteHandler.ReservedAdditionalKeys.Controller}={HttpUtility.UrlEncode("Invalid")}&{RenderRouteHandler.ReservedAdditionalKeys.Action}={HttpUtility.UrlEncode("Index")}&{RenderRouteHandler.ReservedAdditionalKeys.Area}=Umbraco"; - isValid = provider.ValidateAdditionalData(httpContextFactory.HttpContext, "{'Stamp': '636970328040070330', 'WrappedValue': 'default', 'Ufprt': '" + routeParams2.EncryptWithMachineKey() + "'}"); - Assert.IsFalse(isValid); - } - } -} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index cc569d6989..4f4a83dc26 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -147,7 +147,6 @@ - diff --git a/src/Umbraco.Web/Mvc/ValidateUmbracoFormRouteStringAttribute.cs b/src/Umbraco.Web/Mvc/ValidateUmbracoFormRouteStringAttribute.cs index a57ce7e7f7..f3d0cc0e27 100644 --- a/src/Umbraco.Web/Mvc/ValidateUmbracoFormRouteStringAttribute.cs +++ b/src/Umbraco.Web/Mvc/ValidateUmbracoFormRouteStringAttribute.cs @@ -41,9 +41,9 @@ namespace Umbraco.Web.Mvc throw new HttpUmbracoFormRouteStringException("The required Umbraco request data is invalid."); } - if (additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Controller] != filterContext.ActionDescriptor.ControllerDescriptor.ControllerName || - additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Action] != filterContext.ActionDescriptor.ActionName || - additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Area].NullOrWhiteSpaceAsNull() != filterContext.RouteData.DataTokens["area"]?.ToString().NullOrWhiteSpaceAsNull()) + if (!additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Controller].InvariantEquals(filterContext.ActionDescriptor.ControllerDescriptor.ControllerName) || + !additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Action].InvariantEquals(filterContext.ActionDescriptor.ActionName) || + (!additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Area].IsNullOrWhiteSpace() && !additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Area].InvariantEquals(filterContext.RouteData.DataTokens["area"]?.ToString()))) { throw new HttpUmbracoFormRouteStringException("The required Umbraco request data is invalid."); } diff --git a/src/Umbraco.Web/Runtime/WebFinalComponent.cs b/src/Umbraco.Web/Runtime/WebFinalComponent.cs index ba606e8d5e..6177d9b868 100644 --- a/src/Umbraco.Web/Runtime/WebFinalComponent.cs +++ b/src/Umbraco.Web/Runtime/WebFinalComponent.cs @@ -36,8 +36,6 @@ namespace Umbraco.Web.Runtime // ensure WebAPI is initialized, after everything GlobalConfiguration.Configuration.EnsureInitialized(); - - AntiForgeryConfig.AdditionalDataProvider = new UmbracoAntiForgeryAdditionalDataProvider(AntiForgeryConfig.AdditionalDataProvider); } public void Terminate() diff --git a/src/Umbraco.Web/Security/UmbracoAntiForgeryAdditionalDataProvider.cs b/src/Umbraco.Web/Security/UmbracoAntiForgeryAdditionalDataProvider.cs deleted file mode 100644 index c6ad4c6901..0000000000 --- a/src/Umbraco.Web/Security/UmbracoAntiForgeryAdditionalDataProvider.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using Umbraco.Web.Mvc; -using Umbraco.Core; -using System.Web.Helpers; -using System.Web; -using Newtonsoft.Json; -using Umbraco.Web.Composing; - -namespace Umbraco.Web.Security -{ - /// - /// A custom to create a unique antiforgery token/validator per form created with BeginUmbracoForm - /// - public class UmbracoAntiForgeryAdditionalDataProvider : IAntiForgeryAdditionalDataProvider - { - private readonly IAntiForgeryAdditionalDataProvider _defaultProvider; - - /// - /// Constructor, allows wrapping a default provider - /// - /// - public UmbracoAntiForgeryAdditionalDataProvider(IAntiForgeryAdditionalDataProvider defaultProvider) - { - _defaultProvider = defaultProvider; - } - - public string GetAdditionalData(HttpContextBase context) - { - return JsonConvert.SerializeObject(new AdditionalData - { - Stamp = DateTime.UtcNow.Ticks, - //this value will be here if this is a BeginUmbracoForms form - Ufprt = context.Items["ufprt"]?.ToString(), - //if there was a wrapped provider, add it's value to the json, else just a static value - WrappedValue = _defaultProvider?.GetAdditionalData(context) ?? "default" - }); - } - - public bool ValidateAdditionalData(HttpContextBase context, string additionalData) - { - if (!additionalData.DetectIsJson()) - return false; //must be json - - AdditionalData json; - try - { - json = JsonConvert.DeserializeObject(additionalData); - } - catch - { - return false; //couldn't parse - } - - if (json.Stamp == default) return false; - - //if there was a wrapped provider, validate it, else validate the static value - var validateWrapped = _defaultProvider?.ValidateAdditionalData(context, json.WrappedValue) ?? json.WrappedValue == "default"; - if (!validateWrapped) - return false; - - var ufprtRequest = context.Request["ufprt"]?.ToString(); - - //if the custom BeginUmbracoForms route value is not there, then it's nothing more to validate - if (ufprtRequest.IsNullOrWhiteSpace() && json.Ufprt.IsNullOrWhiteSpace()) - return true; - - //if one or the other is null then something is wrong - if (!ufprtRequest.IsNullOrWhiteSpace() && json.Ufprt.IsNullOrWhiteSpace()) return false; - if (ufprtRequest.IsNullOrWhiteSpace() && !json.Ufprt.IsNullOrWhiteSpace()) return false; - - if (!UmbracoHelper.DecryptAndValidateEncryptedRouteString(json.Ufprt, out var additionalDataParts)) - return false; - - if (!UmbracoHelper.DecryptAndValidateEncryptedRouteString(ufprtRequest, out var requestParts)) - return false; - - //ensure they all match - return additionalDataParts.Count == requestParts.Count - && additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Controller] == requestParts[RenderRouteHandler.ReservedAdditionalKeys.Controller] - && additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Action] == requestParts[RenderRouteHandler.ReservedAdditionalKeys.Action] - && additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Area] == requestParts[RenderRouteHandler.ReservedAdditionalKeys.Area]; - } - - internal class AdditionalData - { - public string Ufprt { get; set; } - public long Stamp { get; set; } - public string WrappedValue { get; set; } - } - - } -} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 39a05ddbc4..41a3393fa4 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -232,7 +232,6 @@ - From bfb69a34ef2353229d390b9129103e61f6b4c704 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 17 Jul 2019 21:15:18 +1000 Subject: [PATCH 36/67] re-adds back in the serialization overloads for the custom exception, re-adds detailed error messages, adds more documentation. Adds unit tests. --- src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + .../Mvc/HtmlHelperExtensionMethodsTests.cs | 3 +- ...ateUmbracoFormRouteStringAttributeTests.cs | 37 +++++++++++++++++++ .../HttpUmbracoFormRouteStringException.cs | 18 +++++++++ ...ValidateUmbracoFormRouteStringAttribute.cs | 28 +++++++++----- src/Umbraco.Web/UmbracoHelper.cs | 2 +- 6 files changed, 78 insertions(+), 11 deletions(-) create mode 100644 src/Umbraco.Tests/Web/Mvc/ValidateUmbracoFormRouteStringAttributeTests.cs diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 72939c12d9..d45c556d37 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -398,6 +398,7 @@ + diff --git a/src/Umbraco.Tests/Web/Mvc/HtmlHelperExtensionMethodsTests.cs b/src/Umbraco.Tests/Web/Mvc/HtmlHelperExtensionMethodsTests.cs index e7e4f363c1..7be0717c7e 100644 --- a/src/Umbraco.Tests/Web/Mvc/HtmlHelperExtensionMethodsTests.cs +++ b/src/Umbraco.Tests/Web/Mvc/HtmlHelperExtensionMethodsTests.cs @@ -4,7 +4,8 @@ using Umbraco.Web; namespace Umbraco.Tests.Web.Mvc { - [TestFixture] + + [TestFixture] public class HtmlHelperExtensionMethodsTests { [SetUp] diff --git a/src/Umbraco.Tests/Web/Mvc/ValidateUmbracoFormRouteStringAttributeTests.cs b/src/Umbraco.Tests/Web/Mvc/ValidateUmbracoFormRouteStringAttributeTests.cs new file mode 100644 index 0000000000..a942f846a0 --- /dev/null +++ b/src/Umbraco.Tests/Web/Mvc/ValidateUmbracoFormRouteStringAttributeTests.cs @@ -0,0 +1,37 @@ +using NUnit.Framework; +using Umbraco.Web; +using Umbraco.Web.Mvc; + +namespace Umbraco.Tests.Web.Mvc +{ + [TestFixture] + public class ValidateUmbracoFormRouteStringAttributeTests + { + [Test] + public void Validate_Route_String() + { + var attribute = new ValidateUmbracoFormRouteStringAttribute(); + + Assert.Throws(() => attribute.ValidateRouteString(null, null, null, null)); + + const string ControllerName = "Test"; + const string ControllerAction = "Index"; + const string Area = "MyArea"; + var validUfprt = UmbracoHelper.CreateEncryptedRouteString(ControllerName, ControllerAction, Area); + + var invalidUfprt = validUfprt + "z"; + Assert.Throws(() => attribute.ValidateRouteString(invalidUfprt, null, null, null)); + + Assert.Throws(() => attribute.ValidateRouteString(validUfprt, ControllerName, ControllerAction, "doesntMatch")); + Assert.Throws(() => attribute.ValidateRouteString(validUfprt, ControllerName, ControllerAction, null)); + Assert.Throws(() => attribute.ValidateRouteString(validUfprt, ControllerName, "doesntMatch", Area)); + Assert.Throws(() => attribute.ValidateRouteString(validUfprt, ControllerName, null, Area)); + Assert.Throws(() => attribute.ValidateRouteString(validUfprt, "doesntMatch", ControllerAction, Area)); + Assert.Throws(() => attribute.ValidateRouteString(validUfprt, null, ControllerAction, Area)); + + Assert.DoesNotThrow(() => attribute.ValidateRouteString(validUfprt, ControllerName, ControllerAction, Area)); + Assert.DoesNotThrow(() => attribute.ValidateRouteString(validUfprt, ControllerName.ToLowerInvariant(), ControllerAction.ToLowerInvariant(), Area.ToLowerInvariant())); + } + + } +} diff --git a/src/Umbraco.Web/Mvc/HttpUmbracoFormRouteStringException.cs b/src/Umbraco.Web/Mvc/HttpUmbracoFormRouteStringException.cs index d4734e5d24..b08fde081a 100644 --- a/src/Umbraco.Web/Mvc/HttpUmbracoFormRouteStringException.cs +++ b/src/Umbraco.Web/Mvc/HttpUmbracoFormRouteStringException.cs @@ -1,5 +1,6 @@ using System; using System.Net; +using System.Runtime.Serialization; using System.Web; namespace Umbraco.Web.Mvc @@ -11,6 +12,14 @@ namespace Umbraco.Web.Mvc [Serializable] public sealed class HttpUmbracoFormRouteStringException : HttpException { + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that holds the contextual information about the source or destination. + private HttpUmbracoFormRouteStringException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } + /// /// Initializes a new instance of the class. /// @@ -19,5 +28,14 @@ namespace Umbraco.Web.Mvc : base(message) { } + /// + /// Initializes a new instance of the class. + /// + /// The error message displayed to the client when the exception is thrown. + /// The , if any, that threw the current exception. + public HttpUmbracoFormRouteStringException(string message, Exception innerException) + : base(message, innerException) + { } + } } diff --git a/src/Umbraco.Web/Mvc/ValidateUmbracoFormRouteStringAttribute.cs b/src/Umbraco.Web/Mvc/ValidateUmbracoFormRouteStringAttribute.cs index f3d0cc0e27..8d929197e1 100644 --- a/src/Umbraco.Web/Mvc/ValidateUmbracoFormRouteStringAttribute.cs +++ b/src/Umbraco.Web/Mvc/ValidateUmbracoFormRouteStringAttribute.cs @@ -1,15 +1,21 @@ using System; using System.Net; +using System.Net.Http; using System.Web.Mvc; using Umbraco.Core; namespace Umbraco.Web.Mvc { /// - /// Represents an attribute that is used to prevent an invalid Umbraco form request route string on a request. + /// Attribute used to check that the request contains a valid Umbraco form request string. /// /// /// + /// + /// Applying this attribute/filter to a or SurfaceController Action will ensure that the Action can only be executed + /// when it is routed to from within Umbraco, typically when rendering a form with BegingUmbracoForm. It will mean that the natural MVC route for this Action + /// will fail with a . + /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public sealed class ValidateUmbracoFormRouteStringAttribute : FilterAttribute, IAuthorizationFilter { @@ -26,27 +32,31 @@ namespace Umbraco.Web.Mvc public void OnAuthorization(AuthorizationContext filterContext) { if (filterContext == null) - { throw new ArgumentNullException(nameof(filterContext)); - } var ufprt = filterContext.HttpContext.Request["ufprt"]; + ValidateRouteString(ufprt, filterContext.ActionDescriptor?.ControllerDescriptor.ControllerName, filterContext.ActionDescriptor?.ActionName, filterContext.RouteData?.DataTokens["area"]?.ToString()); + } + + public void ValidateRouteString(string ufprt, string currentController, string currentAction, string currentArea) + { if (ufprt.IsNullOrWhiteSpace()) { - throw new HttpUmbracoFormRouteStringException("The required Umbraco request data is invalid."); + throw new HttpUmbracoFormRouteStringException("The required request field \"ufprt\" is not present."); } if (!UmbracoHelper.DecryptAndValidateEncryptedRouteString(ufprt, out var additionalDataParts)) { - throw new HttpUmbracoFormRouteStringException("The required Umbraco request data is invalid."); + throw new HttpUmbracoFormRouteStringException("The Umbraco form request route string could not be decrypted."); } - if (!additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Controller].InvariantEquals(filterContext.ActionDescriptor.ControllerDescriptor.ControllerName) || - !additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Action].InvariantEquals(filterContext.ActionDescriptor.ActionName) || - (!additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Area].IsNullOrWhiteSpace() && !additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Area].InvariantEquals(filterContext.RouteData.DataTokens["area"]?.ToString()))) + if (!additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Controller].InvariantEquals(currentController) || + !additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Action].InvariantEquals(currentAction) || + (!additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Area].IsNullOrWhiteSpace() && !additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Area].InvariantEquals(currentArea))) { - throw new HttpUmbracoFormRouteStringException("The required Umbraco request data is invalid."); + throw new HttpUmbracoFormRouteStringException("The provided Umbraco form request route string was meant for a different controller and action."); } + } } } diff --git a/src/Umbraco.Web/UmbracoHelper.cs b/src/Umbraco.Web/UmbracoHelper.cs index c00d37f216..5320b6085a 100644 --- a/src/Umbraco.Web/UmbracoHelper.cs +++ b/src/Umbraco.Web/UmbracoHelper.cs @@ -1656,7 +1656,7 @@ namespace Umbraco.Web { decryptedString = ufprt.DecryptWithMachineKey(); } - catch (FormatException) + catch (Exception ex) when (ex is FormatException || ex is ArgumentException) { LogHelper.Warn("A value was detected in the ufprt parameter but Umbraco could not decrypt the string"); parts = null; From 14c4c4815d20de5d2cc69868d303ce58309bd7ba Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 17 Jul 2019 21:35:43 +1000 Subject: [PATCH 37/67] bumps version --- src/SolutionInfo.cs | 4 ++-- src/Umbraco.Core/Configuration/UmbracoVersion.cs | 2 +- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 0d6b3bcc6b..3a85915cf5 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -11,5 +11,5 @@ using System.Resources; [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyFileVersion("7.15.0")] -[assembly: AssemblyInformationalVersion("7.15.0")] +[assembly: AssemblyFileVersion("7.15.1")] +[assembly: AssemblyInformationalVersion("7.15.1")] diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index 6bb5dbf78e..f23078dbd0 100644 --- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs @@ -6,7 +6,7 @@ namespace Umbraco.Core.Configuration { public class UmbracoVersion { - private static readonly Version Version = new Version("7.15.0"); + private static readonly Version Version = new Version("7.15.1"); /// /// Gets the current version of Umbraco. diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index a0624a2dd7..6884f21bcf 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -1027,9 +1027,9 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.1\x86\*.* "$(TargetDir)x86\" True True - 7150 + 7151 / - http://localhost:7150 + http://localhost:7151 False False From 26d4d056be662a3e91e33740c9813b1604c5be96 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 17 Jul 2019 22:28:06 +1000 Subject: [PATCH 38/67] bumps version --- src/SolutionInfo.cs | 4 ++-- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 9ed398d52f..841e054aee 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -18,5 +18,5 @@ using System.Resources; [assembly: AssemblyVersion("8.0.0")] // these are FYI and changed automatically -[assembly: AssemblyFileVersion("8.1.0")] -[assembly: AssemblyInformationalVersion("8.1.0")] +[assembly: AssemblyFileVersion("8.1.1")] +[assembly: AssemblyInformationalVersion("8.1.1")] diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index b27f6aa335..e58f44e1ae 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -345,9 +345,9 @@ False True - 8100 + 8110 / - http://localhost:8100 + http://localhost:8110 False False From 79159b684dabcbe8b54597691380bb17982285cf Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 17 Jul 2019 14:30:02 +0200 Subject: [PATCH 39/67] https://github.com/umbraco/Umbraco-CMS/issues/5902 - Fixed issue with migration of Umbraco.Date if it was never saved in v7, the default format was added the time-part. - Fixed issue with warnings of migrations of build in types, that was renamed between v7 and v8. These are handled correct. - Added constants for some of the legacy property editor aliases. --- src/Umbraco.Core/Constants-PropertyEditors.cs | 17 ++++++++++ .../ConvertRelatedLinksToMultiUrlPicker.cs | 4 +-- .../Upgrade/V_8_0_0/DataTypeMigration.cs | 34 +++++++++++++++---- .../ContentPickerPreValueMigrator.cs | 2 +- .../DataTypes/MediaPickerPreValueMigrator.cs | 6 ++-- .../MergeDateAndDateTimePropertyEditor.cs | 10 +++++- .../V_8_0_0/PropertyEditorsMigration.cs | 14 ++++---- 7 files changed, 66 insertions(+), 21 deletions(-) diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs index 0c2e246721..b48286f197 100644 --- a/src/Umbraco.Core/Constants-PropertyEditors.cs +++ b/src/Umbraco.Core/Constants-PropertyEditors.cs @@ -14,6 +14,23 @@ namespace Umbraco.Core /// public const string InternalGenericPropertiesPrefix = "_umb_"; + public static class Legacy + { + public static class Aliases + { + public const string Textbox = "Umbraco.Textbox"; + public const string Date = "Umbraco.Date"; + public const string ContentPicker2 = "Umbraco.ContentPicker2"; + public const string MediaPicker2 = "Umbraco.MediaPicker2"; + public const string MemberPicker2 = "Umbraco.MemberPicker2"; + public const string MultiNodeTreePicker2 = "Umbraco.MultiNodeTreePicker2"; + public const string TextboxMultiple = "Umbraco.TextboxMultiple"; + public const string RelatedLinks2 = "Umbraco.RelatedLinks2"; + public const string RelatedLinks = "Umbraco.RelatedLinks"; + + } + } + /// /// Defines Umbraco built-in property editor aliases. /// diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs index 44f7affe8e..4802750f4b 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs @@ -19,8 +19,8 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 var sqlDataTypes = Sql() .Select() .From() - .Where(x => x.EditorAlias == "Umbraco.RelatedLinks" - || x.EditorAlias == "Umbraco.RelatedLinks2"); + .Where(x => x.EditorAlias == Constants.PropertyEditors.Legacy.Aliases.RelatedLinks + || x.EditorAlias == Constants.PropertyEditors.Legacy.Aliases.RelatedLinks2); var dataTypes = Database.Fetch(sqlDataTypes); var dataTypeIds = dataTypes.Select(x => x.NodeId).ToList(); diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs index 438b02385b..7b2daa99ef 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using Umbraco.Core.Composing; @@ -18,6 +19,18 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 private readonly PropertyEditorCollection _propertyEditors; private readonly ILogger _logger; + private static readonly ISet LegacyAliases = new HashSet() + { + Constants.PropertyEditors.Legacy.Aliases.Date, + Constants.PropertyEditors.Legacy.Aliases.Textbox, + Constants.PropertyEditors.Legacy.Aliases.ContentPicker2, + Constants.PropertyEditors.Legacy.Aliases.MediaPicker2, + Constants.PropertyEditors.Legacy.Aliases.MemberPicker2, + Constants.PropertyEditors.Legacy.Aliases.RelatedLinks2, + Constants.PropertyEditors.Legacy.Aliases.TextboxMultiple, + Constants.PropertyEditors.Legacy.Aliases.MultiNodeTreePicker2, + }; + public DataTypeMigration(IMigrationContext context, PreValueMigratorCollection preValueMigrators, PropertyEditorCollection propertyEditors, ILogger logger) : base(context) { @@ -70,16 +83,23 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 var newAlias = migrator.GetNewAlias(dataType.EditorAlias); if (newAlias == null) { - _logger.Warn("Skipping validation of configuration for data type {NodeId} : {EditorAlias}." - + " Please ensure that the configuration is valid. The site may fail to start and / or load data types and run.", - dataType.NodeId, dataType.EditorAlias); + if (!LegacyAliases.Contains(dataType.EditorAlias)) + { + _logger.Warn( + "Skipping validation of configuration for data type {NodeId} : {EditorAlias}." + + " Please ensure that the configuration is valid. The site may fail to start and / or load data types and run.", + dataType.NodeId, dataType.EditorAlias); + } } else if (!_propertyEditors.TryGet(newAlias, out var propertyEditor)) { - _logger.Warn("Skipping validation of configuration for data type {NodeId} : {NewEditorAlias} (was: {EditorAlias})" - + " because no property editor with that alias was found." - + " Please ensure that the configuration is valid. The site may fail to start and / or load data types and run.", - dataType.NodeId, newAlias, dataType.EditorAlias); + if (!LegacyAliases.Contains(newAlias)) + { + _logger.Warn("Skipping validation of configuration for data type {NodeId} : {NewEditorAlias} (was: {EditorAlias})" + + " because no property editor with that alias was found." + + " Please ensure that the configuration is valid. The site may fail to start and / or load data types and run.", + dataType.NodeId, newAlias, dataType.EditorAlias); + } } else { diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ContentPickerPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ContentPickerPreValueMigrator.cs index 2e341ad091..f445742aa9 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ContentPickerPreValueMigrator.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ContentPickerPreValueMigrator.cs @@ -3,7 +3,7 @@ class ContentPickerPreValueMigrator : DefaultPreValueMigrator { public override bool CanMigrate(string editorAlias) - => editorAlias == "Umbraco.ContentPicker2"; + => editorAlias == Constants.PropertyEditors.Legacy.Aliases.ContentPicker2; public override string GetNewAlias(string editorAlias) => null; diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/MediaPickerPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/MediaPickerPreValueMigrator.cs index a46b1eefb7..922d886595 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/MediaPickerPreValueMigrator.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/MediaPickerPreValueMigrator.cs @@ -6,15 +6,15 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes { private readonly string[] _editors = { - "Umbraco.MediaPicker2", - "Umbraco.MediaPicker" + Constants.PropertyEditors.Legacy.Aliases.MediaPicker2, + Constants.PropertyEditors.Aliases.MediaPicker }; public override bool CanMigrate(string editorAlias) => _editors.Contains(editorAlias); public override string GetNewAlias(string editorAlias) - => "Umbraco.MediaPicker"; + => Constants.PropertyEditors.Aliases.MediaPicker; // you wish - but MediaPickerConfiguration lives in Umbraco.Web /* diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs index a434b9f1c1..0d451e8460 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs @@ -16,7 +16,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 public override void Migrate() { - var dataTypes = GetDataTypes("Umbraco.Date"); + var dataTypes = GetDataTypes(Constants.PropertyEditors.Legacy.Aliases.Date); foreach (var dataType in dataTypes) { @@ -25,6 +25,14 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 { config = (DateTimeConfiguration) new CustomDateTimeConfigurationEditor().FromDatabase( dataType.Configuration); + + // If the Umbraco.Date type is the default from V7 and it has never been updated, then the + // configuration is empty, and the format stuff is handled by in JS by moment.js. - We can't do that + // after the migration, so we force the format to the default from V7. + if (string.IsNullOrEmpty(dataType.Configuration)) + { + config.Format = "YYYY-MM-DD"; + }; } catch (Exception ex) { diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs index dac62abb75..89a8f010ec 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs @@ -12,12 +12,12 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 public override void Migrate() { - RenameDataType(Constants.PropertyEditors.Aliases.ContentPicker + "2", Constants.PropertyEditors.Aliases.ContentPicker); - RenameDataType(Constants.PropertyEditors.Aliases.MediaPicker + "2", Constants.PropertyEditors.Aliases.MediaPicker); - RenameDataType(Constants.PropertyEditors.Aliases.MemberPicker + "2", Constants.PropertyEditors.Aliases.MemberPicker); - RenameDataType(Constants.PropertyEditors.Aliases.MultiNodeTreePicker + "2", Constants.PropertyEditors.Aliases.MultiNodeTreePicker); - RenameDataType("Umbraco.TextboxMultiple", Constants.PropertyEditors.Aliases.TextArea, false); - RenameDataType("Umbraco.Textbox", Constants.PropertyEditors.Aliases.TextBox, false); + RenameDataType(Constants.PropertyEditors.Legacy.Aliases.ContentPicker2, Constants.PropertyEditors.Aliases.ContentPicker); + RenameDataType(Constants.PropertyEditors.Legacy.Aliases.MediaPicker2, Constants.PropertyEditors.Aliases.MediaPicker); + RenameDataType(Constants.PropertyEditors.Legacy.Aliases.MemberPicker2, Constants.PropertyEditors.Aliases.MemberPicker); + RenameDataType(Constants.PropertyEditors.Legacy.Aliases.MultiNodeTreePicker2, Constants.PropertyEditors.Aliases.MultiNodeTreePicker); + RenameDataType(Constants.PropertyEditors.Legacy.Aliases.TextboxMultiple, Constants.PropertyEditors.Aliases.TextArea, false); + RenameDataType(Constants.PropertyEditors.Legacy.Aliases.Textbox, Constants.PropertyEditors.Aliases.TextBox, false); } private void RenameDataType(string fromAlias, string toAlias, bool checkCollision = true) @@ -43,7 +43,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 $"Property Editors. Before upgrading to v8, any Data Types using property editors that are named with the prefix '(Obsolete)' must be migrated " + $"to the non-obsolete v7 property editors of the same type."); } - + } Database.Execute(Sql() From 36030904212d321af75f96bfc894742e86629ec3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 17 Jul 2019 22:51:14 +1000 Subject: [PATCH 40/67] Fixes race condition in PublishedSnapshotService and more... --- src/Umbraco.Core/AsyncLock.cs | 47 ++++++++-------- src/Umbraco.Core/MainDom.cs | 7 ++- src/Umbraco.Core/Runtime/CoreRuntime.cs | 6 +- src/Umbraco.Core/RuntimeState.cs | 5 ++ src/Umbraco.Core/WaitHandleExtensions.cs | 2 + .../PublishedContent/NuCacheChildrenTests.cs | 2 +- .../PublishedContent/NuCacheTests.cs | 2 +- .../Scoping/ScopedNuCacheTests.cs | 2 +- .../ContentTypeServiceVariantsTests.cs | 2 +- .../PublishedCache/NuCache/NuCacheComposer.cs | 2 +- .../NuCache/PublishedSnapshotService.cs | 56 +++++++++---------- .../PublishedSnapshotServiceOptions.cs | 28 ++++++++++ src/Umbraco.Web/Scheduling/KeepAlive.cs | 2 +- src/Umbraco.Web/Umbraco.Web.csproj | 1 + 14 files changed, 101 insertions(+), 63 deletions(-) create mode 100644 src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotServiceOptions.cs diff --git a/src/Umbraco.Core/AsyncLock.cs b/src/Umbraco.Core/AsyncLock.cs index 158b132f26..6dd866705e 100644 --- a/src/Umbraco.Core/AsyncLock.cs +++ b/src/Umbraco.Core/AsyncLock.cs @@ -67,31 +67,34 @@ namespace Umbraco.Core : new NamedSemaphoreReleaser(_semaphore2); } - public Task LockAsync() - { - var wait = _semaphore != null - ? _semaphore.WaitAsync() - : _semaphore2.WaitOneAsync(); + //NOTE: We don't use the "Async" part of this lock at all + //TODO: Remove this and rename this class something like SystemWideLock, then we can re-instate this logic if we ever need an Async lock again - return wait.IsCompleted - ? _releaserTask ?? Task.FromResult(CreateReleaser()) // anonymous vs named - : wait.ContinueWith((_, state) => (((AsyncLock) state).CreateReleaser()), - this, CancellationToken.None, - TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); - } + //public Task LockAsync() + //{ + // var wait = _semaphore != null + // ? _semaphore.WaitAsync() + // : _semaphore2.WaitOneAsync(); - public Task LockAsync(int millisecondsTimeout) - { - var wait = _semaphore != null - ? _semaphore.WaitAsync(millisecondsTimeout) - : _semaphore2.WaitOneAsync(millisecondsTimeout); + // return wait.IsCompleted + // ? _releaserTask ?? Task.FromResult(CreateReleaser()) // anonymous vs named + // : wait.ContinueWith((_, state) => (((AsyncLock) state).CreateReleaser()), + // this, CancellationToken.None, + // TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + //} - return wait.IsCompleted - ? _releaserTask ?? Task.FromResult(CreateReleaser()) // anonymous vs named - : wait.ContinueWith((_, state) => (((AsyncLock)state).CreateReleaser()), - this, CancellationToken.None, - TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); - } + //public Task LockAsync(int millisecondsTimeout) + //{ + // var wait = _semaphore != null + // ? _semaphore.WaitAsync(millisecondsTimeout) + // : _semaphore2.WaitOneAsync(millisecondsTimeout); + + // return wait.IsCompleted + // ? _releaserTask ?? Task.FromResult(CreateReleaser()) // anonymous vs named + // : wait.ContinueWith((_, state) => (((AsyncLock)state).CreateReleaser()), + // this, CancellationToken.None, + // TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + //} public IDisposable Lock() { diff --git a/src/Umbraco.Core/MainDom.cs b/src/Umbraco.Core/MainDom.cs index ca875c2167..d1012fb669 100644 --- a/src/Umbraco.Core/MainDom.cs +++ b/src/Umbraco.Core/MainDom.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Web.Hosting; @@ -113,7 +114,7 @@ namespace Umbraco.Core lock (_locko) { - _logger.Debug("Signaled {Signaled} ({SignalSource})", _signaled ? "(again)" : string.Empty, source); + _logger.Debug("Signaled ({Signaled}) ({SignalSource})", _signaled ? "again" : "first", source); if (_signaled) return; if (_isMainDom == false) return; // probably not needed _signaled = true; @@ -171,6 +172,7 @@ namespace Umbraco.Core // if more than 1 instance reach that point, one will get the lock // and the other one will timeout, which is accepted + //TODO: This can throw a TimeoutException - in which case should this be in a try/finally to ensure the signal is always reset? _asyncLocker = _asyncLock.Lock(LockTimeoutMilliseconds); _isMainDom = true; @@ -181,6 +183,9 @@ namespace Umbraco.Core // which is accepted _signal.Reset(); + + //WaitOneAsync (ext method) will wait for a signal without blocking the main thread, the waiting is done on a background thread + _signal.WaitOneAsync() .ContinueWith(_ => OnSignal("signal")); diff --git a/src/Umbraco.Core/Runtime/CoreRuntime.cs b/src/Umbraco.Core/Runtime/CoreRuntime.cs index f9a41b4f66..5b069641c4 100644 --- a/src/Umbraco.Core/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Core/Runtime/CoreRuntime.cs @@ -139,7 +139,7 @@ namespace Umbraco.Core.Runtime // there should be none, really - this is here "just in case" Compose(composition); - // acquire the main domain + // acquire the main domain - if this fails then anything that should be registered with MainDom will not operate AcquireMainDom(mainDom); // determine our runtime level @@ -218,13 +218,13 @@ namespace Umbraco.Core.Runtime IOHelper.SetRootDirectory(path); } - private void AcquireMainDom(MainDom mainDom) + private bool AcquireMainDom(MainDom mainDom) { using (var timer = ProfilingLogger.DebugDuration("Acquiring MainDom.", "Acquired.")) { try { - mainDom.Acquire(); + return mainDom.Acquire(); } catch { diff --git a/src/Umbraco.Core/RuntimeState.cs b/src/Umbraco.Core/RuntimeState.cs index 6fb8a04c0d..5d34fe70a1 100644 --- a/src/Umbraco.Core/RuntimeState.cs +++ b/src/Umbraco.Core/RuntimeState.cs @@ -97,6 +97,11 @@ namespace Umbraco.Core /// internal void EnsureApplicationUrl(HttpRequestBase request = null) { + //Fixme: This causes problems with site swap on azure because azure pre-warms a site by calling into `localhost` and when it does that + // it changes the URL to `localhost:80` which actually doesn't work for pinging itself, it only works internally in Azure. The ironic part + // about this is that this is here specifically for the slot swap scenario https://issues.umbraco.org/issue/U4-10626 + + // see U4-10626 - in some cases we want to reset the application url // (this is a simplified version of what was in 7.x) // note: should this be optional? is it expensive? diff --git a/src/Umbraco.Core/WaitHandleExtensions.cs b/src/Umbraco.Core/WaitHandleExtensions.cs index 0d840a2496..7a9294c113 100644 --- a/src/Umbraco.Core/WaitHandleExtensions.cs +++ b/src/Umbraco.Core/WaitHandleExtensions.cs @@ -23,6 +23,8 @@ namespace Umbraco.Core handle, (state, timedOut) => { + //TODO: We aren't checking if this is timed out + tcs.SetResult(null); // we take a lock here to make sure the outer method has completed setting the local variable callbackHandle. diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs index f3a520ead1..55304d257b 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs @@ -124,7 +124,7 @@ namespace Umbraco.Tests.PublishedContent _source = new TestDataSource(kits); // at last, create the complete NuCache snapshot service! - var options = new PublishedSnapshotService.Options { IgnoreLocalDb = true }; + var options = new PublishedSnapshotServiceOptions { IgnoreLocalDb = true }; _snapshotService = new PublishedSnapshotService(options, null, runtime, diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs index b66404c954..399b0c1342 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheTests.cs @@ -169,7 +169,7 @@ namespace Umbraco.Tests.PublishedContent _variationAccesor = new TestVariationContextAccessor(); // at last, create the complete NuCache snapshot service! - var options = new PublishedSnapshotService.Options { IgnoreLocalDb = true }; + var options = new PublishedSnapshotServiceOptions { IgnoreLocalDb = true }; _snapshotService = new PublishedSnapshotService(options, null, runtime, diff --git a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs index 397a22fc62..c974fed99e 100644 --- a/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopedNuCacheTests.cs @@ -72,7 +72,7 @@ namespace Umbraco.Tests.Scoping protected override IPublishedSnapshotService CreatePublishedSnapshotService() { - var options = new PublishedSnapshotService.Options { IgnoreLocalDb = true }; + var options = new PublishedSnapshotServiceOptions { IgnoreLocalDb = true }; var publishedSnapshotAccessor = new UmbracoContextPublishedSnapshotAccessor(Umbraco.Web.Composing.Current.UmbracoContextAccessor); var runtimeStateMock = new Mock(); runtimeStateMock.Setup(x => x.Level).Returns(() => RuntimeLevel.Run); diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs index 3121988bfe..18ea95cd98 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceVariantsTests.cs @@ -42,7 +42,7 @@ namespace Umbraco.Tests.Services protected override IPublishedSnapshotService CreatePublishedSnapshotService() { - var options = new PublishedSnapshotService.Options { IgnoreLocalDb = true }; + var options = new PublishedSnapshotServiceOptions { IgnoreLocalDb = true }; var publishedSnapshotAccessor = new UmbracoContextPublishedSnapshotAccessor(Umbraco.Web.Composing.Current.UmbracoContextAccessor); var runtimeStateMock = new Mock(); runtimeStateMock.Setup(x => x.Level).Returns(() => RuntimeLevel.Run); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs index 72fa39e7e4..f748fd555c 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/NuCacheComposer.cs @@ -15,7 +15,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // register the NuCache published snapshot service // must register default options, required in the service ctor - composition.Register(factory => new PublishedSnapshotService.Options()); + composition.Register(factory => new PublishedSnapshotServiceOptions()); composition.SetPublishedSnapshotService(); // add the NuCache health check (hidden from type finder) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index dad9811af8..5e7c0542db 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -30,7 +30,8 @@ using File = System.IO.File; namespace Umbraco.Web.PublishedCache.NuCache { - class PublishedSnapshotService : PublishedSnapshotServiceBase + + internal class PublishedSnapshotService : PublishedSnapshotServiceBase { private readonly ServiceContext _serviceContext; private readonly IPublishedContentTypeFactory _publishedContentTypeFactory; @@ -56,7 +57,7 @@ namespace Umbraco.Web.PublishedCache.NuCache private BPlusTree _localContentDb; private BPlusTree _localMediaDb; - private readonly bool _localDbExists; + private bool _localDbExists; // define constant - determines whether to use cache when previewing // to store eg routes, property converted values, anything - caching @@ -68,7 +69,7 @@ namespace Umbraco.Web.PublishedCache.NuCache //private static int _singletonCheck; - public PublishedSnapshotService(Options options, IMainDom mainDom, IRuntimeState runtime, + public PublishedSnapshotService(PublishedSnapshotServiceOptions options, IMainDom mainDom, IRuntimeState runtime, ServiceContext serviceContext, IPublishedContentTypeFactory publishedContentTypeFactory, IdkMap idkMap, IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor, ILogger logger, IScopeProvider scopeProvider, IDocumentRepository documentRepository, IMediaRepository mediaRepository, IMemberRepository memberRepository, @@ -115,30 +116,36 @@ namespace Umbraco.Web.PublishedCache.NuCache if (options.IgnoreLocalDb == false) { var registered = mainDom.Register( - null, () => { + //"install" phase of MainDom + //this is inside of a lock in MainDom so this is guaranteed to run if MainDom was acquired and guaranteed + //to not run if MainDom wasn't acquired. + //If MainDom was not acquired, then _localContentDb and _localMediaDb will remain null which means this appdomain + //will load in published content via the DB and in that case this appdomain will probably not exist long enough to + //serve more than a page of content. + + var path = GetLocalFilesPath(); + var localContentDbPath = Path.Combine(path, "NuCache.Content.db"); + var localMediaDbPath = Path.Combine(path, "NuCache.Media.db"); + _localDbExists = File.Exists(localContentDbPath) && File.Exists(localMediaDbPath); + // if both local databases exist then GetTree will open them, else new databases will be created + _localContentDb = BTree.GetTree(localContentDbPath, _localDbExists); + _localMediaDb = BTree.GetTree(localMediaDbPath, _localDbExists); + }, + () => + { + //"release" phase of MainDom + lock (_storesLock) { - _contentStore.ReleaseLocalDb(); + _contentStore?.ReleaseLocalDb(); //null check because we could shut down before being assigned _localContentDb = null; - _mediaStore.ReleaseLocalDb(); + _mediaStore?.ReleaseLocalDb(); //null check because we could shut down before being assigned _localMediaDb = null; } }); - if (registered) - { - var path = GetLocalFilesPath(); - var localContentDbPath = Path.Combine(path, "NuCache.Content.db"); - var localMediaDbPath = Path.Combine(path, "NuCache.Media.db"); - _localDbExists = System.IO.File.Exists(localContentDbPath) && System.IO.File.Exists(localMediaDbPath); - - // if both local databases exist then GetTree will open them, else new databases will be created - _localContentDb = BTree.GetTree(localContentDbPath, _localDbExists); - _localMediaDb = BTree.GetTree(localMediaDbPath, _localDbExists); - } - // stores are created with a db so they can write to it, but they do not read from it, // stores need to be populated, happens in OnResolutionFrozen which uses _localDbExists to // figure out whether it can read the databases or it should populate them from sql @@ -251,19 +258,6 @@ namespace Umbraco.Web.PublishedCache.NuCache base.Dispose(); } - public class Options - { - // disabled: prevents the published snapshot from updating and exposing changes - // or even creating a new published snapshot to see changes, uses old cache = bad - // - //// indicates that the snapshot cache should reuse the application request cache - //// otherwise a new cache object would be created for the snapshot specifically, - //// which is the default - web boot manager uses this to optimize facades - //public bool PublishedSnapshotCacheIsApplicationRequestCache; - - public bool IgnoreLocalDb; - } - #endregion #region Local files diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotServiceOptions.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotServiceOptions.cs new file mode 100644 index 0000000000..ef9d83a802 --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotServiceOptions.cs @@ -0,0 +1,28 @@ +namespace Umbraco.Web.PublishedCache.NuCache +{ + /// + /// Options class for configuring the + /// + public class PublishedSnapshotServiceOptions + { + // disabled: prevents the published snapshot from updating and exposing changes + // or even creating a new published snapshot to see changes, uses old cache = bad + // + //// indicates that the snapshot cache should reuse the application request cache + //// otherwise a new cache object would be created for the snapshot specifically, + //// which is the default - web boot manager uses this to optimize facades + //public bool PublishedSnapshotCacheIsApplicationRequestCache; + + + /// + /// If true this disables the persisted local cache files for content and media + /// + /// + /// By default this is false which means umbraco will use locally persisted cache files for reading in all published content and media on application startup. + /// The reason for this is to improve startup times because the alternative to populating the published content and media on application startup is to read + /// these values from the database. In scenarios where sites are relatively small (below a few thousand nodes) reading the content/media from the database to populate + /// the in memory cache isn't that slow and is only marginally slower than reading from the locally persisted cache files. + /// + public bool IgnoreLocalDb { get; set; } + } +} diff --git a/src/Umbraco.Web/Scheduling/KeepAlive.cs b/src/Umbraco.Web/Scheduling/KeepAlive.cs index 6dd7572b87..c07430df04 100644 --- a/src/Umbraco.Web/Scheduling/KeepAlive.cs +++ b/src/Umbraco.Web/Scheduling/KeepAlive.cs @@ -64,7 +64,7 @@ namespace Umbraco.Web.Scheduling } catch (Exception ex) { - _logger.Error(ex, "Failed (at '{UmbracoAppUrl}').", umbracoAppUrl); + _logger.Error(ex, "Keep alive failed (at '{UmbracoAppUrl}').", umbracoAppUrl); } } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 41a3393fa4..331b2ac7d0 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -224,6 +224,7 @@ + From f3bfc1ffb27618f129d1fdb2e05872e14b3a9f15 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 18 Jul 2019 16:18:11 +1000 Subject: [PATCH 41/67] Puts back UmbracoAntiForgeryAdditionalDataProvider for backwards compat reasons but it is not used --- ...mbracoAntiForgeryAdditionalDataProvider.cs | 91 +++++++++++++++++++ src/Umbraco.Web/Umbraco.Web.csproj | 1 + 2 files changed, 92 insertions(+) create mode 100644 src/Umbraco.Web/Security/UmbracoAntiForgeryAdditionalDataProvider.cs diff --git a/src/Umbraco.Web/Security/UmbracoAntiForgeryAdditionalDataProvider.cs b/src/Umbraco.Web/Security/UmbracoAntiForgeryAdditionalDataProvider.cs new file mode 100644 index 0000000000..12d7cce753 --- /dev/null +++ b/src/Umbraco.Web/Security/UmbracoAntiForgeryAdditionalDataProvider.cs @@ -0,0 +1,91 @@ +using System; +using Umbraco.Web.Mvc; +using Umbraco.Core; +using System.Web.Helpers; +using System.Web; +using Newtonsoft.Json; +using System.ComponentModel; + +namespace Umbraco.Web.Security +{ + [Obsolete("This is no longer used and will be removed from the codebase in future versions")] + [EditorBrowsable(EditorBrowsableState.Never)] + public class UmbracoAntiForgeryAdditionalDataProvider : IAntiForgeryAdditionalDataProvider + { + private readonly IAntiForgeryAdditionalDataProvider _defaultProvider; + + /// + /// Constructor, allows wrapping a default provider + /// + /// + public UmbracoAntiForgeryAdditionalDataProvider(IAntiForgeryAdditionalDataProvider defaultProvider) + { + _defaultProvider = defaultProvider; + } + + public string GetAdditionalData(HttpContextBase context) + { + return JsonConvert.SerializeObject(new AdditionalData + { + Stamp = DateTime.UtcNow.Ticks, + //this value will be here if this is a BeginUmbracoForms form + Ufprt = context.Items["ufprt"]?.ToString(), + //if there was a wrapped provider, add it's value to the json, else just a static value + WrappedValue = _defaultProvider?.GetAdditionalData(context) ?? "default" + }); + } + + public bool ValidateAdditionalData(HttpContextBase context, string additionalData) + { + if (!additionalData.DetectIsJson()) + return false; //must be json + + AdditionalData json; + try + { + json = JsonConvert.DeserializeObject(additionalData); + } + catch + { + return false; //couldn't parse + } + + if (json.Stamp == default) return false; + + //if there was a wrapped provider, validate it, else validate the static value + var validateWrapped = _defaultProvider?.ValidateAdditionalData(context, json.WrappedValue) ?? json.WrappedValue == "default"; + if (!validateWrapped) + return false; + + var ufprtRequest = context.Request["ufprt"]?.ToString(); + + //if the custom BeginUmbracoForms route value is not there, then it's nothing more to validate + if (ufprtRequest.IsNullOrWhiteSpace() && json.Ufprt.IsNullOrWhiteSpace()) + return true; + + //if one or the other is null then something is wrong + if (!ufprtRequest.IsNullOrWhiteSpace() && json.Ufprt.IsNullOrWhiteSpace()) return false; + if (ufprtRequest.IsNullOrWhiteSpace() && !json.Ufprt.IsNullOrWhiteSpace()) return false; + + if (!UmbracoHelper.DecryptAndValidateEncryptedRouteString(json.Ufprt, out var additionalDataParts)) + return false; + + if (!UmbracoHelper.DecryptAndValidateEncryptedRouteString(ufprtRequest, out var requestParts)) + return false; + + //ensure they all match + return additionalDataParts.Count == requestParts.Count + && additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Controller] == requestParts[RenderRouteHandler.ReservedAdditionalKeys.Controller] + && additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Action] == requestParts[RenderRouteHandler.ReservedAdditionalKeys.Action] + && additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Area] == requestParts[RenderRouteHandler.ReservedAdditionalKeys.Area]; + } + + internal class AdditionalData + { + public string Ufprt { get; set; } + public long Stamp { get; set; } + public string WrappedValue { get; set; } + } + + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 2fb54a2c80..01524e5962 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -341,6 +341,7 @@ + From 6544b5c27242be6a80bd1da7b05a774521a75e75 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Thu, 18 Jul 2019 13:03:14 +0100 Subject: [PATCH 42/67] Update src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs Co-Authored-By: Bjarke Berg --- .../V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs index 5f49680d89..1b6597a660 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs @@ -85,7 +85,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_1_0 if (exceptions.Any()) { - throw new AggregateException(exceptions); + throw new AggregateException("One or more errors related to unexpected data in grid values occurred.", exceptions); } Context.AddPostMigration(); From 929c84822aaabe9437756c5272d13b32245d3f6a Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 19 Jul 2019 15:18:00 +1000 Subject: [PATCH 43/67] Fixes the Children ext method and adds unit tests --- .../PublishedContentExtensions.cs | 4 - .../PublishedContent/NuCacheChildrenTests.cs | 306 ++++++++++++------ 2 files changed, 209 insertions(+), 101 deletions(-) diff --git a/src/Umbraco.Core/PublishedContentExtensions.cs b/src/Umbraco.Core/PublishedContentExtensions.cs index f220f307d6..e09e76d06a 100644 --- a/src/Umbraco.Core/PublishedContentExtensions.cs +++ b/src/Umbraco.Core/PublishedContentExtensions.cs @@ -111,10 +111,6 @@ namespace Umbraco.Core /// public static IEnumerable Children(this IPublishedContent content, string culture = null) { - // invariant has invariant value (whatever the requested culture) - if (!content.ContentType.VariesByCulture() && culture != "*") - culture = ""; - // handle context culture for variant if (culture == null) culture = VariationContextAccessor?.VariationContext?.Culture ?? ""; diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs index f3a520ead1..cc6bcab036 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs @@ -151,117 +151,139 @@ namespace Umbraco.Tests.PublishedContent Mock.Get(factory).Setup(x => x.GetInstance(typeof(IVariationContextAccessor))).Returns(_variationAccesor); } + private IEnumerable GetNestedVariantKits() + { + var paths = new Dictionary { { -1, "-1" } }; + + //1x variant (root) + yield return CreateVariantKit(1, -1, 1, paths); + + //1x invariant under root + yield return CreateInvariantKit(4, 1, 1, paths); + + //1x variant under root + yield return CreateVariantKit(7, 1, 4, paths); + + //2x mixed under invariant + yield return CreateVariantKit(10, 4, 1, paths); + yield return CreateInvariantKit(11, 4, 2, paths); + + //2x mixed under variant + yield return CreateVariantKit(12, 7, 1, paths); + yield return CreateInvariantKit(13, 7, 2, paths); + } + private IEnumerable GetInvariantKits() { var paths = new Dictionary { { -1, "-1" } }; - ContentNodeKit CreateKit(int id, int parentId, int sortOrder) + yield return CreateInvariantKit(1, -1, 1, paths); + yield return CreateInvariantKit(2, -1, 2, paths); + yield return CreateInvariantKit(3, -1, 3, paths); + + yield return CreateInvariantKit(4, 1, 1, paths); + yield return CreateInvariantKit(5, 1, 2, paths); + yield return CreateInvariantKit(6, 1, 3, paths); + + yield return CreateInvariantKit(7, 2, 3, paths); + yield return CreateInvariantKit(8, 2, 2, paths); + yield return CreateInvariantKit(9, 2, 1, paths); + + yield return CreateInvariantKit(10, 3, 1, paths); + + yield return CreateInvariantKit(11, 4, 1, paths); + yield return CreateInvariantKit(12, 4, 2, paths); + } + + private ContentNodeKit CreateInvariantKit(int id, int parentId, int sortOrder, Dictionary paths) + { + if (!paths.TryGetValue(parentId, out var parentPath)) + throw new Exception("Unknown parent."); + + var path = paths[id] = parentPath + "," + id; + var level = path.Count(x => x == ','); + var now = DateTime.Now; + + return new ContentNodeKit { - if (!paths.TryGetValue(parentId, out var parentPath)) - throw new Exception("Unknown parent."); - - var path = paths[id] = parentPath + "," + id; - var level = path.Count(x => x == ','); - var now = DateTime.Now; - - return new ContentNodeKit + ContentTypeId = _contentTypeInvariant.Id, + Node = new ContentNode(id, Guid.NewGuid(), level, path, sortOrder, parentId, DateTime.Now, 0), + DraftData = null, + PublishedData = new ContentData { - ContentTypeId = _contentTypeInvariant.Id, - Node = new ContentNode(id, Guid.NewGuid(), level, path, sortOrder, parentId, DateTime.Now, 0), - DraftData = null, - PublishedData = new ContentData - { - Name = "N" + id, - Published = true, - TemplateId = 0, - VersionId = 1, - VersionDate = now, - WriterId = 0, - Properties = new Dictionary(), - CultureInfos = new Dictionary() - } - }; - } - - yield return CreateKit(1, -1, 1); - yield return CreateKit(2, -1, 2); - yield return CreateKit(3, -1, 3); - - yield return CreateKit(4, 1, 1); - yield return CreateKit(5, 1, 2); - yield return CreateKit(6, 1, 3); - - yield return CreateKit(7, 2, 3); - yield return CreateKit(8, 2, 2); - yield return CreateKit(9, 2, 1); - - yield return CreateKit(10, 3, 1); - - yield return CreateKit(11, 4, 1); - yield return CreateKit(12, 4, 2); + Name = "N" + id, + Published = true, + TemplateId = 0, + VersionId = 1, + VersionDate = now, + WriterId = 0, + Properties = new Dictionary(), + CultureInfos = new Dictionary() + } + }; } private IEnumerable GetVariantKits() { var paths = new Dictionary { { -1, "-1" } }; - Dictionary GetCultureInfos(int id, DateTime now) + yield return CreateVariantKit(1, -1, 1, paths); + yield return CreateVariantKit(2, -1, 2, paths); + yield return CreateVariantKit(3, -1, 3, paths); + + yield return CreateVariantKit(4, 1, 1, paths); + yield return CreateVariantKit(5, 1, 2, paths); + yield return CreateVariantKit(6, 1, 3, paths); + + yield return CreateVariantKit(7, 2, 3, paths); + yield return CreateVariantKit(8, 2, 2, paths); + yield return CreateVariantKit(9, 2, 1, paths); + + yield return CreateVariantKit(10, 3, 1, paths); + + yield return CreateVariantKit(11, 4, 1, paths); + yield return CreateVariantKit(12, 4, 2, paths); + } + + private static Dictionary GetCultureInfos(int id, DateTime now) + { + var en = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }; + var fr = new[] { 1, 3, 4, 6, 7, 9, 10, 12 }; + + var infos = new Dictionary(); + if (en.Contains(id)) + infos["en-US"] = new CultureVariation { Name = "N" + id + "-" + "en-US", Date = now, IsDraft = false }; + if (fr.Contains(id)) + infos["fr-FR"] = new CultureVariation { Name = "N" + id + "-" + "fr-FR", Date = now, IsDraft = false }; + return infos; + } + + private ContentNodeKit CreateVariantKit(int id, int parentId, int sortOrder, Dictionary paths) + { + if (!paths.TryGetValue(parentId, out var parentPath)) + throw new Exception("Unknown parent."); + + var path = paths[id] = parentPath + "," + id; + var level = path.Count(x => x == ','); + var now = DateTime.Now; + + return new ContentNodeKit { - var en = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }; - var fr = new[] { 1, 3, 4, 6, 7, 9, 10, 12 }; - - var infos = new Dictionary(); - if (en.Contains(id)) - infos["en-US"] = new CultureVariation { Name = "N" + id + "-" + "en-US", Date = now, IsDraft = false }; - if (fr.Contains(id)) - infos["fr-FR"] = new CultureVariation { Name = "N" + id + "-" + "fr-FR", Date = now, IsDraft = false }; - return infos; - } - - ContentNodeKit CreateKit(int id, int parentId, int sortOrder) - { - if (!paths.TryGetValue(parentId, out var parentPath)) - throw new Exception("Unknown parent."); - - var path = paths[id] = parentPath + "," + id; - var level = path.Count(x => x == ','); - var now = DateTime.Now; - - return new ContentNodeKit + ContentTypeId = _contentTypeVariant.Id, + Node = new ContentNode(id, Guid.NewGuid(), level, path, sortOrder, parentId, DateTime.Now, 0), + DraftData = null, + PublishedData = new ContentData { - ContentTypeId = _contentTypeVariant.Id, - Node = new ContentNode(id, Guid.NewGuid(), level, path, sortOrder, parentId, DateTime.Now, 0), - DraftData = null, - PublishedData = new ContentData - { - Name = "N" + id, - Published = true, - TemplateId = 0, - VersionId = 1, - VersionDate = now, - WriterId = 0, - Properties = new Dictionary(), - CultureInfos = GetCultureInfos(id, now) - } - }; - } - - yield return CreateKit(1, -1, 1); - yield return CreateKit(2, -1, 2); - yield return CreateKit(3, -1, 3); - - yield return CreateKit(4, 1, 1); - yield return CreateKit(5, 1, 2); - yield return CreateKit(6, 1, 3); - - yield return CreateKit(7, 2, 3); - yield return CreateKit(8, 2, 2); - yield return CreateKit(9, 2, 1); - - yield return CreateKit(10, 3, 1); - - yield return CreateKit(11, 4, 1); - yield return CreateKit(12, 4, 2); + Name = "N" + id, + Published = true, + TemplateId = 0, + VersionId = 1, + VersionDate = now, + WriterId = 0, + Properties = new Dictionary(), + CultureInfos = GetCultureInfos(id, now) + } + }; } private IEnumerable GetVariantWithDraftKits() @@ -660,6 +682,96 @@ namespace Umbraco.Tests.PublishedContent Assert.AreEqual(1, snapshot.Content.GetById(7).Parent?.Id); } + [Test] + public void NestedVariationChildrenTest() + { + var mixedKits = GetNestedVariantKits(); + Init(mixedKits); + + var snapshot = _snapshotService.CreatePublishedSnapshot(previewToken: null); + _snapshotAccessor.PublishedSnapshot = snapshot; + + //TEST with en-us variation context + + _variationAccesor.VariationContext = new VariationContext("en-US"); + + var documents = snapshot.Content.GetAtRoot().ToArray(); + AssertDocuments(documents, "N1-en-US"); + + documents = snapshot.Content.GetById(1).Children().ToArray(); + AssertDocuments(documents, "N4", "N7-en-US"); + + //Get the invariant and list children, there's a variation context so it should return invariant AND en-us variants + documents = snapshot.Content.GetById(4).Children().ToArray(); + AssertDocuments(documents, "N10-en-US", "N11"); + + //Get the variant and list children, there's a variation context so it should return invariant AND en-us variants + documents = snapshot.Content.GetById(7).Children().ToArray(); + AssertDocuments(documents, "N12-en-US", "N13"); + + //TEST with fr-fr variation context + + _variationAccesor.VariationContext = new VariationContext("fr-FR"); + + documents = snapshot.Content.GetAtRoot().ToArray(); + AssertDocuments(documents, "N1-fr-FR"); + + documents = snapshot.Content.GetById(1).Children().ToArray(); + AssertDocuments(documents, "N4", "N7-fr-FR"); + + //Get the invariant and list children, there's a variation context so it should return invariant AND en-us variants + documents = snapshot.Content.GetById(4).Children().ToArray(); + AssertDocuments(documents, "N10-fr-FR", "N11"); + + //Get the variant and list children, there's a variation context so it should return invariant AND en-us variants + documents = snapshot.Content.GetById(7).Children().ToArray(); + AssertDocuments(documents, "N12-fr-FR", "N13"); + + //TEST specific cultures + + documents = snapshot.Content.GetAtRoot("fr-FR").ToArray(); + AssertDocuments(documents, "N1-fr-FR"); + + documents = snapshot.Content.GetById(1).Children("fr-FR").ToArray(); + AssertDocuments(documents, "N4", "N7-fr-FR"); //TODO: Returns invariant ... is that expected? + documents = snapshot.Content.GetById(1).Children("").ToArray(); + AssertDocuments(documents, "N4"); //Only returns invariant since that is what was requested + + documents = snapshot.Content.GetById(4).Children("fr-FR").ToArray(); + AssertDocuments(documents, "N10-fr-FR", "N11"); //TODO: Returns invariant ... is that expected? + documents = snapshot.Content.GetById(4).Children("").ToArray(); + AssertDocuments(documents, "N11"); //Only returns invariant since that is what was requested + + documents = snapshot.Content.GetById(7).Children("fr-FR").ToArray(); + AssertDocuments(documents, "N12-fr-FR", "N13"); //TODO: Returns invariant ... is that expected? + documents = snapshot.Content.GetById(7).Children("").ToArray(); + AssertDocuments(documents, "N13"); //Only returns invariant since that is what was requested + + //TEST without variation context + // This will actually convert the culture to "" which will be invariant since that's all it will know how to do + // This will return a NULL name for culture specific entities because there is no variation context + + _variationAccesor.VariationContext = null; + + documents = snapshot.Content.GetAtRoot().ToArray(); + //will return nothing because there's only variant at root + Assert.AreEqual(0, documents.Length); + //so we'll continue to getting the known variant, do not fully assert this because the Name will NULL + documents = snapshot.Content.GetAtRoot("fr-FR").ToArray(); + Assert.AreEqual(1, documents.Length); + + documents = snapshot.Content.GetById(1).Children().ToArray(); + AssertDocuments(documents, "N4"); + + //Get the invariant and list children + documents = snapshot.Content.GetById(4).Children().ToArray(); + AssertDocuments(documents, "N11"); + + //Get the variant and list children + documents = snapshot.Content.GetById(7).Children().ToArray(); + AssertDocuments(documents, "N13"); + } + [Test] public void VariantChildrenTest() { From 487e45b45bf8d74095838bd1268c809c1e3c49b3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 19 Jul 2019 15:26:31 +1000 Subject: [PATCH 44/67] updates note --- src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs index cc6bcab036..65736aabd4 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs @@ -733,17 +733,17 @@ namespace Umbraco.Tests.PublishedContent AssertDocuments(documents, "N1-fr-FR"); documents = snapshot.Content.GetById(1).Children("fr-FR").ToArray(); - AssertDocuments(documents, "N4", "N7-fr-FR"); //TODO: Returns invariant ... is that expected? + AssertDocuments(documents, "N4", "N7-fr-FR"); //NOTE: Returns invariant, this is expected documents = snapshot.Content.GetById(1).Children("").ToArray(); AssertDocuments(documents, "N4"); //Only returns invariant since that is what was requested documents = snapshot.Content.GetById(4).Children("fr-FR").ToArray(); - AssertDocuments(documents, "N10-fr-FR", "N11"); //TODO: Returns invariant ... is that expected? + AssertDocuments(documents, "N10-fr-FR", "N11"); //NOTE: Returns invariant, this is expected documents = snapshot.Content.GetById(4).Children("").ToArray(); AssertDocuments(documents, "N11"); //Only returns invariant since that is what was requested documents = snapshot.Content.GetById(7).Children("fr-FR").ToArray(); - AssertDocuments(documents, "N12-fr-FR", "N13"); //TODO: Returns invariant ... is that expected? + AssertDocuments(documents, "N12-fr-FR", "N13"); //NOTE: Returns invariant, this is expected documents = snapshot.Content.GetById(7).Children("").ToArray(); AssertDocuments(documents, "N13"); //Only returns invariant since that is what was requested From b07df634224443152e8d4aebce967c12fe2c39f4 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 19 Jul 2019 15:49:39 +1000 Subject: [PATCH 45/67] Adds some better notes to the api docs --- src/Umbraco.Core/PublishedContentExtensions.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/PublishedContentExtensions.cs b/src/Umbraco.Core/PublishedContentExtensions.cs index e09e76d06a..921883b822 100644 --- a/src/Umbraco.Core/PublishedContentExtensions.cs +++ b/src/Umbraco.Core/PublishedContentExtensions.cs @@ -103,11 +103,23 @@ namespace Umbraco.Core /// Gets the children of the content item. /// /// The content item. - /// The specific culture to get the url children for. If null is used the current culture is used (Default is null). + /// + /// The specific culture to get the url children for. Default is null which will use the current culture in + /// /// /// Gets children that are available for the specified culture. /// Children are sorted by their sortOrder. - /// The '*' culture and supported and returns everything. + /// + /// For culture, + /// if null is used the current culture is used. + /// If an empty string is used only invariant children are returned. + /// If "*" is used all children are returned. + /// + /// + /// If a variant culture is specified or there is a current culture in the then the Children returned + /// will include both the variant children matching the culture AND the invariant children because the invariant children flow with the current culture. + /// However, if an empty string is specified only invariant children are returned. + /// /// public static IEnumerable Children(this IPublishedContent content, string culture = null) { From 02f49a003931a82d0a827e641206ab15d5acd150 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 19 Jul 2019 16:00:42 +1000 Subject: [PATCH 46/67] Cherry picks fix for 5894 and makes adjustments --- .../PublishedCache/NuCache/PublishedSnapshotService.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index dad9811af8..75f6d2f5c3 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -1056,8 +1056,10 @@ namespace Umbraco.Web.PublishedCache.NuCache : _dataSource.GetTypeMediaSources(scope, refreshedIds).ToArray(); _mediaStore.UpdateContentTypes(removedIds, typesA, kits); - _mediaStore.UpdateContentTypes(CreateContentTypes(PublishedItemType.Media, otherIds.ToArray()).ToArray()); - _mediaStore.NewContentTypes(CreateContentTypes(PublishedItemType.Media, newIds.ToArray()).ToArray()); + if (!otherIds.IsCollectionEmpty()) + _mediaStore.UpdateContentTypes(CreateContentTypes(PublishedItemType.Media, otherIds.ToArray()).ToArray()); + if (!newIds.IsCollectionEmpty()) + _mediaStore.NewContentTypes(CreateContentTypes(PublishedItemType.Media, newIds.ToArray()).ToArray()); scope.Complete(); } } From 47c3e3a79f4c40a53390b1a3867a13bbf50e895b Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 19 Jul 2019 11:20:31 +0200 Subject: [PATCH 47/67] https://github.com/umbraco/Umbraco-CMS/issues/5921 - AB#1794 - Remove trashed nodes from sql when building xml for previews --- .../Repositories/ContentRepository.cs | 2745 +++++++++-------- 1 file changed, 1373 insertions(+), 1372 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index be5cc20cb6..a0b211b6b2 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -1,1372 +1,1373 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Globalization; -using System.Linq; -using System.Xml; -using System.Xml.Linq; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.Models.EntityBase; -using Umbraco.Core.Models.Membership; -using Umbraco.Core.Models.Rdbms; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; -using Umbraco.Core.Persistence.Factories; -using Umbraco.Core.Persistence.Querying; -using Umbraco.Core.Cache; -using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Core.Persistence.SqlSyntax; -using Umbraco.Core.Persistence.UnitOfWork; - -namespace Umbraco.Core.Persistence.Repositories -{ - /// - /// Represents a repository for doing CRUD operations for - /// - internal class ContentRepository : RecycleBinRepository, IContentRepository - { - private readonly IContentTypeRepository _contentTypeRepository; - private readonly ITemplateRepository _templateRepository; - private readonly ITagRepository _tagRepository; - private readonly ContentPreviewRepository _contentPreviewRepository; - private readonly ContentXmlRepository _contentXmlRepository; - private readonly PermissionRepository _permissionRepository; - private readonly ContentByGuidReadRepository _contentByGuidReadRepository; - - public ContentRepository(IScopeUnitOfWork work, CacheHelper cacheHelper, ILogger logger, ISqlSyntaxProvider syntaxProvider, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, IContentSection contentSection) - : base(work, cacheHelper, logger, syntaxProvider, contentSection) - { - if (contentTypeRepository == null) throw new ArgumentNullException("contentTypeRepository"); - if (templateRepository == null) throw new ArgumentNullException("templateRepository"); - if (tagRepository == null) throw new ArgumentNullException("tagRepository"); - _contentTypeRepository = contentTypeRepository; - _templateRepository = templateRepository; - _tagRepository = tagRepository; - _contentPreviewRepository = new ContentPreviewRepository(work, CacheHelper.NoCache, logger, syntaxProvider); - _contentXmlRepository = new ContentXmlRepository(work, CacheHelper.NoCache, logger, syntaxProvider); - _permissionRepository = new PermissionRepository(UnitOfWork, cacheHelper, Logger, SqlSyntax); - _contentByGuidReadRepository = new ContentByGuidReadRepository(this, work, cacheHelper, logger, syntaxProvider); - EnsureUniqueNaming = true; - } - - public bool EnsureUniqueNaming { get; set; } - - #region Overrides of RepositoryBase - - protected override IContent PerformGet(int id) - { - var sql = GetBaseQuery(BaseQueryType.FullSingle) - .Where(GetBaseWhereClause(), new { Id = id }) - .Where(x => x.Newest, SqlSyntax) - .OrderByDescending(x => x.VersionDate, SqlSyntax); - - var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); - - if (dto == null) - return null; - - var content = CreateContentFromDto(dto, sql); - - return content; - } - - protected override IEnumerable PerformGetAll(params int[] ids) - { - Func translate = s => - { - if (ids.Any()) - { - s.Where("umbracoNode.id in (@ids)", new { ids }); - } - //we only want the newest ones with this method - s.Where(x => x.Newest, SqlSyntax); - return s; - }; - - var sqlBaseFull = GetBaseQuery(BaseQueryType.FullMultiple); - var sqlBaseIds = GetBaseQuery(BaseQueryType.Ids); - - return ProcessQuery(translate(sqlBaseFull), new PagingSqlQuery(translate(sqlBaseIds))); - } - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - var sqlBaseFull = GetBaseQuery(BaseQueryType.FullMultiple); - var sqlBaseIds = GetBaseQuery(BaseQueryType.Ids); - - Func, Sql> translate = (translator) => - { - return translator.Translate() - .Where(x => x.Newest, SqlSyntax) - .OrderByDescending(x => x.VersionDate, SqlSyntax) - .OrderBy(x => x.SortOrder, SqlSyntax); - }; - - var translatorFull = new SqlTranslator(sqlBaseFull, query); - var translatorIds = new SqlTranslator(sqlBaseIds, query); - - return ProcessQuery(translate(translatorFull), new PagingSqlQuery(translate(translatorIds))); - } - - #endregion - - #region Overrides of PetaPocoRepositoryBase - - /// - /// Returns the base query to return Content - /// - /// - /// - /// - /// Content queries will differ depending on what needs to be returned: - /// * FullSingle: When querying for a single document, this will include the Outer join to fetch the content item's published version info - /// * FullMultiple: When querying for multiple documents, this will exclude the Outer join to fetch the content item's published version info - this info would need to be fetched separately - /// * Ids: This would essentially be the same as FullMultiple however the columns specified will only return the Ids for the documents - /// * Count: A query to return the count for documents - /// - protected override Sql GetBaseQuery(BaseQueryType queryType) - { - var sql = new Sql(); - sql.Select(queryType == BaseQueryType.Count ? "COUNT(*)" : (queryType == BaseQueryType.Ids ? "cmsDocument.nodeId" : "*")) - .From(SqlSyntax) - .InnerJoin(SqlSyntax) - .On(SqlSyntax, left => left.VersionId, right => right.VersionId) - .InnerJoin(SqlSyntax) - .On(SqlSyntax, left => left.NodeId, right => right.NodeId) - .InnerJoin(SqlSyntax) - .On(SqlSyntax, left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.ContentTypeId); - //TODO: IF we want to enable querying on content type information this will need to be joined - //.InnerJoin(SqlSyntax) - //.On(SqlSyntax, left => left.ContentTypeId, right => right.NodeId, SqlSyntax); - - if (queryType == BaseQueryType.FullSingle) - { - //The only reason we apply this left outer join is to be able to pull back the DocumentPublishedReadOnlyDto - //information with the entire data set, so basically this will get both the latest document and also it's published - //version if it has one. When performing a count or when retrieving Ids like in paging, this is unecessary - //and causes huge performance overhead for the SQL server, especially when sorting the result. - //We also don't include this outer join when querying for multiple entities since it is much faster to fetch this information - //in a separate query. For a single entity this is ok. - - var sqlx = string.Format("LEFT OUTER JOIN {0} {1} ON ({1}.{2}={0}.{2} AND {1}.{3}=1)", - SqlSyntax.GetQuotedTableName("cmsDocument"), - SqlSyntax.GetQuotedTableName("cmsDocument2"), - SqlSyntax.GetQuotedColumnName("nodeId"), - SqlSyntax.GetQuotedColumnName("published")); - - // cannot do this because PetaPoco does not know how to alias the table - //.LeftOuterJoin() - //.On(left => left.NodeId, right => right.NodeId) - // so have to rely on writing our own SQL - sql.Append(sqlx /*, new { @published = true }*/); - } - - sql.Where(x => x.NodeObjectType == NodeObjectTypeId, SqlSyntax); - - return sql; - } - - protected override Sql GetBaseQuery(bool isCount) - { - return GetBaseQuery(isCount ? BaseQueryType.Count : BaseQueryType.FullSingle); - } - - protected override string GetBaseWhereClause() - { - return "umbracoNode.id = @Id"; - } - - protected override IEnumerable GetDeleteClauses() - { - var list = new List - { - "DELETE FROM umbracoRedirectUrl WHERE contentKey IN (SELECT uniqueID FROM umbracoNode WHERE id = @Id)", - "DELETE FROM cmsTask WHERE nodeId = @Id", - "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @Id", - "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @Id", - "DELETE FROM umbracoUserStartNode WHERE startNode = @Id", - "UPDATE umbracoUserGroup SET startContentId = NULL WHERE startContentId = @Id", - "DELETE FROM umbracoRelation WHERE parentId = @Id", - "DELETE FROM umbracoRelation WHERE childId = @Id", - "DELETE FROM cmsTagRelationship WHERE nodeId = @Id", - "DELETE FROM umbracoDomains WHERE domainRootStructureID = @Id", - "DELETE FROM cmsDocument WHERE nodeId = @Id", - "DELETE FROM cmsPropertyData WHERE contentNodeId = @Id", - "DELETE FROM cmsPreviewXml WHERE nodeId = @Id", - "DELETE FROM cmsContentVersion WHERE ContentId = @Id", - "DELETE FROM cmsContentXml WHERE nodeId = @Id", - "DELETE FROM cmsContent WHERE nodeId = @Id", - "DELETE FROM umbracoAccess WHERE nodeId = @Id", - "DELETE FROM umbracoNode WHERE id = @Id" - }; - return list; - } - - protected override Guid NodeObjectTypeId - { - get { return Constants.ObjectTypes.DocumentGuid; } - } - - #endregion - - #region Overrides of VersionableRepositoryBase - - public void RebuildXmlStructures(Func serializer, int groupSize = 200, IEnumerable contentTypeIds = null) - { - // the previous way of doing this was to run it all in one big transaction, - // and to bulk-insert groups of xml rows - which works, until the transaction - // times out - and besides, because v7 transactions are ReadCommited, it does - // not bring much safety - so this reverts to updating each record individually, - // and it may be slower in the end, but should be more resilient. - - var contentTypeIdsA = contentTypeIds == null ? new int[0] : contentTypeIds.ToArray(); - - Func translate = (bId, sql) => - { - if (contentTypeIdsA.Length > 0) - { - sql.WhereIn(x => x.ContentTypeId, contentTypeIdsA, SqlSyntax); - } - - sql - .Where(x => x.NodeId > bId && x.Trashed == false, SqlSyntax) - .Where(x => x.Published, SqlSyntax) - .OrderBy(x => x.NodeId, SqlSyntax); - - return sql; - }; - - var baseId = 0; - - while (true) - { - // get the next group of nodes - var sqlFull = translate(baseId, GetBaseQuery(BaseQueryType.FullMultiple)); - var sqlIds = translate(baseId, GetBaseQuery(BaseQueryType.Ids)); - - var xmlItems = ProcessQuery(SqlSyntax.SelectTop(sqlFull, groupSize), new PagingSqlQuery(SqlSyntax.SelectTop(sqlIds, groupSize))) - .Select(x => new ContentXmlDto { NodeId = x.Id, Xml = serializer(x).ToString() }) - .ToList(); - - // no more nodes, break - if (xmlItems.Count == 0) break; - - foreach (var xmlItem in xmlItems) - { - try - { - // should happen in most cases, then it tries to insert, and it should work - // unless the node has been deleted, and we just report the exception - Database.InsertOrUpdate(xmlItem); - } - catch (Exception e) - { - Logger.Error("Could not rebuild XML for nodeId=" + xmlItem.NodeId, e); - } - } - baseId = xmlItems[xmlItems.Count - 1].NodeId; - } - - //now delete the items that shouldn't be there - var sqlAllIds = translate(0, GetBaseQuery(BaseQueryType.Ids)); - var allContentIds = Database.Fetch(sqlAllIds); - var docObjectType = NodeObjectTypeId; - var xmlIdsQuery = new Sql() - .Select("DISTINCT cmsContentXml.nodeId") - .From(SqlSyntax) - .InnerJoin(SqlSyntax) - .On(SqlSyntax, left => left.NodeId, right => right.NodeId); - - if (contentTypeIdsA.Length > 0) - { - xmlIdsQuery.InnerJoin(SqlSyntax) - .On(SqlSyntax, left => left.NodeId, right => right.NodeId) - .InnerJoin(SqlSyntax) - .On(SqlSyntax, left => left.NodeId, right => right.ContentTypeId) - .WhereIn(x => x.ContentTypeId, contentTypeIdsA, SqlSyntax); - } - - xmlIdsQuery.Where(dto => dto.NodeObjectType == docObjectType, SqlSyntax); - - var allXmlIds = Database.Fetch(xmlIdsQuery); - - var toRemove = allXmlIds.Except(allContentIds).ToArray(); - if (toRemove.Length > 0) - { - foreach (var idGroup in toRemove.InGroupsOf(2000)) - { - Database.Execute("DELETE FROM cmsContentXml WHERE nodeId IN (@ids)", new { ids = idGroup }); - } - } - - } - - public override IEnumerable GetAllVersions(int id) - { - Func translate = s => - { - return s.Where(GetBaseWhereClause(), new {Id = id}) - .OrderByDescending(x => x.VersionDate, SqlSyntax); - }; - - var sqlFull = translate(GetBaseQuery(BaseQueryType.FullMultiple)); - var sqlIds = translate(GetBaseQuery(BaseQueryType.Ids)); - - return ProcessQuery(sqlFull, new PagingSqlQuery(sqlIds), true, includeAllVersions:true); - } - - public override IContent GetByVersion(Guid versionId) - { - var sql = GetBaseQuery(BaseQueryType.FullSingle); - //TODO: cmsContentVersion.VersionId has a Unique Index constraint applied, seems silly then to also add OrderByDescending since it would be impossible to return more than one. - sql.Where("cmsContentVersion.VersionId = @VersionId", new { VersionId = versionId }); - sql.OrderByDescending(x => x.VersionDate, SqlSyntax); - - var dto = Database.Fetch(sql).FirstOrDefault(); - - if (dto == null) - return null; - - var content = CreateContentFromDto(dto, sql); - - return content; - } - - public override void DeleteVersion(Guid versionId) - { - var sql = new Sql() - .Select("*") - .From(SqlSyntax) - .InnerJoin(SqlSyntax) - .On(SqlSyntax, left => left.VersionId, right => right.VersionId) - .Where(x => x.VersionId == versionId, SqlSyntax) - .Where(x => x.Newest != true, SqlSyntax); - var dto = Database.Fetch(sql).FirstOrDefault(); - - if (dto == null) return; - - using (var transaction = Database.GetTransaction()) - { - PerformDeleteVersion(dto.NodeId, versionId); - - transaction.Complete(); - } - } - - public override void DeleteVersions(int id, DateTime versionDate) - { - var sql = new Sql() - .Select("*") - .From() - .InnerJoin() - .On(left => left.VersionId, right => right.VersionId) - .Where(x => x.NodeId == id) - .Where(x => x.VersionDate < versionDate) - .Where(x => x.Newest != true); - var list = Database.Fetch(sql); - if (list.Any() == false) return; - - using (var transaction = Database.GetTransaction()) - { - foreach (var dto in list) - { - PerformDeleteVersion(id, dto.VersionId); - } - - transaction.Complete(); - } - } - - protected override void PerformDeleteVersion(int id, Guid versionId) - { - Database.Delete("WHERE nodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); - Database.Delete("WHERE contentNodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); - Database.Delete("WHERE ContentId = @Id AND VersionId = @VersionId", new { Id = id, VersionId = versionId }); - Database.Delete("WHERE nodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); - } - - #endregion - - #region Unit of Work Implementation - - protected override void PersistDeletedItem(IContent entity) - { - //We need to clear out all access rules but we need to do this in a manual way since - // nothing in that table is joined to a content id - var subQuery = new Sql() - .Select("umbracoAccessRule.accessId") - .From(SqlSyntax) - .InnerJoin(SqlSyntax) - .On(SqlSyntax, left => left.AccessId, right => right.Id) - .Where(dto => dto.NodeId == entity.Id); - Database.Execute(SqlSyntax.GetDeleteSubquery("umbracoAccessRule", "accessId", subQuery)); - - //now let the normal delete clauses take care of everything else - base.PersistDeletedItem(entity); - } - - protected override void PersistNewItem(IContent entity) - { - ((Content)entity).AddingEntity(); - - //ensure the default template is assigned - if (entity.Template == null) - { - entity.Template = entity.ContentType.DefaultTemplate; - } - - //Ensure unique name on the same level - entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name); - - //Ensure that strings don't contain characters that are invalid in XML - entity.SanitizeEntityPropertiesForXmlStorage(); - - var factory = new ContentFactory(NodeObjectTypeId, entity.Id); - var dto = factory.BuildDto(entity); - - //NOTE Should the logic below have some kind of fallback for empty parent ids ? - //Logic for setting Path, Level and SortOrder - var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); - var level = parent.Level + 1; - var maxSortOrder = Database.ExecuteScalar( - "SELECT coalesce(max(sortOrder),-1) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", - new { /*ParentId =*/ entity.ParentId, NodeObjectType = NodeObjectTypeId }); - var sortOrder = maxSortOrder + 1; - - //Create the (base) node data - umbracoNode - var nodeDto = dto.ContentVersionDto.ContentDto.NodeDto; - nodeDto.Path = parent.Path; - nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); - nodeDto.SortOrder = sortOrder; - - // note: - // there used to be a check on Database.IsNew(nodeDto) here to either Insert or Update, - // but I cannot figure out what was the point, as the node should obviously be new if - // we reach that point - removed. - - // see if there's a reserved identifier for this unique id - var sql = new Sql("SELECT id FROM umbracoNode WHERE uniqueID=@0 AND nodeObjectType=@1", nodeDto.UniqueId, Constants.ObjectTypes.IdReservationGuid); - var id = Database.ExecuteScalar(sql); - if (id > 0) - { - nodeDto.NodeId = id; - Database.Update(nodeDto); - } - else - { - Database.Insert(nodeDto); - } - - //Update with new correct path - nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); - nodeDto.ValidatePathWithException(); - Database.Update(nodeDto); - - //Update entity with correct values - entity.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set - entity.Path = nodeDto.Path; - entity.SortOrder = sortOrder; - entity.Level = level; - - //Create the Content specific data - cmsContent - var contentDto = dto.ContentVersionDto.ContentDto; - contentDto.NodeId = nodeDto.NodeId; - Database.Insert(contentDto); - - //Create the first version - cmsContentVersion - //Assumes a new Version guid and Version date (modified date) has been set - var contentVersionDto = dto.ContentVersionDto; - contentVersionDto.NodeId = nodeDto.NodeId; - Database.Insert(contentVersionDto); - - //Create the Document specific data for this version - cmsDocument - //Assumes a new Version guid has been generated - dto.NodeId = nodeDto.NodeId; - Database.Insert(dto); - - //Create the PropertyData for this version - cmsPropertyData - var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); - var propertyDataDtos = propertyFactory.BuildDto(entity.Properties); - var keyDictionary = new Dictionary(); - - //Add Properties - foreach (var propertyDataDto in propertyDataDtos) - { - var primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); - keyDictionary.Add(propertyDataDto.PropertyTypeId, primaryKey); - } - - //Update Properties with its newly set Id - foreach (var property in entity.Properties) - { - property.Id = keyDictionary[property.PropertyTypeId]; - } - - //lastly, check if we are a creating a published version , then update the tags table - if (entity.Published) - { - UpdatePropertyTags(entity, _tagRepository); - } - - // published => update published version infos, else leave it blank - if (entity.Published) - { - dto.DocumentPublishedReadOnlyDto = new DocumentPublishedReadOnlyDto - { - VersionId = dto.VersionId, - VersionDate = dto.UpdateDate, - Newest = true, - NodeId = dto.NodeId, - Published = true - }; - ((Content) entity).PublishedVersionGuid = dto.VersionId; - ((Content) entity).PublishedDate = dto.UpdateDate; - } - - entity.ResetDirtyProperties(); - } - - protected override void PersistUpdatedItem(IContent entity) - { - var publishedState = ((Content)entity).PublishedState; - - //check if we need to make any database changes at all - if (entity.RequiresSaving(publishedState) == false) - { - entity.ResetDirtyProperties(); - return; - } - - //check if we need to create a new version - bool shouldCreateNewVersion = entity.ShouldCreateNewVersion(publishedState); - if (shouldCreateNewVersion) - { - //Updates Modified date and Version Guid - ((Content)entity).UpdatingEntity(); - } - else - { - if (entity.IsPropertyDirty("UpdateDate") == false || entity.UpdateDate == default(DateTime)) - entity.UpdateDate = DateTime.Now; - } - - //Ensure unique name on the same level - entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id); - - //Ensure that strings don't contain characters that are invalid in XML - entity.SanitizeEntityPropertiesForXmlStorage(); - - //Look up parent to get and set the correct Path and update SortOrder if ParentId has changed - if (entity.IsPropertyDirty("ParentId")) - { - var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); - entity.Path = string.Concat(parent.Path, ",", entity.Id); - entity.Level = parent.Level + 1; - var maxSortOrder = - Database.ExecuteScalar( - "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", - new { ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId }); - entity.SortOrder = maxSortOrder + 1; - - //Question: If we move a node, should we update permissions to inherit from the new parent if the parent has permissions assigned? - // if we do that, then we'd need to propogate permissions all the way downward which might not be ideal for many people. - // Gonna just leave it as is for now, and not re-propogate permissions. - } - - var factory = new ContentFactory(NodeObjectTypeId, entity.Id); - //Look up Content entry to get Primary for updating the DTO - var contentDto = Database.SingleOrDefault("WHERE nodeId = @Id", new { Id = entity.Id }); - factory.SetPrimaryKey(contentDto.PrimaryKey); - var dto = factory.BuildDto(entity); - - //Updates the (base) node data - umbracoNode - var nodeDto = dto.ContentVersionDto.ContentDto.NodeDto; - nodeDto.ValidatePathWithException(); - var o = Database.Update(nodeDto); - - //Only update this DTO if the contentType has actually changed - if (contentDto.ContentTypeId != entity.ContentTypeId) - { - //Create the Content specific data - cmsContent - var newContentDto = dto.ContentVersionDto.ContentDto; - Database.Update(newContentDto); - } - - //a flag that we'll use later to create the tags in the tag db table - var publishedStateChanged = false; - - //If Published state has changed then previous versions should have their publish state reset. - //If state has been changed to unpublished the previous versions publish state should also be reset. - //if (((ICanBeDirty)entity).IsPropertyDirty("Published") && (entity.Published || publishedState == PublishedState.Unpublished)) - if (entity.ShouldClearPublishedFlagForPreviousVersions(publishedState, shouldCreateNewVersion)) - { - //TODO: This perf can be improved, it could easily be UPDATE WHERE.... (one SQL call instead of many) - var publishedDocs = Database.Fetch("WHERE nodeId = @Id AND published = @IsPublished", new { Id = entity.Id, IsPublished = true }); - foreach (var doc in publishedDocs) - { - var docDto = doc; - docDto.Published = false; - Database.Update(docDto); - } - - //this is a newly published version so we'll update the tags table too (end of this method) - publishedStateChanged = true; - } - - //Look up (newest) entries by id in cmsDocument table to set newest = false - //TODO: This perf can be improved, it could easily be UPDATE WHERE.... (one SQL call instead of many) - var documentDtos = Database.Fetch("WHERE nodeId = @Id AND newest = @IsNewest", new { Id = entity.Id, IsNewest = true }); - foreach (var documentDto in documentDtos) - { - var docDto = documentDto; - docDto.Newest = false; - Database.Update(docDto); - } - - var contentVersionDto = dto.ContentVersionDto; - if (shouldCreateNewVersion) - { - //Create a new version - cmsContentVersion - //Assumes a new Version guid and Version date (modified date) has been set - Database.Insert(contentVersionDto); - //Create the Document specific data for this version - cmsDocument - //Assumes a new Version guid has been generated - Database.Insert(dto); - } - else - { - //In order to update the ContentVersion we need to retrieve its primary key id - var contentVerDto = Database.SingleOrDefault("WHERE VersionId = @Version", new { Version = entity.Version }); - if (contentVerDto != null) - { - contentVersionDto.Id = contentVerDto.Id; - Database.Update(contentVersionDto); - } - - Database.Update(dto); - } - - //Create the PropertyData for this version - cmsPropertyData - var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); - var propertyDataDtos = propertyFactory.BuildDto(entity.Properties); - var keyDictionary = new Dictionary(); - - //Add Properties - foreach (var propertyDataDto in propertyDataDtos) - { - if (shouldCreateNewVersion == false && propertyDataDto.Id > 0) - { - Database.Update(propertyDataDto); - } - else - { - int primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); - keyDictionary.Add(propertyDataDto.PropertyTypeId, primaryKey); - } - } - - //Update Properties with its newly set Id - if (keyDictionary.Any()) - { - foreach (var property in entity.Properties) - { - if (keyDictionary.ContainsKey(property.PropertyTypeId) == false) continue; - - property.Id = keyDictionary[property.PropertyTypeId]; - } - } - - //lastly, check if we are a newly published version and then update the tags table - if (publishedStateChanged && entity.Published) - { - UpdatePropertyTags(entity, _tagRepository); - } - else if (publishedStateChanged && (entity.Trashed || entity.Published == false)) - { - //it's in the trash or not published remove all entity tags - ClearEntityTags(entity, _tagRepository); - } - - // published => update published version infos, - // else if unpublished then clear published version infos - if (entity.Published) - { - dto.DocumentPublishedReadOnlyDto = new DocumentPublishedReadOnlyDto - { - VersionId = dto.VersionId, - VersionDate = dto.UpdateDate, - Newest = true, - NodeId = dto.NodeId, - Published = true - }; - ((Content) entity).PublishedVersionGuid = dto.VersionId; - ((Content) entity).PublishedDate = dto.UpdateDate; - } - else if (publishedStateChanged) - { - dto.DocumentPublishedReadOnlyDto = new DocumentPublishedReadOnlyDto - { - VersionId = default (Guid), - VersionDate = default (DateTime), - Newest = false, - NodeId = dto.NodeId, - Published = false - }; - ((Content) entity).PublishedVersionGuid = default(Guid); - ((Content) entity).PublishedDate = default (DateTime); - } - - entity.ResetDirtyProperties(); - } - - - #endregion - - #region Implementation of IContentRepository - - public IEnumerable GetByPublishedVersion(IQuery query) - { - Func, Sql> translate = t => - { - return t.Translate() - .Where(x => x.Published, SqlSyntax) - .OrderBy(x => x.Level, SqlSyntax) - .OrderBy(x => x.SortOrder, SqlSyntax); - }; - - // we WANT to return contents in top-down order, ie parents should come before children - // ideal would be pure xml "document order" which can be achieved with: - // ORDER BY substring(path, 1, len(path) - charindex(',', reverse(path))), sortOrder - // but that's probably an overkill - sorting by level,sortOrder should be enough - - var sqlFull = GetBaseQuery(BaseQueryType.FullMultiple); - var translatorFull = new SqlTranslator(sqlFull, query); - var sqlIds = GetBaseQuery(BaseQueryType.Ids); - var translatorIds = new SqlTranslator(sqlIds, query); - - return ProcessQuery(translate(translatorFull), new PagingSqlQuery(translate(translatorIds)), true); - } - - public IEnumerable GetBlueprints(IQuery query) - { - Func, Sql> translate = t => t.Translate(); - - var sqlFull = GetBaseQuery(BaseQueryType.FullMultiple); - var translatorFull = new SqlTranslator(sqlFull, query); - var sqlIds = GetBaseQuery(BaseQueryType.Ids); - var translatorIds = new SqlTranslator(sqlIds, query); - - return ProcessQuery(translate(translatorFull), new PagingSqlQuery(translate(translatorIds)), true); - } - - /// - /// This builds the Xml document used for the XML cache - /// - /// - public XmlDocument BuildXmlCache() - { - //TODO: This is what we should do , but converting to use XDocument would be breaking unless we convert - // to XmlDocument at the end of this, but again, this would be bad for memory... though still not nearly as - // bad as what is happening before! - // We'll keep using XmlDocument for now though, but XDocument xml generation is much faster: - // https://blogs.msdn.microsoft.com/codejunkie/2008/10/08/xmldocument-vs-xelement-performance/ - // I think we already have code in here to convert XDocument to XmlDocument but in case we don't here - // it is: https://blogs.msdn.microsoft.com/marcelolr/2009/03/13/fast-way-to-convert-xmldocument-into-xdocument/ - - //// Prepare an XmlDocument with an appropriate inline DTD to match - //// the expected content - //var parent = new XElement("root", new XAttribute("id", "-1")); - //var xmlDoc = new XDocument( - // new XDocumentType("root", null, null, DocumentType.GenerateDtd()), - // parent); - - var xmlDoc = new XmlDocument(); - var doctype = xmlDoc.CreateDocumentType("root", null, null, - ApplicationContext.Current.Services.ContentTypeService.GetContentTypesDtd()); - xmlDoc.AppendChild(doctype); - var parent = xmlDoc.CreateElement("root"); - var pIdAtt = xmlDoc.CreateAttribute("id"); - pIdAtt.Value = "-1"; - parent.Attributes.Append(pIdAtt); - xmlDoc.AppendChild(parent); - - //Ensure that only nodes that have published versions are selected - var sql = string.Format(@"select umbracoNode.id, umbracoNode.parentID, umbracoNode.sortOrder, cmsContentXml.{0}, umbracoNode.{1} from umbracoNode -inner join cmsContentXml on cmsContentXml.nodeId = umbracoNode.id and umbracoNode.nodeObjectType = @type -where umbracoNode.id in (select cmsDocument.nodeId from cmsDocument where cmsDocument.published = 1) -order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", - SqlSyntax.GetQuotedColumnName("xml"), - SqlSyntax.GetQuotedColumnName("level"), - SqlSyntax.GetQuotedColumnName("level")); - - XmlElement last = null; - - //NOTE: Query creates a reader - does not load all into memory - foreach (var row in Database.Query(sql, new { type = NodeObjectTypeId })) - { - string parentId = ((int)row.parentID).ToInvariantString(); - string xml = row.xml; - int sortOrder = row.sortOrder; - - //if the parentid is changing - if (last != null && last.GetAttribute("parentID") != parentId) - { - parent = xmlDoc.GetElementById(parentId); - if (parent == null) - { - //Need to short circuit here, if the parent is not there it means that the parent is unpublished - // and therefore the child is not published either so cannot be included in the xml cache - continue; - } - } - - var xmlDocFragment = xmlDoc.CreateDocumentFragment(); - xmlDocFragment.InnerXml = xml; - - last = (XmlElement)parent.AppendChild(xmlDocFragment); - - // fix sortOrder - see notes in UpdateSortOrder - last.Attributes["sortOrder"].Value = sortOrder.ToInvariantString(); - } - - return xmlDoc; - - } - - public XmlDocument BuildPreviewXmlCache() - { - var xmlDoc = new XmlDocument(); - var doctype = xmlDoc.CreateDocumentType("root", null, null, - ApplicationContext.Current.Services.ContentTypeService.GetContentTypesDtd()); - xmlDoc.AppendChild(doctype); - var parent = xmlDoc.CreateElement("root"); - var pIdAtt = xmlDoc.CreateAttribute("id"); - pIdAtt.Value = "-1"; - parent.Attributes.Append(pIdAtt); - xmlDoc.AppendChild(parent); - - //Ensure that only nodes that have published versions are selected - var sql = string.Format(@"select umbracoNode.id, umbracoNode.parentID, umbracoNode.sortOrder, cmsPreviewXml.{0}, umbracoNode.{1} from umbracoNode -inner join cmsPreviewXml on cmsPreviewXml.nodeId = umbracoNode.id and umbracoNode.nodeObjectType = @type -inner join cmsDocument on cmsPreviewXml.versionId = cmsDocument.versionId and cmsDocument.newest=1 -order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", - SqlSyntax.GetQuotedColumnName("xml"), - SqlSyntax.GetQuotedColumnName("level"), - SqlSyntax.GetQuotedColumnName("level")); - - XmlElement last = null; - - //NOTE: Query creates a reader - does not load all into memory - foreach (var row in Database.Query(sql, new { type = NodeObjectTypeId })) - { - string parentId = ((int)row.parentID).ToInvariantString(); - string xml = row.xml; - int sortOrder = row.sortOrder; - - //if the parentid is changing - if (last != null && last.GetAttribute("parentID") != parentId) - { - parent = xmlDoc.GetElementById(parentId); - if (parent == null) - { - //Need to short circuit here, if the parent is not there it means that the parent is unpublished - // and therefore the child is not published either so cannot be included in the xml cache - continue; - } - } - - var xmlDocFragment = xmlDoc.CreateDocumentFragment(); - xmlDocFragment.InnerXml = xml; - - last = (XmlElement)parent.AppendChild(xmlDocFragment); - - // fix sortOrder - see notes in UpdateSortOrder - last.Attributes["sortOrder"].Value = sortOrder.ToInvariantString(); - } - - return xmlDoc; - - } - - public int CountPublished(string contentTypeAlias = null) - { - if (contentTypeAlias.IsNullOrWhiteSpace()) - { - var sql = GetBaseQuery(true).Where(x => x.Trashed == false) - .Where(x => x.Published == true); - return Database.ExecuteScalar(sql); - } - else - { - var sql = GetBaseQuery(true).Where(x => x.Trashed == false) - .Where(x => x.Published == true) - .Where(x => x.Alias == contentTypeAlias); - return Database.ExecuteScalar(sql); - } - } - - public void ReplaceContentPermissions(EntityPermissionSet permissionSet) - { - _permissionRepository.ReplaceEntityPermissions(permissionSet); - } - - public void ClearPublished(IContent content) - { - var sql = "UPDATE cmsDocument SET published=0 WHERE nodeId=@id AND published=1"; - Database.Execute(sql, new {id = content.Id}); - } - - /// - /// Assigns a single permission to the current content item for the specified user group ids - /// - /// - /// - /// - public void AssignEntityPermission(IContent entity, char permission, IEnumerable groupIds) - { - _permissionRepository.AssignEntityPermission(entity, permission, groupIds); - } - - /// - /// Gets the explicit list of permissions for the content item - /// - /// - /// - public EntityPermissionCollection GetPermissionsForEntity(int entityId) - { - return _permissionRepository.GetPermissionsForEntity(entityId); - } - - /// - /// Adds/updates content/published xml - /// - /// - /// - public void AddOrUpdateContentXml(IContent content, Func xml) - { - _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); - } - - /// - /// Used to add/update a permission for a content item - /// - /// - public void AddOrUpdatePermissions(ContentPermissionSet permission) - { - _permissionRepository.AddOrUpdate(permission); - } - - /// - /// Used to remove the content xml for a content item - /// - /// - public void DeleteContentXml(IContent content) - { - _contentXmlRepository.Delete(new ContentXmlEntity(content)); - } - - /// - /// Adds/updates preview xml - /// - /// - /// - public void AddOrUpdatePreviewXml(IContent content, Func xml) - { - _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(content, xml)); - } - - /// - /// Gets paged content results - /// - /// Query to excute - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// Flag to indicate when ordering by system field - /// Search text filter - /// An Enumerable list of objects - public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter = null) - { - - //NOTE: This uses the GetBaseQuery method but that does not take into account the required 'newest' field which is - // what we always require for a paged result, so we'll ensure it's included in the filter - - var filterSql = new Sql().Append("AND (cmsDocument.newest = 1)"); - if (filter != null) - { - foreach (var filterClause in filter.GetWhereClauses()) - { - filterSql.Append(string.Format("AND ({0})", filterClause.Item1), filterClause.Item2); - } - } - - Func> filterCallback = () => new Tuple(filterSql.SQL, filterSql.Arguments); - - return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, - new Tuple("cmsDocument", "nodeId"), - (sqlFull, pagingSqlQuery) => ProcessQuery(sqlFull, pagingSqlQuery), orderBy, orderDirection, orderBySystemField, - filterCallback); - - } - - #endregion - - #region IRecycleBinRepository members - - protected override int RecycleBinId - { - get { return Constants.System.RecycleBinContent; } - } - - #endregion - - #region Read Repository implementation for GUID keys - public IContent Get(Guid id) - { - return _contentByGuidReadRepository.Get(id); - } - - IEnumerable IReadRepository.GetAll(params Guid[] ids) - { - return _contentByGuidReadRepository.GetAll(ids); - } - - public bool Exists(Guid id) - { - return _contentByGuidReadRepository.Exists(id); - } - - /// - /// A reading repository purely for looking up by GUID - /// - /// - /// TODO: This is ugly and to fix we need to decouple the IRepositoryQueryable -> IRepository -> IReadRepository which should all be separate things! - /// Then we can do the same thing with repository instances and we wouldn't need to leave all these methods as not implemented because we wouldn't need to implement them - /// - private class ContentByGuidReadRepository : PetaPocoRepositoryBase - { - private readonly ContentRepository _outerRepo; - - public ContentByGuidReadRepository(ContentRepository outerRepo, - IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) - : base(work, cache, logger, sqlSyntax) - { - _outerRepo = outerRepo; - } - - protected override IContent PerformGet(Guid id) - { - var sql = _outerRepo.GetBaseQuery(BaseQueryType.FullSingle) - .Where(GetBaseWhereClause(), new { Id = id }) - .Where(x => x.Newest, SqlSyntax) - .OrderByDescending(x => x.VersionDate, SqlSyntax); - - var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); - - if (dto == null) - return null; - - var content = _outerRepo.CreateContentFromDto(dto, sql); - - return content; - } - - protected override IEnumerable PerformGetAll(params Guid[] ids) - { - Func translate = s => - { - if (ids.Any()) - { - s.Where("umbracoNode.uniqueID in (@ids)", new { ids }); - } - //we only want the newest ones with this method - s.Where(x => x.Newest, SqlSyntax); - return s; - }; - - var sqlBaseFull = _outerRepo.GetBaseQuery(BaseQueryType.FullMultiple); - var sqlBaseIds = _outerRepo.GetBaseQuery(BaseQueryType.Ids); - - return _outerRepo.ProcessQuery(translate(sqlBaseFull), new PagingSqlQuery(translate(sqlBaseIds))); - } - - protected override Sql GetBaseQuery(bool isCount) - { - return _outerRepo.GetBaseQuery(isCount); - } - - protected override string GetBaseWhereClause() - { - return "umbracoNode.uniqueID = @Id"; - } - - protected override Guid NodeObjectTypeId - { - get { return _outerRepo.NodeObjectTypeId; } - } - - #region Not needed to implement - - protected override IEnumerable PerformGetByQuery(IQuery query) - { - throw new NotImplementedException(); - } - protected override IEnumerable GetDeleteClauses() - { - throw new NotImplementedException(); - } - protected override void PersistNewItem(IContent entity) - { - throw new NotImplementedException(); - } - protected override void PersistUpdatedItem(IContent entity) - { - throw new NotImplementedException(); - } - #endregion - } - #endregion - - protected override string GetDatabaseFieldNameForOrderBy(string orderBy) - { - //Some custom ones - switch (orderBy.ToUpperInvariant()) - { - case "NAME": - return "cmsDocument.text"; - case "UPDATER": - //TODO: This isn't going to work very nicely because it's going to order by ID, not by letter - return "cmsDocument.documentUser"; - } - - return base.GetDatabaseFieldNameForOrderBy(orderBy); - } - - /// - /// This is the underlying method that processes most queries for this repository - /// - /// - /// The FullMultiple SQL without the outer join to return all data required to create an IContent excluding it's published state data which this will query separately - /// - /// - /// The Id SQL without the outer join to just return all document ids - used to process the properties for the content item - /// - /// - /// - /// Generally when querying for content we only want to return the most recent version of the content item, however in some cases like when - /// we want to return all versions of a content item, we can't simply return the latest - /// - /// - private IEnumerable ProcessQuery(Sql sqlFull, PagingSqlQuery pagingSqlQuery, bool withCache = false, bool includeAllVersions = false) - { - // fetch returns a list so it's ok to iterate it in this method - var dtos = Database.Fetch(sqlFull); - if (dtos.Count == 0) return Enumerable.Empty(); - - //Go and get all of the published version data separately for this data, this is because when we are querying - //for multiple content items we don't include the outer join to fetch this data in the same query because - //it is insanely slow. Instead we just fetch the published version data separately in one query. - - //we need to parse the original SQL statement and reduce the columns to just cmsDocument.nodeId so that we can use - // the statement to go get the published data for all of the items by using an inner join - var parsedOriginalSql = "SELECT cmsDocument.nodeId " + sqlFull.SQL.Substring(sqlFull.SQL.IndexOf("FROM", StringComparison.Ordinal)); - //now remove everything from an Orderby clause and beyond - if (parsedOriginalSql.InvariantContains("ORDER BY ")) - { - parsedOriginalSql = parsedOriginalSql.Substring(0, parsedOriginalSql.LastIndexOf("ORDER BY ", StringComparison.Ordinal)); - } - - //order by update date DESC, if there is corrupted published flags we only want the latest! - var publishedSql = new Sql(@"SELECT cmsDocument.nodeId, cmsDocument.published, cmsDocument.versionId, cmsDocument.updateDate, cmsDocument.newest -FROM cmsDocument INNER JOIN cmsContentVersion ON cmsContentVersion.VersionId = cmsDocument.versionId -WHERE cmsDocument.published = 1 AND cmsDocument.nodeId IN -(" + parsedOriginalSql + @") -ORDER BY cmsContentVersion.id DESC -", sqlFull.Arguments); - - //go and get the published version data, we do a Query here and not a Fetch so we are - //not allocating a whole list to memory just to allocate another list in memory since - //we are assigning this data to a keyed collection for fast lookup below - var publishedData = Database.Query(publishedSql); - var publishedDataCollection = new DocumentPublishedReadOnlyDtoCollection(); - foreach (var publishedDto in publishedData) - { - //double check that there's no corrupt db data, there should only be a single published item - if (publishedDataCollection.Contains(publishedDto.NodeId) == false) - publishedDataCollection.Add(publishedDto); - } - - //This is a tuple list identifying if the content item came from the cache or not - var content = new List>(); - var defs = new DocumentDefinitionCollection(includeAllVersions); - var templateIds = new List(); - - //track the looked up content types, even though the content types are cached - // they still need to be deep cloned out of the cache and we don't want to add - // the overhead of deep cloning them on every item in this loop - var contentTypes = new Dictionary(); - - foreach (var dto in dtos) - { - DocumentPublishedReadOnlyDto publishedDto; - publishedDataCollection.TryGetValue(dto.NodeId, out publishedDto); - - // if the cache contains the published version, use it - if (withCache) - { - var cached = IsolatedCache.GetCacheItem(GetCacheIdKey(dto.NodeId)); - //only use this cached version if the dto returned is also the publish version, they must match and be teh same version - if (cached != null && cached.Version == dto.VersionId && cached.Published && dto.Published) - { - content.Add(new Tuple(cached, true)); - continue; - } - } - - // else, need to fetch from the database - // content type repository is full-cache so OK to get each one independently - - IContentType contentType; - if (contentTypes.ContainsKey(dto.ContentVersionDto.ContentDto.ContentTypeId)) - { - contentType = contentTypes[dto.ContentVersionDto.ContentDto.ContentTypeId]; - } - else - { - contentType = _contentTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); - contentTypes[dto.ContentVersionDto.ContentDto.ContentTypeId] = contentType; - } - - // track the definition and if it's successfully added or updated then processed - if (defs.AddOrUpdate(new DocumentDefinition(dto, contentType))) - { - // assign template - if (dto.TemplateId.HasValue && dto.TemplateId.Value > 0) - templateIds.Add(dto.TemplateId.Value); - - content.Add(new Tuple(ContentFactory.BuildEntity(dto, contentType, publishedDto), false)); - } - } - - // load all required templates in 1 query - var templates = _templateRepository.GetAll(templateIds.ToArray()) - .ToDictionary(x => x.Id, x => x); - - // load all properties for all documents from database in 1 query - var propertyData = GetPropertyCollection(pagingSqlQuery, defs); - - // assign template and property data - foreach (var contentItem in content) - { - var cc = contentItem.Item1; - var fromCache = contentItem.Item2; - - //if this has come from cache, we do not need to build up it's structure - if (fromCache) continue; - - var def = defs[includeAllVersions ? (ValueType)cc.Version : cc.Id]; - - ITemplate template = null; - if (def.DocumentDto.TemplateId.HasValue) - templates.TryGetValue(def.DocumentDto.TemplateId.Value, out template); // else null - cc.Template = template; - if (propertyData.ContainsKey(cc.Version)) - { - cc.Properties = propertyData[cc.Version]; - } - else - { - throw new InvalidOperationException($"No property data found for version: '{cc.Version}'."); - } - - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - cc.ResetDirtyProperties(false); - } - - return content.Select(x => x.Item1).ToArray(); - } - - /// - /// Private method to create a content object from a DocumentDto, which is used by Get and GetByVersion. - /// - /// - /// - /// - private IContent CreateContentFromDto(DocumentDto dto, Sql docSql) - { - var contentType = _contentTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); - - var content = ContentFactory.BuildEntity(dto, contentType); - - //Check if template id is set on DocumentDto, and get ITemplate if it is. - if (dto.TemplateId.HasValue && dto.TemplateId.Value > 0) - { - content.Template = _templateRepository.Get(dto.TemplateId.Value); - } - - var docDef = new DocumentDefinition(dto, contentType); - - var properties = GetPropertyCollection(docSql, new[] { docDef }); - - content.Properties = properties[dto.VersionId]; - - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - ((Entity)content).ResetDirtyProperties(false); - return content; - } - - private string EnsureUniqueNodeName(int parentId, string nodeName, int id = 0) - { - if (EnsureUniqueNaming == false) - return nodeName; - - var names = Database.Fetch("SELECT id, text AS name FROM umbracoNode WHERE nodeObjectType=@objectType AND parentId=@parentId", - new { objectType = NodeObjectTypeId, parentId }); - - return SimilarNodeName.GetUniqueName(names, id, nodeName); - } - - /// - /// Dispose disposable properties - /// - /// - /// Ensure the unit of work is disposed - /// - protected override void DisposeResources() - { - _contentTypeRepository.Dispose(); - _templateRepository.Dispose(); - _tagRepository.Dispose(); - _contentPreviewRepository.Dispose(); - _contentXmlRepository.Dispose(); - } - - /// - /// A keyed collection for fast lookup when retrieving a separate list of published version data - /// - private class DocumentPublishedReadOnlyDtoCollection : KeyedCollection - { - protected override int GetKeyForItem(DocumentPublishedReadOnlyDto item) - { - return item.NodeId; - } - - public bool TryGetValue(int key, out DocumentPublishedReadOnlyDto val) - { - if (Dictionary == null) - { - val = null; - return false; - } - return Dictionary.TryGetValue(key, out val); - } - } - } -} +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using System.Linq; +using System.Xml; +using System.Xml.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.Factories; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Cache; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.Persistence.UnitOfWork; + +namespace Umbraco.Core.Persistence.Repositories +{ + /// + /// Represents a repository for doing CRUD operations for + /// + internal class ContentRepository : RecycleBinRepository, IContentRepository + { + private readonly IContentTypeRepository _contentTypeRepository; + private readonly ITemplateRepository _templateRepository; + private readonly ITagRepository _tagRepository; + private readonly ContentPreviewRepository _contentPreviewRepository; + private readonly ContentXmlRepository _contentXmlRepository; + private readonly PermissionRepository _permissionRepository; + private readonly ContentByGuidReadRepository _contentByGuidReadRepository; + + public ContentRepository(IScopeUnitOfWork work, CacheHelper cacheHelper, ILogger logger, ISqlSyntaxProvider syntaxProvider, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, IContentSection contentSection) + : base(work, cacheHelper, logger, syntaxProvider, contentSection) + { + if (contentTypeRepository == null) throw new ArgumentNullException("contentTypeRepository"); + if (templateRepository == null) throw new ArgumentNullException("templateRepository"); + if (tagRepository == null) throw new ArgumentNullException("tagRepository"); + _contentTypeRepository = contentTypeRepository; + _templateRepository = templateRepository; + _tagRepository = tagRepository; + _contentPreviewRepository = new ContentPreviewRepository(work, CacheHelper.NoCache, logger, syntaxProvider); + _contentXmlRepository = new ContentXmlRepository(work, CacheHelper.NoCache, logger, syntaxProvider); + _permissionRepository = new PermissionRepository(UnitOfWork, cacheHelper, Logger, SqlSyntax); + _contentByGuidReadRepository = new ContentByGuidReadRepository(this, work, cacheHelper, logger, syntaxProvider); + EnsureUniqueNaming = true; + } + + public bool EnsureUniqueNaming { get; set; } + + #region Overrides of RepositoryBase + + protected override IContent PerformGet(int id) + { + var sql = GetBaseQuery(BaseQueryType.FullSingle) + .Where(GetBaseWhereClause(), new { Id = id }) + .Where(x => x.Newest, SqlSyntax) + .OrderByDescending(x => x.VersionDate, SqlSyntax); + + var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); + + if (dto == null) + return null; + + var content = CreateContentFromDto(dto, sql); + + return content; + } + + protected override IEnumerable PerformGetAll(params int[] ids) + { + Func translate = s => + { + if (ids.Any()) + { + s.Where("umbracoNode.id in (@ids)", new { ids }); + } + //we only want the newest ones with this method + s.Where(x => x.Newest, SqlSyntax); + return s; + }; + + var sqlBaseFull = GetBaseQuery(BaseQueryType.FullMultiple); + var sqlBaseIds = GetBaseQuery(BaseQueryType.Ids); + + return ProcessQuery(translate(sqlBaseFull), new PagingSqlQuery(translate(sqlBaseIds))); + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + var sqlBaseFull = GetBaseQuery(BaseQueryType.FullMultiple); + var sqlBaseIds = GetBaseQuery(BaseQueryType.Ids); + + Func, Sql> translate = (translator) => + { + return translator.Translate() + .Where(x => x.Newest, SqlSyntax) + .OrderByDescending(x => x.VersionDate, SqlSyntax) + .OrderBy(x => x.SortOrder, SqlSyntax); + }; + + var translatorFull = new SqlTranslator(sqlBaseFull, query); + var translatorIds = new SqlTranslator(sqlBaseIds, query); + + return ProcessQuery(translate(translatorFull), new PagingSqlQuery(translate(translatorIds))); + } + + #endregion + + #region Overrides of PetaPocoRepositoryBase + + /// + /// Returns the base query to return Content + /// + /// + /// + /// + /// Content queries will differ depending on what needs to be returned: + /// * FullSingle: When querying for a single document, this will include the Outer join to fetch the content item's published version info + /// * FullMultiple: When querying for multiple documents, this will exclude the Outer join to fetch the content item's published version info - this info would need to be fetched separately + /// * Ids: This would essentially be the same as FullMultiple however the columns specified will only return the Ids for the documents + /// * Count: A query to return the count for documents + /// + protected override Sql GetBaseQuery(BaseQueryType queryType) + { + var sql = new Sql(); + sql.Select(queryType == BaseQueryType.Count ? "COUNT(*)" : (queryType == BaseQueryType.Ids ? "cmsDocument.nodeId" : "*")) + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.VersionId, right => right.VersionId) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.NodeId, right => right.ContentTypeId); + //TODO: IF we want to enable querying on content type information this will need to be joined + //.InnerJoin(SqlSyntax) + //.On(SqlSyntax, left => left.ContentTypeId, right => right.NodeId, SqlSyntax); + + if (queryType == BaseQueryType.FullSingle) + { + //The only reason we apply this left outer join is to be able to pull back the DocumentPublishedReadOnlyDto + //information with the entire data set, so basically this will get both the latest document and also it's published + //version if it has one. When performing a count or when retrieving Ids like in paging, this is unecessary + //and causes huge performance overhead for the SQL server, especially when sorting the result. + //We also don't include this outer join when querying for multiple entities since it is much faster to fetch this information + //in a separate query. For a single entity this is ok. + + var sqlx = string.Format("LEFT OUTER JOIN {0} {1} ON ({1}.{2}={0}.{2} AND {1}.{3}=1)", + SqlSyntax.GetQuotedTableName("cmsDocument"), + SqlSyntax.GetQuotedTableName("cmsDocument2"), + SqlSyntax.GetQuotedColumnName("nodeId"), + SqlSyntax.GetQuotedColumnName("published")); + + // cannot do this because PetaPoco does not know how to alias the table + //.LeftOuterJoin() + //.On(left => left.NodeId, right => right.NodeId) + // so have to rely on writing our own SQL + sql.Append(sqlx /*, new { @published = true }*/); + } + + sql.Where(x => x.NodeObjectType == NodeObjectTypeId, SqlSyntax); + + return sql; + } + + protected override Sql GetBaseQuery(bool isCount) + { + return GetBaseQuery(isCount ? BaseQueryType.Count : BaseQueryType.FullSingle); + } + + protected override string GetBaseWhereClause() + { + return "umbracoNode.id = @Id"; + } + + protected override IEnumerable GetDeleteClauses() + { + var list = new List + { + "DELETE FROM umbracoRedirectUrl WHERE contentKey IN (SELECT uniqueID FROM umbracoNode WHERE id = @Id)", + "DELETE FROM cmsTask WHERE nodeId = @Id", + "DELETE FROM umbracoUser2NodeNotify WHERE nodeId = @Id", + "DELETE FROM umbracoUserGroup2NodePermission WHERE nodeId = @Id", + "DELETE FROM umbracoUserStartNode WHERE startNode = @Id", + "UPDATE umbracoUserGroup SET startContentId = NULL WHERE startContentId = @Id", + "DELETE FROM umbracoRelation WHERE parentId = @Id", + "DELETE FROM umbracoRelation WHERE childId = @Id", + "DELETE FROM cmsTagRelationship WHERE nodeId = @Id", + "DELETE FROM umbracoDomains WHERE domainRootStructureID = @Id", + "DELETE FROM cmsDocument WHERE nodeId = @Id", + "DELETE FROM cmsPropertyData WHERE contentNodeId = @Id", + "DELETE FROM cmsPreviewXml WHERE nodeId = @Id", + "DELETE FROM cmsContentVersion WHERE ContentId = @Id", + "DELETE FROM cmsContentXml WHERE nodeId = @Id", + "DELETE FROM cmsContent WHERE nodeId = @Id", + "DELETE FROM umbracoAccess WHERE nodeId = @Id", + "DELETE FROM umbracoNode WHERE id = @Id" + }; + return list; + } + + protected override Guid NodeObjectTypeId + { + get { return Constants.ObjectTypes.DocumentGuid; } + } + + #endregion + + #region Overrides of VersionableRepositoryBase + + public void RebuildXmlStructures(Func serializer, int groupSize = 200, IEnumerable contentTypeIds = null) + { + // the previous way of doing this was to run it all in one big transaction, + // and to bulk-insert groups of xml rows - which works, until the transaction + // times out - and besides, because v7 transactions are ReadCommited, it does + // not bring much safety - so this reverts to updating each record individually, + // and it may be slower in the end, but should be more resilient. + + var contentTypeIdsA = contentTypeIds == null ? new int[0] : contentTypeIds.ToArray(); + + Func translate = (bId, sql) => + { + if (contentTypeIdsA.Length > 0) + { + sql.WhereIn(x => x.ContentTypeId, contentTypeIdsA, SqlSyntax); + } + + sql + .Where(x => x.NodeId > bId && x.Trashed == false, SqlSyntax) + .Where(x => x.Published, SqlSyntax) + .OrderBy(x => x.NodeId, SqlSyntax); + + return sql; + }; + + var baseId = 0; + + while (true) + { + // get the next group of nodes + var sqlFull = translate(baseId, GetBaseQuery(BaseQueryType.FullMultiple)); + var sqlIds = translate(baseId, GetBaseQuery(BaseQueryType.Ids)); + + var xmlItems = ProcessQuery(SqlSyntax.SelectTop(sqlFull, groupSize), new PagingSqlQuery(SqlSyntax.SelectTop(sqlIds, groupSize))) + .Select(x => new ContentXmlDto { NodeId = x.Id, Xml = serializer(x).ToString() }) + .ToList(); + + // no more nodes, break + if (xmlItems.Count == 0) break; + + foreach (var xmlItem in xmlItems) + { + try + { + // should happen in most cases, then it tries to insert, and it should work + // unless the node has been deleted, and we just report the exception + Database.InsertOrUpdate(xmlItem); + } + catch (Exception e) + { + Logger.Error("Could not rebuild XML for nodeId=" + xmlItem.NodeId, e); + } + } + baseId = xmlItems[xmlItems.Count - 1].NodeId; + } + + //now delete the items that shouldn't be there + var sqlAllIds = translate(0, GetBaseQuery(BaseQueryType.Ids)); + var allContentIds = Database.Fetch(sqlAllIds); + var docObjectType = NodeObjectTypeId; + var xmlIdsQuery = new Sql() + .Select("DISTINCT cmsContentXml.nodeId") + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId); + + if (contentTypeIdsA.Length > 0) + { + xmlIdsQuery.InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.ContentTypeId) + .WhereIn(x => x.ContentTypeId, contentTypeIdsA, SqlSyntax); + } + + xmlIdsQuery.Where(dto => dto.NodeObjectType == docObjectType, SqlSyntax); + + var allXmlIds = Database.Fetch(xmlIdsQuery); + + var toRemove = allXmlIds.Except(allContentIds).ToArray(); + if (toRemove.Length > 0) + { + foreach (var idGroup in toRemove.InGroupsOf(2000)) + { + Database.Execute("DELETE FROM cmsContentXml WHERE nodeId IN (@ids)", new { ids = idGroup }); + } + } + + } + + public override IEnumerable GetAllVersions(int id) + { + Func translate = s => + { + return s.Where(GetBaseWhereClause(), new {Id = id}) + .OrderByDescending(x => x.VersionDate, SqlSyntax); + }; + + var sqlFull = translate(GetBaseQuery(BaseQueryType.FullMultiple)); + var sqlIds = translate(GetBaseQuery(BaseQueryType.Ids)); + + return ProcessQuery(sqlFull, new PagingSqlQuery(sqlIds), true, includeAllVersions:true); + } + + public override IContent GetByVersion(Guid versionId) + { + var sql = GetBaseQuery(BaseQueryType.FullSingle); + //TODO: cmsContentVersion.VersionId has a Unique Index constraint applied, seems silly then to also add OrderByDescending since it would be impossible to return more than one. + sql.Where("cmsContentVersion.VersionId = @VersionId", new { VersionId = versionId }); + sql.OrderByDescending(x => x.VersionDate, SqlSyntax); + + var dto = Database.Fetch(sql).FirstOrDefault(); + + if (dto == null) + return null; + + var content = CreateContentFromDto(dto, sql); + + return content; + } + + public override void DeleteVersion(Guid versionId) + { + var sql = new Sql() + .Select("*") + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.VersionId, right => right.VersionId) + .Where(x => x.VersionId == versionId, SqlSyntax) + .Where(x => x.Newest != true, SqlSyntax); + var dto = Database.Fetch(sql).FirstOrDefault(); + + if (dto == null) return; + + using (var transaction = Database.GetTransaction()) + { + PerformDeleteVersion(dto.NodeId, versionId); + + transaction.Complete(); + } + } + + public override void DeleteVersions(int id, DateTime versionDate) + { + var sql = new Sql() + .Select("*") + .From() + .InnerJoin() + .On(left => left.VersionId, right => right.VersionId) + .Where(x => x.NodeId == id) + .Where(x => x.VersionDate < versionDate) + .Where(x => x.Newest != true); + var list = Database.Fetch(sql); + if (list.Any() == false) return; + + using (var transaction = Database.GetTransaction()) + { + foreach (var dto in list) + { + PerformDeleteVersion(id, dto.VersionId); + } + + transaction.Complete(); + } + } + + protected override void PerformDeleteVersion(int id, Guid versionId) + { + Database.Delete("WHERE nodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); + Database.Delete("WHERE contentNodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); + Database.Delete("WHERE ContentId = @Id AND VersionId = @VersionId", new { Id = id, VersionId = versionId }); + Database.Delete("WHERE nodeId = @Id AND versionId = @VersionId", new { Id = id, VersionId = versionId }); + } + + #endregion + + #region Unit of Work Implementation + + protected override void PersistDeletedItem(IContent entity) + { + //We need to clear out all access rules but we need to do this in a manual way since + // nothing in that table is joined to a content id + var subQuery = new Sql() + .Select("umbracoAccessRule.accessId") + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.AccessId, right => right.Id) + .Where(dto => dto.NodeId == entity.Id); + Database.Execute(SqlSyntax.GetDeleteSubquery("umbracoAccessRule", "accessId", subQuery)); + + //now let the normal delete clauses take care of everything else + base.PersistDeletedItem(entity); + } + + protected override void PersistNewItem(IContent entity) + { + ((Content)entity).AddingEntity(); + + //ensure the default template is assigned + if (entity.Template == null) + { + entity.Template = entity.ContentType.DefaultTemplate; + } + + //Ensure unique name on the same level + entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name); + + //Ensure that strings don't contain characters that are invalid in XML + entity.SanitizeEntityPropertiesForXmlStorage(); + + var factory = new ContentFactory(NodeObjectTypeId, entity.Id); + var dto = factory.BuildDto(entity); + + //NOTE Should the logic below have some kind of fallback for empty parent ids ? + //Logic for setting Path, Level and SortOrder + var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); + var level = parent.Level + 1; + var maxSortOrder = Database.ExecuteScalar( + "SELECT coalesce(max(sortOrder),-1) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", + new { /*ParentId =*/ entity.ParentId, NodeObjectType = NodeObjectTypeId }); + var sortOrder = maxSortOrder + 1; + + //Create the (base) node data - umbracoNode + var nodeDto = dto.ContentVersionDto.ContentDto.NodeDto; + nodeDto.Path = parent.Path; + nodeDto.Level = short.Parse(level.ToString(CultureInfo.InvariantCulture)); + nodeDto.SortOrder = sortOrder; + + // note: + // there used to be a check on Database.IsNew(nodeDto) here to either Insert or Update, + // but I cannot figure out what was the point, as the node should obviously be new if + // we reach that point - removed. + + // see if there's a reserved identifier for this unique id + var sql = new Sql("SELECT id FROM umbracoNode WHERE uniqueID=@0 AND nodeObjectType=@1", nodeDto.UniqueId, Constants.ObjectTypes.IdReservationGuid); + var id = Database.ExecuteScalar(sql); + if (id > 0) + { + nodeDto.NodeId = id; + Database.Update(nodeDto); + } + else + { + Database.Insert(nodeDto); + } + + //Update with new correct path + nodeDto.Path = string.Concat(parent.Path, ",", nodeDto.NodeId); + nodeDto.ValidatePathWithException(); + Database.Update(nodeDto); + + //Update entity with correct values + entity.Id = nodeDto.NodeId; //Set Id on entity to ensure an Id is set + entity.Path = nodeDto.Path; + entity.SortOrder = sortOrder; + entity.Level = level; + + //Create the Content specific data - cmsContent + var contentDto = dto.ContentVersionDto.ContentDto; + contentDto.NodeId = nodeDto.NodeId; + Database.Insert(contentDto); + + //Create the first version - cmsContentVersion + //Assumes a new Version guid and Version date (modified date) has been set + var contentVersionDto = dto.ContentVersionDto; + contentVersionDto.NodeId = nodeDto.NodeId; + Database.Insert(contentVersionDto); + + //Create the Document specific data for this version - cmsDocument + //Assumes a new Version guid has been generated + dto.NodeId = nodeDto.NodeId; + Database.Insert(dto); + + //Create the PropertyData for this version - cmsPropertyData + var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); + var propertyDataDtos = propertyFactory.BuildDto(entity.Properties); + var keyDictionary = new Dictionary(); + + //Add Properties + foreach (var propertyDataDto in propertyDataDtos) + { + var primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); + keyDictionary.Add(propertyDataDto.PropertyTypeId, primaryKey); + } + + //Update Properties with its newly set Id + foreach (var property in entity.Properties) + { + property.Id = keyDictionary[property.PropertyTypeId]; + } + + //lastly, check if we are a creating a published version , then update the tags table + if (entity.Published) + { + UpdatePropertyTags(entity, _tagRepository); + } + + // published => update published version infos, else leave it blank + if (entity.Published) + { + dto.DocumentPublishedReadOnlyDto = new DocumentPublishedReadOnlyDto + { + VersionId = dto.VersionId, + VersionDate = dto.UpdateDate, + Newest = true, + NodeId = dto.NodeId, + Published = true + }; + ((Content) entity).PublishedVersionGuid = dto.VersionId; + ((Content) entity).PublishedDate = dto.UpdateDate; + } + + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IContent entity) + { + var publishedState = ((Content)entity).PublishedState; + + //check if we need to make any database changes at all + if (entity.RequiresSaving(publishedState) == false) + { + entity.ResetDirtyProperties(); + return; + } + + //check if we need to create a new version + bool shouldCreateNewVersion = entity.ShouldCreateNewVersion(publishedState); + if (shouldCreateNewVersion) + { + //Updates Modified date and Version Guid + ((Content)entity).UpdatingEntity(); + } + else + { + if (entity.IsPropertyDirty("UpdateDate") == false || entity.UpdateDate == default(DateTime)) + entity.UpdateDate = DateTime.Now; + } + + //Ensure unique name on the same level + entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id); + + //Ensure that strings don't contain characters that are invalid in XML + entity.SanitizeEntityPropertiesForXmlStorage(); + + //Look up parent to get and set the correct Path and update SortOrder if ParentId has changed + if (entity.IsPropertyDirty("ParentId")) + { + var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; + var maxSortOrder = + Database.ExecuteScalar( + "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", + new { ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId }); + entity.SortOrder = maxSortOrder + 1; + + //Question: If we move a node, should we update permissions to inherit from the new parent if the parent has permissions assigned? + // if we do that, then we'd need to propogate permissions all the way downward which might not be ideal for many people. + // Gonna just leave it as is for now, and not re-propogate permissions. + } + + var factory = new ContentFactory(NodeObjectTypeId, entity.Id); + //Look up Content entry to get Primary for updating the DTO + var contentDto = Database.SingleOrDefault("WHERE nodeId = @Id", new { Id = entity.Id }); + factory.SetPrimaryKey(contentDto.PrimaryKey); + var dto = factory.BuildDto(entity); + + //Updates the (base) node data - umbracoNode + var nodeDto = dto.ContentVersionDto.ContentDto.NodeDto; + nodeDto.ValidatePathWithException(); + var o = Database.Update(nodeDto); + + //Only update this DTO if the contentType has actually changed + if (contentDto.ContentTypeId != entity.ContentTypeId) + { + //Create the Content specific data - cmsContent + var newContentDto = dto.ContentVersionDto.ContentDto; + Database.Update(newContentDto); + } + + //a flag that we'll use later to create the tags in the tag db table + var publishedStateChanged = false; + + //If Published state has changed then previous versions should have their publish state reset. + //If state has been changed to unpublished the previous versions publish state should also be reset. + //if (((ICanBeDirty)entity).IsPropertyDirty("Published") && (entity.Published || publishedState == PublishedState.Unpublished)) + if (entity.ShouldClearPublishedFlagForPreviousVersions(publishedState, shouldCreateNewVersion)) + { + //TODO: This perf can be improved, it could easily be UPDATE WHERE.... (one SQL call instead of many) + var publishedDocs = Database.Fetch("WHERE nodeId = @Id AND published = @IsPublished", new { Id = entity.Id, IsPublished = true }); + foreach (var doc in publishedDocs) + { + var docDto = doc; + docDto.Published = false; + Database.Update(docDto); + } + + //this is a newly published version so we'll update the tags table too (end of this method) + publishedStateChanged = true; + } + + //Look up (newest) entries by id in cmsDocument table to set newest = false + //TODO: This perf can be improved, it could easily be UPDATE WHERE.... (one SQL call instead of many) + var documentDtos = Database.Fetch("WHERE nodeId = @Id AND newest = @IsNewest", new { Id = entity.Id, IsNewest = true }); + foreach (var documentDto in documentDtos) + { + var docDto = documentDto; + docDto.Newest = false; + Database.Update(docDto); + } + + var contentVersionDto = dto.ContentVersionDto; + if (shouldCreateNewVersion) + { + //Create a new version - cmsContentVersion + //Assumes a new Version guid and Version date (modified date) has been set + Database.Insert(contentVersionDto); + //Create the Document specific data for this version - cmsDocument + //Assumes a new Version guid has been generated + Database.Insert(dto); + } + else + { + //In order to update the ContentVersion we need to retrieve its primary key id + var contentVerDto = Database.SingleOrDefault("WHERE VersionId = @Version", new { Version = entity.Version }); + if (contentVerDto != null) + { + contentVersionDto.Id = contentVerDto.Id; + Database.Update(contentVersionDto); + } + + Database.Update(dto); + } + + //Create the PropertyData for this version - cmsPropertyData + var propertyFactory = new PropertyFactory(entity.ContentType.CompositionPropertyTypes.ToArray(), entity.Version, entity.Id); + var propertyDataDtos = propertyFactory.BuildDto(entity.Properties); + var keyDictionary = new Dictionary(); + + //Add Properties + foreach (var propertyDataDto in propertyDataDtos) + { + if (shouldCreateNewVersion == false && propertyDataDto.Id > 0) + { + Database.Update(propertyDataDto); + } + else + { + int primaryKey = Convert.ToInt32(Database.Insert(propertyDataDto)); + keyDictionary.Add(propertyDataDto.PropertyTypeId, primaryKey); + } + } + + //Update Properties with its newly set Id + if (keyDictionary.Any()) + { + foreach (var property in entity.Properties) + { + if (keyDictionary.ContainsKey(property.PropertyTypeId) == false) continue; + + property.Id = keyDictionary[property.PropertyTypeId]; + } + } + + //lastly, check if we are a newly published version and then update the tags table + if (publishedStateChanged && entity.Published) + { + UpdatePropertyTags(entity, _tagRepository); + } + else if (publishedStateChanged && (entity.Trashed || entity.Published == false)) + { + //it's in the trash or not published remove all entity tags + ClearEntityTags(entity, _tagRepository); + } + + // published => update published version infos, + // else if unpublished then clear published version infos + if (entity.Published) + { + dto.DocumentPublishedReadOnlyDto = new DocumentPublishedReadOnlyDto + { + VersionId = dto.VersionId, + VersionDate = dto.UpdateDate, + Newest = true, + NodeId = dto.NodeId, + Published = true + }; + ((Content) entity).PublishedVersionGuid = dto.VersionId; + ((Content) entity).PublishedDate = dto.UpdateDate; + } + else if (publishedStateChanged) + { + dto.DocumentPublishedReadOnlyDto = new DocumentPublishedReadOnlyDto + { + VersionId = default (Guid), + VersionDate = default (DateTime), + Newest = false, + NodeId = dto.NodeId, + Published = false + }; + ((Content) entity).PublishedVersionGuid = default(Guid); + ((Content) entity).PublishedDate = default (DateTime); + } + + entity.ResetDirtyProperties(); + } + + + #endregion + + #region Implementation of IContentRepository + + public IEnumerable GetByPublishedVersion(IQuery query) + { + Func, Sql> translate = t => + { + return t.Translate() + .Where(x => x.Published, SqlSyntax) + .OrderBy(x => x.Level, SqlSyntax) + .OrderBy(x => x.SortOrder, SqlSyntax); + }; + + // we WANT to return contents in top-down order, ie parents should come before children + // ideal would be pure xml "document order" which can be achieved with: + // ORDER BY substring(path, 1, len(path) - charindex(',', reverse(path))), sortOrder + // but that's probably an overkill - sorting by level,sortOrder should be enough + + var sqlFull = GetBaseQuery(BaseQueryType.FullMultiple); + var translatorFull = new SqlTranslator(sqlFull, query); + var sqlIds = GetBaseQuery(BaseQueryType.Ids); + var translatorIds = new SqlTranslator(sqlIds, query); + + return ProcessQuery(translate(translatorFull), new PagingSqlQuery(translate(translatorIds)), true); + } + + public IEnumerable GetBlueprints(IQuery query) + { + Func, Sql> translate = t => t.Translate(); + + var sqlFull = GetBaseQuery(BaseQueryType.FullMultiple); + var translatorFull = new SqlTranslator(sqlFull, query); + var sqlIds = GetBaseQuery(BaseQueryType.Ids); + var translatorIds = new SqlTranslator(sqlIds, query); + + return ProcessQuery(translate(translatorFull), new PagingSqlQuery(translate(translatorIds)), true); + } + + /// + /// This builds the Xml document used for the XML cache + /// + /// + public XmlDocument BuildXmlCache() + { + //TODO: This is what we should do , but converting to use XDocument would be breaking unless we convert + // to XmlDocument at the end of this, but again, this would be bad for memory... though still not nearly as + // bad as what is happening before! + // We'll keep using XmlDocument for now though, but XDocument xml generation is much faster: + // https://blogs.msdn.microsoft.com/codejunkie/2008/10/08/xmldocument-vs-xelement-performance/ + // I think we already have code in here to convert XDocument to XmlDocument but in case we don't here + // it is: https://blogs.msdn.microsoft.com/marcelolr/2009/03/13/fast-way-to-convert-xmldocument-into-xdocument/ + + //// Prepare an XmlDocument with an appropriate inline DTD to match + //// the expected content + //var parent = new XElement("root", new XAttribute("id", "-1")); + //var xmlDoc = new XDocument( + // new XDocumentType("root", null, null, DocumentType.GenerateDtd()), + // parent); + + var xmlDoc = new XmlDocument(); + var doctype = xmlDoc.CreateDocumentType("root", null, null, + ApplicationContext.Current.Services.ContentTypeService.GetContentTypesDtd()); + xmlDoc.AppendChild(doctype); + var parent = xmlDoc.CreateElement("root"); + var pIdAtt = xmlDoc.CreateAttribute("id"); + pIdAtt.Value = "-1"; + parent.Attributes.Append(pIdAtt); + xmlDoc.AppendChild(parent); + + //Ensure that only nodes that have published versions are selected + var sql = string.Format(@"select umbracoNode.id, umbracoNode.parentID, umbracoNode.sortOrder, cmsContentXml.{0}, umbracoNode.{1} from umbracoNode +inner join cmsContentXml on cmsContentXml.nodeId = umbracoNode.id and umbracoNode.nodeObjectType = @type +where umbracoNode.id in (select cmsDocument.nodeId from cmsDocument where cmsDocument.published = 1) +order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", + SqlSyntax.GetQuotedColumnName("xml"), + SqlSyntax.GetQuotedColumnName("level"), + SqlSyntax.GetQuotedColumnName("level")); + + XmlElement last = null; + + //NOTE: Query creates a reader - does not load all into memory + foreach (var row in Database.Query(sql, new { type = NodeObjectTypeId })) + { + string parentId = ((int)row.parentID).ToInvariantString(); + string xml = row.xml; + int sortOrder = row.sortOrder; + + //if the parentid is changing + if (last != null && last.GetAttribute("parentID") != parentId) + { + parent = xmlDoc.GetElementById(parentId); + if (parent == null) + { + //Need to short circuit here, if the parent is not there it means that the parent is unpublished + // and therefore the child is not published either so cannot be included in the xml cache + continue; + } + } + + var xmlDocFragment = xmlDoc.CreateDocumentFragment(); + xmlDocFragment.InnerXml = xml; + + last = (XmlElement)parent.AppendChild(xmlDocFragment); + + // fix sortOrder - see notes in UpdateSortOrder + last.Attributes["sortOrder"].Value = sortOrder.ToInvariantString(); + } + + return xmlDoc; + + } + + public XmlDocument BuildPreviewXmlCache() + { + var xmlDoc = new XmlDocument(); + var doctype = xmlDoc.CreateDocumentType("root", null, null, + ApplicationContext.Current.Services.ContentTypeService.GetContentTypesDtd()); + xmlDoc.AppendChild(doctype); + var parent = xmlDoc.CreateElement("root"); + var pIdAtt = xmlDoc.CreateAttribute("id"); + pIdAtt.Value = "-1"; + parent.Attributes.Append(pIdAtt); + xmlDoc.AppendChild(parent); + + //Ensure that only nodes that have published versions are selected + var sql = string.Format(@"select umbracoNode.id, umbracoNode.parentID, umbracoNode.sortOrder, cmsPreviewXml.{0}, umbracoNode.{1} from umbracoNode +inner join cmsPreviewXml on cmsPreviewXml.nodeId = umbracoNode.id and umbracoNode.nodeObjectType = @type +inner join cmsDocument on cmsPreviewXml.versionId = cmsDocument.versionId and cmsDocument.newest=1 +where umbracoNode.trashed = 0 +order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", + SqlSyntax.GetQuotedColumnName("xml"), + SqlSyntax.GetQuotedColumnName("level"), + SqlSyntax.GetQuotedColumnName("level")); + + XmlElement last = null; + + //NOTE: Query creates a reader - does not load all into memory + foreach (var row in Database.Query(sql, new { type = NodeObjectTypeId })) + { + string parentId = ((int)row.parentID).ToInvariantString(); + string xml = row.xml; + int sortOrder = row.sortOrder; + + //if the parentid is changing + if (last != null && last.GetAttribute("parentID") != parentId) + { + parent = xmlDoc.GetElementById(parentId); + if (parent == null) + { + //Need to short circuit here, if the parent is not there it means that the parent is unpublished + // and therefore the child is not published either so cannot be included in the xml cache + continue; + } + } + + var xmlDocFragment = xmlDoc.CreateDocumentFragment(); + xmlDocFragment.InnerXml = xml; + + last = (XmlElement)parent.AppendChild(xmlDocFragment); + + // fix sortOrder - see notes in UpdateSortOrder + last.Attributes["sortOrder"].Value = sortOrder.ToInvariantString(); + } + + return xmlDoc; + + } + + public int CountPublished(string contentTypeAlias = null) + { + if (contentTypeAlias.IsNullOrWhiteSpace()) + { + var sql = GetBaseQuery(true).Where(x => x.Trashed == false) + .Where(x => x.Published == true); + return Database.ExecuteScalar(sql); + } + else + { + var sql = GetBaseQuery(true).Where(x => x.Trashed == false) + .Where(x => x.Published == true) + .Where(x => x.Alias == contentTypeAlias); + return Database.ExecuteScalar(sql); + } + } + + public void ReplaceContentPermissions(EntityPermissionSet permissionSet) + { + _permissionRepository.ReplaceEntityPermissions(permissionSet); + } + + public void ClearPublished(IContent content) + { + var sql = "UPDATE cmsDocument SET published=0 WHERE nodeId=@id AND published=1"; + Database.Execute(sql, new {id = content.Id}); + } + + /// + /// Assigns a single permission to the current content item for the specified user group ids + /// + /// + /// + /// + public void AssignEntityPermission(IContent entity, char permission, IEnumerable groupIds) + { + _permissionRepository.AssignEntityPermission(entity, permission, groupIds); + } + + /// + /// Gets the explicit list of permissions for the content item + /// + /// + /// + public EntityPermissionCollection GetPermissionsForEntity(int entityId) + { + return _permissionRepository.GetPermissionsForEntity(entityId); + } + + /// + /// Adds/updates content/published xml + /// + /// + /// + public void AddOrUpdateContentXml(IContent content, Func xml) + { + _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); + } + + /// + /// Used to add/update a permission for a content item + /// + /// + public void AddOrUpdatePermissions(ContentPermissionSet permission) + { + _permissionRepository.AddOrUpdate(permission); + } + + /// + /// Used to remove the content xml for a content item + /// + /// + public void DeleteContentXml(IContent content) + { + _contentXmlRepository.Delete(new ContentXmlEntity(content)); + } + + /// + /// Adds/updates preview xml + /// + /// + /// + public void AddOrUpdatePreviewXml(IContent content, Func xml) + { + _contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity(content, xml)); + } + + /// + /// Gets paged content results + /// + /// Query to excute + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// Flag to indicate when ordering by system field + /// Search text filter + /// An Enumerable list of objects + public IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter = null) + { + + //NOTE: This uses the GetBaseQuery method but that does not take into account the required 'newest' field which is + // what we always require for a paged result, so we'll ensure it's included in the filter + + var filterSql = new Sql().Append("AND (cmsDocument.newest = 1)"); + if (filter != null) + { + foreach (var filterClause in filter.GetWhereClauses()) + { + filterSql.Append(string.Format("AND ({0})", filterClause.Item1), filterClause.Item2); + } + } + + Func> filterCallback = () => new Tuple(filterSql.SQL, filterSql.Arguments); + + return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, + new Tuple("cmsDocument", "nodeId"), + (sqlFull, pagingSqlQuery) => ProcessQuery(sqlFull, pagingSqlQuery), orderBy, orderDirection, orderBySystemField, + filterCallback); + + } + + #endregion + + #region IRecycleBinRepository members + + protected override int RecycleBinId + { + get { return Constants.System.RecycleBinContent; } + } + + #endregion + + #region Read Repository implementation for GUID keys + public IContent Get(Guid id) + { + return _contentByGuidReadRepository.Get(id); + } + + IEnumerable IReadRepository.GetAll(params Guid[] ids) + { + return _contentByGuidReadRepository.GetAll(ids); + } + + public bool Exists(Guid id) + { + return _contentByGuidReadRepository.Exists(id); + } + + /// + /// A reading repository purely for looking up by GUID + /// + /// + /// TODO: This is ugly and to fix we need to decouple the IRepositoryQueryable -> IRepository -> IReadRepository which should all be separate things! + /// Then we can do the same thing with repository instances and we wouldn't need to leave all these methods as not implemented because we wouldn't need to implement them + /// + private class ContentByGuidReadRepository : PetaPocoRepositoryBase + { + private readonly ContentRepository _outerRepo; + + public ContentByGuidReadRepository(ContentRepository outerRepo, + IScopeUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + : base(work, cache, logger, sqlSyntax) + { + _outerRepo = outerRepo; + } + + protected override IContent PerformGet(Guid id) + { + var sql = _outerRepo.GetBaseQuery(BaseQueryType.FullSingle) + .Where(GetBaseWhereClause(), new { Id = id }) + .Where(x => x.Newest, SqlSyntax) + .OrderByDescending(x => x.VersionDate, SqlSyntax); + + var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); + + if (dto == null) + return null; + + var content = _outerRepo.CreateContentFromDto(dto, sql); + + return content; + } + + protected override IEnumerable PerformGetAll(params Guid[] ids) + { + Func translate = s => + { + if (ids.Any()) + { + s.Where("umbracoNode.uniqueID in (@ids)", new { ids }); + } + //we only want the newest ones with this method + s.Where(x => x.Newest, SqlSyntax); + return s; + }; + + var sqlBaseFull = _outerRepo.GetBaseQuery(BaseQueryType.FullMultiple); + var sqlBaseIds = _outerRepo.GetBaseQuery(BaseQueryType.Ids); + + return _outerRepo.ProcessQuery(translate(sqlBaseFull), new PagingSqlQuery(translate(sqlBaseIds))); + } + + protected override Sql GetBaseQuery(bool isCount) + { + return _outerRepo.GetBaseQuery(isCount); + } + + protected override string GetBaseWhereClause() + { + return "umbracoNode.uniqueID = @Id"; + } + + protected override Guid NodeObjectTypeId + { + get { return _outerRepo.NodeObjectTypeId; } + } + + #region Not needed to implement + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + throw new NotImplementedException(); + } + protected override IEnumerable GetDeleteClauses() + { + throw new NotImplementedException(); + } + protected override void PersistNewItem(IContent entity) + { + throw new NotImplementedException(); + } + protected override void PersistUpdatedItem(IContent entity) + { + throw new NotImplementedException(); + } + #endregion + } + #endregion + + protected override string GetDatabaseFieldNameForOrderBy(string orderBy) + { + //Some custom ones + switch (orderBy.ToUpperInvariant()) + { + case "NAME": + return "cmsDocument.text"; + case "UPDATER": + //TODO: This isn't going to work very nicely because it's going to order by ID, not by letter + return "cmsDocument.documentUser"; + } + + return base.GetDatabaseFieldNameForOrderBy(orderBy); + } + + /// + /// This is the underlying method that processes most queries for this repository + /// + /// + /// The FullMultiple SQL without the outer join to return all data required to create an IContent excluding it's published state data which this will query separately + /// + /// + /// The Id SQL without the outer join to just return all document ids - used to process the properties for the content item + /// + /// + /// + /// Generally when querying for content we only want to return the most recent version of the content item, however in some cases like when + /// we want to return all versions of a content item, we can't simply return the latest + /// + /// + private IEnumerable ProcessQuery(Sql sqlFull, PagingSqlQuery pagingSqlQuery, bool withCache = false, bool includeAllVersions = false) + { + // fetch returns a list so it's ok to iterate it in this method + var dtos = Database.Fetch(sqlFull); + if (dtos.Count == 0) return Enumerable.Empty(); + + //Go and get all of the published version data separately for this data, this is because when we are querying + //for multiple content items we don't include the outer join to fetch this data in the same query because + //it is insanely slow. Instead we just fetch the published version data separately in one query. + + //we need to parse the original SQL statement and reduce the columns to just cmsDocument.nodeId so that we can use + // the statement to go get the published data for all of the items by using an inner join + var parsedOriginalSql = "SELECT cmsDocument.nodeId " + sqlFull.SQL.Substring(sqlFull.SQL.IndexOf("FROM", StringComparison.Ordinal)); + //now remove everything from an Orderby clause and beyond + if (parsedOriginalSql.InvariantContains("ORDER BY ")) + { + parsedOriginalSql = parsedOriginalSql.Substring(0, parsedOriginalSql.LastIndexOf("ORDER BY ", StringComparison.Ordinal)); + } + + //order by update date DESC, if there is corrupted published flags we only want the latest! + var publishedSql = new Sql(@"SELECT cmsDocument.nodeId, cmsDocument.published, cmsDocument.versionId, cmsDocument.updateDate, cmsDocument.newest +FROM cmsDocument INNER JOIN cmsContentVersion ON cmsContentVersion.VersionId = cmsDocument.versionId +WHERE cmsDocument.published = 1 AND cmsDocument.nodeId IN +(" + parsedOriginalSql + @") +ORDER BY cmsContentVersion.id DESC +", sqlFull.Arguments); + + //go and get the published version data, we do a Query here and not a Fetch so we are + //not allocating a whole list to memory just to allocate another list in memory since + //we are assigning this data to a keyed collection for fast lookup below + var publishedData = Database.Query(publishedSql); + var publishedDataCollection = new DocumentPublishedReadOnlyDtoCollection(); + foreach (var publishedDto in publishedData) + { + //double check that there's no corrupt db data, there should only be a single published item + if (publishedDataCollection.Contains(publishedDto.NodeId) == false) + publishedDataCollection.Add(publishedDto); + } + + //This is a tuple list identifying if the content item came from the cache or not + var content = new List>(); + var defs = new DocumentDefinitionCollection(includeAllVersions); + var templateIds = new List(); + + //track the looked up content types, even though the content types are cached + // they still need to be deep cloned out of the cache and we don't want to add + // the overhead of deep cloning them on every item in this loop + var contentTypes = new Dictionary(); + + foreach (var dto in dtos) + { + DocumentPublishedReadOnlyDto publishedDto; + publishedDataCollection.TryGetValue(dto.NodeId, out publishedDto); + + // if the cache contains the published version, use it + if (withCache) + { + var cached = IsolatedCache.GetCacheItem(GetCacheIdKey(dto.NodeId)); + //only use this cached version if the dto returned is also the publish version, they must match and be teh same version + if (cached != null && cached.Version == dto.VersionId && cached.Published && dto.Published) + { + content.Add(new Tuple(cached, true)); + continue; + } + } + + // else, need to fetch from the database + // content type repository is full-cache so OK to get each one independently + + IContentType contentType; + if (contentTypes.ContainsKey(dto.ContentVersionDto.ContentDto.ContentTypeId)) + { + contentType = contentTypes[dto.ContentVersionDto.ContentDto.ContentTypeId]; + } + else + { + contentType = _contentTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); + contentTypes[dto.ContentVersionDto.ContentDto.ContentTypeId] = contentType; + } + + // track the definition and if it's successfully added or updated then processed + if (defs.AddOrUpdate(new DocumentDefinition(dto, contentType))) + { + // assign template + if (dto.TemplateId.HasValue && dto.TemplateId.Value > 0) + templateIds.Add(dto.TemplateId.Value); + + content.Add(new Tuple(ContentFactory.BuildEntity(dto, contentType, publishedDto), false)); + } + } + + // load all required templates in 1 query + var templates = _templateRepository.GetAll(templateIds.ToArray()) + .ToDictionary(x => x.Id, x => x); + + // load all properties for all documents from database in 1 query + var propertyData = GetPropertyCollection(pagingSqlQuery, defs); + + // assign template and property data + foreach (var contentItem in content) + { + var cc = contentItem.Item1; + var fromCache = contentItem.Item2; + + //if this has come from cache, we do not need to build up it's structure + if (fromCache) continue; + + var def = defs[includeAllVersions ? (ValueType)cc.Version : cc.Id]; + + ITemplate template = null; + if (def.DocumentDto.TemplateId.HasValue) + templates.TryGetValue(def.DocumentDto.TemplateId.Value, out template); // else null + cc.Template = template; + if (propertyData.ContainsKey(cc.Version)) + { + cc.Properties = propertyData[cc.Version]; + } + else + { + throw new InvalidOperationException($"No property data found for version: '{cc.Version}'."); + } + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + cc.ResetDirtyProperties(false); + } + + return content.Select(x => x.Item1).ToArray(); + } + + /// + /// Private method to create a content object from a DocumentDto, which is used by Get and GetByVersion. + /// + /// + /// + /// + private IContent CreateContentFromDto(DocumentDto dto, Sql docSql) + { + var contentType = _contentTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); + + var content = ContentFactory.BuildEntity(dto, contentType); + + //Check if template id is set on DocumentDto, and get ITemplate if it is. + if (dto.TemplateId.HasValue && dto.TemplateId.Value > 0) + { + content.Template = _templateRepository.Get(dto.TemplateId.Value); + } + + var docDef = new DocumentDefinition(dto, contentType); + + var properties = GetPropertyCollection(docSql, new[] { docDef }); + + content.Properties = properties[dto.VersionId]; + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + ((Entity)content).ResetDirtyProperties(false); + return content; + } + + private string EnsureUniqueNodeName(int parentId, string nodeName, int id = 0) + { + if (EnsureUniqueNaming == false) + return nodeName; + + var names = Database.Fetch("SELECT id, text AS name FROM umbracoNode WHERE nodeObjectType=@objectType AND parentId=@parentId", + new { objectType = NodeObjectTypeId, parentId }); + + return SimilarNodeName.GetUniqueName(names, id, nodeName); + } + + /// + /// Dispose disposable properties + /// + /// + /// Ensure the unit of work is disposed + /// + protected override void DisposeResources() + { + _contentTypeRepository.Dispose(); + _templateRepository.Dispose(); + _tagRepository.Dispose(); + _contentPreviewRepository.Dispose(); + _contentXmlRepository.Dispose(); + } + + /// + /// A keyed collection for fast lookup when retrieving a separate list of published version data + /// + private class DocumentPublishedReadOnlyDtoCollection : KeyedCollection + { + protected override int GetKeyForItem(DocumentPublishedReadOnlyDto item) + { + return item.NodeId; + } + + public bool TryGetValue(int key, out DocumentPublishedReadOnlyDto val) + { + if (Dictionary == null) + { + val = null; + return false; + } + return Dictionary.TryGetValue(key, out val); + } + } + } +} From 67f680a675e6f6bd2637e6b5aed94b8dcda3567c Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 19 Jul 2019 11:24:01 +0200 Subject: [PATCH 48/67] Revert "https://github.com/umbraco/Umbraco-CMS/issues/5921 - AB#1794 - Remove trashed nodes from sql when building xml for previews" This reverts commit 47c3e3a7 --- src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index a0b211b6b2..f559b91ba5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -851,7 +851,6 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", var sql = string.Format(@"select umbracoNode.id, umbracoNode.parentID, umbracoNode.sortOrder, cmsPreviewXml.{0}, umbracoNode.{1} from umbracoNode inner join cmsPreviewXml on cmsPreviewXml.nodeId = umbracoNode.id and umbracoNode.nodeObjectType = @type inner join cmsDocument on cmsPreviewXml.versionId = cmsDocument.versionId and cmsDocument.newest=1 -where umbracoNode.trashed = 0 order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", SqlSyntax.GetQuotedColumnName("xml"), SqlSyntax.GetQuotedColumnName("level"), @@ -1185,7 +1184,7 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", //order by update date DESC, if there is corrupted published flags we only want the latest! var publishedSql = new Sql(@"SELECT cmsDocument.nodeId, cmsDocument.published, cmsDocument.versionId, cmsDocument.updateDate, cmsDocument.newest FROM cmsDocument INNER JOIN cmsContentVersion ON cmsContentVersion.VersionId = cmsDocument.versionId -WHERE cmsDocument.published = 1 AND cmsDocument.nodeId IN +WHERE cmsDocument.published = 1 AND cmsDocument.nodeId IN (" + parsedOriginalSql + @") ORDER BY cmsContentVersion.id DESC ", sqlFull.Arguments); From cd27bb210f24f1b37cbca34547a52d1aa46031cb Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 19 Jul 2019 11:25:33 +0200 Subject: [PATCH 49/67] https://github.com/umbraco/Umbraco-CMS/issues/5921 - AB#1794 - Remove trashed nodes from sql when building xml for previews --- src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index f559b91ba5..a0b211b6b2 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -851,6 +851,7 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", var sql = string.Format(@"select umbracoNode.id, umbracoNode.parentID, umbracoNode.sortOrder, cmsPreviewXml.{0}, umbracoNode.{1} from umbracoNode inner join cmsPreviewXml on cmsPreviewXml.nodeId = umbracoNode.id and umbracoNode.nodeObjectType = @type inner join cmsDocument on cmsPreviewXml.versionId = cmsDocument.versionId and cmsDocument.newest=1 +where umbracoNode.trashed = 0 order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", SqlSyntax.GetQuotedColumnName("xml"), SqlSyntax.GetQuotedColumnName("level"), @@ -1184,7 +1185,7 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", //order by update date DESC, if there is corrupted published flags we only want the latest! var publishedSql = new Sql(@"SELECT cmsDocument.nodeId, cmsDocument.published, cmsDocument.versionId, cmsDocument.updateDate, cmsDocument.newest FROM cmsDocument INNER JOIN cmsContentVersion ON cmsContentVersion.VersionId = cmsDocument.versionId -WHERE cmsDocument.published = 1 AND cmsDocument.nodeId IN +WHERE cmsDocument.published = 1 AND cmsDocument.nodeId IN (" + parsedOriginalSql + @") ORDER BY cmsContentVersion.id DESC ", sqlFull.Arguments); From 968463912ab548808ff87bc58760b2b2bc7bb45e Mon Sep 17 00:00:00 2001 From: Steve Megson Date: Sun, 21 Jul 2019 14:47:37 +0100 Subject: [PATCH 50/67] Include CheckBoxList in ValueListPreValueMigrator --- .../Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs index 7249ebd6ec..07fefc8e85 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs @@ -9,6 +9,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes private readonly string[] _editors = { "Umbraco.RadioButtonList", + "Umbraco.CheckBoxList", "Umbraco.DropDown", "Umbraco.DropdownlistPublishingKeys", "Umbraco.DropDownMultiple", From 0320a56cb56606bd468cdda87c24358c6e332834 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 22 Jul 2019 13:27:51 +1000 Subject: [PATCH 51/67] Fixes up dataTypeId var declarations --- .../common/dialogs/linkpicker.controller.js | 6 +- .../linkpicker/linkpicker.controller.js | 9 +- .../mediaPicker/mediapicker.controller.js | 14 +--- .../treepicker/treepicker.controller.js | 6 +- .../contentpicker/contentpicker.controller.js | 84 +++++++++---------- .../grid/editors/media.controller.js | 14 ++-- .../grid/editors/rte.controller.js | 13 +-- .../mediapicker/mediapicker.controller.js | 8 +- .../multiurlpicker.controller.js | 7 +- .../relatedlinks/relatedlinks.controller.js | 14 +--- .../propertyeditors/rte/rte.controller.js | 14 +--- 11 files changed, 66 insertions(+), 123 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/linkpicker.controller.js index 876a9f9426..8cf7f937a3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/linkpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/linkpicker.controller.js @@ -8,17 +8,13 @@ angular.module("umbraco").controller("Umbraco.Dialogs.LinkPickerController", searchText = value + "..."; }); - var dataTypeId = null; - if($scope.model && $scope.model.dataTypeId) { - dataTypeId = $scope.model.dataTypeId; - } $scope.dialogTreeEventHandler = $({}); $scope.target = {}; $scope.searchInfo = { searchFromId: null, searchFromName: null, showSearch: false, - dataTypeId: dataTypeId, + dataTypeId: ($scope.model && $scope.model.dataTypeId) ? $scope.model.dataTypeId : null, results: [], selectedSearchResults: [] } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js index 0c5641ba0a..f1241c1976 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js @@ -17,7 +17,6 @@ angular.module("umbraco").controller("Umbraco.Overlays.LinkPickerController", dataTypeId = dialogOptions.dataTypeId; } - $scope.dialogTreeEventHandler = $({}); $scope.model.target = {}; $scope.searchInfo = { @@ -28,7 +27,7 @@ angular.module("umbraco").controller("Umbraco.Overlays.LinkPickerController", results: [], selectedSearchResults: [] }; - $scope.customTreeParams = dialogOptions.dataTypeId ? "dataTypeId=" + dialogOptions.dataTypeId : ""; + $scope.customTreeParams = dataTypeId !== null ? "dataTypeId=" + dataTypeId : ""; $scope.showTarget = $scope.model.hideTarget !== true; if (dialogOptions.currentTarget) { @@ -121,11 +120,7 @@ angular.module("umbraco").controller("Umbraco.Overlays.LinkPickerController", startNodeId = -1; startNodeIsVirtual = true; } - var dataTypeId = null; - if(dialogOptions && dialogOptions.dataTypeId){ - dataTypeId = dialogOptions.dataTypeId; - } - + $scope.mediaPickerOverlay = { view: "mediapicker", startNodeId: startNodeId, diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js index 61f9619a52..bae4882a3e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js @@ -54,7 +54,7 @@ angular.module("umbraco") totalItems: 0, totalPages: 0, filter: '', - dataTypeId: dataTypeId + dataTypeId: dataTypeId, }; //preload selected item @@ -161,11 +161,7 @@ angular.module("umbraco") } if (folder.id > 0) { - var dataTypeId = null; - if($scope.model && $scope.model.dataTypeId) { - dataTypeId = $scope.model.dataTypeId; - } - + entityResource.getAncestors(folder.id, "media", { dataTypeId: dataTypeId }) .then(function (anc) { $scope.path = _.filter(anc, @@ -318,11 +314,7 @@ angular.module("umbraco") if ($scope.searchOptions.filter) { searchMedia(); } else { - var dataTypeId = null; - if($scope.model && $scope.model.dataTypeId) { - dataTypeId = $scope.model.dataTypeId; - } - + // reset pagination $scope.searchOptions = { pageNumber: 1, diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js index 851d270789..2d5fb132b7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js @@ -5,10 +5,6 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", var tree = null; var dialogOptions = $scope.model; - var dataTypeId = null; - if(dialogOptions && dialogOptions.dataTypeId){ - dataTypeId = dialogOptions.dataTypeId; - } $scope.treeReady = false; $scope.dialogTreeEventHandler = $({}); $scope.section = dialogOptions.section; @@ -21,7 +17,7 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", searchFromId: dialogOptions.startNodeId, searchFromName: null, showSearch: false, - dataTypeId: dataTypeId, + dataTypeId: (dialogOptions && dialogOptions.dataTypeId) ? dialogOptions.dataTypeId : null, results: [], selectedSearchResults: [] } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js index 37a372a5b1..61e841d1af 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js @@ -68,11 +68,11 @@ function contentPickerController($scope, entityResource, editorState, iconHelper showPathOnHover: false, dataTypeId: null, maxNumber: 1, - minNumber : 0, + minNumber: 0, startNode: { query: "", type: "content", - id: $scope.model.config.startNodeId ? $scope.model.config.startNodeId : -1 // get start node for simple Content Picker + id: $scope.model.config.startNodeId ? $scope.model.config.startNodeId : -1 // get start node for simple Content Picker } }; @@ -104,8 +104,8 @@ function contentPickerController($scope, entityResource, editorState, iconHelper var entityType = $scope.model.config.startNode.type === "member" ? "Member" : $scope.model.config.startNode.type === "media" - ? "Media" - : "Document"; + ? "Media" + : "Document"; $scope.allowOpenButton = entityType === "Document"; $scope.allowEditButton = entityType === "Document"; $scope.allowRemoveButton = true; @@ -144,7 +144,7 @@ function contentPickerController($scope, entityResource, editorState, iconHelper dialogOptions.filterCssClass = "not-allowed"; var currFilter = dialogOptions.filter; //now change the filter to be a method - dialogOptions.filter = function(i) { + dialogOptions.filter = function (i) { //filter out the list view nodes if (i.metaData.isContainer) { return true; @@ -179,34 +179,30 @@ function contentPickerController($scope, entityResource, editorState, iconHelper } //dialog - $scope.openContentPicker = function() { - var dataTypeId = null; - if($scope.model && $scope.model.dataTypeId) { - dataTypeId = $scope.model.dataTypeId; + $scope.openContentPicker = function () { + + $scope.contentPickerOverlay = dialogOptions; + $scope.contentPickerOverlay.view = "treepicker"; + $scope.contentPickerOverlay.show = true; + $scope.contentPickerOverlay.dataTypeId = ($scope.model && $scope.model.dataTypeId) ? $scope.model.dataTypeId : null; + + $scope.contentPickerOverlay.submit = function (model) { + + if (angular.isArray(model.selection)) { + _.each(model.selection, function (item, i) { + $scope.add(item); + }); + angularHelper.getCurrentForm($scope).$setDirty(); + } + + $scope.contentPickerOverlay.show = false; + $scope.contentPickerOverlay = null; } - $scope.contentPickerOverlay = dialogOptions; - $scope.contentPickerOverlay.view = "treepicker"; - $scope.contentPickerOverlay.show = true; - $scope.contentPickerOverlay.dataTypeId = dataTypeId; - - $scope.contentPickerOverlay.submit = function(model) { - - if (angular.isArray(model.selection)) { - _.each(model.selection, function (item, i) { - $scope.add(item); - }); - angularHelper.getCurrentForm($scope).$setDirty(); - } - - $scope.contentPickerOverlay.show = false; - $scope.contentPickerOverlay = null; - } - - $scope.contentPickerOverlay.close = function(oldModel) { - $scope.contentPickerOverlay.show = false; - $scope.contentPickerOverlay = null; - } + $scope.contentPickerOverlay.close = function (oldModel) { + $scope.contentPickerOverlay.show = false; + $scope.contentPickerOverlay = null; + } }; @@ -246,13 +242,13 @@ function contentPickerController($scope, entityResource, editorState, iconHelper $scope.renderModel = []; }; - $scope.openMiniEditor = function(node) { - miniEditorHelper.launchMiniEditor(node).then(function(updatedNode){ + $scope.openMiniEditor = function (node) { + miniEditorHelper.launchMiniEditor(node).then(function (updatedNode) { // update the node node.name = updatedNode.name; node.published = updatedNode.hasPublishedVersion; - if(entityType !== "Member") { - entityResource.getUrl(updatedNode.id, entityType).then(function(data){ + if (entityType !== "Member") { + entityResource.getUrl(updatedNode.id, entityType).then(function (data) { node.url = data; }); } @@ -261,7 +257,7 @@ function contentPickerController($scope, entityResource, editorState, iconHelper //when the scope is destroyed we need to unsubscribe $scope.$on('$destroy', function () { - if(unsubscribe) { + if (unsubscribe) { unsubscribe(); } }); @@ -270,12 +266,12 @@ function contentPickerController($scope, entityResource, editorState, iconHelper //load current data if anything selected if (modelIds.length > 0) { - entityResource.getByIds(modelIds, entityType).then(function(data) { + entityResource.getByIds(modelIds, entityType).then(function (data) { _.each(modelIds, - function(id, i) { + function (id, i) { var entity = _.find(data, - function(d) { + function (d) { return $scope.model.config.idType === "udi" ? (d.udi == id) : (d.id == id); }); @@ -299,10 +295,10 @@ function contentPickerController($scope, entityResource, editorState, iconHelper function setEntityUrl(entity) { // get url for content and media items - if(entityType !== "Member") { - entityResource.getUrl(entity.id, entityType).then(function(data){ + if (entityType !== "Member") { + entityResource.getUrl(entity.id, entityType).then(function (data) { // update url - angular.forEach($scope.renderModel, function(item){ + angular.forEach($scope.renderModel, function (item) { if (item.id === entity.id) { if (entity.trashed) { item.url = localizationService.dictionary.general_recycleBin; @@ -324,7 +320,7 @@ function contentPickerController($scope, entityResource, editorState, iconHelper function addSelectedItem(item) { // set icon - if(item.icon) { + if (item.icon) { item.icon = iconHelper.convertFromLegacyIcon(item.icon); } @@ -359,7 +355,7 @@ function contentPickerController($scope, entityResource, editorState, iconHelper function setSortingState(items) { // disable sorting if the list only consist of one item - if(items.length > 1) { + if (items.length > 1) { $scope.sortableOptions.disabled = false; } else { $scope.sortableOptions.disabled = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js index e9c095c1b9..3d0d568866 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js @@ -16,17 +16,13 @@ angular.module("umbraco") } $scope.setImage = function(){ - var dataTypeId = null; - if($scope.model && $scope.model.dataTypeId) { - dataTypeId = $scope.model.dataTypeId; - } - + $scope.mediaPickerOverlay = {}; $scope.mediaPickerOverlay.view = "mediapicker"; - $scope.mediaPickerOverlay.startNodeId = $scope.model.config && $scope.model.config.startNodeId ? $scope.model.config.startNodeId : undefined; - $scope.mediaPickerOverlay.startNodeIsVirtual = $scope.mediaPickerOverlay.startNodeId ? $scope.model.config.startNodeIsVirtual : undefined; - $scope.mediaPickerOverlay.dataTypeId = dataTypeId; - $scope.mediaPickerOverlay.cropSize = $scope.control.editor.config && $scope.control.editor.config.size ? $scope.control.editor.config.size : undefined; + $scope.mediaPickerOverlay.startNodeId = $scope.model.config && $scope.model.config.startNodeId ? $scope.model.config.startNodeId : null; + $scope.mediaPickerOverlay.startNodeIsVirtual = $scope.mediaPickerOverlay.startNodeId ? $scope.model.config.startNodeIsVirtual : null; + $scope.mediaPickerOverlay.dataTypeId = ($scope.model && $scope.model.dataTypeId) ? $scope.model.dataTypeId : null; + $scope.mediaPickerOverlay.cropSize = $scope.control.editor.config && $scope.control.editor.config.size ? $scope.control.editor.config.size : null; $scope.mediaPickerOverlay.showDetails = true; $scope.mediaPickerOverlay.disableFolderSelect = true; $scope.mediaPickerOverlay.onlyImages = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js index 580d231de8..94f2c83cbf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js @@ -10,14 +10,12 @@ vm.openMacroPicker = openMacroPicker; vm.openEmbed = openEmbed; + const dataTypeId = ($scope.model && $scope.model.dataTypeId) ? $scope.model.dataTypeId : null; + function openLinkPicker(editor, currentTarget, anchorElement) { entityResource.getAnchors(JSON.stringify($scope.model.value)).then(function(anchorValues) { - var dataTypeId = null; - if($scope.model && $scope.model.dataTypeId) { - dataTypeId = $scope.model.dataTypeId; - } - + vm.linkPickerOverlay = { view: "linkpicker", currentTarget: currentTarget, @@ -45,11 +43,6 @@ startNodeIsVirtual = true; } - var dataTypeId = null; - if($scope.model && $scope.model.dataTypeId) { - dataTypeId = $scope.model.dataTypeId; - } - vm.mediaPickerOverlay = { currentTarget: currentTarget, onlyImages: true, diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js index 63beddb49c..8aafba8ca1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js @@ -107,17 +107,13 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl }; $scope.add = function() { - var dataTypeId = null; - if($scope.model && $scope.model.dataTypeId) { - dataTypeId = $scope.model.dataTypeId; - } - + $scope.mediaPickerOverlay = { view: "mediapicker", title: "Select media", startNodeId: $scope.model.config.startNodeId, startNodeIsVirtual: $scope.model.config.startNodeIsVirtual, - dataTypeId: dataTypeId, + dataTypeId: ($scope.model && $scope.model.dataTypeId) ? $scope.model.dataTypeId : null, multiPicker: multiPicker, onlyImages: onlyImages, disableFolderSelect: disableFolderSelect, diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js index cab124ad23..a5fc5c50d2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js @@ -68,15 +68,10 @@ function multiUrlPickerController($scope, angularHelper, localizationService, en target: link.target } : null; - var dataTypeId = null; - if($scope.model && $scope.model.dataTypeId) { - dataTypeId = $scope.model.dataTypeId; - } - $scope.linkPickerOverlay = { view: "linkpicker", currentTarget: target, - dataTypeId: dataTypeId, + dataTypeId: ($scope.model && $scope.model.dataTypeId) ? $scope.model.dataTypeId : null, ignoreUserStartNodes : $scope.model.config.ignoreUserStartNodes, show: true, submit: function (model) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js index fa92442eac..78adf2fee5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js @@ -18,12 +18,10 @@ $scope.currentEditLink = null; $scope.hasError = false; - $scope.internal = function($event) { - var dataTypeId = null; - if($scope.model && $scope.model.dataTypeId) { - dataTypeId = $scope.model.dataTypeId; - } + const dataTypeId = ($scope.model && $scope.model.dataTypeId) ? $scope.model.dataTypeId : null; + $scope.internal = function($event) { + $scope.currentEditLink = null; $scope.contentPickerOverlay = {}; @@ -50,11 +48,7 @@ }; $scope.selectInternal = function ($event, link) { - var dataTypeId = null; - if($scope.model && $scope.model.dataTypeId) { - dataTypeId = $scope.model.dataTypeId; - } - + $scope.currentEditLink = link; $scope.contentPickerOverlay = {}; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js index 2b3ee930d9..d92319a3fc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js @@ -52,6 +52,8 @@ angular.module("umbraco") editorConfig.maxImageSize = tinyMceService.defaultPrevalues().maxImageSize; } + const dataTypeId = ($scope.model && $scope.model.dataTypeId) ? $scope.model.dataTypeId : null; + //queue file loading if (typeof tinymce === "undefined") { // Don't reload tinymce if already loaded await.push(assetsService.loadJs("lib/tinymce/tinymce.min.js", $scope)); @@ -272,11 +274,7 @@ angular.module("umbraco") tinyMceService.createLinkPicker(editor, $scope, function(currentTarget, anchorElement) { entityResource.getAnchors($scope.model.value).then(function(anchorValues){ - var dataTypeId = null; - if($scope.model && $scope.model.dataTypeId) { - dataTypeId = $scope.model.dataTypeId; - } - + $scope.linkPickerOverlay = { view: "linkpicker", currentTarget: currentTarget, @@ -305,11 +303,7 @@ angular.module("umbraco") startNodeId = -1; startNodeIsVirtual = true; } - var dataTypeId = null; - if($scope.model && $scope.model.dataTypeId) { - dataTypeId = $scope.model.dataTypeId; - } - + $scope.mediaPickerOverlay = { currentTarget: currentTarget, onlyImages: true, From 6148ce4e52e66ca7e4edbba8228b067cd09ff112 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 22 Jul 2019 13:41:26 +1000 Subject: [PATCH 52/67] oops no es6 support in 7.x --- .../views/common/overlays/mediaPicker/mediapicker.controller.js | 2 +- .../src/views/propertyeditors/grid/editors/rte.controller.js | 2 +- .../propertyeditors/relatedlinks/relatedlinks.controller.js | 2 +- .../src/views/propertyeditors/rte/rte.controller.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js index bae4882a3e..5f76528f5d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js @@ -54,7 +54,7 @@ angular.module("umbraco") totalItems: 0, totalPages: 0, filter: '', - dataTypeId: dataTypeId, + dataTypeId: dataTypeId }; //preload selected item diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js index 94f2c83cbf..e36e398024 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.controller.js @@ -10,7 +10,7 @@ vm.openMacroPicker = openMacroPicker; vm.openEmbed = openEmbed; - const dataTypeId = ($scope.model && $scope.model.dataTypeId) ? $scope.model.dataTypeId : null; + var dataTypeId = ($scope.model && $scope.model.dataTypeId) ? $scope.model.dataTypeId : null; function openLinkPicker(editor, currentTarget, anchorElement) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js index 78adf2fee5..392e0dc0cd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js @@ -18,7 +18,7 @@ $scope.currentEditLink = null; $scope.hasError = false; - const dataTypeId = ($scope.model && $scope.model.dataTypeId) ? $scope.model.dataTypeId : null; + var dataTypeId = ($scope.model && $scope.model.dataTypeId) ? $scope.model.dataTypeId : null; $scope.internal = function($event) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js index d92319a3fc..109aa37fbb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js @@ -52,7 +52,7 @@ angular.module("umbraco") editorConfig.maxImageSize = tinyMceService.defaultPrevalues().maxImageSize; } - const dataTypeId = ($scope.model && $scope.model.dataTypeId) ? $scope.model.dataTypeId : null; + var dataTypeId = ($scope.model && $scope.model.dataTypeId) ? $scope.model.dataTypeId : null; //queue file loading if (typeof tinymce === "undefined") { // Don't reload tinymce if already loaded From 5126521d37fcc1050a5da44f667560e29752d412 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 12 Jul 2019 13:15:41 +1000 Subject: [PATCH 53/67] Fixes #5789 --- src/Umbraco.Web/IPublishedContentQuery.cs | 8 ++++---- src/Umbraco.Web/PublishedContentQuery.cs | 25 +++++++++++++++-------- src/Umbraco.Web/UmbracoContextFactory.cs | 8 ++++++++ 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Web/IPublishedContentQuery.cs b/src/Umbraco.Web/IPublishedContentQuery.cs index 76e7be5e97..35db121a60 100644 --- a/src/Umbraco.Web/IPublishedContentQuery.cs +++ b/src/Umbraco.Web/IPublishedContentQuery.cs @@ -39,10 +39,10 @@ namespace Umbraco.Web /// Optional culture. /// Optional index name. /// - /// When the is not specified, all cultures are searched. + /// When the is not specified or is *, all cultures are searched. To search only invariant use null. /// While enumerating results, the ambient culture is changed to be the searched culture. /// - IEnumerable Search(string term, string culture = null, string indexName = null); + IEnumerable Search(string term, string culture = "*", string indexName = null); /// /// Searches content. @@ -54,10 +54,10 @@ namespace Umbraco.Web /// Optional culture. /// Optional index name. /// - /// When the is not specified, all cultures are searched. + /// When the is not specified or is *, all cultures are searched. To search only invariant use null. /// While enumerating results, the ambient culture is changed to be the searched culture. /// - IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = null, string indexName = null); + IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = null); /// /// Executes the query and converts the results to PublishedSearchResult. diff --git a/src/Umbraco.Web/PublishedContentQuery.cs b/src/Umbraco.Web/PublishedContentQuery.cs index 61180580cb..2772cc94f6 100644 --- a/src/Umbraco.Web/PublishedContentQuery.cs +++ b/src/Umbraco.Web/PublishedContentQuery.cs @@ -175,13 +175,13 @@ namespace Umbraco.Web #region Search /// - public IEnumerable Search(string term, string culture = null, string indexName = null) + public IEnumerable Search(string term, string culture = "*", string indexName = null) { return Search(term, 0, 0, out _, culture, indexName); } /// - public IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = null, string indexName = null) + public IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = null) { indexName = string.IsNullOrEmpty(indexName) ? Constants.UmbracoIndexes.ExternalIndexName @@ -195,20 +195,29 @@ namespace Umbraco.Web // default to max 500 results var count = skip == 0 && take == 0 ? 500 : skip + take; - //set this to the specific culture or to the culture in the request - culture = culture ?? _variationContextAccessor.VariationContext.Culture; - ISearchResults results; - if (culture.IsNullOrWhiteSpace()) + if (culture == "*") { + //search everything + results = searcher.Search(term, count); } + else if (culture.IsNullOrWhiteSpace()) + { + //only search invariant + + var qry = searcher.CreateQuery().Field(UmbracoContentIndex.VariesByCultureFieldName, "n"); //must not vary by culture + qry = qry.And().ManagedQuery(term); + results = qry.Execute(count); + } else { + //search only the specified culture + //get all index fields suffixed with the culture name supplied - var cultureFields = umbIndex.GetCultureFields(culture); + var cultureFields = umbIndex.GetCultureFields(culture).ToArray(); var qry = searcher.CreateQuery().Field(UmbracoContentIndex.VariesByCultureFieldName, "y"); //must vary by culture - qry = qry.And().ManagedQuery(term, cultureFields.ToArray()); + qry = qry.And().ManagedQuery(term, cultureFields); results = qry.Execute(count); } diff --git a/src/Umbraco.Web/UmbracoContextFactory.cs b/src/Umbraco.Web/UmbracoContextFactory.cs index 2a812036bf..11d8952fa6 100644 --- a/src/Umbraco.Web/UmbracoContextFactory.cs +++ b/src/Umbraco.Web/UmbracoContextFactory.cs @@ -53,7 +53,15 @@ namespace Umbraco.Web { // make sure we have a variation context if (_variationContextAccessor.VariationContext == null) + { + // TODO: By using _defaultCultureAccessor.DefaultCulture this means that the VariationContext will always return a variant culture, it will never + // return an empty string signifying that the culture is invariant. But does this matter? Are we actually expecting this to return an empty string + // for invariant routes? From what i can tell throughout the codebase is that whenever we are checking against the VariationContext.Culture we are + // also checking if the content type varies by culture or not. This is fine, however the code in the ctor of VariationContext is then misleading + // since it's assuming that the Culture can be empty (invariant) when in reality of a website this will never be empty since a real culture is always set here. _variationContextAccessor.VariationContext = new VariationContext(_defaultCultureAccessor.DefaultCulture); + } + var webSecurity = new WebSecurity(httpContext, _userService, _globalSettings); From 1c6bf55e5d3a65dd63ba5dcbd05bffc69a89f537 Mon Sep 17 00:00:00 2001 From: Frans de Jong Date: Fri, 19 Jul 2019 14:42:44 +0200 Subject: [PATCH 54/67] GetCurrentLoginStatus() In the summary of MembershipHelper.GetCurrentLoginStatus() it states that null will be returned when no member is logged in but that isn't the case (anymore?). --- src/Umbraco.Web/Security/MembershipHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Security/MembershipHelper.cs b/src/Umbraco.Web/Security/MembershipHelper.cs index cdf696c520..f74897d565 100644 --- a/src/Umbraco.Web/Security/MembershipHelper.cs +++ b/src/Umbraco.Web/Security/MembershipHelper.cs @@ -542,7 +542,7 @@ namespace Umbraco.Web.Security } /// - /// Returns the login status model of the currently logged in member, if no member is logged in it returns null; + /// Returns the login status model of the currently logged in member. /// /// public virtual LoginStatusModel GetCurrentLoginStatus() From a8ed7f2c17512f36ef883a9805603488df6c5b42 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 23 Jul 2019 22:11:02 +1000 Subject: [PATCH 55/67] adds new method --- src/Umbraco.Examine/ExamineExtensions.cs | 25 ++++++++++++++++++++++++ src/Umbraco.Web/PublishedContentQuery.cs | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Examine/ExamineExtensions.cs b/src/Umbraco.Examine/ExamineExtensions.cs index 1b8033c458..d97278f31c 100644 --- a/src/Umbraco.Examine/ExamineExtensions.cs +++ b/src/Umbraco.Examine/ExamineExtensions.cs @@ -48,6 +48,31 @@ namespace Umbraco.Examine } } + /// + /// Returns all index fields that are culture specific (suffixed) or invariant + /// + /// + /// + /// + public static IEnumerable GetCultureAndInvariantFields(this IUmbracoIndex index, string culture) + { + var allFields = index.GetFields(); + // ReSharper disable once LoopCanBeConvertedToQuery + foreach (var field in allFields) + { + var match = CultureIsoCodeFieldNameMatchExpression.Match(field); + if (match.Success && match.Groups.Count == 3 && culture.InvariantEquals(match.Groups[2].Value)) + { + yield return field; //matches this culture field + } + else if (!match.Success) + { + yield return field; //matches no culture field (invariant) + } + + } + } + internal static bool TryParseLuceneQuery(string query) { // TODO: I'd assume there would be a more strict way to parse the query but not that i can find yet, for now we'll diff --git a/src/Umbraco.Web/PublishedContentQuery.cs b/src/Umbraco.Web/PublishedContentQuery.cs index 2772cc94f6..19e303602d 100644 --- a/src/Umbraco.Web/PublishedContentQuery.cs +++ b/src/Umbraco.Web/PublishedContentQuery.cs @@ -215,7 +215,7 @@ namespace Umbraco.Web //search only the specified culture //get all index fields suffixed with the culture name supplied - var cultureFields = umbIndex.GetCultureFields(culture).ToArray(); + var cultureFields = umbIndex.GetCultureAndInvariantFields(culture).ToArray(); var qry = searcher.CreateQuery().Field(UmbracoContentIndex.VariesByCultureFieldName, "y"); //must vary by culture qry = qry.And().ManagedQuery(term, cultureFields); results = qry.Execute(count); From 2f8979bbc3975dea34d1cfd8c60a0ebb90b0896b Mon Sep 17 00:00:00 2001 From: Shannon Deminick Date: Wed, 24 Jul 2019 17:29:00 +1000 Subject: [PATCH 56/67] Update src/Umbraco.Web/PublishedContentQuery.cs Co-Authored-By: Bjarke Berg --- src/Umbraco.Web/PublishedContentQuery.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PublishedContentQuery.cs b/src/Umbraco.Web/PublishedContentQuery.cs index 19e303602d..5af5837495 100644 --- a/src/Umbraco.Web/PublishedContentQuery.cs +++ b/src/Umbraco.Web/PublishedContentQuery.cs @@ -216,7 +216,7 @@ namespace Umbraco.Web //get all index fields suffixed with the culture name supplied var cultureFields = umbIndex.GetCultureAndInvariantFields(culture).ToArray(); - var qry = searcher.CreateQuery().Field(UmbracoContentIndex.VariesByCultureFieldName, "y"); //must vary by culture + var qry = searcher.CreateQuery(); qry = qry.And().ManagedQuery(term, cultureFields); results = qry.Execute(count); } From beb8c7ac7c1076eabc132e3eb824e8ea8c6cd667 Mon Sep 17 00:00:00 2001 From: Shannon Deminick Date: Wed, 24 Jul 2019 17:30:29 +1000 Subject: [PATCH 57/67] Update src/Umbraco.Web/PublishedContentQuery.cs Co-Authored-By: Bjarke Berg --- src/Umbraco.Web/PublishedContentQuery.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PublishedContentQuery.cs b/src/Umbraco.Web/PublishedContentQuery.cs index 5af5837495..cfdd31f138 100644 --- a/src/Umbraco.Web/PublishedContentQuery.cs +++ b/src/Umbraco.Web/PublishedContentQuery.cs @@ -217,7 +217,7 @@ namespace Umbraco.Web //get all index fields suffixed with the culture name supplied var cultureFields = umbIndex.GetCultureAndInvariantFields(culture).ToArray(); var qry = searcher.CreateQuery(); - qry = qry.And().ManagedQuery(term, cultureFields); + qry = qry.ManagedQuery(term, cultureFields); results = qry.Execute(count); } From 210e43fcb084c977a514e92f0f4376442ea85bb3 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 24 Jul 2019 09:53:51 +0200 Subject: [PATCH 58/67] Fix for build --- src/Umbraco.Web/PublishedContentQuery.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web/PublishedContentQuery.cs b/src/Umbraco.Web/PublishedContentQuery.cs index cfdd31f138..887368a3e9 100644 --- a/src/Umbraco.Web/PublishedContentQuery.cs +++ b/src/Umbraco.Web/PublishedContentQuery.cs @@ -216,8 +216,7 @@ namespace Umbraco.Web //get all index fields suffixed with the culture name supplied var cultureFields = umbIndex.GetCultureAndInvariantFields(culture).ToArray(); - var qry = searcher.CreateQuery(); - qry = qry.ManagedQuery(term, cultureFields); + var qry = searcher.CreateQuery().ManagedQuery(term, cultureFields); results = qry.Execute(count); } @@ -313,7 +312,7 @@ namespace Umbraco.Web } } - + #endregion From 8a43e3a87ec28b997878e2433e464b07680bf5e2 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Fri, 12 Jul 2019 19:20:15 +0200 Subject: [PATCH 59/67] Make sure save options are up to date with the content state --- .../common/directives/components/content/edit.controller.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index a548820138..58a22ac74e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -94,6 +94,10 @@ content.apps[0].active = true; $scope.appChanged(content.apps[0]); } + // otherwise make sure the save options are up to date with the current content state + else { + createButtons($scope.content); + } editorState.set(content); From 2c795662d2d5493039eda2a85120c2e3d39b3758 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 24 Jul 2019 18:46:14 +1000 Subject: [PATCH 60/67] Adds tests for PublishedContentQuery search --- .../TestHelpers/RandomIdRamDirectory.cs | 22 +++ src/Umbraco.Tests/Umbraco.Tests.csproj | 2 + .../Web/PublishedContentQueryTests.cs | 157 ++++++++++++++++++ src/Umbraco.Tests/Web/UmbracoHelperTests.cs | 10 +- src/Umbraco.Web/IPublishedContentQuery.cs | 12 +- src/Umbraco.Web/PublishedContentQuery.cs | 17 +- 6 files changed, 210 insertions(+), 10 deletions(-) create mode 100644 src/Umbraco.Tests/TestHelpers/RandomIdRamDirectory.cs create mode 100644 src/Umbraco.Tests/Web/PublishedContentQueryTests.cs diff --git a/src/Umbraco.Tests/TestHelpers/RandomIdRamDirectory.cs b/src/Umbraco.Tests/TestHelpers/RandomIdRamDirectory.cs new file mode 100644 index 0000000000..34904db1ae --- /dev/null +++ b/src/Umbraco.Tests/TestHelpers/RandomIdRamDirectory.cs @@ -0,0 +1,22 @@ +using Lucene.Net.Store; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Tests.TestHelpers +{ + + /// + /// Used for tests with Lucene so that each RAM directory is unique + /// + public class RandomIdRAMDirectory : RAMDirectory + { + private readonly string _lockId = Guid.NewGuid().ToString(); + public override string GetLockId() + { + return _lockId; + } + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index f41ff1dd07..717006b702 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -157,6 +157,7 @@ + @@ -268,6 +269,7 @@ + diff --git a/src/Umbraco.Tests/Web/PublishedContentQueryTests.cs b/src/Umbraco.Tests/Web/PublishedContentQueryTests.cs new file mode 100644 index 0000000000..b2a2741bcf --- /dev/null +++ b/src/Umbraco.Tests/Web/PublishedContentQueryTests.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Examine; +using Examine.LuceneEngine.Providers; +using Lucene.Net.Store; +using Moq; +using NUnit.Framework; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Examine; +using Umbraco.Tests.TestHelpers; +using Umbraco.Web; +using Umbraco.Web.PublishedCache; + +namespace Umbraco.Tests.Web +{ + [TestFixture] + public class PublishedContentQueryTests + { + + private class TestIndex : LuceneIndex, IUmbracoIndex + { + private readonly string[] _fieldNames; + + public TestIndex(string name, Directory luceneDirectory, string[] fieldNames) + : base(name, luceneDirectory, null, null, null, null) + { + _fieldNames = fieldNames; + } + public bool EnableDefaultEventHandler => throw new NotImplementedException(); + public bool PublishedValuesOnly => throw new NotImplementedException(); + public IEnumerable GetFields() => _fieldNames; + } + + private TestIndex CreateTestIndex(Directory luceneDirectory, string[] fieldNames) + { + var indexer = new TestIndex("TestIndex", luceneDirectory, fieldNames); + + //populate with some test data + indexer.IndexItem(new ValueSet("1", "content", new Dictionary + { + [fieldNames[0]] = "Hello world, there are products here", + [UmbracoContentIndex.VariesByCultureFieldName] = "n" + })); + indexer.IndexItem(new ValueSet("2", "content", new Dictionary + { + [fieldNames[1]] = "Hello world, there are products here", + [UmbracoContentIndex.VariesByCultureFieldName] = "y" + })); + indexer.IndexItem(new ValueSet("3", "content", new Dictionary + { + [fieldNames[2]] = "Hello world, there are products here", + [UmbracoContentIndex.VariesByCultureFieldName] = "y" + })); + return indexer; + } + + private PublishedContentQuery CreatePublishedContentQuery(IIndex indexer) + { + var examineManager = new Mock(); + IIndex outarg = indexer; + examineManager.Setup(x => x.TryGetIndex("TestIndex", out outarg)).Returns(true); + + var contentCache = new Mock(); + contentCache.Setup(x => x.GetById(It.IsAny())).Returns((int intId) => Mock.Of(x => x.Id == intId)); + var snapshot = Mock.Of(x => x.Content == contentCache.Object); + var variationContext = new VariationContext(); + var variationContextAccessor = Mock.Of(x => x.VariationContext == variationContext); + + return new PublishedContentQuery(snapshot, variationContextAccessor, examineManager.Object); + } + + [Test] + public void Search_Wildcard() + { + using (var luceneDir = new RandomIdRAMDirectory()) + { + var fieldNames = new[] { "title", "title_en-us", "title_fr-fr" }; + using (var indexer = CreateTestIndex(luceneDir, fieldNames)) + { + var pcq = CreatePublishedContentQuery(indexer); + + var results = pcq.Search("Products", "*", "TestIndex"); + + var ids = results.Select(x => x.Content.Id).ToList(); + Assert.AreEqual(3, ids.Count); + + //returns results for all fields and document types + Assert.IsTrue(ids.Contains(1) && ids.Contains(2) && ids.Contains(3)); + } + } + } + + [Test] + public void Search_Invariant() + { + using (var luceneDir = new RandomIdRAMDirectory()) + { + var fieldNames = new[] { "title", "title_en-us", "title_fr-fr" }; + using (var indexer = CreateTestIndex(luceneDir, fieldNames)) + { + var pcq = CreatePublishedContentQuery(indexer); + + var results = pcq.Search("Products", null, "TestIndex"); + + var ids = results.Select(x => x.Content.Id).ToList(); + Assert.AreEqual(1, ids.Count); + + //returns results for only invariant fields and invariant documents + Assert.IsTrue(ids.Contains(1) && !ids.Contains(2) && !ids.Contains(3)); + } + } + } + + [Test] + public void Search_Culture1() + { + using (var luceneDir = new RandomIdRAMDirectory()) + { + var fieldNames = new[] { "title", "title_en-us", "title_fr-fr" }; + using (var indexer = CreateTestIndex(luceneDir, fieldNames)) + { + var pcq = CreatePublishedContentQuery(indexer); + + var results = pcq.Search("Products", "en-us", "TestIndex"); + + var ids = results.Select(x => x.Content.Id).ToList(); + Assert.AreEqual(2, ids.Count); + + //returns results for en-us fields and invariant fields for all document types + Assert.IsTrue(ids.Contains(1) && ids.Contains(2) && !ids.Contains(3)); + } + } + } + + [Test] + public void Search_Culture2() + { + using (var luceneDir = new RandomIdRAMDirectory()) + { + var fieldNames = new[] { "title", "title_en-us", "title_fr-fr" }; + using (var indexer = CreateTestIndex(luceneDir, fieldNames)) + { + var pcq = CreatePublishedContentQuery(indexer); + + var results = pcq.Search("Products", "fr-fr", "TestIndex"); + + var ids = results.Select(x => x.Content.Id).ToList(); + Assert.AreEqual(2, ids.Count); + + //returns results for fr-fr fields and invariant fields for all document types + Assert.IsTrue(ids.Contains(1) && !ids.Contains(2) && ids.Contains(3)); + } + } + } + } +} diff --git a/src/Umbraco.Tests/Web/UmbracoHelperTests.cs b/src/Umbraco.Tests/Web/UmbracoHelperTests.cs index b23b5bd6b7..26d85f60cf 100644 --- a/src/Umbraco.Tests/Web/UmbracoHelperTests.cs +++ b/src/Umbraco.Tests/Web/UmbracoHelperTests.cs @@ -1,5 +1,8 @@ using System; using System.Text; +using Examine.LuceneEngine; +using Lucene.Net.Analysis; +using Lucene.Net.Analysis.Standard; using Moq; using NUnit.Framework; using Umbraco.Core; @@ -13,18 +16,19 @@ using Umbraco.Web; namespace Umbraco.Tests.Web { + [TestFixture] public class UmbracoHelperTests - { + { [TearDown] public void TearDown() { Current.Reset(); } - - + + // ------- Int32 conversion tests [Test] public static void Converting_Boxed_34_To_An_Int_Returns_34() diff --git a/src/Umbraco.Web/IPublishedContentQuery.cs b/src/Umbraco.Web/IPublishedContentQuery.cs index 35db121a60..8a8d678aba 100644 --- a/src/Umbraco.Web/IPublishedContentQuery.cs +++ b/src/Umbraco.Web/IPublishedContentQuery.cs @@ -39,7 +39,11 @@ namespace Umbraco.Web /// Optional culture. /// Optional index name. /// - /// When the is not specified or is *, all cultures are searched. To search only invariant use null. + /// + /// When the is not specified or is *, all cultures are searched. + /// To search for only invariant documents and fields use null. + /// When searching on a specific culture, all culture specific fields are searched for the provided culture and all invariant fields for all documents. + /// /// While enumerating results, the ambient culture is changed to be the searched culture. /// IEnumerable Search(string term, string culture = "*", string indexName = null); @@ -54,7 +58,11 @@ namespace Umbraco.Web /// Optional culture. /// Optional index name. /// - /// When the is not specified or is *, all cultures are searched. To search only invariant use null. + /// + /// When the is not specified or is *, all cultures are searched. + /// To search for only invariant documents and fields use null. + /// When searching on a specific culture, all culture specific fields are searched for the provided culture and all invariant fields for all documents. + /// /// While enumerating results, the ambient culture is changed to be the searched culture. /// IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = "*", string indexName = null); diff --git a/src/Umbraco.Web/PublishedContentQuery.cs b/src/Umbraco.Web/PublishedContentQuery.cs index cfdd31f138..2dbe4de4c5 100644 --- a/src/Umbraco.Web/PublishedContentQuery.cs +++ b/src/Umbraco.Web/PublishedContentQuery.cs @@ -20,14 +20,22 @@ namespace Umbraco.Web { private readonly IPublishedSnapshot _publishedSnapshot; private readonly IVariationContextAccessor _variationContextAccessor; + private readonly IExamineManager _examineManager; + + [Obsolete("Use the constructor with all parameters instead")] + public PublishedContentQuery(IPublishedSnapshot publishedSnapshot, IVariationContextAccessor variationContextAccessor) + : this (publishedSnapshot, variationContextAccessor, ExamineManager.Instance) + { + } /// /// Initializes a new instance of the class. /// - public PublishedContentQuery(IPublishedSnapshot publishedSnapshot, IVariationContextAccessor variationContextAccessor) + public PublishedContentQuery(IPublishedSnapshot publishedSnapshot, IVariationContextAccessor variationContextAccessor, IExamineManager examineManager) { _publishedSnapshot = publishedSnapshot ?? throw new ArgumentNullException(nameof(publishedSnapshot)); _variationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); + _examineManager = examineManager ?? throw new ArgumentNullException(nameof(examineManager)); } #region Content @@ -187,7 +195,7 @@ namespace Umbraco.Web ? Constants.UmbracoIndexes.ExternalIndexName : indexName; - if (!ExamineManager.Instance.TryGetIndex(indexName, out var index) || !(index is IUmbracoIndex umbIndex)) + if (!_examineManager.TryGetIndex(indexName, out var index) || !(index is IUmbracoIndex umbIndex)) throw new InvalidOperationException($"No index found by name {indexName} or is not of type {typeof(IUmbracoIndex)}"); var searcher = umbIndex.GetSearcher(); @@ -216,8 +224,7 @@ namespace Umbraco.Web //get all index fields suffixed with the culture name supplied var cultureFields = umbIndex.GetCultureAndInvariantFields(culture).ToArray(); - var qry = searcher.CreateQuery(); - qry = qry.ManagedQuery(term, cultureFields); + var qry = searcher.CreateQuery().ManagedQuery(term, cultureFields); results = qry.Execute(count); } @@ -313,7 +320,7 @@ namespace Umbraco.Web } } - + #endregion From 308f929f7b5ba1bfd6b8fc373427a426929db2a2 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 24 Jul 2019 11:13:21 +0200 Subject: [PATCH 61/67] Refactored tests to avoid duplicate code --- .../Web/PublishedContentQueryTests.cs | 78 ++----------------- 1 file changed, 8 insertions(+), 70 deletions(-) diff --git a/src/Umbraco.Tests/Web/PublishedContentQueryTests.cs b/src/Umbraco.Tests/Web/PublishedContentQueryTests.cs index b2a2741bcf..2cff946372 100644 --- a/src/Umbraco.Tests/Web/PublishedContentQueryTests.cs +++ b/src/Umbraco.Tests/Web/PublishedContentQueryTests.cs @@ -70,8 +70,11 @@ namespace Umbraco.Tests.Web return new PublishedContentQuery(snapshot, variationContextAccessor, examineManager.Object); } - [Test] - public void Search_Wildcard() + [TestCase("fr-fr", ExpectedResult = "1, 3", TestName = "Search Culture: fr-fr. Must return both fr-fr and invariant results")] + [TestCase("en-us", ExpectedResult = "1, 2", TestName = "Search Culture: en-us. Must return both en-us and invariant results")] + [TestCase("*", ExpectedResult = "1, 2, 3", TestName = "Search Culture: *. Must return all cultures and all invariant results")] + [TestCase(null, ExpectedResult = "1", TestName = "Search Culture: null. Must return only invariant results")] + public string Search(string culture) { using (var luceneDir = new RandomIdRAMDirectory()) { @@ -80,76 +83,11 @@ namespace Umbraco.Tests.Web { var pcq = CreatePublishedContentQuery(indexer); - var results = pcq.Search("Products", "*", "TestIndex"); + var results = pcq.Search("Products", culture, "TestIndex"); - var ids = results.Select(x => x.Content.Id).ToList(); - Assert.AreEqual(3, ids.Count); + var ids = results.Select(x => x.Content.Id).ToArray(); - //returns results for all fields and document types - Assert.IsTrue(ids.Contains(1) && ids.Contains(2) && ids.Contains(3)); - } - } - } - - [Test] - public void Search_Invariant() - { - using (var luceneDir = new RandomIdRAMDirectory()) - { - var fieldNames = new[] { "title", "title_en-us", "title_fr-fr" }; - using (var indexer = CreateTestIndex(luceneDir, fieldNames)) - { - var pcq = CreatePublishedContentQuery(indexer); - - var results = pcq.Search("Products", null, "TestIndex"); - - var ids = results.Select(x => x.Content.Id).ToList(); - Assert.AreEqual(1, ids.Count); - - //returns results for only invariant fields and invariant documents - Assert.IsTrue(ids.Contains(1) && !ids.Contains(2) && !ids.Contains(3)); - } - } - } - - [Test] - public void Search_Culture1() - { - using (var luceneDir = new RandomIdRAMDirectory()) - { - var fieldNames = new[] { "title", "title_en-us", "title_fr-fr" }; - using (var indexer = CreateTestIndex(luceneDir, fieldNames)) - { - var pcq = CreatePublishedContentQuery(indexer); - - var results = pcq.Search("Products", "en-us", "TestIndex"); - - var ids = results.Select(x => x.Content.Id).ToList(); - Assert.AreEqual(2, ids.Count); - - //returns results for en-us fields and invariant fields for all document types - Assert.IsTrue(ids.Contains(1) && ids.Contains(2) && !ids.Contains(3)); - } - } - } - - [Test] - public void Search_Culture2() - { - using (var luceneDir = new RandomIdRAMDirectory()) - { - var fieldNames = new[] { "title", "title_en-us", "title_fr-fr" }; - using (var indexer = CreateTestIndex(luceneDir, fieldNames)) - { - var pcq = CreatePublishedContentQuery(indexer); - - var results = pcq.Search("Products", "fr-fr", "TestIndex"); - - var ids = results.Select(x => x.Content.Id).ToList(); - Assert.AreEqual(2, ids.Count); - - //returns results for fr-fr fields and invariant fields for all document types - Assert.IsTrue(ids.Contains(1) && !ids.Contains(2) && ids.Contains(3)); + return string.Join(", ", ids); } } } From a97604d6c405c7ebf6372f69807193e8cbbc7a50 Mon Sep 17 00:00:00 2001 From: Steve Megson Date: Sun, 21 Jul 2019 15:19:17 +0100 Subject: [PATCH 62/67] Handle CSV values in TagsPropertyEditor --- .../PropertyEditors/TagsPropertyEditor.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs index 90527a8b8d..b7101aa764 100644 --- a/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using Newtonsoft.Json.Linq; @@ -38,9 +39,15 @@ namespace Umbraco.Web.PropertyEditors /// public override object FromEditor(ContentPropertyData editorValue, object currentValue) { - return editorValue.Value is JArray json - ? json.Select(x => x.Value()) - : null; + if (editorValue.Value is JArray json) + { + return json.Select(x => x.Value()); + } + else if ( editorValue.Value is string stringValue && !String.IsNullOrWhiteSpace(stringValue)) + { + return stringValue.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); + } + return null; } /// From 28a80271795e2b2d149b8fa815271a5994394baf Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 24 Jul 2019 19:25:32 +1000 Subject: [PATCH 63/67] removes reference to old packages.umbraco.org --- .../Properties/Settings.Designer.cs | 12 +- src/Umbraco.Web/Properties/Settings.settings | 3 - src/Umbraco.Web/Umbraco.Web.csproj | 22 - .../org.umbraco.our/Reference.cs | 1046 ----------------- .../org.umbraco.our/Reference.map | 7 - .../org.umbraco.our/repository.disco | 6 - .../org.umbraco.our/repository.wsdl | 995 ---------------- 7 files changed, 1 insertion(+), 2090 deletions(-) delete mode 100644 src/Umbraco.Web/Web References/org.umbraco.our/Reference.cs delete mode 100644 src/Umbraco.Web/Web References/org.umbraco.our/Reference.map delete mode 100644 src/Umbraco.Web/Web References/org.umbraco.our/repository.disco delete mode 100644 src/Umbraco.Web/Web References/org.umbraco.our/repository.wsdl diff --git a/src/Umbraco.Web/Properties/Settings.Designer.cs b/src/Umbraco.Web/Properties/Settings.Designer.cs index 5a5a863f4f..5f7ccbd7e1 100644 --- a/src/Umbraco.Web/Properties/Settings.Designer.cs +++ b/src/Umbraco.Web/Properties/Settings.Designer.cs @@ -12,7 +12,7 @@ namespace Umbraco.Web.Properties { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.7.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.2.0.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); @@ -50,15 +50,5 @@ namespace Umbraco.Web.Properties { return ((string)(this["test"])); } } - - [global::System.Configuration.ApplicationScopedSettingAttribute()] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Configuration.SpecialSettingAttribute(global::System.Configuration.SpecialSetting.WebServiceUrl)] - [global::System.Configuration.DefaultSettingValueAttribute("https://our.umbraco.com/umbraco/webservices/api/repository.asmx")] - public string umbraco_org_umbraco_our_Repository { - get { - return ((string)(this["umbraco_org_umbraco_our_Repository"])); - } - } } } diff --git a/src/Umbraco.Web/Properties/Settings.settings b/src/Umbraco.Web/Properties/Settings.settings index 757b363da2..3a6f68023e 100644 --- a/src/Umbraco.Web/Properties/Settings.settings +++ b/src/Umbraco.Web/Properties/Settings.settings @@ -11,8 +11,5 @@ Something - - https://our.umbraco.com/umbraco/webservices/api/repository.asmx - \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index dcaa3494fd..730a321a16 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -906,11 +906,6 @@ - - True - True - Reference.map - @@ -1220,12 +1215,6 @@ Mvc\web.config - - MSDiscoCodeGenerator - Reference.cs - - - Reference.map @@ -1238,17 +1227,6 @@ - - Dynamic - Web References\org.umbraco.our\ - http://our.umbraco.org/umbraco/webservices/api/repository.asmx - - - - - Settings - umbraco_org_umbraco_our_Repository - Dynamic Web References\org.umbraco.update\ diff --git a/src/Umbraco.Web/Web References/org.umbraco.our/Reference.cs b/src/Umbraco.Web/Web References/org.umbraco.our/Reference.cs deleted file mode 100644 index caa2887797..0000000000 --- a/src/Umbraco.Web/Web References/org.umbraco.our/Reference.cs +++ /dev/null @@ -1,1046 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -// -// This source code was auto-generated by Microsoft.VSDesigner, Version 4.0.30319.42000. -// -#pragma warning disable 1591 - -namespace Umbraco.Web.org.umbraco.our { - using System; - using System.Web.Services; - using System.Diagnostics; - using System.Web.Services.Protocols; - using System.Xml.Serialization; - using System.ComponentModel; - - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - [System.Web.Services.WebServiceBindingAttribute(Name="RepositorySoap", Namespace="http://packages.umbraco.org/webservices/")] - public partial class Repository : System.Web.Services.Protocols.SoapHttpClientProtocol { - - private System.Threading.SendOrPostCallback CategoriesOperationCompleted; - - private System.Threading.SendOrPostCallback ModulesOperationCompleted; - - private System.Threading.SendOrPostCallback ModulesCategorizedOperationCompleted; - - private System.Threading.SendOrPostCallback NitrosOperationCompleted; - - private System.Threading.SendOrPostCallback NitrosCategorizedOperationCompleted; - - private System.Threading.SendOrPostCallback authenticateOperationCompleted; - - private System.Threading.SendOrPostCallback fetchPackageOperationCompleted; - - private System.Threading.SendOrPostCallback fetchPackageByVersionOperationCompleted; - - private System.Threading.SendOrPostCallback fetchProtectedPackageOperationCompleted; - - private System.Threading.SendOrPostCallback SubmitPackageOperationCompleted; - - private System.Threading.SendOrPostCallback PackageByGuidOperationCompleted; - - private bool useDefaultCredentialsSetExplicitly; - - /// - public Repository() { - this.Url = "http://our.umbraco.org/umbraco/webservices/api/repository.asmx"; - if ((this.IsLocalFileSystemWebService(this.Url) == true)) { - this.UseDefaultCredentials = true; - this.useDefaultCredentialsSetExplicitly = false; - } - else { - this.useDefaultCredentialsSetExplicitly = true; - } - } - - public new string Url { - get { - return base.Url; - } - set { - if ((((this.IsLocalFileSystemWebService(base.Url) == true) - && (this.useDefaultCredentialsSetExplicitly == false)) - && (this.IsLocalFileSystemWebService(value) == false))) { - base.UseDefaultCredentials = false; - } - base.Url = value; - } - } - - public new bool UseDefaultCredentials { - get { - return base.UseDefaultCredentials; - } - set { - base.UseDefaultCredentials = value; - this.useDefaultCredentialsSetExplicitly = true; - } - } - - /// - public event CategoriesCompletedEventHandler CategoriesCompleted; - - /// - public event ModulesCompletedEventHandler ModulesCompleted; - - /// - public event ModulesCategorizedCompletedEventHandler ModulesCategorizedCompleted; - - /// - public event NitrosCompletedEventHandler NitrosCompleted; - - /// - public event NitrosCategorizedCompletedEventHandler NitrosCategorizedCompleted; - - /// - public event authenticateCompletedEventHandler authenticateCompleted; - - /// - public event fetchPackageCompletedEventHandler fetchPackageCompleted; - - /// - public event fetchPackageByVersionCompletedEventHandler fetchPackageByVersionCompleted; - - /// - public event fetchProtectedPackageCompletedEventHandler fetchProtectedPackageCompleted; - - /// - public event SubmitPackageCompletedEventHandler SubmitPackageCompleted; - - /// - public event PackageByGuidCompletedEventHandler PackageByGuidCompleted; - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/Categories", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public Category[] Categories(string repositoryGuid) { - object[] results = this.Invoke("Categories", new object[] { - repositoryGuid}); - return ((Category[])(results[0])); - } - - /// - public void CategoriesAsync(string repositoryGuid) { - this.CategoriesAsync(repositoryGuid, null); - } - - /// - public void CategoriesAsync(string repositoryGuid, object userState) { - if ((this.CategoriesOperationCompleted == null)) { - this.CategoriesOperationCompleted = new System.Threading.SendOrPostCallback(this.OnCategoriesOperationCompleted); - } - this.InvokeAsync("Categories", new object[] { - repositoryGuid}, this.CategoriesOperationCompleted, userState); - } - - private void OnCategoriesOperationCompleted(object arg) { - if ((this.CategoriesCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.CategoriesCompleted(this, new CategoriesCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/Modules", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public Package[] Modules() { - object[] results = this.Invoke("Modules", new object[0]); - return ((Package[])(results[0])); - } - - /// - public void ModulesAsync() { - this.ModulesAsync(null); - } - - /// - public void ModulesAsync(object userState) { - if ((this.ModulesOperationCompleted == null)) { - this.ModulesOperationCompleted = new System.Threading.SendOrPostCallback(this.OnModulesOperationCompleted); - } - this.InvokeAsync("Modules", new object[0], this.ModulesOperationCompleted, userState); - } - - private void OnModulesOperationCompleted(object arg) { - if ((this.ModulesCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.ModulesCompleted(this, new ModulesCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/ModulesCategorized", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public Category[] ModulesCategorized() { - object[] results = this.Invoke("ModulesCategorized", new object[0]); - return ((Category[])(results[0])); - } - - /// - public void ModulesCategorizedAsync() { - this.ModulesCategorizedAsync(null); - } - - /// - public void ModulesCategorizedAsync(object userState) { - if ((this.ModulesCategorizedOperationCompleted == null)) { - this.ModulesCategorizedOperationCompleted = new System.Threading.SendOrPostCallback(this.OnModulesCategorizedOperationCompleted); - } - this.InvokeAsync("ModulesCategorized", new object[0], this.ModulesCategorizedOperationCompleted, userState); - } - - private void OnModulesCategorizedOperationCompleted(object arg) { - if ((this.ModulesCategorizedCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.ModulesCategorizedCompleted(this, new ModulesCategorizedCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/Nitros", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public Package[] Nitros() { - object[] results = this.Invoke("Nitros", new object[0]); - return ((Package[])(results[0])); - } - - /// - public void NitrosAsync() { - this.NitrosAsync(null); - } - - /// - public void NitrosAsync(object userState) { - if ((this.NitrosOperationCompleted == null)) { - this.NitrosOperationCompleted = new System.Threading.SendOrPostCallback(this.OnNitrosOperationCompleted); - } - this.InvokeAsync("Nitros", new object[0], this.NitrosOperationCompleted, userState); - } - - private void OnNitrosOperationCompleted(object arg) { - if ((this.NitrosCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.NitrosCompleted(this, new NitrosCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/NitrosCategorized", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public Category[] NitrosCategorized() { - object[] results = this.Invoke("NitrosCategorized", new object[0]); - return ((Category[])(results[0])); - } - - /// - public void NitrosCategorizedAsync() { - this.NitrosCategorizedAsync(null); - } - - /// - public void NitrosCategorizedAsync(object userState) { - if ((this.NitrosCategorizedOperationCompleted == null)) { - this.NitrosCategorizedOperationCompleted = new System.Threading.SendOrPostCallback(this.OnNitrosCategorizedOperationCompleted); - } - this.InvokeAsync("NitrosCategorized", new object[0], this.NitrosCategorizedOperationCompleted, userState); - } - - private void OnNitrosCategorizedOperationCompleted(object arg) { - if ((this.NitrosCategorizedCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.NitrosCategorizedCompleted(this, new NitrosCategorizedCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/authenticate", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public string authenticate(string email, string md5Password) { - object[] results = this.Invoke("authenticate", new object[] { - email, - md5Password}); - return ((string)(results[0])); - } - - /// - public void authenticateAsync(string email, string md5Password) { - this.authenticateAsync(email, md5Password, null); - } - - /// - public void authenticateAsync(string email, string md5Password, object userState) { - if ((this.authenticateOperationCompleted == null)) { - this.authenticateOperationCompleted = new System.Threading.SendOrPostCallback(this.OnauthenticateOperationCompleted); - } - this.InvokeAsync("authenticate", new object[] { - email, - md5Password}, this.authenticateOperationCompleted, userState); - } - - private void OnauthenticateOperationCompleted(object arg) { - if ((this.authenticateCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.authenticateCompleted(this, new authenticateCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/fetchPackage", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - [return: System.Xml.Serialization.XmlElementAttribute(DataType="base64Binary")] - public byte[] fetchPackage(string packageGuid) { - object[] results = this.Invoke("fetchPackage", new object[] { - packageGuid}); - return ((byte[])(results[0])); - } - - /// - public void fetchPackageAsync(string packageGuid) { - this.fetchPackageAsync(packageGuid, null); - } - - /// - public void fetchPackageAsync(string packageGuid, object userState) { - if ((this.fetchPackageOperationCompleted == null)) { - this.fetchPackageOperationCompleted = new System.Threading.SendOrPostCallback(this.OnfetchPackageOperationCompleted); - } - this.InvokeAsync("fetchPackage", new object[] { - packageGuid}, this.fetchPackageOperationCompleted, userState); - } - - private void OnfetchPackageOperationCompleted(object arg) { - if ((this.fetchPackageCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.fetchPackageCompleted(this, new fetchPackageCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/fetchPackageByVersion", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - [return: System.Xml.Serialization.XmlElementAttribute(DataType="base64Binary")] - public byte[] fetchPackageByVersion(string packageGuid, string repoVersion) { - object[] results = this.Invoke("fetchPackageByVersion", new object[] { - packageGuid, - repoVersion}); - return ((byte[])(results[0])); - } - - /// - public void fetchPackageByVersionAsync(string packageGuid, string repoVersion) { - this.fetchPackageByVersionAsync(packageGuid, repoVersion, null); - } - - /// - public void fetchPackageByVersionAsync(string packageGuid, string repoVersion, object userState) { - if ((this.fetchPackageByVersionOperationCompleted == null)) { - this.fetchPackageByVersionOperationCompleted = new System.Threading.SendOrPostCallback(this.OnfetchPackageByVersionOperationCompleted); - } - this.InvokeAsync("fetchPackageByVersion", new object[] { - packageGuid, - repoVersion}, this.fetchPackageByVersionOperationCompleted, userState); - } - - private void OnfetchPackageByVersionOperationCompleted(object arg) { - if ((this.fetchPackageByVersionCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.fetchPackageByVersionCompleted(this, new fetchPackageByVersionCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/fetchProtectedPackage", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - [return: System.Xml.Serialization.XmlElementAttribute(DataType="base64Binary")] - public byte[] fetchProtectedPackage(string packageGuid, string memberKey) { - object[] results = this.Invoke("fetchProtectedPackage", new object[] { - packageGuid, - memberKey}); - return ((byte[])(results[0])); - } - - /// - public void fetchProtectedPackageAsync(string packageGuid, string memberKey) { - this.fetchProtectedPackageAsync(packageGuid, memberKey, null); - } - - /// - public void fetchProtectedPackageAsync(string packageGuid, string memberKey, object userState) { - if ((this.fetchProtectedPackageOperationCompleted == null)) { - this.fetchProtectedPackageOperationCompleted = new System.Threading.SendOrPostCallback(this.OnfetchProtectedPackageOperationCompleted); - } - this.InvokeAsync("fetchProtectedPackage", new object[] { - packageGuid, - memberKey}, this.fetchProtectedPackageOperationCompleted, userState); - } - - private void OnfetchProtectedPackageOperationCompleted(object arg) { - if ((this.fetchProtectedPackageCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.fetchProtectedPackageCompleted(this, new fetchProtectedPackageCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/SubmitPackage", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public SubmitStatus SubmitPackage(string repositoryGuid, string authorGuid, string packageGuid, [System.Xml.Serialization.XmlElementAttribute(DataType="base64Binary")] byte[] packageFile, [System.Xml.Serialization.XmlElementAttribute(DataType="base64Binary")] byte[] packageDoc, [System.Xml.Serialization.XmlElementAttribute(DataType="base64Binary")] byte[] packageThumbnail, string name, string author, string authorUrl, string description) { - object[] results = this.Invoke("SubmitPackage", new object[] { - repositoryGuid, - authorGuid, - packageGuid, - packageFile, - packageDoc, - packageThumbnail, - name, - author, - authorUrl, - description}); - return ((SubmitStatus)(results[0])); - } - - /// - public void SubmitPackageAsync(string repositoryGuid, string authorGuid, string packageGuid, byte[] packageFile, byte[] packageDoc, byte[] packageThumbnail, string name, string author, string authorUrl, string description) { - this.SubmitPackageAsync(repositoryGuid, authorGuid, packageGuid, packageFile, packageDoc, packageThumbnail, name, author, authorUrl, description, null); - } - - /// - public void SubmitPackageAsync(string repositoryGuid, string authorGuid, string packageGuid, byte[] packageFile, byte[] packageDoc, byte[] packageThumbnail, string name, string author, string authorUrl, string description, object userState) { - if ((this.SubmitPackageOperationCompleted == null)) { - this.SubmitPackageOperationCompleted = new System.Threading.SendOrPostCallback(this.OnSubmitPackageOperationCompleted); - } - this.InvokeAsync("SubmitPackage", new object[] { - repositoryGuid, - authorGuid, - packageGuid, - packageFile, - packageDoc, - packageThumbnail, - name, - author, - authorUrl, - description}, this.SubmitPackageOperationCompleted, userState); - } - - private void OnSubmitPackageOperationCompleted(object arg) { - if ((this.SubmitPackageCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.SubmitPackageCompleted(this, new SubmitPackageCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://packages.umbraco.org/webservices/PackageByGuid", RequestNamespace="http://packages.umbraco.org/webservices/", ResponseNamespace="http://packages.umbraco.org/webservices/", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)] - public Package PackageByGuid(string packageGuid) { - object[] results = this.Invoke("PackageByGuid", new object[] { - packageGuid}); - return ((Package)(results[0])); - } - - /// - public void PackageByGuidAsync(string packageGuid) { - this.PackageByGuidAsync(packageGuid, null); - } - - /// - public void PackageByGuidAsync(string packageGuid, object userState) { - if ((this.PackageByGuidOperationCompleted == null)) { - this.PackageByGuidOperationCompleted = new System.Threading.SendOrPostCallback(this.OnPackageByGuidOperationCompleted); - } - this.InvokeAsync("PackageByGuid", new object[] { - packageGuid}, this.PackageByGuidOperationCompleted, userState); - } - - private void OnPackageByGuidOperationCompleted(object arg) { - if ((this.PackageByGuidCompleted != null)) { - System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg)); - this.PackageByGuidCompleted(this, new PackageByGuidCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState)); - } - } - - /// - public new void CancelAsync(object userState) { - base.CancelAsync(userState); - } - - private bool IsLocalFileSystemWebService(string url) { - if (((url == null) - || (url == string.Empty))) { - return false; - } - System.Uri wsUri = new System.Uri(url); - if (((wsUri.Port >= 1024) - && (string.Compare(wsUri.Host, "localHost", System.StringComparison.OrdinalIgnoreCase) == 0))) { - return true; - } - return false; - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.7.3062.0")] - [System.SerializableAttribute()] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - [System.Xml.Serialization.XmlTypeAttribute(Namespace="http://packages.umbraco.org/webservices/")] - public partial class Category { - - private string textField; - - private string descriptionField; - - private string urlField; - - private int idField; - - private Package[] packagesField; - - /// - public string Text { - get { - return this.textField; - } - set { - this.textField = value; - } - } - - /// - public string Description { - get { - return this.descriptionField; - } - set { - this.descriptionField = value; - } - } - - /// - public string Url { - get { - return this.urlField; - } - set { - this.urlField = value; - } - } - - /// - public int Id { - get { - return this.idField; - } - set { - this.idField = value; - } - } - - /// - public Package[] Packages { - get { - return this.packagesField; - } - set { - this.packagesField = value; - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.7.3062.0")] - [System.SerializableAttribute()] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - [System.Xml.Serialization.XmlTypeAttribute(Namespace="http://packages.umbraco.org/webservices/")] - public partial class Package { - - private System.Guid repoGuidField; - - private string textField; - - private string descriptionField; - - private string iconField; - - private string thumbnailField; - - private string documentationField; - - private string demoField; - - private bool acceptedField; - - private bool isModuleField; - - private bool editorsPickField; - - private bool protectedField; - - private bool hasUpgradeField; - - private string upgradeVersionField; - - private string upgradeReadMeField; - - private string urlField; - - /// - public System.Guid RepoGuid { - get { - return this.repoGuidField; - } - set { - this.repoGuidField = value; - } - } - - /// - public string Text { - get { - return this.textField; - } - set { - this.textField = value; - } - } - - /// - public string Description { - get { - return this.descriptionField; - } - set { - this.descriptionField = value; - } - } - - /// - public string Icon { - get { - return this.iconField; - } - set { - this.iconField = value; - } - } - - /// - public string Thumbnail { - get { - return this.thumbnailField; - } - set { - this.thumbnailField = value; - } - } - - /// - public string Documentation { - get { - return this.documentationField; - } - set { - this.documentationField = value; - } - } - - /// - public string Demo { - get { - return this.demoField; - } - set { - this.demoField = value; - } - } - - /// - public bool Accepted { - get { - return this.acceptedField; - } - set { - this.acceptedField = value; - } - } - - /// - public bool IsModule { - get { - return this.isModuleField; - } - set { - this.isModuleField = value; - } - } - - /// - public bool EditorsPick { - get { - return this.editorsPickField; - } - set { - this.editorsPickField = value; - } - } - - /// - public bool Protected { - get { - return this.protectedField; - } - set { - this.protectedField = value; - } - } - - /// - public bool HasUpgrade { - get { - return this.hasUpgradeField; - } - set { - this.hasUpgradeField = value; - } - } - - /// - public string UpgradeVersion { - get { - return this.upgradeVersionField; - } - set { - this.upgradeVersionField = value; - } - } - - /// - public string UpgradeReadMe { - get { - return this.upgradeReadMeField; - } - set { - this.upgradeReadMeField = value; - } - } - - /// - public string Url { - get { - return this.urlField; - } - set { - this.urlField = value; - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Xml", "4.7.3062.0")] - [System.SerializableAttribute()] - [System.Xml.Serialization.XmlTypeAttribute(Namespace="http://packages.umbraco.org/webservices/")] - public enum SubmitStatus { - - /// - Complete, - - /// - Exists, - - /// - NoAccess, - - /// - Error, - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void CategoriesCompletedEventHandler(object sender, CategoriesCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class CategoriesCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal CategoriesCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public Category[] Result { - get { - this.RaiseExceptionIfNecessary(); - return ((Category[])(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void ModulesCompletedEventHandler(object sender, ModulesCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class ModulesCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal ModulesCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public Package[] Result { - get { - this.RaiseExceptionIfNecessary(); - return ((Package[])(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void ModulesCategorizedCompletedEventHandler(object sender, ModulesCategorizedCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class ModulesCategorizedCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal ModulesCategorizedCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public Category[] Result { - get { - this.RaiseExceptionIfNecessary(); - return ((Category[])(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void NitrosCompletedEventHandler(object sender, NitrosCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class NitrosCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal NitrosCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public Package[] Result { - get { - this.RaiseExceptionIfNecessary(); - return ((Package[])(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void NitrosCategorizedCompletedEventHandler(object sender, NitrosCategorizedCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class NitrosCategorizedCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal NitrosCategorizedCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public Category[] Result { - get { - this.RaiseExceptionIfNecessary(); - return ((Category[])(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void authenticateCompletedEventHandler(object sender, authenticateCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class authenticateCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal authenticateCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public string Result { - get { - this.RaiseExceptionIfNecessary(); - return ((string)(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void fetchPackageCompletedEventHandler(object sender, fetchPackageCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class fetchPackageCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal fetchPackageCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public byte[] Result { - get { - this.RaiseExceptionIfNecessary(); - return ((byte[])(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void fetchPackageByVersionCompletedEventHandler(object sender, fetchPackageByVersionCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class fetchPackageByVersionCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal fetchPackageByVersionCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public byte[] Result { - get { - this.RaiseExceptionIfNecessary(); - return ((byte[])(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void fetchProtectedPackageCompletedEventHandler(object sender, fetchProtectedPackageCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class fetchProtectedPackageCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal fetchProtectedPackageCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public byte[] Result { - get { - this.RaiseExceptionIfNecessary(); - return ((byte[])(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void SubmitPackageCompletedEventHandler(object sender, SubmitPackageCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class SubmitPackageCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal SubmitPackageCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public SubmitStatus Result { - get { - this.RaiseExceptionIfNecessary(); - return ((SubmitStatus)(this.results[0])); - } - } - } - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - public delegate void PackageByGuidCompletedEventHandler(object sender, PackageByGuidCompletedEventArgs e); - - /// - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "4.7.3062.0")] - [System.Diagnostics.DebuggerStepThroughAttribute()] - [System.ComponentModel.DesignerCategoryAttribute("code")] - public partial class PackageByGuidCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs { - - private object[] results; - - internal PackageByGuidCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState) : - base(exception, cancelled, userState) { - this.results = results; - } - - /// - public Package Result { - get { - this.RaiseExceptionIfNecessary(); - return ((Package)(this.results[0])); - } - } - } -} - -#pragma warning restore 1591 \ No newline at end of file diff --git a/src/Umbraco.Web/Web References/org.umbraco.our/Reference.map b/src/Umbraco.Web/Web References/org.umbraco.our/Reference.map deleted file mode 100644 index 8d133063a5..0000000000 --- a/src/Umbraco.Web/Web References/org.umbraco.our/Reference.map +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/src/Umbraco.Web/Web References/org.umbraco.our/repository.disco b/src/Umbraco.Web/Web References/org.umbraco.our/repository.disco deleted file mode 100644 index 4bf1a61fca..0000000000 --- a/src/Umbraco.Web/Web References/org.umbraco.our/repository.disco +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/Umbraco.Web/Web References/org.umbraco.our/repository.wsdl b/src/Umbraco.Web/Web References/org.umbraco.our/repository.wsdl deleted file mode 100644 index 2a3f3502db..0000000000 --- a/src/Umbraco.Web/Web References/org.umbraco.our/repository.wsdl +++ /dev/null @@ -1,995 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From ca75a258029bf076ed1c00bbeb7e7456da2f10cd Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Wed, 24 Jul 2019 11:32:18 +0200 Subject: [PATCH 64/67] Makes checks a little more robust --- .../PropertyEditors/TagsPropertyEditor.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs index b7101aa764..578b6fcd00 100644 --- a/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs @@ -39,14 +39,23 @@ namespace Umbraco.Web.PropertyEditors /// public override object FromEditor(ContentPropertyData editorValue, object currentValue) { + var value = editorValue?.Value?.ToString(); + + if (string.IsNullOrEmpty(value)) + { + return null; + } + if (editorValue.Value is JArray json) { return json.Select(x => x.Value()); } - else if ( editorValue.Value is string stringValue && !String.IsNullOrWhiteSpace(stringValue)) + + if (string.IsNullOrWhiteSpace(value) == false) { - return stringValue.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); + return value.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries); } + return null; } From 7bc7dba86d4d7bd3d45f8b73c53006cc7f3ef61f Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 24 Jul 2019 21:46:19 +1000 Subject: [PATCH 65/67] fixes timing issue --- .../Web/PublishedContentQueryTests.cs | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/Umbraco.Tests/Web/PublishedContentQueryTests.cs b/src/Umbraco.Tests/Web/PublishedContentQueryTests.cs index 2cff946372..a3505aeb0e 100644 --- a/src/Umbraco.Tests/Web/PublishedContentQueryTests.cs +++ b/src/Umbraco.Tests/Web/PublishedContentQueryTests.cs @@ -36,22 +36,26 @@ namespace Umbraco.Tests.Web { var indexer = new TestIndex("TestIndex", luceneDirectory, fieldNames); - //populate with some test data - indexer.IndexItem(new ValueSet("1", "content", new Dictionary + using (indexer.ProcessNonAsync()) { - [fieldNames[0]] = "Hello world, there are products here", - [UmbracoContentIndex.VariesByCultureFieldName] = "n" - })); - indexer.IndexItem(new ValueSet("2", "content", new Dictionary - { - [fieldNames[1]] = "Hello world, there are products here", - [UmbracoContentIndex.VariesByCultureFieldName] = "y" - })); - indexer.IndexItem(new ValueSet("3", "content", new Dictionary - { - [fieldNames[2]] = "Hello world, there are products here", - [UmbracoContentIndex.VariesByCultureFieldName] = "y" - })); + //populate with some test data + indexer.IndexItem(new ValueSet("1", "content", new Dictionary + { + [fieldNames[0]] = "Hello world, there are products here", + [UmbracoContentIndex.VariesByCultureFieldName] = "n" + })); + indexer.IndexItem(new ValueSet("2", "content", new Dictionary + { + [fieldNames[1]] = "Hello world, there are products here", + [UmbracoContentIndex.VariesByCultureFieldName] = "y" + })); + indexer.IndexItem(new ValueSet("3", "content", new Dictionary + { + [fieldNames[2]] = "Hello world, there are products here", + [UmbracoContentIndex.VariesByCultureFieldName] = "y" + })); + } + return indexer; } @@ -80,7 +84,7 @@ namespace Umbraco.Tests.Web { var fieldNames = new[] { "title", "title_en-us", "title_fr-fr" }; using (var indexer = CreateTestIndex(luceneDir, fieldNames)) - { + { var pcq = CreatePublishedContentQuery(indexer); var results = pcq.Search("Products", culture, "TestIndex"); From 4a24064783efb45f7eed1db9833ca1f8b9012f3d Mon Sep 17 00:00:00 2001 From: arkadiuszbiel Date: Sat, 27 Jul 2019 20:41:15 +0100 Subject: [PATCH 66/67] 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 49b3351c72cf5e53f67f583d996d60b88ca52c22 Mon Sep 17 00:00:00 2001 From: Rasmus John Pedersen Date: Mon, 29 Jul 2019 12:59:29 +0200 Subject: [PATCH 67/67] Fix exception when stored media url is already absolute --- src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs | 13 +++++++++++++ src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs | 4 ++++ 2 files changed, 17 insertions(+) diff --git a/src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs b/src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs index 5af48e64b1..6489417dc7 100644 --- a/src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs +++ b/src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs @@ -81,6 +81,19 @@ namespace Umbraco.Tests.Routing Assert.AreEqual(expected, resolvedUrl); } + [Test] + public void Get_Media_Url_Returns_Absolute_Url_If_Stored_Url_Is_Absolute() + { + const string expected = "http://localhost/media/rfeiw584/test.jpg"; + + var umbracoContext = GetUmbracoContext("http://localhost", mediaUrlProviders: new[] { _mediaUrlProvider }); + var publishedContent = CreatePublishedContent(Constants.PropertyEditors.Aliases.UploadField, expected, null); + + var resolvedUrl = umbracoContext.UrlProvider.GetMediaUrl(publishedContent, UrlMode.Relative); + + Assert.AreEqual(expected, resolvedUrl); + } + [Test] public void Get_Media_Url_Returns_Empty_String_When_PropertyType_Is_Not_Supported() { diff --git a/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs b/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs index 18d10e577d..02dc4ebf29 100644 --- a/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs +++ b/src/Umbraco.Web/Routing/DefaultMediaUrlProvider.cs @@ -45,6 +45,10 @@ namespace Umbraco.Web.Routing if (string.IsNullOrEmpty(path)) return null; + // the stored path is absolute so we just return it as is + if(Uri.IsWellFormedUriString(path, UriKind.Absolute)) + return new Uri(path); + Uri uri; if (current == null)