import { injectPublishedPageDataContext, resolveMindSpacePageDataContext, } from '../mindspace-public-page-context.mjs'; import { collectInlineScriptHashes, } from '../mindspace-public-delivery.mjs'; import { injectMindSpaceAnalytics, resolveAnalyticsOwnerLabel, resolveAnalyticsOwnerSegment, resolveAnalyticsPlan, } from '../mindspace-analytics.mjs'; import { injectMindSpaceRybbit, } from '../mindspace-rybbit.mjs'; import { rewriteBrokenMindSpacePublicImageUrls, rewritePublicationCanonicalAssetUrls, } from '../mindspace-publications.mjs'; import { allowPlazaEmbedFrame, preparePublicationHtmlForEmbed, stripPublicationHtmlCspMeta, } from '../plaza-embed.mjs'; import { publishedPageCsp, } from '../mindspace-published-page-csp.mjs'; import { injectOgTags, injectWechatShareBridge, } from '../mindspace-og-tags.mjs'; import { isLongImageDownloadRequest, renderLongImageBuffer, } from '../mindspace-long-image.mjs'; import { isWechatUserAgent } from '../wechat-oauth.mjs'; import { appendQueryParam, detectPublishedPageTitle, extractOgImageUrl, extractShareMetaFromPageHtml, publishedPageShellHtml, removeQueryParam, resolveRequestOrigin, } from './portal-publication-shell.mjs'; async function decoratePublicationHtmlAnalytics( html, { result, analyticsConfig, rybbitConfig, getAuthPool = () => null, getUserAuth = () => null, getMindSpacePages = () => null, resolvePageDataContext = resolveMindSpacePageDataContext, logger = console, } = {}, ) { const source = String(html ?? ''); const isFullHtml = /^\s*]/i.test(source); if (!isFullHtml) return source; const userAuth = getUserAuth?.(); const pageOwner = result?.ownerId && userAuth ? await userAuth .getUserById(result.ownerId) .catch(() => null) : null; let pageDataContext = null; const mindSpacePages = getMindSpacePages?.(); if ( result?.ownerId && result?.workspaceRelativePath && mindSpacePages ) { pageDataContext = await resolvePageDataContext({ pool: getAuthPool?.(), pageService: mindSpacePages, userId: result.ownerId, relativePath: result.workspaceRelativePath, logger, }).catch(() => null); } const ownerSegment = resolveAnalyticsOwnerSegment( pageOwner ?? {}, ); const ownerLabel = resolveAnalyticsOwnerLabel( pageOwner ?? {}, ); const pageId = pageDataContext?.pageId ?? result?.pageSource?.pageId ?? ''; const publicationId = pageDataContext?.publicationId ?? pageDataContext?.publication_id ?? result?.publication?.id ?? ''; let decorated = injectMindSpaceAnalytics(source, { ownerId: result?.ownerId ?? '', ownerSegment, ownerLabel, planType: resolveAnalyticsPlan(pageOwner ?? {}), generatedAt: pageDataContext?.generatedAt ?? '', pageId, publicationId, config: analyticsConfig, }); decorated = injectMindSpaceRybbit(decorated, { ownerId: result?.ownerId ?? '', ownerSegment, ownerLabel, pageId, publicationId, channel: 'publication', config: rybbitConfig, }); return decorated; } export function createPortalPublishedPageDelivery({ registerLongImageArtifact, renderLongImage = renderLongImageBuffer, analyticsConfig = { enabled: false }, rybbitConfig = { enabled: false }, getAuthPool = () => null, getUserAuth = () => null, getMindSpacePages = () => null, logger = console, } = {}) { if ( typeof registerLongImageArtifact !== 'function' ) { throw new Error( 'createPortalPublishedPageDelivery requires artifact registration', ); } return async function sendPublishedPage( req, res, result, { embed = false, raw = false, ownerSlug = null, } = {}, ) { let html = rewriteBrokenMindSpacePublicImageUrls( result.html, ); if (ownerSlug) { html = rewritePublicationCanonicalAssetUrls( html, ownerSlug, ); } const origin = resolveRequestOrigin(req); const originalPath = req.originalUrl || req.url || ''; const sharePath = removeQueryParam( removeQueryParam( originalPath, 'download', ), 'export', ); const pageUrl = originalPath ? new URL( sharePath, origin || 'http://localhost', ) .toString() .split('#')[0] : ''; const pageDirUrl = pageUrl ? pageUrl.slice( 0, pageUrl.lastIndexOf('/') + 1, ) : ''; const wechatShare = !embed && isWechatUserAgent( req.get('user-agent') || '', ); if (embed) { html = preparePublicationHtmlForEmbed(html); allowPlazaEmbedFrame(res); } else if (raw) { html = stripPublicationHtmlCspMeta(html); } html = injectPublishedPageDataContext( html, { pageSource: result.pageSource, publication: result.publication, }, ); html = await decoratePublicationHtmlAnalytics(html, { result, analyticsConfig, rybbitConfig, getAuthPool, getUserAuth, getMindSpacePages, logger, }); if (!embed) { try { html = injectOgTags(html, { origin, pageUrl, pageDirUrl, }); if (wechatShare) { html = injectWechatShareBridge( html, { pageUrl }, ); } } catch { // Never block delivery on share metadata. } } const isFullHtml = /^\s*]/i.test(html); if ( !embed && !raw && isFullHtml && isLongImageDownloadRequest(req.query) ) { try { const rawUrl = new URL( appendQueryParam( sharePath || originalPath, 'view', 'raw', ), origin || 'http://localhost', ); const image = await renderLongImage({ url: rawUrl.toString(), }); const longImageUrl = new URL( appendQueryParam( sharePath || originalPath, 'download', 'long-image', ), origin || 'http://localhost', ).toString(); await registerLongImageArtifact({ result, image, canonicalUrl: longImageUrl, }); res.set('Content-Type', 'image/png'); res.set( 'Content-Disposition', 'attachment; filename="mindspace-public-page.long.png"', ); res.set( 'Cache-Control', 'no-store', ); return res.send(image); } catch (error) { return res .status(500) .type( 'text/plain; charset=utf-8', ) .send( `长图生成失败:${ error?.message || '未知错误' }`, ); } } const canWrapWithShell = !embed && !raw && isFullHtml && result.publication?.accessMode !== 'password'; if (canWrapWithShell) { const title = detectPublishedPageTitle(html); const rawUrl = appendQueryParam( sharePath || originalPath, 'view', 'raw', ); const longImageUrl = appendQueryParam( sharePath || originalPath, 'download', 'long-image', ); let shellHtml = publishedPageShellHtml({ iframeUrl: rawUrl, shareUrl: pageUrl, title, longImageUrl, }); try { shellHtml = injectOgTags(shellHtml, { origin, pageUrl, pageDirUrl, fallbackImageUrl: extractOgImageUrl(html), meta: extractShareMetaFromPageHtml( html, ), }); if (wechatShare) { shellHtml = injectWechatShareBridge( shellHtml, { pageUrl }, ); } } catch { // Keep the share shell usable. } shellHtml = await decoratePublicationHtmlAnalytics(shellHtml, { result, analyticsConfig, rybbitConfig, getAuthPool, getUserAuth, getMindSpacePages, logger, }); res.set( 'Content-Type', 'text/html; charset=utf-8', ); res.set( 'Content-Security-Policy', wechatShare ? "default-src 'none'; style-src 'unsafe-inline'; img-src data: https:; font-src 'none'; connect-src 'self' https://cdn.jsdelivr.net; script-src 'unsafe-inline' 'self' https://cdn.jsdelivr.net https://res.wx.qq.com; frame-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'" : "default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src 'none'; connect-src 'self' https://cdn.jsdelivr.net; script-src 'unsafe-inline' 'self' https://cdn.jsdelivr.net; frame-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'", ); res.set( 'Cache-Control', result.publication.accessMode === 'public' ? 'public, max-age=60' : 'private, no-store', ); return res.send(shellHtml); } res.set( 'Content-Type', 'text/html; charset=utf-8', ); res.set( 'Content-Security-Policy', publishedPageCsp(html, { embed, raw, wechatShare, scriptHashes: collectInlineScriptHashes(html), }), ); res.set( 'Cache-Control', result.publication.accessMode === 'public' ? 'public, max-age=60' : 'private, no-store', ); return res.send(html); }; }