Skip to content

Commit 2b272f7

Browse files
authored
Merge pull request #55 from GravityKit/develop
Release 1.7.2
2 parents 3d0ff8e + 311a138 commit 2b272f7

7 files changed

Lines changed: 186 additions & 147 deletions

assets/js/token-injection.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/**
2+
* Gravity Forms Zero Spam — token injection.
3+
*
4+
* Registers an async pre-submission filter (GF 2.9+) or a submit-event
5+
* listener (older GF) for each form configured in gfZeroSpamConfig.
6+
*
7+
* @since TBD
8+
*/
9+
(() => {
10+
if (typeof gfZeroSpamConfig === 'undefined') {
11+
return;
12+
}
13+
14+
const forms = gfZeroSpamConfig.forms;
15+
16+
if (!forms || !forms.length) {
17+
return;
18+
}
19+
20+
/**
21+
* Fetches a fresh token, falling back through REST → admin-ajax → embedded fallback.
22+
*/
23+
function fetchToken(cfg) {
24+
return fetch(cfg.restUrl + '?form_id=' + cfg.formId, {
25+
signal: AbortSignal.timeout(cfg.timeout)
26+
})
27+
.then((res) => {
28+
if (!res.ok) {
29+
throw new Error('REST failed');
30+
}
31+
return res.json();
32+
})
33+
.then((json) => json.token)
34+
.catch(() => fetch(cfg.ajaxUrl + '?action=gf_zero_spam_token&form_id=' + cfg.formId, {
35+
signal: AbortSignal.timeout(cfg.timeout)
36+
})
37+
.then((res) => {
38+
if (!res.ok) {
39+
throw new Error('AJAX failed');
40+
}
41+
return res.json();
42+
})
43+
.then((json) => json.token)
44+
.catch(() => cfg.fallbackToken));
45+
}
46+
47+
/**
48+
* Injects the token hidden input into the form element.
49+
*/
50+
function injectToken(formEl, token) {
51+
const old = formEl.querySelector('input[name="gf_zero_spam_token"]');
52+
53+
if (old) {
54+
old.remove();
55+
}
56+
57+
const input = document.createElement('input');
58+
input.type = 'hidden';
59+
input.name = 'gf_zero_spam_token';
60+
input.value = token;
61+
input.setAttribute('autocomplete', 'new-password');
62+
formEl.appendChild(input);
63+
}
64+
65+
// GF 2.9+ path: register a global async pre-submission filter.
66+
if (typeof gform !== 'undefined' && gform.utils && gform.utils.addAsyncFilter) {
67+
const formIds = {};
68+
69+
forms.forEach((cfg) => {
70+
formIds[cfg.formId] = cfg;
71+
});
72+
73+
gform.utils.addAsyncFilter('gform/submission/pre_submission', (data) => {
74+
const id = parseInt(data.form.dataset.formid, 10);
75+
const cfg = formIds[id];
76+
77+
if (!cfg) {
78+
return Promise.resolve(data);
79+
}
80+
81+
return fetchToken(cfg).then((token) => {
82+
injectToken(data.form, token);
83+
return data;
84+
});
85+
});
86+
87+
return;
88+
}
89+
90+
// Legacy path (GF < 2.9): attach submit handlers per form.
91+
forms.forEach((cfg) => {
92+
const formEl = document.getElementById('gform_' + cfg.formId);
93+
94+
if (!formEl || formEl.dataset.gfzsBound) {
95+
return;
96+
}
97+
98+
formEl.dataset.gfzsBound = '1';
99+
formEl.addEventListener('submit', function (e) {
100+
e.preventDefault();
101+
102+
fetchToken(cfg).then((token) => {
103+
injectToken(this, token);
104+
this.submit();
105+
});
106+
});
107+
});
108+
})();

gravityforms-zero-spam.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Plugin Name: Gravity Forms Zero Spam
44
* Plugin URI: https://www.gravitykit.com?utm_source=plugin&utm_campaign=zero-spam&utm_content=pluginuri
55
* Description: Enhance Gravity Forms to include effective anti-spam measures—without using a CAPTCHA.
6-
* Version: 1.7.1
6+
* Version: 1.7.2
77
* Author: GravityKit
88
* Author URI: https://www.gravitykit.com?utm_source=plugin&utm_campaign=zero-spam&utm_content=authoruri
99
* Requires PHP: 7.4

includes/class-email-rejection-field-settings.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public function maybe_enqueue_assets() {
6464

6565
wp_enqueue_script(
6666
'gf-zero-spam',
67-
$plugin_dir . 'dist/js/gf-zero-spam.js',
67+
$plugin_dir . 'dist/js/gf-zero-spam-admin.js',
6868
[],
6969
$version,
7070
true

includes/class-email-rejection-settings.php

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -124,19 +124,12 @@ private static function sanitize_rule( $rule ) {
124124
/**
125125
* Gets the asset version string for cache-busting.
126126
*
127-
* Uses the plugin version constant when available, falls back to
128-
* the JS file's modification time.
129-
*
130127
* @since 1.5.0
131128
*
132129
* @return string
133130
*/
134131
public static function get_asset_version() {
135-
if ( defined( 'GF_ZERO_SPAM_VERSION' ) ) {
136-
return GF_ZERO_SPAM_VERSION;
137-
}
138-
139-
$mtime = @filemtime( dirname( __DIR__ ) . '/dist/js/gf-zero-spam.js' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Graceful fallback when file is missing.
132+
$mtime = @filemtime( dirname( __DIR__ ) . '/dist/js/gf-zero-spam-admin.js' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Graceful fallback when file is missing.
140133

141134
return $mtime ? (string) $mtime : '1.0.0';
142135
}
@@ -241,7 +234,7 @@ public function maybe_enqueue_assets() {
241234

242235
wp_enqueue_script(
243236
'gf-zero-spam',
244-
$plugin_dir . 'dist/js/gf-zero-spam.js',
237+
$plugin_dir . 'dist/js/gf-zero-spam-admin.js',
245238
[],
246239
$version,
247240
true

includes/class-gf-zero-spam.php

Lines changed: 66 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@
66

77
class GF_Zero_Spam {
88

9+
/**
10+
* Scripts queued for output after each form.
11+
*
12+
* @since TBD
13+
*
14+
* @var array<int, string> Keyed by form ID.
15+
*/
16+
private $pending_scripts = [];
17+
918
/**
1019
* Instantiates the plugin on Gravity Forms loading.
1120
*
@@ -47,6 +56,7 @@ public function __construct() {
4756
new GF_Zero_Spam_Token_Endpoint();
4857

4958
add_action( 'gform_register_init_scripts', [ $this, 'add_key_field' ], 1 );
59+
add_filter( 'gform_get_form_filter', [ $this, 'enqueue_script' ], 10, 2 );
5060
add_filter( 'gform_entry_is_spam', [ $this, 'check_key_field' ], 10, 3 );
5161
add_filter( 'gform_incomplete_submission_pre_save', [ $this, 'add_zero_spam_key_to_entry' ], 10, 3 );
5262
add_filter( 'gform_abort_submission_with_confirmation', [ $this, 'maybe_abort_submission' ], 20, 2 );
@@ -167,11 +177,13 @@ public function get_key() {
167177
}
168178

169179
/**
170-
* Injects the hidden field and key into the form at submission.
180+
* Collects the Zero Spam configuration for a form.
171181
*
172-
* @since 1.0
182+
* The configuration is passed to a separate JavaScript file via
183+
* wp_localize_script to avoid breaking Gravity Forms' conditional logic
184+
* if a JS optimization plugin mangles the inline script.
173185
*
174-
* @uses GFFormDisplay::add_init_script() to inject the code into the `gform_post_render` hook.
186+
* @since 1.0
175187
*
176188
* @param array $form The Form Object.
177189
*
@@ -200,10 +212,6 @@ public function add_key_field( $form ) {
200212

201213
$form_id = (int) $form['id'];
202214

203-
$fallback_token = GF_Zero_Spam_Token::mint( $form_id, DAY_IN_SECONDS );
204-
$rest_url = esc_url( rest_url( 'gf-zero-spam/v1/token' ) );
205-
$ajax_url = esc_url( admin_url( 'admin-ajax.php' ) );
206-
207215
/**
208216
* Filters the timeout (in milliseconds) for AJAX token fetch attempts.
209217
*
@@ -213,131 +221,57 @@ public function add_key_field( $form ) {
213221
*/
214222
$timeout = (int) apply_filters( 'gf_zero_spam_token_fetch_timeout', 3000 );
215223

216-
// Embedded as a JS object literal since add_init_script doesn't use a registered script handle.
217-
$config = wp_json_encode(
218-
[
219-
'restUrl' => $rest_url,
220-
'ajaxUrl' => $ajax_url,
221-
'fallbackToken' => $fallback_token,
222-
'formId' => $form_id,
223-
'timeout' => $timeout,
224-
]
225-
);
226-
227-
if ( version_compare( GFForms::$version, '2.9.0', '>=' ) ) {
228-
$script = <<<EOD
229-
gform.utils.addAsyncFilter('gform/submission/pre_submission', async (data) => {
230-
const cfg = {$config};
231-
232-
// Only process the form this filter was registered for.
233-
if (parseInt(data.form.dataset.formid, 10) !== cfg.formId) {
234-
return data;
235-
}
236-
237-
let token = cfg.fallbackToken;
238-
239-
try {
240-
const ctrl = new AbortController();
241-
const timer = setTimeout(() => ctrl.abort(), cfg.timeout);
242-
const res = await fetch(cfg.restUrl + '?form_id=' + cfg.formId, { signal: ctrl.signal });
243-
244-
clearTimeout(timer);
245-
246-
if (res.ok) {
247-
const json = await res.json();
248-
token = json.token;
249-
} else {
250-
throw new Error('REST failed');
251-
}
252-
} catch (e) {
253-
try {
254-
const ctrl2 = new AbortController();
255-
const timer2 = setTimeout(() => ctrl2.abort(), cfg.timeout);
256-
const res2 = await fetch(cfg.ajaxUrl + '?action=gf_zero_spam_token&form_id=' + cfg.formId, { signal: ctrl2.signal });
257-
258-
clearTimeout(timer2);
259-
260-
if (res2.ok) {
261-
const json2 = await res2.json();
262-
token = json2.token;
263-
}
264-
} catch (e2) {
265-
// Both endpoints failed; use fallback token.
266-
}
267-
}
268-
269-
const old = data.form.querySelector('input[name="gf_zero_spam_token"]');
270-
if (old) { old.remove(); }
271-
272-
const input = document.createElement('input');
273-
input.type = 'hidden';
274-
input.name = 'gf_zero_spam_token';
275-
input.value = token;
276-
input.setAttribute('autocomplete', 'new-password');
277-
data.form.appendChild(input);
278-
279-
return data;
280-
});
281-
EOD;
282-
} else {
283-
$script = <<<EOD
284-
const gfzsForm = document.getElementById('gform_{$form_id}');
285-
286-
if (gfzsForm && !gfzsForm.dataset.gfzsBound) {
287-
gfzsForm.dataset.gfzsBound = '1';
288-
gfzsForm.addEventListener('submit', async function(e) {
289-
e.preventDefault();
290-
291-
const cfg = {$config};
292-
let token = cfg.fallbackToken;
293-
294-
try {
295-
const ctrl = new AbortController();
296-
const timer = setTimeout(() => ctrl.abort(), cfg.timeout);
297-
const res = await fetch(cfg.restUrl + '?form_id=' + cfg.formId, { signal: ctrl.signal });
298-
299-
clearTimeout(timer);
300-
301-
if (res.ok) {
302-
const json = await res.json();
303-
token = json.token;
304-
} else {
305-
throw new Error('REST failed');
306-
}
307-
} catch (e1) {
308-
try {
309-
const ctrl2 = new AbortController();
310-
const timer2 = setTimeout(() => ctrl2.abort(), cfg.timeout);
311-
const res2 = await fetch(cfg.ajaxUrl + '?action=gf_zero_spam_token&form_id=' + cfg.formId, { signal: ctrl2.signal });
312-
313-
clearTimeout(timer2);
314-
315-
if (res2.ok) {
316-
const json2 = await res2.json();
317-
token = json2.token;
318-
}
319-
} catch (e2) {
320-
// Both endpoints failed; use fallback token.
321-
}
322-
}
323-
324-
const old = this.querySelector('input[name="gf_zero_spam_token"]');
325-
if (old) { old.remove(); }
326-
327-
const input = document.createElement('input');
328-
input.type = 'hidden';
329-
input.name = 'gf_zero_spam_token';
330-
input.value = token;
331-
input.setAttribute('autocomplete', 'new-password');
332-
this.appendChild(input);
333-
334-
this.submit();
335-
});
336-
}
337-
EOD;
338-
}
339-
340-
GFFormDisplay::add_init_script( $form_id, 'gf-zero-spam', GFFormDisplay::ON_PAGE_RENDER, $script );
224+
$this->pending_scripts[ $form_id ] = [
225+
'restUrl' => esc_url_raw( rest_url( 'gf-zero-spam/v1/token' ) ),
226+
'ajaxUrl' => esc_url_raw( admin_url( 'admin-ajax.php' ) ),
227+
'fallbackToken' => GF_Zero_Spam_Token::mint( $form_id, DAY_IN_SECONDS ),
228+
'formId' => $form_id,
229+
'timeout' => $timeout,
230+
];
231+
}
232+
233+
/**
234+
* Enqueues the Zero Spam script with collected form configurations.
235+
*
236+
* Uses a separate JavaScript file loaded after Gravity Forms' scripts so
237+
* that any error does not prevent conditional logic from executing, which
238+
* would leave the form hidden with display:none.
239+
*
240+
* @since TBD
241+
*
242+
* @param string $form_string The form HTML.
243+
* @param array $form The Form Object.
244+
*
245+
* @return string The unmodified form HTML.
246+
*/
247+
public function enqueue_script( $form_string, $form ) {
248+
if ( empty( $this->pending_scripts ) ) {
249+
return $form_string;
250+
}
251+
252+
if ( wp_script_is( 'gf-zero-spam', 'enqueued' ) ) {
253+
return $form_string;
254+
}
255+
256+
$handle = version_compare( GFForms::$version, '2.9.0', '>=' )
257+
? 'gform_gravityforms_utils'
258+
: 'gform_gravityforms';
259+
260+
wp_enqueue_script(
261+
'gf-zero-spam',
262+
plugins_url( 'dist/js/gf-zero-spam.js', GF_ZERO_SPAM_FILE ),
263+
[ $handle ],
264+
(string) @filemtime( GF_ZERO_SPAM_DIR . 'dist/js/gf-zero-spam.js' ), // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Graceful fallback when file is missing.
265+
true
266+
);
267+
268+
wp_localize_script(
269+
'gf-zero-spam',
270+
'gfZeroSpamConfig',
271+
[ 'forms' => array_values( $this->pending_scripts ) ]
272+
);
273+
274+
return $form_string;
341275
}
342276

343277
/**

0 commit comments

Comments
 (0)