Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ const appPort = Number(process.env.PLAYWRIGHT_APP_PORT ?? 3001);
const cdnPort = Number(process.env.PLAYWRIGHT_CDN_PORT ?? 3010);
const host = process.env.PLAYWRIGHT_HOST ?? "127.0.0.1";
const isCI = !!process.env.CI;
const headless =
process.env.PLAYWRIGHT_HEADLESS !== undefined
? process.env.PLAYWRIGHT_HEADLESS !== "false"
: isCI;
const headless = true;
const appCommand = isCI
? `pnpm exec next start --port ${appPort} --hostname ${host}`
: `pnpm exec next dev --turbopack --port ${appPort} --hostname ${host}`;
Expand Down
Binary file modified public/packages/onlyoffice/9.3.0/x2t/x2t.js
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

1. **静态资源**:将 OnlyOffice SDK(含 `web-apps/`、`sdkjs/`、`fonts/`、`x2t/`)放到站点可访问目录,默认 `public/packages/onlyoffice/9.3.0/`。自定义字体需在 `AllFonts.js` 的 `__custom_font_registry__` 中注册,详见 [10 - 字体配置](./10-字体配置.md)。
2. **环境变量**(可选):`NEXT_PUBLIC_APP_ROOT=/packages/onlyoffice/9.3.0`,与 `STATIC_RESOURCE.onlyoffice.root` 一致。
3. **x2t Brotli**:`x2t/x2t.js``x2t.wasm` 为 Brotli 预压缩文件;**无需**配置 `Content-Encoding: br`,Worker 内 `fetch-brotli` 会自动解压。
3. **x2t 资源**:`x2t/x2t.js` 为普通 JS 文本,`x2t.wasm` 为 Brotli 预压缩文件;**无需**为 `x2t.wasm` 配置 `Content-Encoding: br`,Worker 内 `fetch-brotli` 会自动解压。
4. **DOM 容器**:页面需预留编辑器挂载点(见 [01-快速开始](./01-快速开始.md))。

## 注意事项
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
import { concatBytes, crc32, writeU16, writeU32 } from "./zip";

const encoder = new TextEncoder();

function utf8(value: string) {
return encoder.encode(value);
}

function xml(strings: TemplateStringsArray, ...values: unknown[]) {
return strings
.reduce((result, value, index) => `${result}${value}${values[index] ?? ""}`, "")
.trim();
}

function escapeXml(value: string) {
return value
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
}

function decodeText(buffer: ArrayBuffer) {
return new TextDecoder("utf-8").decode(buffer);
}

function zip(entries: Array<[string, string | Uint8Array]>) {
const localParts: Uint8Array[] = [];
const centralParts: Uint8Array[] = [];
let localOffset = 0;

for (const [name, content] of entries) {
const nameBytes = utf8(name);
const data = typeof content === "string" ? utf8(content) : content;
const crc = crc32(data);

const localHeader = new Uint8Array(30 + nameBytes.length);
writeU32(localHeader, 0, 0x04034b50);
writeU16(localHeader, 4, 20);
writeU16(localHeader, 6, 0);
writeU16(localHeader, 8, 0);
writeU16(localHeader, 10, 0);
writeU16(localHeader, 12, 0);
writeU32(localHeader, 14, crc);
writeU32(localHeader, 18, data.length);
writeU32(localHeader, 22, data.length);
writeU16(localHeader, 26, nameBytes.length);
writeU16(localHeader, 28, 0);
localHeader.set(nameBytes, 30);
localParts.push(localHeader, data);

const centralHeader = new Uint8Array(46 + nameBytes.length);
writeU32(centralHeader, 0, 0x02014b50);
writeU16(centralHeader, 4, 20);
writeU16(centralHeader, 6, 20);
writeU16(centralHeader, 8, 0);
writeU16(centralHeader, 10, 0);
writeU16(centralHeader, 12, 0);
writeU16(centralHeader, 14, 0);
writeU32(centralHeader, 16, crc);
writeU32(centralHeader, 20, data.length);
writeU32(centralHeader, 24, data.length);
writeU16(centralHeader, 28, nameBytes.length);
writeU16(centralHeader, 30, 0);
writeU16(centralHeader, 32, 0);
writeU16(centralHeader, 34, 0);
writeU16(centralHeader, 36, 0);
writeU32(centralHeader, 38, 0);
writeU32(centralHeader, 42, localOffset);
centralHeader.set(nameBytes, 46);
centralParts.push(centralHeader);

localOffset += localHeader.length + data.length;
}

const centralDirectory = concatBytes(centralParts);
const end = new Uint8Array(22);
writeU32(end, 0, 0x06054b50);
writeU16(end, 8, entries.length);
writeU16(end, 10, entries.length);
writeU32(end, 12, centralDirectory.length);
writeU32(end, 16, localOffset);

return concatBytes([...localParts, centralDirectory, end]).buffer;
}

const coreProps = xml`
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/">
<dc:title>Plain text fallback</dc:title>
</cp:coreProperties>
`;

const appProps = xml`
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties">
<Application>OnlyOffice Web Comp</Application>
</Properties>
`;

export function createDocxFromText(buffer: ArrayBuffer) {
const paragraphs = decodeText(buffer)
.split(/\r\n|\r|\n/)
.map((line) => `<w:p><w:r><w:t xml:space="preserve">${escapeXml(line)}</w:t></w:r></w:p>`)
.join("");

return zip([
[
"[Content_Types].xml",
xml`
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>
<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>
</Types>
`,
],
[
"_rels/.rels",
xml`
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>
<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>
</Relationships>
`,
],
["docProps/core.xml", coreProps],
["docProps/app.xml", appProps],
[
"word/document.xml",
xml`
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>
${paragraphs || '<w:p><w:r><w:t></w:t></w:r></w:p>'}
<w:sectPr>
<w:pgSz w:w="11906" w:h="16838"/>
<w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440"/>
</w:sectPr>
</w:body>
</w:document>
`,
],
[
"word/_rels/document.xml.rels",
xml`
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"/>
`,
],
]);
}

export function createXlsxFromText(buffer: ArrayBuffer) {
const text = escapeXml(decodeText(buffer));

return zip([
[
"[Content_Types].xml",
xml`
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>
<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>
</Types>
`,
],
[
"_rels/.rels",
xml`
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>
<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>
</Relationships>
`,
],
["docProps/core.xml", coreProps],
["docProps/app.xml", appProps],
[
"xl/workbook.xml",
xml`
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<sheets><sheet name="Data" sheetId="1" r:id="rId1"/></sheets>
</workbook>
`,
],
[
"xl/_rels/workbook.xml.rels",
xml`
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
</Relationships>
`,
],
[
"xl/styles.xml",
xml`
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<fonts count="1"><font><sz val="11"/><name val="Calibri"/></font></fonts>
<fills count="1"><fill><patternFill patternType="none"/></fill></fills>
<borders count="1"><border/></borders>
<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>
<cellXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/></cellXfs>
</styleSheet>
`,
],
[
"xl/worksheets/sheet1.xml",
xml`
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<sheetData>
<row r="1"><c r="A1" t="inlineStr"><is><t xml:space="preserve">${text}</t></is></c></row>
</sheetData>
</worksheet>
`,
],
]);
}
44 changes: 40 additions & 4 deletions src/components/onlyoffice-web-comp/internal/editor/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from "./types";
import { emptyDocx, emptyPdf, emptyPptx, emptyXlsx } from "./empty";
import { convertCsvBufferToXlsxBuffer } from "./csv-to-xlsx";
import { createDocxFromText, createXlsxFromText } from "./plain-text-office";
import {
getDocumentType,
getFileExt,
Expand Down Expand Up @@ -91,6 +92,31 @@ function isOfficeZipFileType(fileType: string) {
);
}

function isZipBytes(data: Uint8Array) {
return (
data.length >= 4 &&
data[0] === 0x50 &&
data[1] === 0x4b &&
((data[2] === 0x03 && data[3] === 0x04) ||
(data[2] === 0x05 && data[3] === 0x06) ||
(data[2] === 0x07 && data[3] === 0x08))
);
}

function createPlainTextOfficeFallback(
buffer: ArrayBuffer,
fileType: string,
) {
switch (getDocumentType(fileType)) {
case "word":
return { buffer: createDocxFromText(buffer), fileType: "docx" };
case "cell":
return { buffer: createXlsxFromText(buffer), fileType: "xlsx" };
default:
return null;
}
}

const PDF_MAX_OUTPUT_BYTES = 50 * 1024 * 1024;
const PDF_EOF_MARKER = new TextEncoder().encode("%%EOF");

Expand Down Expand Up @@ -914,23 +940,33 @@ export class EditorServer {
buffer = await buffer();
}

const bytes = new Uint8Array(buffer);
const fallback =
isOfficeZipFileType(fileType) && !isZipBytes(bytes)
? createPlainTextOfficeFallback(buffer, fileType)
: null;
if (fallback) {
buffer = fallback.buffer;
}
const sourceFileType = fallback?.fileType ?? fileType;

let output: Uint8Array | null = null;
let media: { [key: string]: Uint8Array } = {};
let themes: { [key: string]: Uint8Array } = {};

if (fileType == "pdf") {
if (sourceFileType == "pdf") {
output = new Uint8Array(buffer);
} else if (fileType === "csv") {
} else if (sourceFileType === "csv") {
({ output, media } = await this.loadCsvDocument(buffer));
} else {
({ output, media, themes } = await this.convertBufferToEditorBin(
buffer,
fileType,
sourceFileType,
));
}

if (!output) {
throw new Error(`Failed to convert ${fileType} file`);
throw new Error(`Failed to convert ${sourceFileType} file`);
}

if (this.urlsMap.size > 0) {
Expand Down
14 changes: 14 additions & 0 deletions tests/e2e/files/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,19 @@
"fileType": "DOCX",
"source": "extension/content mismatch rejection",
"size": 6524
},
{
"name": "plain-text-as-docx.docx",
"kind": "positive",
"fileType": "DOCX",
"source": "plain text content with DOCX extension fallback",
"size": 14
},
{
"name": "plain-text-as-xlsx.xlsx",
"kind": "positive",
"fileType": "XLSX",
"source": "plain text content with XLSX extension fallback",
"size": 15
}
]
2 changes: 2 additions & 0 deletions tests/e2e/files/plain-text-as-docx.docx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dsadads
dasdas
1 change: 1 addition & 0 deletions tests/e2e/files/plain-text-as-xlsx.xlsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dsadads.dasdas
14 changes: 14 additions & 0 deletions tests/e2e/scripts/generate-office-files.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,20 @@ const fixtures = [
fileType: "DOCX",
source: "extension/content mismatch rejection",
},
{
name: "plain-text-as-docx.docx",
data: utf8("dsadads\ndasdas"),
kind: "positive",
fileType: "DOCX",
source: "plain text content with DOCX extension fallback",
},
{
name: "plain-text-as-xlsx.xlsx",
data: utf8("dsadads.dasdas\n"),
kind: "positive",
fileType: "XLSX",
source: "plain text content with XLSX extension fallback",
},
];

fs.rmSync(outputDir, { recursive: true, force: true });
Expand Down
Loading
Loading