Notice: Trying to access array offset on value of type bool in /home/customer/www/newslover.in/public_html/wp-content/plugins/easy-google-adsense/main.php on line 185

I Have Added reCaptcha to My BlogEngine.Net Blog!

I closed the API hole. Instructions added at the end.

After switching my blog software to use BlogEngine.Net, I noticed an increase in spam attacks to my comments.

The old blog required people to sign in and create an account and while I got the occasional spam in it, it was minimal.

In this one, it was 2 to 3 per day and that bothered me.

Thus, I started to look in to reCaptcha and seeing how to implement it.

reCaptcha actually does a very good thing for developers and wraps its API in to language specific wrappers. They have one for ASP.Net even and it appeared at first glance to be a pretty easy plug and play.

Three days later of continued effort, I have discovered that it’s anything but – when it comes to BlogEngine.Net – and not due to reCaptcha’s API but rather, due to the fact that it has to be validated at the server and BlogEngine.Net submits comments to posts through AJAX.

In order to implement, the following steps had to be taken:

Convert comment posting to non-Ajax (post back)

Create post-back function that handles all the steps of adding a post.

Add the comment

Nest the comment if needed

Send notifications

This was a pretty incredible amount of effort to figure out and thus, I would like to save others the effort by sharing how to do it.

Enjoy!


Step 1: Download the reCaptcha API and set up an account

You can download the API for .Net here. Save it somewhere you can get back to it. Extract it, you need the extract in the next step.

You can sign up for a free account here. Save your key somewhere safe.

Step 2: Add the reCaptcha library to the BlogEngine.Net web site.

You need to upload the Recaptcha.dll file to the BIN folder of your BlogEngine.Net site. You don’t need to copy the PDB file if you are not going to be editing the source code of the reCaptcha library.

If you do plan on editing the reCaptcha library (or looking up code in the case of a failure) then by all means copy the PDB over.

I previously wrote that there was no need to copy the PDB at all and I was surprised it was distributed. I was wrong. The nice folks at reCaptcha (Adrian Godong) pointed out my error. Most specifically, source code can be found here.

If you have no plan to edit the files (or look at them) leave the PDB out.

Step 3: Modify files

This is the biggest step. You have to make changes that let you process the reCaptcha.

I’ll go through them all now, one at a time, and explain each edit.

I am starting with BlogEngine.Net version 1.5.0.7 (the latest as of this writing)

First file to modify: /User Controls/CommentView.ascx

This file drives the display of the comments-at-large and that includes the box that you type a new comment in to.

At the top of the file, below the first line (below the line with the @Control directive), add:

<%@ Register TagPrefix=”recaptcha” Namespace=”Recaptcha” Assembly=”Recaptcha” %>

This registers the reCaptcha API control so that we can reference it later.

Since we are going to be working in non-Ajax mode, I added a hidden input to hold the nested comment ID if the user wants to nest their comment (aka “reply to” another comment).

Add the following line

<input type=”hidden” id=”hiddenReplyID” runat=”server” />

place it under the line for “<asp:HiddenField runat=”Server” ID=”hiddenReplyTo”  />”

I suspect I might have been able to use the “replytoId” but I wasn’t 100% sure and went with safe – which was to add my own field.

There is a drop down list for Countries, titled “ddlCountry” – you need to change the view state to save since we will be posting back to the server.

<asp:DropDownList runat=”server” ID=”ddlCountry” onchange=”BlogEngine.setFlag(this.value)” TabIndex=”5″EnableViewState=”true” ValidationGroup=”AddComment” />&nbsp;

The checkbox for “Send me a notification when comments are added” needs to be readable from the post back (not a client-only checkbox) so we need to add the runat tag:

<input type=”checkbox” id=”cbNotify” runat=”server” style=”width: auto” tabindex=”7″ />

Now, wherever you want the reCaptcha control, you need to add it. I placed mine below the checkbox.

To add the control, you add a tag in similar to the following:

      <recaptcha:RecaptchaControl
          ID="recaptcha"
            runat="server"
            PublicKey="REPLACE THIS"            
            PrivateKey="REPLACE THIS"
        />

Make sure to replace the “REPLACE THIS” sections with the keys you saved from Step 2 above.

Since we are going to be posting back when we save, you now need to change the “submit” button to a non-AJAX control and wire it to a function we will describe later called “AddLocalComment”.

Here is the new button:

<asp:button id=”bnSave” onclick=”AddLocalComment” runat=”server” tabindex=”7″OnClientClick=”if(!Page_ClientValidate(‘AddComment’)) return false;” />

Calling the “Page_ClientValidate” will cause the client side validation to “fire” and if it fails, the function will “return false” which causes the browser to ignore that it was clicked (stopping a submit of the form since the fields aren’t correct).

Scrolling down a bit you’ll find a JavaScript function called “RegisterCommentBox” and in it, a bunch of assignments to variables.

We need to add one for our hidden field we added.

2 new lines, added before the close of the function:

BlogEngine.comments.replyFieldID = ‘<%= hiddenReplyID.ClientID %>’;
BlogEngine.comments.cbNotify = BlogEngine.$(“<%=cbNotify.ClientID %>”);

That ends the edits for this file, setting it up to be able to handle adding comments in a non-AJAX manner.

Next up, we are going to modify the “blog.js” file in the web application root (“/”).

What we are doing is adding handlers to update our hidden field when the nesting and un-nesting occurs.

The “replytoComment” function needs to be changed to the following. This starts on line 81:

replyToComment: function(id) {

// set hidden value
BlogEngine.comments.replyToId.value = id;

// move comment form into position
var commentForm = BlogEngine.$(‘comment-form’);
if (!id || id == ” || id == null || id == ‘00000000-0000-0000-0000-000000000000’) {
// move to after comment list
var base = BlogEngine.$(“commentlist”);
base.appendChild(commentForm);
// hide cancel button
BlogEngine.$(‘cancelReply’).style.display = ‘none’;
if (document.getElementById(BlogEngine.comments.replyFieldID)) {
document.getElementById(BlogEngine.comments.replyFieldID).value = “”;
}
} else {
// show cancel
BlogEngine.$(‘cancelReply’).style.display = ”;

// move to nested position
var parentComment = BlogEngine.$(‘id_’ + id);
var replies = BlogEngine.$(‘replies_’ + id);

// add if necessary
if (replies == null) {
replies = document.createElement(‘div’);
replies.className = ‘comment-replies’;
replies.setAttribute(‘id’) = ‘replies_’ + id;
parentComment.appendChild(replies);
}
replies.style.display = ”;
replies.appendChild(commentForm);
if (document.getElementById(BlogEngine.comments.replyFieldID)) {
document.getElementById(BlogEngine.comments.replyFieldID).value = id;
}
}

BlogEngine.comments.nameBox.focus();
}

The parts I added (four lines) are the parts with document.getElementById – I didn’t use the “BlogEngine.$” shortcut syntax though I very well could have.

The “appendComment” function also gains a line. Find the comment “// reset reply to” and add right below it:

if (BlogEngine.comments.replyToId) BlogEngine.comments.replyToId.value = ”;

The “AddComment” function still references cbNotify by the client ID but cbNotify is now a server control, so the “AddComment” function needs a line changed.

Change:

var notify = BlogEngine.$(“cbNotify”).checked;

to this:

var notify = BlogEngine.comments.cbNotify ? BlogEngine.comments.cbNotify.checked : false;

One last edit, at the near-bottom of the file.

There is a structure defined for comments, starting with the line “comments: {” (If you’ve added the updates above and are on my version, that line will be line 506)

At the end of the structure, where it ends with “replyToId: null” we need to add two lines, so change that line to:

replyToId: null,
replyFieldID: null,
cbNotify: null

That’s it for this file.

It didn’t really change much. We are adding tracking to nesting and unnesting. ;

Our third, last, but most significantly affected file is the code-behind file for the ASCX control we edit before.

That file, specifically, is /User Controls/CommentView.ascx.cs

There are a LOT of changes we are going to have to make here.

The Page_Load function changes significantly. My best advice, rather than go line by line is to change it by copy/paste to this:

protected void Page_Load(object sender, EventArgs e)
{
bnSave.Text = Resources.labels.saveComment;

if (Post == null)
Response.Redirect(Utils.RelativeWebRoot);

if(!Page.IsPostBack && !Page.IsCallback)
{
if(Page.User.Identity.IsAuthenticated)
{
if(Request.QueryString[“deletecomment”] != null)
DeleteComment();

if(Request.QueryString[“deletecommentandchildren”] != null)
DeleteCommentAndChildren();

if(!string.IsNullOrEmpty(Request.QueryString[“approvecomment”]))
ApproveComment();

if(!string.IsNullOrEmpty(Request.QueryString[“approveallcomments”]))
ApproveAllComments();
}

if(BlogSettings.Instance.IsCommentsEnabled)
{

if(!Post.IsCommentsEnabled || (BlogSettings.Instance.DaysCommentsAreEnabled > 0 &&
Post.DateCreated.AddDays(BlogSettings.Instance.DaysCommentsAreEnabled) < DateTime.Now.Date)) { phAddComment.Visible = false; lbCommentsDisabled.Visible = true; } GetCookie(); hfCaptcha.Value = Guid.NewGuid().ToString(); } else { phAddComment.Visible = false; } //InititializeCaptcha(); BindCountries(); } string theme = BlogSettings.Instance.Theme; if(Request.QueryString["theme"] != null) theme = Request.QueryString["theme"]; string path = Utils.RelativeWebRoot + "themes/" + theme + "/CommentView.ascx"; phComments.Controls.Clear(); if(NestingSupported) { // newer, nested comments AddNestedComments(path, Post.NestedComments, phComments); } else { // old, non nested code //Add approved Comments foreach(Comment comment in Post.Comments) { CommentViewBase control = (CommentViewBase) LoadControl(path); if(comment.IsApproved || !BlogSettings.Instance.EnableCommentsModeration) { control.Comment = comment; control.Post = Post; phComments.Controls.Add(control); } } //Add unapproved comments foreach(Comment comment in Post.Comments) { CommentViewBase control = (CommentViewBase) LoadControl(path); if(!comment.IsApproved && Page.User.Identity.IsAuthenticated) { control.Comment = comment; control.Post = Post; phComments.Controls.Add(control); } } } if(IsPostBack || Page.IsCallback) { //Add the blog.js back in. Master page only adds it when not post back string script = "“;
Page.ClientScript.RegisterStartupScript(GetType(), Utils.RelativeWebRoot + “blog.js”.GetHashCode().ToString(), script);
}

if(hiddenReplyID.Value.Length > 0)
{
Page.ClientScript.RegisterStartupScript(GetType(), “HiddenReplyRefresh”.GetHashCode().ToString(),

string.Format(@”

“, hiddenReplyID.Value));
}
//Page.Header.Controls.Add(oJSHeader);

Page.ClientScript.GetCallbackEventReference(this, “arg”, null, string.Empty);
}

For reference purposes (so that if you have to change a different version of the BlogEngine) here is the original Page_Load before I touched it. You can use this to “diff” the changes to see what was done.

protected void Page_Load(object sender, EventArgs e)
{
if (Post == null)
Response.Redirect(Utils.RelativeWebRoot);

if (!Page.IsPostBack && !Page.IsCallback)
{
if (Page.User.Identity.IsAuthenticated)
{
if (Request.QueryString[“deletecomment”] != null)
DeleteComment();

if (Request.QueryString[“deletecommentandchildren”] != null)
DeleteCommentAndChildren();

if (!string.IsNullOrEmpty(Request.QueryString[“approvecomment”]))
ApproveComment();

if (!string.IsNullOrEmpty(Request.QueryString[“approveallcomments”]))
ApproveAllComments();
}

string theme = BlogSettings.Instance.Theme;
if (Request.QueryString[“theme”] != null)
theme = Request.QueryString[“theme”];

string path = Utils.RelativeWebRoot + “themes/” + theme + “/CommentView.ascx”;

if (NestingSupported)
{
// newer, nested comments
AddNestedComments(path, Post.NestedComments, phComments);
}
else
{
// old, non nested code

//Add approved Comments
foreach (Comment comment in Post.Comments)
{
CommentViewBase control = (CommentViewBase)LoadControl(path);
if (comment.IsApproved || !BlogSettings.Instance.EnableCommentsModeration)
{
control.Comment = comment;
control.Post = Post;
phComments.Controls.Add(control);
}
}

//Add unapproved comments
foreach (Comment comment in Post.Comments)
{
CommentViewBase control = (CommentViewBase)LoadControl(path);

if (!comment.IsApproved && Page.User.Identity.IsAuthenticated)
{
control.Comment = comment;
control.Post = Post;
phComments.Controls.Add(control);
}
}

}

if (BlogSettings.Instance.IsCommentsEnabled)
{

if (!Post.IsCommentsEnabled || (BlogSettings.Instance.DaysCommentsAreEnabled > 0 &&
Post.DateCreated.AddDays(BlogSettings.Instance.DaysCommentsAreEnabled) < DateTime.Now.Date)) { phAddComment.Visible = false; lbCommentsDisabled.Visible = true; } BindCountries(); GetCookie(); hfCaptcha.Value = Guid.NewGuid().ToString(); } else { phAddComment.Visible = false; } //InititializeCaptcha(); } Page.ClientScript.GetCallbackEventReference(this, "arg", null, string.Empty); }

Here are some highlights on my changes.

Since the button in the ASCX file is no longer a client button, I update it’s label in the Page_Load instead of through an inline value assignment on the ASCX.

After the theme is applied, I check to see if our new input box has a value. If it does, we will need to re-indent the comment box when the page returns so that the user experience isn’t disrupted if their captcha attempt failed. Thus, I manually register a startup script and in it, call the javascript functions for “registerCommentBox” and “replyToComment” in order to get the comment back in to position.

Before we can call them though, the BlogEngine class has to be compiled by the client browser already, but I found an interesting tidbit when I was writing all this code. It only gets sent the first time the page loads. It disappears after post back!! As such, I add it even on post back first. (I placed a comment in the code to this affect.)

When I added the function to re-post the blog.js back to the file, I had to copy a function to the code file we are now modifying. 

Below the Page_Load, add:

///

/// Resolves the script URL. Copied from BlogBasePage.cs
///

/// The URL. ///
public virtual string ResolveScriptUrl(string url)
{
return Utils.RelativeWebRoot + “js.axd?path=” + HttpUtility.UrlEncode(url) + “&v=” + BlogSettings.Instance.Version();
}

This adds the “ResolveScriptUrl” to our page, accessible from the Page_Load function (where we added it in)

Now, for the work horse.

Below the newly added function, add one more. Here it is, in all its glory:

///

/// Adds a comment through code instead of AJAX. Complete with email notifications.
///

/// Not used /// Not used protected void AddLocalComment(object a, EventArgs b)
{
if(Page.IsValid)
{
Comment oComment = new Comment();
oComment.Author = txtName.Text;
oComment.Email = txtEmail.Text;
oComment.Content = txtContent.Text;
if(ddlCountry.SelectedItem != null)
oComment.Country = ddlCountry.SelectedItem.Value;
try
{
oComment.Website = new Uri(txtWebsite.Text);
}
catch(Exception) { }
oComment.DateCreated = DateTime.Now;
oComment.IsApproved = !BlogEngine.Core.BlogSettings.Instance.EnableCommentsModeration;
if(oComment.IsApproved == null)
oComment.IsApproved = true;
oComment.Id = Guid.NewGuid();
if(Request.ServerVariables[“REMOTE_ADDR”] != null)
oComment.IP = Request.ServerVariables[“REMOTE_ADDR”].ToString();
oComment.Parent = Post;
if(hiddenReplyID != null)
{
//Response.Write(“Hidden ID ” + hiddenReplyID.Value);
if(hiddenReplyID.Value != null && hiddenReplyID.Value.Length > 0)
try
{
oComment.Parent = Post;
oComment.ParentId = new Guid(hiddenReplyID.Value);
}
catch(Exception) { }
}
Post.AddComment(oComment);

if(cbNotify.Checked)
{
if(Post.NotificationEmails.IndexOf(oComment.Email) == -1) //not already signed up for notifications
Post.NotificationEmails.Add(oComment.Email);
}
Post.Save();

//Create a handler function if you want to track failures
//Utils.EmailFailed += new EventHandler(RecordFailure);
System.Net.Mail.MailMessage oMessage = null;
foreach(string MailTo in Post.NotificationEmails)
{
//Post.ApproveComment(oComment); //Sends the email
oMessage = new System.Net.Mail.MailMessage(
BlogSettings.Instance.Email,
MailTo,
“New Comment by ‘” + oComment.Author + “‘ on ‘” + Post.Title + “‘.”,
string.Format(@”
{1} has entered a comment on the post {3}

The content of the comment is:

{4}
“, Environment.NewLine, oComment.Author, Post.PermaLink + “#id_” + oComment.Id, Post.Title, oComment.Content).Replace(Environment.NewLine, “
“)
);
oMessage.From = new System.Net.Mail.MailAddress(BlogSettings.Instance.Email, BlogSettings.Instance.Name);
Utils.SendMailMessageAsync(oMessage);
}

if(BlogSettings.Instance.SendMailOnComment)
{
oMessage = new System.Net.Mail.MailMessage(
BlogSettings.Instance.Email,
BlogSettings.Instance.Email,
BlogSettings.Instance.EmailSubjectPrefix + ((BlogSettings.Instance.EmailSubjectPrefix == “”) ? “” : “: “) +
“New Comment by ‘” + oComment.Author + “‘ on ‘” + Post.Title + “‘.”,
string.Format(@”
{1} has entered a comment on the post {3}

The content of the comment is:

{4}
“, Environment.NewLine, oComment.Author, Post.PermaLink + “#id_” + oComment.Id, Post.Title, oComment.Content).Replace(Environment.NewLine, “
“)
);
oMessage.From = new System.Net.Mail.MailAddress(BlogSettings.Instance.Email, BlogSettings.Instance.Name);
Utils.SendMailMessageAsync(oMessage);
}

txtContent.Text = “”;
}

Page_Load(null, null); //Set the page back up
}

There is a lot to digest in what this does, but it’s completely new, so you don’t have to worry about overwriting another function. It’s what our button on the ASCX now calls when you press it.

I have found that when you add a comment in BlogEngine.Net, you have to do EVERYTHING that pertains to the comment. Set all properties, send all notifications, etc. Thus, there’s quite a bit of code. You should be able to get the gist of what I’m doing when you read it.

Notice: Scott Marlowe notes in the comments that you can add a way for people’s browsers to automagically scroll down to their new comment. Great idea! Check his post out for details.

One final change is in the BindCountries function. Since we made the countries drop down to be view state enabled, it’s good practice to clear the drop list before we add items.

At the top of the function, add:

ddlCountry.Items.Clear();

That’s it.

Update: One additional step

We need to close the ability to emulate the API to add a comment directly.

In the CommentView.ascx.cs file, find the RaiseCallbackEvent function.

Place the following at the top, but after the variables are filled:

if(!isPreview)

{

throw new ApplicationException(“This operation is no longer allowed.”);

}

Upload your files, you should be operating now.

Enjoy.

Leave a Reply