Creating ColdFusion Components In Parent Directories (From Sub Directories) Without Mapped Paths
I was over on Peter Bell's blog talking about my philosophy of building applications without any sort of mapped paths when he raised an excellent point about security:
I'd have to spend some time playing with different names, but I'd probably eventually hit on the name of one of your cfcs. I honestly don't know whether I'd get anything of use in terms of maybe a file with config info or a DSN password or something and I don't even know how CF would handle the direct request for the cfc, so it may well not be an issue other than maybe allowing be to determine a couple of the valid cfc names you use, but I just like to keep my files away from such probing just in case!
All of my CFC are technically available for public access. My Application.cfc doesn't allow direct access nor do any of my methods (via the access="remote" value), but that's just a minor detail. The reason I do it this way is because, as anyone who knows about creating ColdFusion components, you will know that you cannot use parent directory paths such as "../../../cfc/Girl" when using the CreateObject() method. If you try that, you get the following error:
Component names cannot start or end with dots.
And, since I have no mapped paths in my applications, I needed the components to be lower than the www root so that I could access them using a proper dotted path. But, now that Peter has thrown down the gauntlet, it's time to prove that components can be moved outside the web root AND still be called without mapped paths.
The secret to doing this is way in which ColdFusion treats file location relationships. As you know, when you import a custom tag, call a CFModule, or include another file, ColdFusion treats the given path as being relative to the Current template path, NOT the Base template path. As it turns out, this is how it treats the paths to ColdFusion components as well - the path provided to the CreateObject() method is relative to the CURRENT template path, NOT the base template path.
So how do we leverage this information? We combine the fact that CFInclude uses relative paths with the fact that CreateObject() can use sub-directory dotted paths. First, let's talk about the directory structure:
www
...Application.cfc
...index.cfm
www_cfc
...AbstractBaseComponent.cfc
...Girl.cfc
In this example, our web root will be the "www" directory. Anything above or parallel to www folder is NOT accessible via a web url. The www_cfc directory will hold our ColdFusion components in a secure, non-web-accessible location. Ideally what we want to be able to instantiate CFCs in the www_cfc directory FROM the www directory without using a mapped path.
To do this, we need to create a proxy for creating CFCs; we need to have a middle man that has the ability to create CFCs using non-mapped paths that we can call from within our application. This middle man is the following function:
<cffunction
name="CreateCFC"
access="public"
returntype="any"
output="false"
hint="Creates a CFC Creation proxy. Does NOT initialize the component, only creates it.">
<!--- Define arguments. --->
<cfargument name="Path" type="string" required="true" />
<!--- Return the created component. --->
<cfreturn
CreateObject(
"component",
("www_cfc." & ARGUMENTS.Path)
)
/>
</cffunction>
All this function does is create a component at the passed in Path. As you can see, the ColdFusion component that is created uses the path starting with "www_cfc". To do this, this function must be located above the www_cfc directory, and that is exactly how it works. Let's save this method in an ColdFusion template above both the www and the www_cfc directories:
create_cfc.udf.cfm
www
...Application.cfc
...index.cfm
www_cfc
...AbstractBaseComponent.cfc
...Girl.cfc
Here, we have created the create_cfc.udf.cfm file. This file contains nothing BUT the method declaration. Now, remember, since ColdFusion treats component paths as relative to the current template, this file, create_cfc.udf.cfc, should be able to instantiate any component in any of the sub directories www or www_cfc.
But, this method is outside the web root. It doesn't matter. We don't want to call it from a URL; we just want to be able to access it. And how do we access it? Through the flexibility of ColdFusion CFInclude tags and the pre-processing nature of the Application.cfc.
Let's look at our Application.cfc code (located IN the www directory):
<cfcomponent
displayname="Application"
output="true"
hint="I do pre-page processing for the application">
<!---
Run the pseudo constructor to set up default
data structures.
--->
<cfscript>
// Set up the application.
THIS.Name = "CFC_TEST";
THIS.ApplicationTimeout = CreateTimeSpan( 0, 0, 5, 0 );
THIS.SessionManagement = false;
THIS.SetClientCookies = false;
</cfscript>
<!--- Include the CFC creation proxy. --->
<cfinclude template="../create_cfc.udf.cfm" />
<cffunction
name="OnRequestStart"
access="public"
returntype="boolean"
output="true"
hint="I do pre-page processing for the page request.">
<!---
Store the CreateCFC method in the application
scope. We *wouldn't* do this for every page
request... this is JUST an example.
--->
<cfset APPLICATION.CreateCFC = THIS.CreateCFC />
<cfreturn true />
</cffunction>
</cfcomponent>
As you can see, see, as part of the Application.cfc pseudo constructor code (the code outside of method declarations), we are including the create_cfc.udf.cfm template from the parent directory. CFInclude can use relative paths so going up one directory for CFInclude is no problem at all. This overlaps with the idea of CFInclude-based Mixins that I have explored briefly in other posts.
The CFInclude tag pulls in the method declaration CreateCFC() and stores it in the THIS scope of the Application.cfc instance. Then, in the OnRequestStart() method, we are storing the method pointer into the APPLICATION scope. This makes this method APPLICATION-scoped and therefore publicly accessible to the application framework. Now, keep in mind, this is JUST for an example. I would never store the method repeatedly for EVERY page call. This method pointer would obviously be cached during Application initialization.
Let's take a quick look at the Girl.cfc:
<cfcomponent
displayname="Girl"
extends="AbstractBaseComponent"
output="no"
hint="I am a girl object.">
<!--- Run the pseudo constructor to set up default data structures. --->
<cfscript>
// Set up default public properties.
THIS.FirstName = "";
THIS.LastName = "";
THIS.ValidPickupLines = "";
</cfscript>
<cffunction name="Init" access="public" returntype="Girl" output="false"
hint="Returns an initialized girl instance.">
<!--- Define arguments. --->
<cfargument name="FirstName" type="string" required="false" default="" />
<cfargument name="LastName" type="string" required="false" default="" />
<cfargument name="PickupLines" type="array" required="false" default="#ArrayNew( 1 )#" />
<!--- Store arguments. --->
<cfset THIS.FirstName = ARGUMENTS.FirstName />
<cfset THIS.LastName = ARGUMENTS.LastName />
<cfset THIS.ValidPickupLines = ARGUMENTS.PickupLines />
<!--- Return This reference. --->
<cfreturn THIS />
</cffunction>
<cffunction name="TryPickupLine" access="public" returntype="string" output="false"
hint="This tries a pickup line on the Girl.">
<!--- Define arguments. --->
<cfargument name="Line" type="string" required="true" />
<!--- Check to see if the pickup line is valid. --->
<cfif (THIS.ValidPickupLines.IndexOf( JavaCast( "string", ARGUMENTS.Line ) ) GTE 0)>
<!--- This was a valid pickup line. --->
<cfreturn (
"Hey, my name is " & THIS.FirstName & "." &
" Why don't you buy me a drink?"
) />
<cfelse>
<!--- This was NOT a valid pickup line. --->
<cfreturn (
"Does that line usually work with a woman? " &
"Maybe you should try something else."
) />
</cfif>
</cffunction>
</cfcomponent>
Now that we have the method APPLICATION-scoped, and the definition for the Girl.cfc, let's see how we can use it:
<!--- Create a set of pickup lines. --->
<cfset arrLines = ListToArray(
"Hey baby, what's your sign?|" &
"Excuse me miss, I think you are a hottie!|" &
"Whoa! Where have you been all my life?!?",
"|"
) />
<!---
Create a Girl object. Notice that we are not using
ColdFusion's CreateObject() method directly. Instead we
are going through the proxy method that calls it for us.
We then, initialize the returned object just as we would
for the standard CreateObject() method call.
--->
<cfset Girl = APPLICATION.CreateCFC( "Girl" ).Init(
FirstName = "Libby",
LastName = "Smith",
PickupLines = arrLines
) />
<!--- Try to pick up the girl. --->
<p>
#Girl.TryPickupLine(
"Can I buy you a drink?"
)#
</p>
<p>
#Girl.TryPickupLine(
"You look hot in that dress!"
)#
</p>
<p>
#Girl.TryPickupLine(
"Whoa! Where have you been all my life?!?"
)#
</p>
This gives us the output:
Does that line usually work with a woman? Maybe you should try something else.
Does that line usually work with a woman? Maybe you should try something else.
Hey, my name is Libby. Why don't you buy me a drink?
That there, my friends, is a proof of concept that ColdFusion components can be instantiated from sub-directories without using any sort of mapped paths. And what did it take? Just one ColdFusion template and a few more lines of code in the Application.cfc. But, just to go over the example once, you will see that when I create the CFC, Girl, I am calling the APPLICATION-scoped method, CreateCFC(). This, method, while it exists in the Application.cfc and is scope to the APPLICATION scope, was defined outside of the web root. And, since ColdFusion treats component paths relative to the DEFINING template, NOT the CALLING template, everything just falls into place.
And, if you encapsulate the way objects are created anyway, such as through some sort of service or factory, this concept should be totally transparent. The majority of your application will not have to know about it at all.
Want to use code from this post? Check out the license.
Reader Comments
Ben...
This is a great proof of concept AND something I might consider putting into use here at my company.
Thanks for the heavy lifting.
Andy,
Glad to help. I am one who is not fond of mapping as frankly, I generally don't have access to this sort of thing. I like to find all the ways I can to make development as portable as possible in a drop-and-it-works nature. Let me know if you run into any problems.
Ben,
That is a great idea, especially if you are in hosting environment!
I have one question: putting the function in Application scope, shouldn't every call to its method be locked with cflock? It could be quite unconfortable I guess...
@Jack,
There is no need to put a CFLock around the method just because it is in the APPLICATION scope. CFLock is only required when both these conditions apply:
1. A race condition might occur.
2. The worst-case scenario of a race condition would mess up mission-critical data.
Really, neither of those is happening. There is no real race condition since no data is really "shared". True, multiple requests can call this function, but one one read is made at a time (queued) and there is no continuity from method invocation to method invocation.
What about for those of use not using application.cfc yet?
@Derek,
This should work for Application.cfm as well. The other event-level methods obviously won't be available, but certainly you can CFInclude another file into an Application.cfm file.
Great article! I came across this looking for a solution to a similar problem...
<cfajaxproxy> also doesn't allow mapped paths for the location of the CFC. Can you think of a way to point to a CFC that's in a folder above wwwroot?
LiveDocs says this about the CFC attribute: "The path can be an absolute filepath, or relative to location of the CFML page."... but it also has to be in "dot-notation"
Any thoughts?
Daniel
@Dan,
I have not done much of anything with the AJAX stuff in CF8 yet. However, since it is a website calling the server, I imagine that everything would have to be below the web root. Of course, maybe they came up with something to work around that.
Try asking Ray Camden or Sean Corfield. Off hand, I know both of them have done a bunch with the AJAX stuff in CF8.
Like it. I remember putting something like this to the CFDev list sometime ago and getting a "why bother, just use mappings" response.
Our company too uses shared hosting by other companies and portability was my motivation.
On another note, my udf used cfinvoke and allowed you to supply the method name (defaulted to "init") and a structure containing any arguments you wanted to supply. One reason I did this was that one of the hosting companies we use blocks the use of cfobject and CreateObject by default!
Anyways, I'm glad I am not alone!
Dominic
@Dominic,
I am all about the portability. Something in my gut just likes drag and drop working. I don't want to have to rely on other people to get things set up. Of course, there is always some of that, but I prefer to minimize it.
@Ben, my sentiment exactly. If someone makes a typo in setting up a mapping for one of our govornment projects it can take 5days+ for them to resolve it! So my philosophy is to give them as little to do as possible...
Anyways, we are talking the same talk ;)
This is a cool concept. But now that CF8 allows you to define mappings at the application level (<cfset THIS.mappings['/whatever'] = "D:\"> ) does this change anyones aversion to using them?
@Todd,
I haven't use the per-application mappings just yet, but I have a feeling that it will change the way I feel. However, my ColdFusion componet (CFC) creation logic is generally encapsulated in some form of UDF like CreateCFC() or GetCFC(). So, logic in that function might change, but I feel that the code calling that UDF will not change much.
Of course, I might totally change the way I do stuff once I get cooking.
Yeh, it took me about 10 minutes of going through the ColdSpring tutorial to switch to using it to generate my components.
At its most basic level it is like your udfs and you can very quickly just use it at that level. But then it offers so much more... :)
Higly recommended,
Dominic
Oh lol, just noticed it was Ben that posted that - I guess I don't need to extol the virtues of ColdSpring to ya!
:blushes:
Since ColdFusion actually runs as a servlet inside a servlet container (JRUN), if you create a WEB-INF directory in the web root and put .cfm and .cfc files anywhere beneath it, they cannot be accessed via http. A 403 Forbidden will be returned. The reason for this is that Java applications that run in a servlet container put Java class files in WEB-INF/classes and JAR files are put in /WEB-INF/lib. Java class files and JAR files in theses directories will also available to Coldfusion.
@Tim,
I am not sure that I follow. Are you saying that you should put a WEB-INF folder in *each* of your websites?
Yes, that is what I am saying. Then you can access your cfcs using WEB-INF.path.to.cfc and your custom tags using cfimport with taglib = /WEB-INF/path/to/customtags. You just have to make sure you don't use a directory name that is in the main Coldfusion WEB-INF directory, which is C:\CFusionMX7\wwwroot\WEB-INF in the server configuration on Windows.
@Tim,
This is very interesting. I will have to look into this. Thanks for the advice.
Ben,
I have looked all over the web for an answer to this question, and have not managed to find a definitive answer (I see that someone asked you the question in 2007, and you didn't know at the time):
When using cfajaxproxy, is there any way for me to locate my cfc outsied (above) the webroot?
We have several sites that use a common CFC folder, and so far everything works well, but I cannot seem to get the cfajaxproxy tag to function properly unless the targeted CFC is at the webroot, or somewhere inside it.
@Kit,
If your CFCs are outside the web root, the only way that I know of is to use some sort of a proxy that is web accessible.
OK, could you give me some sort of example? I admit my ignorance here, as I am close to the limit of my current knowledge.
@Ben,
I have read about the CF 8.01 updater, which may well take care of the problem (boss needs to apply it, but wants to wait for a few other things first), but in the meantime, have found an interesting work-around: I create my working CFC wherever I want, then create a "bridging CFC", that merely extends the working CFC. It appears to work just fine.
@Kit,
I am not sure I follow what you are saying? Can you expand on that concept?
First of all thank you for useful tip.
I tried Tim's suggestion and all I can say is it doesn't work if root is (re)mapped.In that case CF will search for component in mappedroot.WEB-INF.cfc...