import fs from 'node:fs'; import path from 'node:path'; import { buildMindSpacePublicRoutePath, isPublicWorkspaceHtmlRelativePath, resolveMindSpaceUserPublishDir, } from '../mindspace-runtime-config.mjs'; import { isPlazaEmbedRequest } from '../plaza-embed.mjs'; import { rasterizeThumbnailSvgToPng, } from '../mindspace-thumbnail-png.mjs'; import { PUBLIC_ZONE_DIR } from '../user-publish.mjs'; function escapePublicHtml(value) { return String(value ?? '') .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } export function renderPublicationPasswordGate( action, ) { return ` 受保护页面

此页面受密码保护

请输入发布者提供的访问密码。密码不会写入链接或浏览器日志。

`; } export function renderPublicHomepage(data) { const cards = data.pages .map( (page) => `\
${escapePublicHtml(page.templateId)} ${Number(page.viewCount)} 次浏览

${escapePublicHtml(page.title)}

${escapePublicHtml(page.summary || '暂无摘要')}

${new Date(page.publishedAt).toLocaleDateString('zh-CN')} 打开页面
`, ) .join(''); return ` ${escapePublicHtml(data.owner.displayName)} · MindSpace

MindSpace Public

${escapePublicHtml(data.owner.displayName)}

这里展示 ${escapePublicHtml(data.owner.displayName)} 当前公开发布且在线的 MindSpace 页面。

${Number(data.pageCount)} 个公开页面 ${Number(data.totalViews)} 次累计浏览 /u/${escapePublicHtml(data.owner.slug)}
${ data.pages.length ? `
${cards}
` : '
这个主页还没有公开页面。稍后再来看看,或者直接访问作者分享给你的专属链接。
' }
`; } export function attachPortalPublicationRoutes({ app, urlencodedBody, userAuthReady = Promise.resolve(), h5Root, getAuthPool = () => null, getMindSpacePages = () => null, getMindSpacePublications = () => null, getUserAuth = () => null, sendPublishedPage, fsApi = fs, pathApi = path, publicZoneDir = PUBLIC_ZONE_DIR, resolveUserPublishDir = resolveMindSpaceUserPublishDir, isEmbedRequest = isPlazaEmbedRequest, isPublicWorkspaceHtml = isPublicWorkspaceHtmlRelativePath, buildPublicRoutePath = buildMindSpacePublicRoutePath, rasterizeThumbnail = rasterizeThumbnailSvgToPng, } = {}) { if ( !app || !h5Root || typeof urlencodedBody !== 'function' || typeof sendPublishedPage !== 'function' ) { throw new Error( 'attachPortalPublicationRoutes requires route dependencies', ); } async function resolvePublishedRoute( req, res, password = null, ) { await userAuthReady; const mindSpacePublications = getMindSpacePublications(); if (!mindSpacePublications) { return res .status(503) .send('MindSpace 未启用'); } try { const userAuth = getUserAuth(); const viewer = req.userSession && userAuth ? await userAuth.getMe(req.userToken) : null; const result = await mindSpacePublications.resolvePublic( req.params.ownerSlug, req.params.urlSlug, viewer?.id, password, { userAgent: req.get('user-agent'), referrer: req.get('referer'), }, ); const embed = isEmbedRequest(req.query); const raw = String(req.query.view ?? '') .toLowerCase() === 'raw'; if ( !password && !embed && !raw && result.publication?.accessMode === 'public' && isPublicWorkspaceHtml( result.workspaceRelativePath, ) ) { const redirectPath = buildPublicRoutePath( result.ownerId, String( result.workspaceRelativePath, ).split('/'), ); return res.redirect(301, redirectPath); } return await sendPublishedPage( req, res, result, { embed, raw, ownerSlug: req.params.ownerSlug, }, ); } catch (error) { if ( error?.code === 'publication_password_required' ) { return res .status(password ? 403 : 200) .send( renderPublicationPasswordGate( req.originalUrl, ), ); } if ( error?.code === 'publication_login_required' ) { return res .status(401) .send( '请先登录 TKMind 后再访问此页面', ); } if ( error?.code === 'publication_not_found' ) { return res .status(404) .send('页面不存在或已下线'); } return res .status(500) .send('页面加载失败'); } } app.use(async (req, res, next) => { const thumbnailMatch = /^\/u\/([^/]+)\/pages\/([^/]+)\.thumbnail\.png$/.exec( req.path, ); if (!thumbnailMatch) return next(); const ownerSlug = thumbnailMatch[1]; const urlSlug = thumbnailMatch[2]; await userAuthReady; const authPool = getAuthPool(); const mindSpacePages = getMindSpacePages(); if (!authPool || !mindSpacePages) { return res .status(503) .send('MindSpace 未启用'); } try { const [rows] = await authPool.query( `SELECT pr.user_id, pr.page_id FROM h5_publish_records pr JOIN h5_users u ON u.id = pr.user_id WHERE COALESCE(u.slug, u.username) = ? AND pr.url_slug = ? AND pr.status = 'online' ORDER BY pr.published_at DESC LIMIT 1`, [ownerSlug, urlSlug], ); const row = rows[0]; if (!row) { return res .status(404) .send('缩略图不存在'); } const svg = await mindSpacePages.renderThumbnail( row.user_id, row.page_id, ); res.set('Content-Type', 'image/png'); res.set( 'Cache-Control', 'public, max-age=300', ); return res.send( rasterizeThumbnail(svg), ); } catch { return res .status(500) .send('缩略图加载失败'); } }); app.get( '/u/:ownerSlug/pages/:urlSlug', async (req, res) => resolvePublishedRoute(req, res), ); app.use( '/u/:ownerSlug/public', async (req, res) => { await userAuthReady; const ownerSlug = String( req.params.ownerSlug ?? '', ) .trim() .toLowerCase(); if (!ownerSlug) { return res .status(404) .json({ message: '用户不存在' }); } const authPool = getAuthPool(); if (!authPool) { return res .status(503) .send('MindSpace 未启用'); } try { const [rows] = await authPool.query( `SELECT id FROM h5_users WHERE LOWER(COALESCE(slug, username)) = ? LIMIT 1`, [ownerSlug], ); const userId = rows[0]?.id ? String(rows[0].id) : null; if (!userId) { return res .status(404) .json({ message: '用户不存在' }); } const relativePath = String( req.path ?? '', ) .replace(/^\/+/, '') .split('/') .map((segment) => decodeURIComponent(segment), ) .filter(Boolean); if (relativePath.length === 0) { return res .status(404) .json({ message: '文件不存在' }); } const publishDir = resolveUserPublishDir(h5Root, { id: userId, }); const resolvedRoot = pathApi.resolve(publishDir); const filePath = pathApi.resolve( publishDir, publicZoneDir, ...relativePath, ); if ( filePath !== resolvedRoot && !filePath.startsWith( `${resolvedRoot}${pathApi.sep}`, ) ) { return res .status(403) .json({ message: '禁止访问' }); } if ( !fsApi.existsSync(filePath) || !fsApi.statSync(filePath).isFile() ) { return res .status(404) .json({ message: '文件不存在' }); } return res.sendFile( filePath, (error) => { if ( error && !res.headersSent ) { res .status(404) .json({ message: '文件不存在', }); } }, ); } catch { return res .status(500) .send('文件加载失败'); } }, ); app.get( '/u/:ownerSlug', async (req, res) => { await userAuthReady; const mindSpacePublications = getMindSpacePublications(); if (!mindSpacePublications) { return res .status(503) .send('MindSpace 未启用'); } try { const data = await mindSpacePublications .getPublicHomepage( req.params.ownerSlug, ); res.set( 'Content-Type', 'text/html; charset=utf-8', ); res.set( 'Content-Security-Policy', "default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'", ); res.set( 'Cache-Control', 'public, max-age=60', ); return res.send( renderPublicHomepage(data), ); } catch (error) { if ( error?.code === 'publication_not_found' ) { return res .status(404) .send('主页不存在'); } return res .status(500) .send('主页加载失败'); } }, ); app.post( '/u/:ownerSlug/pages/:urlSlug', urlencodedBody, async (req, res) => resolvePublishedRoute( req, res, req.body?.password, ), ); app.get('/s/:token', async (req, res) => { await userAuthReady; const mindSpacePublications = getMindSpacePublications(); if (!mindSpacePublications) { return res .status(503) .send('MindSpace 未启用'); } try { const userAuth = getUserAuth(); const viewer = req.userSession && userAuth ? await userAuth.getMe(req.userToken) : null; return await sendPublishedPage( req, res, await mindSpacePublications .resolvePrivateLink( req.params.token, viewer?.id, { userAgent: req.get('user-agent'), referrer: req.get('referer'), }, ), { embed: isEmbedRequest(req.query), raw: String(req.query.view ?? '') .toLowerCase() === 'raw', }, ); } catch (error) { if ( error?.code === 'publication_not_found' ) { return res .status(404) .send('页面不存在或已下线'); } return res .status(500) .send('页面加载失败'); } }); }