233 lines
9.6 KiB
C#
233 lines
9.6 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
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.Events;
|
|
using Umbraco.Cms.Core.Hosting;
|
|
using Umbraco.Cms.Core.Logging;
|
|
using Umbraco.Cms.Core.Routing;
|
|
using Umbraco.Cms.Core.Scoping;
|
|
using Umbraco.Cms.Core.Security;
|
|
using Umbraco.Cms.Core.Web;
|
|
using Umbraco.Cms.Infrastructure.PublishedCache;
|
|
using Umbraco.Cms.Infrastructure.WebAssets;
|
|
using Umbraco.Cms.Web.Common.Profiler;
|
|
using Umbraco.Extensions;
|
|
|
|
namespace Umbraco.Cms.Web.Common.Middleware
|
|
{
|
|
|
|
/// <summary>
|
|
/// Manages Umbraco request objects and their lifetime
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// This is responsible for initializing the content cache
|
|
/// </para>
|
|
/// <para>
|
|
/// This is responsible for creating and assigning an <see cref="IUmbracoContext"/>
|
|
/// </para>
|
|
/// </remarks>
|
|
public class UmbracoRequestMiddleware : IMiddleware
|
|
{
|
|
private readonly ILogger<UmbracoRequestMiddleware> _logger;
|
|
|
|
private readonly IUmbracoContextFactory _umbracoContextFactory;
|
|
private readonly IRequestCache _requestCache;
|
|
private readonly PublishedSnapshotServiceEventHandler _publishedSnapshotServiceEventHandler;
|
|
private readonly IEventAggregator _eventAggregator;
|
|
private readonly IHostingEnvironment _hostingEnvironment;
|
|
private readonly UmbracoRequestPaths _umbracoRequestPaths;
|
|
private readonly BackOfficeWebAssets _backOfficeWebAssets;
|
|
private readonly SmidgeOptions _smidgeOptions;
|
|
private readonly WebProfiler _profiler;
|
|
|
|
private static bool s_cacheInitialized;
|
|
private static bool s_cacheInitializedFlag = false;
|
|
private static object s_cacheInitializedLock = new object();
|
|
|
|
#pragma warning disable IDE0044 // Add readonly modifier
|
|
private static bool s_firstBackOfficeRequest;
|
|
private static bool s_firstBackOfficeReqestFlag;
|
|
private static object s_firstBackOfficeRequestLocker = new object();
|
|
#pragma warning restore IDE0044 // Add readonly modifier
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="UmbracoRequestMiddleware"/> class.
|
|
/// </summary>
|
|
public UmbracoRequestMiddleware(
|
|
ILogger<UmbracoRequestMiddleware> logger,
|
|
IUmbracoContextFactory umbracoContextFactory,
|
|
IRequestCache requestCache,
|
|
PublishedSnapshotServiceEventHandler publishedSnapshotServiceEventHandler,
|
|
IEventAggregator eventAggregator,
|
|
IProfiler profiler,
|
|
IHostingEnvironment hostingEnvironment,
|
|
UmbracoRequestPaths umbracoRequestPaths,
|
|
BackOfficeWebAssets backOfficeWebAssets,
|
|
IOptions<SmidgeOptions> smidgeOptions)
|
|
{
|
|
_logger = logger;
|
|
_umbracoContextFactory = umbracoContextFactory;
|
|
_requestCache = requestCache;
|
|
_publishedSnapshotServiceEventHandler = publishedSnapshotServiceEventHandler;
|
|
_eventAggregator = eventAggregator;
|
|
_hostingEnvironment = hostingEnvironment;
|
|
_umbracoRequestPaths = umbracoRequestPaths;
|
|
_backOfficeWebAssets = backOfficeWebAssets;
|
|
_smidgeOptions = smidgeOptions.Value;
|
|
_profiler = profiler as WebProfiler; // Ignore if not a WebProfiler
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
|
|
{
|
|
// do not process if client-side request
|
|
if (context.Request.IsClientSideRequest())
|
|
{
|
|
// 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);
|
|
|
|
EnsureContentCacheInitialized();
|
|
|
|
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 UmbracoRequestBegin(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 UmbracoRequestEnd(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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Used to lazily initialize any back office services when the first request to the back office is made
|
|
/// </summary>
|
|
/// <param name="umbracoContext"></param>
|
|
/// <returns></returns>
|
|
private void LazyInitializeBackOfficeServices(PathString absPath)
|
|
{
|
|
if (s_firstBackOfficeRequest)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_umbracoRequestPaths.IsBackOfficeRequest(absPath)
|
|
|| absPath.Value.InvariantStartsWith($"/{_smidgeOptions.UrlOptions.CompositeFilePath}")
|
|
|| absPath.Value.InvariantStartsWith($"/{_smidgeOptions.UrlOptions.BundleFilePath}"))
|
|
{
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dispose some request scoped objects that we are maintaining the lifecycle for.
|
|
/// </summary>
|
|
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<IHttpScopeReference>();
|
|
httpScopeReference.Register();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes the content cache one time
|
|
/// </summary>
|
|
private void EnsureContentCacheInitialized() => LazyInitializer.EnsureInitialized(
|
|
ref s_cacheInitialized,
|
|
ref s_cacheInitializedFlag,
|
|
ref s_cacheInitializedLock,
|
|
() =>
|
|
{
|
|
_publishedSnapshotServiceEventHandler.Initialize();
|
|
return true;
|
|
});
|
|
}
|
|
}
|