Patrick emailed me last night with a some problems he was having with an Application.cfc file. When I saw the file - I saw numerous problems. I asked if he was cool with me rippingreviewing it publicly and he said sure. I know that many people are still getting used to the idea of Application.cfc, so I hope this is useful, and as true with all code reviews - please take what I say with a grain of salt. Somethings are just personal opinion.
So first off - here is the file in question: http://www.nomorepasting.com/getpaste.php?pasteid=5861
I'm going to move down the file and point out what I see as wrong, or what could be done better. Let's start first in the constructor area where he has this code:
<!--- <cftry>
<cfldap action="QUERY"
name="ldap"
attributes="sn"
start="ou=company,dc=company,dc=com"
server="localhost"
username="localdomain\intranet"
password="pass">
<cfcatch type="any"> --->
<cfset application.noldap = true>
<!--- </cfcatch>
</cftry> --->
While the try/catch/cfldap is commented out - his intent - a "test" hit to see if his ldap server is up - is good. But he did it in the constructor. Stuff like this should be done in an onApplicationStart method.
Moving down - he specifies a cflogin idletimeout value, but earlier had specified that cflogin should be tied to the session. That should then overrule the idletimeout. (I need to test that actually.)
<cfset theusername=trim(form.j_username)>
<cfset thepassword=trim(form.j_password)>
Don't forget that your Var Scope rules apply to the Application.cfc file as well. Also, cflogin can exist for multiple situations, not just a form post. If I hit your site with j_username and j_password in the query string, your site will error out since the form scope values won't exist. If you want to check for cflogin, then use the cflogin scope.
<cfquery name="auth" datasource="intranet"><!--- MySQL Fieldnames are named like Active Directory Fields for easier sync between both --->
SELECT * FROM users WHERE samaccountname='#theusername#' AND password='#thepassword#'
</cfquery>
Repeat after me - I will not write dynamic SQL without using cfqueryparam.
Now look down to...
<cfset onSessionStart()>
Bad. While you can call methods directly, you shouldn't. ColdFusion is responsible for starting the session. It already ran it when you hit the page and you were shown the login screen. If your intent is to set some session variables only after login, then set them here. But do not set them in onSessionStart.
Now let's look at your onSessionStart, where I saw the worst code. Here is the entire method:
<cfif application.noldap>
<cfquery name="application.mitarbeiter" datasource="intranet">
SELECT * FROM users
</cfquery>
<cfelse>
<cfldap action="QUERY"
name="results"
attributes="sn,givenname,department,telephoneNumber,mobile,mail,title,samAccountName,initials,displayname,physicalDeliveryOfficeName,description"
start="ou=company,dc=company,dc=com"
filter="(&(objectclass=user)(company=Our Company))"
server="localhost"
username="localdomain\intranet"
password="pass">
<cflock scope="application" type="exclusive" timeout="20">
<cfset application.mitarbeiter = results>
</cflock>
</cfif>
<cfquery name="application.news" datasource="intranet" maxrows="2">
SELECT * FROM aktuelles ORDER BY id DESC
</cfquery>
<cfquery name="application.accounts" datasource="intranet">
SELECT * FROM accounts
</cfquery>
<cfquery name="application.kontakte" datasource="intranet">
SELECT * FROM kontakte ORDER BY type, firma, name
</cfquery>
<cfquery name="application.marken" datasource="intranet">
SELECT * FROM marken ORDER BY seit DESC
</cfquery>
Application variables being created in onSessionStart? This implies you do not understand the methods of Application.cfc and how they are created. These are all queries that should be moved into onApplicationStart.
In a similar vein - consider this line from onSessionEnd:
<cfset structclear(session)>
The session is already over. This will actually throw an error, silently, because you can't directly access the session scope here. When ColdFusion runs onSessionEnd, it passes the session data as an argument, and you must work with the data via the argument, not the Session scope.
Lastly - I love the error message:
The server made a boo boo! ;)
Archived Comments
I would also criticize:
The lack of var scoping, even in an Application.cfc it should be done.
The select *'s.
And thispath and ThisDirectory not being the same as this.path, and this.directory. They're not used in the application.cfc. They would probably also be better set in onapplicationstart, directly into the application scope. But that might be personal preference.
When you say:
"While the try/catch/cfldap is commented out - his intent - a "test" hit to see if his ldap server is up - is good. But he did it in the constructor. Stuff like this should be done in an onApplicationStart method."
Can you define "stuff"?
Thanks!
Stuff = Things that only need to be done once. Ie, you (typically) would only do such a test when the Application is starting up.
I disagree that you should "never" call the methods and that it should be left up to CF. I have code that will look for some url var and call the onApplicationStart() or onSessionStart() method to re initialize some variables or components. Outside of that I would agree with that statement.
Wow, you could also comment on the sessiontimeout value.
It's kinda cool to look into how other ColdFusion programmers code their applications. Wouldn't it be nice to have a repository where people can submit their Application.cfc for the purpose of sharing standard and best coding practices.
Check out mine here, http://www.williamukoh.com/...
In addition to other specific requirements every application I develop will need in the Application.cfc, I always ensure my Application.cfc does the following
1. Reads the web applications's config file (*.ini file from a non-http accessible location)
2. Tracks users of the application
3. Imports required UDFs
4. Handles error reporting.
@Dan: I do that as well (a url hook to run onApplicationStart()). I don't think I said 'never' for that - but I definitely should have mentioned the times when it is cool to use.
@Dan, instead of calling an onXyz() handler directly, I'd pull the common code out into another function and call that instead (from all of the onXyz() handlers that need it). That keeps the handlers independent and emphasizes the "commonness" of the code in the function that multiple handlers call.
On var scoping, I will point out that a fresh instance of Application.cfc is created *for every request* so there are no threading issues here. Also, unless you use onRequest(), neither your unscoped variables nor your variables scope itself will bleed into the requested template.
On that point, the pseudo-constructor for Application.cfc is executed on *every request* - so you might as well put other "stuff" in onRequestStart() (unless you need it to execute on the first request *before* onApplicationStart() or on the first request of a session *before* onSessionStart()...)
The order of execution is:
* pseudo-constructor
* onApplicationStart() if this is the first request for an application - CF locks the application so that there can be no threading issues (so you don't need to lock)
* onSessionStart() if this is the first request for this session - again CF locks the session so that there can be no threading issues (so you don't need to lock)
* onRequestStart()
* onRequest() if present - and it must include the requested template - and you must not use onRequest() with web services or AJAX/Flex data requests!
* onRequestEnd()
* onSessionEnd() in the background when a session ends (you cannot reference 'session' or 'application' directly at this point)
* onApplicationStart() in the background when an application times out (again, you cannot reference 'application' directly here)
It's important to keep this lifecycle in mind when using Application.cfc. Also, if you reinitialize application or session variables from a "later" handler, you may need to lock to prevent race conditions.
@Sean - I think you meant onApplicationEnd there (your last bullet)
Even if var scoping may not be needed... I'd still use it. Just to be consistent. I've heard consistency is a good thing. ;)
If the pseudo-constructor is called for every request, wouldn't it be better to put the following into onApplicationStart():
<cfset this.name = "Intranet">
<cfset this.loginStorage = "session" />
<cfset this.sessionManagement = true />
<cfset this.scriptProtect = true />
<cfset this.setClientCookies = false />
<cfset this.setDomainCookies = false />
<cfset this.sessionTimeOut = CreateTimeSpan(0,2,0,0) />
<cfset this.applicationTimeOut = CreateTimeSpan(2,0,0,0) />
These app setting don't need to be set for every request, just on app start correct?
No. Those settings are fine where they are.
Ray, can you elaborate a bit?
@Ray, yes, onApplicationEnd() - copy'n'paste bit me. Yes, I'd use var too... just pointing out it's not really needed here.
@Andrew, the settings must execute on every request. Remember that you can change the application name on every request and associate that request with any application.
@Adam - Elaborate on what?
Thanks for posting the order of execution, Sean. If it's not already on Ray's Application.cfc reference, I'm going to bust out a PEN and write it on my printed copy.
Unless code from the pseudo-constructor is needed in your onApplicationStart() and/or onSessionStart() methods, couldn't it just as easily go in onRequestStart()? I know I have applications where the pseudo-constructor defines the DSN for instance, but it isn't used in onApplicationStart() or onSessionStart() [because sessions aren't used].
Ok so I basically just restated something Sean had written. Oh well. Ignore me.
@Ray
Thanks for the review. But in one point i disagree. I won't put my LDAP Test in onApplicationStart(). The Application runs for two days, what if the LDAP crashes after ie 3 hours?
Isn't it better to put it into onRequestStart()?
Sorry for my english...
I meant onSessionStart() of course...
I understand the need to see if LDAP is up, but that is possibly an expensive hit to do on every Request. I wouldn't do it in onSessionStart either. If you didn't trust doing it once in onApplicationStart, I'd consider logging into the Application scope the last time of the test. cfset application.lastldaptest = now. In onRequestStart, check that value and if more than N hours old, run it again.