diff --git a/src/Umbraco.Core/Net/IUmbracoApplicationLifetime.cs b/src/Umbraco.Core/Net/IUmbracoApplicationLifetime.cs index 10d5b10955..4010252571 100644 --- a/src/Umbraco.Core/Net/IUmbracoApplicationLifetime.cs +++ b/src/Umbraco.Core/Net/IUmbracoApplicationLifetime.cs @@ -1,3 +1,5 @@ +using System; + namespace Umbraco.Net { public interface IUmbracoApplicationLifetime @@ -10,5 +12,7 @@ namespace Umbraco.Net /// Terminates the current application. The application restarts the next time a request is received for it. /// void Restart(); + + event EventHandler ApplicationInit; } } diff --git a/src/Umbraco.Core/UriExtensions.cs b/src/Umbraco.Core/UriExtensions.cs index 4321aefd7f..e7be0b6500 100644 --- a/src/Umbraco.Core/UriExtensions.cs +++ b/src/Umbraco.Core/UriExtensions.cs @@ -146,7 +146,7 @@ namespace Umbraco.Core /// /// /// - internal static bool IsClientSideRequest(this Uri url) + public static bool IsClientSideRequest(this Uri url) { try { diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 29d69db0d2..7dd0d6592b 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -99,7 +99,7 @@ - + diff --git a/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs b/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs index 20cfef352d..f597c40252 100644 --- a/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs +++ b/src/Umbraco.Web.BackOffice/AspNetCore/AspNetCoreUmbracoApplicationLifetime.cs @@ -1,3 +1,4 @@ +using System; using System.Threading; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Hosting; @@ -14,6 +15,11 @@ namespace Umbraco.Web.AspNet { _httpContextAccessor = httpContextAccessor; _hostApplicationLifetime = hostApplicationLifetime; + + hostApplicationLifetime.ApplicationStarted.Register(() => + { + ApplicationInit?.Invoke(this, EventArgs.Empty); + }); } public bool IsRestarting { get; set; } @@ -32,5 +38,7 @@ namespace Umbraco.Web.AspNet Thread.CurrentPrincipal = null; _hostApplicationLifetime.StopApplication(); } + + public event EventHandler ApplicationInit; } } diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoRequestApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoRequestApplicationBuilderExtensions.cs new file mode 100644 index 0000000000..110c09e1a9 --- /dev/null +++ b/src/Umbraco.Web.Common/Extensions/UmbracoRequestApplicationBuilderExtensions.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Umbraco.Web.Common.Middleware; + +namespace Umbraco.Web.Common.Extensions +{ + public static class UmbracoRequestApplicationBuilderExtensions + { + public static IApplicationBuilder UseUmbracoRequest(this IApplicationBuilder app) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + app.UseMiddleware(); + + return app; + } + } +} diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoRequestServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoRequestServiceCollectionExtensions.cs new file mode 100644 index 0000000000..2a16b8b4f9 --- /dev/null +++ b/src/Umbraco.Web.Common/Extensions/UmbracoRequestServiceCollectionExtensions.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Web.Common.Middleware; + +namespace Umbraco.Web.Common.Extensions +{ + public static class UmbracoRequestServiceCollectionExtensions + { + public static IServiceCollection AddUmbracoRequest(this IServiceCollection services) + { + var umbracoRequestLifetime = new UmbracoRequestLifetime(); + + services.AddSingleton(umbracoRequestLifetime); + services.AddSingleton(umbracoRequestLifetime); + + return services; + } + + } + +} diff --git a/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs b/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs new file mode 100644 index 0000000000..60b6463152 --- /dev/null +++ b/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs @@ -0,0 +1,52 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Umbraco.Web.Common.Middleware +{ + public class UmbracoRequestMiddleware + { + private readonly RequestDelegate _next; + private readonly IUmbracoRequestLifetimeManager _umbracoRequestLifetimeManager; + public UmbracoRequestMiddleware(RequestDelegate next, IUmbracoRequestLifetimeManager umbracoRequestLifetimeManager) + { + _next = next; + _umbracoRequestLifetimeManager = umbracoRequestLifetimeManager; + } + + public async Task InvokeAsync(HttpContext context) + { + _umbracoRequestLifetimeManager.InitRequest(context); + await _next(context); + _umbracoRequestLifetimeManager.EndRequest(context); + } + } + + public interface IUmbracoRequestLifetime + { + event EventHandler RequestStart; + event EventHandler RequestEnd; + } + + public class UmbracoRequestLifetime : IUmbracoRequestLifetime, IUmbracoRequestLifetimeManager + { + public event EventHandler RequestStart; + public event EventHandler RequestEnd; + + public void InitRequest(HttpContext context) + { + RequestStart?.Invoke(this, context); + } + + public void EndRequest(HttpContext context) + { + RequestEnd?.Invoke(this, context); + } + } + + public interface IUmbracoRequestLifetimeManager + { + void InitRequest(HttpContext context); + void EndRequest(HttpContext context); + } +} diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs new file mode 100644 index 0000000000..4e1a7c7d84 --- /dev/null +++ b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfiler.cs @@ -0,0 +1,81 @@ +using System; +using System.Threading; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using StackExchange.Profiling; +using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Logging; +using Umbraco.Web.Common.Middleware; + +namespace Umbraco.Web.Common.Runtime.Profiler +{ + public class WebProfiler : IProfiler + { + private const string BootRequestItemKey = "Umbraco.Core.Logging.WebProfiler__isBootRequest"; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly WebProfilerProvider _provider; + private int _first; + + public WebProfiler(IHttpContextAccessor httpContextAccessor) + { + // create our own provider, which can provide a profiler even during boot + _provider = new WebProfilerProvider(); + _httpContextAccessor = httpContextAccessor; + } + + public string Render() => MiniProfiler.Current + .RenderIncludes(_httpContextAccessor.HttpContext, RenderPosition.Right).ToString(); + + public IDisposable Step(string name) => MiniProfiler.Current?.Step(name); + + public void Start() + { + MiniProfiler.StartNew(); + } + + public void Stop(bool discardResults = false) + { + MiniProfiler.Current?.Stop(discardResults); + } + + public void UmbracoApplicationBeginRequest(HttpContext context) + { + // if this is the first request, notify our own provider that this request is the boot request + var first = Interlocked.Exchange(ref _first, 1) == 0; + if (first) + { + _provider.BeginBootRequest(); + context.Items[BootRequestItemKey] = true; + // and no need to start anything, profiler is already there + } + // else start a profiler, the normal way + else if (ShouldProfile(context.Request)) + Start(); + } + + public void UmbracoApplicationEndRequest(HttpContext context) + { + // if this is the boot request, or if we should profile this request, stop + // (the boot request is always profiled, no matter what) + if (context.Items.TryGetValue(BootRequestItemKey, out var isBoot) && isBoot.Equals(true)) + { + _provider.EndBootRequest(); + Stop(); + } + else if (ShouldProfile(context.Request)) + { + Stop(); + } + } + + private static bool ShouldProfile(HttpRequest request) + { + if (new Uri(request.GetEncodedUrl(), UriKind.RelativeOrAbsolute).IsClientSideRequest()) return false; + if (bool.TryParse(request.Query["umbDebug"], out var umbDebug)) return umbDebug; + if (bool.TryParse(request.Headers["X-UMB-DEBUG"], out var xUmbDebug)) return xUmbDebug; + if (bool.TryParse(request.Cookies["UMB-DEBUG"], out var cUmbDebug)) return cUmbDebug; + return false; + } + } +} diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs new file mode 100644 index 0000000000..425996a765 --- /dev/null +++ b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComponent.cs @@ -0,0 +1,55 @@ +using System; +using Umbraco.Core.Composing; +using Umbraco.Core.Logging; +using Umbraco.Net; +using Umbraco.Web.Common.Middleware; + +namespace Umbraco.Web.Common.Runtime.Profiler +{ + internal sealed class WebProfilerComponent : IComponent + { + private readonly bool _profile; + private readonly WebProfiler _profiler; + private readonly IUmbracoApplicationLifetime _umbracoApplicationLifetime; + private readonly IUmbracoRequestLifetime _umbracoRequestLifetime; + + public WebProfilerComponent(IProfiler profiler, ILogger logger, IUmbracoRequestLifetime umbracoRequestLifetime, + IUmbracoApplicationLifetime umbracoApplicationLifetime) + { + _umbracoRequestLifetime = umbracoRequestLifetime; + _umbracoApplicationLifetime = umbracoApplicationLifetime; + _profile = true; + + // although registered in WebRuntime.Compose, ensure that we have not + // been replaced by another component, and we are still "the" profiler + _profiler = profiler as WebProfiler; + if (_profiler != null) return; + + // if VoidProfiler was registered, let it be known + if (profiler is VoidProfiler) + logger.Info( + "Profiler is VoidProfiler, not profiling (must run debug mode to profile)."); + _profile = false; + } + + public void Initialize() + { + if (!_profile) return; + + // bind to ApplicationInit - ie execute the application initialization for *each* application + // it would be a mistake to try and bind to the current application events + _umbracoApplicationLifetime.ApplicationInit += InitializeApplication; + } + + public void Terminate() + { + } + + private void InitializeApplication(object sender, EventArgs args) + { + _umbracoRequestLifetime.RequestStart += + (sender, context) => _profiler.UmbracoApplicationBeginRequest(context); + _umbracoRequestLifetime.RequestEnd += (sender, context) => _profiler.UmbracoApplicationEndRequest(context); + } + } +} diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComposer.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComposer.cs new file mode 100644 index 0000000000..688a3e5c28 --- /dev/null +++ b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerComposer.cs @@ -0,0 +1,8 @@ +using Umbraco.Core.Composing; + +namespace Umbraco.Web.Common.Runtime.Profiler +{ + internal class WebProfilerComposer : ComponentComposer, ICoreComposer + { + } +} diff --git a/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerProvider.cs b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerProvider.cs new file mode 100644 index 0000000000..99ad6f724c --- /dev/null +++ b/src/Umbraco.Web.Common/Runtime/Profiler/WebProfilerProvider.cs @@ -0,0 +1,119 @@ +using System; +using System.Threading; +using StackExchange.Profiling; +using StackExchange.Profiling.Internal; +using Umbraco.Core.Cache; + +namespace Umbraco.Web.Common.Runtime.Profiler +{ + public class WebProfilerProvider : DefaultProfilerProvider + { + private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(); + private volatile BootPhase _bootPhase; + private int _first; + private MiniProfiler _startupProfiler; + + public WebProfilerProvider() + { + // booting... + _bootPhase = BootPhase.Boot; + } + + + /// + /// Gets the current profiler. + /// + /// + /// If the boot phase is not Booted, then this will return the startup profiler (this), otherwise + /// returns the base class + /// + public override MiniProfiler CurrentProfiler + { + get + { + // if not booting then just use base (fast) + // no lock, _bootPhase is volatile + if (_bootPhase == BootPhase.Booted) + return base.CurrentProfiler; + + // else + try + { + var current = base.CurrentProfiler; + return current ?? _startupProfiler; + } + catch + { + return _startupProfiler; + } + } + } + + public void BeginBootRequest() + { + _locker.EnterWriteLock(); + try + { + if (_bootPhase != BootPhase.Boot) + throw new InvalidOperationException("Invalid boot phase."); + _bootPhase = BootPhase.BootRequest; + + // assign the profiler to be the current MiniProfiler for the request + // is's already active, starting and all + CurrentProfiler = _startupProfiler; + } + finally + { + _locker.ExitWriteLock(); + } + } + + public void EndBootRequest() + { + _locker.EnterWriteLock(); + try + { + if (_bootPhase != BootPhase.BootRequest) + throw new InvalidOperationException("Invalid boot phase."); + _bootPhase = BootPhase.Booted; + + _startupProfiler = null; + } + finally + { + _locker.ExitWriteLock(); + } + } + + /// + /// Starts a new MiniProfiler. + /// + /// + /// + /// This is called when WebProfiler calls MiniProfiler.Start() so, + /// - as a result of WebRuntime starting the WebProfiler, and + /// - assuming profiling is enabled, on every BeginRequest that should be profiled, + /// - except for the very first one which is the boot request. + /// + /// + public override MiniProfiler Start(string profilerName, MiniProfilerBaseOptions options) + { + var first = Interlocked.Exchange(ref _first, 1) == 0; + if (first == false) return base.Start(profilerName, options); + + _startupProfiler = new MiniProfiler("StartupProfiler", options); + CurrentProfiler = _startupProfiler; + return _startupProfiler; + } + + /// + /// Indicates the boot phase. + /// + private enum BootPhase + { + Boot = 0, // boot phase (before the 1st request even begins) + BootRequest = 1, // request boot phase (during the 1st request) + Booted = 2 // done booting + } + } +} diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj new file mode 100644 index 0000000000..cb9ca9156a --- /dev/null +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -0,0 +1,22 @@ + + + + netcoreapp3.1 + Library + 8 + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index 9ef4985aea..be81261d2a 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Umbraco.Web.BackOffice.AspNetCore; +using Umbraco.Web.Common.Extensions; using Umbraco.Web.Website.AspNetCore; @@ -21,7 +22,9 @@ namespace Umbraco.Web.UI.BackOffice // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { + services.AddUmbracoConfiguration(); + services.AddUmbracoRequest(); services.AddUmbracoWebsite(); services.AddUmbracoBackOffice(); } @@ -29,6 +32,7 @@ namespace Umbraco.Web.UI.BackOffice // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { + app.UseUmbracoRequest(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); 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 ca7c3e26fa..abfb172763 100644 --- a/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj +++ b/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj @@ -7,6 +7,7 @@ + diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index fe11a8d9aa..0c0e6aee50 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -101,7 +101,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + 3.4.0 diff --git a/src/Umbraco.Web/AspNet/AspNetUmbracoApplicationLifetime.cs b/src/Umbraco.Web/AspNet/AspNetUmbracoApplicationLifetime.cs index 245e8ea374..f5ebc03cd0 100644 --- a/src/Umbraco.Web/AspNet/AspNetUmbracoApplicationLifetime.cs +++ b/src/Umbraco.Web/AspNet/AspNetUmbracoApplicationLifetime.cs @@ -1,3 +1,4 @@ +using System; using System.Threading; using System.Web; using Umbraco.Net; @@ -11,6 +12,8 @@ namespace Umbraco.Web.AspNet public AspNetUmbracoApplicationLifetime(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; + + UmbracoApplicationBase.ApplicationInit += ApplicationInit; } public bool IsRestarting { get; set; } @@ -30,5 +33,7 @@ namespace Umbraco.Web.AspNet Thread.CurrentPrincipal = null; HttpRuntime.UnloadAppDomain(); } + + public event EventHandler ApplicationInit; } } diff --git a/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs b/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs index f5a2ca8ac8..3f81c0d589 100644 --- a/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs +++ b/src/Umbraco.Web/Install/InstallSteps/NewInstallStep.cs @@ -56,20 +56,7 @@ 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)); - } + await ResetAdminPassword(user.Password); admin.Email = user.Email.Trim(); admin.Name = user.Name.Trim(); @@ -95,6 +82,26 @@ namespace Umbraco.Web.Install.InstallSteps return null; } + private async Task ResetAdminPassword(string clearTextPassword) + { + 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, clearTextPassword.Trim()); + if (!resetResult.Succeeded) + { + throw new InvalidOperationException("Could not reset password: " + string.Join(", ", resetResult.Errors)); + } + } + /// /// Return a custom view model for this step /// diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index e8aaecdf4d..b9892e1c31 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -92,7 +92,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/src/umbraco.sln b/src/umbraco.sln index be363ef2e6..11098abdec 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -123,6 +123,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Web.Website", "Umbr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Tests.Common", "Umbraco.Tests.Common\Umbraco.Tests.Common.csproj", "{A499779C-1B3B-48A8-B551-458E582E6E96}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Web.Common", "Umbraco.Web.Common\Umbraco.Web.Common.csproj", "{839D3048-9718-4907-BDE0-7CD63D364383}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -195,6 +197,10 @@ Global {A499779C-1B3B-48A8-B551-458E582E6E96}.Debug|Any CPU.Build.0 = Debug|Any CPU {A499779C-1B3B-48A8-B551-458E582E6E96}.Release|Any CPU.ActiveCfg = Release|Any CPU {A499779C-1B3B-48A8-B551-458E582E6E96}.Release|Any CPU.Build.0 = Release|Any CPU + {839D3048-9718-4907-BDE0-7CD63D364383}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {839D3048-9718-4907-BDE0-7CD63D364383}.Debug|Any CPU.Build.0 = Debug|Any CPU + {839D3048-9718-4907-BDE0-7CD63D364383}.Release|Any CPU.ActiveCfg = Release|Any CPU + {839D3048-9718-4907-BDE0-7CD63D364383}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE