November 21, 2008

Pages


Search Site


Topics



Archives

Tweets

Entries Tagged as 'ColdFusion'

Easily Cache the Return Value of Any Function

October 14 2008 by Adam

Work in web development long enough, and you'll run into caching. When done appropriately, it can be a real life saver. In CFML, we have several caching tools at our disposal.

The built in cfcache function will cache an entire page, and there are a bunch of projects on RIA Forge (search for "cache") that deal with caching of whole pages, partial pages, and specific data; and use various methods from memcached to underlying java caching. And of course, you can always roll your own cache using a persistent scope (application, session, client, or cookie) and some date comparison.

After about the 100th time of rolling my own custom caching, I decided it was time to wrap it up in a nice little UDF.

Before I show you the code, here's an example of how to use it:

<cffunction name="currentTime" output="false">
<cfreturn "The current time is #timeformat(now())#<br />"/>
</cffunction>

#cacheCallback("myApp.CurrentTime", CreateTimeSpan(0,0,2,0), variables.currentTime)#

Pretty simple, right? The first argument is a unique string that's used as the cache key, to identify which bit of data we're caching. The second is how long the cache is good for, and the third argument is a reference to a simple function that displays the current time.

Notice that the 3rd argument is not a string containing the name of the function to be executed, but actually a reference to the function itself. My cacheCallback function will check to see if the cache is expired, and either return the cached value or re-run the function being passed in and return the result, as appropriate.

A fourth and optional (boolean) parameter will force a cache refresh if true.

This has already proven useful, as a fellow Mango Blog plugin developer is using it to cache API results from services like Twitter and Flickr in some plugins he'll be releasing soon.

Here's the code for the cacheCallback function:

<cfscript>
function cacheCallback(cacheKey, duration, callback){
var data = "";
//optional argument: forceRefresh
if (arrayLen(arguments) eq 4){
arguments.forceRefresh=arguments[4];
}else{
arguments.forceRefresh=false;
}
//clean cachekey of periods that will cause errors
arguments.cacheKey = replace(arguments.cacheKey, '.', '_', 'ALL');
//ensure cache structure is setup
if (not structKeyExists(application, "CCBCache")){
application.CCBCache = StructNew();
}
if (not structKeyExists(application.CCBCache, arguments.cacheKey)){
application.CCBCache[arguments.cacheKey] = StructNew();
}
if (not structKeyExists(application.CCBCache[arguments.cacheKey], "timeout")){
application.CCBCache[arguments.cacheKey].timeout = dateAdd('yyyy',-10,now());
}
if (not structKeyExists(application.CCBCache[arguments.cacheKey], "data")){
application.CCBCache[arguments.cacheKey].data = '';
}
//update cache if expired
if (arguments.forceRefresh
or
dateCompare(now(), application.CCBCache[arguments.cacheKey].timeout) eq 1){
data = arguments.callback();
application.CCBCache[arguments.cacheKey].data = data;
application.CCBCache[arguments.cacheKey].timeout = arguments.duration;
}
return application.CCBCache[arguments.cacheKey].data;
}
</cfscript>

I've submitted this to CFLib, so hopefully it will be approved soon.

Disadvantages:

I tried for a while to figure out a way to pass arguments into the callback function, but never came up with anything. When I added a callbackArgs argument and passed that in as an argumentCollection to the callback function, CF threw an error telling me that I couldn't use named arguments in this case. I also tried to use CFInvoke, but that required passing in a string containing the name of the function instead of a reference to it, which while it would actually work with some massaging, wouldn't work as well in an Object Oriented architecture where passing objects (like function references) as arguments is common practice.

So for now, your callback functions must run without any arguments. If anyone has any other ideas for how to pass in arguments, I'm all ears.

Posted in ColdFusion | 1 comments

Related Entries plugin for Mango Blog

September 10 2008 by Adam

This plugin has been a long time in the making. It's something I knew from the outset that I would want in Mango, but after getting my feet wet by writing a few other small plugins, I knew I could do it. I made it as simple as I could, but there are still a couple of things you will need to know; one of which is that you might need to modify your theme to broadcast a new event in a couple of places. This new event is going to be a part of future versions of Mango, so there isn't any worry about future-compatability and worries when upgrading.

First of all, though you need a small update to the core of Mango. Even if you've got the latest version of Mango (1.1)! Laura posted a comment on the plugin ideas page with a link to the update zip file, and an explanation that it included a new plugin (home page chooser), some core modifications that were needed to support that plugin, and most importantly, some bug fixes — one of which is necessary for back-linking of related entries to work correctly. So before you do anything else, make sure you grab and install that update!

Once you've got the Mango update installed, go ahead and download my Related Entries plugin.

Now about that event. It's really simple to add. Open up your theme's index.cfm and post.cfm files (and archives, and other places you may want to show related entries…). The code to broadcast the event is really simple:

<mango:Event name="beforePostContentEnd" />

Simple, right? And where do you put it? Anywhere, really… within reason. It uses contextual information to look up related entries data for the current post, so you must broadcast it inside of a (custom tag) block. So for example, here's the relevant information from my theme's index.cfm template:

<mango:Posts count="5">
<mango:Post>
...
<mango:Event name="beforePostContentEnd" />
<p class="date">Posted in ...</p>
...
</mango:Post>
</mango:Posts>

 

That's almost it! After you install the plugin, and add the event broadcast to your theme, there's just 2 more things.

First, relate a couple of entries. Edit a post, and look at the bottom of the form.

This new section should be displayed at or toward the bottom of the form. As it explains, you select a category from the left column to see its entries from the selected categories (use control to select multiple). Then click on any entries from the center column that you want to relate to the current entry; they will be displayed in the right column. (This is all done with jQuery ajax!) To remove an entry from the right column, double click it. When you submit the form, the posts in the right column will be marked as related to the current entry; and in addition to that, the current entry will be related to those posts.

When you submit the form, if you get an error that looks like this, then you didn't install the update!:

If you don't get the above error, then your data should be good to go. Now, you just need to style it.

When you've got related entries data, the event you're broadcasting is going to be replaced with some code along this line:

<div class="related">
<h2 id="RelatedEntries">Related Entries:</h2>
<ul>
<li><a href="http://server/post/your-post">Post Title</a></li>
<li><a href="http://server/post/your-post">Post Title</a></li>
<li><a href="http://server/post/your-post">Post Title</a></li>
</ul>
</div>

So, you can define some css rules for .related, #RelatedEntries (or .related h2), and .related ul, .related ul li, .related ul li a.

That's it. Enjoy!

One note for the future: The update I talk about above should be included in Mango 1.2, so if you've got 1.2 (or later) installed, don't worry about the update.

In case you missed the link before, you can Download my Related Entries Mango Blog Plugin right here.

Posted in AJAX | ColdFusion | JavaScript | My projects | Mango | 7 comments

Tracking the storm… Redux

September 06 2008 by Adam

A couple of days ago I used Rays code for tracking hurricane distance from your local zip code to add tracking of Hurricane Hanna to my family blog. Then my father in law was kind enough to point out that it was wrong. Way wrong! It was reporting distances of over 3,000 miles, when we're really less than 300 miles from it.

Closer inspection of the code showed that Ray accidentally swapped the order of latitude and longitude assignments from the NHC feed; and wasn't accounting for Southern and Western hemispheres being represented as a negative number. Ray posted about some changes he made in a comment on his post to fix these problems (if only I had seen those before now!).

Ray is going to post his updated script soon, but mine is below.

In addition to the same fixes, I've added a touch of encapsulation (though I did remove the zip lookup stuff once I knew the lat/long for my zip — because this was easily the slowest part of the whole script) and setup caching with a forced refresh, and refreshing if the previous attempt had any problems. Here's my entire script:

<!--- catch previous hit found no data --->
<cfif structKeyExists(application, "hannaData") and findNoCase("check back later", application.hannaData)>
   <cfset application.hannaData = getHannaData() />
   <cfset application.hannaDataUpdated = now() />   
</cfif>
<!--- allow force cache refresh --->
<cfif structKeyExists(url, "updateHanna")>
   <cfset application.hannaData = getHannaData() />
   <cfset application.hannaDataUpdated = now() />   
</cfif>
<!--- catch first hit of application lifecycle --->
<cfif not structKeyExists(application, "hannaData") or not structKeyExists(application, "hannaData")>
   <cfset application.hannaData = getHannaData() />
   <cfset application.hannaDataUpdated = now() />   
</cfif>
<!--- catch cache outdated --->
<cfif structKeyExists(application, "hannaDataUpdated") and dateCompare(now(), application.hannaDataUpdated, "n") gt 10>
   <cfset application.hannaData = getHannaData() />
   <cfset application.hannaDataUpdated = now() />   
</cfif>

<cfoutput>
   #application.hannaData#<br/>
   <small><em>Updated: #timeFormat(application.hannaDataUpdated, "h:MM tt")#</em></small>
</cfoutput>

<cfscript>
/**
* Calculates the distance between two latitudes and longitudes.
* This funciton uses forumlae from Ed Williams Aviation Foundry website at http://williams.best.vwh.net/avform.htm.
*
* @param lat1     Latitude of the first point in degrees. (Required)
* @param lon1     Longitude of the first point in degrees. (Required)
* @param lat2     Latitude of the second point in degrees. (Required)
* @param lon2     Longitude of the second point in degrees. (Required)
* @param units     Unit to return distance in. Options are: km (kilometers), sm (statute miles), nm (nautical miles), or radians. (Required)
* @return Returns a number or an error string.
* @author Tom Nunamaker (&#116;&#111;&#109;&#64;&#116;&#111;&#115;&#104;&#111;&#112;&#46;&#99;&#111;&#109;)
* @version 1, May 14, 2002
*/
function LatLonDist(lat1,lon1,lat2,lon2,units)
{
   // Check to make sure latitutdes and longitudes are valid
   if(lat1 GT 90 OR lat1 LT -90 OR
      lon1 GT 180 OR lon1 LT -180 OR
      lat2 GT 90 OR lat2 LT -90 OR
      lon2 GT 280 OR lon2 LT -280) {
      Return ("Incorrect parameters");
   }

   lat1 = lat1 * pi()/180;
   lon1 = lon1 * pi()/180;
   lat2 = lat2 * pi()/180;
   lon2 = lon2 * pi()/180;
   UnitConverter = 1.150779448; //standard is statute miles
   if(units eq 'nm') {
      UnitConverter = 1.0;
   }

   if(units eq 'km') {
      UnitConverter = 1.852;
   }

   distance = 2*asin(sqr((sin((lat1-lat2)/2))^2 + cos(lat1)*cos(lat2)*(sin((lon1-lon2)/2))^2)); //radians

   if(units neq 'radians'){
      distance = UnitConverter * 60 * distance * 180/pi();
   }

   Return (distance);
}
</cfscript>
<cffunction name="logit" output="false" returnType="void">
<cfargument name="str" type="string" required="true">
<cflog file="hurricane_hanna" text="#arguments.str#">
</cffunction>
<cffunction name="getHannaData" returntype="string" output="false">
   <cfset var dst = 0/>
   <cfset var long = 0/>
   <cfset var longdir = "" />
   <cfset var lat = 0/>
   <cfset var latdir = "" />
   <cfset var hannaXML = "http://www.nhc.noaa.gov/nhc_at3.xml">
   <cfset var results = "" />
   <cfset var match = "" />
   <cfset var regex = "" />
   <cfset var text = ""/>
   
   <cffeed source="#hannaXML#" query="results">
   
   <cfif not results.recordCount>
      <cfset logit("Error - no feed entries")>
      <cfreturn "Unable to track hurricane Hanna (no data provided), check back later." />
   </cfif>
   
   <!--- find "Public Advisory" --->
   <cfquery name="pa" dbtype="query" maxrows="1">
      select rsslink, content, title
      from results
      where title like '%HANNA Public Advisory Number%'
   </cfquery>
   
   <cfif not pa.recordCount>
      <cfset logit("Error - cound't find a matching entry")>
      <cfreturn "Couldn't find information on hurricane Hanna, check back later." />
   </cfif>
   
   <cfhttp url="#pa.rsslink#" result="results">
   <cfset text = results.fileContent>
   
   <!--- strip extra white space --->
   <cfset text = reReplace(text, "[\r\n]+", " ", "all")>
   
   <cfset regex = "CENTER OF TROPICAL STORM HANNA WAS LOCATED NEAR LATITUDE ([[:digit:]\.]+)[[:space:]]*([NORTH|SOUTH|EAST|WEST]+)...LONGITUDE ([[:digit:]\.]+)[[:space:]]*([NORTH|SOUTH|EAST|WEST]+)">
   
   <!--- now look for: THE CENTER OF TROPICAL STORM HANNA WAS LOCATED NEAR LATITUDE 19.1 NORTH...LONGITUDE 74.4 WEST --->
   <cfset match = reFind(regex, text, 1, true)>
   <cfif arrayLen(match.pos) is 5>
    <cfset lat = mid(text, match.pos[2], match.len[2])>
    <cfset latdir = mid(text, match.pos[3], match.len[3])>
    <cfset long = mid(text, match.pos[4], match.len[4])>
    <cfset longdir = mid(text, match.pos[5], match.len[5])>
    <cfif longdir eq "WEST"><cfset long = -1 * long /></cfif>
    <cfif latdir eq "SOUTH"><cfset lat = -1 * lat /></cfif>
   <cfelse>
    <cfset logit("Error - couldn't find my matches in the string")>
      <cfreturn "Couldn't find lat/long location info for hurricane Hanna, check back later." />
   </cfif>

   <cfset dst = latLonDist(lat,long,39.98,-75.82,"sm")>
   
   <cfset logit("distance:" & dst)>
   <cfset rtn = "" />
   <cfset rtn &= "<strong>Distance:</strong> " & numberFormat(dst,',9.99') & " miles<br /><small>As of: <a " />
   <cfset rtn &= "href='" />
   <cfset rtn &= pa.rsslink />
   <cfset rtn &= "'><strong>#replaceNoCase(pa.title, 'Tropical Storm HANNA Public ', '')#</strong></a><br />" />
   <cfset rtn &= "Hanna Lat: #lat# #latdir#<br />Hanna Long: #long# #longdir#</small>" />
   <cfreturn rtn />
</cffunction>

Posted in ColdFusion | 1 comments

Protect your websites, logs, and inbox from SQL Injection

August 08 2008 by Adam

If you're like me you leave error reporting emails on for most of the sites that you build, so that wherever possible you are alerted to potential problems — like the recent wave of SQL Injection attacks — before the website owner (your client) knows they're being attacked, and hopefully, before an attack is successful. Of course, you're a responsible coder and you're logging and reporting errors, and then hunting down and eradicating the buggy code… right?

But as was pointed out this moring on twitter by Adam Lehman, if you log and subsequently email yourself or anyone else for every single error your website throws, you could end up flooding your own inbox (or worse, the inboxes of your clients and coworkers!) with error reports; essentially causing a Denial Of Service attack on yourself. And you may overly tax your server's resources in the process. It doesn't get much more lose-lose than that.

As ColdFusion Muse points out, a majority of recent attacks have been an attempt to append some javascript to every text field in a database, and they are using a very specific method: Declaring a text variable that contains some TSQL instructions, and then evaluating it. And like something out of a bad comedey, a few hours after reading Mark's post, it started happening to two of the websites my group manages. And boy, did the emails start coming.

Luckily, because the attack is so specific, it's easy to head off.

If you're using Application.cfm, just add this somewhere near the top:

<cfif findNoCase("DECLARE%20@S", cgi.query_string)><cfabort/></cfif>

And of course, if you're using Application.cfc, you can just put it in the onRequestStart function. Either way, you should position this code so that it is run before any significant work is done on the web server. There's really no generic reason it can't be line 1.

By doing this, you'll protect yourself from this specific attack (but not all SQL Injection) and stop all of those nagging error emails and log entries. Then you can focus on important things like making sure all of your queries use CFQueryParam.

Posted in ColdFusion | 4 comments

Best Practice: Separate App config from Framework Config

July 29 2008 by Adam

Best Practices is something that I don't think anyone, anywhere, ever gets 100% right. There's no "right" way to do everything. But we're striving to do things better all of the time, right?

For example, in the ColdFusion world we have Unit Testing frameworks like MxUnit, CFUnit, CFCUnit, and Selenium. I know Selenium isn't just for CF, but it does integrate nicely! Test-driven Development is all the rage these days, and while that's a good thing it's not for everyone and certainly something very difficult to "get right."

Consider that in a large project — one with 1,000 test cases — if you refactor some code that affects 10% of your tests, you have to re-write 100 test cases. This isn't to say that TDD is a bad thing… far from it! Just that there is no silver bullet, and that you always have to be thinking ahead to try and do things right the first time, and not repeat yourself.

This brings me to a change I recently made in Grub.

Grub uses the Model-Glue framework to, as I like to say, "code less and do more." In Model-Glue's XML config, we have an <include> tag that allows us to separate our config into logical sections to keep maintainability high. But did you know you can do the same thing with your ColdSpring XML config?

Well, not with out-of-the-box 1.0… You have to download the Bleeding Edge Release, here. The feature was first mentioned in November of 2006, when Jared announced that he had written it. It was then blogged by a bunch of other ColdSpring users, but I don't think any of them mentioned that it wasn't yet included in the official release except Mark Drew, and even then I don't think I picked up on that fact until well after my second or third reading of his post. I ended up reading this bug report that clued me in, after about an hour of trying to figure out why the necessary method wasn't where it should be.

After you download the BER and update your local copy of ColdSpring, you can add lines like the following to your beans XML:

<beans>
   <import resource="ColdSpring_MGConfig.xml"/>
</beans>

I used this code to separate out my Model Glue config — which will be constant in every environment, but different between them (debug, reload, allow event generation in dev; no debug, no reloading, and no generation in production or staging) — from my bean definitions, which change frequently as I work on the applicaiton. Now, with my ANT build script, I can build and deploy my project without ever thinking about config, and I know it will always be right because it will never change without me doing so manually.

I'm somewhat surprised that over a year has gone by and it's still not included in the official release. This means that I'm currently using two unreleased frameworks in Grub (the other being the Model-Glue 3 RC), and that I am going to have to include them both in the download to make it as user-friendly as possible.

Posted in Best Practices | ColdFusion | Frameworks | 5 comments