diff --git a/src/Umbraco.Web/Mvc/AreaRegistrationExtensions.cs b/src/Umbraco.Web/Mvc/AreaRegistrationExtensions.cs index c867a0823d..cc7cbbca19 100644 --- a/src/Umbraco.Web/Mvc/AreaRegistrationExtensions.cs +++ b/src/Umbraco.Web/Mvc/AreaRegistrationExtensions.cs @@ -26,7 +26,7 @@ namespace Umbraco.Web.Mvc /// The DataToken value to set for the 'umbraco' key, this defaults to 'backoffice' /// /// - internal static void RouteControllerPlugin(this AreaRegistration area, string controllerName, Type controllerType, RouteCollection routes, + internal static Route RouteControllerPlugin(this AreaRegistration area, string controllerName, Type controllerType, RouteCollection routes, string controllerSuffixName, string defaultAction, object defaultId, string umbracoTokenValue = "backoffice") { @@ -72,6 +72,7 @@ namespace Umbraco.Web.Mvc controllerPluginRoute.DataTokens.Add("area", area.AreaName); controllerPluginRoute.DataTokens.Add("umbraco", umbracoTokenValue); //ensure the umbraco token is set + return controllerPluginRoute; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/IFilteredControllerFactory.cs b/src/Umbraco.Web/Mvc/IFilteredControllerFactory.cs index 9fbb3ac34c..214532b6e4 100644 --- a/src/Umbraco.Web/Mvc/IFilteredControllerFactory.cs +++ b/src/Umbraco.Web/Mvc/IFilteredControllerFactory.cs @@ -1,9 +1,11 @@ +using System; using System.Linq; using System.Web.Mvc; using System.Web.Routing; namespace Umbraco.Web.Mvc { + public interface IFilteredControllerFactory : IControllerFactory { /// @@ -13,5 +15,6 @@ namespace Umbraco.Web.Mvc /// true if this instance can handle the specified request; otherwise, false. /// bool CanHandle(RequestContext request); + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/MasterControllerFactory.cs b/src/Umbraco.Web/Mvc/MasterControllerFactory.cs index 357dbf899c..4a58fb7251 100644 --- a/src/Umbraco.Web/Mvc/MasterControllerFactory.cs +++ b/src/Umbraco.Web/Mvc/MasterControllerFactory.cs @@ -45,6 +45,37 @@ namespace Umbraco.Web.Mvc : base.CreateController(requestContext, controllerName); } + /// + /// Retrieves the controller type for the specified name and request context. + /// + /// + /// + /// The controller type. + /// + /// The context of the HTTP request, which includes the HTTP context and route data. + /// The name of the controller. + internal Type GetControllerTypeInternal(RequestContext requestContext, string controllerName) + { + var factory = _slaveFactories.Factories.FirstOrDefault(x => x.CanHandle(requestContext)); + if (factory != null) + { + //check to see if the factory is of type UmbracoControllerFactory which exposes the GetControllerType method so we don't have to create + // an instance of the controller to figure out what it is. This is a work around for not having a breaking change for: + // http://issues.umbraco.org/issue/U4-1726 + + var umbFactory = factory as UmbracoControllerFactory; + if (umbFactory != null) + { + return umbFactory.GetControllerType(requestContext, controllerName); + } + //we have no choice but to instantiate the controller + var instance = factory.CreateController(requestContext, controllerName); + return instance.GetType(); + } + + return base.GetControllerType(requestContext, controllerName); + } + /// /// Releases the specified controller. /// diff --git a/src/Umbraco.Web/Mvc/PluginControllerArea.cs b/src/Umbraco.Web/Mvc/PluginControllerArea.cs index 591d53480a..42652eafd4 100644 --- a/src/Umbraco.Web/Mvc/PluginControllerArea.cs +++ b/src/Umbraco.Web/Mvc/PluginControllerArea.cs @@ -67,7 +67,9 @@ namespace Umbraco.Web.Mvc { foreach (var s in surfaceControllers) { - this.RouteControllerPlugin(s.ControllerName, s.ControllerType, routes, "Surface", "Index", UrlParameter.Optional, "surface"); + var route = this.RouteControllerPlugin(s.ControllerName, s.ControllerType, routes, "Surface", "Index", UrlParameter.Optional, "surface"); + //set the route handler to our SurfaceRouteHandler + route.RouteHandler = new SurfaceRouteHandler(); } } } diff --git a/src/Umbraco.Web/Mvc/RenderControllerFactory.cs b/src/Umbraco.Web/Mvc/RenderControllerFactory.cs index 56270ea36e..9482815a03 100644 --- a/src/Umbraco.Web/Mvc/RenderControllerFactory.cs +++ b/src/Umbraco.Web/Mvc/RenderControllerFactory.cs @@ -1,109 +1,26 @@ -using System; -using System.Web.Mvc; using System.Web.Routing; -using System.Web.SessionState; -using Umbraco.Core; namespace Umbraco.Web.Mvc { - /// + /// /// A controller factory for the render pipeline of Umbraco. This controller factory tries to create a controller with the supplied /// name, and falls back to UmbracoController if none was found. /// /// - public class RenderControllerFactory : IFilteredControllerFactory + public class RenderControllerFactory : UmbracoControllerFactory { - private readonly OverridenDefaultControllerFactory _innerFactory = new OverridenDefaultControllerFactory(); - - /// - /// Initializes a new instance of the class. - /// - public RenderControllerFactory() - { - - } - - + /// /// Determines whether this instance can handle the specified request. /// /// The request. /// true if this instance can handle the specified request; otherwise, false. /// - public virtual bool CanHandle(RequestContext request) + public override 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. - /// - /// - /// The controller. - /// - /// The request context.The name of the controller. - public virtual IController CreateController(RequestContext requestContext, string controllerName) - { - Type controllerType = GetControllerType(requestContext, controllerName) ?? - _innerFactory.GetControllerType(requestContext, ControllerExtensions.GetControllerName(typeof(RenderMvcController))); - - return _innerFactory.GetControllerInstance(requestContext, controllerType); - } - - /// - /// Gets the controller's session behavior. - /// - /// - /// The controller's session behavior. - /// - /// The request context.The name of the controller whose session behavior you want to get. - public SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName) - { - return ((IControllerFactory)_innerFactory).GetControllerSessionBehavior(requestContext, controllerName); - } - - /// - /// Releases the specified controller. - /// - /// The controller. - public void ReleaseController(IController controller) - { - _innerFactory.ReleaseController(controller); - } - - /// - /// By default, only exposes which throws an exception - /// if the controller is not found. Since we want to try creating a controller, and then fall back to if one isn't found, - /// this nested class changes the visibility of 's internal methods in order to not have to rely on a try-catch. - /// - /// - internal class OverridenDefaultControllerFactory : DefaultControllerFactory - { - public new IController GetControllerInstance(RequestContext requestContext, Type controllerType) - { - return base.GetControllerInstance(requestContext, controllerType); - } - - public new Type GetControllerType(RequestContext requestContext, string controllerName) - { - if (controllerName.IsNullOrWhiteSpace()) - { - return null; - } - return base.GetControllerType(requestContext, controllerName); - } - } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/RenderRouteHandler.cs b/src/Umbraco.Web/Mvc/RenderRouteHandler.cs index e0cf509b72..e8ebc21516 100644 --- a/src/Umbraco.Web/Mvc/RenderRouteHandler.cs +++ b/src/Umbraco.Web/Mvc/RenderRouteHandler.cs @@ -4,6 +4,7 @@ using System.Text; using System.Web; using System.Web.Mvc; using System.Web.Routing; +using System.Web.SessionState; using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Configuration; @@ -181,35 +182,46 @@ namespace Umbraco.Web.Mvc requestContext.RouteData.Values["controller"] = postedInfo.ControllerName; requestContext.RouteData.Values["action"] = postedInfo.ActionName; - IHttpHandler handler = new MvcHandler(requestContext); - - //ensure the controllerType is set if found, meaning it is a plugin, not locally declared + IHttpHandler handler; + + //get the route from the defined routes if (!postedInfo.Area.IsNullOrWhiteSpace()) - { - //requestContext.RouteData.Values["controllerType"] = postedInfo.ControllerType; - //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() - .SingleOrDefault(x => x.Defaults != null && - x.Defaults.ContainsKey("controller") && + using (RouteTable.Routes.GetReadLock()) + { + Route surfaceRoute; + if (postedInfo.Area == standardArea) + { + //find the controller in the route table without an area + surfaceRoute = RouteTable.Routes.OfType() + .SingleOrDefault(x => x.Defaults != null && + x.Defaults.ContainsKey("controller") && + x.Defaults["controller"].ToString() == postedInfo.ControllerName && + !x.DataTokens.ContainsKey("area")); + } + else + { + //find the controller in the route table with the specified area + surfaceRoute = RouteTable.Routes.OfType() + .SingleOrDefault(x => x.Defaults != null && + x.Defaults.ContainsKey("controller") && x.Defaults["controller"].ToString().InvariantEquals(postedInfo.ControllerName) && - x.DataTokens.ContainsKey("area") && + x.DataTokens.ContainsKey("area") && x.DataTokens["area"].ToString().InvariantEquals(postedInfo.Area)); - if (surfaceRoute == null) - throw new InvalidOperationException("Could not find a Surface controller route in the RouteTable for controller name " + postedInfo.ControllerName + " and within the area of " + postedInfo.Area); + } + + if (surfaceRoute == null) + throw new InvalidOperationException("Could not find a Surface controller route in the RouteTable for controller name " + postedInfo.ControllerName); requestContext.RouteData.DataTokens["area"] = surfaceRoute.DataTokens["area"]; - //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); - } + //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 route definition requestContext.RouteData.DataTokens["umbraco-route-def"] = routeDefinition; @@ -230,57 +242,53 @@ namespace Umbraco.Web.Mvc var def = new RouteDefinition { ControllerName = defaultControllerName, - Controller = new RenderMvcController(UmbracoContext), + ControllerType = typeof(RenderMvcController), PublishedContentRequest = publishedContentRequest, ActionName = ((Route)requestContext.RouteData.Route).Defaults["action"].ToString(), HasHijackedRoute = false }; + //check that a template is defined), if it doesn't and there is a hijacked route it will just route + // to the index Action + if (publishedContentRequest.HasTemplate) + { + //the template Alias should always be already saved with a safe name. + //if there are hyphens in the name and there is a hijacked route, then the Action will need to be attributed + // with the action name attribute. + var templateName = publishedContentRequest.TemplateAlias.Split('.')[0].ToSafeAlias(); + def.ActionName = templateName; + } + //check if there's a custom controller assigned, base on the document type alias. - var controller = _controllerFactory.CreateController(requestContext, publishedContentRequest.PublishedContent.DocumentTypeAlias); - - + var controllerType = ((MasterControllerFactory)_controllerFactory).GetControllerTypeInternal(requestContext, publishedContentRequest.PublishedContent.DocumentTypeAlias); + //check if that controller exists - if (controller != null) - { + if (controllerType != null) + { + //ensure the controller is of type 'RenderMvcController' + if (controllerType.IsSubclassOf(typeof (RenderMvcController))) + { + //set the controller and name to the custom one + def.ControllerType = controllerType; + def.ControllerName = ControllerExtensions.GetControllerName(controllerType); + if (def.ControllerName != defaultControllerName) + { + def.HasHijackedRoute = true; + } + } + else + { + LogHelper.Warn( + "The current Document Type {0} matches a locally declared controller of type {1}. Custom Controllers for Umbraco routing must inherit from '{2}'.", + () => publishedContentRequest.PublishedContent.DocumentTypeAlias, + () => controllerType.FullName, + () => typeof (RenderMvcController).FullName); + //exit as we cannnot route to the custom controller, just route to the standard one. + return def; + } + } - //ensure the controller is of type 'RenderMvcController' - if (controller is RenderMvcController) - { - //set the controller and name to the custom one - def.Controller = (ControllerBase)controller; - def.ControllerName = ControllerExtensions.GetControllerName(controller.GetType()); - if (def.ControllerName != defaultControllerName) - { - def.HasHijackedRoute = true; - } - } - else - { - LogHelper.Warn( - "The current Document Type {0} matches a locally declared controller of type {1}. Custom Controllers for Umbraco routing must inherit from '{2}'.", - () => publishedContentRequest.PublishedContent.DocumentTypeAlias, - () => controller.GetType().FullName, - () => typeof(RenderMvcController).FullName); - //exit as we cannnot route to the custom controller, just route to the standard one. - return def; - } - - //check that a template is defined), if it doesn't and there is a hijacked route it will just route - // to the index Action - if (publishedContentRequest.HasTemplate) - { - //the template Alias should always be already saved with a safe name. - //if there are hyphens in the name and there is a hijacked route, then the Action will need to be attributed - // with the action name attribute. - var templateName = publishedContentRequest.TemplateAlias.Split('.')[0].ToSafeAlias(); - def.ActionName = templateName; - } - - } - - - return def; + return def; } internal IHttpHandler GetHandlerOnMissingTemplate(PublishedContentRequest pcr) @@ -363,11 +371,19 @@ namespace Umbraco.Web.Mvc requestContext.RouteData.Values["action"] = routeDef.ActionName; } + // Set the session state requirements + requestContext.HttpContext.SetSessionStateBehavior(GetSessionStateBehavior(requestContext, routeDef.ControllerName)); + // reset the friendly path so in the controllers and anything occuring after this point in time, //the URL is reset back to the original request. requestContext.HttpContext.RewritePath(UmbracoContext.OriginalRequestUrl.PathAndQuery); - return new MvcHandler(requestContext); + return new UmbracoMvcHandler(requestContext); } + + private SessionStateBehavior GetSessionStateBehavior(RequestContext requestContext, string controllerName) + { + return _controllerFactory.GetControllerSessionBehavior(requestContext, controllerName); + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/RouteDefinition.cs b/src/Umbraco.Web/Mvc/RouteDefinition.cs index e9b7ffe378..49ba64fac5 100644 --- a/src/Umbraco.Web/Mvc/RouteDefinition.cs +++ b/src/Umbraco.Web/Mvc/RouteDefinition.cs @@ -1,3 +1,4 @@ +using System; using System.Web.Mvc; using Umbraco.Web.Routing; @@ -16,6 +17,12 @@ namespace Umbraco.Web.Mvc /// public ControllerBase Controller { get; set; } + /// + /// The Controller type found for routing to + /// + public Type ControllerType { get; set; } + + /// /// The current RenderModel found for the request /// diff --git a/src/Umbraco.Web/Mvc/SurfaceRouteHandler.cs b/src/Umbraco.Web/Mvc/SurfaceRouteHandler.cs new file mode 100644 index 0000000000..8e56cd715f --- /dev/null +++ b/src/Umbraco.Web/Mvc/SurfaceRouteHandler.cs @@ -0,0 +1,16 @@ +using System.Web; +using System.Web.Routing; + +namespace Umbraco.Web.Mvc +{ + /// + /// Assigned to all SurfaceController's so that it returns our custom SurfaceMvcHandler to use for rendering + /// + internal class SurfaceRouteHandler : IRouteHandler + { + public IHttpHandler GetHttpHandler(RequestContext requestContext) + { + return new UmbracoMvcHandler(requestContext); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/UmbracoControllerFactory.cs b/src/Umbraco.Web/Mvc/UmbracoControllerFactory.cs new file mode 100644 index 0000000000..a8f3817b6c --- /dev/null +++ b/src/Umbraco.Web/Mvc/UmbracoControllerFactory.cs @@ -0,0 +1,82 @@ +using System; +using System.Web.Mvc; +using System.Web.Routing; +using System.Web.SessionState; +using Umbraco.Core; + +namespace Umbraco.Web.Mvc +{ + /// + /// Abstract filtered controller factory used for all Umbraco controller factory implementations + /// + public abstract class UmbracoControllerFactory : IFilteredControllerFactory + { + private readonly OverridenDefaultControllerFactory _innerFactory = new OverridenDefaultControllerFactory(); + + public abstract bool CanHandle(RequestContext request); + + public virtual Type GetControllerType(RequestContext requestContext, string controllerName) + { + return _innerFactory.GetControllerType(requestContext, controllerName); + } + + /// + /// Creates the specified controller by using the specified request context. + /// + /// + /// The controller. + /// + /// The request context.The name of the controller. + public virtual IController CreateController(RequestContext requestContext, string controllerName) + { + Type controllerType = GetControllerType(requestContext, controllerName) ?? + _innerFactory.GetControllerType(requestContext, ControllerExtensions.GetControllerName(typeof(RenderMvcController))); + + return _innerFactory.GetControllerInstance(requestContext, controllerType); + } + + /// + /// Gets the controller's session behavior. + /// + /// + /// The controller's session behavior. + /// + /// The request context.The name of the controller whose session behavior you want to get. + public SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName) + { + return ((IControllerFactory)_innerFactory).GetControllerSessionBehavior(requestContext, controllerName); + } + + /// + /// Releases the specified controller. + /// + /// The controller. + public void ReleaseController(IController controller) + { + _innerFactory.ReleaseController(controller); + } + + /// + /// By default, only exposes which throws an exception + /// if the controller is not found. Since we want to try creating a controller, and then fall back to if one isn't found, + /// this nested class changes the visibility of 's internal methods in order to not have to rely on a try-catch. + /// + /// + internal class OverridenDefaultControllerFactory : DefaultControllerFactory + { + public new IController GetControllerInstance(RequestContext requestContext, Type controllerType) + { + return base.GetControllerInstance(requestContext, controllerType); + } + + public new Type GetControllerType(RequestContext requestContext, string controllerName) + { + if (controllerName.IsNullOrWhiteSpace()) + { + return null; + } + return base.GetControllerType(requestContext, controllerName); + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/UmbracoMvcHandler.cs b/src/Umbraco.Web/Mvc/UmbracoMvcHandler.cs new file mode 100644 index 0000000000..1ace297bfd --- /dev/null +++ b/src/Umbraco.Web/Mvc/UmbracoMvcHandler.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Web; +using System.Web.Mvc; +using System.Web.Routing; + +namespace Umbraco.Web.Mvc +{ + /// + /// Mvc handler class to intercept creation of controller and store it for later use. + /// This means we create two instances of the same controller to support some features later on. + /// + /// The alternate option for this is to completely rewrite all MvcHandler methods. + /// + /// This is currently needed for the 'return CurrentUmbracoPage()' surface controller functionality + /// because it needs to send data back to the page controller. + /// + internal class UmbracoMvcHandler : MvcHandler + { + public UmbracoMvcHandler(RequestContext requestContext) + : base(requestContext) + { + } + + private void StoreControllerInRouteDefinition() + { + var routeDef = (RouteDefinition)RequestContext.RouteData.DataTokens["umbraco-route-def"]; + + if (routeDef == null) return; + + // Get the factory and controller and create a new instance of the controller + var factory = ControllerBuilder.Current.GetControllerFactory(); + var controller = factory.CreateController(RequestContext, routeDef.ControllerName) as ControllerBase; + + // Store the controller + routeDef.Controller = controller; + } + + protected override void ProcessRequest(HttpContextBase httpContext) + { + StoreControllerInRouteDefinition(); + + // Let MVC do its magic and continue the request + base.ProcessRequest(httpContext); + } + + protected override IAsyncResult BeginProcessRequest(HttpContextBase httpContext, AsyncCallback callback, + object state) + { + StoreControllerInRouteDefinition(); + + return base.BeginProcessRequest(httpContext, callback, state); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index a09aa5ccb7..22238d67e7 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -300,9 +300,12 @@ + + + diff --git a/src/Umbraco.Web/WebBootManager.cs b/src/Umbraco.Web/WebBootManager.cs index ac72fe30c6..7fa62a6c8a 100644 --- a/src/Umbraco.Web/WebBootManager.cs +++ b/src/Umbraco.Web/WebBootManager.cs @@ -153,7 +153,9 @@ namespace Umbraco.Web umbracoPath + "/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("umbraco", "surface"); //ensure the umbraco token is set + route.DataTokens.Add("umbraco", "surface"); //ensure the umbraco token is set + //make it use our custom/special SurfaceMvcHandler + route.RouteHandler = new SurfaceRouteHandler(); } //need to get the plugin controllers that are unique to each area (group by)