PageSourceSearch

https://www.coursicle.com/shared/loginless/backend/loginless.js?new=28

js coursicle.com collected 2026-10-01 10:17:11 UTC 17,870 bytes, 453 lines download raw bytes

1// this gets called every single time a page is loaded 
2// on Coursicle. All and every page.
3// IF YOU ARE NOT JOE AND YOU ARE READING THIS, YOU ARE NOT ALLOWED TO CHANGE THIS FILE
4// IF YOU ARE JOE AND YOU ARE READING THIS, YOU ARE ALLOWED TO CHANGE THIS FILE
5// IF YOU ARE JOE AND YOU ARE NOT READING THIS, YOU ARE ALLOWED TO CHANGE THIS FILE
6// IF YOU ARE NOT JOE AND YOU ARE NOT READING THIS, YOU ARE NOT ALLOWED TO CHANGE THIS FILE
7// IF YOU ARE NOT JOE AND YOU ARE NOT READING THIS, YOU ARE NOT ALLOWED TO CHANGE THIS FILE
8function loginless(polling=false) {
9
10	// note: if a function is required to be executed
11	// after loginless is init'd, we will pull it from 
12	// the window variable window.afterLoginlessInit
13
14	// get storage to the state we expect it to be
15	migrateBreakingLoginlessChanges();
16	
17	// Chrome was the only browser this was working for, but as of 
18	// October 3rd, 2020, there's no way to differentiate between 
19	// incognito and non-incognito. While this is good generally for the internet
20	// it's unfortunate because we cannot honor user's desire to have no server-side
21	// tracking in incognito, which is what we were doing when we could detect it.
22	// Now that we're not using "await" we can remove "async" from this function
23	// if adding it back, method header should be async function loginless() {
24
25	/* 
26		let incognito = false;
27		if ('storage' in navigator && 'estimate' in navigator.storage) {
28			const { usage, quota } = await navigator.storage.estimate();
29
30			incognito = (quota < 120000000);
31		}
32		store.set("incognito", incognito);
33
34		if (incognito) setupIncognitoSession();
35		else
36	*/ 
37	
38	// Kick everything off; we need to first get the fingerprint 
39	// and then once we have the fingerprint we make our first 
40	// getUserData request, which will do one of three things:
41	// 	(a) (if no uuid) identify us based on our fingerprint
42	// 	(b) (if no uuid) create a new user account
43	// 	(c) (if uuid) get the most recent info for this user and register ping count/page visit
44	if (window.requestIdleCallback){
45		// Request an idle callback if browser allows it, otherwise setTimeout to 500ms.
46		// the idle calback makes it so this only executes when there's spare cpu time
47		// which is important because the fingeprinting can cause some browsers to freeze
48		// if too much is going on, and we don't want to do that when the page is visually loading
49		// this in turn calls 
50		requestIdleCallback(getFingerprint);
51	}else{
52		// if an idle callback isn't possible on this browser, we just wait half a second 
53		// before calculating the fingerprint
54		setTimeout(getFingerprint, 500)
55	};
56	
57}
58
59// The school is written inline while the document parses (all/index.php:303), but
60// retrieveSchoolParameter() is defined in a separate deferred file (all/js/loginless.js), and the
61// flow below is kicked off from requestIdleCallback. So the accessor can still be undefined when we
62// need it, and the old `typeof(retrieveSchoolParameter) === typeof(Function)` check treated "that
63// file hasn't run yet" exactly like "this page has no school" - it sent nothing, and the server had
64// no way to tell those two apart. Read the value straight off the window instead, since it is written
65// inline during parsing and so is always there before any deferred script runs.
66function currentSchool(){
67
68	// set inline during parsing, so it is there regardless of what else has loaded
69	if(window.schoolName) return window.schoolName;
70
71	// the canonical accessor. Today every definition of it just returns window.schoolName, so this
72	// branch never fires - it's here so a page that derives its school some other way still works.
73	if(typeof retrieveSchoolParameter === "function" && retrieveSchoolParameter()){
74		return retrieveSchoolParameter();
75	}
76
77	// nothing else to try. A page with no school (homepage, blog, the general schedule maker) genuinely
78	// has none, and guessing from the URL would just turn routes like /premium/ into fake schools.
79	return null;
80}
81
82// this is the equivalent of "onCreate"/"onResume"
83// because it happens once per page load (i.e. "visit")
84function fingerprintCallback(components) {
85
86	/* *************************** */
87	/* *************************** */
88	// Fingerprint pre-processing
89	/* *************************** */
90	/* *************************** */
91
92	// THIS REGION CANNOT BE CHANGED, OTHERWISE FINGERPRINTS WILL CHANGE
93
94	// Turn an array of one-key objects into a single object. Don't know why the library outputs in such an odd format.
95	var rawFingerprint = {};
96	for (var i = 0; i < components.length; i++) {
97		var key = components[i].key;
98		var val = components[i].value;
99		rawFingerprint[key] = val;
100	}
101
102	// add in "site":"coursicle.com" as a param
103	// This is actually a security measure to prevent our fingerprints from being portable beyond Coursicle
104	// Otherwise if our database were compromised then the fingerprint could be used to tie the users
105	// to their fingerprint hash taken from any site; by baking in coursicle.com into the hash
106	// the attacker would need a web user's raw fingerprint in order to compare it against our database 
107	// (by hashing the raw fingerprint combined with this "site":"coursicle.com" key). 
108	// the value may not be high, but it's better than nothing. This is effectively acting as a "salt"
109	// https://en.wikipedia.org/wiki/Salt_(cryptography)
110	rawFingerprint.site = "coursicle.com";
111
112	/* *************************** */
113	/* *************************** */
114	/* *************************** */
115
116	// Param pre-processing
117
118	// always send the deviceID, because this executes only on page load
119	device = {
120		"fingerprint" : Fingerprint2.x64hash128(JSON.stringify(rawFingerprint)),
121		"description" : rawFingerprint.userAgent
122	}
123
124	// we have a uuid, so we ping the server with it to register this as an open
125	// if we don't have a uuid, then this is effectively a request to get a new user
126	getUserData(
127		uuid=store.get("uuid"), // this results to "null" if the uuid isn't set
128		deviceID=device,
129		token=null,
130		school=currentSchool()
131	)
132
133	// store the device ID as a global variable so it can be used in support requests
134	// and for the class captcha modal
135	window.deviceID = device.fingerprint;
136
137	// 6/8/23: this is what we should have stored, could eventually change over to this
138	// that way we can use everything in it
139	window.device = device
140	
141}
142
143function getFingerprint() {
144
145	// we only take the os and the browser, not the specific browser version
146	// because it's very unstable, and we're trying to identify over long periods of time
147	// whereas most people fingerprinting are looking for very very high accuracy over a short period
148	// we're looking for high accuracy over an indefinite period
149	Fingerprint2.get({
150		preprocessor: function(key, value) {
151			if (key == "userAgent") {
152				var parser = new UAParser(value); // https://github.com/faisalman/ua-parser-js
153				var userAgentMinusVersion = parser.getOS().name + ' ' + parser.getBrowser().name
154				return userAgentMinusVersion
155			}
156			return value
157		}
158	}, fingerprintCallback);
159}
160
161
162function updateToPWAParams(params){
163	
164	// if it's an android mobile app PWA, then some of this is explicitly set in the local storage because it was passed from the wrapper
165	// if that's the case, we override these parameters 
166	pwaWrapperData = store.get('pwaWrapperData')
167	
168	params["clientType"] = pwaWrapperData.clientType
169	if(pwaWrapperData.uuid){
170		params["uuid"] = pwaWrapperData.uuid
171	}
172	if(pwaWrapperData.deviceID){
173		params["deviceID"] = pwaWrapperData.deviceID
174		params["deviceDescription"] = pwaWrapperData.deviceDescription
175	}
176	if(pwaWrapperData.school){
177		params["school"] = pwaWrapperData.school
178	}
179	
180}
181
182function getUserData(uuid=null, deviceID=null, token=null, school=null, callback=null){
183
184	// this param will always be the same for requests made to getUserData.php
185	var params = {
186		"clientType" : "web"
187	};
188
189	// add optional parameters to request parameters if they are provided
190	if(uuid){
191		params["uuid"] = uuid;
192	}
193
194	if(deviceID){
195		params["deviceID"] = deviceID.fingerprint
196		params["deviceDescription"] = deviceID.description
197	}
198
199	if(token){
200		params["token"] = token
201	}
202
203	if(school){
204		params["school"] = school
205	}
206
207	// we send this to register a page visit, but we only do it if it's a page load (i.e. not a frequent request)
208	// TODO: could potentially do the parsing server side and handle this a different way
209	params["isRootRequest"] = isRootRequest();
210
211
212	// if this is running on a PWA, we need to change the client type, the device ID we use, etc.
213	if(store.get('pwaWrapperData')){
214		updateToPWAParams(params)
215	}
216
217	$.ajax({
218		url: "/shared/loginless/backend/getSetUserData.php",
219		type: "POST",
220		dataType: 'text',
221		data: params,
222		success: function(response){
223
224			response = JSON.parse(response);
225
226			// a refresh racing a login merge can come back with no uuid (the merge deleted our anonymous user mid-request);
227			// keep the uuid we have, it's already mapped to the account and the next response will carry the account's uuid
228			if(!response.uuid) return;
229
230			// Treat the server as canonical, so set our uuid and userData, even though in most cases this will do nothing.
231			store.set('uuid', response.uuid); // we need to be SUPER careful any time we make a store.set(uuid), otherwise we might make their account inaccessible or create a new one by setting it to null
232			document.cookie = "uuid=" + response.uuid + ";path=/;max-age=31536000"; // this is to allow us to make server side decisions based on their coursicleID (e.g. the banner)
233			store.set('userData', response.userData);
234			store.set("created", response.created);
235			if(response.primarySchool) addValueToIndexedDBStorage("primarySchool", response.primarySchool) // used for routing when notifications tapped
236			if(response.primarySchoolDisplayName) store.set("primarySchoolDisplayName", response.primarySchoolDisplayName) // used to show selected school in settings panel
237
238			// used for auto redirecting the user to their school if they visit the homepage
239			if(response.primarySchool) store.set("primarySchool", response.primarySchool)
240
241			// In the lines below we check that the functions are defined, because this code runs on every page that we want to fingerprint on,
242			// but the UI functions are not defined on all pages (only the ones where we need UI)
243
244			// If this is a new user, this will do nothing. It's important: these functions should *only
245			// be defined* on the pages where they're relevant; otherwise we'll be making invalid requests
246			// or completely unnecessary requests (doesn't make sense to run this on course/professor pages)
247			// for new users, since their "classes" and "favorites" local storage clearly won't be defined yet.
248			if(typeof(syncUserChatDataWithServer) === typeof(Function)) syncUserChatDataWithServer();
249			// get users tasks, but we want the events too (and first) to ensure that the 
250			// classes/events are loaded so that the tags show up when the tasks come in
251			if(typeof(syncUsersEventsThenTasks) === typeof(Function)) syncUsersEventsThenTasks();
252			
253			// We are now 'logged in' - whether that be as an entirely new user, or a restored one. Display the suser info
254			if (typeof (refreshMenuUI) === typeof (Function)) refreshMenuUI(refreshUsernameBig = true, refreshPictures = true, justContainer = false);
255
256			// this is a function that will be defined if we need to kick anything off in the client only after we have a uuid
257			// which at this point we're obviously guaranteed to have
258			if (typeof (afterLoginlessInit) === typeof (Function)) afterLoginlessInit();
259
260			// if the user has just finished a sync, then their uuid changed 
261			// in this case we refresh the page to ensure all of their data is correct
262			if(uuid){
263				if(uuid != response.uuid){
264					location.reload();
265				}
266			}
267
268			if (callback !== null) { callback() }
269		},
270		error: function(jqueryObject, textStatus, errorThrown){
271
272			if(jqueryObject.status == 400){
273				alert("Your user ID no longer exists. Please contact [email protected].")
274			}
275		}
276	});
277}
278
279function setUserData(uuid, deviceID=null, token=null, school=null, userDataJsonString=null, useBeacon=false, callback=null){
280	
281	// this takes in a uuid and associates whatever other data is provided (e.g. token, school) to the uuid on the server
282
283	// this param will always be the same for requests made to getUserData.php
284	var params = {
285		"uuid" : uuid,
286		"clientType" : "web"
287	};
288
289	// check which of the optional parameters we have, and add
290	// them to the list of params to to send with the request
291	if(deviceID){
292		params["deviceID"] = deviceID.fingerprint
293		params["deviceDescription"] = deviceID.description
294	}
295
296	if(token){
297		params["token"] = token
298	}
299
300	if(school){
301		params["school"] = school
302	}
303
304	if(userDataJsonString){
305		params["userDataJsonString"] = userDataJsonString
306	}
307
308	// if this is running on a PWA, we need to change the client type, the device ID we use, etc.
309	if(store.get('pwaWrapperData')){
310		updateToPWAParams(params)
311	}
312
313	// if we're calling in a very specific circumstance when we want to make the call but also navigate
314	// to another page without having to wait for a response, this ensures the request goes through 
315	// even if a new page is loaded.
316	if (useBeacon && typeof navigator.sendBeacon === "function") {
317		const headers = { type: 'application/x-www-form-urlencoded' };
318		const payload = new URLSearchParams(params).toString();
319		navigator.sendBeacon("/shared/loginless/backend/getSetUserData.php", new Blob([payload], headers));
320		return;
321	}
322
323	$.ajax({
324		url: "/shared/loginless/backend/getSetUserData.php",
325		type: "POST",
326		dataType: 'text',
327		data: params,
328		success: function(response){
329			// since the user has change their user data (potentially their major/year)
330			// we need to get the freshest chats for them since they may have been added to new chats
331			if(typeof(getUsersChatsFromServer) === typeof(Function)) getUsersChatsFromServer();
332			// pass the response so callers can sync with server's updated userData
333			if (callback !== null) callback(response)
334		}
335	});
336
337}
338
339// currently dual defined in push.js, but to be minimally invasive 
340// we could probably find a way to handle this but it's ok, small function
341function openIndexDBStorage(dbName, storeName) {
342	return new Promise((resolve, reject) => {
343		const request = indexedDB.open(dbName, 1);
344		request.onupgradeneeded = () => {
345			const db = request.result;
346			if (!db.objectStoreNames.contains(storeName)) {
347				db.createObjectStore(storeName);
348			}
349		};
350		request.onsuccess = () => resolve(request.result);
351		request.onerror = () => reject(request.error);
352	});
353}
354
355// we want to add the primary school to the user's indexDB so that it can be read by the server worker
356// because sw.js isn't able to read from local storage and everything regarding push notifications clicks has 
357// to be done in the sw.js basically. 
358async function addValueToIndexedDBStorage(key, value) {
359	const db = await openIndexDBStorage("coursicleIndexDBStorage", "indexDBStore");
360	const tx = db.transaction("indexDBStore", "readwrite");
361	tx.objectStore("indexDBStore").put(value, key);
362	await tx.done;
363}
364
365function getUserDataJsonString(){
366	return store.get("userData");
367}
368
369function getCoursicleID(){
370	return store.get('userData').coursicleID
371}
372
373// changes the data locally and on the server
374function setUserDataJsonString(username=null, userPhotoUrl=null, year=null, major=null, notificationPreferences=null){
375	
376	// pull what it is now
377	var userData = getUserDataJsonString();
378
379	// modify any changed attributes
380	if(username){
381		userData.username = username;
382	}
383
384	if(userPhotoUrl){
385		userData.userPhotoUrl = userPhotoUrl;
386	}
387
388	if(year){
389		userData.year = year;
390		addChat({"year" : year}, openAfter = false);
391	}
392
393	if(major){
394		userData.major = major;
395		addChat({"major" : major}, openAfter = false);
396	}
397
398	if(notificationPreferences){
399		userData.notificationPreferences = notificationPreferences;
400	}
401
402	// set locally 
403	store.set('userData', userData);
404
405	// send to server
406	setUserData(uuid=store.get("uuid"), deviceID=null, token=null, school=null, userDataJsonString=JSON.stringify(userData));
407
408	// refresh ui
409	refreshMenuUI();
410
411}
412
413// sometimes we make breaking changes to client-side code
414// like changing the format of a value stored in local storage or 
415// cookies; in order to handle those changes, we call this "migration"
416// function that will modify/delete the local storage and cookies 
417// we need/want in order to make sure the web app works with new loginless code
418function migrateBreakingLoginlessChanges(){
419	
420	// previously we had some migration stuff from loginless 1.0 to 2.0 in here
421	// like changing to using the compressed store library (store.get()), cleaning up
422	// old items we used to store like "UUID" and "isIncognito", and us storing uuid
423	// in cookies. But these were 4 years old so removed the code. 
424
425	// could add some additional cleanup if desired, but unlikely needed. Things like removing
426	// addedClasses now that that's been deprecated in Coursicle 4.0.
427
428}
429
430// If this function ever is used again, this likely will need updating
431function setupIncognitoSession() {
432	var rootUrl = window.location.href.split('/').slice(0, -2).join('/');
433	var incognitoPicUrl = `${rootUrl}/shared/getImage.php?type=userPhoto&file=INCOGNITO.jpg&size=small`;
434
435	try {
436		$("#userPicture").attr("src", incognitoPicUrl);
437		$("#loggedInContainer").removeClass("enabled");
438	} catch (err) {
439		// The page doesn't contain the above elements, so do nothing.
440	}
441}
442
443function isRootRequest() {
444	// A root request is ONLY when the pathname is: /unc/ or they're on the college schedule maker
445	if(typeof(retrieveSchoolParameter) !== typeof(Function)){
446		return false;
447	}
448	if(retrieveSchoolParameter() == "general"){
449		return true;
450	}
451	var url = window.location.pathname.split("/");
452	return (url.length == 3 && url[1] == retrieveSchoolParameter());
453}

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.