diff --git a/.gitignore b/.gitignore index d8c3f27d5a..99c21bd287 100644 --- a/.gitignore +++ b/.gitignore @@ -177,3 +177,5 @@ build/temp/ /src/Umbraco.Web.UI.NetCore/wwwroot/Umbraco/views/* /src/Umbraco.Web.UI.NetCore/wwwroot/App_Data/TEMP/* /src/Umbraco.Web.UI.NetCore/App_Data/Logs/* +/src/Umbraco.Web.UI.NetCore/App_Data/TEMP/TypesCache/* +/src/Umbraco.Web.UI.NetCore/App_Data/TEMP/* diff --git a/src/Umbraco.Web.Common/ApplicationModels/UmbracoApiBehaviorApplicationModelProvider.cs b/src/Umbraco.Web.Common/ApplicationModels/UmbracoApiBehaviorApplicationModelProvider.cs new file mode 100644 index 0000000000..d5268a884f --- /dev/null +++ b/src/Umbraco.Web.Common/ApplicationModels/UmbracoApiBehaviorApplicationModelProvider.cs @@ -0,0 +1,85 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core; +using Umbraco.Web.Common.Attributes; + +namespace Umbraco.Web.Common.ApplicationModels +{ + + /// + /// A custom application model provider for Umbraco controllers + /// + /// + /// + /// Conventions will be applied to controllers attributed with + /// + /// + /// This is nearly a copy of aspnetcore's ApiBehaviorApplicationModelProvider which supplies a convention for the + /// [ApiController] attribute, however that convention is too strict for our purposes so we will have our own. + /// + /// + /// See https://shazwazza.com/post/custom-body-model-binding-per-controller-in-asp-net-core/ + /// and https://github.com/dotnet/aspnetcore/issues/21724 + /// + /// + public class UmbracoApiBehaviorApplicationModelProvider : IApplicationModelProvider + { + public UmbracoApiBehaviorApplicationModelProvider(IModelMetadataProvider modelMetadataProvider) + { + // see see https://docs.microsoft.com/en-us/aspnet/core/web-api/?view=aspnetcore-3.1#apicontroller-attribute + // for what these things actually do + // NOTE: we don't have attribute routing requirements and we cannot use ApiVisibilityConvention without attribute routing + + ActionModelConventions = new List() + { + new ClientErrorResultFilterConvention(), // TODO: Need to determine exactly how this affects errors + new InvalidModelStateFilterConvention(), // automatically 400 responses if ModelState is invalid before hitting the controller + new ConsumesConstraintForFormFileParameterConvention(), // If an controller accepts files, it must accept multipart/form-data. + new InferParameterBindingInfoConvention(modelMetadataProvider), // no need for [FromBody] everywhere, A complex type parameter is assigned to FromBody + + // This ensures that all parameters of type BindingSource.Body (based on the above InferParameterBindingInfoConvention) are bound + // using our own UmbracoJsonModelBinder + new UmbracoJsonModelBinderConvention() + }; + + // TODO: Need to determine exactly how this affects errors + var defaultErrorType = typeof(ProblemDetails); + var defaultErrorTypeAttribute = new ProducesErrorResponseTypeAttribute(defaultErrorType); + ActionModelConventions.Add(new ApiConventionApplicationModelConvention(defaultErrorTypeAttribute)); + } + + /// + /// Will execute after + /// + public int Order => 0; + + public List ActionModelConventions { get; } + + public void OnProvidersExecuted(ApplicationModelProviderContext context) + { + } + + public void OnProvidersExecuting(ApplicationModelProviderContext context) + { + foreach (var controller in context.Result.Controllers) + { + if (!IsUmbracoApiController(controller)) + continue; + + foreach (var action in controller.Actions) + { + foreach (var convention in ActionModelConventions) + { + convention.Apply(action); + } + } + + } + } + + private bool IsUmbracoApiController(ControllerModel controller) => controller.Attributes.OfType().Any(); + } +} diff --git a/src/Umbraco.Web.Common/ApplicationModels/UmbracoJsonModelBinderConvention.cs b/src/Umbraco.Web.Common/ApplicationModels/UmbracoJsonModelBinderConvention.cs new file mode 100644 index 0000000000..96c60398f0 --- /dev/null +++ b/src/Umbraco.Web.Common/ApplicationModels/UmbracoJsonModelBinderConvention.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Umbraco.Web.Common.ModelBinding; +using System.Linq; + +namespace Umbraco.Web.Common.ApplicationModels +{ + /// + /// Applies the body model binder to any parameter binding source of type + /// + /// + /// For this to work Microsoft's own convention must be executed before this one + /// + public class UmbracoJsonModelBinderConvention : IActionModelConvention + { + public void Apply(ActionModel action) + { + foreach (var p in action.Parameters.Where(p => p.BindingInfo?.BindingSource == BindingSource.Body)) + { + p.BindingInfo.BinderType = typeof(UmbracoJsonModelBinder); + } + } + } +} diff --git a/src/Umbraco.Web.Common/Attributes/UmbracoApiControllerAttribute.cs b/src/Umbraco.Web.Common/Attributes/UmbracoApiControllerAttribute.cs new file mode 100644 index 0000000000..a3ffc3d9e9 --- /dev/null +++ b/src/Umbraco.Web.Common/Attributes/UmbracoApiControllerAttribute.cs @@ -0,0 +1,13 @@ +using System; +using Umbraco.Web.Common.ApplicationModels; + +namespace Umbraco.Web.Common.Attributes +{ + /// + /// When present on a controller then conventions will apply + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public sealed class UmbracoApiControllerAttribute : Attribute + { + } +} diff --git a/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs b/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs index 4a0bff7ffb..01300d1fa5 100644 --- a/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs +++ b/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs @@ -1,26 +1,26 @@ -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 Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Umbraco.Web.Common.Filters; using Umbraco.Web.Features; -using Umbraco.Web.Routing; -using Umbraco.Web.Security; using Umbraco.Web.WebApi.Filters; +using Umbraco.Web.Common.Attributes; namespace Umbraco.Web.Common.Controllers { /// /// Provides a base class for Umbraco API controllers. /// - /// These controllers are NOT auto-routed. + /// + /// These controllers are NOT auto-routed. + /// The base class is which are netcore API controllers without any view support + /// [FeatureAuthorize] - public abstract class UmbracoApiControllerBase : Controller, IUmbracoFeature + [TypeFilter(typeof(HttpResponseExceptionFilter))] + [UmbracoApiController] + public abstract class UmbracoApiControllerBase : ControllerBase, IUmbracoFeature { - + public UmbracoApiControllerBase() + { + } } } diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoCommonApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs similarity index 62% rename from src/Umbraco.Web.Common/Extensions/UmbracoCommonApplicationBuilderExtensions.cs rename to src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs index 8386a1d8fe..47cee99264 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoCommonApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs @@ -7,8 +7,13 @@ using Umbraco.Web.Common.Middleware; namespace Umbraco.Extensions { - public static class UmbracoCommonApplicationBuilderExtensions + public static class ApplicationBuilderExtensions { + /// + /// Returns true if Umbraco is greater than + /// + /// + /// public static bool UmbracoCanBoot(this IApplicationBuilder app) { var runtime = app.ApplicationServices.GetRequiredService(); @@ -16,8 +21,13 @@ namespace Umbraco.Extensions return runtime.State.Level > RuntimeLevel.BootFailed; } + /// + /// Enables middlewares required to run Umbraco + /// + /// + /// // TODO: Could be internal or part of another call - this is a required system so should't be 'opt-in' - public static IApplicationBuilder UseUmbracoRequestLifetime(this IApplicationBuilder app) + public static IApplicationBuilder UseUmbracoRouting(this IApplicationBuilder app) { if (app == null) throw new ArgumentNullException(nameof(app)); diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs index 256bc1a78c..5078fa5f22 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs @@ -1,6 +1,5 @@ using System; using System.Collections; -using System.Collections.Generic; using System.Data.Common; using System.Data.SqlClient; using System.IO; @@ -13,8 +12,6 @@ using Microsoft.Extensions.Logging; using Serilog; using Serilog.Extensions.Hosting; using Serilog.Extensions.Logging; -using Smidge; -using Smidge.Nuglify; using Umbraco.Composing; using Umbraco.Configuration; using Umbraco.Core; @@ -28,12 +25,19 @@ using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Runtime; using Umbraco.Web.Common.AspNetCore; -using Umbraco.Web.Common.Runtime.Profiler; +using Umbraco.Web.Common.Profiler; namespace Umbraco.Extensions { + + public static class UmbracoCoreServiceCollectionExtensions { + /// + /// Adds SqlCe support for Umbraco + /// + /// + /// public static IServiceCollection AddUmbracoSqlCeSupport(this IServiceCollection services) { try @@ -60,7 +64,7 @@ namespace Umbraco.Extensions var sqlCe = sqlCeAssembly.GetType("System.Data.SqlServerCe.SqlCeProviderFactory"); if (!(sqlCe is null)) { - DbProviderFactories.RegisterFactory(Core.Constants.DbProviderNames.SqlCe, sqlCe ); + DbProviderFactories.RegisterFactory(Core.Constants.DbProviderNames.SqlCe, sqlCe); } } } @@ -72,6 +76,11 @@ namespace Umbraco.Extensions return services; } + /// + /// Adds Sql Server support for Umbraco + /// + /// + /// public static IServiceCollection AddUmbracoSqlServerSupport(this IServiceCollection services) { DbProviderFactories.RegisterFactory(Core.Constants.DbProviderNames.SqlServer, SqlClientFactory.Instance); @@ -102,7 +111,6 @@ namespace Umbraco.Extensions return services; } - /// /// Adds the Umbraco Back Core requirements /// @@ -111,7 +119,7 @@ namespace Umbraco.Extensions /// public static IServiceCollection AddUmbracoCore(this IServiceCollection services, IWebHostEnvironment webHostEnvironment) { - return services.AddUmbracoCore(webHostEnvironment,out _); + return services.AddUmbracoCore(webHostEnvironment, out _); } /// @@ -198,7 +206,7 @@ namespace Umbraco.Extensions configs, webHostEnvironment, loggingConfiguration, - out var logger, out var ioHelper, out var hostingEnvironment, out var backOfficeInfo, out var profiler); + out var logger, out var ioHelper, out var hostingEnvironment, out var backOfficeInfo, out var profiler); var globalSettings = configs.Global(); var umbracoVersion = new UmbracoVersion(globalSettings); @@ -219,8 +227,7 @@ namespace Umbraco.Extensions factory = coreRuntime.Configure(container); return services; - } - + } private static ITypeFinder CreateTypeFinder(Core.Logging.ILogger logger, IProfiler profiler, IWebHostEnvironment webHostEnvironment, Assembly entryAssembly, ITypeFinderSettings typeFinderSettings) { @@ -322,15 +329,6 @@ namespace Umbraco.Extensions return logger; } - public static IServiceCollection AddUmbracoRuntimeMinifier(this IServiceCollection services, - IConfiguration configuration) - { - services.AddSmidge(configuration.GetSection(Core.Constants.Configuration.ConfigRuntimeMinification)); - services.AddSmidgeNuglify(); - - return services; - } - private static IProfiler GetWebProfiler(Umbraco.Core.Hosting.IHostingEnvironment hostingEnvironment) { // create and start asap to profile boot @@ -346,6 +344,7 @@ namespace Umbraco.Extensions return webProfiler; } + private class AspNetCoreBootPermissionsChecker : IUmbracoBootPermissionChecker { public void ThrowIfNotPermissions() @@ -354,7 +353,6 @@ namespace Umbraco.Extensions } } - } } diff --git a/src/Umbraco.Web.Website/AspNetCore/UmbracoWebsiteServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoWebServiceCollectionExtensions.cs similarity index 53% rename from src/Umbraco.Web.Website/AspNetCore/UmbracoWebsiteServiceCollectionExtensions.cs rename to src/Umbraco.Web.Common/Extensions/UmbracoWebServiceCollectionExtensions.cs index 3a327ef867..fb7d379ae5 100644 --- a/src/Umbraco.Web.Website/AspNetCore/UmbracoWebsiteServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoWebServiceCollectionExtensions.cs @@ -1,21 +1,42 @@ +using System.Buffers; using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Web.Caching; using SixLabors.ImageSharp.Web.Commands; using SixLabors.ImageSharp.Web.DependencyInjection; using SixLabors.ImageSharp.Web.Processors; using SixLabors.ImageSharp.Web.Providers; - +using Smidge; +using Smidge.Nuglify; using Umbraco.Core; using Umbraco.Core.Configuration; +using Umbraco.Web.Common.ApplicationModels; +using Umbraco.Web.Common.ModelBinding; -namespace Umbraco.Web.Website.AspNetCore +namespace Umbraco.Extensions { - public static class UmbracoBackOfficeServiceCollectionExtensions + public static class UmbracoWebServiceCollectionExtensions { - public static IServiceCollection AddUmbracoWebsite(this IServiceCollection services) + /// + /// Registers the web components needed for Umbraco + /// + /// + /// + public static IServiceCollection AddUmbracoWebComponents(this IServiceCollection services) { + services.TryAddSingleton(); + services.ConfigureOptions(); + services.TryAddEnumerable(ServiceDescriptor.Transient()); + // TODO: We need to avoid this, surely there's a way? See ContainerTests.BuildServiceProvider_Before_Host_Is_Configured var serviceProvider = services.BuildServiceProvider(); var configs = serviceProvider.GetService(); @@ -25,10 +46,14 @@ namespace Umbraco.Web.Website.AspNetCore return services; } + /// + /// Adds Image Sharp with Umbraco settings + /// + /// + /// + /// public static IServiceCollection AddUmbracoImageSharp(this IServiceCollection services, IImagingSettings imagingSettings) { - - services.AddImageSharpCore( options => { @@ -61,6 +86,21 @@ namespace Umbraco.Web.Website.AspNetCore return services; } + /// + /// Adds the Umbraco runtime minifier + /// + /// + /// + /// + public static IServiceCollection AddUmbracoRuntimeMinifier(this IServiceCollection services, + IConfiguration configuration) + { + services.AddSmidge(configuration.GetSection(Core.Constants.Configuration.ConfigRuntimeMinification)); + services.AddSmidgeNuglify(); + + return services; + } + private static void RemoveIntParamenterIfValueGreatherThen(IDictionary commands, string parameter, int maxValue) { if (commands.TryGetValue(parameter, out var command)) @@ -74,6 +114,30 @@ namespace Umbraco.Web.Website.AspNetCore } } } + + /// + /// Options for globally configuring MVC for Umbraco + /// + /// + /// We generally don't want to change the global MVC settings since we want to be unobtrusive as possible but some + /// global mods are needed - so long as they don't interfere with normal user usages of MVC. + /// + private class UmbracoMvcConfigureOptions : IConfigureOptions + { + + // TODO: we can inject params with DI here + public UmbracoMvcConfigureOptions() + { + } + + // TODO: we can configure global mvc options here if we need to + public void Configure(MvcOptions options) + { + + } + } + + } } diff --git a/src/Umbraco.Web.Common/Attributes/AngularJsonOnlyConfigurationAttribute.cs b/src/Umbraco.Web.Common/Filters/AngularJsonOnlyConfigurationAttribute.cs similarity index 97% rename from src/Umbraco.Web.Common/Attributes/AngularJsonOnlyConfigurationAttribute.cs rename to src/Umbraco.Web.Common/Filters/AngularJsonOnlyConfigurationAttribute.cs index f0281abcba..192b5e7df0 100644 --- a/src/Umbraco.Web.Common/Attributes/AngularJsonOnlyConfigurationAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/AngularJsonOnlyConfigurationAttribute.cs @@ -9,7 +9,7 @@ using Microsoft.Extensions.Options; using Newtonsoft.Json; using Umbraco.Web.Common.Formatters; -namespace Umbraco.Web.Common.Attributes +namespace Umbraco.Web.Common.Filters { /// /// Applying this attribute to any controller will ensure that it only contains one json formatter compatible with the angular json vulnerability prevention. @@ -38,4 +38,5 @@ namespace Umbraco.Web.Common.Attributes base.OnResultExecuting(context); } } + } diff --git a/src/Umbraco.Web.Common/Attributes/FeatureAuthorizeAttribute.cs b/src/Umbraco.Web.Common/Filters/FeatureAuthorizeAttribute.cs similarity index 100% rename from src/Umbraco.Web.Common/Attributes/FeatureAuthorizeAttribute.cs rename to src/Umbraco.Web.Common/Filters/FeatureAuthorizeAttribute.cs diff --git a/src/Umbraco.Web.Common/Install/InstallApiController.cs b/src/Umbraco.Web.Common/Install/InstallApiController.cs index d9771fd7f8..ca3596e93c 100644 --- a/src/Umbraco.Web.Common/Install/InstallApiController.cs +++ b/src/Umbraco.Web.Common/Install/InstallApiController.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; using Newtonsoft.Json.Linq; using Umbraco.Core; using Umbraco.Core.Logging; @@ -11,15 +12,20 @@ using Umbraco.Core.Migrations.Install; using Umbraco.Net; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Exceptions; +using Umbraco.Web.Common.Filters; +using Umbraco.Web.Common.ModelBinding; using Umbraco.Web.Install; using Umbraco.Web.Install.Models; namespace Umbraco.Web.Common.Install { + + [UmbracoApiController] + [TypeFilter(typeof(HttpResponseExceptionFilter))] [TypeFilter(typeof(AngularJsonOnlyConfigurationAttribute))] [HttpInstallAuthorize] [Area("Install")] - public class InstallApiController : Controller + public class InstallApiController : ControllerBase { private readonly DatabaseBuilder _databaseBuilder; private readonly InstallStatusTracker _installStatusTracker; @@ -41,6 +47,7 @@ namespace Umbraco.Web.Common.Install _logger = _proflog; } + internal InstallHelper InstallHelper { get; } public bool PostValidateDatabaseConnection(DatabaseModel model) @@ -88,9 +95,8 @@ namespace Umbraco.Web.Common.Install /// /// Installs. - /// - [HttpPost] - public async Task PostPerformInstall([FromBody] InstallInstructions installModel) + /// + public async Task PostPerformInstall(InstallInstructions installModel) { if (installModel == null) throw new ArgumentNullException(nameof(installModel)); diff --git a/src/Umbraco.Web.Common/Install/InstallController.cs b/src/Umbraco.Web.Common/Install/InstallController.cs index 88171820ec..07f0376697 100644 --- a/src/Umbraco.Web.Common/Install/InstallController.cs +++ b/src/Umbraco.Web.Common/Install/InstallController.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Hosting; @@ -11,6 +12,7 @@ using Umbraco.Web.Security; namespace Umbraco.Web.Common.Install { + /// /// The MVC Installation controller /// @@ -49,7 +51,7 @@ namespace Umbraco.Web.Common.Install [HttpGet] [StatusCodeResult(System.Net.HttpStatusCode.ServiceUnavailable)] [TypeFilter(typeof(StatusCodeResultAttribute), Arguments = new object []{System.Net.HttpStatusCode.ServiceUnavailable})] - public ActionResult Index() + public async Task Index() { if (_runtime.Level == RuntimeLevel.Run) return Redirect(_globalSettings.UmbracoPath.EnsureEndsWith('/')); @@ -77,7 +79,7 @@ namespace Umbraco.Web.Common.Install ViewData.SetUmbracoVersion(_umbracoVersion.SemanticVersion); - _installHelper.InstallStatus(false, ""); + await _installHelper.InstallStatus(false, ""); // always ensure full path (see NOTE in the class remarks) return View(); diff --git a/src/Umbraco.Web.Common/Middleware/UmbracoRequestLoggingMiddleware.cs b/src/Umbraco.Web.Common/Middleware/UmbracoRequestLoggingMiddleware.cs index 0e2158c939..b1d2d01f9d 100644 --- a/src/Umbraco.Web.Common/Middleware/UmbracoRequestLoggingMiddleware.cs +++ b/src/Umbraco.Web.Common/Middleware/UmbracoRequestLoggingMiddleware.cs @@ -1,9 +1,10 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; using Serilog.Context; -using Umbraco.Core.Cache; +using Umbraco.Core; using Umbraco.Core.Logging.Serilog.Enrichers; -using Umbraco.Net; namespace Umbraco.Web.Common.Middleware { @@ -27,6 +28,13 @@ namespace Umbraco.Web.Common.Middleware public async Task Invoke(HttpContext httpContext) { + // do not process if client-side request + if (new Uri(httpContext.Request.GetEncodedUrl(), UriKind.RelativeOrAbsolute).IsClientSideRequest()) + { + await _next(httpContext); + return; + } + // TODO: Need to decide if we want this stuff still, there's new request logging in serilog: // https://github.com/serilog/serilog-aspnetcore#request-logging which i think would suffice and replace all of this? diff --git a/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs b/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs index e8695f3c9c..85cf6607cc 100644 --- a/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs +++ b/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs @@ -1,25 +1,66 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; using Umbraco.Web.Common.Lifetime; +using Umbraco.Core; +using Umbraco.Core.Logging; namespace Umbraco.Web.Common.Middleware { + /// + /// Manages Umbraco request objects and their lifetime + /// public class UmbracoRequestMiddleware { private readonly RequestDelegate _next; + private readonly ILogger _logger; private readonly IUmbracoRequestLifetimeManager _umbracoRequestLifetimeManager; - public UmbracoRequestMiddleware(RequestDelegate next, IUmbracoRequestLifetimeManager umbracoRequestLifetimeManager) + private readonly IUmbracoContextFactory _umbracoContextFactory; + + public UmbracoRequestMiddleware(RequestDelegate next, + ILogger logger, + IUmbracoRequestLifetimeManager umbracoRequestLifetimeManager, + IUmbracoContextFactory umbracoContextFactory) { _next = next; + _logger = logger; _umbracoRequestLifetimeManager = umbracoRequestLifetimeManager; + _umbracoContextFactory = umbracoContextFactory; } public async Task InvokeAsync(HttpContext context) { - _umbracoRequestLifetimeManager.InitRequest(context); - await _next(context); - _umbracoRequestLifetimeManager.EndRequest(context); + // do not process if client-side request + + if (new Uri(context.Request.GetEncodedUrl(), UriKind.RelativeOrAbsolute).IsClientSideRequest()) + { + await _next(context); + return; + } + + var umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext(); + + try + { + try + { + _umbracoRequestLifetimeManager.InitRequest(context); + } + catch (Exception ex) + { + // try catch so we don't kill everything in all requests + _logger.Error(ex); + } + + await _next(context); + + _umbracoRequestLifetimeManager.EndRequest(context); + } + finally + { + umbracoContextReference.Dispose(); + } } } } diff --git a/src/Umbraco.Web.Common/ModelBinding/UmbracoJsonModelBinderProvider.cs b/src/Umbraco.Web.Common/ModelBinding/UmbracoJsonModelBinderProvider.cs new file mode 100644 index 0000000000..7ce781ba45 --- /dev/null +++ b/src/Umbraco.Web.Common/ModelBinding/UmbracoJsonModelBinderProvider.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ObjectPool; +using System.Buffers; + +namespace Umbraco.Web.Common.ModelBinding +{ + /// + /// A custom body model binder that only uses a to bind body action parameters + /// + public class UmbracoJsonModelBinder : BodyModelBinder, IModelBinder + { + public UmbracoJsonModelBinder(ArrayPool arrayPool, ObjectPoolProvider objectPoolProvider, IHttpRequestStreamReaderFactory readerFactory, ILoggerFactory loggerFactory) + : base(GetNewtonsoftJsonFormatter(loggerFactory, arrayPool, objectPoolProvider), readerFactory, loggerFactory) + { + } + + private static IInputFormatter[] GetNewtonsoftJsonFormatter(ILoggerFactory logger, ArrayPool arrayPool, ObjectPoolProvider objectPoolProvider) + { + var jsonOptions = new MvcNewtonsoftJsonOptions + { + AllowInputFormatterExceptionMessages = true + }; + return new IInputFormatter[] + { + new NewtonsoftJsonInputFormatter( + logger.CreateLogger(), + jsonOptions.SerializerSettings, // Just use the defaults + arrayPool, + objectPoolProvider, + new MvcOptions(), // The only option that NewtonsoftJsonInputFormatter uses is SuppressInputFormatterBuffering + jsonOptions) + }; + } + } +} diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs b/src/Umbraco.Web.Common/Profiler/WebProfiler.cs similarity index 94% rename from src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs rename to src/Umbraco.Web.Common/Profiler/WebProfiler.cs index 958e134bab..30777d07a5 100644 --- a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs +++ b/src/Umbraco.Web.Common/Profiler/WebProfiler.cs @@ -7,8 +7,7 @@ using StackExchange.Profiling; using Umbraco.Core; using Umbraco.Core.Logging; -// TODO: This namespace is strange, not sure why i has "Runtime" in the name? -namespace Umbraco.Web.Common.Runtime.Profiler +namespace Umbraco.Web.Common.Profiler { public class WebProfiler : IProfiler @@ -45,9 +44,7 @@ namespace Umbraco.Web.Common.Runtime.Profiler public void UmbracoApplicationBeginRequest(HttpContext context) { if (ShouldProfile(context.Request)) - { Start(); - } } public void UmbracoApplicationEndRequest(HttpContext context) diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs b/src/Umbraco.Web.Common/Profiler/WebProfilerComponent.cs similarity index 98% rename from src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs rename to src/Umbraco.Web.Common/Profiler/WebProfilerComponent.cs index a36753e634..bc5cce9df1 100644 --- a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs +++ b/src/Umbraco.Web.Common/Profiler/WebProfilerComponent.cs @@ -5,7 +5,7 @@ using Umbraco.Net; using Umbraco.Web.Common.Lifetime; using Umbraco.Web.Common.Middleware; -namespace Umbraco.Web.Common.Runtime.Profiler +namespace Umbraco.Web.Common.Profiler { internal sealed class WebProfilerComponent : IComponent { diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComposer.cs b/src/Umbraco.Web.Common/Profiler/WebProfilerComposer.cs similarity index 88% rename from src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComposer.cs rename to src/Umbraco.Web.Common/Profiler/WebProfilerComposer.cs index 523faf2da5..edb3db6f85 100644 --- a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComposer.cs +++ b/src/Umbraco.Web.Common/Profiler/WebProfilerComposer.cs @@ -1,7 +1,7 @@ using Umbraco.Core; using Umbraco.Core.Composing; -namespace Umbraco.Web.Common.Runtime.Profiler +namespace Umbraco.Web.Common.Profiler { internal class WebProfilerComposer : ComponentComposer, ICoreComposer { diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerHtml.cs b/src/Umbraco.Web.Common/Profiler/WebProfilerHtml.cs similarity index 93% rename from src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerHtml.cs rename to src/Umbraco.Web.Common/Profiler/WebProfilerHtml.cs index 9e989d6b5c..40c245dd5a 100644 --- a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerHtml.cs +++ b/src/Umbraco.Web.Common/Profiler/WebProfilerHtml.cs @@ -5,8 +5,7 @@ using StackExchange.Profiling; using StackExchange.Profiling.Internal; using Umbraco.Core.Logging; -// TODO: This namespace is strange, not sure why i has "Runtime" in the name? -namespace Umbraco.Web.Common.Runtime.Profiler +namespace Umbraco.Web.Common.Profiler { public class WebProfilerHtml : IProfilerHtml { diff --git a/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs b/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs index 30d7cdaced..78068b551c 100644 --- a/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs +++ b/src/Umbraco.Web.Common/Runtime/AspNetCoreComposer.cs @@ -12,8 +12,8 @@ using Umbraco.Web.Common.Macros; using Umbraco.Web.Composing.CompositionExtensions; using Umbraco.Web.Macros; using Umbraco.Core.Diagnostics; -using Umbraco.Web.Common.Runtime.Profiler; using Umbraco.Core.Logging; +using Umbraco.Web.Common.Profiler; namespace Umbraco.Web.Common.Runtime { diff --git a/src/Umbraco.Web.Common/Security/WebSecurity.cs b/src/Umbraco.Web.Common/Security/WebSecurity.cs new file mode 100644 index 0000000000..5f54d2e9ee --- /dev/null +++ b/src/Umbraco.Web.Common/Security/WebSecurity.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Umbraco.Core; +using Umbraco.Core.Models.Membership; +using Umbraco.Web.Security; + +namespace Umbraco.Web.Common.Security +{ + // TODO: need to implement this + + public class WebSecurity : IWebSecurity + { + public IUser CurrentUser => throw new NotImplementedException(); + + public ValidateRequestAttempt AuthorizeRequest(bool throwExceptions = false) + { + throw new NotImplementedException(); + } + + public void ClearCurrentLogin() + { + throw new NotImplementedException(); + } + + public Attempt GetUserId() + { + throw new NotImplementedException(); + } + + public bool IsAuthenticated() + { + throw new NotImplementedException(); + } + + public double PerformLogin(int userId) + { + throw new NotImplementedException(); + } + + public bool UserHasSectionAccess(string section, IUser user) + { + throw new NotImplementedException(); + } + + public bool ValidateCurrentUser() + { + throw new NotImplementedException(); + } + + public ValidateRequestAttempt ValidateCurrentUser(bool throwExceptions, bool requiresApproval = true) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index eed88bc491..9f6509cd82 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -21,6 +21,7 @@ + diff --git a/src/Umbraco.Web.Common/UmbracoContext/UmbracoContextFactory.cs b/src/Umbraco.Web.Common/UmbracoContext/UmbracoContextFactory.cs index 97f009968c..89751bfa08 100644 --- a/src/Umbraco.Web.Common/UmbracoContext/UmbracoContextFactory.cs +++ b/src/Umbraco.Web.Common/UmbracoContext/UmbracoContextFactory.cs @@ -6,6 +6,7 @@ using Umbraco.Core.Configuration; using Umbraco.Core.Hosting; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Services; +using Umbraco.Web.Common.Security; using Umbraco.Web.PublishedCache; using Umbraco.Web.Security; @@ -16,8 +17,6 @@ namespace Umbraco.Web /// public class UmbracoContextFactory : IUmbracoContextFactory { - private static readonly NullWriter NullWriterInstance = new NullWriter(); - private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly IPublishedSnapshotService _publishedSnapshotService; private readonly IVariationContextAccessor _variationContextAccessor; @@ -73,7 +72,7 @@ namespace Umbraco.Web _variationContextAccessor.VariationContext = new VariationContext(_defaultCultureAccessor.DefaultCulture); } - IWebSecurity webSecurity = null; // TODO, we need to when users are migrated + IWebSecurity webSecurity = new WebSecurity(); return new UmbracoContext( _publishedSnapshotService, diff --git a/src/Umbraco.Web.UI.Client/src/installer/installer.service.js b/src/Umbraco.Web.UI.Client/src/installer/installer.service.js index 7cebaaada3..8658e6e67e 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/installer.service.js +++ b/src/Umbraco.Web.UI.Client/src/installer/installer.service.js @@ -336,7 +336,11 @@ angular.module("umbraco.install").factory('installerService', function ($rootSco if (status >= 500 && status < 600) { service.status.current = { view: "ysod", model: null }; var ysod = data; - //we need to manually write the html to the iframe - the html contains full html markup + //we need to manually write the html to the iframe + // TODO: In dotnetcore the resulting YSOD isn't HTML, the error is just a string so it looks ugly + // So we shouldn't be using an iframe and will need to change this so that we have an unhandled exception filter for the installer (and eventually + // the rest of the back office) to handle errors and chuck the data into a json format for us to use. + // It might turn out that our new Api Convention `UmbracoApiBehaviorApplicationModelProvider` might handle this for us with it's custom error handling. $timeout(function () { document.getElementById('ysod').contentDocument.write(ysod); }, 500); diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index f74208bac6..bd23ef4229 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -53,20 +53,10 @@ namespace Umbraco.Web.UI.BackOffice { services.AddUmbracoConfiguration(_config); services.AddUmbracoCore(_env, out var factory); - services.AddUmbracoWebsite(); + services.AddUmbracoWebComponents(); services.AddUmbracoRuntimeMinifier(_config); - services.AddMvc(options => - { - options.Filters.Add(); - - }).SetCompatibilityVersion(CompatibilityVersion.Version_3_0) - .AddNewtonsoftJson(options => - { - options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); - - }) - ; + services.AddMvc(); services.AddMiniProfiler(options => { @@ -90,19 +80,24 @@ namespace Umbraco.Web.UI.BackOffice public void Configure(IApplicationBuilder app) { - //app.UseMiniProfiler(); - app.UseUmbracoRequestLifetime(); + //app.UseMiniProfiler(); if (_env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } + app.UseStatusCodePages(); + app.UseRouting(); + + + app.UseUmbracoRouting(); app.UseUmbracoCore(); app.UseUmbracoRequestLogging(); app.UseUmbracoWebsite(); - app.UseUmbracoBackOffice(); - app.UseRouting(); + app.UseUmbracoBackOffice(); app.UseUmbracoRuntimeMinification(); + + app.UseEndpoints(endpoints => { endpoints.MapControllerRoute("Backoffice", "/umbraco/{Action}", new @@ -111,7 +106,7 @@ namespace Umbraco.Web.UI.BackOffice Action = "Default" }); - + // TODO: Fix this routing with an area endpoints.MapControllerRoute("Install", "/install/{controller}/{Action}", defaults:new { Area = "Install"}); //TODO register routing correct: Name must be like this