Fixes: U4-541 Wrong dictionary key when using in backend template names This changes the way that the value that is stored in the auth cookie. Previously we just stored a GUID which was the user's contextid stored in the db, now we store encrypted values of a few necessary user objects. In 6.2 we'll actually set a real .Net user object on the HttpContext. For now, the http module will simply just ensure that the culture is set correctly for the currently logged in user.
554 lines
22 KiB
C#
554 lines
22 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Web;
|
|
using System.Web.Routing;
|
|
using Umbraco.Core;
|
|
using Umbraco.Core.Cache;
|
|
using Umbraco.Core.IO;
|
|
using Umbraco.Core.Logging;
|
|
using Umbraco.Core.Security;
|
|
using Umbraco.Web.Routing;
|
|
using umbraco;
|
|
using umbraco.BasePages;
|
|
using GlobalSettings = Umbraco.Core.Configuration.GlobalSettings;
|
|
using UmbracoSettings = Umbraco.Core.Configuration.UmbracoSettings;
|
|
using Umbraco.Web.Configuration;
|
|
|
|
namespace Umbraco.Web
|
|
{
|
|
// also look at IOHelper.ResolveUrlsFromTextString - nightmarish?!
|
|
|
|
// context.RewritePath supports ~/ or else must begin with /vdir
|
|
// Request.RawUrl is still there
|
|
// response.Redirect does?! always remap to /vdir?!
|
|
|
|
public class UmbracoModule : IHttpModule
|
|
{
|
|
#region HttpModule event handlers
|
|
|
|
/// <summary>
|
|
/// Begins to process a request.
|
|
/// </summary>
|
|
/// <param name="httpContext"></param>
|
|
static void BeginRequest(HttpContextBase httpContext)
|
|
{
|
|
//we need to set the initial url in our ApplicationContext, this is so our keep alive service works and this must
|
|
//exist on a global context because the keep alive service doesn't run in a web context.
|
|
//we are NOT going to put a lock on this because locking will slow down the application and we don't really care
|
|
//if two threads write to this at the exact same time during first page hit.
|
|
//see: http://issues.umbraco.org/issue/U4-2059
|
|
if (ApplicationContext.Current.OriginalRequestUrl.IsNullOrWhiteSpace())
|
|
{
|
|
ApplicationContext.Current.OriginalRequestUrl = string.Format("{0}:{1}{2}", httpContext.Request.ServerVariables["SERVER_NAME"], httpContext.Request.ServerVariables["SERVER_PORT"], IOHelper.ResolveUrl(SystemDirectories.Umbraco));
|
|
}
|
|
|
|
// do not process if client-side request
|
|
if (httpContext.Request.Url.IsClientSideRequest())
|
|
return;
|
|
|
|
//write the trace output for diagnostics at the end of the request
|
|
httpContext.Trace.Write("UmbracoModule", "Umbraco request begins");
|
|
|
|
// ok, process
|
|
|
|
// create the LegacyRequestInitializer
|
|
// and initialize legacy stuff
|
|
var legacyRequestInitializer = new LegacyRequestInitializer(httpContext.Request.Url, httpContext);
|
|
legacyRequestInitializer.InitializeRequest();
|
|
|
|
// create the UmbracoContext singleton, one per request, and assign
|
|
// NOTE: we assign 'true' to ensure the context is replaced if it is already set (i.e. during app startup)
|
|
UmbracoContext.EnsureContext(httpContext, ApplicationContext.Current, true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Processses the Umbraco Request
|
|
/// </summary>
|
|
/// <param name="httpContext"></param>
|
|
void ProcessRequest(HttpContextBase httpContext)
|
|
{
|
|
// do not process if client-side request
|
|
if (httpContext.Request.Url.IsClientSideRequest())
|
|
return;
|
|
|
|
if (UmbracoContext.Current == null)
|
|
throw new InvalidOperationException("The UmbracoContext.Current is null, ProcessRequest cannot proceed unless there is a current UmbracoContext");
|
|
if (UmbracoContext.Current.RoutingContext == null)
|
|
throw new InvalidOperationException("The UmbracoContext.RoutingContext has not been assigned, ProcessRequest cannot proceed unless there is a RoutingContext assigned to the UmbracoContext");
|
|
|
|
var umbracoContext = UmbracoContext.Current;
|
|
|
|
// do not process but remap to handler if it is a base rest request
|
|
if (BaseRest.BaseRestHandler.IsBaseRestRequest(umbracoContext.OriginalRequestUrl))
|
|
{
|
|
httpContext.RemapHandler(new BaseRest.BaseRestHandler());
|
|
return;
|
|
}
|
|
|
|
// do not process if this request is not a front-end routable page
|
|
var isRoutableAttempt = EnsureUmbracoRoutablePage(umbracoContext, httpContext);
|
|
//raise event here
|
|
OnRouteAttempt(new RoutableAttemptEventArgs(isRoutableAttempt.Result, umbracoContext, httpContext));
|
|
if (!isRoutableAttempt.Success)
|
|
{
|
|
return;
|
|
}
|
|
|
|
|
|
httpContext.Trace.Write("UmbracoModule", "Umbraco request confirmed");
|
|
|
|
// ok, process
|
|
|
|
// note: requestModule.UmbracoRewrite also did some stripping of &umbPage
|
|
// from the querystring... that was in v3.x to fix some issues with pre-forms
|
|
// auth. Paul Sterling confirmed in jan. 2013 that we can get rid of it.
|
|
|
|
// instanciate, prepare and process the published content request
|
|
// important to use CleanedUmbracoUrl - lowercase path-only version of the current url
|
|
var pcr = new PublishedContentRequest(umbracoContext.CleanedUmbracoUrl, umbracoContext.RoutingContext);
|
|
umbracoContext.PublishedContentRequest = pcr;
|
|
pcr.Prepare();
|
|
|
|
// HandleHttpResponseStatus returns a value indicating that the request should
|
|
// not be processed any further, eg because it has been redirect. then, exit.
|
|
if (HandleHttpResponseStatus(httpContext, pcr))
|
|
return;
|
|
|
|
if (!pcr.HasPublishedContent)
|
|
httpContext.RemapHandler(new PublishedContentNotFoundHandler());
|
|
else
|
|
RewriteToUmbracoHandler(httpContext, pcr);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if the request is authenticated, if it is it sets the thread culture to the currently logged in user
|
|
/// </summary>
|
|
/// <param name="sender"></param>
|
|
/// <param name="e"></param>
|
|
static void AuthenticateRequest(object sender, EventArgs e)
|
|
{
|
|
var app = (HttpApplication)sender;
|
|
var http = new HttpContextWrapper(app.Context);
|
|
|
|
// do not process if client-side request
|
|
if (http.Request.Url.IsClientSideRequest())
|
|
return;
|
|
|
|
if (app.Request.Url.IsBackOfficeRequest() || app.Request.Url.IsInstallerRequest())
|
|
{
|
|
var ticket = http.GetUmbracoAuthTicket();
|
|
if (ticket != null)
|
|
{
|
|
//create the Umbraco user identity
|
|
var identity = ticket.CreateUmbracoIdentity();
|
|
if (identity != null)
|
|
{
|
|
|
|
//We'll leave setting custom identies/principals for 6.2, for now we'll just ensure that the cultures, etc.. are set
|
|
////set the principal object
|
|
////now we need to see if their session is still valid
|
|
//var timeout = BasePage.GetTimeout(identity.UserContextId);
|
|
//if (timeout > DateTime.Now.Ticks)
|
|
//{
|
|
//var principal = new GenericPrincipal(identity, identity.Roles);
|
|
////It is actually not good enough to set this on the current app Context and the thread, it also needs
|
|
//// to be set explicitly on the HttpContext.Current !! This is a strange web api thing that is actually
|
|
//// an underlying fault of asp.net not propogating the User correctly.
|
|
//if (HttpContext.Current != null)
|
|
//{
|
|
// HttpContext.Current.User = principal;
|
|
//}
|
|
//app.Context.User = principal;
|
|
//Thread.CurrentPrincipal = principal;
|
|
//}
|
|
|
|
//This is a back office/installer request, we will also set the culture/ui culture
|
|
Thread.CurrentThread.CurrentCulture =
|
|
Thread.CurrentThread.CurrentUICulture =
|
|
new System.Globalization.CultureInfo(identity.Culture);
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// returns a value indicating whether redirection took place and the request has
|
|
// been completed - because we don't want to Response.End() here to terminate
|
|
// everything properly.
|
|
internal static bool HandleHttpResponseStatus(HttpContextBase context, PublishedContentRequest pcr)
|
|
{
|
|
var end = false;
|
|
var response = context.Response;
|
|
|
|
LogHelper.Debug<UmbracoModule>("Response status: Redirect={0}, Is404={1}, StatusCode={2}",
|
|
() => pcr.IsRedirect ? (pcr.IsRedirectPermanent ? "permanent" : "redirect") : "none",
|
|
() => pcr.Is404 ? "true" : "false", () => pcr.ResponseStatusCode);
|
|
|
|
if (pcr.IsRedirect)
|
|
{
|
|
if (pcr.IsRedirectPermanent)
|
|
response.RedirectPermanent(pcr.RedirectUrl, false); // do not end response
|
|
else
|
|
response.Redirect(pcr.RedirectUrl, false); // do not end response
|
|
end = true;
|
|
}
|
|
else if (pcr.Is404)
|
|
{
|
|
response.StatusCode = 404;
|
|
response.TrySkipIisCustomErrors = UmbracoSettings.For<WebRouting>().TrySkipIisCustomErrors;
|
|
}
|
|
|
|
if (pcr.ResponseStatusCode > 0)
|
|
{
|
|
// set status code -- even for redirects
|
|
response.StatusCode = pcr.ResponseStatusCode;
|
|
response.StatusDescription = pcr.ResponseStatusDescription;
|
|
}
|
|
//if (pcr.IsRedirect)
|
|
// response.End(); // end response -- kills the thread and does not return!
|
|
|
|
if (pcr.IsRedirect)
|
|
{
|
|
response.Flush();
|
|
// bypass everything and directly execute EndRequest event -- but returns
|
|
context.ApplicationInstance.CompleteRequest();
|
|
// though some say that .CompleteRequest() does not properly shutdown the response
|
|
// and the request will hang until the whole code has run... would need to test?
|
|
LogHelper.Debug<UmbracoModule>("Response status: redirecting, complete request now.");
|
|
}
|
|
|
|
return end;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Route helper methods
|
|
|
|
/// <summary>
|
|
/// Checks the current request and ensures that it is routable based on the structure of the request and URI
|
|
/// </summary>
|
|
/// <param name="context"></param>
|
|
/// <param name="httpContext"></param>
|
|
/// <returns></returns>
|
|
internal Attempt<EnsureRoutableOutcome> EnsureUmbracoRoutablePage(UmbracoContext context, HttpContextBase httpContext)
|
|
{
|
|
var uri = context.OriginalRequestUrl;
|
|
|
|
var reason = EnsureRoutableOutcome.IsRoutable;
|
|
|
|
// ensure this is a document request
|
|
if (!EnsureDocumentRequest(httpContext, uri))
|
|
{
|
|
reason = EnsureRoutableOutcome.NotDocumentRequest;
|
|
}
|
|
// ensure Umbraco is ready to serve documents
|
|
else if (!EnsureIsReady(httpContext, uri))
|
|
{
|
|
reason = EnsureRoutableOutcome.NotReady;
|
|
}
|
|
// ensure Umbraco is properly configured to serve documents
|
|
else if (!EnsureIsConfigured(httpContext, uri))
|
|
{
|
|
reason = EnsureRoutableOutcome.NotConfigured;
|
|
}
|
|
// ensure Umbraco has documents to serve
|
|
else if (!EnsureHasContent(context, httpContext))
|
|
{
|
|
reason = EnsureRoutableOutcome.NoContent;
|
|
}
|
|
|
|
return new Attempt<EnsureRoutableOutcome>(reason == EnsureRoutableOutcome.IsRoutable, reason);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ensures that the request is a document request (i.e. one that the module should handle)
|
|
/// </summary>
|
|
/// <param name="httpContext"></param>
|
|
/// <param name="uri"></param>
|
|
/// <returns></returns>
|
|
bool EnsureDocumentRequest(HttpContextBase httpContext, Uri uri)
|
|
{
|
|
var maybeDoc = true;
|
|
var lpath = uri.AbsolutePath.ToLowerInvariant();
|
|
|
|
// handle directory-urls used for asmx
|
|
// legacy - what's the point really?
|
|
if (/*maybeDoc &&*/ GlobalSettings.UseDirectoryUrls)
|
|
{
|
|
int asmxPos = lpath.IndexOf(".asmx/", StringComparison.OrdinalIgnoreCase);
|
|
if (asmxPos >= 0)
|
|
{
|
|
// use uri.AbsolutePath, not path, 'cos path has been lowercased
|
|
httpContext.RewritePath(uri.AbsolutePath.Substring(0, asmxPos + 5), // filePath
|
|
uri.AbsolutePath.Substring(asmxPos + 5), // pathInfo
|
|
uri.Query.TrimStart('?'));
|
|
maybeDoc = false;
|
|
}
|
|
}
|
|
|
|
// a document request should be
|
|
// /foo/bar/nil
|
|
// /foo/bar/nil/
|
|
// /foo/bar/nil.aspx
|
|
// where /foo is not a reserved path
|
|
|
|
// if the path contains an extension that is not .aspx
|
|
// then it cannot be a document request
|
|
if (maybeDoc && lpath.Contains('.') && !lpath.EndsWith(".aspx"))
|
|
maybeDoc = false;
|
|
|
|
// at that point, either we have no extension, or it is .aspx
|
|
|
|
// if the path is reserved then it cannot be a document request
|
|
if (maybeDoc && GlobalSettings.IsReservedPathOrUrl(lpath, httpContext, RouteTable.Routes))
|
|
maybeDoc = false;
|
|
|
|
//NOTE: No need to warn, plus if we do we should log the document, as this message doesn't really tell us anything :)
|
|
//if (!maybeDoc)
|
|
//{
|
|
// LogHelper.Warn<UmbracoModule>("Not a document");
|
|
//}
|
|
|
|
return maybeDoc;
|
|
}
|
|
|
|
// ensures Umbraco is ready to handle requests
|
|
// if not, set status to 503 and transfer request, and return false
|
|
// if yes, return true
|
|
bool EnsureIsReady(HttpContextBase httpContext, Uri uri)
|
|
{
|
|
var ready = ApplicationContext.Current.IsReady;
|
|
|
|
// ensure we are ready
|
|
if (!ready)
|
|
{
|
|
LogHelper.Warn<UmbracoModule>("Umbraco is not ready");
|
|
|
|
if (!UmbracoSettings.EnableSplashWhileLoading)
|
|
{
|
|
// let requests pile up and wait for 10s then show the splash anyway
|
|
ready = ApplicationContext.Current.WaitForReady(10 * 1000);
|
|
}
|
|
|
|
if (!ready)
|
|
{
|
|
httpContext.Response.StatusCode = 503;
|
|
|
|
var bootUrl = UmbracoSettings.BootSplashPage;
|
|
if (string.IsNullOrWhiteSpace(bootUrl))
|
|
bootUrl = "~/config/splashes/booting.aspx";
|
|
httpContext.RewritePath(UriUtility.ToAbsolute(bootUrl) + "?url=" + HttpUtility.UrlEncode(uri.ToString()));
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// ensures Umbraco has at least one published node
|
|
// if not, rewrites to splash and return false
|
|
// if yes, return true
|
|
private static bool EnsureHasContent(UmbracoContext context, HttpContextBase httpContext)
|
|
{
|
|
if (context.ContentCache.HasContent())
|
|
return true;
|
|
|
|
LogHelper.Warn<UmbracoModule>("Umbraco has no content");
|
|
|
|
httpContext.Response.StatusCode = 503;
|
|
|
|
const string noContentUrl = "~/config/splashes/noNodes.aspx";
|
|
httpContext.RewritePath(UriUtility.ToAbsolute(noContentUrl));
|
|
|
|
return false;
|
|
}
|
|
|
|
// ensures Umbraco is configured
|
|
// if not, redirect to install and return false
|
|
// if yes, return true
|
|
private static bool EnsureIsConfigured(HttpContextBase httpContext, Uri uri)
|
|
{
|
|
if (ApplicationContext.Current.IsConfigured)
|
|
return true;
|
|
|
|
LogHelper.Warn<UmbracoModule>("Umbraco is not configured");
|
|
|
|
var installPath = UriUtility.ToAbsolute(Core.IO.SystemDirectories.Install);
|
|
var installUrl = string.Format("{0}/default.aspx?redir=true&url={1}", installPath, HttpUtility.UrlEncode(uri.ToString()));
|
|
httpContext.Response.Redirect(installUrl, true);
|
|
return false;
|
|
}
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Rewrites to the correct Umbraco handler, either WebForms or Mvc
|
|
/// </summary>
|
|
/// <param name="context"></param>
|
|
/// <param name="pcr"> </param>
|
|
private static void RewriteToUmbracoHandler(HttpContextBase context, PublishedContentRequest pcr)
|
|
{
|
|
// NOTE: we do not want to use TransferRequest even though many docs say it is better with IIS7, turns out this is
|
|
// not what we need. The purpose of TransferRequest is to ensure that .net processes all of the rules for the newly
|
|
// rewritten url, but this is not what we want!
|
|
// read: http://forums.iis.net/t/1146511.aspx
|
|
|
|
string query = pcr.Uri.Query.TrimStart(new[] { '?' });
|
|
|
|
string rewritePath;
|
|
|
|
if (pcr.RenderingEngine == RenderingEngine.Unknown)
|
|
{
|
|
// Unkwnown means that no template was found. Default to Mvc because Mvc supports hijacking
|
|
// routes which sometimes doesn't require a template since the developer may want full control
|
|
// over the rendering. Can't do it in WebForms, so Mvc it is. And Mvc will also handle what to
|
|
// do if no template or hijacked route is exist.
|
|
pcr.RenderingEngine = RenderingEngine.Mvc;
|
|
}
|
|
|
|
switch (pcr.RenderingEngine)
|
|
{
|
|
case RenderingEngine.Mvc:
|
|
// GlobalSettings.Path has already been through IOHelper.ResolveUrl() so it begins with / and vdir (if any)
|
|
rewritePath = GlobalSettings.Path.TrimEnd(new[] { '/' }) + "/RenderMvc";
|
|
// rewrite the path to the path of the handler (i.e. /umbraco/RenderMvc)
|
|
context.RewritePath(rewritePath, "", query, false);
|
|
|
|
//if it is MVC we need to do something special, we are not using TransferRequest as this will
|
|
//require us to rewrite the path with query strings and then reparse the query strings, this would
|
|
//also mean that we need to handle IIS 7 vs pre-IIS 7 differently. Instead we are just going to create
|
|
//an instance of the UrlRoutingModule and call it's PostResolveRequestCache method. This does:
|
|
// * Looks up the route based on the new rewritten URL
|
|
// * Creates the RequestContext with all route parameters and then executes the correct handler that matches the route
|
|
//we also cannot re-create this functionality because the setter for the HttpContext.Request.RequestContext is internal
|
|
//so really, this is pretty much the only way without using Server.TransferRequest and if we did that, we'd have to rethink
|
|
//a bunch of things!
|
|
var urlRouting = new UrlRoutingModule();
|
|
urlRouting.PostResolveRequestCache(context);
|
|
break;
|
|
|
|
case RenderingEngine.WebForms:
|
|
rewritePath = "~/default.aspx";
|
|
// rewrite the path to the path of the handler (i.e. default.aspx)
|
|
context.RewritePath(rewritePath, "", query, false);
|
|
break;
|
|
|
|
default:
|
|
throw new Exception("Invalid RenderingEngine.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if the xml cache file needs to be updated/persisted
|
|
/// </summary>
|
|
/// <param name="httpContext"></param>
|
|
/// <remarks>
|
|
/// TODO: This needs an overhaul, see the error report created here:
|
|
/// https://docs.google.com/document/d/1neGE3q3grB4lVJfgID1keWY2v9JYqf-pw75sxUUJiyo/edit
|
|
/// </remarks>
|
|
static void PersistXmlCache(HttpContextBase httpContext)
|
|
{
|
|
if (content.Instance.IsXmlQueuedForPersistenceToFile)
|
|
{
|
|
content.Instance.RemoveXmlFilePersistenceQueue();
|
|
content.Instance.PersistXmlToFile();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Any object that is in the HttpContext.Items collection that is IDisposable will get disposed on the end of the request
|
|
/// </summary>
|
|
/// <param name="http"></param>
|
|
private static void DisposeHttpContextItems(HttpContext http)
|
|
{
|
|
foreach (DictionaryEntry i in http.Items)
|
|
{
|
|
i.Value.DisposeIfDisposable();
|
|
i.Key.DisposeIfDisposable();
|
|
}
|
|
}
|
|
|
|
#region IHttpModule
|
|
|
|
/// <summary>
|
|
/// Initialize the module, this will trigger for each new application
|
|
/// and there may be more than 1 application per application domain
|
|
/// </summary>
|
|
/// <param name="app"></param>
|
|
public void Init(HttpApplication app)
|
|
{
|
|
app.BeginRequest += (sender, e) =>
|
|
{
|
|
var httpContext = ((HttpApplication)sender).Context;
|
|
httpContext.Trace.Write("UmbracoModule", "Umbraco request begins");
|
|
LogHelper.Debug<UmbracoModule>("Begin request: {0}.", () => httpContext.Request.Url);
|
|
BeginRequest(new HttpContextWrapper(httpContext));
|
|
};
|
|
|
|
app.AuthenticateRequest += AuthenticateRequest;
|
|
|
|
app.PostResolveRequestCache += (sender, e) =>
|
|
{
|
|
var httpContext = ((HttpApplication)sender).Context;
|
|
ProcessRequest(new HttpContextWrapper(httpContext));
|
|
};
|
|
|
|
// used to check if the xml cache file needs to be updated/persisted
|
|
app.PostRequestHandlerExecute += (sender, e) =>
|
|
{
|
|
var httpContext = ((HttpApplication)sender).Context;
|
|
PersistXmlCache(new HttpContextWrapper(httpContext));
|
|
};
|
|
|
|
app.EndRequest += (sender, args) =>
|
|
{
|
|
var httpContext = ((HttpApplication)sender).Context;
|
|
if (UmbracoContext.Current != null && UmbracoContext.Current.IsFrontEndUmbracoRequest)
|
|
{
|
|
//write the trace output for diagnostics at the end of the request
|
|
httpContext.Trace.Write("UmbracoModule", "Umbraco request completed");
|
|
LogHelper.Debug<UmbracoModule>("Total milliseconds for umbraco request to process: " + DateTime.Now.Subtract(UmbracoContext.Current.ObjectCreated).TotalMilliseconds);
|
|
}
|
|
|
|
DisposeHttpContextItems(httpContext);
|
|
};
|
|
|
|
//disable asp.net headers (security)
|
|
app.PreSendRequestHeaders += (sender, args) =>
|
|
{
|
|
var httpContext = ((HttpApplication)sender).Context;
|
|
try
|
|
{
|
|
httpContext.Response.Headers.Remove("Server");
|
|
//this doesn't normally work since IIS sets it but we'll keep it here anyways.
|
|
httpContext.Response.Headers.Remove("X-Powered-By");
|
|
}
|
|
catch (PlatformNotSupportedException ex)
|
|
{
|
|
// can't remove headers this way on IIS6 or cassini.
|
|
}
|
|
};
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Events
|
|
internal static event EventHandler<RoutableAttemptEventArgs> RouteAttempt;
|
|
private void OnRouteAttempt(RoutableAttemptEventArgs args)
|
|
{
|
|
if (RouteAttempt != null)
|
|
RouteAttempt(this, args);
|
|
}
|
|
#endregion
|
|
}
|
|
}
|