2012-08-08 23:37:54 +06:00
using System ;
2012-09-25 13:09:59 +07:00
using System.Linq ;
using System.Text ;
2012-08-07 21:40:34 +06:00
using System.Web ;
using System.Web.Mvc ;
using System.Web.Routing ;
using Umbraco.Core ;
2012-08-08 23:10:34 +06:00
using Umbraco.Core.Logging ;
2012-09-25 13:09:59 +07:00
using Umbraco.Core.Models ;
2012-10-24 09:59:23 +05:00
using Umbraco.Web.Models ;
2012-08-08 23:10:34 +06:00
using Umbraco.Web.Routing ;
2012-08-07 21:40:34 +06:00
using umbraco.cms.businesslogic.template ;
namespace Umbraco.Web.Mvc
{
public class RenderRouteHandler : IRouteHandler
{
2012-08-08 23:10:34 +06:00
2012-08-07 21:40:34 +06:00
public RenderRouteHandler ( IControllerFactory controllerFactory )
{
2012-10-26 02:55:57 +05:00
if ( controllerFactory = = null ) throw new ArgumentNullException ( "controllerFactory" ) ;
2012-08-07 21:40:34 +06:00
_controllerFactory = controllerFactory ;
}
2012-10-26 02:55:57 +05:00
/// <summary>
/// Contructor generally used for unit testing
/// </summary>
/// <param name="controllerFactory"></param>
/// <param name="umbracoContext"></param>
internal RenderRouteHandler ( IControllerFactory controllerFactory , UmbracoContext umbracoContext )
{
if ( controllerFactory = = null ) throw new ArgumentNullException ( "controllerFactory" ) ;
if ( umbracoContext = = null ) throw new ArgumentNullException ( "umbracoContext" ) ;
_controllerFactory = controllerFactory ;
_umbracoContext = umbracoContext ;
}
2012-08-07 21:40:34 +06:00
private readonly IControllerFactory _controllerFactory ;
2012-10-26 02:55:57 +05:00
private readonly UmbracoContext _umbracoContext ;
/// <summary>
/// Returns the current UmbracoContext
/// </summary>
public UmbracoContext UmbracoContext
{
get { return _umbracoContext ? ? UmbracoContext . Current ; }
}
2012-08-07 21:40:34 +06:00
#region IRouteHandler Members
/// <summary>
/// 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.
/// </summary>
/// <param name="requestContext"></param>
/// <returns></returns>
public IHttpHandler GetHttpHandler ( RequestContext requestContext )
2012-08-08 23:10:34 +06:00
{
2012-10-26 02:55:57 +05:00
if ( UmbracoContext = = null )
{
2012-08-08 23:37:54 +06:00
throw new NullReferenceException ( "There is not current UmbracoContext, it must be initialized before the RenderRouteHandler executes" ) ;
2012-08-08 23:10:34 +06:00
}
2012-10-26 02:55:57 +05:00
var docRequest = UmbracoContext . PublishedContentRequest ;
2012-08-08 23:37:54 +06:00
if ( docRequest = = null )
{
2012-10-02 01:40:19 +05:00
throw new NullReferenceException ( "There is not current PublishedContentRequest, it must be initialized before the RenderRouteHandler executes" ) ;
2012-08-08 23:37:54 +06:00
}
2012-11-14 10:47:40 +05:00
SetupRouteDataForRequest (
new RenderModel ( docRequest . PublishedContent , docRequest . Culture ) ,
requestContext ,
docRequest ) ;
2012-08-08 23:10:34 +06:00
return GetHandlerForRoute ( requestContext , docRequest ) ;
2012-08-07 21:40:34 +06:00
}
#endregion
2012-11-14 10:47:40 +05:00
/// <summary>
/// Ensures that all of the correct DataTokens are added to the route values which are all required for rendering front-end umbraco views
/// </summary>
/// <param name="renderModel"></param>
/// <param name="requestContext"></param>
/// <param name="docRequest"></param>
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
}
2012-09-25 13:09:59 +07:00
/// <summary>
/// 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.
/// </summary>
/// <param name="requestContext"></param>
/// <returns></returns>
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 ;
2012-09-26 13:42:03 +07:00
////the controller type, if it contains this then it is a plugin controller, not locally declared.
//if (decodedParts.Any(x => x.Key == "t"))
//{
// 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),
// ControllerType = requestContext.HttpContext.Server.UrlDecode(decodedParts.Single(x => x.Key == "t").Value)
// };
//}
2012-09-25 13:09:59 +07:00
//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 ) ,
} ;
}
/// <summary>
/// Handles a posted form to an Umbraco Url and ensures the correct controller is routed to and that
/// the right DataTokens are set.
/// </summary>
/// <param name="requestContext"></param>
/// <param name="postedInfo"></param>
/// <param name="routeDefinition">The original route definition that would normally be used to route if it were not a POST</param>
2012-09-26 13:42:03 +07:00
private IHttpHandler HandlePostedValues ( RequestContext requestContext , PostedDataProxyInfo postedInfo , RouteDefinition routeDefinition )
2012-09-25 13:09:59 +07:00
{
2012-09-26 13:42:03 +07:00
var standardArea = Umbraco . Core . Configuration . GlobalSettings . UmbracoMvcArea ;
2012-09-25 13:09:59 +07:00
//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 ) ;
2012-09-26 13:42:03 +07:00
//ensure the controllerType is set if found, meaning it is a plugin, not locally declared
if ( postedInfo . Area ! = standardArea )
2012-09-25 13:09:59 +07:00
{
2012-09-26 13:42:03 +07:00
//requestContext.RouteData.Values["controllerType"] = postedInfo.ControllerType;
2012-09-25 13:09:59 +07:00
//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 < Route > ( )
2012-09-26 13:42:03 +07:00
. SingleOrDefault ( x = > x . Defaults ! = null & &
x . Defaults . ContainsKey ( "controller" ) & &
x . Defaults [ "controller" ] . ToString ( ) = = postedInfo . ControllerName & &
x . DataTokens . ContainsKey ( "area" ) & &
x . DataTokens [ "area" ] . ToString ( ) = = postedInfo . Area ) ;
2012-09-25 13:09:59 +07:00
if ( surfaceRoute = = null )
2012-09-26 13:42:03 +07:00
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 ) ;
2012-09-25 13:09:59 +07:00
//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 ;
}
2012-08-07 21:40:34 +06:00
/// <summary>
/// Returns a RouteDefinition object based on the current renderModel
/// </summary>
/// <param name="requestContext"></param>
2012-10-02 01:40:19 +05:00
/// <param name="publishedContentRequest"></param>
2012-08-07 21:40:34 +06:00
/// <returns></returns>
2012-10-02 01:40:19 +05:00
internal virtual RouteDefinition GetUmbracoRouteDefinition ( RequestContext requestContext , PublishedContentRequest publishedContentRequest )
2012-08-07 21:40:34 +06:00
{
2012-09-07 08:28:18 +07:00
var defaultControllerName = ControllerExtensions . GetControllerName < RenderMvcController > ( ) ;
2012-08-07 21:40:34 +06:00
//creates the default route definition which maps to the 'UmbracoController' controller
var def = new RouteDefinition
{
2012-09-07 08:28:18 +07:00
ControllerName = defaultControllerName ,
2012-08-07 21:40:34 +06:00
Controller = new RenderMvcController ( ) ,
2012-10-02 01:40:19 +05:00
PublishedContentRequest = publishedContentRequest ,
2012-09-07 08:28:18 +07:00
ActionName = ( ( Route ) requestContext . RouteData . Route ) . Defaults [ "action" ] . ToString ( ) ,
HasHijackedRoute = false
2012-08-07 21:40:34 +06:00
} ;
2012-09-07 08:28:18 +07:00
//check if there's a custom controller assigned, base on the document type alias.
2012-10-02 01:40:19 +05:00
var controller = _controllerFactory . CreateController ( requestContext , publishedContentRequest . PublishedContent . DocumentTypeAlias ) ;
2012-08-07 21:40:34 +06:00
2012-09-07 08:28:18 +07:00
//check if that controller exists
if ( controller ! = null )
{
2012-08-07 21:40:34 +06:00
2012-09-07 08:28:18 +07:00
//ensure the controller is of type 'RenderMvcController'
if ( controller is RenderMvcController )
2012-08-07 21:40:34 +06:00
{
2012-09-07 08:28:18 +07:00
//set the controller and name to the custom one
def . Controller = ( ControllerBase ) controller ;
def . ControllerName = ControllerExtensions . GetControllerName ( controller . GetType ( ) ) ;
if ( def . ControllerName ! = defaultControllerName )
2012-08-07 21:40:34 +06:00
{
2012-09-07 08:28:18 +07:00
def . HasHijackedRoute = true ;
2012-08-07 21:40:34 +06:00
}
2012-09-07 08:28:18 +07:00
}
else
{
2012-12-04 11:08:02 +05:00
LogHelper . Warn < RenderRouteHandler > (
"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 ) ;
2012-09-07 08:28:18 +07:00
//exit as we cannnot route to the custom controller, just route to the standard one.
return def ;
}
2012-08-07 21:40:34 +06:00
2012-09-07 08:28:18 +07:00
//check that a template is defined), if it doesn't and there is a hijacked route it will just route
// to the index Action
2012-10-02 01:40:19 +05:00
if ( publishedContentRequest . HasTemplate )
2012-09-07 08:28:18 +07:00
{
2012-08-08 23:10:34 +06:00
//check if the custom controller has an action with the same name as the template name (we convert ToUmbracoAlias since the template name might have invalid chars).
//NOTE: This also means that all custom actions MUST be PascalCase.. but that should be standard.
2012-10-02 01:40:19 +05:00
var templateName = publishedContentRequest . Template . Alias . Split ( '.' ) [ 0 ] . ToUmbracoAlias ( StringAliasCaseType . PascalCase ) ;
2012-08-08 23:10:34 +06:00
def . ActionName = templateName ;
2012-08-07 21:40:34 +06:00
}
2012-09-07 08:28:18 +07:00
2012-08-07 21:40:34 +06:00
}
2012-09-07 08:28:18 +07:00
2012-08-07 21:40:34 +06:00
return def ;
}
/// <summary>
/// this will determine the controller and set the values in the route data
/// </summary>
/// <param name="requestContext"></param>
2012-10-02 01:40:19 +05:00
/// <param name="publishedContentRequest"></param>
internal IHttpHandler GetHandlerForRoute ( RequestContext requestContext , PublishedContentRequest publishedContentRequest )
2012-08-07 21:40:34 +06:00
{
2012-10-02 01:40:19 +05:00
var routeDef = GetUmbracoRouteDefinition ( requestContext , publishedContentRequest ) ;
2012-08-07 21:40:34 +06:00
2012-09-26 13:42:03 +07:00
//Need to check for a special case if there is form data being posted back to an Umbraco URL
var postedInfo = GetPostedFormInfo ( requestContext ) ;
if ( postedInfo ! = null )
{
return HandlePostedValues ( requestContext , postedInfo , routeDef ) ;
}
2012-09-07 08:28:18 +07:00
//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.
2012-10-02 01:40:19 +05:00
if ( ! publishedContentRequest . HasTemplate & & ! routeDef . HasHijackedRoute )
2012-09-07 08:28:18 +07:00
{
2012-10-29 12:35:39 -01:00
var handler = publishedContentRequest . ProcessNoTemplateInMvc ( requestContext . HttpContext ) ;
2012-10-30 06:49:36 +06:00
//though this code should never execute if the ProcessNoTemplateInMvc method redirects, it seems that we should check it
//and return null, this could be required for unit testing as well
if ( publishedContentRequest . IsRedirect )
{
return null ;
}
2012-10-29 12:35:39 -01:00
// 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 definition
routeDef = GetUmbracoRouteDefinition ( requestContext , publishedContentRequest ) ;
2012-09-07 08:28:18 +07:00
}
2012-08-07 21:40:34 +06:00
//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 ;
}
2012-10-26 02:55:57 +05:00
// 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 ) ;
2012-08-07 21:40:34 +06:00
return new MvcHandler ( requestContext ) ;
}
}
}