(Grav GitSync) Automatic Commit from GitSync

This commit is contained in:
GitSync
2026-06-14 00:27:27 +00:00
parent a2920f812d
commit 3c1bfda80f
2933 changed files with 491625 additions and 0 deletions
@@ -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);
}
});
}
});
})();
+150
View File
@@ -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;
})();
+13
View File
@@ -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.
+23
View File
@@ -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.
+1
View File
@@ -0,0 +1 @@
0.1.43
+120
View File
@@ -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;
File diff suppressed because one or more lines are too long
Binary file not shown.
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);
});
}
})();