diff --git a/src/Umbraco.Core/Exceptions/HttpResponseException.cs b/src/Umbraco.Core/Exceptions/HttpResponseException.cs new file mode 100644 index 0000000000..e0db712f40 --- /dev/null +++ b/src/Umbraco.Core/Exceptions/HttpResponseException.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Runtime.Serialization; + +namespace Umbraco.Core.Exceptions +{ + [Serializable] + public class HttpResponseException : Exception + { + public HttpResponseException(HttpStatusCode status = HttpStatusCode.InternalServerError, object value = null) + { + Status = status; + Value = value; + } + + public HttpStatusCode Status { get; set; } + public object Value { get; set; } + + public IDictionary AdditionalHeaders { get; } = new Dictionary(); + + + /// + /// When overridden in a derived class, sets the with information about the exception. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. + /// info + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) + { + throw new ArgumentNullException(nameof(info)); + } + + info.AddValue(nameof(Status), Enum.GetName(typeof(HttpStatusCode), Status)); + info.AddValue(nameof(Value), Value); + info.AddValue(nameof(AdditionalHeaders), AdditionalHeaders); + + base.GetObjectData(info, context); + } + } +} diff --git a/src/Umbraco.Core/Features/UmbracoFeatures.cs b/src/Umbraco.Core/Features/UmbracoFeatures.cs index 69fe58f76d..c25c98fbb7 100644 --- a/src/Umbraco.Core/Features/UmbracoFeatures.cs +++ b/src/Umbraco.Core/Features/UmbracoFeatures.cs @@ -29,7 +29,7 @@ namespace Umbraco.Web.Features /// /// Determines whether a controller is enabled. /// - internal bool IsControllerEnabled(Type feature) + public bool IsControllerEnabled(Type feature) { if (typeof(IUmbracoFeature).IsAssignableFrom(feature)) return Disabled.Controllers.Contains(feature) == false; diff --git a/src/Umbraco.Core/Install/InstallSteps/FilePermissionsStep.cs b/src/Umbraco.Core/Install/InstallSteps/FilePermissionsStep.cs index c3d7493084..152c5a831f 100644 --- a/src/Umbraco.Core/Install/InstallSteps/FilePermissionsStep.cs +++ b/src/Umbraco.Core/Install/InstallSteps/FilePermissionsStep.cs @@ -11,7 +11,7 @@ namespace Umbraco.Web.Install.InstallSteps [InstallSetupStep(InstallationType.NewInstall | InstallationType.Upgrade, "Permissions", 0, "", PerformsAppRestart = true)] - internal class FilePermissionsStep : InstallSetupStep + public class FilePermissionsStep : InstallSetupStep { private readonly IFilePermissionHelper _filePermissionHelper; public FilePermissionsStep(IFilePermissionHelper filePermissionHelper) diff --git a/src/Umbraco.Core/Install/InstallSteps/UpgradeStep.cs b/src/Umbraco.Core/Install/InstallSteps/UpgradeStep.cs index 0979f31dc5..3264bb152c 100644 --- a/src/Umbraco.Core/Install/InstallSteps/UpgradeStep.cs +++ b/src/Umbraco.Core/Install/InstallSteps/UpgradeStep.cs @@ -10,7 +10,7 @@ namespace Umbraco.Web.Install.InstallSteps /// This step is purely here to show the button to commence the upgrade /// [InstallSetupStep(InstallationType.Upgrade, "Upgrade", "upgrade", 1, "Upgrading Umbraco to the latest and greatest version.")] - internal class UpgradeStep : InstallSetupStep + public class UpgradeStep : InstallSetupStep { public override bool RequiresExecution(object model) => true; private readonly IUmbracoVersion _umbracoVersion; diff --git a/src/Umbraco.Web/Mvc/PluginControllerMetadata.cs b/src/Umbraco.Core/Models/PluginControllerMetadata.cs similarity index 56% rename from src/Umbraco.Web/Mvc/PluginControllerMetadata.cs rename to src/Umbraco.Core/Models/PluginControllerMetadata.cs index abd755ddf8..b8750d6998 100644 --- a/src/Umbraco.Web/Mvc/PluginControllerMetadata.cs +++ b/src/Umbraco.Core/Models/PluginControllerMetadata.cs @@ -5,17 +5,17 @@ namespace Umbraco.Web.Mvc /// /// Represents some metadata about the controller /// - internal class PluginControllerMetadata + public class PluginControllerMetadata { - internal Type ControllerType { get; set; } - internal string ControllerName { get; set; } - internal string ControllerNamespace { get; set; } - internal string AreaName { get; set; } + public Type ControllerType { get; set; } + public string ControllerName { get; set; } + public string ControllerNamespace { get; set; } + public string AreaName { get; set; } /// /// This is determined by another attribute [IsBackOffice] which slightly modifies the route path /// allowing us to determine if it is indeed a back office request or not /// - internal bool IsBackOffice { get; set; } + public bool IsBackOffice { get; set; } } } diff --git a/src/Umbraco.Core/UmbracoContextReference.cs b/src/Umbraco.Core/UmbracoContextReference.cs index bd021494e6..96404dc1ba 100644 --- a/src/Umbraco.Core/UmbracoContextReference.cs +++ b/src/Umbraco.Core/UmbracoContextReference.cs @@ -20,7 +20,7 @@ namespace Umbraco.Web /// /// Initializes a new instance of the class. /// - internal UmbracoContextReference(IUmbracoContext umbracoContext, bool isRoot, IUmbracoContextAccessor umbracoContextAccessor) + public UmbracoContextReference(IUmbracoContext umbracoContext, bool isRoot, IUmbracoContextAccessor umbracoContextAccessor) { IsRoot = isRoot; diff --git a/src/Umbraco.Core/UriExtensions.cs b/src/Umbraco.Core/UriExtensions.cs index a8aa6f8c91..00cdb8fe48 100644 --- a/src/Umbraco.Core/UriExtensions.cs +++ b/src/Umbraco.Core/UriExtensions.cs @@ -22,26 +22,26 @@ namespace Umbraco.Core /// /// /// There are some special routes we need to check to properly determine this: - /// + /// /// If any route has an extension in the path like .aspx = back office - /// + /// /// These are def back office: /// /Umbraco/BackOffice = back office /// /Umbraco/Preview = back office /// If it's not any of the above, and there's no extension then we cannot determine if it's back office or front-end /// so we can only assume that it is not back office. This will occur if people use an UmbracoApiController for the backoffice /// but do not inherit from UmbracoAuthorizedApiController and do not use [IsBackOffice] attribute. - /// + /// /// These are def front-end: /// /Umbraco/Surface = front-end /// /Umbraco/Api = front-end /// But if we've got this far we'll just have to assume it's front-end anyways. - /// + /// /// - internal static bool IsBackOfficeRequest(this Uri url, IGlobalSettings globalSettings, IHostingEnvironment hostingEnvironment) + public static bool IsBackOfficeRequest(this Uri url, IGlobalSettings globalSettings, IHostingEnvironment hostingEnvironment) { var applicationPath = hostingEnvironment.ApplicationVirtualPath; - + var fullUrlPath = url.AbsolutePath.TrimStart(new[] {'/'}); var appPath = applicationPath.TrimStart(new[] {'/'}); var urlPath = fullUrlPath.TrimStart(appPath).EnsureStartsWith('/'); diff --git a/src/Umbraco.Core/Web/IRequestAccessor.cs b/src/Umbraco.Core/Web/IRequestAccessor.cs index 60f8ccbc40..8a92b6ee22 100644 --- a/src/Umbraco.Core/Web/IRequestAccessor.cs +++ b/src/Umbraco.Core/Web/IRequestAccessor.cs @@ -6,7 +6,7 @@ namespace Umbraco.Web public interface IRequestAccessor { string GetRequestValue(string name); - string GetQueryStringValue(string culture); + string GetQueryStringValue(string name); event EventHandler EndRequest; event EventHandler RouteAttempt; } diff --git a/src/Umbraco.Web/Composing/CompositionExtensions/Installer.cs b/src/Umbraco.Infrastructure/Composing/CompositionExtensions/Installer.cs similarity index 71% rename from src/Umbraco.Web/Composing/CompositionExtensions/Installer.cs rename to src/Umbraco.Infrastructure/Composing/CompositionExtensions/Installer.cs index 64f91939a7..45e3b12582 100644 --- a/src/Umbraco.Web/Composing/CompositionExtensions/Installer.cs +++ b/src/Umbraco.Infrastructure/Composing/CompositionExtensions/Installer.cs @@ -2,6 +2,7 @@ using Umbraco.Core.Composing; using Umbraco.Web.Install; using Umbraco.Web.Install.InstallSteps; +using Umbraco.Web.Install.Models; namespace Umbraco.Web.Composing.CompositionExtensions { @@ -19,11 +20,11 @@ namespace Umbraco.Web.Composing.CompositionExtensions composition.Register(Lifetime.Scope); // TODO: Add these back once we have a compatible Starter kit - // composition.Register(Lifetime.Scope); - // composition.Register(Lifetime.Scope); - // composition.Register(Lifetime.Scope); + // composition.Register(Lifetime.Scope); + // composition.Register(Lifetime.Scope); + // composition.Register(Lifetime.Scope); - composition.Register(Lifetime.Scope); + composition.Register(Lifetime.Scope); composition.Register(); composition.Register(); diff --git a/src/Umbraco.Infrastructure/Composing/UmbracoServiceProviderFactory.cs b/src/Umbraco.Infrastructure/Composing/UmbracoServiceProviderFactory.cs index 6fd0bda61e..b8fad4828c 100644 --- a/src/Umbraco.Infrastructure/Composing/UmbracoServiceProviderFactory.cs +++ b/src/Umbraco.Infrastructure/Composing/UmbracoServiceProviderFactory.cs @@ -13,7 +13,7 @@ namespace Umbraco.Core.Composing public class UmbracoServiceProviderFactory : IServiceProviderFactory { public UmbracoServiceProviderFactory(ServiceContainer container) - { + { _container = new LightInjectContainer(container); } @@ -21,7 +21,11 @@ namespace Umbraco.Core.Composing /// Creates an ASP.NET Core compatible service container /// /// - public static ServiceContainer CreateServiceContainer() => new ServiceContainer(ContainerOptions.Default.Clone().WithMicrosoftSettings().WithAspNetCoreSettings()); + public static ServiceContainer CreateServiceContainer() => new ServiceContainer( + ContainerOptions.Default.Clone() + .WithMicrosoftSettings() + //.WithAspNetCoreSettings() //TODO WithAspNetCoreSettings changes behavior that we need to discuss + ); /// /// Default ctor for use in Host Builder configuration @@ -59,7 +63,7 @@ namespace Umbraco.Core.Composing /// public IServiceContainer CreateBuilder(IServiceCollection services) { - _services = services; + _services = services; return _container.Container; } diff --git a/src/Umbraco.Web/Install/FilePermissionHelper.cs b/src/Umbraco.Infrastructure/Intall/FilePermissionHelper.cs similarity index 89% rename from src/Umbraco.Web/Install/FilePermissionHelper.cs rename to src/Umbraco.Infrastructure/Intall/FilePermissionHelper.cs index 24649b4391..4c3edf0a1b 100644 --- a/src/Umbraco.Web/Install/FilePermissionHelper.cs +++ b/src/Umbraco.Infrastructure/Intall/FilePermissionHelper.cs @@ -36,23 +36,20 @@ namespace Umbraco.Web.Install { report = new Dictionary>(); - using (ChangesMonitor.Suspended()) // hack: ensure this does not trigger a restart - { - if (EnsureDirectories(_permissionDirs, out var errors) == false) - report["Folder creation failed"] = errors.ToList(); + if (EnsureDirectories(_permissionDirs, out var errors) == false) + report["Folder creation failed"] = errors.ToList(); - if (EnsureDirectories(_packagesPermissionsDirs, out errors) == false) - report["File writing for packages failed"] = errors.ToList(); + if (EnsureDirectories(_packagesPermissionsDirs, out errors) == false) + report["File writing for packages failed"] = errors.ToList(); - if (EnsureFiles(_permissionFiles, out errors) == false) - report["File writing failed"] = errors.ToList(); + if (EnsureFiles(_permissionFiles, out errors) == false) + report["File writing failed"] = errors.ToList(); - if (TestPublishedSnapshotService(out errors) == false) - report["Published snapshot environment check failed"] = errors.ToList(); + if (TestPublishedSnapshotService(out errors) == false) + report["Published snapshot environment check failed"] = errors.ToList(); - if (EnsureCanCreateSubDirectory(_globalSettings.UmbracoMediaPath, out errors) == false) - report["Media folder creation failed"] = errors.ToList(); - } + if (EnsureCanCreateSubDirectory(_globalSettings.UmbracoMediaPath, out errors) == false) + report["Media folder creation failed"] = errors.ToList(); return report.Count == 0; } @@ -191,7 +188,7 @@ namespace Umbraco.Web.Install { var writeAllow = false; var writeDeny = false; - var accessControlList = Directory.GetAccessControl(path); + var accessControlList = new DirectorySecurity(path, AccessControlSections.Access | AccessControlSections.Owner | AccessControlSections.Group); if (accessControlList == null) return false; AuthorizationRuleCollection accessRules; diff --git a/src/Umbraco.Web/Install/InstallStepCollection.cs b/src/Umbraco.Infrastructure/Intall/InstallStepCollection.cs similarity index 100% rename from src/Umbraco.Web/Install/InstallStepCollection.cs rename to src/Umbraco.Infrastructure/Intall/InstallStepCollection.cs diff --git a/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs b/src/Umbraco.Infrastructure/Intall/InstallSteps/NewInstallStep.cs similarity index 77% rename from src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs rename to src/Umbraco.Infrastructure/Intall/InstallSteps/NewInstallStep.cs index d93fd3a131..658dbfbf5a 100644 --- a/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs +++ b/src/Umbraco.Infrastructure/Intall/InstallSteps/NewInstallStep.cs @@ -3,7 +3,6 @@ using System.Collections.Specialized; using System.Net.Http; using System.Text; using System.Threading.Tasks; -using System.Web; using Newtonsoft.Json; using Umbraco.Core; using Umbraco.Core.Configuration; @@ -25,7 +24,6 @@ namespace Umbraco.Web.Install.InstallSteps [InstallSetupStep(InstallationType.NewInstall, "User", 20, "")] internal class NewInstallStep : InstallSetupStep { - private readonly IHttpContextAccessor _httpContextAccessor; private readonly IUserService _userService; private readonly DatabaseBuilder _databaseBuilder; private static HttpClient _httpClient; @@ -35,9 +33,8 @@ namespace Umbraco.Web.Install.InstallSteps private readonly IConnectionStrings _connectionStrings; private readonly ICookieManager _cookieManager; - public NewInstallStep(IHttpContextAccessor httpContextAccessor, IUserService userService, DatabaseBuilder databaseBuilder, IGlobalSettings globalSettings, IUserPasswordConfiguration passwordConfiguration, ISecuritySettings securitySettings, IConnectionStrings connectionStrings, ICookieManager cookieManager) + public NewInstallStep(IUserService userService, DatabaseBuilder databaseBuilder, IGlobalSettings globalSettings, IUserPasswordConfiguration passwordConfiguration, ISecuritySettings securitySettings, IConnectionStrings connectionStrings, ICookieManager cookieManager) { - _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); _userService = userService ?? throw new ArgumentNullException(nameof(userService)); _databaseBuilder = databaseBuilder ?? throw new ArgumentNullException(nameof(databaseBuilder)); _globalSettings = globalSettings ?? throw new ArgumentNullException(nameof(globalSettings)); @@ -55,22 +52,23 @@ namespace Umbraco.Web.Install.InstallSteps throw new InvalidOperationException("Could not find the super user!"); } - var userManager = _httpContextAccessor.GetRequiredHttpContext().GetOwinContext().GetBackOfficeUserManager(); - var membershipUser = await userManager.FindByIdAsync(Constants.Security.SuperUserId); - if (membershipUser == null) - { - throw new InvalidOperationException( - $"No user found in membership provider with id of {Constants.Security.SuperUserId}."); - } - - //To change the password here we actually need to reset it since we don't have an old one to use to change - var resetToken = await userManager.GeneratePasswordResetTokenAsync(membershipUser.Id); - var resetResult = - await userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, user.Password.Trim()); - if (!resetResult.Succeeded) - { - throw new InvalidOperationException("Could not reset password: " + string.Join(", ", resetResult.Errors)); - } + //TODO: This needs to be reintroduced, when members are compatible with ASP.NET Core Identity. + // var userManager = _httpContextAccessor.GetRequiredHttpContext().GetOwinContext().GetBackOfficeUserManager(); + // var membershipUser = await userManager.FindByIdAsync(Constants.Security.SuperUserId); + // if (membershipUser == null) + // { + // throw new InvalidOperationException( + // $"No user found in membership provider with id of {Constants.Security.SuperUserId}."); + // } + // + // //To change the password here we actually need to reset it since we don't have an old one to use to change + // var resetToken = await userManager.GeneratePasswordResetTokenAsync(membershipUser.Id); + // var resetResult = + // await userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, user.Password.Trim()); + // if (!resetResult.Succeeded) + // { + // throw new InvalidOperationException("Could not reset password: " + string.Join(", ", resetResult.Errors)); + // } admin.Email = user.Email.Trim(); admin.Name = user.Name.Trim(); diff --git a/src/Umbraco.Core/Install/Models/InstallInstructions.cs b/src/Umbraco.Infrastructure/Intall/Models/InstallInstructions.cs similarity index 100% rename from src/Umbraco.Core/Install/Models/InstallInstructions.cs rename to src/Umbraco.Infrastructure/Intall/Models/InstallInstructions.cs diff --git a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs index 1ea08e3118..c60aa57e16 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreInitialComposer.cs @@ -10,6 +10,7 @@ using Umbraco.Core.Dashboards; using Umbraco.Core.Dictionary; using Umbraco.Core.Events; using Umbraco.Core.Hosting; +using Umbraco.Core.Install; using Umbraco.Core.Logging; using Umbraco.Core.Manifest; using Umbraco.Core.Media; @@ -352,7 +353,7 @@ namespace Umbraco.Core.Runtime // register accessors for cultures composition.RegisterUnique(); - + composition.Register(Lifetime.Singleton); } diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index db3ae1bc25..177b35b27d 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -32,6 +32,7 @@ + diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj index f60b400f3f..c5732870f3 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj @@ -20,6 +20,7 @@ + diff --git a/src/Umbraco.Tests/Macros/MacroTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Macros/MacroTests.cs similarity index 90% rename from src/Umbraco.Tests/Macros/MacroTests.cs rename to src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Macros/MacroTests.cs index 6f1358e9a2..9a1b0b1dd2 100644 --- a/src/Umbraco.Tests/Macros/MacroTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Macros/MacroTests.cs @@ -1,11 +1,5 @@ -using Moq; -using NUnit.Framework; -using Umbraco.Core; +using NUnit.Framework; using Umbraco.Core.Cache; -using Umbraco.Core.Composing; -using Umbraco.Core.Logging; -using Umbraco.Core.Models; -using Umbraco.Tests.TestHelpers; using Umbraco.Web.Macros; namespace Umbraco.Tests.Macros diff --git a/src/Umbraco.Tests/Runtimes/StandaloneTests.cs b/src/Umbraco.Tests/Runtimes/StandaloneTests.cs index 1a4c7f2040..2a87fc7a43 100644 --- a/src/Umbraco.Tests/Runtimes/StandaloneTests.cs +++ b/src/Umbraco.Tests/Runtimes/StandaloneTests.cs @@ -76,7 +76,7 @@ namespace Umbraco.Tests.Runtimes var runtimeState = new RuntimeState(logger, null, umbracoVersion, backOfficeInfo); var configs = TestHelper.GetConfigs(); var variationContextAccessor = TestHelper.VariationContextAccessor; - + // create the register and the composition var register = TestHelper.GetRegister(); @@ -115,7 +115,7 @@ namespace Umbraco.Tests.Runtimes composition.RegisterUnique(); composition.RegisterUnique(); composition.RegisterUnique(); - composition.RegisterUnique(); + //composition.RegisterUnique(); // TODO new reference? composition.RegisterUnique(_ => new MediaUrlProviderCollection(Enumerable.Empty())); // initialize some components only/individually @@ -322,8 +322,8 @@ namespace Umbraco.Tests.Runtimes Assert.AreEqual(0, results.Count); } - - + + } } diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 26c5316e07..a1846ac506 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -362,7 +362,6 @@ - True diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManager.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManager.cs index 15ac06dcf5..c7dd56f9ae 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManager.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManager.cs @@ -20,7 +20,7 @@ namespace Umbraco.Web.Common.AspNetCore var cookieValue = httpContext.Request.Cookies[cookieName]; - httpContext.Response.Cookies.Append(cookieName, cookieValue, new CookieOptions() + httpContext.Response.Cookies.Append(cookieName, cookieValue ?? string.Empty, new CookieOptions() { Expires = DateTime.Now.AddYears(-1) }); diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreRequestAccessor.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreRequestAccessor.cs new file mode 100644 index 0000000000..9ba0f06f64 --- /dev/null +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreRequestAccessor.cs @@ -0,0 +1,27 @@ +using System; +using Microsoft.AspNetCore.Http; +using Umbraco.Web.Common.Extensions; +using Umbraco.Web.Routing; + +namespace Umbraco.Web.Common.AspNetCore +{ + public class AspNetCoreRequestAccessor : IRequestAccessor + { + private readonly IHttpContextAccessor _httpContextAccessor; + public AspNetCoreRequestAccessor(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public string GetRequestValue(string name) => GetFormValue(name) ?? GetQueryStringValue(name); + public string GetFormValue(string name) => _httpContextAccessor.GetRequiredHttpContext().Request.Form[name]; + + public string GetQueryStringValue(string name) => _httpContextAccessor.GetRequiredHttpContext().Request.Query[name]; + + //TODO implement + public event EventHandler EndRequest; + + //TODO implement + public event EventHandler RouteAttempt; + } +} diff --git a/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreUserAgentProvider.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreUserAgentProvider.cs similarity index 92% rename from src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreUserAgentProvider.cs rename to src/Umbraco.Web.Common/AspNetCore/AspNetCoreUserAgentProvider.cs index f9c9884704..cc61070947 100644 --- a/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreUserAgentProvider.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreUserAgentProvider.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Http; using Umbraco.Net; -namespace Umbraco.Web.BackOffice.AspNetCore +namespace Umbraco.Web.Common.AspNetCore { public class AspNetCoreUserAgentProvider : IUserAgentProvider { diff --git a/src/Umbraco.Web.Common/Attributes/AngularJsonOnlyConfigurationAttribute.cs b/src/Umbraco.Web.Common/Attributes/AngularJsonOnlyConfigurationAttribute.cs new file mode 100644 index 0000000000..c721900233 --- /dev/null +++ b/src/Umbraco.Web.Common/Attributes/AngularJsonOnlyConfigurationAttribute.cs @@ -0,0 +1,36 @@ +using System.Buffers; +using System.Collections.Generic; +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Umbraco.Web.Common.Formatters; + +namespace Umbraco.Web.Common.Attributes +{ + /// + /// Applying this attribute to any controller will ensure that it only contains one json formatter compatible with the angular json vulnerability prevention. + /// + public class AngularJsonOnlyConfigurationAttribute : ActionFilterAttribute + { + + public override void OnResultExecuting(ResultExecutingContext context) + { + + var mvcNewtonsoftJsonOptions = context.HttpContext.RequestServices.GetService>(); + var arrayPool = context.HttpContext.RequestServices.GetService>(); + var mvcOptions = context.HttpContext.RequestServices.GetService>(); + + + if (context.Result is ObjectResult objectResult) + { + objectResult.Formatters.Add(new AngularJsonMediaTypeFormatter(mvcNewtonsoftJsonOptions.Value.SerializerSettings, arrayPool, mvcOptions.Value)); + } + + base.OnResultExecuting(context); + } + } +} diff --git a/src/Umbraco.Web.Common/Attributes/FeatureAuthorizeAttribute.cs b/src/Umbraco.Web.Common/Attributes/FeatureAuthorizeAttribute.cs new file mode 100644 index 0000000000..061225334a --- /dev/null +++ b/src/Umbraco.Web.Common/Attributes/FeatureAuthorizeAttribute.cs @@ -0,0 +1,51 @@ + +using System; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Web.Features; +using Umbraco.Core; +using Umbraco.Web.Install; + +namespace Umbraco.Web.WebApi.Filters +{ + /// + /// Ensures that the controller is an authorized feature. + /// + /// Else returns unauthorized. + public class FeatureAuthorizeAttribute : TypeFilterAttribute + { + public FeatureAuthorizeAttribute() : base(typeof(FeatureAuthorizeFilter)) + { + } + + private class FeatureAuthorizeFilter : IAuthorizationFilter + { + public void OnAuthorization(AuthorizationFilterContext context) + { + var serviceProvider = context.HttpContext.RequestServices; + var umbracoFeatures = serviceProvider.GetService(); + + if (!IsAllowed(context, umbracoFeatures)) + { + context.Result = new ForbidResult(); + } + } + + private static bool IsAllowed(AuthorizationFilterContext context, UmbracoFeatures umbracoFeatures) + { + // if no features resolver has been set then return true, this will occur in unit + // tests and we don't want users to have to set a resolver + //just so their unit tests work. + + if (umbracoFeatures == null) return true; + if (!(context.ActionDescriptor is ControllerActionDescriptor contextActionDescriptor)) return true; + + var controllerType = contextActionDescriptor.ControllerTypeInfo.AsType(); + return umbracoFeatures.IsControllerEnabled(controllerType); + } + } + } +} diff --git a/src/Umbraco.Web.Common/Attributes/IsBackOfficeAttribute.cs b/src/Umbraco.Web.Common/Attributes/IsBackOfficeAttribute.cs new file mode 100644 index 0000000000..b625bd3336 --- /dev/null +++ b/src/Umbraco.Web.Common/Attributes/IsBackOfficeAttribute.cs @@ -0,0 +1,13 @@ +using System; + +namespace Umbraco.Web.Common.Attributes +{ + /// + /// When applied to an api controller it will be routed to the /Umbraco/BackOffice prefix route so we can determine if it + /// is a back office route or not. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public sealed class IsBackOfficeAttribute : Attribute + { + } +} diff --git a/src/Umbraco.Web.Common/Attributes/PluginControllerAttribute.cs b/src/Umbraco.Web.Common/Attributes/PluginControllerAttribute.cs new file mode 100644 index 0000000000..83a2611531 --- /dev/null +++ b/src/Umbraco.Web.Common/Attributes/PluginControllerAttribute.cs @@ -0,0 +1,30 @@ +using System; +using System.Linq; + +namespace Umbraco.Web.Common.Attributes +{ + /// + /// Indicates that a controller is a plugin tree controller and should be routed to its own area. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class PluginControllerAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// + public PluginControllerAttribute(string areaName) + { + // validate this, only letters and digits allowed. + if (areaName.Any(c => !char.IsLetterOrDigit(c))) + throw new FormatException($"Invalid area name \"{areaName}\": the area name can only contains letters and digits."); + + AreaName = areaName; + } + + /// + /// Gets the name of the area. + /// + public string AreaName { get; } + } +} diff --git a/src/Umbraco.Web.Common/Controllers/PluginController.cs b/src/Umbraco.Web.Common/Controllers/PluginController.cs new file mode 100644 index 0000000000..77c23b96d5 --- /dev/null +++ b/src/Umbraco.Web.Common/Controllers/PluginController.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Concurrent; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Composing; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; +using Umbraco.Core.Services; +using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Extensions; +using Umbraco.Web.Mvc; + +namespace Umbraco.Web.Common.Controllers +{ + /// + /// Provides a base class for plugin controllers. + /// + public abstract class PluginController : Controller, IDiscoverable + { + private static readonly ConcurrentDictionary MetadataStorage + = new ConcurrentDictionary(); + + // for debugging purposes + internal Guid InstanceId { get; } = Guid.NewGuid(); + + /// + /// Gets the Umbraco context. + /// + public virtual IUmbracoContext UmbracoContext => UmbracoContextAccessor.UmbracoContext; + + /// + /// Gets the database context accessor. + /// + public virtual IUmbracoContextAccessor UmbracoContextAccessor { get; } + + /// + /// Gets the database context. + /// + public IUmbracoDatabaseFactory DatabaseFactory { get; } + + /// + /// Gets or sets the services context. + /// + public ServiceContext Services { get; } + + /// + /// Gets or sets the application cache. + /// + public AppCaches AppCaches { get; } + + /// + /// Gets or sets the logger. + /// + public ILogger Logger { get; } + + /// + /// Gets or sets the profiling logger. + /// + public IProfilingLogger ProfilingLogger { get; } + + /// + /// Gets metadata for this instance. + /// + internal PluginControllerMetadata Metadata => GetMetadata(GetType()); + + protected PluginController(IUmbracoContextAccessor umbracoContextAccessor, IUmbracoDatabaseFactory databaseFactory, ServiceContext services, AppCaches appCaches, ILogger logger, IProfilingLogger profilingLogger) + { + UmbracoContextAccessor = umbracoContextAccessor; + DatabaseFactory = databaseFactory; + Services = services; + AppCaches = appCaches; + Logger = logger; + ProfilingLogger = profilingLogger; + } + + /// + /// Gets metadata for a controller type. + /// + /// The controller type. + /// Metadata for the controller type. + internal static PluginControllerMetadata GetMetadata(Type controllerType) + { + return MetadataStorage.GetOrAdd(controllerType, type => + { + // plugin controller? back-office controller? + var pluginAttribute = controllerType.GetCustomAttribute(false); + var backOfficeAttribute = controllerType.GetCustomAttribute(true); + + return new PluginControllerMetadata + { + AreaName = pluginAttribute?.AreaName, + ControllerName = ControllerExtensions.GetControllerName(controllerType), + ControllerNamespace = controllerType.Namespace, + ControllerType = controllerType, + IsBackOffice = backOfficeAttribute != null + }; + }); + } + } +} diff --git a/src/Umbraco.Web.Common/Controllers/UmbracoApiController.cs b/src/Umbraco.Web.Common/Controllers/UmbracoApiController.cs new file mode 100644 index 0000000000..0fcbee1c5f --- /dev/null +++ b/src/Umbraco.Web.Common/Controllers/UmbracoApiController.cs @@ -0,0 +1,27 @@ +using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Composing; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Mapping; +using Umbraco.Core.Persistence; +using Umbraco.Core.Services; +using Umbraco.Web.Routing; + +namespace Umbraco.Web.Common.Controllers +{ + /// + /// Provides a base class for auto-routed Umbraco API controllers. + /// + public abstract class UmbracoApiController : UmbracoApiControllerBase, IDiscoverable + { + protected UmbracoApiController() + { + } + + // protected UmbracoApiController(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoMapper umbracoMapper, IPublishedUrlProvider publishedUrlProvider) + // : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoMapper, publishedUrlProvider) + // { + // } + } +} diff --git a/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs b/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs new file mode 100644 index 0000000000..794a5c0966 --- /dev/null +++ b/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs @@ -0,0 +1,106 @@ +using System; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Mapping; +using Umbraco.Core.Persistence; +using Umbraco.Core.Services; +using Umbraco.Web.Features; +using Umbraco.Web.Routing; +using Umbraco.Web.Security; +using Umbraco.Web.WebApi.Filters; + +namespace Umbraco.Web.Common.Controllers +{ + /// + /// Provides a base class for Umbraco API controllers. + /// + /// These controllers are NOT auto-routed. + [FeatureAuthorize] + public abstract class UmbracoApiControllerBase : Controller, IUmbracoFeature + { + + // + // /// + // /// Initializes a new instance of the class with all its dependencies. + // /// + // protected UmbracoApiControllerBase(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoMapper umbracoMapper, IPublishedUrlProvider publishedUrlProvider) + // { + // UmbracoContextAccessor = umbracoContextAccessor; + // GlobalSettings = globalSettings; + // SqlContext = sqlContext; + // Services = services; + // AppCaches = appCaches; + // Logger = logger; + // RuntimeState = runtimeState; + // Mapper = umbracoMapper; + // PublishedUrlProvider = publishedUrlProvider; + // } + // + // /// + // /// Gets a unique instance identifier. + // /// + // /// For debugging purposes. + // internal Guid InstanceId { get; } = Guid.NewGuid(); + // + // /// + // /// Gets the Umbraco context. + // /// + // public virtual IGlobalSettings GlobalSettings { get; } + // + // /// + // /// Gets the Umbraco context. + // /// + // public virtual IUmbracoContext UmbracoContext => UmbracoContextAccessor.UmbracoContext; + // + // /// + // /// Gets the Umbraco context accessor. + // /// + // public virtual IUmbracoContextAccessor UmbracoContextAccessor { get; } + // + // + // /// + // /// Gets the sql context. + // /// + // public ISqlContext SqlContext { get; } + // + // /// + // /// Gets the services context. + // /// + // public ServiceContext Services { get; } + // + // /// + // /// Gets the application cache. + // /// + // public AppCaches AppCaches { get; } + // + // /// + // /// Gets the logger. + // /// + // public IProfilingLogger Logger { get; } + // + // /// + // /// Gets the runtime state. + // /// + // internal IRuntimeState RuntimeState { get; } + // + // /// + // /// Gets the application url. + // /// + // protected Uri ApplicationUrl => RuntimeState.ApplicationUrl; + // + // /// + // /// Gets the mapper. + // /// + // public UmbracoMapper Mapper { get; } + // + // protected IPublishedUrlProvider PublishedUrlProvider { get; } + // + // /// + // /// Gets the web security helper. + // /// + // public IWebSecurity Security => UmbracoContext.Security; + } +} diff --git a/src/Umbraco.Web.Common/Extensions/ControllerExtensions.cs b/src/Umbraco.Web.Common/Extensions/ControllerExtensions.cs new file mode 100644 index 0000000000..85ff866d4f --- /dev/null +++ b/src/Umbraco.Web.Common/Extensions/ControllerExtensions.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.AspNetCore.Mvc; + +namespace Umbraco.Web.Common.Extensions +{ + public static class ControllerExtensions + { + /// + /// Return the controller name from the controller type + /// + /// + /// + internal static string GetControllerName(Type controllerType) + { + if (!controllerType.Name.EndsWith("Controller")) + { + throw new InvalidOperationException("The controller type " + controllerType + " does not follow conventions, MVC Controller class names must be suffixed with the term 'Controller'"); + } + return controllerType.Name.Substring(0, controllerType.Name.LastIndexOf("Controller")); + } + + /// + /// Return the controller name from the controller instance + /// + /// + /// + internal static string GetControllerName(this Controller controllerInstance) + { + return GetControllerName(controllerInstance.GetType()); + } + + /// + /// Return the controller name from the controller type + /// + /// + /// + /// + internal static string GetControllerName() + { + return GetControllerName(typeof(T)); + } + } +} diff --git a/src/Umbraco.Web.Common/Extensions/HttpContextAccessorExtensions.cs b/src/Umbraco.Web.Common/Extensions/HttpContextAccessorExtensions.cs new file mode 100644 index 0000000000..f8ca238e36 --- /dev/null +++ b/src/Umbraco.Web.Common/Extensions/HttpContextAccessorExtensions.cs @@ -0,0 +1,18 @@ +using System; +using Microsoft.AspNetCore.Http; + +namespace Umbraco.Web.Common.Extensions +{ + public static class HttpContextAccessorExtensions + { + public static HttpContext GetRequiredHttpContext(this IHttpContextAccessor httpContextAccessor) + { + if (httpContextAccessor == null) throw new ArgumentNullException(nameof(httpContextAccessor)); + var httpContext = httpContextAccessor.HttpContext; + + if(httpContext is null) throw new InvalidOperationException("HttpContext is null"); + + return httpContext; + } + } +} diff --git a/src/Umbraco.Web.Common/Extensions/IUrlHelperExtensions.cs b/src/Umbraco.Web.Common/Extensions/IUrlHelperExtensions.cs new file mode 100644 index 0000000000..b386aaf15d --- /dev/null +++ b/src/Umbraco.Web.Common/Extensions/IUrlHelperExtensions.cs @@ -0,0 +1,157 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Core; +using Umbraco.Web.Common.Controllers; +using Umbraco.Web.Common.Extensions; +using Umbraco.Web.WebApi; + +namespace Umbraco.Web +{ + public static class HttpUrlHelperExtensions + { + /// + /// Return the Url for a Web Api service + /// + /// + /// + /// + /// + /// + /// + public static string GetUmbracoApiService(this IUrlHelper url, UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, string actionName, object id = null) + where T : UmbracoApiController + { + return url.GetUmbracoApiService(umbracoApiControllerTypeCollection, actionName, typeof(T), id); + } + + public static string GetUmbracoApiService(this IUrlHelper url, UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, Expression> methodSelector) + where T : UmbracoApiController + { + var method = ExpressionHelper.GetMethodInfo(methodSelector); + var 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 url.GetUmbracoApiService(umbracoApiControllerTypeCollection, method.Name); + } + return url.GetUmbracoApiService(umbracoApiControllerTypeCollection, method.Name, methodParams.Values.First()); + } + + /// + /// Return the Url for a Web Api service + /// + /// + /// + /// + /// + /// + /// + public static string GetUmbracoApiService(this IUrlHelper url, UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, string actionName, Type apiControllerType, object id = 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)); + + var area = ""; + + var apiController = umbracoApiControllerTypeCollection.SingleOrDefault(x => x == apiControllerType); + if (apiController == null) + throw new InvalidOperationException("Could not find the umbraco api controller of type " + apiControllerType.FullName); + var metaData = PluginController.GetMetadata(apiController); + if (metaData.AreaName.IsNullOrWhiteSpace() == false) + { + //set the area to the plugin area + area = metaData.AreaName; + } + return url.GetUmbracoApiService(actionName, ControllerExtensions.GetControllerName(apiControllerType), area, id); + } + + /// + /// Return the Url for a Web Api service + /// + /// + /// + /// + /// + /// + public static string GetUmbracoApiService(this IUrlHelper url, string actionName, string controllerName, object id = null) + { + return url.GetUmbracoApiService(actionName, controllerName, "", id); + } + + /// + /// Return the Url for a Web Api service + /// + /// + /// + /// + /// + /// + /// + public static string GetUmbracoApiService(this IUrlHelper url, string actionName, string controllerName, string area, object id = 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)); + + string routeName; + if (area.IsNullOrWhiteSpace()) + { + routeName = string.Format("umbraco-{0}-{1}", "api", controllerName); + if (id == null) + { + + return url.RouteUrl(routeName, new { controller = controllerName, action = actionName, httproute = "" }); + } + else + { + return url.RouteUrl(routeName, new { controller = controllerName, action = actionName, id = id, httproute = "" }); + } + } + else + { + routeName = string.Format("umbraco-{0}-{1}-{2}", "api", area, controllerName); + if (id == null) + { + return url.RouteUrl(routeName, new { controller = controllerName, action = actionName, httproute = "" }); + } + else + { + return url.RouteUrl(routeName, new { controller = controllerName, action = actionName, id = id, httproute = "" }); + } + } + } + + /// + /// Return the Base Url (not including the action) for a Web Api service + /// + /// + /// + /// + /// + /// + public static string GetUmbracoApiServiceBaseUrl(this IUrlHelper url, UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, string actionName) + where T : UmbracoApiController + { + return url.GetUmbracoApiService(umbracoApiControllerTypeCollection, actionName).TrimEnd(actionName); + } + + public static string GetUmbracoApiServiceBaseUrl(this IUrlHelper url, UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, Expression> methodSelector) + where T : UmbracoApiController + { + var method = ExpressionHelper.GetMethodInfo(methodSelector); + if (method == null) + { + throw new MissingMethodException("Could not find the method " + methodSelector + " on type " + typeof(T) + " or the result "); + } + return url.GetUmbracoApiService(umbracoApiControllerTypeCollection, method.Name).TrimEnd(method.Name); + } + } +} diff --git a/src/Umbraco.Web.Common/Extensions/ViewDataExtensions.cs b/src/Umbraco.Web.Common/Extensions/ViewDataExtensions.cs new file mode 100644 index 0000000000..d09cffd0dc --- /dev/null +++ b/src/Umbraco.Web.Common/Extensions/ViewDataExtensions.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc.ViewFeatures; + + +namespace Umbraco.Web +{ + public static class ViewDataExtensions + { + public const string TokenUmbracoPath = "UmbracoPath"; + public const string TokenInstallApiBaseUrl = "InstallApiBaseUrl"; + public const string TokenUmbracoBaseFolder = "UmbracoBaseFolder"; + public const string TokenExternalSignInError = "ExternalSignInError"; + public const string TokenPasswordResetCode = "PasswordResetCode"; + + public static bool FromTempData(this ViewDataDictionary viewData, TempDataDictionary tempData, string token) + { + if (tempData[token] == null) return false; + viewData[token] = tempData[token]; + return true; + } + + public static string GetUmbracoPath(this ViewDataDictionary viewData) + { + return (string)viewData[TokenUmbracoPath]; + } + + public static void SetUmbracoPath(this ViewDataDictionary viewData, string value) + { + viewData[TokenUmbracoPath] = value; + } + + public static string GetInstallApiBaseUrl(this ViewDataDictionary viewData) + { + return (string)viewData[TokenInstallApiBaseUrl]; + } + + public static void SetInstallApiBaseUrl(this ViewDataDictionary viewData, string value) + { + viewData[TokenInstallApiBaseUrl] = value; + } + + public static string GetUmbracoBaseFolder(this ViewDataDictionary viewData) + { + return (string)viewData[TokenUmbracoBaseFolder]; + } + + public static void SetUmbracoBaseFolder(this ViewDataDictionary viewData, string value) + { + viewData[TokenUmbracoBaseFolder] = value; + } + + public static IEnumerable GetExternalSignInError(this ViewDataDictionary viewData) + { + return (IEnumerable)viewData[TokenExternalSignInError]; + } + + public static void SetExternalSignInError(this ViewDataDictionary viewData, IEnumerable value) + { + viewData[TokenExternalSignInError] = value; + } + + public static string GetPasswordResetCode(this ViewDataDictionary viewData) + { + return (string)viewData[TokenPasswordResetCode]; + } + + public static void SetPasswordResetCode(this ViewDataDictionary viewData, string value) + { + viewData[TokenPasswordResetCode] = value; + } + } +} diff --git a/src/Umbraco.Web.Common/Filters/HttpResponseExceptionFilter.cs b/src/Umbraco.Web.Common/Filters/HttpResponseExceptionFilter.cs new file mode 100644 index 0000000000..3bc68b5eff --- /dev/null +++ b/src/Umbraco.Web.Common/Filters/HttpResponseExceptionFilter.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Umbraco.Core.Exceptions; + +namespace Umbraco.Web.Common.Filters +{ + public class HttpResponseExceptionFilter : IActionFilter, IOrderedFilter + { + public int Order { get; set; } = int.MaxValue - 10; + + public void OnActionExecuting(ActionExecutingContext context) { } + + public void OnActionExecuted(ActionExecutedContext context) + { + if (context.Exception is HttpResponseException exception) + { + context.Result = new ObjectResult(exception.Value) + { + StatusCode = (int)exception.Status, + }; + + foreach (var (key,value) in exception.AdditionalHeaders) + { + context.HttpContext.Response.Headers[key] = value; + } + + context.ExceptionHandled = true; + } + } + } +} diff --git a/src/Umbraco.Web.Common/Formatters/AngularJsonMediaTypeFormatter.cs b/src/Umbraco.Web.Common/Formatters/AngularJsonMediaTypeFormatter.cs new file mode 100644 index 0000000000..9a10269398 --- /dev/null +++ b/src/Umbraco.Web.Common/Formatters/AngularJsonMediaTypeFormatter.cs @@ -0,0 +1,33 @@ +using System.Buffers; +using System.IO; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Formatters; +using Newtonsoft.Json; + +namespace Umbraco.Web.Common.Formatters +{ + /// + /// This will format the JSON output for use with AngularJs's approach to JSON Vulnerability attacks + /// + /// + /// See: http://docs.angularjs.org/api/ng.$http (Security considerations) + /// + public class AngularJsonMediaTypeFormatter : NewtonsoftJsonOutputFormatter + { + public const string XsrfPrefix = ")]}',\n"; + + public AngularJsonMediaTypeFormatter(JsonSerializerSettings serializerSettings, ArrayPool charPool, MvcOptions mvcOptions) + : base(serializerSettings, charPool, mvcOptions) + { + } + + protected override JsonWriter CreateJsonWriter(TextWriter writer) + { + var jsonWriter = base.CreateJsonWriter(writer); + + jsonWriter.WriteRaw(XsrfPrefix); + + return jsonWriter; + } + } +} diff --git a/src/Umbraco.Web.Common/Install/HttpInstallAuthorizeAttribute.cs b/src/Umbraco.Web.Common/Install/HttpInstallAuthorizeAttribute.cs new file mode 100644 index 0000000000..0c03b1ce28 --- /dev/null +++ b/src/Umbraco.Web.Common/Install/HttpInstallAuthorizeAttribute.cs @@ -0,0 +1,55 @@ +using System; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Core; +using Umbraco.Core.Logging; + +namespace Umbraco.Web.Install +{ + /// + /// Ensures authorization occurs for the installer if it has already completed. + /// If install has not yet occurred then the authorization is successful. + /// + public class HttpInstallAuthorizeAttribute : TypeFilterAttribute + { + public HttpInstallAuthorizeAttribute() : base(typeof(HttpInstallAuthorizeFilter)) + { + } + + private class HttpInstallAuthorizeFilter : IAuthorizationFilter + { + public void OnAuthorization(AuthorizationFilterContext authorizationFilterContext) + { + var serviceProvider = authorizationFilterContext.HttpContext.RequestServices; + var runtimeState = serviceProvider.GetService(); + var umbracoContext = serviceProvider.GetService(); + var logger = serviceProvider.GetService(); + + if (!IsAllowed(runtimeState, umbracoContext, logger)) + { + authorizationFilterContext.Result = new ForbidResult(); + } + + } + + private static bool IsAllowed(IRuntimeState runtimeState, IUmbracoContext umbracoContext, ILogger logger) + { + 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 + || umbracoContext.Security.ValidateCurrentUser(); + } + catch (Exception ex) + { + logger.Error(ex, "An error occurred determining authorization"); + return false; + } + } + } + } + +} diff --git a/src/Umbraco.Web/Install/Controllers/InstallApiController.cs b/src/Umbraco.Web.Common/Install/InstallApiController.cs similarity index 80% rename from src/Umbraco.Web/Install/Controllers/InstallApiController.cs rename to src/Umbraco.Web.Common/Install/InstallApiController.cs index 0b94058137..57a83bdc54 100644 --- a/src/Umbraco.Web/Install/Controllers/InstallApiController.cs +++ b/src/Umbraco.Web.Common/Install/InstallApiController.cs @@ -1,29 +1,34 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Reflection; using System.Threading.Tasks; -using System.Web.Http; +using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json.Linq; using Umbraco.Core; +using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Migrations.Install; +using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Install; using Umbraco.Web.Install.Models; -using Umbraco.Web.WebApi; -namespace Umbraco.Web.Install.Controllers +namespace Umbraco.Web.Common.Install { [AngularJsonOnlyConfiguration] [HttpInstallAuthorize] - public class InstallApiController : ApiController + [Area("Install")] + public class InstallApiController : Controller { private readonly DatabaseBuilder _databaseBuilder; - private readonly IProfilingLogger _proflog; - private readonly InstallStepCollection _installSteps; private readonly InstallStatusTracker _installStatusTracker; + private readonly InstallStepCollection _installSteps; private readonly ILogger _logger; + private readonly IProfilingLogger _proflog; - public InstallApiController(DatabaseBuilder databaseBuilder, IProfilingLogger proflog, InstallHelper installHelper, InstallStepCollection installSteps, InstallStatusTracker installStatusTracker) + public InstallApiController(DatabaseBuilder databaseBuilder, IProfilingLogger proflog, + InstallHelper installHelper, InstallStepCollection installSteps, InstallStatusTracker installStatusTracker) { _databaseBuilder = databaseBuilder ?? throw new ArgumentNullException(nameof(databaseBuilder)); _proflog = proflog ?? throw new ArgumentNullException(nameof(proflog)); @@ -37,12 +42,13 @@ namespace Umbraco.Web.Install.Controllers public bool PostValidateDatabaseConnection(DatabaseModel model) { - var canConnect = _databaseBuilder.CanConnect(model.DatabaseType.ToString(), model.ConnectionString, model.Server, model.DatabaseName, model.Login, model.Password, model.IntegratedAuth); + var canConnect = _databaseBuilder.CanConnect(model.DatabaseType.ToString(), model.ConnectionString, + model.Server, model.DatabaseName, model.Login, model.Password, model.IntegratedAuth); return canConnect; } /// - /// Gets the install setup. + /// Gets the install setup. /// public InstallSetup GetSetup() { @@ -70,9 +76,9 @@ namespace Umbraco.Web.Install.Controllers } /// - /// Installs. + /// Installs. /// - public async Task PostPerformInstall(InstallInstructions installModel) + public async Task PostPerformInstall([FromBody] InstallInstructions installModel) { if (installModel == null) throw new ArgumentNullException(nameof(installModel)); @@ -114,15 +120,16 @@ namespace Umbraco.Web.Install.Controllers // check if there's a custom view to return for this step if (setupData != null && setupData.View.IsNullOrWhiteSpace() == false) { - return new InstallProgressResultModel(false, step.Name, nextStep, setupData.View, setupData.ViewModel); + return new InstallProgressResultModel(false, step.Name, nextStep, setupData.View, + setupData.ViewModel); } return new InstallProgressResultModel(false, step.Name, nextStep); } catch (Exception ex) { - - _logger.Error(ex, "An error occurred during installation step {Step}", step.Name); + _logger.Error(ex, "An error occurred during installation step {Step}", + step.Name); if (ex is TargetInvocationException && ex.InnerException != null) { @@ -132,20 +139,32 @@ namespace Umbraco.Web.Install.Controllers var installException = ex as InstallException; if (installException != null) { - throw new HttpResponseException(Request.CreateValidationErrorResponse(new + throw new HttpResponseException(HttpStatusCode.BadRequest, new { view = installException.View, model = installException.ViewModel, message = installException.Message - })); + }) + { + AdditionalHeaders = + { + ["X-Status-Reason"] = "Validation failed" + } + }; } - throw new HttpResponseException(Request.CreateValidationErrorResponse(new + throw new HttpResponseException(HttpStatusCode.BadRequest,new { step = step.Name, view = "error", message = ex.Message - })); + }) + { + AdditionalHeaders = + { + ["X-Status-Reason"] = "Validation failed" + } + }; } } @@ -153,7 +172,8 @@ namespace Umbraco.Web.Install.Controllers return new InstallProgressResultModel(true, "", ""); } - private static object GetInstruction(InstallInstructions installModel, InstallTrackingItem item, InstallSetupStep step) + private static object GetInstruction(InstallInstructions installModel, InstallTrackingItem item, + InstallSetupStep step) { installModel.Instructions.TryGetValue(item.Name, out var instruction); // else null @@ -166,15 +186,16 @@ namespace Umbraco.Web.Install.Controllers } /// - /// We'll peek ahead and check if it's RequiresExecution is returning true. If it - /// is not, we'll dequeue that step and peek ahead again (recurse) + /// We'll peek ahead and check if it's RequiresExecution is returning true. If it + /// is not, we'll dequeue that step and peek ahead again (recurse) /// /// /// /// /// /// - private string IterateSteps(InstallSetupStep current, Queue queue, Guid installId, InstallInstructions installModel) + private string IterateSteps(InstallSetupStep current, Queue queue, Guid installId, + InstallInstructions installModel) { while (queue.Count > 0) { @@ -217,7 +238,8 @@ namespace Umbraco.Web.Install.Controllers var modelAttempt = instruction.TryConvertTo(step.StepType); if (!modelAttempt.Success) { - throw new InvalidCastException($"Cannot cast/convert {step.GetType().FullName} into {step.StepType.FullName}"); + throw new InvalidCastException( + $"Cannot cast/convert {step.GetType().FullName} into {step.StepType.FullName}"); } var model = modelAttempt.Result; @@ -231,7 +253,8 @@ namespace Umbraco.Web.Install.Controllers } catch (Exception ex) { - _logger.Error(ex, "Checking if step requires execution ({Step}) failed.", step.Name); + _logger.Error(ex, "Checking if step requires execution ({Step}) failed.", + step.Name); throw; } } @@ -239,13 +262,16 @@ namespace Umbraco.Web.Install.Controllers // executes the step internal async Task ExecuteStepAsync(InstallSetupStep step, object instruction) { - using (_proflog.TraceDuration($"Executing installation step: '{step.Name}'.", "Step completed")) + using (_proflog.TraceDuration($"Executing installation step: '{step.Name}'.", + "Step completed")) { var modelAttempt = instruction.TryConvertTo(step.StepType); if (!modelAttempt.Success) { - throw new InvalidCastException($"Cannot cast/convert {step.GetType().FullName} into {step.StepType.FullName}"); + throw new InvalidCastException( + $"Cannot cast/convert {step.GetType().FullName} into {step.StepType.FullName}"); } + var model = modelAttempt.Result; var genericStepType = typeof(InstallSetupStep<>); Type[] typeArgs = { step.StepType }; diff --git a/src/Umbraco.Web.Common/Install/InstallAuthorizeAttribute.cs b/src/Umbraco.Web.Common/Install/InstallAuthorizeAttribute.cs new file mode 100644 index 0000000000..c92256c315 --- /dev/null +++ b/src/Umbraco.Web.Common/Install/InstallAuthorizeAttribute.cs @@ -0,0 +1,50 @@ +using System; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Hosting; + +namespace Umbraco.Web.Common.Install +{ + public class InstallAuthorizeAttribute : TypeFilterAttribute + { + public InstallAuthorizeAttribute() : base(typeof(InstallAuthorizeFilter)) + { + } + + private class InstallAuthorizeFilter : IAuthorizationFilter + { + public void OnAuthorization(AuthorizationFilterContext context) + { + var sp = context.HttpContext.RequestServices; + var runtimeState = sp.GetRequiredService(); + var umbracoContextAccessor = sp.GetRequiredService(); + var globalSettings = sp.GetRequiredService(); + var hostingEnvironment = sp.GetRequiredService(); + + if (!IsAllowed(runtimeState, umbracoContextAccessor)) + { + context.Result = new RedirectResult(globalSettings.GetBackOfficePath(hostingEnvironment)); + } + } + + private bool IsAllowed(IRuntimeState runtimeState, IUmbracoContextAccessor umbracoContextAccessor) + { + 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 + || umbracoContextAccessor.UmbracoContext.Security.ValidateCurrentUser(); + } + catch (Exception) + { + return false; + } + } + } + } +} diff --git a/src/Umbraco.Web/Install/Controllers/InstallController.cs b/src/Umbraco.Web.Common/Install/InstallController.cs similarity index 86% rename from src/Umbraco.Web/Install/Controllers/InstallController.cs rename to src/Umbraco.Web.Common/Install/InstallController.cs index b59692db79..e40f4bcfa7 100644 --- a/src/Umbraco.Web/Install/Controllers/InstallController.cs +++ b/src/Umbraco.Web.Common/Install/InstallController.cs @@ -1,14 +1,13 @@ -using System.Web.Mvc; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Mvc; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Hosting; -using Umbraco.Core.IO; -using Umbraco.Core.Logging; using Umbraco.Core.WebAssets; -using Umbraco.Web.Mvc; +using Umbraco.Web.Install; using Umbraco.Web.Security; -namespace Umbraco.Web.Install.Controllers +namespace Umbraco.Web.Common.Install { /// /// The MVC Installation controller @@ -43,7 +42,7 @@ namespace Umbraco.Web.Install.Controllers } [HttpGet] - [StatusCodeResult(System.Net.HttpStatusCode.ServiceUnavailable)] + // [StatusCodeResult(System.Net.HttpStatusCode.ServiceUnavailable)] //TODO reintroduce public ActionResult Index() { if (_runtime.Level == RuntimeLevel.Run) @@ -60,7 +59,7 @@ namespace Umbraco.Web.Install.Controllers { case ValidateRequestAttempt.FailedNoPrivileges: case ValidateRequestAttempt.FailedNoContextId: - return Redirect(_globalSettings.UmbracoPath + "/AuthorizeUpgrade?redir=" + Server.UrlEncode(Request.RawUrl)); + return Redirect(_globalSettings.UmbracoPath + "/AuthorizeUpgrade?redir=" + Request.GetEncodedUrl()); } } @@ -73,7 +72,7 @@ namespace Umbraco.Web.Install.Controllers _installHelper.InstallStatus(false, ""); // always ensure full path (see NOTE in the class remarks) - return View(_globalSettings.GetBackOfficePath(_hostingEnvironment).EnsureEndsWith('/') + "install/views/index.cshtml"); + return View(); } } } diff --git a/src/Umbraco.Web/Macros/MacroRenderer.cs b/src/Umbraco.Web.Common/Macros/MacroRenderer.cs similarity index 98% rename from src/Umbraco.Web/Macros/MacroRenderer.cs rename to src/Umbraco.Web.Common/Macros/MacroRenderer.cs index 7f6a1cdbf3..36f0224141 100644 --- a/src/Umbraco.Web/Macros/MacroRenderer.cs +++ b/src/Umbraco.Web.Common/Macros/MacroRenderer.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using Microsoft.AspNetCore.Http; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Configuration.UmbracoSettings; @@ -347,8 +348,9 @@ namespace Umbraco.Web.Macros /// The text output of the macro execution. private MacroContent ExecutePartialView(MacroModel macro, IPublishedContent content) { - var engine = new PartialViewMacroEngine(_umbracoContextAccessor, _httpContextAccessor, _ioHelper); - return engine.Execute(macro, content); + throw new NotImplementedException(); + // var engine = new PartialViewMacroEngine(_umbracoContextAccessor, _httpContextAccessor, _ioHelper); + // return engine.Execute(macro, content); } #endregion diff --git a/src/Umbraco.Web.Common/Macros/MemberUserKeyProvider.cs b/src/Umbraco.Web.Common/Macros/MemberUserKeyProvider.cs new file mode 100644 index 0000000000..d5b30bbe0d --- /dev/null +++ b/src/Umbraco.Web.Common/Macros/MemberUserKeyProvider.cs @@ -0,0 +1,14 @@ +using Umbraco.Core.Security; + +namespace Umbraco.Web.Common.Macros +{ + internal class MemberUserKeyProvider : IMemberUserKeyProvider + { + public object GetMemberProviderUserKey() + { + // TODO Implement + + return string.Empty; + } + } +} diff --git a/src/Umbraco.Web/Macros/PartialViewMacroController.cs b/src/Umbraco.Web.Common/Macros/PartialViewMacroViewComponent.cs similarity index 60% rename from src/Umbraco.Web/Macros/PartialViewMacroController.cs rename to src/Umbraco.Web.Common/Macros/PartialViewMacroViewComponent.cs index 21d7b3292c..b90c5b3907 100644 --- a/src/Umbraco.Web/Macros/PartialViewMacroController.cs +++ b/src/Umbraco.Web.Common/Macros/PartialViewMacroViewComponent.cs @@ -1,7 +1,7 @@ -using System.Web.Mvc; -using Umbraco.Web.Models; -using Umbraco.Web.Mvc; +using Umbraco.Web.Models; using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; using Umbraco.Core.Composing; using Umbraco.Core.Models.PublishedContent; @@ -10,25 +10,20 @@ namespace Umbraco.Web.Macros /// /// Controller to render macro content for Partial View Macros /// - [MergeParentContextViewData] + // [MergeParentContextViewData] // TODO is this important now it is a view Component [HideFromTypeFinder] // explicitly used: do *not* find and register it! - internal class PartialViewMacroController : Controller + internal class PartialViewMacroViewComponent : ViewComponent { private readonly MacroModel _macro; private readonly IPublishedContent _content; - public PartialViewMacroController(MacroModel macro, IPublishedContent content) + public PartialViewMacroViewComponent(MacroModel macro, IPublishedContent content) { _macro = macro; _content = content; } - /// - /// Child action to render a macro - /// - /// - [ChildActionOnly] - public PartialViewResult Index() + public async Task InvokeAsync() { var model = new PartialViewMacroModel( _content, @@ -36,7 +31,7 @@ namespace Umbraco.Web.Macros _macro.Alias, _macro.Name, _macro.Properties.ToDictionary(x => x.Key, x => (object)x.Value)); - return PartialView(_macro.MacroSource, model); + return View(_macro.MacroSource, model); } } } diff --git a/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs b/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs index 79c7d3ec25..f194826626 100644 --- a/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs +++ b/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs @@ -6,7 +6,11 @@ using Umbraco.Net; using Umbraco.Core.Runtime; using Umbraco.Core.Security; using Umbraco.Web.Common.AspNetCore; +using Umbraco.Web.Common.Formatters; using Umbraco.Web.Common.Lifetime; +using Umbraco.Web.Common.Macros; +using Umbraco.Web.Composing.CompositionExtensions; +using Umbraco.Web.Macros; namespace Umbraco.Web.Common.Runtime { @@ -23,11 +27,10 @@ namespace Umbraco.Web.Common.Runtime // AspNetCore specific services composition.RegisterUnique(); + composition.RegisterUnique(); // Our own netcore implementations - composition.RegisterUnique(); - composition.RegisterUnique(factory => factory.GetInstance()); - composition.RegisterUnique(factory => factory.GetInstance()); + composition.RegisterMultipleUnique(); composition.RegisterUnique(); @@ -40,8 +43,20 @@ namespace Umbraco.Web.Common.Runtime composition.RegisterUnique(); + composition.RegisterUnique(); composition.RegisterMultipleUnique(); + + composition.RegisterUnique(); + composition.RegisterUnique(); + + composition.RegisterUnique(); + + //register the install components + //NOTE: i tried to not have these registered if we weren't installing or upgrading but post install when the site restarts + //it still needs to use the install controller so we can't do that + composition.ComposeInstaller(); + } } } diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index 7203c4ba29..d62cd838a2 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -17,9 +17,17 @@ + + + + + + <_Parameter1>Umbraco.Tests.UnitTests + + diff --git a/src/Umbraco.Web.Common/UmbracoContext/UmbracoContext.cs b/src/Umbraco.Web.Common/UmbracoContext/UmbracoContext.cs new file mode 100644 index 0000000000..dd7ce36cb5 --- /dev/null +++ b/src/Umbraco.Web.Common/UmbracoContext/UmbracoContext.cs @@ -0,0 +1,232 @@ +using System; +using System.Web; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Umbraco.Composing; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Hosting; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Web.Composing; +using Umbraco.Web.PublishedCache; +using Umbraco.Web.Routing; +using Umbraco.Web.Security; + +namespace Umbraco.Web +{ + /// + /// Class that encapsulates Umbraco information of a specific HTTP request + /// + public class UmbracoContext : DisposableObjectSlim, IDisposeOnRequestEnd, IUmbracoContext + { + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IGlobalSettings _globalSettings; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly ICookieManager _cookieManager; + private readonly IRequestAccessor _requestAccessor; + private readonly Lazy _publishedSnapshot; + private string _previewToken; + private bool? _previewing; + + // initializes a new instance of the UmbracoContext class + // internal for unit tests + // otherwise it's used by EnsureContext above + // warn: does *not* manage setting any IUmbracoContextAccessor + internal UmbracoContext(IHttpContextAccessor httpContextAccessor, + IPublishedSnapshotService publishedSnapshotService, + IWebSecurity webSecurity, + IGlobalSettings globalSettings, + IHostingEnvironment hostingEnvironment, + IVariationContextAccessor variationContextAccessor, + UriUtility uriUtility, + ICookieManager cookieManager, + IRequestAccessor requestAccessor) + { + if (httpContextAccessor == null) throw new ArgumentNullException(nameof(httpContextAccessor)); + if (publishedSnapshotService == null) throw new ArgumentNullException(nameof(publishedSnapshotService)); + if (webSecurity == null) throw new ArgumentNullException(nameof(webSecurity)); + VariationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); + _httpContextAccessor = httpContextAccessor; + _globalSettings = globalSettings ?? throw new ArgumentNullException(nameof(globalSettings)); + _hostingEnvironment = hostingEnvironment; + _cookieManager = cookieManager; + _requestAccessor = requestAccessor; + + // ensure that this instance is disposed when the request terminates, though we *also* ensure + // this happens in the Umbraco module since the UmbracoCOntext is added to the HttpContext items. + // + // also, it *can* be returned by the container with a PerRequest lifetime, meaning that the + // container *could* also try to dispose it. + // + // all in all, this context may be disposed more than once, but DisposableObject ensures that + // it is ok and it will be actually disposed only once. + httpContextAccessor.HttpContext?.DisposeOnPipelineCompleted(this); + + ObjectCreated = DateTime.Now; + UmbracoRequestId = Guid.NewGuid(); + Security = webSecurity; + + // beware - we cannot expect a current user here, so detecting preview mode must be a lazy thing + _publishedSnapshot = new Lazy(() => publishedSnapshotService.CreatePublishedSnapshot(PreviewToken)); + + // set the urls... + // NOTE: The request will not be available during app startup so we can only set this to an absolute URL of localhost, this + // is a work around to being able to access the UmbracoContext during application startup and this will also ensure that people + // 'could' still generate URLs during startup BUT any domain driven URL generation will not work because it is NOT possible to get + // the current domain during application startup. + // see: http://issues.umbraco.org/issue/U4-1890 + // + OriginalRequestUrl = new Uri(GetRequestFromContext()?.GetDisplayUrl() ?? "http://localhost"); + CleanedUmbracoUrl = uriUtility.UriToUmbraco(OriginalRequestUrl); + } + + /// + /// This is used internally for performance calculations, the ObjectCreated DateTime is set as soon as this + /// object is instantiated which in the web site is created during the BeginRequest phase. + /// We can then determine complete rendering time from that. + /// + public DateTime ObjectCreated { get; } + + /// + /// This is used internally for debugging and also used to define anything required to distinguish this request from another. + /// + public Guid UmbracoRequestId { get; } + + /// + /// Gets the WebSecurity class + /// + public IWebSecurity Security { get; } + + /// + /// Gets the uri that is handled by ASP.NET after server-side rewriting took place. + /// + public Uri OriginalRequestUrl { get; } + + /// + /// Gets the cleaned up url that is handled by Umbraco. + /// + /// That is, lowercase, no trailing slash after path, no .aspx... + public Uri CleanedUmbracoUrl { get; } + + /// + /// Gets the published snapshot. + /// + public IPublishedSnapshot PublishedSnapshot => _publishedSnapshot.Value; + + /// + /// Gets the published content cache. + /// + public IPublishedContentCache Content => PublishedSnapshot.Content; + + /// + /// Gets the published media cache. + /// + public IPublishedMediaCache Media => PublishedSnapshot.Media; + + /// + /// Gets the domains cache. + /// + public IDomainCache Domains => PublishedSnapshot.Domains; + + /// + /// Boolean value indicating whether the current request is a front-end umbraco request + /// + public bool IsFrontEndUmbracoRequest => PublishedRequest != null; + + /// + /// Gets/sets the PublishedRequest object + /// + public IPublishedRequest PublishedRequest { get; set; } + + /// + /// Gets the variation context accessor. + /// + public IVariationContextAccessor VariationContextAccessor { get; } + + /// + /// Gets a value indicating whether the request has debugging enabled + /// + /// true if this instance is debug; otherwise, false. + public bool IsDebug + { + get + { + //NOTE: the request can be null during app startup! + return Current.HostingEnvironment.IsDebugMode + && (string.IsNullOrEmpty(_requestAccessor.GetRequestValue("umbdebugshowtrace")) == false + || string.IsNullOrEmpty(_requestAccessor.GetRequestValue("umbdebug")) == false + || string.IsNullOrEmpty(_cookieManager.GetCookieValue("UMB-DEBUG")) == false); + } + } + + /// + /// Determines whether the current user is in a preview mode and browsing the site (ie. not in the admin UI) + /// + public bool InPreviewMode + { + get + { + if (_previewing.HasValue == false) DetectPreviewMode(); + return _previewing ?? false; + } + private set => _previewing = value; + } + + public string PreviewToken + { + get + { + if (_previewing.HasValue == false) DetectPreviewMode(); + return _previewToken; + } + } + + private void DetectPreviewMode() + { + var request = GetRequestFromContext(); + if (request?.GetDisplayUrl() != null + && new Uri(request.GetEncodedUrl()).IsBackOfficeRequest(_globalSettings, _hostingEnvironment) == false + && Security.CurrentUser != null) + { + var previewToken = _cookieManager.GetCookieValue(Constants.Web.PreviewCookieName); // may be null or empty + _previewToken = previewToken.IsNullOrWhiteSpace() ? null : previewToken; + } + + _previewing = _previewToken.IsNullOrWhiteSpace() == false; + } + + // say we render a macro or RTE in a give 'preview' mode that might not be the 'current' one, + // then due to the way it all works at the moment, the 'current' published snapshot need to be in the proper + // default 'preview' mode - somehow we have to force it. and that could be recursive. + public IDisposable ForcedPreview(bool preview) + { + InPreviewMode = preview; + return PublishedSnapshot.ForcedPreview(preview, orig => InPreviewMode = orig); + } + + private HttpRequest GetRequestFromContext() + { + try + { + return _httpContextAccessor.HttpContext?.Request; + } + catch (Exception) + { + return null; + } + } + + protected override void DisposeResources() + { + // DisposableObject ensures that this runs only once + + Security.DisposeIfDisposable(); + + // help caches release resources + // (but don't create caches just to dispose them) + // context is not multi-threaded + if (_publishedSnapshot.IsValueCreated) + _publishedSnapshot.Value.Dispose(); + } + } +} diff --git a/src/Umbraco.Web.Common/UmbracoContext/UmbracoContextFactory.cs b/src/Umbraco.Web.Common/UmbracoContext/UmbracoContextFactory.cs new file mode 100644 index 0000000000..af1481e185 --- /dev/null +++ b/src/Umbraco.Web.Common/UmbracoContext/UmbracoContextFactory.cs @@ -0,0 +1,98 @@ +using System; +using System.IO; +using System.Text; +using Microsoft.AspNetCore.Http; +using Umbraco.Core.Configuration; +using Umbraco.Core.Hosting; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Services; +using Umbraco.Web.PublishedCache; +using Umbraco.Web.Security; + +namespace Umbraco.Web +{ + /// + /// Creates and manages instances. + /// + public class UmbracoContextFactory : IUmbracoContextFactory + { + private static readonly NullWriter NullWriterInstance = new NullWriter(); + + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IPublishedSnapshotService _publishedSnapshotService; + private readonly IVariationContextAccessor _variationContextAccessor; + private readonly IDefaultCultureAccessor _defaultCultureAccessor; + + private readonly IGlobalSettings _globalSettings; + private readonly IUserService _userService; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly ICookieManager _cookieManager; + private readonly UriUtility _uriUtility; + + /// + /// Initializes a new instance of the class. + /// + public UmbracoContextFactory( + IUmbracoContextAccessor umbracoContextAccessor, + IPublishedSnapshotService publishedSnapshotService, + IVariationContextAccessor variationContextAccessor, + IDefaultCultureAccessor defaultCultureAccessor, + IGlobalSettings globalSettings, + IUserService userService, + IHostingEnvironment hostingEnvironment, + UriUtility uriUtility, + IHttpContextAccessor httpContextAccessor, + ICookieManager cookieManager) + { + _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + _publishedSnapshotService = publishedSnapshotService ?? throw new ArgumentNullException(nameof(publishedSnapshotService)); + _variationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); + _defaultCultureAccessor = defaultCultureAccessor ?? throw new ArgumentNullException(nameof(defaultCultureAccessor)); + _globalSettings = globalSettings ?? throw new ArgumentNullException(nameof(globalSettings)); + _userService = userService ?? throw new ArgumentNullException(nameof(userService)); + _hostingEnvironment = hostingEnvironment; + _uriUtility = uriUtility; + _httpContextAccessor = httpContextAccessor; + _cookieManager = cookieManager; + } + + private IUmbracoContext CreateUmbracoContext() + { + // make sure we have a variation context + if (_variationContextAccessor.VariationContext == null) + { + // TODO: By using _defaultCultureAccessor.DefaultCulture this means that the VariationContext will always return a variant culture, it will never + // return an empty string signifying that the culture is invariant. But does this matter? Are we actually expecting this to return an empty string + // for invariant routes? From what i can tell throughout the codebase is that whenever we are checking against the VariationContext.Culture we are + // also checking if the content type varies by culture or not. This is fine, however the code in the ctor of VariationContext is then misleading + // since it's assuming that the Culture can be empty (invariant) when in reality of a website this will never be empty since a real culture is always set here. + _variationContextAccessor.VariationContext = new VariationContext(_defaultCultureAccessor.DefaultCulture); + } + + var webSecurity = new WebSecurity(_httpContextAccessor, _userService, _globalSettings, _hostingEnvironment); + + return new UmbracoContext(_httpContextAccessor, _publishedSnapshotService, webSecurity, _globalSettings, _hostingEnvironment, _variationContextAccessor, _uriUtility, _cookieManager); + } + + /// + public UmbracoContextReference EnsureUmbracoContext() + { + var currentUmbracoContext = _umbracoContextAccessor.UmbracoContext; + if (currentUmbracoContext != null) + return new UmbracoContextReference(currentUmbracoContext, false, _umbracoContextAccessor); + + + var umbracoContext = CreateUmbracoContext(); + _umbracoContextAccessor.UmbracoContext = umbracoContext; + + return new UmbracoContextReference(umbracoContext, true, _umbracoContextAccessor); + } + + // dummy TextWriter that does not write + private class NullWriter : TextWriter + { + public override Encoding Encoding => Encoding.UTF8; + } + } +} diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index 75b2d6f48e..468151fc9d 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -1,18 +1,22 @@ using System; +using System.Data.Common; +using System.Data.SqlClient; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Newtonsoft.Json.Serialization; using Umbraco.Composing; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Web.BackOffice.AspNetCore; -using Umbraco.Web.Common.AspNetCore; using Umbraco.Web.Common.Extensions; +using Umbraco.Web.Common.Filters; using Umbraco.Web.Website.AspNetCore; using IHostingEnvironment = Umbraco.Core.Hosting.IHostingEnvironment; @@ -44,10 +48,24 @@ namespace Umbraco.Web.UI.BackOffice { services.AddUmbracoConfiguration(_config); services.AddUmbracoRuntimeMinifier(_config); + + // need to manually register this factory + DbProviderFactories.RegisterFactory(Constants.DbProviderNames.SqlServer, SqlClientFactory.Instance); + services.AddUmbracoCore(_env, out var factory); services.AddUmbracoWebsite(); - services.AddMvc(); + services.AddMvc(options => + { + options.Filters.Add(); + }).SetCompatibilityVersion(CompatibilityVersion.Version_3_0) + .AddNewtonsoftJson(options => + { + options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); + + }) + ; + services.AddMiniProfiler(options => { options.ShouldProfile = request => false; // WebProfiler determine and start profiling. We should not use the MiniProfilerMiddleware to also profile @@ -87,6 +105,16 @@ namespace Umbraco.Web.UI.BackOffice Controller = "BackOffice", Action = "Default" }); + + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller}/{action=Index}/{id?}"); + + endpoints.MapControllerRoute("Install", "/install/{controller}/{Action}", defaults:new { Area = "Install"}); + + //TODO register routing correct: Name must be like this + endpoints.MapControllerRoute("umbraco-api-UmbracoInstall-InstallApi", "/install/api/{Action}", defaults:new { Area = "Install", Controller = "InstallApi"}); + endpoints.MapGet("/", async context => { await context.Response.WriteAsync($"Hello World!{Current.Profiler.Render()}"); 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 72f29f3c4b..4b925e81c4 100644 --- a/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj +++ b/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj @@ -6,6 +6,7 @@ + @@ -13,26 +14,40 @@ + - - - - + + + + + + + + + + + true + PreserveNewest + + + + + diff --git a/src/Umbraco.Web.UI.NetCore/Views/Install/Index.cshtml b/src/Umbraco.Web.UI.NetCore/Views/Install/Index.cshtml new file mode 100644 index 0000000000..86b4ec4e40 --- /dev/null +++ b/src/Umbraco.Web.UI.NetCore/Views/Install/Index.cshtml @@ -0,0 +1,76 @@ +@using Umbraco.Web +@{ + Layout = null; +} + + + + + + + + Install Umbraco + + + + + + + + + + +
+ +
+ +
+
+

A server error occurred

+

This is most likely due to an error during application startup

+ +
+
+
+
+
+
+ + +
+

Did you know

+

+
+ +

{{installer.feedback}}

+ + + + + + + + diff --git a/src/Umbraco.Web.UI/Umbraco/Install/Views/Index.cshtml b/src/Umbraco.Web.UI/Umbraco/Install/Views/Index.cshtml index 6941fe0367..e4a70e8c62 100644 --- a/src/Umbraco.Web.UI/Umbraco/Install/Views/Index.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/Install/Views/Index.cshtml @@ -31,7 +31,7 @@ ng-cloak ng-animate="'fade'" ng-show="installer.configuring"> - +

A server error occurred

@@ -40,7 +40,7 @@
-
+
@@ -49,11 +49,11 @@

Did you know

- -

{{installer.feedback}}

- + +

{{installer.feedback}}

+