Merge pull request #5332 from emeraldarcher/issue/5331

Update Google Recaptcha API

Fixes #5331
This commit is contained in:
Sébastien Ros
2015-07-23 12:11:06 -07:00
9 changed files with 182 additions and 100 deletions

View File

@@ -12,18 +12,21 @@ using Orchard.Localization;
using Orchard.Logging;
using Orchard.UI.Admin;
using Orchard.UI.Notify;
using Orchard.Services;
namespace Orchard.AntiSpam.Drivers {
public class ReCaptchaPartDriver : ContentPartDriver<ReCaptchaPart> {
private readonly INotifier _notifier;
private readonly IJsonConverter _jsonConverter;
private readonly IWorkContextAccessor _workContextAccessor;
private const string ReCaptchaUrl = "http://www.google.com/recaptcha/api";
private const string ReCaptchaSecureUrl = "https://www.google.com/recaptcha/api";
private const string ReCaptchaSecureUrl = "https://www.google.com/recaptcha/api/siteverify";
public ReCaptchaPartDriver(
INotifier notifier,
IJsonConverter jsonConverter,
IWorkContextAccessor workContextAccessor) {
_notifier = notifier;
_jsonConverter = jsonConverter;
_workContextAccessor = workContextAccessor;
T = NullLocalizer.Instance;
Logger = NullLogger.Instance;
@@ -67,67 +70,55 @@ namespace Orchard.AntiSpam.Drivers {
return null;
}
var submitViewModel = new ReCaptchaPartSubmitViewModel();
var context = workContext.HttpContext;
if(updater.TryUpdateModel(submitViewModel, String.Empty, null, null)) {
var context = workContext.HttpContext;
try {
var result = ExecuteValidateRequest(
settings.PrivateKey,
context.Request.ServerVariables["REMOTE_ADDR"],
context.Request.Form["g-recaptcha-response"]
);
try {
var result = ExecuteValidateRequest(
settings.PrivateKey,
context.Request.ServerVariables["REMOTE_ADDR"],
submitViewModel.recaptcha_challenge_field,
submitViewModel.recaptcha_response_field
);
ReCaptchaPartResponseModel responseModel = _jsonConverter.Deserialize<ReCaptchaPartResponseModel>(result);
if (!HandleValidateResponse(context, result)) {
_notifier.Error(T("The text you entered in the Captcha field does not match the image"));
updater.AddModelError("", T("The text you entered in the Captcha field does not match the image"));
if (!responseModel.Success) {
for (int i = 0; i < responseModel.ErrorCodes.Length; i++) {
if (responseModel.ErrorCodes[i] == "missing-input-response") {
_notifier.Error(T("The Captcha field is required"));
}
else {
_notifier.Error(T("There was an error with the Captcha please try again"));
Logger.Information("Error occurred while submitting a reCaptcha: " + responseModel.ErrorCodes[i]);
}
}
}
catch(Exception e) {
Logger.Error(e, "An unexcepted error occured while submitting a reCaptcha");
updater.AddModelError("Parts_ReCaptcha_Fields", T("There was an error while validating the Captcha image"));
}
}
catch (Exception e) {
Logger.Error(e, "An unexcepted error occurred while submitting a reCaptcha");
updater.AddModelError("Parts_ReCaptcha_Fields", T("There was an error while validating the Captcha image"));
}
return Editor(part, shapeHelper);
}
private static string ExecuteValidateRequest(string privateKey, string remoteip, string challenge, string response) {
WebRequest request = WebRequest.Create(ReCaptchaUrl + "/verify");
request.Method = "POST";
private static string ExecuteValidateRequest(string privateKey, string remoteip, string response) {
var postData = String.Format(CultureInfo.InvariantCulture,
"secret={0}&response={1}&remoteip={2}",
privateKey,
response,
remoteip
);
WebRequest request = WebRequest.Create(ReCaptchaSecureUrl + "?" + postData);
request.Method = "GET";
request.Timeout = 5000; //milliseconds
request.ContentType = "application/x-www-form-urlencoded";
var postData = String.Format(CultureInfo.InvariantCulture,
"privatekey={0}&remoteip={1}&challenge={2}&response={3}",
privateKey,
remoteip,
challenge,
response
);
byte[] content = Encoding.UTF8.GetBytes(postData);
using (Stream stream = request.GetRequestStream()) {
stream.Write(content, 0, content.Length);
}
using (WebResponse webResponse = request.GetResponse()) {
using (var reader = new StreamReader(webResponse.GetResponseStream())) {
return reader.ReadToEnd();
}
}
}
internal static bool HandleValidateResponse(HttpContextBase context, string response) {
if (!String.IsNullOrEmpty(response)) {
string[] results = response.Split('\n');
if (results.Length > 0) {
bool rval = Convert.ToBoolean(results[0], CultureInfo.InvariantCulture);
return rval;
}
}
return false;
}
}
}

View File

@@ -50,6 +50,10 @@
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.CSharp" />
<Reference Include="Newtonsoft.Json, Version=4.5.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\..\..\lib\newtonsoft.json\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Data" />
<Reference Include="System.ComponentModel.DataAnnotations">
@@ -124,6 +128,7 @@
<Compile Include="Services\AkismetSpamFilterProvider.cs" />
<Compile Include="Services\ISpamEventHandler.cs" />
<Compile Include="Services\MissingFilterBanner.cs" />
<Compile Include="Services\MissingSettingsBanner.cs" />
<Compile Include="Services\TypePadSpamFilterProvider.cs" />
<Compile Include="Services\DefaultSpamFilterProvider.cs" />
<Compile Include="Services\NullSpamFilterProvider.cs" />

View File

@@ -0,0 +1,31 @@
using System.Collections.Generic;
using Orchard.AntiSpam.Models;
using Orchard.ContentManagement;
using Orchard.Localization;
using Orchard.UI.Admin.Notification;
using Orchard.UI.Notify;
using System.Web.Mvc;
namespace Orchard.AntiSpam.Services {
public class MissingSettingsBanner : INotificationProvider {
private readonly IOrchardServices _orchardServices;
public MissingSettingsBanner(IOrchardServices orchardServices) {
_orchardServices = orchardServices;
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
public IEnumerable<NotifyEntry> GetNotifications() {
var workContext = _orchardServices.WorkContext;
var RecaptchaSettings = workContext.CurrentSite.As<ReCaptchaSettingsPart>();
if (RecaptchaSettings.PublicKey == null || RecaptchaSettings.PrivateKey == null) {
var urlHelper = new UrlHelper(workContext.HttpContext.Request.RequestContext);
var url = urlHelper.Action("Spam", "Admin", new { Area = "Settings" });
yield return new NotifyEntry { Message = T("The <a href=\"{0}\">ReCaptcha settings</a> need to be configured.", url), Type = NotifyType.Warning };
}
}
}
}

View File

@@ -1,10 +1,15 @@
namespace Orchard.AntiSpam.ViewModels {
using Newtonsoft.Json;
namespace Orchard.AntiSpam.ViewModels {
public class ReCaptchaPartEditViewModel {
public string PublicKey { get; set; }
}
public class ReCaptchaPartSubmitViewModel {
public string recaptcha_challenge_field { get; set; }
public string recaptcha_response_field { get; set; }
public class ReCaptchaPartResponseModel {
[JsonProperty("success")]
public string Success { get; set; }
[JsonProperty("error-codes")]
public string[] ErrorCodes { get; set; }
}
}

View File

@@ -1,8 +1,32 @@
@model Orchard.AntiSpam.ViewModels.ReCaptchaPartEditViewModel
<script type="text/javascript" src="//www.google.com/recaptcha/api/challenge?k=@Model.PublicKey"></script>
<noscript>
<iframe src="//www.google.com/recaptcha/api/noscript?k=@Model.PublicKey" height="300" width="500" frameborder="0"></iframe><br>
<textarea name="recaptcha_challenge_field" rows="3" cols="40">
</textarea>
<input type="hidden" name="recaptcha_response_field" value="manual_challenge">
</noscript>
@{
var publicKey = (string)Model.PublicKey;
}
@if (!HasText(publicKey)) {
<script src='https://www.google.com/recaptcha/api.js'></script>
<fieldset>
<div class="g-recaptcha" data-sitekey="@publicKey"></div>
<noscript>
<div style="width: 302px; height: 352px;">
<div style="width: 302px; height: 352px; position: relative;">
<div style="width: 302px; height: 352px; position: absolute;">
<iframe src="https://www.google.com/recaptcha/api/fallback?k=@publicKey"
frameborder="0" scrolling="no"
style="width: 302px; height:352px; border-style: none;">
</iframe>
</div>
<div style="width: 250px; height: 80px; position: absolute; border-style: none;
bottom: 21px; left: 25px; margin: 0px; padding: 0px; right: 25px;">
<textarea id="g-recaptcha-response" name="g-recaptcha-response"
class="g-recaptcha-response"
style="width: 250px; height: 80px; min-height: 80px; border: 1px solid #c1c1c1;
margin: 0px; padding: 0px; resize: none;" value="">
</textarea>
</div>
</div>
</div>
</noscript>
</fieldset>
}

View File

@@ -292,6 +292,7 @@
<Compile Include="ViewModels\FieldBindingSettings.cs" />
<Compile Include="ViewModels\PartBindingSettings.cs" />
<Compile Include="ViewModels\BlueprintsIndexViewModel.cs" />
<Compile Include="ViewModels\ReCaptchaElementViewModel.cs" />
<Compile Include="ViewModels\SubmissionViewModel.cs" />
<Compile Include="ViewModels\SubmissionsIndexViewModel.cs" />
<Compile Include="Helpers\SubmissionExtensions.cs" />

View File

@@ -7,6 +7,7 @@ using System.Web;
using Orchard.AntiSpam.Models;
using Orchard.ContentManagement;
using Orchard.DynamicForms.Elements;
using Orchard.DynamicForms.ViewModels;
using Orchard.DynamicForms.Helpers;
using Orchard.DynamicForms.Services;
using Orchard.DynamicForms.Services.Models;
@@ -19,7 +20,7 @@ namespace Orchard.DynamicForms.Validators {
_workContextAccessor = workContextAccessor;
}
private const string ReCaptchaUrl = "http://www.google.com/recaptcha/api";
private const string ReCaptchaSecureUrl = "https://www.google.com/recaptcha/api/siteverify";
protected override void OnValidate(ReCaptcha element, ValidateInputContext context) {
var workContext = _workContextAccessor.GetContext();
@@ -30,65 +31,59 @@ namespace Orchard.DynamicForms.Validators {
}
var httpContext = workContext.HttpContext;
var response = context.Values["recaptcha_response_field"];
var challenge = context.Values["recaptcha_challenge_field"];
var response = context.Values["g-recaptcha-response"];
if (context.ModelState.IsValid) {
try {
var result = ExecuteValidateRequest(
settings.PrivateKey,
httpContext.Request.ServerVariables["REMOTE_ADDR"],
challenge,
response
);
if (!HandleValidateResponse(httpContext, result)) {
var validationSettings = element.ValidationSettings;
var validationMessage = validationSettings.CustomValidationMessage.WithDefault("The text you entered in the Captcha field does not match the image. Please try again.");
context.ModelState.AddModelError("recaptcha_response_field", T(validationMessage).Text);
ReCaptchaElementResponseModel responseModel = Newtonsoft.Json.JsonConvert.DeserializeObject<ReCaptchaElementResponseModel>(result);
if (!responseModel.Success) {
for (int i = 0; i < responseModel.ErrorCodes.Length; i++) {
if (responseModel.ErrorCodes[i] == "missing-input-response") {
var validationSettings = element.ValidationSettings;
var validationMessage = validationSettings.CustomValidationMessage.WithDefault("The Captcha field is required");
context.ModelState.AddModelError("g-recaptcha-response", T(validationMessage).Text);
}
else {
var validationSettings = element.ValidationSettings;
var validationMessage = validationSettings.CustomValidationMessage.WithDefault("There was an error with the Captcha please try again");
context.ModelState.AddModelError("g-recaptcha-response", T(validationMessage).Text);
Logger.Information("Error occurred while submitting a reCaptcha: " + responseModel.ErrorCodes[i]);
}
}
}
}
catch (Exception e) {
Logger.Error(e, "An unexcepted error occured while submitting a reCaptcha");
Logger.Error(e, "An unexcepted error occurred while submitting a reCaptcha");
context.ModelState.AddModelError("recaptcha_response_field", T("There was an error while validating the Captcha image.").Text);
}
}
}
private static string ExecuteValidateRequest(string privateKey, string remoteip, string challenge, string response) {
var request = WebRequest.Create(ReCaptchaUrl + "/verify");
request.Method = "POST";
private static string ExecuteValidateRequest(string privateKey, string remoteip, string response) {
var postData = String.Format(CultureInfo.InvariantCulture,
"secret={0}&response={1}&remoteip={2}",
privateKey,
response,
remoteip
);
WebRequest request = WebRequest.Create(ReCaptchaSecureUrl + "?" + postData);
request.Method = "GET";
request.Timeout = 5000; //milliseconds
request.ContentType = "application/x-www-form-urlencoded";
var postData = String.Format(CultureInfo.InvariantCulture,
"privatekey={0}&remoteip={1}&challenge={2}&response={3}",
privateKey,
remoteip,
challenge,
response
);
var content = Encoding.UTF8.GetBytes(postData);
using (var stream = request.GetRequestStream()) {
stream.Write(content, 0, content.Length);
}
using (var webResponse = request.GetResponse()) {
using (WebResponse webResponse = request.GetResponse()) {
using (var reader = new StreamReader(webResponse.GetResponseStream())) {
return reader.ReadToEnd();
}
}
}
internal static bool HandleValidateResponse(HttpContextBase context, string response) {
if (!String.IsNullOrEmpty(response)) {
var results = response.Split('\n');
if (results.Length > 0) {
var rval = Convert.ToBoolean(results[0], CultureInfo.InvariantCulture);
return rval;
}
}
return false;
}
}
}

View File

@@ -0,0 +1,11 @@
using Newtonsoft.Json;
namespace Orchard.DynamicForms.ViewModels {
public class ReCaptchaElementResponseModel {
[JsonProperty("success")]
public bool Success { get; set; }
[JsonProperty("error-codes")]
public string[] ErrorCodes { get; set; }
}
}

View File

@@ -7,14 +7,33 @@
}
@if (!String.IsNullOrWhiteSpace(publicKey)) {
@tagBuilder.StartElement
<script type="text/javascript" src="//www.google.com/recaptcha/api/challenge?k=@publicKey"></script>
<noscript>
<iframe src="//www.google.com/recaptcha/api/noscript?k=@publicKey" height="280" width="500" frameborder="0"></iframe><br>
<textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
<input type="hidden" name="recaptcha_response_field" value="manual_challenge">
</noscript>
<script src='https://www.google.com/recaptcha/api.js'></script>
<fieldset>
<div class="g-recaptcha" data-sitekey="@publicKey"></div>
<noscript>
<div style="width: 302px; height: 352px;">
<div style="width: 302px; height: 352px; position: relative;">
<div style="width: 302px; height: 352px; position: absolute;">
<iframe src="https://www.google.com/recaptcha/api/fallback?k=@publicKey"
frameborder="0" scrolling="no"
style="width: 302px; height:352px; border-style: none;">
</iframe>
</div>
<div style="width: 250px; height: 80px; position: absolute; border-style: none;
bottom: 21px; left: 25px; margin: 0px; padding: 0px; right: 25px;">
<textarea id="g-recaptcha-response" name="g-recaptcha-response"
class="g-recaptcha-response"
style="width: 250px; height: 80px; min-height: 80px; border: 1px solid #c1c1c1;
margin: 0px; padding: 0px; resize: none;" value="">
</textarea>
</div>
</div>
</div>
</noscript>
</fieldset>
@tagBuilder.EndElement
if (element.ValidationSettings.ShowValidationMessage == true) {
@Html.ValidationMessage("recaptcha_response_field")
@Html.ValidationMessage("g-recaptcha-response")
}
}