using System; using System.Linq; using System.Text; using System.Web; using System.Web.Compilation; using System.Web.Mvc; using System.Web.Routing; using System.Web.SessionState; using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Configuration; using Umbraco.Core.Models; using Umbraco.Web.Models; using Umbraco.Web.Routing; using umbraco.cms.businesslogic.template; using System.Collections.Generic; namespace Umbraco.Web.Mvc { public class RenderRouteHandler : IRouteHandler { // Define reserved dictionary keys for controller, action and area specified in route additional values data private static class ReservedAdditionalKeys { internal const string Controller = "c"; internal const string Action = "a"; internal const string Area = "ar"; } public RenderRouteHandler(IControllerFactory controllerFactory) { if (controllerFactory == null) throw new ArgumentNullException("controllerFactory"); _controllerFactory = controllerFactory; } /// /// Contructor generally used for unit testing /// /// /// internal RenderRouteHandler(IControllerFactory controllerFactory, UmbracoContext umbracoContext) { if (controllerFactory == null) throw new ArgumentNullException("controllerFactory"); if (umbracoContext == null) throw new ArgumentNullException("umbracoContext"); _controllerFactory = controllerFactory; _umbracoContext = umbracoContext; } private readonly IControllerFactory _controllerFactory; private readonly UmbracoContext _umbracoContext; /// /// Returns the current UmbracoContext /// public UmbracoContext UmbracoContext { get { return _umbracoContext ?? UmbracoContext.Current; } } #region IRouteHandler Members /// /// Assigns the correct controller based on the Umbraco request and returns a standard MvcHandler to prcess the response, /// this also stores the render model into the data tokens for the current RouteData. /// /// /// public IHttpHandler GetHttpHandler(RequestContext requestContext) { if (UmbracoContext == null) { throw new NullReferenceException("There is not current UmbracoContext, it must be initialized before the RenderRouteHandler executes"); } var docRequest = UmbracoContext.PublishedContentRequest; if (docRequest == null) { throw new NullReferenceException("There is not current PublishedContentRequest, it must be initialized before the RenderRouteHandler executes"); } SetupRouteDataForRequest( new RenderModel(docRequest.PublishedContent, docRequest.Culture), requestContext, docRequest); return GetHandlerForRoute(requestContext, docRequest); } #endregion /// /// Ensures that all of the correct DataTokens are added to the route values which are all required for rendering front-end umbraco views /// /// /// /// internal void SetupRouteDataForRequest(RenderModel renderModel, RequestContext requestContext, PublishedContentRequest docRequest) { //put essential data into the data tokens, the 'umbraco' key is required to be there for the view engine requestContext.RouteData.DataTokens.Add("umbraco", renderModel); //required for the RenderModelBinder and view engine requestContext.RouteData.DataTokens.Add("umbraco-doc-request", docRequest); //required for RenderMvcController requestContext.RouteData.DataTokens.Add("umbraco-context", UmbracoContext); //required for UmbracoTemplatePage } private void UpdateRouteDataForRequest(RenderModel renderModel, RequestContext requestContext) { if (renderModel == null) throw new ArgumentNullException("renderModel"); if (requestContext == null) throw new ArgumentNullException("requestContext"); requestContext.RouteData.DataTokens["umbraco"] = renderModel; // the rest should not change -- it's only the published content that has changed } /// /// Checks the request and query strings to see if it matches the definition of having a Surface controller /// posted/get value, if so, then we return a PostedDataProxyInfo object with the correct information. /// /// /// private static PostedDataProxyInfo GetFormInfo(RequestContext requestContext) { if (requestContext == null) throw new ArgumentNullException("requestContext"); //if it is a POST/GET then a value must be in the request if (requestContext.HttpContext.Request.QueryString["ufprt"].IsNullOrWhiteSpace() && requestContext.HttpContext.Request.Form["ufprt"].IsNullOrWhiteSpace()) { return null; } string encodedVal; switch (requestContext.HttpContext.Request.RequestType) { case "POST": //get the value from the request. //this field will contain an encrypted version of the surface route vals. encodedVal = requestContext.HttpContext.Request.Form["ufprt"]; break; case "GET": //this field will contain an encrypted version of the surface route vals. encodedVal = requestContext.HttpContext.Request.QueryString["ufprt"]; break; default: return null; } string decryptedString; try { decryptedString = encodedVal.DecryptWithMachineKey(); } catch (FormatException) { LogHelper.Warn("A value was detected in the ufprt parameter but Umbraco could not decrypt the string"); return null; } var parsedQueryString = HttpUtility.ParseQueryString(decryptedString); var decodedParts = new Dictionary(); foreach (var key in parsedQueryString.AllKeys) { decodedParts[key] = parsedQueryString[key]; } //validate all required keys exist //the controller if (decodedParts.All(x => x.Key != ReservedAdditionalKeys.Controller)) return null; //the action if (decodedParts.All(x => x.Key != ReservedAdditionalKeys.Action)) return null; //the area if (decodedParts.All(x => x.Key != ReservedAdditionalKeys.Area)) return null; foreach (var item in decodedParts.Where(x => new[] { ReservedAdditionalKeys.Controller, ReservedAdditionalKeys.Action, ReservedAdditionalKeys.Area }.Contains(x.Key) == false)) { // Populate route with additional values which aren't reserved values so they eventually to action parameters requestContext.RouteData.Values[item.Key] = item.Value; } //return the proxy info without the surface id... could be a local controller. return new PostedDataProxyInfo { ControllerName = HttpUtility.UrlDecode(decodedParts.Single(x => x.Key == ReservedAdditionalKeys.Controller).Value), ActionName = HttpUtility.UrlDecode(decodedParts.Single(x => x.Key == ReservedAdditionalKeys.Action).Value), Area = HttpUtility.UrlDecode(decodedParts.Single(x => x.Key == ReservedAdditionalKeys.Area).Value), }; } /// /// Handles a posted form to an Umbraco Url and ensures the correct controller is routed to and that /// the right DataTokens are set. /// /// /// private IHttpHandler HandlePostedValues(RequestContext requestContext, PostedDataProxyInfo postedInfo) { if (requestContext == null) throw new ArgumentNullException("requestContext"); if (postedInfo == null) throw new ArgumentNullException("postedInfo"); //set the standard route values/tokens requestContext.RouteData.Values["controller"] = postedInfo.ControllerName; requestContext.RouteData.Values["action"] = postedInfo.ActionName; IHttpHandler handler; //get the route from the defined routes using (RouteTable.Routes.GetReadLock()) { Route surfaceRoute; if (postedInfo.Area.IsNullOrWhiteSpace()) { //find the controller in the route table without an area var surfaceRoutes = RouteTable.Routes.OfType() .Where(x => x.Defaults != null && x.Defaults.ContainsKey("controller") && x.Defaults["controller"].ToString().InvariantEquals(postedInfo.ControllerName) && x.DataTokens.ContainsKey("area") == false).ToList(); // If more than one route is found, find one with a matching action if (surfaceRoutes.Count() > 1) { surfaceRoute = surfaceRoutes.FirstOrDefault(x => x.Defaults["action"] != null && x.Defaults["action"].ToString().InvariantEquals(postedInfo.ActionName)); } else { surfaceRoute = surfaceRoutes.SingleOrDefault(); } } 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["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); //set the area if one is there. if (surfaceRoute.DataTokens.ContainsKey("area")) { 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); } return handler; } /// /// Returns a RouteDefinition object based on the current renderModel /// /// /// /// internal virtual RouteDefinition GetUmbracoRouteDefinition(RequestContext requestContext, PublishedContentRequest publishedContentRequest) { if (requestContext == null) throw new ArgumentNullException("requestContext"); if (publishedContentRequest == null) throw new ArgumentNullException("publishedContentRequest"); var defaultControllerType = DefaultRenderMvcControllerResolver.Current.GetDefaultControllerType(); var defaultControllerName = ControllerExtensions.GetControllerName(defaultControllerType); //creates the default route definition which maps to the 'UmbracoController' controller var def = new RouteDefinition { ControllerName = defaultControllerName, ControllerType = defaultControllerType, 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 controllerType = _controllerFactory.GetControllerTypeInternal(requestContext, publishedContentRequest.PublishedContent.DocumentTypeAlias); //check if that controller exists if (controllerType != null) { //ensure the controller is of type 'IRenderMvcController' and ControllerBase if (TypeHelper.IsTypeAssignableFrom(controllerType) && TypeHelper.IsTypeAssignableFrom(controllerType)) { //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 implement '{2}' and inherit from '{3}'.", () => publishedContentRequest.PublishedContent.DocumentTypeAlias, () => controllerType.FullName, () => typeof(IRenderMvcController).FullName, () => typeof(ControllerBase).FullName); //we cannot route to this custom controller since it is not of the correct type so we'll continue with the defaults // that have already been set above. } } //store the route definition requestContext.RouteData.DataTokens["umbraco-route-def"] = def; return def; } internal IHttpHandler GetHandlerOnMissingTemplate(PublishedContentRequest pcr) { if (pcr == null) throw new ArgumentNullException("pcr"); // missing template, so we're in a 404 here // so the content, if any, is a custom 404 page of some sort if (!pcr.HasPublishedContent) // means the builder could not find a proper document to handle 404 return new PublishedContentNotFoundHandler(); if (!pcr.HasTemplate) // means the engine could find a proper document, but the document has no template // at that point there isn't much we can do and there is no point returning // to Mvc since Mvc can't do much return new PublishedContentNotFoundHandler("In addition, no template exists to render the custom 404."); // so we have a template, so we should have a rendering engine if (pcr.RenderingEngine == RenderingEngine.WebForms) // back to webforms ? return GetWebFormsHandler(); if (pcr.RenderingEngine != RenderingEngine.Mvc) // else ? return new PublishedContentNotFoundHandler("In addition, no rendering engine exists to render the custom 404."); return null; } /// /// this will determine the controller and set the values in the route data /// /// /// internal IHttpHandler GetHandlerForRoute(RequestContext requestContext, PublishedContentRequest publishedContentRequest) { if (requestContext == null) throw new ArgumentNullException("requestContext"); if (publishedContentRequest == null) throw new ArgumentNullException("publishedContentRequest"); var routeDef = GetUmbracoRouteDefinition(requestContext, publishedContentRequest); //Need to check for a special case if there is form data being posted back to an Umbraco URL var postedInfo = GetFormInfo(requestContext); if (postedInfo != null) { return HandlePostedValues(requestContext, postedInfo); } //Now we can check if we are supposed to render WebForms when the route has not been hijacked if (publishedContentRequest.RenderingEngine == RenderingEngine.WebForms && publishedContentRequest.HasTemplate && routeDef.HasHijackedRoute == false) { return GetWebFormsHandler(); } //here we need to check if there is no hijacked route and no template assigned, if this is the case //we want to return a blank page, but we'll leave that up to the NoTemplateHandler. if (!publishedContentRequest.HasTemplate && !routeDef.HasHijackedRoute) { publishedContentRequest.UpdateOnMissingTemplate(); // will go 404 // HandleHttpResponseStatus returns a value indicating that the request should // not be processed any further, eg because it has been redirect. then, exit. if (UmbracoModule.HandleHttpResponseStatus(requestContext.HttpContext, publishedContentRequest)) return null; var handler = GetHandlerOnMissingTemplate(publishedContentRequest); // if it's not null it can be either the PublishedContentNotFoundHandler (no document was // found to handle 404, or document with no template was found) or the WebForms handler // (a document was found and its template is WebForms) // if it's null it means that a document was found and its template is Mvc // if we have a handler, return now if (handler != null) return handler; // else we are running Mvc // update the route data - because the PublishedContent has changed UpdateRouteDataForRequest( new RenderModel(publishedContentRequest.PublishedContent, publishedContentRequest.Culture), requestContext); // update the route definition routeDef = GetUmbracoRouteDefinition(requestContext, publishedContentRequest); } //no post values, just route to the controller/action requried (local) requestContext.RouteData.Values["controller"] = routeDef.ControllerName; if (!string.IsNullOrWhiteSpace(routeDef.ActionName)) { 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 UmbracoMvcHandler(requestContext); } /// /// Returns the handler for webforms requests /// /// internal static IHttpHandler GetWebFormsHandler() { return (global::umbraco.UmbracoDefault)BuildManager.CreateInstanceFromVirtualPath("~/default.aspx", typeof(global::umbraco.UmbracoDefault)); } private SessionStateBehavior GetSessionStateBehavior(RequestContext requestContext, string controllerName) { return _controllerFactory.GetControllerSessionBehavior(requestContext, controllerName); } } }