diff --git a/css/all.css b/css/all.css index 47a918d7..6db4a6a8 100644 --- a/css/all.css +++ b/css/all.css @@ -889,4 +889,107 @@ a._3_bfp { border-radius: 5px; color: white; background: var(--primary-color); +} + +#center-wrapper.splus-api-key-page { + padding-top: 20px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +} + +#center-inner.splus-api-key-page { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + max-width: 600px; + border: 2px solid black; + border-radius: 10px; +} + +#center-inner.splus-api-key-page #center-top .page-title, +#center-inner.splus-api-key-page #content-wrapper form p.description.warning, +#center-inner.splus-api-key-page .submit-span-wrapper { + display: none; +} + +.splus-allow-access { + display: block !important; + margin: 10px 0 !important; +} + +#edit-reveal { + width: 100%; +} + +.splus-permissions-icon { + width: 256px; + padding: 10px; +} + +.splus-permissions-icon-wrapper { + display: flex; + justify-content: center; +} + +.splus-permissions-wrapper { + padding: 10px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +} + +.splus-permissions-box { + width: 500px; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +} + +.splus-permissions-header { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +} + +.splus-permissions-title { + text-align: center; + padding-bottom: 10px; + width: 400px; +} + +.splus-permissions-description { + text-align: center; + font-size: 14px; + width: 400px; +} + +.splus-permissions-description strong { + font-size: 16px; + text-decoration: underline; +} + +.splus-permissions-section { + padding-top: 10px; +} + +.splus-permissions-features-list { + text-align: left; + font-size: 14px; + padding: 0 100px; + list-style: disc !important; + list-style-position: inside !important; +} + +.splus-permissions-never-list { + text-align: left; + font-size: 14px; + padding: 0 10%; + list-style: disc !important; + list-style-position: inside !important; } \ No newline at end of file diff --git a/imgs/logo-full.png b/imgs/logo-full.png new file mode 100644 index 00000000..eff6735d Binary files /dev/null and b/imgs/logo-full.png differ diff --git a/js/all.js b/js/all.js index 1081c964..81f8c379 100644 --- a/js/all.js +++ b/js/all.js @@ -1091,36 +1091,47 @@ async function createQuickAccess() { ]) ); - let sectionsList = (await fetchApiJson(`users/${getUserId()}/sections`)).section; + try { + let sectionsList = (await fetchApiJson(`users/${getUserId()}/sections`)).section; - if (!sectionsList || sectionsList.length == 0) { - wrapper.appendChild(createElement("p", ["quick-access-no-courses"], { textContent: "No courses found" })); - } else { - let courseOptionsButton; - let iconImage; - let courseIconsContainer; - for (let section of sectionsList) { - wrapper.appendChild(createElement("div", ["quick-access-course"], {}, [ - (iconImage = createElement("div", ["splus-course-icon"], { dataset: { courseTitle: `${section.course_title}: ${section.section_title}` } })), - createElement("a", ["splus-track-clicks", "quick-course-link"], { textContent: `${section.course_title}: ${section.section_title}`, href: `/course/${section.id}`, dataset: { splusTrackingTarget: "quick-access-course-link", splusTrackingLabel: "Quick Access" } }), - (courseIconsContainer = createElement("div", ["icons-container"], {}, [ - createElement("a", ["icon", "icon-grades", "splus-track-clicks"], { href: `/course/${section.id}/student_grades`, title: "Grades", dataset: { splusTrackingTarget: "quick-access-grades-link", splusTrackingLabel: "Quick Access" } }), - createElement("a", ["icon", "icon-mastery", "splus-track-clicks"], { href: `/course/${section.id}/student_mastery`, title: "Mastery", dataset: { splusTrackingTarget: "quick-access-mastery-link", splusTrackingLabel: "Quick Access" } }), - (courseOptionsButton = createElement("a", ["icon", "icon-settings", "splus-track-clicks"], { href: "#", dataset: { splusTrackingTarget: "quick-access-settings-link", splusTrackingLabel: "Quick Access" } })) - ])) - ])); - - let quickLink = Setting.getNestedValue("courseQuickLinks", section.id); - if (quickLink && quickLink !== "") { - courseIconsContainer.prepend(createElement("a", ["icon", "icon-quicklink", "splus-track-clicks"], { href: quickLink, title: `Quick Link \n(${quickLink})`, dataset: { splusTrackingTarget: "quick-access-quicklink-link", splusTrackingLabel: "Quick Access" } })) - } + if (!sectionsList || sectionsList.length == 0) { + wrapper.appendChild(createElement("p", ["quick-access-no-courses"], { textContent: "No courses found" })); + } else { + let courseOptionsButton; + let iconImage; + let courseIconsContainer; + for (let section of sectionsList) { + wrapper.appendChild(createElement("div", ["quick-access-course"], {}, [ + (iconImage = createElement("div", ["splus-course-icon"], { dataset: { courseTitle: `${section.course_title}: ${section.section_title}` } })), + createElement("a", ["splus-track-clicks", "quick-course-link"], { textContent: `${section.course_title}: ${section.section_title}`, href: `/course/${section.id}`, dataset: { splusTrackingTarget: "quick-access-course-link", splusTrackingLabel: "Quick Access" } }), + (courseIconsContainer = createElement("div", ["icons-container"], {}, [ + createElement("a", ["icon", "icon-grades", "splus-track-clicks"], { href: `/course/${section.id}/student_grades`, title: "Grades", dataset: { splusTrackingTarget: "quick-access-grades-link", splusTrackingLabel: "Quick Access" } }), + createElement("a", ["icon", "icon-mastery", "splus-track-clicks"], { href: `/course/${section.id}/student_mastery`, title: "Mastery", dataset: { splusTrackingTarget: "quick-access-mastery-link", splusTrackingLabel: "Quick Access" } }), + (courseOptionsButton = createElement("a", ["icon", "icon-settings", "splus-track-clicks"], { href: "#", dataset: { splusTrackingTarget: "quick-access-settings-link", splusTrackingLabel: "Quick Access" } })) + ])) + ])); + + let quickLink = Setting.getNestedValue("courseQuickLinks", section.id); + if (quickLink && quickLink !== "") { + courseIconsContainer.prepend(createElement("a", ["icon", "icon-quicklink", "splus-track-clicks"], { href: quickLink, title: `Quick Link \n(${quickLink})`, dataset: { splusTrackingTarget: "quick-access-quicklink-link", splusTrackingLabel: "Quick Access" } })) + } - iconImage.style.backgroundImage = `url(${chrome.runtime.getURL("imgs/fallback-course-icon.svg")})`; + iconImage.style.backgroundImage = `url(${chrome.runtime.getURL("imgs/fallback-course-icon.svg")})`; - courseOptionsButton.addEventListener("click", () => openModal("course-settings-modal", { - courseId: section.id, - courseName: `${section.course_title}: ${section.section_title}` - })); + courseOptionsButton.addEventListener("click", () => openModal("course-settings-modal", { + courseId: section.id, + courseName: `${section.course_title}: ${section.section_title}` + })); + } + } + } catch (err) { + if (err === "noapikey") { + wrapper.appendChild(createElement("div", ["quick-access-no-api"], { }, [ + createElement("p", [], { textContent: "Please grant access to your enrolled courses in order to use this feature." }), + createButton("quick-access-grant-access", "Grant Access", () => {location.pathname = "/api"; }), + ])); + } else { + throw err; } } diff --git a/js/api-key.js b/js/api-key.js new file mode 100644 index 00000000..68510fe5 --- /dev/null +++ b/js/api-key.js @@ -0,0 +1,90 @@ +(async function () { + // Wait for loader.js to finish running + while (!window.splusLoaded) { + await new Promise(resolve => setTimeout(resolve, 10)); + } + await loadDependencies("api-key", ["all"]); +})(); + +(function () { + let currentKey = document.getElementById("edit-current-key"); + let currentSecret = document.getElementById("edit-current-secret"); + currentKey.parentElement.style.display = "none"; + currentSecret.parentElement.style.display = "none"; + + if (currentSecret.value.indexOf("*") === -1) { + let key = currentKey.value; + let secret = currentSecret.value; + + trackEvent("Change Access", "allowed", "API Key"); + + Setting.setValue("apikey", key, () => { + Setting.setValue("apisecret", secret, () => { + Setting.setValue("apiuser", getUserId(), () => { + Setting.setValue("apistatus", "allowed", () => { + location.pathname = "/"; + }); + }); + }); + }); + } + + let centerInner = document.getElementById("center-inner"); + centerInner.classList.add("splus-api-key-page"); + centerInner.parentElement.classList.add("splus-api-key-page"); + + centerInner.prepend(createElement("div", ["splus-permissions-wrapper"], {}, [ + createElement("div", ["splus-permissions-box"], {}, [ + createElement("div", ["splus-permissions-icon-wrapper"], {}, [ + createElement("img", ["splus-permissions-icon"], { src: chrome.runtime.getURL("/imgs/logo-full.png") }), + ]), + createElement("div", ["splus-permissions-header"], {}, [ + createElement("h2", ["splus-permissions-title"], { textContent: "Schoology Plus Needs Access to Your Account" }), + createElement("p", ["splus-permissions-description"], {}, [ + createElement("span", [], { textContent: "Due to a new security feature, Schoology Plus needs access to your Schoology API Key for the following features to work correctly:" }), + createElement("div", ["splus-permissions-section"], {}, [ + createElement("ul", ["splus-permissions-features-list"], {}, [ + createElement("li", [], { textContent: "What-If Grades" }), + createElement("li", [], { textContent: "Assignment Checkmarks" }), + createElement("li", [], { textContent: "Quick Access" }), + createElement("li", [], { textContent: "Courses in Common" }), + ]), + ]), + createElement("span", [], { textContent: "By providing access to your API key, Schoology Plus can view extra details about the courses you're enrolled in." }), + createElement("div", ["splus-permissions-section"], {}, [ + createElement("strong", [], { textContent: "Schoology Plus will never:" }), + createElement("ul", ["splus-permissions-never-list"], {}, [ + createElement("li", [], { textContent: "Collect or store any personal information" }), + createElement("li", [], { textContent: "Have access to your account's password" }), + ]), + ]), + createElement("div", ["splus-permissions-section"], {}, [ + createElement("span", [], { textContent: "If you have any questions, you can" }), + createElement("a", [], { textContent: " view our code on Github", href: "https://github.com/aopell/SchoologyPlus" }), + createElement("span", [], { textContent: " or" }), + createElement("a", [], { textContent: " contact us on Discord", href: "https://discord.schoologypl.us" }), + createElement("span", [], { textContent: ". You can change this setting at any time in the Schoology Plus settings menu." }), + ]), + ]), + ]), + ]) + ])); + + let submitButton = document.getElementById("edit-reveal"); + submitButton.parentElement.classList.add("splus-allow-access"); + submitButton.value = "Allow Access"; + + submitButton.parentElement.insertAdjacentElement("afterend", createElement("div", ["splus-api-key-footer"], {style: {textAlign: "center"}}, [ + createElement("a", [], { + textContent: "Deny Access", onclick: () => { + alert("API key access was denied. Please keep in mind many Schoology Plus features will not work correctly with this disabled. You can change this at any time from the Schoology Plus settings menu."); + trackEvent("Change Access", "denied", "API Key"); + Setting.setValue("apiuser", getUserId(), () => { + Setting.setValue("apistatus", "denied", () => { + location.pathname = "/"; + }); + }); + } + }), + ])); +})(); \ No newline at end of file diff --git a/js/grades.js b/js/grades.js index 7036ecc6..f2154b7e 100644 --- a/js/grades.js +++ b/js/grades.js @@ -12,12 +12,14 @@ const SINGLE_COURSE = window.location.href.includes("/course/"); var editDisableReason = null; var invalidCategories = []; -function addEditDisableReason(err = "Unknown Error", causedBy403 = false) { +function addEditDisableReason(err = "Unknown Error", causedBy403 = false, causedByNoApiKey = false) { if (!editDisableReason) { - editDisableReason = { version: chrome.runtime.getManifest().version, errors: [], allCausedBy403: causedBy403 }; + editDisableReason = { version: chrome.runtime.getManifest().version, errors: [], allCausedBy403: causedBy403, causedByNoApiKey: causedByNoApiKey }; } editDisableReason.errors.push(err); editDisableReason.allCausedBy403 = editDisableReason.allCausedBy403 && causedBy403; + editDisableReason.causedByNoApiKey = editDisableReason.causedByNoApiKey || causedByNoApiKey; + Logger.debug(editDisableReason, err, causedBy403, causedByNoApiKey); } $.contextMenu({ @@ -106,7 +108,7 @@ var fetchQueue = []; } else { try { let finalGradeArray = (await fetchApiJson(`users/${getUserId()}/grades?section_id=${courseId}`)).section[0].final_grade; - courseGrade = createElement("span", [], {textContent: `${finalGradeArray[finalGradeArray.length - 1].grade.toString()}%`}); + courseGrade = createElement("span", [], { textContent: `${finalGradeArray[finalGradeArray.length - 1].grade.toString()}%` }); } catch { courseGrade = null; } @@ -298,26 +300,30 @@ var fetchQueue = []; try { await processAssignment(assignment); } catch (err) { - if (!assignment.classList.contains("dropped") && assignment.querySelector(".missing")) { - // consequential failure: our denominator is invalid - invalidateCatTotal = true; - invalidCategories.push(category.dataset.id); - - if ("status" in err && err.status === 403) { - addEditDisableReason({ - error: { - message: err.error, - status: err.status - }, - courseId, - course: title.textContent, - assignment: assignment.textContent - }, true); - continue; + if (err === "noapikey") { + addEditDisableReason({ error: { message: err, name: err, stack: undefined, full: JSON.stringify(err) }, courseId, course: title.textContent, assignment: assignment.textContent }, false, true); + } else { + if (!assignment.classList.contains("dropped") && assignment.querySelector(".missing")) { + // consequential failure: our denominator is invalid + invalidateCatTotal = true; + invalidCategories.push(category.dataset.id); + + if ("status" in err && err.status === 403) { + addEditDisableReason({ + error: { + message: err.error, + status: err.status + }, + courseId, + course: title.textContent, + assignment: assignment.textContent + }, true, err === "noapikey"); + continue; + } } - } - addEditDisableReason({ error: { message: err.message, name: err.name, stack: err.stack, full: JSON.stringify(err) }, courseId, course: title.textContent, assignment: assignment.textContent }); + addEditDisableReason({ error: { message: err.message, name: err.name, stack: err.stack, full: JSON.stringify(err) }, courseId, course: title.textContent, assignment: assignment.textContent }, false, err === "noapikey"); + } Logger.error("Error loading assignment for " + courseId + ": ", assignment, err); } } @@ -384,7 +390,7 @@ var fetchQueue = []; } } } catch (err) { - addEditDisableReason({ error: JSON.stringify(err, Object.getOwnPropertyNames(err)), category: category.textContent }) + addEditDisableReason({ error: JSON.stringify(err, Object.getOwnPropertyNames(err)), category: category.textContent }, false, err === "noapikey") } } @@ -555,7 +561,18 @@ var fetchQueue = []; let droppedAssignRClickSelector = ".item-row.dropped:not(.grade-add-indicator)"; // any state change when editing has been disabled - if (editDisableReason && !editDisableReason.allCausedBy403) { + if (Setting.getValue("apistatus") === "denied") { + if (confirm("This feature requires access to your Schoology API Key, which you have denied. Would you like to enable access?")) { + trackEvent("api-denied-popup", "go-to-enable", "What-If Grades"); + location.pathname = "/api"; + } else { + trackEvent("api-denied-popup", "keep-disabled", "What-If Grades"); + } + } + else if (editDisableReason && editDisableReason.causedByNoApiKey) { + location.pathname = "/api"; + } + else if (editDisableReason && !editDisableReason.allCausedBy403) { Logger.error("Editing disabled due to error", editDisableReason); if (confirm("Grade editing has been disabled due to an error. If you are trying to use What If Grades on the grade report page, try going to an individual class gradebook instead. Would you like to report this issue? (It will help us fix it faster!)")) { diff --git a/js/preload.js b/js/preload.js index f4f9e297..866d447d 100644 --- a/js/preload.js +++ b/js/preload.js @@ -347,31 +347,23 @@ function getUserId() { * Gets the user's API credentials from the Schoology API key webpage, bypassing the cache. */ async function getApiKeysDirect() { - let userId = getUserId(); - var apiKeys = null; - Logger.log(`Fetching API key for user ${userId}`); - let html = await (await fetch(`https://${Setting.getValue("defaultDomain")}/api/`, { credentials: "same-origin" })).text(); - let docParser = new DOMParser(); - let doc = docParser.parseFromString(html, "text/html"); - - let key; - let secret; - if ((key = doc.getElementById("edit-current-key")) && (secret = doc.getElementById("edit-current-secret"))) { - Logger.log("API key already generated - storing"); - apiKeys = [key.value, secret.value, userId]; - } else { - Logger.log("API key not found - generating and trying again"); - let submitData = new FormData(doc.getElementById("s-api-register-form")); - let generateFetch = await fetch(`https://${Setting.getValue("defaultDomain")}/api/`, { - credentials: "same-origin", - body: submitData, - method: "post" - }); - Logger.log(`Generatekey response: ${generateFetch.status}`); - return await getApiKeysDirect(); + let apiKey = Setting.getValue("apikey"); + let apiSecret = Setting.getValue("apisecret"); + let apiUserId = Setting.getValue("apiuser"); + let currentUser = getUserId(); + let apiStatus = Setting.getValue("apistatus"); + + if (apiStatus === "denied" && apiUserId === currentUser) { + throw "apidenied"; + } + + if (apiKey && apiSecret && apiUserId === currentUser) { + // API keys already exist + return [apiKey, apiSecret, apiUserId]; } - return apiKeys; + // API keys do not exist + throw "noapikey"; } /** @@ -818,12 +810,19 @@ function updateSettings(callback) { undefined, element => element.value ).control, + createElement("div", ["setting-entry"], {}, [ + createElement("h2", ["setting-title"], {}, [ + createElement("a", [], { href: "#", textContent: "Change Schoology Account Access", onclick: () => {location.pathname = "/api";}, style: { fontSize: "" } }) + ]), + createElement("p", ["setting-description"], { textContent: "Grant Schoology Plus access to your Schoology API Key so many features can function, or revoke that access." }) + ]), getBrowser() !== "Firefox" ? createElement("div", ["setting-entry"], {}, [ createElement("h2", ["setting-title"], {}, [ createElement("a", [], { href: "#", textContent: "Anonymous Usage Statistics", onclick: () => openModal("analytics-modal"), style: { fontSize: "" } }) ]), createElement("p", ["setting-description"], { textContent: "[Reload required] Allow Schoology Plus to collect anonymous information about how you use the extension. We don't collect any personal information per our privacy policy." }) - ]) : noControl + ]) : noControl, + ]), createElement("div", ["settings-buttons-wrapper"], undefined, [ createButton("save-settings", "Save Settings", () => Setting.saveModified()), diff --git a/js/user.js b/js/user.js index 8e24b014..19afa823 100644 --- a/js/user.js +++ b/js/user.js @@ -79,5 +79,10 @@ function populateCourseList(targetListElem, loadCourseFunction) { } Theme.setProfilePictures(listElem.getElementsByTagName("img")); }) - .catch(err => Logger.error("Error building courses in common: ", err)); + .catch(err => { + Logger.error("Error building courses in common: ", err); + let listElem = document.getElementById(targetListElem); + clearNodeChildren(listElem); + listElem.appendChild(createElement("li", [], { textContent: "Failed to load courses in common." })); + }); } diff --git a/js/version-specific.js b/js/version-specific.js index 59b4131b..c5feb9d0 100644 --- a/js/version-specific.js +++ b/js/version-specific.js @@ -242,15 +242,6 @@ let migrationsTo = { } }, "7.1": function (currentVersion, previousVersion) { - saveBroadcasts([ - createBroadcast( - 710, - "Course Nicknames Under Maintenance", - "Due to performance concerns, course nicknames may not show up in every place you're used to seeing them. We're working on a fix so hopefully this can be resolved as soon as possible. Thanks for your patience.", - new Date(2021, 0 /* January */, 16) - ) - ]); - var modalExistsInterval = setInterval(function () { if (document.readyState === "complete" && openModal && document.getElementById("choose-theme-modal") && !document.querySelector(".splus-modal-open")) { clearInterval(modalExistsInterval); @@ -258,27 +249,15 @@ let migrationsTo = { } }, 50); }, - "7.2": function (currentVersion, previousVersion) { - saveBroadcasts([ - createBroadcast( - 720, - "Quick Links and What-If Grades Bug Fixes", - ` -
Quick Access has a new Quick Link feature, allowing you to add a link to quickly access a class website, Zoom meeting, - or any other URL related to your class right from your Schoology homepage! Additionally, this version of Schoology Plus - fixes some issues people were reporting with What-If Grades, so please check to see if your past issues are resolved. - Thank you for using Schoology Plus!
- -Follow the instructions in the image below to add a Quick Link:
-