diff --git a/build/NuSpecs/UmbracoCms.Core.nuspec b/build/NuSpecs/UmbracoCms.Core.nuspec index 4aa617a170..3f2aafb259 100644 --- a/build/NuSpecs/UmbracoCms.Core.nuspec +++ b/build/NuSpecs/UmbracoCms.Core.nuspec @@ -33,7 +33,7 @@ - + diff --git a/build/build.ps1 b/build/build.ps1 index bd487c17d1..ae59874e6a 100644 --- a/build/build.ps1 +++ b/build/build.ps1 @@ -355,11 +355,12 @@ Write-Host "Copy template files" $this.CopyFiles("$templates", "*", "$tmp\Templates") - Write-Host "Copy program.cs and startup.cs for templates" + Write-Host "Copy files for dotnet templates" $this.CopyFiles("$src\Umbraco.Web.UI.NetCore", "Program.cs", "$tmp\Templates\UmbracoSolution") $this.CopyFiles("$src\Umbraco.Web.UI.NetCore", "Startup.cs", "$tmp\Templates\UmbracoSolution") $this.CopyFiles("$src\Umbraco.Web.UI.NetCore", "appsettings.json", "$tmp\Templates\UmbracoSolution") $this.CopyFiles("$src\Umbraco.Web.UI.NetCore", "appsettings.Development.json", "$tmp\Templates\UmbracoSolution") + $this.CopyFiles("$src\Umbraco.Web.UI.NetCore\Views", "*", "$tmp\Templates\UmbracoSolution\Views") $this.RemoveDirectory("$tmp\Templates\UmbracoSolution\bin") }) diff --git a/build/templates/UmbracoSolution/UmbracoSolution.csproj b/build/templates/UmbracoSolution/UmbracoSolution.csproj index 33a5d5bf79..87271c3dd7 100644 --- a/build/templates/UmbracoSolution/UmbracoSolution.csproj +++ b/build/templates/UmbracoSolution/UmbracoSolution.csproj @@ -1,5 +1,4 @@ - net5.0 @@ -10,4 +9,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + false + diff --git a/build/templates/UmbracoSolution/Views/_ViewImports.cshtml b/build/templates/UmbracoSolution/Views/_ViewImports.cshtml new file mode 100644 index 0000000000..3aaf133335 --- /dev/null +++ b/build/templates/UmbracoSolution/Views/_ViewImports.cshtml @@ -0,0 +1,4 @@ +@using Umbraco.Web.UI.NetCore +@using Umbraco.Extensions +@using Umbraco.Web.PublishedModels +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/src/Umbraco.Core/Configuration/HealthChecks/CompilationDebugCheck.cs b/src/Umbraco.Core/Configuration/HealthChecks/CompilationDebugCheck.cs deleted file mode 100644 index a7b99ea205..0000000000 --- a/src/Umbraco.Core/Configuration/HealthChecks/CompilationDebugCheck.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Collections.Generic; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Umbraco.Core.Configuration.Models; -using Umbraco.Core.HealthCheck; -using Umbraco.Core.HealthCheck.Checks; -using Umbraco.Core.Services; - -namespace Umbraco.Core.Configuration.HealthChecks -{ - [HealthCheck("61214FF3-FC57-4B31-B5CF-1D095C977D6D", "Debug Compilation Mode", - Description = "Leaving debug compilation mode enabled can severely slow down a website and take up more memory on the server.", - Group = "Live Environment")] - public class CompilationDebugCheck : AbstractSettingsCheck - { - private readonly IOptionsMonitor _hostingSettings; - - public CompilationDebugCheck(ILocalizedTextService textService, ILoggerFactory loggerFactory, IOptionsMonitor hostingSettings) - : base(textService, loggerFactory) - { - _hostingSettings = hostingSettings; - } - - public override string ItemPath => Constants.Configuration.ConfigHostingDebug; - - public override ValueComparisonType ValueComparisonType => ValueComparisonType.ShouldEqual; - - public override IEnumerable Values => new List - { - new AcceptableConfiguration - { - IsRecommended = true, - Value = bool.FalseString.ToLower() - } - }; - - public override string CurrentValue => _hostingSettings.CurrentValue.Debug.ToString(); - - public override string CheckSuccessMessage => TextService.Localize("healthcheck/compilationDebugCheckSuccessMessage"); - - public override string CheckErrorMessage => TextService.Localize("healthcheck/compilationDebugCheckErrorMessage"); - - public override string RectifySuccessMessage => TextService.Localize("healthcheck/compilationDebugCheckRectifySuccessMessage"); - } -} diff --git a/src/Umbraco.Core/Configuration/HealthChecks/ConfigurationService.cs b/src/Umbraco.Core/Configuration/HealthChecks/ConfigurationService.cs deleted file mode 100644 index 2459698b7a..0000000000 --- a/src/Umbraco.Core/Configuration/HealthChecks/ConfigurationService.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using Microsoft.Extensions.Logging; -using Umbraco.Core.HealthCheck; -using Umbraco.Core.Services; - -namespace Umbraco.Core.Configuration.HealthChecks -{ - public class ConfigurationService : IConfigurationService - { - private readonly ILocalizedTextService _textService; - private readonly ILogger _logger; - private readonly IConfigManipulator _configManipulator; - - /// - /// - /// - /// - public ConfigurationService(ILocalizedTextService textService, ILogger logger, IConfigManipulator configManipulator) - { - if (textService == null) HandleNullParameter(nameof(textService)); - if (configManipulator == null) HandleNullParameter(nameof(configManipulator)); - if (logger == null) HandleNullParameter(nameof(logger)); - - _configManipulator = configManipulator; - _textService = textService; - _logger = logger; - } - - private void HandleNullParameter(string parameter) - { - _logger.LogError("Error trying to get configuration value", parameter); - throw new ArgumentNullException(parameter); - } - - /// - /// Updates a value in a given configuration file with the given path - /// - /// - /// - /// - public ConfigurationServiceResult UpdateConfigFile(string value, string itemPath) - { - try - { - if (itemPath == null) - { - return new ConfigurationServiceResult - { - Success = false, - Result = _textService.Localize("healthcheck/configurationServiceNodeNotFound", new[] { itemPath, value }) - }; - } - - _configManipulator.SaveConfigValue(itemPath, value); - return new ConfigurationServiceResult - { - Success = true - }; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error trying to update configuration"); - return new ConfigurationServiceResult - { - Success = false, - Result = _textService.Localize("healthcheck/configurationServiceError", new[] { ex.Message }) - }; - } - } - } -} diff --git a/src/Umbraco.Core/Configuration/HealthChecks/TrySkipIisCustomErrorsCheck.cs b/src/Umbraco.Core/Configuration/HealthChecks/TrySkipIisCustomErrorsCheck.cs deleted file mode 100644 index 9710080f35..0000000000 --- a/src/Umbraco.Core/Configuration/HealthChecks/TrySkipIisCustomErrorsCheck.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Umbraco.Core.Configuration.Models; -using Umbraco.Core.Hosting; -using Umbraco.Core.Services; - -namespace Umbraco.Core.HealthCheck.Checks.Configuration -{ - [Obsolete("This is not currently in the appsettings.JSON and so can either be removed, or rewritten in .NET Core fashion")] - [HealthCheck("046A066C-4FB2-4937-B931-069964E16C66", "Try Skip IIS Custom Errors", - Description = "Starting with IIS 7.5, this must be set to true for Umbraco 404 pages to show. Otherwise, IIS will takeover and render its built-in error page.", - Group = "Configuration")] - public class TrySkipIisCustomErrorsCheck : AbstractSettingsCheck - { - private readonly ILocalizedTextService _textService; - private readonly ILoggerFactory _loggerFactory; - private readonly Version _iisVersion; - private readonly GlobalSettings _globalSettings; - - public TrySkipIisCustomErrorsCheck(ILocalizedTextService textService, ILoggerFactory loggerFactory, IOptions globalSettings) - : base(textService, loggerFactory) - { - _textService = textService; - _loggerFactory = loggerFactory; - //TODO: detect if hosted in IIS, and then IIS version if we want to go this route - _iisVersion = new Version("7.5"); - _globalSettings = globalSettings.Value; - } - - public override string ItemPath => "TBC"; - - public override ValueComparisonType ValueComparisonType => ValueComparisonType.ShouldEqual; - - public override string CurrentValue => null; - - public override IEnumerable Values - { - get - { - // beware! 7.5 and 7.5.0 are not the same thing! - var recommendedValue = _iisVersion >= new Version("7.5") - ? bool.TrueString.ToLower() - : bool.FalseString.ToLower(); - return new List { new AcceptableConfiguration { IsRecommended = true, Value = recommendedValue } }; - } - } - - public override string CheckSuccessMessage - { - get - { - return _textService.Localize("healthcheck/trySkipIisCustomErrorsCheckSuccessMessage", - new[] { Values.First(v => v.IsRecommended).Value, _iisVersion.ToString() }); - } - } - - public override string CheckErrorMessage - { - get - { - return _textService.Localize("healthcheck/trySkipIisCustomErrorsCheckErrorMessage", - new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, _iisVersion.ToString() }); - } - } - - public override string RectifySuccessMessage - { - get - { - return _textService.Localize("healthcheck/trySkipIisCustomErrorsCheckRectifySuccessMessage", - new[] { "Not implemented" }); - - //new[] { Values.First(v => v.IsRecommended).Value, _iisVersion.ToString() }); - } - } - } -} diff --git a/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationMethodSettings.cs b/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationMethodSettings.cs index b4bffab4d6..03293180db 100644 --- a/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationMethodSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/HealthChecksNotificationMethodSettings.cs @@ -2,7 +2,7 @@ // See LICENSE for more details. using System.Collections.Generic; -using Umbraco.Core.HealthCheck; +using Umbraco.Core.HealthChecks; namespace Umbraco.Core.Configuration.Models { diff --git a/src/Umbraco.Core/Constants-HealthChecks.cs b/src/Umbraco.Core/Constants-HealthChecks.cs new file mode 100644 index 0000000000..c582e8ac34 --- /dev/null +++ b/src/Umbraco.Core/Constants-HealthChecks.cs @@ -0,0 +1,58 @@ +namespace Umbraco.Core +{ + /// + /// Defines constants. + /// + public static partial class Constants + { + /// + /// Defines constants for ModelsBuilder. + /// + public static class HealthChecks + { + + public static class DocumentationLinks + { + public const string SmtpCheck = "https://umbra.co/healthchecks-smtp"; + + public static class LiveEnvironment + { + + public const string CompilationDebugCheck = "https://umbra.co/healthchecks-compilation-debug"; + } + public static class Configuration + { + public const string MacroErrorsCheck = "https://umbra.co/healthchecks-macro-errors"; + public const string TrySkipIisCustomErrorsCheck = "https://umbra.co/healthchecks-skip-iis-custom-errors"; + public const string NotificationEmailCheck = "https://umbra.co/healthchecks-notification-email"; + } + public static class FolderAndFilePermissionsCheck + { + + public const string FileWriting = "https://umbra.co/healthchecks-file-writing"; + public const string FolderCreation = "https://umbra.co/healthchecks-folder-creation"; + public const string FileWritingForPackages = "https://umbra.co/healthchecks-file-writing-for-packages"; + public const string MediaFolderCreation = "https://umbra.co/healthchecks-media-folder-creation"; + } + + public static class Security + { + + public const string ClickJackingCheck = "https://umbra.co/healthchecks-click-jacking"; + public const string HstsCheck = "https://umbra.co/healthchecks-hsts"; + public const string NoSniffCheck = "https://umbra.co/healthchecks-no-sniff"; + public const string XssProtectionCheck = "https://umbra.co/healthchecks-xss-protection"; + public const string ExcessiveHeadersCheck = "https://umbra.co/healthchecks-excessive-headers"; + + public static class HttpsCheck + { + + public const string CheckIfCurrentSchemeIsHttps = "https://umbra.co/healthchecks-https-request"; + public const string CheckHttpsConfigurationSetting = "https://umbra.co/healthchecks-https-config"; + public const string CheckForValidCertificate = "https://umbra.co/healthchecks-valid-certificate"; + } + } + } + } + } +} diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index 8199d9fbd0..d63106daf6 100644 --- a/src/Umbraco.Core/Constants-Web.cs +++ b/src/Umbraco.Core/Constants-Web.cs @@ -7,14 +7,12 @@ namespace Umbraco.Core /// public static class Web { - public const string UmbracoRouteDefinitionDataToken = "umbraco-route-def"; - /// /// The preview cookie name /// public const string PreviewCookieName = "UMB_PREVIEW"; - /// + /// /// Client-side cookie that determines whether the user has accepted to be in Preview Mode when visiting the website. /// public const string AcceptPreviewCookieName = "UMB-WEBSITE-PREVIEW-ACCEPT"; @@ -54,6 +52,13 @@ namespace Umbraco.Core public const string BackOfficeApiArea = "UmbracoApi"; // Same name as v8 so all routing remains the same public const string BackOfficeTreeArea = "UmbracoTrees"; // Same name as v8 so all routing remains the same } + + public static class Routing + { + public const string ControllerToken = "controller"; + public const string ActionToken = "action"; + public const string AreaToken = "area"; + } } } } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs index f6dc6fd6ff..e964852129 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs @@ -1,8 +1,8 @@ -using System.Security.Cryptography; using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Dashboards; -using Umbraco.Core.HealthCheck; +using Umbraco.Core.HealthChecks; +using Umbraco.Core.HealthChecks.NotificationMethods; using Umbraco.Core.Manifest; using Umbraco.Core.PackageActions; using Umbraco.Core.PropertyEditors; @@ -13,8 +13,6 @@ using Umbraco.Web.Actions; using Umbraco.Web.ContentApps; using Umbraco.Web.Dashboards; using Umbraco.Web.Editors; -using Umbraco.Web.HealthCheck; -using Umbraco.Web.HealthCheck.NotificationMethods; using Umbraco.Web.Media.EmbedProviders; using Umbraco.Web.Routing; using Umbraco.Web.Sections; @@ -54,7 +52,7 @@ namespace Umbraco.Core.DependencyInjection .Append() .Append(); builder.EditorValidators().Add(() => builder.TypeLoader.GetTypes()); - builder.HealthChecks().Add(() => builder.TypeLoader.GetTypes()); + builder.HealthChecks().Add(() => builder.TypeLoader.GetTypes()); builder.HealthCheckNotificationMethods().Add(() => builder.TypeLoader.GetTypes()); builder.TourFilters(); builder.UrlProviders() diff --git a/src/Umbraco.Core/DictionaryExtensions.cs b/src/Umbraco.Core/DictionaryExtensions.cs index 29cbc221ca..b44e9b68c0 100644 --- a/src/Umbraco.Core/DictionaryExtensions.cs +++ b/src/Umbraco.Core/DictionaryExtensions.cs @@ -6,6 +6,7 @@ using System.Collections.Specialized; using System.Linq; using System.Net; using System.Text; +using System.Threading.Tasks; namespace Umbraco.Core { @@ -280,5 +281,24 @@ namespace Umbraco.Core ? dictionary[key] : defaultValue; } + + public static async Task> ToDictionaryAsync( + this IEnumerable enumerable, + Func syncKeySelector, + Func> asyncValueSelector) + { + Dictionary dictionary = new Dictionary(); + + foreach (var item in enumerable) + { + var key = syncKeySelector(item); + + var value = await asyncValueSelector(item); + + dictionary.Add(key,value); + } + + return dictionary; + } } } diff --git a/src/Umbraco.Core/Events/UmbracoApplicationStarting.cs b/src/Umbraco.Core/Events/UmbracoApplicationStarting.cs index 422673a823..e78a6e4608 100644 --- a/src/Umbraco.Core/Events/UmbracoApplicationStarting.cs +++ b/src/Umbraco.Core/Events/UmbracoApplicationStarting.cs @@ -1,5 +1,9 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + namespace Umbraco.Core.Events { + public class UmbracoApplicationStarting : INotification { /// diff --git a/src/Umbraco.Core/Events/UmbracoRequestBegin.cs b/src/Umbraco.Core/Events/UmbracoRequestBegin.cs new file mode 100644 index 0000000000..c72ddc904d --- /dev/null +++ b/src/Umbraco.Core/Events/UmbracoRequestBegin.cs @@ -0,0 +1,23 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Umbraco.Web; + +namespace Umbraco.Core.Events +{ + /// + /// Notification raised on each request begin. + /// + public class UmbracoRequestBegin : INotification + { + /// + /// Initializes a new instance of the class. + /// + public UmbracoRequestBegin(IUmbracoContext umbracoContext) => UmbracoContext = umbracoContext; + + /// + /// Gets the + /// + public IUmbracoContext UmbracoContext { get; } + } +} diff --git a/src/Umbraco.Web.Common/Events/UmbracoRequestEnd.cs b/src/Umbraco.Core/Events/UmbracoRequestEnd.cs similarity index 60% rename from src/Umbraco.Web.Common/Events/UmbracoRequestEnd.cs rename to src/Umbraco.Core/Events/UmbracoRequestEnd.cs index 459eca2bba..1988a2dd50 100644 --- a/src/Umbraco.Web.Common/Events/UmbracoRequestEnd.cs +++ b/src/Umbraco.Core/Events/UmbracoRequestEnd.cs @@ -1,7 +1,7 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. -using Microsoft.AspNetCore.Http; +using Umbraco.Web; namespace Umbraco.Core.Events { @@ -13,11 +13,11 @@ namespace Umbraco.Core.Events /// /// Initializes a new instance of the class. /// - public UmbracoRequestEnd(HttpContext httpContext) => HttpContext = httpContext; + public UmbracoRequestEnd(IUmbracoContext umbracoContext) => UmbracoContext = umbracoContext; /// - /// Gets the + /// Gets the /// - public HttpContext HttpContext { get; } + public IUmbracoContext UmbracoContext { get; } } } diff --git a/src/Umbraco.Core/HealthCheck/Checks/AbstractSettingsCheck.cs b/src/Umbraco.Core/HealthCheck/Checks/AbstractSettingsCheck.cs deleted file mode 100644 index 62543dcfbd..0000000000 --- a/src/Umbraco.Core/HealthCheck/Checks/AbstractSettingsCheck.cs +++ /dev/null @@ -1,160 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.Logging; -using Umbraco.Core.Services; - -namespace Umbraco.Core.HealthCheck.Checks -{ - public abstract class AbstractSettingsCheck : HealthCheck - { - protected ILocalizedTextService TextService { get; } - protected ILoggerFactory LoggerFactory { get; } - - /// - /// Gets key within the JSON to check, in the colon-delimited format - /// https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1 - /// - public abstract string ItemPath { get; } - - /// - /// Gets the values to compare against. - /// - public abstract IEnumerable Values { get; } - - /// - /// Gets the current value of the config setting - /// - public abstract string CurrentValue { get; } - - /// - /// Gets the provided value - /// - public string ProvidedValue { get; set; } - - /// - /// Gets the comparison type for checking the value. - /// - public abstract ValueComparisonType ValueComparisonType { get; } - - protected AbstractSettingsCheck(ILocalizedTextService textService, ILoggerFactory loggerFactory) - { - TextService = textService; - LoggerFactory = loggerFactory; - } - - /// - /// Gets the message for when the check has succeeded. - /// - public virtual string CheckSuccessMessage - { - get - { - return TextService.Localize("healthcheck/checkSuccessMessage", new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, ItemPath }); - } - } - - /// - /// Gets the message for when the check has failed. - /// - public virtual string CheckErrorMessage - { - get - { - return ValueComparisonType == ValueComparisonType.ShouldEqual - ? TextService.Localize("healthcheck/checkErrorMessageDifferentExpectedValue", - new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, ItemPath }) - : TextService.Localize("healthcheck/checkErrorMessageUnexpectedValue", - new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, ItemPath }); - } - } - - /// - /// Gets the rectify success message. - /// - public virtual string RectifySuccessMessage - { - get - { - AcceptableConfiguration recommendedValue = Values.FirstOrDefault(v => v.IsRecommended); - string rectifiedValue = recommendedValue != null ? recommendedValue.Value : ProvidedValue; - return TextService.Localize("healthcheck/rectifySuccessMessage", - new[] - { - CurrentValue, - rectifiedValue, - ItemPath - }); - } - } - - /// - /// Gets a value indicating whether this check can be rectified automatically. - /// - public virtual bool CanRectify => ValueComparisonType == ValueComparisonType.ShouldEqual; - - /// - /// Gets a value indicating whether this check can be rectified automatically if a value is provided. - /// - public virtual bool CanRectifyWithValue => ValueComparisonType == ValueComparisonType.ShouldNotEqual; - - public override IEnumerable GetStatus() - { - // update the successMessage with the CurrentValue - var successMessage = string.Format(CheckSuccessMessage, ItemPath, Values, CurrentValue); - bool valueFound = Values.Any(value => string.Equals(CurrentValue, value.Value, StringComparison.InvariantCultureIgnoreCase)); - - if (ValueComparisonType == ValueComparisonType.ShouldEqual - && valueFound || ValueComparisonType == ValueComparisonType.ShouldNotEqual - && valueFound == false) - { - return new[] - { - new HealthCheckStatus(successMessage) - { - ResultType = StatusResultType.Success - } - }; - } - - // Declare the action for rectifying the config value - var rectifyAction = new HealthCheckAction("rectify", Id) - { - Name = TextService.Localize("healthcheck/rectifyButton"), - ValueRequired = CanRectifyWithValue - }; - - string resultMessage = string.Format(CheckErrorMessage, ItemPath, Values, CurrentValue); - return new[] - { - new HealthCheckStatus(resultMessage) - { - ResultType = StatusResultType.Error, - Actions = CanRectify || CanRectifyWithValue ? new[] { rectifyAction } : new HealthCheckAction[0] - } - }; - } - - /// - /// Rectifies this check. - /// - /// - public virtual HealthCheckStatus Rectify(HealthCheckAction action) - { - if (ValueComparisonType == ValueComparisonType.ShouldNotEqual) - { - throw new InvalidOperationException(TextService.Localize("healthcheck/cannotRectifyShouldNotEqual")); - } - - //TODO: show message instead of actually fixing config - string recommendedValue = Values.First(v => v.IsRecommended).Value; - string resultMessage = string.Format(RectifySuccessMessage, ItemPath, Values); - return new HealthCheckStatus(resultMessage) { ResultType = StatusResultType.Success }; - } - - public override HealthCheckStatus ExecuteAction(HealthCheckAction action) - { - return Rectify(action); - } - } -} diff --git a/src/Umbraco.Core/HealthCheck/Checks/Configuration/NotificationEmailCheck.cs b/src/Umbraco.Core/HealthCheck/Checks/Configuration/NotificationEmailCheck.cs deleted file mode 100644 index a6e6a83c47..0000000000 --- a/src/Umbraco.Core/HealthCheck/Checks/Configuration/NotificationEmailCheck.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Collections.Generic; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Umbraco.Core.Configuration.Models; -using Umbraco.Core.Services; - -namespace Umbraco.Core.HealthCheck.Checks.Configuration -{ - [HealthCheck("3E2F7B14-4B41-452B-9A30-E67FBC8E1206", "Notification Email Settings", - Description = "If notifications are used, the 'from' email address should be specified and changed from the default value.", - Group = "Configuration")] - public class NotificationEmailCheck : AbstractSettingsCheck - { - private readonly IOptionsMonitor _contentSettings; - private const string DefaultFromEmail = "your@email.here"; - - public NotificationEmailCheck(ILocalizedTextService textService, ILoggerFactory loggerFactory, IOptionsMonitor contentSettings) - : base(textService, loggerFactory) - { - _contentSettings = contentSettings; - } - - public override string ItemPath => Constants.Configuration.ConfigContentNotificationsEmail; - - public override ValueComparisonType ValueComparisonType => ValueComparisonType.ShouldNotEqual; - - public override IEnumerable Values => new List - { - new AcceptableConfiguration { IsRecommended = false, Value = DefaultFromEmail } - }; - - public override string CurrentValue => _contentSettings.CurrentValue.Notifications.Email; - - public override string CheckSuccessMessage => TextService.Localize("healthcheck/notificationEmailsCheckSuccessMessage", new[] { CurrentValue }); - - public override string CheckErrorMessage => TextService.Localize("healthcheck/notificationEmailsCheckErrorMessage", new[] { DefaultFromEmail }); - - } -} diff --git a/src/Umbraco.Core/HealthCheck/Checks/LiveEnvironment/CustomErrorsCheck.cs b/src/Umbraco.Core/HealthCheck/Checks/LiveEnvironment/CustomErrorsCheck.cs deleted file mode 100644 index b003506205..0000000000 --- a/src/Umbraco.Core/HealthCheck/Checks/LiveEnvironment/CustomErrorsCheck.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.Logging; -using Umbraco.Core.Services; - -namespace Umbraco.Core.HealthCheck.Checks.LiveEnvironment -{ - [HealthCheck("4090C0A1-2C52-4124-92DD-F028FD066A64", "Custom Errors", - Description = "Leaving custom errors off will display a complete stack trace to your visitors if an exception occurs.", - Group = "Live Environment")] - public class CustomErrorsCheck : AbstractSettingsCheck - { - public CustomErrorsCheck(ILocalizedTextService textService, ILoggerFactory loggerFactory) - : base(textService, loggerFactory) - { } - - public override string ItemPath => Constants.Configuration.ConfigCustomErrorsMode; - - public override ValueComparisonType ValueComparisonType => ValueComparisonType.ShouldEqual; - - public override IEnumerable Values => new List - { - new AcceptableConfiguration { IsRecommended = true, Value = "RemoteOnly" }, - new AcceptableConfiguration { IsRecommended = false, Value = "On" } - }; - - public override string CurrentValue { get; } - - public override string CheckSuccessMessage - { - get - { - return TextService.Localize("healthcheck/customErrorsCheckSuccessMessage", new[] { CurrentValue }); - } - } - - public override string CheckErrorMessage - { - get - { - return TextService.Localize("healthcheck/customErrorsCheckErrorMessage", - new[] { CurrentValue, Values.First(v => v.IsRecommended).Value }); - } - } - - public override string RectifySuccessMessage - { - get - { - return TextService.Localize("healthcheck/customErrorsCheckRectifySuccessMessage", - new[] { Values.First(v => v.IsRecommended).Value }); - } - } - } -} diff --git a/src/Umbraco.Core/HealthCheck/Checks/LiveEnvironment/TraceCheck.cs b/src/Umbraco.Core/HealthCheck/Checks/LiveEnvironment/TraceCheck.cs deleted file mode 100644 index 03a6ecfde2..0000000000 --- a/src/Umbraco.Core/HealthCheck/Checks/LiveEnvironment/TraceCheck.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Collections.Generic; -using Microsoft.Extensions.Logging; -using Umbraco.Core.Services; - -namespace Umbraco.Core.HealthCheck.Checks.LiveEnvironment -{ - [HealthCheck("9BED6EF4-A7F3-457A-8935-B64E9AA8BAB3", "Trace Mode", - Description = "Leaving trace mode enabled can make valuable information about your system available to hackers.", - Group = "Live Environment")] - public class TraceCheck : AbstractSettingsCheck - { - public TraceCheck(ILocalizedTextService textService, ILoggerFactory loggerFactory) - : base(textService, loggerFactory) - { } - - public override string ItemPath => "/configuration/system.web/trace/@enabled"; - - public override ValueComparisonType ValueComparisonType => ValueComparisonType.ShouldEqual; - - public override IEnumerable Values => new List - { - new AcceptableConfiguration { IsRecommended = true, Value = bool.FalseString.ToLower() } - }; - - public override string CurrentValue { get; } - - public override string CheckSuccessMessage => TextService.Localize("healthcheck/traceModeCheckSuccessMessage"); - - public override string CheckErrorMessage => TextService.Localize("healthcheck/traceModeCheckErrorMessage"); - - public override string RectifySuccessMessage => TextService.Localize("healthcheck/traceModeCheckRectifySuccessMessage"); - - } -} diff --git a/src/Umbraco.Core/HealthCheck/Checks/Permissions/FolderAndFilePermissionsCheck.cs b/src/Umbraco.Core/HealthCheck/Checks/Permissions/FolderAndFilePermissionsCheck.cs deleted file mode 100644 index 28e1de3996..0000000000 --- a/src/Umbraco.Core/HealthCheck/Checks/Permissions/FolderAndFilePermissionsCheck.cs +++ /dev/null @@ -1,161 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.Options; -using Umbraco.Core.Configuration.Models; -using Umbraco.Core.Hosting; -using Umbraco.Core.Install; -using Umbraco.Core.Services; - -namespace Umbraco.Core.HealthCheck.Checks.Permissions -{ - [HealthCheck( - "53DBA282-4A79-4B67-B958-B29EC40FCC23", - "Folder & File Permissions", - Description = "Checks that the web server folder and file permissions are set correctly for Umbraco to run.", - Group = "Permissions")] - public class FolderAndFilePermissionsCheck : HealthCheck - { - private readonly ILocalizedTextService _textService; - private readonly IOptionsMonitor _globalSettings; - private readonly IFilePermissionHelper _filePermissionHelper; - private readonly IHostingEnvironment _hostingEnvironment; - - public FolderAndFilePermissionsCheck(ILocalizedTextService textService, IOptionsMonitor globalSettings, IFilePermissionHelper filePermissionHelper, IHostingEnvironment hostingEnvironment) - { - _textService = textService; - _globalSettings = globalSettings; - _filePermissionHelper = filePermissionHelper; - _hostingEnvironment = hostingEnvironment; - } - - /// - /// Get the status for this health check - /// - // TODO: This should really just run the IFilePermissionHelper.RunFilePermissionTestSuite and then we'd have a - // IFilePermissions interface resolved as a collection within the IFilePermissionHelper that runs checks against all - // IFilePermissions registered. Then there's no hard coding things done here and the checks here will be consistent - // with the checks run in IFilePermissionHelper.RunFilePermissionTestSuite which occurs on install too. - public override IEnumerable GetStatus() => new[] { CheckFolderPermissions(), CheckFilePermissions() }; - - /// - /// Executes the action and returns it's status - /// - public override HealthCheckStatus ExecuteAction(HealthCheckAction action) => throw new InvalidOperationException("FolderAndFilePermissionsCheck has no executable actions"); - - private HealthCheckStatus CheckFolderPermissions() - { - // Create lists of paths to check along with a flag indicating if modify rights are required - // in ALL circumstances or just some - var pathsToCheck = new Dictionary - { - { Constants.SystemDirectories.Data, PermissionCheckRequirement.Required }, - { Constants.SystemDirectories.Packages, PermissionCheckRequirement.Required}, - { Constants.SystemDirectories.Preview, PermissionCheckRequirement.Required }, - { Constants.SystemDirectories.AppPlugins, PermissionCheckRequirement.Required }, - { Constants.SystemDirectories.Config, PermissionCheckRequirement.Optional }, - { _globalSettings.CurrentValue.UmbracoCssPath, PermissionCheckRequirement.Optional }, - { _globalSettings.CurrentValue.UmbracoMediaPath, PermissionCheckRequirement.Optional }, - { _globalSettings.CurrentValue.UmbracoScriptsPath, PermissionCheckRequirement.Optional }, - { _globalSettings.CurrentValue.UmbracoPath, PermissionCheckRequirement.Optional }, - { Constants.SystemDirectories.MvcViews, PermissionCheckRequirement.Optional } - }; - - // These are special paths to check that will restart an app domain if a file is written to them, - // so these need to be tested differently - var pathsToCheckWithRestarts = new Dictionary - { - { Constants.SystemDirectories.Bin, PermissionCheckRequirement.Optional } - }; - - // Run checks for required and optional paths for modify permission - var requiredPathCheckResult = _filePermissionHelper.EnsureDirectories( - GetPathsToCheck(pathsToCheck, PermissionCheckRequirement.Required), out var requiredFailedPaths); - var optionalPathCheckResult = _filePermissionHelper.EnsureDirectories( - GetPathsToCheck(pathsToCheck, PermissionCheckRequirement.Optional), out var optionalFailedPaths); - - // now check the special folders - var requiredPathCheckResult2 = _filePermissionHelper.EnsureDirectories( - GetPathsToCheck(pathsToCheckWithRestarts, PermissionCheckRequirement.Required), out var requiredFailedPaths2, writeCausesRestart: true); - var optionalPathCheckResult2 = _filePermissionHelper.EnsureDirectories( - GetPathsToCheck(pathsToCheckWithRestarts, PermissionCheckRequirement.Optional), out var optionalFailedPaths2, writeCausesRestart: true); - - requiredPathCheckResult = requiredPathCheckResult && requiredPathCheckResult2; - optionalPathCheckResult = optionalPathCheckResult && optionalPathCheckResult2; - - // combine the paths - requiredFailedPaths = requiredFailedPaths.Concat(requiredFailedPaths2).ToList(); - optionalFailedPaths = requiredFailedPaths.Concat(optionalFailedPaths2).ToList(); - - return GetStatus(requiredPathCheckResult, requiredFailedPaths, optionalPathCheckResult, optionalFailedPaths, PermissionCheckFor.Folder); - } - - private HealthCheckStatus CheckFilePermissions() - { - // Create lists of paths to check along with a flag indicating if modify rights are required - // in ALL circumstances or just some - var pathsToCheck = new Dictionary - { - { "~/Web.config", PermissionCheckRequirement.Optional }, - }; - - // Run checks for required and optional paths for modify permission - var requiredPathCheckResult = _filePermissionHelper.EnsureFiles(GetPathsToCheck(pathsToCheck, PermissionCheckRequirement.Required), out IEnumerable requiredFailedPaths); - var optionalPathCheckResult = _filePermissionHelper.EnsureFiles(GetPathsToCheck(pathsToCheck, PermissionCheckRequirement.Optional), out IEnumerable optionalFailedPaths); - - return GetStatus(requiredPathCheckResult, requiredFailedPaths, optionalPathCheckResult, optionalFailedPaths, PermissionCheckFor.File); - } - - private string[] GetPathsToCheck( - Dictionary pathsToCheck, - PermissionCheckRequirement requirement) => pathsToCheck - .Where(x => x.Value == requirement) - .Select(x => _hostingEnvironment.MapPathContentRoot(x.Key)) - .OrderBy(x => x) - .ToArray(); - - private HealthCheckStatus GetStatus(bool requiredPathCheckResult, IEnumerable requiredFailedPaths, bool optionalPathCheckResult, IEnumerable optionalFailedPaths, PermissionCheckFor checkingFor) - { - // Return error if any required paths fail the check, or warning if any optional ones do - var resultType = StatusResultType.Success; - var messageKey = string.Format("healthcheck/{0}PermissionsCheckMessage", - checkingFor == PermissionCheckFor.Folder ? "folder" : "file"); - var message = _textService.Localize(messageKey); - if (requiredPathCheckResult == false) - { - resultType = StatusResultType.Error; - messageKey = string.Format("healthcheck/required{0}PermissionFailed", - checkingFor == PermissionCheckFor.Folder ? "Folder" : "File"); - message = GetMessageForPathCheckFailure(messageKey, requiredFailedPaths); - } - else if (optionalPathCheckResult == false) - { - resultType = StatusResultType.Warning; - messageKey = string.Format("healthcheck/optional{0}PermissionFailed", - checkingFor == PermissionCheckFor.Folder ? "Folder" : "File"); - message = GetMessageForPathCheckFailure(messageKey, optionalFailedPaths); - } - - var actions = new List(); - return new HealthCheckStatus(message) - { - ResultType = resultType, - Actions = actions - }; - } - - private string GetMessageForPathCheckFailure(string messageKey, IEnumerable failedPaths) - { - var rootFolder = _hostingEnvironment.MapPathContentRoot("/"); - var failedFolders = failedPaths - .Select(x => ParseFolderFromFullPath(rootFolder, x)); - return _textService.Localize(messageKey, - new[] { string.Join(", ", failedFolders) }); - } - - private string ParseFolderFromFullPath(string rootFolder, string filePath) - { - return filePath.Replace(rootFolder, string.Empty); - } - } -} diff --git a/src/Umbraco.Core/HealthCheck/Checks/Permissions/PermissionCheckFor.cs b/src/Umbraco.Core/HealthCheck/Checks/Permissions/PermissionCheckFor.cs deleted file mode 100644 index bd914d064d..0000000000 --- a/src/Umbraco.Core/HealthCheck/Checks/Permissions/PermissionCheckFor.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Umbraco.Core.HealthCheck.Checks.Permissions -{ - internal enum PermissionCheckFor - { - Folder, - File - } -} diff --git a/src/Umbraco.Core/HealthCheck/Checks/Permissions/PermissionCheckRequirement.cs b/src/Umbraco.Core/HealthCheck/Checks/Permissions/PermissionCheckRequirement.cs deleted file mode 100644 index f77fdbf2e3..0000000000 --- a/src/Umbraco.Core/HealthCheck/Checks/Permissions/PermissionCheckRequirement.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Umbraco.Core.HealthCheck.Checks.Permissions -{ - internal enum PermissionCheckRequirement - { - Required, - Optional - } -} diff --git a/src/Umbraco.Core/HealthCheck/Checks/Security/BaseHttpHeaderCheck.cs b/src/Umbraco.Core/HealthCheck/Checks/Security/BaseHttpHeaderCheck.cs deleted file mode 100644 index 5bf92342bf..0000000000 --- a/src/Umbraco.Core/HealthCheck/Checks/Security/BaseHttpHeaderCheck.cs +++ /dev/null @@ -1,164 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Text.RegularExpressions; -using Microsoft.Extensions.Configuration; -using Umbraco.Core.Services; -using Umbraco.Web; - -namespace Umbraco.Core.HealthCheck.Checks.Security -{ - public abstract class BaseHttpHeaderCheck : HealthCheck - { - protected ILocalizedTextService TextService { get; } - - private const string SetHeaderInConfigAction = "setHeaderInConfig"; - - private readonly string _header; - private readonly string _value; - private readonly string _localizedTextPrefix; - private readonly bool _metaTagOptionAvailable; - private readonly IRequestAccessor _requestAccessor; - - protected BaseHttpHeaderCheck( - IRequestAccessor requestAccessor, - ILocalizedTextService textService, - string header, string value, string localizedTextPrefix, bool metaTagOptionAvailable) - { - TextService = textService ?? throw new ArgumentNullException(nameof(textService)); - _requestAccessor = requestAccessor; - _header = header; - _value = value; - _localizedTextPrefix = localizedTextPrefix; - _metaTagOptionAvailable = metaTagOptionAvailable; - - } - - /// - /// Get the status for this health check - /// - /// - public override IEnumerable GetStatus() - { - //return the statuses - return new[] { CheckForHeader() }; - } - - /// - /// Executes the action and returns it's status - /// - /// - /// - public override HealthCheckStatus ExecuteAction(HealthCheckAction action) - { - switch (action.Alias) - { - case SetHeaderInConfigAction: - return SetHeaderInConfig(); - default: - throw new InvalidOperationException("HTTP Header action requested is either not executable or does not exist"); - } - } - - protected HealthCheckStatus CheckForHeader() - { - var message = string.Empty; - var success = false; - - // Access the site home page and check for the click-jack protection header or meta tag - var url = _requestAccessor.GetApplicationUrl(); - var request = WebRequest.Create(url); - request.Method = "GET"; - try - { - var response = request.GetResponse(); - - // Check first for header - success = HasMatchingHeader(response.Headers.AllKeys); - - // If not found, and available, check for meta-tag - if (success == false && _metaTagOptionAvailable) - { - success = DoMetaTagsContainKeyForHeader(response); - } - - message = success - ? TextService.Localize($"healthcheck/{_localizedTextPrefix}CheckHeaderFound") - : TextService.Localize($"healthcheck/{_localizedTextPrefix}CheckHeaderNotFound"); - } - catch (Exception ex) - { - message = TextService.Localize("healthcheck/healthCheckInvalidUrl", new[] { url.ToString(), ex.Message }); - } - - var actions = new List(); - if (success == false) - { - actions.Add(new HealthCheckAction(SetHeaderInConfigAction, Id) - { - Name = TextService.Localize("healthcheck/setHeaderInConfig"), - Description = TextService.Localize($"healthcheck/{_localizedTextPrefix}SetHeaderInConfigDescription") - }); - } - - return - new HealthCheckStatus(message) - { - ResultType = success ? StatusResultType.Success : StatusResultType.Error, - Actions = actions - }; - } - - private bool HasMatchingHeader(IEnumerable headerKeys) - { - return headerKeys.Contains(_header, StringComparer.InvariantCultureIgnoreCase); - } - - private bool DoMetaTagsContainKeyForHeader(WebResponse response) - { - using (var stream = response.GetResponseStream()) - { - if (stream == null) return false; - using (var reader = new StreamReader(stream)) - { - var html = reader.ReadToEnd(); - var metaTags = ParseMetaTags(html); - return HasMatchingHeader(metaTags.Keys); - } - } - } - - private static Dictionary ParseMetaTags(string html) - { - var regex = new Regex("() - .ToDictionary(m => m.Groups[1].Value, m => m.Groups[2].Value); - } - - private HealthCheckStatus SetHeaderInConfig() - { - var errorMessage = string.Empty; - //TODO: edit to show fix suggestion instead of making fix - var success = true; - - if (success) - { - return - new HealthCheckStatus(TextService.Localize(string.Format("healthcheck/{0}SetHeaderInConfigSuccess", _localizedTextPrefix))) - { - ResultType = StatusResultType.Success - }; - } - - return - new HealthCheckStatus(TextService.Localize("healthcheck/setHeaderInConfigError", new[] { errorMessage })) - { - ResultType = StatusResultType.Error - }; - } - } -} diff --git a/src/Umbraco.Core/HealthCheck/Checks/Security/ClickJackingCheck.cs b/src/Umbraco.Core/HealthCheck/Checks/Security/ClickJackingCheck.cs deleted file mode 100644 index a7b4c0ba85..0000000000 --- a/src/Umbraco.Core/HealthCheck/Checks/Security/ClickJackingCheck.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Umbraco.Core.Services; -using Umbraco.Web; - -namespace Umbraco.Core.HealthCheck.Checks.Security -{ - [HealthCheck( - "ED0D7E40-971E-4BE8-AB6D-8CC5D0A6A5B0", - "Click-Jacking Protection", - Description = "Checks if your site is allowed to be IFRAMEd by another site and thus would be susceptible to click-jacking.", - Group = "Security")] - public class ClickJackingCheck : BaseHttpHeaderCheck - { - public ClickJackingCheck(IRequestAccessor requestAccessor, ILocalizedTextService textService) - : base(requestAccessor, textService, "X-Frame-Options", "sameorigin", "clickJacking", true) - { - } - } -} diff --git a/src/Umbraco.Core/HealthCheck/Checks/Security/HstsCheck.cs b/src/Umbraco.Core/HealthCheck/Checks/Security/HstsCheck.cs deleted file mode 100644 index ee8f733fca..0000000000 --- a/src/Umbraco.Core/HealthCheck/Checks/Security/HstsCheck.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Umbraco.Core.Services; -using Umbraco.Web; - -namespace Umbraco.Core.HealthCheck.Checks.Security -{ - [HealthCheck( - "E2048C48-21C5-4BE1-A80B-8062162DF124", - "Cookie hijacking and protocol downgrade attacks Protection (Strict-Transport-Security Header (HSTS))", - Description = "Checks if your site, when running with HTTPS, contains the Strict-Transport-Security Header (HSTS). If not, it adds with a default of 18 weeks.", - Group = "Security")] - public class HstsCheck : BaseHttpHeaderCheck - { - // The check is mostly based on the instructions in the OWASP CheatSheet - // (https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/HTTP_Strict_Transport_Security_Cheat_Sheet.md) - // and the blog post of Troy Hunt (https://www.troyhunt.com/understanding-http-strict-transport/) - // If you want do to it perfectly, you have to submit it https://hstspreload.org/, - // but then you should include subdomains and I wouldn't suggest to do that for Umbraco-sites. - public HstsCheck(IRequestAccessor requestAccessor, ILocalizedTextService textService) - : base(requestAccessor, textService, "Strict-Transport-Security", "max-age=10886400", "hSTS", true) - { - } - } -} diff --git a/src/Umbraco.Core/HealthCheck/Checks/Security/NoSniffCheck.cs b/src/Umbraco.Core/HealthCheck/Checks/Security/NoSniffCheck.cs deleted file mode 100644 index c392842049..0000000000 --- a/src/Umbraco.Core/HealthCheck/Checks/Security/NoSniffCheck.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Umbraco.Core.Services; -using Umbraco.Web; - -namespace Umbraco.Core.HealthCheck.Checks.Security -{ - [HealthCheck( - "1CF27DB3-EFC0-41D7-A1BB-EA912064E071", - "Content/MIME Sniffing Protection", - Description = "Checks that your site contains a header used to protect against MIME sniffing vulnerabilities.", - Group = "Security")] - public class NoSniffCheck : BaseHttpHeaderCheck - { - public NoSniffCheck(IRequestAccessor requestAccessor, ILocalizedTextService textService) - : base(requestAccessor, textService, "X-Content-Type-Options", "nosniff", "noSniff", false) - { - } - } -} diff --git a/src/Umbraco.Core/HealthCheck/Checks/Security/XssProtectionCheck.cs b/src/Umbraco.Core/HealthCheck/Checks/Security/XssProtectionCheck.cs deleted file mode 100644 index a5f0f28f22..0000000000 --- a/src/Umbraco.Core/HealthCheck/Checks/Security/XssProtectionCheck.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Umbraco.Core.Services; -using Umbraco.Web; - -namespace Umbraco.Core.HealthCheck.Checks.Security -{ - [HealthCheck( - "F4D2B02E-28C5-4999-8463-05759FA15C3A", - "Cross-site scripting Protection (X-XSS-Protection header)", - Description = "This header enables the Cross-site scripting (XSS) filter in your browser. It checks for the presence of the X-XSS-Protection-header.", - Group = "Security")] - public class XssProtectionCheck : BaseHttpHeaderCheck - { - // The check is mostly based on the instructions in the OWASP CheatSheet - // (https://www.owasp.org/index.php/HTTP_Strict_Transport_Security_Cheat_Sheet) - // and the blog post of Troy Hunt (https://www.troyhunt.com/understanding-http-strict-transport/) - // If you want do to it perfectly, you have to submit it https://hstspreload.appspot.com/, - // but then you should include subdomains and I wouldn't suggest to do that for Umbraco-sites. - public XssProtectionCheck(IRequestAccessor requestAccessor,ILocalizedTextService textService) - : base(requestAccessor, textService, "X-XSS-Protection", "1; mode=block", "xssProtection", true) - { - } - } -} diff --git a/src/Umbraco.Core/HealthCheck/HealthCheckCollection.cs b/src/Umbraco.Core/HealthCheck/HealthCheckCollection.cs deleted file mode 100644 index fc8d5dff25..0000000000 --- a/src/Umbraco.Core/HealthCheck/HealthCheckCollection.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; -using Umbraco.Core.Composing; - -namespace Umbraco.Core.HealthCheck -{ - public class HealthCheckCollection : BuilderCollectionBase - { - public HealthCheckCollection(IEnumerable items) - : base(items) - { } - } -} diff --git a/src/Umbraco.Core/HealthCheck/IConfigurationService.cs b/src/Umbraco.Core/HealthCheck/IConfigurationService.cs deleted file mode 100644 index dc513bb765..0000000000 --- a/src/Umbraco.Core/HealthCheck/IConfigurationService.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Umbraco.Core.HealthCheck -{ - public interface IConfigurationService - { - ConfigurationServiceResult UpdateConfigFile(string value, string itemPath); - } -} diff --git a/src/Umbraco.Core/HealthCheck/AcceptableConfiguration.cs b/src/Umbraco.Core/HealthChecks/AcceptableConfiguration.cs similarity index 79% rename from src/Umbraco.Core/HealthCheck/AcceptableConfiguration.cs rename to src/Umbraco.Core/HealthChecks/AcceptableConfiguration.cs index f879172a5d..f0f20fd4a2 100644 --- a/src/Umbraco.Core/HealthCheck/AcceptableConfiguration.cs +++ b/src/Umbraco.Core/HealthChecks/AcceptableConfiguration.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Core.HealthCheck +namespace Umbraco.Core.HealthChecks { public class AcceptableConfiguration { diff --git a/src/Umbraco.Core/HealthChecks/Checks/AbstractSettingsCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/AbstractSettingsCheck.cs new file mode 100644 index 0000000000..0869e7c8ec --- /dev/null +++ b/src/Umbraco.Core/HealthChecks/Checks/AbstractSettingsCheck.cs @@ -0,0 +1,97 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Umbraco.Core.Services; + +namespace Umbraco.Core.HealthChecks.Checks +{ + /// + /// Provides a base class for health checks of configuration values. + /// + public abstract class AbstractSettingsCheck : HealthCheck + { + /// + /// Initializes a new instance of the class. + /// + protected AbstractSettingsCheck(ILocalizedTextService textService) => LocalizedTextService = textService; + + /// + /// Gets the localized text service. + /// + protected ILocalizedTextService LocalizedTextService { get; } + + /// + /// Gets key within the JSON to check, in the colon-delimited format + /// https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1 + /// + public abstract string ItemPath { get; } + + /// + /// Gets a link to an external resource with more information. + /// + public abstract string ReadMoreLink { get; } + + /// + /// Gets the values to compare against. + /// + public abstract IEnumerable Values { get; } + + /// + /// Gets the current value of the config setting + /// + public abstract string CurrentValue { get; } + + /// + /// Gets the comparison type for checking the value. + /// + public abstract ValueComparisonType ValueComparisonType { get; } + + /// + /// Gets the message for when the check has succeeded. + /// + public virtual string CheckSuccessMessage => LocalizedTextService.Localize("healthcheck/checkSuccessMessage", new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, ItemPath }); + + /// + /// Gets the message for when the check has failed. + /// + public virtual string CheckErrorMessage => + ValueComparisonType == ValueComparisonType.ShouldEqual + ? LocalizedTextService.Localize( + "healthcheck/checkErrorMessageDifferentExpectedValue", + new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, ItemPath }) + : LocalizedTextService.Localize( + "healthcheck/checkErrorMessageUnexpectedValue", + new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, ItemPath }); + + /// + public override Task> GetStatus() + { + // update the successMessage with the CurrentValue + var successMessage = string.Format(CheckSuccessMessage, ItemPath, Values, CurrentValue); + bool valueFound = Values.Any(value => string.Equals(CurrentValue, value.Value, StringComparison.InvariantCultureIgnoreCase)); + + if ((ValueComparisonType == ValueComparisonType.ShouldEqual && valueFound) + || (ValueComparisonType == ValueComparisonType.ShouldNotEqual && valueFound == false)) + { + return Task.FromResult(new HealthCheckStatus(successMessage) + { + ResultType = StatusResultType.Success, + }.Yield()); + } + + string resultMessage = string.Format(CheckErrorMessage, ItemPath, Values, CurrentValue); + return Task.FromResult(new HealthCheckStatus(resultMessage) + { + ResultType = StatusResultType.Error, ReadMoreLink = ReadMoreLink + }.Yield()); + } + + /// + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) + => throw new NotSupportedException("Configuration cannot be automatically fixed."); + } +} diff --git a/src/Umbraco.Core/Configuration/HealthChecks/MacroErrorsCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Configuration/MacroErrorsCheck.cs similarity index 54% rename from src/Umbraco.Core/Configuration/HealthChecks/MacroErrorsCheck.cs rename to src/Umbraco.Core/HealthChecks/Checks/Configuration/MacroErrorsCheck.cs index b31f23667d..9ce0ae1404 100644 --- a/src/Umbraco.Core/Configuration/HealthChecks/MacroErrorsCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Configuration/MacroErrorsCheck.cs @@ -1,37 +1,48 @@ -using System; +// Copyright (c) Umbraco. +// See LICENSE for more details. + using System.Collections.Generic; using System.Linq; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Services; -namespace Umbraco.Core.HealthCheck.Checks.Configuration +namespace Umbraco.Core.HealthChecks.Checks.Configuration { - [HealthCheck("D0F7599E-9B2A-4D9E-9883-81C7EDC5616F", "Macro errors", - Description = - "Checks to make sure macro errors are not set to throw a YSOD (yellow screen of death), which would prevent certain or all pages from loading completely.", + /// + /// Health check for the recommended production configuration for Macro Errors. + /// + [HealthCheck( + "D0F7599E-9B2A-4D9E-9883-81C7EDC5616F", + "Macro errors", + Description = "Checks to make sure macro errors are not set to throw a YSOD (yellow screen of death), which would prevent certain or all pages from loading completely.", Group = "Configuration")] public class MacroErrorsCheck : AbstractSettingsCheck { private readonly ILocalizedTextService _textService; - private readonly ILoggerFactory _loggerFactory; private readonly IOptionsMonitor _contentSettings; - public MacroErrorsCheck(ILocalizedTextService textService, ILoggerFactory loggerFactory, + /// + /// Initializes a new instance of the class. + /// + public MacroErrorsCheck( + ILocalizedTextService textService, IOptionsMonitor contentSettings) - : base(textService, loggerFactory) + : base(textService) { _textService = textService; - _loggerFactory = loggerFactory; _contentSettings = contentSettings; } + /// + public override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.Configuration.MacroErrorsCheck; + + /// public override ValueComparisonType ValueComparisonType => ValueComparisonType.ShouldEqual; + /// public override string ItemPath => Constants.Configuration.ConfigContentMacroErrors; - /// /// Gets the values to compare against. /// @@ -57,42 +68,23 @@ namespace Umbraco.Core.HealthCheck.Checks.Configuration } } + /// public override string CurrentValue => _contentSettings.CurrentValue.MacroErrors.ToString(); /// /// Gets the message for when the check has succeeded. /// - public override string CheckSuccessMessage - { - get - { - return _textService.Localize("healthcheck/macroErrorModeCheckSuccessMessage", - new[] { CurrentValue, Values.First(v => v.IsRecommended).Value }); - } - } + public override string CheckSuccessMessage => + _textService.Localize( + "healthcheck/macroErrorModeCheckSuccessMessage", + new[] { CurrentValue, Values.First(v => v.IsRecommended).Value }); /// /// Gets the message for when the check has failed. /// - public override string CheckErrorMessage - { - get - { - return _textService.Localize("healthcheck/macroErrorModeCheckErrorMessage", - new[] { CurrentValue, Values.First(v => v.IsRecommended).Value }); - } - } - - /// - /// Gets the rectify success message. - /// - public override string RectifySuccessMessage - { - get - { - return TextService.Localize("healthcheck/macroErrorModeCheckRectifySuccessMessage", - new[] { Values.First(v => v.IsRecommended).Value }); - } - } + public override string CheckErrorMessage => + _textService.Localize( + "healthcheck/macroErrorModeCheckErrorMessage", + new[] { CurrentValue, Values.First(v => v.IsRecommended).Value }); } } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Configuration/NotificationEmailCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Configuration/NotificationEmailCheck.cs new file mode 100644 index 0000000000..be0793635f --- /dev/null +++ b/src/Umbraco.Core/HealthChecks/Checks/Configuration/NotificationEmailCheck.cs @@ -0,0 +1,60 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using Microsoft.Extensions.Options; +using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Services; + +namespace Umbraco.Core.HealthChecks.Checks.Configuration +{ + /// + /// Health check for the recommended production configuration for Notification Email. + /// + [HealthCheck( + "3E2F7B14-4B41-452B-9A30-E67FBC8E1206", + "Notification Email Settings", + Description = "If notifications are used, the 'from' email address should be specified and changed from the default value.", + Group = "Configuration")] + public class NotificationEmailCheck : AbstractSettingsCheck + { + private readonly IOptionsMonitor _contentSettings; + private const string DefaultFromEmail = "your@email.here"; + + /// + /// Initializes a new instance of the class. + /// + public NotificationEmailCheck( + ILocalizedTextService textService, + IOptionsMonitor contentSettings) + : base(textService) => + _contentSettings = contentSettings; + + /// + public override string ItemPath => Constants.Configuration.ConfigContentNotificationsEmail; + + /// + public override ValueComparisonType ValueComparisonType => ValueComparisonType.ShouldNotEqual; + + /// + public override IEnumerable Values => new List + { + new AcceptableConfiguration { IsRecommended = false, Value = DefaultFromEmail }, + new AcceptableConfiguration { IsRecommended = false, Value = string.Empty } + }; + + /// + public override string CurrentValue => _contentSettings.CurrentValue.Notifications.Email; + + /// + public override string CheckSuccessMessage => + LocalizedTextService.Localize("healthcheck/notificationEmailsCheckSuccessMessage", + new[] { CurrentValue ?? "<null>" }); + + /// + public override string CheckErrorMessage => LocalizedTextService.Localize("healthcheck/notificationEmailsCheckErrorMessage", new[] { DefaultFromEmail }); + + /// + public override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.Configuration.NotificationEmailCheck; + } +} diff --git a/src/Umbraco.Core/HealthCheck/Checks/Data/DatabaseIntegrityCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Data/DatabaseIntegrityCheck.cs similarity index 62% rename from src/Umbraco.Core/HealthCheck/Checks/Data/DatabaseIntegrityCheck.cs rename to src/Umbraco.Core/HealthChecks/Checks/Data/DatabaseIntegrityCheck.cs index 0fb34950bd..a826d4dd45 100644 --- a/src/Umbraco.Core/HealthCheck/Checks/Data/DatabaseIntegrityCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Data/DatabaseIntegrityCheck.cs @@ -1,12 +1,19 @@ -using System; +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System; using System.Collections.Generic; using System.Linq; using System.Text; +using System.Threading.Tasks; using Umbraco.Core.Models; using Umbraco.Core.Services; -namespace Umbraco.Core.HealthCheck.Checks.Data +namespace Umbraco.Core.HealthChecks.Checks.Data { + /// + /// Health check for the integrity of the data in the database. + /// [HealthCheck( "73DD0C1C-E0CA-4C31-9564-1DCA509788AF", "Database data integrity check", @@ -16,12 +23,17 @@ namespace Umbraco.Core.HealthCheck.Checks.Data { private readonly IContentService _contentService; private readonly IMediaService _mediaService; - private const string _fixMediaPaths = "fixMediaPaths"; - private const string _fixContentPaths = "fixContentPaths"; - private const string _fixMediaPathsTitle = "Fix media paths"; - private const string _fixContentPathsTitle = "Fix content paths"; + private const string SSsFixMediaPaths = "fixMediaPaths"; + private const string SFixContentPaths = "fixContentPaths"; + private const string SFixMediaPathsTitle = "Fix media paths"; + private const string SFixContentPathsTitle = "Fix content paths"; - public DatabaseIntegrityCheck(IContentService contentService, IMediaService mediaService) + /// + /// Initializes a new instance of the class. + /// + public DatabaseIntegrityCheck( + IContentService contentService, + IMediaService mediaService) { _contentService = contentService; _mediaService = mediaService; @@ -30,32 +42,32 @@ namespace Umbraco.Core.HealthCheck.Checks.Data /// /// Get the status for this health check /// - /// - public override IEnumerable GetStatus() - { - //return the statuses - return new[] + public override Task> GetStatus() => + Task.FromResult((IEnumerable)new[] { CheckDocuments(false), CheckMedia(false) - }; - } + }); - private HealthCheckStatus CheckMedia(bool fix) - { - return CheckPaths(_fixMediaPaths, _fixMediaPathsTitle, Core.Constants.UdiEntityType.Media, fix, - () => _mediaService.CheckDataIntegrity(new ContentDataIntegrityReportOptions {FixIssues = fix})); - } + private HealthCheckStatus CheckMedia(bool fix) => + CheckPaths( + SSsFixMediaPaths, + SFixMediaPathsTitle, + Constants.UdiEntityType.Media, + fix, + () => _mediaService.CheckDataIntegrity(new ContentDataIntegrityReportOptions { FixIssues = fix })); - private HealthCheckStatus CheckDocuments(bool fix) - { - return CheckPaths(_fixContentPaths, _fixContentPathsTitle, Core.Constants.UdiEntityType.Document, fix, - () => _contentService.CheckDataIntegrity(new ContentDataIntegrityReportOptions {FixIssues = fix})); - } + private HealthCheckStatus CheckDocuments(bool fix) => + CheckPaths( + SFixContentPaths, + SFixContentPathsTitle, + Constants.UdiEntityType.Document, + fix, + () => _contentService.CheckDataIntegrity(new ContentDataIntegrityReportOptions { FixIssues = fix })); private HealthCheckStatus CheckPaths(string actionAlias, string actionName, string entityType, bool detailedReport, Func doCheck) { - var report = doCheck(); + ContentDataIntegrityReport report = doCheck(); var actions = new List(); if (!report.Ok) @@ -82,7 +94,9 @@ namespace Umbraco.Core.HealthCheck.Checks.Data sb.AppendLine($"

All {entityType} paths are valid

"); if (!detailed) + { return sb.ToString(); + } } else { @@ -92,7 +106,7 @@ namespace Umbraco.Core.HealthCheck.Checks.Data if (detailed && report.DetectedIssues.Count > 0) { sb.AppendLine("
    "); - foreach (var issueGroup in report.DetectedIssues.GroupBy(x => x.Value.IssueType)) + foreach (IGrouping> issueGroup in report.DetectedIssues.GroupBy(x => x.Value.IssueType)) { var countByGroup = issueGroup.Count(); var fixedByGroup = issueGroup.Count(x => x.Value.Fixed); @@ -100,19 +114,21 @@ namespace Umbraco.Core.HealthCheck.Checks.Data sb.AppendLine($"{countByGroup} issues of type {issueGroup.Key} ... {fixedByGroup} fixed"); sb.AppendLine(""); } + sb.AppendLine("
"); } return sb.ToString(); } + /// public override HealthCheckStatus ExecuteAction(HealthCheckAction action) { switch (action.Alias) { - case _fixContentPaths: + case SFixContentPaths: return CheckDocuments(true); - case _fixMediaPaths: + case SSsFixMediaPaths: return CheckMedia(true); default: throw new InvalidOperationException("Action not supported"); diff --git a/src/Umbraco.Core/HealthChecks/Checks/LiveEnvironment/CompilationDebugCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/LiveEnvironment/CompilationDebugCheck.cs new file mode 100644 index 0000000000..e134dcd413 --- /dev/null +++ b/src/Umbraco.Core/HealthChecks/Checks/LiveEnvironment/CompilationDebugCheck.cs @@ -0,0 +1,58 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using Microsoft.Extensions.Options; +using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Services; + +namespace Umbraco.Core.HealthChecks.Checks.LiveEnvironment +{ + /// + /// Health check for the configuration of debug-flag. + /// + [HealthCheck( + "61214FF3-FC57-4B31-B5CF-1D095C977D6D", + "Debug Compilation Mode", + Description = "Leaving debug compilation mode enabled can severely slow down a website and take up more memory on the server.", + Group = "Live Environment")] + public class CompilationDebugCheck : AbstractSettingsCheck + { + private readonly IOptionsMonitor _hostingSettings; + + /// + /// Initializes a new instance of the class. + /// + public CompilationDebugCheck(ILocalizedTextService textService, IOptionsMonitor hostingSettings) + : base(textService) => + _hostingSettings = hostingSettings; + + /// + public override string ItemPath => Constants.Configuration.ConfigHostingDebug; + + /// + public override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.LiveEnvironment.CompilationDebugCheck; + + /// + public override ValueComparisonType ValueComparisonType => ValueComparisonType.ShouldEqual; + + /// + public override IEnumerable Values => new List + { + new AcceptableConfiguration + { + IsRecommended = true, + Value = bool.FalseString.ToLower() + } + }; + + /// + public override string CurrentValue => _hostingSettings.CurrentValue.Debug.ToString(); + + /// + public override string CheckSuccessMessage => LocalizedTextService.Localize("healthcheck/compilationDebugCheckSuccessMessage"); + + /// + public override string CheckErrorMessage => LocalizedTextService.Localize("healthcheck/compilationDebugCheckErrorMessage"); + } +} diff --git a/src/Umbraco.Core/HealthChecks/Checks/Permissions/FolderAndFilePermissionsCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Permissions/FolderAndFilePermissionsCheck.cs new file mode 100644 index 0000000000..0bb7c56486 --- /dev/null +++ b/src/Umbraco.Core/HealthChecks/Checks/Permissions/FolderAndFilePermissionsCheck.cs @@ -0,0 +1,101 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Install; +using Umbraco.Core.Services; + +namespace Umbraco.Core.HealthChecks.Checks.Permissions +{ + /// + /// Health check for the folder and file permissions. + /// + [HealthCheck( + "53DBA282-4A79-4B67-B958-B29EC40FCC23", + "Folder & File Permissions", + Description = "Checks that the web server folder and file permissions are set correctly for Umbraco to run.", + Group = "Permissions")] + public class FolderAndFilePermissionsCheck : HealthCheck + { + private readonly ILocalizedTextService _textService; + private readonly IFilePermissionHelper _filePermissionHelper; + + /// + /// Initializes a new instance of the class. + /// + public FolderAndFilePermissionsCheck( + ILocalizedTextService textService, + IFilePermissionHelper filePermissionHelper) + { + _textService = textService; + _filePermissionHelper = filePermissionHelper; + } + + /// + /// Get the status for this health check + /// + public override Task> GetStatus() + { + _filePermissionHelper.RunFilePermissionTestSuite(out Dictionary> errors); + + return Task.FromResult(errors.Select(x => new HealthCheckStatus(GetMessage(x)) + { + ResultType = x.Value.Any() ? StatusResultType.Error : StatusResultType.Success, + ReadMoreLink = GetReadMoreLink(x), + Description = GetErrorDescription(x) + })); + } + + private string GetErrorDescription(KeyValuePair> status) + { + if (!status.Value.Any()) + { + return null; + } + + var sb = new StringBuilder("The following failed:"); + + sb.AppendLine("
    "); + foreach (var error in status.Value) + { + sb.Append("
  • " + error + "
  • "); + } + + sb.AppendLine("
"); + return sb.ToString(); + } + + private string GetMessage(KeyValuePair> status) + => _textService.Localize("permissions", status.Key); + + private string GetReadMoreLink(KeyValuePair> status) + { + if (!status.Value.Any()) + { + return null; + } + + switch (status.Key) + { + case FilePermissionTest.FileWriting: + return Constants.HealthChecks.DocumentationLinks.FolderAndFilePermissionsCheck.FileWriting; + case FilePermissionTest.FolderCreation: + return Constants.HealthChecks.DocumentationLinks.FolderAndFilePermissionsCheck.FolderCreation; + case FilePermissionTest.FileWritingForPackages: + return Constants.HealthChecks.DocumentationLinks.FolderAndFilePermissionsCheck.FileWritingForPackages; + case FilePermissionTest.MediaFolderCreation: + return Constants.HealthChecks.DocumentationLinks.FolderAndFilePermissionsCheck.MediaFolderCreation; + default: return null; + } + } + + /// + /// Executes the action and returns it's status + /// + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) => throw new InvalidOperationException("FolderAndFilePermissionsCheck has no executable actions"); + } +} diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/BaseHttpHeaderCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/BaseHttpHeaderCheck.cs new file mode 100644 index 0000000000..d8869e12fa --- /dev/null +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/BaseHttpHeaderCheck.cs @@ -0,0 +1,143 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Umbraco.Core.Hosting; +using Umbraco.Core.Services; + +namespace Umbraco.Core.HealthChecks.Checks.Security +{ + /// + /// Provides a base class for health checks of http header values. + /// + public abstract class BaseHttpHeaderCheck : HealthCheck + { + private readonly IHostingEnvironment _hostingEnvironment; + private readonly string _header; + private readonly string _value; + private readonly string _localizedTextPrefix; + private readonly bool _metaTagOptionAvailable; + private static HttpClient s_httpClient; + + /// + /// Initializes a new instance of the class. + /// + protected BaseHttpHeaderCheck( + IHostingEnvironment hostingEnvironment, + ILocalizedTextService textService, + string header, + string value, + string localizedTextPrefix, + bool metaTagOptionAvailable) + { + LocalizedTextService = textService ?? throw new ArgumentNullException(nameof(textService)); + _hostingEnvironment = hostingEnvironment; + _header = header; + _value = value; + _localizedTextPrefix = localizedTextPrefix; + _metaTagOptionAvailable = metaTagOptionAvailable; + } + + private static HttpClient HttpClient => s_httpClient ??= new HttpClient(); + + + /// + /// Gets the localized text service. + /// + protected ILocalizedTextService LocalizedTextService { get; } + + /// + /// Gets a link to an external read more page. + /// + protected abstract string ReadMoreLink { get; } + + /// + /// Get the status for this health check + /// + public override async Task> GetStatus() => + await Task.WhenAll(CheckForHeader()); + + /// + /// Executes the action and returns it's status + /// + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) + => throw new InvalidOperationException("HTTP Header action requested is either not executable or does not exist"); + + /// + /// The actual health check method. + /// + protected async Task CheckForHeader() + { + string message; + var success = false; + + // Access the site home page and check for the click-jack protection header or meta tag + Uri url = _hostingEnvironment.ApplicationMainUrl; + + try + { + using HttpResponseMessage response = await HttpClient.GetAsync(url); + + // Check first for header + success = HasMatchingHeader(response.Headers.Select(x => x.Key)); + + // If not found, and available, check for meta-tag + if (success == false && _metaTagOptionAvailable) + { + success = await DoMetaTagsContainKeyForHeader(response); + } + + message = success + ? LocalizedTextService.Localize($"healthcheck/{_localizedTextPrefix}CheckHeaderFound") + : LocalizedTextService.Localize($"healthcheck/{_localizedTextPrefix}CheckHeaderNotFound"); + } + catch (Exception ex) + { + message = LocalizedTextService.Localize("healthcheck/healthCheckInvalidUrl", new[] { url.ToString(), ex.Message }); + } + + return + new HealthCheckStatus(message) + { + ResultType = success ? StatusResultType.Success : StatusResultType.Error, + ReadMoreLink = success ? null : ReadMoreLink + }; + } + + private bool HasMatchingHeader(IEnumerable headerKeys) + => headerKeys.Contains(_header, StringComparer.InvariantCultureIgnoreCase); + + private async Task DoMetaTagsContainKeyForHeader(HttpResponseMessage response) + { + using (Stream stream = await response.Content.ReadAsStreamAsync()) + { + if (stream == null) + { + return false; + } + + using (var reader = new StreamReader(stream)) + { + var html = reader.ReadToEnd(); + Dictionary metaTags = ParseMetaTags(html); + return HasMatchingHeader(metaTags.Keys); + } + } + } + + private static Dictionary ParseMetaTags(string html) + { + var regex = new Regex("() + .ToDictionary(m => m.Groups[1].Value, m => m.Groups[2].Value); + } + } +} diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/ClickJackingCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/ClickJackingCheck.cs new file mode 100644 index 0000000000..6bb92e4176 --- /dev/null +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/ClickJackingCheck.cs @@ -0,0 +1,30 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Umbraco.Core.Hosting; +using Umbraco.Core.Services; + +namespace Umbraco.Core.HealthChecks.Checks.Security +{ + /// + /// Health check for the recommended production setup regarding the X-Frame-Options header. + /// + [HealthCheck( + "ED0D7E40-971E-4BE8-AB6D-8CC5D0A6A5B0", + "Click-Jacking Protection", + Description = "Checks if your site is allowed to be IFRAMEd by another site and thus would be susceptible to click-jacking.", + Group = "Security")] + public class ClickJackingCheck : BaseHttpHeaderCheck + { + /// + /// Initializes a new instance of the class. + /// + public ClickJackingCheck(IHostingEnvironment hostingEnvironment, ILocalizedTextService textService) + : base(hostingEnvironment, textService, "X-Frame-Options", "sameorigin", "clickJacking", true) + { + } + + /// + protected override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.Security.ClickJackingCheck; + } +} diff --git a/src/Umbraco.Core/HealthCheck/Checks/Security/ExcessiveHeadersCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/ExcessiveHeadersCheck.cs similarity index 51% rename from src/Umbraco.Core/HealthCheck/Checks/Security/ExcessiveHeadersCheck.cs rename to src/Umbraco.Core/HealthChecks/Checks/Security/ExcessiveHeadersCheck.cs index 9cf1127bb0..000c14f93d 100644 --- a/src/Umbraco.Core/HealthCheck/Checks/Security/ExcessiveHeadersCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/ExcessiveHeadersCheck.cs @@ -1,12 +1,19 @@ -using System; +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System; using System.Collections.Generic; using System.Linq; -using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Umbraco.Core.Hosting; using Umbraco.Core.Services; -using Umbraco.Web; -namespace Umbraco.Core.HealthCheck.Checks.Security +namespace Umbraco.Core.HealthChecks.Checks.Security { + /// + /// Health check for the recommended production setup regarding unnecessary headers. + /// [HealthCheck( "92ABBAA2-0586-4089-8AE2-9A843439D577", "Excessive Headers", @@ -15,67 +22,64 @@ namespace Umbraco.Core.HealthCheck.Checks.Security public class ExcessiveHeadersCheck : HealthCheck { private readonly ILocalizedTextService _textService; - private readonly IRequestAccessor _requestAccessor; + private readonly IHostingEnvironment _hostingEnvironment; + private static HttpClient s_httpClient; - public ExcessiveHeadersCheck(ILocalizedTextService textService, IRequestAccessor requestAccessor) + /// + /// Initializes a new instance of the class. + /// + public ExcessiveHeadersCheck(ILocalizedTextService textService, IHostingEnvironment hostingEnvironment) { _textService = textService; - _requestAccessor = requestAccessor; + _hostingEnvironment = hostingEnvironment; } + private static HttpClient HttpClient => s_httpClient ??= new HttpClient(); + /// /// Get the status for this health check /// - /// - public override IEnumerable GetStatus() - { - //return the statuses - return new[] { CheckForHeaders() }; - } + public override async Task> GetStatus() => + await Task.WhenAll(CheckForHeaders()); /// /// Executes the action and returns it's status /// - /// - /// public override HealthCheckStatus ExecuteAction(HealthCheckAction action) - { - throw new InvalidOperationException("ExcessiveHeadersCheck has no executable actions"); - } + => throw new InvalidOperationException("ExcessiveHeadersCheck has no executable actions"); - private HealthCheckStatus CheckForHeaders() + private async Task CheckForHeaders() { - var message = string.Empty; + string message; var success = false; - var url = _requestAccessor.GetApplicationUrl(); + Uri url = _hostingEnvironment.ApplicationMainUrl; // Access the site home page and check for the headers - var request = WebRequest.Create(url); - request.Method = "HEAD"; + var request = new HttpRequestMessage(HttpMethod.Head, url); try { - var response = request.GetResponse(); - var allHeaders = response.Headers.AllKeys; - var headersToCheckFor = new [] {"Server", "X-Powered-By", "X-AspNet-Version", "X-AspNetMvc-Version"}; + using HttpResponseMessage response = await HttpClient.SendAsync(request); + + IEnumerable allHeaders = response.Headers.Select(x => x.Key); + var headersToCheckFor = new[] { "Server", "X-Powered-By", "X-AspNet-Version", "X-AspNetMvc-Version" }; var headersFound = allHeaders .Intersect(headersToCheckFor) .ToArray(); success = headersFound.Any() == false; message = success ? _textService.Localize("healthcheck/excessiveHeadersNotFound") - : _textService.Localize("healthcheck/excessiveHeadersFound", new [] { string.Join(", ", headersFound) }); + : _textService.Localize("healthcheck/excessiveHeadersFound", new[] { string.Join(", ", headersFound) }); } catch (Exception ex) { message = _textService.Localize("healthcheck/httpsCheckInvalidUrl", new[] { url.ToString(), ex.Message }); } - var actions = new List(); return new HealthCheckStatus(message) { ResultType = success ? StatusResultType.Success : StatusResultType.Warning, - Actions = actions + ReadMoreLink = success ? null : Constants.HealthChecks.DocumentationLinks.Security.ExcessiveHeadersCheck, }; } } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/HstsCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/HstsCheck.cs new file mode 100644 index 0000000000..828d2d2470 --- /dev/null +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/HstsCheck.cs @@ -0,0 +1,37 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Umbraco.Core.Hosting; +using Umbraco.Core.Services; + +namespace Umbraco.Core.HealthChecks.Checks.Security +{ + /// + /// Health check for the recommended production setup regarding the Strict-Transport-Security header. + /// + [HealthCheck( + "E2048C48-21C5-4BE1-A80B-8062162DF124", + "Cookie hijacking and protocol downgrade attacks Protection (Strict-Transport-Security Header (HSTS))", + Description = "Checks if your site, when running with HTTPS, contains the Strict-Transport-Security Header (HSTS).", + Group = "Security")] + public class HstsCheck : BaseHttpHeaderCheck + { + /// + /// Initializes a new instance of the class. + /// + /// + /// The check is mostly based on the instructions in the OWASP CheatSheet + /// (https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/HTTP_Strict_Transport_Security_Cheat_Sheet.md) + /// and the blog post of Troy Hunt (https://www.troyhunt.com/understanding-http-strict-transport/) + /// If you want do to it perfectly, you have to submit it https://hstspreload.org/, + /// but then you should include subdomains and I wouldn't suggest to do that for Umbraco-sites. + /// + public HstsCheck(IHostingEnvironment hostingEnvironment, ILocalizedTextService textService) + : base(hostingEnvironment, textService, "Strict-Transport-Security", "max-age=10886400", "hSTS", true) + { + } + + /// + protected override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.Security.HstsCheck; + } +} diff --git a/src/Umbraco.Core/HealthCheck/Checks/Security/HttpsCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/HttpsCheck.cs similarity index 52% rename from src/Umbraco.Core/HealthCheck/Checks/Security/HttpsCheck.cs rename to src/Umbraco.Core/HealthChecks/Checks/Security/HttpsCheck.cs index 6bba41b7d5..5916c48b82 100644 --- a/src/Umbraco.Core/HealthCheck/Checks/Security/HttpsCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/HttpsCheck.cs @@ -1,17 +1,23 @@ -using System; +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System; using System.Collections.Generic; using System.Net; +using System.Net.Http; +using System.Net.Security; using System.Security.Cryptography.X509Certificates; -using Microsoft.Extensions.Logging; -using Umbraco.Core.Services; -using Umbraco.Core.Configuration.Models; +using System.Threading.Tasks; using Microsoft.Extensions.Options; -using Umbraco.Core.Configuration.HealthChecks; -using Umbraco.Core.IO; -using Umbraco.Web; +using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Hosting; +using Umbraco.Core.Services; -namespace Umbraco.Core.HealthCheck.Checks.Security +namespace Umbraco.Core.HealthChecks.Checks.Security { + /// + /// Health checks for the recommended production setup regarding https. + /// [HealthCheck( "EB66BB3B-1BCD-4314-9531-9DA2C1D6D9A7", "HTTPS Configuration", @@ -21,82 +27,86 @@ namespace Umbraco.Core.HealthCheck.Checks.Security { private readonly ILocalizedTextService _textService; private readonly IOptionsMonitor _globalSettings; - private readonly IRequestAccessor _requestAccessor; - private readonly ILogger _logger; - private const string FixHttpsSettingAction = "fixHttpsSetting"; - string itemPath => Constants.Configuration.ConfigGlobalUseHttps; + private readonly IHostingEnvironment _hostingEnvironment; - public HttpsCheck(ILocalizedTextService textService, + private static HttpClient s_httpClient; + private static HttpClientHandler s_httpClientHandler; + private static int s_certificateDaysToExpiry; + + /// + /// Initializes a new instance of the class. + /// + public HttpsCheck( + ILocalizedTextService textService, IOptionsMonitor globalSettings, - IIOHelper ioHelper, - IRequestAccessor requestAccessor, - ILogger logger) + IHostingEnvironment hostingEnvironment) { _textService = textService; _globalSettings = globalSettings; - _requestAccessor = requestAccessor; - _logger = logger; + _hostingEnvironment = hostingEnvironment; } + private static HttpClient HttpClient => s_httpClient ??= new HttpClient(HttpClientHandler); + + private static HttpClientHandler HttpClientHandler => s_httpClientHandler ??= new HttpClientHandler() + { + ServerCertificateCustomValidationCallback = ServerCertificateCustomValidation + }; + /// /// Get the status for this health check /// - /// - public override IEnumerable GetStatus() - { - //return the statuses - return new[] { CheckIfCurrentSchemeIsHttps(), CheckHttpsConfigurationSetting(), CheckForValidCertificate() }; - } + public override async Task> GetStatus() => + await Task.WhenAll( + CheckIfCurrentSchemeIsHttps(), + CheckHttpsConfigurationSetting(), + CheckForValidCertificate()); /// /// Executes the action and returns it's status /// - /// - /// public override HealthCheckStatus ExecuteAction(HealthCheckAction action) + => throw new InvalidOperationException("HttpsCheck action requested is either not executable or does not exist"); + + private static bool ServerCertificateCustomValidation(HttpRequestMessage requestMessage, X509Certificate2 certificate, X509Chain chain, SslPolicyErrors sslErrors) { - switch (action.Alias) + if (!(certificate is null) && s_certificateDaysToExpiry == default) { - case FixHttpsSettingAction: - return FixHttpsSetting(); - default: - throw new InvalidOperationException("HttpsCheck action requested is either not executable or does not exist"); + s_certificateDaysToExpiry = (int)Math.Floor((certificate.NotAfter - DateTime.Now).TotalDays); } + + return sslErrors == SslPolicyErrors.None; } - private HealthCheckStatus CheckForValidCertificate() + private async Task CheckForValidCertificate() { - var message = string.Empty; + string message; StatusResultType result; // Attempt to access the site over HTTPS to see if it HTTPS is supported // and a valid certificate has been configured - var url = _requestAccessor.GetApplicationUrl().ToString().Replace("http:", "https:"); - var request = (HttpWebRequest)WebRequest.Create(url); - request.Method = "HEAD"; + var url = _hostingEnvironment.ApplicationMainUrl.ToString().Replace("http:", "https:"); + + var request = new HttpRequestMessage(HttpMethod.Head, url); try { - var response = (HttpWebResponse)request.GetResponse(); + using HttpResponseMessage response = await HttpClient.SendAsync(request); if (response.StatusCode == HttpStatusCode.OK) { // Got a valid response, check now for if certificate expiring within 14 days // Hat-tip: https://stackoverflow.com/a/15343898/489433 const int numberOfDaysForExpiryWarning = 14; - var cert = request.ServicePoint.Certificate; - var cert2 = new X509Certificate2(cert); - var expirationDate = cert2.NotAfter; - var daysToExpiry = (int)Math.Floor((cert2.NotAfter - DateTime.Now).TotalDays); - if (daysToExpiry <= 0) + if (s_certificateDaysToExpiry <= 0) { result = StatusResultType.Error; message = _textService.Localize("healthcheck/httpsCheckExpiredCertificate"); } - else if (daysToExpiry < numberOfDaysForExpiryWarning) + else if (s_certificateDaysToExpiry < numberOfDaysForExpiryWarning) { result = StatusResultType.Warning; - message = _textService.Localize("healthcheck/httpsCheckExpiringCertificate", new[] { daysToExpiry.ToString() }); + message = _textService.Localize("healthcheck/httpsCheckExpiringCertificate", new[] { s_certificateDaysToExpiry.ToString() }); } else { @@ -107,7 +117,7 @@ namespace Umbraco.Core.HealthCheck.Checks.Security else { result = StatusResultType.Error; - message = _textService.Localize("healthcheck/healthCheckInvalidUrl", new[] { url, response.StatusDescription }); + message = _textService.Localize("healthcheck/healthCheckInvalidUrl", new[] { url, response.ReasonPhrase }); } } catch (Exception ex) @@ -127,34 +137,31 @@ namespace Umbraco.Core.HealthCheck.Checks.Security result = StatusResultType.Error; } - var actions = new List(); - return new HealthCheckStatus(message) { ResultType = result, - Actions = actions + ReadMoreLink = result == StatusResultType.Success + ? null + : Constants.HealthChecks.DocumentationLinks.Security.HttpsCheck.CheckIfCurrentSchemeIsHttps }; } - private HealthCheckStatus CheckIfCurrentSchemeIsHttps() + private Task CheckIfCurrentSchemeIsHttps() { - var uri = _requestAccessor.GetApplicationUrl(); + Uri uri = _hostingEnvironment.ApplicationMainUrl; var success = uri.Scheme == "https"; - var actions = new List(); - - return new HealthCheckStatus(_textService.Localize("healthcheck/httpsCheckIsCurrentSchemeHttps", new[] { success ? string.Empty : "not" })) + return Task.FromResult(new HealthCheckStatus(_textService.Localize("healthcheck/httpsCheckIsCurrentSchemeHttps", new[] { success ? string.Empty : "not" })) { ResultType = success ? StatusResultType.Success : StatusResultType.Error, - Actions = actions - }; + ReadMoreLink = success ? null : Constants.HealthChecks.DocumentationLinks.Security.HttpsCheck.CheckIfCurrentSchemeIsHttps + }); } - private HealthCheckStatus CheckHttpsConfigurationSetting() + private Task CheckHttpsConfigurationSetting() { bool httpsSettingEnabled = _globalSettings.CurrentValue.UseHttps; - Uri uri = _requestAccessor.GetApplicationUrl(); - var actions = new List(); + Uri uri = _hostingEnvironment.ApplicationMainUrl; string resultMessage; StatusResultType resultType; @@ -165,35 +172,19 @@ namespace Umbraco.Core.HealthCheck.Checks.Security } else { - if (httpsSettingEnabled == false) - { - actions.Add(new HealthCheckAction(FixHttpsSettingAction, Id) - { - Name = _textService.Localize("healthcheck/httpsCheckEnableHttpsButton"), - Description = _textService.Localize("healthcheck/httpsCheckEnableHttpsDescription") - }); - } - - resultMessage = _textService.Localize("healthcheck/httpsCheckConfigurationCheckResult", + resultMessage = _textService.Localize( + "healthcheck/httpsCheckConfigurationCheckResult", new[] { httpsSettingEnabled.ToString(), httpsSettingEnabled ? string.Empty : "not" }); resultType = httpsSettingEnabled ? StatusResultType.Success : StatusResultType.Error; } - return new HealthCheckStatus(resultMessage) + return Task.FromResult(new HealthCheckStatus(resultMessage) { ResultType = resultType, - Actions = actions - }; - } - - private HealthCheckStatus FixHttpsSetting() - { - //TODO: return message instead of actual fix - - return new HealthCheckStatus(_textService.Localize("healthcheck/httpsCheckEnableHttpsSuccess")) - { - ResultType = StatusResultType.Success - }; + ReadMoreLink = resultType == StatusResultType.Success + ? null + : Constants.HealthChecks.DocumentationLinks.Security.HttpsCheck.CheckHttpsConfigurationSetting + }); } } } diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/NoSniffCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/NoSniffCheck.cs new file mode 100644 index 0000000000..0722f4cf64 --- /dev/null +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/NoSniffCheck.cs @@ -0,0 +1,30 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Umbraco.Core.Hosting; +using Umbraco.Core.Services; + +namespace Umbraco.Core.HealthChecks.Checks.Security +{ + /// + /// Health check for the recommended production setup regarding the X-Content-Type-Options header. + /// + [HealthCheck( + "1CF27DB3-EFC0-41D7-A1BB-EA912064E071", + "Content/MIME Sniffing Protection", + Description = "Checks that your site contains a header used to protect against MIME sniffing vulnerabilities.", + Group = "Security")] + public class NoSniffCheck : BaseHttpHeaderCheck + { + /// + /// Initializes a new instance of the class. + /// + public NoSniffCheck(IHostingEnvironment hostingEnvironment, ILocalizedTextService textService) + : base(hostingEnvironment, textService, "X-Content-Type-Options", "nosniff", "noSniff", false) + { + } + + /// + protected override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.Security.NoSniffCheck; + } +} diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/XssProtectionCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/XssProtectionCheck.cs new file mode 100644 index 0000000000..5a1973d05b --- /dev/null +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/XssProtectionCheck.cs @@ -0,0 +1,37 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Umbraco.Core.Hosting; +using Umbraco.Core.Services; + +namespace Umbraco.Core.HealthChecks.Checks.Security +{ + /// + /// Health check for the recommended production setup regarding the X-XSS-Protection header. + /// + [HealthCheck( + "F4D2B02E-28C5-4999-8463-05759FA15C3A", + "Cross-site scripting Protection (X-XSS-Protection header)", + Description = "This header enables the Cross-site scripting (XSS) filter in your browser. It checks for the presence of the X-XSS-Protection-header.", + Group = "Security")] + public class XssProtectionCheck : BaseHttpHeaderCheck + { + /// + /// Initializes a new instance of the class. + /// + /// + /// The check is mostly based on the instructions in the OWASP CheatSheet + /// (https://www.owasp.org/index.php/HTTP_Strict_Transport_Security_Cheat_Sheet) + /// and the blog post of Troy Hunt (https://www.troyhunt.com/understanding-http-strict-transport/) + /// If you want do to it perfectly, you have to submit it https://hstspreload.appspot.com/, + /// but then you should include subdomains and I wouldn't suggest to do that for Umbraco-sites. + /// + public XssProtectionCheck(IHostingEnvironment hostingEnvironment, ILocalizedTextService textService) + : base(hostingEnvironment, textService, "X-XSS-Protection", "1; mode=block", "xssProtection", true) + { + } + + /// + protected override string ReadMoreLink => Constants.HealthChecks.DocumentationLinks.Security.XssProtectionCheck; + } +} diff --git a/src/Umbraco.Core/HealthCheck/Checks/Services/SmtpCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Services/SmtpCheck.cs similarity index 71% rename from src/Umbraco.Core/HealthCheck/Checks/Services/SmtpCheck.cs rename to src/Umbraco.Core/HealthChecks/Checks/Services/SmtpCheck.cs index 9e1a6f84af..f4e150fbed 100644 --- a/src/Umbraco.Core/HealthCheck/Checks/Services/SmtpCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Services/SmtpCheck.cs @@ -1,13 +1,20 @@ -using System; +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System; using System.Collections.Generic; using System.IO; using System.Net.Sockets; +using System.Threading.Tasks; using Microsoft.Extensions.Options; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Services; -namespace Umbraco.Core.HealthCheck.Checks.Services +namespace Umbraco.Core.HealthChecks.Checks.Services { + /// + /// Health check for the recommended setup regarding SMTP. + /// [HealthCheck( "1B5D221B-CE99-4193-97CB-5F3261EC73DF", "SMTP Settings", @@ -18,6 +25,9 @@ namespace Umbraco.Core.HealthCheck.Checks.Services private readonly ILocalizedTextService _textService; private readonly IOptionsMonitor _globalSettings; + /// + /// Initializes a new instance of the class. + /// public SmtpCheck(ILocalizedTextService textService, IOptionsMonitor globalSettings) { _textService = textService; @@ -27,28 +37,20 @@ namespace Umbraco.Core.HealthCheck.Checks.Services /// /// Get the status for this health check /// - /// - public override IEnumerable GetStatus() - { - //return the statuses - return new[] { CheckSmtpSettings() }; - } + public override Task> GetStatus() => + Task.FromResult(CheckSmtpSettings().Yield()); /// /// Executes the action and returns it's status /// - /// - /// public override HealthCheckStatus ExecuteAction(HealthCheckAction action) - { - throw new InvalidOperationException("SmtpCheck has no executable actions"); - } + => throw new InvalidOperationException("SmtpCheck has no executable actions"); private HealthCheckStatus CheckSmtpSettings() { var success = false; - var smtpSettings = _globalSettings.CurrentValue.Smtp; + SmtpSettings smtpSettings = _globalSettings.CurrentValue.Smtp; string message; if (smtpSettings == null) @@ -66,27 +68,28 @@ namespace Umbraco.Core.HealthCheck.Checks.Services success = CanMakeSmtpConnection(smtpSettings.Host, smtpSettings.Port); message = success ? _textService.Localize("healthcheck/smtpMailSettingsConnectionSuccess") - : _textService.Localize("healthcheck/smtpMailSettingsConnectionFail", new [] { smtpSettings.Host, smtpSettings.Port.ToString() }); + : _textService.Localize( + "healthcheck/smtpMailSettingsConnectionFail", + new[] { smtpSettings.Host, smtpSettings.Port.ToString() }); } } - var actions = new List(); return new HealthCheckStatus(message) { ResultType = success ? StatusResultType.Success : StatusResultType.Error, - Actions = actions + ReadMoreLink = success ? null : Constants.HealthChecks.DocumentationLinks.SmtpCheck }; } - private bool CanMakeSmtpConnection(string host, int port) + private static bool CanMakeSmtpConnection(string host, int port) { try { using (var client = new TcpClient()) { client.Connect(host, port); - using (var stream = client.GetStream()) + using (NetworkStream stream = client.GetStream()) { using (var writer = new StreamWriter(stream)) using (var reader = new StreamReader(stream)) diff --git a/src/Umbraco.Core/HealthCheck/ConfigurationServiceResult.cs b/src/Umbraco.Core/HealthChecks/ConfigurationServiceResult.cs similarity index 78% rename from src/Umbraco.Core/HealthCheck/ConfigurationServiceResult.cs rename to src/Umbraco.Core/HealthChecks/ConfigurationServiceResult.cs index b4940f927a..114f1d9ed2 100644 --- a/src/Umbraco.Core/HealthCheck/ConfigurationServiceResult.cs +++ b/src/Umbraco.Core/HealthChecks/ConfigurationServiceResult.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Core.HealthCheck +namespace Umbraco.Core.HealthChecks { public class ConfigurationServiceResult { diff --git a/src/Umbraco.Core/HealthCheck/HealthCheck.cs b/src/Umbraco.Core/HealthChecks/HealthCheck.cs similarity index 92% rename from src/Umbraco.Core/HealthCheck/HealthCheck.cs rename to src/Umbraco.Core/HealthChecks/HealthCheck.cs index 9f4e364be6..36c60e5164 100644 --- a/src/Umbraco.Core/HealthCheck/HealthCheck.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheck.cs @@ -1,9 +1,10 @@ using System; using System.Collections.Generic; using System.Runtime.Serialization; +using System.Threading.Tasks; using Umbraco.Core.Composing; -namespace Umbraco.Core.HealthCheck +namespace Umbraco.Core.HealthChecks { /// /// Provides a base class for health checks, filling in the healthcheck metadata on construction @@ -42,7 +43,7 @@ namespace Umbraco.Core.HealthCheck /// Get the status for this health check /// /// - public abstract IEnumerable GetStatus(); + public abstract Task> GetStatus(); /// /// Executes the action and returns it's status diff --git a/src/Umbraco.Core/HealthCheck/HealthCheckAction.cs b/src/Umbraco.Core/HealthChecks/HealthCheckAction.cs similarity index 98% rename from src/Umbraco.Core/HealthCheck/HealthCheckAction.cs rename to src/Umbraco.Core/HealthChecks/HealthCheckAction.cs index e1771a4262..a89f6b73ad 100644 --- a/src/Umbraco.Core/HealthCheck/HealthCheckAction.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheckAction.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Core.HealthCheck +namespace Umbraco.Core.HealthChecks { [DataContract(Name = "healthCheckAction", Namespace = "")] public class HealthCheckAction diff --git a/src/Umbraco.Core/HealthCheck/HealthCheckAttribute.cs b/src/Umbraco.Core/HealthChecks/HealthCheckAttribute.cs similarity index 94% rename from src/Umbraco.Core/HealthCheck/HealthCheckAttribute.cs rename to src/Umbraco.Core/HealthChecks/HealthCheckAttribute.cs index bd8c10f899..9a265a2e03 100644 --- a/src/Umbraco.Core/HealthCheck/HealthCheckAttribute.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheckAttribute.cs @@ -1,6 +1,6 @@ using System; -namespace Umbraco.Core.HealthCheck +namespace Umbraco.Core.HealthChecks { /// /// Metadata attribute for Health checks diff --git a/src/Umbraco.Core/HealthChecks/HealthCheckCollection.cs b/src/Umbraco.Core/HealthChecks/HealthCheckCollection.cs new file mode 100644 index 0000000000..88fadee5ec --- /dev/null +++ b/src/Umbraco.Core/HealthChecks/HealthCheckCollection.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Umbraco.Core.Composing; + +namespace Umbraco.Core.HealthChecks +{ + public class HealthCheckCollection : BuilderCollectionBase + { + public HealthCheckCollection(IEnumerable items) + : base(items) + { } + } +} diff --git a/src/Umbraco.Core/HealthCheck/HealthCheckGroup.cs b/src/Umbraco.Core/HealthChecks/HealthCheckGroup.cs similarity index 90% rename from src/Umbraco.Core/HealthCheck/HealthCheckGroup.cs rename to src/Umbraco.Core/HealthChecks/HealthCheckGroup.cs index 2cd1040896..71b0013d8e 100644 --- a/src/Umbraco.Core/HealthCheck/HealthCheckGroup.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheckGroup.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Runtime.Serialization; -namespace Umbraco.Core.HealthCheck +namespace Umbraco.Core.HealthChecks { [DataContract(Name = "healthCheckGroup", Namespace = "")] public class HealthCheckGroup diff --git a/src/Umbraco.Core/HealthCheck/HealthCheckNotificationMethodAttribute.cs b/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodAttribute.cs similarity index 92% rename from src/Umbraco.Core/HealthCheck/HealthCheckNotificationMethodAttribute.cs rename to src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodAttribute.cs index f78df14942..7e5223772f 100644 --- a/src/Umbraco.Core/HealthCheck/HealthCheckNotificationMethodAttribute.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodAttribute.cs @@ -1,6 +1,6 @@ using System; -namespace Umbraco.Core.HealthCheck +namespace Umbraco.Core.HealthChecks { /// /// Metadata attribute for health check notification methods diff --git a/src/Umbraco.Core/HealthCheck/HealthCheckNotificationMethodCollection.cs b/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodCollection.cs similarity index 79% rename from src/Umbraco.Core/HealthCheck/HealthCheckNotificationMethodCollection.cs rename to src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodCollection.cs index 6c5f89e4bc..bcf197aa39 100644 --- a/src/Umbraco.Core/HealthCheck/HealthCheckNotificationMethodCollection.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodCollection.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using Umbraco.Core.Composing; -using Umbraco.Web.HealthCheck.NotificationMethods; +using Umbraco.Core.HealthChecks.NotificationMethods; -namespace Umbraco.Web.HealthCheck +namespace Umbraco.Core.HealthChecks { public class HealthCheckNotificationMethodCollection : BuilderCollectionBase { diff --git a/src/Umbraco.Core/HealthCheck/HealthCheckNotificationMethodCollectionBuilder.cs b/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodCollectionBuilder.cs similarity index 79% rename from src/Umbraco.Core/HealthCheck/HealthCheckNotificationMethodCollectionBuilder.cs rename to src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodCollectionBuilder.cs index d498716b71..e5d91432f5 100644 --- a/src/Umbraco.Core/HealthCheck/HealthCheckNotificationMethodCollectionBuilder.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheckNotificationMethodCollectionBuilder.cs @@ -1,7 +1,7 @@ using Umbraco.Core.Composing; -using Umbraco.Web.HealthCheck.NotificationMethods; +using Umbraco.Core.HealthChecks.NotificationMethods; -namespace Umbraco.Web.HealthCheck +namespace Umbraco.Core.HealthChecks { public class HealthCheckNotificationMethodCollectionBuilder : LazyCollectionBuilderBase { diff --git a/src/Umbraco.Core/HealthCheck/HealthCheckNotificationVerbosity.cs b/src/Umbraco.Core/HealthChecks/HealthCheckNotificationVerbosity.cs similarity index 71% rename from src/Umbraco.Core/HealthCheck/HealthCheckNotificationVerbosity.cs rename to src/Umbraco.Core/HealthChecks/HealthCheckNotificationVerbosity.cs index 74cd4eb93b..e79b1e627c 100644 --- a/src/Umbraco.Core/HealthCheck/HealthCheckNotificationVerbosity.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheckNotificationVerbosity.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Core.HealthCheck +namespace Umbraco.Core.HealthChecks { public enum HealthCheckNotificationVerbosity { diff --git a/src/Umbraco.Core/HealthCheck/HealthCheckResults.cs b/src/Umbraco.Core/HealthChecks/HealthCheckResults.cs similarity index 88% rename from src/Umbraco.Core/HealthCheck/HealthCheckResults.cs rename to src/Umbraco.Core/HealthChecks/HealthCheckResults.cs index 44955bfaae..904649deb1 100644 --- a/src/Umbraco.Core/HealthCheck/HealthCheckResults.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheckResults.cs @@ -2,27 +2,32 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Umbraco.Core; -using Umbraco.Core.HealthCheck; -namespace Umbraco.Infrastructure.HealthCheck +namespace Umbraco.Core.HealthChecks { public class HealthCheckResults { private readonly Dictionary> _results; public readonly bool AllChecksSuccessful; - private ILogger Logger => StaticApplicationLogging.Logger; // TODO: inject + private static ILogger Logger => StaticApplicationLogging.Logger; // TODO: inject - public HealthCheckResults(IEnumerable checks) + private HealthCheckResults(Dictionary> results, bool allChecksSuccessful) { - _results = checks.ToDictionary( + _results = results; + AllChecksSuccessful = allChecksSuccessful; + } + + public static async Task Create(IEnumerable checks) + { + var results = await checks.ToDictionaryAsync( t => t.Name, - t => { + async t => { try { - return t.GetStatus(); + return await t.GetStatus(); } catch (Exception ex) { @@ -39,16 +44,18 @@ namespace Umbraco.Infrastructure.HealthCheck }); // find out if all checks pass or not - AllChecksSuccessful = true; - foreach (var result in _results) + var allChecksSuccessful = true; + foreach (var result in results) { var checkIsSuccess = result.Value.All(x => x.ResultType == StatusResultType.Success || x.ResultType == StatusResultType.Info || x.ResultType == StatusResultType.Warning); if (checkIsSuccess == false) { - AllChecksSuccessful = false; + allChecksSuccessful = false; break; } } + + return new HealthCheckResults(results, allChecksSuccessful); } public void LogResults() diff --git a/src/Umbraco.Core/HealthCheck/HealthCheckStatus.cs b/src/Umbraco.Core/HealthChecks/HealthCheckStatus.cs similarity index 85% rename from src/Umbraco.Core/HealthCheck/HealthCheckStatus.cs rename to src/Umbraco.Core/HealthChecks/HealthCheckStatus.cs index 2eb873603f..84e3933133 100644 --- a/src/Umbraco.Core/HealthCheck/HealthCheckStatus.cs +++ b/src/Umbraco.Core/HealthChecks/HealthCheckStatus.cs @@ -2,7 +2,7 @@ using System.Linq; using System.Runtime.Serialization; -namespace Umbraco.Core.HealthCheck +namespace Umbraco.Core.HealthChecks { /// /// The status returned for a health check when it performs it check @@ -49,6 +49,10 @@ namespace Umbraco.Core.HealthCheck [DataMember(Name = "actions")] public IEnumerable Actions { get; set; } - // TODO: What else? + /// + /// This is optional but would allow a developer to specify a link that is shown as a "read more" button. + /// + [DataMember(Name = "readMoreLink")] + public string ReadMoreLink { get; set; } } } diff --git a/src/Umbraco.Core/HealthCheck/HeathCheckCollectionBuilder.cs b/src/Umbraco.Core/HealthChecks/HeathCheckCollectionBuilder.cs similarity index 94% rename from src/Umbraco.Core/HealthCheck/HeathCheckCollectionBuilder.cs rename to src/Umbraco.Core/HealthChecks/HeathCheckCollectionBuilder.cs index be3a788cff..57d89b00d9 100644 --- a/src/Umbraco.Core/HealthCheck/HeathCheckCollectionBuilder.cs +++ b/src/Umbraco.Core/HealthChecks/HeathCheckCollectionBuilder.cs @@ -1,7 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Core.Composing; -namespace Umbraco.Core.HealthCheck +namespace Umbraco.Core.HealthChecks { public class HealthCheckCollectionBuilder : LazyCollectionBuilderBase { diff --git a/src/Umbraco.Core/HealthCheck/NotificationMethods/EmailNotificationMethod.cs b/src/Umbraco.Core/HealthChecks/NotificationMethods/EmailNotificationMethod.cs similarity index 86% rename from src/Umbraco.Core/HealthCheck/NotificationMethods/EmailNotificationMethod.cs rename to src/Umbraco.Core/HealthChecks/NotificationMethods/EmailNotificationMethod.cs index ad92886ecd..97ef86d205 100644 --- a/src/Umbraco.Core/HealthCheck/NotificationMethods/EmailNotificationMethod.cs +++ b/src/Umbraco.Core/HealthChecks/NotificationMethods/EmailNotificationMethod.cs @@ -1,22 +1,19 @@ using System; -using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Options; -using Umbraco.Core; using Umbraco.Core.Configuration.Models; -using Umbraco.Core.HealthCheck; +using Umbraco.Core.Hosting; using Umbraco.Core.Mail; using Umbraco.Core.Models; using Umbraco.Core.Services; -using Umbraco.Infrastructure.HealthCheck; -namespace Umbraco.Web.HealthCheck.NotificationMethods +namespace Umbraco.Core.HealthChecks.NotificationMethods { [HealthCheckNotificationMethod("email")] public class EmailNotificationMethod : NotificationMethodBase { private readonly ILocalizedTextService _textService; - private readonly IRequestAccessor _requestAccessor; + private readonly IHostingEnvironment _hostingEnvironment; private readonly IEmailSender _emailSender; private readonly IMarkdownToHtmlConverter _markdownToHtmlConverter; @@ -24,7 +21,7 @@ namespace Umbraco.Web.HealthCheck.NotificationMethods public EmailNotificationMethod( ILocalizedTextService textService, - IRequestAccessor requestAccessor, + IHostingEnvironment hostingEnvironment, IEmailSender emailSender, IOptions healthChecksSettings, IOptions contentSettings, @@ -41,7 +38,7 @@ namespace Umbraco.Web.HealthCheck.NotificationMethods RecipientEmail = recipientEmail; _textService = textService ?? throw new ArgumentNullException(nameof(textService)); - _requestAccessor = requestAccessor; + _hostingEnvironment = hostingEnvironment; _emailSender = emailSender; _markdownToHtmlConverter = markdownToHtmlConverter; _contentSettings = contentSettings.Value ?? throw new ArgumentNullException(nameof(contentSettings)); @@ -70,9 +67,9 @@ namespace Umbraco.Web.HealthCheck.NotificationMethods // Include the umbraco Application URL host in the message subject so that // you can identify the site that these results are for. - var host = _requestAccessor.GetApplicationUrl(); + var host = _hostingEnvironment.ApplicationMainUrl?.ToString(); - var subject = _textService.Localize("healthcheck/scheduledHealthCheckEmailSubject", new[] { host.ToString() }); + var subject = _textService.Localize("healthcheck/scheduledHealthCheckEmailSubject", new[] { host }); var mailMessage = CreateMailMessage(subject, message); diff --git a/src/Umbraco.Core/HealthCheck/NotificationMethods/IHealthCheckNotificationMethod.cs b/src/Umbraco.Core/HealthChecks/NotificationMethods/IHealthCheckNotificationMethod.cs similarity index 56% rename from src/Umbraco.Core/HealthCheck/NotificationMethods/IHealthCheckNotificationMethod.cs rename to src/Umbraco.Core/HealthChecks/NotificationMethods/IHealthCheckNotificationMethod.cs index 77614c41e3..1bed571e14 100644 --- a/src/Umbraco.Core/HealthCheck/NotificationMethods/IHealthCheckNotificationMethod.cs +++ b/src/Umbraco.Core/HealthChecks/NotificationMethods/IHealthCheckNotificationMethod.cs @@ -1,9 +1,7 @@ -using System.Threading; -using System.Threading.Tasks; +using System.Threading.Tasks; using Umbraco.Core.Composing; -using Umbraco.Infrastructure.HealthCheck; -namespace Umbraco.Web.HealthCheck.NotificationMethods +namespace Umbraco.Core.HealthChecks.NotificationMethods { public interface IHealthCheckNotificationMethod : IDiscoverable { diff --git a/src/Umbraco.Core/HealthCheck/NotificationMethods/IMarkdownToHtmlConverter.cs b/src/Umbraco.Core/HealthChecks/NotificationMethods/IMarkdownToHtmlConverter.cs similarity index 54% rename from src/Umbraco.Core/HealthCheck/NotificationMethods/IMarkdownToHtmlConverter.cs rename to src/Umbraco.Core/HealthChecks/NotificationMethods/IMarkdownToHtmlConverter.cs index 20d8f0f07e..0ab33eb6d2 100644 --- a/src/Umbraco.Core/HealthCheck/NotificationMethods/IMarkdownToHtmlConverter.cs +++ b/src/Umbraco.Core/HealthChecks/NotificationMethods/IMarkdownToHtmlConverter.cs @@ -1,7 +1,4 @@ -using Umbraco.Core.HealthCheck; -using Umbraco.Infrastructure.HealthCheck; - -namespace Umbraco.Web.HealthCheck.NotificationMethods +namespace Umbraco.Core.HealthChecks.NotificationMethods { public interface IMarkdownToHtmlConverter { diff --git a/src/Umbraco.Core/HealthCheck/NotificationMethods/NotificationMethodBase.cs b/src/Umbraco.Core/HealthChecks/NotificationMethods/NotificationMethodBase.cs similarity index 92% rename from src/Umbraco.Core/HealthCheck/NotificationMethods/NotificationMethodBase.cs rename to src/Umbraco.Core/HealthChecks/NotificationMethods/NotificationMethodBase.cs index eeb8452492..f491e26ae9 100644 --- a/src/Umbraco.Core/HealthCheck/NotificationMethods/NotificationMethodBase.cs +++ b/src/Umbraco.Core/HealthChecks/NotificationMethods/NotificationMethodBase.cs @@ -3,10 +3,8 @@ using System.Reflection; using System.Threading.Tasks; using Microsoft.Extensions.Options; using Umbraco.Core.Configuration.Models; -using Umbraco.Core.HealthCheck; -using Umbraco.Infrastructure.HealthCheck; -namespace Umbraco.Web.HealthCheck.NotificationMethods +namespace Umbraco.Core.HealthChecks.NotificationMethods { public abstract class NotificationMethodBase : IHealthCheckNotificationMethod { diff --git a/src/Umbraco.Core/HealthCheck/StatusResultType.cs b/src/Umbraco.Core/HealthChecks/StatusResultType.cs similarity index 74% rename from src/Umbraco.Core/HealthCheck/StatusResultType.cs rename to src/Umbraco.Core/HealthChecks/StatusResultType.cs index 3f2c392933..ce91080267 100644 --- a/src/Umbraco.Core/HealthCheck/StatusResultType.cs +++ b/src/Umbraco.Core/HealthChecks/StatusResultType.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Core.HealthCheck +namespace Umbraco.Core.HealthChecks { public enum StatusResultType { diff --git a/src/Umbraco.Core/HealthCheck/ValueComparisonType.cs b/src/Umbraco.Core/HealthChecks/ValueComparisonType.cs similarity index 71% rename from src/Umbraco.Core/HealthCheck/ValueComparisonType.cs rename to src/Umbraco.Core/HealthChecks/ValueComparisonType.cs index c5dd6517a8..905c92e7ce 100644 --- a/src/Umbraco.Core/HealthCheck/ValueComparisonType.cs +++ b/src/Umbraco.Core/HealthChecks/ValueComparisonType.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Core.HealthCheck +namespace Umbraco.Core.HealthChecks { public enum ValueComparisonType { diff --git a/src/Umbraco.Core/Hosting/IHostingEnvironment.cs b/src/Umbraco.Core/Hosting/IHostingEnvironment.cs index 60d582c6c9..e01435422d 100644 --- a/src/Umbraco.Core/Hosting/IHostingEnvironment.cs +++ b/src/Umbraco.Core/Hosting/IHostingEnvironment.cs @@ -31,7 +31,10 @@ namespace Umbraco.Core.Hosting /// bool IsHosted { get; } - Version IISVersion { get; } + /// + /// Gets the main application url. + /// + Uri ApplicationMainUrl { get; } /// /// Maps a virtual path to a physical path to the application's web root @@ -63,5 +66,10 @@ namespace Umbraco.Core.Hosting /// If virtualPath does not start with ~/ or / /// string ToAbsolute(string virtualPath); + + /// + /// Ensures that the application know its main Url. + /// + void EnsureApplicationMainUrl(Uri currentApplicationUrl); } } diff --git a/src/Umbraco.Core/Install/FilePermissionTest.cs b/src/Umbraco.Core/Install/FilePermissionTest.cs new file mode 100644 index 0000000000..fe714ca8fa --- /dev/null +++ b/src/Umbraco.Core/Install/FilePermissionTest.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Core.Install +{ + public enum FilePermissionTest + { + FolderCreation, + FileWritingForPackages, + FileWriting, + MediaFolderCreation + } +} diff --git a/src/Umbraco.Core/Install/IFilePermissionHelper.cs b/src/Umbraco.Core/Install/IFilePermissionHelper.cs index ab521d214e..6bddd02db4 100644 --- a/src/Umbraco.Core/Install/IFilePermissionHelper.cs +++ b/src/Umbraco.Core/Install/IFilePermissionHelper.cs @@ -1,26 +1,20 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + using System.Collections.Generic; namespace Umbraco.Core.Install { + /// + /// Helper to test File and folder permissions + /// public interface IFilePermissionHelper { - bool RunFilePermissionTestSuite(out Dictionary> report); - /// - /// This will test the directories for write access + /// Run all tests for permissions of the required files and folders. /// - /// The directories to check - /// The resulting errors, if any - /// - /// If this is false, the easiest way to test for write access is to write a temp file, however some folder will cause - /// an App Domain restart if a file is written to the folder, so in that case we need to use the ACL APIs which aren't as - /// reliable but we cannot write a file since it will cause an app domain restart. - /// - /// Returns true if test succeeds - // TODO: This shouldn't exist, see notes in FolderAndFilePermissionsCheck.GetStatus - bool EnsureDirectories(string[] dirs, out IEnumerable errors, bool writeCausesRestart = false); + /// True if all permissions are correct. False otherwise. + bool RunFilePermissionTestSuite(out Dictionary> report); - // TODO: This shouldn't exist, see notes in FolderAndFilePermissionsCheck.GetStatus - bool EnsureFiles(string[] files, out IEnumerable errors); } } diff --git a/src/Umbraco.Core/Install/InstallSteps/FilePermissionsStep.cs b/src/Umbraco.Core/Install/InstallSteps/FilePermissionsStep.cs index 152c5a831f..d2c2c84339 100644 --- a/src/Umbraco.Core/Install/InstallSteps/FilePermissionsStep.cs +++ b/src/Umbraco.Core/Install/InstallSteps/FilePermissionsStep.cs @@ -1,38 +1,56 @@ -using System; +// Copyright (c) Umbraco. +// See LICENSE for more details. + using System.Collections.Generic; -using System.IO; +using System.Linq; using System.Threading.Tasks; -using Umbraco.Core; -using Umbraco.Core.Install; +using Umbraco.Core.Services; +using Umbraco.Web.Install; using Umbraco.Web.Install.Models; -namespace Umbraco.Web.Install.InstallSteps +namespace Umbraco.Core.Install.InstallSteps { - [InstallSetupStep(InstallationType.NewInstall | InstallationType.Upgrade, - "Permissions", 0, "", + /// + /// Represents a step in the installation that ensure all the required permissions on files and folders are correct. + /// + [InstallSetupStep( + InstallationType.NewInstall | InstallationType.Upgrade, + "Permissions", + 0, + "", PerformsAppRestart = true)] public class FilePermissionsStep : InstallSetupStep { private readonly IFilePermissionHelper _filePermissionHelper; - public FilePermissionsStep(IFilePermissionHelper filePermissionHelper) + private readonly ILocalizedTextService _localizedTextService; + + /// + /// Initializes a new instance of the class. + /// + public FilePermissionsStep( + IFilePermissionHelper filePermissionHelper, + ILocalizedTextService localizedTextService) { _filePermissionHelper = filePermissionHelper; + _localizedTextService = localizedTextService; } + + /// public override Task ExecuteAsync(object model) { // validate file permissions - Dictionary> report; - var permissionsOk = _filePermissionHelper.RunFilePermissionTestSuite(out report); + var permissionsOk = _filePermissionHelper.RunFilePermissionTestSuite(out Dictionary> report); + var translatedErrors = report.ToDictionary(x => _localizedTextService.Localize("permissions", x.Key), x => x.Value); if (permissionsOk == false) - throw new InstallException("Permission check failed", "permissionsreport", new { errors = report }); + { + throw new InstallException("Permission check failed", "permissionsreport", new { errors = translatedErrors }); + } return Task.FromResult(null); } - public override bool RequiresExecution(object model) - { - return true; - } + /// + public override bool RequiresExecution(object model) => true; } } diff --git a/src/Umbraco.Core/Media/EmbedProviders/OEmbedResponse.cs b/src/Umbraco.Core/Media/EmbedProviders/OEmbedResponse.cs index 8178a97742..0719f2ed20 100644 --- a/src/Umbraco.Core/Media/EmbedProviders/OEmbedResponse.cs +++ b/src/Umbraco.Core/Media/EmbedProviders/OEmbedResponse.cs @@ -9,10 +9,13 @@ namespace Umbraco.Web.Media.EmbedProviders [DataContract] public class OEmbedResponse { + [DataMember(Name ="type")] public string Type { get; set; } + [DataMember(Name ="version")] public string Version { get; set; } + [DataMember(Name ="title")] public string Title { get; set; } [DataMember(Name ="author_name")] @@ -36,12 +39,16 @@ namespace Umbraco.Web.Media.EmbedProviders [DataMember(Name ="thumbnail_width")] public double? ThumbnailWidth { get; set; } + [DataMember(Name ="html")] public string Html { get; set; } + [DataMember(Name ="url")] public string Url { get; set; } + [DataMember(Name ="height")] public double? Height { get; set; } + [DataMember(Name ="width")] public double? Width { get; set; } /// diff --git a/src/Umbraco.Core/PropertyEditors/BlockListConfiguration.cs b/src/Umbraco.Core/PropertyEditors/BlockListConfiguration.cs index f0aa1f0b77..27064e2aa7 100644 --- a/src/Umbraco.Core/PropertyEditors/BlockListConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/BlockListConfiguration.cs @@ -50,6 +50,7 @@ namespace Umbraco.Web.PropertyEditors [ConfigurationField("validationLimit", "Amount", "numberrange", Description = "Set a required range of blocks")] public NumberRange ValidationLimit { get; set; } = new NumberRange(); + [DataContract] public class NumberRange { [DataMember(Name ="min")] diff --git a/src/Umbraco.Core/PropertyEditors/RichTextConfiguration.cs b/src/Umbraco.Core/PropertyEditors/RichTextConfiguration.cs index c1fb61bde8..341a4750f6 100644 --- a/src/Umbraco.Core/PropertyEditors/RichTextConfiguration.cs +++ b/src/Umbraco.Core/PropertyEditors/RichTextConfiguration.cs @@ -20,7 +20,7 @@ namespace Umbraco.Web.PropertyEditors Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] public bool IgnoreUserStartNodes { get; set; } - [ConfigurationField("mediaParentId", "Image Upload Folder", "MediaFolderPicker", + [ConfigurationField("mediaParentId", "Image Upload Folder", "mediafolderpicker", Description = "Choose the upload location of pasted images")] public GuidUdi MediaParentId { get; set; } } diff --git a/src/Umbraco.Core/Routing/EnsureRoutableOutcome.cs b/src/Umbraco.Core/Routing/EnsureRoutableOutcome.cs deleted file mode 100644 index 0c6ac7c564..0000000000 --- a/src/Umbraco.Core/Routing/EnsureRoutableOutcome.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace Umbraco.Web.Routing -{ - /// - /// Represents the outcome of trying to route an incoming request. - /// - public enum EnsureRoutableOutcome - { - /// - /// Request routes to a document. - /// - /// - /// Umbraco was ready and configured, and has content. - /// The request looks like it can be a route to a document. This does not - /// mean that there *is* a matching document, ie the request might end up returning - /// 404. - /// - IsRoutable = 0, - - /// - /// Request does not route to a document. - /// - /// - /// Umbraco was ready and configured, and has content. - /// The request does not look like it can be a route to a document. Can be - /// anything else eg back-office, surface controller... - /// - NotDocumentRequest = 10, - - /// - /// Umbraco was not ready. - /// - NotReady = 11, - - /// - /// There was no content at all. - /// - NoContent = 12 - } -} diff --git a/src/Umbraco.Core/Routing/RoutableAttemptEventArgs.cs b/src/Umbraco.Core/Routing/RoutableAttemptEventArgs.cs deleted file mode 100644 index 9f80d8ed3f..0000000000 --- a/src/Umbraco.Core/Routing/RoutableAttemptEventArgs.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Umbraco.Web.Routing -{ - /// - /// Event args containing information about why the request was not routable, or if it is routable - /// - public class RoutableAttemptEventArgs : UmbracoRequestEventArgs - { - public EnsureRoutableOutcome Outcome { get; private set; } - - public RoutableAttemptEventArgs(EnsureRoutableOutcome reason, IUmbracoContext umbracoContext) - : base(umbracoContext) - { - Outcome = reason; - } - } -} diff --git a/src/Umbraco.Core/Routing/UmbracoRequestEventArgs.cs b/src/Umbraco.Core/Routing/UmbracoRequestEventArgs.cs deleted file mode 100644 index d93a27ddf4..0000000000 --- a/src/Umbraco.Core/Routing/UmbracoRequestEventArgs.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Umbraco.Web.Routing -{ - /// - /// Event args used for event launched during a request (like in the UmbracoModule) - /// - public class UmbracoRequestEventArgs : EventArgs - { - public IUmbracoContext UmbracoContext { get; private set; } - - public UmbracoRequestEventArgs(IUmbracoContext umbracoContext) - { - UmbracoContext = umbracoContext; - } - } -} diff --git a/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs b/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs index 92854c5a2b..e02cdec7c9 100644 --- a/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs +++ b/src/Umbraco.Core/Services/LocalizedTextServiceExtensions.cs @@ -2,7 +2,6 @@ using System.Globalization; using System.Linq; using System.Threading; -using Umbraco.Core.Composing; using Umbraco.Core.Dictionary; namespace Umbraco.Core.Services @@ -12,6 +11,13 @@ namespace Umbraco.Core.Services /// public static class LocalizedTextServiceExtensions { + + public static string Localize(this ILocalizedTextService manager, string area, T key) + where T: System.Enum + { + var fullKey = string.Join("/", area, key); + return manager.Localize(fullKey, Thread.CurrentThread.CurrentUICulture); + } public static string Localize(this ILocalizedTextService manager, string area, string key) { var fullKey = string.Join("/", area, key); diff --git a/src/Umbraco.Core/Web/IRequestAccessor.cs b/src/Umbraco.Core/Web/IRequestAccessor.cs index 85ab5cff97..56c8091f94 100644 --- a/src/Umbraco.Core/Web/IRequestAccessor.cs +++ b/src/Umbraco.Core/Web/IRequestAccessor.cs @@ -1,17 +1,22 @@ using System; -using Umbraco.Web.Routing; namespace Umbraco.Web { public interface IRequestAccessor { + /// + /// Returns the request/form/querystring value for the given name + /// string GetRequestValue(string name); - string GetQueryStringValue(string name); - event EventHandler EndRequest; - event EventHandler RouteAttempt; - Uri GetRequestUrl(); - // TODO: Not sure this belongs here but we can leave it for now - Uri GetApplicationUrl(); + /// + /// Returns the query string value for the given name + /// + string GetQueryStringValue(string name); + + /// + /// Returns the current request uri + /// + Uri GetRequestUrl(); } } diff --git a/src/Umbraco.Infrastructure/Cache/DatabaseServerMessengerNotificationHandler.cs b/src/Umbraco.Infrastructure/Cache/DatabaseServerMessengerNotificationHandler.cs index 1ad2b44740..fc3f66c28f 100644 --- a/src/Umbraco.Infrastructure/Cache/DatabaseServerMessengerNotificationHandler.cs +++ b/src/Umbraco.Infrastructure/Cache/DatabaseServerMessengerNotificationHandler.cs @@ -11,10 +11,9 @@ namespace Umbraco.Infrastructure.Cache /// /// Ensures that distributed cache events are setup and the is initialized /// - public sealed class DatabaseServerMessengerNotificationHandler : INotificationHandler + public sealed class DatabaseServerMessengerNotificationHandler : INotificationHandler, INotificationHandler { private readonly IServerMessenger _messenger; - private readonly IRequestAccessor _requestAccessor; private readonly IUmbracoDatabaseFactory _databaseFactory; private readonly IDistributedCacheBinder _distributedCacheBinder; private readonly ILogger _logger; @@ -24,12 +23,10 @@ namespace Umbraco.Infrastructure.Cache /// public DatabaseServerMessengerNotificationHandler( IServerMessenger serverMessenger, - IRequestAccessor requestAccessor, IUmbracoDatabaseFactory databaseFactory, IDistributedCacheBinder distributedCacheBinder, ILogger logger) { - _requestAccessor = requestAccessor; _databaseFactory = databaseFactory; _distributedCacheBinder = distributedCacheBinder; _logger = logger; @@ -38,19 +35,6 @@ namespace Umbraco.Infrastructure.Cache /// public void Handle(UmbracoApplicationStarting notification) - { - // The scheduled tasks - TouchServerTask and InstructionProcessTask - run as .NET Core hosted services. - // The former (as well as other hosted services that run outside of an HTTP request context) depends on the application URL - // being available (via IRequestAccessor), which can only be retrieved within an HTTP request (unless it's explicitly configured). - // Hence we hook up a one-off task on an HTTP request to ensure this is retrieved, which caches the value and makes it available - // for the hosted services to use when the HTTP request is not available. - _requestAccessor.RouteAttempt += EnsureApplicationUrlOnce; - _requestAccessor.EndRequest += EndRequest; - - Startup(); - } - - private void Startup() { if (_databaseFactory.CanConnect == false) { @@ -65,28 +49,9 @@ namespace Umbraco.Infrastructure.Cache } } - // TODO: I don't really know or think that the Application Url plays a role anymore with the DB dist cache, - // this might be really old stuff. I 'think' all this is doing is ensuring that the IRequestAccessor.GetApplicationUrl - // is definitely called during the first request. If that is still required, that logic doesn't belong here. That logic - // should be part of it's own service/middleware. There's also TODO notes within IRequestAccessor.GetApplicationUrl directly - // mentioning that the property doesn't belong on that service either. This should be investigated and resolved in a separate task. - private void EnsureApplicationUrlOnce(object sender, RoutableAttemptEventArgs e) - { - if (e.Outcome == EnsureRoutableOutcome.IsRoutable || e.Outcome == EnsureRoutableOutcome.NotDocumentRequest) - { - _requestAccessor.RouteAttempt -= EnsureApplicationUrlOnce; - EnsureApplicationUrl(); - } - } - - // By retrieving the application URL within the context of a request (as we are here in responding - // to the IRequestAccessor's RouteAttempt event), we'll get it from the HTTP context and save it for - // future requests that may not be within an HTTP request (e.g. from hosted services). - private void EnsureApplicationUrl() => _requestAccessor.GetApplicationUrl(); - /// /// Clear the batch on end request /// - private void EndRequest(object sender, UmbracoRequestEventArgs e) => _messenger?.SendMessages(); + public void Handle(UmbracoRequestEnd notification) => _messenger?.SendMessages(); } } diff --git a/src/Umbraco.Infrastructure/Compose/NotificationsComponent.cs b/src/Umbraco.Infrastructure/Compose/NotificationsComponent.cs index 767f36aaf5..ad3efc88df 100644 --- a/src/Umbraco.Infrastructure/Compose/NotificationsComponent.cs +++ b/src/Umbraco.Infrastructure/Compose/NotificationsComponent.cs @@ -2,11 +2,12 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using Microsoft.Extensions.Options; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Hosting; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; using Umbraco.Core.Models.Membership; @@ -185,7 +186,7 @@ namespace Umbraco.Web.Compose public sealed class Notifier { private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - private readonly IRequestAccessor _requestAccessor; + private readonly IHostingEnvironment _hostingEnvironment; private readonly INotificationService _notificationService; private readonly IUserService _userService; private readonly ILocalizedTextService _textService; @@ -193,18 +194,11 @@ namespace Umbraco.Web.Compose private readonly ILogger _logger; /// - /// Constructor + /// Initializes a new instance of the class. /// - /// - /// - /// - /// - /// - /// - /// public Notifier( IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - IRequestAccessor requestAccessor, + IHostingEnvironment hostingEnvironment, INotificationService notificationService, IUserService userService, ILocalizedTextService textService, @@ -212,7 +206,7 @@ namespace Umbraco.Web.Compose ILogger logger) { _backOfficeSecurityAccessor = backOfficeSecurityAccessor; - _requestAccessor = requestAccessor; + _hostingEnvironment = hostingEnvironment; _notificationService = notificationService; _userService = userService; _textService = textService; @@ -236,7 +230,7 @@ namespace Umbraco.Web.Compose } } - SendNotification(user, entities, action, _requestAccessor.GetApplicationUrl()); + SendNotification(user, entities, action, _hostingEnvironment.ApplicationMainUrl); } private void SendNotification(IUser sender, IEnumerable entities, IAction action, Uri siteUri) diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 53f2d29709..f8fc338ee1 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -7,6 +7,7 @@ using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; using Umbraco.Core.DependencyInjection; +using Umbraco.Core.HealthChecks.NotificationMethods; using Umbraco.Core.Hosting; using Umbraco.Core.Install; using Umbraco.Core.Logging.Serilog.Enrichers; @@ -27,14 +28,13 @@ using Umbraco.Core.Strings; using Umbraco.Core.Templates; using Umbraco.Examine; using Umbraco.Infrastructure.Examine; +using Umbraco.Infrastructure.HealthChecks; using Umbraco.Infrastructure.HostedServices; +using Umbraco.Infrastructure.Install; using Umbraco.Infrastructure.Logging.Serilog.Enrichers; using Umbraco.Infrastructure.Media; using Umbraco.Infrastructure.Runtime; using Umbraco.Web; -using Umbraco.Web.HealthCheck; -using Umbraco.Web.HealthCheck.NotificationMethods; -using Umbraco.Web.Install; using Umbraco.Web.Media; using Umbraco.Web.Migrations.PostMigrations; using Umbraco.Web.Models.PublishedContent; diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.DistributedCache.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.DistributedCache.cs index b88c2346a7..e816972989 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.DistributedCache.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.DistributedCache.cs @@ -38,6 +38,7 @@ namespace Umbraco.Infrastructure.DependencyInjection builder.SetDatabaseServerMessengerCallbacks(GetCallbacks); builder.SetServerMessenger(); builder.AddNotificationHandler(); + builder.AddNotificationHandler(); return builder; } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs index 1e40831f75..21bb4d7ceb 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Installer.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Core.DependencyInjection; +using Umbraco.Core.Install.InstallSteps; using Umbraco.Web.Install; using Umbraco.Web.Install.InstallSteps; using Umbraco.Web.Install.Models; diff --git a/src/Umbraco.Infrastructure/HealthCheck/MarkdownToHtmlConverter.cs b/src/Umbraco.Infrastructure/HealthChecks/MarkdownToHtmlConverter.cs similarity index 88% rename from src/Umbraco.Infrastructure/HealthCheck/MarkdownToHtmlConverter.cs rename to src/Umbraco.Infrastructure/HealthChecks/MarkdownToHtmlConverter.cs index 39c5fe0bf2..739035b177 100644 --- a/src/Umbraco.Infrastructure/HealthCheck/MarkdownToHtmlConverter.cs +++ b/src/Umbraco.Infrastructure/HealthChecks/MarkdownToHtmlConverter.cs @@ -1,9 +1,8 @@ using HeyRed.MarkdownSharp; -using Umbraco.Core.HealthCheck; -using Umbraco.Infrastructure.HealthCheck; -using Umbraco.Web.HealthCheck.NotificationMethods; +using Umbraco.Core.HealthChecks; +using Umbraco.Core.HealthChecks.NotificationMethods; -namespace Umbraco.Web.HealthCheck +namespace Umbraco.Infrastructure.HealthChecks { public class MarkdownToHtmlConverter : IMarkdownToHtmlConverter { diff --git a/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs b/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs index c1412d4169..dcbec3d8d1 100644 --- a/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs +++ b/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs @@ -11,13 +11,11 @@ using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Extensions; using Umbraco.Core.Configuration.Models; -using Umbraco.Core.HealthCheck; +using Umbraco.Core.HealthChecks; +using Umbraco.Core.HealthChecks.NotificationMethods; using Umbraco.Core.Logging; using Umbraco.Core.Scoping; using Umbraco.Core.Sync; -using Umbraco.Infrastructure.HealthCheck; -using Umbraco.Web.HealthCheck; -using Umbraco.Web.HealthCheck.NotificationMethods; namespace Umbraco.Infrastructure.HostedServices { @@ -118,10 +116,10 @@ namespace Umbraco.Infrastructure.HostedServices .Distinct() .ToArray(); - IEnumerable checks = _healthChecks + IEnumerable checks = _healthChecks .Where(x => disabledCheckIds.Contains(x.Id) == false); - var results = new HealthCheckResults(checks); + var results = await HealthCheckResults.Create(checks); results.LogResults(); // Send using registered notification methods that are enabled. diff --git a/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs b/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs index 0ec237c6d6..f0acd22230 100644 --- a/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs +++ b/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs @@ -8,9 +8,9 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Core; using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Hosting; using Umbraco.Core.Logging; using Umbraco.Core.Sync; -using Umbraco.Web; namespace Umbraco.Infrastructure.HostedServices { @@ -19,7 +19,7 @@ namespace Umbraco.Infrastructure.HostedServices /// public class KeepAlive : RecurringHostedServiceBase { - private readonly IRequestAccessor _requestAccessor; + private readonly IHostingEnvironment _hostingEnvironment; private readonly IMainDom _mainDom; private readonly KeepAliveSettings _keepAliveSettings; private readonly ILogger _logger; @@ -38,7 +38,7 @@ namespace Umbraco.Infrastructure.HostedServices /// Provider of server registrations to the distributed cache. /// Factory for instances. public KeepAlive( - IRequestAccessor requestAccessor, + IHostingEnvironment hostingEnvironment, IMainDom mainDom, IOptions keepAliveSettings, ILogger logger, @@ -47,7 +47,7 @@ namespace Umbraco.Infrastructure.HostedServices IHttpClientFactory httpClientFactory) : base(TimeSpan.FromMinutes(5), DefaultDelay) { - _requestAccessor = requestAccessor; + _hostingEnvironment = hostingEnvironment; _mainDom = mainDom; _keepAliveSettings = keepAliveSettings.Value; _logger = logger; @@ -88,7 +88,7 @@ namespace Umbraco.Infrastructure.HostedServices { if (keepAlivePingUrl.Contains("{umbracoApplicationUrl}")) { - var umbracoAppUrl = _requestAccessor.GetApplicationUrl().ToString(); + var umbracoAppUrl = _hostingEnvironment.ApplicationMainUrl.ToString(); if (umbracoAppUrl.IsNullOrWhiteSpace()) { _logger.LogWarning("No umbracoApplicationUrl for service (yet), skip."); diff --git a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs index 69f9280fc0..6771705c8e 100644 --- a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs +++ b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs @@ -7,8 +7,8 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Core; using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Hosting; using Umbraco.Core.Services; -using Umbraco.Web; namespace Umbraco.Infrastructure.HostedServices.ServerRegistration { @@ -19,7 +19,7 @@ namespace Umbraco.Infrastructure.HostedServices.ServerRegistration { private readonly IRuntimeState _runtimeState; private readonly IServerRegistrationService _serverRegistrationService; - private readonly IRequestAccessor _requestAccessor; + private readonly IHostingEnvironment _hostingEnvironment; private readonly ILogger _logger; private readonly GlobalSettings _globalSettings; @@ -31,12 +31,17 @@ namespace Umbraco.Infrastructure.HostedServices.ServerRegistration /// Accessor for the current request. /// The typed logger. /// The configuration for global settings. - public TouchServerTask(IRuntimeState runtimeState, IServerRegistrationService serverRegistrationService, IRequestAccessor requestAccessor, ILogger logger, IOptions globalSettings) + public TouchServerTask( + IRuntimeState runtimeState, + IServerRegistrationService serverRegistrationService, + IHostingEnvironment hostingEnvironment, + ILogger logger, + IOptions globalSettings) : base(globalSettings.Value.DatabaseServerRegistrar.WaitTimeBetweenCalls, TimeSpan.FromSeconds(15)) { _runtimeState = runtimeState; _serverRegistrationService = serverRegistrationService ?? throw new ArgumentNullException(nameof(serverRegistrationService)); - _requestAccessor = requestAccessor; + _hostingEnvironment = hostingEnvironment; _logger = logger; _globalSettings = globalSettings.Value; } @@ -48,7 +53,7 @@ namespace Umbraco.Infrastructure.HostedServices.ServerRegistration return Task.CompletedTask; } - var serverAddress = _requestAccessor.GetApplicationUrl()?.ToString(); + var serverAddress = _hostingEnvironment.ApplicationMainUrl?.ToString(); if (serverAddress.IsNullOrWhiteSpace()) { _logger.LogWarning("No umbracoApplicationUrl for service (yet), skip."); diff --git a/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs b/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs index 00f7c80fe4..ec73035dc2 100644 --- a/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs +++ b/src/Umbraco.Infrastructure/Install/FilePermissionHelper.cs @@ -1,18 +1,21 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + using System; using System.Collections.Generic; -using System.Linq; using System.IO; +using System.Linq; using System.Security.AccessControl; +using Microsoft.Extensions.Options; using Umbraco.Core; +using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Hosting; using Umbraco.Core.Install; using Umbraco.Core.IO; -using Umbraco.Web.PublishedCache; -using Umbraco.Core.Configuration.Models; -using Microsoft.Extensions.Options; -using Umbraco.Core.Hosting; -namespace Umbraco.Web.Install +namespace Umbraco.Infrastructure.Install { + /// public class FilePermissionHelper : IFilePermissionHelper { // ensure that these directories exist and Umbraco can write to them @@ -23,40 +26,54 @@ namespace Umbraco.Web.Install private readonly string[] _permissionFiles = Array.Empty(); private readonly GlobalSettings _globalSettings; private readonly IIOHelper _ioHelper; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly IPublishedSnapshotService _publishedSnapshotService; + private string _basePath; - public FilePermissionHelper(IOptions globalSettings, IIOHelper ioHelper, IHostingEnvironment hostingEnvironment, IPublishedSnapshotService publishedSnapshotService) + /// + /// Initializes a new instance of the class. + /// + public FilePermissionHelper(IOptions globalSettings, IIOHelper ioHelper, IHostingEnvironment hostingEnvironment) { _globalSettings = globalSettings.Value; _ioHelper = ioHelper; - _hostingEnvironment = hostingEnvironment; - _publishedSnapshotService = publishedSnapshotService; - _permissionDirs = new[] { _globalSettings.UmbracoCssPath, Constants.SystemDirectories.Config, Constants.SystemDirectories.Data, _globalSettings.UmbracoMediaPath, Constants.SystemDirectories.Preview }; - _packagesPermissionsDirs = new[] { Constants.SystemDirectories.Bin, _globalSettings.UmbracoPath, Constants.SystemDirectories.Packages }; + _basePath = hostingEnvironment.MapPathContentRoot("/"); + _permissionDirs = new[] + { + hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoCssPath), + hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Config), + hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data), + hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoMediaPath), + hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Preview) + }; + _packagesPermissionsDirs = new[] + { + hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Bin), + hostingEnvironment.MapPathContentRoot(_globalSettings.UmbracoPath), + hostingEnvironment.MapPathWebRoot(_globalSettings.UmbracoPath), + hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Packages) + }; } - public bool RunFilePermissionTestSuite(out Dictionary> report) + /// + public bool RunFilePermissionTestSuite(out Dictionary> report) { - report = new Dictionary>(); + report = new Dictionary>(); - if (EnsureDirectories(_permissionDirs, out var errors) == false) - report["Folder creation failed"] = errors.ToList(); + EnsureDirectories(_permissionDirs, out IEnumerable errors); + report[FilePermissionTest.FolderCreation] = errors.ToList(); - if (EnsureDirectories(_packagesPermissionsDirs, out errors) == false) - report["File writing for packages failed"] = errors.ToList(); + EnsureDirectories(_packagesPermissionsDirs, out errors); + report[FilePermissionTest.FileWritingForPackages] = errors.ToList(); - if (EnsureFiles(_permissionFiles, out errors) == false) - report["File writing failed"] = errors.ToList(); + EnsureFiles(_permissionFiles, out errors); + report[FilePermissionTest.FileWriting] = errors.ToList(); - if (EnsureCanCreateSubDirectory(_globalSettings.UmbracoMediaPath, out errors) == false) - report["Media folder creation failed"] = errors.ToList(); + EnsureCanCreateSubDirectory(_globalSettings.UmbracoMediaPath, out errors); + report[FilePermissionTest.MediaFolderCreation] = errors.ToList(); - return report.Count == 0; + return report.Sum(x => x.Value.Count()) == 0; } - /// - public bool EnsureDirectories(string[] dirs, out IEnumerable errors, bool writeCausesRestart = false) + private bool EnsureDirectories(string[] dirs, out IEnumerable errors, bool writeCausesRestart = false) { List temp = null; var success = true; @@ -65,10 +82,17 @@ namespace Umbraco.Web.Install // we don't want to create/ship unnecessary directories, so // here we just ensure we can access the directory, not create it var tryAccess = TryAccessDirectory(dir, !writeCausesRestart); - if (tryAccess) continue; + if (tryAccess) + { + continue; + } - if (temp == null) temp = new List(); - temp.Add(dir); + if (temp == null) + { + temp = new List(); + } + + temp.Add(dir.TrimStart(_basePath)); success = false; } @@ -76,7 +100,7 @@ namespace Umbraco.Web.Install return success; } - public bool EnsureFiles(string[] files, out IEnumerable errors) + private bool EnsureFiles(string[] files, out IEnumerable errors) { List temp = null; var success = true; @@ -93,7 +117,7 @@ namespace Umbraco.Web.Install temp = new List(); } - temp.Add(file); + temp.Add(file.TrimStart(_basePath)); success = false; } @@ -101,21 +125,26 @@ namespace Umbraco.Web.Install return success; } - public bool EnsureCanCreateSubDirectory(string dir, out IEnumerable errors) - { - return EnsureCanCreateSubDirectories(new[] { dir }, out errors); - } + private bool EnsureCanCreateSubDirectory(string dir, out IEnumerable errors) + => EnsureCanCreateSubDirectories(new[] { dir }, out errors); - public bool EnsureCanCreateSubDirectories(IEnumerable dirs, out IEnumerable errors) + private bool EnsureCanCreateSubDirectories(IEnumerable dirs, out IEnumerable errors) { List temp = null; var success = true; foreach (var dir in dirs) { var canCreate = TryCreateSubDirectory(dir); - if (canCreate) continue; + if (canCreate) + { + continue; + } + + if (temp == null) + { + temp = new List(); + } - if (temp == null) temp = new List(); temp.Add(dir); success = false; } @@ -131,7 +160,7 @@ namespace Umbraco.Web.Install { try { - var path = _hostingEnvironment.MapPathContentRoot(dir + "/" + _ioHelper.CreateRandomFileName()); + var path = Path.Combine(dir, _ioHelper.CreateRandomFileName()); Directory.CreateDirectory(path); Directory.Delete(path); return true; @@ -150,14 +179,14 @@ namespace Umbraco.Web.Install // use the ACL APIs to avoid creating files // // if the directory does not exist, do nothing & success - public bool TryAccessDirectory(string dir, bool canWrite) + private bool TryAccessDirectory(string dirPath, bool canWrite) { try { - var dirPath = _hostingEnvironment.MapPathContentRoot(dir); - if (Directory.Exists(dirPath) == false) + { return true; + } if (canWrite) { @@ -166,10 +195,8 @@ namespace Umbraco.Web.Install File.Delete(filePath); return true; } - else - { - return HasWritePermission(dirPath); - } + + return HasWritePermission(dirPath); } catch { @@ -182,14 +209,11 @@ namespace Umbraco.Web.Install var writeAllow = false; var writeDeny = false; var accessControlList = new DirectorySecurity(path, AccessControlSections.Access | AccessControlSections.Owner | AccessControlSections.Group); - if (accessControlList == null) - return false; + AuthorizationRuleCollection accessRules; try { accessRules = accessControlList.GetAccessRules(true, true, typeof(System.Security.Principal.SecurityIdentifier)); - if (accessRules == null) - return false; } catch (Exception) { @@ -202,12 +226,18 @@ namespace Umbraco.Web.Install foreach (FileSystemAccessRule rule in accessRules) { if ((FileSystemRights.Write & rule.FileSystemRights) != FileSystemRights.Write) + { continue; + } if (rule.AccessControlType == AccessControlType.Allow) + { writeAllow = true; + } else if (rule.AccessControlType == AccessControlType.Deny) + { writeDeny = true; + } } return writeAllow && writeDeny == false; @@ -219,7 +249,7 @@ namespace Umbraco.Web.Install { try { - var path = _hostingEnvironment.MapPathContentRoot(file); + var path = file; File.AppendText(path).Close(); return true; } diff --git a/src/Umbraco.Infrastructure/Install/InstallStepCollection.cs b/src/Umbraco.Infrastructure/Install/InstallStepCollection.cs index 4b352190b0..c8be2fc5a9 100644 --- a/src/Umbraco.Infrastructure/Install/InstallStepCollection.cs +++ b/src/Umbraco.Infrastructure/Install/InstallStepCollection.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Install.InstallSteps; using Umbraco.Web.Install.InstallSteps; using Umbraco.Web.Install.Models; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/GridConfiguration.cs b/src/Umbraco.Infrastructure/PropertyEditors/GridConfiguration.cs index 74ea517fa2..d00f1b5e18 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/GridConfiguration.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/GridConfiguration.cs @@ -22,7 +22,7 @@ namespace Umbraco.Web.PropertyEditors Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] public bool IgnoreUserStartNodes { get; set; } - [ConfigurationField("mediaParentId", "Image Upload Folder", "MediaFolderPicker", + [ConfigurationField("mediaParentId", "Image Upload Folder", "mediafolderpicker", Description = "Choose the upload location of pasted images")] public GuidUdi MediaParentId { get; set; } } diff --git a/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs b/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs index cf5235387d..eafc006c26 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/LiveModelsProvider.cs @@ -117,7 +117,7 @@ namespace Umbraco.ModelsBuilder.Embedded public void Handle(UmbracoRequestEnd notification) { - if (IsEnabled && _mainDom.IsMainDom && !notification.HttpContext.Request.IsClientSideRequest()) + if (IsEnabled && _mainDom.IsMainDom) { GenerateModelsIfRequested(); } diff --git a/src/Umbraco.Tests.Integration/Implementations/TestHelper.cs b/src/Umbraco.Tests.Integration/Implementations/TestHelper.cs index 4c698b221d..8bdca33561 100644 --- a/src/Umbraco.Tests.Integration/Implementations/TestHelper.cs +++ b/src/Umbraco.Tests.Integration/Implementations/TestHelper.cs @@ -141,6 +141,7 @@ namespace Umbraco.Tests.Integration.Implementations public override IHostingEnvironment GetHostingEnvironment() => _hostingEnvironment ??= new TestHostingEnvironment( GetIOptionsMonitorOfHostingSettings(), + GetIOptionsMonitorOfWebRoutingSettings(), _hostEnvironment); private IOptionsMonitor GetIOptionsMonitorOfHostingSettings() @@ -149,6 +150,12 @@ namespace Umbraco.Tests.Integration.Implementations return Mock.Of>(x => x.CurrentValue == hostingSettings); } + private IOptionsMonitor GetIOptionsMonitorOfWebRoutingSettings() + { + var webRoutingSettings = new WebRoutingSettings(); + return Mock.Of>(x => x.CurrentValue == webRoutingSettings); + } + public override IApplicationShutdownRegistry GetHostingEnvironmentLifetime() => _hostingLifetime; public override IIpResolver GetIpResolver() => _ipResolver; diff --git a/src/Umbraco.Tests.Integration/Implementations/TestHostingEnvironment.cs b/src/Umbraco.Tests.Integration/Implementations/TestHostingEnvironment.cs index 2a91b6db83..8690a5f6f8 100644 --- a/src/Umbraco.Tests.Integration/Implementations/TestHostingEnvironment.cs +++ b/src/Umbraco.Tests.Integration/Implementations/TestHostingEnvironment.cs @@ -11,8 +11,8 @@ namespace Umbraco.Tests.Integration.Implementations { public class TestHostingEnvironment : AspNetCoreHostingEnvironment, IHostingEnvironment { - public TestHostingEnvironment(IOptionsMonitor hostingSettings, IWebHostEnvironment webHostEnvironment) - : base(hostingSettings, webHostEnvironment) + public TestHostingEnvironment(IOptionsMonitor hostingSettings,IOptionsMonitor webRoutingSettings, IWebHostEnvironment webHostEnvironment) + : base(hostingSettings,webRoutingSettings, webHostEnvironment) { } diff --git a/src/Umbraco.Tests.Integration/TestServerTest/Controllers/EnsureNotAmbiguousActionNameControllerTests.cs b/src/Umbraco.Tests.Integration/TestServerTest/Controllers/EnsureNotAmbiguousActionNameControllerTests.cs index b0aa42730e..15862ae24f 100644 --- a/src/Umbraco.Tests.Integration/TestServerTest/Controllers/EnsureNotAmbiguousActionNameControllerTests.cs +++ b/src/Umbraco.Tests.Integration/TestServerTest/Controllers/EnsureNotAmbiguousActionNameControllerTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; @@ -21,79 +21,79 @@ namespace Umbraco.Tests.Integration.TestServerTest.Controllers Assert.Multiple(() => { - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(intId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(guidId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(udiId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetNiceUrl(intId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetNiceUrl(guidId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetNiceUrl(udiId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetEmpty("test", 0))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetChildren(intId, string.Empty, 0, 0, "SortOrder", Direction.Ascending, true, string.Empty, string.Empty))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(intId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(guidId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(udiId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetNiceUrl(intId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetNiceUrl(guidId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetNiceUrl(udiId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetEmpty("test", 0))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetChildren(intId, string.Empty, 0, 0, "SortOrder", Direction.Ascending, true, string.Empty, string.Empty))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(intId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(guidId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(udiId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(intId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(guidId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(udiId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(intId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(guidId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(udiId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(intId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(guidId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(udiId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(intId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(guidId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(udiId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(intId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(guidId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(udiId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetPath(intId, UmbracoEntityTypes.Document))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetPath(guidId, UmbracoEntityTypes.Document))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetPath(udiId, UmbracoEntityTypes.Document))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetUrl(intId, UmbracoEntityTypes.Document, null))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetUrl(udiId, null))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetUrlAndAnchors(intId, null))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetUrlAndAnchors(udiId, null))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(intId, UmbracoEntityTypes.Document))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(guidId, UmbracoEntityTypes.Document))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(udiId, UmbracoEntityTypes.Document))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetByIds(new Guid[0], UmbracoEntityTypes.Document))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetByIds(new Udi[0], UmbracoEntityTypes.Document))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetByIds(new int[0], UmbracoEntityTypes.Document))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetPagedChildren(intId, UmbracoEntityTypes.Document, 0, 1, "SortOrder", Direction.Ascending, string.Empty, null))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetPagedChildren(guidId.ToString(), UmbracoEntityTypes.Document, 0, 1, "SortOrder", Direction.Ascending, string.Empty, null))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetPagedChildren(udiId.ToString(), UmbracoEntityTypes.Document, 0, 1, "SortOrder", Direction.Ascending, string.Empty, null))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetPath(intId, UmbracoEntityTypes.Document))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetPath(guidId, UmbracoEntityTypes.Document))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetPath(udiId, UmbracoEntityTypes.Document))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetUrl(intId, UmbracoEntityTypes.Document, null))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetUrl(udiId, null))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetUrlAndAnchors(intId, null))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetUrlAndAnchors(udiId, null))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(intId, UmbracoEntityTypes.Document))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(guidId, UmbracoEntityTypes.Document))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(udiId, UmbracoEntityTypes.Document))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetByIds(new Guid[0], UmbracoEntityTypes.Document))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetByIds(new Udi[0], UmbracoEntityTypes.Document))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetByIds(new int[0], UmbracoEntityTypes.Document))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetPagedChildren(intId, UmbracoEntityTypes.Document, 0, 1, "SortOrder", Direction.Ascending, string.Empty, null))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetPagedChildren(guidId.ToString(), UmbracoEntityTypes.Document, 0, 1, "SortOrder", Direction.Ascending, string.Empty, null))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetPagedChildren(udiId.ToString(), UmbracoEntityTypes.Document, 0, 1, "SortOrder", Direction.Ascending, string.Empty, null))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetIcon(string.Empty))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetIcon(string.Empty))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(intId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(guidId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(udiId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(intId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(guidId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(udiId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(intId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(guidId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(udiId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetChildren(intId, 0, 1, "SortOrder", Direction.Ascending, true, string.Empty))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetChildren(guidId, 0, 1, "SortOrder", Direction.Ascending, true, string.Empty))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetChildren(udiId, 0, 1, "SortOrder", Direction.Ascending, true, string.Empty))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(intId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(guidId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(udiId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetChildren(intId, 0, 1, "SortOrder", Direction.Ascending, true, string.Empty))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetChildren(guidId, 0, 1, "SortOrder", Direction.Ascending, true, string.Empty))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetChildren(udiId, 0, 1, "SortOrder", Direction.Ascending, true, string.Empty))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(intId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(guidId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(udiId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetAllowedChildren(intId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetAllowedChildren(guidId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetAllowedChildren(udiId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(intId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(guidId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(udiId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetAllowedChildren(intId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetAllowedChildren(guidId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetAllowedChildren(udiId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(intId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(guidId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(udiId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(intId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(guidId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(udiId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(intId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(guidId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(udiId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(intId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(guidId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(udiId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(intId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(guidId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(udiId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(intId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(guidId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(udiId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(intId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(guidId))); - EnsureNotAmbiguousActionName(PrepareUrl(x => x.GetById(udiId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(intId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(guidId))); + EnsureNotAmbiguousActionName(PrepareApiControllerUrl(x => x.GetById(udiId))); }); } diff --git a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index aee3d1e1be..21057279ea 100644 --- a/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/src/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -19,7 +19,6 @@ using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.DependencyInjection; using Umbraco.Extensions; -using Umbraco.Infrastructure.PublishedCache.DependencyInjection; using Umbraco.Tests.Integration.DependencyInjection; using Umbraco.Tests.Integration.Testing; using Umbraco.Tests.Testing; @@ -84,11 +83,32 @@ namespace Umbraco.Tests.Integration.TestServerTest /// This returns the url but also sets the HttpContext.request into to use this url. /// /// The string URL of the controller action. - protected string PrepareUrl(Expression> methodSelector) + protected string PrepareApiControllerUrl(Expression> methodSelector) where T : UmbracoApiController { - string url = LinkGenerator.GetUmbracoApiService(methodSelector); + string url = LinkGenerator.GetUmbracoApiService(methodSelector); + return PrepareUrl(url); + } + /// + /// Prepare a url before using . + /// This returns the url but also sets the HttpContext.request into to use this url. + /// + /// The string URL of the controller action. + protected string PrepareSurfaceControllerUrl(Expression> methodSelector) + where T : SurfaceController + { + string url = LinkGenerator.GetUmbracoSurfaceUrl(methodSelector); + return PrepareUrl(url); + } + + /// + /// Prepare a url before using . + /// This returns the url but also sets the HttpContext.request into to use this url. + /// + /// The string URL of the controller action. + protected string PrepareUrl(string url) + { IBackOfficeSecurityFactory backofficeSecurityFactory = GetRequiredService(); IUmbracoContextFactory umbracoContextFactory = GetRequiredService(); IHttpContextAccessor httpContextAccessor = GetRequiredService(); @@ -151,6 +171,9 @@ namespace Umbraco.Tests.Integration.TestServerTest // Adds Umbraco.Web.Website mvcBuilder.AddApplicationPart(typeof(SurfaceController).Assembly); + + // Adds Umbraco.Tests.Integration + mvcBuilder.AddApplicationPart(typeof(UmbracoTestServerTestBase).Assembly); }) .AddWebServer() .AddWebsite() diff --git a/src/Umbraco.Tests.Integration/TestServerTest/Controllers/BackOfficeAssetsControllerTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/BackOfficeAssetsControllerTests.cs similarity index 72% rename from src/Umbraco.Tests.Integration/TestServerTest/Controllers/BackOfficeAssetsControllerTests.cs rename to src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/BackOfficeAssetsControllerTests.cs index f519d939b8..9304f005b3 100644 --- a/src/Umbraco.Tests.Integration/TestServerTest/Controllers/BackOfficeAssetsControllerTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/BackOfficeAssetsControllerTests.cs @@ -5,9 +5,10 @@ using System.Net; using System.Net.Http; using System.Threading.Tasks; using NUnit.Framework; +using Umbraco.Tests.Integration.TestServerTest; using Umbraco.Web.BackOffice.Controllers; -namespace Umbraco.Tests.Integration.TestServerTest.Controllers +namespace Umbraco.Tests.Integration.Umbraco.Web.BackOffice.Controllers { [TestFixture] public class BackOfficeAssetsControllerTests : UmbracoTestServerTestBase @@ -16,7 +17,7 @@ namespace Umbraco.Tests.Integration.TestServerTest.Controllers public async Task EnsureSuccessStatusCode() { // Arrange - string url = PrepareUrl(x => x.GetSupportedLocales()); + string url = PrepareApiControllerUrl(x => x.GetSupportedLocales()); // Act HttpResponseMessage response = await Client.GetAsync(url); diff --git a/src/Umbraco.Tests.Integration/TestServerTest/Controllers/ContentControllerTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/ContentControllerTests.cs similarity index 95% rename from src/Umbraco.Tests.Integration/TestServerTest/Controllers/ContentControllerTests.cs rename to src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/ContentControllerTests.cs index cd7095ba75..a353cbdc7f 100644 --- a/src/Umbraco.Tests.Integration/TestServerTest/Controllers/ContentControllerTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/ContentControllerTests.cs @@ -12,11 +12,12 @@ using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Tests.Common.Builders; using Umbraco.Tests.Common.Builders.Extensions; +using Umbraco.Tests.Integration.TestServerTest; using Umbraco.Web.BackOffice.Controllers; using Umbraco.Web.Common.Formatters; using Umbraco.Web.Models.ContentEditing; -namespace Umbraco.Tests.Integration.TestServerTest.Controllers +namespace Umbraco.Tests.Integration.Umbraco.Web.BackOffice.Controllers { [TestFixture] public class ContentControllerTests : UmbracoTestServerTestBase @@ -35,7 +36,7 @@ namespace Umbraco.Tests.Integration.TestServerTest.Controllers .WithIsDefault(false) .Build()); - string url = PrepareUrl(x => x.PostSave(null)); + string url = PrepareApiControllerUrl(x => x.PostSave(null)); IContentService contentService = GetRequiredService(); IContentTypeService contentTypeService = GetRequiredService(); @@ -93,7 +94,7 @@ namespace Umbraco.Tests.Integration.TestServerTest.Controllers .WithIsDefault(false) .Build()); - string url = PrepareUrl(x => x.PostSave(null)); + string url = PrepareApiControllerUrl(x => x.PostSave(null)); IContentTypeService contentTypeService = GetRequiredService(); @@ -162,7 +163,7 @@ namespace Umbraco.Tests.Integration.TestServerTest.Controllers .WithIsDefault(false) .Build()); - string url = PrepareUrl(x => x.PostSave(null)); + string url = PrepareApiControllerUrl(x => x.PostSave(null)); IContentService contentService = GetRequiredService(); IContentTypeService contentTypeService = GetRequiredService(); @@ -227,7 +228,7 @@ namespace Umbraco.Tests.Integration.TestServerTest.Controllers .WithIsDefault(false) .Build()); - string url = PrepareUrl(x => x.PostSave(null)); + string url = PrepareApiControllerUrl(x => x.PostSave(null)); IContentService contentService = GetRequiredService(); IContentTypeService contentTypeService = GetRequiredService(); @@ -288,7 +289,7 @@ namespace Umbraco.Tests.Integration.TestServerTest.Controllers .WithIsDefault(false) .Build()); - string url = PrepareUrl(x => x.PostSave(null)); + string url = PrepareApiControllerUrl(x => x.PostSave(null)); IContentService contentService = GetRequiredService(); IContentTypeService contentTypeService = GetRequiredService(); @@ -352,7 +353,7 @@ namespace Umbraco.Tests.Integration.TestServerTest.Controllers .WithIsDefault(false) .Build()); - string url = PrepareUrl(x => x.PostSave(null)); + string url = PrepareApiControllerUrl(x => x.PostSave(null)); IContentService contentService = GetRequiredService(); IContentTypeService contentTypeService = GetRequiredService(); diff --git a/src/Umbraco.Tests.Integration/TestServerTest/Controllers/TemplateQueryControllerTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/TemplateQueryControllerTests.cs similarity index 88% rename from src/Umbraco.Tests.Integration/TestServerTest/Controllers/TemplateQueryControllerTests.cs rename to src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/TemplateQueryControllerTests.cs index 8a558c53d4..10478f1078 100644 --- a/src/Umbraco.Tests.Integration/TestServerTest/Controllers/TemplateQueryControllerTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/TemplateQueryControllerTests.cs @@ -11,12 +11,13 @@ using Newtonsoft.Json.Linq; using NUnit.Framework; using Umbraco.Core; using Umbraco.Tests.Common.Builders.Extensions; +using Umbraco.Tests.Integration.TestServerTest; using Umbraco.Tests.Testing; using Umbraco.Web.BackOffice.Controllers; using Umbraco.Web.Common.Formatters; using Umbraco.Web.Models.TemplateQuery; -namespace Umbraco.Tests.Integration.TestServerTest.Controllers +namespace Umbraco.Tests.Integration.Umbraco.Web.BackOffice.Controllers { [TestFixture] public class TemplateQueryControllerTests : UmbracoTestServerTestBase @@ -24,7 +25,7 @@ namespace Umbraco.Tests.Integration.TestServerTest.Controllers [Test] public async Task GetContentTypes__Ensure_camel_case() { - string url = PrepareUrl(x => x.GetContentTypes()); + string url = PrepareApiControllerUrl(x => x.GetContentTypes()); // Act HttpResponseMessage response = await Client.GetAsync(url); diff --git a/src/Umbraco.Tests.Integration/TestServerTest/Controllers/UsersControllerTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/UsersControllerTests.cs similarity index 90% rename from src/Umbraco.Tests.Integration/TestServerTest/Controllers/UsersControllerTests.cs rename to src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/UsersControllerTests.cs index fbba385cdc..4814d158af 100644 --- a/src/Umbraco.Tests.Integration/TestServerTest/Controllers/UsersControllerTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/Controllers/UsersControllerTests.cs @@ -17,13 +17,14 @@ using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; using Umbraco.Tests.Common.Builders; using Umbraco.Tests.Common.Builders.Extensions; +using Umbraco.Tests.Integration.TestServerTest; using Umbraco.Tests.Testing; using Umbraco.Web.BackOffice.Controllers; using Umbraco.Web.Common.Filters; using Umbraco.Web.Common.Formatters; using Umbraco.Web.Models.ContentEditing; -namespace Umbraco.Tests.Integration.TestServerTest.Controllers +namespace Umbraco.Tests.Integration.Umbraco.Web.BackOffice.Controllers { [TestFixture] public class UsersControllerTests : UmbracoTestServerTestBase @@ -31,7 +32,7 @@ namespace Umbraco.Tests.Integration.TestServerTest.Controllers [Test] public async Task Save_User() { - string url = PrepareUrl(x => x.PostSaveUser(null)); + string url = PrepareApiControllerUrl(x => x.PostSaveUser(null)); IUserService userService = GetRequiredService(); @@ -81,7 +82,7 @@ namespace Umbraco.Tests.Integration.TestServerTest.Controllers public async Task GetPagedUsers_Empty() { // We get page 2 to force an empty response because there always in the useradmin user - string url = PrepareUrl(x => x.GetPagedUsers(2, 10, "username", Direction.Ascending, null, null, string.Empty)); + string url = PrepareApiControllerUrl(x => x.GetPagedUsers(2, 10, "username", Direction.Ascending, null, null, string.Empty)); // Act HttpResponseMessage response = await Client.GetAsync(url); @@ -106,7 +107,7 @@ namespace Umbraco.Tests.Integration.TestServerTest.Controllers { int totalNumberOfUsers = 11; int pageSize = totalNumberOfUsers - 1; - string url = PrepareUrl(x => x.GetPagedUsers(1, pageSize, "username", Direction.Ascending, null, null, string.Empty)); + string url = PrepareApiControllerUrl(x => x.GetPagedUsers(1, pageSize, "username", Direction.Ascending, null, null, string.Empty)); IUserService userService = GetRequiredService(); @@ -144,7 +145,7 @@ namespace Umbraco.Tests.Integration.TestServerTest.Controllers [Test] public async Task PostUnlockUsers_When_UserIds_Not_Supplied_Expect_Ok_Response() { - string url = PrepareUrl(x => x.PostUnlockUsers(Array.Empty())); + string url = PrepareApiControllerUrl(x => x.PostUnlockUsers(Array.Empty())); // Act HttpResponseMessage response = await Client.PostAsync(url, new StringContent(string.Empty)); @@ -156,7 +157,7 @@ namespace Umbraco.Tests.Integration.TestServerTest.Controllers public async Task PostUnlockUsers_When_User_Does_Not_Exist_Expect_Zero_Users_Message() { int userId = 42; // Must not exist - string url = PrepareUrl(x => x.PostUnlockUsers(new[] { userId })); + string url = PrepareApiControllerUrl(x => x.PostUnlockUsers(new[] { userId })); // Act HttpResponseMessage response = await Client.PostAsync(url, new StringContent(string.Empty)); @@ -184,7 +185,7 @@ namespace Umbraco.Tests.Integration.TestServerTest.Controllers .Build(); userService.Save(user); - string url = PrepareUrl(x => x.PostUnlockUsers(new[] { user.Id })); + string url = PrepareApiControllerUrl(x => x.PostUnlockUsers(new[] { user.Id })); // Act HttpResponseMessage response = await Client.PostAsync(url, new StringContent(string.Empty)); @@ -228,7 +229,7 @@ namespace Umbraco.Tests.Integration.TestServerTest.Controllers userService.Save(user); } - string url = PrepareUrl(x => x.PostUnlockUsers(users.Select(x => x.Id).ToArray())); + string url = PrepareApiControllerUrl(x => x.PostUnlockUsers(users.Select(x => x.Id).ToArray())); // Act HttpResponseMessage response = await Client.PostAsync(url, new StringContent(string.Empty)); diff --git a/src/Umbraco.Tests.Integration/Umbraco.Web.Website/Routing/FrontEndRouteTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Web.Website/Routing/FrontEndRouteTests.cs new file mode 100644 index 0000000000..a7cbc1646b --- /dev/null +++ b/src/Umbraco.Tests.Integration/Umbraco.Web.Website/Routing/FrontEndRouteTests.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApplicationParts; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Umbraco.Core.Cache; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; +using Umbraco.Core.Services; +using Umbraco.Tests.Integration.TestServerTest; +using Umbraco.Web; +using Umbraco.Web.Routing; +using Umbraco.Web.Website.Controllers; + +namespace Umbraco.Tests.Integration.Umbraco.Web.Website.Routing +{ + [TestFixture] + public class SurfaceControllerTests : UmbracoTestServerTestBase + { + [Test] + public async Task Auto_Routes_For_Default_Action() + { + string url = PrepareSurfaceControllerUrl(x => x.Index()); + + // Act + HttpResponseMessage response = await Client.GetAsync(url); + + string body = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode); + } + + [Test] + public async Task Auto_Routes_For_Custom_Action() + { + string url = PrepareSurfaceControllerUrl(x => x.News()); + + // Act + HttpResponseMessage response = await Client.GetAsync(url); + + string body = await response.Content.ReadAsStringAsync(); + + // Assert + Assert.AreEqual(HttpStatusCode.Forbidden, response.StatusCode); + } + } + + // Test controllers must be non-nested, else we need to jump through some hoops with custom + // IApplicationFeatureProvider + // For future notes if we want this, some example code of this is here + // https://tpodolak.com/blog/2020/06/22/asp-net-core-adding-controllers-directly-integration-tests/ + public class TestSurfaceController : SurfaceController + { + public TestSurfaceController(IUmbracoContextAccessor umbracoContextAccessor, IUmbracoDatabaseFactory databaseFactory, ServiceContext services, AppCaches appCaches, IProfilingLogger profilingLogger, IPublishedUrlProvider publishedUrlProvider) + : base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider) + { + } + + public IActionResult Index() => Ok(); + + public IActionResult News() => Forbid(); + } +} diff --git a/src/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs b/src/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs index b23ccc9080..9da0f84202 100644 --- a/src/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs +++ b/src/Umbraco.Tests.UnitTests/TestHelpers/TestHelper.cs @@ -71,6 +71,7 @@ namespace Umbraco.Tests.TestHelpers var testPath = TestContext.CurrentContext.TestDirectory.Split("bin")[0]; return new AspNetCoreHostingEnvironment( Mock.Of>(x => x.CurrentValue == new HostingSettings()), + Mock.Of>(x => x.CurrentValue == new WebRoutingSettings()), Mock.Of( x => x.WebRootPath == "/" && diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/UriExtensionsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/UriExtensionsTests.cs index fc8ecd0474..fa5ff0df0b 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/UriExtensionsTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/UriExtensionsTests.cs @@ -28,8 +28,10 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.Extensions private AspNetCoreHostingEnvironment CreateHostingEnvironment(string virtualPath = "") { var hostingSettings = new HostingSettings { ApplicationVirtualPath = virtualPath }; + var webRoutingSettings = new WebRoutingSettings(); var mockedOptionsMonitorOfHostingSettings = Mock.Of>(x => x.CurrentValue == hostingSettings); - return new AspNetCoreHostingEnvironment(mockedOptionsMonitorOfHostingSettings, _hostEnvironment); + var mockedOptionsMonitorOfWebRoutingSettings = Mock.Of>(x => x.CurrentValue == webRoutingSettings); + return new AspNetCoreHostingEnvironment(mockedOptionsMonitorOfHostingSettings, mockedOptionsMonitorOfWebRoutingSettings, _hostEnvironment); } [TestCase("http://www.domain.com/foo/bar", "/", "http://www.domain.com/")] diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UmbracoRequestPathsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UmbracoRequestPathsTests.cs index 24f0b04080..57015f30ea 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UmbracoRequestPathsTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/UmbracoRequestPathsTests.cs @@ -25,9 +25,12 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.Routing private AspNetCoreHostingEnvironment CreateHostingEnvironment(string virtualPath = "") { + var hostingSettings = new HostingSettings { ApplicationVirtualPath = virtualPath }; + var webRoutingSettings = new WebRoutingSettings(); var mockedOptionsMonitorOfHostingSettings = Mock.Of>(x => x.CurrentValue == hostingSettings); - return new AspNetCoreHostingEnvironment(mockedOptionsMonitorOfHostingSettings, _hostEnvironment); + var mockedOptionsMonitorOfWebRoutingSettings = Mock.Of>(x => x.CurrentValue == webRoutingSettings); + return new AspNetCoreHostingEnvironment(mockedOptionsMonitorOfHostingSettings, mockedOptionsMonitorOfWebRoutingSettings, _hostEnvironment); } [TestCase("/favicon.ico", true)] @@ -65,7 +68,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.Routing [TestCase("http://www.domain.com/Umbraco/", "", true)] [TestCase("http://www.domain.com/umbraco/default.aspx", "", true)] [TestCase("http://www.domain.com/umbraco/test/test", "", false)] - [TestCase("http://www.domain.com/umbraco/test/test/test", "", false)] + [TestCase("http://www.domain.com/umbraco/test/test/test", "", false)] [TestCase("http://www.domain.com/umbrac", "", false)] [TestCase("http://www.domain.com/test", "", false)] [TestCase("http://www.domain.com/test/umbraco", "", false)] @@ -84,7 +87,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.Routing var umbracoRequestPaths = new UmbracoRequestPaths(Options.Create(_globalSettings), hostingEnvironment); Assert.AreEqual(expected, umbracoRequestPaths.IsBackOfficeRequest(source.AbsolutePath)); } - + [TestCase("http://www.domain.com/install", true)] [TestCase("http://www.domain.com/Install/", true)] [TestCase("http://www.domain.com/install/default.aspx", true)] diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HealthChecks/HealthCheckResultsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HealthChecks/HealthCheckResultsTests.cs index 10349a4f9e..1c20c384df 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HealthChecks/HealthCheckResultsTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HealthChecks/HealthCheckResultsTests.cs @@ -3,9 +3,9 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using NUnit.Framework; -using Umbraco.Core.HealthCheck; -using Umbraco.Infrastructure.HealthCheck; +using Umbraco.Core.HealthChecks; namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.HealthChecks { @@ -26,7 +26,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.HealthChecks public override HealthCheckStatus ExecuteAction(HealthCheckAction action) => throw new NotImplementedException(); - public override IEnumerable GetStatus() => + public override async Task> GetStatus() => new List { new HealthCheckStatus(_message) @@ -62,18 +62,18 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.HealthChecks { } - public override IEnumerable GetStatus() => throw new Exception("Check threw exception"); + public override async Task> GetStatus() => throw new Exception("Check threw exception"); } [Test] - public void HealthCheckResults_WithSuccessfulChecks_ReturnsCorrectResultDescription() + public async Task HealthCheckResults_WithSuccessfulChecks_ReturnsCorrectResultDescription() { var checks = new List { new StubHealthCheck1(StatusResultType.Success, "First check was successful"), new StubHealthCheck2(StatusResultType.Success, "Second check was successful"), }; - var results = new HealthCheckResults(checks); + var results = await HealthCheckResults.Create(checks); Assert.IsTrue(results.AllChecksSuccessful); @@ -83,14 +83,14 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.HealthChecks } [Test] - public void HealthCheckResults_WithFailingChecks_ReturnsCorrectResultDescription() + public async Task HealthCheckResults_WithFailingChecks_ReturnsCorrectResultDescription() { var checks = new List { new StubHealthCheck1(StatusResultType.Success, "First check was successful"), new StubHealthCheck2(StatusResultType.Error, "Second check was not successful"), }; - var results = new HealthCheckResults(checks); + var results = await HealthCheckResults.Create(checks); Assert.IsFalse(results.AllChecksSuccessful); @@ -100,7 +100,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.HealthChecks } [Test] - public void HealthCheckResults_WithErroringCheck_ReturnsCorrectResultDescription() + public async Task HealthCheckResults_WithErroringCheck_ReturnsCorrectResultDescription() { var checks = new List { @@ -108,7 +108,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.HealthChecks new StubHealthCheck3(StatusResultType.Error, "Third check was not successful"), new StubHealthCheck2(StatusResultType.Error, "Second check was not successful"), }; - var results = new HealthCheckResults(checks); + var results = await HealthCheckResults.Create(checks); Assert.IsFalse(results.AllChecksSuccessful); @@ -119,28 +119,28 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.HealthChecks } [Test] - public void HealthCheckResults_WithSummaryVerbosity_ReturnsCorrectResultDescription() + public async Task HealthCheckResults_WithSummaryVerbosity_ReturnsCorrectResultDescription() { var checks = new List { new StubHealthCheck1(StatusResultType.Success, "First check was successful"), new StubHealthCheck2(StatusResultType.Success, "Second check was successful"), }; - var results = new HealthCheckResults(checks); + var results = await HealthCheckResults.Create(checks); var resultAsMarkdown = results.ResultsAsMarkDown(HealthCheckNotificationVerbosity.Summary); Assert.IsTrue(resultAsMarkdown.IndexOf("Result: 'Success'" + Environment.NewLine) > -1); } [Test] - public void HealthCheckResults_WithDetailedVerbosity_ReturnsCorrectResultDescription() + public async Task HealthCheckResults_WithDetailedVerbosity_ReturnsCorrectResultDescription() { var checks = new List { new StubHealthCheck1(StatusResultType.Success, "First check was successful"), new StubHealthCheck2(StatusResultType.Success, "Second check was successful"), }; - var results = new HealthCheckResults(checks); + var results = await HealthCheckResults.Create(checks); var resultAsMarkdown = results.ResultsAsMarkDown(HealthCheckNotificationVerbosity.Detailed); Assert.IsFalse(resultAsMarkdown.IndexOf("Result: 'Success'" + Environment.NewLine) > -1); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/HealthCheckNotifierTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/HealthCheckNotifierTests.cs index d5bd10fe3c..325aa4f74b 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/HealthCheckNotifierTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/HealthCheckNotifierTests.cs @@ -12,14 +12,12 @@ using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.Models; -using Umbraco.Core.HealthCheck; +using Umbraco.Core.HealthChecks; +using Umbraco.Core.HealthChecks.NotificationMethods; using Umbraco.Core.Logging; using Umbraco.Core.Scoping; using Umbraco.Core.Sync; -using Umbraco.Infrastructure.HealthCheck; using Umbraco.Infrastructure.HostedServices; -using Umbraco.Web.HealthCheck; -using Umbraco.Web.HealthCheck.NotificationMethods; namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.HostedServices { @@ -186,7 +184,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.HostedServices { public override HealthCheckStatus ExecuteAction(HealthCheckAction action) => new HealthCheckStatus("Check message"); - public override IEnumerable GetStatus() => Enumerable.Empty(); + public override async Task> GetStatus() => Enumerable.Empty(); } } } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/KeepAliveTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/KeepAliveTests.cs index 752da01f0f..8eca24a724 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/KeepAliveTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/KeepAliveTests.cs @@ -13,11 +13,11 @@ using Moq.Protected; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Hosting; using Umbraco.Core.Logging; using Umbraco.Core.Scoping; using Umbraco.Core.Sync; using Umbraco.Infrastructure.HostedServices; -using Umbraco.Web; namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.HostedServices { @@ -78,8 +78,8 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.HostedServices DisableKeepAliveTask = !enabled, }; - var mockRequestAccessor = new Mock(); - mockRequestAccessor.Setup(x => x.GetApplicationUrl()).Returns(new Uri(ApplicationUrl)); + var mockRequestAccessor = new Mock(); + mockRequestAccessor.SetupGet(x => x.ApplicationMainUrl).Returns(new Uri(ApplicationUrl)); var mockServerRegistrar = new Mock(); mockServerRegistrar.Setup(x => x.CurrentServerRole).Returns(serverRole); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTaskTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTaskTests.cs index d293a5b7e8..5b97c36d16 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTaskTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTaskTests.cs @@ -9,9 +9,9 @@ using Moq; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Hosting; using Umbraco.Core.Services; using Umbraco.Infrastructure.HostedServices.ServerRegistration; -using Umbraco.Web; namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.HostedServices.ServerRegistration { @@ -53,8 +53,8 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.HostedServices.ServerRe private TouchServerTask CreateTouchServerTask(RuntimeLevel runtimeLevel = RuntimeLevel.Run, string applicationUrl = ApplicationUrl) { - var mockRequestAccessor = new Mock(); - mockRequestAccessor.Setup(x => x.GetApplicationUrl()).Returns(!string.IsNullOrEmpty(applicationUrl) ? new Uri(ApplicationUrl) : null); + var mockRequestAccessor = new Mock(); + mockRequestAccessor.SetupGet(x => x.ApplicationMainUrl).Returns(!string.IsNullOrEmpty(applicationUrl) ? new Uri(ApplicationUrl) : null); var mockRunTimeState = new Mock(); mockRunTimeState.SetupGet(x => x.Level).Returns(runtimeLevel); @@ -62,7 +62,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Infrastructure.HostedServices.ServerRe var mockLogger = new Mock>(); _mockServerRegistrationService = new Mock(); - + var settings = new GlobalSettings { DatabaseServerRegistrar = new DatabaseServerRegistrarSettings diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/ContentModelBinderTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/ContentModelBinderTests.cs index 01b6032c36..837a0059f4 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/ContentModelBinderTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ModelBinders/ContentModelBinderTests.cs @@ -60,22 +60,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.ModelBinders // Arrange IPublishedContent pc = CreatePublishedContent(); ModelBindingContext bindingContext = CreateBindingContextForUmbracoRequest(typeof(ContentModel), pc); - bindingContext.ActionContext.RouteData.Values.Remove(Constants.Web.UmbracoRouteDefinitionDataToken); - - // Act - await _contentModelBinder.BindModelAsync(bindingContext); - - // Assert - Assert.False(bindingContext.Result.IsModelSet); - } - - [Test] - public async Task Does_Not_Bind_Model_When_UmbracoToken_Has_Incorrect_Model() - { - // Arrange - IPublishedContent pc = CreatePublishedContent(); - ModelBindingContext bindingContext = CreateBindingContextForUmbracoRequest(typeof(ContentModel), pc); - bindingContext.ActionContext.RouteData.Values[Constants.Web.UmbracoRouteDefinitionDataToken] = new NonContentModel(); + bindingContext.ActionContext.HttpContext.Features.Set(null); // Act await _contentModelBinder.BindModelAsync(bindingContext); @@ -220,9 +205,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.ModelBinders var httpContext = new DefaultHttpContext(); var routeData = new RouteData(); - routeData.Values.Add(Constants.Web.UmbracoRouteDefinitionDataToken, new UmbracoRouteValues(publishedRequest)); - { - } + httpContext.Features.Set(new UmbracoRouteValues(publishedRequest, null)); var actionContext = new ActionContext(httpContext, routeData, new ActionDescriptor()); var metadataProvider = new EmptyModelMetadataProvider(); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Routing/BackOfficeAreaRoutesTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Routing/BackOfficeAreaRoutesTests.cs index e221e88dd1..f501305b67 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Routing/BackOfficeAreaRoutesTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Routing/BackOfficeAreaRoutesTests.cs @@ -17,6 +17,7 @@ using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Controllers; using Umbraco.Web.WebApi; using Constants = Umbraco.Core.Constants; +using static Umbraco.Core.Constants.Web.Routing; namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.Routing { @@ -65,27 +66,27 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.Routing var endpoint4 = (RouteEndpoint)route.Endpoints[2]; string apiControllerName = ControllerExtensions.GetControllerName(); Assert.AreEqual($"umbraco/backoffice/api/{apiControllerName.ToLowerInvariant()}/{{action}}/{{id?}}", endpoint4.RoutePattern.RawText); - Assert.IsFalse(endpoint4.RoutePattern.Defaults.ContainsKey("area")); - Assert.IsFalse(endpoint4.RoutePattern.Defaults.ContainsKey("action")); - Assert.AreEqual(apiControllerName, endpoint4.RoutePattern.Defaults["controller"]); + Assert.IsFalse(endpoint4.RoutePattern.Defaults.ContainsKey(AreaToken)); + Assert.IsFalse(endpoint4.RoutePattern.Defaults.ContainsKey(ActionToken)); + Assert.AreEqual(apiControllerName, endpoint4.RoutePattern.Defaults[ControllerToken]); } private void AssertMinimalBackOfficeRoutes(EndpointDataSource route) { var endpoint1 = (RouteEndpoint)route.Endpoints[0]; Assert.AreEqual($"umbraco/{{action}}/{{id?}}", endpoint1.RoutePattern.RawText); - Assert.AreEqual(Constants.Web.Mvc.BackOfficeArea, endpoint1.RoutePattern.Defaults["area"]); - Assert.AreEqual("Default", endpoint1.RoutePattern.Defaults["action"]); - Assert.AreEqual(ControllerExtensions.GetControllerName(), endpoint1.RoutePattern.Defaults["controller"]); - Assert.AreEqual(endpoint1.RoutePattern.Defaults["area"], typeof(BackOfficeController).GetCustomAttribute(false).RouteValue); + Assert.AreEqual(Constants.Web.Mvc.BackOfficeArea, endpoint1.RoutePattern.Defaults[AreaToken]); + Assert.AreEqual("Default", endpoint1.RoutePattern.Defaults[ActionToken]); + Assert.AreEqual(ControllerExtensions.GetControllerName(), endpoint1.RoutePattern.Defaults[ControllerToken]); + Assert.AreEqual(endpoint1.RoutePattern.Defaults[AreaToken], typeof(BackOfficeController).GetCustomAttribute(false).RouteValue); var endpoint2 = (RouteEndpoint)route.Endpoints[1]; string controllerName = ControllerExtensions.GetControllerName(); Assert.AreEqual($"umbraco/backoffice/{Constants.Web.Mvc.BackOfficeApiArea.ToLowerInvariant()}/{controllerName.ToLowerInvariant()}/{{action}}/{{id?}}", endpoint2.RoutePattern.RawText); - Assert.AreEqual(Constants.Web.Mvc.BackOfficeApiArea, endpoint2.RoutePattern.Defaults["area"]); - Assert.IsFalse(endpoint2.RoutePattern.Defaults.ContainsKey("action")); - Assert.AreEqual(controllerName, endpoint2.RoutePattern.Defaults["controller"]); - Assert.AreEqual(endpoint1.RoutePattern.Defaults["area"], typeof(BackOfficeController).GetCustomAttribute(false).RouteValue); + Assert.AreEqual(Constants.Web.Mvc.BackOfficeApiArea, endpoint2.RoutePattern.Defaults[AreaToken]); + Assert.IsFalse(endpoint2.RoutePattern.Defaults.ContainsKey(ActionToken)); + Assert.AreEqual(controllerName, endpoint2.RoutePattern.Defaults[ControllerToken]); + Assert.AreEqual(endpoint1.RoutePattern.Defaults[AreaToken], typeof(BackOfficeController).GetCustomAttribute(false).RouteValue); } private BackOfficeAreaRoutes GetBackOfficeAreaRoutes(RuntimeLevel level) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Routing/EndpointRouteBuilderExtensionsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Routing/EndpointRouteBuilderExtensionsTests.cs index 0990cb9d9a..062e0f079d 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Routing/EndpointRouteBuilderExtensionsTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Routing/EndpointRouteBuilderExtensionsTests.cs @@ -9,6 +9,7 @@ using Umbraco.Core; using Umbraco.Extensions; using Umbraco.Web.Common.Extensions; using Constants = Umbraco.Core.Constants; +using static Umbraco.Core.Constants.Web.Routing; namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.Routing { @@ -67,7 +68,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.Routing if (!area.IsNullOrWhiteSpace()) { - Assert.AreEqual(area, endpoint.RoutePattern.Defaults["area"]); + Assert.AreEqual(area, endpoint.RoutePattern.Defaults[AreaToken]); } if (!defaultAction.IsNullOrWhiteSpace()) @@ -75,7 +76,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.Routing Assert.AreEqual(defaultAction, endpoint.RoutePattern.Defaults["action"]); } - Assert.AreEqual(controllerName, endpoint.RoutePattern.Defaults["controller"]); + Assert.AreEqual(controllerName, endpoint.RoutePattern.Defaults[ControllerToken]); } [TestCase("umbraco", Constants.Web.Mvc.BackOfficeApiArea, true, null)] @@ -123,7 +124,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.Routing if (!area.IsNullOrWhiteSpace()) { - Assert.AreEqual(area, endpoint.RoutePattern.Defaults["area"]); + Assert.AreEqual(area, endpoint.RoutePattern.Defaults[AreaToken]); } if (!defaultAction.IsNullOrWhiteSpace()) @@ -131,7 +132,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.Routing Assert.AreEqual(defaultAction, endpoint.RoutePattern.Defaults["action"]); } - Assert.AreEqual(controllerName, endpoint.RoutePattern.Defaults["controller"]); + Assert.AreEqual(controllerName, endpoint.RoutePattern.Defaults[ControllerToken]); } private class Testing1Controller : ControllerBase diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Routing/InstallAreaRoutesTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Routing/InstallAreaRoutesTests.cs index 74671f819a..c32d54e072 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Routing/InstallAreaRoutesTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Routing/InstallAreaRoutesTests.cs @@ -10,6 +10,7 @@ using Umbraco.Core; using Umbraco.Core.Hosting; using Umbraco.Extensions; using Umbraco.Web.Common.Install; +using static Umbraco.Core.Constants.Web.Routing; namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.Routing { @@ -42,17 +43,17 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.Routing var endpoint1 = (RouteEndpoint)route.Endpoints[0]; Assert.AreEqual($"install/api/{{action}}/{{id?}}", endpoint1.RoutePattern.RawText); - Assert.AreEqual(Constants.Web.Mvc.InstallArea, endpoint1.RoutePattern.Defaults["area"]); - Assert.AreEqual("Index", endpoint1.RoutePattern.Defaults["action"]); - Assert.AreEqual(ControllerExtensions.GetControllerName(), endpoint1.RoutePattern.Defaults["controller"]); - Assert.AreEqual(endpoint1.RoutePattern.Defaults["area"], typeof(InstallApiController).GetCustomAttribute(false).RouteValue); + Assert.AreEqual(Constants.Web.Mvc.InstallArea, endpoint1.RoutePattern.Defaults[AreaToken]); + Assert.AreEqual("Index", endpoint1.RoutePattern.Defaults[ActionToken]); + Assert.AreEqual(ControllerExtensions.GetControllerName(), endpoint1.RoutePattern.Defaults[ControllerToken]); + Assert.AreEqual(endpoint1.RoutePattern.Defaults[AreaToken], typeof(InstallApiController).GetCustomAttribute(false).RouteValue); var endpoint2 = (RouteEndpoint)route.Endpoints[1]; Assert.AreEqual($"install/{{action}}/{{id?}}", endpoint2.RoutePattern.RawText); - Assert.AreEqual(Constants.Web.Mvc.InstallArea, endpoint2.RoutePattern.Defaults["area"]); - Assert.AreEqual("Index", endpoint2.RoutePattern.Defaults["action"]); - Assert.AreEqual(ControllerExtensions.GetControllerName(), endpoint2.RoutePattern.Defaults["controller"]); - Assert.AreEqual(endpoint2.RoutePattern.Defaults["area"], typeof(InstallController).GetCustomAttribute(false).RouteValue); + Assert.AreEqual(Constants.Web.Mvc.InstallArea, endpoint2.RoutePattern.Defaults[AreaToken]); + Assert.AreEqual("Index", endpoint2.RoutePattern.Defaults[ActionToken]); + Assert.AreEqual(ControllerExtensions.GetControllerName(), endpoint2.RoutePattern.Defaults[ControllerToken]); + Assert.AreEqual(endpoint2.RoutePattern.Defaults[AreaToken], typeof(InstallController).GetCustomAttribute(false).RouteValue); EndpointDataSource fallbackRoute = endpoints.DataSources.Last(); Assert.AreEqual(1, fallbackRoute.Endpoints.Count); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Routing/PreviewRoutesTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Routing/PreviewRoutesTests.cs index 32d4f41f8a..82e5628c3a 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Routing/PreviewRoutesTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Routing/PreviewRoutesTests.cs @@ -14,6 +14,7 @@ using Umbraco.Extensions; using Umbraco.Web.BackOffice.Controllers; using Umbraco.Web.BackOffice.Routing; using Constants = Umbraco.Core.Constants; +using static Umbraco.Core.Constants.Web.Routing; namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.Routing { @@ -54,8 +55,8 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Common.Routing var previewControllerName = ControllerExtensions.GetControllerName(); Assert.AreEqual($"umbraco/{previewControllerName.ToLowerInvariant()}/{{action}}/{{id?}}", endpoint3.RoutePattern.RawText); Assert.AreEqual(Constants.Web.Mvc.BackOfficeArea, endpoint3.RoutePattern.Defaults["area"]); - Assert.AreEqual("Index", endpoint3.RoutePattern.Defaults["action"]); - Assert.AreEqual(previewControllerName, endpoint3.RoutePattern.Defaults["controller"]); + Assert.AreEqual("Index", endpoint3.RoutePattern.Defaults[ActionToken]); + Assert.AreEqual(previewControllerName, endpoint3.RoutePattern.Defaults[ControllerToken]); Assert.AreEqual(endpoint3.RoutePattern.Defaults["area"], typeof(PreviewController).GetCustomAttribute(false).RouteValue); } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/SurfaceControllerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/SurfaceControllerTests.cs index e286603f1c..21143662cb 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/SurfaceControllerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/SurfaceControllerTests.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Moq; @@ -124,17 +125,17 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Controllers builder.SetPublishedContent(content); IPublishedRequest publishedRequest = builder.Build(); - var routeDefinition = new UmbracoRouteValues(publishedRequest); + var routeDefinition = new UmbracoRouteValues(publishedRequest, null); - var routeData = new RouteData(); - routeData.Values.Add(CoreConstants.Web.UmbracoRouteDefinitionDataToken, routeDefinition); + var httpContext = new DefaultHttpContext(); + httpContext.Features.Set(routeDefinition); var ctrl = new TestSurfaceController(umbracoContextAccessor, Mock.Of(), Mock.Of()) { ControllerContext = new ControllerContext() { - HttpContext = Mock.Of(), - RouteData = routeData + HttpContext = httpContext, + RouteData = new RouteData() } }; diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/HijackedRouteEvaluatorTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/ControllerActionSearcherTests.cs similarity index 52% rename from src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/HijackedRouteEvaluatorTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/ControllerActionSearcherTests.cs index 2d96476b30..d5d3b3e26b 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/HijackedRouteEvaluatorTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/ControllerActionSearcherTests.cs @@ -1,67 +1,48 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ViewEngines; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Primitives; +using Moq; using NUnit.Framework; using Umbraco.Core; using Umbraco.Extensions; using Umbraco.Web; using Umbraco.Web.Common.Controllers; using Umbraco.Web.Website.Routing; +using static Umbraco.Core.Constants.Web.Routing; namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Routing { [TestFixture] - public class HijackedRouteEvaluatorTests + public class ControllerActionSearcherTests { - private class TestActionDescriptorCollectionProvider : ActionDescriptorCollectionProvider - { - private readonly IEnumerable _actions; - - public TestActionDescriptorCollectionProvider(IEnumerable actions) => _actions = actions; - - public override ActionDescriptorCollection ActionDescriptors => new ActionDescriptorCollection(_actions.ToList(), 1); - - public override IChangeToken GetChangeToken() => NullChangeToken.Singleton; - } - - private IActionDescriptorCollectionProvider GetActionDescriptors() => new TestActionDescriptorCollectionProvider( - new ActionDescriptor[] + private ControllerActionDescriptor GetDescriptor(string action) + => new ControllerActionDescriptor { - new ControllerActionDescriptor - { - ActionName = "Index", - ControllerName = ControllerExtensions.GetControllerName(), - ControllerTypeInfo = typeof(RenderController).GetTypeInfo() - }, - new ControllerActionDescriptor - { - ActionName = "Index", - ControllerName = ControllerExtensions.GetControllerName(), - ControllerTypeInfo = typeof(Render1Controller).GetTypeInfo() - }, - new ControllerActionDescriptor - { - ActionName = "Custom", - ControllerName = ControllerExtensions.GetControllerName(), - ControllerTypeInfo = typeof(Render1Controller).GetTypeInfo() - }, - new ControllerActionDescriptor - { - ActionName = "Index", - ControllerName = ControllerExtensions.GetControllerName(), - ControllerTypeInfo = typeof(Render2Controller).GetTypeInfo() - } - }); + ActionName = action, + ControllerName = ControllerExtensions.GetControllerName(), + ControllerTypeInfo = typeof(RenderController).GetTypeInfo(), + DisplayName = $"{ControllerExtensions.GetControllerName()}.{action}" + }; + + private IReadOnlyList GetActionDescriptors() => new List + { + GetDescriptor(nameof(RenderController.Index)), + GetDescriptor(nameof(Render1Controller.Index)), + GetDescriptor(nameof(Render1Controller.Custom)), + GetDescriptor(nameof(Render2Controller.Index)) + }; private class Render1Controller : ControllerBase, IRenderController { @@ -88,12 +69,26 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Routing [TestCase("Custom", "Render1", nameof(Render1Controller.Custom), true)] public void Matches_Controller(string action, string controller, string resultAction, bool matches) { - var evaluator = new HijackedRouteEvaluator( - new NullLogger(), - GetActionDescriptors()); + IReadOnlyList descriptors = GetActionDescriptors(); - HijackedRouteResult result = evaluator.Evaluate(controller, action); - Assert.AreEqual(matches, result.Success); + var actionSelector = new Mock(); + actionSelector.Setup(x => x.SelectCandidates(It.IsAny())) + .Returns((RouteContext r) => + { + // our own rudimentary search + var controller = r.RouteData.Values[ControllerToken].ToString(); + var action = r.RouteData.Values[ActionToken].ToString(); + return descriptors.Where(x => x.ControllerName.InvariantEquals(controller) && x.ActionName.InvariantEquals(action)).ToList(); + }); + + var query = new ControllerActionSearcher( + new NullLogger(), + actionSelector.Object); + + var httpContext = new DefaultHttpContext(); + + ControllerActionDescriptor result = query.Find(httpContext, controller, action); + Assert.IsTrue(matches == (result != null)); if (matches) { Assert.IsTrue(result.ActionName.InvariantEquals(resultAction), "expected {0} does not match resulting action {1}", resultAction, result.ActionName); diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformerTests.cs index a531c77fe1..a47d3acb20 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformerTests.cs @@ -1,6 +1,9 @@ using System; +using System.Reflection; using System.Threading.Tasks; +using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; @@ -19,6 +22,7 @@ using Umbraco.Web.PublishedCache; using Umbraco.Web.Routing; using Umbraco.Web.Website.Controllers; using Umbraco.Web.Website.Routing; +using static Umbraco.Core.Constants.Web.Routing; namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Routing { @@ -49,7 +53,9 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Routing TestHelper.GetHostingEnvironment(), state, routeValuesFactory ?? Mock.Of(), - filter ?? Mock.Of(x => x.IsDocumentRequest(It.IsAny()) == true)); + filter ?? Mock.Of(x => x.IsDocumentRequest(It.IsAny()) == true), + Mock.Of(), + Mock.Of()); return transformer; } @@ -70,11 +76,14 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Routing private UmbracoRouteValues GetRouteValues(IPublishedRequest request) => new UmbracoRouteValues( request, - ControllerExtensions.GetControllerName(), - typeof(TestController)); + new ControllerActionDescriptor + { + ControllerTypeInfo = typeof(TestController).GetTypeInfo(), + ControllerName = ControllerExtensions.GetControllerName() + }); private IUmbracoRouteValuesFactory GetRouteValuesFactory(IPublishedRequest request) - => Mock.Of(x => x.Create(It.IsAny(), It.IsAny(), It.IsAny()) == GetRouteValues(request)); + => Mock.Of(x => x.Create(It.IsAny(), It.IsAny()) == GetRouteValues(request)); private IPublishedRouter GetRouter(IPublishedRequest request) => Mock.Of(x => x.RouteRequestAsync(It.IsAny(), It.IsAny()) == Task.FromResult(request)); @@ -121,8 +130,8 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Routing RouteValueDictionary result = await transformer.TransformAsync(new DefaultHttpContext(), new RouteValueDictionary()); Assert.AreEqual(2, result.Count); - Assert.AreEqual(ControllerExtensions.GetControllerName(), result["controller"]); - Assert.AreEqual(nameof(RenderNoContentController.Index), result["action"]); + Assert.AreEqual(ControllerExtensions.GetControllerName(), result[ControllerToken]); + Assert.AreEqual(nameof(RenderNoContentController.Index), result[ActionToken]); } [Test] @@ -140,6 +149,25 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Routing Assert.AreEqual(request, umbracoContext.PublishedRequest); } + [Test] + public async Task Assigns_UmbracoRouteValues_To_HttpContext_Feature() + { + IUmbracoContext umbracoContext = GetUmbracoContext(true); + IPublishedRequest request = Mock.Of(); + + UmbracoRouteValueTransformer transformer = GetTransformerWithRunState( + Mock.Of(x => x.UmbracoContext == umbracoContext), + router: GetRouter(request), + routeValuesFactory: GetRouteValuesFactory(request)); + + var httpContext = new DefaultHttpContext(); + RouteValueDictionary result = await transformer.TransformAsync(httpContext, new RouteValueDictionary()); + + UmbracoRouteValues routeVals = httpContext.Features.Get(); + Assert.IsNotNull(routeVals); + Assert.AreEqual(routeVals.PublishedRequest, umbracoContext.PublishedRequest); + } + [Test] public async Task Assigns_Values_To_RouteValueDictionary() { @@ -154,8 +182,8 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Routing RouteValueDictionary result = await transformer.TransformAsync(new DefaultHttpContext(), new RouteValueDictionary()); - Assert.AreEqual(routeValues.ControllerName, result["controller"]); - Assert.AreEqual(routeValues.ActionName, result["action"]); + Assert.AreEqual(routeValues.ControllerName, result[ControllerToken]); + Assert.AreEqual(routeValues.ActionName, result[ActionToken]); } private class TestController : RenderController diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactoryTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactoryTests.cs index 17ce59862f..58fd52496d 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactoryTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactoryTests.cs @@ -1,5 +1,9 @@ using System; +using System.Collections.Generic; +using System.Reflection; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging.Abstractions; @@ -10,6 +14,8 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Services; using Umbraco.Core.Strings; +using Umbraco.Extensions; +using Umbraco.Web.Common.Controllers; using Umbraco.Web.Common.Routing; using Umbraco.Web.Features; using Umbraco.Web.Routing; @@ -22,11 +28,14 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Routing [TestFixture] public class UmbracoRouteValuesFactoryTests { - private UmbracoRouteValuesFactory GetFactory(out Mock publishedRouter, out UmbracoRenderingDefaults renderingDefaults) + private UmbracoRouteValuesFactory GetFactory( + out Mock publishedRouter, + out UmbracoRenderingDefaults renderingDefaults, + out IPublishedRequest request) { var builder = new PublishedRequestBuilder(new Uri("https://example.com"), Mock.Of()); builder.SetPublishedContent(Mock.Of()); - IPublishedRequest request = builder.Build(); + request = builder.Build(); publishedRouter = new Mock(); publishedRouter.Setup(x => x.UpdateRequestToNotFound(It.IsAny())) @@ -35,13 +44,26 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Routing renderingDefaults = new UmbracoRenderingDefaults(); + // add the default one + var actionDescriptors = new List + { + new ControllerActionDescriptor + { + ControllerName = ControllerExtensions.GetControllerName(), + ActionName = nameof(RenderController.Index), + ControllerTypeInfo = typeof(RenderController).GetTypeInfo() + } + }; + var actionSelector = new Mock(); + actionSelector.Setup(x => x.SelectCandidates(It.IsAny())).Returns(actionDescriptors); + var factory = new UmbracoRouteValuesFactory( renderingDefaults, Mock.Of(), new UmbracoFeatures(), - new HijackedRouteEvaluator( - new NullLogger(), - Mock.Of()), + new ControllerActionSearcher( + new NullLogger(), + actionSelector.Object), publishedRouter.Object); return factory; @@ -50,13 +72,9 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Routing [Test] public void Update_Request_To_Not_Found_When_No_Template() { - var builder = new PublishedRequestBuilder(new Uri("https://example.com"), Mock.Of()); - builder.SetPublishedContent(Mock.Of()); - IPublishedRequest request = builder.Build(); + UmbracoRouteValuesFactory factory = GetFactory(out Mock publishedRouter, out _, out IPublishedRequest request); - UmbracoRouteValuesFactory factory = GetFactory(out Mock publishedRouter, out _); - - UmbracoRouteValues result = factory.Create(new DefaultHttpContext(), new RouteValueDictionary(), request); + UmbracoRouteValues result = factory.Create(new DefaultHttpContext(), request); // The request has content, no template, no hijacked route and no disabled template features so UpdateRequestToNotFound will be called publishedRouter.Verify(m => m.UpdateRequestToNotFound(It.IsAny()), Times.Once); @@ -65,19 +83,11 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Routing [Test] public void Adds_Result_To_Route_Value_Dictionary() { - var builder = new PublishedRequestBuilder(new Uri("https://example.com"), Mock.Of()); - builder.SetPublishedContent(Mock.Of()); - builder.SetTemplate(Mock.Of()); - IPublishedRequest request = builder.Build(); + UmbracoRouteValuesFactory factory = GetFactory(out _, out UmbracoRenderingDefaults renderingDefaults, out IPublishedRequest request); - UmbracoRouteValuesFactory factory = GetFactory(out _, out UmbracoRenderingDefaults renderingDefaults); - - var routeVals = new RouteValueDictionary(); - UmbracoRouteValues result = factory.Create(new DefaultHttpContext(), routeVals, request); + UmbracoRouteValues result = factory.Create(new DefaultHttpContext(), request); Assert.IsNotNull(result); - Assert.IsTrue(routeVals.ContainsKey(Constants.Web.UmbracoRouteDefinitionDataToken)); - Assert.AreEqual(result, routeVals[Constants.Web.UmbracoRouteDefinitionDataToken]); Assert.AreEqual(renderingDefaults.DefaultControllerType, result.ControllerType); Assert.AreEqual(UmbracoRouteValues.DefaultActionName, result.ActionName); Assert.IsNull(result.TemplateName); diff --git a/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs b/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs deleted file mode 100644 index 7edb316c2e..0000000000 --- a/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs +++ /dev/null @@ -1,202 +0,0 @@ -using System; -using System.Linq; -using System.Web.Mvc; -using System.Web.Routing; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.Logging; -using Moq; -using NUnit.Framework; -using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Configuration.Models; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.Services; -using Umbraco.Core.Strings; -using Umbraco.Tests.Common; -using Umbraco.Tests.PublishedContent; -using Umbraco.Tests.TestHelpers; -using Umbraco.Tests.TestHelpers.Stubs; -using Umbraco.Tests.Testing; -using Umbraco.Web; -using Umbraco.Web.Models; -using Umbraco.Web.Mvc; -using Umbraco.Web.Runtime; -using Umbraco.Web.WebApi; -using Current = Umbraco.Web.Composing.Current; -using Umbraco.Core.DependencyInjection; -using System.Threading.Tasks; - -namespace Umbraco.Tests.Routing -{ - [TestFixture] - [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)] - public class RenderRouteHandlerTests : BaseWebTest - { - public override void SetUp() - { - base.SetUp(); - - WebInitialComponent.CreateRoutes( - new TestUmbracoContextAccessor(), - TestObjects.GetGlobalSettings(), - ShortStringHelper, - // new SurfaceControllerTypeCollection(Enumerable.Empty()), - new UmbracoApiControllerTypeCollection(Enumerable.Empty()), - HostingEnvironment); - } - - protected override void Compose() - { - base.Compose(); - - // set the default RenderMvcController - Current.DefaultRenderMvcControllerType = typeof(RenderMvcController); // FIXME: Wrong! - - // var surfaceControllerTypes = new SurfaceControllerTypeCollection(Composition.TypeLoader.GetSurfaceControllers()); - // Composition.Services.AddUnique(surfaceControllerTypes); - - var umbracoApiControllerTypes = new UmbracoApiControllerTypeCollection(Builder.TypeLoader.GetUmbracoApiControllers()); - Builder.Services.AddUnique(umbracoApiControllerTypes); - - var requestHandlerSettings = new RequestHandlerSettings(); - Builder.Services.AddUnique(_ => new DefaultShortStringHelper(Microsoft.Extensions.Options.Options.Create(requestHandlerSettings))); - } - - public override void TearDown() - { - base.TearDown(); - RouteTable.Routes.Clear(); - } - - Template CreateTemplate(string alias) - { - var name = "Template"; - var template = new Template(ShortStringHelper, name, alias); - template.Content = ""; // else saving throws with a dirty internal error - ServiceContext.FileService.SaveTemplate(template); - return template; - } - - /// - /// Will route to the default controller and action since no custom controller is defined for this node route - /// - [Test] - public async Task Umbraco_Route_Umbraco_Defined_Controller_Action() - { - var url = "~/dummy-page"; - var template = CreateTemplate("homePage"); - var route = RouteTable.Routes["Umbraco_default"]; - var routeData = new RouteData { Route = route }; - var umbracoContext = GetUmbracoContext(url, template.Id, routeData); - var httpContext = GetHttpContextFactory(url, routeData).HttpContext; - var publishedRouter = CreatePublishedRouter(GetUmbracoContextAccessor(umbracoContext)); - var frequest = await publishedRouter .CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); - frequest.SetPublishedContent(umbracoContext.Content.GetById(1174)); - frequest.SetTemplate(template); - - var umbracoContextAccessor = new TestUmbracoContextAccessor(umbracoContext); - var handler = new RenderRouteHandler(umbracoContext, new TestControllerFactory(umbracoContextAccessor, Mock.Of>()), ShortStringHelper); - - handler.GetHandlerForRoute(httpContext.Request.RequestContext, frequest.Build()); - Assert.AreEqual("RenderMvc", routeData.Values["controller"].ToString()); - //the route action will still be the one we've asked for because our RenderActionInvoker is the thing that decides - // if the action matches. - Assert.AreEqual("homePage", routeData.Values["action"].ToString()); - } - - //test all template name styles to match the ActionName - - //[TestCase("home-\\234^^*32page")] // TODO: This fails! - [TestCase("home-page")] - [TestCase("home-page")] - [TestCase("home-page")] - [TestCase("Home-Page")] - [TestCase("HomePage")] - [TestCase("homePage")] - [TestCase("site1/template2")] - [TestCase("site1\\template2")] - public async Task Umbraco_Route_User_Defined_Controller_Action(string templateName) - { - // NOTE - here we create templates with crazy aliases... assuming that these - // could exist in the database... yet creating templates should sanitize - // aliases one way or another... - - var url = "~/dummy-page"; - var template = CreateTemplate(templateName); - var route = RouteTable.Routes["Umbraco_default"]; - var routeData = new RouteData() { Route = route }; - var umbracoContext = GetUmbracoContext("~/dummy-page", template.Id, routeData, true); - var httpContext = GetHttpContextFactory(url, routeData).HttpContext; - var publishedRouter = CreatePublishedRouter(GetUmbracoContextAccessor(umbracoContext)); - var frequest = await publishedRouter .CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); - frequest.SetPublishedContent(umbracoContext.Content.GetById(1172)); - frequest.SetTemplate(template); - - var umbracoContextAccessor = new TestUmbracoContextAccessor(umbracoContext); - var type = new AutoPublishedContentType(Guid.NewGuid(), 22, "CustomDocument", new PublishedPropertyType[] { }); - ContentTypesCache.GetPublishedContentTypeByAlias = alias => type; - - var handler = new RenderRouteHandler(umbracoContext, new TestControllerFactory(umbracoContextAccessor, Mock.Of>(), context => - { - - return new CustomDocumentController(Factory.GetRequiredService>(), - umbracoContextAccessor, - Factory.GetRequiredService(), - Factory.GetRequiredService(), - Factory.GetRequiredService(), - Factory.GetRequiredService()); - }), ShortStringHelper); - - handler.GetHandlerForRoute(httpContext.Request.RequestContext, frequest.Build()); - Assert.AreEqual("CustomDocument", routeData.Values["controller"].ToString()); - Assert.AreEqual( - //global::umbraco.cms.helpers.Casing.SafeAlias(template.Alias), - template.Alias.ToSafeAlias(ShortStringHelper), - routeData.Values["action"].ToString()); - } - - - #region Internal classes - - ///// - ///// Used to test a user route (non-umbraco) - ///// - //private class CustomUserController : Controller - //{ - - // public ActionResult Index() - // { - // return View(); - // } - - // public ActionResult Test(int id) - // { - // return View(); - // } - - //} - - /// - /// Used to test a user route umbraco route - /// - public class CustomDocumentController : RenderMvcController - { - public CustomDocumentController(IOptions globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ServiceContext services, AppCaches appCaches, IProfilingLogger profilingLogger, ILoggerFactory loggerFactory) - - { - } - - public ActionResult HomePage(ContentModel model) - { - // ReSharper disable once Mvc.ViewNotResolved - return View(); - } - - } - - #endregion - } -} diff --git a/src/Umbraco.Tests/Routing/UmbracoModuleTests.cs b/src/Umbraco.Tests/Routing/UmbracoModuleTests.cs deleted file mode 100644 index 2ec0113c2f..0000000000 --- a/src/Umbraco.Tests/Routing/UmbracoModuleTests.cs +++ /dev/null @@ -1,124 +0,0 @@ -using System; -using System.Threading; -using Microsoft.Extensions.Logging; -using Moq; -using NUnit.Framework; -using Umbraco.Core; -using Umbraco.Core.Configuration.Models; -using Umbraco.Tests.Common.Builders; -using Umbraco.Tests.TestHelpers; -using Umbraco.Web; - -namespace Umbraco.Tests.Routing -{ - [TestFixture] - [Apartment(ApartmentState.STA)] - public class UmbracoModuleTests : BaseWebTest - { - private UmbracoInjectedModule _module; - - public override void SetUp() - { - base.SetUp(); - - // FIXME: be able to get the UmbracoModule from the container. any reason settings were from testobjects? - //create the module - var logger = Mock.Of(); - - var globalSettings = new GlobalSettings { ReservedPaths = GlobalSettings.StaticReservedPaths + "~/umbraco" }; - var runtime = Umbraco.Core.RuntimeState.Booting(); - - _module = new UmbracoInjectedModule - ( - runtime, - logger, - Mock.Of(), - globalSettings, - HostingEnvironment - ); - - runtime.Level = RuntimeLevel.Run; - - - //SettingsForTests.ReservedPaths = "~/umbraco,~/install/"; - //SettingsForTests.ReservedUrls = "~/config/splashes/booting.aspx,~/install/default.aspx,~/config/splashes/noNodes.aspx,~/VSEnterpriseHelper.axd"; - - } - - public override void TearDown() - { - base.TearDown(); - - _module.DisposeIfDisposable(); - } - - // do not test for /base here as it's handled before EnsureUmbracoRoutablePage is called - [TestCase("/umbraco_client/Tree/treeIcons.css", false)] - [TestCase("/umbraco_client/Tree/Themes/umbraco/style.css?cdv=37", false)] - [TestCase("/umbraco/editContent.aspx", false)] - [TestCase("/install/default.aspx", false)] - [TestCase("/install/?installStep=license", false)] - [TestCase("/install?installStep=license", false)] - [TestCase("/install/test.aspx", false)] - [TestCase("/config/splashes/noNodes.aspx", false)] - [TestCase("/", true)] - [TestCase("/home.aspx", true)] - [TestCase("/umbraco-test", true)] - [TestCase("/install-test", true)] - public void Ensure_Request_Routable(string url, bool assert) - { - var httpContextFactory = new FakeHttpContextFactory(url); - var httpContext = httpContextFactory.HttpContext; - var umbracoContext = GetUmbracoContext(url); - - var result = _module.EnsureUmbracoRoutablePage(umbracoContext, httpContext); - - Assert.AreEqual(assert, result.Success); - } - - //NOTE: This test shows how we can test most of the HttpModule, it however is testing a method that no longer exists and is testing too much, - // we need to write unit tests for each of the components: NiceUrlProvider, all of the Lookup classes, etc... - // to ensure that each one is individually tested. - - //[TestCase("/", 1046)] - //[TestCase("/home.aspx", 1046)] - //[TestCase("/home/sub1.aspx", 1173)] - //[TestCase("/home.aspx?altTemplate=blah", 1046)] - //public void Process_Front_End_Document_Request_Match_Node(string url, int nodeId) - //{ - // var httpContextFactory = new FakeHttpContextFactory(url); - // var httpContext = httpContextFactory.HttpContext; - // var umbracoContext = new UmbracoContext(httpContext, ApplicationContext.Current, new NullRoutesCache()); - // var contentStore = new ContentStore(umbracoContext); - // var niceUrls = new NiceUrlProvider(contentStore, umbracoContext); - // umbracoContext.RoutingContext = new RoutingContext( - // new IPublishedContentLookup[] {new LookupByNiceUrl()}, - // new DefaultLastChanceLookup(), - // contentStore, - // niceUrls); - - // StateHelper.HttpContext = httpContext; - - // //because of so much dependency on the db, we need to create som stuff here, i originally abstracted out stuff but - // //was turning out to be quite a deep hole because ultimately we'd have to abstract the old 'Domain' and 'Language' classes - // Domain.MakeNew("Test.com", 1000, Language.GetByCultureCode("en-US").id); - - // //need to create a template with id 1045 - // var template = Template.MakeNew("test", new User(0)); - - // SetupUmbracoContextForTest(umbracoContext, template); - - // _module.AssignDocumentRequest(httpContext, umbracoContext, httpContext.Request.Url); - - // Assert.IsNotNull(umbracoContext.PublishedContentRequest); - // Assert.IsNotNull(umbracoContext.PublishedContentRequest.XmlNode); - // Assert.IsFalse(umbracoContext.PublishedContentRequest.IsRedirect); - // Assert.IsFalse(umbracoContext.PublishedContentRequest.Is404); - // Assert.AreEqual(umbracoContext.PublishedContentRequest.Culture, Thread.CurrentThread.CurrentCulture); - // Assert.AreEqual(umbracoContext.PublishedContentRequest.Culture, Thread.CurrentThread.CurrentUICulture); - // Assert.AreEqual(nodeId, umbracoContext.PublishedContentRequest.NodeId); - - //} - - } -} diff --git a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs index 78f9fc3013..c306451f66 100644 --- a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs +++ b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs @@ -226,7 +226,6 @@ namespace Umbraco.Tests.Testing services.AddUnique(ipResolver); services.AddUnique(); services.AddUnique(TestHelper.ShortStringHelper); - services.AddUnique(); services.AddUnique(); diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index b09787dd2c..730e2e80b1 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -198,7 +198,6 @@ - @@ -217,12 +216,10 @@ - - @@ -243,7 +240,6 @@ - diff --git a/src/Umbraco.Tests/Web/Controllers/PluginControllerAreaTests.cs b/src/Umbraco.Tests/Web/Controllers/PluginControllerAreaTests.cs deleted file mode 100644 index a91a305314..0000000000 --- a/src/Umbraco.Tests/Web/Controllers/PluginControllerAreaTests.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using NUnit.Framework; -using Umbraco.Tests.TestHelpers; -using Umbraco.Web; -using Umbraco.Web.Mvc; - -namespace Umbraco.Tests.Web.Controllers -{ - [TestFixture] - public class PluginControllerAreaTests : BaseWebTest - { - - [Test] - public void Ensure_Same_Area1() - { - Assert.Throws(() => - new PluginControllerArea(TestObjects.GetGlobalSettings(), HostingEnvironment, - new PluginControllerMetadata[] - { - PluginController.GetMetadata(typeof(Plugin1Controller)), - PluginController.GetMetadata(typeof(Plugin2Controller)), - PluginController.GetMetadata(typeof(Plugin3Controller)) //not same area - })); - } - - [Test] - public void Ensure_Same_Area3() - { - Assert.Throws(() => - new PluginControllerArea(TestObjects.GetGlobalSettings(), HostingEnvironment, - new PluginControllerMetadata[] - { - PluginController.GetMetadata(typeof(Plugin1Controller)), - PluginController.GetMetadata(typeof(Plugin2Controller)), - PluginController.GetMetadata(typeof(Plugin4Controller)) //no area assigned - })); - } - - [Test] - public void Ensure_Same_Area2() - { - var area = new PluginControllerArea(TestObjects.GetGlobalSettings(), HostingEnvironment, - new PluginControllerMetadata[] - { - PluginController.GetMetadata(typeof(Plugin1Controller)), - PluginController.GetMetadata(typeof(Plugin2Controller)) - }); - Assert.Pass(); - } - - #region Test classes - - [PluginController("Area1")] - public class Plugin1Controller : PluginController - { - public Plugin1Controller(IUmbracoContextAccessor umbracoContextAccessor) - : base(umbracoContextAccessor, null, null, null, null) - { - } - } - - [PluginController("Area1")] - public class Plugin2Controller : PluginController - { - public Plugin2Controller(IUmbracoContextAccessor umbracoContextAccessor) - : base(umbracoContextAccessor, null, null, null, null) - { - } - } - - [PluginController("Area2")] - public class Plugin3Controller : PluginController - { - public Plugin3Controller(IUmbracoContextAccessor umbracoContextAccessor) - : base(umbracoContextAccessor, null, null, null, null) - { - } - } - - public class Plugin4Controller : PluginController - { - public Plugin4Controller(IUmbracoContextAccessor umbracoContextAccessor) - : base(umbracoContextAccessor, null, null, null, null) - { - } - } - - #endregion - - } -} diff --git a/src/Umbraco.Tests/Web/Mvc/SurfaceControllerTests.cs b/src/Umbraco.Tests/Web/Mvc/SurfaceControllerTests.cs deleted file mode 100644 index 09748e9621..0000000000 --- a/src/Umbraco.Tests/Web/Mvc/SurfaceControllerTests.cs +++ /dev/null @@ -1,221 +0,0 @@ -using System; -using System.Threading.Tasks; -using System.Web; -using System.Web.Mvc; -using System.Web.Routing; -using Moq; -using NUnit.Framework; -using Umbraco.Core.Cache; -using Umbraco.Core.Configuration.Models; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.Services; -using Umbraco.Tests.Common; -using Umbraco.Tests.TestHelpers; -using Umbraco.Tests.Testing; -using Umbraco.Web; -using Umbraco.Web.Mvc; -using Umbraco.Web.PublishedCache; -using Current = Umbraco.Web.Composing.Current; - -namespace Umbraco.Tests.Web.Mvc -{ - [TestFixture] - [UmbracoTest(WithApplication = true)] - public class SurfaceControllerTests : UmbracoTestBase - { - - public override void SetUp() - { - base.SetUp(); - Current.UmbracoContextAccessor = new TestUmbracoContextAccessor(); - } - - [Test] - public void Can_Construct_And_Get_Result() - { - var globalSettings = TestObjects.GetGlobalSettings(); - var httpContextAccessor = TestHelper.GetHttpContextAccessor(); - - var umbracoContextFactory = new UmbracoContextFactory( - Current.UmbracoContextAccessor, - Mock.Of(), - new TestVariationContextAccessor(), - new TestDefaultCultureAccessor(), - globalSettings, - Mock.Of(), - HostingEnvironment, - UriUtility, - httpContextAccessor, - new AspNetCookieManager(httpContextAccessor)); - - var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(); - var umbracoContext = umbracoContextReference.UmbracoContext; - - var umbracoContextAccessor = new TestUmbracoContextAccessor(umbracoContext); - - var ctrl = new TestSurfaceController(umbracoContextAccessor, Mock.Of()); - - var result = ctrl.Index(); - - Assert.IsNotNull(result); - } - - [Test] - public void Umbraco_Context_Not_Null() - { - var globalSettings = TestObjects.GetGlobalSettings(); - var httpContextAccessor = TestHelper.GetHttpContextAccessor(); - - var umbracoContextFactory = new UmbracoContextFactory( - Current.UmbracoContextAccessor, - Mock.Of(), - new TestVariationContextAccessor(), - new TestDefaultCultureAccessor(), - globalSettings, - Mock.Of(), - HostingEnvironment, - UriUtility, - httpContextAccessor, - new AspNetCookieManager(httpContextAccessor)); - - var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(); - var umbCtx = umbracoContextReference.UmbracoContext; - - var umbracoContextAccessor = new TestUmbracoContextAccessor(umbCtx); - - var ctrl = new TestSurfaceController(umbracoContextAccessor, Mock.Of()); - - Assert.IsNotNull(ctrl.UmbracoContext); - } - - [Test] - public void Can_Lookup_Content() - { - var publishedSnapshot = new Mock(); - publishedSnapshot.Setup(x => x.Members).Returns(Mock.Of()); - var content = new Mock(); - content.Setup(x => x.Id).Returns(2); - var publishedSnapshotService = new Mock(); - var globalSettings = TestObjects.GetGlobalSettings(); - var httpContextAccessor = TestHelper.GetHttpContextAccessor(); - - var umbracoContextFactory = new UmbracoContextFactory( - Current.UmbracoContextAccessor, - publishedSnapshotService.Object, - new TestVariationContextAccessor(), - new TestDefaultCultureAccessor(), - globalSettings, - Mock.Of(), - HostingEnvironment, - UriUtility, - httpContextAccessor, - new AspNetCookieManager(httpContextAccessor)); - - var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(); - var umbracoContext = umbracoContextReference.UmbracoContext; - - var umbracoContextAccessor = new TestUmbracoContextAccessor(umbracoContext); - - var publishedContentQuery = Mock.Of(query => query.Content(2) == content.Object); - - var ctrl = new TestSurfaceController(umbracoContextAccessor,publishedContentQuery); - var result = ctrl.GetContent(2) as PublishedContentResult; - - Assert.IsNotNull(result); - Assert.IsNotNull(result.Content); - Assert.AreEqual(2, result.Content.Id); - } - - [Test] - public async Task Mock_Current_Page() - { - var globalSettings = TestObjects.GetGlobalSettings(); - var httpContextAccessor = TestHelper.GetHttpContextAccessor(); - - var umbracoContextFactory = new UmbracoContextFactory( - Current.UmbracoContextAccessor, - Mock.Of(), - new TestVariationContextAccessor(), - new TestDefaultCultureAccessor(), - globalSettings, - Mock.Of(), - HostingEnvironment, - UriUtility, - httpContextAccessor, - new AspNetCookieManager(httpContextAccessor)); - - var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(); - var umbracoContext = umbracoContextReference.UmbracoContext; - - var umbracoContextAccessor = new TestUmbracoContextAccessor(umbracoContext); - - var content = Mock.Of(publishedContent => publishedContent.Id == 12345); - - var webRoutingSettings = new WebRoutingSettings(); - var publishedRouter = BaseWebTest.CreatePublishedRouter(umbracoContextAccessor, webRoutingSettings); - var frequest = await publishedRouter.CreateRequestAsync(new Uri("http://localhost/test")); - frequest.SetPublishedContent(content); - - var routeDefinition = new RouteDefinition - { - PublishedRequest = frequest.Build() - }; - - var routeData = new RouteData(); - routeData.Values.Add(Core.Constants.Web.UmbracoRouteDefinitionDataToken, routeDefinition); - - var ctrl = new TestSurfaceController(umbracoContextAccessor, Mock.Of()); - ctrl.ControllerContext = new ControllerContext(Mock.Of(), routeData, ctrl); - - var result = ctrl.GetContentFromCurrentPage() as PublishedContentResult; - - Assert.AreEqual(12345, result.Content.Id); - } - - public class TestSurfaceController : SurfaceController - { - private readonly IPublishedContentQuery _publishedContentQuery; - - public TestSurfaceController(IUmbracoContextAccessor umbracoContextAccessor, IPublishedContentQuery publishedContentQuery) - : base(umbracoContextAccessor, null, ServiceContext.CreatePartial(), AppCaches.Disabled, null) - { - _publishedContentQuery = publishedContentQuery; - } - - public ActionResult Index() - { - // ReSharper disable once Mvc.ViewNotResolved - return View(); - } - - public ActionResult GetContent(int id) - { - var content = _publishedContentQuery.Content(id); - - return new PublishedContentResult(content); - } - - public ActionResult GetContentFromCurrentPage() - { - var content = CurrentPage; - - return new PublishedContentResult(content); - } - } - - public class PublishedContentResult : ActionResult - { - public IPublishedContent Content { get; set; } - - public PublishedContentResult(IPublishedContent content) - { - Content = content; - } - - public override void ExecuteResult(ControllerContext context) - { - } - - } - } -} diff --git a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs index 7bfa2d6387..e634a9923d 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs @@ -65,7 +65,6 @@ namespace Umbraco.Web.BackOffice.Controllers private readonly IEmailSender _emailSender; private readonly ISmsSender _smsSender; private readonly Core.Hosting.IHostingEnvironment _hostingEnvironment; - private readonly IRequestAccessor _requestAccessor; private readonly LinkGenerator _linkGenerator; private readonly IBackOfficeExternalLoginProviders _externalAuthenticationOptions; private readonly IBackOfficeTwoFactorOptions _backOfficeTwoFactorOptions; @@ -87,7 +86,6 @@ namespace Umbraco.Web.BackOffice.Controllers IEmailSender emailSender, ISmsSender smsSender, Core.Hosting.IHostingEnvironment hostingEnvironment, - IRequestAccessor requestAccessor, LinkGenerator linkGenerator, IBackOfficeExternalLoginProviders externalAuthenticationOptions, IBackOfficeTwoFactorOptions backOfficeTwoFactorOptions) @@ -106,7 +104,6 @@ namespace Umbraco.Web.BackOffice.Controllers _emailSender = emailSender; _smsSender = smsSender; _hostingEnvironment = hostingEnvironment; - _requestAccessor = requestAccessor; _linkGenerator = linkGenerator; _externalAuthenticationOptions = externalAuthenticationOptions; _backOfficeTwoFactorOptions = backOfficeTwoFactorOptions; @@ -625,7 +622,7 @@ namespace Umbraco.Web.BackOffice.Controllers }); // Construct full URL using configured application URL (which will fall back to request) - var applicationUri = _requestAccessor.GetApplicationUrl(); + var applicationUri = _hostingEnvironment.ApplicationMainUrl; var callbackUri = new Uri(applicationUri, action); return callbackUri.ToString(); } diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs index 066f6e3463..92c6d887ff 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs @@ -13,7 +13,7 @@ using Umbraco.Core.Hosting; using Umbraco.Core.Media; using Umbraco.Core.WebAssets; using Umbraco.Extensions; -using Umbraco.Web.BackOffice.HealthCheck; +using Umbraco.Web.BackOffice.HealthChecks; using Umbraco.Web.BackOffice.Profiling; using Umbraco.Web.BackOffice.PropertyEditors; using Umbraco.Web.BackOffice.Routing; diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index 895f2f76b5..0d2b925e31 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -1661,7 +1661,7 @@ namespace Umbraco.Web.BackOffice.Controllers } var toCopyResult = ValidateMoveOrCopy(copy); - if ((toCopyResult.Result is null)) + if (!(toCopyResult.Result is null)) { return toCopyResult.Result; } diff --git a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs index e9761973be..d8e4b83ac9 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs @@ -55,7 +55,6 @@ namespace Umbraco.Web.BackOffice.Controllers private readonly ISqlContext _sqlContext; private readonly IImageUrlGenerator _imageUrlGenerator; private readonly SecuritySettings _securitySettings; - private readonly IRequestAccessor _requestAccessor; private readonly IEmailSender _emailSender; private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; private readonly AppCaches _appCaches; @@ -78,7 +77,6 @@ namespace Umbraco.Web.BackOffice.Controllers ISqlContext sqlContext, IImageUrlGenerator imageUrlGenerator, IOptions securitySettings, - IRequestAccessor requestAccessor, IEmailSender emailSender, IBackOfficeSecurityAccessor backofficeSecurityAccessor, AppCaches appCaches, @@ -99,7 +97,6 @@ namespace Umbraco.Web.BackOffice.Controllers _sqlContext = sqlContext; _imageUrlGenerator = imageUrlGenerator; _securitySettings = securitySettings.Value; - _requestAccessor = requestAccessor; _emailSender = emailSender; _backofficeSecurityAccessor = backofficeSecurityAccessor; _appCaches = appCaches; @@ -567,7 +564,7 @@ namespace Umbraco.Web.BackOffice.Controllers }); // Construct full URL using configured application URL (which will fall back to request) - var applicationUri = _requestAccessor.GetApplicationUrl(); + var applicationUri = _hostingEnvironment.ApplicationMainUrl; var inviteUri = new Uri(applicationUri, action); var emailSubject = _localizedTextService.Localize("user/inviteEmailCopySubject", diff --git a/src/Umbraco.Web.BackOffice/HealthCheck/HealthCheckController.cs b/src/Umbraco.Web.BackOffice/HealthChecks/HealthCheckController.cs similarity index 67% rename from src/Umbraco.Web.BackOffice/HealthCheck/HealthCheckController.cs rename to src/Umbraco.Web.BackOffice/HealthChecks/HealthCheckController.cs index 002cfe4d7b..8bd86eb106 100644 --- a/src/Umbraco.Web.BackOffice/HealthCheck/HealthCheckController.cs +++ b/src/Umbraco.Web.BackOffice/HealthChecks/HealthCheckController.cs @@ -1,19 +1,22 @@ -using System; +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Core; using Umbraco.Core.Configuration.Models; -using Umbraco.Core.HealthCheck; +using Umbraco.Core.HealthChecks; using Umbraco.Web.BackOffice.Controllers; -using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; -using Microsoft.Extensions.Logging; -using Microsoft.AspNetCore.Authorization; using Umbraco.Web.Common.Authorization; -namespace Umbraco.Web.BackOffice.HealthCheck +namespace Umbraco.Web.BackOffice.HealthChecks { /// /// The API controller used to display the health check info and execute any actions @@ -26,12 +29,15 @@ namespace Umbraco.Web.BackOffice.HealthCheck private readonly IList _disabledCheckIds; private readonly ILogger _logger; + /// + /// Initializes a new instance of the class. + /// public HealthCheckController(HealthCheckCollection checks, ILogger logger, IOptions healthChecksSettings) { _checks = checks ?? throw new ArgumentNullException(nameof(checks)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - var healthCheckConfig = healthChecksSettings.Value ?? throw new ArgumentNullException(nameof(healthChecksSettings)); + HealthChecksSettings healthCheckConfig = healthChecksSettings.Value ?? throw new ArgumentNullException(nameof(healthChecksSettings)); _disabledCheckIds = healthCheckConfig.DisabledChecks .Select(x => x.Id) .ToList(); @@ -43,12 +49,12 @@ namespace Umbraco.Web.BackOffice.HealthCheck /// Returns a collection of anonymous objects representing each group. public object GetAllHealthChecks() { - var groups = _checks + IOrderedEnumerable> groups = _checks .Where(x => _disabledCheckIds.Contains(x.Id) == false) .GroupBy(x => x.Group) .OrderBy(x => x.Key); var healthCheckGroups = new List(); - foreach (var healthCheckGroup in groups) + foreach (IGrouping healthCheckGroup in groups) { var hcGroup = new HealthCheckGroup { @@ -63,15 +69,18 @@ namespace Umbraco.Web.BackOffice.HealthCheck return healthCheckGroups; } + /// + /// Gets the status of the HealthCheck with the specified id. + /// [HttpGet] - public object GetStatus(Guid id) + public async Task GetStatus(Guid id) { - var check = GetCheckById(id); + HealthCheck check = GetCheckById(id); try { - //Core.Logging.LogHelper.Debug("Running health check: " + check.Name); - return check.GetStatus(); + _logger.LogDebug("Running health check: " + check.Name); + return await check.GetStatus(); } catch (Exception ex) { @@ -80,20 +89,26 @@ namespace Umbraco.Web.BackOffice.HealthCheck } } + /// + /// Executes a given action from a HealthCheck. + /// [HttpPost] public HealthCheckStatus ExecuteAction(HealthCheckAction action) { - var check = GetCheckById(action.HealthCheckId); + HealthCheck check = GetCheckById(action.HealthCheckId); return check.ExecuteAction(action); } - private Core.HealthCheck.HealthCheck GetCheckById(Guid id) + private HealthCheck GetCheckById(Guid id) { - var check = _checks + HealthCheck check = _checks .Where(x => _disabledCheckIds.Contains(x.Id) == false) .FirstOrDefault(x => x.Id == id); - if (check == null) throw new InvalidOperationException($"No health check found with id {id}"); + if (check == null) + { + throw new InvalidOperationException($"No health check found with id {id}"); + } return check; } diff --git a/src/Umbraco.Web.BackOffice/Routing/BackOfficeAreaRoutes.cs b/src/Umbraco.Web.BackOffice/Routing/BackOfficeAreaRoutes.cs index 39cfe0002b..56b5d26d92 100644 --- a/src/Umbraco.Web.BackOffice/Routing/BackOfficeAreaRoutes.cs +++ b/src/Umbraco.Web.BackOffice/Routing/BackOfficeAreaRoutes.cs @@ -1,3 +1,4 @@ +using System; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Options; using Umbraco.Core; @@ -8,6 +9,7 @@ using Umbraco.Web.BackOffice.Controllers; using Umbraco.Web.Common.Controllers; using Umbraco.Web.Common.Extensions; using Umbraco.Web.Common.Routing; +using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; namespace Umbraco.Web.BackOffice.Routing @@ -15,7 +17,7 @@ namespace Umbraco.Web.BackOffice.Routing /// /// Creates routes for the back office area /// - public class BackOfficeAreaRoutes : IAreaRoutes + public sealed class BackOfficeAreaRoutes : IAreaRoutes { private readonly GlobalSettings _globalSettings; private readonly IHostingEnvironment _hostingEnvironment; @@ -23,6 +25,9 @@ namespace Umbraco.Web.BackOffice.Routing private readonly UmbracoApiControllerTypeCollection _apiControllers; private readonly string _umbracoPathSegment; + /// + /// Initializes a new instance of the class. + /// public BackOfficeAreaRoutes( IOptions globalSettings, IHostingEnvironment hostingEnvironment, @@ -36,6 +41,7 @@ namespace Umbraco.Web.BackOffice.Routing _umbracoPathSegment = _globalSettings.GetUmbracoMvcArea(_hostingEnvironment); } + /// public void CreateRoutes(IEndpointRouteBuilder endpoints) { switch (_runtimeState.Level) @@ -50,7 +56,7 @@ namespace Umbraco.Web.BackOffice.Routing case RuntimeLevel.Run: MapMinimalBackOffice(endpoints); - AutoRouteBackOfficeControllers(endpoints); + AutoRouteBackOfficeApiControllers(endpoints); break; case RuntimeLevel.BootFailed: case RuntimeLevel.Unknown: @@ -85,26 +91,30 @@ namespace Umbraco.Web.BackOffice.Routing } /// - /// Auto-routes all back office controllers + /// Auto-routes all back office api controllers /// - private void AutoRouteBackOfficeControllers(IEndpointRouteBuilder endpoints) + private void AutoRouteBackOfficeApiControllers(IEndpointRouteBuilder endpoints) { // TODO: We could investigate dynamically routing plugin controllers so we don't have to eagerly type scan for them, // it would probably work well, see https://www.strathweb.com/2019/08/dynamic-controller-routing-in-asp-net-core-3-0/ // will probably be what we use for front-end routing too. BTW the orig article about migrating from IRouter to endpoint // routing for things like a CMS is here https://github.com/dotnet/aspnetcore/issues/4221 - foreach (var controller in _apiControllers) + foreach (Type controller in _apiControllers) { + PluginControllerMetadata meta = PluginController.GetMetadata(controller); + // exclude front-end api controllers - var meta = PluginController.GetMetadata(controller); - if (!meta.IsBackOffice) continue; + if (!meta.IsBackOffice) + { + continue; + } endpoints.MapUmbracoApiRoute( meta.ControllerType, _umbracoPathSegment, meta.AreaName, - true, + meta.IsBackOffice, defaultAction: string.Empty); // no default action (this is what we had before) } } diff --git a/src/Umbraco.Web.BackOffice/Routing/PreviewRoutes.cs b/src/Umbraco.Web.BackOffice/Routing/PreviewRoutes.cs index 947e7ac468..d8c93e5985 100644 --- a/src/Umbraco.Web.BackOffice/Routing/PreviewRoutes.cs +++ b/src/Umbraco.Web.BackOffice/Routing/PreviewRoutes.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Options; using Umbraco.Core; @@ -15,7 +15,7 @@ namespace Umbraco.Web.BackOffice.Routing /// /// Creates routes for the preview hub /// - public class PreviewRoutes : IAreaRoutes + public sealed class PreviewRoutes : IAreaRoutes { private readonly IRuntimeState _runtimeState; private readonly string _umbracoPathSegment; diff --git a/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs index 22667f0c30..ec90965455 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs @@ -18,6 +18,7 @@ using Umbraco.Web.Common.ModelBinders; using Umbraco.Web.Models.Trees; using Umbraco.Web.Services; using Umbraco.Web.Trees; +using static Umbraco.Core.Constants.Web.Routing; namespace Umbraco.Web.BackOffice.Trees { @@ -302,8 +303,8 @@ namespace Umbraco.Web.BackOffice.Trees // create proxy route data specifying the action & controller to execute var routeData = new RouteData(new RouteValueDictionary() { - ["action"] = action, - ["controller"] = controllerName + [ActionToken] = action, + [ControllerToken] = controllerName }); if (!(querystring is null)) { diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs index 6b06db315c..ac306809db 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreHostingEnvironment.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Options; @@ -10,26 +11,33 @@ namespace Umbraco.Web.Common.AspNetCore { public class AspNetCoreHostingEnvironment : Core.Hosting.IHostingEnvironment { - private IOptionsMonitor _hostingSettings; + private readonly ISet _applicationUrls = new HashSet(); + private readonly IOptionsMonitor _hostingSettings; + private readonly IOptionsMonitor _webRoutingSettings; private readonly IWebHostEnvironment _webHostEnvironment; private string _localTempPath; - public AspNetCoreHostingEnvironment(IOptionsMonitor hostingSettings, IWebHostEnvironment webHostEnvironment) + public AspNetCoreHostingEnvironment( + IOptionsMonitor hostingSettings, + IOptionsMonitor webRoutingSettings, + IWebHostEnvironment webHostEnvironment) { _hostingSettings = hostingSettings ?? throw new ArgumentNullException(nameof(hostingSettings)); + _webRoutingSettings = webRoutingSettings ?? throw new ArgumentNullException(nameof(webRoutingSettings)); _webHostEnvironment = webHostEnvironment ?? throw new ArgumentNullException(nameof(webHostEnvironment)); SiteName = webHostEnvironment.ApplicationName; ApplicationId = AppDomain.CurrentDomain.Id.ToString(); ApplicationPhysicalPath = webHostEnvironment.ContentRootPath; - - IISVersion = new Version(0, 0); // TODO not necessary IIS } /// public bool IsHosted { get; } = true; + /// + public Uri ApplicationMainUrl { get; private set; } + /// public string SiteName { get; } @@ -39,8 +47,6 @@ namespace Umbraco.Web.Common.AspNetCore /// public string ApplicationPhysicalPath { get; } - public string ApplicationServerAddress { get; } - // TODO how to find this, This is a server thing, not application thing. public string ApplicationVirtualPath => _hostingSettings.CurrentValue.ApplicationVirtualPath?.EnsureStartsWith('/') ?? "/"; @@ -125,6 +131,35 @@ namespace Umbraco.Web.Common.AspNetCore return fullPath; } + + public void EnsureApplicationMainUrl(Uri currentApplicationUrl) + { + // 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? + + if (currentApplicationUrl is null) + { + return; + } + + if (!(_webRoutingSettings.CurrentValue.UmbracoApplicationUrl is null)) + { + return; + } + + var change = !_applicationUrls.Contains(currentApplicationUrl); + if (change) + { + _applicationUrls.Add(currentApplicationUrl); + + ApplicationMainUrl = currentApplicationUrl; + } + } } diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreRequestAccessor.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreRequestAccessor.cs index c88612c2c7..7e1dbb3c9c 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreRequestAccessor.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreRequestAccessor.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Options; @@ -10,42 +11,45 @@ using Umbraco.Web.Routing; namespace Umbraco.Web.Common.AspNetCore { - public class AspNetCoreRequestAccessor : IRequestAccessor, INotificationHandler, INotificationHandler + public class AspNetCoreRequestAccessor : IRequestAccessor, INotificationHandler { private readonly IHttpContextAccessor _httpContextAccessor; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly WebRoutingSettings _webRoutingSettings; private readonly ISet _applicationUrls = new HashSet(); private Uri _currentApplicationUrl; + private object _initLocker = new object(); + private bool _hasAppUrl = false; + private bool _isInit = false; - public AspNetCoreRequestAccessor(IHttpContextAccessor httpContextAccessor, - IUmbracoContextAccessor umbracoContextAccessor, + /// + /// Initializes a new instance of the class. + /// + public AspNetCoreRequestAccessor( + IHttpContextAccessor httpContextAccessor, IOptions webRoutingSettings) { _httpContextAccessor = httpContextAccessor; - _umbracoContextAccessor = umbracoContextAccessor; _webRoutingSettings = webRoutingSettings.Value; } - + /// public string GetRequestValue(string name) => GetFormValue(name) ?? GetQueryStringValue(name); - public string GetFormValue(string name) + private string GetFormValue(string name) { var request = _httpContextAccessor.GetRequiredHttpContext().Request; if (!request.HasFormContentType) return null; return request.Form[name]; } + /// public string GetQueryStringValue(string name) => _httpContextAccessor.GetRequiredHttpContext().Request.Query[name]; - public event EventHandler EndRequest; - - public event EventHandler RouteAttempt; - + /// public Uri GetRequestUrl() => _httpContextAccessor.HttpContext != null ? new Uri(_httpContextAccessor.HttpContext.Request.GetEncodedUrl()) : null; + /// public Uri GetApplicationUrl() { // Fixme: This causes problems with site swap on azure because azure pre-warms a site by calling into `localhost` and when it does that @@ -80,17 +84,16 @@ namespace Umbraco.Web.Common.AspNetCore return _currentApplicationUrl; } + /// + /// This just initializes the application URL on first request attempt + /// TODO: This doesn't belong here, the GetApplicationUrl doesn't belong to IRequestAccessor + /// this should be part of middleware not a lazy init based on an INotification + /// public void Handle(UmbracoRequestBegin notification) - { - var reason = EnsureRoutableOutcome.IsRoutable; //TODO get the correct value here like in UmbracoInjectedModule - RouteAttempt?.Invoke(this, new RoutableAttemptEventArgs(reason, _umbracoContextAccessor.UmbracoContext)); - } - - public void Handle(UmbracoRequestEnd notification) - { - EndRequest?.Invoke(this, new UmbracoRequestEventArgs(_umbracoContextAccessor.UmbracoContext)); - } - - + => LazyInitializer.EnsureInitialized(ref _hasAppUrl, ref _isInit, ref _initLocker, () => + { + GetApplicationUrl(); + return true; + }); } } diff --git a/src/Umbraco.Web.Common/AspNetCore/UmbracoViewPage.cs b/src/Umbraco.Web.Common/AspNetCore/UmbracoViewPage.cs index 74b40403f0..23ae7d7f32 100644 --- a/src/Umbraco.Web.Common/AspNetCore/UmbracoViewPage.cs +++ b/src/Umbraco.Web.Common/AspNetCore/UmbracoViewPage.cs @@ -51,17 +51,29 @@ namespace Umbraco.Web.Common.AspNetCore { get { - if (_helper != null) return _helper; + if (_helper != null) + { + return _helper; + } - var model = ViewData.Model; + TModel model = ViewData.Model; var content = model as IPublishedContent; - if (content == null && model is IContentModel) - content = ((IContentModel) model).Content; + if (content is null && model is IContentModel contentModel) + { + content = contentModel.Content; + } + + if (content is null) + { + content = UmbracoContext?.PublishedRequest?.PublishedContent; + } _helper = Context.RequestServices.GetRequiredService(); - if (content != null) + if (!(content is null)) + { _helper.AssignedContentItem = content; + } return _helper; } diff --git a/src/Umbraco.Web.Common/Controllers/ProxyViewDataFeature.cs b/src/Umbraco.Web.Common/Controllers/ProxyViewDataFeature.cs new file mode 100644 index 0000000000..a672fdfd3c --- /dev/null +++ b/src/Umbraco.Web.Common/Controllers/ProxyViewDataFeature.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Mvc.ViewFeatures; + +namespace Umbraco.Web.Common.Controllers +{ + /// + /// A request feature to allowing proxying viewdata from one controller to another + /// + public sealed class ProxyViewDataFeature + { + /// + /// Initializes a new instance of the class. + /// + public ProxyViewDataFeature(ViewDataDictionary viewData) => ViewData = viewData; + + /// + /// Gets the + /// + public ViewDataDictionary ViewData { get; } + } +} diff --git a/src/Umbraco.Web.Common/Controllers/PublishedRequestFilterAttribute.cs b/src/Umbraco.Web.Common/Controllers/PublishedRequestFilterAttribute.cs index a33dba9ca6..a2c403bf7b 100644 --- a/src/Umbraco.Web.Common/Controllers/PublishedRequestFilterAttribute.cs +++ b/src/Umbraco.Web.Common/Controllers/PublishedRequestFilterAttribute.cs @@ -17,12 +17,13 @@ namespace Umbraco.Web.Common.Controllers /// protected UmbracoRouteValues GetUmbracoRouteValues(ResultExecutingContext context) { - if (!context.RouteData.Values.TryGetValue(Core.Constants.Web.UmbracoRouteDefinitionDataToken, out var def)) + UmbracoRouteValues routeVals = context.HttpContext.Features.Get(); + if (routeVals == null) { - throw new InvalidOperationException($"No route value found with key {Core.Constants.Web.UmbracoRouteDefinitionDataToken}"); + throw new InvalidOperationException($"No {nameof(UmbracoRouteValues)} feature was found in the HttpContext"); } - return (UmbracoRouteValues)def; + return routeVals; } /// diff --git a/src/Umbraco.Web.Common/Controllers/RenderController.cs b/src/Umbraco.Web.Common/Controllers/RenderController.cs index d9a2d05979..48fe50facc 100644 --- a/src/Umbraco.Web.Common/Controllers/RenderController.cs +++ b/src/Umbraco.Web.Common/Controllers/RenderController.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; @@ -71,11 +72,11 @@ namespace Umbraco.Web.Common.Controllers return _umbracoRouteValues; } - _umbracoRouteValues = HttpContext.GetRouteValue(Core.Constants.Web.UmbracoRouteDefinitionDataToken) as UmbracoRouteValues; + _umbracoRouteValues = HttpContext.Features.Get(); if (_umbracoRouteValues == null) { - throw new InvalidOperationException($"No route value found with key {Core.Constants.Web.UmbracoRouteDefinitionDataToken}"); + throw new InvalidOperationException($"No {nameof(UmbracoRouteValues)} feature was found in the HttpContext"); } return _umbracoRouteValues; @@ -149,6 +150,20 @@ namespace Umbraco.Web.Common.Controllers break; case UmbracoRouteResult.Success: default: + + // Check if there's a ProxyViewDataFeature in the request. + // If there it is means that we are proxying/executing this controller + // from another controller and we need to merge it's ViewData with this one + // since this one will be empty. + ProxyViewDataFeature saveViewData = HttpContext.Features.Get(); + if (saveViewData != null) + { + foreach (KeyValuePair kv in saveViewData.ViewData) + { + ViewData[kv.Key] = kv.Value; + } + } + // continue normally await next(); break; diff --git a/src/Umbraco.Web.Common/Controllers/UmbracoApiController.cs b/src/Umbraco.Web.Common/Controllers/UmbracoApiController.cs index e3250c2983..019e3cffdd 100644 --- a/src/Umbraco.Web.Common/Controllers/UmbracoApiController.cs +++ b/src/Umbraco.Web.Common/Controllers/UmbracoApiController.cs @@ -1,4 +1,4 @@ -using Umbraco.Core.Composing; +using Umbraco.Core.Composing; namespace Umbraco.Web.Common.Controllers { @@ -7,8 +7,9 @@ namespace Umbraco.Web.Common.Controllers /// public abstract class UmbracoApiController : UmbracoApiControllerBase, IDiscoverable { - // TODO: Should this only exist in the back office project? These really are only ever used for the back office AFAIK - + /// + /// Initializes a new instance of the class. + /// protected UmbracoApiController() { } diff --git a/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs b/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs index 364c3c1211..811b0dfd69 100644 --- a/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs +++ b/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs @@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Authorization; -using Umbraco.Web.Common.Filters; using Umbraco.Web.Features; namespace Umbraco.Web.Common.Controllers @@ -18,9 +17,10 @@ namespace Umbraco.Web.Common.Controllers [UmbracoApiController] public abstract class UmbracoApiControllerBase : ControllerBase, IUmbracoFeature { - // TODO: Should this only exist in the back office project? These really are only ever used for the back office AFAIK - - public UmbracoApiControllerBase() + /// + /// Initializes a new instance of the class. + /// + protected UmbracoApiControllerBase() { } } diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index b81b0382aa..ba6cbe03a9 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -234,7 +234,6 @@ namespace Umbraco.Web.Common.DependencyInjection builder.Services.AddUnique(); builder.Services.AddUnique(); builder.AddNotificationHandler(); - builder.AddNotificationHandler(); // Password hasher builder.Services.AddUnique(); @@ -383,9 +382,11 @@ namespace Umbraco.Web.Common.DependencyInjection private static IHostingEnvironment GetTemporaryHostingEnvironment(IWebHostEnvironment webHostEnvironment, IConfiguration config) { var hostingSettings = config.GetSection(Core.Constants.Configuration.ConfigHosting).Get() ?? new HostingSettings(); + var webRoutingSettings = config.GetSection(Core.Constants.Configuration.ConfigWebRouting).Get() ?? new WebRoutingSettings(); var wrappedHostingSettings = new OptionsMonitorAdapter(hostingSettings); + var wrappedWebRoutingSettings = new OptionsMonitorAdapter(webRoutingSettings); - return new AspNetCoreHostingEnvironment(wrappedHostingSettings, webHostEnvironment); + return new AspNetCoreHostingEnvironment(wrappedHostingSettings,wrappedWebRoutingSettings, webHostEnvironment); } } } diff --git a/src/Umbraco.Web.Common/Events/UmbracoRequestBegin.cs b/src/Umbraco.Web.Common/Events/UmbracoRequestBegin.cs deleted file mode 100644 index 82d9edacbc..0000000000 --- a/src/Umbraco.Web.Common/Events/UmbracoRequestBegin.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Umbraco. -// See LICENSE for more details. - -using Microsoft.AspNetCore.Http; - -namespace Umbraco.Core.Events -{ - /// - /// Notification raised on each request begin. - /// - public class UmbracoRequestBegin : INotification - { - public UmbracoRequestBegin(HttpContext httpContext) - { - HttpContext = httpContext; - } - - public HttpContext HttpContext { get; } - }; -} diff --git a/src/Umbraco.Web.Common/Extensions/BlockListTemplateExtensions.cs b/src/Umbraco.Web.Common/Extensions/BlockListTemplateExtensions.cs index 06c3efaf02..3edc3714e2 100644 --- a/src/Umbraco.Web.Common/Extensions/BlockListTemplateExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/BlockListTemplateExtensions.cs @@ -1,18 +1,18 @@ using System; -using Umbraco.Core.Models.Blocks; -using Umbraco.Core.Models.PublishedContent; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Umbraco.Core.Models.Blocks; +using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Extensions { public static class BlockListTemplateExtensions { - public const string DefaultFolder = "BlockList/"; - public const string DefaultTemplate = "Default"; + public const string DefaultFolder = "blocklist/"; + public const string DefaultTemplate = "default"; - public static IHtmlContent GetBlockListHtml(this HtmlHelper html, BlockListModel model, string template = DefaultTemplate) + public static IHtmlContent GetBlockListHtml(this IHtmlHelper html, BlockListModel model, string template = DefaultTemplate) { if (model?.Count == 0) return new HtmlString(string.Empty); @@ -20,11 +20,11 @@ namespace Umbraco.Extensions return html.Partial(view, model); } - public static IHtmlContent GetBlockListHtml(this HtmlHelper html, IPublishedProperty property, string template = DefaultTemplate) => GetBlockListHtml(html, property?.GetValue() as BlockListModel, template); + public static IHtmlContent GetBlockListHtml(this IHtmlHelper html, IPublishedProperty property, string template = DefaultTemplate) => GetBlockListHtml(html, property?.GetValue() as BlockListModel, template); - public static IHtmlContent GetBlockListHtml(this HtmlHelper html, IPublishedContent contentItem, string propertyAlias) => GetBlockListHtml(html, contentItem, propertyAlias, DefaultTemplate); + public static IHtmlContent GetBlockListHtml(this IHtmlHelper html, IPublishedContent contentItem, string propertyAlias) => GetBlockListHtml(html, contentItem, propertyAlias, DefaultTemplate); - public static IHtmlContent GetBlockListHtml(this HtmlHelper html, IPublishedContent contentItem, string propertyAlias, string template) + public static IHtmlContent GetBlockListHtml(this IHtmlHelper html, IPublishedContent contentItem, string propertyAlias, string template) { if (propertyAlias == null) throw new ArgumentNullException(nameof(propertyAlias)); if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyAlias)); diff --git a/src/Umbraco.Web.Common/Extensions/EndpointRouteBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/EndpointRouteBuilderExtensions.cs index ccaa29544b..7ebb2f71c1 100644 --- a/src/Umbraco.Web.Common/Extensions/EndpointRouteBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/EndpointRouteBuilderExtensions.cs @@ -29,21 +29,20 @@ namespace Umbraco.Web.Common.Extensions var pattern = new StringBuilder(rootSegment); if (!prefixPathSegment.IsNullOrWhiteSpace()) { - pattern.Append("/").Append(prefixPathSegment); + pattern.Append('/').Append(prefixPathSegment); } if (includeControllerNameInRoute) { - pattern.Append("/").Append(controllerName); + pattern.Append('/').Append(controllerName); } - pattern.Append("/").Append("{action}/{id?}"); + pattern.Append("/{action}/{id?}"); var defaults = defaultAction.IsNullOrWhiteSpace() ? (object)new { controller = controllerName } : new { controller = controllerName, action = defaultAction }; - if (areaName.IsNullOrWhiteSpace()) { endpoints.MapControllerRoute( @@ -70,6 +69,7 @@ namespace Umbraco.Web.Common.Extensions /// /// Used to map Umbraco controllers consistently /// + /// The type to route public static void MapUmbracoRoute( this IEndpointRouteBuilder endpoints, string rootSegment, @@ -82,8 +82,9 @@ namespace Umbraco.Web.Common.Extensions => endpoints.MapUmbracoRoute(typeof(T), rootSegment, areaName, prefixPathSegment, defaultAction, includeControllerNameInRoute, constraints); /// - /// Used to map Umbraco api controllers consistently + /// Used to map controllers as Umbraco API routes consistently /// + /// The type to route public static void MapUmbracoApiRoute( this IEndpointRouteBuilder endpoints, string rootSegment, @@ -95,7 +96,7 @@ namespace Umbraco.Web.Common.Extensions => endpoints.MapUmbracoApiRoute(typeof(T), rootSegment, areaName, isBackOffice, defaultAction, constraints); /// - /// Used to map Umbraco api controllers consistently + /// Used to map controllers as Umbraco API routes consistently /// public static void MapUmbracoApiRoute( this IEndpointRouteBuilder endpoints, diff --git a/src/Umbraco.Web.Common/Extensions/GridTemplateExtensions.cs b/src/Umbraco.Web.Common/Extensions/GridTemplateExtensions.cs index e4a1c0d117..3c3a9d69a1 100644 --- a/src/Umbraco.Web.Common/Extensions/GridTemplateExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/GridTemplateExtensions.cs @@ -12,7 +12,7 @@ namespace Umbraco.Extensions var asString = property.GetValue() as string; if (asString != null && string.IsNullOrEmpty(asString)) return new HtmlString(string.Empty); - var view = "Grid/" + framework; + var view = "grid/" + framework; return html.Partial(view, property.GetValue()); } @@ -34,7 +34,7 @@ namespace Umbraco.Extensions if (propertyAlias == null) throw new ArgumentNullException(nameof(propertyAlias)); if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyAlias)); - var view = "Grid/" + framework; + var view = "grid/" + framework; var prop = contentItem.GetProperty(propertyAlias); if (prop == null) throw new InvalidOperationException("No property type found with alias " + propertyAlias); var model = prop.GetValue(); @@ -63,7 +63,7 @@ namespace Umbraco.Extensions if (propertyAlias == null) throw new ArgumentNullException(nameof(propertyAlias)); if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyAlias)); - var view = "Grid/" + framework; + var view = "grid/" + framework; var prop = contentItem.GetProperty(propertyAlias); if (prop == null) throw new InvalidOperationException("No property type found with alias " + propertyAlias); var model = prop.GetValue(); @@ -78,7 +78,7 @@ namespace Umbraco.Extensions var asString = property.GetValue() as string; if (asString != null && string.IsNullOrEmpty(asString)) return new HtmlString(string.Empty); - var view = "Grid/" + framework; + var view = "grid/" + framework; return html.Partial(view, property.GetValue()); } @@ -100,7 +100,7 @@ namespace Umbraco.Extensions if (propertyAlias == null) throw new ArgumentNullException(nameof(propertyAlias)); if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyAlias)); - var view = "Grid/" + framework; + var view = "grid/" + framework; var prop = contentItem.GetProperty(propertyAlias); if (prop == null) throw new InvalidOperationException("No property type found with alias " + propertyAlias); var model = prop.GetValue(); diff --git a/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs b/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs index 6bfa402154..e0c94efb83 100644 --- a/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs @@ -2,13 +2,15 @@ using System; using System.Collections.Generic; using System.Dynamic; using System.Linq; -using Umbraco.Core; -using Microsoft.AspNetCore.Routing; -using System.Reflection; -using Umbraco.Web.Common.Install; -using Umbraco.Core.Hosting; using System.Linq.Expressions; +using System.Reflection; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Umbraco.Core; +using Umbraco.Core.Hosting; using Umbraco.Web.Common.Controllers; +using Umbraco.Web.Common.Install; +using Umbraco.Web.Mvc; namespace Umbraco.Extensions { @@ -17,8 +19,6 @@ namespace Umbraco.Extensions /// /// Return the back office url if the back office is installed /// - /// - /// public static string GetBackOfficeUrl(this LinkGenerator linkGenerator, IHostingEnvironment hostingEnvironment) { @@ -26,7 +26,10 @@ namespace Umbraco.Extensions try { backOfficeControllerType = Assembly.Load("Umbraco.Web.BackOffice")?.GetType("Umbraco.Web.BackOffice.Controllers.BackOfficeController"); - if (backOfficeControllerType == null) return "/"; // this would indicate that the installer is installed without the back office + if (backOfficeControllerType == null) + { + return "/"; // this would indicate that the installer is installed without the back office + } } catch { @@ -39,47 +42,33 @@ namespace Umbraco.Extensions /// /// Returns the URL for the installer /// - /// - /// public static string GetInstallerUrl(this LinkGenerator linkGenerator) - { - return linkGenerator.GetPathByAction(nameof(InstallController.Index), ControllerExtensions.GetControllerName(), new { area = Constants.Web.Mvc.InstallArea }); - } + => linkGenerator.GetPathByAction(nameof(InstallController.Index), ControllerExtensions.GetControllerName(), new { area = Constants.Web.Mvc.InstallArea }); /// /// Returns the URL for the installer api /// - /// - /// public static string GetInstallerApiUrl(this LinkGenerator linkGenerator) - { - return linkGenerator.GetPathByAction(nameof(InstallApiController.GetSetup), + => linkGenerator.GetPathByAction( + nameof(InstallApiController.GetSetup), ControllerExtensions.GetControllerName(), new { area = Constants.Web.Mvc.InstallArea }).TrimEnd(nameof(InstallApiController.GetSetup)); - } /// /// Return the Url for a Web Api service /// - /// - /// - /// - /// - /// + /// The public static string GetUmbracoApiService(this LinkGenerator linkGenerator, string actionName, object id = null) - where T : UmbracoApiControllerBase - { - return linkGenerator.GetUmbracoApiService(actionName, typeof(T), new Dictionary() - { - ["id"] = id - }); - } + where T : UmbracoApiControllerBase => linkGenerator.GetUmbracoControllerUrl( + actionName, + typeof(T), + new Dictionary() + { + ["id"] = id + }); public static string GetUmbracoApiService(this LinkGenerator linkGenerator, string actionName, IDictionary values) - where T : UmbracoApiControllerBase - { - return linkGenerator.GetUmbracoApiService(actionName, typeof(T), values); - } + where T : UmbracoApiControllerBase => linkGenerator.GetUmbracoControllerUrl(actionName, typeof(T), values); public static string GetUmbracoApiServiceBaseUrl(this LinkGenerator linkGenerator, Expression> methodSelector) where T : UmbracoApiControllerBase @@ -93,66 +82,86 @@ namespace Umbraco.Extensions } /// - /// Return the Url for a Web Api service + /// Return the Url for an Umbraco controller /// - /// - /// - /// - /// - /// - /// - public static string GetUmbracoApiService(this LinkGenerator linkGenerator, string actionName, string controllerName, string area, IDictionary dict = null) + public static string GetUmbracoControllerUrl(this LinkGenerator linkGenerator, string actionName, string controllerName, string area, IDictionary dict = null) { - if (actionName == null) throw new ArgumentNullException(nameof(actionName)); - if (string.IsNullOrWhiteSpace(actionName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(actionName)); - if (controllerName == null) throw new ArgumentNullException(nameof(controllerName)); - if (string.IsNullOrWhiteSpace(controllerName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(controllerName)); + if (actionName == null) + { + throw new ArgumentNullException(nameof(actionName)); + } + + if (string.IsNullOrWhiteSpace(actionName)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(actionName)); + } + + if (controllerName == null) + { + throw new ArgumentNullException(nameof(controllerName)); + } + + if (string.IsNullOrWhiteSpace(controllerName)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(controllerName)); + } if (dict is null) { dict = new Dictionary(); } - - if (!area.IsNullOrWhiteSpace()) { dict["area"] = area; } - - var values = dict.Aggregate(new ExpandoObject() as IDictionary, - (a, p) => { a.Add(p.Key, p.Value); return a; }); + IDictionary values = dict.Aggregate( + new ExpandoObject() as IDictionary, + (a, p) => + { + a.Add(p.Key, p.Value); + return a; + }); return linkGenerator.GetPathByAction(actionName, controllerName, values); } /// - /// Return the Url for a Web Api service + /// Return the Url for an Umbraco controller /// - /// - /// - /// - /// - /// - public static string GetUmbracoApiService(this LinkGenerator linkGenerator, string actionName, Type apiControllerType, IDictionary values = null) + public static string GetUmbracoControllerUrl(this LinkGenerator linkGenerator, string actionName, Type controllerType, IDictionary values = null) { - if (actionName == null) throw new ArgumentNullException(nameof(actionName)); - if (string.IsNullOrWhiteSpace(actionName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(actionName)); - if (apiControllerType == null) throw new ArgumentNullException(nameof(apiControllerType)); + if (actionName == null) + { + throw new ArgumentNullException(nameof(actionName)); + } - var area = ""; + if (string.IsNullOrWhiteSpace(actionName)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(actionName)); + } - if (!typeof(UmbracoApiControllerBase).IsAssignableFrom(apiControllerType)) - throw new InvalidOperationException($"The controller {apiControllerType} is of type {typeof(UmbracoApiControllerBase)}"); + if (controllerType == null) + { + throw new ArgumentNullException(nameof(controllerType)); + } - var metaData = PluginController.GetMetadata(apiControllerType); + var area = string.Empty; + + if (!typeof(ControllerBase).IsAssignableFrom(controllerType)) + { + throw new InvalidOperationException($"The controller {controllerType} is of type {typeof(ControllerBase)}"); + } + + PluginControllerMetadata metaData = PluginController.GetMetadata(controllerType); if (metaData.AreaName.IsNullOrWhiteSpace() == false) { - //set the area to the plugin area + // set the area to the plugin area area = metaData.AreaName; } - return linkGenerator.GetUmbracoApiService(actionName, ControllerExtensions.GetControllerName(apiControllerType), area, values); + + return linkGenerator.GetUmbracoControllerUrl(actionName, ControllerExtensions.GetControllerName(controllerType), area, values); } public static string GetUmbracoApiService(this LinkGenerator linkGenerator, Expression> methodSelector) @@ -170,6 +179,7 @@ namespace Umbraco.Extensions { return linkGenerator.GetUmbracoApiService(method.Name); } + return linkGenerator.GetUmbracoApiService(method.Name, methodParams); } } diff --git a/src/Umbraco.Web.Common/Localization/UmbracoPublishedContentCultureProvider.cs b/src/Umbraco.Web.Common/Localization/UmbracoPublishedContentCultureProvider.cs index 7322ad2869..66177e965a 100644 --- a/src/Umbraco.Web.Common/Localization/UmbracoPublishedContentCultureProvider.cs +++ b/src/Umbraco.Web.Common/Localization/UmbracoPublishedContentCultureProvider.cs @@ -29,7 +29,8 @@ namespace Umbraco.Web.Common.Localization /// public override Task DetermineProviderCultureResult(HttpContext httpContext) { - if (httpContext.GetRouteValue(Core.Constants.Web.UmbracoRouteDefinitionDataToken) is UmbracoRouteValues routeValues) + UmbracoRouteValues routeValues = httpContext.Features.Get(); + if (routeValues != null) { string culture = routeValues.PublishedRequest?.Culture; if (culture != null) diff --git a/src/Umbraco.Web.Common/Macros/PartialViewMacroEngine.cs b/src/Umbraco.Web.Common/Macros/PartialViewMacroEngine.cs index 790e437148..051f545293 100644 --- a/src/Umbraco.Web.Common/Macros/PartialViewMacroEngine.cs +++ b/src/Umbraco.Web.Common/Macros/PartialViewMacroEngine.cs @@ -17,6 +17,7 @@ using Umbraco.Core.Hosting; using Umbraco.Core.Models.PublishedContent; using Umbraco.Extensions; using Umbraco.Web.Macros; +using static Umbraco.Core.Constants.Web.Routing; namespace Umbraco.Web.Common.Macros { @@ -87,8 +88,8 @@ namespace Umbraco.Web.Common.Macros var httpContext = _httpContextAccessor.GetRequiredHttpContext(); //var umbCtx = _getUmbracoContext(); var routeVals = new RouteData(); - routeVals.Values.Add("controller", "PartialViewMacro"); - routeVals.Values.Add("action", "Index"); + routeVals.Values.Add(ControllerToken, "PartialViewMacro"); + routeVals.Values.Add(ActionToken, "Index"); //TODO: Was required for UmbracoViewPage need to figure out if we still need that, i really don't think this is necessary //routeVals.DataTokens.Add(Core.Constants.Web.UmbracoContextDataToken, umbCtx); diff --git a/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs b/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs index 069c38d3c5..5dc604fea9 100644 --- a/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs +++ b/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Events; +using Umbraco.Core.Hosting; using Umbraco.Core.Logging; using Umbraco.Extensions; using Umbraco.Web.Common.Profiler; @@ -36,6 +37,7 @@ namespace Umbraco.Web.Common.Middleware private readonly IBackOfficeSecurityFactory _backofficeSecurityFactory; private readonly PublishedSnapshotServiceEventHandler _publishedSnapshotServiceEventHandler; private readonly IEventAggregator _eventAggregator; + private readonly IHostingEnvironment _hostingEnvironment; private readonly WebProfiler _profiler; private static bool s_cacheInitialized = false; private static bool s_cacheInitializedFlag = false; @@ -51,7 +53,8 @@ namespace Umbraco.Web.Common.Middleware IBackOfficeSecurityFactory backofficeSecurityFactory, PublishedSnapshotServiceEventHandler publishedSnapshotServiceEventHandler, IEventAggregator eventAggregator, - IProfiler profiler) + IProfiler profiler, + IHostingEnvironment hostingEnvironment) { _logger = logger; _umbracoContextFactory = umbracoContextFactory; @@ -59,6 +62,7 @@ namespace Umbraco.Web.Common.Middleware _backofficeSecurityFactory = backofficeSecurityFactory; _publishedSnapshotServiceEventHandler = publishedSnapshotServiceEventHandler; _eventAggregator = eventAggregator; + _hostingEnvironment = hostingEnvironment; _profiler = profiler as WebProfiler; // Ignore if not a WebProfiler } @@ -81,6 +85,10 @@ namespace Umbraco.Web.Common.Middleware _backofficeSecurityFactory.EnsureBackOfficeSecurity(); // Needs to be before UmbracoContext, TODO: Why? UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext(); + Uri currentApplicationUrl = GetApplicationUrlFromCurrentRequest(context.Request); + _hostingEnvironment.EnsureApplicationMainUrl(currentApplicationUrl); + + bool isFrontEndRequest = umbracoContextReference.UmbracoContext.IsFrontEndUmbracoRequest(); var pathAndQuery = context.Request.GetEncodedPathAndQuery(); @@ -95,7 +103,7 @@ namespace Umbraco.Web.Common.Middleware try { - await _eventAggregator.PublishAsync(new UmbracoRequestBegin(context)); + await _eventAggregator.PublishAsync(new UmbracoRequestBegin(umbracoContextReference.UmbracoContext)); } catch (Exception ex) { @@ -111,7 +119,7 @@ namespace Umbraco.Web.Common.Middleware } finally { - await _eventAggregator.PublishAsync(new UmbracoRequestEnd(context)); + await _eventAggregator.PublishAsync(new UmbracoRequestEnd(umbracoContextReference.UmbracoContext)); } } } @@ -119,7 +127,7 @@ namespace Umbraco.Web.Common.Middleware { if (isFrontEndRequest) { - LogHttpRequest.TryGetCurrentHttpRequestId(out var httpRequestId, _requestCache); + LogHttpRequest.TryGetCurrentHttpRequestId(out Guid httpRequestId, _requestCache); _logger.LogTrace("End Request [{HttpRequestId}]: {RequestUrl} ({RequestDuration}ms)", httpRequestId, pathAndQuery, DateTime.Now.Subtract(umbracoContextReference.UmbracoContext.ObjectCreated).TotalMilliseconds); } @@ -138,6 +146,18 @@ namespace Umbraco.Web.Common.Middleware _profiler?.UmbracoApplicationEndRequest(context); } + private Uri GetApplicationUrlFromCurrentRequest(HttpRequest request) + { + // We only consider GET and POST. + // Especially the DEBUG sent when debugging the application is annoying because it uses http, even when the https is available. + if (request.Method == "GET" || request.Method == "POST") + { + return new Uri($"{request.Scheme}://{request.Host}{request.PathBase}", UriKind.Absolute); + + } + return null; + } + /// /// Any object that is in the HttpContext.Items collection that is IDisposable will get disposed on the end of the request /// diff --git a/src/Umbraco.Web.Common/ModelBinders/ContentModelBinder.cs b/src/Umbraco.Web.Common/ModelBinders/ContentModelBinder.cs index cc6678abfc..e3aabe71be 100644 --- a/src/Umbraco.Web.Common/ModelBinders/ContentModelBinder.cs +++ b/src/Umbraco.Web.Common/ModelBinders/ContentModelBinder.cs @@ -30,8 +30,8 @@ namespace Umbraco.Web.Common.ModelBinders // only IPublishedContent will ever exist in the request so when this model binder is used as an IModelBinder // in the aspnet pipeline it will really only support converting from IPublishedContent which is contained // in the UmbracoRouteValues --> IContentModel - if (!bindingContext.ActionContext.RouteData.Values.TryGetValue(Core.Constants.Web.UmbracoRouteDefinitionDataToken, out var source) - || !(source is UmbracoRouteValues umbracoRouteValues)) + UmbracoRouteValues umbracoRouteValues = bindingContext.HttpContext.Features.Get(); + if (umbracoRouteValues is null) { return Task.CompletedTask; } diff --git a/src/Umbraco.Web.Common/Routing/UmbracoRouteValues.cs b/src/Umbraco.Web.Common/Routing/UmbracoRouteValues.cs index 8622bc689e..9ac8c1d2e6 100644 --- a/src/Umbraco.Web.Common/Routing/UmbracoRouteValues.cs +++ b/src/Umbraco.Web.Common/Routing/UmbracoRouteValues.cs @@ -1,4 +1,5 @@ using System; +using Microsoft.AspNetCore.Mvc.Controllers; using Umbraco.Core.Models.PublishedContent; using Umbraco.Extensions; using Umbraco.Web.Common.Controllers; @@ -21,29 +22,25 @@ namespace Umbraco.Web.Common.Routing /// public UmbracoRouteValues( IPublishedRequest publishedRequest, - string controllerName = null, - Type controllerType = null, - string actionName = DefaultActionName, + ControllerActionDescriptor controllerActionDescriptor, string templateName = null, bool hasHijackedRoute = false) { - ControllerName = controllerName ?? ControllerExtensions.GetControllerName(); - ControllerType = controllerType ?? typeof(RenderController); PublishedRequest = publishedRequest; + ControllerActionDescriptor = controllerActionDescriptor; HasHijackedRoute = hasHijackedRoute; - ActionName = actionName; TemplateName = templateName; } /// /// Gets the controller name /// - public string ControllerName { get; } + public string ControllerName => ControllerActionDescriptor.ControllerName; /// /// Gets the action name /// - public string ActionName { get; } + public string ActionName => ControllerActionDescriptor.ActionName; /// /// Gets the template name @@ -51,9 +48,14 @@ namespace Umbraco.Web.Common.Routing public string TemplateName { get; } /// - /// Gets the Controller type found for routing to + /// Gets the controller type /// - public Type ControllerType { get; } + public Type ControllerType => ControllerActionDescriptor.ControllerTypeInfo; + + /// + /// Gets the Controller descriptor found for routing to + /// + public ControllerActionDescriptor ControllerActionDescriptor { get; } /// /// Gets the diff --git a/src/Umbraco.Web.Common/Security/EncryptionHelper.cs b/src/Umbraco.Web.Common/Security/EncryptionHelper.cs index 300afd530d..9dc1cd7497 100644 --- a/src/Umbraco.Web.Common/Security/EncryptionHelper.cs +++ b/src/Umbraco.Web.Common/Security/EncryptionHelper.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; using System.Net; using System.Web; @@ -12,64 +13,80 @@ namespace Umbraco.Web.Common.Security { public class EncryptionHelper { + // TODO: Decide if these belong here... I don't think so since this all has to do with surface controller routes + // could also just be injected too.... + private static IDataProtector CreateDataProtector(IDataProtectionProvider dataProtectionProvider) - { - return dataProtectionProvider.CreateProtector(nameof(EncryptionHelper)); - } + => dataProtectionProvider.CreateProtector(nameof(EncryptionHelper)); public static string Decrypt(string encryptedString, IDataProtectionProvider dataProtectionProvider) - { - return CreateDataProtector(dataProtectionProvider).Unprotect(encryptedString); - } + => CreateDataProtector(dataProtectionProvider).Unprotect(encryptedString); public static string Encrypt(string plainString, IDataProtectionProvider dataProtectionProvider) - { - return CreateDataProtector(dataProtectionProvider).Protect(plainString); - } + => CreateDataProtector(dataProtectionProvider).Protect(plainString); /// /// This is used in methods like BeginUmbracoForm and SurfaceAction to generate an encrypted string which gets submitted in a request for which /// Umbraco can decrypt during the routing process in order to delegate the request to a specific MVC Controller. /// - /// - /// - /// - /// - /// - /// public static string CreateEncryptedRouteString(IDataProtectionProvider dataProtectionProvider, string controllerName, string controllerAction, string area, object additionalRouteVals = null) { - if (dataProtectionProvider == null) throw new ArgumentNullException(nameof(dataProtectionProvider)); - if (controllerName == null) throw new ArgumentNullException(nameof(controllerName)); - if (string.IsNullOrEmpty(controllerName)) throw new ArgumentException("Value can't be empty.", nameof(controllerName)); - if (controllerAction == null) throw new ArgumentNullException(nameof(controllerAction)); - if (string.IsNullOrEmpty(controllerAction)) throw new ArgumentException("Value can't be empty.", nameof(controllerAction)); - if (area == null) throw new ArgumentNullException(nameof(area)); + if (dataProtectionProvider is null) + { + throw new ArgumentNullException(nameof(dataProtectionProvider)); + } - //need to create a params string as Base64 to put into our hidden field to use during the routes + if (string.IsNullOrEmpty(controllerName)) + { + throw new ArgumentException($"'{nameof(controllerName)}' cannot be null or empty.", nameof(controllerName)); + } + + if (string.IsNullOrEmpty(controllerAction)) + { + throw new ArgumentException($"'{nameof(controllerAction)}' cannot be null or empty.", nameof(controllerAction)); + } + + if (area is null) + { + throw new ArgumentNullException(nameof(area)); + } + + // need to create a params string as Base64 to put into our hidden field to use during the routes var surfaceRouteParams = $"{ViewConstants.ReservedAdditionalKeys.Controller}={WebUtility.UrlEncode(controllerName)}&{ViewConstants.ReservedAdditionalKeys.Action}={WebUtility.UrlEncode(controllerAction)}&{ViewConstants.ReservedAdditionalKeys.Area}={area}"; - //checking if the additional route values is already a dictionary and convert to querystring + // checking if the additional route values is already a dictionary and convert to querystring string additionalRouteValsAsQuery; if (additionalRouteVals != null) { if (additionalRouteVals is Dictionary additionalRouteValsAsDictionary) + { additionalRouteValsAsQuery = additionalRouteValsAsDictionary.ToQueryString(); + } else + { additionalRouteValsAsQuery = additionalRouteVals.ToDictionary().ToQueryString(); + } } else + { additionalRouteValsAsQuery = null; + } if (additionalRouteValsAsQuery.IsNullOrWhiteSpace() == false) + { surfaceRouteParams += "&" + additionalRouteValsAsQuery; + } return Encrypt(surfaceRouteParams, dataProtectionProvider); } public static bool DecryptAndValidateEncryptedRouteString(IDataProtectionProvider dataProtectionProvider, string encryptedString, out IDictionary parts) { - if (dataProtectionProvider == null) throw new ArgumentNullException(nameof(dataProtectionProvider)); + if (dataProtectionProvider == null) + { + throw new ArgumentNullException(nameof(dataProtectionProvider)); + } + string decryptedString; try { @@ -81,22 +98,32 @@ namespace Umbraco.Web.Common.Security parts = null; return false; } - var parsedQueryString = HttpUtility.ParseQueryString(decryptedString); + + NameValueCollection parsedQueryString = HttpUtility.ParseQueryString(decryptedString); parts = new Dictionary(); foreach (var key in parsedQueryString.AllKeys) { parts[key] = parsedQueryString[key]; } - //validate all required keys exist - //the controller + + // validate all required keys exist + // the controller if (parts.All(x => x.Key != ViewConstants.ReservedAdditionalKeys.Controller)) + { return false; - //the action + } + + // the action if (parts.All(x => x.Key != ViewConstants.ReservedAdditionalKeys.Action)) + { return false; - //the area + } + + // the area if (parts.All(x => x.Key != ViewConstants.ReservedAdditionalKeys.Area)) + { return false; + } return true; } diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index dea27a0d69..452b5c2071 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -1842,8 +1842,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "optional": true + "dev": true }, "base64id": { "version": "1.0.0", @@ -2052,8 +2051,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true, - "optional": true + "dev": true }, "got": { "version": "8.3.2", @@ -2131,7 +2129,6 @@ "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", "integrity": "sha1-2N0ZeVldLcATnh/ka4tkbLPN8Dg=", "dev": true, - "optional": true, "requires": { "p-finally": "^1.0.0" } @@ -2173,7 +2170,6 @@ "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", "dev": true, - "optional": true, "requires": { "readable-stream": "^2.3.5", "safe-buffer": "^5.1.1" @@ -2183,15 +2179,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true, - "optional": true + "dev": true }, "readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "dev": true, - "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -2207,7 +2201,6 @@ "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, - "optional": true, "requires": { "safe-buffer": "~5.1.0" } @@ -2348,7 +2341,6 @@ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "dev": true, - "optional": true, "requires": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -2374,8 +2366,7 @@ "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", - "dev": true, - "optional": true + "dev": true }, "buffer-equal": { "version": "1.0.0", @@ -2572,7 +2563,6 @@ "resolved": "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz", "integrity": "sha1-bDygcfwZRyCIPC3F2psHS/x+npU=", "dev": true, - "optional": true, "requires": { "get-proxy": "^2.0.0", "isurl": "^1.0.0-alpha5", @@ -3004,8 +2994,7 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "optional": true + "dev": true }, "component-bind": { "version": "1.0.0", @@ -3097,7 +3086,6 @@ "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", "integrity": "sha1-D96NCRIA616AjK8l/mGMAvSOTvo=", "dev": true, - "optional": true, "requires": { "ini": "^1.3.4", "proto-list": "~1.2.1" @@ -3153,7 +3141,6 @@ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", "integrity": "sha1-4TDK9+cnkIfFYWwgB9BIVpiYT70=", "dev": true, - "optional": true, "requires": { "safe-buffer": "5.1.2" } @@ -3595,7 +3582,6 @@ "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", "integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==", "dev": true, - "optional": true, "requires": { "decompress-tar": "^4.0.0", "decompress-tarbz2": "^4.0.0", @@ -3612,7 +3598,6 @@ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", "integrity": "sha1-ecEDO4BRW9bSTsmTPoYMp17ifww=", "dev": true, - "optional": true, "requires": { "pify": "^3.0.0" }, @@ -3621,8 +3606,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true, - "optional": true + "dev": true } } } @@ -3633,7 +3617,6 @@ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", "dev": true, - "optional": true, "requires": { "mimic-response": "^1.0.0" } @@ -3643,7 +3626,6 @@ "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", "integrity": "sha1-cYy9P8sWIJcW5womuE57pFkuWvE=", "dev": true, - "optional": true, "requires": { "file-type": "^5.2.0", "is-stream": "^1.1.0", @@ -3654,8 +3636,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", - "dev": true, - "optional": true + "dev": true } } }, @@ -3664,7 +3645,6 @@ "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", "integrity": "sha1-MIKluIDqQEOBY0nzeLVsUWvho5s=", "dev": true, - "optional": true, "requires": { "decompress-tar": "^4.1.0", "file-type": "^6.1.0", @@ -3677,8 +3657,7 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", "integrity": "sha1-5QzXXTVv/tTjBtxPW89Sp5kDqRk=", - "dev": true, - "optional": true + "dev": true } } }, @@ -3687,7 +3666,6 @@ "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", "integrity": "sha1-wJvDXE0R894J8tLaU+neI+fOHu4=", "dev": true, - "optional": true, "requires": { "decompress-tar": "^4.1.1", "file-type": "^5.2.0", @@ -3698,8 +3676,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", - "dev": true, - "optional": true + "dev": true } } }, @@ -3708,7 +3685,6 @@ "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", "integrity": "sha1-3qrM39FK6vhVePczroIQ+bSEj2k=", "dev": true, - "optional": true, "requires": { "file-type": "^3.8.0", "get-stream": "^2.2.0", @@ -3720,15 +3696,13 @@ "version": "3.9.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=", - "dev": true, - "optional": true + "dev": true }, "get-stream": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", "integrity": "sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=", "dev": true, - "optional": true, "requires": { "object-assign": "^4.0.1", "pinkie-promise": "^2.0.0" @@ -3738,8 +3712,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true, - "optional": true + "dev": true } } }, @@ -4027,8 +4000,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true, - "optional": true + "dev": true } } }, @@ -4045,8 +4017,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", - "dev": true, - "optional": true + "dev": true }, "duplexify": { "version": "3.7.1", @@ -4691,7 +4662,6 @@ "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", "dev": true, - "optional": true, "requires": { "cross-spawn": "^6.0.0", "get-stream": "^4.0.0", @@ -4833,7 +4803,6 @@ "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", "integrity": "sha1-C5jmTtgvWs8PKTG6v2khLvUt3Tc=", "dev": true, - "optional": true, "requires": { "mime-db": "^1.28.0" } @@ -4843,7 +4812,6 @@ "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", "integrity": "sha1-cHgZgdGD7hXROZPIgiBFxQbI8KY=", "dev": true, - "optional": true, "requires": { "ext-list": "^2.0.0", "sort-keys-length": "^1.0.0" @@ -5081,7 +5049,6 @@ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", "dev": true, - "optional": true, "requires": { "pend": "~1.2.0" } @@ -5120,15 +5087,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", "integrity": "sha1-q/c9+rc10EVECr/qLZHzieu/oik=", - "dev": true, - "optional": true + "dev": true }, "filenamify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-2.1.0.tgz", "integrity": "sha1-iPr0lfsbR6v9YSMAACoWIoxnfuk=", "dev": true, - "optional": true, "requires": { "filename-reserved-regex": "^2.0.0", "strip-outer": "^1.0.0", @@ -5477,8 +5442,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha1-a+Dem+mYzhavivwkSXue6bfM2a0=", - "dev": true, - "optional": true + "dev": true }, "fs-mkdirp-stream": { "version": "1.0.0", @@ -5525,8 +5489,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -5547,14 +5510,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5569,20 +5530,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -5699,8 +5657,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -5712,7 +5669,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5727,7 +5683,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -5735,14 +5690,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -5761,7 +5714,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -5842,8 +5794,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -5855,7 +5806,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -5941,8 +5891,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -5978,7 +5927,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5998,7 +5946,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -6042,14 +5989,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -6076,7 +6021,6 @@ "resolved": "https://registry.npmjs.org/get-proxy/-/get-proxy-2.1.0.tgz", "integrity": "sha1-NJ8rTZHUTE1NTpy6KtkBQ/rF75M=", "dev": true, - "optional": true, "requires": { "npm-conf": "^1.1.0" } @@ -6085,15 +6029,13 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true, - "optional": true + "dev": true }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", "dev": true, - "optional": true, "requires": { "pump": "^3.0.0" }, @@ -6103,7 +6045,6 @@ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "dev": true, - "optional": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -6216,8 +6157,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "optional": true + "dev": true }, "pump": { "version": "3.0.0", @@ -7322,8 +7262,7 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", "integrity": "sha1-FAn5i8ACR9pF2mfO4KNvKC/yZFU=", - "dev": true, - "optional": true + "dev": true }, "has-symbols": { "version": "1.0.0", @@ -7336,7 +7275,6 @@ "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", "integrity": "sha1-oEWrOD17SyASoAFIqwql8pAETU0=", "dev": true, - "optional": true, "requires": { "has-symbol-support-x": "^1.4.1" } @@ -7542,8 +7480,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "optional": true + "dev": true }, "ignore": { "version": "4.0.6", @@ -7683,8 +7620,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true + "dev": true }, "svgo": { "version": "1.3.2", @@ -7756,7 +7692,6 @@ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", "dev": true, - "optional": true, "requires": { "repeating": "^2.0.0" } @@ -8083,8 +8018,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", - "dev": true, - "optional": true + "dev": true }, "is-fullwidth-code-point": { "version": "1.0.0", @@ -8134,8 +8068,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", "integrity": "sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=", - "dev": true, - "optional": true + "dev": true }, "is-negated-glob": { "version": "1.0.0", @@ -8173,15 +8106,13 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", "integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==", - "dev": true, - "optional": true + "dev": true }, "is-plain-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", - "dev": true, - "optional": true + "dev": true }, "is-plain-object": { "version": "2.0.4", @@ -8251,15 +8182,13 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", "integrity": "sha1-13hIi9CkZmo76KFIK58rqv7eqLQ=", - "dev": true, - "optional": true + "dev": true }, "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true, - "optional": true + "dev": true }, "is-svg": { "version": "3.0.0", @@ -8354,7 +8283,6 @@ "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", "integrity": "sha1-sn9PSfPNqj6kSgpbfzRi5u3DnWc=", "dev": true, - "optional": true, "requires": { "has-to-string-tag-x": "^1.2.0", "is-object": "^1.0.1" @@ -9250,8 +9178,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", "integrity": "sha1-b54wtHCE2XGnyCD/FabFFnt0wm8=", - "dev": true, - "optional": true + "dev": true }, "lpad-align": { "version": "1.1.2", @@ -9321,8 +9248,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true, - "optional": true + "dev": true }, "map-visit": { "version": "1.0.0", @@ -9490,8 +9416,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", "integrity": "sha1-SSNTiHju9CBjy4o+OweYeBSHqxs=", - "dev": true, - "optional": true + "dev": true }, "minimatch": { "version": "3.0.4", @@ -12838,7 +12763,6 @@ "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", "integrity": "sha1-JWzEe9DiGMJZxOlVC/QTvCGSr/k=", "dev": true, - "optional": true, "requires": { "config-chain": "^1.1.11", "pify": "^3.0.0" @@ -12848,8 +12772,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true, - "optional": true + "dev": true } } }, @@ -12858,7 +12781,6 @@ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", "dev": true, - "optional": true, "requires": { "path-key": "^2.0.0" } @@ -13227,8 +13149,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true, - "optional": true + "dev": true }, "p-is-promise": { "version": "1.1.0", @@ -13265,7 +13186,6 @@ "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-1.2.1.tgz", "integrity": "sha1-XrOzU7f86Z8QGhA4iAuwVOu+o4Y=", "dev": true, - "optional": true, "requires": { "p-finally": "^1.0.0" } @@ -13456,8 +13376,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", - "dev": true, - "optional": true + "dev": true }, "performance-now": { "version": "2.1.0", @@ -13964,8 +13883,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", - "dev": true, - "optional": true + "dev": true }, "prr": { "version": "1.0.1", @@ -14323,7 +14241,6 @@ "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", "dev": true, - "optional": true, "requires": { "is-finite": "^1.0.0" } @@ -14678,7 +14595,6 @@ "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", "dev": true, - "optional": true, "requires": { "commander": "^2.8.1" } @@ -15073,7 +14989,6 @@ "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", "dev": true, - "optional": true, "requires": { "is-plain-obj": "^1.0.0" } @@ -15083,7 +14998,6 @@ "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", "integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=", "dev": true, - "optional": true, "requires": { "sort-keys": "^1.0.0" } @@ -15413,7 +15327,6 @@ "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", "integrity": "sha1-SYdzYmT8NEzyD2w0rKnRPR1O1sU=", "dev": true, - "optional": true, "requires": { "is-natural-number": "^4.0.1" } @@ -15422,8 +15335,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true, - "optional": true + "dev": true }, "strip-final-newline": { "version": "2.0.0", @@ -15453,7 +15365,6 @@ "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", "integrity": "sha1-sv0qv2YEudHmATBXGV34Nrip1jE=", "dev": true, - "optional": true, "requires": { "escape-string-regexp": "^1.0.2" } @@ -15579,7 +15490,6 @@ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", "integrity": "sha1-jqVdqzeXIlPZqa+Q/c1VmuQ1xVU=", "dev": true, - "optional": true, "requires": { "bl": "^1.0.0", "buffer-alloc": "^1.2.0", @@ -15594,15 +15504,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true, - "optional": true + "dev": true }, "readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "dev": true, - "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -15618,7 +15526,6 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, - "optional": true, "requires": { "safe-buffer": "~5.1.0" } @@ -15629,15 +15536,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", "integrity": "sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=", - "dev": true, - "optional": true + "dev": true }, "tempfile": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/tempfile/-/tempfile-2.0.0.tgz", "integrity": "sha1-awRGhWqbERTRhW/8vlCczLCXcmU=", "dev": true, - "optional": true, "requires": { "temp-dir": "^1.0.0", "uuid": "^3.0.1" @@ -15732,8 +15637,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", - "dev": true, - "optional": true + "dev": true }, "timers-ext": { "version": "0.1.7", @@ -15790,8 +15694,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", "integrity": "sha1-STvUj2LXxD/N7TE6A9ytsuEhOoA=", - "dev": true, - "optional": true + "dev": true }, "to-fast-properties": { "version": "2.0.0", @@ -15893,7 +15796,6 @@ "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", "dev": true, - "optional": true, "requires": { "escape-string-regexp": "^1.0.2" } @@ -16029,7 +15931,6 @@ "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", "dev": true, - "optional": true, "requires": { "buffer": "^5.2.1", "through": "^2.3.8" @@ -16238,8 +16139,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", "integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=", - "dev": true, - "optional": true + "dev": true }, "use": { "version": "3.1.1", @@ -16733,7 +16633,6 @@ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", "dev": true, - "optional": true, "requires": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" diff --git a/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js index 6e5ee82ec7..1b3765a5f5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js @@ -435,16 +435,16 @@ function mediaHelper(umbRequestHelper, $http, $log) { imagePath, animationProcessMode: options.animationProcessMode, cacheBusterValue: options.cacheBusterValue, - focalPointLeft: options.focalPoint.left, - focalPointTop: options.focalPoint.top, + focalPointLeft: options.focalPoint ? options.focalPoint.left : null, + focalPointTop: options.focalPoint ? options.focalPoint.top : null, height: options.height, mode: options.mode, upscale: options.upscale || false, width: options.width, - cropX1: options.crop.x1, - cropX2: options.crop.x2, - cropY1: options.crop.y1, - cropY2: options.crop.y2 + cropX1: options.crop ? options.crop.x1 : null, + cropX2: options.crop ? options.crop.x2 : null, + cropY1: options.crop ? options.crop.y1 : null, + cropY2: options.crop ? options.crop.y : null })), "Failed to retrieve processed image URL for image: " + imagePath); } diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/permissionsreport.html b/src/Umbraco.Web.UI.Client/src/installer/steps/permissionsreport.html index 0c78e211dc..366f8b9d19 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/permissionsreport.html +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/permissionsreport.html @@ -1,25 +1,26 @@ 

Your permission settings are not ready for Umbraco

-

+

In order to run Umbraco, you'll need to update your permission settings. - Detailed information about the correct file and folder permissions for Umbraco can be found - here. + Detailed information about the correct file and folder permissions for Umbraco can be found + here.

The following report list the permissions that are currently failing. Once the permissions are fixed press the 'Go back' button to restart the installation.

- +
  • {{category}}

      +
    • {{item}}
- +

diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/healthcheck.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/healthcheck.controller.js index f3446663e1..9810ff9aff 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/healthcheck.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/healthcheck.controller.js @@ -73,7 +73,7 @@ healthCheckResource.getStatus(check.id) .then(function (response) { check.loading = false; - check.status = response; + check.status = response; }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/healthcheck.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/healthcheck.html index 76e4d74458..c20ab18be1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/healthcheck.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/healthcheck.html @@ -149,6 +149,11 @@
+ + diff --git a/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj b/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj index eaf2534341..15fefb0593 100644 --- a/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj +++ b/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj @@ -22,9 +22,6 @@ - - - diff --git a/src/Umbraco.Web.UI.NetCore/Views/Partials/BlockList/Default.cshtml b/src/Umbraco.Web.UI.NetCore/Views/Partials/blocklist/default.cshtml similarity index 72% rename from src/Umbraco.Web.UI.NetCore/Views/Partials/BlockList/Default.cshtml rename to src/Umbraco.Web.UI.NetCore/Views/Partials/blocklist/default.cshtml index 6da7e63ac6..0ad9fd83c3 100644 --- a/src/Umbraco.Web.UI.NetCore/Views/Partials/BlockList/Default.cshtml +++ b/src/Umbraco.Web.UI.NetCore/Views/Partials/blocklist/default.cshtml @@ -1,5 +1,4 @@ -@inherits Umbraco.Web.Common.AspNetCore.UmbracoViewPage -@using Umbraco.Core.Models.Blocks +@inherits Umbraco.Web.Common.AspNetCore.UmbracoViewPage @{ if (!Model.Any()) { return; } } diff --git a/src/Umbraco.Web.UI.NetCore/Views/Partials/Grid/Bootstrap3-Fluid.cshtml b/src/Umbraco.Web.UI.NetCore/Views/Partials/grid/bootstrap3-fluid.cshtml similarity index 88% rename from src/Umbraco.Web.UI.NetCore/Views/Partials/Grid/Bootstrap3-Fluid.cshtml rename to src/Umbraco.Web.UI.NetCore/Views/Partials/grid/bootstrap3-fluid.cshtml index b8bb0b521e..30f55f2058 100644 --- a/src/Umbraco.Web.UI.NetCore/Views/Partials/Grid/Bootstrap3-Fluid.cshtml +++ b/src/Umbraco.Web.UI.NetCore/Views/Partials/grid/bootstrap3-fluid.cshtml @@ -1,7 +1,7 @@ -@inherits Umbraco.Web.Common.AspNetCore.UmbracoViewPage -@using System.Web +@using System.Web @using Microsoft.AspNetCore.Html @using Newtonsoft.Json.Linq +@inherits Umbraco.Web.Common.AspNetCore.UmbracoViewPage @* Razor helpers located at the bottom of this file @@ -16,8 +16,9 @@ { foreach (var section in Model.sections) {
- @foreach (var row in section.rows) { - @renderRow(row) + @foreach (var row in section.rows) + { + renderRow(row); }
} @@ -26,8 +27,9 @@ @foreach (var s in Model.sections) {
- @foreach (var row in s.rows) { - @renderRow(row) + @foreach (var row in s.rows) + { + renderRow(row); }
diff --git a/src/Umbraco.Web.UI.NetCore/Views/Partials/Grid/Bootstrap3.cshtml b/src/Umbraco.Web.UI.NetCore/Views/Partials/grid/bootstrap3.cshtml similarity index 89% rename from src/Umbraco.Web.UI.NetCore/Views/Partials/Grid/Bootstrap3.cshtml rename to src/Umbraco.Web.UI.NetCore/Views/Partials/grid/bootstrap3.cshtml index 5453a7aac5..68ded16619 100644 --- a/src/Umbraco.Web.UI.NetCore/Views/Partials/Grid/Bootstrap3.cshtml +++ b/src/Umbraco.Web.UI.NetCore/Views/Partials/grid/bootstrap3.cshtml @@ -1,7 +1,7 @@ -@inherits Umbraco.Web.Common.AspNetCore.UmbracoViewPage -@using System.Web +@using System.Web @using Microsoft.AspNetCore.Html @using Newtonsoft.Json.Linq +@inherits Umbraco.Web.Common.AspNetCore.UmbracoViewPage @if (Model != null && Model.sections != null) { @@ -12,8 +12,9 @@ { foreach (var section in Model.sections) {
- @foreach (var row in section.rows) { - @renderRow(row, true) + @foreach (var row in section.rows) + { + renderRow(row, true); }
} @@ -23,8 +24,9 @@ @foreach (var s in Model.sections) {
- @foreach (var row in s.rows) { - @renderRow(row, false) + @foreach (var row in s.rows) + { + renderRow(row, false); }
diff --git a/src/Umbraco.Web.UI.NetCore/Views/Partials/Grid/Editors/Base.cshtml b/src/Umbraco.Web.UI.NetCore/Views/Partials/grid/editors/base.cshtml similarity index 100% rename from src/Umbraco.Web.UI.NetCore/Views/Partials/Grid/Editors/Base.cshtml rename to src/Umbraco.Web.UI.NetCore/Views/Partials/grid/editors/base.cshtml diff --git a/src/Umbraco.Web.UI.NetCore/Views/Partials/Grid/Editors/Embed.cshtml b/src/Umbraco.Web.UI.NetCore/Views/Partials/grid/editors/embed.cshtml similarity index 77% rename from src/Umbraco.Web.UI.NetCore/Views/Partials/Grid/Editors/Embed.cshtml rename to src/Umbraco.Web.UI.NetCore/Views/Partials/grid/editors/embed.cshtml index 250310217c..1cb413ef06 100644 --- a/src/Umbraco.Web.UI.NetCore/Views/Partials/Grid/Editors/Embed.cshtml +++ b/src/Umbraco.Web.UI.NetCore/Views/Partials/grid/editors/embed.cshtml @@ -1,5 +1,5 @@ @using Umbraco.Core -@model dynamic +@inherits Umbraco.Web.Common.AspNetCore.UmbracoViewPage @{ string embedValue = Convert.ToString(Model.value); diff --git a/src/Umbraco.Web.UI.NetCore/Views/Partials/Grid/Editors/Macro.cshtml b/src/Umbraco.Web.UI.NetCore/Views/Partials/grid/editors/macro.cshtml similarity index 87% rename from src/Umbraco.Web.UI.NetCore/Views/Partials/Grid/Editors/Macro.cshtml rename to src/Umbraco.Web.UI.NetCore/Views/Partials/grid/editors/macro.cshtml index 26c6e8a09c..87f6ec04af 100644 --- a/src/Umbraco.Web.UI.NetCore/Views/Partials/Grid/Editors/Macro.cshtml +++ b/src/Umbraco.Web.UI.NetCore/Views/Partials/grid/editors/macro.cshtml @@ -1,6 +1,4 @@ @inherits Umbraco.Web.Common.AspNetCore.UmbracoViewPage -@using Umbraco.Web.Website -@inject UmbracoHelper Umbraco; @if (Model.value != null) { diff --git a/src/Umbraco.Web.UI.NetCore/Views/Partials/grid/editors/media.cshtml b/src/Umbraco.Web.UI.NetCore/Views/Partials/grid/editors/media.cshtml new file mode 100644 index 0000000000..41155a390e --- /dev/null +++ b/src/Umbraco.Web.UI.NetCore/Views/Partials/grid/editors/media.cshtml @@ -0,0 +1,61 @@ +@model dynamic +@using Umbraco.Core.PropertyEditors.ValueConverters +@using Umbraco.Core.Media +@inject IImageUrlGenerator ImageUrlGenerator +@if (Model.value != null) +{ + var url = Model.value.image; + if(Model.editor.config != null && Model.editor.config.size != null){ + if (Model.value.coordinates != null) + { + url = ImageCropperTemplateCoreExtensions.GetCropUrl( + (string)url, + ImageUrlGenerator, + width: (int)Model.editor.config.size.width, + height: (int)Model.editor.config.size.height, + cropAlias: "default", + cropDataSet: new ImageCropperValue + { + Crops = new[] + { + new ImageCropperValue.ImageCropperCrop + { + Alias = "default", + Coordinates = new ImageCropperValue.ImageCropperCropCoordinates + { + X1 = (decimal)Model.value.coordinates.x1, + Y1 = (decimal)Model.value.coordinates.y1, + X2 = (decimal)Model.value.coordinates.x2, + Y2 = (decimal)Model.value.coordinates.y2 + } + } + } + }); + } + else + { + url = ImageCropperTemplateCoreExtensions.GetCropUrl( + (string)url, + ImageUrlGenerator, + width: (int)Model.editor.config.size.width, + height: (int)Model.editor.config.size.height, + cropDataSet: new ImageCropperValue + { + FocalPoint = new ImageCropperValue.ImageCropperFocalPoint + { + Top = Model.value.focalPoint == null ? 0.5m : Model.value.focalPoint.top, + Left = Model.value.focalPoint == null ? 0.5m : Model.value.focalPoint.left + } + }); + } + } + + var altText = Model.value.altText ?? Model.value.caption ?? string.Empty; + + @altText + + if (Model.value.caption != null) + { +

@Model.value.caption

+ } +} diff --git a/src/Umbraco.Web.UI.NetCore/Views/Partials/Grid/Editors/Rte.cshtml b/src/Umbraco.Web.UI.NetCore/Views/Partials/grid/editors/rte.cshtml similarity index 88% rename from src/Umbraco.Web.UI.NetCore/Views/Partials/Grid/Editors/Rte.cshtml rename to src/Umbraco.Web.UI.NetCore/Views/Partials/grid/editors/rte.cshtml index 45dff44575..76b665af46 100644 --- a/src/Umbraco.Web.UI.NetCore/Views/Partials/Grid/Editors/Rte.cshtml +++ b/src/Umbraco.Web.UI.NetCore/Views/Partials/grid/editors/rte.cshtml @@ -1,5 +1,5 @@ -@model dynamic -@using Umbraco.Web.Templates +@using Umbraco.Web.Templates +@model dynamic @inject HtmlLocalLinkParser HtmlLocalLinkParser; @inject HtmlUrlParser HtmlUrlParser; @inject HtmlImageSourceParser HtmlImageSourceParser; diff --git a/src/Umbraco.Web.UI.NetCore/Views/Partials/Grid/Editors/TextString.cshtml b/src/Umbraco.Web.UI.NetCore/Views/Partials/grid/editors/textstring.cshtml similarity index 95% rename from src/Umbraco.Web.UI.NetCore/Views/Partials/Grid/Editors/TextString.cshtml rename to src/Umbraco.Web.UI.NetCore/Views/Partials/grid/editors/textstring.cshtml index 2ceac54e26..77d92d6825 100644 --- a/src/Umbraco.Web.UI.NetCore/Views/Partials/Grid/Editors/TextString.cshtml +++ b/src/Umbraco.Web.UI.NetCore/Views/Partials/grid/editors/textstring.cshtml @@ -1,5 +1,4 @@ @using System.Web -@using Umbraco.Extensions @model dynamic @if (Model.editor.config.markup != null) diff --git a/src/Umbraco.Web.UI.NetCore/Views/_ViewImports.cshtml b/src/Umbraco.Web.UI.NetCore/Views/_ViewImports.cshtml new file mode 100644 index 0000000000..7770ecdc5f --- /dev/null +++ b/src/Umbraco.Web.UI.NetCore/Views/_ViewImports.cshtml @@ -0,0 +1,4 @@ +@using Umbraco.Extensions +@using Umbraco.Web.UI.NetCore +@using Umbraco.Web.PublishedModels +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/da.xml b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/da.xml index d7dccbc08c..f0f292bae5 100644 --- a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/da.xml @@ -751,6 +751,7 @@ Ja Mappe Søgeresultater + Læs mere Sortér Afslut sortering Eksempel @@ -1877,4 +1878,10 @@ Mange hilsner fra Umbraco robotten Se udgivet version Forbliv i forhåndsvisning + + Mappeoprettelse + Filskrivning for pakker + Filskrivning + Medie mappeoprettelse + diff --git a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en.xml index c7b974e208..0792c1b70f 100644 --- a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en.xml @@ -740,6 +740,7 @@ One moment please... Previous Properties + Read more Rebuild Email to receive form data Recycle Bin @@ -2551,4 +2552,10 @@ To manage your website, simply open the Umbraco back office and start adding con View published version Stay in preview mode + + Folder creation + File writing for packages + File writing + Media folder creation + diff --git a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en_us.xml index 7a0afb0a69..dc5664213d 100644 --- a/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en_us.xml @@ -749,6 +749,7 @@ One moment please... Previous Properties + Read more Rebuild Email to receive form data Recycle Bin @@ -2130,13 +2131,13 @@ To manage your website, simply open the Umbraco back office and start adding con Your website's SSL certificate is expiring in %0% days. Error pinging the URL %0% - '%1%' You are currently %0% viewing the site using the HTTPS scheme. - The appSetting 'Umbraco.Core.UseHttps' is set to 'false' in your web.config file. Once you access this site using the HTTPS scheme, that should be set to 'true'. - The appSetting 'Umbraco.Core.UseHttps' is set to '%0%' in your web.config file, your cookies are %1% marked as secure. - Could not update the 'Umbraco.Core.UseHttps' setting in your web.config file. Error: %0% + The configuration value 'Umbraco:CMS:Global:UseHttps' is set to 'false' in your web.config file. Once you access this site using the HTTPS scheme, that should be set to 'true'. + The configuration value 'Umbraco:CMS:Global:UseHttps' is set to '%0%' in your web.config file, your cookies are %1% marked as secure. + Could not update the 'Umbraco:CMS:Global:UseHttps' setting in your web.config file. Error: %0% Enable HTTPS Sets umbracoSSL setting to true in the appSettings of the web.config file. - The appSetting 'Umbraco.Core.UseHttps' is now set to 'true' in your web.config file, your cookies will be marked as secure. + The configuration value 'Umbraco:CMS:Global:UseHttps' is now set to 'true' in your web.config file, your cookies will be marked as secure. Fix Cannot fix a check with a value comparison type of 'ShouldNotEqual'. Cannot fix a check with a value comparison type of 'ShouldEqual' with a provided value. @@ -2186,10 +2187,10 @@ To manage your website, simply open the Umbraco back office and start adding con --> %0%.]]> No headers revealing information about the website technology were found. - In the Web.config file, system.net/mailsettings could not be found. - In the Web.config file system.net/mailsettings section, the host is not configured. + The 'Umbraco:CMS:Global:Smtp' configuration could not be found. + The 'Umbraco:CMS:Global:Smtp:Host' configuration could not be found. SMTP settings are configured correctly and the service is operating as expected. - The SMTP server configured with host '%0%' and port '%1%' could not be reached. Please check to ensure the SMTP settings in the Web.config file system.net/mailsettings are correct. + The SMTP server configured with host '%0%' and port '%1%' could not be reached. Please check to ensure the SMTP settings in the configuration 'Umbraco:CMS:Global:Smtp' are correct. %0%.]]> %0%.]]>

Results of the scheduled Umbraco Health Checks run on %0% at %1% are as follows:

%2%]]>
@@ -2572,4 +2573,10 @@ To manage your website, simply open the Umbraco back office and start adding con View published version Stay in preview mode + + Folder creation + File writing for packages + File writing + Media folder creation + diff --git a/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoPageResult.cs b/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoPageResult.cs index d8816c48a9..8a106c0846 100644 --- a/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoPageResult.cs +++ b/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoPageResult.cs @@ -16,122 +16,121 @@ namespace Umbraco.Web.Website.ActionResults /// /// Redirects to an Umbraco page by Id or Entity /// - public class RedirectToUmbracoPageResult : IActionResult + public class RedirectToUmbracoPageResult : IActionResult, IKeepTempDataResult { private IPublishedContent _publishedContent; - private readonly Guid _key; private readonly QueryString _queryString; private readonly IPublishedUrlProvider _publishedUrlProvider; private readonly IUmbracoContextAccessor _umbracoContextAccessor; private string _url; + /// + /// Initializes a new instance of the class. + /// + public RedirectToUmbracoPageResult(Guid key, IPublishedUrlProvider publishedUrlProvider, IUmbracoContextAccessor umbracoContextAccessor) + { + Key = key; + _publishedUrlProvider = publishedUrlProvider; + _umbracoContextAccessor = umbracoContextAccessor; + } + + /// + /// Initializes a new instance of the class. + /// + public RedirectToUmbracoPageResult(Guid key, QueryString queryString, IPublishedUrlProvider publishedUrlProvider, IUmbracoContextAccessor umbracoContextAccessor) + { + Key = key; + _queryString = queryString; + _publishedUrlProvider = publishedUrlProvider; + _umbracoContextAccessor = umbracoContextAccessor; + } + + /// + /// Initializes a new instance of the class. + /// + public RedirectToUmbracoPageResult(IPublishedContent publishedContent, IPublishedUrlProvider publishedUrlProvider, IUmbracoContextAccessor umbracoContextAccessor) + { + _publishedContent = publishedContent; + Key = publishedContent.Key; + _publishedUrlProvider = publishedUrlProvider; + _umbracoContextAccessor = umbracoContextAccessor; + } + + /// + /// Initializes a new instance of the class. + /// + public RedirectToUmbracoPageResult(IPublishedContent publishedContent, QueryString queryString, IPublishedUrlProvider publishedUrlProvider, IUmbracoContextAccessor umbracoContextAccessor) + { + _publishedContent = publishedContent; + Key = publishedContent.Key; + _queryString = queryString; + _publishedUrlProvider = publishedUrlProvider; + _umbracoContextAccessor = umbracoContextAccessor; + } + private string Url { get { - if (!string.IsNullOrWhiteSpace(_url)) return _url; + if (!string.IsNullOrWhiteSpace(_url)) + { + return _url; + } if (PublishedContent is null) + { throw new InvalidOperationException($"Cannot redirect, no entity was found for key {Key}"); + } var result = _publishedUrlProvider.GetUrl(PublishedContent.Id); if (result == "#") + { throw new InvalidOperationException( $"Could not route to entity with key {Key}, the NiceUrlProvider could not generate a URL"); + } _url = result; return _url; } } - public Guid Key => _key; + + public Guid Key { get; } + private IPublishedContent PublishedContent { get { - if (!(_publishedContent is null)) return _publishedContent; + if (!(_publishedContent is null)) + { + return _publishedContent; + } - //need to get the URL for the page - _publishedContent = _umbracoContextAccessor.GetRequiredUmbracoContext().Content.GetById(_key); + // need to get the URL for the page + _publishedContent = _umbracoContextAccessor.GetRequiredUmbracoContext().Content.GetById(Key); - return _publishedContent; + return _publishedContent; } } - /// - /// Creates a new RedirectToUmbracoResult - /// - /// - /// - public RedirectToUmbracoPageResult(Guid key, IPublishedUrlProvider publishedUrlProvider, IUmbracoContextAccessor umbracoContextAccessor) - { - _key = key; - _publishedUrlProvider = publishedUrlProvider; - _umbracoContextAccessor = umbracoContextAccessor; - } - - /// - /// Creates a new RedirectToUmbracoResult - /// - /// - /// - /// - public RedirectToUmbracoPageResult(Guid key, QueryString queryString, IPublishedUrlProvider publishedUrlProvider, IUmbracoContextAccessor umbracoContextAccessor) - { - _key = key; - _queryString = queryString; - _publishedUrlProvider = publishedUrlProvider; - _umbracoContextAccessor = umbracoContextAccessor; - } - - /// - /// Creates a new RedirectToUmbracoResult - /// - /// - /// - /// - public RedirectToUmbracoPageResult(IPublishedContent publishedContent, IPublishedUrlProvider publishedUrlProvider, IUmbracoContextAccessor umbracoContextAccessor) - { - _publishedContent = publishedContent; - _key = publishedContent.Key; - _publishedUrlProvider = publishedUrlProvider; - _umbracoContextAccessor = umbracoContextAccessor; - } - - /// - /// Creates a new RedirectToUmbracoResult - /// - /// - /// - /// - /// - public RedirectToUmbracoPageResult(IPublishedContent publishedContent, QueryString queryString, IPublishedUrlProvider publishedUrlProvider, IUmbracoContextAccessor umbracoContextAccessor) - { - _publishedContent = publishedContent; - _key = publishedContent.Key; - _queryString = queryString; - _publishedUrlProvider = publishedUrlProvider; - _umbracoContextAccessor = umbracoContextAccessor; - } - + /// public Task ExecuteResultAsync(ActionContext context) { - if (context is null) throw new ArgumentNullException(nameof(context)); + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } - var httpContext = context.HttpContext; - var ioHelper = httpContext.RequestServices.GetRequiredService(); - var destinationUrl = ioHelper.ResolveUrl(Url); + HttpContext httpContext = context.HttpContext; + IIOHelper ioHelper = httpContext.RequestServices.GetRequiredService(); + string destinationUrl = ioHelper.ResolveUrl(Url); if (_queryString.HasValue) { destinationUrl += _queryString.ToUriComponent(); } - var tempDataDictionaryFactory = context.HttpContext.RequestServices.GetRequiredService(); - var tempData = tempDataDictionaryFactory.GetTempData(context.HttpContext); - tempData?.Keep(); - httpContext.Response.Redirect(destinationUrl); return Task.CompletedTask; diff --git a/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoUrlResult.cs b/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoUrlResult.cs index 6a7b4d678d..1c12a33714 100644 --- a/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoUrlResult.cs +++ b/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoUrlResult.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ViewFeatures; @@ -14,27 +14,24 @@ namespace Umbraco.Web.Website.ActionResults /// to the current page but the current page is actually a rewritten URL normally done with something like /// Server.Transfer. It is also handy if you want to persist the query strings. /// - public class RedirectToUmbracoUrlResult : IActionResult + public class RedirectToUmbracoUrlResult : IActionResult, IKeepTempDataResult { private readonly IUmbracoContext _umbracoContext; /// - /// Creates a new RedirectToUmbracoResult + /// Initializes a new instance of the class. /// - /// - public RedirectToUmbracoUrlResult(IUmbracoContext umbracoContext) - { - _umbracoContext = umbracoContext; - } + public RedirectToUmbracoUrlResult(IUmbracoContext umbracoContext) => _umbracoContext = umbracoContext; + /// public Task ExecuteResultAsync(ActionContext context) { - if (context is null) throw new ArgumentNullException(nameof(context)); + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } var destinationUrl = _umbracoContext.OriginalRequestUrl.PathAndQuery; - var tempDataDictionaryFactory = context.HttpContext.RequestServices.GetRequiredService(); - var tempData = tempDataDictionaryFactory.GetTempData(context.HttpContext); - tempData?.Keep(); context.HttpContext.Response.Redirect(destinationUrl); diff --git a/src/Umbraco.Web.Website/ActionResults/UmbracoPageResult.cs b/src/Umbraco.Web.Website/ActionResults/UmbracoPageResult.cs index abf269e062..90478c3c89 100644 --- a/src/Umbraco.Web.Website/ActionResults/UmbracoPageResult.cs +++ b/src/Umbraco.Web.Website/ActionResults/UmbracoPageResult.cs @@ -1,169 +1,68 @@ using System; -using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; -using Microsoft.AspNetCore.Mvc.Controllers; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Mvc.Rendering; -using Microsoft.AspNetCore.Mvc.ViewEngines; +using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.ViewFeatures; -using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; -using Umbraco.Core; using Umbraco.Core.Logging; -using Umbraco.Extensions; +using Umbraco.Web.Common.Routing; +using Umbraco.Web.Website.Controllers; +using Umbraco.Web.Website.Routing; +using static Umbraco.Core.Constants.Web.Routing; namespace Umbraco.Web.Website.ActionResults { /// /// Used by posted forms to proxy the result to the page in which the current URL matches on /// + /// + /// This page does not redirect therefore it does not implement because TempData should + /// only be used in situations when a redirect occurs. It is not good practice to use TempData when redirects do not occur + /// so we'll be strict about it and not save it. + /// public class UmbracoPageResult : IActionResult { private readonly IProfilingLogger _profilingLogger; - public UmbracoPageResult(IProfilingLogger profilingLogger) + /// + /// Initializes a new instance of the class. + /// + public UmbracoPageResult(IProfilingLogger profilingLogger) => _profilingLogger = profilingLogger; + + /// + public async Task ExecuteResultAsync(ActionContext context) { - _profilingLogger = profilingLogger; - } - - public Task ExecuteResultAsync(ActionContext context) - { - var routeData = context.RouteData; - - ResetRouteData(routeData); - ValidateRouteData(routeData); - - var factory = context.HttpContext.RequestServices.GetRequiredService(); - Controller controller = null; - - if (!(context is ControllerContext controllerContext)) - return Task.FromCanceled(new System.Threading.CancellationToken()); - - try + UmbracoRouteValues umbracoRouteValues = context.HttpContext.Features.Get(); + if (umbracoRouteValues == null) { - controller = CreateController(controllerContext, factory); - - CopyControllerData(controllerContext, controller); - - ExecuteControllerAction(controllerContext, controller); - } - finally - { - CleanupController(controllerContext, controller, factory); + throw new InvalidOperationException($"Can only use {nameof(UmbracoPageResult)} in the context of an Http POST when using a {nameof(SurfaceController)} form"); } - return Task.CompletedTask; + // Change the route values back to the original request vals + context.RouteData.Values[ControllerToken] = umbracoRouteValues.ControllerName; + context.RouteData.Values[ActionToken] = umbracoRouteValues.ActionName; + + // Create a new context and excute the original controller... + // Copy the action context - this also copies the ModelState + var renderActionContext = new ActionContext(context) + { + ActionDescriptor = umbracoRouteValues.ControllerActionDescriptor + }; + IActionInvokerFactory actionInvokerFactory = context.HttpContext.RequestServices.GetRequiredService(); + IActionInvoker actionInvoker = actionInvokerFactory.CreateInvoker(renderActionContext); + await ExecuteControllerAction(actionInvoker); } /// /// Executes the controller action /// - private void ExecuteControllerAction(ControllerContext context, Controller controller) + private async Task ExecuteControllerAction(IActionInvoker actionInvoker) { using (_profilingLogger.TraceDuration("Executing Umbraco RouteDefinition controller", "Finished")) { - //TODO I do not think this will work, We need to test this, when we can, in the .NET Core executable. - var aec = new ActionExecutingContext(context, new List(), new Dictionary(), controller); - var actionExecutedDelegate = CreateActionExecutedDelegate(aec); - - controller.OnActionExecutionAsync(aec, actionExecutedDelegate); + await actionInvoker.InvokeAsync(); } } - - /// - /// Creates action execution delegate from ActionExecutingContext - /// - private static ActionExecutionDelegate CreateActionExecutedDelegate(ActionExecutingContext context) - { - var actionExecutedContext = new ActionExecutedContext(context, context.Filters, context.Controller) - { - Result = context.Result, - }; - return () => Task.FromResult(actionExecutedContext); - } - - /// - /// Since we could be returning the current page from a surface controller posted values in which the routing values are changed, we - /// need to revert these values back to nothing in order for the normal page to render again. - /// - private static void ResetRouteData(RouteData routeData) - { - routeData.DataTokens["area"] = null; - routeData.DataTokens["Namespaces"] = null; - } - - /// - /// Validate that the current page execution is not being handled by the normal umbraco routing system - /// - private static void ValidateRouteData(RouteData routeData) - { - if (routeData.Values.ContainsKey(Constants.Web.UmbracoRouteDefinitionDataToken) == false) - { - throw new InvalidOperationException("Can only use " + typeof(UmbracoPageResult).Name + - " in the context of an Http POST when using a SurfaceController form"); - } - } - - /// - /// Ensure ModelState, ViewData and TempData is copied across - /// - private static void CopyControllerData(ControllerContext context, Controller controller) - { - controller.ViewData.ModelState.Merge(context.ModelState); - - foreach (var d in controller.ViewData) - controller.ViewData[d.Key] = d.Value; - - // We cannot simply merge the temp data because during controller execution it will attempt to 'load' temp data - // but since it has not been saved, there will be nothing to load and it will revert to nothing, so the trick is - // to Save the state of the temp data first then it will automatically be picked up. - // http://issues.umbraco.org/issue/U4-1339 - - var targetController = controller; - var tempDataDictionaryFactory = context.HttpContext.RequestServices.GetRequiredService(); - var tempData = tempDataDictionaryFactory.GetTempData(context.HttpContext); - - targetController.TempData = tempData; - targetController.TempData.Save(); - } - - /// - /// Creates a controller using the controller factory - /// - private static Controller CreateController(ControllerContext context, IControllerFactory factory) - { - if (!(factory.CreateController(context) is Controller controller)) - throw new InvalidOperationException("Could not create controller with name " + context.ActionDescriptor.ControllerName + "."); - - return controller; - } - - /// - /// Cleans up the controller by releasing it using the controller factory, and by disposing it. - /// - private static void CleanupController(ControllerContext context, Controller controller, IControllerFactory factory) - { - if (!(controller is null)) - factory.ReleaseController(context, controller); - - controller?.DisposeIfDisposable(); - } - - private class DummyView : IView - { - public DummyView(string path) - { - Path = path; - } - - public Task RenderAsync(ViewContext context) - { - return Task.CompletedTask; - } - - public string Path { get; } - } } } diff --git a/src/Umbraco.Web.Website/Controllers/SurfaceController.cs b/src/Umbraco.Web.Website/Controllers/SurfaceController.cs index 3a6a7a3507..6306695391 100644 --- a/src/Umbraco.Web.Website/Controllers/SurfaceController.cs +++ b/src/Umbraco.Web.Website/Controllers/SurfaceController.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Specialized; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Logging; @@ -20,13 +21,15 @@ namespace Umbraco.Web.Website.Controllers // TODO: Migrate MergeModelStateToChildAction and MergeParentContextViewData action filters // [MergeModelStateToChildAction] // [MergeParentContextViewData] + [AutoValidateAntiforgeryToken] public abstract class SurfaceController : PluginController { + /// + /// Initializes a new instance of the class. + /// protected SurfaceController(IUmbracoContextAccessor umbracoContextAccessor, IUmbracoDatabaseFactory databaseFactory, ServiceContext services, AppCaches appCaches, IProfilingLogger profilingLogger, IPublishedUrlProvider publishedUrlProvider) : base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger) - { - PublishedUrlProvider = publishedUrlProvider; - } + => PublishedUrlProvider = publishedUrlProvider; protected IPublishedUrlProvider PublishedUrlProvider { get; } @@ -37,14 +40,13 @@ namespace Umbraco.Web.Website.Controllers { get { - var routeDefAttempt = TryGetRouteDefinitionFromAncestorViewContexts(); - if (routeDefAttempt.Success == false) + UmbracoRouteValues umbracoRouteValues = HttpContext.Features.Get(); + if (umbracoRouteValues is null) { - throw routeDefAttempt.Exception; + throw new InvalidOperationException($"No {nameof(UmbracoRouteValues)} feature was found in the HttpContext"); } - var routeDef = routeDefAttempt.Result; - return routeDef.PublishedRequest.PublishedContent; + return umbracoRouteValues.PublishedRequest.PublishedContent; } } @@ -52,49 +54,37 @@ namespace Umbraco.Web.Website.Controllers /// Redirects to the Umbraco page with the given id /// protected RedirectToUmbracoPageResult RedirectToUmbracoPage(Guid contentKey) - { - return new RedirectToUmbracoPageResult(contentKey, PublishedUrlProvider, UmbracoContextAccessor); - } + => new RedirectToUmbracoPageResult(contentKey, PublishedUrlProvider, UmbracoContextAccessor); /// /// Redirects to the Umbraco page with the given id and passes provided querystring /// protected RedirectToUmbracoPageResult RedirectToUmbracoPage(Guid contentKey, QueryString queryString) - { - return new RedirectToUmbracoPageResult(contentKey, queryString, PublishedUrlProvider, UmbracoContextAccessor); - } + => new RedirectToUmbracoPageResult(contentKey, queryString, PublishedUrlProvider, UmbracoContextAccessor); /// /// Redirects to the Umbraco page with the given published content /// protected RedirectToUmbracoPageResult RedirectToUmbracoPage(IPublishedContent publishedContent) - { - return new RedirectToUmbracoPageResult(publishedContent, PublishedUrlProvider, UmbracoContextAccessor); - } + => new RedirectToUmbracoPageResult(publishedContent, PublishedUrlProvider, UmbracoContextAccessor); /// /// Redirects to the Umbraco page with the given published content and passes provided querystring /// protected RedirectToUmbracoPageResult RedirectToUmbracoPage(IPublishedContent publishedContent, QueryString queryString) - { - return new RedirectToUmbracoPageResult(publishedContent, queryString, PublishedUrlProvider, UmbracoContextAccessor); - } + => new RedirectToUmbracoPageResult(publishedContent, queryString, PublishedUrlProvider, UmbracoContextAccessor); /// /// Redirects to the currently rendered Umbraco page /// protected RedirectToUmbracoPageResult RedirectToCurrentUmbracoPage() - { - return new RedirectToUmbracoPageResult(CurrentPage, PublishedUrlProvider, UmbracoContextAccessor); - } + => new RedirectToUmbracoPageResult(CurrentPage, PublishedUrlProvider, UmbracoContextAccessor); /// /// Redirects to the currently rendered Umbraco page and passes provided querystring /// protected RedirectToUmbracoPageResult RedirectToCurrentUmbracoPage(QueryString queryString) - { - return new RedirectToUmbracoPageResult(CurrentPage, queryString, PublishedUrlProvider, UmbracoContextAccessor); - } + => new RedirectToUmbracoPageResult(CurrentPage, queryString, PublishedUrlProvider, UmbracoContextAccessor); /// /// Redirects to the currently rendered Umbraco URL @@ -105,35 +95,15 @@ namespace Umbraco.Web.Website.Controllers /// Server.Transfer.* /// protected RedirectToUmbracoUrlResult RedirectToCurrentUmbracoUrl() - { - return new RedirectToUmbracoUrlResult(UmbracoContext); - } + => new RedirectToUmbracoUrlResult(UmbracoContext); /// /// Returns the currently rendered Umbraco page /// protected UmbracoPageResult CurrentUmbracoPage() { + HttpContext.Features.Set(new ProxyViewDataFeature(ViewData)); return new UmbracoPageResult(ProfilingLogger); } - - /// - /// we need to recursively find the route definition based on the parent view context - /// - private Attempt TryGetRouteDefinitionFromAncestorViewContexts() - { - var currentContext = ControllerContext; - while (!(currentContext is null)) - { - var currentRouteData = currentContext.RouteData; - if (currentRouteData.Values.ContainsKey(Core.Constants.Web.UmbracoRouteDefinitionDataToken)) - { - return Attempt.Succeed((UmbracoRouteValues)currentRouteData.Values[Core.Constants.Web.UmbracoRouteDefinitionDataToken]); - } - } - - return Attempt.Fail( - new InvalidOperationException("Cannot find the Umbraco route definition in the route values, the request must be made in the context of an Umbraco request")); - } } } diff --git a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs index 3d0c482a30..b1d21e87b9 100644 --- a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs @@ -1,11 +1,9 @@ using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Core.DependencyInjection; using Umbraco.Extensions; using Umbraco.Infrastructure.DependencyInjection; -using Umbraco.Infrastructure.PublishedCache.DependencyInjection; using Umbraco.ModelsBuilder.Embedded.DependencyInjection; using Umbraco.Web.Common.Routing; using Umbraco.Web.Website.Collections; @@ -39,11 +37,13 @@ namespace Umbraco.Web.Website.DependencyInjection builder.Services.AddDataProtection(); builder.Services.AddScoped(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder .AddDistributedCache() .AddModelsBuilder(); diff --git a/src/Umbraco.Web.Website/Extensions/HtmlHelperRenderExtensions.cs b/src/Umbraco.Web.Website/Extensions/HtmlHelperRenderExtensions.cs index 48367c8e44..1c0e417047 100644 --- a/src/Umbraco.Web.Website/Extensions/HtmlHelperRenderExtensions.cs +++ b/src/Umbraco.Web.Website/Extensions/HtmlHelperRenderExtensions.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Umbraco.Core; using Umbraco.Core.Cache; @@ -22,6 +23,7 @@ using Umbraco.Web; using Umbraco.Web.Common.Controllers; using Umbraco.Web.Common.Mvc; using Umbraco.Web.Common.Security; +using Umbraco.Web.Mvc; using Umbraco.Web.Website.Collections; using Umbraco.Web.Website.Controllers; @@ -33,34 +35,20 @@ namespace Umbraco.Extensions public static class HtmlHelperRenderExtensions { private static T GetRequiredService(IHtmlHelper htmlHelper) - { - return GetRequiredService(htmlHelper.ViewContext); - } + => GetRequiredService(htmlHelper.ViewContext); private static T GetRequiredService(ViewContext viewContext) - { - return viewContext.HttpContext.RequestServices.GetRequiredService(); - } + => viewContext.HttpContext.RequestServices.GetRequiredService(); /// /// Renders the markup for the profiler /// - /// - /// public static IHtmlContent RenderProfiler(this IHtmlHelper helper) - { - return new HtmlString(GetRequiredService(helper).Render()); - } + => new HtmlString(GetRequiredService(helper).Render()); /// /// Renders a partial view that is found in the specified area /// - /// - /// - /// - /// - /// - /// public static IHtmlContent AreaPartial(this IHtmlHelper helper, string partial, string area, object model = null, ViewDataDictionary viewData = null) { var originalArea = helper.ViewContext.RouteData.DataTokens["area"]; @@ -74,8 +62,6 @@ namespace Umbraco.Extensions /// Will render the preview badge when in preview mode which is not required ever unless the MVC page you are /// using does not inherit from UmbracoViewPage /// - /// - /// /// /// See: http://issues.umbraco.org/issue/U4-1614 /// @@ -92,7 +78,8 @@ namespace Umbraco.Extensions umbrcoContext.PublishedRequest.PublishedContent.Id); return new HtmlString(htmlBadge); } - return new HtmlString(string.Empty); + + return HtmlString.Empty; } @@ -107,9 +94,9 @@ namespace Umbraco.Extensions Func contextualKeyBuilder = null) { var cacheKey = new StringBuilder(partialViewName); - //let's always cache by the current culture to allow variants to have different cache results + // let's always cache by the current culture to allow variants to have different cache results var cultureName = System.Threading.Thread.CurrentThread.CurrentUICulture.Name; - if (!String.IsNullOrEmpty(cultureName)) + if (!string.IsNullOrEmpty(cultureName)) { cacheKey.AppendFormat("{0}-", cultureName); } @@ -121,16 +108,19 @@ namespace Umbraco.Extensions { throw new InvalidOperationException("Cannot cache by page if the UmbracoContext has not been initialized, this parameter can only be used in the context of an Umbraco request"); } + cacheKey.AppendFormat("{0}-", umbracoContext.PublishedRequest?.PublishedContent?.Id ?? 0); } + if (cacheByMember) { - //TODO reintroduce when members are migrated + // TODO reintroduce when members are migrated throw new NotImplementedException("Reintroduce when members are migrated"); // var helper = Current.MembershipHelper; // var currentMember = helper.GetCurrentMember(); // cacheKey.AppendFormat("m{0}-", currentMember?.Id ?? 0); } + if (contextualKeyBuilder != null) { var contextualKey = contextualKeyBuilder(model, viewData); @@ -201,60 +191,100 @@ namespace Umbraco.Extensions return tagBuilder; } - // TODO what to do here? This will be view components right? - // /// - // /// Returns the result of a child action of a strongly typed SurfaceController - // /// - // /// - // /// - // /// - // /// - // public static IHtmlContent Action(this IHtmlHelper htmlHelper, string actionName) - // where T : SurfaceController - // { - // return htmlHelper.Action(actionName, typeof(T)); - // } - // + /// + /// Returns the result of a child action of a strongly typed SurfaceController + /// + /// The + public static IHtmlContent ActionLink(this IHtmlHelper htmlHelper, string actionName) + where T : SurfaceController => htmlHelper.ActionLink(actionName, typeof(T)); - // TODO what to do here? This will be view components right? - // /// - // /// Returns the result of a child action of a SurfaceController - // /// - // /// - // /// - // /// - // /// - // /// - // public static IHtmlContent Action(this IHtmlHelper htmlHelper, string actionName, Type surfaceType) - // { - // if (actionName == null) throw new ArgumentNullException(nameof(actionName)); - // if (string.IsNullOrWhiteSpace(actionName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(actionName)); - // if (surfaceType == null) throw new ArgumentNullException(nameof(surfaceType)); - // - // var routeVals = new RouteValueDictionary(new {area = ""}); - // - // var surfaceControllerTypeCollection = GetRequiredService(htmlHelper); - // var surfaceController = surfaceControllerTypeCollection.SingleOrDefault(x => x == surfaceType); - // if (surfaceController == null) - // throw new InvalidOperationException("Could not find the surface controller of type " + surfaceType.FullName); - // var metaData = PluginController.GetMetadata(surfaceController); - // if (!metaData.AreaName.IsNullOrWhiteSpace()) - // { - // //set the area to the plugin area - // if (routeVals.ContainsKey("area")) - // { - // routeVals["area"] = metaData.AreaName; - // } - // else - // { - // routeVals.Add("area", metaData.AreaName); - // } - // } - // - // return htmlHelper.Action(actionName, metaData.ControllerName, routeVals); - // } + /// + /// Returns the result of a child action of a SurfaceController + /// + public static IHtmlContent ActionLink(this IHtmlHelper htmlHelper, string actionName, Type surfaceType) + { + if (actionName == null) + { + throw new ArgumentNullException(nameof(actionName)); + } - #region BeginUmbracoForm + if (string.IsNullOrWhiteSpace(actionName)) + { + throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(actionName)); + } + + if (surfaceType == null) + { + throw new ArgumentNullException(nameof(surfaceType)); + } + + SurfaceControllerTypeCollection surfaceControllerTypeCollection = GetRequiredService(htmlHelper); + Type surfaceController = surfaceControllerTypeCollection.SingleOrDefault(x => x == surfaceType); + if (surfaceController == null) + { + throw new InvalidOperationException("Could not find the surface controller of type " + surfaceType.FullName); + } + + var routeVals = new RouteValueDictionary(new { area = "" }); + + PluginControllerMetadata metaData = PluginController.GetMetadata(surfaceController); + if (!metaData.AreaName.IsNullOrWhiteSpace()) + { + // set the area to the plugin area + if (routeVals.ContainsKey("area")) + { + routeVals["area"] = metaData.AreaName; + } + else + { + routeVals.Add("area", metaData.AreaName); + } + } + + return htmlHelper.ActionLink(actionName, metaData.ControllerName, routeVals); + } + + /// + /// Outputs the hidden html input field for Surface Controller route information + /// + /// The type + /// + /// Typically not used directly because BeginUmbracoForm automatically outputs this value when routing + /// for surface controllers. But this could be used in case a form tag is manually created. + /// + public static IHtmlContent SurfaceControllerHiddenInput( + this IHtmlHelper htmlHelper, + string controllerAction, + string area, + object additionalRouteVals = null) + where TSurface : SurfaceController + { + var inputField = GetSurfaceControllerHiddenInput( + GetRequiredService(htmlHelper), + ControllerExtensions.GetControllerName(), + controllerAction, + area, + additionalRouteVals); + + return new HtmlString(inputField); + } + + private static string GetSurfaceControllerHiddenInput( + IDataProtectionProvider dataProtectionProvider, + string controllerName, + string controllerAction, + string area, + object additionalRouteVals = null) + { + var encryptedString = EncryptionHelper.CreateEncryptedRouteString( + dataProtectionProvider, + controllerName, + controllerAction, + area, + additionalRouteVals); + + return ""; + } /// /// Used for rendering out the Form for BeginUmbracoForm @@ -262,9 +292,7 @@ namespace Umbraco.Extensions internal class UmbracoForm : MvcForm { private readonly ViewContext _viewContext; - private bool _disposed; - private readonly string _encryptedString; - private readonly string _controllerName; + private readonly string _surfaceControllerInput; /// /// Initializes a new instance of the class. @@ -279,34 +307,25 @@ namespace Umbraco.Extensions : base(viewContext, htmlEncoder) { _viewContext = viewContext; - _controllerName = controllerName; - _encryptedString = EncryptionHelper.CreateEncryptedRouteString(GetRequiredService(viewContext), controllerName, controllerAction, area, additionalRouteVals); + _surfaceControllerInput = GetSurfaceControllerHiddenInput( + GetRequiredService(viewContext), + controllerName, + controllerAction, + area, + additionalRouteVals); } - protected new void Dispose() + protected override void GenerateEndForm() { - if (_disposed) - { - return; - } - - _disposed = true; - - // Detect if the call is targeting UmbRegisterController/UmbProfileController/UmbLoginStatusController/UmbLoginController and if it is we automatically output a AntiForgeryToken() - // We have a controllerName and area so we can match - if (_controllerName == "UmbRegister" - || _controllerName == "UmbProfile" - || _controllerName == "UmbLoginStatus" - || _controllerName == "UmbLogin") - { - IAntiforgery antiforgery = _viewContext.HttpContext.RequestServices.GetRequiredService(); - _viewContext.Writer.Write(antiforgery.GetHtml(_viewContext.HttpContext).ToString()); - } + // Always output an anti-forgery token + IAntiforgery antiforgery = _viewContext.HttpContext.RequestServices.GetRequiredService(); + IHtmlContent antiforgeryHtml = antiforgery.GetHtml(_viewContext.HttpContext); + _viewContext.Writer.Write(antiforgeryHtml.ToHtmlString()); // write out the hidden surface form routes - _viewContext.Writer.Write(""); + _viewContext.Writer.Write(_surfaceControllerInput); - base.Dispose(); + base.GenerateEndForm(); } } @@ -424,7 +443,7 @@ namespace Umbraco.Extensions throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(controllerName)); } - return html.BeginUmbracoForm(action, controllerName, "", additionalRouteVals, htmlAttributes); + return html.BeginUmbracoForm(action, controllerName, string.Empty, additionalRouteVals, htmlAttributes); } /// @@ -721,7 +740,6 @@ namespace Umbraco.Extensions return theForm; } - #endregion #region If diff --git a/src/Umbraco.Web.Website/Extensions/LinkGeneratorExtensions.cs b/src/Umbraco.Web.Website/Extensions/LinkGeneratorExtensions.cs new file mode 100644 index 0000000000..86f90c5a97 --- /dev/null +++ b/src/Umbraco.Web.Website/Extensions/LinkGeneratorExtensions.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Dynamic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using Microsoft.AspNetCore.Routing; +using Umbraco.Core; +using Umbraco.Web.Website.Controllers; + +namespace Umbraco.Extensions +{ + public static class LinkGeneratorExtensions + { + /// + /// Return the Url for a Surface Controller + /// + /// The + public static string GetUmbracoSurfaceUrl(this LinkGenerator linkGenerator, Expression> methodSelector) + where T : SurfaceController + { + MethodInfo method = ExpressionHelper.GetMethodInfo(methodSelector); + IDictionary methodParams = ExpressionHelper.GetMethodParams(methodSelector); + + if (method == null) + { + throw new MissingMethodException( + $"Could not find the method {methodSelector} on type {typeof(T)} or the result "); + } + + if (methodParams.Any() == false) + { + return linkGenerator.GetUmbracoSurfaceUrl(method.Name); + } + + return linkGenerator.GetUmbracoSurfaceUrl(method.Name, methodParams); + } + + /// + /// Return the Url for a Surface Controller + /// + /// The + public static string GetUmbracoSurfaceUrl(this LinkGenerator linkGenerator, string actionName, object id = null) + where T : SurfaceController => linkGenerator.GetUmbracoControllerUrl( + actionName, + typeof(T), + new Dictionary() + { + ["id"] = id + }); + } +} diff --git a/src/Umbraco.Web.Website/Extensions/UmbracoWebsiteApplicationBuilderExtensions.cs b/src/Umbraco.Web.Website/Extensions/UmbracoWebsiteApplicationBuilderExtensions.cs index 36e5ff9214..af7041011c 100644 --- a/src/Umbraco.Web.Website/Extensions/UmbracoWebsiteApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Website/Extensions/UmbracoWebsiteApplicationBuilderExtensions.cs @@ -37,6 +37,9 @@ namespace Umbraco.Extensions { app.UseEndpoints(endpoints => { + FrontEndRoutes surfaceRoutes = app.ApplicationServices.GetRequiredService(); + surfaceRoutes.CreateRoutes(endpoints); + endpoints.MapDynamicControllerRoute("/{**slug}"); }); diff --git a/src/Umbraco.Web.Website/Routing/ControllerActionSearcher.cs b/src/Umbraco.Web.Website/Routing/ControllerActionSearcher.cs new file mode 100644 index 0000000000..2af0e5362f --- /dev/null +++ b/src/Umbraco.Web.Website/Routing/ControllerActionSearcher.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Logging; +using Umbraco.Core.Composing; +using Umbraco.Web.Common.Controllers; +using static Umbraco.Core.Constants.Web.Routing; + +namespace Umbraco.Web.Website.Routing +{ + /// + /// Used to find a controller/action in the current available routes + /// + public class ControllerActionSearcher : IControllerActionSearcher + { + private readonly ILogger _logger; + private readonly IActionSelector _actionSelector; + private const string DefaultActionName = nameof(RenderController.Index); + + /// + /// Initializes a new instance of the class. + /// + public ControllerActionSearcher( + ILogger logger, + IActionSelector actionSelector) + { + _logger = logger; + _actionSelector = actionSelector; + } + + /// + /// Determines if a custom controller can hijack the current route + /// + /// The controller type to find + public ControllerActionDescriptor Find(HttpContext httpContext, string controller, string action) + { + IReadOnlyList candidates = FindControllerCandidates(httpContext, controller, action, DefaultActionName); + + if (candidates.Count > 0) + { + return candidates[0]; + } + + return null; + } + + /// + /// Return a list of controller candidates that match the custom controller and action names + /// + private IReadOnlyList FindControllerCandidates( + HttpContext httpContext, + string customControllerName, + string customActionName, + string defaultActionName) + { + // Use aspnetcore's IActionSelector to do the finding since it uses an optimized cache lookup + var routeValues = new RouteValueDictionary + { + [ControllerToken] = customControllerName, + [ActionToken] = customActionName, // first try to find the custom action + }; + var routeData = new RouteData(routeValues); + var routeContext = new RouteContext(httpContext) + { + RouteData = routeData + }; + + // try finding candidates for the custom action + var candidates = _actionSelector.SelectCandidates(routeContext) + .Cast() + .Where(x => TypeHelper.IsTypeAssignableFrom(x.ControllerTypeInfo)) + .ToList(); + + if (candidates.Count > 0) + { + // return them if found + return candidates; + } + + // now find for the default action since we couldn't find the custom one + routeValues[ActionToken] = defaultActionName; + candidates = _actionSelector.SelectCandidates(routeContext) + .Cast() + .Where(x => TypeHelper.IsTypeAssignableFrom(x.ControllerTypeInfo)) + .ToList(); + + return candidates; + } + } +} diff --git a/src/Umbraco.Web.Website/Routing/FrontEndRoutes.cs b/src/Umbraco.Web.Website/Routing/FrontEndRoutes.cs new file mode 100644 index 0000000000..67ce14e7aa --- /dev/null +++ b/src/Umbraco.Web.Website/Routing/FrontEndRoutes.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Options; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.Models; +using Umbraco.Core.Hosting; +using Umbraco.Web.Common.Controllers; +using Umbraco.Web.Common.Extensions; +using Umbraco.Web.Common.Routing; +using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi; +using Umbraco.Web.Website.Collections; + +namespace Umbraco.Web.Website.Routing +{ + /// + /// Creates routes for surface controllers + /// + public sealed class FrontEndRoutes : IAreaRoutes + { + private readonly GlobalSettings _globalSettings; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IRuntimeState _runtimeState; + private readonly SurfaceControllerTypeCollection _surfaceControllerTypeCollection; + private readonly UmbracoApiControllerTypeCollection _apiControllers; + private readonly string _umbracoPathSegment; + + /// + /// Initializes a new instance of the class. + /// + public FrontEndRoutes( + IOptions globalSettings, + IHostingEnvironment hostingEnvironment, + IRuntimeState runtimeState, + SurfaceControllerTypeCollection surfaceControllerTypeCollection, + UmbracoApiControllerTypeCollection apiControllers) + { + _globalSettings = globalSettings.Value; + _hostingEnvironment = hostingEnvironment; + _runtimeState = runtimeState; + _surfaceControllerTypeCollection = surfaceControllerTypeCollection; + _apiControllers = apiControllers; + _umbracoPathSegment = _globalSettings.GetUmbracoMvcArea(_hostingEnvironment); + } + + /// + public void CreateRoutes(IEndpointRouteBuilder endpoints) + { + if (_runtimeState.Level != RuntimeLevel.Run) + { + return; + } + + AutoRouteSurfaceControllers(endpoints); + AutoRouteFrontEndApiControllers(endpoints); + } + + /// + /// Auto-routes all front-end surface controllers + /// + private void AutoRouteSurfaceControllers(IEndpointRouteBuilder endpoints) + { + foreach (Type controller in _surfaceControllerTypeCollection) + { + // exclude front-end api controllers + PluginControllerMetadata meta = PluginController.GetMetadata(controller); + + endpoints.MapUmbracoRoute( + meta.ControllerType, + _umbracoPathSegment, + meta.AreaName, + "Surface"); + } + } + + /// + /// Auto-routes all front-end api controllers + /// + private void AutoRouteFrontEndApiControllers(IEndpointRouteBuilder endpoints) + { + foreach (Type controller in _apiControllers) + { + PluginControllerMetadata meta = PluginController.GetMetadata(controller); + + // exclude back-end api controllers + if (meta.IsBackOffice) + { + continue; + } + + endpoints.MapUmbracoApiRoute( + meta.ControllerType, + _umbracoPathSegment, + meta.AreaName, + meta.IsBackOffice, + defaultAction: string.Empty); // no default action (this is what we had before) + } + } + } +} diff --git a/src/Umbraco.Web.Website/Routing/HijackedRouteEvaluator.cs b/src/Umbraco.Web.Website/Routing/HijackedRouteEvaluator.cs deleted file mode 100644 index 79036a01e1..0000000000 --- a/src/Umbraco.Web.Website/Routing/HijackedRouteEvaluator.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Controllers; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.Extensions.Logging; -using Umbraco.Core; -using Umbraco.Core.Composing; -using Umbraco.Web.Common.Controllers; - -namespace Umbraco.Web.Website.Routing -{ - /// - /// Determines if a custom controller can hijack the current route - /// - public class HijackedRouteEvaluator - { - private readonly ILogger _logger; - private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider; - private const string DefaultActionName = nameof(RenderController.Index); - - /// - /// Initializes a new instance of the class. - /// - public HijackedRouteEvaluator( - ILogger logger, - IActionDescriptorCollectionProvider actionDescriptorCollectionProvider) - { - _logger = logger; - _actionDescriptorCollectionProvider = actionDescriptorCollectionProvider; - } - - /// - /// Determines if a custom controller can hijack the current route - /// - public HijackedRouteResult Evaluate(string controller, string action) - { - IReadOnlyList candidates = FindControllerCandidates(controller, action, DefaultActionName); - - // check if there's a custom controller assigned, base on the document type alias. - var customControllerCandidates = candidates.Where(x => x.ControllerName.InvariantEquals(controller)).ToList(); - - // check if that custom controller exists - if (customControllerCandidates.Count > 0) - { - ControllerActionDescriptor controllerDescriptor = customControllerCandidates[0]; - - // ensure the controller is of type IRenderController and ControllerBase - if (TypeHelper.IsTypeAssignableFrom(controllerDescriptor.ControllerTypeInfo) - && TypeHelper.IsTypeAssignableFrom(controllerDescriptor.ControllerTypeInfo)) - { - // now check if the custom action matches - var resultingAction = DefaultActionName; - if (action != null) - { - var found = customControllerCandidates.FirstOrDefault(x => x.ActionName.InvariantEquals(action))?.ActionName; - if (found != null) - { - resultingAction = found; - } - } - - // it's a hijacked route with a custom controller, so return the the values - return new HijackedRouteResult( - true, - controllerDescriptor.ControllerName, - controllerDescriptor.ControllerTypeInfo, - resultingAction); - } - else - { - _logger.LogWarning( - "The current Document Type {ContentTypeAlias} matches a locally declared controller of type {ControllerName}. Custom Controllers for Umbraco routing must implement '{UmbracoRenderController}' and inherit from '{UmbracoControllerBase}'.", - controller, - controllerDescriptor.ControllerTypeInfo.FullName, - typeof(IRenderController).FullName, - typeof(ControllerBase).FullName); - - // we cannot route to this custom controller since it is not of the correct type so we'll continue with the defaults - // that have already been set above. - } - } - - return HijackedRouteResult.Failed(); - } - - /// - /// Return a list of controller candidates that match the custom controller and action names - /// - private IReadOnlyList FindControllerCandidates(string customControllerName, string customActionName, string defaultActionName) - { - var descriptors = _actionDescriptorCollectionProvider.ActionDescriptors.Items - .Cast() - .Where(x => x.ControllerName.InvariantEquals(customControllerName) - && (x.ActionName.InvariantEquals(defaultActionName) || (customActionName != null && x.ActionName.InvariantEquals(customActionName)))) - .ToList(); - - return descriptors; - } - } -} diff --git a/src/Umbraco.Web.Website/Routing/HijackedRouteResult.cs b/src/Umbraco.Web.Website/Routing/HijackedRouteResult.cs deleted file mode 100644 index f88bdfa2fd..0000000000 --- a/src/Umbraco.Web.Website/Routing/HijackedRouteResult.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; - -namespace Umbraco.Web.Website.Routing -{ - /// - /// The result from evaluating if a route can be hijacked - /// - public class HijackedRouteResult - { - /// - /// Returns a failed result - /// - public static HijackedRouteResult Failed() => new HijackedRouteResult(false, null, null, null); - - /// - /// Initializes a new instance of the class. - /// - public HijackedRouteResult( - bool success, - string controllerName, - Type controllerType, - string actionName) - { - Success = success; - ControllerName = controllerName; - ControllerType = controllerType; - ActionName = actionName; - } - - /// - /// Gets a value indicating if the route could be hijacked - /// - public bool Success { get; } - - /// - /// Gets the Controller name - /// - public string ControllerName { get; } - - /// - /// Gets the Controller type - /// - public Type ControllerType { get; } - - /// - /// Gets the Acton name - /// - public string ActionName { get; } - } -} diff --git a/src/Umbraco.Web.Website/Routing/IControllerActionSearcher.cs b/src/Umbraco.Web.Website/Routing/IControllerActionSearcher.cs new file mode 100644 index 0000000000..6236a2b8f0 --- /dev/null +++ b/src/Umbraco.Web.Website/Routing/IControllerActionSearcher.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Controllers; + +namespace Umbraco.Web.Website.Routing +{ + public interface IControllerActionSearcher + { + ControllerActionDescriptor Find(HttpContext httpContext, string controller, string action); + } +} diff --git a/src/Umbraco.Web.Website/Routing/IUmbracoRouteValuesFactory.cs b/src/Umbraco.Web.Website/Routing/IUmbracoRouteValuesFactory.cs index 7af41d865b..f584627e31 100644 --- a/src/Umbraco.Web.Website/Routing/IUmbracoRouteValuesFactory.cs +++ b/src/Umbraco.Web.Website/Routing/IUmbracoRouteValuesFactory.cs @@ -13,6 +13,6 @@ namespace Umbraco.Web.Website.Routing /// /// Creates /// - UmbracoRouteValues Create(HttpContext httpContext, RouteValueDictionary values, IPublishedRequest request); + UmbracoRouteValues Create(HttpContext httpContext, IPublishedRequest request); } } diff --git a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs index af23105099..7d0837b4eb 100644 --- a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs +++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs @@ -1,16 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; using System.Threading.Tasks; +using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; using Umbraco.Core; using Umbraco.Core.Configuration.Models; using Umbraco.Core.Hosting; using Umbraco.Extensions; using Umbraco.Web.Common.Routing; +using Umbraco.Web.Common.Security; using Umbraco.Web.Routing; using Umbraco.Web.Website.Controllers; +using static Umbraco.Core.Constants.Web.Routing; using RouteDirection = Umbraco.Web.Routing.RouteDirection; namespace Umbraco.Web.Website.Routing @@ -35,6 +45,8 @@ namespace Umbraco.Web.Website.Routing private readonly IRuntimeState _runtime; private readonly IUmbracoRouteValuesFactory _routeValuesFactory; private readonly IRoutableDocumentFilter _routableDocumentFilter; + private readonly IDataProtectionProvider _dataProtectionProvider; + private readonly IControllerActionSearcher _controllerActionSearcher; /// /// Initializes a new instance of the class. @@ -47,21 +59,25 @@ namespace Umbraco.Web.Website.Routing IHostingEnvironment hostingEnvironment, IRuntimeState runtime, IUmbracoRouteValuesFactory routeValuesFactory, - IRoutableDocumentFilter routableDocumentFilter) + IRoutableDocumentFilter routableDocumentFilter, + IDataProtectionProvider dataProtectionProvider, + IControllerActionSearcher controllerActionSearcher) { if (globalSettings is null) { - throw new System.ArgumentNullException(nameof(globalSettings)); + throw new ArgumentNullException(nameof(globalSettings)); } - _logger = logger ?? throw new System.ArgumentNullException(nameof(logger)); - _umbracoContextAccessor = umbracoContextAccessor ?? throw new System.ArgumentNullException(nameof(umbracoContextAccessor)); - _publishedRouter = publishedRouter ?? throw new System.ArgumentNullException(nameof(publishedRouter)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + _publishedRouter = publishedRouter ?? throw new ArgumentNullException(nameof(publishedRouter)); _globalSettings = globalSettings.Value; - _hostingEnvironment = hostingEnvironment ?? throw new System.ArgumentNullException(nameof(hostingEnvironment)); - _runtime = runtime ?? throw new System.ArgumentNullException(nameof(runtime)); - _routeValuesFactory = routeValuesFactory ?? throw new System.ArgumentNullException(nameof(routeValuesFactory)); - _routableDocumentFilter = routableDocumentFilter ?? throw new System.ArgumentNullException(nameof(routableDocumentFilter)); + _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); + _runtime = runtime ?? throw new ArgumentNullException(nameof(runtime)); + _routeValuesFactory = routeValuesFactory ?? throw new ArgumentNullException(nameof(routeValuesFactory)); + _routableDocumentFilter = routableDocumentFilter ?? throw new ArgumentNullException(nameof(routableDocumentFilter)); + _dataProtectionProvider = dataProtectionProvider; + _controllerActionSearcher = controllerActionSearcher; } /// @@ -87,23 +103,33 @@ namespace Umbraco.Web.Website.Routing // Check if there is no existing content and return the no content controller if (!_umbracoContextAccessor.UmbracoContext.Content.HasContent()) { - values["controller"] = ControllerExtensions.GetControllerName(); - values["action"] = nameof(RenderNoContentController.Index); + values[ControllerToken] = ControllerExtensions.GetControllerName(); + values[ActionToken] = nameof(RenderNoContentController.Index); - return await Task.FromResult(values); + return values; } IPublishedRequest publishedRequest = await RouteRequestAsync(_umbracoContextAccessor.UmbracoContext); - UmbracoRouteValues routeDef = _routeValuesFactory.Create(httpContext, values, publishedRequest); + UmbracoRouteValues umbracoRouteValues = _routeValuesFactory.Create(httpContext, publishedRequest); - values["controller"] = routeDef.ControllerName; - if (string.IsNullOrWhiteSpace(routeDef.ActionName) == false) + // Store the route values as a httpcontext feature + httpContext.Features.Set(umbracoRouteValues); + + // Need to check if there is form data being posted back to an Umbraco URL + PostedDataProxyInfo postedInfo = GetFormInfo(httpContext, values); + if (postedInfo != null) { - values["action"] = routeDef.ActionName; + return HandlePostedValues(postedInfo, httpContext, values); } - return await Task.FromResult(values); + values[ControllerToken] = umbracoRouteValues.ControllerName; + if (string.IsNullOrWhiteSpace(umbracoRouteValues.ActionName) == false) + { + values[ActionToken] = umbracoRouteValues.ActionName; + } + + return values; } private async Task RouteRequestAsync(IUmbracoContext umbracoContext) @@ -123,5 +149,92 @@ namespace Umbraco.Web.Website.Routing return routedRequest; } + + /// + /// Checks the request and query strings to see if it matches the definition of having a Surface controller + /// posted/get value, if so, then we return a PostedDataProxyInfo object with the correct information. + /// + private PostedDataProxyInfo GetFormInfo(HttpContext httpContext, RouteValueDictionary values) + { + if (httpContext is null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + // if it is a POST/GET then a value must be in the request + if (!httpContext.Request.Query.TryGetValue("ufprt", out StringValues encodedVal) + && (!httpContext.Request.HasFormContentType || !httpContext.Request.Form.TryGetValue("ufprt", out encodedVal))) + { + return null; + } + + if (!EncryptionHelper.DecryptAndValidateEncryptedRouteString( + _dataProtectionProvider, + encodedVal, + out IDictionary decodedParts)) + { + return null; + } + + // Get all route values that are not the default ones and add them separately so they eventually get to action parameters + foreach (KeyValuePair item in decodedParts.Where(x => ReservedAdditionalKeys.AllKeys.Contains(x.Key) == false)) + { + values[item.Key] = item.Value; + } + + // return the proxy info without the surface id... could be a local controller. + return new PostedDataProxyInfo + { + ControllerName = WebUtility.UrlDecode(decodedParts.First(x => x.Key == ReservedAdditionalKeys.Controller).Value), + ActionName = WebUtility.UrlDecode(decodedParts.First(x => x.Key == ReservedAdditionalKeys.Action).Value), + Area = WebUtility.UrlDecode(decodedParts.First(x => x.Key == ReservedAdditionalKeys.Area).Value), + }; + } + + private RouteValueDictionary HandlePostedValues(PostedDataProxyInfo postedInfo, HttpContext httpContext, RouteValueDictionary values) + { + // set the standard route values/tokens + values[ControllerToken] = postedInfo.ControllerName; + values[ActionToken] = postedInfo.ActionName; + + ControllerActionDescriptor surfaceControllerDescriptor = _controllerActionSearcher.Find(httpContext, postedInfo.ControllerName, postedInfo.ActionName); + + if (surfaceControllerDescriptor == null) + { + throw new InvalidOperationException("Could not find a Surface controller route in the RouteTable for controller name " + postedInfo.ControllerName); + } + + // set the area if one is there. + if (!postedInfo.Area.IsNullOrWhiteSpace()) + { + values["area"] = postedInfo.Area; + } + + return values; + } + + private class PostedDataProxyInfo + { + public string ControllerName { get; set; } + + public string ActionName { get; set; } + + public string Area { get; set; } + } + + // Define reserved dictionary keys for controller, action and area specified in route additional values data + private static class ReservedAdditionalKeys + { + internal static readonly string[] AllKeys = new[] + { + Controller, + Action, + Area + }; + + internal const string Controller = "c"; + internal const string Action = "a"; + internal const string Area = "ar"; + } } } diff --git a/src/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactory.cs b/src/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactory.cs index d26216204e..000a3e252a 100644 --- a/src/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactory.cs +++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactory.cs @@ -1,9 +1,11 @@ using System; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Routing; using Umbraco.Core; using Umbraco.Core.Strings; using Umbraco.Extensions; +using Umbraco.Web.Common.Controllers; using Umbraco.Web.Common.Routing; using Umbraco.Web.Features; using Umbraco.Web.Routing; @@ -20,9 +22,10 @@ namespace Umbraco.Web.Website.Routing private readonly IUmbracoRenderingDefaults _renderingDefaults; private readonly IShortStringHelper _shortStringHelper; private readonly UmbracoFeatures _umbracoFeatures; - private readonly HijackedRouteEvaluator _hijackedRouteEvaluator; + private readonly IControllerActionSearcher _controllerActionSearcher; private readonly IPublishedRouter _publishedRouter; private readonly Lazy _defaultControllerName; + private readonly Lazy _defaultControllerDescriptor; /// /// Initializes a new instance of the class. @@ -31,15 +34,29 @@ namespace Umbraco.Web.Website.Routing IUmbracoRenderingDefaults renderingDefaults, IShortStringHelper shortStringHelper, UmbracoFeatures umbracoFeatures, - HijackedRouteEvaluator hijackedRouteEvaluator, + IControllerActionSearcher controllerActionSearcher, IPublishedRouter publishedRouter) { _renderingDefaults = renderingDefaults; _shortStringHelper = shortStringHelper; _umbracoFeatures = umbracoFeatures; - _hijackedRouteEvaluator = hijackedRouteEvaluator; + _controllerActionSearcher = controllerActionSearcher; _publishedRouter = publishedRouter; _defaultControllerName = new Lazy(() => ControllerExtensions.GetControllerName(_renderingDefaults.DefaultControllerType)); + _defaultControllerDescriptor = new Lazy(() => + { + ControllerActionDescriptor descriptor = _controllerActionSearcher.Find( + new DefaultHttpContext(), // this actually makes no difference for this method + DefaultControllerName, + UmbracoRouteValues.DefaultActionName); + + if (descriptor == null) + { + throw new InvalidOperationException($"No controller/action found by name {DefaultControllerName}.{UmbracoRouteValues.DefaultActionName}"); + } + + return descriptor; + }); } /// @@ -48,25 +65,18 @@ namespace Umbraco.Web.Website.Routing protected string DefaultControllerName => _defaultControllerName.Value; /// - public UmbracoRouteValues Create(HttpContext httpContext, RouteValueDictionary values, IPublishedRequest request) + public UmbracoRouteValues Create(HttpContext httpContext, IPublishedRequest request) { if (httpContext is null) { throw new ArgumentNullException(nameof(httpContext)); } - if (values is null) - { - throw new ArgumentNullException(nameof(values)); - } - if (request is null) { throw new ArgumentNullException(nameof(request)); } - Type defaultControllerType = _renderingDefaults.DefaultControllerType; - string customActionName = null; // check that a template is defined), if it doesn't and there is a hijacked route it will just route @@ -79,19 +89,15 @@ namespace Umbraco.Web.Website.Routing customActionName = request.GetTemplateAlias()?.Split('.')[0].ToSafeAlias(_shortStringHelper); } - // creates the default route definition which maps to the 'UmbracoController' controller + // The default values for the default controller/action var def = new UmbracoRouteValues( request, - DefaultControllerName, - defaultControllerType, + _defaultControllerDescriptor.Value, templateName: customActionName); - def = CheckHijackedRoute(def); + def = CheckHijackedRoute(httpContext, def); - def = CheckNoTemplate(def); - - // store the route definition - values.TryAdd(Constants.Web.UmbracoRouteDefinitionDataToken, def); + def = CheckNoTemplate(httpContext, def); return def; } @@ -99,21 +105,19 @@ namespace Umbraco.Web.Website.Routing /// /// Check if the route is hijacked and return new route values /// - private UmbracoRouteValues CheckHijackedRoute(UmbracoRouteValues def) + private UmbracoRouteValues CheckHijackedRoute(HttpContext httpContext, UmbracoRouteValues def) { IPublishedRequest request = def.PublishedRequest; var customControllerName = request.PublishedContent?.ContentType?.Alias; if (customControllerName != null) { - HijackedRouteResult hijackedResult = _hijackedRouteEvaluator.Evaluate(customControllerName, def.TemplateName); - if (hijackedResult.Success) + ControllerActionDescriptor descriptor = _controllerActionSearcher.Find(httpContext, customControllerName, def.TemplateName); + if (descriptor != null) { return new UmbracoRouteValues( request, - hijackedResult.ControllerName, - hijackedResult.ControllerType, - hijackedResult.ActionName, + descriptor, def.TemplateName, true); } @@ -125,7 +129,7 @@ namespace Umbraco.Web.Website.Routing /// /// Special check for when no template or hijacked route is done which needs to re-run through the routing pipeline again for last chance finders /// - private UmbracoRouteValues CheckNoTemplate(UmbracoRouteValues def) + private UmbracoRouteValues CheckNoTemplate(HttpContext httpContext, UmbracoRouteValues def) { IPublishedRequest request = def.PublishedRequest; @@ -154,15 +158,13 @@ namespace Umbraco.Web.Website.Routing def = new UmbracoRouteValues( request, - def.ControllerName, - def.ControllerType, - def.ActionName, + def.ControllerActionDescriptor, def.TemplateName); // if the content has changed, we must then again check for hijacked routes if (content != request.PublishedContent) { - def = CheckHijackedRoute(def); + def = CheckHijackedRoute(httpContext, def); } } diff --git a/src/Umbraco.Web/AspNet/AspNetHostingEnvironment.cs b/src/Umbraco.Web/AspNet/AspNetHostingEnvironment.cs index ec9d66859e..a58a5f3a14 100644 --- a/src/Umbraco.Web/AspNet/AspNetHostingEnvironment.cs +++ b/src/Umbraco.Web/AspNet/AspNetHostingEnvironment.cs @@ -17,6 +17,7 @@ namespace Umbraco.Web.Hosting private readonly HostingSettings _hostingSettings; private string _localTempPath; + private Uri _applicationMainUrl; public AspNetHostingEnvironment(IOptions hostingSettings) @@ -29,7 +30,6 @@ namespace Umbraco.Web.Hosting ApplicationVirtualPath = _hostingSettings.ApplicationVirtualPath?.EnsureStartsWith('/') ?? HostingEnvironment.ApplicationVirtualPath?.EnsureStartsWith("/") ?? "/"; - IISVersion = HttpRuntime.IISVersion; } public string SiteName { get; } @@ -42,7 +42,11 @@ namespace Umbraco.Web.Hosting /// public bool IsHosted => (HttpContext.Current != null || HostingEnvironment.IsHosted); - public Version IISVersion { get; } + public Uri ApplicationMainUrl + { + get => _applicationMainUrl; + set => _applicationMainUrl = value; + } public string MapPathWebRoot(string path) { @@ -57,6 +61,7 @@ namespace Umbraco.Web.Hosting public string MapPathContentRoot(string path) => MapPathWebRoot(path); public string ToAbsolute(string virtualPath) => VirtualPathUtility.ToAbsolute(virtualPath, ApplicationVirtualPath); + public void EnsureApplicationMainUrl(Uri currentApplicationUrl) => throw new NotImplementedException(); public string LocalTempPath diff --git a/src/Umbraco.Web/AspNet/AspNetRequestAccessor.cs b/src/Umbraco.Web/AspNet/AspNetRequestAccessor.cs deleted file mode 100644 index ae38dc2c05..0000000000 --- a/src/Umbraco.Web/AspNet/AspNetRequestAccessor.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.Extensions.Options; -using Umbraco.Core.Configuration.Models; -using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Web.Routing; - -namespace Umbraco.Web.AspNet -{ - public class AspNetRequestAccessor : IRequestAccessor - { - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly WebRoutingSettings _webRoutingSettings; - private readonly ISet _applicationUrls = new HashSet(); - private Uri _currentApplicationUrl; - public AspNetRequestAccessor(IHttpContextAccessor httpContextAccessor, IOptions webRoutingSettings) - { - _httpContextAccessor = httpContextAccessor; - _webRoutingSettings = webRoutingSettings.Value; - - UmbracoModule.EndRequest += OnEndRequest; - UmbracoModule.RouteAttempt += OnRouteAttempt; - } - - - - public string GetRequestValue(string name) - { - return _httpContextAccessor.GetRequiredHttpContext().Request[name]; - } - - public string GetQueryStringValue(string name) - { - return _httpContextAccessor.GetRequiredHttpContext().Request.QueryString[name]; - } - - private void OnEndRequest(object sender, UmbracoRequestEventArgs args) - { - EndRequest?.Invoke(sender, args); - } - - private void OnRouteAttempt(object sender, RoutableAttemptEventArgs args) - { - RouteAttempt?.Invoke(sender, args); - } - public event EventHandler EndRequest; - public event EventHandler RouteAttempt; - public Uri GetRequestUrl() => _httpContextAccessor.HttpContext?.Request.Url; - public Uri GetApplicationUrl() - { - //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? - - if (!(_webRoutingSettings.UmbracoApplicationUrl is null)) - { - return new Uri(_webRoutingSettings.UmbracoApplicationUrl); - } - - var request = _httpContextAccessor.HttpContext?.Request; - - var url = request?.Url.GetLeftPart(UriPartial.Authority); - var change = url != null && !_applicationUrls.Contains(url); - if (change) - { - _applicationUrls.Add(url); - - _currentApplicationUrl ??= new Uri(url); - } - - return _currentApplicationUrl; - } - } -} diff --git a/src/Umbraco.Web/Composing/Current.cs b/src/Umbraco.Web/Composing/Current.cs index 8f698db995..19eedfc1c1 100644 --- a/src/Umbraco.Web/Composing/Current.cs +++ b/src/Umbraco.Web/Composing/Current.cs @@ -1,35 +1,26 @@ -using System; +using System; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Umbraco.Core; using Umbraco.Core.Cache; -using Umbraco.Core.Dictionary; +using Umbraco.Core.Configuration; using Umbraco.Core.Events; +using Umbraco.Core.HealthChecks; +using Umbraco.Core.Hosting; using Umbraco.Core.IO; using Umbraco.Core.Logging; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.Persistence; -using Umbraco.Core.Composing; -using Umbraco.Core.Configuration; -using Umbraco.Core.HealthCheck; -using Umbraco.Core.Hosting; using Umbraco.Core.Mapping; -using Umbraco.Core.Templates; -using Umbraco.Net; -using Umbraco.Core.PackageActions; -using Umbraco.Core.Packaging; -using Umbraco.Core.PropertyEditors; -using Umbraco.Core.Runtime; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Scoping; using Umbraco.Core.Security; using Umbraco.Core.Services; -using Umbraco.Core.Strings; using Umbraco.Core.Sync; +using Umbraco.Core.Templates; using Umbraco.Core.WebAssets; +using Umbraco.Net; using Umbraco.Web.Actions; using Umbraco.Web.Cache; using Umbraco.Web.Editors; -using Umbraco.Web.HealthCheck; using Umbraco.Web.Mvc; using Umbraco.Web.PublishedCache; using Umbraco.Web.Routing; @@ -151,28 +142,7 @@ namespace Umbraco.Web.Composing #endregion - #region Web Constants - - // these are different - not 'resolving' anything, and nothing that could be managed - // by the container - just registering some sort of application-wide constants or - // settings - but they fit in Current nicely too - - private static Type _defaultRenderMvcControllerType; - - // internal - can only be accessed through Composition at compose time - internal static Type DefaultRenderMvcControllerType - { - get => _defaultRenderMvcControllerType; - set - { - if (value.Implements() == false) - throw new ArgumentException($"Type {value.FullName} does not implement {typeof(IRenderController).FullName}.", nameof(value)); - _defaultRenderMvcControllerType = value; - } - } - - #endregion - + #region Core Getters // proxy Core for convenience diff --git a/src/Umbraco.Web/Composing/ModuleInjector.cs b/src/Umbraco.Web/Composing/ModuleInjector.cs deleted file mode 100644 index 5cfec1c484..0000000000 --- a/src/Umbraco.Web/Composing/ModuleInjector.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Web; -using Microsoft.Extensions.DependencyInjection; -using Umbraco.Core; -using Umbraco.Core.Composing; -using Umbraco.Core.Exceptions; - -namespace Umbraco.Web.Composing -{ - /// - /// Provides a base class for module injectors. - /// - /// The type of the injected module. - public abstract class ModuleInjector : IHttpModule - where TModule : class, IHttpModule - { - protected TModule Module { get; private set; } - - /// - public void Init(HttpApplication context) - { - try - { - // using the service locator here - no other way, really - Module = Current.Factory.GetRequiredService(); - } - catch - { - // if GetInstance fails, it may be because of a boot error, in - // which case that is the error we actually want to report - IRuntimeState runtimeState = null; - - try - { - runtimeState = Current.Factory.GetRequiredService(); - } - catch { /* don't make it worse */ } - - if (runtimeState?.BootFailedException != null) - BootFailedException.Rethrow(runtimeState.BootFailedException); - - // else... throw what we have - throw; - } - - // initialize - Module.Init(context); - } - - /// - public void Dispose() - { - Module?.Dispose(); - } - } -} diff --git a/src/Umbraco.Web/Mvc/AreaRegistrationExtensions.cs b/src/Umbraco.Web/Mvc/AreaRegistrationExtensions.cs deleted file mode 100644 index c59c701d42..0000000000 --- a/src/Umbraco.Web/Mvc/AreaRegistrationExtensions.cs +++ /dev/null @@ -1,137 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Web.Http; -using System.Web.Mvc; -using System.Web.Routing; -using System.Web.SessionState; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.Models; -using Umbraco.Core.Hosting; -using Umbraco.Web.WebApi; - -namespace Umbraco.Web.Mvc -{ - internal static class AreaRegistrationExtensions - { - /// - /// Creates a custom individual route for the specified controller plugin. Individual routes - /// are required by controller plugins to map to a unique URL based on ID. - /// - /// - /// - /// - /// An existing route collection - /// - /// The suffix name that the controller name must end in before the "Controller" string for example: - /// ContentTreeController has a controllerSuffixName of "Tree", this is used for route constraints. - /// - /// - /// - /// - /// The DataToken value to set for the 'umbraco' key, this defaults to 'backoffice' - /// By default this value is just {action}/{id} but can be modified for things like web api routes - /// Default is true for MVC, otherwise false for WebAPI - /// - /// If specified will add this string to the path between the umbraco path and the area path name, for example: - /// /umbraco/CUSTOMPATHPREFIX/areaname - /// if not specified, will just route like: - /// /umbraco/areaname - /// - /// - /// - /// - internal static Route RouteControllerPlugin(this AreaRegistration area, - GlobalSettings globalSettings, IHostingEnvironment hostingEnvironment, - string controllerName, Type controllerType, RouteCollection routes, - string controllerSuffixName, string defaultAction, object defaultId, - string umbracoTokenValue = "backoffice", - string routeTokens = "{action}/{id}", - bool isMvc = true, - string areaPathPrefix = "") - { - if (controllerName == null) throw new ArgumentNullException(nameof(controllerName)); - if (string.IsNullOrEmpty(controllerName)) throw new ArgumentException("Value can't be empty.", nameof(controllerName)); - if (controllerSuffixName == null) throw new ArgumentNullException(nameof(controllerSuffixName)); - - if (controllerType == null) throw new ArgumentNullException(nameof(controllerType)); - if (routes == null) throw new ArgumentNullException(nameof(routes)); - if (defaultId == null) throw new ArgumentNullException(nameof(defaultId)); - - var umbracoArea = globalSettings.GetUmbracoMvcArea(hostingEnvironment); - - //routes are explicitly named with controller names and IDs - var url = umbracoArea + "/" + - (areaPathPrefix.IsNullOrWhiteSpace() ? "" : areaPathPrefix + "/") + - area.AreaName + "/" + controllerName + "/" + routeTokens; - - Route controllerPluginRoute; - //var meta = PluginController.GetMetadata(controllerType); - if (isMvc) - { - //create a new route with custom name, specified URL, and the namespace of the controller plugin - controllerPluginRoute = routes.MapRoute( - //name - string.Format("umbraco-{0}-{1}", area.AreaName, controllerName), - //URL format - url, - //set the namespace of the controller to match - new[] {controllerType.Namespace}); - - //set defaults - controllerPluginRoute.Defaults = new RouteValueDictionary( - new Dictionary - { - {"controller", controllerName}, - {"action", defaultAction}, - {"id", defaultId} - }); - } - else - { - controllerPluginRoute = routes.MapHttpRoute( - //name - string.Format("umbraco-{0}-{1}-{2}", "api", area.AreaName, controllerName), - //URL format - url, - new {controller = controllerName, id = defaultId}); - //web api routes don't set the data tokens object - if (controllerPluginRoute.DataTokens == null) - { - controllerPluginRoute.DataTokens = new RouteValueDictionary(); - } - - //look in this namespace to create the controller - controllerPluginRoute.DataTokens.Add("Namespaces", new[] {controllerType.Namespace}); - - //Special case! Check if the controller type implements IRequiresSessionState and if so use our - //custom webapi session handler - if (typeof(IRequiresSessionState).IsAssignableFrom(controllerType)) - { - controllerPluginRoute.RouteHandler = new SessionHttpControllerRouteHandler(); - } - } - - //Don't look anywhere else except this namespace! - controllerPluginRoute.DataTokens.Add("UseNamespaceFallback", false); - - //constraints: only match controllers ending with 'controllerSuffixName' and only match this controller's ID for this route - if (controllerSuffixName.IsNullOrWhiteSpace() == false) - { - controllerPluginRoute.Constraints = new RouteValueDictionary( - new Dictionary - { - {"controller", @"(\w+)" + controllerSuffixName} - }); - } - - //match this area - controllerPluginRoute.DataTokens.Add("area", area.AreaName); - - // TODO: No longer needed, remove - //controllerPluginRoute.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, umbracoTokenValue); //ensure the umbraco token is set - - return controllerPluginRoute; - } - } -} diff --git a/src/Umbraco.Web/Mvc/ControllerExtensions.cs b/src/Umbraco.Web/Mvc/ControllerExtensions.cs index d7a693be2d..c72a7554f6 100644 --- a/src/Umbraco.Web/Mvc/ControllerExtensions.cs +++ b/src/Umbraco.Web/Mvc/ControllerExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Threading; using System.Web.Mvc; @@ -43,164 +43,5 @@ namespace Umbraco.Web.Mvc return GetControllerName(typeof(T)); } - /// - /// This is generally used for proxying to a ChildAction which requires a ViewContext to be setup - /// but since the View isn't actually rendered the IView object is null, however the rest of the - /// properties are filled in. - /// - /// - /// - internal static ViewContext CreateEmptyViewContext(this ControllerBase controller) - { - return new ViewContext - { - Controller = controller, - HttpContext = controller.ControllerContext.HttpContext, - RequestContext = controller.ControllerContext.RequestContext, - RouteData = controller.ControllerContext.RouteData, - TempData = controller.TempData, - ViewData = controller.ViewData - }; - } - - /// - /// Returns the string output from a ViewResultBase object - /// - /// - /// - /// - internal static string RenderViewResultAsString(this ControllerBase controller, ViewResultBase viewResult) - { - using (var sw = new StringWriter()) - { - controller.EnsureViewObjectDataOnResult(viewResult); - - var viewContext = new ViewContext(controller.ControllerContext, viewResult.View, viewResult.ViewData, viewResult.TempData, sw); - viewResult.View.Render(viewContext, sw); - foreach (var v in viewResult.ViewEngineCollection) - { - v.ReleaseView(controller.ControllerContext, viewResult.View); - } - return sw.ToString().Trim(); - } - } - - /// - /// Renders the partial view to string. - /// - /// The controller context. - /// Name of the view. - /// The model. - /// true if it is a Partial view, otherwise false for a normal view - /// - internal static string RenderViewToString(this ControllerBase controller, string viewName, object model, bool isPartial = false) - { - if (controller.ControllerContext == null) - throw new ArgumentException("The controller must have an assigned ControllerContext to execute this method."); - - controller.ViewData.Model = model; - - using (var sw = new StringWriter()) - { - var viewResult = isPartial == false - ? ViewEngines.Engines.FindView(controller.ControllerContext, viewName, null) - : ViewEngines.Engines.FindPartialView(controller.ControllerContext, viewName); - if (viewResult.View == null) - throw new InvalidOperationException("No view could be found by name " + viewName); - var viewContext = new ViewContext(controller.ControllerContext, viewResult.View, controller.ViewData, controller.TempData, sw); - viewResult.View.Render(viewContext, sw); - viewResult.ViewEngine.ReleaseView(controller.ControllerContext, viewResult.View); - return sw.GetStringBuilder().ToString(); - } - } - - /// - /// Renders the partial view to string. - /// - /// The request context. - /// - /// - /// Name of the view. - /// The model. - /// true if it is a Partial view, otherwise false for a normal view - /// - internal static string RenderViewToString( - this RequestContext requestContext, - ViewDataDictionary viewData, - TempDataDictionary tempData, - string viewName, object model, bool isPartial = false) - { - if (requestContext == null) throw new ArgumentNullException("requestContext"); - if (viewData == null) throw new ArgumentNullException("viewData"); - if (tempData == null) throw new ArgumentNullException("tempData"); - - var routeData = requestContext.RouteData; - if (routeData.Values.ContainsKey("controller") == false) - routeData.Values.Add("controller", "Fake"); - viewData.Model = model; - var controllerContext = new ControllerContext( - requestContext.HttpContext, routeData, - new FakeController - { - ViewData = viewData - }); - - using (var sw = new StringWriter()) - { - var viewResult = isPartial == false - ? ViewEngines.Engines.FindView(controllerContext, viewName, null) - : ViewEngines.Engines.FindPartialView(controllerContext, viewName); - if (viewResult.View == null) - throw new InvalidOperationException("No view could be found by name " + viewName); - var viewContext = new ViewContext(controllerContext, viewResult.View, viewData, tempData, sw); - viewResult.View.Render(viewContext, sw); - viewResult.ViewEngine.ReleaseView(controllerContext, viewResult.View); - return sw.GetStringBuilder().ToString(); - } - } - - private class FakeController : ControllerBase { protected override void ExecuteCore() { } } - - /// - /// Normally in MVC the way that the View object gets assigned to the result is to Execute the ViewResult, this however - /// will write to the Response output stream which isn't what we want. Instead, this method will use the same logic inside - /// of MVC to assign the View object to the result but without executing it. - /// This is only relevant for view results of PartialViewResult or ViewResult. - /// - /// - /// - private static void EnsureViewObjectDataOnResult(this ControllerBase controller, ViewResultBase result) - { - if (result.View != null) return; - - if (string.IsNullOrEmpty(result.ViewName)) - result.ViewName = controller.ControllerContext.RouteData.GetRequiredString("action"); - - if (result.View != null) return; - - if (result is PartialViewResult) - { - var viewEngineResult = ViewEngines.Engines.FindPartialView(controller.ControllerContext, result.ViewName); - - if (viewEngineResult.View == null) - { - throw new InvalidOperationException("Could not find the view " + result.ViewName + ", the following locations were searched: " + Environment.NewLine + string.Join(Environment.NewLine, viewEngineResult.SearchedLocations)); - } - - result.View = viewEngineResult.View; - } - else if (result is ViewResult) - { - var vr = (ViewResult)result; - var viewEngineResult = ViewEngines.Engines.FindView(controller.ControllerContext, vr.ViewName, vr.MasterName); - - if (viewEngineResult.View == null) - { - throw new InvalidOperationException("Could not find the view " + vr.ViewName + ", the following locations were searched: " + Environment.NewLine + string.Join(Environment.NewLine, viewEngineResult.SearchedLocations)); - } - - result.View = viewEngineResult.View; - } - } } } diff --git a/src/Umbraco.Web/Mvc/ControllerFactoryExtensions.cs b/src/Umbraco.Web/Mvc/ControllerFactoryExtensions.cs deleted file mode 100644 index a69b09efe0..0000000000 --- a/src/Umbraco.Web/Mvc/ControllerFactoryExtensions.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Web.Mvc; -using System.Web.Routing; - -namespace Umbraco.Web.Mvc -{ - internal static class ControllerFactoryExtensions - { - /// - /// Gets a controller type by the name - /// - /// - /// - /// - /// - /// - /// This is related to issue: http://issues.umbraco.org/issue/U4-1726. We already have a method called GetControllerTypeInternal on our MasterControllerFactory, - /// however, we cannot always guarantee that the usage of this will be a MasterControllerFactory like during unit tests. So we needed to create - /// this extension method to do the checks instead. - /// - internal static Type GetControllerTypeInternal(this IControllerFactory factory, RequestContext requestContext, string controllerName) - { - - //TODO Reintroduce for netcore - // if (factory is MasterControllerFactory controllerFactory) - // return controllerFactory.GetControllerTypeInternal(requestContext, controllerName); - - //we have no choice but to instantiate the controller - var instance = factory.CreateController(requestContext, controllerName); - var controllerType = instance?.GetType(); - factory.ReleaseController(instance); - - return controllerType; - } - } -} diff --git a/src/Umbraco.Web/Mvc/HttpUmbracoFormRouteStringException.cs b/src/Umbraco.Web/Mvc/HttpUmbracoFormRouteStringException.cs deleted file mode 100644 index db2040665c..0000000000 --- a/src/Umbraco.Web/Mvc/HttpUmbracoFormRouteStringException.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Runtime.Serialization; -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. - /// - 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/IRenderController.cs b/src/Umbraco.Web/Mvc/IRenderController.cs deleted file mode 100644 index cddd51ef5f..0000000000 --- a/src/Umbraco.Web/Mvc/IRenderController.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Umbraco.Web.Mvc -{ - //Migrated to .NET Core - public interface IRenderController - { - - } -} diff --git a/src/Umbraco.Web/Mvc/IRenderMvcController.cs b/src/Umbraco.Web/Mvc/IRenderMvcController.cs deleted file mode 100644 index 542e46ac2c..0000000000 --- a/src/Umbraco.Web/Mvc/IRenderMvcController.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Umbraco.Web.Mvc -{ - //Migrated to .NET Core - public interface IRenderMvcController : IRenderController - { - - } -} diff --git a/src/Umbraco.Web/Mvc/NotChildAction.cs b/src/Umbraco.Web/Mvc/NotChildAction.cs deleted file mode 100644 index fc8ca7c1ae..0000000000 --- a/src/Umbraco.Web/Mvc/NotChildAction.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Web.Mvc; - -namespace Umbraco.Web.Mvc -{ - /// - /// Used to ensure that actions with duplicate names that are not child actions don't get executed when - /// we are Posting and not redirecting. - /// - /// - /// See issue: http://issues.umbraco.org/issue/U4-1819 - /// - public class NotChildAction : ActionMethodSelectorAttribute - { - public override bool IsValidForRequest(ControllerContext controllerContext, System.Reflection.MethodInfo methodInfo) - { - var isChildAction = controllerContext.IsChildAction; - return !isChildAction; - } - } -} diff --git a/src/Umbraco.Web/Mvc/NotFoundHandler.cs b/src/Umbraco.Web/Mvc/NotFoundHandler.cs deleted file mode 100644 index bb98b42028..0000000000 --- a/src/Umbraco.Web/Mvc/NotFoundHandler.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Web; - -namespace Umbraco.Web.Mvc -{ - public class NotFoundHandler : IHttpHandler - { - public void ProcessRequest(HttpContext context) - { - context.Response.StatusCode = 404; - } - - public bool IsReusable - { - get { return true; } - } - } -} diff --git a/src/Umbraco.Web/Mvc/PluginControllerArea.cs b/src/Umbraco.Web/Mvc/PluginControllerArea.cs deleted file mode 100644 index a4440ec4a6..0000000000 --- a/src/Umbraco.Web/Mvc/PluginControllerArea.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web.Mvc; -using System.Web.Routing; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Composing; -using Umbraco.Core.Configuration.Models; -using Umbraco.Core.Hosting; -using Umbraco.Core.IO; -using Umbraco.Web.WebApi; - -namespace Umbraco.Web.Mvc -{ - /// - /// A custom area for controllers that are plugins - /// - internal class PluginControllerArea : AreaRegistration - { - private readonly GlobalSettings _globalSettings; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly IEnumerable _surfaceControllers; - private readonly IEnumerable _apiControllers; - private readonly string _areaName; - - /// - /// The constructor accepts all types of plugin controllers and will verify that ALL of them have the same areaName assigned to them - /// based on their PluginControllerAttribute. If they are not the same an exception will be thrown. - /// - /// - /// - /// - public PluginControllerArea(GlobalSettings globalSettings, IHostingEnvironment hostingEnvironment, IEnumerable pluginControllers) - { - _globalSettings = globalSettings; - _hostingEnvironment = hostingEnvironment; - var controllers = pluginControllers.ToArray(); - - if (controllers.Any(x => x.AreaName.IsNullOrWhiteSpace())) - { - throw new InvalidOperationException("Cannot create a PluginControllerArea unless all plugin controllers assigned have a PluginControllerAttribute assigned"); - } - _areaName = controllers.First().AreaName; - foreach(var c in controllers) - { - if (c.AreaName != _areaName) - { - throw new InvalidOperationException("Cannot create a PluginControllerArea unless all plugin controllers assigned have the same AreaName. The first AreaName found was " + _areaName + " however, the controller of type " + c.GetType().FullName + " has an AreaName of " + c.AreaName); - } - } - - //get the controllers - _surfaceControllers = controllers.Where(x => TypeHelper.IsTypeAssignableFrom(x.ControllerType)); - _apiControllers = controllers.Where(x => TypeHelper.IsTypeAssignableFrom(x.ControllerType)); - } - - public override void RegisterArea(AreaRegistrationContext context) - { - MapRouteSurfaceControllers(context.Routes, _surfaceControllers); - MapRouteApiControllers(context.Routes, _apiControllers); - } - - public override string AreaName - { - get { return _areaName; } - } - - /// - /// Registers all surface controller routes - /// - /// - /// - /// - /// The routes will be: - /// - /// /Umbraco/[AreaName]/[ControllerName]/[Action]/[Id] - /// - private void MapRouteSurfaceControllers(RouteCollection routes, IEnumerable surfaceControllers) - { - foreach (var s in surfaceControllers) - { - var route = this.RouteControllerPlugin(_globalSettings, _hostingEnvironment, s.ControllerName, s.ControllerType, routes, "", "Index", UrlParameter.Optional, "surface"); - //set the route handler to our SurfaceRouteHandler - route.RouteHandler = new SurfaceRouteHandler(); - } - } - - /// - /// Registers all api controller routes - /// - /// - /// - private void MapRouteApiControllers(RouteCollection routes, IEnumerable apiControllers) - { - foreach (var s in apiControllers) - { - this.RouteControllerPlugin(_globalSettings, _hostingEnvironment, s.ControllerName, s.ControllerType, routes, "", "", UrlParameter.Optional, "api", - isMvc: false, - areaPathPrefix: s.IsBackOffice ? "backoffice" : null); - } - } - } -} diff --git a/src/Umbraco.Web/Mvc/PostedDataProxyInfo.cs b/src/Umbraco.Web/Mvc/PostedDataProxyInfo.cs deleted file mode 100644 index 2af2e0fbd0..0000000000 --- a/src/Umbraco.Web/Mvc/PostedDataProxyInfo.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace Umbraco.Web.Mvc -{ - /// - /// Represents the data required to proxy a request to a surface controller for posted data - /// - internal class PostedDataProxyInfo : RouteDefinition - { - public string Area { get; set; } - } -} diff --git a/src/Umbraco.Web/Mvc/ProfilingView.cs b/src/Umbraco.Web/Mvc/ProfilingView.cs deleted file mode 100644 index ce733552d6..0000000000 --- a/src/Umbraco.Web/Mvc/ProfilingView.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.IO; -using System.Web.Mvc; -using Umbraco.Web.Composing; - -namespace Umbraco.Web.Mvc -{ - public class ProfilingView : IView - { - private readonly IView _inner; - private readonly string _name; - private readonly string _viewPath; - - public ProfilingView(IView inner) - { - _inner = inner; - _name = inner.GetType().Name; - var razorView = inner as RazorView; - _viewPath = razorView != null ? razorView.ViewPath : "Unknown"; - } - - public void Render(ViewContext viewContext, TextWriter writer) - { - using (Current.Profiler.Step(string.Format("{0}.Render: {1}", _name, _viewPath))) - { - _inner.Render(viewContext, writer); - } - } - } -} diff --git a/src/Umbraco.Web/Mvc/QueryStringFilterAttribute.cs b/src/Umbraco.Web/Mvc/QueryStringFilterAttribute.cs deleted file mode 100644 index 6551eb99f3..0000000000 --- a/src/Umbraco.Web/Mvc/QueryStringFilterAttribute.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web.Mvc; -using Umbraco.Core; - -namespace Umbraco.Web.Mvc -{ - - /// - /// Allows an Action to execute with an arbitrary number of QueryStrings - /// - /// - /// Just like you can POST an arbitrary number of parameters to an Action, you can't GET an arbitrary number - /// but this will allow you to do it - /// - /// http://stackoverflow.com/questions/488061/passing-multiple-parameters-to-controller-in-asp-net-mvc-also-generating-on-the - /// - public class QueryStringFilterAttribute : ActionFilterAttribute - { - public string ParameterName { get; private set; } - - public QueryStringFilterAttribute(string parameterName) - { - if (string.IsNullOrEmpty(parameterName)) - throw new ArgumentException("ParameterName is required."); - ParameterName = parameterName; - } - - public override void OnActionExecuting(ActionExecutingContext filterContext) - { - var nonNullKeys = filterContext.HttpContext.Request.QueryString.AllKeys.Where(x => !string.IsNullOrEmpty(x)); - var vals = nonNullKeys.ToDictionary(q => q, q => (object)filterContext.HttpContext.Request.QueryString[q]); - var qs = ToFormCollection(vals); - - filterContext.ActionParameters[ParameterName] = qs; - - base.OnActionExecuting(filterContext); - } - - /// - /// Converts a dictionary to a FormCollection - /// - /// - /// - public static FormCollection ToFormCollection(IDictionary d) - { - var n = new FormCollection(); - foreach (var i in d) - { - n.Add(i.Key, Convert.ToString(i.Value)); - } - return n; - } - - } - -} diff --git a/src/Umbraco.Web/Mvc/RedirectToUmbracoPageResult.cs b/src/Umbraco.Web/Mvc/RedirectToUmbracoPageResult.cs deleted file mode 100644 index 4658910ab0..0000000000 --- a/src/Umbraco.Web/Mvc/RedirectToUmbracoPageResult.cs +++ /dev/null @@ -1,278 +0,0 @@ -using System; -using System.Collections.Specialized; -using System.Linq; -using System.Web; -using System.Web.Mvc; -using Umbraco.Core; -using Umbraco.Core.Models; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.Web.Composing; -using Umbraco.Web.Routing; - -namespace Umbraco.Web.Mvc -{ - /// - /// Redirects to an Umbraco page by Id or Entity - /// - /// Migrated already to .Net Core - public class RedirectToUmbracoPageResult : ActionResult - { - private IPublishedContent _publishedContent; - private readonly int _pageId; - private readonly Guid _key; - private NameValueCollection _queryStringValues; - private IPublishedUrlProvider _publishedUrlProvider; - private string _url; - - public string Url - { - get - { - if (!_url.IsNullOrWhiteSpace()) return _url; - - if (PublishedContent == null) - { - throw new InvalidOperationException(string.Format("Cannot redirect, no entity was found for id {0}", _pageId)); - } - - var result = _publishedUrlProvider.GetUrl(PublishedContent.Id); - if (result != "#") - { - _url = result; - return _url; - } - - throw new InvalidOperationException(string.Format("Could not route to entity with id {0}, the NiceUrlProvider could not generate a URL", _pageId)); - - } - } - - public int PageId - { - get { return _pageId; } - } - - public Guid Key - { - get { return _key; } - } - public IPublishedContent PublishedContent - { - get - { - if (_publishedContent != null) return _publishedContent; - - if (_pageId != default(int)) - { - _publishedContent = Current.UmbracoContext.Content.GetById(_pageId); - } - - else if (_key != default(Guid)) - { - _publishedContent = Current.UmbracoContext.Content.GetById(_key); - } - - return _publishedContent; - } - } - - /// - /// Creates a new RedirectToUmbracoResult - /// - /// - public RedirectToUmbracoPageResult(int pageId) - : this(pageId, Current.PublishedUrlProvider) - { - } - - /// - /// Creates a new RedirectToUmbracoResult - /// - /// - /// - public RedirectToUmbracoPageResult(int pageId, NameValueCollection queryStringValues) - : this(pageId, queryStringValues, Current.PublishedUrlProvider) - { - } - - /// - /// Creates a new RedirectToUmbracoResult - /// - /// - /// - public RedirectToUmbracoPageResult(int pageId, string queryString) - : this(pageId, queryString, Current.PublishedUrlProvider) - { - } - - /// - /// Creates a new RedirectToUmbracoResult - /// - /// - public RedirectToUmbracoPageResult(IPublishedContent publishedContent) - : this(publishedContent, Current.PublishedUrlProvider) - { - } - - /// - /// Creates a new RedirectToUmbracoResult - /// - /// - /// - public RedirectToUmbracoPageResult(IPublishedContent publishedContent, NameValueCollection queryStringValues) - : this(publishedContent, queryStringValues, Current.PublishedUrlProvider) - { - } - - /// - /// Creates a new RedirectToUmbracoResult - /// - /// - /// - public RedirectToUmbracoPageResult(IPublishedContent publishedContent, string queryString) - : this(publishedContent, queryString, Current.PublishedUrlProvider) - { - } - - /// - /// Creates a new RedirectToUmbracoResult - /// - /// - /// - public RedirectToUmbracoPageResult(int pageId, IPublishedUrlProvider publishedUrlProvider) - { - _pageId = pageId; - _publishedUrlProvider = publishedUrlProvider; - } - - /// - /// Creates a new RedirectToUmbracoResult - /// - /// - /// - /// - public RedirectToUmbracoPageResult(int pageId, NameValueCollection queryStringValues, IPublishedUrlProvider publishedUrlProvider) - { - _pageId = pageId; - _queryStringValues = queryStringValues; - _publishedUrlProvider = publishedUrlProvider; - } - - /// - /// Creates a new RedirectToUmbracoResult - /// - /// - /// - /// - public RedirectToUmbracoPageResult(int pageId, string queryString, IPublishedUrlProvider publishedUrlProvider) - { - _pageId = pageId; - _queryStringValues = ParseQueryString(queryString); - _publishedUrlProvider = publishedUrlProvider; - } - - /// - /// Creates a new RedirectToUmbracoResult - /// - /// - /// - public RedirectToUmbracoPageResult(Guid key) - { - _key = key; - } - - /// - /// Creates a new RedirectToUmbracoResult - /// - /// - /// - public RedirectToUmbracoPageResult(Guid key, NameValueCollection queryStringValues) - { - _key = key; - _queryStringValues = queryStringValues; - } - - /// - /// Creates a new RedirectToUmbracoResult - /// - /// - /// - public RedirectToUmbracoPageResult(Guid key, string queryString) - { - _key = key; - _queryStringValues = ParseQueryString(queryString); - } - - /// - /// Creates a new RedirectToUmbracoResult - /// - /// - /// - public RedirectToUmbracoPageResult(IPublishedContent publishedContent, IPublishedUrlProvider publishedUrlProvider) - { - _publishedContent = publishedContent; - _pageId = publishedContent.Id; - _publishedUrlProvider = publishedUrlProvider; - } - - /// - /// Creates a new RedirectToUmbracoResult - /// - /// - /// - /// - public RedirectToUmbracoPageResult(IPublishedContent publishedContent, NameValueCollection queryStringValues, IPublishedUrlProvider publishedUrlProvider) - { - _publishedContent = publishedContent; - _pageId = publishedContent.Id; - _queryStringValues = queryStringValues; - _publishedUrlProvider = publishedUrlProvider; - } - - /// - /// Creates a new RedirectToUmbracoResult - /// - /// - /// - /// - public RedirectToUmbracoPageResult(IPublishedContent publishedContent, string queryString, IPublishedUrlProvider publishedUrlProvider) - { - _publishedContent = publishedContent; - _pageId = publishedContent.Id; - _queryStringValues = ParseQueryString(queryString); - _publishedUrlProvider = publishedUrlProvider; - } - - public override void ExecuteResult(ControllerContext context) - { - if (context == null) throw new ArgumentNullException("context"); - - if (context.IsChildAction) - { - throw new InvalidOperationException("Cannot redirect from a Child Action"); - } - - var destinationUrl = UrlHelper.GenerateContentUrl(Url, context.HttpContext); - - if (_queryStringValues != null && _queryStringValues.Count > 0) - { - destinationUrl = destinationUrl += "?" + string.Join("&", - _queryStringValues.AllKeys.Select(x => x + "=" + HttpUtility.UrlEncode(_queryStringValues[x]))); - } - - context.Controller.TempData.Keep(); - - context.HttpContext.Response.Redirect(destinationUrl, endResponse: false); - } - - private NameValueCollection ParseQueryString(string queryString) - { - if (!string.IsNullOrEmpty(queryString)) - { - return HttpUtility.ParseQueryString(queryString); - } - - return null; - } - } -} diff --git a/src/Umbraco.Web/Mvc/RedirectToUmbracoUrlResult.cs b/src/Umbraco.Web/Mvc/RedirectToUmbracoUrlResult.cs deleted file mode 100644 index 6f97bff534..0000000000 --- a/src/Umbraco.Web/Mvc/RedirectToUmbracoUrlResult.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Web.Mvc; - -namespace Umbraco.Web.Mvc -{ - /// - /// Redirects to the current URL rendering an Umbraco page including it's query strings - /// - /// - /// This is useful if you need to redirect - /// to the current page but the current page is actually a rewritten URL normally done with something like - /// Server.Transfer. It is also handy if you want to persist the query strings. - /// - /// Migrated already to .Net Core - public class RedirectToUmbracoUrlResult : ActionResult - { - private readonly IUmbracoContext _umbracoContext; - - /// - /// Creates a new RedirectToUmbracoResult - /// - /// - public RedirectToUmbracoUrlResult(IUmbracoContext umbracoContext) - { - _umbracoContext = umbracoContext; - } - - public override void ExecuteResult(ControllerContext context) - { - if (context == null) throw new ArgumentNullException("context"); - - if (context.IsChildAction) - { - throw new InvalidOperationException("Cannot redirect from a Child Action"); - } - - var destinationUrl = _umbracoContext.OriginalRequestUrl.PathAndQuery; - context.Controller.TempData.Keep(); - - context.HttpContext.Response.Redirect(destinationUrl, endResponse: false); - } - } -} diff --git a/src/Umbraco.Web/Mvc/RenderActionInvoker.cs b/src/Umbraco.Web/Mvc/RenderActionInvoker.cs deleted file mode 100644 index 47e77bb4f4..0000000000 --- a/src/Umbraco.Web/Mvc/RenderActionInvoker.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using System.Web.Mvc; -using System.Web.Mvc.Async; - -namespace Umbraco.Web.Mvc -{ - /// - /// Ensures that if an action for the Template name is not explicitly defined by a user, that the 'Index' action will execute - /// - public class RenderActionInvoker : AsyncControllerActionInvoker - { - /// - /// Ensures that if an action for the Template name is not explicitly defined by a user, that the 'Index' action will execute - /// - /// - /// - /// - /// - protected override ActionDescriptor FindAction(ControllerContext controllerContext, ControllerDescriptor controllerDescriptor, string actionName) - { - var ad = base.FindAction(controllerContext, controllerDescriptor, actionName); - - //now we need to check if it exists, if not we need to return the Index by default - if (ad == null) - { - //check if the controller is an instance of IRenderController and find the index - if (controllerContext.Controller is IRenderController) - { - // ReSharper disable once Mvc.ActionNotResolved - return controllerDescriptor.FindAction(controllerContext, "Index"); - } - } - return ad; - } - - } -} diff --git a/src/Umbraco.Web/Mvc/RenderMvcController.cs b/src/Umbraco.Web/Mvc/RenderMvcController.cs deleted file mode 100644 index 1a4a32eadf..0000000000 --- a/src/Umbraco.Web/Mvc/RenderMvcController.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Web.Mvc; - -namespace Umbraco.Web.Mvc -{ - //Migrated to .NET Core - public class RenderMvcController : Controller, IRenderMvcController - { - - - } -} diff --git a/src/Umbraco.Web/Mvc/RenderRouteHandler.cs b/src/Umbraco.Web/Mvc/RenderRouteHandler.cs index c88958d2fe..2cd491b7a7 100644 --- a/src/Umbraco.Web/Mvc/RenderRouteHandler.cs +++ b/src/Umbraco.Web/Mvc/RenderRouteHandler.cs @@ -3,21 +3,18 @@ using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Routing; -using System.Web.SessionState; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Umbraco.Core; -using Umbraco.Core.Composing; using Umbraco.Core.Strings; using Umbraco.Web.Features; using Umbraco.Web.Models; using Umbraco.Web.Routing; -using Umbraco.Core.Strings; using Current = Umbraco.Web.Composing.Current; namespace Umbraco.Web.Mvc { - public class RenderRouteHandler : IRouteHandler + // NOTE: already migrated to netcore, just here since the below is referenced still + public class RenderRouteHandler { // Define reserved dictionary keys for controller, action and area specified in route additional values data internal static class ReservedAdditionalKeys @@ -26,207 +23,5 @@ namespace Umbraco.Web.Mvc internal const string Action = "a"; internal const string Area = "ar"; } - - private readonly IControllerFactory _controllerFactory; - private readonly IShortStringHelper _shortStringHelper; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly IUmbracoContext _umbracoContext; - - public RenderRouteHandler(IUmbracoContextAccessor umbracoContextAccessor, IControllerFactory controllerFactory, IShortStringHelper shortStringHelper) - { - _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); - _controllerFactory = controllerFactory ?? throw new ArgumentNullException(nameof(controllerFactory)); - _shortStringHelper = shortStringHelper ?? throw new ArgumentNullException(nameof(shortStringHelper)); - } - - public RenderRouteHandler(IUmbracoContext umbracoContext, IControllerFactory controllerFactory, IShortStringHelper shortStringHelper) - { - _umbracoContext = umbracoContext ?? throw new ArgumentNullException(nameof(umbracoContext)); - _controllerFactory = controllerFactory ?? throw new ArgumentNullException(nameof(controllerFactory)); - _shortStringHelper = shortStringHelper ?? throw new ArgumentNullException(nameof(shortStringHelper)); - } - - private IUmbracoContext UmbracoContext => _umbracoContext ?? _umbracoContextAccessor.UmbracoContext; - - private UmbracoFeatures Features => Current.Factory.GetRequiredService(); // TODO: inject - - #region IRouteHandler Members - - /// - /// Assigns the correct controller based on the Umbraco request and returns a standard MvcHandler to process the response, - /// this also stores the render model into the data tokens for the current RouteData. - /// - public IHttpHandler GetHttpHandler(RequestContext requestContext) - { - if (UmbracoContext == null) - { - throw new NullReferenceException("There is no current UmbracoContext, it must be initialized before the RenderRouteHandler executes"); - } - var request = UmbracoContext.PublishedRequest; - if (request == null) - { - throw new NullReferenceException("There is no current PublishedRequest, it must be initialized before the RenderRouteHandler executes"); - } - - return GetHandlerForRoute(requestContext, request); - - } - - #endregion - - private void UpdateRouteDataForRequest(ContentModel contentModel, RequestContext requestContext) - { - if (contentModel == null) throw new ArgumentNullException(nameof(contentModel)); - if (requestContext == null) throw new ArgumentNullException(nameof(requestContext)); - - // requestContext.RouteData.DataTokens[Core.Constants.Web.UmbracoDataToken] = contentModel; - // the rest should not change -- it's only the published content that has changed - } - - /// - /// Checks the request and query strings to see if it matches the definition of having a Surface controller - /// posted/get value, if so, then we return a PostedDataProxyInfo object with the correct information. - /// - /// - /// - internal static PostedDataProxyInfo GetFormInfo(RequestContext requestContext) - { - if (requestContext == null) throw new ArgumentNullException(nameof(requestContext)); - - //if it is a POST/GET then a value must be in the request - if (requestContext.HttpContext.Request.QueryString["ufprt"].IsNullOrWhiteSpace() - && requestContext.HttpContext.Request.Form["ufprt"].IsNullOrWhiteSpace()) - { - return null; - } - - string encodedVal; - - switch (requestContext.HttpContext.Request.RequestType) - { - case "POST": - //get the value from the request. - //this field will contain an encrypted version of the surface route vals. - encodedVal = requestContext.HttpContext.Request.Form["ufprt"]; - break; - case "GET": - //this field will contain an encrypted version of the surface route vals. - encodedVal = requestContext.HttpContext.Request.QueryString["ufprt"]; - break; - default: - return null; - } - - if (!UmbracoHelper.DecryptAndValidateEncryptedRouteString(encodedVal, out var decodedParts)) - return null; - - foreach (var item in decodedParts.Where(x => new[] { - ReservedAdditionalKeys.Controller, - ReservedAdditionalKeys.Action, - ReservedAdditionalKeys.Area }.Contains(x.Key) == false)) - { - // Populate route with additional values which aren't reserved values so they eventually to action parameters - requestContext.RouteData.Values[item.Key] = item.Value; - } - - //return the proxy info without the surface id... could be a local controller. - return new PostedDataProxyInfo - { - ControllerName = HttpUtility.UrlDecode(decodedParts.Single(x => x.Key == ReservedAdditionalKeys.Controller).Value), - ActionName = HttpUtility.UrlDecode(decodedParts.Single(x => x.Key == ReservedAdditionalKeys.Action).Value), - Area = HttpUtility.UrlDecode(decodedParts.Single(x => x.Key == ReservedAdditionalKeys.Area).Value), - }; - } - - - - /// - /// Handles a posted form to an Umbraco URL and ensures the correct controller is routed to and that - /// the right DataTokens are set. - /// - /// - /// - internal static IHttpHandler HandlePostedValues(RequestContext requestContext, PostedDataProxyInfo postedInfo) - { - if (requestContext == null) throw new ArgumentNullException(nameof(requestContext)); - if (postedInfo == null) throw new ArgumentNullException(nameof(postedInfo)); - - //set the standard route values/tokens - requestContext.RouteData.Values["controller"] = postedInfo.ControllerName; - requestContext.RouteData.Values["action"] = postedInfo.ActionName; - - IHttpHandler handler; - - //get the route from the defined routes - using (RouteTable.Routes.GetReadLock()) - { - Route surfaceRoute; - - //find the controller in the route table - var surfaceRoutes = RouteTable.Routes.OfType() - .Where(x => x.Defaults != null && - x.Defaults.ContainsKey("controller") && - x.Defaults["controller"].ToString().InvariantEquals(postedInfo.ControllerName) && - // Only return surface controllers - x.DataTokens["umbraco"].ToString().InvariantEquals("surface") && - // Check for area token if the area is supplied - (postedInfo.Area.IsNullOrWhiteSpace() ? !x.DataTokens.ContainsKey("area") : x.DataTokens["area"].ToString().InvariantEquals(postedInfo.Area))) - .ToList(); - - // If more than one route is found, find one with a matching action - if (surfaceRoutes.Count > 1) - { - surfaceRoute = surfaceRoutes.FirstOrDefault(x => - x.Defaults["action"] != null && - x.Defaults["action"].ToString().InvariantEquals(postedInfo.ActionName)); - } - else - { - surfaceRoute = surfaceRoutes.FirstOrDefault(); - } - - if (surfaceRoute == null) - throw new InvalidOperationException("Could not find a Surface controller route in the RouteTable for controller name " + postedInfo.ControllerName); - - //set the area if one is there. - if (surfaceRoute.DataTokens.ContainsKey("area")) - { - requestContext.RouteData.DataTokens["area"] = surfaceRoute.DataTokens["area"]; - } - - //set the 'Namespaces' token so the controller factory knows where to look to construct it - if (surfaceRoute.DataTokens.ContainsKey("Namespaces")) - { - requestContext.RouteData.DataTokens["Namespaces"] = surfaceRoute.DataTokens["Namespaces"]; - } - handler = surfaceRoute.RouteHandler.GetHttpHandler(requestContext); - - } - - return handler; - } - - /// - /// this will determine the controller and set the values in the route data - /// - internal IHttpHandler GetHandlerForRoute(RequestContext requestContext, IPublishedRequest request) - { - if (requestContext == null) throw new ArgumentNullException(nameof(requestContext)); - if (request == null) throw new ArgumentNullException(nameof(request)); - - //var routeDef = GetUmbracoRouteDefinition(requestContext, request); - - // TODO: Need to port this to netcore - // Need to check for a special case if there is form data being posted back to an Umbraco URL - var postedInfo = GetFormInfo(requestContext); - if (postedInfo != null) - { - return HandlePostedValues(requestContext, postedInfo); - } - - // NOTE: Code here has been removed and ported to netcore - throw new NotSupportedException("This code was already ported to netcore"); - } - } } diff --git a/src/Umbraco.Web/Mvc/RouteValueDictionaryExtensions.cs b/src/Umbraco.Web/Mvc/RouteValueDictionaryExtensions.cs deleted file mode 100644 index acfa4e5715..0000000000 --- a/src/Umbraco.Web/Mvc/RouteValueDictionaryExtensions.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Linq; -using System.Web.Mvc; -using System.Web.Routing; - -namespace Umbraco.Web.Mvc -{ - internal static class RouteValueDictionaryExtensions - { - - /// - /// Converts a route value dictionary to a form collection - /// - /// - /// - public static FormCollection ToFormCollection(this RouteValueDictionary items) - { - var formCollection = new FormCollection(); - foreach (var i in items) - { - formCollection.Add(i.Key, i.Value != null ? i.Value.ToString() : null); - } - return formCollection; - } - - /// - /// Returns the value of a mandatory item in the route items - /// - /// - /// - /// - public static object GetRequiredObject(this RouteValueDictionary items, string key) - { - if (key == null) throw new ArgumentNullException("key"); - if (items.Keys.Contains(key) == false) - throw new ArgumentNullException("The " + key + " parameter was not found but is required"); - return items[key]; - } - - } -} diff --git a/src/Umbraco.Web/Mvc/SurfaceController.cs b/src/Umbraco.Web/Mvc/SurfaceController.cs index fa67248e7d..483461ae57 100644 --- a/src/Umbraco.Web/Mvc/SurfaceController.cs +++ b/src/Umbraco.Web/Mvc/SurfaceController.cs @@ -10,9 +10,6 @@ using Umbraco.Web.Composing; namespace Umbraco.Web.Mvc { - /// - /// Provides a base class for front-end add-in controllers. - /// /// Migrated already to .Net Core without MergeModelStateToChildAction and MergeParentContextViewData action filters /// TODO: Migrate MergeModelStateToChildAction and MergeParentContextViewData action filters [MergeModelStateToChildAction] @@ -26,197 +23,5 @@ namespace Umbraco.Web.Mvc : base(umbracoContextAccessor, databaseFactory, services, appCaches,profilingLogger) { } - /// - /// Redirects to the Umbraco page with the given id - /// - /// - /// - protected RedirectToUmbracoPageResult RedirectToUmbracoPage(int pageId) - { - return new RedirectToUmbracoPageResult(pageId, Current.PublishedUrlProvider); - } - - /// - /// Redirects to the Umbraco page with the given id and passes provided querystring - /// - /// - /// - /// - protected RedirectToUmbracoPageResult RedirectToUmbracoPage(int pageId, NameValueCollection queryStringValues) - { - return new RedirectToUmbracoPageResult(pageId, queryStringValues, Current.PublishedUrlProvider); - } - - /// - /// Redirects to the Umbraco page with the given id and passes provided querystring - /// - /// - /// - /// - protected RedirectToUmbracoPageResult RedirectToUmbracoPage(int pageId, string queryString) - { - return new RedirectToUmbracoPageResult(pageId, queryString, Current.PublishedUrlProvider); - } - - /// - /// Redirects to the Umbraco page with the given id - /// - /// - /// - protected RedirectToUmbracoPageResult RedirectToUmbracoPage(Guid key) - { - return new RedirectToUmbracoPageResult(key); - } - - /// - /// Redirects to the Umbraco page with the given id and passes provided querystring - /// - /// - /// - /// - protected RedirectToUmbracoPageResult RedirectToUmbracoPage(Guid key, NameValueCollection queryStringValues) - { - return new RedirectToUmbracoPageResult(key, queryStringValues); - } - - /// - /// Redirects to the Umbraco page with the given id and passes provided querystring - /// - /// - /// - /// - protected RedirectToUmbracoPageResult RedirectToUmbracoPage(Guid key, string queryString) - { - return new RedirectToUmbracoPageResult(key, queryString); - } - - /// - /// Redirects to the Umbraco page with the given id - /// - /// - /// - protected RedirectToUmbracoPageResult RedirectToUmbracoPage(IPublishedContent publishedContent) - { - return new RedirectToUmbracoPageResult(publishedContent, Current.PublishedUrlProvider); - } - - /// - /// Redirects to the Umbraco page with the given id and passes provided querystring - /// - /// - /// - /// - protected RedirectToUmbracoPageResult RedirectToUmbracoPage(IPublishedContent publishedContent, NameValueCollection queryStringValues) - { - return new RedirectToUmbracoPageResult(publishedContent, queryStringValues, Current.PublishedUrlProvider); - } - - /// - /// Redirects to the Umbraco page with the given id and passes provided querystring - /// - /// - /// - /// - protected RedirectToUmbracoPageResult RedirectToUmbracoPage(IPublishedContent publishedContent, string queryString) - { - return new RedirectToUmbracoPageResult(publishedContent, queryString, Current.PublishedUrlProvider); - } - - /// - /// Redirects to the currently rendered Umbraco page - /// - /// - protected RedirectToUmbracoPageResult RedirectToCurrentUmbracoPage() - { - return new RedirectToUmbracoPageResult(CurrentPage, Current.PublishedUrlProvider); - } - - /// - /// Redirects to the currently rendered Umbraco page and passes provided querystring - /// - /// - /// - protected RedirectToUmbracoPageResult RedirectToCurrentUmbracoPage(NameValueCollection queryStringValues) - { - return new RedirectToUmbracoPageResult(CurrentPage, queryStringValues, Current.PublishedUrlProvider); - } - - /// - /// Redirects to the currently rendered Umbraco page and passes provided querystring - /// - /// - /// - protected RedirectToUmbracoPageResult RedirectToCurrentUmbracoPage(string queryString) - { - return new RedirectToUmbracoPageResult(CurrentPage, queryString, Current.PublishedUrlProvider); - } - - /// - /// Redirects to the currently rendered Umbraco URL - /// - /// - /// - /// this is useful if you need to redirect - /// to the current page but the current page is actually a rewritten URL normally done with something like - /// Server.Transfer. - /// - protected RedirectToUmbracoUrlResult RedirectToCurrentUmbracoUrl() - { - return new RedirectToUmbracoUrlResult(UmbracoContext); - } - - /// - /// Returns the currently rendered Umbraco page - /// - /// - protected UmbracoPageResult CurrentUmbracoPage() - { - return new UmbracoPageResult(ProfilingLogger); - } - - /// - /// Gets the current page. - /// - protected virtual IPublishedContent CurrentPage - { - get - { - var routeDefAttempt = TryGetRouteDefinitionFromAncestorViewContexts(); - if (routeDefAttempt.Success == false) - throw routeDefAttempt.Exception; - - var routeDef = routeDefAttempt.Result; - return routeDef.PublishedRequest.PublishedContent; - } - } - - /// - /// we need to recursively find the route definition based on the parent view context - /// - /// - /// - /// We may have Child Actions within Child actions so we need to recursively look this up. - /// see: http://issues.umbraco.org/issue/U4-1844 - /// - private Attempt TryGetRouteDefinitionFromAncestorViewContexts() - { - var currentContext = ControllerContext; - while (currentContext != null) - { - var currentRouteData = currentContext.RouteData; - if (currentRouteData.Values.ContainsKey(Core.Constants.Web.UmbracoRouteDefinitionDataToken)) - { - return Attempt.Succeed((RouteDefinition)currentRouteData.Values[Core.Constants.Web.UmbracoRouteDefinitionDataToken]); - } - - currentContext = currentContext.IsChildAction - ? currentContext.ParentActionViewContext - : null; - } - return Attempt.Fail( - new InvalidOperationException("Cannot find the Umbraco route definition in the route values, the request must be made in the context of an Umbraco request")); - } - - } } diff --git a/src/Umbraco.Web/Mvc/SurfaceRouteHandler.cs b/src/Umbraco.Web/Mvc/SurfaceRouteHandler.cs deleted file mode 100644 index 065e51dfe0..0000000000 --- a/src/Umbraco.Web/Mvc/SurfaceRouteHandler.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Web; -using System.Web.Routing; - -namespace Umbraco.Web.Mvc -{ - /// - /// Assigned to all SurfaceController's so that it returns our custom SurfaceMvcHandler to use for rendering - /// - internal class SurfaceRouteHandler : IRouteHandler - { - public IHttpHandler GetHttpHandler(RequestContext requestContext) - { - return new UmbracoMvcHandler(requestContext); - } - } -} diff --git a/src/Umbraco.Web/Mvc/UmbracoAuthorizeAttribute.cs b/src/Umbraco.Web/Mvc/UmbracoAuthorizeAttribute.cs deleted file mode 100644 index 73b2674706..0000000000 --- a/src/Umbraco.Web/Mvc/UmbracoAuthorizeAttribute.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using System.Web; -using System.Web.Mvc; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Security; -using Umbraco.Core.Configuration.Models; -using Umbraco.Web.Composing; -using Umbraco.Web.Security; - -namespace Umbraco.Web.Mvc -{ - // TODO: This has been migrated to netcore and can be removed when ready - - public sealed class UmbracoAuthorizeAttribute : AuthorizeAttribute - { - // see note in HttpInstallAuthorizeAttribute - private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; - private readonly IRuntimeState _runtimeState; - private readonly string _redirectUrl; - - private IRuntimeState RuntimeState => _runtimeState ?? Current.RuntimeState; - - private IBackOfficeSecurity BackOfficeSecurity => _backOfficeSecurityAccessor.BackOfficeSecurity ?? Current.BackOfficeSecurityAccessor.BackOfficeSecurity; - - /// - /// THIS SHOULD BE ONLY USED FOR UNIT TESTS - /// - /// - /// - public UmbracoAuthorizeAttribute(IBackOfficeSecurityAccessor backofficeSecurityAccessor, IRuntimeState runtimeState) - { - _backOfficeSecurityAccessor = backofficeSecurityAccessor ?? throw new ArgumentNullException(nameof(backofficeSecurityAccessor)); - _runtimeState = runtimeState ?? throw new ArgumentNullException(nameof(runtimeState)); - } - - /// - /// Default constructor - /// - public UmbracoAuthorizeAttribute() - { } - - /// - /// Constructor specifying to redirect to the specified location if not authorized - /// - /// - public UmbracoAuthorizeAttribute(string redirectUrl) - { - _redirectUrl = redirectUrl ?? throw new ArgumentNullException(nameof(redirectUrl)); - } - - /// - /// Constructor specifying to redirect to the umbraco login page if not authorized - /// - /// - public UmbracoAuthorizeAttribute(bool redirectToUmbracoLogin) - { - if (redirectToUmbracoLogin) - { - _redirectUrl = new GlobalSettings().GetBackOfficePath(Current.HostingEnvironment).EnsureStartsWith("~"); - } - } - - /// - /// Ensures that the user must be in the Administrator or the Install role - /// - /// - /// - protected override bool AuthorizeCore(HttpContextBase httpContext) - { - if (httpContext == null) throw new ArgumentNullException(nameof(httpContext)); - - try - { - // if not configured (install or upgrade) then we can continue - // otherwise we need to ensure that a user is logged in - return RuntimeState.Level == RuntimeLevel.Install - || RuntimeState.Level == RuntimeLevel.Upgrade - || (httpContext.User?.Identity?.IsAuthenticated ?? false); - } - catch (Exception) - { - return false; - } - } - - /// - /// Override to ensure no redirect occurs - /// - /// - protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) - { - if (_redirectUrl.IsNullOrWhiteSpace()) - { - filterContext.Result = (ActionResult)new HttpUnauthorizedResult("You must login to view this resource."); - - - } - else - { - filterContext.Result = new RedirectResult(_redirectUrl); - } - - // DON'T do a FormsAuth redirect... argh!! thankfully we're running .Net 4.5 :) - filterContext.RequestContext.HttpContext.Response.SuppressFormsAuthenticationRedirect = true; - } - - } -} diff --git a/src/Umbraco.Web/Mvc/UmbracoControllerFactory.cs b/src/Umbraco.Web/Mvc/UmbracoControllerFactory.cs deleted file mode 100644 index b4dde6c540..0000000000 --- a/src/Umbraco.Web/Mvc/UmbracoControllerFactory.cs +++ /dev/null @@ -1,88 +0,0 @@ -// using System; -// using System.Web.Mvc; -// using System.Web.Routing; -// using System.Web.SessionState; -// using Umbraco.Core; -// using Umbraco.Core.Composing; -// using Umbraco.Web.Composing; -// -// namespace Umbraco.Web.Mvc -// { -// /// -// /// Abstract filtered controller factory used for all Umbraco controller factory implementations -// /// -// public abstract class UmbracoControllerFactory : IFilteredControllerFactory -// { -// private readonly OverridenDefaultControllerFactory _innerFactory = new OverridenDefaultControllerFactory(); -// -// public abstract bool CanHandle(RequestContext request); -// -// public virtual Type GetControllerType(RequestContext requestContext, string controllerName) -// { -// return _innerFactory.GetControllerType(requestContext, controllerName); -// } -// -// /// -// /// Creates the specified controller by using the specified request context. -// /// -// /// -// /// The controller. -// /// -// /// The request context.The name of the controller. -// public virtual IController CreateController(RequestContext requestContext, string controllerName) -// { -// var controllerType = GetControllerType(requestContext, controllerName) ?? -// _innerFactory.GetControllerType( -// requestContext, -// ControllerExtensions.GetControllerName(Current.DefaultRenderMvcControllerType)); -// -// return _innerFactory.GetControllerInstance(requestContext, controllerType); -// } -// -// /// -// /// Gets the controller's session behavior. -// /// -// /// -// /// The controller's session behavior. -// /// -// /// The request context.The name of the controller whose session behavior you want to get. -// public SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName) -// { -// return ((IControllerFactory)_innerFactory).GetControllerSessionBehavior(requestContext, controllerName); -// } -// -// /// -// /// Releases the specified controller. -// /// -// /// The controller. -// public virtual void ReleaseController(IController controller) -// { -// _innerFactory.ReleaseController(controller); -// } -// -// /// -// /// By default, only exposes which throws an exception -// /// if the controller is not found. Since we want to try creating a controller, and then fall back to if one isn't found, -// /// this nested class changes the visibility of 's internal methods in order to not have to rely on a try-catch. -// /// -// /// -// internal class OverridenDefaultControllerFactory : ContainerControllerFactory -// { -// public OverridenDefaultControllerFactory() -// : base(new Lazy(() => Current.Factory)) -// { } -// -// public new IController GetControllerInstance(RequestContext requestContext, Type controllerType) -// { -// return base.GetControllerInstance(requestContext, controllerType); -// } -// -// public new Type GetControllerType(RequestContext requestContext, string controllerName) -// { -// return controllerName.IsNullOrWhiteSpace() -// ? null -// : base.GetControllerType(requestContext, controllerName); -// } -// } -// } -// } diff --git a/src/Umbraco.Web/Mvc/UmbracoMvcHandler.cs b/src/Umbraco.Web/Mvc/UmbracoMvcHandler.cs deleted file mode 100644 index 0357853c86..0000000000 --- a/src/Umbraco.Web/Mvc/UmbracoMvcHandler.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Web.Mvc; -using System.Web.Routing; - -namespace Umbraco.Web.Mvc -{ - /// - /// MVC handler to facilitate the TemplateRenderer. This handler can execute an MVC request and return it as a string. - /// - /// Original: - /// - /// This handler also used to intercept creation of controllers and store it for later use. - /// This was needed for the 'return CurrentUmbracoPage()' surface controller functionality - /// because it needs to send data back to the page controller. - /// - /// The creation of this controller has been moved to the UmbracoPageResult class which will create a controller when needed. - /// - internal class UmbracoMvcHandler : MvcHandler - { - public UmbracoMvcHandler(RequestContext requestContext) - : base(requestContext) - { - } - - /// - /// This is used internally purely to render an Umbraco MVC template to string and shouldn't be used for anything else. - /// - internal void ExecuteUmbracoRequest() - { - base.ProcessRequest(RequestContext.HttpContext); - } - } -} diff --git a/src/Umbraco.Web/Mvc/UmbracoPageResult.cs b/src/Umbraco.Web/Mvc/UmbracoPageResult.cs deleted file mode 100644 index 580924b909..0000000000 --- a/src/Umbraco.Web/Mvc/UmbracoPageResult.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System; -using System.IO; -using System.Web.Mvc; -using System.Web.Routing; -using Umbraco.Core; -using Umbraco.Core.Logging; - -namespace Umbraco.Web.Mvc -{ - /// - /// Used by posted forms to proxy the result to the page in which the current URL matches on - /// - /// Migrated already to .Net Core - public class UmbracoPageResult : ActionResult - { - private readonly IProfilingLogger _profilingLogger; - - public UmbracoPageResult(IProfilingLogger profilingLogger) - { - _profilingLogger = profilingLogger; - } - - public override void ExecuteResult(ControllerContext context) - { - ResetRouteData(context.RouteData); - - ValidateRouteData(context.RouteData); - - var routeDef = (RouteDefinition)context.RouteData.Values[Umbraco.Core.Constants.Web.UmbracoRouteDefinitionDataToken]; - - var factory = ControllerBuilder.Current.GetControllerFactory(); - context.RouteData.Values["action"] = routeDef.ActionName; - ControllerBase controller = null; - - try - { - controller = CreateController(context, factory, routeDef); - - CopyControllerData(context, controller); - - ExecuteControllerAction(context, controller); - } - finally - { - CleanupController(controller, factory); - } - } - - /// - /// Executes the controller action - /// - private void ExecuteControllerAction(ControllerContext context, IController controller) - { - using (_profilingLogger.TraceDuration("Executing Umbraco RouteDefinition controller", "Finished")) - { - controller.Execute(context.RequestContext); - } - } - - /// - /// Since we could be returning the current page from a surface controller posted values in which the routing values are changed, we - /// need to revert these values back to nothing in order for the normal page to render again. - /// - private static void ResetRouteData(RouteData routeData) - { - routeData.DataTokens["area"] = null; - routeData.DataTokens["Namespaces"] = null; - } - - /// - /// Validate that the current page execution is not being handled by the normal umbraco routing system - /// - private static void ValidateRouteData(RouteData routeData) - { - if (routeData.Values.ContainsKey(Umbraco.Core.Constants.Web.UmbracoRouteDefinitionDataToken) == false) - { - throw new InvalidOperationException("Can only use " + typeof(UmbracoPageResult).Name + - " in the context of an Http POST when using a SurfaceController form"); - } - } - - /// - /// Ensure ModelState, ViewData and TempData is copied across - /// - private static void CopyControllerData(ControllerContext context, ControllerBase controller) - { - controller.ViewData.ModelState.Merge(context.Controller.ViewData.ModelState); - - foreach (var d in context.Controller.ViewData) - controller.ViewData[d.Key] = d.Value; - - //We cannot simply merge the temp data because during controller execution it will attempt to 'load' temp data - // but since it has not been saved, there will be nothing to load and it will revert to nothing, so the trick is - // to Save the state of the temp data first then it will automatically be picked up. - // http://issues.umbraco.org/issue/U4-1339 - - var targetController = controller as Controller; - var sourceController = context.Controller as Controller; - if (targetController != null && sourceController != null) - { - targetController.TempDataProvider = sourceController.TempDataProvider; - targetController.TempData = sourceController.TempData; - targetController.TempData.Save(sourceController.ControllerContext, sourceController.TempDataProvider); - } - - } - - /// - /// Creates a controller using the controller factory - /// - private static ControllerBase CreateController(ControllerContext context, IControllerFactory factory, RouteDefinition routeDef) - { - var controller = factory.CreateController(context.RequestContext, routeDef.ControllerName) as ControllerBase; - - if (controller == null) - throw new InvalidOperationException("Could not create controller with name " + routeDef.ControllerName + "."); - - return controller; - } - - /// - /// Cleans up the controller by releasing it using the controller factory, and by disposing it. - /// - private static void CleanupController(IController controller, IControllerFactory factory) - { - if (controller != null) - factory.ReleaseController(controller); - - if (controller != null) - controller.DisposeIfDisposable(); - } - - private class DummyView : IView - { - public void Render(ViewContext viewContext, TextWriter writer) - { - } - } - } -} diff --git a/src/Umbraco.Web/Mvc/UmbracoRequireHttpsAttribute.cs b/src/Umbraco.Web/Mvc/UmbracoRequireHttpsAttribute.cs deleted file mode 100644 index b88d1c0736..0000000000 --- a/src/Umbraco.Web/Mvc/UmbracoRequireHttpsAttribute.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Web.Mvc; -using Umbraco.Core; -using Umbraco.Web.Composing; - -namespace Umbraco.Web.Mvc -{ - /// - /// If Umbraco.Core.UseHttps property in web.config is set to true, this filter will redirect any http access to https. - /// - public class UmbracoRequireHttpsAttribute : RequireHttpsAttribute - { - /// - /// If Umbraco.Core.UseHttps is true and we have a non-HTTPS request, handle redirect. - /// - /// Filter context - protected override void HandleNonHttpsRequest(AuthorizationContext filterContext) - { - // If Umbraco.Core.UseHttps is set, let base method handle redirect. Otherwise, we don't care. - if (/*Current.Configs.Global().UseHttps*/ false) - { - base.HandleNonHttpsRequest(filterContext); - } - } - - /// - /// Check to see if HTTPS is currently being used if Umbraco.Core.UseHttps is true. - /// - /// Filter context - public override void OnAuthorization(AuthorizationContext filterContext) - { - // If umbracoSSL is set, let base method handle checking for HTTPS. Otherwise, we don't care. - if (/*Current.Configs.Global().UseHttps*/ false) - { - base.OnAuthorization(filterContext); - } - } - - - } -} diff --git a/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs b/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs deleted file mode 100644 index 43dc341655..0000000000 --- a/src/Umbraco.Web/Mvc/UmbracoViewPageOfTModel.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System; -using System.Text; -using System.Web; -using System.Web.Mvc; -using System.Web.WebPages; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.Models; -using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.Services; -using Umbraco.Core.Strings; -using Umbraco.Web.Models; -using Umbraco.Web.Routing; -using Umbraco.Web.Security; -using Current = Umbraco.Web.Composing.Current; - -namespace Umbraco.Web.Mvc -{ - // TODO: This has been ported to netcore, just needs testing - public abstract class UmbracoViewPage : WebViewPage - { - private readonly GlobalSettings _globalSettings; - private readonly ContentSettings _contentSettings; - - private IUmbracoContext _umbracoContext; - private UmbracoHelper _helper; - - /// - /// Gets or sets the database context. - /// - public ServiceContext Services { get; set; } - - /// - /// Gets or sets the application cache. - /// - public AppCaches AppCaches { get; set; } - - // TODO: previously, Services and ApplicationCache would derive from UmbracoContext.Application, which - // was an ApplicationContext - so that everything derived from UmbracoContext. - // UmbracoContext is fetched from the data tokens, thus allowing the view to be rendered with a - // custom context and NOT the Current.UmbracoContext - eg outside the normal Umbraco routing - // process. - // leaving it as-it for the time being but really - the UmbracoContext should be injected just - // like the Services & ApplicationCache properties, and have a setter for those special weird - // cases. - - // TODO: Can be injected to the view in netcore, else injected to the base model - // public IUmbracoContext UmbracoContext => _umbracoContext - // ?? (_umbracoContext = ViewContext.GetUmbracoContext() ?? Current.UmbracoContext); - - /// - /// Gets the public content request. - /// - internal IPublishedRequest PublishedRequest - { - get - { - // TODO: we only have one data token for a route now: Constants.Web.UmbracoRouteDefinitionDataToken - - throw new NotImplementedException("Probably needs to be ported to netcore"); - - //// we should always try to return the object from the data tokens just in case its a custom object and not - //// the one from UmbracoContext. Fallback to UmbracoContext if necessary. - - //// try view context - //if (ViewContext.RouteData.DataTokens.ContainsKey(Constants.Web.UmbracoRouteDefinitionDataToken)) - //{ - // return (IPublishedRequest) ViewContext.RouteData.DataTokens.GetRequiredObject(Constants.Web.UmbracoRouteDefinitionDataToken); - //} - - //// child action, try parent view context - //if (ViewContext.IsChildAction && ViewContext.ParentActionViewContext.RouteData.DataTokens.ContainsKey(Constants.Web.UmbracoRouteDefinitionDataToken)) - //{ - // return (IPublishedRequest) ViewContext.ParentActionViewContext.RouteData.DataTokens.GetRequiredObject(Constants.Web.UmbracoRouteDefinitionDataToken); - //} - - //// fallback to UmbracoContext - //return UmbracoContext.PublishedRequest; - } - } - - /// - /// Gets the Umbraco helper. - /// - public UmbracoHelper Umbraco - { - get - { - if (_helper != null) return _helper; - - var model = ViewData.Model; - var content = model as IPublishedContent; - if (content == null && model is IContentModel) - content = ((IContentModel) model).Content; - - _helper = Current.UmbracoHelper; - - if (content != null) - _helper.AssignedContentItem = content; - - return _helper; - } - } - - protected UmbracoViewPage() - : this( - Current.Factory.GetRequiredService(), - Current.Factory.GetRequiredService(), - Current.Factory.GetRequiredService>(), - Current.Factory.GetRequiredService>() - ) - { - } - - protected UmbracoViewPage(ServiceContext services, AppCaches appCaches, IOptions globalSettings, IOptions contentSettings) - { - if (globalSettings == null) throw new ArgumentNullException(nameof(globalSettings)); - if (contentSettings == null) throw new ArgumentNullException(nameof(contentSettings)); - Services = services; - AppCaches = appCaches; - _globalSettings = globalSettings.Value; - _contentSettings = contentSettings.Value; - } - - } -} diff --git a/src/Umbraco.Web/Mvc/ValidateUmbracoFormRouteStringAttribute.cs b/src/Umbraco.Web/Mvc/ValidateUmbracoFormRouteStringAttribute.cs deleted file mode 100644 index 7ee44a6abc..0000000000 --- a/src/Umbraco.Web/Mvc/ValidateUmbracoFormRouteStringAttribute.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Web.Mvc; -using Umbraco.Core; - -namespace Umbraco.Web.Mvc -{ - /// - /// 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 BeginUmbracoForm. 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 - { - /// - /// 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"]; - 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 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].InvariantEquals(currentController) || - !additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Action].InvariantEquals(currentAction) || - (!additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Area].IsNullOrWhiteSpace() && !additionalDataParts[RenderRouteHandler.ReservedAdditionalKeys.Area].InvariantEquals(currentArea))) - { - throw new HttpUmbracoFormRouteStringException("The provided Umbraco form request route string was meant for a different controller and action."); - } - - } - } -} diff --git a/src/Umbraco.Web/Mvc/ViewDataContainerExtensions.cs b/src/Umbraco.Web/Mvc/ViewDataContainerExtensions.cs deleted file mode 100644 index 2852e80619..0000000000 --- a/src/Umbraco.Web/Mvc/ViewDataContainerExtensions.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Web.Mvc; - -namespace Umbraco.Web.Mvc -{ - internal static class ViewDataContainerExtensions - { - private class ViewDataContainer : IViewDataContainer - { - public ViewDataContainer() - { - ViewData = new ViewDataDictionary(); - } - public ViewDataDictionary ViewData { get; set; } - } - - /// - /// Creates a new IViewDataContainer but with a filtered ModelState - /// - /// - /// - /// - public static IViewDataContainer FilterContainer(this IViewDataContainer container, string prefix) - { - var newContainer = new ViewDataContainer(); - newContainer.ViewData.ModelState.Merge(container.ViewData.ModelState, prefix); - //change the HTML field name too - newContainer.ViewData.TemplateInfo.HtmlFieldPrefix = prefix; - return newContainer; - } - - /// - /// Returns a new IViewContainer based on the current one but supplies a different model to the ViewData - /// - /// - /// - /// - public static IViewDataContainer CopyWithModel(this IViewDataContainer container, object model) - { - return new ViewDataContainer - { - ViewData = new ViewDataDictionary(container.ViewData) - { - Model = model - } - }; - } - - } -} diff --git a/src/Umbraco.Web/Runtime/WebInitialComponent.cs b/src/Umbraco.Web/Runtime/WebInitialComponent.cs deleted file mode 100644 index c11d648b37..0000000000 --- a/src/Umbraco.Web/Runtime/WebInitialComponent.cs +++ /dev/null @@ -1,197 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web.Http; -using System.Web.Http.Dispatcher; -using System.Web.Mvc; -using System.Web.Routing; -using Umbraco.Core; -using Umbraco.Core.Composing; -using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.Models; -using Umbraco.Core.Hosting; -using Umbraco.Core.Strings; -using Umbraco.Web.Mvc; -using Umbraco.Web.WebApi; -using Constants = Umbraco.Core.Constants; -using Current = Umbraco.Web.Composing.Current; - -namespace Umbraco.Web.Runtime -{ - public sealed class WebInitialComponent : IComponent - { - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly UmbracoApiControllerTypeCollection _apiControllerTypes; - private readonly GlobalSettings _globalSettings; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly IShortStringHelper _shortStringHelper; - - public WebInitialComponent( - IUmbracoContextAccessor umbracoContextAccessor, - UmbracoApiControllerTypeCollection apiControllerTypes, - GlobalSettings globalSettings, - IHostingEnvironment hostingEnvironment, - IShortStringHelper shortStringHelper) - { - _umbracoContextAccessor = umbracoContextAccessor; - _apiControllerTypes = apiControllerTypes; - _globalSettings = globalSettings; - _hostingEnvironment = hostingEnvironment; - _shortStringHelper = shortStringHelper; - } - - public void Initialize() - { - // setup mvc and webapi services - SetupMvcAndWebApi(); - - // Disable the X-AspNetMvc-Version HTTP Header - MvcHandler.DisableMvcResponseHeader = true; - - // wrap view engines in the profiling engine - WrapViewEngines(ViewEngines.Engines); - - // add global filters - ConfigureGlobalFilters(); - - // set routes - CreateRoutes(_umbracoContextAccessor, _globalSettings, _shortStringHelper, _apiControllerTypes, _hostingEnvironment); - } - - public void Terminate() - { } - - private static void ConfigureGlobalFilters() - { - //GlobalFilters.Filters.Add(new EnsurePartialViewMacroViewContextFilterAttribute()); - } - - // internal for tests - internal static void WrapViewEngines(IList viewEngines) - { - if (viewEngines == null || viewEngines.Count == 0) return; - - var originalEngines = viewEngines.ToList(); - viewEngines.Clear(); - foreach (var engine in originalEngines) - { - var wrappedEngine = engine;// TODO introduce in NETCORE: is ProfilingViewEngine ? engine : new ProfilingViewEngine(engine); - viewEngines.Add(wrappedEngine); - } - } - - private void SetupMvcAndWebApi() - { - //don't output the MVC version header (security) - //MvcHandler.DisableMvcResponseHeader = true; - - // set master controller factory - // var controllerFactory = new MasterControllerFactory(() => Current.FilteredControllerFactories); - // ControllerBuilder.Current.SetControllerFactory(controllerFactory); - - // set the render & plugin view engines - // ViewEngines.Engines.Add(new RenderViewEngine(_hostingEnvironment)); - // ViewEngines.Engines.Add(new PluginViewEngine()); - - ////add the profiling action filter - //GlobalFilters.Filters.Add(new ProfilingActionFilter()); - - GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerSelector), - new NamespaceHttpControllerSelector(GlobalConfiguration.Configuration)); - } - - // internal for tests - internal static void CreateRoutes( - IUmbracoContextAccessor umbracoContextAccessor, - GlobalSettings globalSettings, - IShortStringHelper shortStringHelper, - UmbracoApiControllerTypeCollection apiControllerTypes, - IHostingEnvironment hostingEnvironment) - { - var umbracoPath = globalSettings.GetUmbracoMvcArea(hostingEnvironment); - - // create the front-end route - var defaultRoute = RouteTable.Routes.MapRoute( - "Umbraco_default", - umbracoPath + "/RenderMvc/{action}/{id}", - new { controller = "RenderMvc", action = "Index", id = UrlParameter.Optional } - ); - defaultRoute.RouteHandler = new RenderRouteHandler(umbracoContextAccessor, ControllerBuilder.Current.GetControllerFactory(), shortStringHelper); - - // register install routes - // RouteTable.Routes.RegisterArea(); - - // register all back office routes - // RouteTable.Routes.RegisterArea(new BackOfficeArea(globalSettings, hostingEnvironment)); - - // plugin controllers must come first because the next route will catch many things - RoutePluginControllers(globalSettings, apiControllerTypes, hostingEnvironment); - } - - private static void RoutePluginControllers( - GlobalSettings globalSettings, - UmbracoApiControllerTypeCollection apiControllerTypes, - IHostingEnvironment hostingEnvironment) - { - var umbracoPath = globalSettings.GetUmbracoMvcArea(hostingEnvironment); - - // need to find the plugin controllers and route them - var pluginControllers = apiControllerTypes; //TODO was: surfaceControllerTypes.Concat(apiControllerTypes).ToArray(); - - // local controllers do not contain the attribute - var localControllers = pluginControllers.Where(x => PluginController.GetMetadata(x).AreaName.IsNullOrWhiteSpace()); - foreach (var s in localControllers) - { - if (TypeHelper.IsTypeAssignableFrom(s)) - RouteLocalSurfaceController(s, umbracoPath); - else if (TypeHelper.IsTypeAssignableFrom(s)) - RouteLocalApiController(s, umbracoPath); - } - - // get the plugin controllers that are unique to each area (group by) - var pluginSurfaceControlleres = pluginControllers.Where(x => PluginController.GetMetadata(x).AreaName.IsNullOrWhiteSpace() == false); - var groupedAreas = pluginSurfaceControlleres.GroupBy(controller => PluginController.GetMetadata(controller).AreaName); - // loop through each area defined amongst the controllers - foreach (var g in groupedAreas) - { - // create & register an area for the controllers (this will throw an exception if all controllers are not in the same area) - var pluginControllerArea = new PluginControllerArea(globalSettings, hostingEnvironment, g.Select(PluginController.GetMetadata)); - RouteTable.Routes.RegisterArea(pluginControllerArea); - } - } - - private static void RouteLocalApiController(Type controller, string umbracoPath) - { - var meta = PluginController.GetMetadata(controller); - var url = umbracoPath + (meta.IsBackOffice ? "/BackOffice" : "") + "/Api/" + meta.ControllerName + "/{action}/{id}"; - var route = RouteTable.Routes.MapHttpRoute( - $"umbraco-api-{meta.ControllerName}", - url, // URL to match - new { controller = meta.ControllerName, id = UrlParameter.Optional }, - new[] { meta.ControllerNamespace }); - if (route.DataTokens == null) // web api routes don't set the data tokens object - route.DataTokens = new RouteValueDictionary(); - - // TODO: Pretty sure this is not necessary, we'll see - //route.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, "api"); //ensure the umbraco token is set - } - - private static void RouteLocalSurfaceController(Type controller, string umbracoPath) - { - var meta = PluginController.GetMetadata(controller); - var url = umbracoPath + "/Surface/" + meta.ControllerName + "/{action}/{id}"; - var route = RouteTable.Routes.MapRoute( - $"umbraco-surface-{meta.ControllerName}", - url, // URL to match - new { controller = meta.ControllerName, action = "Index", id = UrlParameter.Optional }, - new[] { meta.ControllerNamespace }); // look in this namespace to create the controller - - // TODO: Pretty sure this is not necessary, we'll see - //route.DataTokens.Add(Core.Constants.Web.UmbracoDataToken, "surface"); // ensure the umbraco token is set - - route.DataTokens.Add("UseNamespaceFallback", false); // don't look anywhere else except this namespace! - // make it use our custom/special SurfaceMvcHandler - route.RouteHandler = new SurfaceRouteHandler(); - } - } -} diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs deleted file mode 100644 index d4e989854f..0000000000 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System.Web.Security; -using Microsoft.Extensions.DependencyInjection; -using Umbraco.Core; -using Umbraco.Core.DependencyInjection; -using Umbraco.Core.Composing; -using Umbraco.Core.Dictionary; -using Umbraco.Core.Templates; -using Umbraco.Core.Security; -using Umbraco.Core.Services; -using Umbraco.Web.Macros; -using Umbraco.Web.PublishedCache; -using Umbraco.Web.Security; -using Umbraco.Web.Security.Providers; - -namespace Umbraco.Web.Runtime -{ - [ComposeBefore(typeof(ICoreComposer))] - public sealed class WebInitialComposer : ComponentComposer - { - public override void Compose(IUmbracoBuilder builder) - { - base.Compose(builder); - - builder.Services.AddTransient(); - - - // register membership stuff - builder.Services.AddTransient(factory => MembershipProviderExtensions.GetMembersMembershipProvider()); - builder.Services.AddTransient(factory => Roles.Enabled ? Roles.Provider : new MembersRoleProvider(factory.GetRequiredService())); - builder.Services.AddScoped(); - builder.Services.AddTransient(factory => factory.GetRequiredService().PublishedSnapshot.Members); - builder.Services.AddUnique(); - builder.Services.AddUnique(); - - builder.Services.AddTransient(factory => - { - var state = factory.GetRequiredService(); - - if (state.Level == RuntimeLevel.Run) - { - var umbCtx = factory.GetRequiredService(); - return new UmbracoHelper(umbCtx.IsFrontEndUmbracoRequest() ? umbCtx.PublishedRequest?.PublishedContent : null, factory.GetRequiredService(), - factory.GetRequiredService(), factory.GetRequiredService(), factory.GetRequiredService()); - } - - return new UmbracoHelper(); - }); - - // configure the container for web - //composition.ConfigureForWeb(); - - //composition - /* TODO: This will depend on if we use ServiceBasedControllerActivator - see notes in Startup.cs - * You will likely need to set DefaultRenderMvcControllerType on Umbraco.Web.Composing.Current - * which is what the extension method below did previously. - */ - //.ComposeUmbracoControllers(GetType().Assembly) - //.SetDefaultRenderMvcController(); // default controller for template views - - //we need to eagerly scan controller types since they will need to be routed - // composition.WithCollectionBuilder() - // .Add(composition.TypeLoader.GetSurfaceControllers()); - - - // auto-register views - //composition.RegisterAuto(typeof(UmbracoViewPage<>)); - } - } -} - diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index f5445a4bc9..8b4d742aeb 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -131,23 +131,16 @@ - - - - - - - @@ -163,7 +156,6 @@ - @@ -176,17 +168,10 @@ - - - - - - - @@ -199,10 +184,6 @@ - - - - @@ -211,31 +192,20 @@ - - - - - - True True Strings.resx - - - - - @@ -245,7 +215,6 @@ Code - Component @@ -264,8 +233,6 @@ Mvc\web.config - - - + \ No newline at end of file diff --git a/src/Umbraco.Web/UmbracoInjectedModule.cs b/src/Umbraco.Web/UmbracoInjectedModule.cs deleted file mode 100644 index b50f4ce23e..0000000000 --- a/src/Umbraco.Web/UmbracoInjectedModule.cs +++ /dev/null @@ -1,243 +0,0 @@ -using System; -using System.Web; -using System.Web.Routing; -using Microsoft.Extensions.Logging; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.Models; -using Umbraco.Core.Exceptions; -using Umbraco.Core.Hosting; -using Umbraco.Core.Security; -using Umbraco.Web.Composing; -using Umbraco.Web.Routing; - -namespace Umbraco.Web -{ - // notes - // - // also look at IOHelper.ResolveUrlsFromTextString - nightmarish?! - // - // context.RewritePath supports ~/ or else must begin with /vdir - // Request.RawUrl is still there - // response.Redirect does?! always remap to /vdir?! - - /// - /// Represents the main Umbraco module. - /// - /// - /// Is registered by the . - /// Do *not* try to use that one as a module in web.config. - /// - public class UmbracoInjectedModule : IHttpModule - { - private readonly IRuntimeState _runtime; - private readonly ILogger _logger; - private readonly IUmbracoContextFactory _umbracoContextFactory; - private readonly GlobalSettings _globalSettings; - private readonly IHostingEnvironment _hostingEnvironment; - - public UmbracoInjectedModule( - IRuntimeState runtime, - ILogger logger, - IUmbracoContextFactory umbracoContextFactory, - GlobalSettings globalSettings, - IHostingEnvironment hostingEnvironment) - { - _runtime = runtime; - _logger = logger; - _umbracoContextFactory = umbracoContextFactory; - _globalSettings = globalSettings; - _hostingEnvironment = hostingEnvironment; - } - - /// - /// Begins to process a request. - /// - private void BeginRequest(HttpContextBase httpContext) - { - // write the trace output for diagnostics at the end of the request - httpContext.Trace.Write("UmbracoModule", "Umbraco request begins"); - - // ok, process - - // TODO: should we move this to after we've ensured we are processing a routable page? - // ensure there's an UmbracoContext registered for the current request - // registers the context reference so its disposed at end of request - var umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext(); - httpContext.DisposeOnPipelineCompleted(umbracoContextReference); - } - - /// - /// Processes the Umbraco Request - /// - /// - /// This will check if we are trying to route to the default back office page (i.e. ~/Umbraco/ or ~/Umbraco or ~/Umbraco/Default ) - /// and ensure that the MVC handler executes for that. This is required because the route for /Umbraco will never execute because - /// files/folders exist there and we cannot set the RouteCollection.RouteExistingFiles = true since that will muck a lot of other things up. - /// So we handle it here and explicitly execute the MVC controller. - /// - void ProcessRequest(HttpContextBase httpContext) - { - - var umbracoContext = Current.UmbracoContext; - - // do not process if this request is not a front-end routable page - var isRoutableAttempt = EnsureUmbracoRoutablePage(umbracoContext, httpContext); - - // raise event here - UmbracoModule.OnRouteAttempt(this, new RoutableAttemptEventArgs(isRoutableAttempt.Result, umbracoContext)); - if (isRoutableAttempt.Success == false) return; - } - - /// - /// Checks the current request and ensures that it is routable based on the structure of the request and URI - /// - internal Attempt EnsureUmbracoRoutablePage(IUmbracoContext context, HttpContextBase httpContext) - { - var uri = context.OriginalRequestUrl; - - var reason = EnsureRoutableOutcome.IsRoutable; - - //// ensure this is a document request - //if (!_routableDocumentLookup.IsDocumentRequest(httpContext, context.OriginalRequestUrl)) - //{ - // reason = EnsureRoutableOutcome.NotDocumentRequest; - //} - - // ensure the runtime is in the proper state - // and deal with needed redirects, etc - if (!EnsureRuntime(httpContext, uri)) - { - reason = EnsureRoutableOutcome.NotReady; - } - // ensure Umbraco has documents to serve - else if (!EnsureHasContent(context, httpContext)) - { - reason = EnsureRoutableOutcome.NoContent; - } - - return Attempt.If(reason == EnsureRoutableOutcome.IsRoutable, reason); - } - - // TODO: Where should this execute in netcore? This will have to be a middleware - // executing before UseRouting so that it is done before any endpoint routing takes place. - private bool EnsureRuntime(HttpContextBase httpContext, Uri uri) - { - var level = _runtime.Level; - switch (level) - { - // we should never handle Unknown nor Boot: the runtime boots in Application_Start - // and as long as it has not booted, no request other than the initial request is - // going to be served (see https://stackoverflow.com/a/21402100) - // we should never handle BootFailed: if boot failed, the pipeline should not run - // at all - case RuntimeLevel.Unknown: - case RuntimeLevel.Boot: - case RuntimeLevel.BootFailed: - throw new PanicException($"Unexpected runtime level: {level}."); - - case RuntimeLevel.Run: - // ok - return true; - - case RuntimeLevel.Install: - case RuntimeLevel.Upgrade: - - // NOTE: We have moved the logic that was here to netcore already - - return false; // cannot serve content - - default: - throw new NotSupportedException($"Unexpected runtime level: {level}."); - } - } - - // ensures Umbraco has at least one published node - // if not, rewrites to splash and return false - // if yes, return true - private bool EnsureHasContent(IUmbracoContext context, HttpContextBase httpContext) - { - if (context.Content.HasContent()) - return true; - - _logger.LogWarning("Umbraco has no content"); - - if (RouteTable.Routes[Constants.Web.NoContentRouteName] is Route route) - { - httpContext.RewritePath(route.Url); - } - - return false; - } - - /// - /// Rewrites to the default back office page. - /// - /// - private void RewriteToBackOfficeHandler(HttpContextBase context) - { - // GlobalSettings.Path has already been through IOHelper.ResolveUrl() so it begins with / and vdir (if any) - var rewritePath = _globalSettings.GetBackOfficePath(_hostingEnvironment).TrimEnd('/') + "/Default"; - // rewrite the path to the path of the handler (i.e. /umbraco/RenderMvc) - context.RewritePath(rewritePath, "", "", false); - - //if it is MVC we need to do something special, we are not using TransferRequest as this will - //require us to rewrite the path with query strings and then re-parse the query strings, this would - //also mean that we need to handle IIS 7 vs pre-IIS 7 differently. Instead we are just going to create - //an instance of the UrlRoutingModule and call it's PostResolveRequestCache method. This does: - // * Looks up the route based on the new rewritten URL - // * Creates the RequestContext with all route parameters and then executes the correct handler that matches the route - //we also cannot re-create this functionality because the setter for the HttpContext.Request.RequestContext is internal - //so really, this is pretty much the only way without using Server.TransferRequest and if we did that, we'd have to rethink - //a bunch of things! - var urlRouting = new UrlRoutingModule(); - urlRouting.PostResolveRequestCache(context); - } - - - - #region IHttpModule - - /// - /// Initialize the module, this will trigger for each new application - /// and there may be more than 1 application per application domain - /// - /// - public void Init(HttpApplication app) - { - app.BeginRequest += (sender, e) => - { - var httpContext = ((HttpApplication) sender).Context; - - BeginRequest(new HttpContextWrapper(httpContext)); - }; - - app.PostAuthenticateRequest += (sender, e) => - { - var httpContext = ((HttpApplication) sender).Context; - //ensure the thread culture is set - httpContext.User?.Identity?.EnsureCulture(); - }; - - app.PostResolveRequestCache += (sender, e) => - { - var httpContext = ((HttpApplication) sender).Context; - ProcessRequest(new HttpContextWrapper(httpContext)); - }; - - app.EndRequest += (sender, args) => - { - var httpContext = ((HttpApplication) sender).Context; - - UmbracoModule.OnEndRequest(this, new UmbracoRequestEventArgs(Current.UmbracoContext)); - }; - } - - public void Dispose() - { } - - #endregion - - - } -} diff --git a/src/Umbraco.Web/UmbracoModule.cs b/src/Umbraco.Web/UmbracoModule.cs deleted file mode 100644 index 2f9c6d518a..0000000000 --- a/src/Umbraco.Web/UmbracoModule.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Web; -using Microsoft.Extensions.Logging; -using Umbraco.Core; -using Umbraco.Web.Composing; -using Umbraco.Web.Routing; - -namespace Umbraco.Web -{ - /// - /// Represents the main Umbraco module. - /// - /// - /// Register that one in web.config. - /// It will inject which contains most of the actual code. - /// - public class UmbracoModule : ModuleInjector - { - /// - /// Occurs when... - /// - internal static event EventHandler RouteAttempt; - - /// - /// Occurs when... - /// - public static event EventHandler EndRequest; - - /// - /// Triggers the RouteAttempt event. - /// - internal static void OnRouteAttempt(object sender, RoutableAttemptEventArgs args) - { - RouteAttempt?.Invoke(sender, args); - } - - /// - /// Triggers the EndRequest event. - /// - internal static void OnEndRequest(object sender, UmbracoRequestEventArgs args) - { - EndRequest?.Invoke(sender, args); - } - } -} diff --git a/src/Umbraco.Web/WebApi/NamespaceHttpControllerSelector.cs b/src/Umbraco.Web/WebApi/NamespaceHttpControllerSelector.cs index d27ec6e91f..b9539a0520 100644 --- a/src/Umbraco.Web/WebApi/NamespaceHttpControllerSelector.cs +++ b/src/Umbraco.Web/WebApi/NamespaceHttpControllerSelector.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; @@ -7,12 +7,13 @@ using System.Net.Http; using System.Web.Http; using System.Web.Http.Controllers; using System.Web.Http.Dispatcher; +using static Umbraco.Core.Constants.Web.Routing; namespace Umbraco.Web.WebApi { public class NamespaceHttpControllerSelector : DefaultHttpControllerSelector { - private const string ControllerKey = "controller"; + private const string ControllerKey = ControllerToken; private readonly HttpConfiguration _configuration; private readonly Lazy> _duplicateControllerTypes;