(Grav GitSync) Automatic Commit from GitSync
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Function to refresh a captcha image
|
||||
const refreshCaptchaImage = function(container) {
|
||||
const img = container.querySelector('img');
|
||||
if (!img) {
|
||||
console.warn('Cannot find captcha image in container');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the base URL and field ID
|
||||
const baseUrl = img.dataset.baseUrl || img.src.split('?')[0];
|
||||
const fieldId = img.dataset.fieldId || container.dataset.fieldId;
|
||||
|
||||
// Force reload by adding/updating timestamp and field ID
|
||||
const timestamp = new Date().getTime();
|
||||
let newUrl = baseUrl + '?t=' + timestamp;
|
||||
if (fieldId) {
|
||||
newUrl += '&field=' + fieldId;
|
||||
}
|
||||
img.src = newUrl;
|
||||
|
||||
// Also clear the input field if we can find it
|
||||
const formField = container.closest('.form-field');
|
||||
if (formField) {
|
||||
const input = formField.querySelector('input[type="text"]');
|
||||
if (input) {
|
||||
input.value = '';
|
||||
// Try to focus the input
|
||||
try { input.focus(); } catch(e) {}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Function to set up click handlers for refresh buttons
|
||||
const setupRefreshButtons = function() {
|
||||
// Find all captcha containers
|
||||
const containers = document.querySelectorAll('[data-captcha-provider="basic-captcha"]');
|
||||
|
||||
containers.forEach(function(container) {
|
||||
// Find the refresh button within this container
|
||||
const button = container.querySelector('button');
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove any existing listeners (just in case)
|
||||
button.removeEventListener('click', handleRefreshClick);
|
||||
|
||||
// Add the click handler
|
||||
button.addEventListener('click', handleRefreshClick);
|
||||
});
|
||||
};
|
||||
|
||||
// Click handler function
|
||||
const handleRefreshClick = function(event) {
|
||||
// Prevent default behavior and stop propagation
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
// Find the container
|
||||
const container = this.closest('[data-captcha-provider="basic-captcha"]');
|
||||
if (!container) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Refresh the image
|
||||
refreshCaptchaImage(container);
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// Set up a mutation observer to handle dynamically added captchas
|
||||
const setupMutationObserver = function() {
|
||||
// Check if MutationObserver is available
|
||||
if (typeof MutationObserver === 'undefined') return;
|
||||
|
||||
// Create a mutation observer to watch for new captcha elements
|
||||
const observer = new MutationObserver(function(mutations) {
|
||||
let needsSetup = false;
|
||||
|
||||
mutations.forEach(function(mutation) {
|
||||
if (mutation.type === 'childList' && mutation.addedNodes.length) {
|
||||
// Check if any of the added nodes contain our captcha containers
|
||||
for (let i = 0; i < mutation.addedNodes.length; i++) {
|
||||
const node = mutation.addedNodes[i];
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
// Check if this element has or contains captcha containers
|
||||
if (node.querySelector && (
|
||||
node.matches('[data-captcha-provider="basic-captcha"]') ||
|
||||
node.querySelector('[data-captcha-provider="basic-captcha"]')
|
||||
)) {
|
||||
needsSetup = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (needsSetup) {
|
||||
setupRefreshButtons();
|
||||
}
|
||||
});
|
||||
|
||||
// Start observing the document
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
};
|
||||
|
||||
// Initialize on DOM ready
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
setupRefreshButtons();
|
||||
setupMutationObserver();
|
||||
|
||||
// Also connect to XHR system if available (for best of both worlds)
|
||||
if (window.GravFormXHR && window.GravFormXHR.captcha) {
|
||||
window.GravFormXHR.captcha.register('basic-captcha', {
|
||||
reset: function(container, form) {
|
||||
refreshCaptchaImage(container);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,150 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Set window.CAP_CUSTOM_WASM_URL from any cap container's data attribute
|
||||
* so the vendored WASM binary is used instead of the default jsDelivr CDN.
|
||||
* Safe to call multiple times; noop after first assignment.
|
||||
*/
|
||||
function ensureWasmUrl(root) {
|
||||
if (window.CAP_CUSTOM_WASM_URL) return;
|
||||
const scope = root || document;
|
||||
const c = scope.querySelector('[data-captcha-provider="cap"][data-cap-wasm-url]');
|
||||
if (c) window.CAP_CUSTOM_WASM_URL = c.dataset.capWasmUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wire up a single invisible-mode Cap container:
|
||||
* - starts a speculative background solve
|
||||
* - intercepts the enclosing form's submit to wait for the token
|
||||
* - exposes __capReset / __capWired on the container so we can
|
||||
* skip double-wiring and re-arm after an XHR submit.
|
||||
*
|
||||
* Safe to call repeatedly: returns early if the container is already wired.
|
||||
*/
|
||||
function wireInvisibleContainer(container) {
|
||||
if (!container || container.__capWired) return;
|
||||
if (typeof window.Cap !== 'function') {
|
||||
// cap.min.js hasn't loaded yet — try again shortly.
|
||||
setTimeout(() => wireInvisibleContainer(container), 50);
|
||||
return;
|
||||
}
|
||||
|
||||
const form = container.closest('form') || document.getElementById(container.dataset.formId);
|
||||
if (!form) return;
|
||||
|
||||
const tokenInput = container.querySelector('input[name="cap-token"]');
|
||||
if (!tokenInput) return;
|
||||
|
||||
const endpoint = container.dataset.capApiEndpoint || '/forms-cap/';
|
||||
|
||||
container.__capWired = true;
|
||||
|
||||
const cap = new window.Cap({ apiEndpoint: endpoint });
|
||||
let solvePromise = null;
|
||||
let verified = false;
|
||||
|
||||
const startSolve = () => {
|
||||
verified = false;
|
||||
tokenInput.value = '';
|
||||
solvePromise = cap.solve()
|
||||
.then((r) => { tokenInput.value = r.token; return r; })
|
||||
.catch((err) => { console.error('[cap] solve failed', err); throw err; });
|
||||
};
|
||||
startSolve();
|
||||
|
||||
container.__capReset = () => {
|
||||
try { cap.reset(); } catch (e) { /* ignore */ }
|
||||
startSolve();
|
||||
};
|
||||
|
||||
form.addEventListener('submit', async (event) => {
|
||||
if (verified && tokenInput.value) return;
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
const submitter = event.submitter || null;
|
||||
try {
|
||||
await solvePromise;
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
verified = true;
|
||||
// Defer: HTMLFormElement.requestSubmit() is a no-op while
|
||||
// the form's "firing submission events" flag is still set,
|
||||
// i.e. while we're still inside the original submit handler.
|
||||
setTimeout(() => {
|
||||
if (submitter) {
|
||||
form.requestSubmit(submitter);
|
||||
} else {
|
||||
form.requestSubmit();
|
||||
}
|
||||
}, 0);
|
||||
}, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan the document (or a specific root) for any invisible Cap containers
|
||||
* that haven't been wired yet and wire them up.
|
||||
*/
|
||||
function wireAllInvisible(root) {
|
||||
const scope = root || document;
|
||||
const containers = scope.querySelectorAll(
|
||||
'[data-captcha-provider="cap"][data-cap-mode="invisible"]'
|
||||
);
|
||||
containers.forEach(wireInvisibleContainer);
|
||||
}
|
||||
|
||||
function registerXhrHandler() {
|
||||
if (!window.GravFormXHR || !window.GravFormXHR.captcha) return false;
|
||||
|
||||
window.GravFormXHR.captcha.register('cap', {
|
||||
reset: function (container, form) {
|
||||
const capContainer = (container && container.matches('[data-captcha-provider="cap"]'))
|
||||
? container
|
||||
: form.querySelector('[data-captcha-provider="cap"]');
|
||||
|
||||
if (!capContainer || !capContainer.isConnected) return;
|
||||
|
||||
const mode = capContainer.dataset.capMode || 'invisible';
|
||||
|
||||
if (mode === 'invisible') {
|
||||
// After an XHR form re-render, the container is usually a
|
||||
// brand-new element — wire it up from scratch. If it's the
|
||||
// same element we already wired, re-arm in place.
|
||||
ensureWasmUrl(form);
|
||||
if (capContainer.__capWired && typeof capContainer.__capReset === 'function') {
|
||||
capContainer.__capReset();
|
||||
} else {
|
||||
wireInvisibleContainer(capContainer);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Checkbox mode: reset the <cap-widget> if it's solved.
|
||||
const widget = capContainer.querySelector('cap-widget');
|
||||
if (!widget || !widget.isConnected || !widget.token) return;
|
||||
try { widget.reset(); } catch (e) { console.error('Error resetting Cap widget:', e); }
|
||||
const tokenInput = form.querySelector('input[name="cap-token"]');
|
||||
if (tokenInput) tokenInput.value = '';
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function init() {
|
||||
ensureWasmUrl(document);
|
||||
wireAllInvisible(document);
|
||||
registerXhrHandler();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
// Expose for manual re-wiring (e.g., when a form is dynamically inserted).
|
||||
window.GravCapCaptcha = window.GravCapCaptcha || {};
|
||||
window.GravCapCaptcha.wireAll = wireAllInvisible;
|
||||
window.GravCapCaptcha.wire = wireInvisibleContainer;
|
||||
})();
|
||||
@@ -0,0 +1,13 @@
|
||||
Copyright 2025 Tiago
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Cap widget (vendored)
|
||||
|
||||
Vendored from:
|
||||
- `@cap.js/widget` (see `VERSION`)
|
||||
- `@cap.js/wasm@0.0.6` — `cap_wasm_bg.wasm`
|
||||
|
||||
Upstream: https://github.com/tiagozip/cap
|
||||
|
||||
## Updating
|
||||
|
||||
```bash
|
||||
npm pack @cap.js/widget
|
||||
npm pack @cap.js/wasm
|
||||
# extract and copy cap.min.js, cap.d.ts, wasm-hashes.min.js, LICENSE
|
||||
# and browser/cap_wasm_bg.wasm into this directory, then update VERSION.
|
||||
```
|
||||
|
||||
## Why vendored
|
||||
|
||||
The upstream widget fetches its WASM module from `cdn.jsdelivr.net` by
|
||||
default. We set `window.CAP_CUSTOM_WASM_URL` to the locally vendored
|
||||
copy so Cap captcha works fully self-hosted with no third-party runtime
|
||||
dependency — matching the privacy-preserving ethos of the project.
|
||||
@@ -0,0 +1 @@
|
||||
0.1.43
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
declare global {
|
||||
interface Window {
|
||||
CAP_CUSTOM_FETCH?: typeof fetch;
|
||||
CAP_CUSTOM_WASM_URL?: string;
|
||||
CAP_CSS_NONCE?: string;
|
||||
CAP_DONT_SKIP_REDEFINE?: boolean;
|
||||
Cap: typeof Cap;
|
||||
}
|
||||
}
|
||||
|
||||
interface CapProgressEventDetail {
|
||||
progress: number;
|
||||
}
|
||||
|
||||
interface CapSolveEventDetail {
|
||||
token: string;
|
||||
}
|
||||
|
||||
interface CapErrorEventDetail {
|
||||
isCap: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface CapProgressEvent extends CustomEvent {
|
||||
detail: CapProgressEventDetail;
|
||||
}
|
||||
|
||||
interface CapSolveEvent extends CustomEvent {
|
||||
detail: CapSolveEventDetail;
|
||||
}
|
||||
|
||||
interface CapErrorEvent extends CustomEvent {
|
||||
detail: CapErrorEventDetail;
|
||||
}
|
||||
|
||||
interface CapResetEvent extends CustomEvent {
|
||||
detail: Record<string, never>;
|
||||
}
|
||||
|
||||
interface SolveResult {
|
||||
success: boolean;
|
||||
token: string;
|
||||
}
|
||||
|
||||
interface CapConfig {
|
||||
apiEndpoint?: string;
|
||||
"data-cap-api-endpoint"?: string;
|
||||
"data-cap-worker-count"?: string;
|
||||
"data-cap-hidden-field-name"?: string;
|
||||
"data-cap-i18n-initial-state"?: string;
|
||||
"data-cap-i18n-verifying-label"?: string;
|
||||
"data-cap-i18n-solved-label"?: string;
|
||||
"data-cap-i18n-error-label"?: string;
|
||||
"data-cap-i18n-verify-aria-label"?: string;
|
||||
"data-cap-i18n-verifying-aria-label"?: string;
|
||||
"data-cap-i18n-verified-aria-label"?: string;
|
||||
"data-cap-i18n-error-aria-label"?: string;
|
||||
"data-cap-i18n-wasm-disabled"?: string;
|
||||
"data-cap-troubleshooting-url"?: string;
|
||||
onsolve?: string;
|
||||
onprogress?: string;
|
||||
onreset?: string;
|
||||
onerror?: string;
|
||||
}
|
||||
|
||||
interface CapWidget extends HTMLElement {
|
||||
readonly token: string | null;
|
||||
readonly tokenValue: string | null;
|
||||
|
||||
solve(): Promise<SolveResult>;
|
||||
reset(): void;
|
||||
setWorkersCount(workers: number): void;
|
||||
|
||||
addEventListener(type: "progress", listener: (event: CapProgressEvent) => void): void;
|
||||
addEventListener(type: "solve", listener: (event: CapSolveEvent) => void): void;
|
||||
addEventListener(type: "error", listener: (event: CapErrorEvent) => void): void;
|
||||
addEventListener(type: "reset", listener: (event: CapResetEvent) => void): void;
|
||||
addEventListener(type: string, listener: EventListener): void;
|
||||
|
||||
removeEventListener(type: "progress", listener: (event: CapProgressEvent) => void): void;
|
||||
removeEventListener(type: "solve", listener: (event: CapSolveEvent) => void): void;
|
||||
removeEventListener(type: "error", listener: (event: CapErrorEvent) => void): void;
|
||||
removeEventListener(type: "reset", listener: (event: CapResetEvent) => void): void;
|
||||
removeEventListener(type: string, listener: EventListener): void;
|
||||
}
|
||||
|
||||
declare class Cap {
|
||||
readonly widget: CapWidget;
|
||||
readonly token: string | null;
|
||||
|
||||
constructor(config?: CapConfig, el?: CapWidget);
|
||||
|
||||
solve(): Promise<SolveResult>;
|
||||
reset(): void;
|
||||
|
||||
addEventListener(type: "progress", listener: (event: CapProgressEvent) => void): void;
|
||||
addEventListener(type: "solve", listener: (event: CapSolveEvent) => void): void;
|
||||
addEventListener(type: "error", listener: (event: CapErrorEvent) => void): void;
|
||||
addEventListener(type: "reset", listener: (event: CapResetEvent) => void): void;
|
||||
addEventListener(type: string, listener: EventListener): void;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"cap-widget": CapWidget;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
Cap,
|
||||
type CapWidget,
|
||||
type CapConfig,
|
||||
type CapProgressEvent,
|
||||
type CapSolveEvent,
|
||||
type CapErrorEvent,
|
||||
type CapResetEvent,
|
||||
type SolveResult,
|
||||
};
|
||||
|
||||
export default Cap;
|
||||
+1
File diff suppressed because one or more lines are too long
Binary file not shown.
+327
File diff suppressed because one or more lines are too long
@@ -0,0 +1,166 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Register the handler with the form system when it's ready
|
||||
const registerRecaptchaHandler = function() {
|
||||
if (window.GravFormXHR && window.GravFormXHR.captcha) {
|
||||
window.GravFormXHR.captcha.register('recaptcha', {
|
||||
reset: function(container, form) {
|
||||
if (!form || !form.id) {
|
||||
console.warn('Cannot reset reCAPTCHA: form is invalid or missing ID');
|
||||
return;
|
||||
}
|
||||
|
||||
const formId = form.id;
|
||||
console.log(`Attempting to reset reCAPTCHA for form: ${formId}`);
|
||||
|
||||
// First try the expected ID pattern from the Twig template
|
||||
const recaptchaId = `g-recaptcha-${formId}`;
|
||||
// We need to look more flexibly for the container
|
||||
let widgetContainer = document.getElementById(recaptchaId);
|
||||
|
||||
// If not found by ID, look for the div inside the captcha provider container
|
||||
if (!widgetContainer) {
|
||||
// Try to find it inside the captcha provider container
|
||||
widgetContainer = container.querySelector('.g-recaptcha');
|
||||
|
||||
if (!widgetContainer) {
|
||||
// If that fails, look more broadly in the form
|
||||
widgetContainer = form.querySelector('.g-recaptcha');
|
||||
|
||||
if (!widgetContainer) {
|
||||
// Last resort - create a new container if needed
|
||||
console.warn(`reCAPTCHA container #${recaptchaId} not found. Creating a new one.`);
|
||||
widgetContainer = document.createElement('div');
|
||||
widgetContainer.id = recaptchaId;
|
||||
widgetContainer.className = 'g-recaptcha';
|
||||
container.appendChild(widgetContainer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Found reCAPTCHA container for form: ${formId}`);
|
||||
|
||||
// Get configuration from data attributes
|
||||
const parentContainer = container.closest('[data-captcha-provider="recaptcha"]');
|
||||
if (!parentContainer) {
|
||||
console.warn('Cannot find reCAPTCHA parent container with data-captcha-provider attribute.');
|
||||
return;
|
||||
}
|
||||
|
||||
const sitekey = parentContainer.dataset.sitekey;
|
||||
const version = parentContainer.dataset.version || '2-checkbox';
|
||||
const isV3 = version.startsWith('3');
|
||||
const isInvisible = version === '2-invisible';
|
||||
|
||||
if (!sitekey) {
|
||||
console.warn('Cannot reinitialize reCAPTCHA - missing sitekey attribute');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Re-rendering reCAPTCHA widget for form: ${formId}, version: ${version}`);
|
||||
|
||||
// Handle V3 reCAPTCHA differently
|
||||
if (isV3) {
|
||||
try {
|
||||
// For v3, we don't need to reset anything visible, just make sure we have the API
|
||||
if (typeof grecaptcha !== 'undefined' && typeof grecaptcha.execute === 'function') {
|
||||
// Create a new execution context for the form
|
||||
const actionName = `form_${formId}`;
|
||||
const tokenInput = form.querySelector('input[name="token"]') ||
|
||||
form.querySelector('input[name="data[token]"]');
|
||||
const actionInput = form.querySelector('input[name="action"]') ||
|
||||
form.querySelector('input[name="data[action]"]');
|
||||
|
||||
if (tokenInput && actionInput) {
|
||||
// Clear previous token
|
||||
tokenInput.value = '';
|
||||
|
||||
// Set the action name
|
||||
actionInput.value = actionName;
|
||||
|
||||
console.log(`reCAPTCHA v3 ready for execution on form: ${formId}`);
|
||||
} else {
|
||||
console.warn(`Cannot find token or action inputs for reCAPTCHA v3 in form: ${formId}`);
|
||||
}
|
||||
} else {
|
||||
console.warn('reCAPTCHA v3 API not properly loaded.');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Error setting up reCAPTCHA v3: ${e.message}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// For v2, handle visible widget reset
|
||||
// Clear the container to ensure fresh rendering
|
||||
widgetContainer.innerHTML = '';
|
||||
|
||||
// Check if reCAPTCHA API is available
|
||||
if (typeof grecaptcha !== 'undefined' && typeof grecaptcha.render === 'function') {
|
||||
try {
|
||||
// Render with a slight delay to ensure DOM is settled
|
||||
setTimeout(() => {
|
||||
grecaptcha.render(widgetContainer.id || widgetContainer, {
|
||||
'sitekey': sitekey,
|
||||
'theme': parentContainer.dataset.theme || 'light',
|
||||
'size': isInvisible ? 'invisible' : 'normal',
|
||||
'callback': function(token) {
|
||||
console.log(`reCAPTCHA verification completed for form: ${formId}`);
|
||||
|
||||
// If it's invisible reCAPTCHA, submit the form automatically
|
||||
if (isInvisible && window.GravFormXHR && typeof window.GravFormXHR.submit === 'function') {
|
||||
window.GravFormXHR.submit(form);
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log(`Successfully rendered reCAPTCHA for form: ${formId}`);
|
||||
}, 100);
|
||||
} catch (e) {
|
||||
console.error(`Error rendering reCAPTCHA widget: ${e.message}`);
|
||||
widgetContainer.innerHTML = '<p style="color:red;">Error initializing reCAPTCHA.</p>';
|
||||
}
|
||||
} else {
|
||||
console.warn('reCAPTCHA API not available. Attempting to reload...');
|
||||
|
||||
// Remove existing script if any
|
||||
const existingScript = document.querySelector('script[src*="google.com/recaptcha/api.js"]');
|
||||
if (existingScript) {
|
||||
existingScript.parentNode.removeChild(existingScript);
|
||||
}
|
||||
|
||||
// Create new script element
|
||||
const script = document.createElement('script');
|
||||
script.src = `https://www.google.com/recaptcha/api.js${isV3 ? '?render=' + sitekey : ''}`;
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
script.onload = function() {
|
||||
console.log('reCAPTCHA API loaded, retrying widget render...');
|
||||
setTimeout(() => {
|
||||
const retryContainer = document.querySelector(`[data-captcha-provider="recaptcha"]`);
|
||||
if (retryContainer && form) {
|
||||
window.GravFormXHR.captcha.getProvider('recaptcha').reset(retryContainer, form);
|
||||
}
|
||||
}, 200);
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log('reCAPTCHA XHR handler registered successfully');
|
||||
} else {
|
||||
console.error('GravFormXHR.captcha not found. Make sure the Form plugin is loaded correctly.');
|
||||
}
|
||||
};
|
||||
|
||||
// Try to register the handler immediately if GravFormXHR is already available
|
||||
if (window.GravFormXHR && window.GravFormXHR.captcha) {
|
||||
registerRecaptchaHandler();
|
||||
} else {
|
||||
// Otherwise, wait for the DOM to be fully loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Give a small delay to ensure GravFormXHR is initialized
|
||||
setTimeout(registerRecaptchaHandler, 100);
|
||||
});
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,121 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Register the handler with the form system when it's ready
|
||||
const registerTurnstileHandler = function() {
|
||||
if (window.GravFormXHR && window.GravFormXHR.captcha) {
|
||||
window.GravFormXHR.captcha.register('turnstile', {
|
||||
reset: function(container, form) {
|
||||
const formId = form.id;
|
||||
const containerId = `cf-turnstile-${formId}`;
|
||||
const widgetContainer = document.getElementById(containerId);
|
||||
|
||||
if (!widgetContainer) {
|
||||
console.warn(`Turnstile container #${containerId} not found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get configuration from data attributes
|
||||
const parentContainer = container.closest('[data-captcha-provider="turnstile"]');
|
||||
const sitekey = parentContainer ? parentContainer.dataset.sitekey : null;
|
||||
|
||||
if (!sitekey) {
|
||||
console.warn('Cannot reinitialize Turnstile - missing sitekey attribute');
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear the container to ensure fresh rendering
|
||||
widgetContainer.innerHTML = '';
|
||||
|
||||
console.log(`Re-rendering Turnstile widget for form: ${formId}`);
|
||||
|
||||
// Check if Turnstile API is available
|
||||
if (typeof window.turnstile !== 'undefined') {
|
||||
try {
|
||||
// Reset any existing widgets
|
||||
try {
|
||||
window.turnstile.reset(containerId);
|
||||
} catch (e) {
|
||||
// Ignore reset errors, we'll re-render anyway
|
||||
}
|
||||
|
||||
// Render with a slight delay to ensure DOM is settled
|
||||
setTimeout(() => {
|
||||
window.turnstile.render(`#${containerId}`, {
|
||||
sitekey: sitekey,
|
||||
theme: parentContainer ? (parentContainer.dataset.theme || 'light') : 'light',
|
||||
callback: function(token) {
|
||||
console.log(`Turnstile verification completed for form: ${formId} with token:`, token.substring(0, 10) + '...');
|
||||
|
||||
// Create or update hidden input for token
|
||||
let tokenInput = form.querySelector('input[name="cf-turnstile-response"]');
|
||||
if (!tokenInput) {
|
||||
console.log('Creating new hidden input for Turnstile token');
|
||||
tokenInput = document.createElement('input');
|
||||
tokenInput.type = 'hidden';
|
||||
tokenInput.name = 'cf-turnstile-response';
|
||||
form.appendChild(tokenInput);
|
||||
} else {
|
||||
console.log('Updating existing hidden input for Turnstile token');
|
||||
}
|
||||
tokenInput.value = token;
|
||||
|
||||
// Also add a debug attribute
|
||||
form.setAttribute('data-turnstile-verified', 'true');
|
||||
},
|
||||
'expired-callback': function() {
|
||||
console.log(`Turnstile token expired for form: ${formId}`);
|
||||
},
|
||||
'error-callback': function(error) {
|
||||
console.error(`Turnstile error for form ${formId}: ${error}`);
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
} catch (e) {
|
||||
console.error(`Error rendering Turnstile widget: ${e.message}`);
|
||||
widgetContainer.innerHTML = '<p style="color:red;">Error initializing Turnstile.</p>';
|
||||
}
|
||||
} else {
|
||||
console.warn('Turnstile API not available. Attempting to reload...');
|
||||
|
||||
// Remove existing script if any
|
||||
const existingScript = document.querySelector('script[src*="challenges.cloudflare.com/turnstile/v0/api.js"]');
|
||||
if (existingScript) {
|
||||
existingScript.parentNode.removeChild(existingScript);
|
||||
}
|
||||
|
||||
// Create new script element
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
script.onload = function() {
|
||||
console.log('Turnstile API loaded, retrying widget render...');
|
||||
setTimeout(() => {
|
||||
const retryContainer = document.querySelector('[data-captcha-provider="turnstile"]');
|
||||
if (retryContainer && form) {
|
||||
window.GravFormXHR.captcha.getProvider('turnstile').reset(retryContainer, form);
|
||||
}
|
||||
}, 200);
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
}
|
||||
});
|
||||
console.log('Turnstile XHR handler registered successfully');
|
||||
} else {
|
||||
console.error('GravFormXHR.captcha not found. Make sure the Form plugin is loaded correctly.');
|
||||
}
|
||||
};
|
||||
|
||||
// Try to register the handler immediately if GravFormXHR is already available
|
||||
if (window.GravFormXHR && window.GravFormXHR.captcha) {
|
||||
registerTurnstileHandler();
|
||||
} else {
|
||||
// Otherwise, wait for the DOM to be fully loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Give a small delay to ensure GravFormXHR is initialized
|
||||
setTimeout(registerTurnstileHandler, 100);
|
||||
});
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user