From fbc35cef0c8d6be27757f0ca090824d8f9035726 Mon Sep 17 00:00:00 2001 From: adi3433 <96505738+adi3433@users.noreply.github.com> Date: Wed, 8 Apr 2026 19:39:45 +0530 Subject: [PATCH] refactor: deduplicate shared utilities and fix cookie store robustness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract `getSesskey()` into `services/sesskey.ts` (was duplicated in scraper.ts and dashboard.ts) - Extract `isLoginHtml()` into `utils/moodle-url.ts` (was copy-pasted across 4 service files) - Fix cookie store: validate Date before assigning expires (malformed Set-Cookie headers created Invalid Date objects that were never cleaned up) No behavioral changes — all imports updated, TypeScript compiles clean. --- src/services/assignment.ts | 10 +--------- src/services/cookie-store.ts | 5 ++++- src/services/dashboard.ts | 12 +----------- src/services/feedback-autofill.ts | 10 +--------- src/services/lms-download.ts | 9 +-------- src/services/resources-scraper.ts | 10 +--------- src/services/scraper.ts | 17 +---------------- src/services/sesskey.ts | 14 ++++++++++++++ src/utils/moodle-url.ts | 10 ++++++++++ 9 files changed, 34 insertions(+), 63 deletions(-) create mode 100644 src/services/sesskey.ts diff --git a/src/services/assignment.ts b/src/services/assignment.ts index a523bfa..edaff4d 100644 --- a/src/services/assignment.ts +++ b/src/services/assignment.ts @@ -12,6 +12,7 @@ import type { import { debug } from "@/utils/debug"; import { getQueryParamValue, + isLoginHtml, parseAssignmentIdFromMoodleUrl, } from "@/utils/moodle-url"; import { @@ -158,15 +159,6 @@ const parseNumberValue = ( return null; }; -const isLoginHtml = (html: string): boolean => { - const normalized = html.replace(/\s+/g, " "); - return ( - normalized.includes('name="logintoken"') || - normalized.includes('id="login"') || - normalized.includes("/login/index.php") - ); -}; - const ensureAuthenticatedSession = async (): Promise => { const hasSession = await checkSession(); if (hasSession) return true; diff --git a/src/services/cookie-store.ts b/src/services/cookie-store.ts index 8bfbb41..22acf69 100644 --- a/src/services/cookie-store.ts +++ b/src/services/cookie-store.ts @@ -59,7 +59,10 @@ class CookieStore { if (keyLower === "domain") cookie.domain = val?.trim(); if (keyLower === "path") cookie.path = val?.trim(); if (keyLower === "expires" && val) { - cookie.expires = new Date(val.trim()); + const parsed = new Date(val.trim()); + if (!Number.isNaN(parsed.getTime())) { + cookie.expires = parsed; + } } } diff --git a/src/services/dashboard.ts b/src/services/dashboard.ts index 64157c0..36f8dc4 100644 --- a/src/services/dashboard.ts +++ b/src/services/dashboard.ts @@ -2,6 +2,7 @@ import type { TimelineEvent } from "@/types"; import { debug } from "@/utils/debug"; import { api } from "./api"; import { dedupeTimelineEvents } from "./dashboard-event-utils"; +import { getSesskey } from "./sesskey"; interface MoodleTimelineResponse { error: boolean; @@ -13,17 +14,6 @@ interface MoodleTimelineResponse { }; } -const getSesskey = async (): Promise => { - const response = await api.get("/my/"); - const match = response.data.match(/"sesskey":"([^"]+)"/); - if (match) { - debug.scraper(`Found sesskey: ${match[1]}`); - return match[1]; - } - debug.scraper("Sesskey not found"); - return null; -}; - type ActionEventsByTimesortArgs = { limitnum: number; timesortfrom: number; diff --git a/src/services/feedback-autofill.ts b/src/services/feedback-autofill.ts index 04c81b3..1fc6c9a 100644 --- a/src/services/feedback-autofill.ts +++ b/src/services/feedback-autofill.ts @@ -13,6 +13,7 @@ import { querySelectorAll, } from "@/utils/html-parser"; import { debug } from "@/utils/debug"; +import { isLoginHtml } from "@/utils/moodle-url"; import type { Element } from "domhandler"; type FeedbackAutofillOptions = { @@ -60,15 +61,6 @@ type FillResult = { textFieldsFilled: number; }; -const isLoginHtml = (html: string): boolean => { - const normalized = html.replace(/\s+/g, " "); - return ( - normalized.includes('name="logintoken"') || - normalized.includes('id="login"') || - normalized.includes("/login/index.php") - ); -}; - const toAbsoluteUrl = (href: string): string => { const baseUrl = getCurrentBaseUrl(); try { diff --git a/src/services/lms-download.ts b/src/services/lms-download.ts index 20515a2..6dddec3 100644 --- a/src/services/lms-download.ts +++ b/src/services/lms-download.ts @@ -9,6 +9,7 @@ import type { LmsDownloadSuccess, } from "@/types"; import { debug } from "@/utils/debug"; +import { isLoginHtml } from "@/utils/moodle-url"; import { File, Paths } from "expo-file-system"; import { fetch as expoFetch } from "expo/fetch"; @@ -83,14 +84,6 @@ const toAbsoluteLmsUrl = (url: string): string => { return `${baseUrl}/${url.replace(/^\.?\//, "")}`; }; -const isLoginHtml = (html: string): boolean => { - const condensed = html.replace(/\s+/g, " "); - return ( - condensed.includes('name="logintoken"') || - condensed.includes('id="login"') || - condensed.includes("/login/index.php") - ); -}; const buildFailure = ( reason: LmsDownloadFailureReason, diff --git a/src/services/resources-scraper.ts b/src/services/resources-scraper.ts index b00aadf..04b2d90 100644 --- a/src/services/resources-scraper.ts +++ b/src/services/resources-scraper.ts @@ -13,21 +13,13 @@ import { querySelector, querySelectorAll, } from "@/utils/html-parser"; +import { isLoginHtml } from "@/utils/moodle-url"; import type { Element } from "domhandler"; import { api, BASE_URL } from "./api"; const normalizeText = (value: string): string => value.replace(/\s+/g, " ").trim(); -const isLoginHtml = (html: string): boolean => { - const normalized = html.replace(/\s+/g, " "); - return ( - normalized.includes('name="logintoken"') || - normalized.includes('id="login"') || - normalized.includes("/login/index.php") - ); -}; - const toAbsoluteUrl = (href: string): string => { if (href.startsWith("http://") || href.startsWith("https://")) { return href; diff --git a/src/services/scraper.ts b/src/services/scraper.ts index ad8808d..d25754e 100644 --- a/src/services/scraper.ts +++ b/src/services/scraper.ts @@ -16,22 +16,7 @@ import { querySelectorAll, } from "@/utils/html-parser"; import { api, BASE_URL } from "./api"; - -// Get sesskey from page for API calls -const getSesskey = async (): Promise => { - const response = await api.get("/my/"); - - // Find sesskey in page - it's in M.cfg.sesskey in a script tag - const match = response.data.match(/"sesskey":"([^"]+)"/); - - if (match) { - debug.scraper(`Found sesskey: ${match[1]}`); - return match[1]; - } - - debug.scraper("Sesskey not found"); - return null; -}; +import { getSesskey } from "./sesskey"; // Fetch courses using Moodle's AJAX API (only "in progress" courses) export const fetchCourses = async (): Promise => { diff --git a/src/services/sesskey.ts b/src/services/sesskey.ts new file mode 100644 index 0000000..8f7c1a8 --- /dev/null +++ b/src/services/sesskey.ts @@ -0,0 +1,14 @@ +import { debug } from "@/utils/debug"; +import { api } from "./api"; + +/** Extract sesskey from the Moodle dashboard page (M.cfg.sesskey). */ +export const getSesskey = async (): Promise => { + const response = await api.get("/my/"); + const match = response.data.match(/"sesskey":"([^"]+)"/); + if (match) { + debug.scraper(`Found sesskey: ${match[1]}`); + return match[1]; + } + debug.scraper("Sesskey not found"); + return null; +}; diff --git a/src/utils/moodle-url.ts b/src/utils/moodle-url.ts index 040a48f..e0c25d0 100644 --- a/src/utils/moodle-url.ts +++ b/src/utils/moodle-url.ts @@ -1,5 +1,15 @@ const ASSIGNMENT_VIEW_PATH = "/mod/assign/view.php"; +/** Check if HTML response is a Moodle login page (session expired). */ +export const isLoginHtml = (html: string): boolean => { + const normalized = html.replace(/\s+/g, " "); + return ( + normalized.includes('name="logintoken"') || + normalized.includes('id="login"') || + normalized.includes("/login/index.php") + ); +}; + export const getQueryParamValue = (url: string, key: string): string | null => { if (!url) return null;