Tuesday, August 29, 2006

The overlooked INI file

Here's one that you may not have looked at before.  ColdFusion has three functions that let you work with Windows style .ini files.  I think most people pretty much ignore these functions, but I've found them to be quite handy in setting up applications.

Granted, if you're using one of the popular CF frameworks like Fusebox or Model-Glue, this may be of little interest since they tend to have ways of dealing with setting up environment variables.

For the rest of us, we generally wind up with some sort of cfswitch/cfcase construct in Application.cfm/.cfc to establish a set of environment variables in the application scope.  When you move your app to a new environment, we edit the values there or add a new case to our switch.

Using ini files instead of the switch approach is just an alternative and doesn't really add much in the way of different capabilities.  It's pretty much a stylistic decision.

I personally find the simplicity of ini files hard to beat, though.  Maybe that's because of my familiarity with them from past computing days.  Ini files (if you haven't worked with them before) are simply collections of name/value pairs stored in a plain text file.

You can segregate groups of name/value pairs with a section name and some bracket syntax.

A typical beginning ini file for an application might look like this:


;This file holds environment variables for myApp
;
;
[default]
dsn=myDatasource
root=/myApp
fullroot=http://myCompany.com/myApp
adminEmail=IBrokeIt@myCompany.com



I tend to name my file cfconfig.ini and store it in the root of my app, but you can do what you like.  You just need to put it in a place where cf can see it.

Enough yammering, how about some code:

Application.cfm/cfc excerpt




<!--- Environment variables --->
<cfset iniPath = getDirectoryFromPath(getCurrentTemplatePath()) & "/cfconfig.ini" />
<cfset section = cgi.SERVER_NAME & ":" & cgi.SERVER_PORT & ":" & cgi.REMOTE_USER />
<cfset sectionStruct = getProfileSections(iniPath) />
<cfif structKeyExists(sectionStruct, section)>
     <cfscript>
               iniPath = getDirectoryFromPath(getCurrentTemplatePath()) & "/cfconfig.ini";
               section = cgi.SERVER_NAME & ":" & cgi.SERVER_PORT & ":" & cgi.REMOTE_USER;
               keylist = structFind(getProfileSections(iniPath),section);
               
               for (k=1;k lte listLen(keylist);k=k+1)
               {
                    "application.#listGetAt(keylist,k)#" = getProfileString(iniPath,section,listGetAt(keylist,k));
               }
     </cfscript>
<cfelse>
     <cfinclude template="setup.cfm" />
     <cfabort>
</cfif>
    



If you use Application.cfc, then this would most likely go in your onApplicationStart method.  For Application.cfm, it usually just appears near the top.  Notice that I use a combination of the cgi.server_name, cgi.server_port and cgi.remote_user as a way to differentiate the different server environments.  This seems to work pretty good for me, although I sometimes have to implement a webserver based login in order to keep different development environments separate.  That's probably not needed by most people, but I tend to have more than one working environment for some apps.  The only reason I had to add the remote_user was because I tended to wind up with several localhost:80 for development environments that might not be set up exactly the same.

I have a file that I use to get an app started that I call setup.cfm.  The complete code follows:




Setup.cfm
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
     <cfscript>
          iniPath = getDirectoryFromPath(getCurrentTemplatePath()) & "cfconfig.ini";
          section = cgi.SERVER_NAME & ":" & cgi.SERVER_PORT & ":" & cgi.REMOTE_USER;

          if (isDefined("form.fieldnames"))
          {
               // get rid of the two fields for a new value since we handle those separately
               form.fieldnames = listDeleteAt(form.fieldnames,listFind(form.fieldnames,"NEWKEY"));
               form.fieldnames = listDeleteAt(form.fieldnames,listFind(form.fieldnames,"NEWVALUE"));
               
               // loop over the form fields and add corresponding entries to the current section of the ini file
               for (k=1;k lte listLen(form.fieldnames);k=k+1)
               {
                    setProfileString(iniPath,section,listGetAt(form.fieldnames,k), evaluate("form." & listGetAt(form.fieldnames,k)));     
               }
               
               // add new name/value pairs to the ini file if they exist
               if (form.newKey neq "")
               {
                    setProfileString(iniPath,section,form.newKey,form.newValue);     
               }
          } else if (structKeyExists(getProfileSections(iniPath),"default")) {
               // Move any default entries to this section if it is the first time this section is being set up.
               defaultKeys = structFind(getProfileSections(iniPath),"default");
               if (NOT structKeyExists(getProfileSections(iniPath),section))
               {
                    for (i=1;i lte listLen(defaultKeys);i=i+1)
                    {
                         setProfileString(iniPath,section,listGetAt(defaultKeys,i),getProfileString(iniPath,"default",listGetAt(defaultKeys,i)));
                    }     
               }     
          }
          // reset the application
     </cfscript>
     
     <style type="text/css">
          body{
               background-color: #cfc;
          }
     </style>
</head>

<body>
     <cfoutput>
     <h1>Setup for #application.applicationName#</h1>
     <h2>[#section#]</h2>
     <a href="cfconfig.ini" target="_blank">cfconfig.ini</a>
     <hr/>
     <p>Run this page to establish application level variables the first time
     you install the application on a server or visit it with a new url.  
     You can re-run this file at any time to refresh the application
     variables.</p>
          <form id="setupForm" action="" method="post" name="setupForm">
               <table cellpadding="3" cellspacing="0" border="1">
                    <tr>
                         <th>Key</th>
                         <th>Value</th>
                    </tr>
                    <cfset keylist="">
                    <cfif structKeyExists(getProfileSections(iniPath),section)>
                         <cfset keylist = structFind(getProfileSections(iniPath),section)>
                    </cfif>
                    
                    <cfloop list="#keylist#" index="key">
                         <tr>
                              <td>#key#</td>
                              <td>
                                   <input type="text" name="#key#" value="#getProfileString(iniPath,section,key)#" size="100">
                              </td>
                         </tr>
                    </cfloop>
                    <tr>
                         <td>
                              <input type="text" name="newKey" value="">
                         </td>
                         <td>
                              <input type="text" name="newValue" value="" size="100">
                         </td>
                    </tr>
                    <tr>
                         <th colspan="2">
                              <input type="submit" value="Update">
                         </th>
                    </tr>
                    
                    </table>
          </form>
          <hr/>
          <h3>Full text of cfconfig.ini</h3>
          <cffile action="read" file="#iniPath#" variable="ini">
          <pre>#ini#</pre>
     </cfoutput>

</body>
</html>





The last piece is to put together a starting ini file.  If you create a section called [default], the startup.cfm file will copy any items you have under that section to any new section it creates when you move your app to a new location.

A few other items to note; if you use cflogin to protect your entire app, you probably will not be able to get to your setup.cfm page to get started.  I usually write a small exception for the setup.cfm file in the cflogin logic.  You also may want to disable the setup file or delete it once you have your ini file established in a production environment.  Let's face it, you probably don't need to deploy setup to production.  You can ALWAYS edit the ini file directly.  It's not really that hard.

The application.cfm file excerpt I have here is being used in a development environment, which is why you don't see a test for determining if the application has been initialized.  It's less robust, but it also makes any changes to environment variables immediately available.  Otherwise, I'd have to do things like restart the cf service to see changes to the environment variables.

Let me know if you find this as straight forward as I do.  

Monday, August 07, 2006

cflogin strangeness

I had an issue about a month ago with using cflogin on a site where special cflogin structure was outliving the session. After the session died, I wanted the user to be logged out so they could go back to the login screen and re-establish the session.

The answer seemed to be to set the loginstorage attribute of the cfapplication tag to "session". That way whenever the session timed out, the authentication credentials would be gone as well. It seemed to work great.

Except for one thing: Because I'm using j2ee sessions, the thought goes that after the last browser is closed, the current session is destroyed. That works as expected. The problem arises in that unless a cflogout was executed before closing the last browser, it seems like the authentication credentials survive.

The next time the user opens the application, the special cflogin structure is not detected, so the application throws the login screen (as expected). But, when the user authenticates, it appears to authenticate the user without executing any code inside the cflogin block. Very strange.

Biometrics hacked

We all knew it was bound to happen. Pretty much anything that can be recorded can be copied and no biometric security system is any more secure then the system that manages the data behind it.

ABC News: Expert Issues Warning About E-Passports