From: <ha...@us...> - 2006-09-26 10:35:29
|
Revision: 1775 http://svn.sourceforge.net/subtext/?rev=1775&view=rev Author: haacked Date: 2006-09-26 03:35:16 -0700 (Tue, 26 Sep 2006) Log Message: ----------- Implemented an InvisibleCaptcha control. Will post a blog entry about it. Modified Paths: -------------- branches/Release1.9/SubtextSolution/Subtext.Akismet/AkismetClient.cs branches/Release1.9/SubtextSolution/Subtext.Akismet/HttpClient.cs branches/Release1.9/SubtextSolution/Subtext.Framework/CommentFilter.cs branches/Release1.9/SubtextSolution/Subtext.Web/UI/Controls/PostComment.Designer.cs branches/Release1.9/SubtextSolution/Subtext.Web/UI/Controls/PostComment.cs branches/Release1.9/SubtextSolution/Subtext.Web/UI/Pages/SubTextMasterPage.cs branches/Release1.9/SubtextSolution/Subtext.Web/Web.config branches/Release1.9/SubtextSolution/Subtext.Web.Controls/AssemblyInfo.cs branches/Release1.9/SubtextSolution/Subtext.Web.Controls/Subtext.Web.Controls.csproj Added Paths: ----------- branches/Release1.9/SubtextSolution/Subtext.Web.Controls/InvisibleCaptcha.cs branches/Release1.9/SubtextSolution/Subtext.Web.Controls/Resources/InvisibleCaptcha.js Modified: branches/Release1.9/SubtextSolution/Subtext.Akismet/AkismetClient.cs =================================================================== --- branches/Release1.9/SubtextSolution/Subtext.Akismet/AkismetClient.cs 2006-09-26 09:08:48 UTC (rev 1774) +++ branches/Release1.9/SubtextSolution/Subtext.Akismet/AkismetClient.cs 2006-09-26 10:35:16 UTC (rev 1775) @@ -143,7 +143,12 @@ public bool VerifyApiKey() { string parameters = "key=" + HttpUtility.UrlEncode(this.ApiKey) + "&blog=" + HttpUtility.UrlEncode(this.BlogUrl.ToString()); - return String.Equals("valid", this.httpClient.PostRequest(verifyUrl, this.UserAgent, this.Timeout, parameters), StringComparison.InvariantCultureIgnoreCase); + string result = this.httpClient.PostRequest(verifyUrl, this.UserAgent, this.Timeout, parameters); + + if (String.IsNullOrEmpty(result)) + throw new InvalidResponseException("Akismet returned an empty response"); + + return String.Equals("valid", result, StringComparison.InvariantCultureIgnoreCase); } /// <summary> @@ -154,9 +159,13 @@ public bool CheckCommentForSpam(IComment comment) { string result = SubmitComment(comment, this.checkUrl); + + if (String.IsNullOrEmpty(result)) + throw new InvalidResponseException("Akismet returned an empty response"); + if (result != "true" && result != "false") throw new InvalidResponseException(string.Format("Received the response '{0}' from Akismet. Probably a bad API key.", result)); - + return bool.Parse(result); } Modified: branches/Release1.9/SubtextSolution/Subtext.Akismet/HttpClient.cs =================================================================== --- branches/Release1.9/SubtextSolution/Subtext.Akismet/HttpClient.cs 2006-09-26 09:08:48 UTC (rev 1774) +++ branches/Release1.9/SubtextSolution/Subtext.Akismet/HttpClient.cs 2006-09-26 10:35:16 UTC (rev 1775) @@ -53,8 +53,7 @@ { responseText = reader.ReadToEnd(); } - if (String.IsNullOrEmpty(responseText)) - throw new InvalidResponseException("Akismet returned an empty response", response.StatusCode); + return responseText; } } Modified: branches/Release1.9/SubtextSolution/Subtext.Framework/CommentFilter.cs =================================================================== --- branches/Release1.9/SubtextSolution/Subtext.Framework/CommentFilter.cs 2006-09-26 09:08:48 UTC (rev 1774) +++ branches/Release1.9/SubtextSolution/Subtext.Framework/CommentFilter.cs 2006-09-26 10:35:16 UTC (rev 1775) @@ -77,7 +77,7 @@ feedbackItem.FlaggedAsSpam = true; feedbackItem.Approved = false; FeedbackItem.Update(feedbackItem); - throw new CommentSpamException("Sorry, but your comment has been flagged as spam."); + return; } } feedbackItem.Approved = true; Modified: branches/Release1.9/SubtextSolution/Subtext.Web/UI/Controls/PostComment.Designer.cs =================================================================== --- branches/Release1.9/SubtextSolution/Subtext.Web/UI/Controls/PostComment.Designer.cs 2006-09-26 09:08:48 UTC (rev 1774) +++ branches/Release1.9/SubtextSolution/Subtext.Web/UI/Controls/PostComment.Designer.cs 2006-09-26 10:35:16 UTC (rev 1775) @@ -1,4 +1,5 @@ using System; +using Subtext.Web.Controls; namespace Subtext.Web.UI.Controls { @@ -13,6 +14,7 @@ protected Subtext.Web.Controls.CompliantButton btnCompliantSubmit; protected System.Web.UI.WebControls.Label Message; protected System.Web.UI.WebControls.CheckBox chkRemember; + protected InvisibleCaptcha invisibleCaptchaValidator; protected SubtextCoComment coComment; } } Modified: branches/Release1.9/SubtextSolution/Subtext.Web/UI/Controls/PostComment.cs =================================================================== --- branches/Release1.9/SubtextSolution/Subtext.Web/UI/Controls/PostComment.cs 2006-09-26 09:08:48 UTC (rev 1774) +++ branches/Release1.9/SubtextSolution/Subtext.Web/UI/Controls/PostComment.cs 2006-09-26 10:35:16 UTC (rev 1775) @@ -25,6 +25,7 @@ using Subtext.Framework.Exceptions; using Subtext.Framework.Text; using Subtext.Framework.Web; +using Subtext.Web.Controls; namespace Subtext.Web.UI.Controls { @@ -110,26 +111,26 @@ /// <summary> /// Called when an approved comment is added. /// </summary> - protected virtual void OnCommentAdded() + protected virtual void OnCommentApproved(FeedbackItem feedback) { + if (feedback.Approved) + { + EventHandler<EventArgs> theEvent = this.CommentApproved; + if (theEvent != null) + theEvent(this, EventArgs.Empty); + } + } + + private void RemoveCommentControls() + { for (int i = this.Controls.Count - 1; i >= 0; i--) { this.Controls.RemoveAt(i); } - Message = new Label(); - Message.Text = "Thanks for your comment!"; - Message.CssClass = "success"; - this.Controls.Add(Message); - - //TODO: Provide some means to add a new comment. For now they can click the title to refresh the page. - - EventHandler<EventArgs> theEvent = CommentPosted; - if (theEvent != null) - theEvent(this, EventArgs.Empty); } - - public event EventHandler<EventArgs> CommentPosted; + public event EventHandler<EventArgs> CommentApproved; + #region Web Form Designer generated code override protected void OnInit(EventArgs e) { @@ -145,15 +146,24 @@ /// </summary> private void InitializeComponent() { + int btnIndex = 0; + if(this.btnSubmit != null) { this.btnSubmit.Click += new System.EventHandler(this.btnSubmit_Click); + btnIndex = Controls.IndexOf(btnSubmit); } if(this.btnCompliantSubmit != null) { this.btnCompliantSubmit.Click += new EventHandler(this.btnSubmit_Click); + btnIndex = Controls.IndexOf(btnCompliantSubmit); } + + invisibleCaptchaValidator = new InvisibleCaptcha(); + invisibleCaptchaValidator.ErrorMessage = "Please enter the answer to the supplied question."; + + Controls.AddAt(btnIndex, invisibleCaptchaValidator); } #endregion @@ -166,23 +176,12 @@ Entry currentEntry = Cacher.GetEntryFromRequest(CacheDuration.Short); if(IsCommentAllowed(currentEntry)) { - FeedbackItem feedbackItem = new FeedbackItem(FeedbackType.Comment); - feedbackItem.Author = tbName.Text; - if(this.tbEmail != null) - feedbackItem.Email = tbEmail.Text; - feedbackItem.SourceUrl = HtmlHelper.CheckForUrl(tbUrl.Text); - feedbackItem.Body = tbComment.Text; - feedbackItem.Title = tbTitle.Text; - feedbackItem.EntryId = currentEntry.Id; - feedbackItem.IpAddress = HttpHelper.GetUserIpAddress(Context); - + FeedbackItem feedbackItem = CreateFeedbackInstanceFromFormInput(currentEntry); FeedbackItem.Create(feedbackItem); + CommentFilter filter = new CommentFilter(HttpContext.Current.Cache); filter.DetermineFeedbackApproval(feedbackItem); - if (feedbackItem.Approved) - OnCommentAdded(); - if(chkRemember == null || chkRemember.Checked) { HttpCookie user = new HttpCookie("CommentUser"); @@ -193,13 +192,8 @@ user.Expires = DateTime.Now.AddDays(30); Response.Cookies.Add(user); } - - ResetCommentFields(currentEntry); - - if(Config.CurrentBlog.ModerationEnabled) - { - Message.Text = "Thank you for your comment. It will be displayed soon."; - } + + DisplayResultMessage(feedbackItem); } } catch(BaseCommentException exception) @@ -209,6 +203,46 @@ } } + private void DisplayResultMessage(FeedbackItem feedbackItem) + { + RemoveCommentControls(); + Message = new Label(); + + if (feedbackItem.Approved) + { + Message.Text = "Thanks for your comment!"; + Message.CssClass = "success"; + this.Controls.Add(Message); //This needs to be here for ajax calls. + OnCommentApproved(feedbackItem); + return; + } + else if(feedbackItem.NeedsModeratorApproval) + { + Message.Text = "Thank you for your comment. It will be displayed soon."; + Message.CssClass = "error moderation"; + } + else + { + this.Message.Text = "Sorry, but your comment was flagged as spam and will be moderated."; + this.Message.CssClass = "error"; + } + this.Controls.Add(Message); + } + + private FeedbackItem CreateFeedbackInstanceFromFormInput(Entry currentEntry) + { + FeedbackItem feedbackItem = new FeedbackItem(FeedbackType.Comment); + feedbackItem.Author = this.tbName.Text; + if(this.tbEmail != null) + feedbackItem.Email = this.tbEmail.Text; + feedbackItem.SourceUrl = HtmlHelper.CheckForUrl(this.tbUrl.Text); + feedbackItem.Body = this.tbComment.Text; + feedbackItem.Title = this.tbTitle.Text; + feedbackItem.EntryId = currentEntry.Id; + feedbackItem.IpAddress = HttpHelper.GetUserIpAddress(Context); + return feedbackItem; + } + private void ResetCommentFields(Entry entry) { if (this.tbComment != null) Modified: branches/Release1.9/SubtextSolution/Subtext.Web/UI/Pages/SubTextMasterPage.cs =================================================================== --- branches/Release1.9/SubtextSolution/Subtext.Web/UI/Pages/SubTextMasterPage.cs 2006-09-26 09:08:48 UTC (rev 1774) +++ branches/Release1.9/SubtextSolution/Subtext.Web/UI/Pages/SubTextMasterPage.cs 2006-09-26 10:35:16 UTC (rev 1775) @@ -91,7 +91,7 @@ else if (controlId.Equals("PostComment.ascx")) { postCommentControl = (PostComment)control; - postCommentControl.CommentPosted += new EventHandler<EventArgs>(postCommentControl_CommentPosted); + postCommentControl.CommentApproved += new EventHandler<EventArgs>(postCommentControl_CommentPosted); apnlCommentsWrapper.Controls.Add(control); CenterBodyControl.Controls.Add(apnlCommentsWrapper); } Modified: branches/Release1.9/SubtextSolution/Subtext.Web/Web.config =================================================================== --- branches/Release1.9/SubtextSolution/Subtext.Web/Web.config 2006-09-26 09:08:48 UTC (rev 1774) +++ branches/Release1.9/SubtextSolution/Subtext.Web/Web.config 2006-09-26 10:35:16 UTC (rev 1775) @@ -364,18 +364,18 @@ timeout="60" /> </authentication> - <httpHandlers> - <!-- + <httpHandlers> + <!-- Can not see to load asmx like .aspx, since we will grab all requests later, make sure these are processed by their default factory --> <add verb="*" path="*.asmx" type="System.Web.Services.Protocols.WebServiceHandlerFactory, System.Web.Services, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" validate="false"/> - <add path="WebResource.axd" verb="GET" type="System.Web.Handlers.AssemblyResourceLoader" validate="true"/> + <add path="WebResource.axd" verb="GET" type="System.Web.Handlers.AssemblyResourceLoader" validate="true" /> <!-- This is for FTB 3.0 support --> - <add verb="GET" path="FtbWebResource.axd" type="FreeTextBoxControls.AssemblyResourceHandler, FreeTextBox"/> + <add verb="GET" path="FtbWebResource.axd" type="FreeTextBoxControls.AssemblyResourceHandler, FreeTextBox" /> <!-- Since we are grabbing all requests after this, Modified: branches/Release1.9/SubtextSolution/Subtext.Web.Controls/AssemblyInfo.cs =================================================================== --- branches/Release1.9/SubtextSolution/Subtext.Web.Controls/AssemblyInfo.cs 2006-09-26 09:08:48 UTC (rev 1774) +++ branches/Release1.9/SubtextSolution/Subtext.Web.Controls/AssemblyInfo.cs 2006-09-26 10:35:16 UTC (rev 1775) @@ -17,6 +17,7 @@ using System.Reflection; using System.Runtime.InteropServices; using System.Security.Permissions; +using System.Web.UI; // // General Information about an assembly is controlled through the following @@ -29,4 +30,6 @@ [assembly: ComVisible(false)] [assembly: CLSCompliant(false)] -[assembly: SecurityPermission(SecurityAction.RequestMinimum, Execution = true)] \ No newline at end of file +[assembly: SecurityPermission(SecurityAction.RequestMinimum, Execution = true)] + +[assembly: WebResource("Subtext.Web.Controls.Resources.InvisibleCaptcha.js", "text/javascript")] \ No newline at end of file Added: branches/Release1.9/SubtextSolution/Subtext.Web.Controls/InvisibleCaptcha.cs =================================================================== --- branches/Release1.9/SubtextSolution/Subtext.Web.Controls/InvisibleCaptcha.cs (rev 0) +++ branches/Release1.9/SubtextSolution/Subtext.Web.Controls/InvisibleCaptcha.cs 2006-09-26 10:35:16 UTC (rev 1775) @@ -0,0 +1,195 @@ +using System; +using System.ComponentModel; +using System.Globalization; +using System.Security.Cryptography; +using System.Text; +using System.Web.UI; +using System.Web.UI.WebControls; + +namespace Subtext.Web.Controls +{ + /// <summary> + /// <para>Simple CAPTCHA control that requires the browser to perform a + /// simple calculation via javascript to pass. + /// </para> + /// <para> + /// If javascript is not enabled, a form is rendered asking the user to add two random + /// small numbers, unless the <see cref="Accessible" /> property is set to false. + /// </para> + /// </summary> + public class InvisibleCaptcha : BaseValidator + { + Random rnd = new Random(); + string directions = string.Empty; + + /// <summary> + /// Initializes a new instance of the <see cref="InvisibleCaptcha"/> class. + /// </summary> + public InvisibleCaptcha() : base() + { + } + + /// <summary> + /// Gets or sets a value indicating whether this <see cref="InvisibleCaptcha"/> is accessible + /// to non-javascript browsers. If false, then non-javascript browsers will always fail + /// validation. + /// </summary> + /// <value><c>true</c> if accessible; otherwise, <c>false</c>.</value> + [Description("Determines whether or not this control will work for non-javascript enabled browsers")] + [DefaultValue(true)] + [Browsable(true)] + [Category("Behavior")] + public bool Accessible + { + get { return (bool)(ViewState["Accessible"] ?? true); } + set { ViewState["Accessible"] = value; } + } + + /// <summary> + /// Sets up a hashed answer. + /// </summary> + /// <param name="e"></param> + protected override void OnInit(EventArgs e) + { + Page.RegisterRequiresControlState(this); + + Page.ClientScript.RegisterHiddenField(HiddenAnswerFieldName, ""); + base.OnInit(e); + } + + /// <summary> + /// Raises the <see cref="E:System.Web.UI.Control.PreRender"></see> event. + /// </summary> + /// <param name="e">A <see cref="T:System.EventArgs"></see> that contains the event data.</param> + protected override void OnPreRender(EventArgs e) + { + int first = rnd.Next(1, 9); + int second = rnd.Next(1, 9); + + directions = string.Format("Please add {0} and {1} and type the answer here: ", first, second); + Display = ValidatorDisplay.Dynamic; + + string answer = (first + second).ToString(CultureInfo.InvariantCulture); + //A little obsfucation. + Page.ClientScript.RegisterHiddenField(HiddenAnswerHashFieldName, Convert.ToBase64String(Encoding.UTF8.GetBytes(answer))); + + Page.ClientScript.RegisterStartupScript(typeof(InvisibleCaptcha), "MakeCaptchaInvisible", string.Format("<script type=\"text/javascript\">\r\nsubtext_invisible_captcha_hideFromJavascriptEnabledBrowsers('{0}');\r\n</script>", this.CaptchaInputClientId)); + + Page.ClientScript.RegisterClientScriptInclude("InvisibleCaptcha", + Page.ClientScript.GetWebResourceUrl(this.GetType(), "Subtext.Web.Controls.Resources.InvisibleCaptcha.js")); + + Page.ClientScript.RegisterStartupScript(typeof(InvisibleCaptcha), "ComputeCaptchaAnswer", string.Format("<script type=\"text/javascript\">\r\nsubtext_invisible_captcha_setAnswer({0}, {1}, '{2}');\r\n</script>", first, second, this.HiddenAnswerFieldName)); + base.OnPreRender(e); + } + + /// <summary> + /// Displays the control on the client. + /// </summary> + /// <param name="writer">A <see cref="T:System.Web.UI.HtmlTextWriter"></see> that contains the output stream for rendering on the client.</param> + protected override void Render(HtmlTextWriter writer) + { + string answer = Page.Request.Form[HiddenAnswerFieldName]; + // In an Ajax postback, we don't want to render this if javascript is enabled + // because the page won't know to set this span to be invisible. + if (Accessible && String.IsNullOrEmpty(answer)) + { + base.Render(writer); + writer.AddAttribute("id", CaptchaInputClientId); + if (!string.IsNullOrEmpty(CssClass)) + writer.AddAttribute("class", CssClass); + writer.RenderBeginTag("span"); + writer.Write(directions); + + writer.Write("<input type=\"text\" name=\"{0}\" value=\"\" />", this.VisibleAnswerFieldName); + + writer.RenderEndTag(); + } + } + + /// <summary>Checks the properties of the control for valid values.</summary> + /// <returns>true if the control properties are valid; otherwise, false.</returns> + protected override bool ControlPropertiesValid() + { + if (!String.IsNullOrEmpty(ControlToValidate)) + { + CheckControlValidationProperty(ControlToValidate, "ControlToValidate"); + } + return true; + } + + string ComputeAnswerHash(string answer) + { + string saltedAnswer = answer + "_" + SecretCode; + Byte[] clearBytes = Encoding.UTF8.GetBytes(saltedAnswer); + Byte[] hashedBytes = new MD5CryptoServiceProvider().ComputeHash(clearBytes); + return Convert.ToBase64String(hashedBytes); + } + + string CaptchaInputClientId + { + get + { + return this.ClientID + "_subtext_captcha"; + } + } + + string VisibleAnswerFieldName + { + get + { + return ClientID + "_visibleanswer"; + } + } + + string HiddenAnswerFieldName + { + get + { + return ClientID + "_answer"; + } + } + + string HiddenAnswerHashFieldName + { + get + { + return ClientID + "_answerhash"; + } + } + + string SecretCode + { + get + { + if(Page.Cache["SecretCode"] == null) + { + Page.Cache["SecretCode"] = Guid.NewGuid().ToString(); + } + return (string)Page.Cache["SecretCode"]; + } + } + + ///<summary> + ///When overridden in a derived class, this method contains the code to determine whether the value in the input control is valid. + ///</summary> + ///<returns> + ///true if the value in the input control is valid; otherwise, false. + ///</returns> + /// + protected override bool EvaluateIsValid() + { + string answer = Page.Request.Form[HiddenAnswerFieldName]; + if(String.IsNullOrEmpty(answer)) + answer = Page.Request.Form[VisibleAnswerFieldName]; + string answerHash = ComputeAnswerHash(answer); + + string encodedExpectedAnswer = Page.Request.Form[HiddenAnswerHashFieldName]; + if (String.IsNullOrEmpty(encodedExpectedAnswer)) + return false; //Somebody is tampering with the form. + + string actualAnswer = Encoding.UTF8.GetString(Convert.FromBase64String(encodedExpectedAnswer)); + string expectedAnswerHash = ComputeAnswerHash(actualAnswer); + return answerHash == expectedAnswerHash; + } + } +} Added: branches/Release1.9/SubtextSolution/Subtext.Web.Controls/Resources/InvisibleCaptcha.js =================================================================== --- branches/Release1.9/SubtextSolution/Subtext.Web.Controls/Resources/InvisibleCaptcha.js (rev 0) +++ branches/Release1.9/SubtextSolution/Subtext.Web.Controls/Resources/InvisibleCaptcha.js 2006-09-26 10:35:16 UTC (rev 1775) @@ -0,0 +1,17 @@ +function subtext_invisible_captcha_hideFromJavascriptEnabledBrowsers(id) +{ + var captcha = document.getElementById(id); + if(captcha) + { + captcha.style.display = 'none'; + } +} + +function subtext_invisible_captcha_setAnswer(first, second, formInputId) +{ + var formInput = document.getElementById(formInputId); + if(formInput) + { + formInput.value = (first + second); + } +} \ No newline at end of file Modified: branches/Release1.9/SubtextSolution/Subtext.Web.Controls/Subtext.Web.Controls.csproj =================================================================== --- branches/Release1.9/SubtextSolution/Subtext.Web.Controls/Subtext.Web.Controls.csproj 2006-09-26 09:08:48 UTC (rev 1774) +++ branches/Release1.9/SubtextSolution/Subtext.Web.Controls/Subtext.Web.Controls.csproj 2006-09-26 10:35:16 UTC (rev 1775) @@ -164,6 +164,7 @@ <Compile Include="HelpToolTip.cs"> <SubType>Code</SubType> </Compile> + <Compile Include="InvisibleCaptcha.cs" /> <Compile Include="MasterPage.cs"> <SubType>Code</SubType> </Compile> @@ -197,6 +198,9 @@ <EmbeddedResource Include="Resources\CollapsiblePanel.js" /> <EmbeddedResource Include="Resources\ScrollPositionSaver.js" /> </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="Resources\InvisibleCaptcha.js" /> + </ItemGroup> <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> <PropertyGroup> <PreBuildEvent> This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |