Merge branch 'netcore/dev' into netcore/members-userstore

This commit is contained in:
Emma Garland
2021-02-09 13:22:40 +00:00
220 changed files with 2869 additions and 6196 deletions

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Options;
@@ -10,26 +11,33 @@ namespace Umbraco.Web.Common.AspNetCore
{
public class AspNetCoreHostingEnvironment : Core.Hosting.IHostingEnvironment
{
private IOptionsMonitor<HostingSettings> _hostingSettings;
private readonly ISet<Uri> _applicationUrls = new HashSet<Uri>();
private readonly IOptionsMonitor<HostingSettings> _hostingSettings;
private readonly IOptionsMonitor<WebRoutingSettings> _webRoutingSettings;
private readonly IWebHostEnvironment _webHostEnvironment;
private string _localTempPath;
public AspNetCoreHostingEnvironment(IOptionsMonitor<HostingSettings> hostingSettings, IWebHostEnvironment webHostEnvironment)
public AspNetCoreHostingEnvironment(
IOptionsMonitor<HostingSettings> hostingSettings,
IOptionsMonitor<WebRoutingSettings> webRoutingSettings,
IWebHostEnvironment webHostEnvironment)
{
_hostingSettings = hostingSettings ?? throw new ArgumentNullException(nameof(hostingSettings));
_webRoutingSettings = webRoutingSettings ?? throw new ArgumentNullException(nameof(webRoutingSettings));
_webHostEnvironment = webHostEnvironment ?? throw new ArgumentNullException(nameof(webHostEnvironment));
SiteName = webHostEnvironment.ApplicationName;
ApplicationId = AppDomain.CurrentDomain.Id.ToString();
ApplicationPhysicalPath = webHostEnvironment.ContentRootPath;
IISVersion = new Version(0, 0); // TODO not necessary IIS
}
/// <inheritdoc/>
public bool IsHosted { get; } = true;
/// <inheritdoc/>
public Uri ApplicationMainUrl { get; private set; }
/// <inheritdoc/>
public string SiteName { get; }
@@ -39,8 +47,6 @@ namespace Umbraco.Web.Common.AspNetCore
/// <inheritdoc/>
public string ApplicationPhysicalPath { get; }
public string ApplicationServerAddress { get; }
// TODO how to find this, This is a server thing, not application thing.
public string ApplicationVirtualPath => _hostingSettings.CurrentValue.ApplicationVirtualPath?.EnsureStartsWith('/') ?? "/";
@@ -125,6 +131,35 @@ namespace Umbraco.Web.Common.AspNetCore
return fullPath;
}
public void EnsureApplicationMainUrl(Uri currentApplicationUrl)
{
// Fixme: 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 null))
{
return;
}
var change = !_applicationUrls.Contains(currentApplicationUrl);
if (change)
{
_applicationUrls.Add(currentApplicationUrl);
ApplicationMainUrl = currentApplicationUrl;
}
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.Extensions.Options;
@@ -10,42 +11,45 @@ using Umbraco.Web.Routing;
namespace Umbraco.Web.Common.AspNetCore
{
public class AspNetCoreRequestAccessor : IRequestAccessor, INotificationHandler<UmbracoRequestBegin>, INotificationHandler<UmbracoRequestEnd>
public class AspNetCoreRequestAccessor : IRequestAccessor, INotificationHandler<UmbracoRequestBegin>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly WebRoutingSettings _webRoutingSettings;
private readonly ISet<string> _applicationUrls = new HashSet<string>();
private Uri _currentApplicationUrl;
private object _initLocker = new object();
private bool _hasAppUrl = false;
private bool _isInit = false;
public AspNetCoreRequestAccessor(IHttpContextAccessor httpContextAccessor,
IUmbracoContextAccessor umbracoContextAccessor,
/// <summary>
/// Initializes a new instance of the <see cref="AspNetCoreRequestAccessor"/> class.
/// </summary>
public AspNetCoreRequestAccessor(
IHttpContextAccessor httpContextAccessor,
IOptions<WebRoutingSettings> webRoutingSettings)
{
_httpContextAccessor = httpContextAccessor;
_umbracoContextAccessor = umbracoContextAccessor;
_webRoutingSettings = webRoutingSettings.Value;
}
/// <inheritdoc/>
public string GetRequestValue(string name) => GetFormValue(name) ?? GetQueryStringValue(name);
public string GetFormValue(string name)
private string GetFormValue(string name)
{
var request = _httpContextAccessor.GetRequiredHttpContext().Request;
if (!request.HasFormContentType) return null;
return request.Form[name];
}
/// <inheritdoc/>
public string GetQueryStringValue(string name) => _httpContextAccessor.GetRequiredHttpContext().Request.Query[name];
public event EventHandler<UmbracoRequestEventArgs> EndRequest;
public event EventHandler<RoutableAttemptEventArgs> RouteAttempt;
/// <inheritdoc/>
public Uri GetRequestUrl() => _httpContextAccessor.HttpContext != null ? new Uri(_httpContextAccessor.HttpContext.Request.GetEncodedUrl()) : null;
/// <inheritdoc/>
public Uri GetApplicationUrl()
{
// Fixme: This causes problems with site swap on azure because azure pre-warms a site by calling into `localhost` and when it does that
@@ -80,17 +84,16 @@ namespace Umbraco.Web.Common.AspNetCore
return _currentApplicationUrl;
}
/// <summary>
/// This just initializes the application URL on first request attempt
/// TODO: This doesn't belong here, the GetApplicationUrl doesn't belong to IRequestAccessor
/// this should be part of middleware not a lazy init based on an INotification
/// </summary>
public void Handle(UmbracoRequestBegin notification)
{
var reason = EnsureRoutableOutcome.IsRoutable; //TODO get the correct value here like in UmbracoInjectedModule
RouteAttempt?.Invoke(this, new RoutableAttemptEventArgs(reason, _umbracoContextAccessor.UmbracoContext));
}
public void Handle(UmbracoRequestEnd notification)
{
EndRequest?.Invoke(this, new UmbracoRequestEventArgs(_umbracoContextAccessor.UmbracoContext));
}
=> LazyInitializer.EnsureInitialized(ref _hasAppUrl, ref _isInit, ref _initLocker, () =>
{
GetApplicationUrl();
return true;
});
}
}

View File

@@ -51,17 +51,29 @@ namespace Umbraco.Web.Common.AspNetCore
{
get
{
if (_helper != null) return _helper;
if (_helper != null)
{
return _helper;
}
var model = ViewData.Model;
TModel model = ViewData.Model;
var content = model as IPublishedContent;
if (content == null && model is IContentModel)
content = ((IContentModel) model).Content;
if (content is null && model is IContentModel contentModel)
{
content = contentModel.Content;
}
if (content is null)
{
content = UmbracoContext?.PublishedRequest?.PublishedContent;
}
_helper = Context.RequestServices.GetRequiredService<UmbracoHelper>();
if (content != null)
if (!(content is null))
{
_helper.AssignedContentItem = content;
}
return _helper;
}

View File

@@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Mvc.ViewFeatures;
namespace Umbraco.Web.Common.Controllers
{
/// <summary>
/// A request feature to allowing proxying viewdata from one controller to another
/// </summary>
public sealed class ProxyViewDataFeature
{
/// <summary>
/// Initializes a new instance of the <see cref="ProxyViewDataFeature"/> class.
/// </summary>
public ProxyViewDataFeature(ViewDataDictionary viewData) => ViewData = viewData;
/// <summary>
/// Gets the <see cref="ViewDataDictionary"/>
/// </summary>
public ViewDataDictionary ViewData { get; }
}
}

View File

@@ -17,12 +17,13 @@ namespace Umbraco.Web.Common.Controllers
/// </summary>
protected UmbracoRouteValues GetUmbracoRouteValues(ResultExecutingContext context)
{
if (!context.RouteData.Values.TryGetValue(Core.Constants.Web.UmbracoRouteDefinitionDataToken, out var def))
UmbracoRouteValues routeVals = context.HttpContext.Features.Get<UmbracoRouteValues>();
if (routeVals == null)
{
throw new InvalidOperationException($"No route value found with key {Core.Constants.Web.UmbracoRouteDefinitionDataToken}");
throw new InvalidOperationException($"No {nameof(UmbracoRouteValues)} feature was found in the HttpContext");
}
return (UmbracoRouteValues)def;
return routeVals;
}
/// <summary>

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
@@ -71,11 +72,11 @@ namespace Umbraco.Web.Common.Controllers
return _umbracoRouteValues;
}
_umbracoRouteValues = HttpContext.GetRouteValue(Core.Constants.Web.UmbracoRouteDefinitionDataToken) as UmbracoRouteValues;
_umbracoRouteValues = HttpContext.Features.Get<UmbracoRouteValues>();
if (_umbracoRouteValues == null)
{
throw new InvalidOperationException($"No route value found with key {Core.Constants.Web.UmbracoRouteDefinitionDataToken}");
throw new InvalidOperationException($"No {nameof(UmbracoRouteValues)} feature was found in the HttpContext");
}
return _umbracoRouteValues;
@@ -149,6 +150,20 @@ namespace Umbraco.Web.Common.Controllers
break;
case UmbracoRouteResult.Success:
default:
// Check if there's a ProxyViewDataFeature in the request.
// If there it is means that we are proxying/executing this controller
// from another controller and we need to merge it's ViewData with this one
// since this one will be empty.
ProxyViewDataFeature saveViewData = HttpContext.Features.Get<ProxyViewDataFeature>();
if (saveViewData != null)
{
foreach (KeyValuePair<string, object> kv in saveViewData.ViewData)
{
ViewData[kv.Key] = kv.Value;
}
}
// continue normally
await next();
break;

View File

@@ -1,4 +1,4 @@
using Umbraco.Core.Composing;
using Umbraco.Core.Composing;
namespace Umbraco.Web.Common.Controllers
{
@@ -7,8 +7,9 @@ namespace Umbraco.Web.Common.Controllers
/// </summary>
public abstract class UmbracoApiController : UmbracoApiControllerBase, IDiscoverable
{
// TODO: Should this only exist in the back office project? These really are only ever used for the back office AFAIK
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoApiController"/> class.
/// </summary>
protected UmbracoApiController()
{
}

View File

@@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Web.Common.Attributes;
using Umbraco.Web.Common.Authorization;
using Umbraco.Web.Common.Filters;
using Umbraco.Web.Features;
namespace Umbraco.Web.Common.Controllers
@@ -18,9 +17,10 @@ namespace Umbraco.Web.Common.Controllers
[UmbracoApiController]
public abstract class UmbracoApiControllerBase : ControllerBase, IUmbracoFeature
{
// TODO: Should this only exist in the back office project? These really are only ever used for the back office AFAIK
public UmbracoApiControllerBase()
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoApiControllerBase"/> class.
/// </summary>
protected UmbracoApiControllerBase()
{
}
}

View File

@@ -234,7 +234,6 @@ namespace Umbraco.Web.Common.DependencyInjection
builder.Services.AddUnique<IHttpContextAccessor, HttpContextAccessor>();
builder.Services.AddUnique<IRequestAccessor, AspNetCoreRequestAccessor>();
builder.AddNotificationHandler<UmbracoRequestBegin, AspNetCoreRequestAccessor>();
builder.AddNotificationHandler<UmbracoRequestEnd, AspNetCoreRequestAccessor>();
// Password hasher
builder.Services.AddUnique<IPasswordHasher, AspNetCorePasswordHasher>();
@@ -383,9 +382,11 @@ namespace Umbraco.Web.Common.DependencyInjection
private static IHostingEnvironment GetTemporaryHostingEnvironment(IWebHostEnvironment webHostEnvironment, IConfiguration config)
{
var hostingSettings = config.GetSection(Core.Constants.Configuration.ConfigHosting).Get<HostingSettings>() ?? new HostingSettings();
var webRoutingSettings = config.GetSection(Core.Constants.Configuration.ConfigWebRouting).Get<WebRoutingSettings>() ?? new WebRoutingSettings();
var wrappedHostingSettings = new OptionsMonitorAdapter<HostingSettings>(hostingSettings);
var wrappedWebRoutingSettings = new OptionsMonitorAdapter<WebRoutingSettings>(webRoutingSettings);
return new AspNetCoreHostingEnvironment(wrappedHostingSettings, webHostEnvironment);
return new AspNetCoreHostingEnvironment(wrappedHostingSettings,wrappedWebRoutingSettings, webHostEnvironment);
}
}
}

View File

@@ -1,20 +0,0 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Microsoft.AspNetCore.Http;
namespace Umbraco.Core.Events
{
/// <summary>
/// Notification raised on each request begin.
/// </summary>
public class UmbracoRequestBegin : INotification
{
public UmbracoRequestBegin(HttpContext httpContext)
{
HttpContext = httpContext;
}
public HttpContext HttpContext { get; }
};
}

View File

@@ -1,23 +0,0 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Microsoft.AspNetCore.Http;
namespace Umbraco.Core.Events
{
/// <summary>
/// Notification raised on each request end.
/// </summary>
public class UmbracoRequestEnd : INotification
{
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoRequestEnd"/> class.
/// </summary>
public UmbracoRequestEnd(HttpContext httpContext) => HttpContext = httpContext;
/// <summary>
/// Gets the <see cref="HttpContext"/>
/// </summary>
public HttpContext HttpContext { get; }
}
}

View File

@@ -1,18 +1,18 @@
using System;
using Umbraco.Core.Models.Blocks;
using Umbraco.Core.Models.PublishedContent;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Umbraco.Core.Models.Blocks;
using Umbraco.Core.Models.PublishedContent;
namespace Umbraco.Extensions
{
public static class BlockListTemplateExtensions
{
public const string DefaultFolder = "BlockList/";
public const string DefaultTemplate = "Default";
public const string DefaultFolder = "blocklist/";
public const string DefaultTemplate = "default";
public static IHtmlContent GetBlockListHtml(this HtmlHelper html, BlockListModel model, string template = DefaultTemplate)
public static IHtmlContent GetBlockListHtml(this IHtmlHelper html, BlockListModel model, string template = DefaultTemplate)
{
if (model?.Count == 0) return new HtmlString(string.Empty);
@@ -20,11 +20,11 @@ namespace Umbraco.Extensions
return html.Partial(view, model);
}
public static IHtmlContent GetBlockListHtml(this HtmlHelper html, IPublishedProperty property, string template = DefaultTemplate) => GetBlockListHtml(html, property?.GetValue() as BlockListModel, template);
public static IHtmlContent GetBlockListHtml(this IHtmlHelper html, IPublishedProperty property, string template = DefaultTemplate) => GetBlockListHtml(html, property?.GetValue() as BlockListModel, template);
public static IHtmlContent GetBlockListHtml(this HtmlHelper html, IPublishedContent contentItem, string propertyAlias) => GetBlockListHtml(html, contentItem, propertyAlias, DefaultTemplate);
public static IHtmlContent GetBlockListHtml(this IHtmlHelper html, IPublishedContent contentItem, string propertyAlias) => GetBlockListHtml(html, contentItem, propertyAlias, DefaultTemplate);
public static IHtmlContent GetBlockListHtml(this HtmlHelper html, IPublishedContent contentItem, string propertyAlias, string template)
public static IHtmlContent GetBlockListHtml(this IHtmlHelper html, IPublishedContent contentItem, string propertyAlias, string template)
{
if (propertyAlias == null) throw new ArgumentNullException(nameof(propertyAlias));
if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyAlias));

View File

@@ -29,21 +29,20 @@ namespace Umbraco.Web.Common.Extensions
var pattern = new StringBuilder(rootSegment);
if (!prefixPathSegment.IsNullOrWhiteSpace())
{
pattern.Append("/").Append(prefixPathSegment);
pattern.Append('/').Append(prefixPathSegment);
}
if (includeControllerNameInRoute)
{
pattern.Append("/").Append(controllerName);
pattern.Append('/').Append(controllerName);
}
pattern.Append("/").Append("{action}/{id?}");
pattern.Append("/{action}/{id?}");
var defaults = defaultAction.IsNullOrWhiteSpace()
? (object)new { controller = controllerName }
: new { controller = controllerName, action = defaultAction };
if (areaName.IsNullOrWhiteSpace())
{
endpoints.MapControllerRoute(
@@ -70,6 +69,7 @@ namespace Umbraco.Web.Common.Extensions
/// <summary>
/// Used to map Umbraco controllers consistently
/// </summary>
/// <typeparam name="T">The <see cref="ControllerBase"/> type to route</typeparam>
public static void MapUmbracoRoute<T>(
this IEndpointRouteBuilder endpoints,
string rootSegment,
@@ -82,8 +82,9 @@ namespace Umbraco.Web.Common.Extensions
=> endpoints.MapUmbracoRoute(typeof(T), rootSegment, areaName, prefixPathSegment, defaultAction, includeControllerNameInRoute, constraints);
/// <summary>
/// Used to map Umbraco api controllers consistently
/// Used to map controllers as Umbraco API routes consistently
/// </summary>
/// <typeparam name="T">The <see cref="ControllerBase"/> type to route</typeparam>
public static void MapUmbracoApiRoute<T>(
this IEndpointRouteBuilder endpoints,
string rootSegment,
@@ -95,7 +96,7 @@ namespace Umbraco.Web.Common.Extensions
=> endpoints.MapUmbracoApiRoute(typeof(T), rootSegment, areaName, isBackOffice, defaultAction, constraints);
/// <summary>
/// Used to map Umbraco api controllers consistently
/// Used to map controllers as Umbraco API routes consistently
/// </summary>
public static void MapUmbracoApiRoute(
this IEndpointRouteBuilder endpoints,

View File

@@ -12,7 +12,7 @@ namespace Umbraco.Extensions
var asString = property.GetValue() as string;
if (asString != null && string.IsNullOrEmpty(asString)) return new HtmlString(string.Empty);
var view = "Grid/" + framework;
var view = "grid/" + framework;
return html.Partial(view, property.GetValue());
}
@@ -34,7 +34,7 @@ namespace Umbraco.Extensions
if (propertyAlias == null) throw new ArgumentNullException(nameof(propertyAlias));
if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyAlias));
var view = "Grid/" + framework;
var view = "grid/" + framework;
var prop = contentItem.GetProperty(propertyAlias);
if (prop == null) throw new InvalidOperationException("No property type found with alias " + propertyAlias);
var model = prop.GetValue();
@@ -63,7 +63,7 @@ namespace Umbraco.Extensions
if (propertyAlias == null) throw new ArgumentNullException(nameof(propertyAlias));
if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyAlias));
var view = "Grid/" + framework;
var view = "grid/" + framework;
var prop = contentItem.GetProperty(propertyAlias);
if (prop == null) throw new InvalidOperationException("No property type found with alias " + propertyAlias);
var model = prop.GetValue();
@@ -78,7 +78,7 @@ namespace Umbraco.Extensions
var asString = property.GetValue() as string;
if (asString != null && string.IsNullOrEmpty(asString)) return new HtmlString(string.Empty);
var view = "Grid/" + framework;
var view = "grid/" + framework;
return html.Partial(view, property.GetValue());
}
@@ -100,7 +100,7 @@ namespace Umbraco.Extensions
if (propertyAlias == null) throw new ArgumentNullException(nameof(propertyAlias));
if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyAlias));
var view = "Grid/" + framework;
var view = "grid/" + framework;
var prop = contentItem.GetProperty(propertyAlias);
if (prop == null) throw new InvalidOperationException("No property type found with alias " + propertyAlias);
var model = prop.GetValue();

View File

@@ -2,13 +2,15 @@ using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using Umbraco.Core;
using Microsoft.AspNetCore.Routing;
using System.Reflection;
using Umbraco.Web.Common.Install;
using Umbraco.Core.Hosting;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Umbraco.Core;
using Umbraco.Core.Hosting;
using Umbraco.Web.Common.Controllers;
using Umbraco.Web.Common.Install;
using Umbraco.Web.Mvc;
namespace Umbraco.Extensions
{
@@ -17,8 +19,6 @@ namespace Umbraco.Extensions
/// <summary>
/// Return the back office url if the back office is installed
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
public static string GetBackOfficeUrl(this LinkGenerator linkGenerator, IHostingEnvironment hostingEnvironment)
{
@@ -26,7 +26,10 @@ namespace Umbraco.Extensions
try
{
backOfficeControllerType = Assembly.Load("Umbraco.Web.BackOffice")?.GetType("Umbraco.Web.BackOffice.Controllers.BackOfficeController");
if (backOfficeControllerType == null) return "/"; // this would indicate that the installer is installed without the back office
if (backOfficeControllerType == null)
{
return "/"; // this would indicate that the installer is installed without the back office
}
}
catch
{
@@ -39,47 +42,33 @@ namespace Umbraco.Extensions
/// <summary>
/// Returns the URL for the installer
/// </summary>
/// <param name="linkGenerator"></param>
/// <returns></returns>
public static string GetInstallerUrl(this LinkGenerator linkGenerator)
{
return linkGenerator.GetPathByAction(nameof(InstallController.Index), ControllerExtensions.GetControllerName<InstallController>(), new { area = Constants.Web.Mvc.InstallArea });
}
=> linkGenerator.GetPathByAction(nameof(InstallController.Index), ControllerExtensions.GetControllerName<InstallController>(), new { area = Constants.Web.Mvc.InstallArea });
/// <summary>
/// Returns the URL for the installer api
/// </summary>
/// <param name="linkGenerator"></param>
/// <returns></returns>
public static string GetInstallerApiUrl(this LinkGenerator linkGenerator)
{
return linkGenerator.GetPathByAction(nameof(InstallApiController.GetSetup),
=> linkGenerator.GetPathByAction(
nameof(InstallApiController.GetSetup),
ControllerExtensions.GetControllerName<InstallApiController>(),
new { area = Constants.Web.Mvc.InstallArea }).TrimEnd(nameof(InstallApiController.GetSetup));
}
/// <summary>
/// Return the Url for a Web Api service
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="url"></param>
/// <param name="actionName"></param>
/// <param name="id"></param>
/// <returns></returns>
/// <typeparam name="T">The <see cref="UmbracoApiControllerBase"/></typeparam>
public static string GetUmbracoApiService<T>(this LinkGenerator linkGenerator, string actionName, object id = null)
where T : UmbracoApiControllerBase
{
return linkGenerator.GetUmbracoApiService(actionName, typeof(T), new Dictionary<string, object>()
{
["id"] = id
});
}
where T : UmbracoApiControllerBase => linkGenerator.GetUmbracoControllerUrl(
actionName,
typeof(T),
new Dictionary<string, object>()
{
["id"] = id
});
public static string GetUmbracoApiService<T>(this LinkGenerator linkGenerator, string actionName, IDictionary<string, object> values)
where T : UmbracoApiControllerBase
{
return linkGenerator.GetUmbracoApiService(actionName, typeof(T), values);
}
where T : UmbracoApiControllerBase => linkGenerator.GetUmbracoControllerUrl(actionName, typeof(T), values);
public static string GetUmbracoApiServiceBaseUrl<T>(this LinkGenerator linkGenerator, Expression<Func<T, object>> methodSelector)
where T : UmbracoApiControllerBase
@@ -93,66 +82,86 @@ namespace Umbraco.Extensions
}
/// <summary>
/// Return the Url for a Web Api service
/// Return the Url for an Umbraco controller
/// </summary>
/// <param name="url"></param>
/// <param name="actionName"></param>
/// <param name="controllerName"></param>
/// <param name="area"></param>
/// <param name="id"></param>
/// <returns></returns>
public static string GetUmbracoApiService(this LinkGenerator linkGenerator, string actionName, string controllerName, string area, IDictionary<string,object> dict = null)
public static string GetUmbracoControllerUrl(this LinkGenerator linkGenerator, string actionName, string controllerName, string area, IDictionary<string, object> dict = null)
{
if (actionName == null) throw new ArgumentNullException(nameof(actionName));
if (string.IsNullOrWhiteSpace(actionName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(actionName));
if (controllerName == null) throw new ArgumentNullException(nameof(controllerName));
if (string.IsNullOrWhiteSpace(controllerName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(controllerName));
if (actionName == null)
{
throw new ArgumentNullException(nameof(actionName));
}
if (string.IsNullOrWhiteSpace(actionName))
{
throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(actionName));
}
if (controllerName == null)
{
throw new ArgumentNullException(nameof(controllerName));
}
if (string.IsNullOrWhiteSpace(controllerName))
{
throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(controllerName));
}
if (dict is null)
{
dict = new Dictionary<string, object>();
}
if (!area.IsNullOrWhiteSpace())
{
dict["area"] = area;
}
var values = dict.Aggregate(new ExpandoObject() as IDictionary<string, object>,
(a, p) => { a.Add(p.Key, p.Value); return a; });
IDictionary<string, object> values = dict.Aggregate(
new ExpandoObject() as IDictionary<string, object>,
(a, p) =>
{
a.Add(p.Key, p.Value);
return a;
});
return linkGenerator.GetPathByAction(actionName, controllerName, values);
}
/// <summary>
/// Return the Url for a Web Api service
/// Return the Url for an Umbraco controller
/// </summary>
/// <param name="url"></param>
/// <param name="actionName"></param>
/// <param name="apiControllerType"></param>
/// <param name="id"></param>
/// <returns></returns>
public static string GetUmbracoApiService(this LinkGenerator linkGenerator, string actionName, Type apiControllerType, IDictionary<string,object> values = null)
public static string GetUmbracoControllerUrl(this LinkGenerator linkGenerator, string actionName, Type controllerType, IDictionary<string, object> values = null)
{
if (actionName == null) throw new ArgumentNullException(nameof(actionName));
if (string.IsNullOrWhiteSpace(actionName)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(actionName));
if (apiControllerType == null) throw new ArgumentNullException(nameof(apiControllerType));
if (actionName == null)
{
throw new ArgumentNullException(nameof(actionName));
}
var area = "";
if (string.IsNullOrWhiteSpace(actionName))
{
throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(actionName));
}
if (!typeof(UmbracoApiControllerBase).IsAssignableFrom(apiControllerType))
throw new InvalidOperationException($"The controller {apiControllerType} is of type {typeof(UmbracoApiControllerBase)}");
if (controllerType == null)
{
throw new ArgumentNullException(nameof(controllerType));
}
var metaData = PluginController.GetMetadata(apiControllerType);
var area = string.Empty;
if (!typeof(ControllerBase).IsAssignableFrom(controllerType))
{
throw new InvalidOperationException($"The controller {controllerType} is of type {typeof(ControllerBase)}");
}
PluginControllerMetadata metaData = PluginController.GetMetadata(controllerType);
if (metaData.AreaName.IsNullOrWhiteSpace() == false)
{
//set the area to the plugin area
// set the area to the plugin area
area = metaData.AreaName;
}
return linkGenerator.GetUmbracoApiService(actionName, ControllerExtensions.GetControllerName(apiControllerType), area, values);
return linkGenerator.GetUmbracoControllerUrl(actionName, ControllerExtensions.GetControllerName(controllerType), area, values);
}
public static string GetUmbracoApiService<T>(this LinkGenerator linkGenerator, Expression<Func<T, object>> methodSelector)
@@ -170,6 +179,7 @@ namespace Umbraco.Extensions
{
return linkGenerator.GetUmbracoApiService<T>(method.Name);
}
return linkGenerator.GetUmbracoApiService<T>(method.Name, methodParams);
}
}

View File

@@ -29,7 +29,8 @@ namespace Umbraco.Web.Common.Localization
/// <inheritdoc/>
public override Task<ProviderCultureResult> DetermineProviderCultureResult(HttpContext httpContext)
{
if (httpContext.GetRouteValue(Core.Constants.Web.UmbracoRouteDefinitionDataToken) is UmbracoRouteValues routeValues)
UmbracoRouteValues routeValues = httpContext.Features.Get<UmbracoRouteValues>();
if (routeValues != null)
{
string culture = routeValues.PublishedRequest?.Culture;
if (culture != null)

View File

@@ -17,6 +17,7 @@ using Umbraco.Core.Hosting;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Extensions;
using Umbraco.Web.Macros;
using static Umbraco.Core.Constants.Web.Routing;
namespace Umbraco.Web.Common.Macros
{
@@ -87,8 +88,8 @@ namespace Umbraco.Web.Common.Macros
var httpContext = _httpContextAccessor.GetRequiredHttpContext();
//var umbCtx = _getUmbracoContext();
var routeVals = new RouteData();
routeVals.Values.Add("controller", "PartialViewMacro");
routeVals.Values.Add("action", "Index");
routeVals.Values.Add(ControllerToken, "PartialViewMacro");
routeVals.Values.Add(ActionToken, "Index");
//TODO: Was required for UmbracoViewPage need to figure out if we still need that, i really don't think this is necessary
//routeVals.DataTokens.Add(Core.Constants.Web.UmbracoContextDataToken, umbCtx);

View File

@@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Events;
using Umbraco.Core.Hosting;
using Umbraco.Core.Logging;
using Umbraco.Extensions;
using Umbraco.Web.Common.Profiler;
@@ -36,6 +37,7 @@ namespace Umbraco.Web.Common.Middleware
private readonly IBackOfficeSecurityFactory _backofficeSecurityFactory;
private readonly PublishedSnapshotServiceEventHandler _publishedSnapshotServiceEventHandler;
private readonly IEventAggregator _eventAggregator;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly WebProfiler _profiler;
private static bool s_cacheInitialized = false;
private static bool s_cacheInitializedFlag = false;
@@ -51,7 +53,8 @@ namespace Umbraco.Web.Common.Middleware
IBackOfficeSecurityFactory backofficeSecurityFactory,
PublishedSnapshotServiceEventHandler publishedSnapshotServiceEventHandler,
IEventAggregator eventAggregator,
IProfiler profiler)
IProfiler profiler,
IHostingEnvironment hostingEnvironment)
{
_logger = logger;
_umbracoContextFactory = umbracoContextFactory;
@@ -59,6 +62,7 @@ namespace Umbraco.Web.Common.Middleware
_backofficeSecurityFactory = backofficeSecurityFactory;
_publishedSnapshotServiceEventHandler = publishedSnapshotServiceEventHandler;
_eventAggregator = eventAggregator;
_hostingEnvironment = hostingEnvironment;
_profiler = profiler as WebProfiler; // Ignore if not a WebProfiler
}
@@ -81,6 +85,10 @@ namespace Umbraco.Web.Common.Middleware
_backofficeSecurityFactory.EnsureBackOfficeSecurity(); // Needs to be before UmbracoContext, TODO: Why?
UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext();
Uri currentApplicationUrl = GetApplicationUrlFromCurrentRequest(context.Request);
_hostingEnvironment.EnsureApplicationMainUrl(currentApplicationUrl);
bool isFrontEndRequest = umbracoContextReference.UmbracoContext.IsFrontEndUmbracoRequest();
var pathAndQuery = context.Request.GetEncodedPathAndQuery();
@@ -95,7 +103,7 @@ namespace Umbraco.Web.Common.Middleware
try
{
await _eventAggregator.PublishAsync(new UmbracoRequestBegin(context));
await _eventAggregator.PublishAsync(new UmbracoRequestBegin(umbracoContextReference.UmbracoContext));
}
catch (Exception ex)
{
@@ -111,7 +119,7 @@ namespace Umbraco.Web.Common.Middleware
}
finally
{
await _eventAggregator.PublishAsync(new UmbracoRequestEnd(context));
await _eventAggregator.PublishAsync(new UmbracoRequestEnd(umbracoContextReference.UmbracoContext));
}
}
}
@@ -119,7 +127,7 @@ namespace Umbraco.Web.Common.Middleware
{
if (isFrontEndRequest)
{
LogHttpRequest.TryGetCurrentHttpRequestId(out var httpRequestId, _requestCache);
LogHttpRequest.TryGetCurrentHttpRequestId(out Guid httpRequestId, _requestCache);
_logger.LogTrace("End Request [{HttpRequestId}]: {RequestUrl} ({RequestDuration}ms)", httpRequestId, pathAndQuery, DateTime.Now.Subtract(umbracoContextReference.UmbracoContext.ObjectCreated).TotalMilliseconds);
}
@@ -138,6 +146,18 @@ namespace Umbraco.Web.Common.Middleware
_profiler?.UmbracoApplicationEndRequest(context);
}
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>
/// Any object that is in the HttpContext.Items collection that is IDisposable will get disposed on the end of the request
/// </summary>

View File

@@ -30,8 +30,8 @@ namespace Umbraco.Web.Common.ModelBinders
// only IPublishedContent will ever exist in the request so when this model binder is used as an IModelBinder
// in the aspnet pipeline it will really only support converting from IPublishedContent which is contained
// in the UmbracoRouteValues --> IContentModel
if (!bindingContext.ActionContext.RouteData.Values.TryGetValue(Core.Constants.Web.UmbracoRouteDefinitionDataToken, out var source)
|| !(source is UmbracoRouteValues umbracoRouteValues))
UmbracoRouteValues umbracoRouteValues = bindingContext.HttpContext.Features.Get<UmbracoRouteValues>();
if (umbracoRouteValues is null)
{
return Task.CompletedTask;
}

View File

@@ -1,4 +1,5 @@
using System;
using Microsoft.AspNetCore.Mvc.Controllers;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Extensions;
using Umbraco.Web.Common.Controllers;
@@ -21,29 +22,25 @@ namespace Umbraco.Web.Common.Routing
/// </summary>
public UmbracoRouteValues(
IPublishedRequest publishedRequest,
string controllerName = null,
Type controllerType = null,
string actionName = DefaultActionName,
ControllerActionDescriptor controllerActionDescriptor,
string templateName = null,
bool hasHijackedRoute = false)
{
ControllerName = controllerName ?? ControllerExtensions.GetControllerName<RenderController>();
ControllerType = controllerType ?? typeof(RenderController);
PublishedRequest = publishedRequest;
ControllerActionDescriptor = controllerActionDescriptor;
HasHijackedRoute = hasHijackedRoute;
ActionName = actionName;
TemplateName = templateName;
}
/// <summary>
/// Gets the controller name
/// </summary>
public string ControllerName { get; }
public string ControllerName => ControllerActionDescriptor.ControllerName;
/// <summary>
/// Gets the action name
/// </summary>
public string ActionName { get; }
public string ActionName => ControllerActionDescriptor.ActionName;
/// <summary>
/// Gets the template name
@@ -51,9 +48,14 @@ namespace Umbraco.Web.Common.Routing
public string TemplateName { get; }
/// <summary>
/// Gets the Controller type found for routing to
/// Gets the controller type
/// </summary>
public Type ControllerType { get; }
public Type ControllerType => ControllerActionDescriptor.ControllerTypeInfo;
/// <summary>
/// Gets the Controller descriptor found for routing to
/// </summary>
public ControllerActionDescriptor ControllerActionDescriptor { get; }
/// <summary>
/// Gets the <see cref="IPublishedRequest"/>

View File

@@ -1,5 +1,6 @@
using System;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Net;
using System.Web;
@@ -12,64 +13,80 @@ namespace Umbraco.Web.Common.Security
{
public class EncryptionHelper
{
// TODO: Decide if these belong here... I don't think so since this all has to do with surface controller routes
// could also just be injected too....
private static IDataProtector CreateDataProtector(IDataProtectionProvider dataProtectionProvider)
{
return dataProtectionProvider.CreateProtector(nameof(EncryptionHelper));
}
=> dataProtectionProvider.CreateProtector(nameof(EncryptionHelper));
public static string Decrypt(string encryptedString, IDataProtectionProvider dataProtectionProvider)
{
return CreateDataProtector(dataProtectionProvider).Unprotect(encryptedString);
}
=> CreateDataProtector(dataProtectionProvider).Unprotect(encryptedString);
public static string Encrypt(string plainString, IDataProtectionProvider dataProtectionProvider)
{
return CreateDataProtector(dataProtectionProvider).Protect(plainString);
}
=> CreateDataProtector(dataProtectionProvider).Protect(plainString);
/// <summary>
/// This is used in methods like BeginUmbracoForm and SurfaceAction to generate an encrypted string which gets submitted in a request for which
/// Umbraco can decrypt during the routing process in order to delegate the request to a specific MVC Controller.
/// </summary>
/// <param name="dataProtectionProvider"></param>
/// <param name="controllerName"></param>
/// <param name="controllerAction"></param>
/// <param name="area"></param>
/// <param name="additionalRouteVals"></param>
/// <returns></returns>
public static string CreateEncryptedRouteString(IDataProtectionProvider dataProtectionProvider, string controllerName, string controllerAction, string area, object additionalRouteVals = null)
{
if (dataProtectionProvider == null) throw new ArgumentNullException(nameof(dataProtectionProvider));
if (controllerName == null) throw new ArgumentNullException(nameof(controllerName));
if (string.IsNullOrEmpty(controllerName)) throw new ArgumentException("Value can't be empty.", nameof(controllerName));
if (controllerAction == null) throw new ArgumentNullException(nameof(controllerAction));
if (string.IsNullOrEmpty(controllerAction)) throw new ArgumentException("Value can't be empty.", nameof(controllerAction));
if (area == null) throw new ArgumentNullException(nameof(area));
if (dataProtectionProvider is null)
{
throw new ArgumentNullException(nameof(dataProtectionProvider));
}
//need to create a params string as Base64 to put into our hidden field to use during the routes
if (string.IsNullOrEmpty(controllerName))
{
throw new ArgumentException($"'{nameof(controllerName)}' cannot be null or empty.", nameof(controllerName));
}
if (string.IsNullOrEmpty(controllerAction))
{
throw new ArgumentException($"'{nameof(controllerAction)}' cannot be null or empty.", nameof(controllerAction));
}
if (area is null)
{
throw new ArgumentNullException(nameof(area));
}
// need to create a params string as Base64 to put into our hidden field to use during the routes
var surfaceRouteParams = $"{ViewConstants.ReservedAdditionalKeys.Controller}={WebUtility.UrlEncode(controllerName)}&{ViewConstants.ReservedAdditionalKeys.Action}={WebUtility.UrlEncode(controllerAction)}&{ViewConstants.ReservedAdditionalKeys.Area}={area}";
//checking if the additional route values is already a dictionary and convert to querystring
// checking if the additional route values is already a dictionary and convert to querystring
string additionalRouteValsAsQuery;
if (additionalRouteVals != null)
{
if (additionalRouteVals is Dictionary<string, object> additionalRouteValsAsDictionary)
{
additionalRouteValsAsQuery = additionalRouteValsAsDictionary.ToQueryString();
}
else
{
additionalRouteValsAsQuery = additionalRouteVals.ToDictionary<object>().ToQueryString();
}
}
else
{
additionalRouteValsAsQuery = null;
}
if (additionalRouteValsAsQuery.IsNullOrWhiteSpace() == false)
{
surfaceRouteParams += "&" + additionalRouteValsAsQuery;
}
return Encrypt(surfaceRouteParams, dataProtectionProvider);
}
public static bool DecryptAndValidateEncryptedRouteString(IDataProtectionProvider dataProtectionProvider, string encryptedString, out IDictionary<string, string> parts)
{
if (dataProtectionProvider == null) throw new ArgumentNullException(nameof(dataProtectionProvider));
if (dataProtectionProvider == null)
{
throw new ArgumentNullException(nameof(dataProtectionProvider));
}
string decryptedString;
try
{
@@ -81,22 +98,32 @@ namespace Umbraco.Web.Common.Security
parts = null;
return false;
}
var parsedQueryString = HttpUtility.ParseQueryString(decryptedString);
NameValueCollection parsedQueryString = HttpUtility.ParseQueryString(decryptedString);
parts = new Dictionary<string, string>();
foreach (var key in parsedQueryString.AllKeys)
{
parts[key] = parsedQueryString[key];
}
//validate all required keys exist
//the controller
// validate all required keys exist
// the controller
if (parts.All(x => x.Key != ViewConstants.ReservedAdditionalKeys.Controller))
{
return false;
//the action
}
// the action
if (parts.All(x => x.Key != ViewConstants.ReservedAdditionalKeys.Action))
{
return false;
//the area
}
// the area
if (parts.All(x => x.Key != ViewConstants.ReservedAdditionalKeys.Area))
{
return false;
}
return true;
}