umbraco / Umbraco.Forms.Issues

Public issue tracker for Umbraco Forms
29 stars 0 forks source link

Creating a Custom field type in Umbraco to supports the invisible enterprise reCaptcha #1191

Closed Namrata-V2 closed 3 months ago

Namrata-V2 commented 3 months ago

Recently, I found that Umbraco doesn't have an option to support the enterprise invisible reCaptcha. After following a bit along I did came across this documentation where we can create custom field type and have a custom validation logic.

What I have done so far :

Add a new class

ReCaptchaEnterprise.cs

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Umbraco.Forms.Core.Enums;
using Umbraco.Forms.Core.Models;
using Umbraco.Forms.Core.Services;

namespace FormsExtensions
{
    public class ReCaptchaEnterprise : Umbraco.Forms.Core.FieldType
    {
        public ReCaptchaEnterprise()
        {
            Id = new Guid("e3ab6a21-bc99-4cb8-999e-ad01203d61bd"); // Replace this!
            Name = "ReCaptcha Enterprise";
            Description = "Render a custom reCaptcha enterprise field.";
            Icon = "icon-autofill";
            SortOrder = 10;
            FieldTypeViewName = "FieldType.ReCaptchaEnterprise.cshtml";
        }

        // Custom validation in here which will occur when the form is submitted.
        // Any strings returned will cause the submission to be considered invalid.
        // Returning an empty collection of strings will indicate that it's valid to proceed.
        public override IEnumerable<string> ValidateField(Form form, Field field, IEnumerable<object> postedValues, HttpContext context, IPlaceholderParsingService placeholderParsingService, IFieldTypeStorage fieldTypeStorage)
        {
            var returnStrings = new List<string>();

            if (!postedValues.Any(value => value.ToString().ToLower().Contains("custom")))
            {
                returnStrings.Add("You need to include 'custom' in the field!");
            }

            var reCaptchaResponse = context.Request.Form["g-recaptcha-response"].ToString();
            Console.WriteLine("+++++++++++++++++++++ReCaptcha response+++++++++++++++++++++++++++" + reCaptchaResponse);

            // Also validate it against the default method (to handle mandatory fields and regular expressions)
            return base.ValidateField(form, field, postedValues, context, placeholderParsingService, fieldTypeStorage, returnStrings);
        }
    }    
} 

Partial View

FieldType.ReCaptchaEnterprise.html

@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage
@using Umbraco.Forms.Web
@using Microsoft.Extensions.Configuration
@model Umbraco.Forms.Web.Models.FieldViewModel
@inject IConfiguration Configuration
@{
    var siteKey = Configuration.GetSection("ReCaptchaEnterprise")["SiteKey"];
    if (!string.IsNullOrEmpty(siteKey))
    {
        Html.AddFormThemeScriptFile("https://www.google.com/recaptcha/enterprise.js?render=" + siteKey);
        <div class="g-recaptcha"
             data-sitekey="@siteKey"
             data-callback="onSubmit" data-size="invisible">
        </div>
         <input type="hidden" name="g-recaptcha-response" />

        <script type="application/javascript">
            function  onSubmit(event){
                event.preventDefault();
                grecaptcha.execute();
            }
        </script>
    }
    else
    {
        <p class="error">ERROR: reCAPTCHA is missing the Site Key. Please update the configuration to include a value.</p>
    }
} 

Umbraco backoffice view

App_plugins/UmbracoFotms/backoffice/Common/FieldTypes/ReCaptchaEnterprise.html

<div class="g-recaptcha" data-sitekey="<site-key>" data-size="invisible"></div> 

I am able to add the reCaptcha enterprise in the form. However, when I go to Content and add it over there via Rich Text Editor > Macro the following exception is thrown.

Received an error from the server An error occurred Cannot bind source type Umbraco.Forms.Web.Models.FieldViewModel to model type Umbraco.Cms.Core.Models.PublishedContent.IPublishedContent.

Exception Details Umbraco.Cms.Web.Common.ModelBinders.ModelBindingException, Umbraco.Web.Common, Version=12.0.1.0, Culture=neutral, PublicKeyToken=null: Cannot bind source type Umbraco.Forms.Web.Models.FieldViewModel to model type Umbraco.Cms.Core.Models.PublishedContent.IPublishedContent.

A complete stacktrace:

at Umbraco.Cms.Web.Common.ModelBinders.ContentModelBinder.ThrowModelBindingException(Boolean sourceContent, Boolean modelContent, Type sourceType, Type modelType) at Umbraco.Cms.Web.Common.ModelBinders.ContentModelBinder.BindModel(ModelBindingContext bindingContext, Object source, Type modelType) at Umbraco.Cms.Web.Common.Views.UmbracoViewPage1.BindViewData(ContentModelBinder contentModelBinder, ViewDataDictionary viewData) at Umbraco.Cms.Web.Common.Views.UmbracoViewPage1.set_ViewContext(ViewContext value) at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderPageCoreAsync(IRazorPage page, ViewContext context) at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderPageAsync(IRazorPage page, ViewContext context, Boolean invokeViewStarts) at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderAsync(ViewContext context) at Microsoft.AspNetCore.Mvc.ViewFeatures.HtmlHelper.RenderPartialCoreAsync(String partialViewName, Object model, ViewDataDictionary viewData, TextWriter writer) at Microsoft.AspNetCore.Mvc.ViewFeatures.HtmlHelper.PartialAsync(String partialViewName, Object model, ViewDataDictionary viewData) at AspNetCoreGeneratedDocument.Views_Partials_Forms_Themes_default_Form.ExecuteAsync() at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderPageCoreAsync(IRazorPage page, ViewContext context) at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderPageAsync(IRazorPage page, ViewContext context, Boolean invokeViewStarts) at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderAsync(ViewContext context) at Microsoft.AspNetCore.Mvc.ViewFeatures.HtmlHelper.RenderPartialCoreAsync(String partialViewName, Object model, ViewDataDictionary viewData, TextWriter writer) at Microsoft.AspNetCore.Mvc.ViewFeatures.HtmlHelper.PartialAsync(String partialViewName, Object model, ViewDataDictionary viewData) at AspNetCoreGeneratedDocument.Views_Partials_Forms_Themes_default_Render.ExecuteAsync() at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderPageCoreAsync(IRazorPage page, ViewContext context) at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderPageAsync(IRazorPage page, ViewContext context, Boolean invokeViewStarts) at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderAsync(ViewContext context) at Microsoft.AspNetCore.Mvc.ViewComponents.ViewViewComponentResult.ExecuteAsync(ViewComponentContext context) at Microsoft.AspNetCore.Mvc.ViewComponents.DefaultViewComponentInvoker.InvokeAsync(ViewComponentContext context) at Microsoft.AspNetCore.Mvc.ViewComponents.DefaultViewComponentHelper.InvokeCoreAsync(ViewComponentDescriptor descriptor, Object arguments) at AspNetCoreGeneratedDocument.Views_MacroPartials_InsertUmbracoFormWithTheme.ExecuteAsync() at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderPageCoreAsync(IRazorPage page, ViewContext context) at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderPageAsync(IRazorPage page, ViewContext context, Boolean invokeViewStarts) at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderAsync(ViewContext context) at Microsoft.AspNetCore.Mvc.ViewComponents.ViewViewComponentResult.ExecuteAsync(ViewComponentContext context) at Microsoft.AspNetCore.Mvc.ViewComponents.ViewViewComponentResult.Execute(ViewComponentContext context) at Umbraco.Cms.Web.Common.Macros.PartialViewMacroEngine.Execute(MacroModel macro, IPublishedContent content) at Umbraco.Cms.Web.Common.Macros.MacroRenderer.<>cDisplayClass26_0.b0() at Umbraco.Cms.Web.Common.Macros.MacroRenderer.ExecuteProfileMacroWithErrorWrapper(MacroModel macro, String msgIn, Func1 getMacroContent, Func1 msgErr) at Umbraco.Cms.Web.Common.Macros.MacroRenderer.ExecuteMacroWithErrorWrapper(MacroModel macro, String msgIn, String msgOut, Func1 getMacroContent, Func1 msgErr) at Umbraco.Cms.Web.Common.Macros.MacroRenderer.ExecuteMacroOfType(MacroModel model, IPublishedContent content) at Umbraco.Cms.Web.Common.Macros.MacroRenderer.RenderAsync(MacroModel macro, IPublishedContent content) at Umbraco.Cms.Web.Common.Macros.MacroRenderer.RenderAsync(String macroAlias, IPublishedContent content, IDictionary2 macroParams) at Umbraco.Cms.Core.Templates.UmbracoComponentRenderer.RenderMacroAsync(IPublishedContent content, String alias, IDictionary2 parameters) at Umbraco.Cms.Core.Templates.UmbracoComponentRenderer.RenderMacroForContent(IPublishedContent content, String alias, IDictionary2 parameters) at Umbraco.Cms.Web.BackOffice.Controllers.MacroRenderingController.GetMacroResultAsHtml(String macroAlias, Int32 pageId, IDictionary2 macroParams) at Umbraco.Cms.Web.BackOffice.Controllers.MacroRenderingController.GetMacroResultAsHtmlForEditor(MacroParameterModel model) at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.TaskOfIActionResultExecutor.Execute(ActionContext actionContext, IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.gAwaited|12_0(ControllerActionInvoker invoker, ValueTask`1 actionResultValueTask) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.gAwaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync() --- End of stack trace from previous location --- at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|26_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)

I have followed other packages that tried to perform something similar to implement reCaptcha but I cannot find anything that is being missed out.

I have no idea what is going wrong here I am debugging the issue since last week and cannot find any solution.

I will really appreciate any leads to resolve this issue or if you can share your experience with similar feature. Thank you so much for your time for reading through my question until here.

AndyButland commented 3 months ago

I'm not sure what the issue is here but I'd suggest just to simplify things a bit, try rendering the form directly on the template without the complications of the rich text editor and macro.

And then use a simple template like the following (assumes you have a doc type with a form picker property called "Form").

@using Umbraco.Cms.Web.Common.PublishedModels;
@using Umbraco.Forms.Web;
@using Umbraco.Forms.Web.Extensions;
@using Umbraco.Cms.Core.Configuration.Models;
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<ContentModels.HomePage>
@using ContentModels = Umbraco.Cms.Web.Common.PublishedModels;
@{
  Layout = null;
}
<html>
<head>
<title>Sample Form Page</title>
</head>
<body>
<h1>Sample Form Page</h1>
@if (Model.Form.HasValue)
{
  @await Component.InvokeAsync("RenderForm", new { formId = Model.Form, theme = "default", includeScripts = true })
}
@Html.RenderUmbracoFormDependencies()

</body>
</html>
Namrata-V2 commented 3 months ago

Thanks @AndyButland for your response.

As per your suggestion. I have created a template

Views/ReCaptchaEnterprise.html

@using Umbraco.Cms.Web.Common.PublishedModels;
@using Umbraco.Forms.Web;
@using Umbraco.Forms.Web.Extensions;
@using Umbraco.Cms.Core.Configuration.Models;
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<ContentModels.ReCaptchaEnterprise>
@using ContentModels = Umbraco.Cms.Web.Common.PublishedModels;
@{
  Layout = null;
}
<html>
<head>
<title>Sample Form Page</title>
</head>
<body>
<h1>Sample Form Page</h1>

@await Component.InvokeAsync("RenderForm", new { formId = Guid.Parse(<guid>), theme = "default", includeScripts = true })
@Html.RenderUmbracoFormDependencies()

</body>
</html>

and then created a Content ReCaptchaEnterprise using the same template and published the content.

When I check https://localhost:44326/recaptcha-enterprise/

It throws the exception mentioned in the original post.

image

This is what the ReCaptchaEnterprise.generated.cs looks like :

namespace Umbraco.Cms.Web.Common.PublishedModels
{
    /// <summary>ReCaptcha Enterprise</summary>
    [PublishedModel("reCaptchaEnterprise")]
    public partial class ReCaptchaEnterprise : PublishedContentModel
    {
        // helpers
#pragma warning disable 0109 // new is redundant
        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Umbraco.ModelsBuilder.Embedded", "12.0.1+20a4e47")]
        public new const string ModelTypeAlias = "reCaptchaEnterprise";
        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Umbraco.ModelsBuilder.Embedded", "12.0.1+20a4e47")]
        public new const PublishedItemType ModelItemType = PublishedItemType.Content;
        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Umbraco.ModelsBuilder.Embedded", "12.0.1+20a4e47")]
        [return: global::System.Diagnostics.CodeAnalysis.MaybeNull]
        public new static IPublishedContentType GetModelContentType(IPublishedSnapshotAccessor publishedSnapshotAccessor)
            => PublishedModelUtility.GetModelContentType(publishedSnapshotAccessor, ModelItemType, ModelTypeAlias);
        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Umbraco.ModelsBuilder.Embedded", "12.0.1+20a4e47")]
        [return: global::System.Diagnostics.CodeAnalysis.MaybeNull]
        public static IPublishedPropertyType GetModelPropertyType<TValue>(IPublishedSnapshotAccessor publishedSnapshotAccessor, Expression<Func<ReCaptchaEnterprise, TValue>> selector)
            => PublishedModelUtility.GetModelPropertyType(GetModelContentType(publishedSnapshotAccessor), selector);
#pragma warning restore 0109

        private IPublishedValueFallback _publishedValueFallback;

        // ctor
        public ReCaptchaEnterprise(IPublishedContent content, IPublishedValueFallback publishedValueFallback)
            : base(content, publishedValueFallback)
        {
            _publishedValueFallback = publishedValueFallback;
        }

        // properties
    }
}
AndyButland commented 3 months ago

I think the problem might be in your FieldType.ReCaptchaEnterprise.html.

You have this line:

@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage

But we don't have that in the documentation for the partial view. We just have a @model. Try removing this as that looks to be related to the error you are seeing.

Namrata-V2 commented 3 months ago

Thanks a ton, @AndyButland. It indeed resolved the issue.