PluginProbe
Contact Forms by Cimatti / 2.3.0
Contact Forms by Cimatti v2.3.0
2.3.6 2.3.5 2.3.0 2.2.32 2.2.4 2.2.0 2.1.2 2.1.1 trunk 1.0 1.1 1.2 1.2.1 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 All 62 releases
contact-forms / assets / js / frontend / recaptcha3.js

recaptcha3.js in Contact Forms by Cimatti 2.3.0, at assets/js/frontend/recaptcha3.js

204 lines 7.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Google reCAPTCHA v3 loader for Contact Forms.
3 *
4 * Each captcha_v3 field registers itself through the global queue
5 * `accuaformRecaptcha3Queue` (so registration works whether the field markup
6 * is parsed before or after this file loads). The Google api.js script is
7 * injected only after the visitor starts interacting with a form — or on
8 * demand at submit time — so no third-party request is made on page load.
9 *
10 * v3 tokens are single-use and expire after ~2 minutes, so the token is
11 * fetched when the form is submitted:
12 * - AJAX forms: the submit handler generated by AccuaForm.php calls
13 * accuaformRecaptcha3.getToken() (async), fills the hidden input and
14 * re-triggers the submit. After every AJAX response the handler calls
15 * accuaformRecaptcha3.reset() so a retry gets a fresh token.
16 * - Non-AJAX forms emit no submit JS at all (the whole handler block in
17 * AccuaForm.php is AJAX-only), so the delegated document-level submit
18 * handler at the bottom of this file does the same dance for them and
19 * resubmits natively (form.submit() skips handlers - no loop).
20 *
21 * This file is independent from recaptcha2.js (v2 checkbox): if a v2 form on
22 * the same page already loaded api.js in explicit-render mode, that same
23 * script is reused — grecaptcha.execute() works there too.
24 */
25
26 (function ($) {
27 var registered = {}; // inputId -> {sitekey: ..., action: ...}
28 var tokenTimestamps = {}; // inputId -> ms timestamp of last token
29 var listenerAttached = false;
30 var apiPromise = null;
31 var lang = '';
32 var TOKEN_MAX_AGE_MS = 110000; // Google tokens expire after 2 minutes
33
34 function firstSitekey() {
35 for (var id in registered) {
36 if (Object.prototype.hasOwnProperty.call(registered, id)) {
37 return registered[id].sitekey;
38 }
39 }
40 return '';
41 }
42
43 // Resolves when window.grecaptcha is available (never rejects).
44 function loadApi() {
45 if (apiPromise) {
46 return apiPromise;
47 }
48 apiPromise = new Promise(function (resolve) {
49 if (window.grecaptcha) {
50 resolve();
51 return;
52 }
53 // api.js may already be present (injected by recaptcha2.js for a v2
54 // form on the same page) but not yet executed: wait for it instead of
55 // injecting a second copy.
56 if (!document.querySelector('script[src*="recaptcha/api.js"]')) {
57 var script = document.createElement('script');
58 script.src = 'https://www.recaptcha.net/recaptcha/api.js?render=' +
59 encodeURIComponent(firstSitekey()) + '&hl=' + encodeURIComponent(lang);
60 script.async = true;
61 document.head.appendChild(script);
62 }
63 var waited = 0;
64 var poll = setInterval(function () {
65 waited += 200;
66 if (window.grecaptcha || waited >= 20000) {
67 clearInterval(poll);
68 resolve();
69 }
70 }, 200);
71 });
72 return apiPromise;
73 }
74
75 function formInputs(formEl) {
76 return $(formEl).find('input.accua_forms_recaptcha3_input').filter(function () {
77 return !!registered[this.id];
78 });
79 }
80
81 window.accuaformRecaptcha3 = {
82 // True when the form has a registered v3 input with no recent getToken()
83 // attempt. The timestamp is stamped on failed attempts too, so a broken
84 // grecaptcha cannot cause an endless submit -> fetch -> resubmit loop:
85 // the submit proceeds with an empty token and the server rejects it.
86 needsToken: function (formEl) {
87 var needed = false;
88 formInputs(formEl).each(function () {
89 var ts = tokenTimestamps[this.id];
90 if (!ts || (Date.now() - ts) > TOKEN_MAX_AGE_MS) {
91 needed = true;
92 }
93 });
94 return needed;
95 },
96
97 // Fetch fresh tokens for the form's v3 inputs. Always resolves: on any
98 // failure the input stays empty and the server rejects the submission
99 // (server-side validation is fail-closed) — the client must not hang.
100 getToken: function (formEl) {
101 var inputs = formInputs(formEl);
102 if (!inputs.length) {
103 return Promise.resolve();
104 }
105 var stampAll = function () {
106 inputs.each(function () {
107 tokenTimestamps[this.id] = Date.now();
108 });
109 };
110 return loadApi().then(function () {
111 if (!window.grecaptcha || !window.grecaptcha.execute) {
112 stampAll();
113 return;
114 }
115 return new Promise(function (resolve) {
116 window.grecaptcha.ready(function () {
117 var pending = inputs.length;
118 inputs.each(function () {
119 var input = this;
120 var reg = registered[input.id];
121 var done = function () {
122 tokenTimestamps[input.id] = Date.now();
123 pending--;
124 if (pending <= 0) {
125 resolve();
126 }
127 };
128 try {
129 window.grecaptcha.execute(reg.sitekey, {action: reg.action}).then(function (token) {
130 input.value = token;
131 done();
132 }, done);
133 } catch (e) {
134 done();
135 }
136 });
137 });
138 });
139 });
140 },
141
142 // Clear used tokens so the next submit attempt fetches fresh ones.
143 reset: function (formEl) {
144 formInputs(formEl).each(function () {
145 this.value = '';
146 delete tokenTimestamps[this.id];
147 });
148 }
149 };
150
151 // item = [inputId, {sitekey, action}, language]
152 function register(item) {
153 var id = item[0];
154 if (registered[id]) {
155 return;
156 }
157 registered[id] = item[1];
158 if (item[2] && !lang) {
159 lang = item[2];
160 }
161 if (!listenerAttached) {
162 listenerAttached = true;
163 // Same privacy-preserving lazy trigger as recaptcha2.js: no Google
164 // request until the visitor interacts with a form. getToken() also
165 // loads the api on demand as a fallback (form submitted without any
166 // change event).
167 $(document).one('change', '.accuaforms-field-required, .pfbc-fieldwrap > *', function () {
168 loadApi();
169 });
170 }
171 }
172
173 var queue = window.accuaformRecaptcha3Queue = window.accuaformRecaptcha3Queue || [];
174 for (var i = 0; i < queue.length; i++) {
175 register(queue[i]);
176 }
177 queue.push = function (item) {
178 register(item);
179 return queue.length;
180 };
181
182 // Token gate for non-AJAX forms. AJAX forms are gated by the inline
183 // onsubmit handler generated in AccuaForm.php, which cancels the event
184 // while it fetches the token — in that case the event arrives here already
185 // default-prevented (or with the token in place) and this handler is a
186 // no-op. Non-AJAX forms have no submit JS at all, so this handler cancels
187 // the native submit, fetches the token and resubmits with form.submit(),
188 // which does not re-fire submit handlers.
189 $(document).on('submit', 'form', function (e) {
190 var form = this;
191 if (e.isDefaultPrevented()) {
192 return;
193 }
194 if (!window.accuaformRecaptcha3.needsToken(form)) {
195 return;
196 }
197 e.preventDefault();
198 var resubmit = function () {
199 form.submit();
200 };
201 window.accuaformRecaptcha3.getToken(form).then(resubmit, resubmit);
202 });
203 })(jQuery);
204