PageSourceSearch

https://tomwoods.com/wp-content/plugins/wp-access-sso/assets/js/login-form-detector.js

js tomwoods.com collected 2026-10-01 10:42:29 UTC 15,238 bytes, 434 lines download raw bytes

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.