fix(learn): skip plaza share widget on learning assistant pages
Wire delivery path detection through portal static routes and strip baked share chrome from learn parent/child HTML. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import { stripPublicationHtmlCspMeta } from './plaza-embed.mjs';
|
||||
import {
|
||||
decorateMindSpaceSeoGeoHtml,
|
||||
} from './mindspace-seo-geo-delivery.mjs';
|
||||
import { stripPublicFileShareWidget } from './mindspace-public-share-widget.mjs';
|
||||
|
||||
const INLINE_SCRIPT_PATTERN = /<script\b(?![^>]*\bsrc\b)[^>]*>([\s\S]*?)<\/script>/gi;
|
||||
|
||||
@@ -24,6 +25,22 @@ export function collectInlineScriptHashes(html) {
|
||||
return hashes;
|
||||
}
|
||||
|
||||
export function shouldSkipPublicShareWidget({
|
||||
htmlFilePath = '',
|
||||
pageUrl = '',
|
||||
relativePath = '',
|
||||
requestPath = '',
|
||||
} = {}) {
|
||||
const haystack = `${htmlFilePath}\n${pageUrl}\n${relativePath}\n${requestPath}`
|
||||
.replace(/\\/g, '/')
|
||||
.toLowerCase();
|
||||
return (
|
||||
haystack.includes('learning-assistant') ||
|
||||
haystack.includes('/public/learn/') ||
|
||||
/\/learn(?:\/|$|[?#])/.test(haystack)
|
||||
);
|
||||
}
|
||||
|
||||
export async function handleMindSpaceLongImageDownload({
|
||||
query,
|
||||
filePath,
|
||||
@@ -59,6 +76,8 @@ export function decorateMindSpacePublishedHtml({
|
||||
pageDataContext = null,
|
||||
context,
|
||||
htmlFilePath = '',
|
||||
relativePath = '',
|
||||
requestPath = '',
|
||||
fileExists = fs.existsSync,
|
||||
userAgent = '',
|
||||
preparePublicationHtmlForEmbed,
|
||||
@@ -97,10 +116,17 @@ export function decorateMindSpacePublishedHtml({
|
||||
if (wechatShare) {
|
||||
nextHtml = injectWechatShareBridge(nextHtml, { pageUrl: context.pageUrl });
|
||||
}
|
||||
const shareInjection = !embed
|
||||
? injectPublicFileShareButton(nextHtml, { isOwner })
|
||||
: { html: nextHtml, scriptHashes: [] };
|
||||
nextHtml = shareInjection.html;
|
||||
const skipShareWidget = !embed && shouldSkipPublicShareWidget({
|
||||
htmlFilePath,
|
||||
pageUrl: context?.pageUrl ?? '',
|
||||
relativePath,
|
||||
requestPath,
|
||||
});
|
||||
if (skipShareWidget) {
|
||||
nextHtml = stripPublicFileShareWidget(nextHtml);
|
||||
} else if (!embed) {
|
||||
nextHtml = injectPublicFileShareButton(nextHtml, { isOwner }).html;
|
||||
}
|
||||
// Retries transient asset-download failures client-side before a page's own onerror
|
||||
// permanently swaps the <img> for a placeholder. See mindspace-public-image-retry.mjs.
|
||||
if (!embed) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
collectInlineScriptHashes,
|
||||
decorateMindSpacePublishedHtml,
|
||||
handleMindSpaceLongImageDownload,
|
||||
shouldSkipPublicShareWidget,
|
||||
} from './mindspace-public-delivery.mjs';
|
||||
|
||||
test('collectInlineScriptHashes returns sha256 hashes for inline scripts only', () => {
|
||||
@@ -173,6 +174,67 @@ test('decorateMindSpacePublishedHtml forwards isOwner=false to the share button
|
||||
assert.equal(sharedIsOwner, false);
|
||||
});
|
||||
|
||||
test('shouldSkipPublicShareWidget matches learning assistant delivery paths', () => {
|
||||
assert.equal(
|
||||
shouldSkipPublicShareWidget({
|
||||
pageUrl: 'https://m.tkmind.cn/MindSpace/u/public/learning-assistant.html',
|
||||
}),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
shouldSkipPublicShareWidget({
|
||||
htmlFilePath: '/Users/john/Project/Memind/public/learn/child.html',
|
||||
}),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
shouldSkipPublicShareWidget({
|
||||
relativePath: 'public/learning-assistant.html',
|
||||
}),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
shouldSkipPublicShareWidget({
|
||||
requestPath: '/MindSpace/u/public/learning-assistant-parent.html',
|
||||
}),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
shouldSkipPublicShareWidget({
|
||||
pageUrl: 'https://m.tkmind.cn/MindSpace/u/public/report.html',
|
||||
}),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('decorateMindSpacePublishedHtml skips plaza share widget on learning assistant pages', () => {
|
||||
let shareInjected = false;
|
||||
const baked = `<html><body><h1>Learn</h1><style id="mindspace-public-share-style">x</style><script>x</script></body></html>`;
|
||||
const result = decorateMindSpacePublishedHtml({
|
||||
html: baked,
|
||||
embed: false,
|
||||
htmlFilePath: '/tmp/MindSpace/u/public/learning-assistant.html',
|
||||
context: {
|
||||
origin: 'https://m.tkmind.cn',
|
||||
pageUrl: 'https://m.tkmind.cn/MindSpace/u/public/learning-assistant.html',
|
||||
pageDirUrl: 'https://m.tkmind.cn/MindSpace/u/public/',
|
||||
fallbackImageUrl: '',
|
||||
},
|
||||
preparePublicationHtmlForEmbed: (value) => value,
|
||||
injectOgTags: (value) => value,
|
||||
injectWechatShareBridge: (value) => value,
|
||||
injectPublicFileShareButton: (value) => {
|
||||
shareInjected = true;
|
||||
return { html: `${value}<!--share-->`, scriptHashes: [] };
|
||||
},
|
||||
publishedPageCsp: (value) => value,
|
||||
isWechatUserAgent: () => false,
|
||||
});
|
||||
|
||||
assert.equal(shareInjected, false);
|
||||
assert.doesNotMatch(result.html, /mindspace-public-share/);
|
||||
});
|
||||
|
||||
test('decorateMindSpacePublishedHtml supports embed mode without share injection', () => {
|
||||
const result = decorateMindSpacePublishedHtml({
|
||||
html: '<html></html>',
|
||||
|
||||
@@ -202,7 +202,10 @@ const PUBLIC_FILE_SHARE_WIDGET_BLOCK_PATTERN =
|
||||
/<style id="mindspace-public-share-style">[\s\S]*?<\/script>\s*/i;
|
||||
|
||||
export function stripPublicFileShareWidget(html) {
|
||||
return String(html ?? '').replace(PUBLIC_FILE_SHARE_WIDGET_BLOCK_PATTERN, '');
|
||||
let next = String(html ?? '').replace(PUBLIC_FILE_SHARE_WIDGET_BLOCK_PATTERN, '');
|
||||
next = next.replace(/<div[^>]*data-mindspace-wechat-dialog[\s\S]*?<\/div>\s*/gi, '');
|
||||
next = next.replace(/<div[^>]*data-mindspace-public-share[\s\S]*?<\/div>\s*/gi, '');
|
||||
return next;
|
||||
}
|
||||
|
||||
export function injectPublicFileShareButton(html, { isOwner = true } = {}) {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { injectPublicFileShareButton } from './mindspace-public-share-widget.mjs';
|
||||
import {
|
||||
injectPublicFileShareButton,
|
||||
stripPublicFileShareWidget,
|
||||
} from './mindspace-public-share-widget.mjs';
|
||||
|
||||
test('injectPublicFileShareButton adds plaza entry and confirm dialog', () => {
|
||||
const result = injectPublicFileShareButton('<!doctype html><html><body><h1>Hi</h1></body></html>');
|
||||
@@ -63,3 +66,11 @@ test('injectPublicFileShareButton keeps Plaza entry when isOwner explicitly true
|
||||
assert.match(result.html, /data-action="plaza"/);
|
||||
assert.match(result.html, /data-mindspace-public-share-dialog/);
|
||||
});
|
||||
|
||||
test('stripPublicFileShareWidget removes injected share chrome', () => {
|
||||
const injected = injectPublicFileShareButton('<!doctype html><html><body><h1>Hi</h1></body></html>');
|
||||
const stripped = stripPublicFileShareWidget(injected.html);
|
||||
assert.doesNotMatch(stripped, /data-mindspace-public-share/);
|
||||
assert.doesNotMatch(stripped, /公众号/);
|
||||
assert.doesNotMatch(stripped, /保存长图/);
|
||||
});
|
||||
|
||||
@@ -67,7 +67,8 @@ th{font-size:11px;color:#6b7280}
|
||||
.tabs{display:flex;gap:8px;margin-top:14px}
|
||||
.tabs button{flex:1;background:#e8ebf2;color:#6b7280}
|
||||
.tabs button.on{background:#3b5bfd;color:#fff}
|
||||
.empty{text-align:center;color:#9ca3af;font-size:13px;padding:18px 0;line-height:1.7}</style></head><body>
|
||||
.empty{text-align:center;color:#9ca3af;font-size:13px;padding:18px 0;line-height:1.7}
|
||||
[data-mindspace-public-share],[data-mindspace-wechat-dialog],.publication-share-fab,.publication-share-sheet{display:none!important;visibility:hidden!important}</style></head><body>
|
||||
<div class="wrap" id="app"></div><div class="tabbar" id="tabbar"></div>
|
||||
<div style="text-align:center;font-size:10px;color:#c3c9d6;padding:6px 0 76px">v2</div>
|
||||
<script>
|
||||
|
||||
@@ -67,7 +67,8 @@ th{font-size:11px;color:#6b7280}
|
||||
.tabs{display:flex;gap:8px;margin-top:14px}
|
||||
.tabs button{flex:1;background:#e8ebf2;color:#6b7280}
|
||||
.tabs button.on{background:#3b5bfd;color:#fff}
|
||||
.empty{text-align:center;color:#9ca3af;font-size:13px;padding:18px 0;line-height:1.7}body{padding-bottom:30px}.tabs{position:sticky;top:0;background:#f2f4fa;padding-top:2px;z-index:10}.pl{max-width:720px}</style></head><body>
|
||||
.empty{text-align:center;color:#9ca3af;font-size:13px;padding:18px 0;line-height:1.7}
|
||||
[data-mindspace-public-share],[data-mindspace-wechat-dialog],.publication-share-fab,.publication-share-sheet{display:none!important;visibility:hidden!important}body{padding-bottom:30px}.tabs{position:sticky;top:0;background:#f2f4fa;padding-top:2px;z-index:10}.pl{max-width:720px}</style></head><body>
|
||||
<div class="wrap pl" id="app"></div>
|
||||
<script>
|
||||
var FAMILY_SLUG=new URLSearchParams(location.search).get('f')||'';
|
||||
|
||||
@@ -79,7 +79,8 @@ th{font-size:11px;color:#6b7280}
|
||||
.tabs{display:flex;gap:8px;margin-top:14px}
|
||||
.tabs button{flex:1;background:#e8ebf2;color:#6b7280}
|
||||
.tabs button.on{background:#3b5bfd;color:#fff}
|
||||
.empty{text-align:center;color:#9ca3af;font-size:13px;padding:18px 0;line-height:1.7}`;
|
||||
.empty{text-align:center;color:#9ca3af;font-size:13px;padding:18px 0;line-height:1.7}
|
||||
[data-mindspace-public-share],[data-mindspace-wechat-dialog],.publication-share-fab,.publication-share-sheet{display:none!important;visibility:hidden!important}`;
|
||||
|
||||
function childScript() {
|
||||
return `
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
} from '../mindspace-public-page-context.mjs';
|
||||
import {
|
||||
collectInlineScriptHashes,
|
||||
shouldSkipPublicShareWidget,
|
||||
} from '../mindspace-public-delivery.mjs';
|
||||
import {
|
||||
buildViewerAnalyticsIdentity,
|
||||
@@ -336,7 +337,15 @@ export function createPortalPublishedPageDelivery({
|
||||
!raw &&
|
||||
isFullHtml &&
|
||||
result.publication?.accessMode !==
|
||||
'password';
|
||||
'password' &&
|
||||
!shouldSkipPublicShareWidget({
|
||||
pageUrl,
|
||||
requestPath: originalPath,
|
||||
relativePath:
|
||||
result.relativePath ??
|
||||
result.pageSource?.relativePath ??
|
||||
'',
|
||||
});
|
||||
if (canWrapWithShell) {
|
||||
const title =
|
||||
detectPublishedPageTitle(html);
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
injectOgTags,
|
||||
renderWechatSharePreviewHtml,
|
||||
} from '../mindspace-og-tags.mjs';
|
||||
import { stripPublicFileShareWidget } from '../mindspace-public-share-widget.mjs';
|
||||
|
||||
export function attachPortalStaticDeliveryRoutes({
|
||||
app,
|
||||
@@ -161,12 +162,32 @@ export function attachPortalStaticDeliveryRoutes({
|
||||
{ maxAge: '1d' },
|
||||
),
|
||||
);
|
||||
app.use(
|
||||
'/learn',
|
||||
staticMiddleware(pathApi.join(h5Root, 'public/learn'), {
|
||||
maxAge: '0',
|
||||
}),
|
||||
);
|
||||
const learnRoot = pathApi.join(h5Root, 'public/learn');
|
||||
const learnStatic = staticMiddleware(learnRoot, { maxAge: '0' });
|
||||
app.use('/learn', (req, res, next) => {
|
||||
const pathname = String(req.path ?? '').split('?')[0];
|
||||
if (!/\.html$/i.test(pathname)) {
|
||||
return learnStatic(req, res, next);
|
||||
}
|
||||
const filePath = pathApi.join(learnRoot, pathname);
|
||||
const resolvedRoot = pathApi.resolve(learnRoot);
|
||||
const resolvedPath = pathApi.resolve(filePath);
|
||||
if (
|
||||
!resolvedPath.startsWith(`${resolvedRoot}${pathApi.sep}`) &&
|
||||
resolvedPath !== resolvedRoot
|
||||
) {
|
||||
return res.status(403).json({ message: '禁止访问' });
|
||||
}
|
||||
if (!fsApi.existsSync(resolvedPath) || !fsApi.statSync(resolvedPath).isFile()) {
|
||||
return learnStatic(req, res, next);
|
||||
}
|
||||
const html = stripPublicFileShareWidget(
|
||||
fsApi.readFileSync(resolvedPath, 'utf8'),
|
||||
);
|
||||
res.set('Content-Type', 'text/html; charset=utf-8');
|
||||
res.set('Cache-Control', 'no-store');
|
||||
return res.send(html);
|
||||
});
|
||||
|
||||
app.get(
|
||||
'/dev/wechat-share-preview',
|
||||
|
||||
@@ -288,6 +288,9 @@ export function createPortalWorkspacePublicationDelivery({
|
||||
isOwner,
|
||||
pageDataContext,
|
||||
context,
|
||||
htmlFilePath: delivery.filePath ?? '',
|
||||
relativePath: delivery.relativePath ?? '',
|
||||
requestPath: req.originalUrl || req.url || '',
|
||||
userAgent:
|
||||
req.get('user-agent') || '',
|
||||
preparePublicationHtmlForEmbed,
|
||||
|
||||
Reference in New Issue
Block a user