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;