Skip to content

Instantly share code, notes, and snippets.

@mcaskill
Last active June 7, 2023 17:05
Show Gist options
  • Save mcaskill/e38aa65658d04c683da1b88aa2c7b104 to your computer and use it in GitHub Desktop.
Save mcaskill/e38aa65658d04c683da1b88aa2c7b104 to your computer and use it in GitHub Desktop.

Revisions

  1. mcaskill revised this gist Jun 7, 2023. 3 changed files with 19 additions and 1 deletion.
    7 changes: 6 additions & 1 deletion WP-NF-Fix-reCAPTCHA_V2.md
    Original file line number Diff line number Diff line change
    @@ -4,7 +4,12 @@ When a reCAPTCHA field is configured as "invisible", Ninja Forms will execute `g

    This patch replaces the interval callback with a more complex solution that involves interrupting the form submit with a temporary validation error (that displays "Processing…" error). When reCAPTCHA is done (checks and challenges completed), the temporary validation error is removed and the form is submitted to the server for final validation.

    This also requires removing the reCAPTCHA controller in ninja-forms-multi-part which is no longer needed with this patch.
    If you use Ninja Forms Multi-Part, you will have to remove its reCAPTCHA controller which is no longer needed with this patch.

    Apply the following patches:

    * Ninja Forms: `nf-front-end-deps.js.diff`
    * Ninja Forms Multi-Part: `nf-mp-front-end.js.diff`

    Ideally, Ninja Forms would implement support for a promise-based solution to allow for controllers to take their time with whatever needs to be processed.

    3 changes: 3 additions & 0 deletions front-end-deps.js.diff → nf-front-end-deps.js.diff
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,6 @@
    #
    # Tested with Ninja Forms Multo Part v3.6.24
    #
    --- assets/js/min/front-end-deps.js
    +++ assets/js/min/front-end-deps.js
    @@ -67,57 +67,314 @@
    10 changes: 10 additions & 0 deletions nf-mp-front-end.js.diff
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,10 @@
    #
    # Tested with Ninja Forms Multo Part v3.0.26
    #
    --- assets/js/min/front-end.js
    +++ assets/js/min/front-end.js
    @@ -1,3 +1,3 @@
    -!function(){var e,t,n;!function(i){function r(e,t){return y.call(e,t)}function o(e,t){var n,i,r,o,l,s,c,a,h,f,u,d=t&&t.split("/"),g=v.map,m=g&&g["*"]||{};if(e&&"."===e.charAt(0))if(t){for(e=e.split("/"),l=e.length-1,v.nodeIdCompat&&x.test(e[l])&&(e[l]=e[l].replace(x,"")),e=d.slice(0,d.length-1).concat(e),h=0;h<e.length;h+=1)if("."===(u=e[h]))e.splice(h,1),h-=1;else if(".."===u){if(1===h&&(".."===e[2]||".."===e[0]))break;h>0&&(e.splice(h-1,2),h-=2)}e=e.join("/")}else 0===e.indexOf("./")&&(e=e.substring(2));if((d||m)&&g){for(n=e.split("/"),h=n.length;h>0;h-=1){if(i=n.slice(0,h).join("/"),d)for(f=d.length;f>0;f-=1)if((r=g[d.slice(0,f).join("/")])&&(r=r[i])){o=r,s=h;break}if(o)break;!c&&m&&m[i]&&(c=m[i],a=h)}!o&&c&&(o=c,s=a),o&&(n.splice(0,s,o),e=n.join("/"))}return e}function l(e,t){return function(){var n=P.call(arguments,0);return"string"!=typeof n[0]&&1===n.length&&n.push(null),d.apply(i,n.concat([e,t]))}}function s(e){return function(t){return o(t,e)}}function c(e){return function(t){p[e]=t}}function a(e){if(r(b,e)){var t=b[e];delete b[e],w[e]=!0,u.apply(i,t)}if(!r(p,e)&&!r(w,e))throw new Error("No "+e);return p[e]}function h(e){var t,n=e?e.indexOf("!"):-1;return n>-1&&(t=e.substring(0,n),e=e.substring(n+1,e.length)),[t,e]}function f(e){return function(){return v&&v.config&&v.config[e]||{}}}var u,d,g,m,p={},b={},v={},w={},y=Object.prototype.hasOwnProperty,P=[].slice,x=/\.js$/;g=function(e,t){var n,i=h(e),r=i[0];return e=i[1],r&&(r=o(r,t),n=a(r)),r?e=n&&n.normalize?n.normalize(e,s(t)):o(e,t):(e=o(e,t),i=h(e),r=i[0],e=i[1],r&&(n=a(r))),{f:r?r+"!"+e:e,n:e,pr:r,p:n}},m={require:function(e){return l(e)},exports:function(e){var t=p[e];return void 0!==t?t:p[e]={}},module:function(e){return{id:e,uri:"",exports:p[e],config:f(e)}}},u=function(e,t,n,o){var s,h,f,u,d,v,y=[],P=typeof n;if(o=o||e,"undefined"===P||"function"===P){for(t=!t.length&&n.length?["require","exports","module"]:t,d=0;d<t.length;d+=1)if(u=g(t[d],o),"require"===(h=u.f))y[d]=m.require(e);else if("exports"===h)y[d]=m.exports(e),v=!0;else if("module"===h)s=y[d]=m.module(e);else if(r(p,h)||r(b,h)||r(w,h))y[d]=a(h);else{if(!u.p)throw new Error(e+" missing "+h);u.p.load(u.n,l(o,!0),c(h),{}),y[d]=p[h]}f=n?n.apply(p[e],y):void 0,e&&(s&&s.exports!==i&&s.exports!==p[e]?p[e]=s.exports:f===i&&v||(p[e]=f))}else e&&(p[e]=n)},e=t=d=function(e,t,n,r,o){if("string"==typeof e)return m[e]?m[e](t):a(g(e,t).f);if(!e.splice){if(v=e,v.deps&&d(v.deps,v.callback),!t)return;t.splice?(e=t,t=n,n=null):e=i}return t=t||function(){},"function"==typeof n&&(n=r,r=o),r?u(i,e,t,n):setTimeout(function(){u(i,e,t,n)},4),d},d.config=function(e){return d(e)},e._defined=p,n=function(e,t,n){if("string"!=typeof e)throw new Error("See almond README: incorrect module build, no module name");t.splice||(n=t,t=[]),r(p,e)||r(b,e)||(b[e]=[e,t,n])},n.amd={jQuery:!0}}(),n("../lib/almond",function(){}),n("views/header",[],function(){return Marionette.ItemView.extend({template:"#tmpl-nf-mp-header",fullProgressBar:!1,initialize:function(e){this.listenTo(this.collection,"change:part",this.reRender),this.listenTo(this.collection,"change:errors",this.reRender),this.listenTo(i.channel("forms"),"before:submit",this.fillProgressBar)},reRender:function(){this.model=this.collection.getElement(),this.render()},templateHelpers:function(){var e=this;return{renderPartTitle:function(){if(void 0===e.collection.formModel.get("settings").mp_display_titles||0==e.collection.formModel.get("settings").mp_display_titles)return"";var t=Backbone.Radio.channel("app").request("get:template","#tmpl-nf-mp-part-title");return 1<_.invoke(e.collection.getVisibleParts(),"pick",["title","errors","visible"]).length?t({title:this.title}):""},renderBreadcrumbs:function(){if(void 0===e.collection.formModel.get("settings").mp_breadcrumb||0==e.collection.formModel.get("settings").mp_breadcrumb)return"";var t=Backbone.Radio.channel("app").request("get:template","#tmpl-nf-mp-breadcrumbs"),n=_.invoke(e.collection.getVisibleParts(),"pick",["title","errors","visible"]);return 1<n.length?t({parts:n,currentIndex:e.collection.getVisibleParts().indexOf(e.model)}):""},renderProgressBar:function(){if(void 0===e.collection.formModel.get("settings").mp_progress_bar||0==e.collection.formModel.get("settings").mp_progress_bar)return"";var t=Backbone.Radio.channel("app").request("get:template","#tmpl-nf-mp-progress-bar"),n=e.collection.getVisibleParts().indexOf(e.model),i=e.fullProgressBar?100:n/e.collection.getVisibleParts().length*100;return 1<e.collection.getVisibleParts().length?t({percent:i}):""}}},events:{"click .nf-breadcrumb":"clickBreadcrumb"},clickBreadcrumb:function(e){e.preventDefault(),this.collection.setElement(this.collection.getVisibleParts()[jQuery(e.target).data("index")])},fillProgressBar:function(e){this.fullProgressBar=!0,this.render(),this.fullProgressBar=!1}})}),n("views/footer",[],function(){return Marionette.ItemView.extend({template:"#tmpl-nf-mp-footer",initialize:function(e){this.listenTo(this.collection,"change:part",this.reRender)},reRender:function(){this.model=this.collection.getElement(),this.render()},templateHelpers:function(){var e=this;return{renderNextPrevious:function(){var t=Backbone.Radio.channel("app").request("get:template","#tmpl-nf-mp-next-previous"),n=!1,i=!1,r=e.collection.where({visible:!0});return r.indexOf(e.model)!=r.length-1&&(n=!0),0!=r.indexOf(e.model)&&(i=!0),n||i?t({showNext:n,showPrevious:i,prevLabel:e.collection.formModel.get("mp_prev_label")||nfMPSettings.prevLabel,nextLabel:e.collection.formModel.get("mp_next_label")||nfMPSettings.nextLabel}):""}}}})}),n("views/formContent",["views/header","views/footer"],function(e,t){return Marionette.LayoutView.extend({template:"#tmpl-nf-mp-form-content",regions:{header:".nf-mp-header",body:".nf-mp-body",footer:".nf-mp-footer"},initialize:function(e){this.formModel=e.formModel,this.collection=e.data,this.listenTo(this.collection,"change:part",this.changePart),this.listenTo(this.collection,"change:visible",this.renderHeaderFooter)},onRender:function(){this.header.show(new e({collection:this.collection,model:this.collection.getElement()}));var n=i.channel("formContent").request("get:viewFilters"),r=_.without(n,void 0),o=r[1];this.formContentView=o(),this.body.show(new this.formContentView({collection:this.collection.getElement().get("formContentData")})),this.footer.show(new t({collection:this.collection,model:this.collection.getElement()}))},renderHeaderFooter:function(){this.header.show(new e({collection:this.collection,model:this.collection.getElement()})),this.footer.show(new t({collection:this.collection,model:this.collection.getElement()}))},changePart:function(){this.body.show(new this.formContentView({collection:this.collection.getElement().get("formContentData")}));var e=jQuery(this.body.el).closest(".nf-form-cont").offset().top;jQuery(window).scrollTop()>e-50&&jQuery(window).scrollTop(e-50)},events:{"click .nf-next":"clickNext","click .nf-previous":"clickPrevious"},clickNext:function(e){e.preventDefault(),this.collection.next()},clickPrevious:function(e){e.preventDefault(),this.collection.previous()}})}),n("models/partModel",[],function(){return Backbone.Model.extend({fieldErrors:{},defaults:{errors:!1,visible:!0,title:""},initialize:function(){this.filterFormContentData(),this.listenTo(this.get("formContentData"),"change:errors",this.maybeChangeActivePart),this.fieldErrors[this.cid]=[],this.on("change:visible",this.changeVisible,this),this.set("order",Number(this.get("order")))},filterFormContentData:function(){if(this.get("formContentData")){var e=this.get("formContentData"),t=i.channel("formContent").request("get:loadFilters"),n=_.without(t,void 0),r=n[1],o=0==e.length;if(void 0===t[4]&&_.isArray(e)&&0!=e.length&&void 0!==e[0].cells){var l=[],s=_.pluck(e,"cells");_.each(s,function(e){var t=_.flatten(_.pluck(e,"fields"));l=_.union(l,t)}),e=l,this.set("formContentData",e)}this.set("formContentData",r(e,this.collection.formModel,o,e))}},maybeChangeActivePart:function(e){0<e.get("errors").length?(this.set("errors",!0),this.fieldErrors[this.cid].push(e.get("key")),this.collection.getElement()!=this&&this.collection.indexOf(this.collection.getElement())>this.collection.indexOf(this)&&this.collection.setElement(this)):(this.fieldErrors[this.cid]=_.without(this.fieldErrors[this.cid],e.get("key")),0==this.fieldErrors[this.cid].length&&this.set("errors",!1))},validateFields:function(){this.get("formContentData").validateFields()},changeVisible:function(){this.get("visible")?this.get("formContentData").showFields():this.get("formContentData").hideFields()}})}),n("models/partCollection",["models/partModel"],function(e){return Backbone.Collection.extend({model:e,currentElement:!1,initialize:function(e,t){this.formModel=t.formModel},getElement:function(){return this.currentElement||this.setElement(this.at(0),!0),this.currentElement},setElement:function(e,t){!(t=t||!1)&&this.partErrors()||(this.currentElement=e,t||(this.trigger("change:part",this),i.channel("nfMP").trigger("change:part",this)))},setNextElement:function(e,t){!(t=t||!1)&&this.partErrors()||(this.currentElement=e,t||(this.trigger("change:part",this),i.channel("nfMP").trigger("change:part",this)))},setPreviousElement:function(e,t){t=t||!1,this.currentElement=e,t||(this.trigger("change:part",this),i.channel("nfMP").trigger("change:part",this))},next:function(){return this.getVisibleParts().length-1!=this.getVisibleParts().indexOf(this.getElement())&&this.setNextElement(this.getVisibleParts()[this.getVisibleParts().indexOf(this.getElement())+1]),this},previous:function(){return 0!=this.getVisibleParts().indexOf(this.getElement())&&this.setPreviousElement(this.getVisibleParts()[this.getVisibleParts().indexOf(this.getElement())-1]),this},partErrors:function(){return void 0!==this.formModel.get("settings").mp_validate&&0!=this.formModel.get("settings").mp_validate&&(this.currentElement.validateFields(),this.currentElement.get("errors"))},validateFields:function(){_.each(this.getVisibleParts(),function(e){e.validateFields()})},getVisibleParts:function(){return this.where({visible:!0})}})}),n("controllers/loadFilters",["views/formContent","models/partCollection"],function(e,t){return Marionette.Object.extend({initialize:function(){i.channel("formContent").request("add:viewFilter",this.getformContentView,1),i.channel("formContent").request("add:loadFilter",this.formContentLoad,1)},getformContentView:function(t){return e},formContentLoad:function(e,n){if(!0==e instanceof t)return e;if(_.isArray(e)&&0!=_.isArray(e).length&&void 0!==_.first(e)&&"part"==_.first(e).type)var i=new t(e,{formModel:n});else var i=new t({formContentData:e},{formModel:n});return i}})}),n("controllers/conditionalLogic",[],function(){return Marionette.Object.extend({initialize:function(){i.channel("condition:trigger").reply("show_part",this.showPart,this),i.channel("condition:trigger").reply("hide_part",this.hidePart,this)},showPart:function(e,t){e.set("alreadyTriggered",!0),this.changePartVisibility(e,t,!0),e.set("alreadyTriggered",!1)},hidePart:function(e,t){e.set("alreadyTriggered",!0),this.changePartVisibility(e,t,!1),e.set("alreadyTriggered",!1)},changePartVisibility:function(e,t,n){var i=Date.now();e.collection.mpResetFlag||(e.collection.mpResetFlag=i),e.collection.formModel.get("formContentData").findWhere({key:t.key}).set("visible",n),e.collection.each(function(t){t!=e&&(t.get("alreadyTriggered")||(t.checkWhen(),t.set("alreadyTriggered",!0)))}),i==e.collection.mpResetFlag&&(e.collection.invoke("set",{alreadyTriggered:!1}),e.collection.mpResetFlag=!1)}})}),n("controllers/renderRecaptcha",[],function(){return Marionette.Object.extend({initialize:function(){this.listenTo(i.channel("nfMP"),"change:part",this.changePart,this)},changePart:function(e,t){jQuery(".g-recaptcha").each(function(){var e=jQuery(this).data("callback"),t=jQuery(this).data("fieldid");"function"!=typeof window[e]&&(window[e]=function(e){i.channel("recaptcha").request("update:response",e,t)});var n={theme:jQuery(this).data("theme"),sitekey:jQuery(this).data("sitekey"),callback:e};grecaptcha.render(jQuery(this)[0],n)})}})}),n("controllers/renderHelpText",[],function(){return Marionette.Object.extend({initialize:function(){this.listenTo(i.channel("nfMP"),"change:part",this.changePart,this)},changePart:function(e,t){jQuery(".nf-help").each(function(){jQuery(this).jBox("Tooltip",{theme:"TooltipBorder",content:jQuery(this).data("text")})})}})}),n("controllers/loadControllers",["controllers/conditionalLogic","controllers/renderRecaptcha","controllers/renderHelpText"],function(e,t,n){return Marionette.Object.extend({initialize:function(){new e,new t,new n}})});var i=Backbone.Radio;t(["controllers/loadFilters","controllers/loadControllers"],function(e,t){(new(Marionette.Application.extend({initialize:function(e){this.listenTo(i.channel("form"),"before:filterData",this.loadFilters),this.listenTo(i.channel("form"),"loaded",this.loadControllers)},loadFilters:function(t){new e},loadControllers:function(e){new t},onStart:function(){}}))).start()}),n("main",function(){})}();
    +!function(){var e,t,n;!function(i){function r(e,t){return y.call(e,t)}function o(e,t){var n,i,r,o,l,s,c,a,h,f,u,d=t&&t.split("/"),g=v.map,m=g&&g["*"]||{};if(e&&"."===e.charAt(0))if(t){for(e=e.split("/"),l=e.length-1,v.nodeIdCompat&&x.test(e[l])&&(e[l]=e[l].replace(x,"")),e=d.slice(0,d.length-1).concat(e),h=0;h<e.length;h+=1)if("."===(u=e[h]))e.splice(h,1),h-=1;else if(".."===u){if(1===h&&(".."===e[2]||".."===e[0]))break;h>0&&(e.splice(h-1,2),h-=2)}e=e.join("/")}else 0===e.indexOf("./")&&(e=e.substring(2));if((d||m)&&g){for(n=e.split("/"),h=n.length;h>0;h-=1){if(i=n.slice(0,h).join("/"),d)for(f=d.length;f>0;f-=1)if((r=g[d.slice(0,f).join("/")])&&(r=r[i])){o=r,s=h;break}if(o)break;!c&&m&&m[i]&&(c=m[i],a=h)}!o&&c&&(o=c,s=a),o&&(n.splice(0,s,o),e=n.join("/"))}return e}function l(e,t){return function(){var n=P.call(arguments,0);return"string"!=typeof n[0]&&1===n.length&&n.push(null),d.apply(i,n.concat([e,t]))}}function s(e){return function(t){return o(t,e)}}function c(e){return function(t){p[e]=t}}function a(e){if(r(b,e)){var t=b[e];delete b[e],w[e]=!0,u.apply(i,t)}if(!r(p,e)&&!r(w,e))throw new Error("No "+e);return p[e]}function h(e){var t,n=e?e.indexOf("!"):-1;return n>-1&&(t=e.substring(0,n),e=e.substring(n+1,e.length)),[t,e]}function f(e){return function(){return v&&v.config&&v.config[e]||{}}}var u,d,g,m,p={},b={},v={},w={},y=Object.prototype.hasOwnProperty,P=[].slice,x=/\.js$/;g=function(e,t){var n,i=h(e),r=i[0];return e=i[1],r&&(r=o(r,t),n=a(r)),r?e=n&&n.normalize?n.normalize(e,s(t)):o(e,t):(e=o(e,t),i=h(e),r=i[0],e=i[1],r&&(n=a(r))),{f:r?r+"!"+e:e,n:e,pr:r,p:n}},m={require:function(e){return l(e)},exports:function(e){var t=p[e];return void 0!==t?t:p[e]={}},module:function(e){return{id:e,uri:"",exports:p[e],config:f(e)}}},u=function(e,t,n,o){var s,h,f,u,d,v,y=[],P=typeof n;if(o=o||e,"undefined"===P||"function"===P){for(t=!t.length&&n.length?["require","exports","module"]:t,d=0;d<t.length;d+=1)if(u=g(t[d],o),"require"===(h=u.f))y[d]=m.require(e);else if("exports"===h)y[d]=m.exports(e),v=!0;else if("module"===h)s=y[d]=m.module(e);else if(r(p,h)||r(b,h)||r(w,h))y[d]=a(h);else{if(!u.p)throw new Error(e+" missing "+h);u.p.load(u.n,l(o,!0),c(h),{}),y[d]=p[h]}f=n?n.apply(p[e],y):void 0,e&&(s&&s.exports!==i&&s.exports!==p[e]?p[e]=s.exports:f===i&&v||(p[e]=f))}else e&&(p[e]=n)},e=t=d=function(e,t,n,r,o){if("string"==typeof e)return m[e]?m[e](t):a(g(e,t).f);if(!e.splice){if(v=e,v.deps&&d(v.deps,v.callback),!t)return;t.splice?(e=t,t=n,n=null):e=i}return t=t||function(){},"function"==typeof n&&(n=r,r=o),r?u(i,e,t,n):setTimeout(function(){u(i,e,t,n)},4),d},d.config=function(e){return d(e)},e._defined=p,n=function(e,t,n){if("string"!=typeof e)throw new Error("See almond README: incorrect module build, no module name");t.splice||(n=t,t=[]),r(p,e)||r(b,e)||(b[e]=[e,t,n])},n.amd={jQuery:!0}}(),n("../lib/almond",function(){}),n("views/header",[],function(){return Marionette.ItemView.extend({template:"#tmpl-nf-mp-header",fullProgressBar:!1,initialize:function(e){this.listenTo(this.collection,"change:part",this.reRender),this.listenTo(this.collection,"change:errors",this.reRender),this.listenTo(i.channel("forms"),"before:submit",this.fillProgressBar)},reRender:function(){this.model=this.collection.getElement(),this.render()},templateHelpers:function(){var e=this;return{renderPartTitle:function(){if(void 0===e.collection.formModel.get("settings").mp_display_titles||0==e.collection.formModel.get("settings").mp_display_titles)return"";var t=Backbone.Radio.channel("app").request("get:template","#tmpl-nf-mp-part-title");return 1<_.invoke(e.collection.getVisibleParts(),"pick",["title","errors","visible"]).length?t({title:this.title}):""},renderBreadcrumbs:function(){if(void 0===e.collection.formModel.get("settings").mp_breadcrumb||0==e.collection.formModel.get("settings").mp_breadcrumb)return"";var t=Backbone.Radio.channel("app").request("get:template","#tmpl-nf-mp-breadcrumbs"),n=_.invoke(e.collection.getVisibleParts(),"pick",["title","errors","visible"]);return 1<n.length?t({parts:n,currentIndex:e.collection.getVisibleParts().indexOf(e.model)}):""},renderProgressBar:function(){if(void 0===e.collection.formModel.get("settings").mp_progress_bar||0==e.collection.formModel.get("settings").mp_progress_bar)return"";var t=Backbone.Radio.channel("app").request("get:template","#tmpl-nf-mp-progress-bar"),n=e.collection.getVisibleParts().indexOf(e.model),i=e.fullProgressBar?100:n/e.collection.getVisibleParts().length*100;return 1<e.collection.getVisibleParts().length?t({percent:i}):""}}},events:{"click .nf-breadcrumb":"clickBreadcrumb"},clickBreadcrumb:function(e){e.preventDefault(),this.collection.setElement(this.collection.getVisibleParts()[jQuery(e.target).data("index")])},fillProgressBar:function(e){this.fullProgressBar=!0,this.render(),this.fullProgressBar=!1}})}),n("views/footer",[],function(){return Marionette.ItemView.extend({template:"#tmpl-nf-mp-footer",initialize:function(e){this.listenTo(this.collection,"change:part",this.reRender)},reRender:function(){this.model=this.collection.getElement(),this.render()},templateHelpers:function(){var e=this;return{renderNextPrevious:function(){var t=Backbone.Radio.channel("app").request("get:template","#tmpl-nf-mp-next-previous"),n=!1,i=!1,r=e.collection.where({visible:!0});return r.indexOf(e.model)!=r.length-1&&(n=!0),0!=r.indexOf(e.model)&&(i=!0),n||i?t({showNext:n,showPrevious:i,prevLabel:e.collection.formModel.get("mp_prev_label")||nfMPSettings.prevLabel,nextLabel:e.collection.formModel.get("mp_next_label")||nfMPSettings.nextLabel}):""}}}})}),n("views/formContent",["views/header","views/footer"],function(e,t){return Marionette.LayoutView.extend({template:"#tmpl-nf-mp-form-content",regions:{header:".nf-mp-header",body:".nf-mp-body",footer:".nf-mp-footer"},initialize:function(e){this.formModel=e.formModel,this.collection=e.data,this.listenTo(this.collection,"change:part",this.changePart),this.listenTo(this.collection,"change:visible",this.renderHeaderFooter)},onRender:function(){this.header.show(new e({collection:this.collection,model:this.collection.getElement()}));var n=i.channel("formContent").request("get:viewFilters"),r=_.without(n,void 0),o=r[1];this.formContentView=o(),this.body.show(new this.formContentView({collection:this.collection.getElement().get("formContentData")})),this.footer.show(new t({collection:this.collection,model:this.collection.getElement()}))},renderHeaderFooter:function(){this.header.show(new e({collection:this.collection,model:this.collection.getElement()})),this.footer.show(new t({collection:this.collection,model:this.collection.getElement()}))},changePart:function(){this.body.show(new this.formContentView({collection:this.collection.getElement().get("formContentData")}));var e=jQuery(this.body.el).closest(".nf-form-cont").offset().top;jQuery(window).scrollTop()>e-50&&jQuery(window).scrollTop(e-50)},events:{"click .nf-next":"clickNext","click .nf-previous":"clickPrevious"},clickNext:function(e){e.preventDefault(),this.collection.next()},clickPrevious:function(e){e.preventDefault(),this.collection.previous()}})}),n("models/partModel",[],function(){return Backbone.Model.extend({fieldErrors:{},defaults:{errors:!1,visible:!0,title:""},initialize:function(){this.filterFormContentData(),this.listenTo(this.get("formContentData"),"change:errors",this.maybeChangeActivePart),this.fieldErrors[this.cid]=[],this.on("change:visible",this.changeVisible,this),this.set("order",Number(this.get("order")))},filterFormContentData:function(){if(this.get("formContentData")){var e=this.get("formContentData"),t=i.channel("formContent").request("get:loadFilters"),n=_.without(t,void 0),r=n[1],o=0==e.length;if(void 0===t[4]&&_.isArray(e)&&0!=e.length&&void 0!==e[0].cells){var l=[],s=_.pluck(e,"cells");_.each(s,function(e){var t=_.flatten(_.pluck(e,"fields"));l=_.union(l,t)}),e=l,this.set("formContentData",e)}this.set("formContentData",r(e,this.collection.formModel,o,e))}},maybeChangeActivePart:function(e){0<e.get("errors").length?(this.set("errors",!0),this.fieldErrors[this.cid].push(e.get("key")),this.collection.getElement()!=this&&this.collection.indexOf(this.collection.getElement())>this.collection.indexOf(this)&&this.collection.setElement(this)):(this.fieldErrors[this.cid]=_.without(this.fieldErrors[this.cid],e.get("key")),0==this.fieldErrors[this.cid].length&&this.set("errors",!1))},validateFields:function(){this.get("formContentData").validateFields()},changeVisible:function(){this.get("visible")?this.get("formContentData").showFields():this.get("formContentData").hideFields()}})}),n("models/partCollection",["models/partModel"],function(e){return Backbone.Collection.extend({model:e,currentElement:!1,initialize:function(e,t){this.formModel=t.formModel},getElement:function(){return this.currentElement||this.setElement(this.at(0),!0),this.currentElement},setElement:function(e,t){!(t=t||!1)&&this.partErrors()||(this.currentElement=e,t||(this.trigger("change:part",this),i.channel("nfMP").trigger("change:part",this)))},setNextElement:function(e,t){!(t=t||!1)&&this.partErrors()||(this.currentElement=e,t||(this.trigger("change:part",this),i.channel("nfMP").trigger("change:part",this)))},setPreviousElement:function(e,t){t=t||!1,this.currentElement=e,t||(this.trigger("change:part",this),i.channel("nfMP").trigger("change:part",this))},next:function(){return this.getVisibleParts().length-1!=this.getVisibleParts().indexOf(this.getElement())&&this.setNextElement(this.getVisibleParts()[this.getVisibleParts().indexOf(this.getElement())+1]),this},previous:function(){return 0!=this.getVisibleParts().indexOf(this.getElement())&&this.setPreviousElement(this.getVisibleParts()[this.getVisibleParts().indexOf(this.getElement())-1]),this},partErrors:function(){return void 0!==this.formModel.get("settings").mp_validate&&0!=this.formModel.get("settings").mp_validate&&(this.currentElement.validateFields(),this.currentElement.get("errors"))},validateFields:function(){_.each(this.getVisibleParts(),function(e){e.validateFields()})},getVisibleParts:function(){return this.where({visible:!0})}})}),n("controllers/loadFilters",["views/formContent","models/partCollection"],function(e,t){return Marionette.Object.extend({initialize:function(){i.channel("formContent").request("add:viewFilter",this.getformContentView,1),i.channel("formContent").request("add:loadFilter",this.formContentLoad,1)},getformContentView:function(t){return e},formContentLoad:function(e,n){if(!0==e instanceof t)return e;if(_.isArray(e)&&0!=_.isArray(e).length&&void 0!==_.first(e)&&"part"==_.first(e).type)var i=new t(e,{formModel:n});else var i=new t({formContentData:e},{formModel:n});return i}})}),n("controllers/conditionalLogic",[],function(){return Marionette.Object.extend({initialize:function(){i.channel("condition:trigger").reply("show_part",this.showPart,this),i.channel("condition:trigger").reply("hide_part",this.hidePart,this)},showPart:function(e,t){e.set("alreadyTriggered",!0),this.changePartVisibility(e,t,!0),e.set("alreadyTriggered",!1)},hidePart:function(e,t){e.set("alreadyTriggered",!0),this.changePartVisibility(e,t,!1),e.set("alreadyTriggered",!1)},changePartVisibility:function(e,t,n){var i=Date.now();e.collection.mpResetFlag||(e.collection.mpResetFlag=i),e.collection.formModel.get("formContentData").findWhere({key:t.key}).set("visible",n),e.collection.each(function(t){t!=e&&(t.get("alreadyTriggered")||(t.checkWhen(),t.set("alreadyTriggered",!0)))}),i==e.collection.mpResetFlag&&(e.collection.invoke("set",{alreadyTriggered:!1}),e.collection.mpResetFlag=!1)}})}),n("controllers/renderHelpText",[],function(){return Marionette.Object.extend({initialize:function(){this.listenTo(i.channel("nfMP"),"change:part",this.changePart,this)},changePart:function(e,t){jQuery(".nf-help").each(function(){jQuery(this).jBox("Tooltip",{theme:"TooltipBorder",content:jQuery(this).data("text")})})}})}),n("controllers/loadControllers",["controllers/conditionalLogic","controllers/renderHelpText"],function(e,t){return Marionette.Object.extend({initialize:function(){new e,new t}})});var i=Backbone.Radio;t(["controllers/loadFilters","controllers/loadControllers"],function(e,t){(new(Marionette.Application.extend({initialize:function(e){this.listenTo(i.channel("form"),"before:filterData",this.loadFilters),this.listenTo(i.channel("form"),"loaded",this.loadControllers)},loadFilters:function(t){new e},loadControllers:function(e){new t},onStart:function(){}}))).start()}),n("main",function(){})}();
    //# sourceMappingURL=almond.build.js.map
    //# sourceMappingURL=front-end.js.map
  2. mcaskill renamed this gist Jun 7, 2023. 1 changed file with 1 addition and 1 deletion.
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,4 @@
    ## README: Fix eager execution/challenge of reCAPTCHA V2 Invisible
    ## README: Fix eager execution/challenge of reCAPTCHA V2

    When a reCAPTCHA field is configured as "invisible", Ninja Forms will execute `grecaptcha.execute()` when the form is rendered and in intervals of 110 seconds, regardless of the form's state. If reCAPTCHA determines a challenge is necessary, its appearance on page load is unexpected and confusing, and its potential recurrence is very annoying for users.

  3. mcaskill revised this gist Jun 6, 2023. 3 changed files with 604 additions and 388 deletions.
    2 changes: 2 additions & 0 deletions WP-NF-Fix-reCAPTCHA_V2_Invisible.md
    Original file line number Diff line number Diff line change
    @@ -4,6 +4,8 @@ When a reCAPTCHA field is configured as "invisible", Ninja Forms will execute `g

    This patch replaces the interval callback with a more complex solution that involves interrupting the form submit with a temporary validation error (that displays "Processing…" error). When reCAPTCHA is done (checks and challenges completed), the temporary validation error is removed and the form is submitted to the server for final validation.

    This also requires removing the reCAPTCHA controller in ninja-forms-multi-part which is no longer needed with this patch.

    Ideally, Ninja Forms would implement support for a promise-based solution to allow for controllers to take their time with whatever needs to be processed.

    ---
    451 changes: 270 additions & 181 deletions front-end-deps.js
    Original file line number Diff line number Diff line change
    @@ -4,211 +4,292 @@
    * The following must replace its equivalent in 'assets/js/min/front-end-deps.js'. See diff file.
    */

    nfRadio.channel( 'form' ).on( 'render:view', function() {
    jQuery( '.g-recaptcha' ).each( function() {
    var callback = jQuery( this ).data( 'callback' );
    var fieldID = jQuery( this ).data( 'fieldid' );
    if ( typeof window[ callback ] !== 'function' ){
    window[ callback ] = function( response ) {
    nfRecaptcha.DEBUG && console.group(callback);
    nfRecaptcha.DEBUG && console.log('fieldID:', fieldID);
    nfRecaptcha.DEBUG && console.log('response:', response);

    nfRadio.channel( 'recaptcha' ).request( 'update:response', response, fieldID );
    nfRadio.channel( 'captcha' ).request( 'update:response', response, fieldID );

    nfRecaptcha.DEBUG && console.groupEnd();
    };
    }
    } );
    } );
    var nfRadio = Backbone.Radio;

    var nfRecaptcha = Marionette.Object.extend( {
    /** @var {boolean} - Validate only one reCAPTCHA at a time. */
    isBusy: false,
    /** @var {?{ninja-forms:FormModel}} - Track which form is submitting. */
    isSubmitting: null,

    initialize: function () {
    /*
    * If we've already rendered our form view, render our recaptcha fields.
    */
    if ( 0 != jQuery( '.g-recaptcha' ).length ) {
    this.renderCaptcha();
    }
    /*
    * We haven't rendered our form view, so hook into the view render radio message, and then render.
    */
    this.listenTo( nfRadio.channel( 'form' ), 'render:view', this.renderCaptcha );
    this.listenTo( nfRadio.channel( 'captcha' ), 'reset', this.renderCaptcha );
    this.listenTo( nfRadio.channel( 'submit' ), 'validate:field', this.validateCaptcha );
    },
    /** @var {boolean} - Validate only one reCAPTCHA at a time. */
    isBusy: false,
    /** @var {?{ninja-forms:FormModel}} - Track which form is submitting. */
    isSubmitting: null,

    /**
    * @param {ninja-forms:FormModel} formModel - The Ninja Forms form model.
    */
    beforeSubmit: function ( formModel ) {
    var DEBUG = nfRecaptcha.DEBUG;
    initialize: function () {
    var DEBUG = nfRecaptcha.DEBUG;

    DEBUG && console.group('nfRecaptcha.beforeSubmit');
    DEBUG && console.group('nfRecaptcha.initialize');
    DEBUG && console.log('.g-recaptcha', document.querySelectorAll('.g-recaptcha'));

    this.isSubmitting = formModel;
    DEBUG && console.log('Form:', formModel);
    /*
    * If we've already rendered our form view, render our recaptcha fields.
    */
    if ( 0 != jQuery( '.g-recaptcha' ).length ) {
    this.renderAll();
    }

    DEBUG && console.groupEnd();
    },
    /*
    * We haven't rendered our form view, so hook into the view render radio message, and then render.
    */
    this.listenTo( nfRadio.channel( 'recaptcha' ), 'render:view', this.renderFieldView );
    this.listenTo( nfRadio.channel( 'submit' ), 'validate:field', this.validateCaptcha );

    renderCaptcha: function () {
    var nfController = this,
    DEBUG = nfRecaptcha.DEBUG;
    DEBUG && console.groupEnd();
    },

    DEBUG && console.group('nfRecaptcha.renderCaptcha');
    /**
    * @param {ninja-forms:FormModel} formModel - The Ninja Forms form model.
    */
    beforeSubmit: function ( formModel ) {
    var DEBUG = nfRecaptcha.DEBUG;

    jQuery( '.g-recaptcha:empty' ).each( function() {
    DEBUG && console.group('.g-recaptcha', this);
    DEBUG && console.group('nfRecaptcha.beforeSubmit');

    var fieldID = jQuery( this ).data( 'fieldid' );
    DEBUG && console.log( 'Field:', fieldID );
    this.isSubmitting = formModel;
    DEBUG && console.log('Form:', formModel);

    var opts = {
    fieldid: fieldID,
    size: jQuery( this ).data( 'size' ),
    theme: jQuery( this ).data( 'theme' ),
    sitekey: jQuery( this ).data( 'sitekey' ),
    callback: jQuery( this ).data( 'callback' )
    };
    DEBUG && console.groupEnd();
    },

    DEBUG && console.log( 'Size:', opts.size );
    DEBUG && console.log( 'Callback:', opts.callback );
    renderAll: function ( formView ) {
    var DEBUG = nfRecaptcha.DEBUG;

    var grecaptchaID = grecaptcha.render( this, opts );
    DEBUG && console.log( 'reCAPTCHA:', grecaptchaID );
    DEBUG && console.group('nfRecaptcha.renderAll');

    if ( opts.size === 'invisible' ) {
    var fieldModel = nfRadio.channel( 'fields' ).request( 'get:field', fieldID );
    var nfController = this;

    if ( ! fieldModel ) {
    DEBUG && console.log( 'Skipping; Missing Field Model' );
    DEBUG && console.groupEnd();
    return;
    }
    jQuery( '.g-recaptcha:empty' ).each( function () {
    nfController.renderCaptcha( this );
    } );

    fieldModel.set( 'grecaptchaID', grecaptchaID );
    DEBUG && console.groupEnd();
    },

    nfController.listenTo( nfRadio.channel( 'form-' + fieldModel.get( 'formID' ) ), 'before:submit', nfController.beforeSubmit );
    /**
    * @param {ninja-forms:FieldItem} fieldView - The Ninja Forms field view model.
    */
    renderFieldView: function ( fieldView ) {
    var DEBUG = nfRecaptcha.DEBUG;

    nfRadio.channel( 'captcha' ).reply( 'update:response', nfController.updateValidation, nfController );
    }
    DEBUG && console.group('nfRecaptcha.renderFieldView');

    DEBUG && console.groupEnd();
    } );
    DEBUG && console.log('Field View:', fieldView);
    DEBUG && console.log('Field Model:', fieldView.model);
    DEBUG && console.log('Field Element:', fieldView.el);

    DEBUG && console.groupEnd();
    },
    var nfController = this;

    /**
    * @param {string} response - The reCAPTCHA response value.
    * @param {string|number} fieldID - The Ninja Forms CAPTCHA field ID.
    */
    updateValidation: function ( response, fieldID ) {
    var DEBUG = nfRecaptcha.DEBUG;
    jQuery( fieldView.el ).find( '.g-recaptcha:empty' ).each( function () {
    nfController.renderCaptcha( this, fieldView.model );
    } );

    DEBUG && console.group('nfRecaptcha.updateValidation');
    DEBUG && console.groupEnd();
    },

    var fieldModel = nfRadio.channel( 'fields' ).request( 'get:field', fieldID );
    DEBUG && console.log('Field:', fieldModel);
    /**
    * @param {ninja-forms-multi-part:MainLayoutView} formView - The Ninja Forms form view model.
    */
    renderFormView: function ( formView ) {
    var DEBUG = nfRecaptcha.DEBUG;

    if ( ! fieldModel ) {
    DEBUG && console.log( 'Skipping; Missing Field Model' );
    DEBUG && console.groupEnd();
    return;
    }
    DEBUG && console.group('nfRecaptcha.renderFormView');
    DEBUG && console.log('Form View:', formView);
    DEBUG && console.log('Form Model:', formView.model);

    nfRadio.channel( 'fields' ).request( 'remove:error', fieldID, 'recaptcha-processing' );
    var nfController = this;

    var formModel = this.isSubmitting;
    mainLayoutView.$el.find( '.g-recaptcha:empty' ).each( function () {
    nfController.renderCaptcha( this );
    } );

    this.isSubmitting = null;
    this.isBusy = false;
    DEBUG && console.groupEnd();
    },

    if ( formModel ) {
    DEBUG && console.log( 'Attempting to re-submit form' );
    nfRadio.channel( 'form-' + formModel.id ).request( 'submit', formModel );
    } else {
    DEBUG_CONTROLLER && console.log( 'Failed; Missing Form' );
    }
    /**
    * @param {HTMLElement} fieldElement
    * @param {NFFieldModel} fieldModel
    */
    renderCaptcha: function ( fieldElement, fieldModel ) {
    var DEBUG = nfRecaptcha.DEBUG;

    DEBUG && console.group('nfRecaptcha.renderCaptcha');
    DEBUG && console.log( 'Field Element:', fieldElement );

    if ( fieldElement.childElementCount > 0 ) {
    DEBUG && console.log('Skipping; Not Empty');
    DEBUG && console.groupEnd();
    return;
    }

    var $fieldEl = jQuery( fieldElement );

    var fieldID = fieldModel && fieldModel.id || $fieldEl.data( 'fieldid' );

    if ( ! fieldModel ) {
    fieldModel = nfRadio.channel( 'fields' ).request( 'get:field', fieldID );

    if ( ! fieldModel ) {
    DEBUG && console.log( 'Skipping; Missing Field Model' );
    DEBUG && console.groupEnd();
    return;
    }
    }

    var grecaptchaID = fieldModel.get( 'grecaptchaID' );
    if ( grecaptchaID ) {
    DEBUG && console.log( 'Skipping; reCAPTCHA already rendered' );
    DEBUG && console.groupEnd();
    return;
    }

    DEBUG && console.log( 'Form ID:', Number.parseInt( fieldModel.get( 'formID' ) ) );
    DEBUG && console.log( 'Field ID:', fieldID );
    DEBUG && console.log( 'Field Model:', fieldModel );

    var opts = {
    fieldid: fieldID,
    size: $fieldEl.data( 'size' ),
    theme: $fieldEl.data( 'theme' ),
    sitekey: $fieldEl.data( 'sitekey' ),
    callback: $fieldEl.data( 'callback' )
    };

    DEBUG && console.log( 'Options:', opts );

    if ( typeof window[ opts.callback ] !== 'function' ) {
    DEBUG && console.log( 'Create callback:', opts.callback );
    window[ opts.callback ] = function ( response ) {
    DEBUG && console.group(opts.callback);
    DEBUG && console.log('fieldID:', fieldID);
    DEBUG && console.log('response:', response);

    nfRadio.channel( 'recaptcha' ).request( 'update:response', response, fieldID );
    nfRadio.channel( 'captcha' ).request( 'update:response', response, fieldID );

    DEBUG && console.groupEnd();
    };
    }

    try {
    grecaptchaID = grecaptcha.render( fieldElement, opts );
    DEBUG && console.log( 'reCAPTCHA:', grecaptchaID );
    } catch ( e ) {
    this.isBusy = false;
    console.warn( 'Notice: Error trying to render grecaptcha.' );
    DEBUG && console.log( 'Caught error:', e );
    }

    if ( opts.size === 'invisible' ) {
    fieldModel.set( 'grecaptchaID', grecaptchaID );

    this.listenTo( nfRadio.channel( 'form-' + fieldModel.get( 'formID' ) ), 'before:submit', this.beforeSubmit );

    nfRadio.channel( 'captcha' ).reply( 'update:response', this.updateValidation, this );
    }

    DEBUG && console.groupEnd();
    },

    /**
    * @param {string} response - The reCAPTCHA response value.
    * @param {string|number} fieldID - The Ninja Forms CAPTCHA field ID.
    */
    updateValidation: function ( response, fieldID ) {
    var DEBUG = nfRecaptcha.DEBUG;

    DEBUG && console.groupEnd();
    },
    DEBUG && console.group('nfRecaptcha.updateValidation');

    /**
    * @param {NFFieldModel} fieldModel
    */
    validateCaptcha: function ( fieldModel ) {
    var DEBUG = nfRecaptcha.DEBUG;
    var fieldModel = nfRadio.channel( 'fields' ).request( 'get:field', fieldID );
    DEBUG && console.log('Field:', fieldModel);

    DEBUG && console.group('nfRecaptcha.validateCaptcha --', 'Field:', fieldModel.id);
    if ( ! fieldModel ) {
    DEBUG && console.log( 'Skipping; Missing Field Model' );
    DEBUG && console.groupEnd();
    return;
    }

    var fieldID = fieldModel.id;
    var fieldType = fieldModel.get( 'type' );
    var fieldSize = fieldModel.get( 'size' );
    var grecaptchaID = fieldModel.get( 'grecaptchaID' );
    nfRadio.channel( 'fields' ).request( 'remove:error', fieldID, 'recaptcha-processing' );

    if ( this.isBusy ) {
    DEBUG && console.log( 'Skipping; Busy' );
    DEBUG && console.groupEnd();
    return;
    }
    var formModel = this.isSubmitting;

    if ( ! this.isSubmitting ) {
    DEBUG && console.log( 'Skipping; Not Submitting' );
    DEBUG && console.groupEnd();
    return;
    }

    if (
    fieldType !== 'recaptcha' ||
    fieldSize !== 'invisible' ||
    fieldModel.get( 'clean' )
    ) {
    DEBUG && console.log( 'Skipping; Not reCAPTCHA Field' );
    DEBUG && console.groupEnd();
    return;
    }
    this.isSubmitting = null;
    this.isBusy = false;

    if ( grecaptchaID == null ) {
    DEBUG && console.log( 'Skipping; Missing reCAPTCHA Widget ID' );
    DEBUG && console.groupEnd();
    return;
    }

    this.isBusy = true;
    if ( formModel ) {
    DEBUG && console.log( 'Attempting to re-submit form' );
    nfRadio.channel( 'form-' + formModel.id ).request( 'submit', formModel );
    } else {
    DEBUG_CONTROLLER && console.log( 'Failed; Missing Form' );
    }

    var fieldValue = fieldModel.get( 'value' );
    var fieldOldValue = fieldModel.get( 'old_value' );
    DEBUG && console.log( 'New Value:', fieldValue );
    DEBUG && console.log( 'Old Value:', fieldOldValue );

    nfRadio.channel( 'fields' ).request( 'remove:error', fieldID, 'required-error' );

    if ( fieldOldValue != null && fieldValue ) {
    fieldModel.set( 'old_value', null );

    this.isBusy = false;
    DEBUG && console.log( 'Validated: Same Value' );
    DEBUG && console.groupEnd();
    return;
    }

    fieldModel.set( 'old_value', fieldValue );

    var formModel = nfRadio.channel( 'form-' + fieldModel.get( 'formID' ) ).request( 'get:form' );
    if ( ! formModel ) {
    this.isBusy = false;
    DEBUG && console.log( 'Skipping; Missing Form Model' );
    DEBUG && console.groupEnd();
    return;
    }
    DEBUG && console.groupEnd();
    },

    /**
    * @param {NFFieldModel} fieldModel
    */
    validateCaptcha: function ( fieldModel ) {
    var DEBUG = nfRecaptcha.DEBUG;

    DEBUG && console.group('nfRecaptcha.validateCaptcha --', 'Field:', fieldModel.id);

    var fieldID = fieldModel.id;
    var fieldType = fieldModel.get( 'type' );
    var fieldSize = fieldModel.get( 'size' );
    var grecaptchaID = fieldModel.get( 'grecaptchaID' );

    if ( this.isBusy ) {
    DEBUG && console.log( 'Skipping; Busy' );
    DEBUG && console.groupEnd();
    return;
    }

    if ( ! this.isSubmitting ) {
    DEBUG && console.log( 'Skipping; Not Submitting' );
    DEBUG && console.groupEnd();
    return;
    }

    if (
    fieldType !== 'recaptcha' ||
    fieldSize !== 'invisible' ||
    fieldModel.get( 'clean' )
    ) {
    DEBUG && console.log( 'Skipping; Not reCAPTCHA Field' );
    DEBUG && console.groupEnd();
    return;
    }

    if ( grecaptchaID == null ) {
    DEBUG && console.log( 'Skipping; Missing reCAPTCHA Widget ID' );
    DEBUG && console.groupEnd();
    return;
    }

    this.isBusy = true;

    var fieldValue = fieldModel.get( 'value' );
    var fieldOldValue = fieldModel.get( 'old_value' );
    DEBUG && console.log( 'New Value:', fieldValue );
    DEBUG && console.log( 'Old Value:', fieldOldValue );

    nfRadio.channel( 'fields' ).request( 'remove:error', fieldID, 'required-error' );

    if ( fieldOldValue != null && fieldValue ) {
    fieldModel.set( 'old_value', null );

    this.isBusy = false;
    DEBUG && console.log( 'CAPTCHA Validated' );
    DEBUG && console.groupEnd();
    return;
    }

    fieldModel.set( 'old_value', fieldValue );

    var formModel = nfRadio.channel( 'form-' + fieldModel.get( 'formID' ) ).request( 'get:form' );
    if ( ! formModel ) {
    this.isBusy = false;
    DEBUG && console.log( 'Skipping; Missing Form Model' );
    DEBUG && console.groupEnd();
    return;
    }

    nfRadio.channel( 'fields' ).request(
    'add:error',
    @@ -217,18 +298,26 @@ var nfRecaptcha = Marionette.Object.extend( {
    formModel.get('settings').recaptchaProcessing
    );

    try {
    DEBUG && console.log( 'Attempting to execute reCAPTCHA' );
    nf_reprocess_recaptcha( grecaptchaID );
    } catch ( e ) {
    this.isBusy = false;
    console.log( 'Notice: Error trying to execute grecaptcha.' );
    DEBUG && console.log( 'Caught error:', e );
    }

    DEBUG && console.groupEnd();
    }
    try {
    DEBUG && console.log( 'Attempting to execute reCAPTCHA' );
    nf_reprocess_recaptcha( grecaptchaID );
    } catch ( e ) {
    this.isBusy = false;
    console.warn( 'Notice: Error trying to execute grecaptcha.' );
    DEBUG && console.log( 'Caught error:', e );
    }

    DEBUG && console.groupEnd();
    }
    } );

    /** @var {boolean} - Display debug information in console. */
    nfRecaptcha.DEBUG = true;
    nfRecaptcha.DEBUG = false;

    var nfRenderRecaptcha = function() {
    new nfRecaptcha();
    }

    const nf_reprocess_recaptcha = ( grecaptchaID ) => {
    grecaptcha.execute( grecaptchaID );
    }
    539 changes: 332 additions & 207 deletions front-end-deps.js.diff
    Original file line number Diff line number Diff line change
    @@ -1,216 +1,341 @@
    --- assets/js/min/front-end-deps.js
    +++ assets/js/min/front-end-deps.js
    @@ -73,14 +73,26 @@
    var fieldID = jQuery( this ).data( 'fieldid' );
    if ( typeof window[ callback ] !== 'function' ){
    window[ callback ] = function( response ) {
    + nfRecaptcha.DEBUG && console.group(callback);
    + nfRecaptcha.DEBUG && console.log('fieldID:', fieldID);
    + nfRecaptcha.DEBUG && console.log('response:', response);
    +
    nfRadio.channel( 'recaptcha' ).request( 'update:response', response, fieldID );
    + nfRadio.channel( 'captcha' ).request( 'update:response', response, fieldID );
    +
    + nfRecaptcha.DEBUG && console.groupEnd();
    };
    }
    } );
    } );

    @@ -67,57 +67,314 @@

    var nfRadio = Backbone.Radio;

    -nfRadio.channel( 'form' ).on( 'render:view', function() {
    - jQuery( '.g-recaptcha' ).each( function() {
    - var callback = jQuery( this ).data( 'callback' );
    - var fieldID = jQuery( this ).data( 'fieldid' );
    - if ( typeof window[ callback ] !== 'function' ){
    - window[ callback ] = function( response ) {
    - nfRadio.channel( 'recaptcha' ).request( 'update:response', response, fieldID );
    - };
    - }
    - } );
    -} );
    -
    var nfRecaptcha = Marionette.Object.extend( {
    - initialize: function() {
    + /** @var {boolean} - Validate only one reCAPTCHA at a time. */
    + isBusy: false,
    + /** @var {?{ninja-forms:FormModel}} - Track which form is submitting. */
    + isSubmitting: null,
    +
    + initialize: function () {
    /*
    * If we've already rendered our form view, render our recaptcha fields.
    */
    @@ -91,33 +103,199 @@
    * We haven't rendered our form view, so hook into the view render radio message, and then render.
    */
    this.listenTo( nfRadio.channel( 'form' ), 'render:view', this.renderCaptcha );
    - /*
    - * If we've already rendered our form view, render our recaptcha fields.
    - */
    - if ( 0 != jQuery( '.g-recaptcha' ).length ) {
    - this.renderCaptcha();
    - }
    - /*
    - * We haven't rendered our form view, so hook into the view render radio message, and then render.
    - */
    - this.listenTo( nfRadio.channel( 'form' ), 'render:view', this.renderCaptcha );
    - this.listenTo( nfRadio.channel( 'captcha' ), 'reset', this.renderCaptcha );
    + this.listenTo( nfRadio.channel( 'captcha' ), 'reset', this.renderCaptcha );
    + this.listenTo( nfRadio.channel( 'submit' ), 'validate:field', this.validateCaptcha );
    + },
    - },
    -
    - renderCaptcha: function() {
    - jQuery( '.g-recaptcha:empty' ).each( function() {
    - var opts = {
    - fieldid: jQuery( this ).data( 'fieldid' ),
    - size: jQuery( this ).data( 'size' ),
    - theme: jQuery( this ).data( 'theme' ),
    - sitekey: jQuery( this ).data( 'sitekey' ),
    - callback: jQuery( this ).data( 'callback' )
    - };
    -
    - var grecaptchaID = grecaptcha.render( jQuery( this )[0], opts );
    -
    - if ( opts.size === 'invisible' ) {
    - try {
    - nf_reprocess_recaptcha( grecaptchaID );
    - setInterval(nf_reprocess_recaptcha, 110000, grecaptchaID);
    - } catch( e ){
    - console.log( 'Notice: Error trying to execute grecaptcha.' );
    - }
    - }
    - } );
    - }
    + /** @var {boolean} - Validate only one reCAPTCHA at a time. */
    + isBusy: false,
    + /** @var {?{ninja-forms:FormModel}} - Track which form is submitting. */
    + isSubmitting: null,
    +
    + initialize: function () {
    + var DEBUG = nfRecaptcha.DEBUG;
    +
    + DEBUG && console.group('nfRecaptcha.initialize');
    + DEBUG && console.log('.g-recaptcha', document.querySelectorAll('.g-recaptcha'));
    +
    + /*
    + * If we've already rendered our form view, render our recaptcha fields.
    + */
    + if ( 0 != jQuery( '.g-recaptcha' ).length ) {
    + this.renderAll();
    + }
    +
    + /*
    + * We haven't rendered our form view, so hook into the view render radio message, and then render.
    + */
    + this.listenTo( nfRadio.channel( 'recaptcha' ), 'render:view', this.renderFieldView );
    + this.listenTo( nfRadio.channel( 'submit' ), 'validate:field', this.validateCaptcha );
    +
    + DEBUG && console.groupEnd();
    + },
    +
    + /**
    + * @param {ninja-forms:FormModel} formModel - The Ninja Forms form model.
    + */
    + beforeSubmit: function ( formModel ) {
    + var DEBUG = nfRecaptcha.DEBUG;
    + beforeSubmit: function ( formModel ) {
    + var DEBUG = nfRecaptcha.DEBUG;
    +
    + DEBUG && console.group('nfRecaptcha.beforeSubmit');
    + DEBUG && console.group('nfRecaptcha.beforeSubmit');
    +
    + this.isSubmitting = formModel;
    + DEBUG && console.log('Form:', formModel);
    + this.isSubmitting = formModel;
    + DEBUG && console.log('Form:', formModel);
    +
    + DEBUG && console.groupEnd();
    },

    - renderCaptcha: function() {
    + renderCaptcha: function () {
    + var nfController = this,
    + DEBUG = nfRecaptcha.DEBUG;
    + DEBUG && console.groupEnd();
    + },
    +
    + DEBUG && console.group('nfRecaptcha.renderCaptcha');
    + renderAll: function ( formView ) {
    + var DEBUG = nfRecaptcha.DEBUG;
    +
    jQuery( '.g-recaptcha:empty' ).each( function() {
    + DEBUG && console.group('.g-recaptcha', this);
    + DEBUG && console.group('nfRecaptcha.renderAll');
    +
    + var fieldID = jQuery( this ).data( 'fieldid' );
    + DEBUG && console.log( 'Field:', fieldID );
    + var nfController = this;
    +
    var opts = {
    - fieldid: jQuery( this ).data( 'fieldid' ),
    + fieldid: fieldID,
    size: jQuery( this ).data( 'size' ),
    theme: jQuery( this ).data( 'theme' ),
    sitekey: jQuery( this ).data( 'sitekey' ),
    callback: jQuery( this ).data( 'callback' )
    };

    - var grecaptchaID = grecaptcha.render( jQuery( this )[0], opts );
    + DEBUG && console.log( 'Size:', opts.size );
    + DEBUG && console.log( 'Callback:', opts.callback );
    + jQuery( '.g-recaptcha:empty' ).each( function () {
    + nfController.renderCaptcha( this );
    + } );
    +
    + var grecaptchaID = grecaptcha.render( this, opts );
    + DEBUG && console.log( 'reCAPTCHA:', grecaptchaID );

    if ( opts.size === 'invisible' ) {
    - try {
    - nf_reprocess_recaptcha( grecaptchaID );
    - setInterval(nf_reprocess_recaptcha, 110000, grecaptchaID);
    - } catch( e ){
    - console.log( 'Notice: Error trying to execute grecaptcha.' );
    + var fieldModel = nfRadio.channel( 'fields' ).request( 'get:field', fieldID );
    +
    + if ( ! fieldModel ) {
    + DEBUG && console.log( 'Skipping; Missing Field Model' );
    + DEBUG && console.groupEnd();
    + return;
    }
    - }
    +
    + fieldModel.set( 'grecaptchaID', grecaptchaID );
    + DEBUG && console.groupEnd();
    + },
    +
    + /**
    + * @param {ninja-forms:FieldItem} fieldView - The Ninja Forms field view model.
    + */
    + renderFieldView: function ( fieldView ) {
    + var DEBUG = nfRecaptcha.DEBUG;
    +
    + DEBUG && console.group('nfRecaptcha.renderFieldView');
    +
    + DEBUG && console.log('Field View:', fieldView);
    + DEBUG && console.log('Field Model:', fieldView.model);
    + DEBUG && console.log('Field Element:', fieldView.el);
    +
    + var nfController = this;
    +
    + jQuery( fieldView.el ).find( '.g-recaptcha:empty' ).each( function () {
    + nfController.renderCaptcha( this, fieldView.model );
    + } );
    +
    + nfController.listenTo( nfRadio.channel( 'form-' + fieldModel.get( 'formID' ) ), 'before:submit', nfController.beforeSubmit );
    +
    + nfRadio.channel( 'captcha' ).reply( 'update:response', nfController.updateValidation, nfController );
    + }
    +
    + DEBUG && console.groupEnd();
    } );
    + DEBUG && console.groupEnd();
    + },
    +
    + DEBUG && console.groupEnd();
    + },
    +
    + /**
    + * @param {string} response - The reCAPTCHA response value.
    + * @param {string|number} fieldID - The Ninja Forms CAPTCHA field ID.
    + */
    + updateValidation: function ( response, fieldID ) {
    + var DEBUG = nfRecaptcha.DEBUG;
    +
    + DEBUG && console.group('nfRecaptcha.updateValidation');
    +
    + var fieldModel = nfRadio.channel( 'fields' ).request( 'get:field', fieldID );
    + DEBUG && console.log('Field:', fieldModel);
    +
    + if ( ! fieldModel ) {
    + DEBUG && console.log( 'Skipping; Missing Field Model' );
    + DEBUG && console.groupEnd();
    + return;
    + }
    +
    + nfRadio.channel( 'fields' ).request( 'remove:error', fieldID, 'recaptcha-processing' );
    +
    + var formModel = this.isSubmitting;
    +
    + this.isSubmitting = null;
    + this.isBusy = false;
    +
    + if ( formModel ) {
    + DEBUG && console.log( 'Attempting to re-submit form' );
    + nfRadio.channel( 'form-' + formModel.id ).request( 'submit', formModel );
    + } else {
    + DEBUG_CONTROLLER && console.log( 'Failed; Missing Form' );
    + }
    +
    + DEBUG && console.groupEnd();
    + },
    +
    + /**
    + * @param {NFFieldModel} fieldModel
    + */
    + validateCaptcha: function ( fieldModel ) {
    + var DEBUG = nfRecaptcha.DEBUG;
    +
    + DEBUG && console.group('nfRecaptcha.validateCaptcha --', 'Field:', fieldModel.id);
    +
    + var fieldID = fieldModel.id;
    + var fieldType = fieldModel.get( 'type' );
    + var fieldSize = fieldModel.get( 'size' );
    + var grecaptchaID = fieldModel.get( 'grecaptchaID' );
    +
    + if ( this.isBusy ) {
    + DEBUG && console.log( 'Skipping; Busy' );
    + DEBUG && console.groupEnd();
    + return;
    + }
    +
    + if ( ! this.isSubmitting ) {
    + DEBUG && console.log( 'Skipping; Not Submitting' );
    + DEBUG && console.groupEnd();
    + return;
    + }
    +
    + if (
    + fieldType !== 'recaptcha' ||
    + fieldSize !== 'invisible' ||
    + fieldModel.get( 'clean' )
    + ) {
    + DEBUG && console.log( 'Skipping; Not reCAPTCHA Field' );
    + DEBUG && console.groupEnd();
    + return;
    + }
    +
    + if ( grecaptchaID == null ) {
    + DEBUG && console.log( 'Skipping; Missing reCAPTCHA Widget ID' );
    + DEBUG && console.groupEnd();
    + return;
    + }
    +
    + this.isBusy = true;
    +
    + var fieldValue = fieldModel.get( 'value' );
    + var fieldOldValue = fieldModel.get( 'old_value' );
    + DEBUG && console.log( 'New Value:', fieldValue );
    + DEBUG && console.log( 'Old Value:', fieldOldValue );
    +
    + nfRadio.channel( 'fields' ).request( 'remove:error', fieldID, 'required-error' );
    +
    + if ( fieldOldValue != null && fieldValue ) {
    + fieldModel.set( 'old_value', null );
    +
    + this.isBusy = false;
    + DEBUG && console.log( 'Validated: Same Value' );
    + DEBUG && console.groupEnd();
    + return;
    + }
    +
    + fieldModel.set( 'old_value', fieldValue );
    +
    + var formModel = nfRadio.channel( 'form-' + fieldModel.get( 'formID' ) ).request( 'get:form' );
    + if ( ! formModel ) {
    + this.isBusy = false;
    + DEBUG && console.log( 'Skipping; Missing Form Model' );
    + DEBUG && console.groupEnd();
    + return;
    + }
    + /**
    + * @param {ninja-forms-multi-part:MainLayoutView} formView - The Ninja Forms form view model.
    + */
    + renderFormView: function ( formView ) {
    + var DEBUG = nfRecaptcha.DEBUG;
    +
    + DEBUG && console.group('nfRecaptcha.renderFormView');
    + DEBUG && console.log('Form View:', formView);
    + DEBUG && console.log('Form Model:', formView.model);
    +
    + var nfController = this;
    +
    + mainLayoutView.$el.find( '.g-recaptcha:empty' ).each( function () {
    + nfController.renderCaptcha( this );
    + } );
    +
    + DEBUG && console.groupEnd();
    + },
    +
    + /**
    + * @param {HTMLElement} fieldElement
    + * @param {NFFieldModel} fieldModel
    + */
    + renderCaptcha: function ( fieldElement, fieldModel ) {
    + var DEBUG = nfRecaptcha.DEBUG;
    +
    + DEBUG && console.group('nfRecaptcha.renderCaptcha');
    + DEBUG && console.log( 'Field Element:', fieldElement );
    +
    + if ( fieldElement.childElementCount > 0 ) {
    + DEBUG && console.log('Skipping; Not Empty');
    + DEBUG && console.groupEnd();
    + return;
    + }
    +
    + var $fieldEl = jQuery( fieldElement );
    +
    + var fieldID = fieldModel && fieldModel.id || $fieldEl.data( 'fieldid' );
    +
    + if ( ! fieldModel ) {
    + fieldModel = nfRadio.channel( 'fields' ).request( 'get:field', fieldID );
    +
    + if ( ! fieldModel ) {
    + DEBUG && console.log( 'Skipping; Missing Field Model' );
    + DEBUG && console.groupEnd();
    + return;
    + }
    + }
    +
    + var grecaptchaID = fieldModel.get( 'grecaptchaID' );
    + if ( grecaptchaID ) {
    + DEBUG && console.log( 'Skipping; reCAPTCHA already rendered' );
    + DEBUG && console.groupEnd();
    + return;
    + }
    +
    + DEBUG && console.log( 'Form ID:', Number.parseInt( fieldModel.get( 'formID' ) ) );
    + DEBUG && console.log( 'Field ID:', fieldID );
    + DEBUG && console.log( 'Field Model:', fieldModel );
    +
    + var opts = {
    + fieldid: fieldID,
    + size: $fieldEl.data( 'size' ),
    + theme: $fieldEl.data( 'theme' ),
    + sitekey: $fieldEl.data( 'sitekey' ),
    + callback: $fieldEl.data( 'callback' )
    + };
    +
    + DEBUG && console.log( 'Options:', opts );
    +
    + if ( typeof window[ opts.callback ] !== 'function' ) {
    + DEBUG && console.log( 'Create callback:', opts.callback );
    + window[ opts.callback ] = function ( response ) {
    + DEBUG && console.group(opts.callback);
    + DEBUG && console.log('fieldID:', fieldID);
    + DEBUG && console.log('response:', response);
    +
    + nfRadio.channel( 'recaptcha' ).request( 'update:response', response, fieldID );
    + nfRadio.channel( 'captcha' ).request( 'update:response', response, fieldID );
    +
    + DEBUG && console.groupEnd();
    + };
    + }
    +
    + try {
    + grecaptchaID = grecaptcha.render( fieldElement, opts );
    + DEBUG && console.log( 'reCAPTCHA:', grecaptchaID );
    + } catch ( e ) {
    + this.isBusy = false;
    + console.warn( 'Notice: Error trying to render grecaptcha.' );
    + DEBUG && console.log( 'Caught error:', e );
    + }
    +
    + if ( opts.size === 'invisible' ) {
    + fieldModel.set( 'grecaptchaID', grecaptchaID );
    +
    + this.listenTo( nfRadio.channel( 'form-' + fieldModel.get( 'formID' ) ), 'before:submit', this.beforeSubmit );
    +
    + nfRadio.channel( 'captcha' ).reply( 'update:response', this.updateValidation, this );
    + }
    +
    + DEBUG && console.groupEnd();
    + },
    +
    + /**
    + * @param {string} response - The reCAPTCHA response value.
    + * @param {string|number} fieldID - The Ninja Forms CAPTCHA field ID.
    + */
    + updateValidation: function ( response, fieldID ) {
    + var DEBUG = nfRecaptcha.DEBUG;
    +
    + DEBUG && console.group('nfRecaptcha.updateValidation');
    +
    + var fieldModel = nfRadio.channel( 'fields' ).request( 'get:field', fieldID );
    + DEBUG && console.log('Field:', fieldModel);
    +
    + if ( ! fieldModel ) {
    + DEBUG && console.log( 'Skipping; Missing Field Model' );
    + DEBUG && console.groupEnd();
    + return;
    + }
    +
    + nfRadio.channel( 'fields' ).request( 'remove:error', fieldID, 'recaptcha-processing' );
    +
    + var formModel = this.isSubmitting;
    +
    + this.isSubmitting = null;
    + this.isBusy = false;
    +
    + if ( formModel ) {
    + DEBUG && console.log( 'Attempting to re-submit form' );
    + nfRadio.channel( 'form-' + formModel.id ).request( 'submit', formModel );
    + } else {
    + DEBUG_CONTROLLER && console.log( 'Failed; Missing Form' );
    + }
    +
    + DEBUG && console.groupEnd();
    + },
    +
    + /**
    + * @param {NFFieldModel} fieldModel
    + */
    + validateCaptcha: function ( fieldModel ) {
    + var DEBUG = nfRecaptcha.DEBUG;
    +
    + DEBUG && console.group('nfRecaptcha.validateCaptcha --', 'Field:', fieldModel.id);
    +
    + var fieldID = fieldModel.id;
    + var fieldType = fieldModel.get( 'type' );
    + var fieldSize = fieldModel.get( 'size' );
    + var grecaptchaID = fieldModel.get( 'grecaptchaID' );
    +
    + if ( this.isBusy ) {
    + DEBUG && console.log( 'Skipping; Busy' );
    + DEBUG && console.groupEnd();
    + return;
    + }
    +
    + if ( ! this.isSubmitting ) {
    + DEBUG && console.log( 'Skipping; Not Submitting' );
    + DEBUG && console.groupEnd();
    + return;
    + }
    +
    + if (
    + fieldType !== 'recaptcha' ||
    + fieldSize !== 'invisible' ||
    + fieldModel.get( 'clean' )
    + ) {
    + DEBUG && console.log( 'Skipping; Not reCAPTCHA Field' );
    + DEBUG && console.groupEnd();
    + return;
    + }
    +
    + if ( grecaptchaID == null ) {
    + DEBUG && console.log( 'Skipping; Missing reCAPTCHA Widget ID' );
    + DEBUG && console.groupEnd();
    + return;
    + }
    +
    + this.isBusy = true;
    +
    + var fieldValue = fieldModel.get( 'value' );
    + var fieldOldValue = fieldModel.get( 'old_value' );
    + DEBUG && console.log( 'New Value:', fieldValue );
    + DEBUG && console.log( 'Old Value:', fieldOldValue );
    +
    + nfRadio.channel( 'fields' ).request( 'remove:error', fieldID, 'required-error' );
    +
    + if ( fieldOldValue != null && fieldValue ) {
    + fieldModel.set( 'old_value', null );
    +
    + this.isBusy = false;
    + DEBUG && console.log( 'CAPTCHA Validated' );
    + DEBUG && console.groupEnd();
    + return;
    + }
    +
    + fieldModel.set( 'old_value', fieldValue );
    +
    + var formModel = nfRadio.channel( 'form-' + fieldModel.get( 'formID' ) ).request( 'get:form' );
    + if ( ! formModel ) {
    + this.isBusy = false;
    + DEBUG && console.log( 'Skipping; Missing Form Model' );
    + DEBUG && console.groupEnd();
    + return;
    + }
    +
    + nfRadio.channel( 'fields' ).request(
    + 'add:error',
    @@ -219,21 +344,21 @@
    + formModel.get('settings').recaptchaProcessing
    + );
    +
    + try {
    + DEBUG && console.log( 'Attempting to execute reCAPTCHA' );
    + nf_reprocess_recaptcha( grecaptchaID );
    + } catch ( e ) {
    + this.isBusy = false;
    + console.log( 'Notice: Error trying to execute grecaptcha.' );
    + DEBUG && console.log( 'Caught error:', e );
    + }
    +
    + DEBUG && console.groupEnd();
    }
    + try {
    + DEBUG && console.log( 'Attempting to execute reCAPTCHA' );
    + nf_reprocess_recaptcha( grecaptchaID );
    + } catch ( e ) {
    + this.isBusy = false;
    + console.warn( 'Notice: Error trying to execute grecaptcha.' );
    + DEBUG && console.log( 'Caught error:', e );
    + }
    +
    + DEBUG && console.groupEnd();
    + }
    } );

    +/** @var {boolean} - Display debug information in console. */
    +nfRecaptcha.DEBUG = true;
    +nfRecaptcha.DEBUG = false;
    +
    var nfRenderRecaptcha = function() {
    new nfRecaptcha();
  4. mcaskill revised this gist Jun 5, 2023. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion WP-NF-Fix-reCAPTCHA_V2_Invisible.md
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,4 @@
    ## Fixes the eager execution/challenge of reCAPTCHA V2 Invisible
    ## README: Fix eager execution/challenge of reCAPTCHA V2 Invisible

    When a reCAPTCHA field is configured as "invisible", Ninja Forms will execute `grecaptcha.execute()` when the form is rendered and in intervals of 110 seconds, regardless of the form's state. If reCAPTCHA determines a challenge is necessary, its appearance on page load is unexpected and confusing, and its potential recurrence is very annoying for users.

  5. mcaskill renamed this gist Jun 5, 2023. 1 changed file with 0 additions and 0 deletions.
  6. mcaskill renamed this gist Jun 5, 2023. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  7. mcaskill revised this gist Jun 5, 2023. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,6 @@
    ## Fixes the eager execution/challenge of reCAPTCHA V2 Invisible

    When a reCAPTCHA field is configured as "invisible", Ninja Forms will execute `grecaptcha.execute()` every 110 seconds, regardless if the form's state, sometimes even rendering the challenge on page load.
    When a reCAPTCHA field is configured as "invisible", Ninja Forms will execute `grecaptcha.execute()` when the form is rendered and in intervals of 110 seconds, regardless of the form's state. If reCAPTCHA determines a challenge is necessary, its appearance on page load is unexpected and confusing, and its potential recurrence is very annoying for users.

    This patch replaces the interval callback with a more complex solution that involves interrupting the form submit with a temporary validation error (that displays "Processing…" error). When reCAPTCHA is done (checks and challenges completed), the temporary validation error is removed and the form is submitted to the server for final validation.

  8. mcaskill revised this gist Jun 2, 2023. 2 changed files with 246 additions and 0 deletions.
    6 changes: 6 additions & 0 deletions front-end-deps.js
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,9 @@
    /**
    * Customization of Ninja Forms reCAPTCHA Field
    *
    * The following must replace its equivalent in 'assets/js/min/front-end-deps.js'. See diff file.
    */

    nfRadio.channel( 'form' ).on( 'render:view', function() {
    jQuery( '.g-recaptcha' ).each( function() {
    var callback = jQuery( this ).data( 'callback' );
    240 changes: 240 additions & 0 deletions front-end-deps.js.diff
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,240 @@
    --- assets/js/min/front-end-deps.js
    +++ assets/js/min/front-end-deps.js
    @@ -73,14 +73,26 @@
    var fieldID = jQuery( this ).data( 'fieldid' );
    if ( typeof window[ callback ] !== 'function' ){
    window[ callback ] = function( response ) {
    + nfRecaptcha.DEBUG && console.group(callback);
    + nfRecaptcha.DEBUG && console.log('fieldID:', fieldID);
    + nfRecaptcha.DEBUG && console.log('response:', response);
    +
    nfRadio.channel( 'recaptcha' ).request( 'update:response', response, fieldID );
    + nfRadio.channel( 'captcha' ).request( 'update:response', response, fieldID );
    +
    + nfRecaptcha.DEBUG && console.groupEnd();
    };
    }
    } );
    } );

    var nfRecaptcha = Marionette.Object.extend( {
    - initialize: function() {
    + /** @var {boolean} - Validate only one reCAPTCHA at a time. */
    + isBusy: false,
    + /** @var {?{ninja-forms:FormModel}} - Track which form is submitting. */
    + isSubmitting: null,
    +
    + initialize: function () {
    /*
    * If we've already rendered our form view, render our recaptcha fields.
    */
    @@ -91,33 +103,199 @@
    * We haven't rendered our form view, so hook into the view render radio message, and then render.
    */
    this.listenTo( nfRadio.channel( 'form' ), 'render:view', this.renderCaptcha );
    - this.listenTo( nfRadio.channel( 'captcha' ), 'reset', this.renderCaptcha );
    + this.listenTo( nfRadio.channel( 'captcha' ), 'reset', this.renderCaptcha );
    + this.listenTo( nfRadio.channel( 'submit' ), 'validate:field', this.validateCaptcha );
    + },
    +
    + /**
    + * @param {ninja-forms:FormModel} formModel - The Ninja Forms form model.
    + */
    + beforeSubmit: function ( formModel ) {
    + var DEBUG = nfRecaptcha.DEBUG;
    +
    + DEBUG && console.group('nfRecaptcha.beforeSubmit');
    +
    + this.isSubmitting = formModel;
    + DEBUG && console.log('Form:', formModel);
    +
    + DEBUG && console.groupEnd();
    },

    - renderCaptcha: function() {
    + renderCaptcha: function () {
    + var nfController = this,
    + DEBUG = nfRecaptcha.DEBUG;
    +
    + DEBUG && console.group('nfRecaptcha.renderCaptcha');
    +
    jQuery( '.g-recaptcha:empty' ).each( function() {
    + DEBUG && console.group('.g-recaptcha', this);
    +
    + var fieldID = jQuery( this ).data( 'fieldid' );
    + DEBUG && console.log( 'Field:', fieldID );
    +
    var opts = {
    - fieldid: jQuery( this ).data( 'fieldid' ),
    + fieldid: fieldID,
    size: jQuery( this ).data( 'size' ),
    theme: jQuery( this ).data( 'theme' ),
    sitekey: jQuery( this ).data( 'sitekey' ),
    callback: jQuery( this ).data( 'callback' )
    };

    - var grecaptchaID = grecaptcha.render( jQuery( this )[0], opts );
    + DEBUG && console.log( 'Size:', opts.size );
    + DEBUG && console.log( 'Callback:', opts.callback );
    +
    + var grecaptchaID = grecaptcha.render( this, opts );
    + DEBUG && console.log( 'reCAPTCHA:', grecaptchaID );

    if ( opts.size === 'invisible' ) {
    - try {
    - nf_reprocess_recaptcha( grecaptchaID );
    - setInterval(nf_reprocess_recaptcha, 110000, grecaptchaID);
    - } catch( e ){
    - console.log( 'Notice: Error trying to execute grecaptcha.' );
    + var fieldModel = nfRadio.channel( 'fields' ).request( 'get:field', fieldID );
    +
    + if ( ! fieldModel ) {
    + DEBUG && console.log( 'Skipping; Missing Field Model' );
    + DEBUG && console.groupEnd();
    + return;
    }
    - }
    +
    + fieldModel.set( 'grecaptchaID', grecaptchaID );
    +
    + nfController.listenTo( nfRadio.channel( 'form-' + fieldModel.get( 'formID' ) ), 'before:submit', nfController.beforeSubmit );
    +
    + nfRadio.channel( 'captcha' ).reply( 'update:response', nfController.updateValidation, nfController );
    + }
    +
    + DEBUG && console.groupEnd();
    } );
    +
    + DEBUG && console.groupEnd();
    + },
    +
    + /**
    + * @param {string} response - The reCAPTCHA response value.
    + * @param {string|number} fieldID - The Ninja Forms CAPTCHA field ID.
    + */
    + updateValidation: function ( response, fieldID ) {
    + var DEBUG = nfRecaptcha.DEBUG;
    +
    + DEBUG && console.group('nfRecaptcha.updateValidation');
    +
    + var fieldModel = nfRadio.channel( 'fields' ).request( 'get:field', fieldID );
    + DEBUG && console.log('Field:', fieldModel);
    +
    + if ( ! fieldModel ) {
    + DEBUG && console.log( 'Skipping; Missing Field Model' );
    + DEBUG && console.groupEnd();
    + return;
    + }
    +
    + nfRadio.channel( 'fields' ).request( 'remove:error', fieldID, 'recaptcha-processing' );
    +
    + var formModel = this.isSubmitting;
    +
    + this.isSubmitting = null;
    + this.isBusy = false;
    +
    + if ( formModel ) {
    + DEBUG && console.log( 'Attempting to re-submit form' );
    + nfRadio.channel( 'form-' + formModel.id ).request( 'submit', formModel );
    + } else {
    + DEBUG_CONTROLLER && console.log( 'Failed; Missing Form' );
    + }
    +
    + DEBUG && console.groupEnd();
    + },
    +
    + /**
    + * @param {NFFieldModel} fieldModel
    + */
    + validateCaptcha: function ( fieldModel ) {
    + var DEBUG = nfRecaptcha.DEBUG;
    +
    + DEBUG && console.group('nfRecaptcha.validateCaptcha --', 'Field:', fieldModel.id);
    +
    + var fieldID = fieldModel.id;
    + var fieldType = fieldModel.get( 'type' );
    + var fieldSize = fieldModel.get( 'size' );
    + var grecaptchaID = fieldModel.get( 'grecaptchaID' );
    +
    + if ( this.isBusy ) {
    + DEBUG && console.log( 'Skipping; Busy' );
    + DEBUG && console.groupEnd();
    + return;
    + }
    +
    + if ( ! this.isSubmitting ) {
    + DEBUG && console.log( 'Skipping; Not Submitting' );
    + DEBUG && console.groupEnd();
    + return;
    + }
    +
    + if (
    + fieldType !== 'recaptcha' ||
    + fieldSize !== 'invisible' ||
    + fieldModel.get( 'clean' )
    + ) {
    + DEBUG && console.log( 'Skipping; Not reCAPTCHA Field' );
    + DEBUG && console.groupEnd();
    + return;
    + }
    +
    + if ( grecaptchaID == null ) {
    + DEBUG && console.log( 'Skipping; Missing reCAPTCHA Widget ID' );
    + DEBUG && console.groupEnd();
    + return;
    + }
    +
    + this.isBusy = true;
    +
    + var fieldValue = fieldModel.get( 'value' );
    + var fieldOldValue = fieldModel.get( 'old_value' );
    + DEBUG && console.log( 'New Value:', fieldValue );
    + DEBUG && console.log( 'Old Value:', fieldOldValue );
    +
    + nfRadio.channel( 'fields' ).request( 'remove:error', fieldID, 'required-error' );
    +
    + if ( fieldOldValue != null && fieldValue ) {
    + fieldModel.set( 'old_value', null );
    +
    + this.isBusy = false;
    + DEBUG && console.log( 'Validated: Same Value' );
    + DEBUG && console.groupEnd();
    + return;
    + }
    +
    + fieldModel.set( 'old_value', fieldValue );
    +
    + var formModel = nfRadio.channel( 'form-' + fieldModel.get( 'formID' ) ).request( 'get:form' );
    + if ( ! formModel ) {
    + this.isBusy = false;
    + DEBUG && console.log( 'Skipping; Missing Form Model' );
    + DEBUG && console.groupEnd();
    + return;
    + }
    +
    + nfRadio.channel( 'fields' ).request(
    + 'add:error',
    + fieldID,
    + 'recaptcha-processing',
    + formModel.get('settings').recaptchaProcessing
    + );
    +
    + try {
    + DEBUG && console.log( 'Attempting to execute reCAPTCHA' );
    + nf_reprocess_recaptcha( grecaptchaID );
    + } catch ( e ) {
    + this.isBusy = false;
    + console.log( 'Notice: Error trying to execute grecaptcha.' );
    + DEBUG && console.log( 'Caught error:', e );
    + }
    +
    + DEBUG && console.groupEnd();
    }
    } );

    +/** @var {boolean} - Display debug information in console. */
    +nfRecaptcha.DEBUG = true;
    +
    var nfRenderRecaptcha = function() {
    new nfRecaptcha();
    }
  9. mcaskill created this gist Jun 2, 2023.
    11 changes: 11 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,11 @@
    ## Fixes the eager execution/challenge of reCAPTCHA V2 Invisible

    When a reCAPTCHA field is configured as "invisible", Ninja Forms will execute `grecaptcha.execute()` every 110 seconds, regardless if the form's state, sometimes even rendering the challenge on page load.

    This patch replaces the interval callback with a more complex solution that involves interrupting the form submit with a temporary validation error (that displays "Processing…" error). When reCAPTCHA is done (checks and challenges completed), the temporary validation error is removed and the form is submitted to the server for final validation.

    Ideally, Ninja Forms would implement support for a promise-based solution to allow for controllers to take their time with whatever needs to be processed.

    ---

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    228 changes: 228 additions & 0 deletions front-end-deps.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,228 @@
    nfRadio.channel( 'form' ).on( 'render:view', function() {
    jQuery( '.g-recaptcha' ).each( function() {
    var callback = jQuery( this ).data( 'callback' );
    var fieldID = jQuery( this ).data( 'fieldid' );
    if ( typeof window[ callback ] !== 'function' ){
    window[ callback ] = function( response ) {
    nfRecaptcha.DEBUG && console.group(callback);
    nfRecaptcha.DEBUG && console.log('fieldID:', fieldID);
    nfRecaptcha.DEBUG && console.log('response:', response);

    nfRadio.channel( 'recaptcha' ).request( 'update:response', response, fieldID );
    nfRadio.channel( 'captcha' ).request( 'update:response', response, fieldID );

    nfRecaptcha.DEBUG && console.groupEnd();
    };
    }
    } );
    } );

    var nfRecaptcha = Marionette.Object.extend( {
    /** @var {boolean} - Validate only one reCAPTCHA at a time. */
    isBusy: false,
    /** @var {?{ninja-forms:FormModel}} - Track which form is submitting. */
    isSubmitting: null,

    initialize: function () {
    /*
    * If we've already rendered our form view, render our recaptcha fields.
    */
    if ( 0 != jQuery( '.g-recaptcha' ).length ) {
    this.renderCaptcha();
    }
    /*
    * We haven't rendered our form view, so hook into the view render radio message, and then render.
    */
    this.listenTo( nfRadio.channel( 'form' ), 'render:view', this.renderCaptcha );
    this.listenTo( nfRadio.channel( 'captcha' ), 'reset', this.renderCaptcha );
    this.listenTo( nfRadio.channel( 'submit' ), 'validate:field', this.validateCaptcha );
    },

    /**
    * @param {ninja-forms:FormModel} formModel - The Ninja Forms form model.
    */
    beforeSubmit: function ( formModel ) {
    var DEBUG = nfRecaptcha.DEBUG;

    DEBUG && console.group('nfRecaptcha.beforeSubmit');

    this.isSubmitting = formModel;
    DEBUG && console.log('Form:', formModel);

    DEBUG && console.groupEnd();
    },

    renderCaptcha: function () {
    var nfController = this,
    DEBUG = nfRecaptcha.DEBUG;

    DEBUG && console.group('nfRecaptcha.renderCaptcha');

    jQuery( '.g-recaptcha:empty' ).each( function() {
    DEBUG && console.group('.g-recaptcha', this);

    var fieldID = jQuery( this ).data( 'fieldid' );
    DEBUG && console.log( 'Field:', fieldID );

    var opts = {
    fieldid: fieldID,
    size: jQuery( this ).data( 'size' ),
    theme: jQuery( this ).data( 'theme' ),
    sitekey: jQuery( this ).data( 'sitekey' ),
    callback: jQuery( this ).data( 'callback' )
    };

    DEBUG && console.log( 'Size:', opts.size );
    DEBUG && console.log( 'Callback:', opts.callback );

    var grecaptchaID = grecaptcha.render( this, opts );
    DEBUG && console.log( 'reCAPTCHA:', grecaptchaID );

    if ( opts.size === 'invisible' ) {
    var fieldModel = nfRadio.channel( 'fields' ).request( 'get:field', fieldID );

    if ( ! fieldModel ) {
    DEBUG && console.log( 'Skipping; Missing Field Model' );
    DEBUG && console.groupEnd();
    return;
    }

    fieldModel.set( 'grecaptchaID', grecaptchaID );

    nfController.listenTo( nfRadio.channel( 'form-' + fieldModel.get( 'formID' ) ), 'before:submit', nfController.beforeSubmit );

    nfRadio.channel( 'captcha' ).reply( 'update:response', nfController.updateValidation, nfController );
    }

    DEBUG && console.groupEnd();
    } );

    DEBUG && console.groupEnd();
    },

    /**
    * @param {string} response - The reCAPTCHA response value.
    * @param {string|number} fieldID - The Ninja Forms CAPTCHA field ID.
    */
    updateValidation: function ( response, fieldID ) {
    var DEBUG = nfRecaptcha.DEBUG;

    DEBUG && console.group('nfRecaptcha.updateValidation');

    var fieldModel = nfRadio.channel( 'fields' ).request( 'get:field', fieldID );
    DEBUG && console.log('Field:', fieldModel);

    if ( ! fieldModel ) {
    DEBUG && console.log( 'Skipping; Missing Field Model' );
    DEBUG && console.groupEnd();
    return;
    }

    nfRadio.channel( 'fields' ).request( 'remove:error', fieldID, 'recaptcha-processing' );

    var formModel = this.isSubmitting;

    this.isSubmitting = null;
    this.isBusy = false;

    if ( formModel ) {
    DEBUG && console.log( 'Attempting to re-submit form' );
    nfRadio.channel( 'form-' + formModel.id ).request( 'submit', formModel );
    } else {
    DEBUG_CONTROLLER && console.log( 'Failed; Missing Form' );
    }

    DEBUG && console.groupEnd();
    },

    /**
    * @param {NFFieldModel} fieldModel
    */
    validateCaptcha: function ( fieldModel ) {
    var DEBUG = nfRecaptcha.DEBUG;

    DEBUG && console.group('nfRecaptcha.validateCaptcha --', 'Field:', fieldModel.id);

    var fieldID = fieldModel.id;
    var fieldType = fieldModel.get( 'type' );
    var fieldSize = fieldModel.get( 'size' );
    var grecaptchaID = fieldModel.get( 'grecaptchaID' );

    if ( this.isBusy ) {
    DEBUG && console.log( 'Skipping; Busy' );
    DEBUG && console.groupEnd();
    return;
    }

    if ( ! this.isSubmitting ) {
    DEBUG && console.log( 'Skipping; Not Submitting' );
    DEBUG && console.groupEnd();
    return;
    }

    if (
    fieldType !== 'recaptcha' ||
    fieldSize !== 'invisible' ||
    fieldModel.get( 'clean' )
    ) {
    DEBUG && console.log( 'Skipping; Not reCAPTCHA Field' );
    DEBUG && console.groupEnd();
    return;
    }

    if ( grecaptchaID == null ) {
    DEBUG && console.log( 'Skipping; Missing reCAPTCHA Widget ID' );
    DEBUG && console.groupEnd();
    return;
    }

    this.isBusy = true;

    var fieldValue = fieldModel.get( 'value' );
    var fieldOldValue = fieldModel.get( 'old_value' );
    DEBUG && console.log( 'New Value:', fieldValue );
    DEBUG && console.log( 'Old Value:', fieldOldValue );

    nfRadio.channel( 'fields' ).request( 'remove:error', fieldID, 'required-error' );

    if ( fieldOldValue != null && fieldValue ) {
    fieldModel.set( 'old_value', null );

    this.isBusy = false;
    DEBUG && console.log( 'Validated: Same Value' );
    DEBUG && console.groupEnd();
    return;
    }

    fieldModel.set( 'old_value', fieldValue );

    var formModel = nfRadio.channel( 'form-' + fieldModel.get( 'formID' ) ).request( 'get:form' );
    if ( ! formModel ) {
    this.isBusy = false;
    DEBUG && console.log( 'Skipping; Missing Form Model' );
    DEBUG && console.groupEnd();
    return;
    }

    nfRadio.channel( 'fields' ).request(
    'add:error',
    fieldID,
    'recaptcha-processing',
    formModel.get('settings').recaptchaProcessing
    );

    try {
    DEBUG && console.log( 'Attempting to execute reCAPTCHA' );
    nf_reprocess_recaptcha( grecaptchaID );
    } catch ( e ) {
    this.isBusy = false;
    console.log( 'Notice: Error trying to execute grecaptcha.' );
    DEBUG && console.log( 'Caught error:', e );
    }

    DEBUG && console.groupEnd();
    }
    } );

    /** @var {boolean} - Display debug information in console. */
    nfRecaptcha.DEBUG = true;
    88 changes: 88 additions & 0 deletions nf-field-recaptcha.php
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,88 @@
    <?php

    /**
    * Customization of Ninja Forms reCAPTCHA Field
    */
    class FieldRecaptcha
    {
    public function boot() : void
    {
    add_filter( 'ninja_forms_display_form_settings', [ $this, 'filter_ninja_forms_display_form_settings' ], 10, 2 );
    add_filter( 'ninja_forms_form_display_settings', [ $this, 'filter_ninja_forms_form_display_settings' ] );
    }

    /**
    * Adds custom labeling settings to the pool of all form settings in Ninja Forms.
    *
    * @listens filter:ninja_forms_form_display_settings
    *
    * @param array<string, array<string, mixed>> $settings The form settings.
    * @return array<string, array<string, mixed>>
    */
    public function filter_ninja_forms_form_display_settings( array $settings ) : array {
    if ( isset( $settings['custom_messages']['settings'] ) ) {
    $settings['custom_messages']['settings'] = array_merge(
    $settings['custom_messages']['settings'],
    $this->get_messages_form_settings()
    );
    }

    return $settings;
    }

    /**
    * Adds any custom labels to form settings in Ninja Forms.
    *
    * @listens filter:ninja_forms_display_form_settings
    *
    * @param array<string, array<string, mixed>> $settings The form settings.
    * @param int|string $form_id The form ID.
    * @return array<string, array<string, mixed>>
    */
    public function filter_ninja_forms_display_form_settings( array $settings, $form_id ) : array {
    foreach ( $this->get_messages() as $name => $label ) {
    if ( empty( $settings[ $name ] ) ) {
    $settings[ $name ] = $label;
    }
    }

    return $settings;
    }

    /**
    * Returns an associative array of error/messages.
    *
    * @return array<string, string>
    */
    public function get_messages() : array {
    $messages = [
    'recaptchaProcessing' => _x( 'Please wait, processing CAPTCHA field.', 'recaptcha field', 'nf-custom-recaptcha' ),
    'recaptchaRequired' => _x( 'Please complete the CAPTCHA field.', 'recaptcha field', 'nf-custom-recaptcha' ),
    'recaptchaMismatch' => _x( 'Please enter the correct value in the CAPTCHA field.', 'recaptcha field', 'nf-custom-recaptcha' ),
    ];

    return $messages;
    }

    /**
    * Retrieves custom validation form settings to Ninja Forms.
    *
    * @return array<string, array<string, mixed>>
    */
    public function get_messages_form_settings() : array {
    $settings = [];

    foreach ( $this->get_messages() as $name => $label ) {
    $setting = [
    'name' => $name,
    'type' => 'textbox',
    'label' => esc_html( $label ),
    'width' => 'full',
    ];

    $settings[] = $setting;
    }

    return $settings;
    }
    }