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 ;
2013-02-16 16:00:28 -01:00
using System.Web.SessionState ;
2012-08-07 21:40:34 +06:00
using Umbraco.Core ;
2012-08-08 23:10:34 +06:00
using Umbraco.Core.Logging ;
2013-02-07 13:30:06 -01:00
using Umbraco.Core.Configuration ;
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 ;
2013-01-15 00:58:11 +03:00
using System.Collections.Generic ;
2012-08-07 21:40:34 +06:00
namespace Umbraco.Web.Mvc
{
public class RenderRouteHandler : IRouteHandler
{
2013-01-15 00:58:11 +03:00
// 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" ;
}
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" ] ;
2013-01-15 20:07:07 +00:00
var decryptedString = encodedVal . DecryptWithMachineKey ( ) ;
var parsedQueryString = HttpUtility . ParseQueryString ( decryptedString ) ;
2013-01-15 00:58:11 +03:00
var decodedParts = new Dictionary < string , string > ( ) ;
foreach ( var key in parsedQueryString . AllKeys )
{
decodedParts [ key ] = parsedQueryString [ key ] ;
}
2012-09-25 13:09:59 +07:00
//validate all required keys exist
//the controller
2013-01-15 00:58:11 +03:00
if ( ! decodedParts . Any ( x = > x . Key = = ReservedAdditionalKeys . Controller ) )
2012-09-25 13:09:59 +07:00
return null ;
//the action
2013-01-15 00:58:11 +03:00
if ( ! decodedParts . Any ( x = > x . Key = = ReservedAdditionalKeys . Action ) )
2012-09-25 13:09:59 +07:00
return null ;
//the area
2013-01-15 00:58:11 +03:00
if ( ! decodedParts . Any ( x = > x . Key = = ReservedAdditionalKeys . Area ) )
2012-09-25 13:09:59 +07:00
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
2013-01-15 00:58:11 +03:00
foreach ( var item in decodedParts . Where ( x = > ! new string [ ] {
ReservedAdditionalKeys . Controller ,
ReservedAdditionalKeys . Action ,
ReservedAdditionalKeys . Area } . Contains ( x . Key ) ) )
{
// Populate route with additional values which aren't reserved values so they eventually to action parameters
requestContext . RouteData . Values . Add ( item . Key , item . 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
{
2013-01-15 00:58:11 +03:00
ControllerName = requestContext . HttpContext . Server . UrlDecode ( decodedParts . Single ( x = > x . Key = = ReservedAdditionalKeys . Controller ) . Value ) ,
ActionName = requestContext . HttpContext . Server . UrlDecode ( decodedParts . Single ( x = > x . Key = = ReservedAdditionalKeys . Action ) . Value ) ,
Area = requestContext . HttpContext . Server . UrlDecode ( decodedParts . Single ( x = > x . Key = = ReservedAdditionalKeys . Area ) . Value ) ,
2012-09-25 13:09:59 +07:00
} ;
}
/// <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 ;
2013-02-16 16:00:28 -01:00
IHttpHandler handler = new UmbracoMvcHandler ( requestContext ) ;
2012-09-25 13:09:59 +07:00
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 ,
2013-02-16 16:00:28 -01:00
ControllerType = typeof ( 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
} ;
2013-02-16 16:00:28 -01: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
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 ;
}
2012-08-07 21:40:34 +06:00
2013-02-16 16:00:28 -01:00
//check if there's a custom controller assigned, base on the document type alias.
var controllerType = ( ( MasterControllerFactory ) _controllerFactory ) . GetControllerTypeInternal ( requestContext , publishedContentRequest . PublishedContent . DocumentTypeAlias ) ;
2012-09-07 08:28:18 +07:00
//check if that controller exists
2013-02-16 16:00:28 -01:00
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 < 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 ,
( ) = > controllerType . FullName ,
( ) = > typeof ( RenderMvcController ) . FullName ) ;
//exit as we cannnot route to the custom controller, just route to the standard one.
return def ;
}
}
return def ;
2012-08-07 21:40:34 +06:00
}
2013-01-18 13:13:06 -01:00
internal IHttpHandler GetHandlerOnMissingTemplate ( PublishedContentRequest pcr )
{
2013-01-28 18:36:58 -01:00
// missing template, so we're in a 404 here
// so the content, if any, is a custom 404 page of some sort
2013-01-18 13:13:06 -01:00
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." ) ;
2013-01-28 18:36:58 -01:00
// so we have a template, so we should have a rendering engine
if ( pcr . RenderingEngine = = RenderingEngine . WebForms ) // back to webforms ?
return ( global :: umbraco . UmbracoDefault ) System . Web . Compilation . BuildManager . CreateInstanceFromVirtualPath ( "~/default.aspx" , typeof ( global :: umbraco . UmbracoDefault ) ) ;
else if ( pcr . RenderingEngine ! = RenderingEngine . Mvc ) // else ?
return new PublishedContentNotFoundHandler ( "In addition, no rendering engine exists to render the custom 404." ) ;
2013-01-18 13:13:06 -01:00
return null ;
}
2012-08-07 21:40:34 +06:00
/// <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
{
2013-01-28 18:36:58 -01:00
publishedContentRequest . UpdateOnMissingTemplate ( ) ; // will go 404
2012-10-30 06:49:36 +06:00
if ( publishedContentRequest . IsRedirect )
{
2013-01-18 13:13:06 -01:00
requestContext . HttpContext . Response . Redirect ( publishedContentRequest . RedirectUrl , true ) ;
2012-10-30 06:49:36 +06:00
return null ;
}
2013-02-04 16:10:43 -01:00
if ( publishedContentRequest . Is404 ) // should always be the case
{
requestContext . HttpContext . Response . StatusCode = 404 ;
2013-02-07 13:30:06 -01:00
requestContext . HttpContext . Response . TrySkipIisCustomErrors = UmbracoSettings . TrySkipIisCustomErrors ;
2013-02-04 16:10:43 -01:00
}
2013-01-18 13:13:06 -01:00
var handler = GetHandlerOnMissingTemplate ( publishedContentRequest ) ;
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
2013-02-16 16:00:28 -01:00
// Set the session state requirements
requestContext . HttpContext . SetSessionStateBehavior ( GetSessionStateBehavior ( requestContext , routeDef . ControllerName ) ) ;
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 ) ;
2013-02-16 16:00:28 -01:00
return new UmbracoMvcHandler ( requestContext ) ;
2012-08-07 21:40:34 +06:00
}
2013-02-16 16:00:28 -01:00
private SessionStateBehavior GetSessionStateBehavior ( RequestContext requestContext , string controllerName )
{
return _controllerFactory . GetControllerSessionBehavior ( requestContext , controllerName ) ;
}
2012-08-07 21:40:34 +06:00
}
}