Merge remote-tracking branch 'origin/netcore/netcore' into feature/netcore/msdi-refactor-use-DefaultControllerActivator
# Conflicts: # src/Umbraco.Web.Website/Extensions/UmbracoWebstiteServiceCollectionExtensions.cs # src/Umbraco.Web.Website/Runtime/WebsiteComposer.cs
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using Microsoft.AspNetCore.Mvc.Razor;
|
||||
@@ -8,12 +9,12 @@ using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Configuration.Models;
|
||||
using Umbraco.Core.IO;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Core.Strings;
|
||||
using Umbraco.Extensions;
|
||||
using Umbraco.Web.Common.ModelBinders;
|
||||
|
||||
namespace Umbraco.Web.Common.AspNetCore
|
||||
@@ -110,6 +111,8 @@ namespace Umbraco.Web.Common.AspNetCore
|
||||
ViewData = (ViewDataDictionary<TModel>) viewData;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// viewData is the ViewDataDictionary (maybe <TModel>) that we have
|
||||
// modelType is the type of the model that we need to bind to
|
||||
//
|
||||
@@ -143,5 +146,16 @@ namespace Umbraco.Web.Common.AspNetCore
|
||||
var nViewData = (ViewDataDictionary)Activator.CreateInstance(nViewDataType, tViewData);
|
||||
return nViewData;
|
||||
}
|
||||
|
||||
public HtmlString RenderSection(string name, HtmlString defaultContents)
|
||||
{
|
||||
return RazorPageExtensions.RenderSection(this, name, defaultContents);
|
||||
}
|
||||
|
||||
public HtmlString RenderSection(string name, string defaultContents)
|
||||
{
|
||||
return RazorPageExtensions.RenderSection(this, name, defaultContents);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,5 +8,12 @@
|
||||
internal const string ViewLocation = "~/Views";
|
||||
|
||||
internal const string DataTokenCurrentViewContext = "umbraco-current-view-context";
|
||||
|
||||
internal static class ReservedAdditionalKeys
|
||||
{
|
||||
internal const string Controller = "c";
|
||||
internal const string Action = "a";
|
||||
internal const string Area = "ar";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace Umbraco.Web.Common.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Exception that occurs when an Umbraco form route string is invalid
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public sealed class HttpUmbracoFormRouteStringException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HttpUmbracoFormRouteStringException" /> class.
|
||||
/// </summary>
|
||||
public HttpUmbracoFormRouteStringException()
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HttpUmbracoFormRouteStringException" /> class.
|
||||
/// </summary>
|
||||
/// <param name="info">The <see cref="T:System.Runtime.Serialization.SerializationInfo" /> that holds the serialized object data about the exception being thrown.</param>
|
||||
/// <param name="context">The <see cref="T:System.Runtime.Serialization.StreamingContext" /> that holds the contextual information about the source or destination.</param>
|
||||
private HttpUmbracoFormRouteStringException(SerializationInfo info, StreamingContext context)
|
||||
: base(info, context)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HttpUmbracoFormRouteStringException" /> class.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message displayed to the client when the exception is thrown.</param>
|
||||
public HttpUmbracoFormRouteStringException(string message)
|
||||
: base(message)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HttpUmbracoFormRouteStringException" /> class.
|
||||
/// </summary>
|
||||
/// <param name="message">The error message displayed to the client when the exception is thrown.</param>
|
||||
/// <param name="innerException">The <see cref="P:System.Exception.InnerException" />, if any, that threw the current exception.</param>
|
||||
public HttpUmbracoFormRouteStringException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{ }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
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;
|
||||
|
||||
namespace Umbraco.Extensions
|
||||
{
|
||||
public static class BlockListTemplateExtensions
|
||||
{
|
||||
public const string DefaultFolder = "BlockList/";
|
||||
public const string DefaultTemplate = "Default";
|
||||
|
||||
public static IHtmlContent GetBlockListHtml(this HtmlHelper html, BlockListModel model, string template = DefaultTemplate)
|
||||
{
|
||||
if (model?.Count == 0) return new HtmlString(string.Empty);
|
||||
|
||||
var view = DefaultFolder + template;
|
||||
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 HtmlHelper html, IPublishedContent contentItem, string propertyAlias) => GetBlockListHtml(html, contentItem, propertyAlias, DefaultTemplate);
|
||||
|
||||
public static IHtmlContent GetBlockListHtml(this HtmlHelper 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));
|
||||
|
||||
var prop = contentItem.GetProperty(propertyAlias);
|
||||
if (prop == null) throw new InvalidOperationException("No property type found with alias " + propertyAlias);
|
||||
|
||||
return GetBlockListHtml(html, prop?.GetValue() as BlockListModel, template);
|
||||
}
|
||||
}
|
||||
}
|
||||
52
src/Umbraco.Web.Common/Extensions/CacheHelperExtensions.cs
Normal file
52
src/Umbraco.Web.Common/Extensions/CacheHelperExtensions.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
using Umbraco.Core.Cache;
|
||||
using Umbraco.Core.Hosting;
|
||||
|
||||
namespace Umbraco.Extensions
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for the cache helper
|
||||
/// </summary>
|
||||
public static class CacheHelperExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Outputs and caches a partial view in MVC
|
||||
/// </summary>
|
||||
/// <param name="appCaches"></param>
|
||||
/// <param name="hostingEnvironment"></param>
|
||||
/// <param name="htmlHelper"></param>
|
||||
/// <param name="partialViewName"></param>
|
||||
/// <param name="model"></param>
|
||||
/// <param name="cachedSeconds"></param>
|
||||
/// <param name="cacheKey">used to cache the partial view, this key could change if it is cached by page or by member</param>
|
||||
/// <param name="viewData"></param>
|
||||
/// <returns></returns>
|
||||
public static IHtmlContent CachedPartialView(
|
||||
this AppCaches appCaches,
|
||||
IHostingEnvironment hostingEnvironment,
|
||||
IHtmlHelper htmlHelper,
|
||||
string partialViewName,
|
||||
object model,
|
||||
int cachedSeconds,
|
||||
string cacheKey,
|
||||
ViewDataDictionary viewData = null)
|
||||
{
|
||||
//disable cached partials in debug mode: http://issues.umbraco.org/issue/U4-5940
|
||||
if (hostingEnvironment.IsDebugMode)
|
||||
{
|
||||
// just return a normal partial view instead
|
||||
return htmlHelper.Partial(partialViewName, model, viewData);
|
||||
}
|
||||
|
||||
return appCaches.RuntimeCache.GetCacheItem<IHtmlContent>(
|
||||
Core.CacheHelperExtensions.PartialViewCacheKey + cacheKey,
|
||||
() => htmlHelper.Partial(partialViewName, model, viewData),
|
||||
timeout: new TimeSpan(0, 0, 0, cachedSeconds));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
19
src/Umbraco.Web.Common/Extensions/RazorPageExtensions.cs
Normal file
19
src/Umbraco.Web.Common/Extensions/RazorPageExtensions.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Microsoft.AspNetCore.Mvc.Razor;
|
||||
|
||||
namespace Umbraco.Extensions
|
||||
{
|
||||
public static class RazorPageExtensions
|
||||
{
|
||||
public static HtmlString RenderSection(this RazorPage webPage, string name, HtmlString defaultContents)
|
||||
{
|
||||
return webPage.IsSectionDefined(name) ? webPage.RenderSection(name) : defaultContents;
|
||||
}
|
||||
|
||||
public static HtmlString RenderSection(this RazorPage webPage, string name, string defaultContents)
|
||||
{
|
||||
return webPage.IsSectionDefined(name) ? webPage.RenderSection(name) : new HtmlString(defaultContents);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
75
src/Umbraco.Web.Common/Extensions/ViewContextExtensions.cs
Normal file
75
src/Umbraco.Web.Common/Extensions/ViewContextExtensions.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
|
||||
namespace Umbraco.Extensions
|
||||
{
|
||||
public static class ViewContextExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new ViewContext from an existing one but specifies a new Model for the ViewData
|
||||
/// </summary>
|
||||
/// <param name="vc"></param>
|
||||
/// <param name="model"></param>
|
||||
/// <returns></returns>
|
||||
public static ViewContext CopyWithModel(this ViewContext vc, object model)
|
||||
{
|
||||
return new ViewContext
|
||||
{
|
||||
View = vc.View,
|
||||
Writer = vc.Writer,
|
||||
ActionDescriptor = vc.ActionDescriptor,
|
||||
FormContext = vc.FormContext,
|
||||
HttpContext = vc.HttpContext,
|
||||
RouteData = vc.RouteData,
|
||||
TempData = vc.TempData,
|
||||
ViewData = new ViewDataDictionary(vc.ViewData)
|
||||
{
|
||||
Model = model
|
||||
},
|
||||
ClientValidationEnabled = vc.ClientValidationEnabled,
|
||||
ExecutingFilePath = vc.ExecutingFilePath,
|
||||
ValidationMessageElement = vc.ValidationMessageElement,
|
||||
Html5DateRenderingMode = vc.Html5DateRenderingMode,
|
||||
ValidationSummaryMessageElement = vc.ValidationSummaryMessageElement
|
||||
};
|
||||
}
|
||||
|
||||
public static ViewContext Clone(this ViewContext vc)
|
||||
{
|
||||
return new ViewContext
|
||||
{
|
||||
View = vc.View,
|
||||
Writer = vc.Writer,
|
||||
ActionDescriptor = vc.ActionDescriptor,
|
||||
FormContext = vc.FormContext,
|
||||
HttpContext = vc.HttpContext,
|
||||
RouteData = vc.RouteData,
|
||||
TempData = vc.TempData,
|
||||
ViewData = new ViewDataDictionary(vc.ViewData),
|
||||
ClientValidationEnabled = vc.ClientValidationEnabled,
|
||||
ExecutingFilePath = vc.ExecutingFilePath,
|
||||
ValidationMessageElement = vc.ValidationMessageElement,
|
||||
Html5DateRenderingMode = vc.Html5DateRenderingMode,
|
||||
ValidationSummaryMessageElement = vc.ValidationSummaryMessageElement
|
||||
};
|
||||
}
|
||||
|
||||
//public static ViewContext CloneWithWriter(this ViewContext vc, TextWriter writer)
|
||||
//{
|
||||
// return new ViewContext
|
||||
// {
|
||||
// Controller = vc.Controller,
|
||||
// HttpContext = vc.HttpContext,
|
||||
// RequestContext = vc.RequestContext,
|
||||
// RouteData = vc.RouteData,
|
||||
// TempData = vc.TempData,
|
||||
// View = vc.View,
|
||||
// ViewData = vc.ViewData,
|
||||
// FormContext = vc.FormContext,
|
||||
// ClientValidationEnabled = vc.ClientValidationEnabled,
|
||||
// UnobtrusiveJavaScriptEnabled = vc.UnobtrusiveJavaScriptEnabled,
|
||||
// Writer = writer
|
||||
// };
|
||||
//}
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,12 @@ namespace Umbraco.Web.Common.Filters
|
||||
/// <summary>
|
||||
/// Ensures authorization is successful for a back office user.
|
||||
/// </summary>
|
||||
public class UmbracoAuthorizeAttribute : TypeFilterAttribute
|
||||
public class UmbracoBackOfficeAuthorizeAttribute : TypeFilterAttribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Default constructor
|
||||
/// </summary>
|
||||
public UmbracoAuthorizeAttribute() : this(false, false)
|
||||
public UmbracoBackOfficeAuthorizeAttribute() : this(false, false)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace Umbraco.Web.Common.Filters
|
||||
/// <param name="redirectToUmbracoLogin"></param>
|
||||
/// <param name="requireApproval"></param>
|
||||
|
||||
public UmbracoAuthorizeAttribute(bool redirectToUmbracoLogin, bool requireApproval) : base(typeof(UmbracoAuthorizeFilter))
|
||||
public UmbracoBackOfficeAuthorizeAttribute(bool redirectToUmbracoLogin, bool requireApproval) : base(typeof(UmbracoBackOfficeAuthorizeFilter))
|
||||
{
|
||||
Arguments = new object[] { redirectToUmbracoLogin, requireApproval };
|
||||
}
|
||||
@@ -29,7 +29,7 @@ namespace Umbraco.Web.Common.Filters
|
||||
/// Constructor with redirect url behavior
|
||||
/// </summary>
|
||||
/// <param name="redirectUrl"></param>
|
||||
public UmbracoAuthorizeAttribute(string redirectUrl) : base(typeof(UmbracoAuthorizeFilter))
|
||||
public UmbracoBackOfficeAuthorizeAttribute(string redirectUrl) : base(typeof(UmbracoBackOfficeAuthorizeFilter))
|
||||
{
|
||||
Arguments = new object[] { redirectUrl };
|
||||
}
|
||||
@@ -13,7 +13,7 @@ namespace Umbraco.Web.Common.Filters
|
||||
/// <summary>
|
||||
/// Ensures authorization is successful for a back office user.
|
||||
/// </summary>
|
||||
public class UmbracoAuthorizeFilter : IAuthorizationFilter
|
||||
public class UmbracoBackOfficeAuthorizeFilter : IAuthorizationFilter
|
||||
{
|
||||
private readonly bool _requireApproval;
|
||||
|
||||
@@ -28,7 +28,7 @@ namespace Umbraco.Web.Common.Filters
|
||||
private readonly bool _redirectToUmbracoLogin;
|
||||
private string _redirectUrl;
|
||||
|
||||
private UmbracoAuthorizeFilter(
|
||||
private UmbracoBackOfficeAuthorizeFilter(
|
||||
IHostingEnvironment hostingEnvironment,
|
||||
IUmbracoContextAccessor umbracoContext,
|
||||
IRuntimeState runtimeState, LinkGenerator linkGenerator,
|
||||
@@ -51,7 +51,7 @@ namespace Umbraco.Web.Common.Filters
|
||||
/// <param name="runtimeState"></param>
|
||||
/// <param name="linkGenerator"></param>
|
||||
/// <param name="redirectUrl"></param>
|
||||
public UmbracoAuthorizeFilter(
|
||||
public UmbracoBackOfficeAuthorizeFilter(
|
||||
IHostingEnvironment hostingEnvironment,
|
||||
IUmbracoContextAccessor umbracoContext,
|
||||
IRuntimeState runtimeState, LinkGenerator linkGenerator,
|
||||
@@ -59,7 +59,7 @@ namespace Umbraco.Web.Common.Filters
|
||||
{
|
||||
}
|
||||
|
||||
public UmbracoAuthorizeFilter(
|
||||
public UmbracoBackOfficeAuthorizeFilter(
|
||||
IHostingEnvironment hostingEnvironment,
|
||||
IUmbracoContextAccessor umbracoContext,
|
||||
IRuntimeState runtimeState, LinkGenerator linkGenerator,
|
||||
@@ -0,0 +1,20 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Umbraco.Web.Common.Filters
|
||||
{
|
||||
/// <summary>
|
||||
/// Ensures authorization is successful for a website user (member).
|
||||
/// </summary>
|
||||
public class UmbracoMemberAuthorizeAttribute : TypeFilterAttribute
|
||||
{
|
||||
public UmbracoMemberAuthorizeAttribute() : this(string.Empty, string.Empty, string.Empty)
|
||||
{
|
||||
}
|
||||
|
||||
public UmbracoMemberAuthorizeAttribute(string allowType, string allowGroup, string allowMembers) : base(typeof(UmbracoMemberAuthorizeFilter))
|
||||
{
|
||||
Arguments = new object[] { allowType, allowGroup, allowMembers};
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using System.Collections.Generic;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Web.Common.Filters
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Ensures authorization is successful for a back office user.
|
||||
/// </summary>
|
||||
public class UmbracoMemberAuthorizeFilter : IAuthorizationFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// Comma delimited list of allowed member types
|
||||
/// </summary>
|
||||
public string AllowType { get; private set;}
|
||||
|
||||
/// <summary>
|
||||
/// Comma delimited list of allowed member groups
|
||||
/// </summary>
|
||||
public string AllowGroup { get; private set;}
|
||||
|
||||
/// <summary>
|
||||
/// Comma delimited list of allowed members
|
||||
/// </summary>
|
||||
public string AllowMembers { get; private set; }
|
||||
|
||||
|
||||
private UmbracoMemberAuthorizeFilter(
|
||||
string allowType, string allowGroup, string allowMembers)
|
||||
{
|
||||
AllowType = allowType;
|
||||
AllowGroup = allowGroup;
|
||||
AllowMembers = allowMembers;
|
||||
}
|
||||
|
||||
public void OnAuthorization(AuthorizationFilterContext context)
|
||||
{
|
||||
if (!IsAuthorized())
|
||||
{
|
||||
context.HttpContext.SetReasonPhrase("Resource restricted: either member is not logged on or is not of a permitted type or group.");
|
||||
context.Result = new ForbidResult();
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsAuthorized()
|
||||
{
|
||||
if (AllowMembers.IsNullOrWhiteSpace())
|
||||
AllowMembers = "";
|
||||
if (AllowGroup.IsNullOrWhiteSpace())
|
||||
AllowGroup = "";
|
||||
if (AllowType.IsNullOrWhiteSpace())
|
||||
AllowType = "";
|
||||
|
||||
var members = new List<int>();
|
||||
foreach (var s in AllowMembers.Split(','))
|
||||
{
|
||||
if (int.TryParse(s, out var id))
|
||||
{
|
||||
members.Add(id);
|
||||
}
|
||||
}
|
||||
|
||||
return false;// TODO reintroduce when members are implemented: _memberHelper.IsMemberAuthorized(AllowType.Split(','), AllowGroup.Split(','), members);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Web.Common.Constants;
|
||||
using Umbraco.Web.Common.Exceptions;
|
||||
using Umbraco.Web.Common.Security;
|
||||
|
||||
namespace Umbraco.Web.Common.Filters
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Attribute used to check that the request contains a valid Umbraco form request string.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Applying this attribute/filter to a <see cref="SurfaceController"/> or SurfaceController Action will ensure that the Action can only be executed
|
||||
/// when it is routed to from within Umbraco, typically when rendering a form with BeginUmbracoForm. It will mean that the natural MVC route for this Action
|
||||
/// will fail with a <see cref="HttpUmbracoFormRouteStringException"/>.
|
||||
/// </remarks>
|
||||
public class ValidateUmbracoFormRouteStringAttribute : TypeFilterAttribute
|
||||
{
|
||||
|
||||
public ValidateUmbracoFormRouteStringAttribute() : base(typeof(ValidateUmbracoFormRouteStringFilter))
|
||||
{
|
||||
Arguments = new object[] { };
|
||||
}
|
||||
|
||||
internal class ValidateUmbracoFormRouteStringFilter: IAuthorizationFilter
|
||||
{
|
||||
private readonly IDataProtectionProvider _dataProtectionProvider;
|
||||
|
||||
public ValidateUmbracoFormRouteStringFilter(IDataProtectionProvider dataProtectionProvider)
|
||||
{
|
||||
_dataProtectionProvider = dataProtectionProvider;
|
||||
}
|
||||
|
||||
public void OnAuthorization(AuthorizationFilterContext context)
|
||||
{
|
||||
if (context == null) throw new ArgumentNullException(nameof(context));
|
||||
|
||||
var ufprt = context.HttpContext.Request.Form["ufprt"];
|
||||
|
||||
if (context.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor)
|
||||
{
|
||||
ValidateRouteString(ufprt, controllerActionDescriptor.ControllerName, controllerActionDescriptor.ActionName, context.RouteData?.DataTokens["area"]?.ToString());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void ValidateRouteString(string ufprt, string currentController, string currentAction, string currentArea)
|
||||
{
|
||||
if (ufprt.IsNullOrWhiteSpace())
|
||||
{
|
||||
throw new HttpUmbracoFormRouteStringException("The required request field \"ufprt\" is not present.");
|
||||
}
|
||||
|
||||
if (!EncryptionHelper.DecryptAndValidateEncryptedRouteString(_dataProtectionProvider, ufprt, out var additionalDataParts))
|
||||
{
|
||||
throw new HttpUmbracoFormRouteStringException("The Umbraco form request route string could not be decrypted.");
|
||||
}
|
||||
|
||||
if (!additionalDataParts[ViewConstants.ReservedAdditionalKeys.Controller].InvariantEquals(currentController) ||
|
||||
!additionalDataParts[ViewConstants.ReservedAdditionalKeys.Action].InvariantEquals(currentAction) ||
|
||||
(!additionalDataParts[ViewConstants.ReservedAdditionalKeys.Area].IsNullOrWhiteSpace() && !additionalDataParts[ViewConstants.ReservedAdditionalKeys.Area].InvariantEquals(currentArea)))
|
||||
{
|
||||
throw new HttpUmbracoFormRouteStringException("The provided Umbraco form request route string was meant for a different controller and action.");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
328
src/Umbraco.Web.Common/Mvc/HtmlStringUtilities.cs
Normal file
328
src/Umbraco.Web.Common/Mvc/HtmlStringUtilities.cs
Normal file
@@ -0,0 +1,328 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Web;
|
||||
using HtmlAgilityPack;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
|
||||
namespace Umbraco.Web.Common.Mvc
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides utility methods for UmbracoHelper for working with strings and HTML in views.
|
||||
/// </summary>
|
||||
public sealed class HtmlStringUtilities
|
||||
{
|
||||
/// <summary>
|
||||
/// HTML encodes the text and replaces text line breaks with HTML line breaks.
|
||||
/// </summary>
|
||||
/// <param name="text">The text.</param>
|
||||
/// <returns>
|
||||
/// The HTML encoded text with text line breaks replaced with HTML line breaks (<c><br /></c>).
|
||||
/// </returns>
|
||||
public IHtmlContent ReplaceLineBreaks(string text)
|
||||
{
|
||||
var value = HttpUtility.HtmlEncode(text)?
|
||||
.Replace("\r\n", "<br />")
|
||||
.Replace("\r", "<br />")
|
||||
.Replace("\n", "<br />");
|
||||
|
||||
return new HtmlString(value);
|
||||
}
|
||||
|
||||
public HtmlString StripHtmlTags(string html, params string[] tags)
|
||||
{
|
||||
var doc = new HtmlDocument();
|
||||
doc.LoadHtml("<p>" + html + "</p>");
|
||||
|
||||
var targets = new List<HtmlNode>();
|
||||
|
||||
var nodes = doc.DocumentNode.FirstChild.SelectNodes(".//*");
|
||||
if (nodes != null)
|
||||
{
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
//is element
|
||||
if (node.NodeType != HtmlNodeType.Element) continue;
|
||||
var filterAllTags = (tags == null || !tags.Any());
|
||||
if (filterAllTags || tags.Any(tag => string.Equals(tag, node.Name, StringComparison.CurrentCultureIgnoreCase)))
|
||||
{
|
||||
targets.Add(node);
|
||||
}
|
||||
}
|
||||
foreach (var target in targets)
|
||||
{
|
||||
HtmlNode content = doc.CreateTextNode(target.InnerText);
|
||||
target.ParentNode.ReplaceChild(content, target);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return new HtmlString(html);
|
||||
}
|
||||
return new HtmlString(doc.DocumentNode.FirstChild.InnerHtml.Replace(" ", " "));
|
||||
}
|
||||
|
||||
public string Join(string separator, params object[] args)
|
||||
{
|
||||
var results = args
|
||||
.Where(x => x != null)
|
||||
.Select(x => x.ToString())
|
||||
.Where(x => string.IsNullOrWhiteSpace(x) == false);
|
||||
return string.Join(separator, results);
|
||||
}
|
||||
|
||||
public string Concatenate(params object[] args)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
foreach (var arg in args
|
||||
.Where(x => x != null)
|
||||
.Select(x => x.ToString())
|
||||
.Where(x => string.IsNullOrWhiteSpace(x) == false))
|
||||
{
|
||||
sb.Append(arg);
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public string Coalesce(params object[] args)
|
||||
{
|
||||
var arg = args
|
||||
.Where(x => x != null)
|
||||
.Select(x => x.ToString())
|
||||
.FirstOrDefault(x => string.IsNullOrWhiteSpace(x) == false);
|
||||
|
||||
return arg ?? string.Empty;
|
||||
}
|
||||
|
||||
public IHtmlContent Truncate(string html, int length, bool addElipsis, bool treatTagsAsContent)
|
||||
{
|
||||
const string hellip = "…";
|
||||
|
||||
using (var outputms = new MemoryStream())
|
||||
{
|
||||
bool lengthReached = false;
|
||||
|
||||
using (var outputtw = new StreamWriter(outputms))
|
||||
{
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
using (var tw = new StreamWriter(ms))
|
||||
{
|
||||
tw.Write(html);
|
||||
tw.Flush();
|
||||
ms.Position = 0;
|
||||
var tagStack = new Stack<string>();
|
||||
|
||||
using (TextReader tr = new StreamReader(ms))
|
||||
{
|
||||
bool isInsideElement = false,
|
||||
insideTagSpaceEncountered = false,
|
||||
isTagClose = false;
|
||||
|
||||
int ic = 0,
|
||||
//currentLength = 0,
|
||||
currentTextLength = 0;
|
||||
|
||||
string currentTag = string.Empty,
|
||||
tagContents = string.Empty;
|
||||
|
||||
while ((ic = tr.Read()) != -1)
|
||||
{
|
||||
bool write = true;
|
||||
|
||||
switch ((char)ic)
|
||||
{
|
||||
case '<':
|
||||
if (!lengthReached)
|
||||
{
|
||||
isInsideElement = true;
|
||||
}
|
||||
|
||||
insideTagSpaceEncountered = false;
|
||||
currentTag = string.Empty;
|
||||
tagContents = string.Empty;
|
||||
isTagClose = false;
|
||||
if (tr.Peek() == (int)'/')
|
||||
{
|
||||
isTagClose = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case '>':
|
||||
isInsideElement = false;
|
||||
|
||||
if (isTagClose && tagStack.Count > 0)
|
||||
{
|
||||
string thisTag = tagStack.Pop();
|
||||
outputtw.Write("</" + thisTag + ">");
|
||||
if (treatTagsAsContent)
|
||||
{
|
||||
currentTextLength++;
|
||||
}
|
||||
}
|
||||
if (!isTagClose && currentTag.Length > 0)
|
||||
{
|
||||
if (!lengthReached)
|
||||
{
|
||||
tagStack.Push(currentTag);
|
||||
outputtw.Write("<" + currentTag);
|
||||
if (treatTagsAsContent)
|
||||
{
|
||||
currentTextLength++;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(tagContents))
|
||||
{
|
||||
if (tagContents.EndsWith("/"))
|
||||
{
|
||||
// No end tag e.g. <br />.
|
||||
tagStack.Pop();
|
||||
}
|
||||
|
||||
outputtw.Write(tagContents);
|
||||
write = true;
|
||||
insideTagSpaceEncountered = false;
|
||||
}
|
||||
outputtw.Write(">");
|
||||
}
|
||||
}
|
||||
// Continue to next iteration of the text reader.
|
||||
continue;
|
||||
|
||||
default:
|
||||
if (isInsideElement)
|
||||
{
|
||||
if (ic == (int)' ')
|
||||
{
|
||||
if (!insideTagSpaceEncountered)
|
||||
{
|
||||
insideTagSpaceEncountered = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!insideTagSpaceEncountered)
|
||||
{
|
||||
currentTag += (char)ic;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (isInsideElement || insideTagSpaceEncountered)
|
||||
{
|
||||
write = false;
|
||||
if (insideTagSpaceEncountered)
|
||||
{
|
||||
tagContents += (char)ic;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isInsideElement || treatTagsAsContent)
|
||||
{
|
||||
currentTextLength++;
|
||||
}
|
||||
|
||||
if (currentTextLength <= length || (lengthReached && isInsideElement))
|
||||
{
|
||||
if (write)
|
||||
{
|
||||
var charToWrite = (char)ic;
|
||||
outputtw.Write(charToWrite);
|
||||
//currentLength++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!lengthReached)
|
||||
{
|
||||
if (currentTextLength == length)
|
||||
{
|
||||
// if the last character added was the first of a two character unicode pair, add the second character
|
||||
if (char.IsHighSurrogate((char)ic))
|
||||
{
|
||||
var lowSurrogate = tr.Read();
|
||||
outputtw.Write((char)lowSurrogate);
|
||||
}
|
||||
|
||||
}
|
||||
// only add elipsis if current length greater than original length
|
||||
if (currentTextLength > length)
|
||||
{
|
||||
if (addElipsis)
|
||||
{
|
||||
outputtw.Write(hellip);
|
||||
}
|
||||
lengthReached = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
outputtw.Flush();
|
||||
outputms.Position = 0;
|
||||
using (TextReader outputtr = new StreamReader(outputms))
|
||||
{
|
||||
string result = string.Empty;
|
||||
|
||||
string firstTrim = outputtr.ReadToEnd().Replace(" ", " ").Trim();
|
||||
|
||||
// Check to see if there is an empty char between the hellip and the output string
|
||||
// if there is, remove it
|
||||
if (addElipsis && lengthReached && string.IsNullOrWhiteSpace(firstTrim) == false)
|
||||
{
|
||||
result = firstTrim[firstTrim.Length - hellip.Length - 1] == ' ' ? firstTrim.Remove(firstTrim.Length - hellip.Length - 1, 1) : firstTrim;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = firstTrim;
|
||||
}
|
||||
|
||||
return new HtmlString(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the length of the words from a HTML block
|
||||
/// </summary>
|
||||
/// <param name="html">HTML text</param>
|
||||
/// <param name="words">Amount of words you would like to measure</param>
|
||||
/// <param name="tagsAsContent"></param>
|
||||
/// <returns></returns>
|
||||
public int WordsToLength(string html, int words)
|
||||
{
|
||||
HtmlDocument doc = new HtmlDocument();
|
||||
doc.LoadHtml(html);
|
||||
|
||||
int wordCount = 0,
|
||||
length = 0,
|
||||
maxWords = words;
|
||||
|
||||
html = StripHtmlTags(html, null).ToString();
|
||||
|
||||
while (length < html.Length)
|
||||
{
|
||||
// Check to see if the current wordCount reached the maxWords allowed
|
||||
if (wordCount.Equals(maxWords)) break;
|
||||
// Check if current char is part of a word
|
||||
while (length < html.Length && char.IsWhiteSpace(html[length]) == false)
|
||||
{
|
||||
length++;
|
||||
}
|
||||
|
||||
wordCount++;
|
||||
|
||||
// Skip whitespace until the next word
|
||||
while (length < html.Length && char.IsWhiteSpace(html[length]) && wordCount.Equals(maxWords) == false)
|
||||
{
|
||||
length++;
|
||||
}
|
||||
}
|
||||
return length;
|
||||
}
|
||||
}
|
||||
}
|
||||
104
src/Umbraco.Web.Common/Security/EncryptionHelper.cs
Normal file
104
src/Umbraco.Web.Common/Security/EncryptionHelper.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Web;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Web.Common.Constants;
|
||||
|
||||
namespace Umbraco.Web.Common.Security
|
||||
{
|
||||
public class EncryptionHelper
|
||||
{
|
||||
private static IDataProtector CreateDataProtector(IDataProtectionProvider dataProtectionProvider)
|
||||
{
|
||||
return dataProtectionProvider.CreateProtector(nameof(EncryptionHelper));
|
||||
}
|
||||
|
||||
public static string Decrypt(string encryptedString, IDataProtectionProvider dataProtectionProvider)
|
||||
{
|
||||
return CreateDataProtector(dataProtectionProvider).Unprotect(encryptedString);
|
||||
}
|
||||
|
||||
public static string Encrypt(string plainString, IDataProtectionProvider dataProtectionProvider)
|
||||
{
|
||||
return 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));
|
||||
|
||||
//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
|
||||
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));
|
||||
string decryptedString;
|
||||
try
|
||||
{
|
||||
decryptedString = Decrypt(encryptedString, dataProtectionProvider);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
StaticApplicationLogging.Logger.LogWarning("A value was detected in the ufprt parameter but Umbraco could not decrypt the string");
|
||||
parts = null;
|
||||
return false;
|
||||
}
|
||||
var 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
|
||||
if (parts.All(x => x.Key != ViewConstants.ReservedAdditionalKeys.Controller))
|
||||
return false;
|
||||
//the action
|
||||
if (parts.All(x => x.Key != ViewConstants.ReservedAdditionalKeys.Action))
|
||||
return false;
|
||||
//the area
|
||||
if (parts.All(x => x.Key != ViewConstants.ReservedAdditionalKeys.Area))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user