功能(文档预览):集成 ONLYOFFICE 安全只读预览与工作台体验
新增 ONLYOFFICE 配置签名、内部内容接口、容器编排与反向代理。 打通网页端和桌面端独立预览工作区,完善文档入口、布局及帮助体验。 补充桌面安全发布门禁、开发脚本、使用文档和前后端测试。
This commit is contained in:
@@ -30,6 +30,12 @@ const walk = async (directory) => {
|
||||
const permissionIdentifier = (permission) =>
|
||||
typeof permission === "string" ? permission : typeof permission?.identifier === "string" ? permission.identifier : "";
|
||||
const toPosixPath = (path) => path.split("\\").join("/");
|
||||
const cspDirective = (csp, name) =>
|
||||
csp
|
||||
.split(";")
|
||||
.map((directive) => directive.trim().split(/\s+/))
|
||||
.find(([directiveName]) => directiveName === name)
|
||||
?.slice(1) || [];
|
||||
|
||||
const assertPathScope = (permission, expectedPrefix, description) => {
|
||||
const allow = Array.isArray(permission.allow) ? permission.allow : [];
|
||||
@@ -70,6 +76,17 @@ const verifyTauriConfig = async () => {
|
||||
"Tauri CSP must not use wildcard sources.",
|
||||
);
|
||||
assert(!/\bconnect-src\b[^;]*\bhttp:\b/.test(csp), "Tauri CSP must not allow broad http: API access.");
|
||||
const scriptSources = cspDirective(csp, "script-src");
|
||||
const frameSources = cspDirective(csp, "frame-src");
|
||||
assert(
|
||||
scriptSources.length === 1 && scriptSources[0] === "'self'",
|
||||
"Tauri script-src must remain self-only when ONLYOFFICE preview is enabled.",
|
||||
);
|
||||
assert(
|
||||
JSON.stringify(frameSources) ===
|
||||
JSON.stringify(["'self'", "blob:", "https:", "http://localhost:*", "http://127.0.0.1:*"]),
|
||||
"Tauri frame-src may only add HTTPS and local development servers for the isolated ONLYOFFICE host.",
|
||||
);
|
||||
assert(mainWindow?.minWidth === 1180, "Main desktop window must keep the minimum width at 1180.");
|
||||
assert(mainWindow?.minHeight === 760, "Main desktop window must keep the minimum height at 760.");
|
||||
assert(mainWindow?.decorations === true, "Main desktop window must keep native window decorations enabled.");
|
||||
@@ -123,6 +140,11 @@ const verifyCapabilities = async () => {
|
||||
|
||||
for (const file of files) {
|
||||
const capability = await readJson(resolve(capabilitiesDir, file));
|
||||
assert(!capability.remote, `${file}: remote origins must not receive Tauri capabilities.`);
|
||||
assert(
|
||||
Array.isArray(capability.windows) && capability.windows.length === 1 && capability.windows[0] === "main",
|
||||
`${file}: capabilities must remain scoped to the main local window.`,
|
||||
);
|
||||
const permissions = Array.isArray(capability.permissions) ? capability.permissions : [];
|
||||
const identifiers = permissions.map(permissionIdentifier).filter(Boolean);
|
||||
|
||||
@@ -165,6 +187,30 @@ const verifyCapabilities = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const verifyOnlyOfficeBoundary = async () => {
|
||||
const runtimeSource = await readFile(resolve(sourceDir, "runtime/onlyoffice.ts"), "utf8");
|
||||
const pageSource = await readFile(resolve(sourceDir, "views/OfficePreviewWorkspace.vue"), "utf8");
|
||||
const viewerSource = await readFile(resolve(sourceDir, "components/OnlyOfficeViewer.vue"), "utf8");
|
||||
const apiSource = await readFile(resolve(sourceDir, "api/onlyoffice.ts"), "utf8");
|
||||
const hostSource = await readFile(resolve(frontendDir, "public/onlyoffice-host.js"), "utf8");
|
||||
|
||||
assert(runtimeSource.includes('ONLYOFFICE_HOST_PATH = "/onlyoffice-host.html"'), "ONLYOFFICE host path must remain fixed.");
|
||||
assert(runtimeSource.includes("resolveApiBaseUrl()"), "ONLYOFFICE host must derive from the validated runtime server base URL.");
|
||||
assert(!runtimeSource.includes("url:"), "ONLYOFFICE runtime URL resolver must not accept a business-provided URL.");
|
||||
assert(pageSource.includes("response.data.host_path !== ONLYOFFICE_HOST_PATH"), "ONLYOFFICE config host path must be checked against the fixed host path.");
|
||||
assert(pageSource.includes("onDeactivated") && pageSource.includes("destroyViewer()"), "ONLYOFFICE viewer must be destroyed when a desktop task is deactivated.");
|
||||
assert(viewerSource.includes("event.source !== frameWindow"), "ONLYOFFICE bridge must validate the message source window.");
|
||||
assert(viewerSource.includes("event.origin !== hostUrl.origin"), "ONLYOFFICE bridge must validate message origin.");
|
||||
assert(viewerSource.includes("message.nonce !== nonce"), "ONLYOFFICE bridge must validate its in-memory nonce.");
|
||||
assert(hostSource.includes("event.source !== window.parent"), "ONLYOFFICE host must only accept parent-window messages.");
|
||||
assert(hostSource.includes("!isAllowedParentOrigin(event.origin)"), "ONLYOFFICE host must validate the parent origin.");
|
||||
assert(hostSource.includes("initialized ||"), "ONLYOFFICE host must only accept one initialization.");
|
||||
assert(hostSource.includes("message?.nonce"), "ONLYOFFICE host must require the in-memory nonce.");
|
||||
assert(!/\b(?:localStorage|sessionStorage|console\.)\b/.test(hostSource), "ONLYOFFICE host must not persist or log signed configuration.");
|
||||
assert(apiSource.includes("cache: false"), "ONLYOFFICE signed config must not enter the desktop data cache.");
|
||||
assert(apiSource.includes("disableRequestDedupe: true"), "ONLYOFFICE signed config requests must not be deduplicated.");
|
||||
};
|
||||
|
||||
const verifyRustBoundary = async () => {
|
||||
const libSource = await readFile(resolve(tauriDir, "src/lib.rs"), "utf8");
|
||||
const forbiddenRust = ["tauri_plugin_shell", "std::process::Command", "std::process"];
|
||||
@@ -445,6 +491,7 @@ await verifySourceSafety();
|
||||
await verifyNotificationBoundary();
|
||||
await verifySessionBoundary();
|
||||
await verifyUpdaterBoundary();
|
||||
await verifyOnlyOfficeBoundary();
|
||||
await verifyWorkflowGates();
|
||||
|
||||
if (failures.length > 0) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
|
||||
|
||||
const main = readFileSync("src/styles/main.css", "utf8");
|
||||
const unified = readFileSync("src/styles/unified-page.css", "utf8");
|
||||
const webLayout = readFileSync("src/components/WebLayout.vue", "utf8");
|
||||
|
||||
const requiredMainTokens = [
|
||||
"--ctms-primary",
|
||||
@@ -31,6 +32,29 @@ const missing = [
|
||||
...requiredUnifiedTokens.filter((t) => !unified.includes(t))
|
||||
];
|
||||
|
||||
const requiredWebEdgeTokens = [
|
||||
".web-layout-container .ctms-route-shell > *",
|
||||
".web-layout-container .ctms-route-shell > .page > .table-card"
|
||||
];
|
||||
|
||||
for (const token of requiredWebEdgeTokens) {
|
||||
if (!main.includes(token)) {
|
||||
missing.push(`src/styles/main.css:${token}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!/\.web-layout-container \.content-wrapper\s*\{[^}]*padding:\s*0;/s.test(webLayout)) {
|
||||
missing.push("src/components/WebLayout.vue:web content wrapper edge alignment");
|
||||
}
|
||||
|
||||
if (!/\.web-layout-container \.content-wrapper\s*\{[^}]*height:\s*100%;[^}]*min-height:\s*0;/s.test(webLayout)) {
|
||||
missing.push("src/components/WebLayout.vue:web content height chain");
|
||||
}
|
||||
|
||||
if (webLayout.includes("padding: 18px 24px 24px;") || webLayout.includes("padding: 22px 32px 32px;")) {
|
||||
missing.push("src/components/WebLayout.vue:large-screen content padding override");
|
||||
}
|
||||
|
||||
const pageContractChecks = [
|
||||
{
|
||||
file: "src/views/ia/ProjectMilestones.vue",
|
||||
|
||||
Reference in New Issue
Block a user