From 3587172e02f27a106b262aa5406b31bf9cfd9527 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 20 Feb 2023 14:22:35 +0100 Subject: [PATCH] Fixed and added profiling endpoints --- .../Profiling/GetStatusProfilingController.cs | 25 +++++++++ .../Profiling/ProfilingControllerBase.cs | 16 +++++- .../Profiling/StatusProfilingController.cs | 19 ------- .../UpdateStatusProfilingController.cs | 25 +++++++++ .../DependencyInjection/UmbracoBuilder.cs | 1 + .../Repositories/IWebProfilerRepository.cs | 7 +++ .../Services/IWebProfilerService.cs | 9 +++ .../WebProfilerOperationStatus.cs | 7 +++ .../Services/WebProfilerService.cs | 55 +++++++++++++++++++ .../UmbracoBuilderExtensions.cs | 4 ++ .../Profiler/WebProfiler.cs | 19 ++++++- .../Repositories/WebProfilerRepository.cs | 30 ++++++++++ .../UmbracoContext/UmbracoContext.cs | 40 ++++++++++++-- .../UmbracoContext/UmbracoContextFactory.cs | 33 ++++++++++- 14 files changed, 259 insertions(+), 31 deletions(-) create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/Profiling/GetStatusProfilingController.cs delete mode 100644 src/Umbraco.Cms.Api.Management/Controllers/Profiling/StatusProfilingController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/Profiling/UpdateStatusProfilingController.cs create mode 100644 src/Umbraco.Core/Persistence/Repositories/IWebProfilerRepository.cs create mode 100644 src/Umbraco.Core/Services/IWebProfilerService.cs create mode 100644 src/Umbraco.Core/Services/OperationStatus/WebProfilerOperationStatus.cs create mode 100644 src/Umbraco.Core/Services/WebProfilerService.cs create mode 100644 src/Umbraco.Web.Common/Repositories/WebProfilerRepository.cs diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Profiling/GetStatusProfilingController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Profiling/GetStatusProfilingController.cs new file mode 100644 index 0000000000..700689f0a3 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Profiling/GetStatusProfilingController.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.Profiling; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.Profiling; + +public class GetStatusProfilingController : ProfilingControllerBase +{ + private readonly IWebProfilerService _webProfilerService; + + public GetStatusProfilingController(IWebProfilerService webProfilerService) => _webProfilerService = webProfilerService; + + [HttpGet("status")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(ProfilingStatusViewModel), StatusCodes.Status200OK)] + public async Task Status() + { + var result = await _webProfilerService.GetStatus(); + return result.Success + ? Ok(new ProfilingStatusViewModel(result.Result)) + : WebProfilerOperationStatusResult(result.Status); + } +} + diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Profiling/ProfilingControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Profiling/ProfilingControllerBase.cs index 07b068f5c9..49be02c341 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Profiling/ProfilingControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Profiling/ProfilingControllerBase.cs @@ -1,5 +1,8 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.Builders; using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Core.Services.OperationStatus; namespace Umbraco.Cms.Api.Management.Controllers.Profiling; @@ -9,4 +12,15 @@ namespace Umbraco.Cms.Api.Management.Controllers.Profiling; [ApiExplorerSettings(GroupName = "Profiling")] public class ProfilingControllerBase : ManagementApiControllerBase { + + protected IActionResult WebProfilerOperationStatusResult(WebProfilerOperationStatus status) => + status switch + { + WebProfilerOperationStatus.ExecutingUserNotFound => Unauthorized(new ProblemDetailsBuilder() + .WithTitle("Executing user not found") + .WithDetail("Executing this action requires a signed in user.") + .Build()), + + _ => StatusCode(StatusCodes.Status500InternalServerError, "Unknown profiling operation status") + }; } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Profiling/StatusProfilingController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Profiling/StatusProfilingController.cs deleted file mode 100644 index 5738655e14..0000000000 --- a/src/Umbraco.Cms.Api.Management/Controllers/Profiling/StatusProfilingController.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Umbraco.Cms.Core.Hosting; -using Umbraco.Cms.Api.Management.ViewModels.Profiling; - -namespace Umbraco.Cms.Api.Management.Controllers.Profiling; - -public class StatusProfilingController : ProfilingControllerBase -{ - private readonly IHostingEnvironment _hosting; - - public StatusProfilingController(IHostingEnvironment hosting) => _hosting = hosting; - - [HttpGet("status")] - [MapToApiVersion("1.0")] - [ProducesResponseType(typeof(ProfilingStatusViewModel), StatusCodes.Status200OK)] - public async Task> Status() - => await Task.FromResult(Ok(new ProfilingStatusViewModel(_hosting.IsDebugMode))); -} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Profiling/UpdateStatusProfilingController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Profiling/UpdateStatusProfilingController.cs new file mode 100644 index 0000000000..15ed5bae45 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Profiling/UpdateStatusProfilingController.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.Profiling; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.Profiling; + +public class UpdateStatusProfilingController : ProfilingControllerBase +{ + private readonly IWebProfilerService _webProfilerService; + + public UpdateStatusProfilingController(IWebProfilerService webProfilerService) => _webProfilerService = webProfilerService; + + [HttpPut("status")] + [MapToApiVersion("1.0")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task Status(ProfilingStatusViewModel model) + { + var result = await _webProfilerService.SetStatus(model.Enabled); + return result.Success + ? Ok() + : WebProfilerOperationStatusResult(result.Status); + } +} + diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 5c4a2b6477..6ec1ec733b 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -282,6 +282,7 @@ namespace Umbraco.Cms.Core.DependencyInjection Services.AddTransient(); Services.AddUnique(); Services.AddUnique(); + Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); Services.AddUnique(); diff --git a/src/Umbraco.Core/Persistence/Repositories/IWebProfilerRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IWebProfilerRepository.cs new file mode 100644 index 0000000000..d78bcdcd2c --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/IWebProfilerRepository.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface IWebProfilerRepository +{ + void SetStatus(int userId, bool status); + bool GetStatus(int userId); +} diff --git a/src/Umbraco.Core/Services/IWebProfilerService.cs b/src/Umbraco.Core/Services/IWebProfilerService.cs new file mode 100644 index 0000000000..5366c8781d --- /dev/null +++ b/src/Umbraco.Core/Services/IWebProfilerService.cs @@ -0,0 +1,9 @@ +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Core.Services; + +public interface IWebProfilerService +{ + Task> GetStatus(); + Task> SetStatus(bool status); +} diff --git a/src/Umbraco.Core/Services/OperationStatus/WebProfilerOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/WebProfilerOperationStatus.cs new file mode 100644 index 0000000000..2a3a0bc3f2 --- /dev/null +++ b/src/Umbraco.Core/Services/OperationStatus/WebProfilerOperationStatus.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Cms.Core.Services.OperationStatus; + +public enum WebProfilerOperationStatus +{ + Success, + ExecutingUserNotFound +} diff --git a/src/Umbraco.Core/Services/WebProfilerService.cs b/src/Umbraco.Core/Services/WebProfilerService.cs new file mode 100644 index 0000000000..83c984aeea --- /dev/null +++ b/src/Umbraco.Core/Services/WebProfilerService.cs @@ -0,0 +1,55 @@ +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services.OperationStatus; + +namespace Umbraco.Cms.Core.Services; + +internal sealed class WebProfilerService : IWebProfilerService +{ + private readonly IWebProfilerRepository _webProfilerRepository; + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + + public WebProfilerService(IWebProfilerRepository webProfilerRepository, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + { + _webProfilerRepository = webProfilerRepository; + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + } + + public async Task> GetStatus() + { + Attempt userIdAttempt = GetExecutingUserId(); + + if (!userIdAttempt.Success) + { + return Attempt.FailWithStatus(WebProfilerOperationStatus.ExecutingUserNotFound, false); + } + + var result = _webProfilerRepository.GetStatus(userIdAttempt.Result); + return await Task.FromResult(Attempt.SucceedWithStatus(WebProfilerOperationStatus.Success, result)); + } + + public async Task> SetStatus(bool status) + { + Attempt userIdAttempt = GetExecutingUserId(); + + if (!userIdAttempt.Success) + { + return Attempt.FailWithStatus(WebProfilerOperationStatus.ExecutingUserNotFound, false); + } + + _webProfilerRepository.SetStatus(userIdAttempt.Result, status); + return await Task.FromResult(Attempt.SucceedWithStatus(WebProfilerOperationStatus.Success, status)); + } + + private Attempt GetExecutingUserId() + { + //FIXME when we can get current user + return Attempt.Succeed(-1); + + Attempt? userIdAttempt = _backOfficeSecurityAccessor?.BackOfficeSecurity?.GetUserId(); + + return (userIdAttempt.HasValue && userIdAttempt.Value.Success) + ? Attempt.Succeed(userIdAttempt.Value.Result) + : Attempt.Fail(0); + } +} diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index e44a7449aa..a1c62d0641 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -31,6 +31,7 @@ using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Macros; using Umbraco.Cms.Core.Net; using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Telemetry; @@ -55,6 +56,7 @@ using Umbraco.Cms.Web.Common.Middleware; using Umbraco.Cms.Web.Common.ModelBinders; using Umbraco.Cms.Web.Common.Mvc; using Umbraco.Cms.Web.Common.Profiler; +using Umbraco.Cms.Web.Common.Repositories; using Umbraco.Cms.Web.Common.RuntimeMinification; using Umbraco.Cms.Web.Common.Security; using Umbraco.Cms.Web.Common.Templates; @@ -203,6 +205,8 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddMiniProfiler(); builder.Services.ConfigureOptions(); + builder.Services.AddSingleton(); + builder.AddNotificationHandler(); return builder; } diff --git a/src/Umbraco.Web.Common/Profiler/WebProfiler.cs b/src/Umbraco.Web.Common/Profiler/WebProfiler.cs index 50a0de19a9..e960bab30b 100644 --- a/src/Umbraco.Web.Common/Profiler/WebProfiler.cs +++ b/src/Umbraco.Web.Common/Profiler/WebProfiler.cs @@ -1,5 +1,6 @@ using System.Net; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; @@ -8,6 +9,8 @@ using StackExchange.Profiling; using StackExchange.Profiling.Internal; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Logging; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; @@ -38,7 +41,10 @@ public class WebProfiler : IProfiler public void Start() { - MiniProfiler.StartNew(_httpContextAccessor.HttpContext?.Request.Path); + var name = $"{_httpContextAccessor.HttpContext?.Request.Method} {_httpContextAccessor.HttpContext?.Request.GetDisplayUrl()}"; + + MiniProfiler.StartNew(name); + MiniProfilerContext.Value = MiniProfiler.Current; } @@ -135,9 +141,16 @@ public class WebProfiler : IProfiler return xUmbDebug; } - if (bool.TryParse(request.Cookies["UMB-DEBUG"], out var cUmbDebug)) + var webProfilerService = _httpContextAccessor.HttpContext?.RequestServices?.GetService(); + + if (webProfilerService is not null) { - return cUmbDebug; + Attempt shouldProfile = webProfilerService.GetStatus().GetAwaiter().GetResult(); + + if (shouldProfile.Success) + { + return shouldProfile.Result; + } } return false; diff --git a/src/Umbraco.Web.Common/Repositories/WebProfilerRepository.cs b/src/Umbraco.Web.Common/Repositories/WebProfilerRepository.cs new file mode 100644 index 0000000000..b12242a02c --- /dev/null +++ b/src/Umbraco.Web.Common/Repositories/WebProfilerRepository.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Http; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Web.Common.Repositories; + +internal class WebProfilerRepository : IWebProfilerRepository +{ + private readonly IHttpContextAccessor _httpContextAccessor; + private const string CookieName = "UMB-DEBUG"; + + public WebProfilerRepository(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public void SetStatus(int userId, bool status) + { + if (status) + { + _httpContextAccessor.GetRequiredHttpContext().Response.Cookies.Append(CookieName, string.Empty, new CookieOptions { Expires = DateTime.Now.AddYears(1) }); + } + else + { + _httpContextAccessor.GetRequiredHttpContext().Response.Cookies.Delete(CookieName); + } + } + + public bool GetStatus(int userId) => _httpContextAccessor.GetRequiredHttpContext().Request.Cookies.ContainsKey(CookieName); +} diff --git a/src/Umbraco.Web.Common/UmbracoContext/UmbracoContext.cs b/src/Umbraco.Web.Common/UmbracoContext/UmbracoContext.cs index 878824c383..8ce8c564c7 100644 --- a/src/Umbraco.Web.Common/UmbracoContext/UmbracoContext.cs +++ b/src/Umbraco.Web.Common/UmbracoContext/UmbracoContext.cs @@ -4,6 +4,7 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; @@ -17,6 +18,7 @@ public class UmbracoContext : DisposableObjectSlim, IUmbracoContext private readonly ICookieManager _cookieManager; private readonly IHostingEnvironment _hostingEnvironment; private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IWebProfilerService _webProfilerService; private readonly Lazy _publishedSnapshot; private readonly UmbracoRequestPaths _umbracoRequestPaths; private readonly UriUtility _uriUtility; @@ -36,7 +38,8 @@ public class UmbracoContext : DisposableObjectSlim, IUmbracoContext IHostingEnvironment hostingEnvironment, UriUtility uriUtility, ICookieManager cookieManager, - IHttpContextAccessor httpContextAccessor) + IHttpContextAccessor httpContextAccessor, + IWebProfilerService webProfilerService) { if (publishedSnapshotService == null) { @@ -47,6 +50,7 @@ public class UmbracoContext : DisposableObjectSlim, IUmbracoContext _hostingEnvironment = hostingEnvironment; _cookieManager = cookieManager; _httpContextAccessor = httpContextAccessor; + _webProfilerService = webProfilerService; ObjectCreated = DateTime.Now; UmbracoRequestId = Guid.NewGuid(); _umbracoRequestPaths = umbracoRequestPaths; @@ -116,11 +120,35 @@ _cleanedUmbracoUrl ??= _uriUtility.UriToUmbraco(OriginalRequestUrl); public IPublishedRequest? PublishedRequest { get; set; } /// - public bool IsDebug => // NOTE: the request can be null during app startup! - _hostingEnvironment.IsDebugMode - && (string.IsNullOrEmpty(_httpContextAccessor.HttpContext?.GetRequestValue("umbdebugshowtrace")) == false - || string.IsNullOrEmpty(_httpContextAccessor.HttpContext?.GetRequestValue("umbdebug")) == false - || string.IsNullOrEmpty(_cookieManager.GetCookieValue("UMB-DEBUG")) == false); + public bool IsDebug + { + get + { + if (!_hostingEnvironment.IsDebugMode) + { + return false; + } + + if(string.IsNullOrEmpty(_httpContextAccessor.HttpContext?.GetRequestValue("umbdebugshowtrace")) == false) + { + return true; + } + + if(string.IsNullOrEmpty(_httpContextAccessor.HttpContext?.GetRequestValue("umbdebug")) == false) + { + return true; + } + + var webProfilerStatusAttempt = _webProfilerService.GetStatus().GetAwaiter().GetResult(); + + if (webProfilerStatusAttempt.Success) + { + return webProfilerStatusAttempt.Result; + } + + return true; + } + } /// public bool InPreviewMode diff --git a/src/Umbraco.Web.Common/UmbracoContext/UmbracoContextFactory.cs b/src/Umbraco.Web.Common/UmbracoContext/UmbracoContextFactory.cs index 726f96cf31..2333cf2230 100644 --- a/src/Umbraco.Web.Common/UmbracoContext/UmbracoContextFactory.cs +++ b/src/Umbraco.Web.Common/UmbracoContext/UmbracoContextFactory.cs @@ -1,8 +1,11 @@ using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; namespace Umbraco.Cms.Web.Common.UmbracoContext; @@ -15,11 +18,34 @@ public class UmbracoContextFactory : IUmbracoContextFactory private readonly ICookieManager _cookieManager; private readonly IHostingEnvironment _hostingEnvironment; private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IWebProfilerService _webProfilerService; private readonly IPublishedSnapshotService _publishedSnapshotService; private readonly IUmbracoContextAccessor _umbracoContextAccessor; private readonly UmbracoRequestPaths _umbracoRequestPaths; private readonly UriUtility _uriUtility; + + [Obsolete("Use non-obsolete ctor. This will be removed in Umbraco 15.")] + public UmbracoContextFactory( + IUmbracoContextAccessor umbracoContextAccessor, + IPublishedSnapshotService publishedSnapshotService, + UmbracoRequestPaths umbracoRequestPaths, + IHostingEnvironment hostingEnvironment, + UriUtility uriUtility, + ICookieManager cookieManager, + IHttpContextAccessor httpContextAccessor) + :this( + umbracoContextAccessor, + publishedSnapshotService, + umbracoRequestPaths, + hostingEnvironment, + uriUtility, + cookieManager, + httpContextAccessor, + StaticServiceProvider.Instance.GetRequiredService()) + { + + } /// /// Initializes a new instance of the class. /// @@ -30,7 +56,8 @@ public class UmbracoContextFactory : IUmbracoContextFactory IHostingEnvironment hostingEnvironment, UriUtility uriUtility, ICookieManager cookieManager, - IHttpContextAccessor httpContextAccessor) + IHttpContextAccessor httpContextAccessor, + IWebProfilerService webProfilerService) { _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); @@ -41,6 +68,7 @@ public class UmbracoContextFactory : IUmbracoContextFactory _uriUtility = uriUtility ?? throw new ArgumentNullException(nameof(uriUtility)); _cookieManager = cookieManager ?? throw new ArgumentNullException(nameof(cookieManager)); _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); + _webProfilerService = webProfilerService; } /// @@ -63,5 +91,6 @@ public class UmbracoContextFactory : IUmbracoContextFactory _hostingEnvironment, _uriUtility, _cookieManager, - _httpContextAccessor); + _httpContextAccessor, + _webProfilerService); }