I'm Tom Jaeschke and you may contact me at tomjaeschke@tomjaeschke.com or (512) 419-8788.

Sunday, April 4, 2010

of both CMS routing and forcing browsers to close

This is a piece on ASP.NET MVC routing which strays into closing browsers with JavaScript. Enjoy!



At Headspring, I recently was asked to build a web site for a client of Launch Marketing which needed to have some minimal CMS functionality. Specifically, administrators needed to be able to add their own pages and populate the pages with content. There was not the need for a multi-tiered permissions system for various user types or the need to allow for CSS re-skinning. It seemed that I could write something simple myself in lieu of repurposing a different, feature-heavy CMS package and I did.



I wanted to give administrators the ability to create their own URLs for pages that were up to three "folders" deep and thus a RESTful approach to routing in which URLs looked like this:



http://www.example.com/page/33/



...was forgone in the name of allowing URLs to appear as such:



http://www.example.com/services/consulting/onsite/



My Global.asax.cs file thus is crafted like so:






using System.Web.Mvc;
using System.Web.Routing;

namespace AdministrableApplication
{
// Note: For instructions on enabling IIS6 or IIS7 classic mode,
// visit http://go.microsoft.com/?LinkId=9394801

public class MvcApplication : System.Web.HttpApplication
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");



routes.MapRoute(
"Default", // Route name
"{id1}/{id2}/{id3}/{id4}", // URL with parameters
new { controller = "Home", action = "Index", id1 = "", id2 = "", id3 = "", id4 = "" } // Parameter defaults
);

}

protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
}
}
}




 

Regardless of the URL path, one is routed to one action at one controller. Every piece of the route is a variable. The universal action then decides what to do based upon what it is given. There is thus some branching logic in HomeController.cs which I could stand to refine into a switch/case approach. It is:






using System.Web;
using System.Web.Mvc;
using AdministrableApplication.Core;
using AdministrableApplication.Core.interfaces;
using AdministrableApplication.Objects;
using StructureMap;

namespace AdministrableApplication.Controllers
{
[HandleError]
public class HomeController : Controller
{
public Settings settings;

[ValidateInput(false)]
public ActionResult Index(string id1, string id2, string id3, string id4, FormCollection values)
{
//home
ISettingsRepository settingsRepository = ObjectFactory.GetInstance();
settings = settingsRepository.retrieveSettings();
if (id1 == "") return View();
if (id1 == "favicon.ico") return View();
 

 

 

//administration verification
if (id1.ToLower() == "json")
{
return Json(new {isAdmin = isVerifiedAdministrator()});
}
 

 

 

//administration logout
if (id1.ToLower() == "logout")
{
logout();
return View("Logout");
}
 

 

 

//administration login
if (id1.ToLower() == "login")
{
if (!isVerifiedAdministrator())
{
if (values["accessAttempt"] != null)
{
if (!mayLogin(values["accessAttempt"] as string))
{
return View("Login");
} else {
Response.Redirect("/administration/");
}
} else {
return View("Login");
}
} else {
Response.Redirect("/administration/");
}
}
 

 

 

//administration
ViewData["SubmissionErrors"] = "";
IPageRepository pageRepository = ObjectFactory.GetInstance();
Page[] pages = pageRepository.retrieveAllPages();
ViewData["Pages"] = pages;
string formSubmission = values["formSubmission"] as string;
if (id1.ToLower() == "administration")
{
if (!isVerifiedAdministrator())
{
Response.Redirect("/login/");
}
if (id2 == "" id2 == "favicon.ico")
{
if (formSubmission == "administration")
{
ViewData["SubmissionErrors"] = FormProcessor.AttemptAndReportAnyErrors(values);
pages = pageRepository.retrieveAllPages();
ViewData["Pages"] = pages;
}
if (ViewData["SubmissionErrors"] as string == "The password has changed. ") return View("Logout");
return View("Administration");
}
 

 

 

//administration of pages
Page page = pageRepository.retrievePage(id2.ToLower(), id3.ToLower(), id4.ToLower());
if (page == null) return View("Administration");
ViewData["Page"] = page;
IDetailsRepository detailsRepository = ObjectFactory.GetInstance();
Details details = detailsRepository.retrieveDetails(page.pageId);
ViewData["Details"] = details;
if (formSubmission == "administration")
{
ViewData["SubmissionErrors"] = FormProcessor.AttemptAndReportAnyErrors(values);
details = detailsRepository.retrieveDetails(page.pageId);
ViewData["Details"] = details;
}
return View("Proxy");
 

 

 

//pages
} else {
Page page = pageRepository.retrievePage(id1.ToLower(), id2.ToLower(), id3.ToLower());
if (page == null) return View();
ViewData["Page"] = page;
IDetailsRepository detailsRepository = ObjectFactory.GetInstance();
Details details = detailsRepository.retrieveDetails(page.pageId);
ViewData["Details"] = details;
ViewData["SubmissionSuccess"] = false;
if (formSubmission == "communication")
{
string errorMessage = FormProcessor.AttemptAndReportAnyErrors(values);
ViewData["SubmissionErrors"] = errorMessage;
if (errorMessage == "") Response.Redirect(values["formRedirectsTo"] as string);
}
return View("Page");
}
}
 

 

 

public bool isVerifiedAdministrator()
{
bool isAuthenticated = false;
HttpCookie applicationPassword = Request.Cookies["passwordCookie"];
if (applicationPassword != null)
{
if (EncryptionEnactor.Garble(applicationPassword.Value) == settings.globalPassword)
{
isAuthenticated = true;
}
}
return isAuthenticated;
}
 

 

 

public void logout()
{
HttpCookie cookie = new HttpCookie("passwordCookie", "");
IClockRepository clockRepository = ObjectFactory.GetInstance();
cookie.Expires = clockRepository.retrieveClock().whenCookiesShouldExpire;
Response.Cookies.Add(cookie);
}
 

 

 

public bool mayLogin(string potentialPassword)
{
bool isAuthenticated = false;
if (EncryptionEnactor.Garble(potentialPassword) == settings.globalPassword)
{
isAuthenticated = true;
HttpCookie cookie = new HttpCookie("passwordCookie", potentialPassword);
IClockRepository clockRepository = ObjectFactory.GetInstance();
cookie.Expires = clockRepository.retrieveClock().whenCookiesShouldExpire;
Response.Cookies.Add(cookie);
}
return isAuthenticated;
}
}
}




 

The first piece of the route will take the user to a "home page" if left blank or will do something special if it is given as json, logout, login, or administration. Otherwise, we end up at a line of code reading:



return View("Page");



Cool. But, how do we know what content to populate the Page view with? Well, thirteen lines prior is:



Page page = pageRepository.retrievePage(id1.ToLower(), id2.ToLower(), id3.ToLower());



The pieces of the route are thus used to try to match against parameters for a Page object that has persistence to a database. (This posting has a little bit more on the database end of things: When to Alphabetize)



When one goes to http://www.example.com/services/consulting/onsite/ one will get content for /services/consulting/onsite/



What is more, when administering the site, an administrator may go to http://www.example.com/administration/services/consulting/onsite/ to edit the content that appears at http://www.example.com/services/consulting/onsite/



Yeah!



And now, to switch topics: As I've already given the code for HomeController.cs and as it obviously has a lot of cookie-based login/logout functionality within it I thought I might as well touch on a hurdle I faced herein.



I take administrators out of administration by taking them to http://www.example.com/logout/ which first alters their cookies and then returns a view that has this in it:






<@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
<title>Exit</title>
</asp:Content>

<asp:content id="Content2" runat="server" contentplaceholderid="MainContent">
<script type="text/javascript">
$(document).ready(function() {
window.open('', '_self', '');
window.close();
window.location.replace("/");
});
</script>
</asp:content>




 

Note the three lines of JavaScript. The first two will close browsers and the third, a redirect to the "home page," should, in theory, never be run given the line preceding it.



Why do I need to close the browsers? Why can't I leave the web site up after altering the cookie that keeps a user logged in? In Internet Explorer, at least within Cassini testing, I found that I could alter the cookie that is used to validate an administrator to contain no copy for password matching and then nonetheless access things that only administrators should be able to see by way of the URL line due to page caching. I couldn't make changes as an administrator as the emptiness in the cookie would be noticed upon a post, but I could still view things that I shouldn't have been allowed to view.



Forcing closed Internet Explorer closed this loophole.



What about the line of code that takes a user to the "home page" and should in theory never be run? This is for Firefox which both did not seem to have the caching issue that Internet Explorer did and did not want to close. I've read numerous blog postings on how to force shut Firefox, but nothing I read wanted to work for me so I had to create an exception to the rule for Firefox.



Note that I am trying to close browsers without a dialog box popping up saying: "The Web page you are viewing is trying to close the window. Do you want to close this window?" I can't seem to escape the dialog box in Internet Explorer 6, but then I also haven't tried too hard to support IE6. (It's dead.)

No comments: