diff --git a/.editorconfig b/.editorconfig index 80eabec233..29e21d01ed 100644 --- a/.editorconfig +++ b/.editorconfig @@ -27,4 +27,10 @@ dotnet_naming_symbols.private_fields.applicable_kinds = field dotnet_naming_symbols.private_fields.applicable_accessibilities = private dotnet_naming_style.prefix_underscore.capitalization = camel_case -dotnet_naming_style.prefix_underscore.required_prefix = _ \ No newline at end of file +dotnet_naming_style.prefix_underscore.required_prefix = _ + +# https://github.com/MicrosoftDocs/visualstudio-docs/blob/master/docs/ide/editorconfig-code-style-settings-reference.md +[*.cs] +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = true:suggestion \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs index 0ec5f9353b..e15560264a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs @@ -150,6 +150,8 @@ UNION SELECT '4CountOfLockedOut' AS colName, COUNT(id) AS num FROM umbracoUser WHERE userNoConsole = 1 UNION SELECT '5CountOfInvited' AS colName, COUNT(id) AS num FROM umbracoUser WHERE lastLoginDate IS NULL AND userDisabled = 1 AND invitedDate IS NOT NULL +UNION +SELECT '6CountOfDisabled' AS colName, COUNT(id) AS num FROM umbracoUser WHERE userDisabled = 0 AND userNoConsole = 0 AND lastLoginDate IS NULL ORDER BY colName"; var result = Database.Fetch(sql); @@ -160,7 +162,8 @@ ORDER BY colName"; {UserState.Active, (int) result[1].num}, {UserState.Disabled, (int) result[2].num}, {UserState.LockedOut, (int) result[3].num}, - {UserState.Invited, (int) result[4].num} + {UserState.Invited, (int) result[4].num}, + {UserState.Inactive, (int) result[5].num} }; } @@ -766,6 +769,12 @@ ORDER BY colName"; sb.Append("(userDisabled = 0 AND userNoConsole = 0 AND lastLoginDate IS NOT NULL)"); appended = true; } + if (userState.Contains(UserState.Inactive)) + { + if (appended) sb.Append(" OR "); + sb.Append("(userDisabled = 0 AND userNoConsole = 0 AND lastLoginDate IS NULL)"); + appended = true; + } if (userState.Contains(UserState.Disabled)) { if (appended) sb.Append(" OR "); diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValue.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValue.cs index 38a8808856..a6ec9af01d 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValue.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValue.cs @@ -138,7 +138,7 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters // fixme MOVE TO MODELS O /// /// public bool HasFocalPoint() - => FocalPoint != null && FocalPoint.Left != 0.5m && FocalPoint.Top != 0.5m; + => FocalPoint != null && (FocalPoint.Left != 0.5m || FocalPoint.Top != 0.5m); /// /// Determines whether the value has a specified crop. diff --git a/src/Umbraco.Core/Services/OperationResult.cs b/src/Umbraco.Core/Services/OperationResult.cs index d22fd0147f..e901b58119 100644 --- a/src/Umbraco.Core/Services/OperationResult.cs +++ b/src/Umbraco.Core/Services/OperationResult.cs @@ -11,6 +11,10 @@ namespace Umbraco.Core.Services /// Represents the result of a service operation. /// /// The type of the result type. + /// Type must be an enumeration, and its + /// underlying type must be byte. Values indicating success should be in the 0-127 + /// range, while values indicating failure should be in the 128-255 range. See + /// for a base implementation. public class OperationResult where TResultType : struct { @@ -56,6 +60,10 @@ namespace Umbraco.Core.Services /// /// The type of the result type. /// The type of the entity. + /// Type must be an enumeration, and its + /// underlying type must be byte. Values indicating success should be in the 0-127 + /// range, while values indicating failure should be in the 128-255 range. See + /// for a base implementation. public class OperationResult : OperationResult where TResultType : struct { @@ -111,7 +119,8 @@ namespace Umbraco.Core.Services return new OperationResult(OperationResultType.FailedCancelledByEvent, eventMessages); } - // fixme wtf? + // fixme - this exists to support services that still return Attempt + // these services should directly return an OperationResult, and then this static class should be deleted internal static class Attempt { /// diff --git a/src/Umbraco.Tests/Web/Mvc/HtmlStringUtilitiesTests.cs b/src/Umbraco.Tests/Web/Mvc/HtmlStringUtilitiesTests.cs index 102a310806..04bd4f6e15 100644 --- a/src/Umbraco.Tests/Web/Mvc/HtmlStringUtilitiesTests.cs +++ b/src/Umbraco.Tests/Web/Mvc/HtmlStringUtilitiesTests.cs @@ -18,7 +18,7 @@ namespace Umbraco.Tests.Web.Mvc [Test] public void ReplaceLineBreaksWithHtmlBreak() { - var output = _htmlStringUtilities.ReplaceLineBreaksForHtml("

hello world

hello world\r\nhello world\rhello world\nhello world

"); + var output = _htmlStringUtilities.ReplaceLineBreaksForHtml("

hello world

hello world\r\nhello world\rhello world\nhello world

").ToString(); var expected = "

hello world

hello world
hello world
hello world
hello world

"; Assert.AreEqual(expected, output); } @@ -58,4 +58,4 @@ namespace Umbraco.Tests.Web.Mvc } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js index 36640eae75..81dfcfd5d3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js @@ -4,7 +4,7 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", var vm = this; var dialogOptions = $scope.model; - var anchorPattern = //gi; + var searchText = "Search..."; vm.submit = submit; @@ -60,8 +60,14 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", }); } else if ($scope.model.target.url.length) { // a url but no id/udi indicates an external link - trim the url to remove the anchor/qs - $scope.model.target.url = $scope.model.target.url.substring(0, $scope.model.target.url.search(/(#|\?)/)); - } + // only do the substring if there's a # or a ? + var indexOfAnchor = $scope.model.target.url.search(/(#|\?)/); + if (indexOfAnchor > -1) { + // populate the anchor + $scope.model.target.anchor = $scope.model.target.url.substring(indexOfAnchor); + // then rewrite the model and populate the link + $scope.model.target.url = $scope.model.target.url.substring(0, indexOfAnchor); + } } } else if (dialogOptions.anchors) { $scope.anchorValues = dialogOptions.anchors; } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html index dd258f6c75..a50ab4242d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html @@ -23,7 +23,7 @@ placeholder="@general_url" class="umb-property-editor umb-textstring" ng-model="model.target.url" - ng-disabled="model.target.id" /> + ng-disabled="model.target.id || model.target.udi" /> diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html index 5a858accbb..22db58e7de 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html @@ -37,7 +37,8 @@ type="button" label-key="general_upload" action="upload()" - disabled="lockedFolder"> + disabled="lockedFolder" + button-style="info"> diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.html index 7270abbc7c..7270676b1e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.html @@ -74,7 +74,8 @@ entity-type="{{vm.entityType}}" start-node-id="vm.startNodeId" on-select="vm.selectListViewNode(node)" - on-close="vm.closeMiniListView()"> + on-close="vm.closeMiniListView()" + entity-type-filter="filter"> diff --git a/src/Umbraco.Web/Components/DatabaseServerRegistrarAndMessengerComponent.cs b/src/Umbraco.Web/Components/DatabaseServerRegistrarAndMessengerComponent.cs index ea7f3364af..02dede1da9 100644 --- a/src/Umbraco.Web/Components/DatabaseServerRegistrarAndMessengerComponent.cs +++ b/src/Umbraco.Web/Components/DatabaseServerRegistrarAndMessengerComponent.cs @@ -160,7 +160,8 @@ namespace Umbraco.Web.Components var task = new InstructionProcessTask(_processTaskRunner, 60000, //delay before first execution _messenger.Options.ThrottleSeconds*1000, //amount of ms between executions - _messenger); + _messenger, + _logger); _processTaskRunner.TryAdd(task); return task; } @@ -178,12 +179,14 @@ namespace Umbraco.Web.Components private class InstructionProcessTask : RecurringTaskBase { private readonly DatabaseServerMessenger _messenger; + private readonly ILogger _logger; public InstructionProcessTask(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, - DatabaseServerMessenger messenger) + DatabaseServerMessenger messenger, ILogger logger) : base(runner, delayMilliseconds, periodMilliseconds) { _messenger = messenger; + _logger = logger; } public override bool IsAsync => false; @@ -194,8 +197,14 @@ namespace Umbraco.Web.Components /// A value indicating whether to repeat the task. public override bool PerformRun() { - // TODO what happens in case of an exception? - _messenger.Sync(); + try + { + _messenger.Sync(); + } + catch (Exception e) + { + _logger.Error("Failed (will repeat).", e); + } return true; // repeat } } diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index 835920f0f1..9bb80cc2b3 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -328,10 +328,8 @@ namespace Umbraco.Web.Editors var tryCreateTemplate = Services.FileService.CreateTemplateForContentType(contentTypeAlias, contentTypeName); if (tryCreateTemplate == false) { - Logger.Warn( - "Could not create a template for the Content Type: {0}, status: {1}", - () => contentTypeAlias, - () => tryCreateTemplate.Result.StatusType); + Logger.Warn("Could not create a template for Content Type: \"{ContentTypeAlias}\", status: {Status}", + contentTypeAlias, tryCreateTemplate.Result.Result); } template = tryCreateTemplate.Result.Entity; diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/FlexibleDropdownPropertyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/FlexibleDropdownPropertyValueConverter.cs index 5fa537f561..521817a7de 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/FlexibleDropdownPropertyValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/FlexibleDropdownPropertyValueConverter.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using Umbraco.Core; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PropertyEditors; @@ -23,29 +22,27 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { if (inter == null) - { return null; - } - var selectedValues = (string[])inter; - if (selectedValues.Any()) + var multiple = propertyType.DataType.ConfigurationAs().Multiple; + var selectedValues = (string[]) inter; + if (selectedValues.Length > 0) { - if (propertyType.DataType.ConfigurationAs().Multiple) - { - return selectedValues; - } - - return selectedValues.First(); + return multiple + ? (object) selectedValues + : selectedValues[0]; } - return inter; + return multiple + ? inter + : string.Empty; } public override Type GetPropertyValueType(PublishedPropertyType propertyType) { return propertyType.DataType.ConfigurationAs().Multiple - ? typeof(IEnumerable) - : typeof(string); - } + ? typeof(IEnumerable) + : typeof(string); + } } } diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/RelatedLinksValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/RelatedLinksValueConverter.cs index b3b3919278..983d122a83 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/RelatedLinksValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/RelatedLinksValueConverter.cs @@ -77,7 +77,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters { var strLinkId = linkData.Link; var udiAttempt = strLinkId.TryConvertTo(); - if (udiAttempt.Success) + if (udiAttempt.Success && udiAttempt.Result != null) { var content = _publishedSnapshotAccessor.PublishedSnapshot.Content.GetById(udiAttempt.Result.Guid); if (content != null) diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 45782f872d..dc70e7c348 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -93,7 +93,7 @@ namespace Umbraco.Web if (content.TemplateId != templateId && UmbracoConfig.For.UmbracoSettings().WebRouting.ValidateAlternativeTemplates == true) { // fixme - perfs? nothing cached here - var publishedContentContentType = ApplicationContext.Current.Services.ContentTypeService.GetContentType(content.ContentType.Id); + var publishedContentContentType = Current.Services.ContentTypeService.Get(content.ContentType.Id); if (publishedContentContentType == null) throw new NullReferenceException("No content type returned for published content (contentType='" + content.ContentType.Id + "')"); @@ -105,8 +105,8 @@ namespace Umbraco.Web public static bool IsAllowedTemplate(this IPublishedContent content, string templateAlias) { // fixme - perfs? nothing cached here - var template = ApplicationContext.Current.Services.FileService.GetTemplate(templateAlias); - return = template == null ? false : content.IsAllowedTemplate(template.Id); + var template = Current.Services.FileService.GetTemplate(templateAlias); + return template != null && content.IsAllowedTemplate(template.Id); } #endregion diff --git a/src/Umbraco.Web/Routing/ContentFinderByUrlAndTemplate.cs b/src/Umbraco.Web/Routing/ContentFinderByUrlAndTemplate.cs index 1d1661966f..1e86b40a79 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByUrlAndTemplate.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByUrlAndTemplate.cs @@ -40,34 +40,49 @@ namespace Umbraco.Web.Routing if (frequest.HasDomain) path = DomainHelper.PathRelativeToDomain(frequest.Domain.Uri, path); - if (path != "/") // no template if "/" - { - var pos = path.LastIndexOf('/'); - var templateAlias = path.Substring(pos + 1); - path = pos == 0 ? "/" : path.Substring(0, pos); - - var template = _fileService.GetTemplate(templateAlias); - if (template != null) - { - Logger.Debug("Valid template: '{TemplateAlias}'", templateAlias); - - var route = frequest.HasDomain ? (frequest.Domain.ContentId.ToString() + path) : path; - node = FindContent(frequest, route); - - if (UmbracoConfig.For.UmbracoSettings().WebRouting.DisableAlternativeTemplates == false && node != null) - frequest.TemplateModel = template; - } - else - { - Logger.Debug("Not a valid template: '{TemplateAlias}'", templateAlias); - } - } - else + // no template if "/" + if (path == "/") { Logger.Debug("No template in path '/'"); + return false; } - return node != null; + // look for template in last position + var pos = path.LastIndexOf('/'); + var templateAlias = path.Substring(pos + 1); + path = pos == 0 ? "/" : path.Substring(0, pos); + + var template = _fileService.GetTemplate(templateAlias); + + if (template == null) + { + Logger.Debug("Not a valid template: '{TemplateAlias}'", templateAlias); + return false; + } + + Logger.Debug("Valid template: '{TemplateAlias}'", templateAlias); + + // look for node corresponding to the rest of the route + var route = frequest.HasDomain ? (frequest.Domain.ContentId + path) : path; + node = FindContent(frequest, route); // also assigns to published request + + if (node == null) + { + Logger.Debug("Not a valid route to node: '{Route}'", route); + return false; + } + + // IsAllowedTemplate deals both with DisableAlternativeTemplates and ValidateAlternativeTemplates settings + if (!node.IsAllowedTemplate(template.Id)) + { + Logger.Warn("Alternative template '{TemplateAlias}' is not allowed on node {NodeId}.", template.Alias, node.Id); + frequest.PublishedContent = null; // clear + return false; + } + + // got it + frequest.TemplateModel = template; + return true; } } } diff --git a/src/Umbraco.Web/Routing/PublishedRouter.cs b/src/Umbraco.Web/Routing/PublishedRouter.cs index a728798253..f23473e369 100644 --- a/src/Umbraco.Web/Routing/PublishedRouter.cs +++ b/src/Umbraco.Web/Routing/PublishedRouter.cs @@ -42,7 +42,7 @@ namespace Umbraco.Web.Routing ProfilingLogger proflog, Func> getRolesForLogin = null) { - _webRoutingSection = webRoutingSection ?? throw new ArgumentNullException(nameof(webRoutingSection)); // fixme usage? + _webRoutingSection = webRoutingSection ?? throw new ArgumentNullException(nameof(webRoutingSection)); _contentFinders = contentFinders ?? throw new ArgumentNullException(nameof(contentFinders)); _contentLastChanceFinder = contentLastChanceFinder ?? throw new ArgumentNullException(nameof(contentLastChanceFinder)); _services = services ?? throw new ArgumentNullException(nameof(services)); @@ -678,9 +678,8 @@ namespace Umbraco.Web.Routing // only if the published content is the initial once, else the alternate template // does not apply // + optionnally, apply the alternate template on internal redirects - var useAltTemplate = _webRoutingSection.DisableAlternativeTemplates == false - && (request.IsInitialPublishedContent - || (_webRoutingSection.InternalRedirectPreservesTemplate && request.IsInternalRedirectPublishedContent)); + var useAltTemplate = request.IsInitialPublishedContent + || (_webRoutingSection.InternalRedirectPreservesTemplate && request.IsInternalRedirectPublishedContent); var altTemplate = useAltTemplate ? request.UmbracoContext.HttpContext.Request[Constants.Conventions.Url.AltTemplate] : null; @@ -693,28 +692,15 @@ namespace Umbraco.Web.Routing if (request.HasTemplate) { - _logger.Debug("{0}Has a template already, and no alternate template."); + _logger.Debug("FindTemplate: Has a template already, and no alternate template."); return; } - // TODO: When we remove the need for a database for templates, then this id should be irrelavent, + // TODO: When we remove the need for a database for templates, then this id should be irrelevant, // not sure how were going to do this nicely. var templateId = request.PublishedContent.TemplateId; - - if (templateId > 0) - { - _logger.Debug("FindTemplate: Look for template id={TemplateId}", templateId); - var template = _services.FileService.GetTemplate(templateId); - if (template == null) - throw new InvalidOperationException("The template with Id " + templateId + " does not exist, the page cannot render"); - request.TemplateModel = template; - _logger.Debug("FindTemplate: Got template id={TemplateId} alias='{TemplateAlias}'", template.Id, template.Alias); - } - else - { - _logger.Debug("FindTemplate: No specified template."); - } + request.TemplateModel = GetTemplateModel(templateId); } else { @@ -725,18 +711,32 @@ namespace Umbraco.Web.Routing // ignore if the alias does not match - just trace if (request.HasTemplate) - _logger.Debug("FindTemplate: Has a template already, but also an alternate template."); - _logger.Debug("FindTemplate: Look for alternate template alias='{AltTemplate}'", altTemplate); + _logger.Debug("FindTemplate: Has a template already, but also an alternative template."); + _logger.Debug("FindTemplate: Look for alternative template alias='{AltTemplate}'", altTemplate); - var template = _services.FileService.GetTemplate(altTemplate); - if (template != null) + // IsAllowedTemplate deals both with DisableAlternativeTemplates and ValidateAlternativeTemplates settings + if (request.PublishedContent.IsAllowedTemplate(altTemplate)) { - request.TemplateModel = template; - _logger.Debug("FindTemplate: Got template id={TemplateId} alias='{TemplateAlias}'", template.Id, template.Alias); + // allowed, use + var template = _services.FileService.GetTemplate(altTemplate); + + if (template != null) + { + request.TemplateModel = template; + _logger.Debug("FindTemplate: Got alternative template id={TemplateId} alias='{TemplateAlias}'", template.Id, template.Alias); + } + else + { + _logger.Debug("FindTemplate: The alternative template with alias='{AltTemplate}' does not exist, ignoring.", altTemplate); + } } else { - _logger.Debug("FindTemplate: The template with alias='{AltTemplate}' does not exist, ignoring.", altTemplate); + _logger.Warn("FindTemplate: Alternative template '{TemplateAlias}' is not allowed on node {NodeId}, ignoring.", altTemplate, request.PublishedContent.Id); + + // no allowed, back to default + var templateId = request.PublishedContent.TemplateId; + request.TemplateModel = GetTemplateModel(templateId); } } @@ -759,6 +759,23 @@ namespace Umbraco.Web.Routing } } + private ITemplate GetTemplateModel(int templateId) + { + if (templateId <= 0) + { + _logger.Debug("GetTemplateModel: No template."); + return null; + } + + _logger.Debug("GetTemplateModel: Get template id={TemplateId}", templateId); + + var template = _services.FileService.GetTemplate(templateId); + if (template == null) + throw new InvalidOperationException("The template with Id " + templateId + " does not exist, the page cannot render."); + _logger.Debug("GetTemplateModel: Got template id={TemplateId} alias=\"{TemplateAlias}\"", template.Id, template.Alias); + return template; + } + /// /// Follows external redirection through umbracoRedirect document property. /// diff --git a/src/Umbraco.Web/Trees/ApplicationTreeController.cs b/src/Umbraco.Web/Trees/ApplicationTreeController.cs index 5f1480a192..d489421353 100644 --- a/src/Umbraco.Web/Trees/ApplicationTreeController.cs +++ b/src/Umbraco.Web/Trees/ApplicationTreeController.cs @@ -5,6 +5,7 @@ using System.Net; using System.Net.Http.Formatting; using System.Threading.Tasks; using System.Web.Http; +using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Web.Models.Trees;