using Microsoft.AspNetCore.DataProtection.Infrastructure; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Collections; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Extensions; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Extensions; using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; namespace Umbraco.Cms.Web.Common.AspNetCore; public class AspNetCoreHostingEnvironment : IHostingEnvironment { private readonly IApplicationDiscriminator? _applicationDiscriminator; private readonly ConcurrentHashSet _applicationUrls = new(); private readonly IOptionsMonitor _hostingSettings; private readonly IWebHostEnvironment _webHostEnvironment; private readonly IOptionsMonitor _webRoutingSettings; private readonly UrlMode _urlProviderMode; private string? _applicationId; private string? _localTempPath; public AspNetCoreHostingEnvironment( IOptionsMonitor hostingSettings, IOptionsMonitor webRoutingSettings, IWebHostEnvironment webHostEnvironment, IApplicationDiscriminator applicationDiscriminator) : this(hostingSettings, webRoutingSettings, webHostEnvironment) => _applicationDiscriminator = applicationDiscriminator; public AspNetCoreHostingEnvironment( IOptionsMonitor hostingSettings, IOptionsMonitor webRoutingSettings, IWebHostEnvironment webHostEnvironment) { _hostingSettings = hostingSettings ?? throw new ArgumentNullException(nameof(hostingSettings)); _webRoutingSettings = webRoutingSettings ?? throw new ArgumentNullException(nameof(webRoutingSettings)); _webHostEnvironment = webHostEnvironment ?? throw new ArgumentNullException(nameof(webHostEnvironment)); _urlProviderMode = _webRoutingSettings.CurrentValue.UrlProviderMode; SetSiteNameAndDebugMode(hostingSettings.CurrentValue); // We have to ensure that the OptionsMonitor is an actual options monitor since we have a hack // where we initially use an OptionsMonitorAdapter, which doesn't implement OnChange. // See summery of OptionsMonitorAdapter for more information. if (hostingSettings is OptionsMonitor) { hostingSettings.OnChange(settings => SetSiteNameAndDebugMode(settings)); } ApplicationPhysicalPath = webHostEnvironment.ContentRootPath; if (_webRoutingSettings.CurrentValue.UmbracoApplicationUrl is not null) { ApplicationMainUrl = new Uri(_webRoutingSettings.CurrentValue.UmbracoApplicationUrl); } } /// public bool IsHosted { get; } = true; /// public Uri ApplicationMainUrl { get; private set; } = null!; /// public string? SiteName { get; private set; } /// public string ApplicationId { get { if (_applicationId != null) { return _applicationId; } _applicationId = _applicationDiscriminator?.GetApplicationId() ?? _webHostEnvironment.GetTemporaryApplicationId(); return _applicationId; } } /// public string ApplicationPhysicalPath { get; } // TODO how to find this, This is a server thing, not application thing. public string ApplicationVirtualPath => _hostingSettings.CurrentValue.ApplicationVirtualPath?.EnsureStartsWith('/') ?? "/"; /// public bool IsDebugMode { get; private set; } public string LocalTempPath { get { if (_localTempPath != null) { return _localTempPath; } switch (_hostingSettings.CurrentValue.LocalTempStorageLocation) { case LocalTempStorage.EnvironmentTemp: // environment temp is unique, we need a folder per site // use a hash // combine site name and application id // site name is a Guid on Cloud // application id is eg /LM/W3SVC/123456/ROOT // the combination is unique on one server // and, if a site moves from worker A to B and then back to A... // hopefully it gets a new Guid or new application id? var hashString = SiteName + "::" + ApplicationId; var hash = hashString.GenerateHash(); var siteTemp = Path.Combine(Path.GetTempPath(), "UmbracoData", hash); return _localTempPath = siteTemp; default: return _localTempPath = MapPathContentRoot(Core.Constants.SystemDirectories.TempData); } } } /// public string MapPathWebRoot(string path) => _webHostEnvironment.MapPathWebRoot(path); /// public string MapPathContentRoot(string path) => _webHostEnvironment.MapPathContentRoot(path); /// public string ToAbsolute(string virtualPath) { if (!virtualPath.StartsWith("~/") && !virtualPath.StartsWith("/") && _urlProviderMode != UrlMode.Absolute) { throw new InvalidOperationException( $"The value {virtualPath} for parameter {nameof(virtualPath)} must start with ~/ or /"); } // will occur if it starts with "/" if (Uri.IsWellFormedUriString(virtualPath, UriKind.Absolute)) { return virtualPath; } var fullPath = ApplicationVirtualPath.EnsureEndsWith('/') + virtualPath.TrimStart(Core.Constants.CharArrays.TildeForwardSlash); return fullPath; } public void EnsureApplicationMainUrl(Uri? currentApplicationUrl) { // TODO: This causes problems with site swap on azure because azure pre-warms a site by calling into `localhost` and when it does that // it changes the URL to `localhost:80` which actually doesn't work for pinging itself, it only works internally in Azure. The ironic part // about this is that this is here specifically for the slot swap scenario https://issues.umbraco.org/issue/U4-10626 // see U4-10626 - in some cases we want to reset the application url // (this is a simplified version of what was in 7.x) // note: should this be optional? is it expensive? if (currentApplicationUrl is null) { return; } if (_webRoutingSettings.CurrentValue.UmbracoApplicationUrl is not null) { return; } var change = !_applicationUrls.Contains(currentApplicationUrl); if (change) { if (_applicationUrls.TryAdd(currentApplicationUrl)) { ApplicationMainUrl = currentApplicationUrl; } } } private void SetSiteNameAndDebugMode(HostingSettings hostingSettings) { SiteName = string.IsNullOrWhiteSpace(hostingSettings.SiteName) ? _webHostEnvironment.ApplicationName : hostingSettings.SiteName; IsDebugMode = hostingSettings.Debug; } }