Managing ColdFusion Sessions In A ColdFusion Builder Extension
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).
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.
Want to use code from this post? Check out the license.
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.
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?
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.
@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.
@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?
@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.
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?
@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?
"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.
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.
@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.
"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.
@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.
@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?
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.
@Raymond,
It appears to come through as the same Mozilla user agent.
Ok, so it is NOT using the 'preferred' preview browser. Interesting.
I feel like you two are having a conversation at the kitchen table and I'm outside listening through an open window!
@Papichulu,
I hope you're getting some value out of it :) Step on in if you think of anything - more than welcome.