PageSourceSearch

https://www.knkv.nl/app/themes/saas-theme/assets/js/user-token.js?ver=200

js knkv.nl collected 2026-09-24 21:31:27 UTC 10,124 bytes, 333 lines download raw bytes

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.