Adding CFAkismet to BlogCFC

Adding CFAkismet to BlogCFC

The other day I posted about some mods I made to BlogCFC. A few people have asked me to share the code so I thought I'd walk through the changes in a couple of blog posts. For this post I'm going to show how I added Brandon Harper's CFAkismet to my BlogCFC installation.

This post is going to assume you are making the changes to the default download of BlogCFC 5.9.

Note: To make applying this mod a little easier I've posted a .zip with diffs of all the files I modified. (Click the download link at the bottom of the post to get files.)

Install CFAkismet

The first thing you need to do is checkout CFAkismet from the Google Code repo to a temp directory. Once you have the CFAkismet code you can copy the com folder to the root BlogCFC folder and delete the com/devnulled/cfakismet/tests folder, leaving only the com/devnulled/cfakismet/CFAkismet.cfc in your BlogCFC folder.

Modify blog.cfc

Now that you have the component installed we need to modify the core BlogCFC component (org/camden/blog.cfc) to use it. The first thing we will do is modify the init method to load two new settings from the blog.ini file:

...
<cfset instance.usecaptcha = variables.utils.configParam(variables.cfgFile, arguments.name, "usecaptcha")>
<cfset instance.useakismet = variables.utils.configParam(variables.cfgFile, arguments.name, "useakismet")>
<cfset instance.akismetkey = variables.utils.configParam(variables.cfgFile, arguments.name, "akismetkey")>
<cfset instance.allowgravatars = variables.utils.configParam(variables.cfgFile, arguments.name, "allowgravatars")>
...

A little further down in the init method we get a copy of the CFAkismet component:

...
<!--- get a copy of textblock --->
<cfset variables.textblock = createObject("component","textblock").init(dsn=instance.dsn, username=instance.username, password=instance.password, blog=instance.name)>

<!--- get a copy of cfakismet --->
<cfif instance.useakismet>
<cfset variables.cfakismet = createObject("component","com.devnulled.cfakismet.CFAkismet").init()>
<cfset variables.cfakismet.setBlogURL(instance.blogURL)>
<cfset variables.cfakismet.setKey(instance.akismetkey)>
<cfset variables.cfakismet.setApplicationName("BlogCFC/" & version)>
</cfif>

<!--- prepare rendering --->
<cfset renderDir = getDirectoryFromPath(GetCurrentTemplatePath()) & "/render/">
...

The next method we need to change is the addComment method. First we add a few new var scoped variables:

...
<cfset var newID = createUUID()>
<cfset var entry = "">
<cfset var spam = "">
<cfset var modEntry = "">
<cfset var akismetArgs = StructNew()>
<cfset var commentIsSpam = false>
...

Next we need to determine if we should moderate the comment, either due to the blog's settings or the result of the Akismet check. We use a new variable, modEntry, to track this state and then set the comment's moderated flag accordingly.

...
<!--- check spam and IPs --->
<cfloop index="spam" list="#instance.trackbackspamlist#">
<cfif findNoCase(spam, arguments.comments) or
findNoCase(spam, arguments.name) or
findNoCase(spam, arguments.website) or
findNoCase(spam, arguments.email)>

<cfset variables.utils.throw("Comment blocked for spam.")>
</cfif>
</cfloop>
<cfif len(cgi.REMOTE_ADDR) and listFind(instance.ipblocklist, cgi.REMOTE_ADDR)>
<cfset variables.utils.throw("Comment blocked for spam.")>
</cfif>

<cfset modEntry = instance.moderate>

<!--- check akismet --->
<cfif instance.useakismet>
<cfif variables.cfakismet.verifyKey()>
<cfset akismetArgs.CommentAuthor = arguments.name />
<cfset akismetArgs.CommentAuthorEmail = arguments.email />
<cfset akismetArgs.CommentAuthorURL = arguments.website />
<cfset akismetArgs.CommentContent = arguments.comments />
<cfset akismetArgs.Permalink = makeLink(arguments.entryID) />
<cfset commentIsSpam = variables.cfakismet.isCommentSpam(ArgumentCollection=akismetArgs)>
<cfif commentIsSpam>
<cfset modEntry = true>
</cfif>
<cfelse>
<cfset variables.utils.throw("Invalid akismet key.")>
</cfif>
</cfif>

<cfquery datasource="#instance.dsn#" username="#instance.username#" password="#instance.password#">
<!--- RBB 11/02/2005: Added website element --->
insert into tblblogcomments(id,entryidfk,name,email,website,comment<cfif instance.blogDBTYPE is "ORACLE">s</cfif>,posted,subscribe,moderated)
values(<cfqueryparam value="#newID#" cfsqltype="CF_SQL_VARCHAR" maxlength="35">,
<cfqueryparam value="#arguments.entryid#" cfsqltype="CF_SQL_VARCHAR" maxlength="35">,
<cfqueryparam value="#arguments.name#" maxlength="50">,
<cfqueryparam value="#arguments.email#" maxlength="50">,
<!--- RBB 11/02/2005: Added website element --->
<cfqueryparam value="#arguments.website#" maxlength="255">,
<cfif instance.blogDBType is "ORACLE">
<cfqueryparam value="#arguments.comments#" cfsqltype="CF_SQL_CLOB">,
<cfelse>
<cfqueryparam value="#arguments.comments#" cfsqltype="CF_SQL_LONGVARCHAR">,
</cfif>
<cfqueryparam value="#blogNow()#" cfsqltype="CF_SQL_TIMESTAMP">,
<cfif instance.blogDBType is "MSSQL" or instance.blogDBType is "MSACCESS">
<cfqueryparam value="#arguments.subscribe#" cfsqltype="CF_SQL_BIT">
<cfelse>
<!--- convert yes/no to 1 or 0 --->
<cfif arguments.subscribe>
<cfset arguments.subscribe = 1>
<cfelse>
<cfset arguments.subscribe = 0>
</cfif>
<cfqueryparam value="#arguments.subscribe#" cfsqltype="CF_SQL_TINYINT">
</cfif>
<!--- do we need to moderate this comment, either via moderate comments setting or akismet --->
,<cfif modEntry>0<cfelse>1</cfif>
)
</cfquery>
...

The next step is to modify the getComments method to return only moderated comments. Basically we remove the conditional logic around the "and tblblogcomments.moderated = 1" where predicate because we only ever want to show moderated comments.

...
<!--- RBB 11/02/2005: Added website to query --->
<cfquery name="getC" datasource="#instance.dsn#" username="#instance.username#" password="#instance.password#">
select tblblogcomments.id, tblblogcomments.name, tblblogcomments.email, tblblogcomments.website,
<cfif instance.blogDBTYPE is NOT "ORACLE">tblblogcomments.comment<cfelse>to_char(tblblogcomments.comments) as comments</cfif>, tblblogcomments.posted, tblblogcomments.subscribe, tblblogentries.title as entrytitle, tblblogcomments.entryidfk
from tblblogcomments, tblblogentries
where tblblogcomments.entryidfk = tblblogentries.id
<cfif structKeyExists(arguments, "id")>
and tblblogcomments.entryidfk = <cfqueryparam value="#arguments.id#" cfsqltype="CF_SQL_VARCHAR" maxlength="35">
</cfif>
and tblblogentries.blog = <cfqueryparam value="#instance.name#" cfsqltype="CF_SQL_VARCHAR" maxlength="50">

and tblblogcomments.moderated = 1

order by tblblogcomments.posted #arguments.sortdir#
</cfquery>
...

Finally we add two new methods to the core blog.cfc which allow us to report comments as either spam or ham (comments incorrectly flagged as spam by the Akismet service).

<cffunction name="reportCommentHam" access="public" returnType="void" roles="admin" output="false"
hint="Reports a comment to the Akismet service.">

<cfargument name="id" type="uuid" required="true">

<cfset var theComment = getComment(arguments.id)>
<cfset var akismetArgs = StructNew()>

<cfif variables.cfakismet.verifyKey()>
<cfset akismetArgs.CommentAuthor = theComment.name />
<cfset akismetArgs.CommentAuthorEmail = theComment.email />
<cfset akismetArgs.CommentAuthorURL = theComment.website />
<cfset akismetArgs.CommentContent = theComment.comment />
<cfset akismetArgs.Permalink = makeLink(theComment.entryidfk) />
<cfset variables.cfakismet.submitHam(ArgumentCollection=akismetArgs)>
<cfelse>
<cfset variables.utils.throw("Invalid Akismet key.")>
</cfif>

</cffunction>


<cffunction name="reportCommentSpam" access="public" returnType="void" roles="admin" output="false"
hint="Reports a comment to the Akismet service.">

<cfargument name="id" type="uuid" required="true">

<cfset var theComment = getComment(arguments.id)>
<cfset var akismetArgs = StructNew()>

<cfif variables.cfakismet.verifyKey()>
<cfset akismetArgs.CommentAuthor = theComment.name />
<cfset akismetArgs.CommentAuthorEmail = theComment.email />
<cfset akismetArgs.CommentAuthorURL = theComment.website />
<cfset akismetArgs.CommentContent = theComment.comment />
<cfset akismetArgs.Permalink = makeLink(theComment.entryidfk) />
<cfset variables.cfakismet.submitSpam(ArgumentCollection=akismetArgs)>
<cfelse>
<cfset variables.utils.throw("Invalid Akismet key.")>
</cfif>

</cffunction>

Modify Admin Templates

Now that the core blog.cfc is updated we need to update the BlogCFC administrator.

comments.cfm

First we will add the ability to report spam. We need to make two mods to client/admin/comments.cfm to handle this, the first to report and delete the spam:

...
<!--- handle deletes --->
<cfif structKeyExists(form, "mark")>
<cfloop index="u" list="#form.mark#">
<cfset application.blog.deleteComment(u)>
</cfloop>
</cfif>

<!--- report spam --->
<cfif structKeyExists(url, "spamID")>
<cfset application.blog.reportCommentSpam(url.spamID)>
<cfset application.blog.deleteComment(url.spamID)>
</cfif>

<cfset comments = application.blog.getComments(sortdir="desc")>
...

and the second to add the link to trigger the above processing:

...
<cfmodule template="../tags/datatable.cfm" data="#comments#" editlink="comment.cfm" label="Comments"
linkcol="comment" defaultsort="posted" defaultdir="desc" showAdd="false">

<cfmodule template="../tags/datacol.cfm" colname="name" label="Name" width="150" />
<cfmodule template="../tags/datacol.cfm" colname="entrytitle" label="Entry" width="300" left="100" />
<cfmodule template="../tags/datacol.cfm" colname="posted" label="Posted" format="datetime" width="150" />
<cfmodule template="../tags/datacol.cfm" colname="comment" label="Comment" left="100"/>
<cfmodule template="../tags/datacol.cfm" label="View" data="<a href=""#application.rooturl#/index.cfm?mode=entry&entry=$entryidfk$##c$id$"">View</a>" sort="false"/>
<cfif application.useakismet>
<cfmodule template="../tags/datacol.cfm" label="Spam" data="<a href=""#cgi.script_name#?spamID=$id$"" onclick=""return confirm('Are you sure you want to report the select comment as spam? This will submit the comment to the Akismet service and delete it from your database.');"">Report Spam</a>" sort="false"/>
</cfif>
</cfmodule>
...

moderate.cfm

Next we need to modify client/admin/moderate.cfm so that it will report ham (comments incorrectly marked as spam) if we approve a comment in our moderate queue:

...
<cfset application.blog.notifyEntry(entryid=c.entryidfk, message=trim(email), subject=subject, from=c.email, noadmin=true)>

<!--- Only report ham if we are not moderating all comments --->
<cfif application.useakismet and not application.commentmoderation>
<cfset application.blog.reportCommentHam(url.approve)>
</cfif>


</cfif>
...

settings.cfm

Next we update client/admin/settings.cfm to support two new blog properties, useakismet and akismetkey. First we add some validation that prevents us from using akismet with comment moderation. (If you are moderating all comments then there is no benefit to using Akismet.)

...
<cfif not len(trim(form.locale))>
<cfset arrayAppend(errors, "Your blog must have a locale.")>
</cfif>

<cfif form.useakismet and form.moderate>
<cfset arrayAppend(errors, "You cannot use Akismet with comment moderation.")>
</cfif>
...

Then a couple more bits to handle the new settings:

...
<cfif not arrayLen(errors)>
<!--- make a list of the keys we will send. --->
<cfset keylist = "blogtitle,blogdescription,blogkeywords,blogurl,commentsfrom,maxentries,offset,pingurls,dsn,blogdbtype,locale,ipblocklist,moderate,allowtrackbacks,trackbackspamlist,mailserver,mailusername,mailpassword,users,usecaptcha,useakismet,akismetkey,allowgravatars,owneremail,username,password,filebrowse,imageroot">
<cfloop index="key" list="#keylist#">
<cfif structKeyExists(form, key)>
<cfset application.blog.setProperty(key, trim(form[key]))>
</cfif>
</cfloop>
<cflocation url="index.cfm?reinit=1&settingsupdated=1" addToken="false">
</cfif>
...

...
<td align="right">use captcha:</td>
<td>
<select name="usecaptcha">
<option value="yes" <cfif form.usecaptcha>selected</cfif>>Yes</option>
<option value="no" <cfif not form.usecaptcha>selected</cfif>>No</option>
</select>
</td>
</tr>
<tr>
<td align="right">use akismet:</td>
<td>
<select name="useakismet">
<option value="yes" <cfif form.useakismet>selected</cfif>>Yes</option>
<option value="no" <cfif not form.useakismet>selected</cfif>>No</option>
</select>
</td>
</tr>
<tr valign="top">
<td align="right">akismet key:</td>
<td><input type="text" name="akismetkey" value="#form.akismetkey#" class="txtField" maxlength="255"></td>
</tr>
<tr>
<td align="right">allow trackbacks:</td>
<td>
<select name="allowtrackbacks">
<option value="yes" <cfif form.allowtrackbacks>selected</cfif>>Yes</option>
<option value="no" <cfif not form.allowtrackbacks>selected</cfif>>No</option>
</select>
</td>
</tr>
...

adminlayout.cfm

We also need to modify client/tags/adminlayout.cfm so that we see any comments waiting for moderation. Basically we change the logic to show the "Moderate Comments" link in the menu any time we have comments to be moderated.

...
<li><a href="categories.cfm">Categories</a></li>
<li><a href="comments.cfm">Comments</a></li>
<cfif application.blog.getNumberUnmoderated() gt 0>
<li><a href="moderate.cfm">Moderate Comments (<cfoutput>#application.blog.getNumberUnmoderated()#</cfoutput>)</a></li>
</cfif>
<li><a href="index.cfm?reinit=1">Refresh Blog Cache</a></li>
<cfif application.settings>
<li><a href="settings.cfm">Settings</a></li>
</cfif>
...

Application.cfm

Finally we just need to modify client/Application.cfm to set an application wide variable that tracks if we are using Akismet.

...
<!--- Use Captcha? --->
<cfset application.usecaptcha = application.blog.getProperty("usecaptcha")>

<cfif application.usecaptcha>
<cfset application.captcha = createObject("component","org.captcha.captchaService").init(configFile="#lylaFile#") />
<cfset application.captcha.setup() />
</cfif>

<!--- Are we using akismet --->
<cfset application.useakismet = application.blog.getProperty("useakismet")>

<!--- clear scopecache --->
<cfmodule template="tags/scopecache.cfm" scope="application" clearall="true">

...

That should be it. I've attached a zip file with diffs for all the files I modified which you can apply to version 5.9 of blogCFC.

I will say that I've been using the Akismet service for a little over ten days now and so far it has correctly moderated about 15 spam comments. It did let one through, but with this mod I was easily able to report it as spam and delete it from my blog. So if you are running BlogCFC and thinking of upgrading to version 5.9, I highly recommend giving this mod a try.

Related Blog Entries

Comments
Scott P's Gravatar cool - thanks
# Posted By Scott P | 10/18/07 7:14 PM
James Marshall's Gravatar I'd just like to thank you for posting this! Yesterday I migrated my blog from WordPress to BlogCFC and one of my concerns was spam (which I never had a problem with in WP using Akismet).

The instructions worked for me, but it wasn't until I got to the end of the post that I realised you had posted a ZIP, so I did the changes manually!
# Posted By James Marshall | 10/19/07 6:10 AM
Nathan Mische's Gravatar @James - I'm glad the instructions worked for you. I've edited the post to mention the download before I dive into the mods.
# Posted By Nathan Mische | 10/19/07 8:50 AM
Mike Henke's Gravatar Thanks. You inspired me to ask for help with getting CFAkismet into Machblog in Machii's user group. And it was answered http://tinyurl.com/38cd76
# Posted By Mike Henke | 10/19/07 11:38 AM
Greg Nilsen's Gravatar Wow...nice work, and thanks for helping us figure this out!
# Posted By Greg Nilsen | 11/5/07 1:04 AM
Sebastiaan's Gravatar Hi,

how about using CFFormProtect instead, which incorporates CFAkismet?

Then people don't need ot fill in those silly CAPTCHA's ;-)
# Posted By Sebastiaan | 10/23/08 5:35 AM
Nathan Mische's Gravatar @Sebastiaan - If someone publishes detailed instructions on how to integrate CFFormProtect into BlogCFC such that I can easily report spam back to Akismet I'll consider it ;)
# Posted By Nathan Mische | 10/23/08 3:20 PM
BlogCFC was created by Raymond Camden. This blog is running version 5.8.001.