PageSourceSearch

https://trangotech.com/blog/wp-content/plugins/local-cf7-submissio…/js/lcf7-fallback-capture.js?ver=3.5.0

js trangotech.com collected 2026-10-02 14:41:33 UTC 13,748 bytes, 424 lines download raw bytes

1(function () {
2    'use strict';
3
4    if (!window.LCF7FallbackCapture || !window.fetch) {
5        return;
6    }
7
8    var config = window.LCF7FallbackCapture;
9    var pending = new WeakMap();
10    var capturedTokens = Object.create(null);
11    var ignoredStatuses = ['init', 'validation_failed', 'acceptance_missing', 'invalid'];
12
13    function randomToken() {
14        if (window.crypto && typeof window.crypto.randomUUID === 'function') {
15            return window.crypto.randomUUID();
16        }
17
18        if (window.crypto && typeof window.crypto.getRandomValues === 'function') {
19            var bytes = new Uint8Array(24);
20            window.crypto.getRandomValues(bytes);
21            return Array.prototype.map.call(bytes, function (byte) {
22                return byte.toString(16).padStart(2, '0');
23            }).join('');
24        }
25
26        return String(Date.now()) + '-' + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
27    }
28
29    function getFormFromEvent(event) {
30        if (event && event.target) {
31            if (event.target.matches && event.target.matches('form.wpcf7-form')) {
32                return event.target;
33            }
34
35            if (event.target.querySelector) {
36                return event.target.querySelector('form.wpcf7-form');
37            }
38        }
39
40        return null;
41    }
42
43    function getSubmitButtonForm(target) {
44        if (!target || !target.closest) {
45            return null;
46        }
47
48        var button = target.closest('input[type="submit"], button[type="submit"], button:not([type]), .wpcf7-submit');
49
50        if (!button) {
51            return null;
52        }
53
54        var form = button.form || button.closest('form.wpcf7-form');
55        return form && form.matches && form.matches('form.wpcf7-form') ? form : null;
56    }
57
58    function ensureTokenInput(form, forceNew) {
59        var input = form.querySelector('input[name="_lcf7_capture_token"]');
60
61        if (!input) {
62            input = document.createElement('input');
63            input.type = 'hidden';
64            input.name = '_lcf7_capture_token';
65            form.appendChild(input);
66        }
67
68        if (forceNew || !input.value) {
69            input.value = randomToken();
70        }
71
72        return input.value;
73    }
74
75    function formSnapshot(form, forceNewToken) {
76        var token = ensureTokenInput(form, !!forceNewToken);
77        var data = new FormData(form);
78        var fields = [];
79        var formId = parseInt(data.get('_wpcf7'), 10) || 0;
80
81        data.forEach(function (value, name) {
82            if (value instanceof File) {
83                if (value.name) {
84                    fields.push({
85                        name: name + '_file_name',
86                        value: String(value.name)
87                    });
88                }
89                return;
90            }
91
92            if (name.indexOf('_wpcf7') === 0 || name === '_lcf7_capture_token') {
93                return;
94            }
95
96            fields.push({
97                name: name,
98                value: String(value)
99            });
100        });
101
102        return {
103            form: form,
104            formId: formId,
105            token: token,
106            fields: fields,
107            pageUrl: window.location.href,
108            timer: null,
109            sending: false,
110            sent: !!capturedTokens[token],
111            promise: null
112        };
113    }
114
115    function clearPending(form) {
116        var item = pending.get(form);
117
118        if (item && item.timer) {
119            window.clearTimeout(item.timer);
120        }
121
122        pending.delete(form);
123    }
124
125    function clearCaptureToken(form) {
126        var input = form.querySelector('input[name="_lcf7_capture_token"]');
127
128        if (input) {
129            input.value = '';
130        }
131
132        clearPending(form);
133    }
134
135    function showSavedSuccess(form) {
136        var wrapper = form.closest('.wpcf7');
137        var output = wrapper ? wrapper.querySelector('.wpcf7-response-output') : null;
138
139        form.classList.remove('invalid', 'failed', 'spam', 'aborted');
140        form.classList.add('sent');
141        form.setAttribute('data-status', 'sent');
142
143        if (wrapper) {
144            wrapper.classList.remove('invalid', 'failed', 'spam', 'aborted');
145            wrapper.classList.add('sent');
146        }
147
148        if (output) {
149            output.textContent = config.successMessage || 'Thank you. Your submission has been received.';
150            output.setAttribute('aria-hidden', 'false');
151        }
152    }
153
154    function shouldReplaceError(status) {
155        return ['mail_failed', 'mail_error', 'error', 'aborted', 'transport_error', 'failed'].indexOf(status) !== -1;
156    }
157
158    function isCaptchaField(control) {
159        var name = control && control.name ? String(control.name) : '';
160
161        return name === 'cf-turnstile-response' ||
162            name === '_wpcf7_turnstile_response' ||
163            name === 'g-recaptcha-response' ||
164            name.indexOf('turnstile') !== -1 ||
165            name.indexOf('recaptcha') !== -1;
166    }
167
168    function hasTurnstile(form) {
169        return !!form.querySelector(
170            'input[name="cf-turnstile-response"], input[name="_wpcf7_turnstile_response"], .cf-turnstile, .wpcf7-turnstile, [class*="turnstile"], iframe[src*="challenges.cloudflare.com"]'
171        );
172    }
173
174    function isTurnstilePending(form) {
175        if (!hasTurnstile(form)) {
176            return false;
177        }
178
179        var response = form.querySelector('input[name="_wpcf7_turnstile_response"], input[name="cf-turnstile-response"]');
180        return !response || !String(response.value || '').trim();
181    }
182
183    // Validate normal user fields but deliberately exclude Turnstile/reCAPTCHA.
184    // This prevents blank or obviously invalid leads from being emailed while still
185    // allowing a complete lead to be captured before a delayed CAPTCHA is ready.
186    function normalFieldsAreValid(form) {
187        var controls = Array.prototype.slice.call(form.querySelectorAll('input, select, textarea'));
188        var checkedGroups = Object.create(null);
189        var valid = true;
190
191        controls.forEach(function (control) {
192            if (!valid || control.disabled || isCaptchaField(control)) {
193                return;
194            }
195
196            var type = String(control.type || '').toLowerCase();
197
198            if (['hidden', 'submit', 'button', 'reset', 'image'].indexOf(type) !== -1) {
199                return;
200            }
201
202            var acceptance = control.closest ? control.closest('.wpcf7-acceptance') : null;
203            var acceptanceOptional = acceptance && acceptance.classList && acceptance.classList.contains('optional');
204            var required = !!control.required ||
205                control.getAttribute('aria-required') === 'true' ||
206                (control.classList && control.classList.contains('wpcf7-validates-as-required')) ||
207                (!!acceptance && !acceptanceOptional);
208
209            if ((type === 'checkbox' || type === 'radio') && required) {
210                var groupName = control.name || ('__single_' + controls.indexOf(control));
211
212                if (checkedGroups[groupName] === undefined) {
213                    var group = control.name
214                        ? controls.filter(function (item) {
215                            return item.name === control.name;
216                        })
217                        : [control];
218                    var anyChecked = group.some(function (item) {
219                        return !item.disabled && item.checked;
220                    });
221                    checkedGroups[groupName] = anyChecked;
222                }
223
224                if (!checkedGroups[groupName]) {
225                    valid = false;
226                }
227                return;
228            }
229
230            if (required) {
231                if (type === 'file') {
232                    if (!control.files || !control.files.length) {
233                        valid = false;
234                        return;
235                    }
236                } else if (!String(control.value || '').trim()) {
237                    valid = false;
238                    return;
239                }
240            }
241
242            if (String(control.value || '').trim() && typeof control.checkValidity === 'function' && !control.checkValidity()) {
243                valid = false;
244            }
245        });
246
247        return valid;
248    }
249
250    function sendFallback(item, status, replaceError) {
251        if (!item || !item.formId || !item.token) {
252            return Promise.resolve(false);
253        }
254
255        if (item.sent || capturedTokens[item.token]) {
256            return Promise.resolve(true);
257        }
258
259        if (item.sending && item.promise) {
260            return item.promise;
261        }
262
263        item.sending = true;
264
265        item.promise = fetch(config.endpoint, {
266            method: 'POST',
267            credentials: 'same-origin',
268            keepalive: true,
269            headers: {
270                'Content-Type': 'application/json'
271            },
272            body: JSON.stringify({
273                nonce: config.nonce,
274                form_id: item.formId,
275                token: item.token,
276                status: status || 'error',
277                page_url: item.pageUrl,
278                fields: item.fields
279            })
280        }).then(function (response) {
281            return response.json().then(function (body) {
282                return {
283                    ok: response.ok,
284                    body: body
285                };
286            });
287        }).then(function (result) {
288            item.sending = false;
289            item.promise = null;
290
291            if (!result.ok || !result.body || !result.body.saved) {
292                return false;
293            }
294
295            item.sent = true;
296            capturedTokens[item.token] = true;
297
298            if (replaceError) {
299                showSavedSuccess(item.form);
300            }
301
302            clearPending(item.form);
303            return true;
304        }).catch(function () {
305            item.sending = false;
306            item.promise = null;
307            return false;
308        });
309
310        return item.promise;
311    }
312
313    function getOrCreateItem(form) {
314        var token = ensureTokenInput(form, false);
315        var item = pending.get(form);
316
317        if (!item || item.token !== token) {
318            item = formSnapshot(form, false);
319            pending.set(form, item);
320        }
321
322        return item;
323    }
324
325    // Capture immediately when the visitor presses Submit while Turnstile has not
326    // generated its token yet. This runs before CF7/Turnstile can reject or postpone
327    // the request and does not prevent their normal validation from continuing.
328    function captureBeforeDelayedTurnstile(form) {
329        if (!form || !isTurnstilePending(form) || !normalFieldsAreValid(form)) {
330            return;
331        }
332
333        var item = getOrCreateItem(form);
334        sendFallback(item, 'turnstile_pending', false);
335    }
336
337    function preSubmitPointerHandler(event) {
338        var form = getSubmitButtonForm(event.target);
339
340        if (form) {
341            captureBeforeDelayedTurnstile(form);
342        }
343    }
344
345    // Capture-phase listeners run before Contact Form 7's button/submit handlers.
346    document.addEventListener('pointerdown', preSubmitPointerHandler, true);
347    document.addEventListener('click', preSubmitPointerHandler, true);
348
349    // Keyboard Enter and normal form submission are covered here. The same token is
350    // reused, so pointer/click/submit and PHP hooks cannot create duplicate records.
351    document.addEventListener('submit', function (event) {
352        var form = event.target;
353
354        if (!form || !form.matches || !form.matches('form.wpcf7-form')) {
355            return;
356        }
357
358        var item = getOrCreateItem(form);
359
360        if (isTurnstilePending(form) && normalFieldsAreValid(form)) {
361            sendFallback(item, 'turnstile_pending', false);
362        }
363
364        if (item.timer) {
365            window.clearTimeout(item.timer);
366        }
367
368        item.timer = window.setTimeout(function () {
369            sendFallback(item, 'transport_error', true);
370        }, Math.max(4000, parseInt(config.timeout, 10) || 9000));
371    }, true);
372
373    document.addEventListener('wpcf7invalid', function (event) {
374        var form = getFormFromEvent(event);
375        if (form) {
376            clearPending(form);
377        }
378    }, false);
379
380    document.addEventListener('wpcf7submit', function (event) {
381        var form = getFormFromEvent(event);
382
383        if (!form) {
384            return;
385        }
386
387        var status = event.detail && event.detail.apiResponse && event.detail.apiResponse.lcf7_original_status
388            ? String(event.detail.apiResponse.lcf7_original_status)
389            : (event.detail && event.detail.status ? String(event.detail.status) : 'error');
390
391        if (ignoredStatuses.indexOf(status) !== -1) {
392            clearPending(form);
393            return;
394        }
395
396        var item = getOrCreateItem(form);
397
398        if (item.timer) {
399            window.clearTimeout(item.timer);
400            item.timer = null;
401        }
402
403        // No extra fallback POST after a normal CF7 response. PHP has already
404        // captured and dispatched the lead in the same request. Browser fallback is
405        // used only for Turnstile-pending or transport-timeout cases.
406        if (shouldReplaceError(status)) {
407            showSavedSuccess(form);
408        }
409        clearPending(form);
410
411        if (status === 'mail_sent') {
412            window.setTimeout(function () {
413                clearCaptureToken(form);
414            }, 0);
415        }
416    }, false);
417
418    document.addEventListener('wpcf7reset', function (event) {
419        var form = getFormFromEvent(event);
420        if (form) {
421            clearCaptureToken(form);
422        }
423    }, false);
424}());

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.