diff --git a/CHANGELOG.md b/CHANGELOG.md index 7003409..1145d2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # CHANGELOG -## v4.0.0-rc1 – 2026-04-14 +## v4.0.0-rc2 – 2026-04-21 - Version jump to 4.0.0 to align numbering with eXeLearning for consistency across related projects. - Add Docker image (published to GitHub Container Registry) for optional containerized deployment. @@ -9,6 +9,7 @@ - Improve Web Worker lifecycle management by properly terminating the worker, clearing its reference and standardizing error reporting. - Align license declaration for consistency across the project. - Improve error handling and user feedback for storage-related failures, including insufficient storage space conditions. +- Add `github-proxy.exelearning.dev` as the primary CORS proxy fallback for GitHub URL downloads, improving reliability when loading files from CORS-restricted sources. --- diff --git a/js/app.js b/js/app.js index d4f07ea..6713188 100644 --- a/js/app.js +++ b/js/app.js @@ -613,6 +613,16 @@ return urlObj.toString(); } + // GitHub blob viewer: convert to raw content URL + // Format: https://github.com/{user}/{repo}/blob/{ref}/{path} + // Convert to: https://raw.githubusercontent.com/{user}/{repo}/{ref}/{path} + if (urlObj.hostname === 'github.com') { + const match = urlObj.pathname.match(/^\/([^/]+)\/([^/]+)\/blob\/(.+)$/); + if (match) { + return `https://raw.githubusercontent.com/${match[1]}/${match[2]}/${match[3]}`; + } + } + // Return original URL if no conversion needed return url; } catch (e) { @@ -696,11 +706,30 @@ * Each proxy has a different URL format */ const CORS_PROXIES = [ + { url: 'https://github-proxy.exelearning.dev/?url=', encode: true, githubOnly: true }, { url: 'https://corsproxy.io/?', encode: true }, { url: 'https://api.allorigins.win/raw?url=', encode: true }, { url: 'https://cors.eu.org/', encode: false } ]; + const GITHUB_HOSTNAMES = new Set([ + 'github.com', + 'raw.githubusercontent.com', + 'gist.githubusercontent.com', + 'objects.githubusercontent.com', + 'codeload.github.com', + 'releases.githubusercontent.com', + 'media.githubusercontent.com' + ]); + + function isGithubUrl(url) { + try { + return GITHUB_HOSTNAMES.has(new URL(url).hostname); + } catch { + return false; + } + } + /** * Fetch with CORS proxy fallback * @param {string} url - The URL to fetch @@ -717,25 +746,34 @@ } // Try each proxy until one works + const isGithub = isGithubUrl(url); let lastError; for (const proxy of CORS_PROXIES) { - try { - const proxyUrl = proxy.encode - ? proxy.url + encodeURIComponent(url) - : proxy.url + url; + if (proxy.githubOnly && !isGithub) continue; + + const proxyUrl = proxy.encode + ? proxy.url + encodeURIComponent(url) + : proxy.url + url; + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), 8000); + + try { console.log(`[App] Trying proxy: ${proxy.url}`); const response = await fetch(proxyUrl, { method: 'GET', mode: 'cors', - credentials: 'omit' + credentials: 'omit', + signal: controller.signal }); + clearTimeout(timer); if (response.ok) { console.log(`[App] Proxy ${proxy.url} succeeded`); return response; } } catch (err) { + clearTimeout(timer); console.warn(`[App] Proxy ${proxy.url} failed:`, err.message); lastError = err; }