1/** 2 * Zero Spam for WordPress David Walsh vanilla JavaScript implementation. 3 * 4 * Handles adding the required functionality for spam detections. 5 * Features: 6 * - No jQuery dependency 7 * - MutationObserver for dynamically loaded forms 8 * - AJAX key refresh for cached pages 9 * - Centralized selector management 10 * 11 * @package ZeroSpam 12 */ 13( function() { 14 'use strict'; 15 16 /** 17 * Configuration from WordPress. 18 * 19 * @type {Object} 20 */ 21 const config = window.ZeroSpamDavidWalsh || {}; 22 23 /** 24 * Input field name for the David Walsh key. 25 * 26 * @type {string} 27 */ 28 const INPUT_NAME = 'zerospam_david_walsh_key'; 29 30 /** 31 * Data attribute to mark protected forms. 32 * 33 * @type {string} 34 */ 35 const DATA_ATTR = 'data-zerospam-davidwalsh'; 36 37 /** 38 * Threshold in seconds to trigger AJAX key refresh (12 hours). 39 * 40 * @type {number} 41 */ 42 const REFRESH_THRESHOLD = 43200; 43 44 /** 45 * Current key value (may be updated via AJAX). 46 * 47 * @type {string} 48 */ 49 let currentKey = config.key || ''; 50 51 /** 52 * Flag to track if we've already attempted a key refresh. 53 * 54 * @type {boolean} 55 */ 56 let keyRefreshAttempted = false; 57 58 /** 59 * Initialize protection on a single form element. 60 * 61 * @param {HTMLFormElement} form - The form element to protect. 62 */ 63 function initForm( form ) { 64 // Skip if already initialized. 65 if ( form.getAttribute( DATA_ATTR ) === 'protected' ) { 66 return; 67 } 68 69 // Skip if no key available. 70 if ( ! currentKey ) { 71 return; 72 } 73 74 // Mark as protected. 75 form.setAttribute( DATA_ATTR, 'protected' ); 76 77 // Check if the hidden input already exists. 78 let input = form.querySelector( `input[name="${INPUT_NAME}"]` ); 79 80 if ( input ) { 81 // Update existing input's value. 82 input.value = currentKey; 83 } else { 84 // Create and append new hidden input. 85 input = document.createElement( 'input' ); 86 input.type = 'hidden'; 87 input.name = INPUT_NAME; 88 input.value = currentKey; 89 form.appendChild( input ); 90 } 91 } 92 93 /** 94 * Initialize protection on all matching forms. 95 */ 96 function initAllForms() { 97 if ( ! config.selectors ) { 98 return; 99 } 100 101 try { 102 const forms = document.querySelectorAll( config.selectors ); 103 forms.forEach( initForm ); 104 } catch ( e ) { 105 // Invalid selector, fail silently. 106 if ( console && console.warn ) { 107 console.warn( 'Zero Spam: Invalid selector in davidwalsh.js', e ); 108 } 109 } 110 } 111 112 /** 113 * Refresh the key via AJAX if it's stale. 114 * 115 * @return {Promise<void>} 116 */ 117 async function maybeRefreshKey() { 118 // Only attempt refresh once per page load. 119 if ( keyRefreshAttempted ) { 120 return; 121 } 122 123 // Check if we have the necessary data. 124 if ( ! config.restUrl || ! config.generated ) { 125 return; 126 } 127 128 // Check if key is older than threshold. 129 const now = Math.floor( Date.now() / 1000 ); 130 const keyAge = now - config.generated; 131 132 if ( keyAge < REFRESH_THRESHOLD ) { 133 return; 134 } 135 136 keyRefreshAttempted = true; 137 138 try { 139 const response = await fetch( config.restUrl, { 140 method: 'GET', 141 } ); 142 143 if ( ! response.ok ) { 144 return; 145 } 146 147 const data = await response.json(); 148 149 if ( data && data.key ) { 150 currentKey = data.key; 151 config.generated = data.generated; 152 153 // Update all already-initialized forms with the new key. 154 updateExistingForms(); 155 } 156 } catch ( e ) { 157 // Fetch failed, continue with existing key. 158 if ( console && console.warn ) { 159 console.warn( 'Zero Spam: Failed to refresh David Walsh key', e ); 160 } 161 } 162 } 163 164 /** 165 * Update the key value in all already-protected forms. 166 */ 167 function updateExistingForms() { 168 const protectedForms = document.querySelectorAll( `form[${DATA_ATTR}="protected"]` ); 169 170 protectedForms.forEach( function( form ) { 171 const input = form.querySelector( `input[name="${INPUT_NAME}"]` ); 172 if ( input ) { 173 input.value = currentKey; 174 } 175 } ); 176 } 177 178 /** 179 * Set up MutationObserver to handle dynamically loaded forms. 180 */ 181 function setupMutationObserver() { 182 // Check for MutationObserver support. 183 if ( typeof MutationObserver === 'undefined' ) { 184 return; 185 } 186 187 const observer = new MutationObserver( function( mutations ) { 188 let shouldInit = false;
189 190 mutations.forEach( function( mutation ) { 191 if ( mutation.addedNodes.length > 0 ) { 192 shouldInit = true; 193 } 194 } ); 195 196 if ( shouldInit ) { 197 // Debounce the initialization. 198 clearTimeout( observer.timeout ); 199 observer.timeout = setTimeout( initAllForms, 100 ); 200 } 201 } ); 202 203 observer.observe( document.body, { 204 childList: true, 205 subtree: true, 206 } ); 207 } 208 209 /** 210 * Main initialization. 211 */ 212 function init() { 213 // Check for required config. 214 if ( typeof config.key === 'undefined' ) { 215 return; 216 } 217 218 // Initialize all forms on page load. 219 initAllForms(); 220 221 // Set up observer for dynamic forms. 222 setupMutationObserver(); 223 224 // Attempt key refresh if page might be cached. 225 maybeRefreshKey(); 226 } 227 228 // Initialize when DOM is ready. 229 if ( document.readyState === 'loading' ) { 230 document.addEventListener( 'DOMContentLoaded', init ); 231 } else { 232 // DOM is already ready. 233 init(); 234 } 235} )();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.