Pretty URL's SES Support

ColdBox now supports SES/Pretty URL's support right out of the box thanks to Adam Fortuna's Coldcourse project. ColdBox SES support is based on coldcourse and its rails style routing but taken to an interceptor form and with further enhancements. You will now see an ses interceptor in the core ColdBox interceptors. So now instead of going to urls like:

http://localhost/index.cfm?event=home.about

You can have the same URL available at (Which is our preferred way)

http://localhost/home/about

Benefits

There are several benefits that you will get by using our routing system:

  • Complete control of how URL's are built and maintained
  • Ability to create or build url's dynamically
  • Internal implementation encapsulations and technology hiding
  • Greater application portability
  • URL's are more descriptive and easier to remember

Requirements

There are no requirements for using the ses interceptor if you will be using the index.cfm file as a routing mechanism:

http://localhost/index.cfm/home/about

However, if you would like to see minimal url's (those without index.cfm in the URL) like:

http://localhost/home/about

Then you must be able to run either apache mod_rewrite .htaccess files, which are enabled with apache, or be running IIS and be able to install an ISAPI filter for URL rewriting. If you have any of these rewrite engines installed then you can use the following rules. Please note that these rules are very basic and might not work on ALL rewrite engines. So as Always, test your rules and expand them.

.htaccess

RewriteEngine on
#RepeatLimit 0

#SQL Injection Protection --Read More www.cybercrime.gov
#Please uncomment to use these rules if below words does not conflict with your friendly-urls. You may modify accordingly. 
#RewriteRule ^.*EXEC\(@.*$       /includes/templates/404.html [L,F,NC]
#RewriteRule ^.*CAST\(.*$        /includes/templates/404.html [L,F,NC] 
#RewriteRule ^.*DECLARE.*$       /includes/templates/404.html [L,F,NC]  
#RewriteRule ^.*DECLARE%20.*$    /includes/templates/404.html [L,F,NC]
#RewriteRule ^.*NVARCHAR.*$      /includes/templates/404.html [L,F,NC]  
#RewriteRule ^.*sp_password.*$   /includes/templates/404.html [L,F,NC]
#RewriteRule ^.*%20xp_.*$        /includes/templates/404.html [L,F,NC]

#if this call related to adminstrators or non rewrite folders, you can add more here.
RewriteCond %{REQUEST_URI} ^/(.*(CFIDE|cfide|CFFormGateway|jrunscripts|railo-context|fckeditor)).*$
RewriteRule ^(.*)$ - [NC,L]

#dealing with flash / flex communication
RewriteCond %{REQUEST_URI} ^/(.*(flashservices|flex2gateway|flex-remoting)).*$
RewriteRule ^(.*)$ - [NC,L]

#Images, css, javascript and docs, add your own extensions if needed.
RewriteCond %{REQUEST_URI} \.(bmp|gif|jpe?g|png|css|js|txt|pdf|doc|xls|xml)$
RewriteRule ^(.*)$ - [NC,L]

#The ColdBox index.cfm/{path_info} rules.
RewriteRule ^$ index.cfm [QSA]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.cfm/%{REQUEST_URI} [QSA,L]

IsapiRewrite.ini

IterationLimit 0
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(/.+/.+/.*\?.+\..*)$ /index.cfm/$1
RewriteRule ^(/[^.]*)$ /index.cfm/$1

Once either of those rules are applied, the rewrite engine will correctly route to the ses interceptor.

Important: Not all rewrite engines are the same, so the rules shown above might need to be tweaked slightly according to which engine you use. The .htaccess rules are pretty much the same across the board, but the Windows rewrites can be tricky.

Some Resources

Configuring Your Application For SES Support

Let's start off by setting up SES support in our application. So just open your coldbox configuration file and look for the interceptors elements. If not found, create it like so:

<Interceptors>
  <Interceptor class="coldbox.system.interceptors.ses">
   <Property name="configFile">config/routes.cfm</Property>
  </Interceptor>
</Interceptors>

That interceptor declaration tells coldbox to use the ses interceptor and look for a configuration file in the config folder called routes.cfm. You can name your routes configuration file anything you like.

Note: Your configuration file must be reachable from your application root, as it will be included into the ses interceptor for configuration.

Loose Matching Property

You can configure the interceptor to do loose matching from the incoming url's by using the LooseMatching property in your configuration file, like we just declared above.

<Interceptors>
  <Interceptor class="coldbox.system.interceptors.ses">
   <Property name="configFile">config/routes.cfm</Property>
   <Property name="LooseMatching">true</Property>
  </Interceptor>
</Interceptors>

The default value is false. What this directive does, is that it will try to find the routes ANYWHERE in the incoming url instead of starting from the beginning of the url. This can be useful, when you can have loose matching and your URL ROUTE might be in the middle or the end of an incoming url.

For example, I have a route defined as /test/:id-numeric. If I have loose matching turned OFF and I receive the following url:

/index.cfm/test/1233

The route would match to it, but if the incoming url would be

/index.cfm/blog/test/12333

The route would NOT match because it starts with blog and not the route we defined. However, if I turn on Loose Matching, then the last url would match, because the route /test/:id-numeric can be found in the URL anywhere in no specific order.

The routes configuration file

Now that you have defined where the interceptor will find its routes, all you need to do is grab the sample routes.cfm from the Application Template and start modifying it.

Important Note: The routes configuration file gets executed within the interceptor, so ALL interceptor methods are available for your usage. You can use tier detection, settings, etc.

Below are the basic methods you will use for SES Routing:

setEnabled Enable/Disable routing, enabled by default
setUniqueURLS Enables SES only URL's with permanent redirects for non-ses urls. Default is true
setBaseURL The base URL to use. If using the index.cfm approach, please write it.
addCourse Method to add routes.

setEnabled

public void setEnabled(boolean enabled)

Parameters:

  • enabled

Description: Set whether the interceptor is enabled or not.

setUniqueURLS

public void setUniqueURLs(boolean uniqueURLs)

Parameters:

  • uniqueURLs

Description: This determines if non-ses urls should be routed back to the interceptor with permanent redirects. If someone goes to

http://localhost/index.cfm?event=home.main

should we redirect (301, permanantly moved) to the url:

http://localhost/home/main

This will also make sure that any trailing index pages get redirected as well, so if you go to

http://localhost/home/index

it would redirect to

http://localhost/home

In conclusion, if you would like ONLY ses enabled URL's, then set this flag to true.

setBaseURL

public void setBaseURL(string baseURL)

Parameters:

  • baseURL

Description:

Used to set the Base URL for your site and URL's when they are built. This is the main location of how ALL your URL's will be prefixed with. If you want your URLs to look like

http://localhost/handler/action

then you should set this variable to the following:

<cfset setBaseURL('http://localhost')>

For this option to work you'll need .htaccess or isapi rewrite support as described above. If you want your URLs to look more like

http://localhost/index.cfm/handler/action

then do the following:

<cfset setBaseURL('http://localhost/index.cfm')>

If you want only SSL enabled URL's, then do this:

<cfset setBaseURL('https://#cgi.http_host#')>
NOTE:The interceptor will create a new setting called: sesBaseURL with this value so it can be used by your application for any HTML base tags or relocations. The interceptor will also create the setting: htmlBaseURL so you can use in your base html tags, this is the same as the baseURL without the index.cfm if used. Else, htmlBaseURL and sesBaseURL should be the same.

addCourse

public any addCourse(string pattern, [string handler], [string action], [boolean packageResolverExempt='false'], [string matchVariables])

Parameters:

  • pattern - The pattern to match against the incoming URL
  • handler - The handler to execute if passed.
  • action - The action to assign if passed.
  • packageResolverExempt - If this is set to true, then the interceptor will not try to do handler package resolving. Else a package will always be resolved.
  • matchVariables - A string of name-value pair variables to add to the request collection when this pattern matches. This is a comma delimmitted list. Ex: spaceFound=true,missingAction=onTest

Description:

This is the meat and potatoes for enabling ses routing. You use this method to declare routes that will be dispatched by the interceptor. The syntax of these are similar to Ruby on Rails. The idea is that the number of variables in a URL will be the first indicator of which course to use. Basically, the interceptor goes through each rule in a top-down format and tries to match the incoming URL to the route. If a route matches it will try to create the appropriate event and extra variables in order to respond to a request.

Note: You can pass any named argument and value to the addCourse() method and the interceptor will create a new variable with the name of the argument in the request collection for you. All this is done by convention as long as it does not use the named arguments declared by the method: pattern,handler,action,packageResolverExempt and matchVariables. This can be used as an alternative to using the matchVariables argument.

Here's the general setup:

<cfset addCourse(pattern="handler/action/:id",
                 handler="handler_name",
                 action="action_name",
                 packageResolverExempt="false",
                 matchVariables="name-value-pair-list")>

Notice how the pattern argument has three parts. In this case if a request comes in that starts with /handler/action, such as

http://localhost/handler/action/oneOtherItem

then this course will match. The oneOtherItem variable will be set to the request collection as a variable with the name ID

<cfset rc["ID"] = "oneOtherItem"> 

This can be compared to normal event syntax:

http://localhost/index.cfm?event=handler_name.action_name&id=oneOtherItem

It's important to remember that courses are evaluated in order and go with the first one that matches. For instance, if you have a course for /:handler/:action/:username, and another course later for /:handler/:action/:id, then the second course will NEVER be run. If instead you change your first course to something more generic such as /user/:action/:username, and specify (handler="user") as a second parameter, then only URLs that start with /user will qualify for having the third argument of username, while everywhere else will use a third argument of ID. Best Practice: always try to create meaningful courses that can be distinct in order to increase the number of course permutations.

By the way, what's with the :' syntax? In the Ruby programming language, any variable starting with a : is a symbol. Symbols are just like strings but they always point to the same place in memory and are therefore more efficient. They don't work that way here in ColdFusion, but they make a good variable marker without worrying about where to put quotes and stuff. ColdBox takes this to a new level by adding some extra features.

  • :varname This is how we do URL placeholders in CodlBox SES.
  • In order to have a route with a :handler placeholder to NOT resolve directories or packages, use the packageResolverExempt argument (Set it to TRUE).

Adding variables per route

You can add variables to the request collection by using the matchVariables argument or by adding your extra name-value pairs to the method as arguments. The matchVariables argument is a simple string of name-value pairs that you can pass to the route definition. This basically tells the ses processor that if that specific route matches, then add those name-value pairs to the request collection. This is a great way to add YOUR OWN variables hidden from the URL/FORM.

Example:

<cfset addRoute(pattern="space/:space",handler="page",action="show",matchVariables="spaceUsed=true,foundAt=#now()#")>

As mentioned above, you can also add variables by just adding them as arguments to the addCourse() method. This is more by convention and it has the limitations that you cannot use the following as named arguments as they are already arguments: pattern,handler,action,packageResolverExempt and matchVariables.

Example: Let's say I want to add the following variables to my route if it is found:

  • foundAt = #now()#
  • internalNamespace = "_internal"
  • hashMap = structnew()

Then I would setup a route like so:

<cfset addRoute(pattern="page/:page",handler="page",action="show",foundAt=now(),internalNamespace="_internal",hashMap=structnew())>

Numeric Routes

ColdBox gives you also the ability to declare numeric only routes alongside the normal alphanumeric routes by appending -numeric to the variable placeholder.

Numeric Route:

/blog/:year-numeric

Alphanumeric Route :

/blog/:year

That's it. Everytime you append the -numeric tag, the placeholder will only work if its numeric.

Optional Variables

ColdBox 2.6 also introduces optional variables for your placeholders. Most of the time we need to create several routes in order to determine possible routings. A great example is the declaration of the following:

/:handler/:action/:id
/:handler/:action
/:handler

We just wrote 3 routes for this when we can just use optional variables by using the ? symbol at the end of the placeholder.

/:handler/:action?/:id?

This syntax will create the three routes for you, but you only need to declare 1. Just remember that an optional placeholder cannot be followed by a non-optional one. It doesn't make sense. So remember, just append a ? to make a placeholder optional.

/blog/:year-numeric/:month-numeric?/:day-numeric?

Convention name-value pairs

ColdBox introduces name-value pairs by convention by inspecting the incoming URL. This means that after a route has been matched and there are still values in the request string, the interceptor will try to create name-value pairs out of them. Example, if we have the following route:

addRoute(':handler/:action')

Then if we have the following url: index.cfm/users/list/page/2/issues/5 Then the interceptor would route it to the event = users.list and nothing more. With convention name-value pairs, the interceptor will try to create name-value pairs from the remaining string, in our case: page/2/issues/5. So the interceptor will create the following variables in the request collection for you, without YOU doing anything:

  • page = 2
  • issues = 5

Is this cool or what, you can easily create more custom routes and even have convention routing, all provided for free. Of course, if there are no name-value pairs, they won't be created, or if the name is missing its value, it won't be created.

Route Examples

<cfset addCourse(pattern="blog/entry/:year/:month-numeric?/:day-numeric?",
                handler="blog",
                action="entry" )>
<cfset addCourse(pattern="profile/view/:username",
                handler="profile",
                action="view" )>        
<cfset addCourse(":handler/:action?/:id?")>

The Default Routes

As you can see from the sample above, the last route is actually the default route that your application should have

<cfset addCourse(":handler/:action?/:id?")>

So by having these default routes, you can easily route to any handler-action combination, or just handler combo. Very easy and snappy to use.

Package Resolver

From the route above we know that a url can look like this: index.cfm/handler/action, but what if I want to execute a handler within a package? Well, there are mostly two approaches to this:

  1. Use the built-in package resolver (Automatically done for you)
  2. Create specific routes to your handler

The first approach is done automatically for you by just using the default route of :handler/:action?/:id?. When ColdBox detects the incoming URL and this route is matched, it will try to expand the placeholder for the handler and see if it is a directory or not. If it is a directory, it will save it and continue parsing the URL until a handler is matched. Mumbo Jumbo, give me samples. Ok Ok, here you go.

Handler Structure

+handlers
  + admin
     + user.cfc
  + general.cfc

In previous 2.6.2 versions, a URL to a list action for the admin user controller would look like this:

index.cfm/admin.user/list

This works great, but it is not that user friendly and I think we could do better. Therefore, since version 2.6.2, ColdBox can resolve packages. So the URL now looks like this:

index.cfm/admin/user/list

WOW!! That does look better and achieves better SEO than the other one. So there you go, you can add as many packages until you reach your handler. So what about the second way you mentioned about creating specific routes. Well, for this to work we must add the route ourselves:

<cfset addRoute(pattern="/admin/user/:action?",handler="admin.user")>

As you can see from the pattern above, I created a specific route of admin/user/ and this becomes the alias for the admin.user handler. I could have easily renamed the alias but my handler is hidden from the URL. So the question is, which one should I use? Well, the best practice is to always hide implementation, so I would go with the latter solution as it provides a better way to refactor the application without changing url's.

How do I relocate?

The next important part of your paths into SES is that you need to relocate to other events and also create links. So how do I do this? By using the following two methods:

  • setNextEvent([string event], [string queryString], [boolean addToken], [string persist], [struct varStruct], [boolean ssl],[string baseURL])
  • setNextRoute(string route, [string persist], [struct varStruct],[boolean addToken],[boolean ssl])

setNextEvent

The setNextEvent method can be used for both normal and ses urls, here are its parameters:

Argument Required Type Description
event false string The event to relocate to, if empty it defaults to the default event. ex: main.home
queryString false string The query string to append to the relocation
addToken false boolean Whether to add the cf tokens or not. Default is false
persist false string(list) A comma-delimited list of request collection key names that will be flash persisted in the framework's flash RAM and re-inflated in the next request.
varStruct false struct A structure of key-value pairs that will be flash persisted in the framework's flash RAM and re-inflated in the next request.
ssl false boolean(false) Flag indicating if redirect should be done in ssl mode or not
baseURL false string If used, then it is the base url for normal syntax redirection instead of just redirecting to the index.cfm

All the arguments above are pretty straightforward except: persist & varStruct. Persist can be a comma-delimited list of request collection' key names that you want to persist in the internal flash memory of the framework for the relocation.varStruct is a structure of key-value pairs that will be persisted across a request. So when the user get's relocated, those variables (can be simple, complex, or even objects) will be flash stored and re-inflated back to the request collection on the relocation. Very useful to silently keep state on temporary objects.

Important: Please note that the persist argument refers to items ALREADY in the request collection.

setNextRoute

The setNextRoute method can be used for ses urls ONLY and it can use the following arguments:

Argument Required Type Description
route true string The route to relocate to. ex: main/home
persist false string(list) A comma-delimited list of request collection key names to persist in the relocation
varStruct false struct A structure of key-value pairs that will be flash persisted in the framework's flash RAM and re-inflated in the next request.
addToken false boolean(false) Flag to add the CF tokens to the redirection
ssl false boolean(false) Flag indicating if redirect should be done in ssl mode or not

Now, please note that this method is only for routing. If you want to use a method that can encompass both scenarios then use setNextEvent(). Since version 2.6.2, setNextEvent() supports both normal event syntax and route redirection. The persist and varStruct arguments apply the same as setNextEvent().

Below are some samples:

<cfset setNextRoute("handler/action")>
<cfset setNextEvent('handler.action')>

<cfset setNextRoute("home/about")>
<cfset setNextEvent('home.about')>


<cfset setNextRoute("blog/entry/223443")>
<cfset setNextEvent(event='blog.entry.223443')>

How about links on my pages?

Well, you would use the settings that the interceptor places for you in your configuration structure and write the absolute URL by hand or just use the best method in the world: event.buildLink()

<a href="#getSetting('sesBaseURL')#/home/about">About</a>
<a href="/home/about">About</a>

<!--- The best one is --->
<a href="#event.buildLink('home.about')#">About</a>

How about Form Submissions (Posts)?

As you are now using SES, the ses interceptor will parse all requests, so you have to accurately change your posting actions to the correct processor. You cannot just post it to the index with an event variable. You need to post to the correct ses action.

<form action="index.cfm/handler/action" method="post" name="testform">

All your form elements here
</form>

<!--- OR the following --->
<form action="#event.buildLink('handler.action')#" method="post" name="testform">
</form>

So as you can see from the sample, the action points to: index.cfm/handler/action if you are not using the index.cfm processor then it would be: /handler/action.

The best way to automating the writing of the links in your application is to use the event.buildLink() method that is available in the event object. This method is used to create links for you whether you are in SES mode or not. The other cool thing, is that you can keep your event notation and the method will translate it to routes for you.

  • buildLink(string linkto, [boolean translate], [boolean ssl], [string baseURL])

Arguments

argument type required default description
linkTo string true --- the event + query string combination
translate boolean false true If set to true it will translate any . to / if in SES mode. If false, it will not translate
ssl boolean false false creates the url link in ssl mode
baseURL string false --- Only applicable in non-ses mode. The base full URL path to append to index.cfm


Important: If your route contains embedded periods (.) then please remember to pass in a false to the translate argument, so they don't get translated to "/"

How about the BASE tag

Well, for the base tag you can use either the sesBaseURL setting, if using minimal URL support, or the htmlBaseURL setting if using the index.cfm in the URL. Both of these settings are set by the interceptor at configuration time and available for your usage via the getSetting() method.

<base href="#getSetting('htmlBaseURL')#">
<base href="#getSetting('sesBaseURL')#">

Conclusion

As you can see, SES support in ColdBox is a breeze. Very easy to configure and use:

  1. Declare your interceptor
  2. Create your routes configuration file and create your routes
  3. Code away with the new setNextRoute() method and your two new settings: sesBaseURL & htmlBaseURL