Changeset 3714007
- Timestamp:
- 09/26/2026 08:40:01 AM (3 weeks ago)
- Location:
- spamoff-bot-protection/tags/1.0.0
- Files:
-
- 12 edited
-
assets/css/spamoff-bot-protection-style.css (modified) (2 diffs)
-
assets/js/spamoff-bot-protection-blocks-checkout.js (modified) (7 diffs)
-
assets/js/spamoff-bot-protection-handler.js (modified) (1 diff)
-
includes/class-spamoff-bot-protection-admin.php (modified) (2 diffs)
-
includes/class-spamoff-bot-protection-captcha.php (modified) (2 diffs)
-
includes/class-spamoff-bot-protection-frontend.php (modified) (10 diffs)
-
includes/class-spamoff-bot-protection-helper.php (modified) (7 diffs)
-
includes/class-spamoff-bot-protection-settings.php (modified) (15 diffs)
-
languages/spamoff-bot-protection.pot (modified) (2 diffs)
-
readme.txt (modified) (1 diff)
-
spamoff-bot-protection.php (modified) (5 diffs)
-
uninstall.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
spamoff-bot-protection/tags/1.0.0/assets/css/spamoff-bot-protection-style.css
r3603369 r3714007 1 1 /* ========================================================= 2 SpamOff – Spam & Bot Protection for WordPress — Checkoutcaptcha styling2 SpamOff – Spam & Bot Protection for WordPress — captcha styling 3 3 ========================================================= */ 4 4 5 /* Remove box, background, border from the captcha wrapper */ 5 .spamoff-bot-protection-v2 { 6 margin: 12px 0 16px 0; 7 clear: both; 8 } 9 10 /* Google's notice, shown in forms when the v3 badge is hidden */ 11 .spamoff-bot-protection-notice { 12 margin: 8px 0; 13 font-size: 0.8em; 14 opacity: 0.8; 15 } 16 17 /* Only added by the handler when the notice above is on the page, as Google requires */ 18 .spamoff-bot-protection-hide-badge .grecaptcha-badge { 19 visibility: hidden !important; 20 } 21 22 /* Remove box, background, border from the checkout captcha wrapper */ 6 23 .woocommerce-checkout .spamoff-bot-protection-container, 7 24 .woocommerce-checkout .spamoff-bot-protection-v2, … … 37 54 box-shadow: none !important; 38 55 } 39 40 /* Disable old mobile scaling (handled via JS now) */41 @media (max-width: 768px) {42 .spamoff-bot-protection-v2 {43 transform: none !important;44 }45 } -
spamoff-bot-protection/tags/1.0.0/assets/js/spamoff-bot-protection-blocks-checkout.js
r3603369 r3714007 5 5 * where it's read server-side by SpamOff_Bot_Protection_Frontend::validate_blocks_checkout. 6 6 * 7 * Google v3 tokens expire after 120s . WC Blocks reads the token from the store at8 * submission time, so a token sitting idle for the customer's full checkout dwell9 * would be rejected as `timeout-or-duplicate`. We refresh on a timer (well inside10 * the TTL) and immediately on tab-focus, so the token in the store is always recent.7 * Google v3 tokens expire after 120s and are single-use. WC Blocks reads the token from 8 * the store at submission time, so we refresh it on a timer (well inside the TTL), on 9 * tab focus, and after every order attempt that doesn't complete — that attempt has 10 * already spent the token on the server. 11 11 */ 12 12 (function () { … … 14 14 return; 15 15 } 16 17 var STORE_KEY = 'wc/store/checkout'; 18 var NAMESPACE = 'spamoff-bot-protection'; 16 19 17 20 // Refresh well before Google's 120s TTL. … … 25 28 26 29 var initialized = false; 30 var wasProcessing = false; 27 31 var refreshTimer = null; 32 33 function getSiteKey() { 34 return window.spamoff_bot_protection && window.spamoff_bot_protection.v3SiteKey; 35 } 28 36 29 37 function clearTimer() { … … 35 43 36 44 function setExtensionToken(token) { 37 wp.data 38 .dispatch('wc/store/checkout') 39 .setExtensionData('spamoff-bot-protection', { 40 token: token, 41 action: 'checkout', 42 }); 45 wp.data.dispatch(STORE_KEY).setExtensionData(NAMESPACE, { 46 token: token, 47 action: 'checkout', 48 }); 43 49 } 44 50 … … 46 52 clearTimer(); 47 53 48 if ( 49 !window.spamoff_bot_protection || 50 !window.spamoff_bot_protection.siteKey || 51 typeof grecaptcha === 'undefined' || 52 !grecaptcha.ready 53 ) { 54 if (!getSiteKey() || typeof grecaptcha === 'undefined' || !grecaptcha.ready) { 54 55 refreshTimer = setTimeout(fetchAndSetToken, GRECAPTCHA_WAIT_MS); 55 56 return; … … 57 58 58 59 grecaptcha.ready(function () { 59 grecaptcha.execute( window.spamoff_bot_protection.siteKey, { action: 'checkout' })60 grecaptcha.execute(getSiteKey(), { action: 'checkout' }) 60 61 .then(function (token) { 61 62 setExtensionToken(token); … … 70 71 } 71 72 72 var unsubscribe = wp.data.subscribe(function () { 73 if (initialized) return; 74 if (!wp.data.select('wc/store/checkout')) return; 73 wp.data.subscribe(function () { 74 var checkout = wp.data.select(STORE_KEY); 75 if (!checkout) { 76 return; 77 } 75 78 76 initialized = true; 77 if (typeof unsubscribe === 'function') { 78 unsubscribe(); 79 if (!initialized) { 80 initialized = true; 81 fetchAndSetToken(); 82 return; 79 83 } 80 fetchAndSetToken(); 84 85 // An order attempt finished without completing (validation or payment error): 86 // its token is spent, so get a new one before the customer retries. 87 var processing = checkout.isProcessing() || checkout.isBeforeProcessing() || checkout.isAfterProcessing(); 88 if (wasProcessing && !processing && !checkout.isComplete()) { 89 fetchAndSetToken(); 90 } 91 wasProcessing = processing; 81 92 }); 82 93 -
spamoff-bot-protection/tags/1.0.0/assets/js/spamoff-bot-protection-handler.js
r3603369 r3714007 1 1 /** 2 2 * SpamOff – Spam & Bot Protection for WordPress — classic form handler. 3 * Handles reCAPTCHA v2 and v3 token execution and form submission for non-Blocks forms. 3 * 4 * v3: when a protected form is submitted, fetch a token, then submit the form again 5 * with the button the visitor clicked, so handlers that check it (WooCommerce's 6 * `login` and `register`) still see it. 7 * v2: render each checkbox explicitly so its widget ID is known and it can be reset 8 * after a failed classic checkout. 4 9 */ 5 (function ($) {10 (function ($) { 6 11 'use strict'; 7 12 8 // Google v3 tokens expire after 120s. Treat anything older than this as stale 9 // and re-execute on form submit, so a slow customer never sends an expired token. 13 var config = window.spamoff_bot_protection || {}; 14 15 // Google v3 tokens expire after 120s; fetch a new one past this age. 10 16 var TOKEN_FRESH_MS = 90 * 1000; 11 17 12 function hasFreshToken($tokenField) { 13 if (!$tokenField || !$tokenField.length) return false; 14 if (!$tokenField.val()) return false; 15 var fetchedAt = parseInt($tokenField.data('spamoff-bot-protection-fetched-at') || 0, 10); 16 return (Date.now() - fetchedAt) < TOKEN_FRESH_MS; 17 } 18 19 function stampToken($tokenField, token) { 20 $tokenField.val(token); 21 $tokenField.data('spamoff-bot-protection-fetched-at', Date.now()); 22 } 23 24 var SpamOffBotProtection = { 25 init: function() { 26 if (typeof spamoff_bot_protection === 'undefined') { 18 // Give up waiting for Google's script (blocked by an extension, offline) after this. 19 var READY_TIMEOUT_MS = 15 * 1000; 20 var READY_POLL_MS = 100; 21 22 var readyCallbacks = null; 23 24 /** 25 * Run onReady once grecaptcha is usable, or onFail if it never loads. 26 */ 27 function whenReady(onReady, onFail) { 28 if (window.grecaptcha && typeof window.grecaptcha.ready === 'function') { 29 window.grecaptcha.ready(onReady); 30 return; 31 } 32 33 if (!readyCallbacks) { 34 readyCallbacks = []; 35 var waited = 0; 36 var timer = setInterval(function () { 37 waited += READY_POLL_MS; 38 var loaded = window.grecaptcha && typeof window.grecaptcha.ready === 'function'; 39 if (!loaded && waited < READY_TIMEOUT_MS) { 40 return; 41 } 42 clearInterval(timer); 43 var callbacks = readyCallbacks; 44 readyCallbacks = null; 45 callbacks.forEach(function (callback) { 46 if (loaded) { 47 window.grecaptcha.ready(callback.onReady); 48 } else if (callback.onFail) { 49 callback.onFail(); 50 } 51 }); 52 }, READY_POLL_MS); 53 } 54 55 readyCallbacks.push({ onReady: onReady, onFail: onFail }); 56 } 57 58 /* ------------------------------------------------------------------ v3 */ 59 60 function getTokenField(form) { 61 return form.querySelector('input[name="spamoff_bot_protection_token"]'); 62 } 63 64 function hasFreshToken(field) { 65 var fetchedAt = parseInt(field.getAttribute('data-fetched-at') || '0', 10); 66 return field.value !== '' && Date.now() - fetchedAt < TOKEN_FRESH_MS; 67 } 68 69 function getAction(field) { 70 var container = field.closest('.spamoff-bot-protection-container'); 71 return (container && container.getAttribute('data-action')) || 'general'; 72 } 73 74 function defaultSubmitter(form) { 75 return form.querySelector('button[type="submit"][name], input[type="submit"][name], button:not([type])[name]'); 76 } 77 78 /** 79 * Submit the form again as if the visitor had clicked `submitter`. 80 */ 81 function resubmit(form, submitter) { 82 if (typeof form.requestSubmit === 'function') { 83 // Fires the submit event again, so other handlers (e.g. WooCommerce's AJAX 84 // checkout) still run, and includes the clicked button in the form data. 85 try { 86 form.requestSubmit(submitter && submitter.form === form ? submitter : undefined); 27 87 return; 28 } 29 30 if (spamoff_bot_protection.type === 'v3') { 31 this.initV3(); 32 } else if (spamoff_bot_protection.type === 'v2') { 33 this.initV2(); 34 } 35 }, 36 37 initV3: function() { 38 var self = this; 39 40 if (typeof grecaptcha === 'undefined') { 41 setTimeout(function() { 42 self.initV3(); 43 }, 100); 88 } catch (e) { 89 form.requestSubmit(); 44 90 return; 45 91 } 46 47 this.handleForm('form.login, form.woocommerce-form-login', 'login'); 48 this.handleForm('form.register, form.woocommerce-form-register', 'registration'); 49 this.handleCheckoutForm(); 50 this.handleForm('form.lost_reset_password, form.woocommerce-ResetPassword', 'lost_password'); 51 this.handleForm('form#commentform', 'comments'); 52 53 // Custom forms with shortcode. 54 $('[id^="spamoff_bot_protection_token_"]').each(function() { 55 var action = $(this).attr('id').replace('spamoff_bot_protection_token_', ''); 56 var form = $(this).closest('form'); 57 if (form.length) { 58 self.handleForm(form, action); 92 } 93 94 // Older browsers: forms submitted over AJAX by jQuery handlers. 95 if ($(form).is('form.checkout')) { 96 $(form).trigger('submit'); 97 return; 98 } 99 100 // Carry the clicked button's name/value by hand, and call the prototype method 101 // because a control named "submit" (WordPress comment form) shadows form.submit. 102 if (submitter && submitter.name) { 103 var hidden = document.createElement('input'); 104 hidden.type = 'hidden'; 105 hidden.name = submitter.name; 106 hidden.value = submitter.value; 107 form.appendChild(hidden); 108 } 109 HTMLFormElement.prototype.submit.call(form); 110 } 111 112 /** 113 * Capture-phase submit listener: runs before any handler bound on the form itself, 114 * so a submission never leaves without a fresh token. 115 */ 116 function onSubmit(event) { 117 var form = event.target; 118 if (!(form instanceof HTMLFormElement) || !config.v3SiteKey) { 119 return; 120 } 121 122 var field = getTokenField(form); 123 if (!field) { 124 return; 125 } 126 127 // Token fetch failed: let the submission through; the server shows the error. 128 if (form.getAttribute('data-spamoff-bot-protection-skip') === '1') { 129 form.removeAttribute('data-spamoff-bot-protection-skip'); 130 return; 131 } 132 133 if (hasFreshToken(field)) { 134 // Tokens are single-use. Keep the value for this submission (payment gateways 135 // may re-trigger it), but make the next one fetch a new token. 136 field.setAttribute('data-fetched-at', '0'); 137 return; 138 } 139 140 event.preventDefault(); 141 event.stopImmediatePropagation(); 142 143 if (form.getAttribute('data-spamoff-bot-protection-pending') === '1') { 144 return; 145 } 146 form.setAttribute('data-spamoff-bot-protection-pending', '1'); 147 148 var submitter = event.submitter || defaultSubmitter(form); 149 150 function done(token) { 151 form.removeAttribute('data-spamoff-bot-protection-pending'); 152 if (token) { 153 field.value = token; 154 field.setAttribute('data-fetched-at', String(Date.now())); 155 } else { 156 form.setAttribute('data-spamoff-bot-protection-skip', '1'); 157 } 158 resubmit(form, submitter); 159 } 160 161 whenReady(function () { 162 window.grecaptcha.execute(config.v3SiteKey, { action: getAction(field) }).then(done, function () { 163 done(''); 164 }); 165 }, function () { 166 done(''); 167 }); 168 } 169 170 document.addEventListener('submit', onSubmit, true); 171 172 /* ------------------------------------------------------------------ v2 */ 173 174 function renderV2Widgets() { 175 var widgets = document.querySelectorAll('.spamoff-bot-protection-v2 .g-recaptcha[data-sitekey]:not([data-widget-id])'); 176 if (!widgets.length) { 177 return; 178 } 179 180 whenReady(function () { 181 Array.prototype.forEach.call(widgets, function (el) { 182 if (el.hasAttribute('data-widget-id')) { 183 return; 184 } 185 try { 186 var widgetId = window.grecaptcha.render(el, { 187 sitekey: el.getAttribute('data-sitekey'), 188 theme: el.getAttribute('data-theme') || 'light', 189 size: el.getAttribute('data-size') || 'normal' 190 }); 191 el.setAttribute('data-widget-id', String(widgetId)); 192 } catch (e) { 193 // Already rendered. 59 194 } 60 195 }); 61 }, 62 63 initV2: function() { 64 // v2 is handled automatically by Google's script after rendering the div. 65 }, 66 67 handleCheckoutForm: function() { 68 var $checkoutForm = $('form.checkout, form.woocommerce-checkout'); 69 70 if (!$checkoutForm.length) { 196 fitV2Widgets(); 197 }); 198 } 199 200 function resetV2Widgets(scope) { 201 if (!window.grecaptcha || typeof window.grecaptcha.reset !== 'function') { 202 return; 203 } 204 Array.prototype.forEach.call(scope.querySelectorAll('.g-recaptcha[data-widget-id]'), function (el) { 205 try { 206 window.grecaptcha.reset(parseInt(el.getAttribute('data-widget-id'), 10)); 207 } catch (e) { 208 // Widget gone. 209 } 210 }); 211 } 212 213 /** 214 * Google's widget has a fixed width; scale it down when its column is narrower. 215 */ 216 function fitV2Widgets() { 217 Array.prototype.forEach.call(document.querySelectorAll('.spamoff-bot-protection-v2'), function (box) { 218 var widget = box.querySelector('.g-recaptcha'); 219 if (!widget) { 71 220 return; 72 221 } 73 74 // For v2, ensure the widget renders. 75 if (spamoff_bot_protection.type === 'v2') { 76 setTimeout(function() { 77 if (typeof grecaptcha !== 'undefined' && grecaptcha.render) { 78 $('.g-recaptcha[data-sitekey="' + spamoff_bot_protection.siteKey + '"]').each(function() { 79 if (!$(this).attr('data-rendered')) { 80 try { 81 grecaptcha.render(this, { 82 'sitekey': spamoff_bot_protection.siteKey, 83 'theme': $(this).data('theme') || 'light', 84 'size': $(this).data('size') || 'normal' 85 }); 86 $(this).attr('data-rendered', 'true'); 87 } catch(e) { 88 // noop 89 } 90 } 91 }); 92 } 93 }, 1000); 94 } 95 96 // Pre-execute a v3 token so quick submitters don't see a delay. Slow 97 // submitters get a fresh token on click — see hasFreshToken() check below. 98 // handleCheckoutForm() can re-run on every ajaxComplete, so skip the 99 // pre-execute if a recent token is already in the field. 100 if (spamoff_bot_protection.type === 'v3') { 101 var $tokenField = $('input[id^="spamoff_bot_protection_token_checkout"]'); 102 var needsPreExecute = $tokenField.length 103 && !hasFreshToken($tokenField.first()) 104 && typeof grecaptcha !== 'undefined' 105 && grecaptcha.ready; 106 if (needsPreExecute) { 107 grecaptcha.ready(function() { 108 grecaptcha.execute(spamoff_bot_protection.siteKey, {action: 'checkout'}) 109 .then(function(token) { 110 $tokenField.each(function() { 111 stampToken($(this), token); 112 }); 113 }) 114 .catch(function() { 115 // noop — submit-time handler will retry. 116 }); 117 }); 118 } 119 } 120 121 // Place Order click: always ensure a fresh token. The previous version 122 // only intercepted when the field was empty, so a stale pre-fetched 123 // token (>120s old, Google's TTL) would be sent and rejected. 124 $(document).off('click', '#place_order').on('click', '#place_order', function(e) { 125 if (spamoff_bot_protection.type !== 'v3') return; 126 127 var tokenField = $('input[id^="spamoff_bot_protection_token_checkout"]').first(); 128 if (!tokenField.length) return; 129 if (hasFreshToken(tokenField)) return; // existing token is recent — let the submit proceed 130 131 e.preventDefault(); 132 e.stopImmediatePropagation(); 133 134 if (typeof grecaptcha !== 'undefined' && grecaptcha.ready) { 135 grecaptcha.ready(function() { 136 grecaptcha.execute(spamoff_bot_protection.siteKey, {action: 'checkout'}) 137 .then(function(token) { 138 stampToken(tokenField, token); 139 $checkoutForm.submit(); 140 }) 141 .catch(function() { 142 if (typeof wc_add_notice !== 'undefined') { 143 wc_add_notice('Verification failed. Please try again.', 'error'); 144 } 145 }); 146 }); 147 } 148 return false; 149 }); 150 151 // Also handle regular form submit as fallback (e.g. Enter key in a field). 152 this.handleForm('form.checkout, form.woocommerce-checkout', 'checkout'); 153 }, 154 155 handleForm: function(selector, action) { 156 var $form = $(selector); 157 158 if (!$form.length) { 159 return; 160 } 161 162 if ($form.data('spamoff-bot-protection-handled')) { 163 return; 164 } 165 166 $form.data('spamoff-bot-protection-handled', true); 167 168 // Execute the captcha on form submit. A token from page-load pre-execution 169 // is reused only while still fresh (under Google's 120s TTL) — otherwise 170 // we fetch a new one before letting the form submit. 171 var submitHandler = function(e) { 172 var $formElement = $(this); 173 var tokenField = $formElement.find('input[id^="spamoff_bot_protection_token_' + action + '"]').first(); 174 175 if (!tokenField.length) return; 176 if (hasFreshToken(tokenField)) return true; 177 178 e.preventDefault(); 179 e.stopImmediatePropagation(); 180 181 if (typeof grecaptcha !== 'undefined' && grecaptcha.ready) { 182 grecaptcha.ready(function() { 183 grecaptcha.execute(spamoff_bot_protection.siteKey, {action: action}) 184 .then(function(token) { 185 stampToken(tokenField, token); 186 // Unbind only our handler so WooCommerce's submit handler remains. 187 $formElement.off('submit', submitHandler).trigger('submit'); 188 }) 189 .catch(function() { 190 $formElement.off('submit', submitHandler).trigger('submit'); 191 }); 192 }); 193 } else { 194 $formElement.off('submit', submitHandler).trigger('submit'); 195 } 196 197 return false; 198 }; 199 200 $form.on('submit', submitHandler); 201 } 202 }; 203 204 205 // Initialize when document is ready. 206 $(document).ready(function() { 207 SpamOffBotProtection.init(); 222 var compact = widget.getAttribute('data-size') === 'compact'; 223 var width = compact ? 164 : 304; 224 var height = compact ? 144 : 78; 225 var available = box.clientWidth; 226 var scale = available > 0 && available < width ? available / width : 1; 227 228 widget.style.transform = scale < 1 ? 'scale(' + scale + ')' : ''; 229 widget.style.transformOrigin = '0 0'; 230 box.style.height = scale < 1 ? Math.ceil(height * scale) + 'px' : ''; 231 }); 232 } 233 234 /* ------------------------------------------------------------- general */ 235 236 /** 237 * A theme that fires a form hook twice would put two captchas in one form, which 238 * would post two tokens/responses under one name. Keep the first. 239 */ 240 function removeDuplicateCaptchas() { 241 Array.prototype.forEach.call(document.forms, function (form) { 242 var containers = form.querySelectorAll('.spamoff-bot-protection-container'); 243 for (var i = 1; i < containers.length; i++) { 244 containers[i].parentNode.removeChild(containers[i]); 245 } 246 }); 247 } 248 249 function init() { 250 removeDuplicateCaptchas(); 251 renderV2Widgets(); 252 253 // Google allows hiding the v3 badge only when its notice is shown instead. 254 if (document.querySelector('.spamoff-bot-protection-notice')) { 255 document.documentElement.classList.add('spamoff-bot-protection-hide-badge'); 256 } 257 } 258 259 $(init); 260 261 // Forms added by AJAX (e.g. a theme's login popup). 262 $(document).ajaxComplete(function () { 263 removeDuplicateCaptchas(); 264 renderV2Widgets(); 208 265 }); 209 266 210 // Also initialize on AJAX-loaded content (for WooCommerce checkout AJAX updates). 211 $(document.body).on('updated_checkout updated_wc_div checkout_updated', function() { 212 $('form.checkout, form.woocommerce-checkout').removeData('spamoff-bot-protection-handled'); 213 // Google tokens are one-time-use. Clear the stored token so the pre-execute 214 // in handleCheckoutForm always fetches a fresh one after any checkout update 215 // (including failed payment attempts where the old token was already consumed). 216 $('input[id^="spamoff_bot_protection_token_checkout"]') 217 .val('') 218 .removeData('spamoff-bot-protection-fetched-at'); 219 // Reset v2 widget so the user doesn't resubmit an already-consumed response. 220 if (typeof grecaptcha !== 'undefined' && window.spamoff_bot_protection && spamoff_bot_protection.type === 'v2') { 221 try { grecaptcha.reset(); } catch(e) {} 222 } 223 setTimeout(function() { 224 SpamOffBotProtection.init(); 225 }, 500); 267 // A failed classic checkout has already spent the token/response on the server. 268 // Clear it so the next attempt gets a new one. (Checkout refreshes that don't 269 // submit the order leave a ticked v2 box alone.) 270 $(document.body).on('checkout_error', function () { 271 var form = document.querySelector('form.checkout'); 272 if (!form) { 273 return; 274 } 275 Array.prototype.forEach.call(form.querySelectorAll('input[name="spamoff_bot_protection_token"]'), function (field) { 276 field.value = ''; 277 field.setAttribute('data-fetched-at', '0'); 278 }); 279 resetV2Widgets(form); 226 280 }); 227 281 228 // Re-initialize after AJAX complete. 229 $(document).ajaxComplete(function() { 230 setTimeout(function() { 231 SpamOffBotProtection.init(); 232 }, 100); 282 var resizeTimer = null; 283 $(window).on('resize', function () { 284 clearTimeout(resizeTimer); 285 resizeTimer = setTimeout(fitV2Widgets, 150); 233 286 }); 234 235 // Special handling for checkout page load: re-init, then trigger the v2 width-match resize.236 $(window).on('load', function() {237 if ($('body').hasClass('woocommerce-checkout')) {238 setTimeout(function() {239 SpamOffBotProtection.init();240 setTimeout(function() {241 $(window).trigger('resize');242 }, 400);243 }, 300);244 }245 });246 247 // Match the v2 reCAPTCHA widget width to the Place Order button via CSS transform.248 function matchRecaptchaToPlaceOrder() {249 var $recaptcha = $('.woocommerce-checkout .g-recaptcha');250 var $placeOrder = $('.woocommerce-checkout #place_order');251 252 if (!$recaptcha.length || !$placeOrder.length) return;253 if (!$recaptcha.find('iframe').length) return;254 255 var baseWidth = 304; // Google's fixed widget width256 // Cap at 1 — only scale DOWN for narrow buttons, never upscale beyond natural size.257 var scale = Math.min(1, $placeOrder.outerWidth() / baseWidth);258 259 $recaptcha.css({260 transform: 'scale(' + scale + ')',261 'transform-origin': 'left top',262 width: baseWidth + 'px',263 height: 'auto'264 });265 }266 267 $(document.body).on('updated_checkout updated_wc_div checkout_updated', function() {268 setTimeout(matchRecaptchaToPlaceOrder, 800);269 });270 271 $(window).on('resize', function() {272 setTimeout(matchRecaptchaToPlaceOrder, 300);273 });274 275 287 })(jQuery); -
spamoff-bot-protection/tags/1.0.0/includes/class-spamoff-bot-protection-admin.php
r3603369 r3714007 8 8 public function __construct() { 9 9 add_filter( 'plugin_action_links_' . SPAMOFF_BOT_PROTECTION_PLUGIN_BASENAME, array( $this, 'add_settings_link' ) ); 10 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_settings_script' ) ); 11 add_action( 'admin_init', array( $this, 'add_privacy_policy_content' ) ); 12 } 13 14 /** 15 * Suggested text for the site's privacy policy (Settings → Privacy → Policy Guide), 16 * since protected forms send visitor data to Google. 17 */ 18 public function add_privacy_policy_content() { 19 if ( ! function_exists( 'wp_add_privacy_policy_content' ) ) { 20 return; 21 } 22 23 $content = '<p class="privacy-policy-tutorial">' 24 . esc_html__( 'SpamOff adds Google reCAPTCHA to the forms you protect. On pages with a protected form, Google\'s reCAPTCHA script runs in the visitor\'s browser, and when the form is submitted your server sends the reCAPTCHA response and the visitor\'s IP address to Google to check it.', 'spamoff-bot-protection' ) 25 . '</p><p><strong class="privacy-policy-tutorial">' . esc_html__( 'Suggested text:', 'spamoff-bot-protection' ) . ' </strong>' 26 . esc_html__( 'We use Google reCAPTCHA on our login, registration, password reset, checkout and comment forms to protect this site from spam and abuse. reCAPTCHA collects hardware and software information, such as device and application data, and sends it to Google for analysis. Your use of reCAPTCHA is subject to the Google Privacy Policy and Terms of Service.', 'spamoff-bot-protection' ) 27 . ' <a href="https://policies.google.com/privacy">https://policies.google.com/privacy</a>' 28 . ' <a href="https://policies.google.com/terms">https://policies.google.com/terms</a></p>'; 29 30 wp_add_privacy_policy_content( __( 'SpamOff – Google reCaptcha for WooCommerce', 'spamoff-bot-protection' ), wp_kses_post( $content ) ); 10 31 } 11 32 … … 16 37 return $links; 17 38 } 39 40 /** 41 * Show only the selected version's key fields on the General Settings screen. 42 */ 43 public function enqueue_settings_script( $hook_suffix ) { 44 // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only screen detection. 45 $is_settings_tab = 'woocommerce_page_wc-settings' === $hook_suffix 46 && isset( $_GET['tab'] ) 47 && 'spamoff-bot-protection' === sanitize_key( wp_unslash( $_GET['tab'] ) ); 48 // phpcs:enable WordPress.Security.NonceVerification.Recommended 49 50 if ( ! $is_settings_tab ) { 51 return; 52 } 53 54 wp_enqueue_script( 55 'spamoff-bot-protection-admin', 56 SPAMOFF_BOT_PROTECTION_PLUGIN_URL . 'assets/js/spamoff-bot-protection-admin.js', 57 array(), 58 SPAMOFF_BOT_PROTECTION_PLUGIN_VERSION, 59 array( 'in_footer' => true ) 60 ); 61 62 wp_localize_script( 63 'spamoff-bot-protection-admin', 64 'spamoff_bot_protection_admin', 65 array( 66 // The Checkout block always uses the v3 keys, so keep them visible when it's in use. 67 'alwaysShowV3' => SpamOff_Bot_Protection_Helper::checkout_page_uses_blocks() ? '1' : '', 68 ) 69 ); 70 } 18 71 } -
spamoff-bot-protection/tags/1.0.0/includes/class-spamoff-bot-protection-captcha.php
r3603369 r3714007 4 4 } 5 5 6 /** 7 * Loads Google's reCAPTCHA script and the plugin's assets, only on pages that need them. 8 * 9 * Forms call require_type() as they render a captcha. Google's script is added in the 10 * footer once the page knows which versions it rendered, so pages without a protected 11 * form (including login pages whose toggle is off) never load it — and never show the 12 * v3 badge. 13 */ 6 14 class SpamOff_Bot_Protection_Captcha { 7 15 16 /** @var array<string, bool> reCAPTCHA versions rendered on the current page. */ 17 private static $required_types = array(); 18 8 19 public function __construct() { 9 add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); 20 add_action( 'wp_enqueue_scripts', array( __CLASS__, 'register_assets' ), 5 ); 21 add_action( 'wp_enqueue_scripts', array( $this, 'maybe_load_everywhere' ) ); 22 // Just before footer scripts print, so forms output in wp_footer (login popups) count too. 23 add_action( 'wp_print_footer_scripts', array( __CLASS__, 'enqueue_google_api' ), 1 ); 24 25 // The Checkout block has no PHP form hook; detect it as it renders. 26 add_filter( 'render_block_woocommerce/checkout', array( $this, 'on_checkout_block_render' ) ); 10 27 } 11 28 12 public function enqueue_scripts() { 13 $site_key = SpamOff_Bot_Protection_Helper::get_option( 'spamoff_bot_protection_site_key' ); 14 if ( empty( $site_key ) ) { 29 public static function register_assets() { 30 if ( wp_script_is( 'spamoff-bot-protection-handler', 'registered' ) ) { 15 31 return; 16 32 } 17 33 18 if ( is_admin() && ! wp_doing_ajax() ) { 19 return; 20 } 21 22 // Blocks Checkout supports only v3. 23 $recaptcha_type = SpamOff_Bot_Protection_Helper::get_option( 'spamoff_bot_protection_type', 'v3' ); 24 if ( SpamOff_Bot_Protection_Helper::is_blocks_checkout() ) { 25 $recaptcha_type = 'v3'; 26 } 27 28 $google_api_url = 'v3' === $recaptcha_type 29 ? 'https://www.google.com/recaptcha/api.js?render=' . rawurlencode( $site_key ) 30 : 'https://www.google.com/recaptcha/api.js'; 31 32 wp_enqueue_script( 'spamoff-bot-protection-api', $google_api_url, array(), SPAMOFF_BOT_PROTECTION_PLUGIN_VERSION, true ); 33 34 wp_enqueue_script( 34 wp_register_script( 35 35 'spamoff-bot-protection-handler', 36 36 SPAMOFF_BOT_PROTECTION_PLUGIN_URL . 'assets/js/spamoff-bot-protection-handler.js', 37 array( 'jquery' , 'spamoff-bot-protection-api'),37 array( 'jquery' ), 38 38 SPAMOFF_BOT_PROTECTION_PLUGIN_VERSION, 39 true39 array( 'in_footer' => true ) 40 40 ); 41 41 42 wp_localize_script( 43 'spamoff-bot-protection-handler', 44 'spamoff_bot_protection', 45 array( 46 'siteKey' => $site_key, 47 'type' => $recaptcha_type, 48 ) 42 wp_register_script( 43 'spamoff-bot-protection-blocks-checkout', 44 SPAMOFF_BOT_PROTECTION_PLUGIN_URL . 'assets/js/spamoff-bot-protection-blocks-checkout.js', 45 array( 'wp-data' ), 46 SPAMOFF_BOT_PROTECTION_PLUGIN_VERSION, 47 array( 'in_footer' => true ) 49 48 ); 50 49 51 if ( SpamOff_Bot_Protection_Helper::is_blocks_checkout() ) { 52 wp_enqueue_script( 53 'spamoff-bot-protection-blocks-checkout', 54 SPAMOFF_BOT_PROTECTION_PLUGIN_URL . 'assets/js/spamoff-bot-protection-blocks-checkout.js', 55 array( 'wp-data', 'wp-hooks', 'spamoff-bot-protection-handler' ), 56 SPAMOFF_BOT_PROTECTION_PLUGIN_VERSION, 57 true 58 ); 59 } 60 61 wp_enqueue_style( 50 wp_register_style( 62 51 'spamoff-bot-protection-style', 63 52 SPAMOFF_BOT_PROTECTION_PLUGIN_URL . 'assets/css/spamoff-bot-protection-style.css', … … 66 55 ); 67 56 } 57 58 /** 59 * Mark a reCAPTCHA version as used on this page and enqueue the plugin's assets. 60 * 61 * Safe to call while the page body renders: scripts are printed in the footer. 62 */ 63 public static function require_type( $type ) { 64 self::register_assets(); 65 66 self::$required_types[ $type ] = true; 67 68 wp_enqueue_style( 'spamoff-bot-protection-style' ); 69 wp_enqueue_script( 'spamoff-bot-protection-handler' ); 70 } 71 72 /** 73 * Opt-in fallback for forms injected by AJAX after the page loads (e.g. login modals), 74 * which render too late to enqueue scripts themselves. 75 */ 76 public function maybe_load_everywhere() { 77 /** 78 * Filters whether reCAPTCHA assets load on every front-end page. 79 * 80 * @since 1.1.0 81 * 82 * @param bool $load_everywhere Default false: load only where a protected form renders. 83 */ 84 if ( apply_filters( 'spamoff_bot_protection_load_everywhere', false ) && SpamOff_Bot_Protection_Helper::has_keys() ) { 85 self::require_type( SpamOff_Bot_Protection_Helper::get_type() ); 86 } 87 } 88 89 /** 90 * Enqueue the Blocks Checkout token script when the Checkout block renders. 91 * 92 * @param string $block_content Rendered block HTML, returned unchanged. 93 * @return string 94 */ 95 public function on_checkout_block_render( $block_content ) { 96 if ( SpamOff_Bot_Protection_Frontend::is_blocks_checkout_protected() ) { 97 self::require_type( 'v3' ); 98 wp_enqueue_script( 'spamoff-bot-protection-blocks-checkout' ); 99 } 100 return $block_content; 101 } 102 103 /** 104 * Add Google's script once the page has rendered every captcha it needs. 105 * 106 * v3 needs the site key in the URL (and shows the badge); v2 widgets are rendered 107 * explicitly by the handler, which also works alongside a v3 script on the same page. 108 */ 109 public static function enqueue_google_api() { 110 if ( empty( self::$required_types ) ) { 111 return; 112 } 113 114 $needs_v3 = isset( self::$required_types['v3'] ); 115 $v3_key = SpamOff_Bot_Protection_Helper::get_site_key( 'v3' ); 116 117 if ( $needs_v3 && '' !== $v3_key ) { 118 $google_api_url = add_query_arg( 'render', rawurlencode( $v3_key ), 'https://www.google.com/recaptcha/api.js' ); 119 } else { 120 $needs_v3 = false; 121 $google_api_url = add_query_arg( 'render', 'explicit', 'https://www.google.com/recaptcha/api.js' ); 122 } 123 124 wp_enqueue_script( 125 'spamoff-bot-protection-api', 126 $google_api_url, 127 array(), 128 SPAMOFF_BOT_PROTECTION_PLUGIN_VERSION, 129 array( 130 'in_footer' => true, 131 'strategy' => 'async', 132 ) 133 ); 134 135 $config = array( 136 'v3SiteKey' => $needs_v3 ? $v3_key : '', 137 'failedMessage' => SpamOff_Bot_Protection_Helper::get_error_message( 'failed' ), 138 ); 139 140 foreach ( array( 'spamoff-bot-protection-handler', 'spamoff-bot-protection-blocks-checkout' ) as $handle ) { 141 if ( wp_script_is( $handle, 'enqueued' ) ) { 142 wp_localize_script( $handle, 'spamoff_bot_protection', $config ); 143 } 144 } 145 } 68 146 } -
spamoff-bot-protection/tags/1.0.0/includes/class-spamoff-bot-protection-frontend.php
r3603369 r3714007 4 4 } 5 5 6 use Automattic\WooCommerce\StoreApi\Exceptions\RouteException; 7 use Automattic\WooCommerce\StoreApi\Schemas\V1\CheckoutSchema; 8 use SpamOff_Bot_Protection_Helper as Helper; 9 6 10 class SpamOff_Bot_Protection_Frontend { 7 11 8 /** Tracks which (action, suffix) pairs have already been rendered to avoid duplicates. */ 9 private static $rendered_actions = array(); 12 /** Store API extension namespace the Blocks Checkout script writes its token to. */ 13 const STORE_API_NAMESPACE = 'spamoff-bot-protection'; 14 15 /** Captchas rendered per (action, suffix) on this page, for unique element IDs. */ 16 private static $render_counts = array(); 10 17 11 18 public function __construct() { 12 // Blocks Checkout (Store API): reads token from request.extensions and throws on failure.13 add_action( 'woocommerce_store_api_checkout_update_order_from_request', array( $this, 'validate_blocks_checkout' ), 10, 2 );14 15 19 // Render captcha on WooCommerce / WP forms. 16 20 add_action( 'woocommerce_login_form', array( $this, 'add_login_captcha' ) ); … … 21 25 add_action( 'comment_form_logged_in_after', array( $this, 'add_comments_captcha' ) ); 22 26 23 // Server-side validation. 27 // WordPress's own forms: wp-login.php, plus login forms made with wp_login_form(). 28 add_action( 'login_form', array( $this, 'add_wp_login_captcha' ) ); 29 add_filter( 'login_form_middle', array( $this, 'add_wp_login_form_captcha' ) ); 30 add_action( 'register_form', array( $this, 'add_wp_register_captcha' ) ); 31 add_action( 'lostpassword_form', array( $this, 'add_wp_lost_password_captcha' ) ); 32 33 // Server-side validation. Each hook fires only for its own form, after WooCommerce 34 // has checked that form's nonce. 24 35 add_filter( 'woocommerce_process_login_errors', array( $this, 'validate_login' ), 10, 3 ); 25 add_ action( 'woocommerce_register_post', array( $this, 'validate_registration' ), 10, 3);26 add_action( 'woocommerce_ checkout_process', array( $this, 'validate_checkout' ), 1);27 add_action( ' woocommerce_lostpassword_post', array( $this, 'validate_lost_password' ), 1);36 add_filter( 'woocommerce_process_registration_errors', array( $this, 'validate_registration' ), 10, 4 ); 37 add_action( 'woocommerce_after_checkout_validation', array( $this, 'validate_checkout' ), 10, 2 ); 38 add_action( 'lostpassword_post', array( $this, 'validate_lost_password' ), 10, 2 ); 28 39 add_action( 'pre_comment_on_post', array( $this, 'validate_comment' ) ); 29 40 41 // After WordPress checks the password (priority 20), so a failed captcha always wins. 42 add_filter( 'authenticate', array( $this, 'validate_wp_login' ), 30 ); 43 add_filter( 'registration_errors', array( $this, 'validate_wp_registration' ) ); 44 45 // Blocks Checkout (Store API): reads the token from request.extensions and throws on failure. 46 add_action( 'init', array( $this, 'register_store_api_data' ) ); 47 add_action( 'woocommerce_store_api_checkout_update_order_from_request', array( $this, 'validate_blocks_checkout' ), 10, 2 ); 48 30 49 add_shortcode( 'spamoff_bot_protection', array( $this, 'shortcode' ) ); 31 50 } 32 51 33 52 /** 34 * Validate the captcha for WooCommerce Blocks Checkout. 35 * 36 * The Blocks JS injects the token via setExtensionData('spamoff-bot-protection', { token, action }), 37 * which arrives here on the Store API request's `extensions` payload. 38 */ 39 public function validate_blocks_checkout( $order, $request ) { 40 if ( ! SpamOff_Bot_Protection_Helper::is_form_enabled( 'checkout' ) ) { 41 return; 42 } 43 44 $extensions = is_array( $request['extensions'] ?? null ) ? $request['extensions'] : array(); 45 $data = isset( $extensions['spamoff-bot-protection'] ) && is_array( $extensions['spamoff-bot-protection'] ) 46 ? $extensions['spamoff-bot-protection'] 47 : array(); 48 49 $token = isset( $data['token'] ) ? sanitize_text_field( (string) $data['token'] ) : ''; 50 $action = isset( $data['action'] ) ? sanitize_text_field( (string) $data['action'] ) : 'checkout'; 51 52 if ( empty( $token ) ) { 53 throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException( 54 'spamoff_bot_protection_missing', 55 esc_html( SpamOff_Bot_Protection_Helper::get_error_message( 'missing' ) ), 56 400 57 ); 58 } 59 60 $result = SpamOff_Bot_Protection_Helper::verify_recaptcha( $token, $action ); 61 62 if ( empty( $result['success'] ) || true !== $result['success'] ) { 63 $error_type = isset( $result['error'] ) ? $result['error'] : 'failed'; 64 throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException( 65 'spamoff_bot_protection_failed', 66 esc_html( SpamOff_Bot_Protection_Helper::get_error_message( $error_type ) ), 67 400 68 ); 69 } 53 * Blocks Checkout protection. The Checkout block only supports v3, so it always 54 * uses the v3 keys, whichever version is selected for the other forms. 55 */ 56 public static function is_blocks_checkout_protected() { 57 return Helper::is_checkout_enabled_for_current_user() && Helper::has_keys( 'v3' ); 70 58 } 71 59 72 60 public function add_login_captcha() { 73 if ( SpamOff_Bot_Protection_Helper::is_form_enabled( 'login' ) ) {61 if ( Helper::is_form_protected( 'login' ) ) { 74 62 $this->render_captcha( 'login' ); 75 63 } … … 77 65 78 66 public function add_register_captcha() { 79 if ( SpamOff_Bot_Protection_Helper::is_form_enabled( 'registration' ) ) {67 if ( Helper::is_form_protected( 'registration' ) ) { 80 68 $this->render_captcha( 'registration' ); 81 69 } … … 83 71 84 72 public function add_checkout_captcha() { 85 if ( ! SpamOff_Bot_Protection_Helper::is_form_enabled( 'checkout' ) ) { 86 return; 87 } 88 89 if ( empty( SpamOff_Bot_Protection_Helper::get_option( 'spamoff_bot_protection_site_key' ) ) ) { 73 if ( ! Helper::is_checkout_protected() ) { 90 74 return; 91 75 } … … 97 81 98 82 public function add_lost_password_captcha() { 99 if ( SpamOff_Bot_Protection_Helper::is_form_enabled( 'lost_password' ) ) {83 if ( Helper::is_form_protected( 'lost_password' ) ) { 100 84 $this->render_captcha( 'lost_password' ); 101 85 } … … 103 87 104 88 public function add_comments_captcha() { 105 if ( SpamOff_Bot_Protection_Helper::is_form_enabled( 'comments' ) ) {89 if ( Helper::is_form_protected( 'comments' ) ) { 106 90 $this->render_captcha( 'comments' ); 107 91 } 108 92 } 109 93 94 public function add_wp_login_captcha() { 95 if ( Helper::is_form_protected( 'wp_login' ) ) { 96 $this->render_captcha( 'wp_login' ); 97 } 98 } 99 100 /** 101 * Login forms built with wp_login_form() (theme widgets, custom pages) post to 102 * wp-login.php but don't fire the `login_form` action. 103 * 104 * @param string $content HTML inserted before the form's submit button. 105 * @return string 106 */ 107 public function add_wp_login_form_captcha( $content ) { 108 if ( ! Helper::is_form_protected( 'wp_login' ) ) { 109 return $content; 110 } 111 112 ob_start(); 113 $this->render_captcha( 'wp_login' ); 114 return $content . ob_get_clean(); 115 } 116 117 public function add_wp_register_captcha() { 118 if ( Helper::is_form_protected( 'wp_registration' ) ) { 119 $this->render_captcha( 'wp_registration' ); 120 } 121 } 122 123 public function add_wp_lost_password_captcha() { 124 if ( Helper::is_form_protected( 'wp_lost_password' ) ) { 125 $this->render_captcha( 'wp_lost_password' ); 126 } 127 } 128 110 129 public function validate_login( $validation_error, $username, $password ) { 111 if ( ! SpamOff_Bot_Protection_Helper::is_form_enabled( 'login' ) ) {130 if ( ! Helper::is_form_protected( 'login' ) || ! $this->should_validate( 'login' ) ) { 112 131 return $validation_error; 113 132 } … … 121 140 } 122 141 123 public function validate_registration( $username, $email, $validation_errors ) { 124 if ( ! SpamOff_Bot_Protection_Helper::is_form_enabled( 'registration' ) ) { 125 return; 142 /** 143 * Runs only for the My Account registration form. (`woocommerce_register_post` also 144 * fires for accounts created at checkout, in the admin and over REST, none of which 145 * carry this captcha.) 146 */ 147 public function validate_registration( $validation_error, $username, $password, $email ) { 148 if ( ! Helper::is_form_protected( 'registration' ) || ! $this->should_validate( 'registration' ) ) { 149 return $validation_error; 126 150 } 127 151 128 152 $result = $this->validate_captcha( 'registration' ); 129 153 if ( is_wp_error( $result ) ) { 130 $validation_errors->add( 'spamoff_bot_protection_error', $result->get_error_message() ); 131 } 132 } 133 134 public function validate_checkout() { 135 if ( ! SpamOff_Bot_Protection_Helper::is_form_enabled( 'checkout' ) ) { 154 $validation_error->add( 'spamoff_bot_protection_error', $result->get_error_message() ); 155 } 156 157 return $validation_error; 158 } 159 160 /** 161 * Classic checkout. Runs after WooCommerce's own field validation: Google tokens are 162 * single-use, so spending one on an order that fails for another reason would make 163 * the customer's corrected retry fail verification too. 164 * 165 * @param array $data Posted checkout data. 166 * @param WP_Error $errors Checkout validation errors. 167 */ 168 public function validate_checkout( $data, $errors ) { 169 if ( ! Helper::is_checkout_protected() || ! $this->should_validate( 'checkout' ) ) { 170 return; 171 } 172 173 if ( $errors->has_errors() || wc_notice_count( 'error' ) > 0 ) { 136 174 return; 137 175 } … … 139 177 $result = $this->validate_captcha( 'checkout' ); 140 178 if ( is_wp_error( $result ) ) { 141 wc_add_notice( $result->get_error_message(), 'error' ); 142 } 143 } 144 145 public function validate_lost_password() { 146 if ( ! SpamOff_Bot_Protection_Helper::is_form_enabled( 'lost_password' ) ) { 147 return; 148 } 149 150 $result = $this->validate_captcha( 'lost_password' ); 151 if ( is_wp_error( $result ) ) { 152 wc_add_notice( $result->get_error_message(), 'error' ); 153 } 179 $errors->add( 'spamoff_bot_protection_error', $result->get_error_message() ); 180 } 181 } 182 183 /** 184 * Both lost-password forms fire core's `lostpassword_post`: WooCommerce's (identified by 185 * its hidden `wc_reset_password` field) and wp-login.php's. Each has its own toggle. 186 * Other callers, such as an admin sending a reset link from the Users screen, have no 187 * captcha and are left alone. The reset only stops when the error is added to `$errors`. 188 * 189 * @param WP_Error $errors Errors that stop the reset email. 190 * @param WP_User|false $user_data User requesting the reset, if found. 191 */ 192 public function validate_lost_password( $errors, $user_data = false ) { 193 if ( ! is_wp_error( $errors ) ) { 194 return; 195 } 196 197 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Presence check only; validate_captcha() verifies the form nonce. 198 if ( isset( $_POST['wc_reset_password'] ) ) { 199 if ( ! Helper::is_form_protected( 'lost_password' ) || ! $this->should_validate( 'lost_password' ) ) { 200 return; 201 } 202 $result = $this->validate_captcha( 'lost_password' ); 203 } elseif ( did_action( 'login_form_lostpassword' ) || did_action( 'login_form_retrievepassword' ) ) { 204 if ( ! Helper::is_form_protected( 'wp_lost_password' ) || ! $this->should_validate( 'wp_lost_password' ) ) { 205 return; 206 } 207 // wp-login.php's form has no nonce; the captcha is the check. 208 $result = self::verify_posted_captcha( 'wp_lost_password' ); 209 } else { 210 return; 211 } 212 213 if ( is_wp_error( $result ) ) { 214 $errors->add( 'spamoff_bot_protection_error', $result->get_error_message() ); 215 } 216 } 217 218 /** 219 * wp-login.php login. `authenticate` also runs for XML-RPC, application passwords, 220 * WooCommerce's login and other plugins calling wp_signon(); none of those carry this 221 * captcha, so only the wp-login.php login action is checked. 222 * 223 * @param WP_User|WP_Error|null $user Result of the earlier authentication checks. 224 * @return WP_User|WP_Error|null 225 */ 226 public function validate_wp_login( $user ) { 227 // wp-login.php also runs authentication when the form is first displayed (GET). 228 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Presence check only; wp-login.php's form has no nonce. 229 if ( ! did_action( 'login_form_login' ) || ! isset( $_POST['log'] ) ) { 230 return $user; 231 } 232 233 if ( ! Helper::is_form_protected( 'wp_login' ) || ! $this->should_validate( 'wp_login' ) ) { 234 return $user; 235 } 236 237 $result = self::verify_posted_captcha( 'wp_login' ); 238 if ( is_wp_error( $result ) ) { 239 return new WP_Error( 'spamoff_bot_protection_error', $result->get_error_message() ); 240 } 241 242 return $user; 243 } 244 245 /** 246 * wp-login.php registration. `registration_errors` also fires when other plugins call 247 * register_new_user(), so only the wp-login.php register action is checked. 248 * 249 * @param WP_Error $errors Registration errors. 250 * @return WP_Error 251 */ 252 public function validate_wp_registration( $errors ) { 253 if ( ! did_action( 'login_form_register' ) || ! is_wp_error( $errors ) ) { 254 return $errors; 255 } 256 257 if ( ! Helper::is_form_protected( 'wp_registration' ) || ! $this->should_validate( 'wp_registration' ) ) { 258 return $errors; 259 } 260 261 // wp-login.php's form has no nonce; the captcha is the check. 262 $result = self::verify_posted_captcha( 'wp_registration' ); 263 if ( is_wp_error( $result ) ) { 264 $errors->add( 'spamoff_bot_protection_error', $result->get_error_message() ); 265 } 266 267 return $errors; 154 268 } 155 269 156 270 public function validate_comment( $post_id ) { 157 if ( ! SpamOff_Bot_Protection_Helper::is_form_enabled( 'comments' ) ) { 158 return; 159 } 160 161 $result = $this->validate_captcha( 'comments' ); 271 if ( ! Helper::is_form_protected( 'comments' ) || ! $this->should_validate( 'comments' ) ) { 272 return; 273 } 274 275 // WordPress core puts no nonce on the comment form for visitors, so the 276 // captcha itself is the check here. 277 $result = self::verify_posted_captcha( 'comments' ); 162 278 if ( is_wp_error( $result ) ) { 163 279 wp_die( 164 280 esc_html( $result->get_error_message() ), 165 281 esc_html__( 'Comment Submission Error', 'spamoff-bot-protection' ), 166 array( 'back_link' => true ) 282 array( 283 'response' => 403, 284 'back_link' => true, 285 ) 167 286 ); 168 287 } … … 170 289 171 290 /** 172 * Verify a captcha submission for a classic (non-Blocks) form. 173 * 174 * Always returns either WP_Error or true — never bare false — so callers 175 * can rely on is_wp_error() to decide whether to block the submission. 291 * Describe the Blocks Checkout's extension data so the Store API validates and 292 * sanitizes it. 293 */ 294 public function register_store_api_data() { 295 if ( ! function_exists( 'woocommerce_store_api_register_endpoint_data' ) || ! class_exists( CheckoutSchema::class ) ) { 296 return; 297 } 298 299 woocommerce_store_api_register_endpoint_data( 300 array( 301 'endpoint' => CheckoutSchema::IDENTIFIER, 302 'namespace' => self::STORE_API_NAMESPACE, 303 'schema_callback' => array( $this, 'get_store_api_schema' ), 304 ) 305 ); 306 } 307 308 public function get_store_api_schema() { 309 $sanitize = function ( $value ) { 310 return is_string( $value ) ? sanitize_text_field( $value ) : ''; 311 }; 312 313 return array( 314 'token' => array( 315 'description' => __( 'reCAPTCHA v3 token for the checkout.', 'spamoff-bot-protection' ), 316 'type' => array( 'string', 'null' ), 317 'context' => array(), 318 'arg_options' => array( 'sanitize_callback' => $sanitize ), 319 ), 320 'action' => array( 321 'description' => __( 'reCAPTCHA v3 action the token was created for.', 'spamoff-bot-protection' ), 322 'type' => array( 'string', 'null' ), 323 'context' => array(), 324 'arg_options' => array( 'sanitize_callback' => $sanitize ), 325 ), 326 ); 327 } 328 329 /** 330 * Validate the captcha for a Store API checkout (the Checkout block, or anything 331 * else posting to /wc/store/v1/checkout). 332 * 333 * The Blocks JS writes the token with setExtensionData('spamoff-bot-protection', { token, action }). 334 * 335 * @param WC_Order $order Order being placed. 336 * @param WP_REST_Request $request Store API request. 337 * @throws RouteException When the captcha is missing or fails. 338 */ 339 public function validate_blocks_checkout( $order, $request ) { 340 // Only when the order is placed. WooCommerce also fires this hook on the PUT sent 341 // while a customer edits a failed order; a token spent there would be rejected as 342 // a duplicate when they place the order. 343 if ( 'POST' !== $request->get_method() ) { 344 return; 345 } 346 347 if ( ! Helper::is_checkout_enabled_for_current_user() || ! $this->should_validate( 'checkout' ) ) { 348 return; 349 } 350 351 if ( ! Helper::has_keys( 'v3' ) ) { 352 /** 353 * Filters whether Store API checkouts are rejected while checkout protection is 354 * on but no reCAPTCHA v3 keys are saved. 355 * 356 * The Store API only supports v3. Rejecting keeps bots from bypassing a v2-protected 357 * classic checkout by posting to the Store API directly. 358 * 359 * @since 1.1.0 360 * 361 * @param bool $reject Default true. 362 */ 363 if ( apply_filters( 'spamoff_bot_protection_store_api_requires_v3_keys', true ) ) { 364 throw new RouteException( 365 'spamoff_bot_protection_not_configured', 366 esc_html( Helper::get_error_message( 'failed' ) ), 367 400 368 ); 369 } 370 return; 371 } 372 373 $extensions = $request->get_param( 'extensions' ); 374 $data = is_array( $extensions ) && isset( $extensions[ self::STORE_API_NAMESPACE ] ) && is_array( $extensions[ self::STORE_API_NAMESPACE ] ) 375 ? $extensions[ self::STORE_API_NAMESPACE ] 376 : array(); 377 378 $token = isset( $data['token'] ) && is_string( $data['token'] ) ? sanitize_text_field( $data['token'] ) : ''; 379 380 if ( '' === $token ) { 381 throw new RouteException( 382 'spamoff_bot_protection_missing', 383 esc_html( Helper::get_error_message( 'missing' ) ), 384 400 385 ); 386 } 387 388 $result = Helper::verify_recaptcha( $token, 'v3', 'checkout' ); 389 390 if ( empty( $result['success'] ) ) { 391 $error_type = isset( $result['error'] ) ? $result['error'] : 'failed'; 392 throw new RouteException( 393 'spamoff_bot_protection_failed', 394 esc_html( Helper::get_error_message( $error_type ) ), 395 400 396 ); 397 } 398 } 399 400 /** 401 * Check the WooCommerce form's nonce, then the posted captcha. 402 * 403 * WooCommerce has already verified the nonce before these hooks fire; checking it 404 * again fails closed if another plugin calls the hooks outside the real form handler. 176 405 */ 177 406 private function validate_captcha( $action ) { 178 $nonce_error = new WP_Error( 'spamoff_bot_protection_nonce_invalid', SpamOff_Bot_Protection_Helper::get_error_message( 'failed' ) ); 179 180 // Re-verify the upstream form nonce. Failing closed (WP_Error) prevents 181 // a bypass of WC/WP's own nonce check from silently letting us through. 182 switch ( $action ) { 183 case 'checkout': 184 if ( ! isset( $_POST['woocommerce-process-checkout-nonce'] ) ) { 185 return $nonce_error; 186 } 187 $nonce = sanitize_text_field( wp_unslash( $_POST['woocommerce-process-checkout-nonce'] ) ); 188 if ( ! wp_verify_nonce( $nonce, 'woocommerce-process_checkout' ) ) { 189 return $nonce_error; 190 } 191 break; 192 193 case 'login': 194 if ( ! isset( $_POST['woocommerce-login-nonce'] ) ) { 195 return $nonce_error; 196 } 197 $nonce = sanitize_text_field( wp_unslash( $_POST['woocommerce-login-nonce'] ) ); 198 if ( ! wp_verify_nonce( $nonce, 'woocommerce-login' ) ) { 199 return $nonce_error; 200 } 201 break; 202 203 case 'registration': 204 if ( ! isset( $_POST['woocommerce-register-nonce'] ) ) { 205 return $nonce_error; 206 } 207 $nonce = sanitize_text_field( wp_unslash( $_POST['woocommerce-register-nonce'] ) ); 208 if ( ! wp_verify_nonce( $nonce, 'woocommerce-register' ) ) { 209 return $nonce_error; 210 } 211 break; 212 213 case 'lost_password': 214 if ( ! isset( $_POST['woocommerce-lost-password-nonce'] ) ) { 215 return $nonce_error; 216 } 217 $nonce = sanitize_text_field( wp_unslash( $_POST['woocommerce-lost-password-nonce'] ) ); 218 if ( ! wp_verify_nonce( $nonce, 'lost_password' ) ) { 219 return $nonce_error; 220 } 221 break; 222 223 case 'comments': 224 if ( isset( $_POST['_wp_unfiltered_html_comment'], $_POST['comment_post_ID'] ) ) { 225 $post_id = absint( $_POST['comment_post_ID'] ); 226 $nonce = sanitize_text_field( wp_unslash( $_POST['_wp_unfiltered_html_comment'] ) ); 227 if ( ! wp_verify_nonce( $nonce, 'unfiltered-html-comment_' . $post_id ) ) { 228 return $nonce_error; 229 } 230 } elseif ( isset( $_POST['_wpnonce'], $_POST['comment_post_ID'] ) ) { 231 $post_id = absint( $_POST['comment_post_ID'] ); 232 $nonce = sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ); 233 if ( ! wp_verify_nonce( $nonce, 'comment-post_' . $post_id ) ) { 234 return $nonce_error; 235 } 236 } else { 237 return $nonce_error; 238 } 239 break; 240 241 default: 242 return $nonce_error; 243 } 244 245 $recaptcha_type = SpamOff_Bot_Protection_Helper::get_option( 'spamoff_bot_protection_type', 'v3' ); 246 247 if ( 'v3' === $recaptcha_type ) { 248 $token = isset( $_POST['spamoff_bot_protection_token'] ) 249 ? sanitize_text_field( wp_unslash( $_POST['spamoff_bot_protection_token'] ) ) 250 : ''; 251 $action_param = isset( $_POST['spamoff_bot_protection_action'] ) 252 ? sanitize_text_field( wp_unslash( $_POST['spamoff_bot_protection_action'] ) ) 253 : $action; 407 if ( ! $this->verify_form_nonce( $action ) ) { 408 return new WP_Error( 'spamoff_bot_protection_nonce_invalid', Helper::get_error_message( 'failed' ) ); 409 } 410 411 return self::verify_posted_captcha( $action ); 412 } 413 414 private function verify_form_nonce( $action ) { 415 $nonces = array( 416 'checkout' => array( 'woocommerce-process-checkout-nonce', 'woocommerce-process_checkout' ), 417 'login' => array( 'woocommerce-login-nonce', 'woocommerce-login' ), 418 'registration' => array( 'woocommerce-register-nonce', 'woocommerce-register' ), 419 'lost_password' => array( 'woocommerce-lost-password-nonce', 'lost_password' ), 420 ); 421 422 if ( ! isset( $nonces[ $action ] ) ) { 423 return false; 424 } 425 426 list( $field, $nonce_action ) = $nonces[ $action ]; 427 428 // WooCommerce templates older than 3.3 used the generic `_wpnonce` field. 429 if ( isset( $_POST[ $field ] ) ) { 430 $nonce = sanitize_text_field( wp_unslash( $_POST[ $field ] ) ); 431 } elseif ( isset( $_POST['_wpnonce'] ) ) { 432 $nonce = sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ); 254 433 } else { 255 $token = isset( $_POST['g-recaptcha-response'] ) 256 ? sanitize_text_field( wp_unslash( $_POST['g-recaptcha-response'] ) ) 257 : ''; 258 $action_param = ''; 259 } 260 261 if ( empty( $token ) ) { 262 return new WP_Error( 'spamoff_bot_protection_missing', SpamOff_Bot_Protection_Helper::get_error_message( 'missing' ) ); 263 } 264 265 $result = SpamOff_Bot_Protection_Helper::verify_recaptcha( $token, $action_param ); 266 267 if ( empty( $result['success'] ) || true !== $result['success'] ) { 434 return false; 435 } 436 437 return false !== wp_verify_nonce( $nonce, $nonce_action ); 438 } 439 440 /** 441 * Verify the captcha response posted with the current request. 442 * 443 * Callers must check the form's own nonce first, where the form has one. 444 * 445 * @param string $action Form action; for v3 the token must have been created for it. 446 * @return true|WP_Error 447 */ 448 public static function verify_posted_captcha( $action ) { 449 $type = Helper::get_type(); 450 $action = Helper::sanitize_action( $action ); 451 452 // phpcs:disable WordPress.Security.NonceVerification.Missing -- Callers verify the form nonce; core's comment form has none. 453 $field = 'v3' === $type ? 'spamoff_bot_protection_token' : 'g-recaptcha-response'; 454 $token = isset( $_POST[ $field ] ) && is_string( $_POST[ $field ] ) ? sanitize_text_field( wp_unslash( $_POST[ $field ] ) ) : ''; 455 // phpcs:enable WordPress.Security.NonceVerification.Missing 456 457 if ( '' === $token ) { 458 return new WP_Error( 'spamoff_bot_protection_missing', Helper::get_error_message( 'missing' ) ); 459 } 460 461 $result = Helper::verify_recaptcha( $token, $type, 'v3' === $type ? $action : '' ); 462 463 if ( empty( $result['success'] ) ) { 268 464 $error_type = isset( $result['error'] ) ? $result['error'] : 'failed'; 269 return new WP_Error( 'spamoff_bot_protection_failed', SpamOff_Bot_Protection_Helper::get_error_message( $error_type ) );465 return new WP_Error( 'spamoff_bot_protection_failed', Helper::get_error_message( $error_type ) ); 270 466 } 271 467 … … 274 470 275 471 /** 472 * Lets developers skip validation in special cases, such as an express-payment 473 * button that places orders without the checkout form. 474 */ 475 private function should_validate( $form ) { 476 /** 477 * Filters whether the captcha is checked for a form submission. 478 * 479 * @since 1.1.0 480 * 481 * @param bool $should_validate Default true. 482 * @param string $form wp_login, wp_registration, wp_lost_password, comments, login, 483 * registration, lost_password or checkout. 484 */ 485 return (bool) apply_filters( 'spamoff_bot_protection_should_validate', true, $form ); 486 } 487 488 /** 276 489 * Render the captcha markup for a given action. 277 490 * 278 * For v3 this is hidden inputs that the JS populates with a token.279 * For v2 it's the standard `g-recaptcha` div that Google's script auto-renders.491 * For v3 this is a hidden input that the JS fills with a token on submit. 492 * For v2 it's a `g-recaptcha` div that the JS renders explicitly. 280 493 */ 281 494 public function render_captcha( $action = 'general', $id_suffix = '' ) { 282 $recaptcha_type = SpamOff_Bot_Protection_Helper::get_option( 'spamoff_bot_protection_type', 'v3' ); 283 $site_key = SpamOff_Bot_Protection_Helper::get_option( 'spamoff_bot_protection_site_key' ); 284 $customization = SpamOff_Bot_Protection_Helper::get_option( 'spamoff_bot_protection_customization', array() ); 285 286 if ( empty( $site_key ) ) { 287 return; 288 } 289 290 $render_key = $action . ( $id_suffix ? '_' . $id_suffix : '' ); 291 if ( isset( self::$rendered_actions[ $render_key ] ) ) { 292 return; 293 } 294 self::$rendered_actions[ $render_key ] = true; 295 296 $id = $action . ( $id_suffix ? '_' . $id_suffix : '' ); 297 298 if ( 'v3' === $recaptcha_type ) { 495 $type = Helper::get_type(); 496 $site_key = Helper::get_site_key( $type ); 497 498 if ( '' === $site_key ) { 499 return; 500 } 501 502 // Several forms of one kind can share a page (e.g. a theme's header login box on 503 // the My Account page), so every call renders a captcha with its own ID. If a hook 504 // fires twice inside one form, the handler keeps only the first captcha. 505 $action = Helper::sanitize_action( $action ); 506 $base = $action . ( $id_suffix ? '_' . $id_suffix : '' ); 507 508 self::$render_counts[ $base ] = isset( self::$render_counts[ $base ] ) ? self::$render_counts[ $base ] + 1 : 1; 509 510 $count = self::$render_counts[ $base ]; 511 $id = sanitize_html_class( 1 === $count ? $base : $base . '_' . $count ); 512 513 SpamOff_Bot_Protection_Captcha::require_type( $type ); 514 515 if ( 'v3' === $type ) { 299 516 echo '<div class="spamoff-bot-protection-container spamoff-bot-protection-v3" data-action="' . esc_attr( $action ) . '">'; 300 517 echo '<input type="hidden" name="spamoff_bot_protection_token" id="spamoff_bot_protection_token_' . esc_attr( $id ) . '" value="">'; 301 echo '<input type="hidden" name="spamoff_bot_protection_action" value="' . esc_attr( $action ) . '">'; 518 if ( $this->is_badge_hidden() ) { 519 $this->render_google_notice(); 520 } 302 521 echo '</div>'; 303 } elseif ( 'v2' === $recaptcha_type ) { 304 $theme = isset( $customization['v2_theme'] ) ? $customization['v2_theme'] : 'light'; 305 $size = isset( $customization['v2_size'] ) ? $customization['v2_size'] : 'normal'; 306 307 echo '<div class="spamoff-bot-protection-container spamoff-bot-protection-v2" data-action="' . esc_attr( $action ) . '" style="display: block !important; visibility: visible !important;">'; 308 echo '<div class="g-recaptcha" data-sitekey="' . esc_attr( $site_key ) . '" data-theme="' . esc_attr( $theme ) . '" data-size="' . esc_attr( $size ) . '" data-spamoff-bot-protection-id="' . esc_attr( $id ) . '" style="display: inline-block !important;"></div>'; 309 echo '</div>'; 310 } 522 return; 523 } 524 525 $customization = Helper::get_option( 'spamoff_bot_protection_customization', array() ); 526 $theme = is_array( $customization ) && isset( $customization['v2_theme'] ) && 'dark' === $customization['v2_theme'] ? 'dark' : 'light'; 527 $size = is_array( $customization ) && isset( $customization['v2_size'] ) && 'compact' === $customization['v2_size'] ? 'compact' : 'normal'; 528 529 echo '<div class="spamoff-bot-protection-container spamoff-bot-protection-v2" data-action="' . esc_attr( $action ) . '">'; 530 echo '<div class="g-recaptcha" id="spamoff_bot_protection_widget_' . esc_attr( $id ) . '" data-sitekey="' . esc_attr( $site_key ) . '" data-theme="' . esc_attr( $theme ) . '" data-size="' . esc_attr( $size ) . '"></div>'; 531 echo '</div>'; 532 } 533 534 private function is_badge_hidden() { 535 $customization = Helper::get_option( 'spamoff_bot_protection_customization', array() ); 536 return is_array( $customization ) && isset( $customization['hide_v3_badge'] ) && 'yes' === $customization['hide_v3_badge']; 537 } 538 539 /** 540 * Google allows hiding the v3 badge only if this notice appears in the user flow. 541 */ 542 private function render_google_notice() { 543 echo '<p class="spamoff-bot-protection-notice">'; 544 echo wp_kses( 545 sprintf( 546 /* translators: 1: Google Privacy Policy URL, 2: Google Terms of Service URL. */ 547 __( 'This site is protected by reCAPTCHA and the Google <a href="%1$s" target="_blank" rel="noopener noreferrer">Privacy Policy</a> and <a href="%2$s" target="_blank" rel="noopener noreferrer">Terms of Service</a> apply.', 'spamoff-bot-protection' ), 548 'https://policies.google.com/privacy', 549 'https://policies.google.com/terms' 550 ), 551 array( 552 'a' => array( 553 'href' => array(), 554 'target' => array(), 555 'rel' => array(), 556 ), 557 ) 558 ); 559 echo '</p>'; 311 560 } 312 561 313 562 public function shortcode( $atts ) { 314 $atts = shortcode_atts( array( 'action' => 'general' ), $atts, 'spamoff_bot_protection' ); 315 $action = $atts['action']; 316 317 // SpamOff_Bot_Protection_Captcha::enqueue_scripts has already registered the real handles 318 // on wp_enqueue_scripts; nudge them in case the shortcode is the only consumer. 319 wp_enqueue_script( 'spamoff-bot-protection-api' ); 320 wp_enqueue_script( 'spamoff-bot-protection-handler' ); 321 wp_enqueue_style( 'spamoff-bot-protection-style' ); 563 $atts = shortcode_atts( array( 'action' => 'general' ), $atts, 'spamoff_bot_protection' ); 564 565 if ( ! Helper::has_keys() ) { 566 return ''; 567 } 322 568 323 569 ob_start(); 324 $this->render_captcha( $a ction);570 $this->render_captcha( $atts['action'] ); 325 571 return ob_get_clean(); 326 572 } -
spamoff-bot-protection/tags/1.0.0/includes/class-spamoff-bot-protection-helper.php
r3603369 r3714007 6 6 class SpamOff_Bot_Protection_Helper { 7 7 8 /** Supported reCAPTCHA versions. Each keeps its own key pair. */ 9 const TYPES = array( 'v3', 'v2' ); 10 11 /** Forms that can be toggled under Advanced Settings. The wp_* keys are WordPress's own wp-login.php forms. */ 12 const FORMS = array( 'wp_login', 'wp_registration', 'wp_lost_password', 'comments', 'login', 'registration', 'lost_password', 'checkout', 'guest_checkout' ); 13 8 14 public static function get_option( $key, $default = '' ) { 9 15 return get_option( $key, $default ); 16 } 17 18 /** 19 * The reCAPTCHA version selected in General Settings. 20 */ 21 public static function get_type() { 22 $type = self::get_option( 'spamoff_bot_protection_type', 'v3' ); 23 return in_array( $type, self::TYPES, true ) ? $type : 'v3'; 24 } 25 26 public static function get_site_key( $type = null ) { 27 $type = self::normalize_type( $type ); 28 return trim( (string) self::get_option( 'spamoff_bot_protection_' . $type . '_site_key', '' ) ); 29 } 30 31 public static function get_secret_key( $type = null ) { 32 $type = self::normalize_type( $type ); 33 return trim( (string) self::get_option( 'spamoff_bot_protection_' . $type . '_secret_key', '' ) ); 34 } 35 36 /** 37 * Both keys for a version are saved. Without them the captcha can't render or 38 * verify, so forms are left unprotected rather than locking every visitor out. 39 */ 40 public static function has_keys( $type = null ) { 41 return '' !== self::get_site_key( $type ) && '' !== self::get_secret_key( $type ); 10 42 } 11 43 … … 16 48 17 49 /** 18 * Check if reCAPTCHA is enabledfor a given form key (login, checkout, …).50 * Check if reCAPTCHA is switched on for a given form key (login, checkout, …). 19 51 */ 20 52 public static function is_form_enabled( $form ) { … … 24 56 25 57 /** 58 * Switched on and usable with the selected version's keys. 59 */ 60 public static function is_form_protected( $form ) { 61 return self::is_form_enabled( $form ) && self::has_keys(); 62 } 63 64 /** 65 * Whether the current checkout needs a captcha, before looking at keys. 66 * 67 * "Checkout" covers every customer; "Guest Checkout" covers only visitors who 68 * aren't logged in. 69 */ 70 public static function is_checkout_enabled_for_current_user() { 71 if ( self::is_form_enabled( 'checkout' ) ) { 72 return true; 73 } 74 return self::is_form_enabled( 'guest_checkout' ) && ! is_user_logged_in(); 75 } 76 77 /** 78 * Classic checkout protection, using the selected version. 79 */ 80 public static function is_checkout_protected() { 81 return self::is_checkout_enabled_for_current_user() && self::has_keys(); 82 } 83 84 /** 26 85 * Verify a reCAPTCHA token with Google's siteverify endpoint. 27 */ 28 public static function verify_recaptcha( $token, $action = '' ) { 29 $secret_key = self::get_option( 'spamoff_bot_protection_secret_key' ); 30 31 if ( empty( $secret_key ) || empty( $token ) ) { 86 * 87 * @param string $token Token (v3) or g-recaptcha-response (v2) from the client. 88 * @param string|null $type 'v3' or 'v2'; defaults to the selected version. 89 * @param string $expected_action v3 action the token must have been created for. Empty skips the check. 90 * @return array Google's response with `success` => true, or `success` => false and an `error` code. 91 */ 92 public static function verify_recaptcha( $token, $type = null, $expected_action = '' ) { 93 $type = self::normalize_type( $type ); 94 $secret_key = self::get_secret_key( $type ); 95 96 if ( '' === $secret_key ) { 32 97 return array( 33 98 'success' => false, 34 99 'error' => 'missing_credentials', 35 100 ); 101 } 102 103 if ( empty( $token ) ) { 104 return array( 105 'success' => false, 106 'error' => 'missing', 107 ); 108 } 109 110 $body = array( 111 'secret' => $secret_key, 112 'response' => $token, 113 ); 114 115 $remote_ip = self::get_user_ip(); 116 if ( '' !== $remote_ip ) { 117 $body['remoteip'] = $remote_ip; 36 118 } 37 119 … … 39 121 'https://www.google.com/recaptcha/api/siteverify', 40 122 array( 41 'body' => array( 42 'secret' => $secret_key, 43 'response' => $token, 44 'remoteip' => self::get_user_ip(), 45 ), 123 'body' => $body, 46 124 'timeout' => 10, 47 125 ) … … 56 134 } 57 135 58 $body = wp_remote_retrieve_body( $response ); 59 $data = json_decode( $body, true ); 136 $data = json_decode( wp_remote_retrieve_body( $response ), true ); 60 137 61 138 if ( ! is_array( $data ) ) { … … 67 144 68 145 if ( ! isset( $data['success'] ) || true !== $data['success'] ) { 69 $error_codes = isset( $data['error-codes'] ) ? $data['error-codes'] : array( 'unknown_error' );70 146 return array( 71 147 'success' => false, 72 148 'error' => 'verification_failed', 73 'error_codes' => $error_codes, 74 ); 75 } 76 77 // For v3, enforce the configured minimum score. 78 $recaptcha_type = self::get_option( 'spamoff_bot_protection_type', 'v3' ); 79 if ( 'v3' === $recaptcha_type ) { 149 'error_codes' => isset( $data['error-codes'] ) ? (array) $data['error-codes'] : array( 'unknown_error' ), 150 ); 151 } 152 153 // The token must have been issued for this site, not harvested from another one. 154 if ( ! empty( $data['hostname'] ) && ! in_array( strtolower( $data['hostname'] ), self::get_allowed_hostnames(), true ) ) { 155 return array( 156 'success' => false, 157 'error' => 'hostname_mismatch', 158 'hostname' => $data['hostname'], 159 ); 160 } 161 162 if ( 'v3' === $type ) { 163 // A token minted for one form (e.g. comments) must not unlock another (e.g. checkout). 164 if ( '' !== $expected_action && isset( $data['action'] ) && $data['action'] !== $expected_action ) { 165 return array( 166 'success' => false, 167 'error' => 'action_mismatch', 168 'action' => $data['action'], 169 ); 170 } 171 80 172 $score = isset( $data['score'] ) ? floatval( $data['score'] ) : 0.0; 81 173 $minimum_score = self::sanitize_score( self::get_option( 'spamoff_bot_protection_minimum_score', '0.5' ) ); … … 97 189 98 190 /** 99 * Resolve the visitor's IP, preferring proxy headers when present. 191 * Hostnames Google may report for a valid token on this site. 192 */ 193 public static function get_allowed_hostnames() { 194 $hosts = array(); 195 foreach ( array( home_url(), site_url() ) as $url ) { 196 $host = wp_parse_url( $url, PHP_URL_HOST ); 197 if ( $host ) { 198 $host = strtolower( $host ); 199 $hosts[] = $host; 200 $hosts[] = 0 === strpos( $host, 'www.' ) ? substr( $host, 4 ) : 'www.' . $host; 201 } 202 } 203 204 // Google's published test keys report this hostname. 205 $hosts[] = 'testkey.google.com'; 206 207 /** 208 * Filters the hostnames accepted in Google's verification response. 209 * 210 * Add every domain the protected forms are served from, e.g. for multi-domain setups. 211 * 212 * @since 1.1.0 213 * 214 * @param string[] $hosts Lowercase hostnames. 215 */ 216 $hosts = apply_filters( 'spamoff_bot_protection_allowed_hostnames', $hosts ); 217 218 return array_values( array_unique( array_map( 'strtolower', (array) $hosts ) ) ); 219 } 220 221 /** 222 * The visitor's IP address, sent to Google as an optional hint. 223 * 224 * Only REMOTE_ADDR is trusted: proxy headers can be forged by the client. Sites 225 * behind a trusted proxy or CDN can supply the real address through the filter. 100 226 */ 101 227 public static function get_user_ip() { 102 $ip_keys = array( 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR' ); 103 104 foreach ( $ip_keys as $key ) { 105 if ( isset( $_SERVER[ $key ] ) ) { 106 foreach ( explode( ',', sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) ) ) as $ip ) { 107 $ip = trim( $ip ); 108 if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) !== false ) { 109 return $ip; 110 } 111 } 112 } 113 } 114 115 return isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; 228 $ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : ''; 229 230 /** 231 * Filters the visitor IP address sent to Google's siteverify endpoint. 232 * 233 * @since 1.1.0 234 * 235 * @param string $ip IP address from REMOTE_ADDR. 236 */ 237 $ip = (string) apply_filters( 'spamoff_bot_protection_remote_ip', $ip ); 238 239 return false !== filter_var( $ip, FILTER_VALIDATE_IP ) ? $ip : ''; 116 240 } 117 241 118 242 /** 119 243 * Get the error message shown to the user for a given failure type. 244 * 245 * The customizable message covers failed checks; a missing captcha keeps its own 246 * wording so v2 users know they still need to tick the box. 120 247 */ 121 248 public static function get_error_message( $error_type = 'failed' ) { 249 if ( 'missing' === $error_type ) { 250 return __( 'Please complete the verification.', 'spamoff-bot-protection' ); 251 } 252 122 253 $customization = self::get_option( 'spamoff_bot_protection_customization', array() ); 123 254 124 if ( is set( $customization['error_message_failed'] ) && '' !== $customization['error_message_failed'] ) {255 if ( is_array( $customization ) && isset( $customization['error_message_failed'] ) && '' !== $customization['error_message_failed'] ) { 125 256 return $customization['error_message_failed']; 126 257 } 127 258 128 $messages = array( 129 'failed' => __( 'Verification failed. Please try again.', 'spamoff-bot-protection' ), 130 'missing' => __( 'Please complete the verification.', 'spamoff-bot-protection' ), 131 'low_score' => __( 'Verification failed. Please try again.', 'spamoff-bot-protection' ), 132 ); 133 134 return isset( $messages[ $error_type ] ) ? $messages[ $error_type ] : $messages['failed']; 135 } 136 137 /** 138 * Check if the current request is rendering the WooCommerce Blocks Checkout. 139 */ 140 public static function is_blocks_checkout() { 141 if ( ! function_exists( 'has_block' ) ) { 259 return __( 'Verification failed. Please try again.', 'spamoff-bot-protection' ); 260 } 261 262 /** 263 * Whether the page configured as the WooCommerce checkout uses the Checkout block. 264 * 265 * Used for admin warnings only; the front end detects the block as it renders. 266 */ 267 public static function checkout_page_uses_blocks() { 268 if ( ! function_exists( 'has_block' ) || ! function_exists( 'wc_get_page_id' ) ) { 142 269 return false; 143 270 } 144 // Check the configured checkout page directly — more reliable than the global 145 // $post during wp_enqueue_scripts, where some caching plugins may not set it. 146 $checkout_page_id = function_exists( 'wc_get_page_id' ) ? wc_get_page_id( 'checkout' ) : 0; 147 if ( $checkout_page_id > 0 ) { 148 return has_block( 'woocommerce/checkout', $checkout_page_id ); 149 } 150 return has_block( 'woocommerce/checkout' ); 271 $checkout_page_id = wc_get_page_id( 'checkout' ); 272 return $checkout_page_id > 0 && has_block( 'woocommerce/checkout', $checkout_page_id ); 273 } 274 275 /** 276 * Google action names allow only letters, numbers, slashes and underscores. 277 */ 278 public static function sanitize_action( $action ) { 279 $action = preg_replace( '/[^A-Za-z0-9_\/]/', '_', (string) $action ); 280 return '' === $action ? 'general' : $action; 281 } 282 283 private static function normalize_type( $type ) { 284 return in_array( $type, self::TYPES, true ) ? $type : self::get_type(); 151 285 } 152 286 } 287 288 if ( ! function_exists( 'spamoff_bot_protection_verify_submission' ) ) { 289 /** 290 * Verify the captcha posted by a custom form that uses the [spamoff_bot_protection] shortcode. 291 * 292 * Call it from the form's own submission handler, after checking the form's nonce. 293 * 294 * @since 1.1.0 295 * 296 * @param string $action The `action` attribute given to the shortcode (default "general"). 297 * @return true|WP_Error True when verified, WP_Error with a user-facing message otherwise. 298 */ 299 function spamoff_bot_protection_verify_submission( $action = 'general' ) { 300 return SpamOff_Bot_Protection_Frontend::verify_posted_captcha( $action ); 301 } 302 } -
spamoff-bot-protection/tags/1.0.0/includes/class-spamoff-bot-protection-settings.php
r3603369 r3714007 4 4 } 5 5 6 use SpamOff_Bot_Protection_Helper as Helper; 7 6 8 class SpamOff_Bot_Protection_Settings { 7 9 … … 32 34 $this->output_settings_tabs(); 33 35 echo '</div>'; 36 37 $this->output_configuration_notices(); 34 38 35 39 echo '<div class="spamoff-bot-protection-settings-content">'; … … 61 65 } 62 66 67 /** 68 * Warn about settings that leave forms unprotected or would reject orders. 69 */ 70 private function output_configuration_notices() { 71 $notices = array(); 72 $type = Helper::get_type(); 73 $forms = get_option( 'spamoff_bot_protection_forms', array() ); 74 $any_on = is_array( $forms ) && in_array( 'yes', $forms, true ); 75 76 if ( $any_on && ! Helper::has_keys( $type ) ) { 77 $notices[] = sprintf( 78 /* translators: %s: reCAPTCHA version, e.g. v3. */ 79 __( 'Protection is off: add the reCAPTCHA %s Site Key and Secret Key under General Settings.', 'spamoff-bot-protection' ), 80 $type 81 ); 82 } 83 84 $checkout_on = Helper::is_form_enabled( 'checkout' ) || Helper::is_form_enabled( 'guest_checkout' ); 85 if ( $checkout_on && ! Helper::has_keys( 'v3' ) && Helper::checkout_page_uses_blocks() ) { 86 $notices[] = __( 'Your checkout page uses the Checkout block, which only supports reCAPTCHA v3. Orders placed there are rejected until you save v3 keys. Select v3 under General Settings to enter them; they stay saved if you switch back to v2.', 'spamoff-bot-protection' ); 87 } 88 89 foreach ( $notices as $notice ) { 90 echo '<div class="notice notice-warning inline"><p>' . esc_html( $notice ) . '</p></div>'; 91 } 92 } 93 63 94 public function output_settings_content( $section ) { 64 95 switch ( $section ) { … … 96 127 } 97 128 129 /** 130 * Each version keeps its own key pair, so switching between v3 and v2 never 131 * loses the other version's keys. The admin script shows the selected version's rows. 132 */ 98 133 public function get_general_settings() { 99 134 return array( … … 101 136 'name' => __( 'General Settings', 'spamoff-bot-protection' ), 102 137 'type' => 'title', 103 'desc' => __( 'Configure your basic reCAPTCHA settings and API keys.', 'spamoff-bot-protection' ), 138 'desc' => __( 'Configure your basic reCAPTCHA settings and API keys.', 'spamoff-bot-protection' ) . ' ' 139 . __( 'Protected forms send visitor data to Google reCAPTCHA; suggested privacy-policy text is under Settings → Privacy → Policy Guide.', 'spamoff-bot-protection' ), 104 140 'id' => 'spamoff_bot_protection_general_title', 105 141 ), 106 142 array( 107 143 'name' => __( 'reCAPTCHA Type', 'spamoff-bot-protection' ), 108 'desc' => __( 'Choose between reCAPTCHA v3 (invisible) and v2 (checkbox). ', 'spamoff-bot-protection' ),144 'desc' => __( 'Choose between reCAPTCHA v3 (invisible) and v2 (checkbox). Google issues separate keys for each; both sets stay saved when you switch.', 'spamoff-bot-protection' ), 109 145 'id' => 'spamoff_bot_protection_type', 110 146 'type' => 'select', … … 116 152 ), 117 153 array( 118 'name' => __( 'Site Key', 'spamoff-bot-protection' ), 119 'desc' => __( 'Enter your Google reCAPTCHA site key.', 'spamoff-bot-protection' ), 120 'id' => 'spamoff_bot_protection_site_key', 121 'type' => 'text', 122 'css' => 'min-width: 400px;', 123 ), 124 array( 125 'name' => __( 'Secret Key', 'spamoff-bot-protection' ), 126 'desc' => __( 'Enter your Google reCAPTCHA secret key.', 'spamoff-bot-protection' ), 127 'id' => 'spamoff_bot_protection_secret_key', 128 'type' => 'password', 129 'css' => 'min-width: 400px;', 130 ), 131 array( 132 'name' => __( 'Minimum Score for reCAPTCHA v3', 'spamoff-bot-protection' ), 133 'desc' => __( 'Minimum score required (0.0 - 1.0). Submissions below this score will be rejected.', 'spamoff-bot-protection' ), 134 'id' => 'spamoff_bot_protection_minimum_score', 135 'type' => 'number', 136 'default' => '0.5', 137 'step' => '0.1', 138 'min' => '0.0', 139 'max' => '1.0', 140 'css' => 'width: 80px;', 154 'name' => __( 'v3 Site Key', 'spamoff-bot-protection' ), 155 'desc' => __( 'Enter your Google reCAPTCHA v3 site key. The Checkout block always uses the v3 keys.', 'spamoff-bot-protection' ), 156 'id' => 'spamoff_bot_protection_v3_site_key', 157 'type' => 'text', 158 'css' => 'min-width: 400px;', 159 'row_class' => 'spamoff-bot-protection-v3', 160 ), 161 array( 162 'name' => __( 'v3 Secret Key', 'spamoff-bot-protection' ), 163 'desc' => __( 'Enter your Google reCAPTCHA v3 secret key.', 'spamoff-bot-protection' ), 164 'id' => 'spamoff_bot_protection_v3_secret_key', 165 'type' => 'password', 166 'css' => 'min-width: 400px;', 167 'row_class' => 'spamoff-bot-protection-v3', 168 'custom_attributes' => array( 'autocomplete' => 'new-password' ), 169 ), 170 array( 171 'name' => __( 'Minimum Score for reCAPTCHA v3', 'spamoff-bot-protection' ), 172 'desc' => __( 'Minimum score required (0.0 - 1.0). Submissions below this score will be rejected.', 'spamoff-bot-protection' ), 173 'id' => 'spamoff_bot_protection_minimum_score', 174 'type' => 'number', 175 'default' => '0.5', 176 'css' => 'width: 80px;', 177 'row_class' => 'spamoff-bot-protection-v3', 178 'custom_attributes' => array( 179 'step' => '0.1', 180 'min' => '0', 181 'max' => '1', 182 ), 183 ), 184 array( 185 'name' => __( 'v2 Site Key', 'spamoff-bot-protection' ), 186 'desc' => __( 'Enter your Google reCAPTCHA v2 ("I\'m not a robot" Checkbox) site key.', 'spamoff-bot-protection' ), 187 'id' => 'spamoff_bot_protection_v2_site_key', 188 'type' => 'text', 189 'css' => 'min-width: 400px;', 190 'row_class' => 'spamoff-bot-protection-v2', 191 ), 192 array( 193 'name' => __( 'v2 Secret Key', 'spamoff-bot-protection' ), 194 'desc' => __( 'Enter your Google reCAPTCHA v2 secret key.', 'spamoff-bot-protection' ), 195 'id' => 'spamoff_bot_protection_v2_secret_key', 196 'type' => 'password', 197 'css' => 'min-width: 400px;', 198 'row_class' => 'spamoff-bot-protection-v2', 199 'custom_attributes' => array( 'autocomplete' => 'new-password' ), 141 200 ), 142 201 array( … … 185 244 } 186 245 246 /** 247 * Labels for the Advanced Settings toggles, keyed like Helper::FORMS. 248 */ 249 private function get_form_labels() { 250 return array( 251 'wp_login' => __( 'WordPress Login (wp-login.php)', 'spamoff-bot-protection' ), 252 'wp_registration' => __( 'WordPress Registration (wp-login.php)', 'spamoff-bot-protection' ), 253 'wp_lost_password' => __( 'WordPress Lost Password (wp-login.php)', 'spamoff-bot-protection' ), 254 'comments' => __( 'WordPress Comments', 'spamoff-bot-protection' ), 255 'login' => __( 'WooCommerce Login (My Account and checkout)', 'spamoff-bot-protection' ), 256 'registration' => __( 'WooCommerce Registration (My Account)', 'spamoff-bot-protection' ), 257 'lost_password' => __( 'WooCommerce Lost Password (My Account)', 'spamoff-bot-protection' ), 258 'checkout' => __( 'WooCommerce Checkout: all customers', 'spamoff-bot-protection' ), 259 'guest_checkout' => __( 'WooCommerce Checkout: guests only', 'spamoff-bot-protection' ), 260 ); 261 } 262 187 263 public function output_forms_table() { 188 264 $forms = get_option( 'spamoff_bot_protection_forms', array() ); … … 190 266 $forms = array(); 191 267 } 192 193 $form_list = array(194 'login' => __( 'Customer Login', 'spamoff-bot-protection' ),195 'registration' => __( 'Customer Registration', 'spamoff-bot-protection' ),196 'checkout' => __( 'Checkout Page', 'spamoff-bot-protection' ),197 'guest_checkout' => __( 'Guest Checkout', 'spamoff-bot-protection' ),198 'lost_password' => __( 'Lost Password', 'spamoff-bot-protection' ),199 'comments' => __( 'WordPress Comments', 'spamoff-bot-protection' ),200 );201 268 ?> 202 <tr valign="top">269 <tr> 203 270 <th scope="row" class="titledesc"><?php esc_html_e( 'Protected Forms', 'spamoff-bot-protection' ); ?></th> 204 271 <td class="forminp"> … … 211 278 </thead> 212 279 <tbody> 213 <?php foreach ( $form_list as $key => $label ) : 214 $checked = ( isset( $forms[ $key ] ) && 'yes' === $forms[ $key ] ) ? 'checked' : ''; 215 ?> 280 <?php foreach ( $this->get_form_labels() as $key => $label ) : ?> 216 281 <tr> 217 <td style="padding: 10px;">< ?php echo esc_html( $label ); ?></td>282 <td style="padding: 10px;"><label for="spamoff_bot_protection_forms_<?php echo esc_attr( $key ); ?>"><?php echo esc_html( $label ); ?></label></td> 218 283 <td style="padding: 10px;"> 219 <input type="checkbox" name="spamoff_bot_protection_forms[<?php echo esc_attr( $key ); ?>]" value="yes" <?php echo esc_attr( $checked); ?>>284 <input type="checkbox" id="spamoff_bot_protection_forms_<?php echo esc_attr( $key ); ?>" name="spamoff_bot_protection_forms[<?php echo esc_attr( $key ); ?>]" value="yes" <?php checked( isset( $forms[ $key ] ) && 'yes' === $forms[ $key ] ); ?>> 220 285 </td> 221 286 </tr> … … 223 288 </tbody> 224 289 </table> 290 <p class="description"><?php esc_html_e( '"Checkout: guests only" skips customers who are logged in. It has no effect while "Checkout: all customers" is on.', 'spamoff-bot-protection' ); ?></p> 291 <p class="description"><?php esc_html_e( 'WordPress Login also covers login forms made with wp_login_form(). XML-RPC and application-password logins have no form and are not covered.', 'spamoff-bot-protection' ); ?></p> 225 292 </td> 226 293 </tr> … … 237 304 $v2_size = isset( $customization['v2_size'] ) ? $customization['v2_size'] : 'normal'; 238 305 $error_failed = isset( $customization['error_message_failed'] ) ? $customization['error_message_failed'] : __( 'Verification failed. Please try again.', 'spamoff-bot-protection' ); 306 $hide_badge = isset( $customization['hide_v3_badge'] ) && 'yes' === $customization['hide_v3_badge']; 239 307 ?> 240 <tr valign="top">308 <tr> 241 309 <th scope="row" class="titledesc"><?php esc_html_e( 'Customization Options', 'spamoff-bot-protection' ); ?></th> 242 310 <td class="forminp"> … … 247 315 <td style="padding: 10px;"> 248 316 <code>[spamoff_bot_protection]</code> 249 <p class="description"><?php esc_html_e( ' Use this shortcode to manually place the captcha anywhere.', 'spamoff-bot-protection' ); ?></p>317 <p class="description"><?php esc_html_e( 'Places the captcha inside a custom form. The form\'s handler must verify it by calling spamoff_bot_protection_verify_submission().', 'spamoff-bot-protection' ); ?></p> 250 318 </td> 251 319 </tr> 252 320 <tr> 253 <td style="padding: 10px;">< ?php esc_html_e( 'reCAPTCHA v2 Theme', 'spamoff-bot-protection' ); ?></td>321 <td style="padding: 10px;"><label for="spamoff_bot_protection_v2_theme"><?php esc_html_e( 'reCAPTCHA v2 Theme', 'spamoff-bot-protection' ); ?></label></td> 254 322 <td style="padding: 10px;"> 255 <select name="spamoff_bot_protection_customization[v2_theme]">323 <select id="spamoff_bot_protection_v2_theme" name="spamoff_bot_protection_customization[v2_theme]"> 256 324 <option value="light" <?php selected( $v2_theme, 'light' ); ?>><?php esc_html_e( 'Light', 'spamoff-bot-protection' ); ?></option> 257 325 <option value="dark" <?php selected( $v2_theme, 'dark' ); ?>><?php esc_html_e( 'Dark', 'spamoff-bot-protection' ); ?></option> … … 260 328 </tr> 261 329 <tr> 262 <td style="padding: 10px;">< ?php esc_html_e( 'reCAPTCHA v2 Size', 'spamoff-bot-protection' ); ?></td>330 <td style="padding: 10px;"><label for="spamoff_bot_protection_v2_size"><?php esc_html_e( 'reCAPTCHA v2 Size', 'spamoff-bot-protection' ); ?></label></td> 263 331 <td style="padding: 10px;"> 264 <select name="spamoff_bot_protection_customization[v2_size]">332 <select id="spamoff_bot_protection_v2_size" name="spamoff_bot_protection_customization[v2_size]"> 265 333 <option value="normal" <?php selected( $v2_size, 'normal' ); ?>><?php esc_html_e( 'Normal', 'spamoff-bot-protection' ); ?></option> 266 334 <option value="compact" <?php selected( $v2_size, 'compact' ); ?>><?php esc_html_e( 'Compact', 'spamoff-bot-protection' ); ?></option> … … 269 337 </tr> 270 338 <tr> 271 <td style="padding: 10px;">< ?php esc_html_e( 'Failed Verification Message', 'spamoff-bot-protection' ); ?></td>339 <td style="padding: 10px;"><label for="spamoff_bot_protection_hide_v3_badge"><?php esc_html_e( 'Hide reCAPTCHA v3 Badge', 'spamoff-bot-protection' ); ?></label></td> 272 340 <td style="padding: 10px;"> 273 <input type="text" name="spamoff_bot_protection_customization[error_message_failed]" value="<?php echo esc_attr( $error_failed ); ?>" style="width: 100%;"> 341 <input type="checkbox" id="spamoff_bot_protection_hide_v3_badge" name="spamoff_bot_protection_customization[hide_v3_badge]" value="yes" <?php checked( $hide_badge ); ?>> 342 <p class="description"><?php esc_html_e( 'Hides the floating badge and shows Google\'s required reCAPTCHA notice inside each protected form instead. The Checkout block keeps the badge.', 'spamoff-bot-protection' ); ?></p> 343 </td> 344 </tr> 345 <tr> 346 <td style="padding: 10px;"><label for="spamoff_bot_protection_error_message_failed"><?php esc_html_e( 'Failed Verification Message', 'spamoff-bot-protection' ); ?></label></td> 347 <td style="padding: 10px;"> 348 <input type="text" id="spamoff_bot_protection_error_message_failed" name="spamoff_bot_protection_customization[error_message_failed]" value="<?php echo esc_attr( $error_failed ); ?>" style="width: 100%;"> 274 349 </td> 275 350 </tr> … … 281 356 } 282 357 358 /** 359 * WooCommerce checks the nonce and capability before saving; check again because 360 * this reads $_POST directly. 361 */ 283 362 public function save_forms_settings() { 284 if ( ! isset( $_POST['_wpnonce']) ) {363 if ( ! current_user_can( 'manage_woocommerce' ) || ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), 'woocommerce-settings' ) ) { 285 364 return; 286 365 } 287 $nonce = sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ); 288 if ( ! wp_verify_nonce( $nonce, 'woocommerce-settings' ) ) { 366 367 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Only whitelisted keys with the value "yes" are kept below. 368 $posted = isset( $_POST['spamoff_bot_protection_forms'] ) ? (array) wp_unslash( $_POST['spamoff_bot_protection_forms'] ) : array(); 369 370 $forms = array(); 371 foreach ( Helper::FORMS as $form ) { 372 if ( isset( $posted[ $form ] ) && 'yes' === $posted[ $form ] ) { 373 $forms[ $form ] = 'yes'; 374 } 375 } 376 377 update_option( 'spamoff_bot_protection_forms', $forms ); 378 } 379 380 public function save_customization_settings() { 381 if ( ! current_user_can( 'manage_woocommerce' ) || ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), 'woocommerce-settings' ) ) { 289 382 return; 290 383 } 291 384 292 if ( isset( $_POST['spamoff_bot_protection_forms'] ) ) { 293 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized below. 294 $forms_data = wp_unslash( $_POST['spamoff_bot_protection_forms'] ); 295 $forms = array_map( 'sanitize_text_field', (array) $forms_data ); 296 update_option( 'spamoff_bot_protection_forms', $forms ); 297 } else { 298 update_option( 'spamoff_bot_protection_forms', array() ); 299 } 300 } 301 302 public function save_customization_settings() { 303 if ( ! isset( $_POST['_wpnonce'] ) ) { 385 if ( ! isset( $_POST['spamoff_bot_protection_customization'] ) ) { 304 386 return; 305 387 } 306 $nonce = sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ); 307 if ( ! wp_verify_nonce( $nonce, 'woocommerce-settings' ) ) { 308 return; 309 } 310 311 if ( isset( $_POST['spamoff_bot_protection_customization'] ) ) { 312 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized below. 313 $customization_data = wp_unslash( $_POST['spamoff_bot_protection_customization'] ); 314 315 $customization = array( 316 'v2_theme' => sanitize_text_field( $customization_data['v2_theme'] ?? 'light' ), 317 'v2_size' => sanitize_text_field( $customization_data['v2_size'] ?? 'normal' ), 318 'error_message_failed' => sanitize_text_field( $customization_data['error_message_failed'] ?? '' ), 319 ); 320 321 update_option( 'spamoff_bot_protection_customization', $customization ); 322 } 388 389 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Each field is whitelisted or sanitized below. 390 $posted = (array) wp_unslash( $_POST['spamoff_bot_protection_customization'] ); 391 392 $customization = array( 393 'v2_theme' => isset( $posted['v2_theme'] ) && 'dark' === $posted['v2_theme'] ? 'dark' : 'light', 394 'v2_size' => isset( $posted['v2_size'] ) && 'compact' === $posted['v2_size'] ? 'compact' : 'normal', 395 'hide_v3_badge' => isset( $posted['hide_v3_badge'] ) && 'yes' === $posted['hide_v3_badge'] ? 'yes' : 'no', 396 'error_message_failed' => isset( $posted['error_message_failed'] ) && is_string( $posted['error_message_failed'] ) ? sanitize_text_field( $posted['error_message_failed'] ) : '', 397 ); 398 399 update_option( 'spamoff_bot_protection_customization', $customization ); 323 400 } 324 401 } -
spamoff-bot-protection/tags/1.0.0/languages/spamoff-bot-protection.pot
r3603369 r3714007 1 1 # Copyright (C) 2026 pluginos 2 # This file is distributed under the GPLv2 or later license.2 # This file is distributed under the GPLv2 or later. 3 3 msgid "" 4 4 msgstr "" 5 "Project-Id-Version: SpamOff – Spam & Bot Protection for WordPress 1.0.0\n"6 "Report-Msgid-Bugs-To: \n"5 "Project-Id-Version: SpamOff – Google reCaptcha for WooCommerce 1.1.0\n" 6 "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/spamoff-bot-protection\n" 7 7 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" 8 8 "Language-Team: LANGUAGE <LL@li.org>\n" … … 10 10 "Content-Type: text/plain; charset=UTF-8\n" 11 11 "Content-Transfer-Encoding: 8bit\n" 12 "POT-Creation-Date: 2026-0 5-21T00:00:00+00:00\n"12 "POT-Creation-Date: 2026-09-25T03:58:02+02:00\n" 13 13 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 14 "X-Generator: hand-authored template, regenerate with `wp i18n make-pot`\n"14 "X-Generator: WP-CLI 2.12.0\n" 15 15 "X-Domain: spamoff-bot-protection\n" 16 16 17 #. Plugin Name of the plugin 17 18 #: spamoff-bot-protection.php 18 msgid "SpamOff – Spam & Bot Protection for WordPress requires WooCommerce to be installed and active." 19 msgstr "" 20 21 #: includes/class-spamoff-bot-protection-admin.php 19 #: includes/class-spamoff-bot-protection-admin.php:30 20 msgid "SpamOff – Google reCaptcha for WooCommerce" 21 msgstr "" 22 23 #. Description of the plugin 24 #: spamoff-bot-protection.php 25 msgid "Protects WooCommerce and WordPress forms from bots and spam using Google reCAPTCHA v2 and v3, with full WooCommerce Block Checkout support." 26 msgstr "" 27 28 #. Author of the plugin 29 #: spamoff-bot-protection.php 30 msgid "pluginos" 31 msgstr "" 32 33 #: includes/class-spamoff-bot-protection-admin.php:24 34 msgid "SpamOff adds Google reCAPTCHA to the forms you protect. On pages with a protected form, Google's reCAPTCHA script runs in the visitor's browser, and when the form is submitted your server sends the reCAPTCHA response and the visitor's IP address to Google to check it." 35 msgstr "" 36 37 #: includes/class-spamoff-bot-protection-admin.php:25 38 msgid "Suggested text:" 39 msgstr "" 40 41 #: includes/class-spamoff-bot-protection-admin.php:26 42 msgid "We use Google reCAPTCHA on our login, registration, password reset, checkout and comment forms to protect this site from spam and abuse. reCAPTCHA collects hardware and software information, such as device and application data, and sends it to Google for analysis. Your use of reCAPTCHA is subject to the Google Privacy Policy and Terms of Service." 43 msgstr "" 44 45 #: includes/class-spamoff-bot-protection-admin.php:35 22 46 msgid "Settings" 23 47 msgstr "" 24 48 25 #: includes/class-spamoff-bot-protection-frontend.php 49 #: includes/class-spamoff-bot-protection-frontend.php:281 26 50 msgid "Comment Submission Error" 27 51 msgstr "" 28 52 29 #: includes/class-spamoff-bot-protection-helper.php 53 #: includes/class-spamoff-bot-protection-frontend.php:315 54 msgid "reCAPTCHA v3 token for the checkout." 55 msgstr "" 56 57 #: includes/class-spamoff-bot-protection-frontend.php:321 58 msgid "reCAPTCHA v3 action the token was created for." 59 msgstr "" 60 61 #. translators: 1: Google Privacy Policy URL, 2: Google Terms of Service URL. 62 #: includes/class-spamoff-bot-protection-frontend.php:547 63 #, php-format 64 msgid "This site is protected by reCAPTCHA and the Google <a href=\"%1$s\" target=\"_blank\" rel=\"noopener noreferrer\">Privacy Policy</a> and <a href=\"%2$s\" target=\"_blank\" rel=\"noopener noreferrer\">Terms of Service</a> apply." 65 msgstr "" 66 67 #: includes/class-spamoff-bot-protection-helper.php:250 68 msgid "Please complete the verification." 69 msgstr "" 70 71 #: includes/class-spamoff-bot-protection-helper.php:259 72 #: includes/class-spamoff-bot-protection-settings.php:305 30 73 msgid "Verification failed. Please try again." 31 74 msgstr "" 32 75 33 #: includes/class-spamoff-bot-protection-helper.php 34 msgid "Please complete the verification." 35 msgstr "" 36 37 #: includes/class-spamoff-bot-protection-settings.php 76 #: includes/class-spamoff-bot-protection-settings.php:24 38 77 msgid "SpamOff" 39 78 msgstr "" 40 79 41 #: includes/class-spamoff-bot-protection-settings.php 80 #: includes/class-spamoff-bot-protection-settings.php:49 81 #: includes/class-spamoff-bot-protection-settings.php:136 42 82 msgid "General Settings" 43 83 msgstr "" 44 84 45 #: includes/class-spamoff-bot-protection-settings.php 85 #: includes/class-spamoff-bot-protection-settings.php:50 46 86 msgid "Advanced Settings" 47 87 msgstr "" 48 88 49 #: includes/class-spamoff-bot-protection-settings.php 89 #: includes/class-spamoff-bot-protection-settings.php:51 90 #: includes/class-spamoff-bot-protection-settings.php:230 50 91 msgid "Customization" 51 92 msgstr "" 52 93 53 #: includes/class-spamoff-bot-protection-settings.php 94 #. translators: %s: reCAPTCHA version, e.g. v3. 95 #: includes/class-spamoff-bot-protection-settings.php:79 96 #, php-format 97 msgid "Protection is off: add the reCAPTCHA %s Site Key and Secret Key under General Settings." 98 msgstr "" 99 100 #: includes/class-spamoff-bot-protection-settings.php:86 101 msgid "Your checkout page uses the Checkout block, which only supports reCAPTCHA v3. Orders placed there are rejected until you save v3 keys. Select v3 under General Settings to enter them; they stay saved if you switch back to v2." 102 msgstr "" 103 104 #: includes/class-spamoff-bot-protection-settings.php:138 54 105 msgid "Configure your basic reCAPTCHA settings and API keys." 55 106 msgstr "" 56 107 57 #: includes/class-spamoff-bot-protection-settings.php 108 #: includes/class-spamoff-bot-protection-settings.php:139 109 msgid "Protected forms send visitor data to Google reCAPTCHA; suggested privacy-policy text is under Settings → Privacy → Policy Guide." 110 msgstr "" 111 112 #: includes/class-spamoff-bot-protection-settings.php:143 58 113 msgid "reCAPTCHA Type" 59 114 msgstr "" 60 115 61 #: includes/class-spamoff-bot-protection-settings.php 62 msgid "Choose between reCAPTCHA v3 (invisible) and v2 (checkbox). "63 msgstr "" 64 65 #: includes/class-spamoff-bot-protection-settings.php 116 #: includes/class-spamoff-bot-protection-settings.php:144 117 msgid "Choose between reCAPTCHA v3 (invisible) and v2 (checkbox). Google issues separate keys for each; both sets stay saved when you switch." 118 msgstr "" 119 120 #: includes/class-spamoff-bot-protection-settings.php:148 66 121 msgid "reCAPTCHA v3 (Invisible)" 67 122 msgstr "" 68 123 69 #: includes/class-spamoff-bot-protection-settings.php 124 #: includes/class-spamoff-bot-protection-settings.php:149 70 125 msgid "reCAPTCHA v2 (Checkbox)" 71 126 msgstr "" 72 127 73 #: includes/class-spamoff-bot-protection-settings.php 74 msgid " Site Key"75 msgstr "" 76 77 #: includes/class-spamoff-bot-protection-settings.php 78 msgid "Enter your Google reCAPTCHA site key."79 msgstr "" 80 81 #: includes/class-spamoff-bot-protection-settings.php 82 msgid " Secret Key"83 msgstr "" 84 85 #: includes/class-spamoff-bot-protection-settings.php 86 msgid "Enter your Google reCAPTCHA secret key."87 msgstr "" 88 89 #: includes/class-spamoff-bot-protection-settings.php 128 #: includes/class-spamoff-bot-protection-settings.php:154 129 msgid "v3 Site Key" 130 msgstr "" 131 132 #: includes/class-spamoff-bot-protection-settings.php:155 133 msgid "Enter your Google reCAPTCHA v3 site key. The Checkout block always uses the v3 keys." 134 msgstr "" 135 136 #: includes/class-spamoff-bot-protection-settings.php:162 137 msgid "v3 Secret Key" 138 msgstr "" 139 140 #: includes/class-spamoff-bot-protection-settings.php:163 141 msgid "Enter your Google reCAPTCHA v3 secret key." 142 msgstr "" 143 144 #: includes/class-spamoff-bot-protection-settings.php:171 90 145 msgid "Minimum Score for reCAPTCHA v3" 91 146 msgstr "" 92 147 93 #: includes/class-spamoff-bot-protection-settings.php 148 #: includes/class-spamoff-bot-protection-settings.php:172 94 149 msgid "Minimum score required (0.0 - 1.0). Submissions below this score will be rejected." 95 150 msgstr "" 96 151 97 #: includes/class-spamoff-bot-protection-settings.php 152 #: includes/class-spamoff-bot-protection-settings.php:185 153 msgid "v2 Site Key" 154 msgstr "" 155 156 #: includes/class-spamoff-bot-protection-settings.php:186 157 msgid "Enter your Google reCAPTCHA v2 (\"I'm not a robot\" Checkbox) site key." 158 msgstr "" 159 160 #: includes/class-spamoff-bot-protection-settings.php:193 161 msgid "v2 Secret Key" 162 msgstr "" 163 164 #: includes/class-spamoff-bot-protection-settings.php:194 165 msgid "Enter your Google reCAPTCHA v2 secret key." 166 msgstr "" 167 168 #: includes/class-spamoff-bot-protection-settings.php:211 98 169 msgid "Form Protection" 99 170 msgstr "" 100 171 101 #: includes/class-spamoff-bot-protection-settings.php 172 #: includes/class-spamoff-bot-protection-settings.php:213 102 173 msgid "Select which forms to protect." 103 174 msgstr "" 104 175 105 #: includes/class-spamoff-bot-protection-settings.php 176 #: includes/class-spamoff-bot-protection-settings.php:232 106 177 msgid "Customize the appearance and messages." 107 178 msgstr "" 108 179 109 #: includes/class-spamoff-bot-protection-settings.php 110 msgid "Customer Login" 111 msgstr "" 112 113 #: includes/class-spamoff-bot-protection-settings.php 114 msgid "Customer Registration" 115 msgstr "" 116 117 #: includes/class-spamoff-bot-protection-settings.php 118 msgid "Checkout Page" 119 msgstr "" 120 121 #: includes/class-spamoff-bot-protection-settings.php 122 msgid "Guest Checkout" 123 msgstr "" 124 125 #: includes/class-spamoff-bot-protection-settings.php 126 msgid "Lost Password" 127 msgstr "" 128 129 #: includes/class-spamoff-bot-protection-settings.php 180 #: includes/class-spamoff-bot-protection-settings.php:251 181 msgid "WordPress Login (wp-login.php)" 182 msgstr "" 183 184 #: includes/class-spamoff-bot-protection-settings.php:252 185 msgid "WordPress Registration (wp-login.php)" 186 msgstr "" 187 188 #: includes/class-spamoff-bot-protection-settings.php:253 189 msgid "WordPress Lost Password (wp-login.php)" 190 msgstr "" 191 192 #: includes/class-spamoff-bot-protection-settings.php:254 130 193 msgid "WordPress Comments" 131 194 msgstr "" 132 195 133 #: includes/class-spamoff-bot-protection-settings.php 196 #: includes/class-spamoff-bot-protection-settings.php:255 197 msgid "WooCommerce Login (My Account and checkout)" 198 msgstr "" 199 200 #: includes/class-spamoff-bot-protection-settings.php:256 201 msgid "WooCommerce Registration (My Account)" 202 msgstr "" 203 204 #: includes/class-spamoff-bot-protection-settings.php:257 205 msgid "WooCommerce Lost Password (My Account)" 206 msgstr "" 207 208 #: includes/class-spamoff-bot-protection-settings.php:258 209 msgid "WooCommerce Checkout: all customers" 210 msgstr "" 211 212 #: includes/class-spamoff-bot-protection-settings.php:259 213 msgid "WooCommerce Checkout: guests only" 214 msgstr "" 215 216 #: includes/class-spamoff-bot-protection-settings.php:270 134 217 msgid "Protected Forms" 135 218 msgstr "" 136 219 137 #: includes/class-spamoff-bot-protection-settings.php 220 #: includes/class-spamoff-bot-protection-settings.php:275 138 221 msgid "Form" 139 222 msgstr "" 140 223 141 #: includes/class-spamoff-bot-protection-settings.php 224 #: includes/class-spamoff-bot-protection-settings.php:276 142 225 msgid "Enable" 143 226 msgstr "" 144 227 145 #: includes/class-spamoff-bot-protection-settings.php 228 #: includes/class-spamoff-bot-protection-settings.php:290 229 msgid "\"Checkout: guests only\" skips customers who are logged in. It has no effect while \"Checkout: all customers\" is on." 230 msgstr "" 231 232 #: includes/class-spamoff-bot-protection-settings.php:291 233 msgid "WordPress Login also covers login forms made with wp_login_form(). XML-RPC and application-password logins have no form and are not covered." 234 msgstr "" 235 236 #: includes/class-spamoff-bot-protection-settings.php:309 146 237 msgid "Customization Options" 147 238 msgstr "" 148 239 149 #: includes/class-spamoff-bot-protection-settings.php 240 #: includes/class-spamoff-bot-protection-settings.php:314 150 241 msgid "Shortcode" 151 242 msgstr "" 152 243 153 #: includes/class-spamoff-bot-protection-settings.php 154 msgid " Use this shortcode to manually place the captcha anywhere."155 msgstr "" 156 157 #: includes/class-spamoff-bot-protection-settings.php 244 #: includes/class-spamoff-bot-protection-settings.php:317 245 msgid "Places the captcha inside a custom form. The form's handler must verify it by calling spamoff_bot_protection_verify_submission()." 246 msgstr "" 247 248 #: includes/class-spamoff-bot-protection-settings.php:321 158 249 msgid "reCAPTCHA v2 Theme" 159 250 msgstr "" 160 251 161 #: includes/class-spamoff-bot-protection-settings.php 252 #: includes/class-spamoff-bot-protection-settings.php:324 162 253 msgid "Light" 163 254 msgstr "" 164 255 165 #: includes/class-spamoff-bot-protection-settings.php 256 #: includes/class-spamoff-bot-protection-settings.php:325 166 257 msgid "Dark" 167 258 msgstr "" 168 259 169 #: includes/class-spamoff-bot-protection-settings.php 260 #: includes/class-spamoff-bot-protection-settings.php:330 170 261 msgid "reCAPTCHA v2 Size" 171 262 msgstr "" 172 263 173 #: includes/class-spamoff-bot-protection-settings.php 264 #: includes/class-spamoff-bot-protection-settings.php:333 174 265 msgid "Normal" 175 266 msgstr "" 176 267 177 #: includes/class-spamoff-bot-protection-settings.php 268 #: includes/class-spamoff-bot-protection-settings.php:334 178 269 msgid "Compact" 179 270 msgstr "" 180 271 181 #: includes/class-spamoff-bot-protection-settings.php 272 #: includes/class-spamoff-bot-protection-settings.php:339 273 msgid "Hide reCAPTCHA v3 Badge" 274 msgstr "" 275 276 #: includes/class-spamoff-bot-protection-settings.php:342 277 msgid "Hides the floating badge and shows Google's required reCAPTCHA notice inside each protected form instead. The Checkout block keeps the badge." 278 msgstr "" 279 280 #: includes/class-spamoff-bot-protection-settings.php:346 182 281 msgid "Failed Verification Message" 183 282 msgstr "" 283 284 #: spamoff-bot-protection.php:61 285 msgid "SpamOff – Google reCaptcha for WooCommerce requires WooCommerce to be installed and active." 286 msgstr "" -
spamoff-bot-protection/tags/1.0.0/readme.txt
r3621350 r3714007 130 130 == Changelog == 131 131 132 = 1.0.2 = 133 * Fixed a functionality issue. 134 132 135 = 1.0.1 = 133 136 * Renamed the plugin to SpamOff – Google reCAPTCHA for WooCommerce. -
spamoff-bot-protection/tags/1.0.0/spamoff-bot-protection.php
r3603369 r3714007 1 1 <?php 2 2 /** 3 * Plugin Name: SpamOff – Spam & Bot Protection3 * Plugin Name: SpamOff – Google reCaptcha for WooCommerce 4 4 * Description: Protects WooCommerce and WordPress forms from bots and spam using Google reCAPTCHA v2 and v3, with full WooCommerce Block Checkout support. 5 * Version: 1.1.0 6 * Requires at least: 6.5 7 * Requires PHP: 7.4 8 * Requires Plugins: woocommerce 9 * Author: pluginos 5 10 * License: GPLv2 or later 6 11 * License URI: https://www.gnu.org/licenses/gpl-2.0.html 7 * Version: 1.0.08 * Author: pluginos9 12 * Text Domain: spamoff-bot-protection 10 13 * Domain Path: /languages 11 * Requires at least: 5.8 12 * Requires PHP: 7.4 13 * WC requires at least: 6.0 14 * WC tested up to: 10.5 14 * WC requires at least: 8.0 15 * WC tested up to: 11.1 15 16 */ 16 17 … … 22 23 define( 'SPAMOFF_BOT_PROTECTION_PLUGIN_PATH', plugin_dir_path( __FILE__ ) ); 23 24 define( 'SPAMOFF_BOT_PROTECTION_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); 24 define( 'SPAMOFF_BOT_PROTECTION_PLUGIN_VERSION', '1. 0.0' );25 define( 'SPAMOFF_BOT_PROTECTION_PLUGIN_VERSION', '1.1.0' ); 25 26 26 27 class SpamOff_Bot_Protection_Plugin { … … 57 58 public function woocommerce_missing_notice() { 58 59 ?> 59 <div class=" error">60 <p><?php esc_html_e( 'SpamOff – Spam & Bot Protection for WordPressrequires WooCommerce to be installed and active.', 'spamoff-bot-protection' ); ?></p>60 <div class="notice notice-error"> 61 <p><?php esc_html_e( 'SpamOff – Google reCaptcha for WooCommerce requires WooCommerce to be installed and active.', 'spamoff-bot-protection' ); ?></p> 61 62 </div> 62 63 <?php … … 72 73 73 74 /** 74 * Declare compatibility with WooCommerce HPOS .75 * Declare compatibility with WooCommerce HPOS and the Cart & Checkout Blocks. 75 76 */ 76 77 public function declare_compatibility() { 77 78 if ( class_exists( '\Automattic\WooCommerce\Utilities\FeaturesUtil' ) ) { 78 79 \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true ); 80 \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'cart_checkout_blocks', __FILE__, true ); 79 81 } 82 } 83 84 /** 85 * 1.0.0 stored one key pair. Since 1.1.0 each reCAPTCHA version has its own, so move 86 * the old pair into the slot for the version that was selected. 87 */ 88 private function maybe_upgrade() { 89 $db_version = get_option( 'spamoff_bot_protection_db_version', '1.0.0' ); 90 if ( version_compare( $db_version, '1.1.0', '>=' ) ) { 91 return; 92 } 93 94 $type = get_option( 'spamoff_bot_protection_type', 'v3' ); 95 $type = in_array( $type, array( 'v3', 'v2' ), true ) ? $type : 'v3'; 96 97 foreach ( array( 'site_key', 'secret_key' ) as $key ) { 98 $legacy = get_option( 'spamoff_bot_protection_' . $key, '' ); 99 if ( '' !== $legacy && '' === get_option( 'spamoff_bot_protection_' . $type . '_' . $key, '' ) ) { 100 update_option( 'spamoff_bot_protection_' . $type . '_' . $key, $legacy ); 101 } 102 delete_option( 'spamoff_bot_protection_' . $key ); 103 } 104 105 update_option( 'spamoff_bot_protection_db_version', '1.1.0' ); 80 106 } 81 107 … … 86 112 87 113 $this->load_files(); 114 $this->maybe_upgrade(); 88 115 89 116 new SpamOff_Bot_Protection_Captcha(); -
spamoff-bot-protection/tags/1.0.0/uninstall.php
r3603369 r3714007 11 11 $spamoff_bot_protection_options = array( 12 12 'spamoff_bot_protection_type', 13 'spamoff_bot_protection_site_key', 14 'spamoff_bot_protection_secret_key', 13 'spamoff_bot_protection_v3_site_key', 14 'spamoff_bot_protection_v3_secret_key', 15 'spamoff_bot_protection_v2_site_key', 16 'spamoff_bot_protection_v2_secret_key', 15 17 'spamoff_bot_protection_minimum_score', 16 18 'spamoff_bot_protection_forms', 17 19 'spamoff_bot_protection_customization', 20 'spamoff_bot_protection_db_version', 21 // Single key pair used before 1.1.0. 22 'spamoff_bot_protection_site_key', 23 'spamoff_bot_protection_secret_key', 18 24 ); 19 25
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)