From 91c973ee06f1a8b94b46baa1d36627f81d6e81e0 Mon Sep 17 00:00:00 2001 From: Akhileshwar Shriram <112577383+AkhilTheBoss@users.noreply.github.com> Date: Wed, 14 Oct 2026 11:15:57 -0700 Subject: [PATCH 1/2] fix: generate asset permalinks on the canonical Commons host --- server/api/projectfiles.ts | 38 ++++++++++++++++++++++++++------------ server/util/helpers.js | 21 +++++++++++++++++++++ 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/server/api/projectfiles.ts b/server/api/projectfiles.ts index fc57e5b31..f99ba502c 100644 --- a/server/api/projectfiles.ts +++ b/server/api/projectfiles.ts @@ -1,4 +1,4 @@ -import logger from "../logger.js"; +import logger, { childLogger } from "../logger.js"; import { NextFunction, Request, Response } from "express"; import conductorErrors from "../conductor-errors.js"; import ProjectFile, { @@ -22,6 +22,7 @@ import { isObjectIdOrHexString } from "mongoose"; import async from "async"; import { assembleUrl, + getCommonsCanonicalURL, getPaginationOffset, getRandomOffset, } from "../util/helpers.js"; @@ -75,6 +76,7 @@ import { generateVideoStreamURL } from "../util/videoutils.js"; import axios from "axios"; import mime from "mime"; +const permalinkLog = childLogger("permalink"); const filesStorage = multer.memoryStorage(); const MAX_UPLOAD_FILES = 20; const MAX_UPLOAD_FILE_SIZE = 100000000; // 100mb @@ -634,14 +636,6 @@ async function getPermanentLink( }); } - const { orgID } = project; - const organization = await Organization.findOne({ orgID }).lean(); - if (!organization) { - return res.status(404).send({ - err: true, - errMsg: conductorErrors.err11, - }); - } const projectFile = await ProjectFile.findOne({ projectID, fileID, @@ -659,8 +653,7 @@ async function getPermanentLink( errMsg: conductorErrors.err29 }); } - const domain = organization.domain; - const permanentLink = `${domain}/permalink/${projectID}/${fileID}`; + const permanentLink = `${getCommonsCanonicalURL()}/permalink/${projectID}/${fileID}`; return res.status(200).send({ error: false, url: permanentLink, @@ -675,6 +668,15 @@ async function getPermanentLink( } } +function receivingHost(req: Request) { + return (req.get("host") || req.hostname || "").toLowerCase(); +} + +function isCanonicalPermalinkHost(req: Request) { + const canonicalHost = new URL(getCommonsCanonicalURL()).hostname.toLowerCase(); + return req.hostname.toLowerCase() === canonicalHost; +} + async function redirectPermanentLink( req: ZodReqWithOptionalUser>, res: Response @@ -682,6 +684,18 @@ async function redirectPermanentLink( try { const { projectID, fileID } = req.params; const { direct } = req.query; + + if (!isCanonicalPermalinkHost(req)) { + const queryIndex = req.originalUrl.indexOf("?"); + const query = queryIndex === -1 ? "" : req.originalUrl.slice(queryIndex); + const target = `${getCommonsCanonicalURL()}/permalink/${projectID}/${fileID}${query}`; + permalinkLog.debug( + { host: receivingHost(req) }, + "redirecting permalink to canonical host" + ); + return res.redirect(301, target); + } + const project = await Project.findOne({ projectID }).lean(); if (!project) { @@ -692,7 +706,7 @@ async function redirectPermanentLink( } if (!direct) { - return res.redirect('https://commons.libretexts.org/download/' + projectID + '/' + fileID); + return res.redirect(`${getCommonsCanonicalURL()}/download/${projectID}/${fileID}`); } const downloadURLs = await downloadProjectFiles( diff --git a/server/util/helpers.js b/server/util/helpers.js index 6d0feae82..ea363c9b2 100644 --- a/server/util/helpers.js +++ b/server/util/helpers.js @@ -218,6 +218,27 @@ export const getProductionURL = () => { return process.env.CONDUCTOR_DOMAIN || ''; }; +const DEFAULT_COMMONS_CANONICAL_URL = "https://commons.libretexts.org"; + +/** + * Origin of the permanent Commons host. Asset permalinks are generated and + * resolved against this origin, not the instance's own CONDUCTOR_DOMAIN. + * Set COMMONS_CANONICAL_DOMAIN (host or absolute URL) so staging points at + * the staging commons host. + * + * @returns {string} Origin with no trailing slash. + */ +export const getCommonsCanonicalURL = () => { + const configured = (process.env.COMMONS_CANONICAL_DOMAIN || "").trim(); + const raw = configured || DEFAULT_COMMONS_CANONICAL_URL; + const withProtocol = /^https?:\/\//i.test(raw) ? raw : `https://${raw}`; + try { + return new URL(withProtocol).origin; + } catch { + return DEFAULT_COMMONS_CANONICAL_URL; + } +}; + /** * Removes a leading slash, if any, from a string. * From ffd9fdd74dd0ab7c1847d5212b51da316cb40baa Mon Sep 17 00:00:00 2001 From: Akhileshwar Shriram <112577383+AkhilTheBoss@users.noreply.github.com> Date: Mon, 19 Oct 2026 20:09:39 -0700 Subject: [PATCH 2/2] fix: address permalink review feedback --- server/api/projectfiles.ts | 32 +++++++++++++++++++------------- server/util/helpers.js | 21 +++++++++++++++++++++ 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/server/api/projectfiles.ts b/server/api/projectfiles.ts index f99ba502c..c4b0d5b77 100644 --- a/server/api/projectfiles.ts +++ b/server/api/projectfiles.ts @@ -25,6 +25,7 @@ import { getCommonsCanonicalURL, getPaginationOffset, getRandomOffset, + isCanonicalCommonsInstance, } from "../util/helpers.js"; import { CopyObjectCommand, @@ -669,31 +670,36 @@ async function getPermanentLink( } function receivingHost(req: Request) { - return (req.get("host") || req.hostname || "").toLowerCase(); -} - -function isCanonicalPermalinkHost(req: Request) { - const canonicalHost = new URL(getCommonsCanonicalURL()).hostname.toLowerCase(); - return req.hostname.toLowerCase() === canonicalHost; + return (req.get("host") || "").toLowerCase(); } async function redirectPermanentLink( - req: ZodReqWithOptionalUser>, + req: Request & + ZodReqWithOptionalUser>, res: Response ) { try { const { projectID, fileID } = req.params; const { direct } = req.query; - if (!isCanonicalPermalinkHost(req)) { + if (!isCanonicalCommonsInstance()) { const queryIndex = req.originalUrl.indexOf("?"); const query = queryIndex === -1 ? "" : req.originalUrl.slice(queryIndex); const target = `${getCommonsCanonicalURL()}/permalink/${projectID}/${fileID}${query}`; - permalinkLog.debug( - { host: receivingHost(req) }, - "redirecting permalink to canonical host" - ); - return res.redirect(301, target); + if (target.endsWith(req.originalUrl)) { + permalinkLog.warn( + { host: receivingHost(req) }, + "canonical host check rejected its own origin; skipping redirect" + ); + } else { + permalinkLog.debug( + { host: receivingHost(req) }, + "redirecting permalink to canonical host" + ); + return res + .setHeader("Cache-Control", "no-store") + .redirect(302, target); + } } const project = await Project.findOne({ projectID }).lean(); diff --git a/server/util/helpers.js b/server/util/helpers.js index ea363c9b2..cac338fee 100644 --- a/server/util/helpers.js +++ b/server/util/helpers.js @@ -235,10 +235,31 @@ export const getCommonsCanonicalURL = () => { try { return new URL(withProtocol).origin; } catch { + logger.warn( + { configured }, + "Invalid COMMONS_CANONICAL_DOMAIN, falling back to default" + ); return DEFAULT_COMMONS_CANONICAL_URL; } }; +/** + * Whether this Conductor instance is the permanent Commons host. + * Compares CONDUCTOR_DOMAIN (this instance) to getCommonsCanonicalURL(). + * + * @returns {boolean} + */ +export const isCanonicalCommonsInstance = () => { + const self = (process.env.CONDUCTOR_DOMAIN || "").trim(); + if (!self) return false; + const withProtocol = /^https?:\/\//i.test(self) ? self : `https://${self}`; + try { + return new URL(withProtocol).origin === getCommonsCanonicalURL(); + } catch { + return false; + } +}; + /** * Removes a leading slash, if any, from a string. *