1 2var User_Token = { 3 4 token: '', 5 user_id: '', 6 user_data: {}, 7 performed_text_replacements: false, 8 9 get_token: function() { 10 return this.token; 11 }, 12 13 set_token: function( token ) { 14 this.token = token; 15 }, 16 17 get_user_id: function() { 18 return this.user_id; 19 }, 20 21 set_user_id: function( user_id ) { 22 this.user_id = user_id; 23 }, 24 25 get_user_data: function( key ) { 26 if ( key ) { 27 return typeof this.user_data[ key ] !== 'undefined' ? this.user_data[ key ] : null; 28 } 29 else { 30 return this.user_data; 31 } 32 }, 33 34 set_user_data: function( user_data, perform_text_replacements ) { 35 36 this.user_data = { ...this.user_data, ...user_data }; 37 38 if ( perform_text_replacements ) { 39 this.perform_text_replacements(); 40 } 41 42 }, 43 44 /** 45 * Clear the single-space markers that GF_Custom renders into {user.*} prefill fields 46 * (see class-gf-custom.php). For logged-in users these get replaced with user data via 47 * saas_set_node_values(); for anonymous visitors nothing replaces them, and the space 48 * suppresses the placeholder in text inputs (email inputs strip whitespace per the HTML 49 * spec, hence the inconsistent look) and gets submitted as the field value. Only the 50 * value property is cleared â the value=" " attribute stays, since saas_set_node_values() 51 * uses it as the "not overwritten yet" gate for a later logged-in fill. 52 */ 53 clear_dynamic_default_values: function() { 54 55 var clear = function() { 56 [ ... document.querySelectorAll( 'input[data-node-default-value]' ) ].map( function( el ) { 57 if ( ' ' === el.value ) { 58 el.value = ''; 59 } 60 } ); 61 }; 62 63 // The script loads in the head, so wait for the DOM when needed. 64 if ( 'loading' === document.readyState ) { 65 document.addEventListener( 'DOMContentLoaded', clear ); 66 } 67 else { 68 clear(); 69 } 70 71 }, 72 73 perform_text_replacements: function() { 74 75 if ( ! this.performed_text_replacements ) { 76 77 if ( typeof saas_set_node_values === 'function' ) { 78 saas_set_node_values( this.get_user_data(), document.getElementById( 'main-content' ), false ); 79 this.performed_text_replacements = true; 80 } 81 82 // Try again later if the find/replace method is not ready yet. 83 else { 84 document.addEventListener( 'saas-search-ready', () => { 85 User_Token.perform_text_replacements(); 86 } ); 87 } 88 89 } 90 91 }, 92 93 init: function() { 94 95 // Set current tokens 96 var cookies = Object.fromEntries( document.cookie.split( '; ' ).map( v=>v.split( /=(.*)/s ).map( decodeURIComponent ) ) ); 97 this.set_user_id( cookies['keycloak_token--id'] ); 98 99 // Set user/page detail text replacements once 100 if ( typeof cookies['keycloak_token--data'] !== 'undefined' ) { 101 this.set_user_data( JSON.parse( cookies['keycloak_token--data'] ), true ); 102 } 103 104 // Use the keycloak token from cookie if there is one 105 if ( typeof cookies['keycloak_token--token'] !== 'undefined' ) { 106 this.set_token( cookies['keycloak_token--token'] ); 107 } 108 109 // Do a validity check on the token we have. Request a refresh token if needed. Schedule next check around token expiry. 110 if ( this.get_token() ) { 111 // Logged in: clear the silent-SSO reload guard so a later token expiry can re-probe. 112 this.set_sso_reloaded( false ); 113 this.refresh_token(); 114 } 115 116 // No token: the visitor is anonymous. Clear the {user.*} prefill markers so field 117 // placeholders show. (If a silent SSO probe finds a session below, the page reloads 118 // with token cookies set and the fields get filled on that load instead.) 119 else { 120 this.clear_dynamic_default_values(); 121 } 122 123 // If this page requires authentication... 124 if ( saas_user.page_requires_login ) { 125 126 // Redirect to keycloak if required token is missing. 127 if ( ! this.get_token() ) { 128 window.location.href = saas_user.login_url; 129 } 130 131 } 132 133 // On login-optional pages with no token, silently probe Keycloak for an existing SSO 134 // session via a hidden iframe (no login screen). If found, the probe reloads the page 135 // so the token is populated everywhere. If not, nothing happens and the next cold load 136 // probes again. The reload guard only prevents a reload loop, not the probe itself. 137 else if ( ! this.get_token() && ! this.get_sso_reloaded() ) { 138 this.silent_sso_check(); 139 } 140 141 }, 142 143 /** 144 * Read/write a session-scoped guard that marks we already reloaded once after a successful 145 * silent SSO probe. Prevents an infinite reload loop if cookies somehow fail to take. 146 */ 147 get_sso_reloaded: function() { 148 try { 149 return !! window.sessionStorage.getItem( 'saas-silent-sso-reloaded' ); 150 } 151 catch ( e ) { 152 return false;
153 } 154 }, 155 156 set_sso_reloaded: function( value ) { 157 try { 158 if ( value ) { 159 window.sessionStorage.setItem( 'saas-silent-sso-reloaded', '1' ); 160 } 161 else { 162 window.sessionStorage.removeItem( 'saas-silent-sso-reloaded' ); 163 } 164 } 165 catch ( e ) {} 166 }, 167 168 /** 169 * Silently probe Keycloak for an existing SSO session using a hidden iframe (prompt=none). 170 * The iframe callback sets the keycloak_token cookies when a session exists; we then reload 171 * once so the token is populated everywhere. No session = no action. 172 */ 173 silent_sso_check: function() { 174 175 if ( ! saas_user.silent_check_url ) { 176 return; 177 } 178 179 var expected_origin = new URL( saas_user.silent_check_url ).origin; 180 var iframe = document.createElement( 'iframe' ); 181 var done = false; 182 var timeout; 183 184 var finish = function() { 185 186 if ( done ) { 187 return; 188 } 189 done = true; 190 191 window.removeEventListener( 'message', on_message ); 192 clearTimeout( timeout ); 193 if ( iframe.parentNode ) { 194 iframe.parentNode.removeChild( iframe ); 195 } 196 197 // Re-read cookies: a session was found if the token cookie is now set. 198 var cookies = Object.fromEntries( document.cookie.split( '; ' ).map( v => v.split( /=(.*)/s ).map( decodeURIComponent ) ) ); 199 if ( typeof cookies['keycloak_token--token'] !== 'undefined' ) { 200 // Reload once so the token populates everywhere (incl. server-rendered chrome). 201 User_Token.set_sso_reloaded( true ); 202 window.location.reload(); 203 } 204 205 }; 206 207 var on_message = function( event ) { 208 if ( event.origin === expected_origin && event.data === 'saas-silent-sso-done' ) { 209 finish(); 210 } 211 }; 212 213 // Fail-safe: if Keycloak / the iframe never responds, clean up and stay anonymous. 214 timeout = setTimeout( finish, 5000 ); 215 window.addEventListener( 'message', on_message ); 216 217 iframe.style.display = 'none'; 218 iframe.setAttribute( 'aria-hidden', 'true' ); 219 iframe.src = saas_user.silent_check_url; 220 ( document.body || document.documentElement ).appendChild( iframe ); 221 222 }, 223 224 /** 225 * Get/refresh the user token by making an ajax request to the refresh endpoint. 226 * If successful, updates the token, user ID and user data cookies. 227 * Schedules next refresh check before token expiry. 228 * Redirects to login page if refresh fails and page requires authentication. 229 * Triggers 'refreshed-tokens' event when complete. 230 */ 231 refresh_token: function( force_refresh = false ) { 232 233 let xhr = new XMLHttpRequest(); 234 let endpoint = saas_user.refresh_token_endpoint; 235 if ( force_refresh ) { 236 endpoint += ( endpoint.includes( '?' ) ? '&' : '?' ) + 'force_refresh=1'; 237 } 238 239 xhr.onreadystatechange = function() { 240 if ( xhr.readyState === 4 && xhr.status === 200 ) { 241 242 let data; 243 if ( xhr.responseText ) { 244 data = JSON.parse( xhr.responseText ); 245 } 246 const data_is_empty_array = Array.isArray( data ) && data.length === 0; 247 const data_is_empty_object = typeof data === 'object' && Object.keys( data ).length === 0; 248 const data_is_empty = ! data || data_is_empty_array || data_is_empty_object; 249 250 // Compare current and new tokens 251 const refreshed = data_is_empty || ( User_Token.get_token() !== data.token ); 252 253 // Save new token and timeout for when it expires. 254 if ( ! data_is_empty ) { 255 256 // Set new data 257 User_Token.set_token( data.token ); 258 User_Token.set_user_id( data.user_id ); 259 260 // Set user/page detail text replacements once 261 User_Token.set_user_data( data.data, true ); 262 263 // Schedule a new check 30 seconds before expiry 264 setTimeout( User_Token.refresh_token, parseInt( data.expires_in, 10 ) * 1000 ); 265 266 } 267 268 // Session is gone: clear the {user.*} prefill markers so field placeholders show. 269 if ( data_is_empty ) { 270 User_Token.clear_dynamic_default_values(); 271 } 272 273 // Redirect to login page if the page requires the user to be logged in but they are not / no longer logged in. 274 //todo Check if the tab is in focus. If not, add focus event where we check the latest situation again. 275 if ( data_is_empty && saas_user.page_requires_login ) { 276 window.location.href = saas_user.login_url; 277 } 278 279 // Trigger event 280 if ( refreshed ) { 281 const event = new CustomEvent( 'refreshed-token', { data: data } ); 282 document.dispatchEvent( event ); 283 } 284 285 // Reload page on token force refresh 286 if ( force_refresh ) { 287 window.location.reload(); 288 } 289 290 } 291 }; 292 293 xhr.open( 'GET', endpoint ); 294 xhr.send(); 295 296 } 297 298}; 299 300User_Token.init(); 301 302// Handle group switching 303document.addEventListener( 'click', ( e ) => { 304 const group_id = e.target.getAttribute( 'data-user-group-switch' ); 305 if ( group_id ) { 306 307 e.preventDefault(); 308 const group_name = e.target.getAttribute( 'data-user-group-name' ); 309 310 // Remove group preference 311 if ( group_id === '*' ) { 312 document.cookie = 'keycloak_token--preferred-group=; SameSite=lax; Secure; path=/; expires=Thu, 01 Jan 1970 00:00:01 GMT'; 313 } 314 315 // Apply group preference 316 else { 317 document.cookie = 'keycloak_token--preferred-group=' + group_id + '; SameSite=lax; Secure; path=/'; 318 } 319 320 // Redirect + toast 321 //todo Toast via querystring only works for entity pages. 322 // if ( saas_ajax_template_parts && group_name ) { 323 if ( saas_ajax_template_parts ) { 324 qs = window.location.search; 325 // qs += qs ? '&' : '?'; 326 // qs += 'moddse-toast[style]=success'; 327 // qs += '&moddse-toast[title]=' + saas_user.translation.switched_to_group + group_name; 328 // qs += '&moddse-toast[description]=' + saas_user.translation.switched_to_group + group_name; 329 window.location = window.location.pathname + qs + window.location.hash; 330 } 331 332 } 333} );
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.