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.