1/** 2 * Access Platform SSO - Login Form Detector 3 * Automatically detects and enhances login forms across the site 4 * Supports: MemberPress, LearnDash, WooCommerce, Ultimate Member, and generic WordPress forms 5 */ 6 7(function() { 8 'use strict'; 9 10 // Configuration from WordPress 11 const config = window.accessSSODetector || {}; 12 13 // Common login form selectors for various plugins 14 const LOGIN_FORM_SELECTORS = { 15 // WordPress Core 16 wordpress: [ 17 '#loginform', 18 '.login-form', 19 'form[name="loginform"]', 20 '.wp-login-form' 21 ], 22 23 // MemberPress - based on official docs: https://memberpress.com/docs/memberpress-login-page/ 24 // Form uses class .mepr-login-form or .mepr-form with name="mepr-loginform" 25 // Wrapped in .mepr-login container 26 memberpress: [ 27 '.mepr-login-form', // Main form class 28 'form.mepr-form[name="mepr-loginform"]', // Form with name attribute (hyphenated) 29 'form[name="mepr-loginform"]', // Any form with this name 30 '.mepr-login form', // Form inside .mepr-login wrapper
31 '.mepr-login-widget form', // Widget login form 32 '#mepr-login-form', // ID selector 33 '[class*="mepr-login"] form', // Any mepr-login class variant 34 '.mepr_login_form', // Underscore variant (older versions) 35 'form.mepr-form:has(input.mepr-username)', // Form with MemberPress username input 36 'form:has(input.mepr-username):has(input.mepr-password)' // Form with both MP fields 37 ], 38 39 // LearnDash 40 learndash: [ 41 '.learndash-login-form', 42 '#learndash-login-form', 43 '.ld-login-form', 44 '.ld-login', 45 'form.learndash-wrapper-login', 46 '[class*="learndash"] form[action*="login"]', 47 '.sfwd-login-form' 48 ], 49 50 // WooCommerce 51 woocommerce: [ 52 '.woocommerce-form-login', 53 'form.woocommerce-form-login', 54 '.woocommerce-form--login', 55 '#customer_login .woocommerce-form-login', 56 '.woocommerce-MyAccount-content form.login' 57 ], 58 59 // Ultimate Member 60 ultimatemember: [ 61 '.um-form form', 62 '.um-login form', 63 'form.um-form', 64 '.um-login-form' 65 ], 66 67 // BuddyPress / BuddyBoss 68 buddypress: [ 69 '#bp-login-widget-form', 70 '.bp-login-widget-form', 71 '#sidebar-login-form', 72 'form#bp-login-form' 73 ], 74 75 // Generic patterns (catch-all) 76 generic: [ 77 'form[action*="wp-login"]', 78 'form[action*="login"]', 79 'form:has(input[name="log"]):has(input[name="pwd"])', 80 'form:has(input[type="password"]):has(input[name*="user"], input[name*="email"], input[name*="log"])', 81 '.login-form form', 82 '#login-form', 83 '.member-login form', 84 '.user-login form' 85 ] 86 }; 87 88 // Track forms we've already processed 89 const processedForms = new WeakSet(); 90 91 /** 92 * The main SSO script performs the browser capability preflight. If it is 93 * absent or failed to initialize, preserve the detector's existing behavior. 94 */ 95 function isBrowserSupported() { 96 try { 97 const support = window.AccessSSOBrowserSupport; 98 return !support || typeof support.isAvailable !== 'function' || support.isAvailable(); 99 } catch (error) { 100 console.warn('Access SSO: Browser support check failed; preserving the existing form detector flow.', error); 101 return true; 102 } 103 } 104 105 /** 106 * Build the SSO login URL 107 */ 108 function getReturnUrl(form) { 109 const pageUrl = new URL(window.location.href); 110 const queryRedirect = pageUrl.searchParams.get('redirect_to'); 111 if (queryRedirect) { 112 return queryRedirect; 113 } 114 115 const formRedirect = form && form.querySelector('input[name="redirect_to"]'); 116 if (formRedirect && formRedirect.value) { 117 return formRedirect.value; 118 } 119 120 return pageUrl.toString(); 121 } 122 123 function buildSSOUrl(form) { 124 if (!config.login_url) { 125 console.warn('Access SSO: Missing login URL configuration'); 126 return null; 127 } 128 129 const loginUrl = new URL(config.login_url, window.location.origin); 130 loginUrl.searchParams.set('return_to', getReturnUrl(form)); 131 return loginUrl.toString(); 132 } 133 134 /** 135 * Create the SSO button element 136 */ 137 function createSSOButton(formType = 'generic', form = null) { 138 if (!isBrowserSupported()) return null; 139 140 const ssoUrl = buildSSOUrl(form); 141 if (!ssoUrl) return null; 142 143 // Create wrapper 144 const wrapper = document.createElement('div'); 145 wrapper.className = 'access-sso-login-wrapper access-sso-injected'; 146 wrapper.setAttribute('data-form-type', formType); 147 148 // Create button 149 const button = document.createElement('a'); 150 button.href = ssoUrl; 151 button.className = 'access-sso-login-button'; 152 button.textContent = config.button_text || 'Login with Access Platform'; 153 button.setAttribute('role', 'button'); 154 button.setAttribute('aria-label', 'Login with Access Platform Single Sign-On'); 155 156 // Create divider 157 const divider = document.createElement('div'); 158 divider.className = 'access-sso-divider'; 159 const dividerText = document.createElement('span'); 160 dividerText.textContent = config.divider_text || 'or'; 161 divider.appendChild(dividerText); 162 163 wrapper.appendChild(button); 164 wrapper.appendChild(divider); 165 166 // Add click handler 167 button.addEventListener('click', function() { 168 button.classList.add('loading'); 169 button.textContent = 'Redirecting...'; 170 }); 171 172 return wrapper; 173 } 174 175 /** 176 * Inject SSO button into a form 177 */ 178 function injectSSOButton(form, formType) { 179 // Skip if already processed 180 if (processedForms.has(form)) return false;
181 182 // Skip if already has an SSO button 183 if (form.querySelector('.access-sso-login-wrapper, .access-sso-injected')) return false; 184 185 // Check if parent already has SSO button (for nested forms) 186 if (form.closest('.access-sso-login-wrapper')) return false; 187 188 const button = createSSOButton(formType, form); 189 if (!button) return false; 190 191 // Find the best insertion point 192 let insertionPoint = null; 193 let insertMethod = 'before'; // 'before', 'after', 'prepend' 194 195 // Try to find a submit button or form actions 196 const submitButton = form.querySelector( 197 'input[type="submit"], button[type="submit"], .submit, .login-submit, ' + 198 '.mepr-submit, .ld-login-button, .woocommerce-form-login__submit' 199 ); 200 201 const passwordField = form.querySelector('input[type="password"]'); 202 const firstInput = form.querySelector('input:not([type="hidden"]):not([type="submit"])'); 203 204 if (firstInput) { 205 // Insert before the first visible input 206 insertionPoint = firstInput.closest('p, div, .form-row, .mepr-form-row, label') || firstInput; 207 insertMethod = 'before'; 208 } else if (passwordField) { 209 insertionPoint = passwordField.closest('p, div, .form-row') || passwordField; 210 insertMethod = 'before'; 211 } else if (submitButton) { 212 insertionPoint = submitButton.closest('p, div, .form-row') || submitButton; 213 insertMethod = 'before'; 214 } else { 215 // Prepend to form 216 insertionPoint = form; 217 insertMethod = 'prepend'; 218 } 219 220 // Insert the button 221 try { 222 if (insertMethod === 'prepend') { 223 form.insertBefore(button, form.firstChild); 224 } else if (insertMethod === 'before') { 225 insertionPoint.parentNode.insertBefore(button, insertionPoint); 226 } else { 227 insertionPoint.parentNode.insertBefore(button, insertionPoint.nextSibling); 228 } 229 230 processedForms.add(form); 231 return true; 232 } catch (e) { 233 return false; 234 } 235 } 236 237 /** 238 * Detect and enhance all login forms on the page 239 */ 240 function detectAndEnhanceForms() { 241 if (!isBrowserSupported()) return 0; 242 243 let formsEnhanced = 0; 244 245 // Get enabled form types from config 246 const enabledTypes = config.enabled_form_types || Object.keys(LOGIN_FORM_SELECTORS); 247 248 // Process each form type 249 enabledTypes.forEach(formType => { 250 const selectors = LOGIN_FORM_SELECTORS[formType]; 251 if (!selectors) return; 252 253 selectors.forEach(selector => { 254 try { 255 const forms = document.querySelectorAll(selector); 256 forms.forEach(form => { 257 // Ensure it's actually a form or contains a form 258 const targetForm = form.tagName === 'FORM' ? form : form.querySelector('form'); 259 if (targetForm && injectSSOButton(targetForm, formType)) { 260 formsEnhanced++; 261 } 262 }); 263 } catch (e) { 264 // Selector might not be valid in all browsers (e.g., :has()) 265 // Silently ignore 266 } 267 }); 268 }); 269 270 return formsEnhanced; 271 } 272 273 /** 274 * Watch for dynamically added forms (SPA support, AJAX-loaded content) 275 */ 276 function watchForNewForms() { 277 if (!window.MutationObserver) return; 278 279 const observer = new MutationObserver((mutations) => { 280 let shouldScan = false; 281 282 mutations.forEach(mutation => { 283 if (mutation.addedNodes.length > 0) { 284 mutation.addedNodes.forEach(node => { 285 if (node.nodeType === Node.ELEMENT_NODE) {
286 // Check if added node contains or is a form 287 if (node.tagName === 'FORM' || node.querySelector?.('form')) { 288 shouldScan = true; 289 } 290 // Check for login-related classes 291 if (node.className && typeof node.className === 'string' && 292 (node.className.includes('login') || 293 node.className.includes('mepr') || 294 node.className.includes('learndash') || 295 node.className.includes('woocommerce'))) { 296 shouldScan = true; 297 } 298 } 299 }); 300 } 301 }); 302 303 if (shouldScan) { 304 // Debounce the scan 305 clearTimeout(watchForNewForms.timeout); 306 watchForNewForms.timeout = setTimeout(detectAndEnhanceForms, 100); 307 } 308 }); 309 310 observer.observe(document.body, { 311 childList: true, 312 subtree: true 313 }); 314 } 315 316 /** 317 * Check if current page is excluded from SSO injection 318 */ 319 function isPageExcluded() { 320 const excludedRoutes = config.excluded_routes || []; 321 if (!excludedRoutes.length) return false; 322 323 const currentPath = window.location.pathname.toLowerCase(); 324 const currentUrl = window.location.href.toLowerCase(); 325 326 for (const route of excludedRoutes) { 327 if (!route) continue; 328 const pattern = route.toLowerCase().trim(); 329 330 // Check if it's a regex pattern (starts and ends with /) 331 if (pattern.startsWith('/') && pattern.endsWith('/') && pattern.length > 2) { 332 try { 333 const regex = new RegExp(pattern.slice(1, -1), 'i'); 334 if (regex.test(currentPath) || regex.test(currentUrl)) { 335 return true; 336 } 337 } catch (e) { 338 // Invalid regex, treat as string 339 } 340 } 341 342 // Exact path match 343 if (currentPath === pattern || currentPath === pattern + '/') { 344 return true; 345 } 346 347 // Starts with match (for path prefixes like /checkout) 348 if (pattern.endsWith('*')) { 349 const prefix = pattern.slice(0, -1); 350 if (currentPath.startsWith(prefix)) { 351 return true; 352 } 353 } 354 355 // Contains match 356 if (currentPath.includes(pattern) || currentUrl.includes(pattern)) { 357 return true; 358 } 359 } 360 361 return false; 362 } 363 364 /** 365 * Initialize the detector 366 */ 367 function init() { 368 if (!isBrowserSupported()) { 369 const support = window.AccessSSOBrowserSupport; 370 if (support && typeof support.showWarning === 'function') { 371 support.showWarning(); 372 } 373 return; 374 } 375 376 // Don't run if user is already logged in (unless config says otherwise) 377 if (!config.show_when_logged_in && document.body.classList.contains('logged-in')) { 378 return; 379 } 380 381 // Don't run if disabled 382 if (config.disabled) { 383 return; 384 } 385 386 // Don't run on admin pages 387 if (document.body.classList.contains('wp-admin')) { 388 return; 389 } 390 391 // Don't run on excluded pages 392 if (isPageExcluded()) { 393 return; 394 } 395 396 // Run initial detection 397 const formsFound = detectAndEnhanceForms(); 398 399 // Watch for dynamically added forms 400 watchForNewForms(); 401 402 // Also run after a short delay (for slow-loading content) 403 setTimeout(detectAndEnhanceForms, 1000); 404 setTimeout(detectAndEnhanceForms, 3000); 405 406 } 407 408 // Initialize when DOM is ready 409 if (document.readyState === 'loading') { 410 document.addEventListener('DOMContentLoaded', init); 411 } else { 412 init(); 413 } 414 415 // Also run on window load (catches late-loading forms) 416 window.addEventListener('load', function() { 417 setTimeout(detectAndEnhanceForms, 500); 418 }); 419 420 // Expose API for manual usage 421 window.AccessSSODetector = { 422 detect: detectAndEnhanceForms, 423 injectInto: function(selector) { 424 const form = document.querySelector(selector); 425 if (form) { 426 return injectSSOButton(form, 'manual'); 427 } 428 return false;
429 }, 430 config: config, 431 selectors: LOGIN_FORM_SELECTORS 432 }; 433 434})();
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.