From 0b7f04ab37d1ee54617dc74eeb84fff68b867a89 Mon Sep 17 00:00:00 2001 From: Shannon Deminick Date: Tue, 25 Sep 2012 13:09:59 +0700 Subject: [PATCH] Added all code to support auto-routing surface controllers and started implementing the code for BeginUmbracoForm. --- src/Umbraco.Core/ActivatorHelper.cs | 20 +++ .../Configuration/GlobalSettings.cs | 2 +- src/Umbraco.Core/Mandate.cs | 107 ++++++++++++++++ src/Umbraco.Core/Umbraco.Core.csproj | 2 + src/Umbraco.Web/AreaRegistrationExtensions.cs | 87 +++++++++++++ src/Umbraco.Web/HtmlHelperRenderExtensions.cs | 2 +- .../MergeModelStateToChildActionAttribute.cs | 42 +++++++ src/Umbraco.Web/Mvc/PostedDataProxyInfo.cs | 13 ++ .../Mvc/RedirectToUmbracoPageResult.cs | 111 +++++++++++++++++ .../Mvc/RenderControllerFactory.cs | 19 ++- src/Umbraco.Web/Mvc/RenderRouteHandler.cs | 110 +++++++++++++++++ src/Umbraco.Web/Mvc/RouteDefinition.cs | 3 +- src/Umbraco.Web/Mvc/SurfaceController.cs | 115 ++++++++++++++++++ src/Umbraco.Web/Mvc/SurfaceControllerArea.cs | 63 ++++++++++ .../Mvc/SurfaceControllerAttribute.cs | 18 +++ .../Mvc/SurfaceControllerFactory.cs | 67 ++++++++++ .../Mvc/SurfaceControllerMetadata.cs | 15 +++ .../Mvc/SurfaceControllerResolver.cs | 23 ++++ src/Umbraco.Web/Mvc/UmbracoPageResult.cs | 50 ++++++++ src/Umbraco.Web/PluginManagerExtensions.cs | 7 +- src/Umbraco.Web/RouteCollectionExtensions.cs | 89 ++++++++++++++ src/Umbraco.Web/Routing/NiceUrlProvider.cs | 9 +- src/Umbraco.Web/Umbraco.Web.csproj | 12 ++ src/Umbraco.Web/UmbracoContext.cs | 5 + src/Umbraco.Web/WebBootManager.cs | 24 +++- .../umbraco.MacroEngines.csproj | 1 + 26 files changed, 1001 insertions(+), 15 deletions(-) create mode 100644 src/Umbraco.Core/ActivatorHelper.cs create mode 100644 src/Umbraco.Core/Mandate.cs create mode 100644 src/Umbraco.Web/AreaRegistrationExtensions.cs create mode 100644 src/Umbraco.Web/Mvc/MergeModelStateToChildActionAttribute.cs create mode 100644 src/Umbraco.Web/Mvc/PostedDataProxyInfo.cs create mode 100644 src/Umbraco.Web/Mvc/RedirectToUmbracoPageResult.cs create mode 100644 src/Umbraco.Web/Mvc/SurfaceController.cs create mode 100644 src/Umbraco.Web/Mvc/SurfaceControllerArea.cs create mode 100644 src/Umbraco.Web/Mvc/SurfaceControllerAttribute.cs create mode 100644 src/Umbraco.Web/Mvc/SurfaceControllerFactory.cs create mode 100644 src/Umbraco.Web/Mvc/SurfaceControllerMetadata.cs create mode 100644 src/Umbraco.Web/Mvc/SurfaceControllerResolver.cs create mode 100644 src/Umbraco.Web/Mvc/UmbracoPageResult.cs create mode 100644 src/Umbraco.Web/RouteCollectionExtensions.cs diff --git a/src/Umbraco.Core/ActivatorHelper.cs b/src/Umbraco.Core/ActivatorHelper.cs new file mode 100644 index 0000000000..88ec01ae35 --- /dev/null +++ b/src/Umbraco.Core/ActivatorHelper.cs @@ -0,0 +1,20 @@ +using System; + +namespace Umbraco.Core +{ + /// + /// Helper methods for Activation + /// + internal static class ActivatorHelper + { + /// + /// Creates an instance of a type using that type's default constructor. + /// + /// + /// + public static T CreateInstance() where T : class, new() + { + return Activator.CreateInstance(typeof(T)) as T; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/GlobalSettings.cs b/src/Umbraco.Core/Configuration/GlobalSettings.cs index f330049fb7..424e7b2f24 100644 --- a/src/Umbraco.Core/Configuration/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/GlobalSettings.cs @@ -115,7 +115,7 @@ namespace Umbraco.Core.Configuration /// We will use the 'Path' (default ~/umbraco) to create it but since it cannot contain '/' and people may specify a path of ~/asdf/asdf/admin /// we will convert the '/' to '-' and use that as the path. its a bit lame but will work. /// - internal static string MvcArea + internal static string UmbracoMvcArea { get { diff --git a/src/Umbraco.Core/Mandate.cs b/src/Umbraco.Core/Mandate.cs new file mode 100644 index 0000000000..54d589b90a --- /dev/null +++ b/src/Umbraco.Core/Mandate.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Umbraco.Core +{ + /// + /// Helper class for mandating values, for example on method parameters. + /// + internal static class Mandate + { + /// + /// Mandates that the specified parameter is not null. + /// + /// The value. + /// Name of the param. + /// If is null. + public static void ParameterNotNull(T value, string paramName) where T : class + { + That(value != null, () => new ArgumentNullException(paramName)); + } + + + /// + /// Mandates that the specified parameter is not null. + /// + /// The value. + /// Name of the param. + /// If is null or whitespace. + public static void ParameterNotNullOrEmpty(string value, string paramName) + { + That(!string.IsNullOrWhiteSpace(value), () => new ArgumentNullException(paramName)); + } + + /// + /// Mandates that the specified sequence is not null and has at least one element. + /// + /// + /// The sequence. + /// Name of the param. + public static void ParameterNotNullOrEmpty(IEnumerable sequence, string paramName) + { + ParameterNotNull(sequence, paramName); + ParameterCondition(sequence.Any(), paramName); + } + + + /// + /// Mandates that the specified parameter matches the condition. + /// + /// The condition to check. + /// Name of the param. + /// If the condition is false. + public static void ParameterCondition(bool condition, string paramName) + { + ParameterCondition(condition, paramName, (string)null); + } + + /// + /// Mandates that the specified parameter matches the condition. + /// + /// The condition to check. + /// Name of the param. + /// The message. + /// If the condition is false. + public static void ParameterCondition(bool condition, string paramName, string message) + { + // Warning: don't make this method have an optional message parameter (removing the other ParameterCondition overload) as it will + // make binaries compiled against previous Framework libs incompatible unneccesarily + message = message ?? "A parameter passed into a method was not a valid value"; + That(condition, () => new ArgumentException(message, paramName)); + } + + /// + /// Mandates that the specified condition is true, otherwise throws an exception specified in . + /// + /// The type of the exception. + /// if set to true, throws exception . + /// An exception of type is raised if the condition is false. + public static void That(bool condition) where TException : Exception, new() + { + if (!condition) + throw ActivatorHelper.CreateInstance(); + } + + /// + /// Mandates that the specified condition is true, otherwise throws an exception specified in . + /// + /// The type of the exception. + /// if set to true, throws exception . + /// Deffered expression to call if the exception should be raised. + /// An exception of type is raised if the condition is false. + public static void That(bool condition, Func defer) where TException : Exception, new() + { + if (!condition) + { + throw defer.Invoke(); + } + + // Here is an example of how this method is actually called + //object myParam = null; + //Mandate.That(myParam != null, + // textManager => new ArgumentNullException(textManager.Get("blah", new {User = "blah"}))); + } + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 5a362dfaa2..59142f2652 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -49,6 +49,7 @@ Properties\SolutionInfo.cs + @@ -89,6 +90,7 @@ + diff --git a/src/Umbraco.Web/AreaRegistrationExtensions.cs b/src/Umbraco.Web/AreaRegistrationExtensions.cs new file mode 100644 index 0000000000..ccdf7d6395 --- /dev/null +++ b/src/Umbraco.Web/AreaRegistrationExtensions.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Web.Mvc; +using System.Web.Routing; +using Umbraco.Core; +using Umbraco.Core.Configuration; + +namespace Umbraco.Web +{ + internal static class AreaRegistrationExtensions + { + /// + /// Creates a custom individual route for the specified controller plugin. Individual routes + /// are required by controller plugins to map to a unique URL based on ID. + /// + /// An PluginAttribute + /// + /// + /// An existing route collection + /// the data token name for the controller plugin + /// + /// The suffix name that the controller name must end in before the "Controller" string for example: + /// ContentTreeController has a controllerSuffixName of "Tree" + /// + /// + /// The base name of the URL to create for example: Umbraco/[PackageName]/Trees/ContentTree/1 has a baseUrlPath of "Trees" + /// + /// + /// + /// + /// + /// The DataToken value to set for the 'umbraco' key, this defaults to 'backoffice' + internal static void RouteControllerPlugin(this AreaRegistration area, Guid controllerId, string controllerName, Type controllerType, RouteCollection routes, + string routeIdParameterName, string controllerSuffixName, string baseUrlPath, string defaultAction, object defaultId, + string umbracoTokenValue = "backoffice") + { + Mandate.ParameterNotNullOrEmpty(controllerName, "controllerName"); + Mandate.ParameterNotNullOrEmpty(routeIdParameterName, "routeIdParameterName"); + Mandate.ParameterNotNullOrEmpty(controllerSuffixName, "controllerSuffixName"); + Mandate.ParameterNotNullOrEmpty(defaultAction, "defaultAction"); + Mandate.ParameterNotNull(controllerType, "controllerType"); + Mandate.ParameterNotNull(routes, "routes"); + Mandate.ParameterNotNull(defaultId, "defaultId"); + + var umbracoArea = GlobalSettings.UmbracoMvcArea; + + //routes are explicitly name with controller names. + var url = baseUrlPath.IsNullOrWhiteSpace() + ? umbracoArea + "/" + area.AreaName + "/" + controllerName + "/{action}/{id}" + : umbracoArea + "/" + area.AreaName + "/" + baseUrlPath + "/" + controllerName + "/{action}/{id}"; + + //create a new route with custom name, specified url, and the namespace of the controller plugin + var controllerPluginRoute = routes.MapRoute( + //name + string.Format("umbraco-{0}-{1}", controllerName, controllerId), + //url format + url, + //set the namespace of the controller to match + new[] { controllerType.Namespace }); + + //set defaults + controllerPluginRoute.Defaults = new RouteValueDictionary( + new Dictionary + { + { "controller", controllerName }, + { routeIdParameterName, controllerId.ToString("N") }, + { "action", defaultAction }, + { "id", defaultId } + }); + + //constraints: only match controllers ending with 'controllerSuffixName' and only match this controller's ID for this route + controllerPluginRoute.Constraints = new RouteValueDictionary( + new Dictionary + { + { "controller", @"(\w+)" + controllerSuffixName }, + { routeIdParameterName, Regex.Escape(controllerId.ToString("N")) } + }); + + + //match this area + controllerPluginRoute.DataTokens.Add("area", area.AreaName); + controllerPluginRoute.DataTokens.Add("umbraco", umbracoTokenValue); //ensure the umbraco token is set + + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/HtmlHelperRenderExtensions.cs b/src/Umbraco.Web/HtmlHelperRenderExtensions.cs index 6590854ad1..ff17d1a7b5 100644 --- a/src/Umbraco.Web/HtmlHelperRenderExtensions.cs +++ b/src/Umbraco.Web/HtmlHelperRenderExtensions.cs @@ -167,7 +167,7 @@ namespace Umbraco.Web IDictionary htmlAttributes) { var settings = DependencyResolver.Current.GetService(); - var area = Umbraco.Core.Configuration.GlobalSettings.MvcArea; + var area = Umbraco.Core.Configuration.GlobalSettings.UmbracoMvcArea; var formAction = html.ViewContext.HttpContext.Request.Url.AbsolutePath; return html.RenderForm(formAction, FormMethod.Post, htmlAttributes, controllerName, action, area, null); diff --git a/src/Umbraco.Web/Mvc/MergeModelStateToChildActionAttribute.cs b/src/Umbraco.Web/Mvc/MergeModelStateToChildActionAttribute.cs new file mode 100644 index 0000000000..7d2df43962 --- /dev/null +++ b/src/Umbraco.Web/Mvc/MergeModelStateToChildActionAttribute.cs @@ -0,0 +1,42 @@ +using System.Linq; +using System.Web.Mvc; + +namespace Umbraco.Web.Mvc +{ + /// + /// When a ChildAction is executing and we want the ModelState from the Parent context to be merged in + /// to help with validation, this filter can be used. + /// + /// + /// By default, this filter will only merge when an Http POST is detected but this can be modified in the ctor + /// + public class MergeModelStateToChildActionAttribute : ActionFilterAttribute + { + private readonly string[] _verb; + + public MergeModelStateToChildActionAttribute() + : this(HttpVerbs.Post) + { + + } + + public MergeModelStateToChildActionAttribute(params HttpVerbs[] verb) + { + _verb = verb.Select(x => x.ToString().ToUpper()).ToArray(); + } + + public override void OnActionExecuting(ActionExecutingContext filterContext) + { + //check if the verb matches, if so merge the ModelState before the action is executed. + if (_verb.Contains(filterContext.HttpContext.Request.HttpMethod)) + { + if (filterContext.Controller.ControllerContext.IsChildAction) + { + filterContext.Controller.ViewData.ModelState.Merge( + filterContext.Controller.ControllerContext.ParentActionViewContext.ViewData.ModelState); + } + } + base.OnActionExecuting(filterContext); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/PostedDataProxyInfo.cs b/src/Umbraco.Web/Mvc/PostedDataProxyInfo.cs new file mode 100644 index 0000000000..eb664fba0c --- /dev/null +++ b/src/Umbraco.Web/Mvc/PostedDataProxyInfo.cs @@ -0,0 +1,13 @@ +using System; + +namespace Umbraco.Web.Mvc +{ + /// + /// Represents the data required to proxy a request to a surface controller for posted data + /// + internal class PostedDataProxyInfo : RouteDefinition + { + public string Area { get; set; } + public Guid SurfaceId { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/RedirectToUmbracoPageResult.cs b/src/Umbraco.Web/Mvc/RedirectToUmbracoPageResult.cs new file mode 100644 index 0000000000..9f74385458 --- /dev/null +++ b/src/Umbraco.Web/Mvc/RedirectToUmbracoPageResult.cs @@ -0,0 +1,111 @@ +using System; +using System.Web.Mvc; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Web.Routing; + +namespace Umbraco.Web.Mvc +{ + /// + /// Redirects to an Umbraco page by Id or Entity + /// + public class RedirectToUmbracoPageResult : ActionResult + { + private IDocument _document; + private readonly int _pageId; + private readonly UmbracoContext _umbracoContext; + private string _url; + public string Url + { + get + { + if (!_url.IsNullOrWhiteSpace()) return _url; + + if (Document == null) + { + throw new InvalidOperationException("Cannot redirect, no entity was found for id " + _pageId); + } + + var result = _umbracoContext.RoutingContext.NiceUrlProvider.GetNiceUrl(Document.Id); + if (result != NiceUrlProvider.NullUrl) + { + _url = result; + return _url; + } + + throw new InvalidOperationException("Could not route to entity with id " + _pageId + ", the NiceUrlProvider could not generate a URL"); + + } + } + + public IDocument Document + { + get + { + if (_document != null) return _document; + + //need to get the URL for the page + _document = PublishedContentStoreResolver.Current.PublishedContentStore.GetDocumentById(_umbracoContext, _pageId); + + return _document; + } + } + + /// + /// Creates a new RedirectToUmbracoResult + /// + /// + public RedirectToUmbracoPageResult(int pageId) + : this(pageId, UmbracoContext.Current) + { + } + + /// + /// Creates a new RedirectToUmbracoResult + /// + /// + public RedirectToUmbracoPageResult(IDocument document) + : this(document, UmbracoContext.Current) + { + } + + /// + /// Creates a new RedirectToUmbracoResult + /// + /// + /// + public RedirectToUmbracoPageResult(IDocument document, UmbracoContext umbracoContext) + { + _document = document; + _pageId = document.Id; + _umbracoContext = umbracoContext; + } + + /// + /// Creates a new RedirectToUmbracoResult + /// + /// + /// + public RedirectToUmbracoPageResult(int pageId, UmbracoContext umbracoContext) + { + _pageId = pageId; + _umbracoContext = umbracoContext; + } + + public override void ExecuteResult(ControllerContext context) + { + if (context == null) throw new ArgumentNullException("context"); + + if (context.IsChildAction) + { + throw new InvalidOperationException("Cannot redirect from a Child Action"); + } + + var destinationUrl = UrlHelper.GenerateContentUrl(Url, context.HttpContext); + context.Controller.TempData.Keep(); + + context.HttpContext.Response.Redirect(destinationUrl, endResponse: false); + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/RenderControllerFactory.cs b/src/Umbraco.Web/Mvc/RenderControllerFactory.cs index 12b34f2e45..2934bc7b56 100644 --- a/src/Umbraco.Web/Mvc/RenderControllerFactory.cs +++ b/src/Umbraco.Web/Mvc/RenderControllerFactory.cs @@ -29,12 +29,23 @@ namespace Umbraco.Web.Mvc /// The request. /// true if this instance can handle the specified request; otherwise, false. /// - public bool CanHandle(RequestContext request) + public virtual bool CanHandle(RequestContext request) { var dataToken = request.RouteData.DataTokens["area"]; return dataToken == null || string.IsNullOrWhiteSpace(dataToken.ToString()); } + /// + /// Returns the controller type for the controller name otherwise null if not found + /// + /// + /// + /// + protected Type GetControllerType(RequestContext requestContext, string controllerName) + { + return _innerFactory.GetControllerType(requestContext, controllerName); + } + /// /// Creates the specified controller by using the specified request context. /// @@ -42,9 +53,9 @@ namespace Umbraco.Web.Mvc /// The controller. /// /// The request context.The name of the controller. - public IController CreateController(RequestContext requestContext, string controllerName) + public virtual IController CreateController(RequestContext requestContext, string controllerName) { - Type controllerType = _innerFactory.GetControllerType(requestContext, controllerName) ?? + Type controllerType = GetControllerType(requestContext, controllerName) ?? _innerFactory.GetControllerType(requestContext, ControllerExtensions.GetControllerName(typeof(RenderMvcController))); return _innerFactory.GetControllerInstance(requestContext, controllerType); @@ -77,7 +88,7 @@ namespace Umbraco.Web.Mvc /// this nested class changes the visibility of 's internal methods in order to not have to rely on a try-catch. /// /// - public class OverridenDefaultControllerFactory : DefaultControllerFactory + internal class OverridenDefaultControllerFactory : DefaultControllerFactory { public new IController GetControllerInstance(RequestContext requestContext, Type controllerType) { diff --git a/src/Umbraco.Web/Mvc/RenderRouteHandler.cs b/src/Umbraco.Web/Mvc/RenderRouteHandler.cs index a637d236d1..e2f5603b95 100644 --- a/src/Umbraco.Web/Mvc/RenderRouteHandler.cs +++ b/src/Umbraco.Web/Mvc/RenderRouteHandler.cs @@ -1,9 +1,12 @@ using System; +using System.Linq; +using System.Text; using System.Web; using System.Web.Mvc; using System.Web.Routing; using Umbraco.Core; using Umbraco.Core.Logging; +using Umbraco.Core.Models; using Umbraco.Web.Routing; using umbraco.cms.businesslogic.template; @@ -55,6 +58,113 @@ namespace Umbraco.Web.Mvc #endregion + /// + /// Checks the request and query strings to see if it matches the definition of having a Surface controller + /// posted value, if so, then we return a PostedDataProxyInfo object with the correct information. + /// + /// + /// + private static PostedDataProxyInfo GetPostedFormInfo(RequestContext requestContext) + { + if (requestContext.HttpContext.Request.RequestType != "POST") + return null; + + //this field will contain a base64 encoded version of the surface route vals + if (requestContext.HttpContext.Request["uformpostroutevals"].IsNullOrWhiteSpace()) + return null; + + var encodedVal = requestContext.HttpContext.Request["uformpostroutevals"]; + var decodedString = Encoding.UTF8.GetString(Convert.FromBase64String(encodedVal)); + //the value is formatted as query strings + var decodedParts = decodedString.Split('&').Select(x => new { Key = x.Split('=')[0], Value = x.Split('=')[1] }).ToArray(); + + //validate all required keys exist + + //the controller + if (!decodedParts.Any(x => x.Key == "c")) + return null; + //the action + if (!decodedParts.Any(x => x.Key == "a")) + return null; + //the area + if (!decodedParts.Any(x => x.Key == "ar")) + return null; + + //the surface id + if (decodedParts.Any(x => x.Key == "i")) + { + Guid id; + if (Guid.TryParse(decodedParts.Single(x => x.Key == "i").Value, out id)) + { + return new PostedDataProxyInfo + { + ControllerName = requestContext.HttpContext.Server.UrlDecode(decodedParts.Single(x => x.Key == "c").Value), + ActionName = requestContext.HttpContext.Server.UrlDecode(decodedParts.Single(x => x.Key == "a").Value), + Area = requestContext.HttpContext.Server.UrlDecode(decodedParts.Single(x => x.Key == "ar").Value), + SurfaceId = id, + }; + } + } + + //return the proxy info without the surface id... could be a local controller. + return new PostedDataProxyInfo + { + ControllerName = requestContext.HttpContext.Server.UrlDecode(decodedParts.Single(x => x.Key == "c").Value), + ActionName = requestContext.HttpContext.Server.UrlDecode(decodedParts.Single(x => x.Key == "a").Value), + Area = requestContext.HttpContext.Server.UrlDecode(decodedParts.Single(x => x.Key == "ar").Value), + }; + + } + + /// + /// Handles a posted form to an Umbraco Url and ensures the correct controller is routed to and that + /// the right DataTokens are set. + /// + /// + /// + /// + /// The original route definition that would normally be used to route if it were not a POST + private IHttpHandler HandlePostedValues(RequestContext requestContext, PostedDataProxyInfo postedInfo, IDocument document, RouteDefinition routeDefinition) + { + + //set the standard route values/tokens + requestContext.RouteData.Values["controller"] = postedInfo.ControllerName; + requestContext.RouteData.Values["action"] = postedInfo.ActionName; + requestContext.RouteData.DataTokens["area"] = postedInfo.Area; + + IHttpHandler handler = new MvcHandler(requestContext); + + //ensure the surface id is set if found, meaning it is a plugin, not locally declared + if (postedInfo.SurfaceId != default(Guid)) + { + requestContext.RouteData.Values["surfaceId"] = postedInfo.SurfaceId.ToString("N"); + //find the other data tokens for this route and merge... things like Namespace will be included here + using (RouteTable.Routes.GetReadLock()) + { + var surfaceRoute = RouteTable.Routes.OfType() + .Where(x => x.Defaults != null && x.Defaults.ContainsKey("surfaceId") && + x.Defaults["surfaceId"].ToString() == postedInfo.SurfaceId.ToString("N")) + .SingleOrDefault(); + if (surfaceRoute == null) + throw new InvalidOperationException("Could not find a Surface controller route in the RouteTable for id " + postedInfo.SurfaceId); + //set the 'Namespaces' token so the controller factory knows where to look to construct it + if (surfaceRoute.DataTokens.ContainsKey("Namespaces")) + { + requestContext.RouteData.DataTokens["Namespaces"] = surfaceRoute.DataTokens["Namespaces"]; + } + handler = surfaceRoute.RouteHandler.GetHttpHandler(requestContext); + } + + } + + //store the original URL this came in on + requestContext.RouteData.DataTokens["umbraco-item-url"] = requestContext.HttpContext.Request.Url.AbsolutePath; + //store the original route definition + requestContext.RouteData.DataTokens["umbraco-route-def"] = routeDefinition; + + return handler; + } + /// /// Returns a RouteDefinition object based on the current renderModel /// diff --git a/src/Umbraco.Web/Mvc/RouteDefinition.cs b/src/Umbraco.Web/Mvc/RouteDefinition.cs index a157c034e6..997815db0b 100644 --- a/src/Umbraco.Web/Mvc/RouteDefinition.cs +++ b/src/Umbraco.Web/Mvc/RouteDefinition.cs @@ -1,4 +1,5 @@ using System.Web.Mvc; +using Umbraco.Web.Routing; namespace Umbraco.Web.Mvc { @@ -18,7 +19,7 @@ namespace Umbraco.Web.Mvc /// /// The current RenderModel found for the request /// - public object DocumentRequest { get; set; } + public DocumentRequest DocumentRequest { get; set; } /// /// Gets/sets whether the current request has a hijacked route/user controller routed for it diff --git a/src/Umbraco.Web/Mvc/SurfaceController.cs b/src/Umbraco.Web/Mvc/SurfaceController.cs new file mode 100644 index 0000000000..5b7c38a6ce --- /dev/null +++ b/src/Umbraco.Web/Mvc/SurfaceController.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Concurrent; +using System.Web.Mvc; +using Umbraco.Core.Models; +using Umbraco.Core; + +namespace Umbraco.Web.Mvc +{ + /// + /// The base controller that all Presentation Add-in controllers should inherit from + /// + [MergeModelStateToChildAction] + public abstract class SurfaceController : Controller, IRequiresUmbracoContext + { + ///// + ///// stores the metadata about surface controllers + ///// + //private static ConcurrentDictionary _metadata = new ConcurrentDictionary(); + + public UmbracoContext UmbracoContext { get; set; } + + /// + /// Useful for debugging + /// + internal Guid InstanceId { get; private set; } + + /// + /// Default constructor + /// + /// + protected SurfaceController(UmbracoContext umbracoContext) + { + UmbracoContext = umbracoContext; + InstanceId = Guid.NewGuid(); + } + + /// + /// Empty constructor, uses Singleton to resolve the UmbracoContext + /// + protected SurfaceController() + { + UmbracoContext = UmbracoContext.Current; + InstanceId = Guid.NewGuid(); + } + + /// + /// Redirects to the Umbraco page with the given id + /// + /// + /// + protected RedirectToUmbracoPageResult RedirectToUmbracoPage(int pageId) + { + return new RedirectToUmbracoPageResult(pageId, UmbracoContext); + } + + /// + /// Redirects to the Umbraco page with the given id + /// + /// + /// + protected RedirectToUmbracoPageResult RedirectToUmbracoPage(IDocument pageDocument) + { + return new RedirectToUmbracoPageResult(pageDocument, UmbracoContext); + } + + /// + /// Redirects to the currently rendered Umbraco page + /// + /// + protected RedirectToUmbracoPageResult RedirectToCurrentUmbracoPage() + { + return new RedirectToUmbracoPageResult(CurrentPage, UmbracoContext); + } + + /// + /// Returns the currently rendered Umbraco page + /// + /// + protected UmbracoPageResult CurrentUmbracoPage() + { + return new UmbracoPageResult(); + } + + /// + /// Gets the current page. + /// + protected IDocument CurrentPage + { + get + { + if (!ControllerContext.RouteData.DataTokens.ContainsKey("umbraco-route-def")) + throw new InvalidOperationException("Can only use " + typeof(UmbracoPageResult).Name + " in the context of an Http POST when using the BeginUmbracoForm helper"); + + var routeDef = (RouteDefinition)ControllerContext.RouteData.DataTokens["umbraco-route-def"]; + return routeDef.DocumentRequest.Document; + } + } + + /// + /// Returns the metadata for this instance + /// + internal SurfaceControllerMetadata GetMetadata() + { + var controllerId = this.GetType().GetCustomAttribute(false); + + return new SurfaceControllerMetadata() + { + ControllerId = controllerId == null ? null : (Guid?) controllerId.Id, + ControllerName = ControllerExtensions.GetControllerName(this.GetType()), + ControllerNamespace = this.GetType().Namespace, + ControllerType = this.GetType() + }; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/SurfaceControllerArea.cs b/src/Umbraco.Web/Mvc/SurfaceControllerArea.cs new file mode 100644 index 0000000000..9712cd547d --- /dev/null +++ b/src/Umbraco.Web/Mvc/SurfaceControllerArea.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.Linq; +using System.Web.Mvc; +using System.Web.Routing; +using Umbraco.Core; +using Umbraco.Core.Configuration; + +namespace Umbraco.Web.Mvc +{ + /// + /// A custom area for surface controller routes + /// + internal class SurfaceControllerArea : AreaRegistration + { + private readonly IEnumerable _surfaceControllers; + + public SurfaceControllerArea(IEnumerable surfaceControllers) + { + _surfaceControllers = surfaceControllers; + } + + public override void RegisterArea(AreaRegistrationContext context) + { + MapRouteSurfaceControllers(context.Routes, _surfaceControllers); + } + + public override string AreaName + { + get { return "Surface"; } + } + + /// + /// Registers all surface controller routes + /// + /// + /// + private void MapRouteSurfaceControllers(RouteCollection routes, IEnumerable surfaceControllers) + { + var areaName = GlobalSettings.UmbracoMvcArea; + + //local surface controllers do not contain the attribute + var localSurfaceControlleres = surfaceControllers.Where(x => TypeExtensions.GetCustomAttribute(x.GetType(), false) == null); + foreach (var s in localSurfaceControlleres) + { + var meta = s.GetMetadata(); + var route = routes.MapRoute( + string.Format("umbraco-{0}-{1}", "surface", meta.ControllerName), + areaName + "/Surface/" + meta.ControllerName + "/{action}/{id}",//url to match + new { controller = meta.ControllerName, action = "Index", id = UrlParameter.Optional }, + new[] { meta.ControllerNamespace }); //only match this namespace + route.DataTokens.Add("area", areaName); //only match this area + route.DataTokens.Add("umbraco", "surface"); //ensure the umbraco token is set + } + + var pluginSurfaceControllers = surfaceControllers.Where(x => x.GetType().GetCustomAttribute(false) != null); + foreach (var s in pluginSurfaceControllers) + { + var meta = s.GetMetadata(); + this.RouteControllerPlugin(meta.ControllerId.Value, meta.ControllerName, meta.ControllerType, routes, "surfaceId", "Surface", "", "Index", UrlParameter.Optional, "surface"); + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/SurfaceControllerAttribute.cs b/src/Umbraco.Web/Mvc/SurfaceControllerAttribute.cs new file mode 100644 index 0000000000..8facd8ed91 --- /dev/null +++ b/src/Umbraco.Web/Mvc/SurfaceControllerAttribute.cs @@ -0,0 +1,18 @@ +using System; + +namespace Umbraco.Web.Mvc +{ + /// + /// An attribute applied to surface controllers that are not locally declared (i.e. they are shipped as part of a package) + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class SurfaceControllerAttribute : Attribute + { + public Guid Id { get; private set; } + + public SurfaceControllerAttribute(Guid id) + { + Id = id; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/SurfaceControllerFactory.cs b/src/Umbraco.Web/Mvc/SurfaceControllerFactory.cs new file mode 100644 index 0000000000..a6eeda68d6 --- /dev/null +++ b/src/Umbraco.Web/Mvc/SurfaceControllerFactory.cs @@ -0,0 +1,67 @@ +using System; +using System.Web.Mvc; +using System.Web.Routing; + +namespace Umbraco.Web.Mvc +{ + /// + /// Creates SurfaceControllers + /// + public class SurfaceControllerFactory : RenderControllerFactory + { + /// + /// Check if the correct data tokens are in the route values so that we know its a surface controller route + /// + /// + /// + public override bool CanHandle(RequestContext request) + { + var area = request.RouteData.DataTokens["area"]; + + //if its a non-area route don't handle, all surface controllers will be in the 'umbraco' area + if (area == null || string.IsNullOrWhiteSpace(area.ToString())) + return false; + + //ensure there is an umbraco token set + var umbracoToken = request.RouteData.DataTokens["umbraco"]; + if (umbracoToken == null || string.IsNullOrWhiteSpace(umbracoToken.ToString())) + return false; + + return true; + } + + /// + /// Create the controller + /// + /// + /// + /// + public override IController CreateController(RequestContext requestContext, string controllerName) + { + //first try to instantiate with the DependencyResolver, if that fails, try with the UmbracoContext as a param, if that fails try with no params. + var controllerType = GetControllerType(requestContext, controllerName); + if (controllerType == null) + throw new InvalidOperationException("Could not find a controller type for the controller name " + controllerName); + + object controllerObject; + try + { + controllerObject = DependencyResolver.Current.GetService(controllerType); + } + catch (Exception) + { + try + { + controllerObject = Activator.CreateInstance(controllerType, UmbracoContext.Current); + } + catch (Exception) + { + //if this throws an exception, we'll let it + controllerObject = Activator.CreateInstance(controllerType); + } + } + //if an exception is thrown here, we want it to be thrown as its an invalid cast. + return (IController)controllerObject; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/SurfaceControllerMetadata.cs b/src/Umbraco.Web/Mvc/SurfaceControllerMetadata.cs new file mode 100644 index 0000000000..ca95965de8 --- /dev/null +++ b/src/Umbraco.Web/Mvc/SurfaceControllerMetadata.cs @@ -0,0 +1,15 @@ +using System; + +namespace Umbraco.Web.Mvc +{ + /// + /// Represents some metadata about the surface controller + /// + internal class SurfaceControllerMetadata + { + internal Type ControllerType { get; set; } + internal string ControllerName { get; set; } + internal string ControllerNamespace { get; set; } + internal Guid? ControllerId { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/SurfaceControllerResolver.cs b/src/Umbraco.Web/Mvc/SurfaceControllerResolver.cs new file mode 100644 index 0000000000..ec835ee00f --- /dev/null +++ b/src/Umbraco.Web/Mvc/SurfaceControllerResolver.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.ObjectResolution; + +namespace Umbraco.Web.Mvc +{ + internal class SurfaceControllerResolver : ManyObjectsResolverBase + { + public SurfaceControllerResolver(IEnumerable surfaceControllers) + : base(surfaceControllers) + { + + } + + /// + /// Gets the surface controllers + /// + public IEnumerable SurfaceControllers + { + get { return Values; } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/UmbracoPageResult.cs b/src/Umbraco.Web/Mvc/UmbracoPageResult.cs new file mode 100644 index 0000000000..63919c1d5c --- /dev/null +++ b/src/Umbraco.Web/Mvc/UmbracoPageResult.cs @@ -0,0 +1,50 @@ +using System; +using System.Web.Mvc; +using Umbraco.Core; + +namespace Umbraco.Web.Mvc +{ + /// + /// Used by posted forms to proxy the result to the page in which the current URL matches on + /// + public class UmbracoPageResult : ActionResult + { + public override void ExecuteResult(ControllerContext context) + { + + //since we could be returning the current page from a surface controller posted values in which the routing values are changed, we + //need to revert these values back to nothing in order for the normal page to render again. + context.RouteData.DataTokens["area"] = null; + context.RouteData.DataTokens["Namespaces"] = null; + + //validate that the current page execution is not being handled by the normal umbraco routing system + if (!context.RouteData.DataTokens.ContainsKey("umbraco-route-def")) + { + throw new InvalidOperationException("Can only use " + typeof(UmbracoPageResult).Name + " in the context of an Http POST when using a SurfaceController form"); + } + + var routeDef = (RouteDefinition)context.RouteData.DataTokens["umbraco-route-def"]; + + //ensure ModelState is copied across + routeDef.Controller.ViewData.ModelState.Merge(context.Controller.ViewData.ModelState); + + //ensure TempData and ViewData is copied across + foreach (var d in context.Controller.ViewData) + routeDef.Controller.ViewData[d.Key] = d.Value; + routeDef.Controller.TempData = context.Controller.TempData; + + using (DisposableTimer.TraceDuration("Executing Umbraco RouteDefinition controller", "Finished")) + { + try + { + ((IController)routeDef.Controller).Execute(context.RequestContext); + } + finally + { + routeDef.Controller.DisposeIfDisposable(); + } + } + + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/PluginManagerExtensions.cs b/src/Umbraco.Web/PluginManagerExtensions.cs index 5ff0e9f81d..07a3ce9127 100644 --- a/src/Umbraco.Web/PluginManagerExtensions.cs +++ b/src/Umbraco.Web/PluginManagerExtensions.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Threading; using Umbraco.Core; - +using Umbraco.Web.Mvc; using Umbraco.Web.Routing; using umbraco; using umbraco.interfaces; @@ -14,6 +14,11 @@ namespace Umbraco.Web /// public static class PluginManagerExtensions { + internal static IEnumerable ResolveSurfaceControllers(this PluginManager resolver) + { + return resolver.ResolveTypes(); + } + /// /// Returns all available ITrees in application /// diff --git a/src/Umbraco.Web/RouteCollectionExtensions.cs b/src/Umbraco.Web/RouteCollectionExtensions.cs new file mode 100644 index 0000000000..e0950e99b2 --- /dev/null +++ b/src/Umbraco.Web/RouteCollectionExtensions.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web.Mvc; +using System.Web.Routing; + +namespace Umbraco.Web +{ + internal static class RouteCollectionExtensions + { + + public static void IgnoreStandardExclusions(this RouteCollection routes) + { + // Ignore standard stuff... + using (routes.GetWriteLock()) + { + var exclusions = new Dictionary() + { + {"{resource}.axd/{*pathInfo}", null}, + {"{*allaxd}", new { allaxd = @".*\.axd(/.*)?" }}, + {"{*allashx}", new { allashx = @".*\.ashx(/.*)?" }}, + {"{*allaspx}", new { allaspx = @".*\.aspx(/.*)?" }}, + {"{*favicon}", new { favicon = @"(.*/)?favicon.ico(/.*)?" }}, + }; + //ensure they're not re-added + foreach (var e in exclusions.Where(e => !routes.OfType().Any(x => x.Url == e.Key))) + { + if (e.Value == null) + { + routes.IgnoreRoute(e.Key); + } + else + { + routes.IgnoreRoute(e.Key, e.Value); + } + } + } + } + + /// + /// Extension method to manually regsiter an area + /// + /// + /// + public static void RegisterArea(this RouteCollection routes) + where T : AreaRegistration + { + + // instantiate the area registration + var area = Activator.CreateInstance(); + + // create a context, which is just the name and routes collection + var context = new AreaRegistrationContext(area.AreaName, routes); + + // register it! + area.RegisterArea(context); + } + + ///// + ///// Extension method to manually regsiter an area from the container + ///// + ///// + ///// + ///// + //public static void RegisterArea(this RouteCollection routes, Framework.DependencyManagement.IDependencyResolver container) + // where T : AreaRegistration + //{ + + // var area = container.Resolve() as AreaRegistration; + // if (area == null) + // { + // throw new InvalidCastException("Could not resolve type " + typeof(T).FullName + " to AreaRegistration"); + // } + + // // create a context, which is just the name and routes collection + // var context = new AreaRegistrationContext(area.AreaName, routes); + + // // register it! + // area.RegisterArea(context); + //} + + + public static void RegisterArea(this RouteCollection routes, T area) where T : AreaRegistration + { + area.RegisterArea(new AreaRegistrationContext(area.AreaName, routes)); + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Routing/NiceUrlProvider.cs b/src/Umbraco.Web/Routing/NiceUrlProvider.cs index 21fb08e5bb..efb1a25cf8 100644 --- a/src/Umbraco.Web/Routing/NiceUrlProvider.cs +++ b/src/Umbraco.Web/Routing/NiceUrlProvider.cs @@ -17,7 +17,10 @@ namespace Umbraco.Web.Routing /// Provides nice urls for a nodes. /// internal class NiceUrlProvider - { + { + + internal const string NullUrl = "#"; + /// /// Initializes a new instance of the class. /// @@ -81,7 +84,7 @@ namespace Umbraco.Web.Routing "Couldn't find any page with nodeId={0}. This is most likely caused by the page not being published.", nodeId); - return "#"; + return NullUrl; } // walk up from that node until we hit a node with a domain, @@ -169,7 +172,7 @@ namespace Umbraco.Web.Routing { var node = _publishedContentStore.GetDocumentById(_umbracoContext, nodeId); if (node == null) - return new string[] { "#" }; // legacy wrote to the log here... + return new string[] { NullUrl }; // legacy wrote to the log here... var pathParts = new List(); int id = nodeId; diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 7ce6953df8..cce5ce9568 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -240,6 +240,7 @@ Properties\SolutionInfo.cs + @@ -271,17 +272,28 @@ + + + True True Strings.resx + + + + + + + + ASPXCodeBehind diff --git a/src/Umbraco.Web/UmbracoContext.cs b/src/Umbraco.Web/UmbracoContext.cs index 31cd89979b..bfbaf2ae38 100644 --- a/src/Umbraco.Web/UmbracoContext.cs +++ b/src/Umbraco.Web/UmbracoContext.cs @@ -16,6 +16,11 @@ using Examine; namespace Umbraco.Web { + public interface IRequiresUmbracoContext + { + UmbracoContext UmbracoContext { get; set; } + } + /// /// Class that encapsulates Umbraco information of a specific HTTP request /// diff --git a/src/Umbraco.Web/WebBootManager.cs b/src/Umbraco.Web/WebBootManager.cs index 25b8a8617e..483a3b090f 100644 --- a/src/Umbraco.Web/WebBootManager.cs +++ b/src/Umbraco.Web/WebBootManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Web.Mvc; using System.Web.Routing; using Umbraco.Core; +using Umbraco.Core.Configuration; using Umbraco.Core.Dictionary; using Umbraco.Core.Dynamics; using Umbraco.Core.PropertyEditors; @@ -13,6 +14,7 @@ using Umbraco.Web.PropertyEditors; using Umbraco.Web.Routing; using umbraco.businesslogic; + namespace Umbraco.Web { /// @@ -55,8 +57,6 @@ namespace Umbraco.Web //set model binder ModelBinders.Binders.Add(new KeyValuePair(typeof(RenderModel), new RenderModelBinder())); - //set routes - CreateRoutes(); //find and initialize the application startup handlers, we need to initialize this resolver here because //it is a special resolver where they need to be instantiated first before any other resolvers in order to bind to @@ -100,6 +100,9 @@ namespace Umbraco.Web { base.Complete(afterComplete); + //set routes + CreateRoutes(); + //call OnApplicationStarting of each application events handler ApplicationEventsResolver.Current.ApplicationEventHandlers .ForEach(x => x.OnApplicationStarted(_umbracoApplication, ApplicationContext)); @@ -112,15 +115,25 @@ namespace Umbraco.Web /// protected internal void CreateRoutes() { + //set routes - var route = RouteTable.Routes.MapRoute( + var defaultRoute = RouteTable.Routes.MapRoute( "Umbraco_default", "Umbraco/RenderMvc/{action}/{id}", new { controller = "RenderMvc", action = "Index", id = UrlParameter.Optional } ); - route.RouteHandler = new RenderRouteHandler(ControllerBuilder.Current.GetControllerFactory()); + defaultRoute.RouteHandler = new RenderRouteHandler(ControllerBuilder.Current.GetControllerFactory()); + + //now we need to find the surface controllers and route them too + var surfaceControllers = SurfaceControllerResolver.Current.SurfaceControllers; + //create a custom area for them + var surfaceControllerArea = new SurfaceControllerArea(surfaceControllers); + //register it + RouteTable.Routes.RegisterArea(surfaceControllerArea); } + + /// /// Initializes all web based and core resolves /// @@ -128,6 +141,9 @@ namespace Umbraco.Web { base.InitializeResolvers(); + SurfaceControllerResolver.Current = new SurfaceControllerResolver( + PluginManager.Current.ResolveSurfaceControllers()); + //the base creates the PropertyEditorValueConvertersResolver but we want to modify it in the web app and replace //the TinyMcePropertyEditorValueConverter with the RteMacroRenderingPropertyEditorValueConverter PropertyEditorValueConvertersResolver.Current.RemoveType(); diff --git a/src/umbraco.MacroEngines/umbraco.MacroEngines.csproj b/src/umbraco.MacroEngines/umbraco.MacroEngines.csproj index 58786d2ce8..38b25e1993 100644 --- a/src/umbraco.MacroEngines/umbraco.MacroEngines.csproj +++ b/src/umbraco.MacroEngines/umbraco.MacroEngines.csproj @@ -54,6 +54,7 @@ + False ..\..\lib\WebPages\System.Web.Razor.dll