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 the jQuery Conference 2010 (Boston, MA) with:

Sandbox ColdFusion Custom Tag Encapsulates Code

By Ben Nadel on
Tags: ColdFusion

I was helping a co-worker debug some code earlier and after about 5 minutes of digging through different includes, we found that a ColdFusion query deep inside of a nested CFInclude was overriding a query made on a top level page by the time the top level query was getting used for output. The problem here isn't one that I come across very often, but I can see this being a potential problem an procedural applications.

This got me thinking - wouldn't it be interesting if you could wrap a bunch of code with a tag and stop the changes in that tag from persisting to the variables outside of the tag. To experiment with this concept, I created the Sandbox ColdFusion custom tag that simply wraps chunks of code to protect the REQUEST and VARIABLES scopes:

  • <!--- Simple value. --->
  • <cfset REQUEST.Girl = "Maria Bello" />
  •  
  • <!--- Array value. --->
  • <cfset arrGirls = ListToArray( "Christina Cox,Madonna" ) />
  •  
  • <!--- Struct value. --->
  • <cfset objGirls = StructNew() />
  • <cfset objGirls[ "Maria" ] = "HOT!!" />
  • <cfset objGirls[ "Ellen" ] = "Quirky" />
  •  
  •  
  •  
  • <!---
  • Run this bit of code within the sandbox. This
  • *attempts* to keep the REQUEST and VARIABLES scopes
  • from being altered. This will not be fool-proof for
  • complext objects.
  • --->
  • <cf_sandbox>
  •  
  • <!--- Override the string. --->
  • <cfset REQUEST.Girl = "Lori Petty" />
  •  
  • <!--- Update the array. --->
  • <cfset ArrayAppend( arrGirls, "Jodie Foster" ) />
  •  
  • <!--- Alter the struct. --->
  • <cfset objGirls[ "Maria" ] = "Ugly" />
  •  
  • </cf_sandbox>
  •  
  •  
  •  
  • <!---
  • Output the page variables to see if they have been
  • protected by the sandbox.
  • --->
  •  
  • <cfdump
  • var="#REQUEST#"
  • label="REQUEST Variables"
  • />
  •  
  • <cfdump
  • var="#VARIABLES#"
  • label="VARIABLES Variables"
  • />

In the code, we are setting a string, array, and struct variable across the REQUEST and VARIABLES scope. Then, within the sandbox custom tag, we update all three of those variables. Once the Sandbox ColdFusion custom tag has finished executing, we CFDump out the REQUEST and VARIABLES scope to see if anything has changed:


 
 
 

 
Sandbox ColdFusion Custom Tag - REQUEST and VARIABLES Scope  
 
 
 

If you look at the output, you will see that the girl name, Maria Bello, persisted and was not overridden by Lori Petty. The array has no traces of Jodie Foster, who was appended within the sandbox. And, you will see that Maria is still HOT, and not ugly as defined within the sandbox.

The CF_Sandbox call was able to prevent the updates to the VARIABLES and REQUEST scopes from affecting the rest of the page. Now, why would you want to do something like this? The particular bug that we found was that a query was running to populate a select box. Now, like I said, there are better ways to deal with this, but using this tag could potentially protect something like this:

  • <cf_sandbox>
  • <!--- Run Query. --->
  • <!--- Populate select box with previous query. --->
  • </cf_sandbox>

Whether or not you even find this interesting, the execution of such Sandbox protection was actually quite easy to perform. All the ColdFusion custom tag does is duplicate the REQUEST and VARIABLES scope into the custom tag and then copy them back out at the end:

  • <!--- Kill extra output. --->
  • <cfsilent>
  •  
  • <!--- Check to see which tag mode we are in. --->
  • <cfswitch expression="#THISTAG.ExecutionMode#">
  •  
  • <!---
  • In the start tag, we want to store the local
  • values of the variables and request scopes.
  • --->
  • <cfcase value="start">
  •  
  • <!--- Copy of the request to the tag. --->
  • <cfset THISTAG.CallerRequest = Duplicate(
  • CALLER.REQUEST
  • ) />
  •  
  • <!--- Copy of the variables to the tag. --->
  • <cfset THISTAG.CallerVariables = Duplicate(
  • CALLER.VARIABLES
  • ) />
  •  
  • </cfcase>
  •  
  •  
  • <!---
  • In the end mode of the tag, we want to take the
  • tag-stored variables and store them back into the
  • caller scopes.
  • --->
  • <cfcase value="end">
  •  
  • <!--- First, clear callers scopes. --->
  • <cfset StructClear( CALLER.REQUEST ) />
  • <cfset StructClear( CALLER.VARIABLES ) />
  •  
  • <!--- Copy back stored request --->
  • <cfset StructAppend(
  • CALLER.REQUEST,
  • THISTAG.CallerRequest
  • ) />
  •  
  • <!--- Copy back stored variables. --->
  • <cfset StructAppend(
  • CALLER.VARIABLES,
  • THISTAG.CallerVariables
  • ) />
  •  
  • </cfcase>
  •  
  • </cfswitch>
  •  
  • </cfsilent>

When we copy the scopes, it is important to use Duplicate() so as to cut down on the number of ties to "by reference" variables. Then, when copying them back, we need to clear the target scopes before we append the stored scopes so as to remove any artifacts that have been added to the scopes within the sandbox.

Of course, if you have objects, such as ColdFusion components, that maintain an internal state, the Sandbox cannot "rollback" methods called on such objects. This can really only protect against built-in type updates (string, array, struct, query, etc.).



Reader Comments

Ben, I had to do a similar thing just a few days ago. I had the same situation where an include file overwrote one of the values in the calling template, so I needed to isolate the calling template from the includes. Unfortunately, most of the code re-use in this application is via includes, so my solution focuses on isolating includes.

Basically, I create a custom tag ( /Modules/cfincludeWrapper.cfm ) with the contents below. It simply executes the include within the context of a custom tag to prevent the calling template's variables scope from being polluted. Any attributes you pass into the custom tag get placed in the variables scope of the custom tag before the include is called so that the include can access the variables it needs still without having to look at different scopes. Then at the end, the variables scope of the custom tag is returned to the calling template so the caller can read any values that were created as a result of the include.

[begin: cfincludeWrapper.cfm]

<!--- Initialize default/required parameters --->
<cfparam name="attributes.includeToWrap" type="string" default="" >
<cfparam name="attributes.returnVariable" type="variablename" default="includeResults" >

<cfif NOT len(attributes.includeToWrap) >
<cfthrow message="Include to Wrap must be specified" >
<cfelseif NOT len(attributes.returnVariable) >
<cfthrow message="Include to Wrap must be specified" >
</cfif>

<!---
Copy all the attributes passed into the variables
scope so that they're available to the include
--->
<cfset structAppend(variables, attributes, "yes") >
<cfset structDelete(variables, "includeToWrap") >
<cfset structDelete(variables, "returnVariable") >

<!--- Run the include --->
<cfinclude template="../#attributes.includeToWrap#" >

<!--- Send all the variables that the include generated back to the caller --->
<cfset "caller.#attributes.returnVariable#" = variables >

[end: cfincludeWrapper.cfm]

@Kurt,

I like it a lot. The one thing I am cautious of it the CFInclude inside of the custom tag. Since the CFInclude paths are relative to the currently executing template, it must be relative to the custom tag. I guess, you just have to be sure that the path you pass in via the custom tag attribute is relative to the application root? and then it looks like the custom tag assumes itself to be level below the root?

But overall, I like your idea better than mine because it doesn't really mess with the top-level VARIABLES or REQUEST scopes like mine were doing.

Good stuff.

Glad you like it Ben. I agree, the only downfall to this is the use of the include path. I didn't include all the documentation on the code to keep my comment to a reasonable size, but basically, the path to the include should always be a path relative to the web root (/somedir/somefile.cfm).

I always call the custom tag by doing a CFIMPORT and then using the JSP-like syntax for calling the custom tag. This way, the only place you have to worry about the path to the include file is in the custom tag itself. And since I tend to organize my custom tags like I do my CFCs (i.e. /com/company/project/tags/includeWrapper.cfm; I can count on the custom tag ALWAYS being X directories deep and can always figure out the path to the include as long as the path passed in is web-root relative.

Of course if mappings are being used that may cause an issue.

Love your blog, keep up the posts :)

I'm late on this one but could not resist... how can you just treat girls as objects Ben. Shame on you...
Haha

Hey Ben,

I just wanted to thank you for your blog post.

I was having an issue with the request scope being used in a Left Join within a query nested deep within a custom tag. This post helped me isolate the request scope, preventing the request scope from being used while looping over the custom tag cause a bug that was very annoying.

Thanks to you, no more annoyance!

Joel