Files
memind/server/portal-published-page-delivery.mjs
T

282 lines
6.9 KiB
JavaScript

import {
injectPublishedPageDataContext,
} from '../mindspace-public-page-context.mjs';
import {
collectInlineScriptHashes,
} from '../mindspace-public-delivery.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';
export function createPortalPublishedPageDelivery({
registerLongImageArtifact,
renderLongImage =
renderLongImageBuffer,
} = {}) {
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,
},
);
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*<!doctype html/i.test(html) ||
/^\s*<html[\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.
}
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' 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' 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);
};
}