mirror of
https://github.com/GuySandler/CanvasRefined.git
synced 2026-11-03 13:57:17 +01:00
slight changes + changes to auto detect
This commit is contained in:
parent
8e02f174c2
commit
8b75c74380
@ -294,7 +294,7 @@
|
|||||||
|
|
||||||
.canvasrefined-gs-modal {
|
.canvasrefined-gs-modal {
|
||||||
position: fixed; inset: 0; z-index: 2147483100;
|
position: fixed; inset: 0; z-index: 2147483100;
|
||||||
background: rgba(0,0,0,.42);
|
background: rgba(0,0,0,.26);
|
||||||
display: flex; align-items: flex-start; justify-content: center;
|
display: flex; align-items: flex-start; justify-content: center;
|
||||||
padding-top: 12vh; animation: canvasrefined-gs-fade .12s ease;
|
padding-top: 12vh; animation: canvasrefined-gs-fade .12s ease;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1446,6 +1446,14 @@ td.status [class*="-pill"]:hover,
|
|||||||
background: var(--bcbackground-1) !important;
|
background: var(--bcbackground-1) !important;
|
||||||
color: var(--bctext-0) !important;
|
color: var(--bctext-0) !important;
|
||||||
}
|
}
|
||||||
|
/* CanvasRefined's global search overlay is a full-screen scrim, not a panel.
|
||||||
|
The [class$="-modal"] rule above would otherwise paint it solid chrome and
|
||||||
|
cover the whole page in an opaque block. Keep it a light dim instead; the
|
||||||
|
dialog card inside carries its own background. */
|
||||||
|
.canvasrefined-gs-modal {
|
||||||
|
background: rgba(0, 0, 0, 0.52) !important;
|
||||||
|
border-color: transparent !important;
|
||||||
|
}
|
||||||
[class*="-mask"] {
|
[class*="-mask"] {
|
||||||
background: rgba(0, 0, 0, 0.60) !important;
|
background: rgba(0, 0, 0, 0.60) !important;
|
||||||
}
|
}
|
||||||
|
|||||||
153
js/content.js
153
js/content.js
@ -720,18 +720,39 @@ function showExampleReminder() {
|
|||||||
// Canvas detection: empty list auto-adds the first canvas, otherwise ask once per domain.
|
// Canvas detection: empty list auto-adds the first canvas, otherwise ask once per domain.
|
||||||
isDomainCanvasPage();
|
isDomainCanvasPage();
|
||||||
|
|
||||||
|
// Saved entries are whatever the user/prompt put in storage ("school.instructure.com",
|
||||||
|
// "https://school.instructure.com", "https://school.instructure.com/", ...). Matching
|
||||||
|
// used to be a raw substring test, so a stored URL with a scheme or trailing slash
|
||||||
|
// never matched the origin — the extension looked like it wasn't detecting the site.
|
||||||
|
// Compare hostnames instead, and let a parent domain cover its subdomains.
|
||||||
|
function normalizeDomain(value) {
|
||||||
|
return String(value || "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/^[a-z][a-z0-9+.-]*:\/\//, "")
|
||||||
|
.replace(/[/?#].*$/, "")
|
||||||
|
.replace(/^www\./, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function domainMatches(entry) {
|
||||||
|
const saved = normalizeDomain(entry);
|
||||||
|
if (!saved) return false;
|
||||||
|
const here = normalizeDomain(domain) || normalizeDomain(location.host);
|
||||||
|
return here === saved || here.endsWith("." + saved);
|
||||||
|
}
|
||||||
|
|
||||||
function isDomainCanvasPage() {
|
function isDomainCanvasPage() {
|
||||||
chrome.storage.sync.get(['custom_domain', 'custom_domain_denied', 'auto_detect_disabled', 'dark_mode', 'dark_preset', 'device_dark', 'remind'], result => {
|
chrome.storage.sync.get(['custom_domain', 'custom_domain_denied', 'auto_detect_disabled', 'dark_mode', 'dark_preset', 'device_dark', 'remind'], result => {
|
||||||
options = result;
|
options = result;
|
||||||
const domains = (result.custom_domain || []).filter(d => d && d !== "");
|
const domains = (result.custom_domain || []).filter(d => d && d !== "");
|
||||||
const denied = result.custom_domain_denied || [];
|
const denied = result.custom_domain_denied || [];
|
||||||
if (domains.some(d => domain.includes(d))) {
|
if (domains.some(domainMatches)) {
|
||||||
startExtension();
|
startExtension();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// auto detect off or already declined here: no prompt, just reminders
|
// auto detect off or already declined here: no prompt, just reminders
|
||||||
if (result.auto_detect_disabled === true || denied.some(d => domain.includes(d))) {
|
if (result.auto_detect_disabled === true || denied.some(domainMatches)) {
|
||||||
startReminderMode();
|
startReminderMode();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -763,18 +784,95 @@ function startReminderMode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The courses API only answers on real Canvas installs. Resolves { isCanvas, courses }.
|
// The courses API only answers on real Canvas installs. Resolves { isCanvas, courses }.
|
||||||
|
// Retried: the content script starts before the session/redirect dance settles
|
||||||
|
// (and on Firefox the first request can race a login redirect), so a single
|
||||||
|
// attempt used to silently give up and leave the whole extension disabled.
|
||||||
async function detectCanvasPage() {
|
async function detectCanvasPage() {
|
||||||
|
const RETRY_DELAYS = [0, 1500, 4000, 9000];
|
||||||
|
let lastError = null;
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt < RETRY_DELAYS.length; attempt++) {
|
||||||
|
if (RETRY_DELAYS[attempt]) await sleep(RETRY_DELAYS[attempt]);
|
||||||
|
|
||||||
|
// At document_start the document is empty, so the markup fingerprints only
|
||||||
|
// mean something once parsing has finished. The API call itself runs right
|
||||||
|
// away: it is the fast path for normal Canvas pages.
|
||||||
|
const domReady = document.readyState !== "loading";
|
||||||
|
const score = canvasMarkupScore();
|
||||||
|
|
||||||
|
// Keep retrying only when the page looks like Canvas or the API answered
|
||||||
|
// with an auth/server error (i.e. we were too early). Otherwise we would
|
||||||
|
// fire four API calls at every site the user visits.
|
||||||
|
if (attempt > 0 && domReady && score === 0 && !isTransientDetectError(lastError)) break;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const courses = await getData(`${domain}/api/v1/courses?${/*enrollment_state=active&*/""}per_page=100`);
|
const courses = await getData(`${domain}/api/v1/courses?per_page=100`);
|
||||||
if (isCanvasCourseList(courses)) {
|
if (isCanvasCourseList(courses)) {
|
||||||
return { isCanvas: true, courses };
|
return { isCanvas: true, courses };
|
||||||
}
|
}
|
||||||
console.log("Canvas Refined - this url doesn't seem to be a canvas url (1)");
|
if (Array.isArray(courses)) {
|
||||||
return { isCanvas: false, courses: null };
|
// A JSON array is Canvas' own API shape. With zero courses the
|
||||||
|
// account simply has no active enrollments (observer/parent, new
|
||||||
|
// term) — those used to be missed because the check required items.
|
||||||
|
if (score > 0) return { isCanvas: true, courses };
|
||||||
|
// Empty-handed before the DOM exists: not enough information yet.
|
||||||
|
if (!domReady) continue;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("Canvas Refined - this url doesn't seem to be a canvas url (2)");
|
lastError = err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Canvas Refined - this url doesn't seem to be a canvas url");
|
||||||
return { isCanvas: false, courses: null };
|
return { isCanvas: false, courses: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Canvas' window.ENV is a page-defined global: Firefox hides those behind Xray
|
||||||
|
// wrappers, so read it through wrappedJSObject there.
|
||||||
|
function pageWindow() {
|
||||||
|
try {
|
||||||
|
return window.wrappedJSObject || window;
|
||||||
|
} catch (_) {
|
||||||
|
return window;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cheap Canvas fingerprints from the DOM/globals. Used to accept an otherwise
|
||||||
|
// empty courses list and to decide whether a failed request deserves a retry.
|
||||||
|
// Score >= 2 is "almost certainly Canvas".
|
||||||
|
function canvasMarkupScore() {
|
||||||
|
let score = 0;
|
||||||
|
try {
|
||||||
|
const env = pageWindow().ENV;
|
||||||
|
if (env && typeof env === "object" && (
|
||||||
|
env.CANVAS_URL || env.DOMAIN_ROOT_ACCOUNT_UUID || env.APP_CONTEXT ||
|
||||||
|
env.CURRENT_USER || env.current_user || env.context_asset_string || env.LOCALE
|
||||||
|
)) {
|
||||||
|
score += 2;
|
||||||
|
}
|
||||||
|
} catch (_) { /* ignore */ }
|
||||||
|
|
||||||
|
const has = selector => {
|
||||||
|
try { return !!document.querySelector(selector); } catch (_) { return false; }
|
||||||
|
};
|
||||||
|
if (has('link[href*="brandable_css"], script[src*="brandable_css"]')) score++;
|
||||||
|
if (has('#application.ic-Layout-wrapper, .ic-Layout-wrapper')) score++;
|
||||||
|
if (has('#header.ic-app-header, .ic-app-header__main-navigation, .ic-Dashboard-header__layout')) score++;
|
||||||
|
if (has('.ic-Login__body, #login_form')) score++;
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isTransientDetectError(err) {
|
||||||
|
const match = /\((\d{3})\)/.exec((err && err.message) || "");
|
||||||
|
if (!match) return true; // network/opaque failure: worth one more try
|
||||||
|
const status = Number(match[1]);
|
||||||
|
return status === 401 || status === 403 || status === 408 || status === 429 || status >= 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Real Canvas courses carry a numeric id and name; looser checks misdetected sites like apstudents.collegeboard.org.
|
// Real Canvas courses carry a numeric id and name; looser checks misdetected sites like apstudents.collegeboard.org.
|
||||||
@ -795,7 +893,7 @@ function addCanvasDomain(courses) {
|
|||||||
domains.push(domain);
|
domains.push(domain);
|
||||||
// just accepted: drop it from the decline list
|
// just accepted: drop it from the decline list
|
||||||
chrome.storage.sync.get(["custom_domain_denied"], deniedStorage => {
|
chrome.storage.sync.get(["custom_domain_denied"], deniedStorage => {
|
||||||
const denied = (deniedStorage.custom_domain_denied || []).filter(d => !domain.includes(d));
|
const denied = (deniedStorage.custom_domain_denied || []).filter(d => !domainMatches(d));
|
||||||
chrome.storage.sync.set({ custom_domain: domains, custom_domain_denied: denied }).then(resolve);
|
chrome.storage.sync.set({ custom_domain: domains, custom_domain_denied: denied }).then(resolve);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -842,7 +940,7 @@ function showCanvasPrompt(courses) {
|
|||||||
dismiss();
|
dismiss();
|
||||||
chrome.storage.sync.get(["custom_domain_denied"], storage => {
|
chrome.storage.sync.get(["custom_domain_denied"], storage => {
|
||||||
const denied = storage.custom_domain_denied || [];
|
const denied = storage.custom_domain_denied || [];
|
||||||
if (!denied.some(d => domain.includes(d))) denied.push(domain);
|
if (!denied.some(domainMatches)) denied.push(domain);
|
||||||
chrome.storage.sync.set({ custom_domain_denied: denied });
|
chrome.storage.sync.set({ custom_domain_denied: denied });
|
||||||
});
|
});
|
||||||
startReminderMode();
|
startReminderMode();
|
||||||
@ -2909,7 +3007,7 @@ async function createCanvasPlannerNote(payload) {
|
|||||||
|
|
||||||
let lastError = getMsg("canvas_rejected_create", "Canvas rejected task creation.");
|
let lastError = getMsg("canvas_rejected_create", "Canvas rejected task creation.");
|
||||||
for (const attempt of attempts) {
|
for (const attempt of attempts) {
|
||||||
const response = await fetch(domain + "/api/v1/planner_notes", {
|
const response = await pageFetch(domain + "/api/v1/planner_notes", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: attempt.headers,
|
headers: attempt.headers,
|
||||||
body: attempt.body,
|
body: attempt.body,
|
||||||
@ -3025,7 +3123,7 @@ async function updateCanvasPlannerNote(id, payload) {
|
|||||||
|
|
||||||
let lastError = getMsg("canvas_rejected_update", "Canvas rejected task update.");
|
let lastError = getMsg("canvas_rejected_update", "Canvas rejected task update.");
|
||||||
for (const attempt of attempts) {
|
for (const attempt of attempts) {
|
||||||
const response = await fetch(`${domain}/api/v1/planner_notes/${id}`, {
|
const response = await pageFetch(`${domain}/api/v1/planner_notes/${id}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: attempt.headers,
|
headers: attempt.headers,
|
||||||
body: attempt.body,
|
body: attempt.body,
|
||||||
@ -3052,7 +3150,7 @@ async function updateCanvasPlannerNote(id, payload) {
|
|||||||
async function deleteCanvasPlannerNote(id) {
|
async function deleteCanvasPlannerNote(id) {
|
||||||
if (!id) throw new Error(getMsg("missing_task_id", "Missing task id."));
|
if (!id) throw new Error(getMsg("missing_task_id", "Missing task id."));
|
||||||
const csrfToken = CSRFtoken();
|
const csrfToken = CSRFtoken();
|
||||||
const response = await fetch(`${domain}/api/v1/planner_notes/${id}`, {
|
const response = await pageFetch(`${domain}/api/v1/planner_notes/${id}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: {
|
headers: {
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
@ -3875,7 +3973,7 @@ async function loadTodoScores(items) {
|
|||||||
let url = `${domain}/api/v1/courses/${cid}/students/submissions?student_ids[]=self&per_page=100`;
|
let url = `${domain}/api/v1/courses/${cid}/students/submissions?student_ids[]=self&per_page=100`;
|
||||||
// Page cap guards against a malformed next link.
|
// Page cap guards against a malformed next link.
|
||||||
for (let page = 0; page < 10 && url; page++) {
|
for (let page = 0; page < 10 && url; page++) {
|
||||||
const response = await fetch(url, {
|
const response = await pageFetch(url, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: { "Content-Type": "application/json", "Accept": "application/json" }
|
headers: { "Content-Type": "application/json", "Accept": "application/json" }
|
||||||
});
|
});
|
||||||
@ -4915,7 +5013,7 @@ function markAs(item, element, makeComplete, opts = {}) {
|
|||||||
|
|
||||||
// --- Persistence (background, best-effort) ---
|
// --- Persistence (background, best-effort) ---
|
||||||
// Store the override id so later toggles PUT instead of POSTing a duplicate.
|
// Store the override id so later toggles PUT instead of POSTing a duplicate.
|
||||||
const sendOverride = (override) => fetch(domain + "/api/v1/planner/overrides" + (override && override.id ? "/" + override.id : ""), {
|
const sendOverride = (override) => pageFetch(domain + "/api/v1/planner/overrides" + (override && override.id ? "/" + override.id : ""), {
|
||||||
method: override && override.id ? "PUT" : "POST",
|
method: override && override.id ? "PUT" : "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
@ -4936,7 +5034,7 @@ function markAs(item, element, makeComplete, opts = {}) {
|
|||||||
const all = [];
|
const all = [];
|
||||||
let url = domain + "/api/v1/planner/overrides?per_page=100";
|
let url = domain + "/api/v1/planner/overrides?per_page=100";
|
||||||
for (let page = 0; page < 10 && url; page++) {
|
for (let page = 0; page < 10 && url; page++) {
|
||||||
const pageResp = await fetch(url, { headers: { "accept": "application/json" } });
|
const pageResp = await pageFetch(url, { headers: { "accept": "application/json" } });
|
||||||
if (!pageResp.ok) return null;
|
if (!pageResp.ok) return null;
|
||||||
const data = await pageResp.json();
|
const data = await pageResp.json();
|
||||||
if (!Array.isArray(data)) return null;
|
if (!Array.isArray(data)) return null;
|
||||||
@ -5767,8 +5865,8 @@ async function getPaletteCards() {
|
|||||||
if (cards.length > 0) return { cards, apiColors };
|
if (cards.length > 0) return { cards, apiColors };
|
||||||
try {
|
try {
|
||||||
const [cardsRes, colorsRes] = await Promise.all([
|
const [cardsRes, colorsRes] = await Promise.all([
|
||||||
fetch(domain + "/api/v1/dashboard/dashboard_cards", { headers: { "accept": "application/json" } }),
|
pageFetch(domain + "/api/v1/dashboard/dashboard_cards", { headers: { "accept": "application/json" } }),
|
||||||
fetch(domain + "/api/v1/users/self/colors", { headers: { "accept": "application/json" } })
|
pageFetch(domain + "/api/v1/users/self/colors", { headers: { "accept": "application/json" } })
|
||||||
]);
|
]);
|
||||||
const apiCards = await cardsRes.json();
|
const apiCards = await cardsRes.json();
|
||||||
apiColors = (await colorsRes.json())?.custom_colors || {};
|
apiColors = (await colorsRes.json())?.custom_colors || {};
|
||||||
@ -5838,7 +5936,7 @@ async function changeColorPreset(colors) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let changeCardColor = () => {
|
let changeCardColor = () => {
|
||||||
fetch(domain + "/api/v1/users/self/colors/courses_" + course_id,
|
pageFetch(domain + "/api/v1/users/self/colors/courses_" + course_id,
|
||||||
{
|
{
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
@ -9423,7 +9521,7 @@ async function fetchActiveCourseIds() {
|
|||||||
let response;
|
let response;
|
||||||
let data;
|
let data;
|
||||||
try {
|
try {
|
||||||
response = await fetch(url, {
|
response = await pageFetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -9486,7 +9584,7 @@ async function fetchPlannerItemsSince(startDate) {
|
|||||||
let response;
|
let response;
|
||||||
let data;
|
let data;
|
||||||
try {
|
try {
|
||||||
response = await fetch(url, {
|
response = await pageFetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -11306,8 +11404,23 @@ function makeElement(element, location, options, prepend = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Firefox content scripts run `fetch` with the *extension's* origin, so the
|
||||||
|
// request loses the page's cookies and Referer/Origin; Canvas then answers
|
||||||
|
// 401/403 (or a login page), which made detection and every API call fail on
|
||||||
|
// Firefox only. `content.fetch` (Firefox 58+) sends the request exactly as the
|
||||||
|
// page would. Chrome has no `content` global, so this is a no-op there.
|
||||||
|
function pageFetch(url, init) {
|
||||||
|
const opts = { credentials: "include", cache: "no-store", ...init };
|
||||||
|
try {
|
||||||
|
if (typeof content !== "undefined" && content && typeof content.fetch === "function") {
|
||||||
|
return content.fetch(url, opts);
|
||||||
|
}
|
||||||
|
} catch (_) { /* fall through to the normal fetch */ }
|
||||||
|
return fetch(url, opts);
|
||||||
|
}
|
||||||
|
|
||||||
async function getData(url) {
|
async function getData(url) {
|
||||||
let response = await fetch(url, {
|
let response = await pageFetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user