using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Smidge.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Infrastructure.WebAssets; using Umbraco.Cms.Web.Common.Profiler; using Umbraco.Cms.Web.Common.Routing; using Umbraco.Extensions; namespace Umbraco.Cms.Web.Common.Middleware; /// /// Manages Umbraco request objects and their lifetime /// /// /// /// This is responsible for initializing the content cache /// /// /// This is responsible for creating and assigning an /// /// public class UmbracoRequestMiddleware : IMiddleware { private readonly BackOfficeWebAssets _backOfficeWebAssets; private readonly IDefaultCultureAccessor _defaultCultureAccessor; private readonly IEventAggregator _eventAggregator; private readonly IHostingEnvironment _hostingEnvironment; private readonly ILogger _logger; private readonly WebProfiler? _profiler; private readonly IRequestCache _requestCache; private readonly IRuntimeState _runtimeState; private readonly IUmbracoContextFactory _umbracoContextFactory; private readonly IOptions _umbracoRequestOptions; private readonly UmbracoRequestPaths _umbracoRequestPaths; private readonly IVariationContextAccessor _variationContextAccessor; private SmidgeOptions _smidgeOptions; /// /// Initializes a new instance of the class. /// public UmbracoRequestMiddleware( ILogger logger, IUmbracoContextFactory umbracoContextFactory, IRequestCache requestCache, IEventAggregator eventAggregator, IProfiler profiler, IHostingEnvironment hostingEnvironment, UmbracoRequestPaths umbracoRequestPaths, BackOfficeWebAssets backOfficeWebAssets, IOptionsMonitor smidgeOptions, IRuntimeState runtimeState, IVariationContextAccessor variationContextAccessor, IDefaultCultureAccessor defaultCultureAccessor, IOptions umbracoRequestOptions) { _logger = logger; _umbracoContextFactory = umbracoContextFactory; _requestCache = requestCache; _eventAggregator = eventAggregator; _hostingEnvironment = hostingEnvironment; _umbracoRequestPaths = umbracoRequestPaths; _backOfficeWebAssets = backOfficeWebAssets; _runtimeState = runtimeState; _variationContextAccessor = variationContextAccessor; _defaultCultureAccessor = defaultCultureAccessor; _umbracoRequestOptions = umbracoRequestOptions; _smidgeOptions = smidgeOptions.CurrentValue; _profiler = profiler as WebProfiler; // Ignore if not a WebProfiler smidgeOptions.OnChange(x => _smidgeOptions = x); } /// public async Task InvokeAsync(HttpContext context, RequestDelegate next) { // do not process if client-side request if (context.Request.IsClientSideRequest() && !_umbracoRequestOptions.Value.HandleAsServerSideRequest(context.Request)) { // we need this here because for bundle requests, these are 'client side' requests that we need to handle LazyInitializeBackOfficeServices(context.Request.Path); await next(context); return; } // Profiling start needs to be one of the first things that happens. // Also MiniProfiler.Current becomes null if it is handled by the event aggregator due to async/await _profiler?.UmbracoApplicationBeginRequest(context, _runtimeState.Level); _variationContextAccessor.VariationContext ??= new VariationContext(_defaultCultureAccessor.DefaultCulture); UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext(); Uri? currentApplicationUrl = GetApplicationUrlFromCurrentRequest(context.Request); _hostingEnvironment.EnsureApplicationMainUrl(currentApplicationUrl); var pathAndQuery = context.Request.GetEncodedPathAndQuery(); try { // Verbose log start of every request LogHttpRequest.TryGetCurrentHttpRequestId(out Guid? httpRequestId, _requestCache); _logger.LogTrace("Begin request [{HttpRequestId}]: {RequestUrl}", httpRequestId, pathAndQuery); try { LazyInitializeBackOfficeServices(context.Request.Path); await _eventAggregator.PublishAsync( new UmbracoRequestBeginNotification(umbracoContextReference.UmbracoContext)); } catch (Exception ex) { // try catch so we don't kill everything in all requests _logger.LogError(ex.Message); } finally { try { await next(context); } finally { await _eventAggregator.PublishAsync( new UmbracoRequestEndNotification(umbracoContextReference.UmbracoContext)); } } } finally { // Verbose log end of every request (in v8 we didn't log the end request of ALL requests, only the front-end which was // strange since we always logged the beginning, so now we just log start/end of all requests) LogHttpRequest.TryGetCurrentHttpRequestId(out Guid? httpRequestId, _requestCache); _logger.LogTrace( "End Request [{HttpRequestId}]: {RequestUrl} ({RequestDuration}ms)", httpRequestId, pathAndQuery, DateTime.Now.Subtract(umbracoContextReference.UmbracoContext.ObjectCreated).TotalMilliseconds); try { DisposeHttpContextItems(context.Request); } finally { // Dispose the umbraco context reference which will in turn dispose the UmbracoContext itself. umbracoContextReference.Dispose(); } } // Profiling end needs to be last of the first things that happens. // Also MiniProfiler.Current becomes null if it is handled by the event aggregator due to async/await _profiler?.UmbracoApplicationEndRequest(context, _runtimeState.Level); } /// /// Used to lazily initialize any back office services when the first request to the back office is made /// private void LazyInitializeBackOfficeServices(PathString absPath) { if (s_firstBackOfficeRequest) { return; } if (_umbracoRequestPaths.IsBackOfficeRequest(absPath) || (absPath.Value?.InvariantStartsWith($"/{_smidgeOptions.UrlOptions.CompositeFilePath}") ?? false) || (absPath.Value?.InvariantStartsWith($"/{_smidgeOptions.UrlOptions.BundleFilePath}") ?? false) || _runtimeState.EnableInstaller()) { LazyInitializer.EnsureInitialized(ref s_firstBackOfficeRequest, ref s_firstBackOfficeReqestFlag, ref s_firstBackOfficeRequestLocker, () => { _backOfficeWebAssets.CreateBundles(); return true; }); } } private Uri? GetApplicationUrlFromCurrentRequest(HttpRequest request) { // We only consider GET and POST. // Especially the DEBUG sent when debugging the application is annoying because it uses http, even when the https is available. if (request.Method == "GET" || request.Method == "POST") { return new Uri($"{request.Scheme}://{request.Host}{request.PathBase}", UriKind.Absolute); } return null; } /// /// Dispose some request scoped objects that we are maintaining the lifecycle for. /// private void DisposeHttpContextItems(HttpRequest request) { // do not process if client-side request if (request.IsClientSideRequest()) { return; } // ensure this is disposed by DI at the end of the request IHttpScopeReference httpScopeReference = request.HttpContext.RequestServices.GetRequiredService(); httpScopeReference.Register(); } #pragma warning disable IDE0044 // Add readonly modifier private static bool s_firstBackOfficeRequest; private static bool s_firstBackOfficeReqestFlag; private static object s_firstBackOfficeRequestLocker = new(); #pragma warning restore IDE0044 // Add readonly modifier }