Skip to content

Commit 7334022

Browse files
committed
Merge branch 'release/8.0.0'
2 parents 3ae7e59 + 764b917 commit 7334022

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+17268
-4869
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@
33
/.idea
44
node_modules
55
*.js.map
6+
/repomix-output.md
7+
/repomix.config.json
8+
/.repomixignore
9+
/grav-form-plugin.md

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
# v8.0.0
2+
## 08/25/2025
3+
4+
1. [](#new)
5+
* Rewrote XHR support to be more robust and easier to use
6+
* Added `hCpatcha` field support
7+
* Added `Turnstile` XHR support
8+
* Added ability to support 3rd party captcha mechanisms
9+
* Added `Filepond` field support for alternate upload type
10+
* Dropzone XHR support
11+
* PHP 8.4 compatibility
12+
1. [](#improved)
13+
* Added support for `data_label:` in fields for use with data twig templates to override the displayed label
14+
* Matched formatting of minus operator in BasicCaptcha to plus operator [#596](https://github.com/getgrav/grav-plugin-form/pull/596)
15+
* Dynamic field proxying
16+
117
# v7.4.2
218
## 10/28/2024
319

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
(function() {
2+
'use strict';
3+
4+
// Function to refresh a captcha image
5+
const refreshCaptchaImage = function(container) {
6+
const img = container.querySelector('img');
7+
if (!img) {
8+
console.warn('Cannot find captcha image in container');
9+
return;
10+
}
11+
12+
// Force reload by adding/updating timestamp
13+
const timestamp = new Date().getTime();
14+
const baseUrl = img.src.split('?')[0];
15+
img.src = baseUrl + '?t=' + timestamp;
16+
17+
// Also clear the input field if we can find it
18+
const formField = container.closest('.form-field');
19+
if (formField) {
20+
const input = formField.querySelector('input[type="text"]');
21+
if (input) {
22+
input.value = '';
23+
// Try to focus the input
24+
try { input.focus(); } catch(e) {}
25+
}
26+
}
27+
};
28+
29+
// Function to set up click handlers for refresh buttons
30+
const setupRefreshButtons = function() {
31+
// Find all captcha containers
32+
const containers = document.querySelectorAll('[data-captcha-provider="basic-captcha"]');
33+
34+
containers.forEach(function(container) {
35+
// Find the refresh button within this container
36+
const button = container.querySelector('button');
37+
if (!button) {
38+
return;
39+
}
40+
41+
// Remove any existing listeners (just in case)
42+
button.removeEventListener('click', handleRefreshClick);
43+
44+
// Add the click handler
45+
button.addEventListener('click', handleRefreshClick);
46+
});
47+
};
48+
49+
// Click handler function
50+
const handleRefreshClick = function(event) {
51+
// Prevent default behavior and stop propagation
52+
event.preventDefault();
53+
event.stopPropagation();
54+
55+
// Find the container
56+
const container = this.closest('[data-captcha-provider="basic-captcha"]');
57+
if (!container) {
58+
return false;
59+
}
60+
61+
// Refresh the image
62+
refreshCaptchaImage(container);
63+
64+
return false;
65+
};
66+
67+
// Set up a mutation observer to handle dynamically added captchas
68+
const setupMutationObserver = function() {
69+
// Check if MutationObserver is available
70+
if (typeof MutationObserver === 'undefined') return;
71+
72+
// Create a mutation observer to watch for new captcha elements
73+
const observer = new MutationObserver(function(mutations) {
74+
let needsSetup = false;
75+
76+
mutations.forEach(function(mutation) {
77+
if (mutation.type === 'childList' && mutation.addedNodes.length) {
78+
// Check if any of the added nodes contain our captcha containers
79+
for (let i = 0; i < mutation.addedNodes.length; i++) {
80+
const node = mutation.addedNodes[i];
81+
if (node.nodeType === Node.ELEMENT_NODE) {
82+
// Check if this element has or contains captcha containers
83+
if (node.querySelector && (
84+
node.matches('[data-captcha-provider="basic-captcha"]') ||
85+
node.querySelector('[data-captcha-provider="basic-captcha"]')
86+
)) {
87+
needsSetup = true;
88+
break;
89+
}
90+
}
91+
}
92+
}
93+
});
94+
95+
if (needsSetup) {
96+
setupRefreshButtons();
97+
}
98+
});
99+
100+
// Start observing the document
101+
observer.observe(document.body, {
102+
childList: true,
103+
subtree: true
104+
});
105+
};
106+
107+
// Initialize on DOM ready
108+
document.addEventListener('DOMContentLoaded', function() {
109+
setupRefreshButtons();
110+
setupMutationObserver();
111+
112+
// Also connect to XHR system if available (for best of both worlds)
113+
if (window.GravFormXHR && window.GravFormXHR.captcha) {
114+
window.GravFormXHR.captcha.register('basic-captcha', {
115+
reset: function(container, form) {
116+
refreshCaptchaImage(container);
117+
}
118+
});
119+
}
120+
});
121+
})();
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
(function() {
2+
'use strict';
3+
4+
// Register the handler with the form system when it's ready
5+
const registerRecaptchaHandler = function() {
6+
if (window.GravFormXHR && window.GravFormXHR.captcha) {
7+
window.GravFormXHR.captcha.register('recaptcha', {
8+
reset: function(container, form) {
9+
if (!form || !form.id) {
10+
console.warn('Cannot reset reCAPTCHA: form is invalid or missing ID');
11+
return;
12+
}
13+
14+
const formId = form.id;
15+
console.log(`Attempting to reset reCAPTCHA for form: ${formId}`);
16+
17+
// First try the expected ID pattern from the Twig template
18+
const recaptchaId = `g-recaptcha-${formId}`;
19+
// We need to look more flexibly for the container
20+
let widgetContainer = document.getElementById(recaptchaId);
21+
22+
// If not found by ID, look for the div inside the captcha provider container
23+
if (!widgetContainer) {
24+
// Try to find it inside the captcha provider container
25+
widgetContainer = container.querySelector('.g-recaptcha');
26+
27+
if (!widgetContainer) {
28+
// If that fails, look more broadly in the form
29+
widgetContainer = form.querySelector('.g-recaptcha');
30+
31+
if (!widgetContainer) {
32+
// Last resort - create a new container if needed
33+
console.warn(`reCAPTCHA container #${recaptchaId} not found. Creating a new one.`);
34+
widgetContainer = document.createElement('div');
35+
widgetContainer.id = recaptchaId;
36+
widgetContainer.className = 'g-recaptcha';
37+
container.appendChild(widgetContainer);
38+
}
39+
}
40+
}
41+
42+
console.log(`Found reCAPTCHA container for form: ${formId}`);
43+
44+
// Get configuration from data attributes
45+
const parentContainer = container.closest('[data-captcha-provider="recaptcha"]');
46+
if (!parentContainer) {
47+
console.warn('Cannot find reCAPTCHA parent container with data-captcha-provider attribute.');
48+
return;
49+
}
50+
51+
const sitekey = parentContainer.dataset.sitekey;
52+
const version = parentContainer.dataset.version || '2-checkbox';
53+
const isV3 = version.startsWith('3');
54+
const isInvisible = version === '2-invisible';
55+
56+
if (!sitekey) {
57+
console.warn('Cannot reinitialize reCAPTCHA - missing sitekey attribute');
58+
return;
59+
}
60+
61+
console.log(`Re-rendering reCAPTCHA widget for form: ${formId}, version: ${version}`);
62+
63+
// Handle V3 reCAPTCHA differently
64+
if (isV3) {
65+
try {
66+
// For v3, we don't need to reset anything visible, just make sure we have the API
67+
if (typeof grecaptcha !== 'undefined' && typeof grecaptcha.execute === 'function') {
68+
// Create a new execution context for the form
69+
const actionName = `form_${formId}`;
70+
const tokenInput = form.querySelector('input[name="token"]') ||
71+
form.querySelector('input[name="data[token]"]');
72+
const actionInput = form.querySelector('input[name="action"]') ||
73+
form.querySelector('input[name="data[action]"]');
74+
75+
if (tokenInput && actionInput) {
76+
// Clear previous token
77+
tokenInput.value = '';
78+
79+
// Set the action name
80+
actionInput.value = actionName;
81+
82+
console.log(`reCAPTCHA v3 ready for execution on form: ${formId}`);
83+
} else {
84+
console.warn(`Cannot find token or action inputs for reCAPTCHA v3 in form: ${formId}`);
85+
}
86+
} else {
87+
console.warn('reCAPTCHA v3 API not properly loaded.');
88+
}
89+
} catch (e) {
90+
console.error(`Error setting up reCAPTCHA v3: ${e.message}`);
91+
}
92+
return;
93+
}
94+
95+
// For v2, handle visible widget reset
96+
// Clear the container to ensure fresh rendering
97+
widgetContainer.innerHTML = '';
98+
99+
// Check if reCAPTCHA API is available
100+
if (typeof grecaptcha !== 'undefined' && typeof grecaptcha.render === 'function') {
101+
try {
102+
// Render with a slight delay to ensure DOM is settled
103+
setTimeout(() => {
104+
grecaptcha.render(widgetContainer.id || widgetContainer, {
105+
'sitekey': sitekey,
106+
'theme': parentContainer.dataset.theme || 'light',
107+
'size': isInvisible ? 'invisible' : 'normal',
108+
'callback': function(token) {
109+
console.log(`reCAPTCHA verification completed for form: ${formId}`);
110+
111+
// If it's invisible reCAPTCHA, submit the form automatically
112+
if (isInvisible && window.GravFormXHR && typeof window.GravFormXHR.submit === 'function') {
113+
window.GravFormXHR.submit(form);
114+
}
115+
}
116+
});
117+
console.log(`Successfully rendered reCAPTCHA for form: ${formId}`);
118+
}, 100);
119+
} catch (e) {
120+
console.error(`Error rendering reCAPTCHA widget: ${e.message}`);
121+
widgetContainer.innerHTML = '<p style="color:red;">Error initializing reCAPTCHA.</p>';
122+
}
123+
} else {
124+
console.warn('reCAPTCHA API not available. Attempting to reload...');
125+
126+
// Remove existing script if any
127+
const existingScript = document.querySelector('script[src*="google.com/recaptcha/api.js"]');
128+
if (existingScript) {
129+
existingScript.parentNode.removeChild(existingScript);
130+
}
131+
132+
// Create new script element
133+
const script = document.createElement('script');
134+
script.src = `https://www.google.com/recaptcha/api.js${isV3 ? '?render=' + sitekey : ''}`;
135+
script.async = true;
136+
script.defer = true;
137+
script.onload = function() {
138+
console.log('reCAPTCHA API loaded, retrying widget render...');
139+
setTimeout(() => {
140+
const retryContainer = document.querySelector(`[data-captcha-provider="recaptcha"]`);
141+
if (retryContainer && form) {
142+
window.GravFormXHR.captcha.getProvider('recaptcha').reset(retryContainer, form);
143+
}
144+
}, 200);
145+
};
146+
document.head.appendChild(script);
147+
}
148+
}
149+
});
150+
console.log('reCAPTCHA XHR handler registered successfully');
151+
} else {
152+
console.error('GravFormXHR.captcha not found. Make sure the Form plugin is loaded correctly.');
153+
}
154+
};
155+
156+
// Try to register the handler immediately if GravFormXHR is already available
157+
if (window.GravFormXHR && window.GravFormXHR.captcha) {
158+
registerRecaptchaHandler();
159+
} else {
160+
// Otherwise, wait for the DOM to be fully loaded
161+
document.addEventListener('DOMContentLoaded', function() {
162+
// Give a small delay to ensure GravFormXHR is initialized
163+
setTimeout(registerRecaptchaHandler, 100);
164+
});
165+
}
166+
})();

0 commit comments

Comments
 (0)