import crypto from 'node:crypto'; import fs from 'node:fs'; import path from 'node:path'; import { injectMindSpacePageDataContext } from './mindspace-public-page-context.mjs'; import { preparePublishedPlatformBrand } from './mindspace-page-tag.mjs'; import { injectPublicImageRetryScript } from './mindspace-public-image-retry.mjs'; import { applyWechatSurveyCompat } from './mindspace-page-data-wechat-survey-compat.mjs'; import { stripPublicationHtmlCspMeta } from './plaza-embed.mjs'; const INLINE_SCRIPT_PATTERN = /]*\bsrc\b)[^>]*>([\s\S]*?)<\/script>/gi; export function collectInlineScriptHashes(html) { const hashes = []; const seen = new Set(); for (const match of String(html ?? '').matchAll(INLINE_SCRIPT_PATTERN)) { const hash = crypto.createHash('sha256').update(match[1] ?? '').digest('base64'); if (seen.has(hash)) continue; seen.add(hash); hashes.push(hash); } return hashes; } export async function handleMindSpaceLongImageDownload({ query, filePath, pageUrl = null, res, isLongImageDownloadRequest, longImagePathForHtml, renderLongImage, } = {}) { if (!isLongImageDownloadRequest?.(query)) return false; const longImagePath = longImagePathForHtml(filePath); try { await renderLongImage(pageUrl ? { url: pageUrl, outputPath: longImagePath } : { htmlPath: filePath, outputPath: longImagePath }); res.set('Cache-Control', 'no-store'); res.download(longImagePath, path.basename(longImagePath), (err) => { if (err && !res.headersSent) res.status(404).json({ message: '长图文件不存在' }); }); } catch (error) { res .status(500) .type('text/plain; charset=utf-8') .send(`长图生成失败:${error?.message || '未知错误'}`); } return true; } export function decorateMindSpacePublishedHtml({ html, embed = false, isOwner = true, pageDataContext = null, context, htmlFilePath = '', fileExists = fs.existsSync, userAgent = '', preparePublicationHtmlForEmbed, injectOgTags, injectWechatShareBridge, injectPublicFileShareButton, publishedPageCsp, isWechatUserAgent, } = {}) { // Preview HTML carries a restrictive inline CSP (often script-src 'none'). // Published delivery sets the authoritative CSP response header below; keep // the preview meta out of the delivered document so Page Data and other // same-origin scripts are governed by that header instead of being blocked // by a stale preview policy. let nextHtml = stripPublicationHtmlCspMeta(html); let allowEmbedFrame = false; if (embed) { nextHtml = preparePublicationHtmlForEmbed(nextHtml); allowEmbedFrame = true; } try { nextHtml = preparePublishedPlatformBrand(nextHtml); nextHtml = injectOgTags(nextHtml, { origin: context.origin, pageUrl: context.pageUrl, pageDirUrl: context.pageDirUrl, fallbackImageUrl: context.fallbackImageUrl, htmlFilePath, fileExists, }); const wechatShare = !embed && isWechatUserAgent(userAgent || ''); if (wechatShare) { nextHtml = injectWechatShareBridge(nextHtml, { pageUrl: context.pageUrl }); } const shareInjection = !embed ? injectPublicFileShareButton(nextHtml, { isOwner }) : { html: nextHtml, scriptHashes: [] }; nextHtml = shareInjection.html; // Retries transient asset-download failures client-side before a page's own onerror // permanently swaps the for a placeholder. See mindspace-public-image-retry.mjs. if (!embed) { nextHtml = injectPublicImageRetryScript(nextHtml).html; } if (pageDataContext?.pageId) { nextHtml = injectMindSpacePageDataContext(nextHtml, pageDataContext); } if (!embed && isWechatUserAgent(userAgent || '')) { nextHtml = applyWechatSurveyCompat(nextHtml).html; } const scriptHashes = collectInlineScriptHashes(nextHtml); return { html: nextHtml, csp: publishedPageCsp(nextHtml, { embed, wechatShare, scriptHashes, }), allowEmbedFrame, }; } catch { return { html: nextHtml, csp: publishedPageCsp(nextHtml, { embed }), allowEmbedFrame, }; } }