Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at RIA Unleashed (Nov. 2009) with:

Managing ColdFusion Sessions In A ColdFusion Builder Extension

By Ben Nadel on
Tags: ColdFusion

There's been a lot of confusion in my mind as to how exactly ColdFusion session management works in a ColdFusion Builder Extension; sometimes it appears to work and sometimes it doesn't. I've been told by some people that I should rely on the Application scope for persistence. But, at the same time, I've seen with my own eyes that it does work occassionally. At the end of the day, ColdFusion Builder Extensions are powered by ColdFusion Applications, which we all know and love; so, why is this so confusing?

This session-ambiguity exists because both points of view are actually correct - at least, some of the time. ColdFusion Builder Extensions can execute in two very different contexts. One context is a sort of XML-RPC (XML Remote Procedure Call) work flow in which XML packets are posted (by Builder) to your ColdFusion application. In this work flow, your ColdFusion application must return an XML response. That XML response may contain markup that defines a Builder form. Or, it may contain markup that defines a standard web page.

If your XML response contains Builder XML, then the XML-RPC work flow will be continued. If, however, your XML response contains HTML markup, then your ColdFusion Builder Extension window will create a browser panel to render that HTML. Once that HTML panel has been rendered, the XML-RPC work flow is terminated and any subsequent requests performed within that HTML panel execute in a standard Web HTTP request context (for lack of a better term).

 
 
 
 
 
 
ColdFusion Builder Extension Session Management Work Flows. 
 
 
 

These two different contexts - XML-RPC and Web HTTP Request - have different behaviors. From what I can see, when ColdFusion Builder is performing XML-RPC calls, there is no additional information outside of the XML posted to your ColdFusion application; this means, no cookies, which means no sessions. Once you drop down into the browser panel and start making Web HTTP requests, the browser engine (probably WebKit based on its anti-aliasing) behaves like a normal browser, posting cookies along with each request. These cookie-containing posts ensure that ColdFusion session management is persisted across requests.

Now that we understand the difference between these two work flows, the question becomes, how do we cross the divide while maintaining session? It's actually not that difficult if you understand the ColdFusion request life cycle. When ColdFusion Builder makes an XML-RPC request to your application, your application generates a new session complete with CFID and CFTOKEN values. To maintain session across the two different contexts, all we have to do is pass those existing CFID and CFTOKEN values to the new context (the Web HTTP Request work flow) where we can start to use them as session tokens in the subsequent requests.

 
 
 
 
 
 
 
 
 
 

If you're going to be entering a web request context at any point in your ColdFusion Builder Extension execution, you're going to have one request that is a mish-mash of the two concepts - an XML-RPC response that contains HTML markup. To me, this feels like a very uncomfortable limbo and I try to skip over it as quickly as possible. To do so, the HTML contained within my XML-RPC response is nothing more than a META Refresh that forwards my browser panel to my core application. Of course, the most key aspect of this meta refresh is that it passes my existing session tokens onto the next context:

  • <!---
  • Param the FORM value that will contain the data posted from
  • the ColdFusion Builder extension. This will be in the form of
  • the following XML file:
  •  
  • <event>
  • <ide>
  • <projectview
  • projectname="SomeThing"
  • projectlocation="..." >
  •  
  • <resource
  • path="C:/....txt"
  • type="file"
  • />
  •  
  • </projectview>
  • </ide>
  • <user>
  • <input name="message" value="..." />
  • <input name="name" value="..." />
  • </user>
  • </event>
  • --->
  • <cfparam
  • name="form.ideEventInfo"
  • type="string"
  • default=""
  • />
  •  
  •  
  • <!---
  • Parse the posted XML string into a ColdFusion XML document
  • so that we can access the nodes within it.
  • --->
  • <cfset requestXml = xmlParse( trim( form.ideEventInfo ) ) />
  •  
  • <!---
  • Grab the resource node's PATH attribute from the XML post
  • into the document we got from ColdFusion builder.
  • --->
  • <cfset resourceNodes = xmlSearch(
  • requestXml,
  • "//resource[ position() = 1 ]/@path"
  • ) />
  •  
  • <!---
  • Get the file path - we are going to have to forward it onto
  • the primary web application.
  • --->
  • <cfset filePath = resourceNodes[ 1 ].xmlValue />
  •  
  •  
  • <!---
  • Let's store the file path in the current session to see
  • if the session is maintained between this version of the
  • ColdFusion Builder work flow (XML Responses) and the next
  • work flow (HTTP Web Requests).
  • --->
  • <cfset session.filePath = filePath />
  •  
  •  
  • <!--- Store the response xml. --->
  • <cfsavecontent variable="responseXml">
  • <cfoutput>
  •  
  • <response showresponse="true">
  • <ide>
  • <dialog
  • height="800"
  • width="1000" title="Session Tester"
  • />
  •  
  • <body>
  • <![CDATA[
  •  
  • <!---
  • Our response will do nothing more than
  • forward the user to the target URL.
  • When building the forwarding URL, we
  • need to pass the CFID / CFTOKEN created
  • in this request. Because we are crossing
  • from the XML RPC work flow into the web
  • work flow, we need to transfer our
  • session across contexts.
  • --->
  • <cfset targetUrl = (
  • "#application.rootURL#index.cfm?" &
  • "builderCFID=#session.cfid#&" &
  • "builderCFTOKEN=#session.cftoken#"
  • ) />
  •  
  • <!DOCTYPE HTML>
  • <html>
  • <head>
  • <title>Loading....</title>
  • <meta
  • http-equiv="refresh"
  • content="0;url=#targetUrl#">
  • </meta>
  • </head>
  • <body>
  • <em>Loading....</em>
  • </body>
  • </html>
  •  
  • ]]>
  • </body>
  •  
  • </ide>
  • </response>
  •  
  • </cfoutput>
  • </cfsavecontent>
  •  
  •  
  • <!---
  • Now, convert the response XML to binary and stream it
  • back to builder.
  • --->
  • <cfset responseBinary = toBinary(
  • toBase64(
  • trim( responseXml )
  • )
  • ) />
  •  
  • <!---
  • Set response content data. This will reset the output
  • buffer, write the data, and then close the response. At
  • this point, ColdFusion Builder relocate to a screen that
  • will build the web portal for our application.
  • --->
  • <cfcontent
  • type="text/xml"
  • variable="#responseBinary#"
  • />

As you can see, this "handler" file parses the posted XML request and extracts the file name of the given resource (the file on which ColdFusion Builder has initiated your extension). This file path is then stored in the session and our XML response is defined. The body of the XML response contains a tiny HTML web page that simply forwards the browser panel to our core web application. Notice that the target URL of the refresh contains "builderCFID" and "builderCFTOKEN" - these are the session tokens associated with the current request that we are passing onto the next context.

These session tokens, that we are passing with the URL, become the persisted session cookies in the target context (the Web HTTP Request work flow). Once we persist them as cookies, the browser panel will take care of implicitly posting them along with every subsequent request. As such, our ColdFusion session is successfully maintained across contexts.

Application.cfc

  • <cfcomponent
  • output="false"
  • hint="I define the application settings and event handlers.">
  •  
  • <!--- Set up application properties. --->
  • <cfset this.name = hash( getCurrentTemplatePath() ) />
  • <cfset this.applicationTimeout = createTimeSpan( 0, 0, 30, 0 ) />
  •  
  • <!--- Turn on session management. --->
  • <cfset this.sessionManagement = true />
  • <cfset this.sessionTimeout = createTimeSpan( 0, 0, 10, 0 ) />
  •  
  • <!--- Set up request level properties. --->
  • <cfsetting showdebugoutput="false" />
  •  
  •  
  • <!--- ------------------------------------------------- --->
  • <!--- ------------------------------------------------- --->
  •  
  • <!---
  • Because we might be coming from another context (XML-RPC),
  • we need to check if the previous context's session cookies
  • have been passed in the URL. If so, we want to transfer
  • those cookies to the new context for session maintenance.
  • --->
  • <cfif (
  • !isNull( url.builderCFID ) &&
  • !isNull( url.builderCFTOKEN )
  • )>
  •  
  • <!---
  • Store the previous context's cookies. Notice that we
  • are storing them as "session cookies" (ie. no
  • expiration date); this will ensure that the session
  • is ended when the console window is closed.
  • --->
  • <cfcookie
  • name="CFID"
  • value="#url.builderCFID#"
  • />
  •  
  • <cfcookie
  • name="CFTOKEN"
  • value="#url.builderCFTOKEN#"
  • />
  •  
  • </cfif>
  •  
  • <!--- ------------------------------------------------- --->
  • <!--- ------------------------------------------------- --->
  •  
  •  
  • <cffunction
  • name="onApplicationStart"
  • access="public"
  • returntype="boolean"
  • output="false"
  • hint="I initialize the application.">
  •  
  • <!--- The root of the application. --->
  • <cfset application.rootDirectory = getDirectoryFromPath(
  • getCurrentTemplatePath()
  • ) />
  •  
  • <!--- Get the base script for our application. --->
  • <cfset application.rootScriptName = getDirectoryFromPath(
  • cgi.script_name
  • ) />
  •  
  • <!---
  • Let's figure out how deep the current request is. We
  • will need this to figure out the root URL.
  • --->
  • <cfset local.scriptDepth = (
  • listLen( expandPath( application.rootScriptName ), "\/" ) -
  • listLen( application.rootDirectory, "\/" )
  • ) />
  •  
  • <!---
  • Based on the depth, remove as many directories from
  • the end of the root script as is needed to get to a
  • script name that points to teh root directory.
  • --->
  • <cfset application.rootScriptName = reReplace(
  • application.rootScriptName,
  • "([^\\/]+[\\/]){#local.scriptDepth#}$",
  • "",
  • "one"
  • ) />
  •  
  • <!---
  • Now that we have the root script name, we can compile
  • our root URL location.
  • --->
  • <cfset application.rootURL = (
  • "http://" &
  • cgi.server_name & ":" &
  • cgi.server_port &
  • application.rootScriptName
  • ) />
  •  
  • <!--- Return true so the application can load. --->
  • <cfreturn true />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="onSessionStart"
  • access="public"
  • returntype="void"
  • output="false"
  • hint="I initialize the session.">
  •  
  • <!---
  • Override the session tokens without any expiration
  • dates. This will create "session cookies" that will
  • expire when the user closes the browser (which in
  • our case is the ColdFusion Builder Exntension window).
  • --->
  • <cfcookie
  • name="CFID"
  • value="#session.cfid#"
  • />
  •  
  • <cfcookie
  • name="CFTOKEN"
  • value="#session.cftoken#"
  • />
  •  
  • <!--- Store a hit-count to test page requests. --->
  • <cfset session.hitCount = 0 />
  •  
  • <!--- Return out. --->
  • <cfreturn />
  • </cffunction>
  •  
  • </cfcomponent>

As you can see above, in the pseudo constructor of the Application.cfc, I am checking to see if "builderCFID" and "builderCFTOKEN" exist in the URL scope. If they do, I am using those values to override the session cookies in the current request. Because we are doing this in the pseudo constructor, we are acting before the current request has been associated with any given session. As such, by overriding the session cookies at this point, we will cause the current request to become associated with the previously created session (the ColdFusion application framework is so hot and sexy).

Notice that when I override the cookie values, I am defining them without any expiration dates. This creates them as "session cookies". "Session cookies", not to be confused with ColdFusion session cookies, are cookies that expire when the browser is closed. In our case, since the browser is really just a panel in a ColdFusion Builder Extension window, these session cookies will expire when the user closes the ColdFusion Builder modal window. This isn't required, but it felt like an appropriate life span for the type of applications used as ColdFusion Builder Extensions.

To test all of this out, I created a very simple Index.cfm page which outputs the Session structure as well as the URL values passed from the ColdFusion Builder Extension handler file:

index.cfm

  • <!--- Increment the session hit count. --->
  • <cfset session.hitCount++ />
  •  
  •  
  • <!DOCTYPE HTML>
  • <html>
  • <head>
  • <title>ColdFusion Builder Extension Session Testing</title>
  • </head>
  • <body>
  •  
  • <h1>
  • ColdFusion Builder Extension Session Testing
  • </h1>
  •  
  • <h2>
  • URL
  • </h2>
  •  
  • <!--- Display the URL. --->
  • <cfdump
  • var="#url#"
  • label="URL"
  • />
  •  
  • <h2>
  • Session
  • </h2>
  •  
  • <!--- Output the session. --->
  • <cfdump
  • var="#session#"
  • label="Session"
  • />
  •  
  • <p>
  • <a href="./index.cfm">Refresh Page</a>
  • </p>
  •  
  • </body>
  • </html>

The ability to extend ColdFusion Builder is probably the single most exciting feature of Adobe's new ColdFusion IDE. Of course, to be able to extend it well, it's important that we understand how it works. Session management is a crucial part of any application; getting session management to work in a ColdFusion Builder Extension is tricky, but not impossible. Understanding the different contexts and how they cross over is the key to maintaining a single ColdFusion session per extension execution.




Reader Comments

Dumb question - if all you do in hit one is an auto push to get out of the XML-RPC 'nature', why bother passing the session values? Just forget about the session and let it start again on the second hit. In theory it means you have a 'wasted' session, but as it will take up next to no RAM, it shouldn't be a big deal.

Reply to this Comment

Also - you could consider simply using urlSessionFormat. I've used that in my CFLib browser. It begins in XML-RPC mode for step 1 (pick a category). In step 2 we have a list of UDFs, paged. If you go to page 2, you've left XML-RPC mode (I don't think it's right to call it that - more on that in my next comment) - I just use urlSessionFormat and it seems to work fine. I -have- seen issues where urlSessionFormat refuses to add the url params.

Of course, to make things even easier, you could just add session.urltoken to your links. Folks don't normally do that in a web app, but for CFB extensions it wouldn't be hard to do, and would be simpler too, right?

Reply to this Comment

XML-RPC:
I'm not sure I'd call it XML-RPC mode. XML-RPC implies quite a bit (in terms of the type of XML request/response) and I don't think it really matches here at all.

Reply to this Comment

@Raymond,

I'll be honest with you - I am not sure what XML-RPC embodies; I probably should have explained that more clearly. I just mean that one work flow as defined by XML post/response data packets and that one was defined by "standard" web requests for HTML markup. Bad terminology on my part.

As far as why bother maintaining session across the two different types of work flows, my thought was that you could actually use the session in the first request more effectively. For example, I'm storing the "file path" of the resource in my file.

I could technically pass it through the URL in my redirect; but, I think there's something warm and fuzzy about using the existing session object.

Reply to this Comment

@Raymond,

As far as the UrlSessionFormat(), I am not sure I follow what you mean? Are you saying that you are adding the session tokens into the target URLs in your response XML? Or in the HTML markup?

Reply to this Comment

@Raymond,

Also, I should probably clarify that this really only works IF you intend to switch from XML to HTML after the first request. I am not sure how cross-XML requests (multi-step XML wizard) would work with session... or rather how one might maintain session.

Perhaps you can shed some light on that? I am sure you know way more on that aspect than I do.

Reply to this Comment

URLSessionFormat does a check to see if cookies are enabled. If not, it auto-adds the url token to the end. This _should_ work all the time, but I've seen it fail.

Seriously though - if your extension has a few sets of links, wouldn't adding session.urltoken manually be _far_ simpler?

Reply to this Comment

@Raymond,

Once you're in the "web" work flow, I don't think you should need to modify any links? At that point, I think the user is basically in a stand-alone browser type of situation (theory) where cookies are always enabled.

What I'd really like is to know more about how the web panel is rendered. I assume it's using webkit like AIR, but again, just another theory.

I guess the greater question is - once inside the web panel, can cookies even be disabled?

Reply to this Comment

"Once you're in the "web" work flow, I don't think you should need to modify any links? At that point, I think the user is basically in a stand-alone browser type of situation (theory) where cookies are always enabled."

Sure - but my point is - in a CFB Extension, the # of links should be small, so why not just append session.urlformat? Looking over most of my extensions, I think that would be like 5-6 links. (FYI, I'm NOT doing this now for my extensions - I've used other hacks, so this is a matter of 'Do as I say, not as I do' ;)

"What I'd really like is to know more about how the web panel is rendered. I assume it's using webkit like AIR, but again, just another theory."

Do what I did - dump the CGI scope. ;) If I remember right the user agent was Javasomething or another.

Reply to this Comment

FYI, you can also consider using a "one page app", ala Terry's Flex based "Builder Stats" - or just using jQuery for a rich app.

Reply to this Comment

@Raymond,

It's all just good conversation - I certainly am not that well versed in Builder yet; heck, I wrote this blog post AS I was exploring the concept ;)

I tried dumping out the CGI; you actually get two different things depending on the context. When you in the XML work flow, the user agent looks to be the "Jakarta Commons HTTP Client". Once you enter the web mode, the user agent gets reports as some "Mozilla" compatible client (I'll double check on what it actually says).

Building a one-page would be cool, no doubt. Whatever browser engine it's using seems to be quite capable.

Reply to this Comment

"It's all just good conversation - I certainly am not that well versed in Builder yet; heck, I wrote this blog post AS I was exploring the concept ;)"

Heh true - didn't mean to imply you shouldn't play as that would certainly be the pot calling the kettle a PHP developer.

"I tried dumping out the CGI; you actually get two different things depending on the context. When you in the XML work flow, the user agent looks to be the "Jakarta Commons HTTP Client". Once you enter the web mode, the user agent gets reports as some "Mozilla" compatible client (I'll double check on what it actually says)."

Interesting - in your settings for Preview, is the browser set to Mozilla? If you switch to Safari or IE, does it impact the result? In other words, is the browser used for 'web' mode the same used for the full page preview.

Reply to this Comment

@Raymond,

Uggg, I had a bear of a time getting the Meta Refresh to work again. For some reason, Builder just totally crapped out. Had to restart it twice before it started to work again.

I think perhaps booting Builder and the ColdFusion service at the same time crosses too many wires :)

Anyway, just got it up and running and the User Agent in the web work flow gets reported as:

Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; InfoPath.1)

I was not aware that there was a preview settings; I'll see if I can change that.

Reply to this Comment

@Raymond,

I am not sure where the preview feature is? When I type "preview" into the preferences, there are checkboxes for FireFox and IE... but they are both checked? Is that a different preview?

Reply to this Comment

Preview is shown at the bottom of the code editor. It lets you switch from code to a rendered view. Since both are checked, you should have 2 tabs. My question is - if you deselect Firefox and only have IE, would it then impact the UA of the Extension browser.

Reply to this Comment

I feel like you two are having a conversation at the kitchen table and I'm outside listening through an open window!

Reply to this Comment

@Papichulu,

I hope you're getting some value out of it :) Step on in if you think of anything - more than welcome.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.