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 ;
2014-02-13 13:19:51 +11:00
using System.Web.Compilation ;
2012-08-07 21:40:34 +06:00
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
{
2013-12-19 13:22:33 +11:00
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 ;
}
/// <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 ;
}
private readonly IControllerFactory _controllerFactory ;
private readonly UmbracoContext _umbracoContext ;
/// <summary>
/// Returns the current UmbracoContext
/// </summary>
public UmbracoContext UmbracoContext
{
get { return _umbracoContext ? ? UmbracoContext . Current ; }
}
#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 )
{
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
/// <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-11-14 10:47:40 +05:00
2013-05-03 20:09:14 -02:00
private void UpdateRouteDataForRequest ( RenderModel renderModel , RequestContext requestContext )
{
2013-09-17 11:53:57 +10:00
if ( renderModel = = null ) throw new ArgumentNullException ( "renderModel" ) ;
if ( requestContext = = null ) throw new ArgumentNullException ( "requestContext" ) ;
2013-05-03 20:09:14 -02:00
requestContext . RouteData . DataTokens [ "umbraco" ] = renderModel ;
// the rest should not change -- it's only the published content that has changed
}
2013-12-19 13:22:33 +11:00
/// <summary>
/// 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.
/// </summary>
/// <param name="requestContext"></param>
/// <returns></returns>
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 ( )
2013-09-06 11:40:37 +10:00
& & requestContext . HttpContext . Request . Form [ "ufprt" ] . IsNullOrWhiteSpace ( ) )
2013-12-19 13:22:33 +11:00
{
return null ;
}
string encodedVal ;
switch ( requestContext . HttpContext . Request . RequestType )
{
2013-09-05 14:01:28 +10:00
case "POST" :
//get the value from the request.
//this field will contain an encrypted version of the surface route vals.
2013-09-06 11:40:37 +10:00
encodedVal = requestContext . HttpContext . Request . Form [ "ufprt" ] ;
2013-12-19 13:22:33 +11:00
break ;
2013-09-05 14:01:28 +10:00
case "GET" :
//this field will contain an encrypted version of the surface route vals.
2013-09-06 11:40:37 +10:00
encodedVal = requestContext . HttpContext . Request . QueryString [ "ufprt" ] ;
2013-12-19 13:22:33 +11:00
break ;
2013-09-05 14:01:28 +10:00
default :
2013-12-19 13:22:33 +11:00
return null ;
}
2013-01-15 00:58:11 +03:00
2013-09-06 11:40:37 +10:00
string decryptedString ;
try
{
decryptedString = encodedVal . DecryptWithMachineKey ( ) ;
}
catch ( FormatException )
{
LogHelper . Warn < RenderRouteHandler > ( "A value was detected in the ufprt parameter but Umbraco could not decrypt the string" ) ;
return null ;
}
2013-09-05 14:01:28 +10:00
var parsedQueryString = HttpUtility . ParseQueryString ( decryptedString ) ;
2013-12-19 13:22:33 +11:00
var decodedParts = new Dictionary < string , string > ( ) ;
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 [ ] {
2013-09-06 11:40:37 +10:00
ReservedAdditionalKeys . Controller ,
ReservedAdditionalKeys . Action ,
ReservedAdditionalKeys . Area } . Contains ( x . Key ) = = false ) )
2013-12-19 13:22:33 +11:00
{
// 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 ) ,
2013-09-06 11:40:37 +10:00
ActionName = HttpUtility . UrlDecode ( decodedParts . Single ( x = > x . Key = = ReservedAdditionalKeys . Action ) . Value ) ,
Area = HttpUtility . UrlDecode ( decodedParts . Single ( x = > x . Key = = ReservedAdditionalKeys . Area ) . Value ) ,
2013-12-19 13:22:33 +11: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>
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 ;
2012-09-25 13:09:59 +07:00
2013-02-19 07:30:46 +06:00
IHttpHandler handler ;
2013-12-19 13:22:33 +11:00
2013-02-19 07:30:46 +06:00
//get the route from the defined routes
2013-12-19 13:22:33 +11:00
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 < Route > ( )
. 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 )
{
2014-07-16 13:41:02 +10:00
surfaceRoute = surfaceRoutes . FirstOrDefault ( x = >
x . Defaults [ "action" ] ! = null & &
x . Defaults [ "action" ] . ToString ( ) . InvariantEquals ( postedInfo . ActionName ) ) ;
2013-12-19 13:22:33 +11:00
}
else
{
surfaceRoute = surfaceRoutes . SingleOrDefault ( ) ;
}
}
else
{
2013-02-19 07:30:46 +06:00
//find the controller in the route table with the specified area
2013-12-19 13:22:33 +11:00
surfaceRoute = RouteTable . Routes . OfType < Route > ( )
. 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 ) ) ;
}
2013-02-19 07:30:46 +06:00
if ( surfaceRoute = = null )
throw new InvalidOperationException ( "Could not find a Surface controller route in the RouteTable for controller name " + postedInfo . ControllerName ) ;
2013-12-19 13:22:33 +11:00
2013-02-19 07:45:01 +06:00
//set the area if one is there.
if ( surfaceRoute . DataTokens . ContainsKey ( "area" ) )
{
2013-02-19 05:39:38 +06:00
requestContext . RouteData . DataTokens [ "area" ] = surfaceRoute . DataTokens [ "area" ] ;
2013-02-19 07:45:01 +06:00
}
2013-02-19 07:30:46 +06: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 ) ;
2012-09-25 13:09:59 +07:00
2013-12-19 13:22:33 +11:00
}
2012-09-25 13:09:59 +07:00
2013-12-19 13:22:33 +11:00
return handler ;
}
2012-09-25 13:09:59 +07:00
2013-12-19 13:22:33 +11:00
/// <summary>
/// Returns a RouteDefinition object based on the current renderModel
/// </summary>
/// <param name="requestContext"></param>
/// <param name="publishedContentRequest"></param>
/// <returns></returns>
internal virtual RouteDefinition GetUmbracoRouteDefinition ( RequestContext requestContext , PublishedContentRequest publishedContentRequest )
{
if ( requestContext = = null ) throw new ArgumentNullException ( "requestContext" ) ;
if ( publishedContentRequest = = null ) throw new ArgumentNullException ( "publishedContentRequest" ) ;
2013-09-17 11:53:57 +10:00
2013-12-19 13:22:33 +11:00
var defaultControllerType = DefaultRenderMvcControllerResolver . Current . GetDefaultControllerType ( ) ;
2013-04-28 16:09:24 -10:00
var defaultControllerName = ControllerExtensions . GetControllerName ( defaultControllerType ) ;
2013-12-19 13:22:33 +11:00
//creates the default route definition which maps to the 'UmbracoController' controller
var def = new RouteDefinition
{
ControllerName = defaultControllerName ,
2013-04-28 16:09:24 -10:00
ControllerType = defaultControllerType ,
2013-12-19 13:22:33 +11:00
PublishedContentRequest = publishedContentRequest ,
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-12-19 13:22:33 +11:00
//check if there's a custom controller assigned, base on the document type alias.
2013-02-20 06:30:06 +06:00
var controllerType = _controllerFactory . GetControllerTypeInternal ( requestContext , publishedContentRequest . PublishedContent . DocumentTypeAlias ) ;
2013-04-28 16:09:24 -10:00
2013-12-19 13:22:33 +11:00
//check if that controller exists
if ( controllerType ! = null )
{
2013-06-25 09:33:48 +10:00
//ensure the controller is of type 'IRenderMvcController' and ControllerBase
2013-06-25 09:55:45 +10:00
if ( TypeHelper . IsTypeAssignableFrom < IRenderMvcController > ( controllerType )
& & TypeHelper . IsTypeAssignableFrom < ControllerBase > ( controllerType ) )
2013-12-19 13:22:33 +11:00
{
//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 implement '{2}' and inherit from '{3}'." ,
( ) = > publishedContentRequest . PublishedContent . DocumentTypeAlias ,
( ) = > controllerType . FullName ,
2013-06-25 09:33:48 +10:00
( ) = > typeof ( IRenderMvcController ) . FullName ,
( ) = > typeof ( ControllerBase ) . FullName ) ;
2013-12-19 13:22:33 +11:00
2013-09-11 12:15:29 +10:00
//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.
2013-12-19 13:22:33 +11:00
}
}
2013-02-16 16:00:28 -01:00
2013-02-19 20:04:50 +06:00
//store the route definition
requestContext . RouteData . DataTokens [ "umbraco-route-def" ] = def ;
2013-12-19 13:22:33 +11:00
return def ;
}
2012-08-07 21:40:34 +06:00
2013-12-19 13:22:33 +11:00
internal IHttpHandler GetHandlerOnMissingTemplate ( PublishedContentRequest pcr )
{
if ( pcr = = null ) throw new ArgumentNullException ( "pcr" ) ;
2013-09-17 11:53:57 +10:00
2013-12-19 13:22:33 +11:00
// missing template, so we're in a 404 here
2013-01-28 18:36:58 -01:00
// so the content, if any, is a custom 404 page of some sort
2013-12-19 13:22:33 +11:00
if ( ! pcr . HasPublishedContent )
// means the builder could not find a proper document to handle 404
return new PublishedContentNotFoundHandler ( ) ;
2013-01-18 13:13:06 -01:00
2013-12-19 13:22:33 +11:00
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-18 13:13:06 -01:00
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 ?
2014-02-13 13:19:51 +11:00
return GetWebFormsHandler ( ) ;
if ( pcr . RenderingEngine ! = RenderingEngine . Mvc ) // else ?
2013-01-28 18:36:58 -01:00
return new PublishedContentNotFoundHandler ( "In addition, no rendering engine exists to render the custom 404." ) ;
2013-01-18 13:13:06 -01:00
2013-12-19 13:22:33 +11:00
return null ;
}
/// <summary>
/// this will determine the controller and set the values in the route data
/// </summary>
/// <param name="requestContext"></param>
/// <param name="publishedContentRequest"></param>
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 ) ;
}
2014-02-13 13:19:51 +11:00
//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 ( ) ;
}
2013-12-19 13:22:33 +11: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.
if ( ! publishedContentRequest . HasTemplate & & ! routeDef . HasHijackedRoute )
{
publishedContentRequest . UpdateOnMissingTemplate ( ) ; // will go 404
2013-02-26 16:52:42 -01:00
// HandleHttpResponseStatus returns a value indicating that the request should
// not be processed any further, eg because it has been redirect. then, exit.
2013-12-19 13:22:33 +11:00
if ( UmbracoModule . HandleHttpResponseStatus ( requestContext . HttpContext , publishedContentRequest ) )
return null ;
2013-02-26 16:52:42 -01:00
var handler = GetHandlerOnMissingTemplate ( publishedContentRequest ) ;
2012-10-29 12:35:39 -01:00
2013-12-19 13:22:33 +11: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)
2012-10-29 12:35:39 -01:00
2013-12-19 13:22:33 +11:00
// if it's null it means that a document was found and its template is Mvc
2012-10-29 12:35:39 -01:00
2013-12-19 13:22:33 +11:00
// if we have a handler, return now
if ( handler ! = null )
return handler ;
2012-10-29 12:35:39 -01:00
2013-12-19 13:22:33 +11:00
// else we are running Mvc
2013-05-03 20:09:14 -02:00
// update the route data - because the PublishedContent has changed
UpdateRouteDataForRequest (
new RenderModel ( publishedContentRequest . PublishedContent , publishedContentRequest . Culture ) ,
requestContext ) ;
// update the route definition
2013-12-19 13:22:33 +11:00
routeDef = GetUmbracoRouteDefinition ( requestContext , publishedContentRequest ) ;
}
2012-09-07 08:28:18 +07:00
2013-12-19 13:22:33 +11:00
//no post values, just route to the controller/action requried (local)
2012-08-07 21:40:34 +06:00
2013-12-19 13:22:33 +11:00
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 ) ) ;
2013-12-19 13:22:33 +11: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-10-26 02:55:57 +05:00
2013-02-16 16:00:28 -01:00
return new UmbracoMvcHandler ( requestContext ) ;
2013-12-19 13:22:33 +11:00
}
2013-02-16 16:00:28 -01:00
2014-02-13 13:19:51 +11:00
/// <summary>
/// Returns the handler for webforms requests
/// </summary>
/// <returns></returns>
internal static IHttpHandler GetWebFormsHandler ( )
{
return ( global :: umbraco . UmbracoDefault ) BuildManager . CreateInstanceFromVirtualPath ( "~/default.aspx" , typeof ( global :: umbraco . UmbracoDefault ) ) ;
}
2013-02-16 16:00:28 -01:00
private SessionStateBehavior GetSessionStateBehavior ( RequestContext requestContext , string controllerName )
{
return _controllerFactory . GetControllerSessionBehavior ( requestContext , controllerName ) ;
}
2014-02-13 13:19:51 +11:00
2013-12-19 13:22:33 +11:00
}
2012-08-07 21:40:34 +06:00
}