From 28a2e6a2709e8caeb82aa5526e461694f4ada8cd Mon Sep 17 00:00:00 2001 From: chaxus Date: Sun, 23 Aug 2026 23:15:06 +0800 Subject: [PATCH] fix(license): restore the ONLYOFFICE product logo and add the notices AGPL Section 7 requires The vendor's AGPL-3.0 headers carry two additional terms under Section 7: 7(b) requires a derivative work to retain the original product logo, and 7(e) declines to grant any rights under trademark law. This build honoured neither. guards/chrome.ts hid `#header-logo` and the DocEditor config set `customization.about: false`, which between them left no product mark anywhere in the interface, and no trademark notice existed in the repository or on the site. Both removals were deliberate UI tidy-ups, so a comment would not have held: - guards/chrome.ts keeps hiding the current-user and co-users widgets (a collaboration session a serverless build cannot have) and nothing else. - `customization.about` goes back to its default. The vendor's own `canBrandingExt` check exists to force it back on; this offline build just does not enforce it. - New guard 12 (guards/about-source.ts) appends this build's own two facts to the About pane -- that it is a modified version and not an official ONLYOFFICE product, and where its source is (Section 13). The pane is populated lazily, so it watches for the content rather than writing once. - NOTICE quotes the terms verbatim, pins the vendor version, and lists every change made to the vendor tree (Section 5(a)). - The eight readmes point at it; all seven site languages carry a trademark line in both footers. Pinned by test/unit/branding-notice.test.ts and test/e2e/vendor-branding.spec.ts. Reverse-verified twice: restoring the two suppressions turns the three editor cases red, and dropping the footer notice turns the three page cases red. Also in passing: /zh/ used to 404 (the real prefix is the BCP-47 /zh-CN/), and the embed docs never said prominently that `document:open-url` and `?src=` need CORS on the file's own host. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 8 ++ CLAUDE.md | 15 +- NOTICE | 127 +++++++++++++++++ bin/build-pages.mjs | 9 ++ docs/embed-api.md | 11 ++ docs/embed-api.zh.md | 7 + ...3-onlyoffice-attribution-agpl-section-7.md | 90 ++++++++++++ index.html | 1 + lib/onlyoffice-editor.ts | 7 +- lib/onlyoffice/guards/about-source.ts | 66 +++++++++ lib/onlyoffice/guards/chrome.ts | 15 +- lib/onlyoffice/iframe-guards.ts | 2 + public/_redirects | 6 + public/home.css | 10 ++ public/landing.css | 11 ++ readme.de.md | 12 +- readme.es.md | 12 +- readme.fa.md | 11 +- readme.ja.md | 11 +- readme.ko.md | 11 +- readme.md | 11 +- readme.pt.md | 12 +- readme.zh.md | 10 +- test/e2e/vendor-branding.spec.ts | 121 +++++++++++++++++ test/unit/branding-notice.test.ts | 128 ++++++++++++++++++ test/unit/hosting-contract.test.ts | 3 + 26 files changed, 712 insertions(+), 15 deletions(-) create mode 100644 NOTICE create mode 100644 docs/explorations/2026-08-23-onlyoffice-attribution-agpl-section-7.md create mode 100644 lib/onlyoffice/guards/about-source.ts create mode 100644 test/e2e/vendor-branding.spec.ts create mode 100644 test/unit/branding-notice.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index acdcc3e35..434af3689 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,14 @@ notes. Entries describe what users experience, not internal refactors. the session on them, so a laptop keeps a recovery point about every 30 seconds while a phone under load backs off instead of competing with the document you are editing. +- **The ONLYOFFICE logo is back in the editor header, and the About pane with + it.** Both had been switched off as interface clutter. They are not: this + editor is a modified version of ONLYOFFICE, and the license it is published + under requires the product logo to stay. The About pane now also says that + this build is not an official ONLYOFFICE product and links to the source it + was built from. Every page of the site carries the same trademark notice in + its footer, and the repository has a NOTICE file with the full terms and the + list of changes made to the vendor build. ### Known issues diff --git a/CLAUDE.md b/CLAUDE.md index 24a8b6e66..bcfc3884d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -63,11 +63,12 @@ lib/ # 应用层(纯 TypeScript,只在本站点用) loading.ts # 加载状态 UI onlyoffice-editor.ts # 编辑器生命周期门面:挂载/重建/loadEditorApi,并对外统一导出下面这些模块 onlyoffice/ # 编辑器周边(2026-08-19 从 1975 行的单文件拆出,公开导出面不变) - iframe-guards.ts # 10 条运行时守卫的编排;每条守卫一个文件在 guards/ + iframe-guards.ts # 12 条运行时守卫的编排;每条守卫一个文件在 guards/ guards/ # chrome / shared-worker / fetch-fonts / image-pipeline / # serverless-save / long-action / series-settings / # font-loading / comment-selection / canvas-loss / - # wasm-binary-release / unload-prompt / hint-fallback + # wasm-binary-release / unload-prompt / hint-fallback / + # about-source open-state.ts # 就绪、打开失败、frame 首个错误(三处共用的单一状态源) open-failure.ts # 失败分类、-82 guard、环境类失败重开一次(经 setOpenRunner 注入避免环) font-system.ts # 字体系统就绪判定 + awaitFontSystem(#144) @@ -621,6 +622,16 @@ docs/explorations/2026-08-19-ci-e2e-sharding.md。 7. **循环依赖处理**:`onlyoffice-editor.ts` 与 `converter.ts` 之间通过回调注入(`setConverterCallbacks`)解耦;`ui.ts` 与 `document.ts` 之间通过 `setUICallbacks` 解耦 8. **编辑器操作队列**:`createEditorInstance` 内部有 `editorOperationQueue`,防止并发创建/销毁编辑器 9. **.claude/ 目录**:已加入 `.gitignore`,不提交本地 Claude Code 配置 +10. **ONLYOFFICE 署名不许再动(2026-08-23)**:本站是 ONLYOFFICE 的衍生作品,vendor + 的 AGPL 头附了第 7 条附加条款——**7(b) 必须保留原产品 logo**、**7(e) 不授予任何 + 商标权利**。曾经两处都被当成"界面净化"去掉了(`guards/chrome.ts` 隐藏 + `#header-logo`、DocEditor 配置 `customization.about: false`),界面上一处标识都不剩。 + 现在:header logo 与 About 面板都保留,守卫 12(`guards/about-source.ts`)往 About + 面板追加"非官方产品 + 源码地址"(AGPL §13),7 种语言的两个页脚都有商标声明,根目录 + `NOTICE` 存条款原文、vendor 版本与我们对 vendor 树的全部改动(§5(a) 要求)。 + `test/unit/branding-notice.test.ts` 与 `test/e2e/vendor-branding.spec.ts` 钉死; + 改 vendor 版本后要核对 NOTICE 里的引文与版本号。见 + docs/explorations/2026-08-23-onlyoffice-attribution-agpl-section-7.md。 --- diff --git a/NOTICE b/NOTICE new file mode 100644 index 000000000..bd50fd121 --- /dev/null +++ b/NOTICE @@ -0,0 +1,127 @@ +NOTICE +====== + +This product is a derivative work of ONLYOFFICE. + +The site (https://edit.chaxus.com/, https://github.com/ranuts/document) is +licensed under the GNU Affero General Public License version 3 -- see LICENSE. +It embeds the ONLYOFFICE document editors, which are distributed under the same +license with the additional terms permitted by Section 7 of the GNU AGPL. Those +terms are reproduced verbatim below and apply to this work as well. + + +1. ONLYOFFICE editors (sdkjs, web-apps) +--------------------------------------- + +Copyright (C) Ascensio System SIA. All rights reserved. +https://www.onlyoffice.com/ + +Upstream sources: + https://github.com/ONLYOFFICE/sdkjs + https://github.com/ONLYOFFICE/web-apps + +Version shipped in this repository: 9.3.0.133 (build:1), under public/sdkjs/ +and public/web-apps/. It is a third-party compiled offline build of those two +projects; the corresponding source is the 9.3.0 release of the repositories +above. + +The notice carried by the upstream source files, reproduced in full: + + (c) Copyright Ascensio System SIA 2010-2024 + + This program is a free software product. You can redistribute it and/or + modify it under the terms of the GNU Affero General Public License (AGPL) + version 3 as published by the Free Software Foundation. In accordance with + Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect + that Ascensio System SIA expressly excludes the warranty of non-infringement + of any third-party rights. + + This program is distributed WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For + details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html + + You can contact Ascensio System SIA at 20A-6 Ernesta Birznieka-Upish + street, Riga, Latvia, EU, LV-1050. + + The interactive user interfaces in modified source and object code versions + of the Program must display Appropriate Legal Notices, as required under + Section 5 of the GNU AGPL version 3. + + Pursuant to Section 7(b) of the License you must retain the original Product + logo when distributing the program. Pursuant to Section 7(e) we decline to + grant you any rights under trademark law for use of our trademarks. + + All the Product's GUI elements, including illustrations and icon sets, as + well as technical writing content are licensed under the terms of the + Creative Commons Attribution-ShareAlike 4.0 International. See the License + terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode + + +How this work complies with those terms +--------------------------------------- + +Section 7(b) -- product logo. The ONLYOFFICE logo is shown in the editor +header, and the About pane (product logo, version, Ascensio System SIA +copyright, onlyoffice.com link) is reachable from the left rail. Neither is +suppressed. lib/onlyoffice/guards/chrome.ts hides only the current-user and +co-users widgets, which describe a collaboration session this serverless build +cannot have, and lib/onlyoffice-editor.ts leaves `customization.about` at its +default. test/unit/branding-notice.test.ts and +test/e2e/vendor-branding.spec.ts keep both from being "cleaned up" again. + +Section 7(e) -- trademarks. ONLYOFFICE and the ONLYOFFICE logo are trademarks +of Ascensio System SIA. No rights under trademark law are granted by the +license, and none are claimed here. This project is not an official ONLYOFFICE +product and is not affiliated with, sponsored by or endorsed by Ascensio System +SIA. The name is used only to identify the engine this site is built on. + +Section 5(a) -- modifications. The vendor tree under public/ is not pristine. +The changes made to it are: + + * public/sdkjs/common/wasm/x2t/x2t_helper.js -- patched to load the x2t + WebAssembly module through `Module.instantiateWasm` from a gzip-compressed + response, so the 40 MB binary is never materialised in memory, and to + report load failures to the host page. See + docs/changelogs/2026-08-20-issue-144-memory-and-delivery.md. + * public/sdkjs/common/wasm/x2t/x2t.wasm -- published gzip-compressed only + (x2t.wasm.gz), because the uncompressed file exceeds the host's per-file + limit. The decompressed bytes are unmodified and pinned by sha256 in + test/unit/vendor-contract.test.ts. + * public/web-apps/apps/*/main/locale/*.json -- missing keys filled in from + en.json by bin/locale-fill.mjs for the seven site languages. Untranslated + keys returned `undefined` and crashed the tooltip renderer. Values only; + no key is removed. + * public/web-apps help content trimmed from the shipped tree. + * public/fonts/ -- the font catalog's proprietary font files were replaced + with redistributable open-source families (see docs/font-licenses.md and + docs/changelogs/2026-08-22-font-licensing.md). The catalog registry itself + is unchanged in shape; only the files behind the entries differ. + +Nothing else in public/sdkjs/ or public/web-apps/ is edited. The behavioural +patches this site needs are applied at runtime from lib/onlyoffice/guards/, +which leaves the vendor files themselves byte-identical to the build they came +from. + + +2. x2t WebAssembly converter +---------------------------- + +The document converter (x2t) is part of ONLYOFFICE core (AGPL-3.0, Ascensio +System SIA) compiled to WebAssembly. The build used here comes from the +third-party project https://github.com/cryptpad/onlyoffice-x2t-wasm. + + +3. Other components +------------------- + + * ranui / ranuts (https://github.com/chaxus/ran) -- the design system and + utilities this site's own interface is built with. + * Fonts under public/fonts/ and public/ran-fonts/ -- see docs/font-licenses.md + for the license of each family. + * Monaco Editor, under public/web-apps/vendor/monaco/ -- shipped by the + vendor build; see the LICENSE and ThirdPartyNotices.txt files in that + directory. + +Other trademarks named in this repository and on the site (Microsoft Word, +Excel, PowerPoint, and others) are the property of their respective owners and +are used only to describe the file formats this editor reads and writes. diff --git a/bin/build-pages.mjs b/bin/build-pages.mjs index 28baa1e91..56ba887c2 100644 --- a/bin/build-pages.mjs +++ b/bin/build-pages.mjs @@ -162,6 +162,7 @@ const UI = { ], generatedNote: (src) => `Source: ${src} in the repository`, ossNote: `Open source & self-hostable. Under AGPL-3.0 — verify that nothing is uploaded, or run your own copy: github.com/ranuts/document.`, + trademark: `ONLYOFFICE is a trademark of Ascensio System SIA. This site is not an official ONLYOFFICE product and is not affiliated with or endorsed by Ascensio System SIA.`, }, 'zh-CN': { siteName: 'Document Editor', @@ -190,6 +191,7 @@ const UI = { ], generatedNote: (src) => `来源:仓库中的 ${src}`, ossNote: `开源 · 可自托管。采用 AGPL-3.0——你可以核实没有任何上传,或者自建一份:github.com/ranuts/document。`, + trademark: `ONLYOFFICE 是 Ascensio System SIA 的商标。本站并非官方 ONLYOFFICE 产品,与 Ascensio System SIA 无隶属关系,也未获其背书。`, }, ja: { siteName: 'Document Editor', @@ -218,6 +220,7 @@ const UI = { ], generatedNote: (src) => `ソース: リポジトリの ${src}`, ossNote: `オープンソース・セルフホスト可能。AGPL-3.0 のもとで公開——何もアップロードされないことを自分で確認でき、自分で運用することもできます: github.com/ranuts/document。`, + trademark: `ONLYOFFICE は Ascensio System SIA の商標です。本サイトは公式の ONLYOFFICE 製品ではなく、Ascensio System SIA との提携も推奨関係もありません。`, }, de: { siteName: 'Document Editor', @@ -246,6 +249,7 @@ const UI = { ], generatedNote: (src) => `Quelle: ${src} im Repository`, ossNote: `Open Source & selbst hostbar. Unter AGPL-3.0 — prüfen Sie selbst, dass nichts hochgeladen wird, oder betreiben Sie eine eigene Kopie: github.com/ranuts/document.`, + trademark: `ONLYOFFICE ist eine Marke von Ascensio System SIA. Diese Website ist kein offizielles ONLYOFFICE-Produkt und steht in keiner Verbindung zu Ascensio System SIA.`, }, es: { siteName: 'Document Editor', @@ -274,6 +278,7 @@ const UI = { ], generatedNote: (src) => `Fuente: ${src} en el repositorio`, ossNote: `Código abierto y autoalojable. Bajo AGPL-3.0: comprueba que no se sube nada, o ejecuta tu propia copia: github.com/ranuts/document.`, + trademark: `ONLYOFFICE es una marca de Ascensio System SIA. Este sitio no es un producto oficial de ONLYOFFICE ni está afiliado a Ascensio System SIA ni respaldado por ella.`, }, ko: { siteName: 'Document Editor', @@ -302,6 +307,7 @@ const UI = { ], generatedNote: (src) => `출처: 저장소의 ${src}`, ossNote: `오픈 소스이며 직접 호스팅할 수 있습니다. AGPL-3.0으로 공개되어 있어, 아무것도 업로드되지 않는다는 것을 직접 확인하거나 직접 운영할 수 있습니다: github.com/ranuts/document.`, + trademark: `ONLYOFFICE는 Ascensio System SIA의 상표입니다. 이 사이트는 공식 ONLYOFFICE 제품이 아니며 Ascensio System SIA와 제휴하거나 후원받지 않았습니다.`, }, pt: { siteName: 'Document Editor', @@ -330,6 +336,7 @@ const UI = { ], generatedNote: (src) => `Fonte: ${src} no repositório`, ossNote: `Código aberto e auto-hospedável. Sob a AGPL-3.0 — confira você mesmo que nada é enviado, ou rode a sua própria cópia: github.com/ranuts/document.`, + trademark: `ONLYOFFICE é uma marca da Ascensio System SIA. Este site não é um produto oficial do ONLYOFFICE nem tem afiliação ou endosso da Ascensio System SIA.`, }, }; @@ -1020,6 +1027,7 @@ ${footLinks} ${e(data.foot.license)} +

${e(ui.trademark)}

@@ -1266,6 +1274,7 @@ ${aside} ${footer} GitHub +

${escapeHtml(ui.trademark)}

diff --git a/docs/embed-api.md b/docs/embed-api.md index a24022021..944592bbd 100644 --- a/docs/embed-api.md +++ b/docs/embed-api.md @@ -71,6 +71,17 @@ window.addEventListener('message', (event) => { ### From URL +> **The URL must allow CORS.** The browser -- not a server -- fetches the file, +> so the response needs an `Access-Control-Allow-Origin` header that covers the +> page the editor runs on. Without it the request is blocked before the editor +> sees a single byte, and the same is true of `?src=` and `?file=` on the URL. +> This is the single most common thing to get wrong when integrating: if a file +> opens when you download it by hand but not through the editor, check the +> response headers first. Cross-origin redirects have to keep the header too. +> When you cannot add it (a third-party host, a signed URL, anything behind +> auth), fetch the file in the parent page and pass the bytes with +> `document:open-buffer` instead. + ```js sendEditorCommand('document:open-url', { url: 'https://example.com/files/demo.xlsx', diff --git a/docs/embed-api.zh.md b/docs/embed-api.zh.md index 34e4c51d8..2e80e9139 100644 --- a/docs/embed-api.zh.md +++ b/docs/embed-api.zh.md @@ -71,6 +71,13 @@ window.addEventListener('message', (event) => { ### 通过 URL +> **这个 URL 必须允许 CORS。** 取文件的是浏览器而不是服务器,所以响应里要有覆盖编辑器所在 +> 页面的 `Access-Control-Allow-Origin` 头。没有它,请求在编辑器拿到第一个字节之前就被拦掉了; +> URL 上的 `?src=`、`?file=` 同理。这是接入时最容易踩的一脚:如果一个文件手动下载能打开、 +> 走编辑器打不开,先看响应头。跨域跳转也要一路带着这个头。 +> 加不了的场景(第三方主机、签名 URL、需要鉴权的接口),改由父页面自己 fetch,再用 +> `document:open-buffer` 把字节传进来。 + ```js sendEditorCommand('document:open-url', { url: 'https://example.com/files/demo.xlsx', diff --git a/docs/explorations/2026-08-23-onlyoffice-attribution-agpl-section-7.md b/docs/explorations/2026-08-23-onlyoffice-attribution-agpl-section-7.md new file mode 100644 index 000000000..6e67e036a --- /dev/null +++ b/docs/explorations/2026-08-23-onlyoffice-attribution-agpl-section-7.md @@ -0,0 +1,90 @@ +# ONLYOFFICE 署名合规:AGPL §7(b) 保留 logo 与 §7(e) 商标声明 + +2026-08-23 + +## 问题 + +用户问:OnlyOffice 要求衍生作品保留 ONLYOFFICE 的 logo(§7(b))与商标权声明(§7(e)), +本项目有没有做到。 + +答案是两条都没做到,而且第一条是我们主动去掉的。 + +## 现场取证 + +vendor 源码头(`public/web-apps/apps/common/main/lib/util/fix-ie-compat.js`, +是包里少数没被压缩、还带完整 header 的文件)写着: + +> Pursuant to Section 7(b) of the License you must retain the original Product +> logo when distributing the program. Pursuant to Section 7(e) we decline to +> grant you any rights under trademark law for use of our trademarks. + +而我们做了三件事把它抹掉: + +1. `lib/onlyoffice/guards/chrome.ts` 往编辑器 iframe 注入 + `#header-logo, .btn-current-user, #tlb-box-users { display: none !important }`。 + 线上实测 edit.chaxus.com:`#header-logo` 计算样式确为 `display: none`;手动放出来 + 是官方 logo(背景图 `web-apps/apps/common/main/resources/img/header/header-logo_s.svg`, + 86×20),资源一直在,只是不显示。 +2. `lib/onlyoffice-editor.ts` 的 DocEditor 配置里 `customization.about: false`。 + 线上实测左栏 `#left-btn-about` 也是 `display: none`——**这条本以为无效**:vendor + 的 `hidePreloader` 里有一句 + `this.appOptions.canBrandingExt || (this.appOptions.customization.about = true)`, + 即没有商用 branding 授权时把 about 强行扳回 true。但这个离线包 `canBrandingExt` + 为真(没有 license 校验),兜底没起作用,所以我们确实关掉了它。 + 对照组:同一行左栏里 `left-btn-searchbar/comments/navigation` 都是 `block`, + 只有 `support`(`help: false`)与 `about` 是 `none`,能排除"只是文件菜单没展开"。 +3. 全仓 grep 不到 trademark / 商标;readme 的 License 一节只有一行 `AGPL-3.0`, + 没有 NOTICE / THIRD-PARTY 文件(只有管字体的 `docs/font-licenses.md`)。 + +于是界面上一处 ONLYOFFICE 标识都没有,只有 JS 文件头那 5 行注释里还有 +`Copyright (C) Ascensio System SIA`。 + +值得记一笔的是**它是怎么变成这样的**:两处都不是疏忽,是刻意的"界面净化"需求, +见 [2026-08-12-v9-pure-ui-and-issue-regression-sweep.md](2026-08-12-v9-pure-ui-and-issue-regression-sweep.md) +开头那句"需求:去掉编辑器头部的 ONLYOFFICE logo(`#header-logo`)"。下一次净化会 +再做一遍,而当时套件里没有任何东西会红。 + +## 改动 + +- `guards/chrome.ts` 选择器里删掉 `#header-logo`。`.btn-current-user` / + `#tlb-box-users` 留着——那是协作会话的 UI,无服务器版本根本没有,与 7(b) 无关。 +- `onlyoffice-editor.ts` 删掉 `about: false`(默认即 true)。`help: false` 与授权 + 无关,保留(vendor 的 help 内容本来就从包里裁掉了)。 +- 新增守卫 12 `guards/about-source.ts`:往 About 面板末尾追加两行——"这是 ONLYOFFICE + 编辑器的修改版本,不是官方产品;ONLYOFFICE 是 Ascensio System SIA 的商标",以及 + AGPL §13 要的源码地址。**只增不改**,vendor 渲染的内容一个字不动。 + 实现上要注意:`#about-menu-panel` 从启动起就在 DOM 里,但**内容是首次打开时才渲染 + 的**(实测 `children.length === 0`),所以不能一次性写入,要用 MutationObserver 等 + 内容到齐;往被观察节点里 append 会再次触发回调,靠 id 判重收敛。 +- 新增根目录 `NOTICE`:逐字引用 vendor 那三段(AGPL + 7(a) 免责 + 7(b)/7(e))、写明 + vendor 版本 9.3.0.133 与上游仓库、以及 §5(a) 要求的**我们对 vendor 树做过的全部改动** + (x2t_helper 的 instantiateWasm 补丁、x2t.wasm 只发 gzip、locale 补键、help 裁剪、 + 字体 catalog 换成开源字体),并声明商标归属与"非官方、无隶属"。 +- 8 份 readme 的 License 一节补上同样的两段(指向 NOTICE + 商标免责)。 +- 站点 7 种语言的两个页脚(首页 hero 页脚与生成页 `.page-foot`)都渲染一行商标声明, + 词条在 `bin/build-pages.mjs` 的 `UI` 表里,样式 `.tm` 在 `landing.css` / `home.css`。 + +## 用例固化与反向验证 + +- 单测 `test/unit/branding-notice.test.ts`(17 条):钉住两处抑制不能回来、守卫已挂载、 + NOTICE 逐字包含 7(b)/7(e) 那段**且 vendor 文件里那段仍然存在**(vendor 升级后引文要 + 重新核对)、8 份 readme 都指向 NOTICE、154 个生成页每页都有 `.tm` 且中文页说中文。 +- E2E `test/e2e/vendor-branding.spec.ts`(6 条):真实编辑器里断言 header logo 可见且 + 真的被画出来(背景图 + 尺寸,不只是"元素存在")、About 入口在左栏且面板含 + `Ascensio System SIA`、面板里有我们的源码声明;再加三个页面的页脚商标行。 +- **反向验证**(两轮,都做了): + - 把 `#header-logo` 加回 chrome.ts、`about: false` 加回配置、守卫从 + `iframe-guards.ts` 摘掉 → 编辑器那 3 条全红,页脚那 3 条仍绿(正确,它们测的是 + 另一半)。 + - 把两个模板里的 `

` 删掉 → 页脚那 3 条全红。 + - 恢复后 6 条全绿。 + +## 没做的事 + +- **没有改产品名**。readme 的 H1 已经是 "Online Document Editor",站点也没用 + ONLYOFFICE 当自己的名字,7(e) 层面不需要动。 +- **没有去动 vendor 的 About 面板内容本身**(版本号、版权行、链接),只在末尾追加。 + 改它是另一种风险:那正是 §5 要显示的 Appropriate Legal Notices。 +- **没有把 corresponding source 打包进仓库**。vendor 是第三方编译的 9.3.0.133 离线包, + 对应源码在 ONLYOFFICE 的公开仓库,NOTICE 里写了版本与地址;真要更严格,可以在 + release 里附一份 written offer。 diff --git a/index.html b/index.html index 59bff1e1b..960184a7e 100644 --- a/index.html +++ b/index.html @@ -525,6 +525,7 @@

Questions, answered

AGPL-3.0 +

ONLYOFFICE is a trademark of Ascensio System SIA. This site is not an official ONLYOFFICE product and is not affiliated with or endorsed by Ascensio System SIA.

diff --git a/lib/onlyoffice-editor.ts b/lib/onlyoffice-editor.ts index 95bb8cc9b..024eeeeed 100644 --- a/lib/onlyoffice-editor.ts +++ b/lib/onlyoffice-editor.ts @@ -213,7 +213,12 @@ function createPersonalEditorInstance(config: { }, customization: { help: false, - about: false, + // `about` stays at its default (true). The About pane is where the + // ONLYOFFICE product logo, version and Ascensio System SIA copyright + // live, and Section 7(b) of the vendor's AGPL-3.0 headers requires a + // derivative work to keep the product logo. Switching it off is also + // what the vendor's own `canBrandingExt` check exists to prevent -- + // this offline build just happens not to enforce it. See NOTICE. hideRightMenu: true, uiTheme: resolveUiTheme(), ...(isCompactViewport() ? compactViewportCustomization() : {}), diff --git a/lib/onlyoffice/guards/about-source.ts b/lib/onlyoffice/guards/about-source.ts new file mode 100644 index 000000000..bb9eb2029 --- /dev/null +++ b/lib/onlyoffice/guards/about-source.ts @@ -0,0 +1,66 @@ +/** + * Guard 12: the source offer and the "not an official product" line, added to + * the editor's own About pane. + * + * The vendor's About pane already carries what Section 7(b) of the ONLYOFFICE + * AGPL terms asks for -- product logo, version, Ascensio System SIA copyright. + * What it cannot carry is the two things that are true of THIS build and not of + * theirs: that it is a modified version, and where its corresponding source is. + * AGPL-3.0 Section 13 asks a network-interactive modified version to offer that + * source to the people using it, and Section 7(e) is the reason to say plainly + * that the mark on the pane above is not ours. + * + * The pane is populated lazily -- `#about-menu-panel` exists from boot but is + * empty until the user opens it -- so this watches for the content to arrive + * instead of writing once. Appending inside the observed node re-enters the + * callback; the id check is what stops that after one pass. + * + * Additive only: nothing the vendor renders is moved, restyled or removed. + */ +const NOTICE_ID = 'oo-source-notice'; +const SOURCE_URL = 'https://github.com/ranuts/document'; +const WATCHED = '__ranSourceNoticeWatched'; + +function renderNotice(doc: Document, panel: HTMLElement): void { + // Nothing to append to yet: the pane has not been opened for the first time. + if (panel.children.length === 0) return; + if (doc.getElementById(NOTICE_ID)) return; + + const box = doc.createElement('div'); + box.id = NOTICE_ID; + box.style.cssText = 'padding:12px 0;font-size:11px;line-height:1.6;opacity:0.75;'; + + const line = doc.createElement('div'); + line.textContent = + 'This is a modified version of the ONLYOFFICE editors, not an official ONLYOFFICE product. ' + + 'ONLYOFFICE is a trademark of Ascensio System SIA.'; + box.appendChild(line); + + const source = doc.createElement('div'); + source.textContent = 'Source code (AGPL-3.0): '; + const link = doc.createElement('a'); + link.href = SOURCE_URL; + link.target = '_blank'; + link.rel = 'noopener noreferrer'; + link.textContent = SOURCE_URL; + source.appendChild(link); + box.appendChild(source); + + panel.appendChild(box); +} + +export function installAboutSourceNotice(doc: Document): boolean { + const panel = doc.getElementById('about-menu-panel'); + if (!panel) return false; + + const flagged = panel as HTMLElement & { [WATCHED]?: boolean }; + if (flagged[WATCHED]) return true; + flagged[WATCHED] = true; + + renderNotice(doc, panel); + const view = doc.defaultView; + if (view?.MutationObserver) { + new view.MutationObserver(() => renderNotice(doc, panel)).observe(panel, { childList: true }); + } + return true; +} diff --git a/lib/onlyoffice/guards/chrome.ts b/lib/onlyoffice/guards/chrome.ts index 2951f3f1d..a48e70035 100644 --- a/lib/onlyoffice/guards/chrome.ts +++ b/lib/onlyoffice/guards/chrome.ts @@ -2,9 +2,16 @@ import { COMPACT_VIEWPORT_MAX_WIDTH } from '../viewport'; /** * Strip the OnlyOffice chrome that has no place in a single-user local editor - * -- the header logo and the current-user / co-users widgets -- and hide the - * right panel on phone-sized viewports. There is no DocEditor config switch - * for any of it in this build. + * -- the current-user / co-users widgets, which describe a collaboration + * session this build cannot have -- and hide the right panel on phone-sized + * viewports. There is no DocEditor config switch for either in this build. + * + * The header logo is deliberately NOT in that list. This site is a derivative + * work of ONLYOFFICE, whose AGPL-3.0 headers add, under Section 7(b), the term + * that the original product logo must be retained when the program is + * distributed. It used to be hidden here (and the About pane switched off in + * the DocEditor config) which left no product mark anywhere in the interface. + * Both are back on purpose; do not "clean up" the header again. */ export function injectLocalChromeCss(doc: Document): void { if (!doc.getElementById('oo-local-chrome-css')) { @@ -16,7 +23,7 @@ export function injectLocalChromeCss(doc: Document): void { // opened in. The JS side (syncCompactLayout) only handles what CSS // cannot: the thumbnails panel and the SDK's own canvas geometry. style.textContent = [ - '#header-logo, .btn-current-user, #tlb-box-users { display: none !important; }', + '.btn-current-user, #tlb-box-users { display: none !important; }', `@media (max-width: ${COMPACT_VIEWPORT_MAX_WIDTH}px), (pointer: coarse) and (max-height: ${COMPACT_VIEWPORT_MAX_WIDTH}px) {`, ' [data-layout-name="rightMenu"] { display: none !important; }', '}', diff --git a/lib/onlyoffice/iframe-guards.ts b/lib/onlyoffice/iframe-guards.ts index 26ef41abf..091caaa95 100644 --- a/lib/onlyoffice/iframe-guards.ts +++ b/lib/onlyoffice/iframe-guards.ts @@ -12,6 +12,7 @@ import { installCanvasLossGuard } from './guards/canvas-loss'; import { releaseWasmBinary } from './guards/wasm-binary-release'; import { installSingleUnloadPrompt } from './guards/unload-prompt'; import { installHintFallbackGuard } from './guards/hint-fallback'; +import { installAboutSourceNotice } from './guards/about-source'; /** * Same-origin preparation of the editor iframe, applied from onAppReady and @@ -54,6 +55,7 @@ export function prepareEditorIframe(): boolean { installCanvasLossGuard(win, doc); installSingleUnloadPrompt(win); installHintFallbackGuard(win); + installAboutSourceNotice(doc); const wasmBinaryHandled = releaseWasmBinary(win); if ( diff --git a/public/_redirects b/public/_redirects index 5452b6893..666284d5b 100644 --- a/public/_redirects +++ b/public/_redirects @@ -4,6 +4,12 @@ # we pin the important one explicitly so /zh-CN never 404s or double-serves. /zh-CN /zh-CN/ 308 +# /zh/ is what people (and other sites) guess for the Chinese pages -- the real +# prefix is the BCP-47 tag /zh-CN/, which is what hreflang and the sitemap use. +# The guess used to 404. +/zh /zh-CN/ 301 +/zh/* /zh-CN/:splat 301 + # The comparison pages are gone (no "vs X" positioning) — send old links home. /vs/google-docs / 301 /zh-CN/vs/google-docs /zh-CN/ 301 diff --git a/public/home.css b/public/home.css index 3f6953b03..6effbe7ac 100644 --- a/public/home.css +++ b/public/home.css @@ -934,6 +934,16 @@ body { #landing-hero .foot .lic { font: 500 12px/1 var(--ran-font-mono); } +/* Same notice as the generated pages carry (see landing.css .page-foot .tm). + The footer is a space-between flex row, so the notice takes a row of its + own rather than competing with the links for the line. */ +#landing-hero .foot .tm { + flex: 1 0 100%; + margin: 0; + max-width: 72ch; + font-size: 12.5px; + line-height: 1.6; +} /* ---------- theme switch (footer) ---------- Real ranui (three-state pill wired to setTheme). The diff --git a/public/landing.css b/public/landing.css index 9e084f649..4adac3309 100644 --- a/public/landing.css +++ b/public/landing.css @@ -184,6 +184,17 @@ r-select.lang-select:not(:defined) { margin: 0 auto; padding: 0 clamp(20px, 5vw, 56px) var(--ran-space-10); } +/* The ONLYOFFICE trademark notice. Section 7(e) of the vendor's AGPL terms + declines to grant any rights under trademark law, so every page states whose + mark it is and that this site is not their product. Muted and last: it is a + legal notice, not navigation. */ +.page-foot .tm { + margin: var(--ran-space-4) 0 0; + max-width: 72ch; + color: var(--ran-color-text-secondary); + font-size: 12.5px; + line-height: 1.6; +} @media (min-width: 1081px) { .page { display: grid; diff --git a/readme.de.md b/readme.de.md index 7770189f8..05da69052 100644 --- a/readme.de.md +++ b/readme.de.md @@ -266,4 +266,14 @@ Branch und öffnen Sie einen PR, der Lint, Unit-Tests und drei End-to-End-Suites ## 📄 Lizenz -[AGPL-3.0](LICENSE) +[AGPL-3.0](LICENSE). + +Dieses Projekt ist ein abgeleitetes Werk von ONLYOFFICE (sdkjs und web-apps, +(c) Ascensio System SIA). Das Original steht unter der AGPL mit Zusatzbedingungen nach +deren Abschnitt 7: Das ursprüngliche Produktlogo muss erhalten bleiben, und es werden +keine Markenrechte eingeräumt. Deshalb bleiben das ONLYOFFICE-Logo in der Kopfzeile des +Editors und dessen Info-Bereich unangetastet. Den vollständigen Text, die verwendete +Vendor-Version und jede daran vorgenommene Änderung finden Sie in [NOTICE](NOTICE). + +ONLYOFFICE ist eine Marke von Ascensio System SIA. Dieses Projekt ist kein offizielles +ONLYOFFICE-Produkt und steht in keiner Verbindung zu Ascensio System SIA. diff --git a/readme.es.md b/readme.es.md index 1abc11799..571a2f294 100644 --- a/readme.es.md +++ b/readme.es.md @@ -266,4 +266,14 @@ producción). ## 📄 Licencia -[AGPL-3.0](LICENSE) +[AGPL-3.0](LICENSE). + +Este proyecto es una obra derivada de ONLYOFFICE (sdkjs y web-apps, (c) Ascensio +System SIA). El original se distribuye bajo la AGPL con términos adicionales de su +Sección 7: debe conservarse el logotipo original del producto y no se conceden derechos +de marca. Por eso el logotipo de ONLYOFFICE en la cabecera del editor y su panel Acerca +de se mantienen intactos. El texto completo, la versión del vendor y todos los cambios +que le hicimos están en [NOTICE](NOTICE). + +ONLYOFFICE es una marca de Ascensio System SIA. Este proyecto no es un producto oficial +de ONLYOFFICE ni está afiliado a Ascensio System SIA ni respaldado por ella. diff --git a/readme.fa.md b/readme.fa.md index b75222e8b..5cc27642d 100644 --- a/readme.fa.md +++ b/readme.fa.md @@ -263,4 +263,13 @@ Pages و تصویر تولیدی Docker) اجرا شوند. ## 📄 پروانه -[AGPL-3.0](LICENSE) +[AGPL-3.0](LICENSE). + +این پروژه اثری مشتق‌شده از ONLYOFFICE (سامانه‌های sdkjs و web-apps، (c) Ascensio System SIA) +است. نسخهٔ اصلی زیر پروانهٔ AGPL همراه با شرط‌های افزودهٔ بخش ۷ آن منتشر می‌شود: نشان اصلی +محصول باید حفظ شود و هیچ حقی در قانون علائم تجاری واگذار نمی‌شود. به همین دلیل نشان +ONLYOFFICE در سربرگ ویرایشگر و پنل «درباره» دست‌نخورده باقی مانده است. متن کامل، نسخهٔ +vendor و همهٔ تغییرهایی که روی آن داده‌ایم در [NOTICE](NOTICE) آمده است. + +ONLYOFFICE علامت تجاری Ascensio System SIA است. این پروژه محصول رسمی ONLYOFFICE نیست و +هیچ وابستگی یا تأییدی از سوی Ascensio System SIA ندارد. diff --git a/readme.ja.md b/readme.ja.md index 1b8e66cea..6b657c05e 100644 --- a/readme.ja.md +++ b/readme.ja.md @@ -263,4 +263,13 @@ Cloudflare Pages の挙動、本番 Docker イメージ)が実行されます ## 📄 ライセンス -[AGPL-3.0](LICENSE) +[AGPL-3.0](LICENSE)。 + +本プロジェクトは ONLYOFFICE(sdkjs と web-apps、(c) Ascensio System SIA)の二次的著作物です。 +上流は AGPL に第 7 条の追加条項を付して配布されており、元の製品ロゴを保持すること、そして +商標に関する権利は許諾されないことが定められています。そのためエディタのヘッダーにある +ONLYOFFICE ロゴと「バージョン情報」パネルはそのまま残しています。条項の全文、使用している +vendor のバージョン、加えた変更の一覧は [NOTICE](NOTICE) を参照してください。 + +ONLYOFFICE は Ascensio System SIA の商標です。本プロジェクトは公式の ONLYOFFICE 製品では +なく、Ascensio System SIA との提携も推奨関係もありません。 diff --git a/readme.ko.md b/readme.ko.md index 50c56f235..60693a927 100644 --- a/readme.ko.md +++ b/readme.ko.md @@ -262,4 +262,13 @@ pnpm run test:e2e # 종단 간 테스트(Playwright, 실제 편집기 + ## 📄 라이선스 -[AGPL-3.0](LICENSE) +[AGPL-3.0](LICENSE). + +이 프로젝트는 ONLYOFFICE(sdkjs 및 web-apps, (c) Ascensio System SIA)의 2차적 저작물입니다. +원본은 AGPL과 함께 제7조의 추가 조항으로 배포되며, 원래의 제품 로고를 유지해야 하고 상표법상 +어떤 권리도 부여되지 않습니다. 그래서 편집기 헤더의 ONLYOFFICE 로고와 정보 패널을 그대로 +두었습니다. 조항 전문, 사용 중인 vendor 버전, 우리가 가한 모든 변경 사항은 +[NOTICE](NOTICE)를 참고하세요. + +ONLYOFFICE는 Ascensio System SIA의 상표입니다. 이 프로젝트는 공식 ONLYOFFICE 제품이 아니며 +Ascensio System SIA와 제휴하거나 후원받지 않았습니다. diff --git a/readme.md b/readme.md index ea5709bc1..d57cca843 100644 --- a/readme.md +++ b/readme.md @@ -270,4 +270,13 @@ Cloudflare Pages semantics, and the production Docker image). ## 📄 License -[AGPL-3.0](LICENSE) +[AGPL-3.0](LICENSE). + +This is a derivative work of ONLYOFFICE (sdkjs and web-apps, (c) Ascensio System SIA), +distributed under the AGPL with additional terms under its Section 7: the original +product logo must be retained, and no rights under trademark law are granted. The +editor therefore keeps the ONLYOFFICE logo in its header and its About pane. See +[NOTICE](NOTICE) for the full text, the vendor version and every change made to it. + +ONLYOFFICE is a trademark of Ascensio System SIA. This project is not an official +ONLYOFFICE product and is not affiliated with or endorsed by Ascensio System SIA. diff --git a/readme.pt.md b/readme.pt.md index eb6091712..bd6a20959 100644 --- a/readme.pt.md +++ b/readme.pt.md @@ -265,4 +265,14 @@ desenvolvimento, comportamento do Cloudflare Pages e imagem Docker de produção ## 📄 Licença -[AGPL-3.0](LICENSE) +[AGPL-3.0](LICENSE). + +Este projeto é uma obra derivada do ONLYOFFICE (sdkjs e web-apps, (c) Ascensio System +SIA). O original é distribuído sob a AGPL com termos adicionais da sua Seção 7: o +logotipo original do produto deve ser mantido e nenhum direito de marca é concedido. Por +isso o logotipo do ONLYOFFICE no cabeçalho do editor e o painel Sobre continuam +intactos. O texto completo, a versão do vendor e todas as alterações que fizemos nele +estão em [NOTICE](NOTICE). + +ONLYOFFICE é uma marca da Ascensio System SIA. Este projeto não é um produto oficial do +ONLYOFFICE nem tem afiliação ou endosso da Ascensio System SIA. diff --git a/readme.zh.md b/readme.zh.md index 7efac4d90..5072b9d1f 100644 --- a/readme.zh.md +++ b/readme.zh.md @@ -253,4 +253,12 @@ pnpm run test:e2e # 端到端测试(Playwright,真实编辑器 + 真 ## 📄 许可证 -[AGPL-3.0](LICENSE) +[AGPL-3.0](LICENSE)。 + +本项目是 ONLYOFFICE(sdkjs 与 web-apps,(c) Ascensio System SIA)的衍生作品。上游以 +AGPL 发布并附带第 7 条的附加条款:必须保留原产品 logo,且不授予任何商标权利。因此编辑器 +头部的 ONLYOFFICE logo 与“关于”面板都予以保留。条款原文、所用 vendor 版本以及我们对它做过 +的全部改动见 [NOTICE](NOTICE)。 + +ONLYOFFICE 是 Ascensio System SIA 的商标。本项目并非官方 ONLYOFFICE 产品,与 Ascensio +System SIA 无隶属关系,也未获其背书。 diff --git a/test/e2e/vendor-branding.spec.ts b/test/e2e/vendor-branding.spec.ts new file mode 100644 index 000000000..e82f1ec80 --- /dev/null +++ b/test/e2e/vendor-branding.spec.ts @@ -0,0 +1,121 @@ +import { expect, test } from './lib/l0'; +import type { Frame, Page } from '@playwright/test'; + +/** + * The ONLYOFFICE attribution, on screen. + * + * Section 7(b) of the vendor's AGPL terms requires a derivative work to retain + * the original product logo. This build used to hide it: an injected stylesheet + * took out `#header-logo` and the DocEditor config set `customization.about` + * to false, which between them left no product mark anywhere in the interface. + * The unit half of this (test/unit/branding-notice.test.ts) pins the two source + * changes; this is the half that proves the result is actually visible, which + * is the only claim the license cares about. + * + * Reverse-verified: re-adding `#header-logo` to guards/chrome.ts fails the + * first case, and setting `about: false` back fails the second and third. + */ +const editorFrame = (page: Page) => page.frames().find((f) => /documenteditor/.test(f.url())); + +async function openBlankDocument(page: Page): Promise { + await page.goto('/editor?new=docx'); + await expect.poll(() => editorFrame(page)?.url() ?? null, { timeout: 60_000 }).not.toBeNull(); + const frame = editorFrame(page)!; + // The header and the left rail render with the rest of the chrome, so wait + // for the ribbon rather than for the frame's own load event. + await expect + .poll(() => frame.evaluate(() => document.querySelectorAll('.ribtab a').length).catch(() => 0), { + timeout: 60_000, + }) + .toBeGreaterThan(3); + return frame; +} + +/** Opens the About pane and returns its text, or '' if it never populates. */ +async function openAbout(frame: Frame): Promise { + await frame.click('#left-btn-about'); + const text = () => + frame + .evaluate(() => { + const panel = document.querySelector('#about-menu-panel'); + return panel && panel.children.length > 0 ? (panel.textContent || '').replace(/\s+/g, ' ') : ''; + }) + .catch(() => ''); + await expect.poll(text, { timeout: 30_000 }).not.toBe(''); + return text(); +} + +test.describe('ONLYOFFICE branding (AGPL-3.0 Section 7(b))', () => { + test('the product logo is visible in the editor header', async ({ page }) => { + const frame = await openBlankDocument(page); + await expect + .poll(() => frame.evaluate(() => !!document.querySelector('#header-logo')), { timeout: 30_000 }) + .toBe(true); + + const logo = await frame.evaluate(() => { + const el = document.querySelector('#header-logo') as HTMLElement; + const mark = (el.querySelector('i') as HTMLElement) || el; + const box = mark.getBoundingClientRect(); + return { + hidden: getComputedStyle(el).display === 'none' || getComputedStyle(el).visibility === 'hidden', + image: getComputedStyle(mark).backgroundImage, + width: box.width, + height: box.height, + }; + }); + + expect(logo.hidden, 'the ONLYOFFICE header logo must not be hidden -- see NOTICE').toBe(false); + // Painted, not a zero-sized element that merely exists in the DOM. + expect(logo.image).toContain('header-logo'); + expect(logo.width).toBeGreaterThan(20); + expect(logo.height).toBeGreaterThan(8); + }); + + test('the About entry is reachable and carries the vendor copyright', async ({ page }) => { + const frame = await openBlankDocument(page); + + const inRail = await frame.evaluate(() => { + const el = document.querySelector('#left-btn-about'); + return el ? getComputedStyle(el).display !== 'none' : false; + }); + expect(inRail, 'the About entry must stay in the left rail -- see NOTICE').toBe(true); + + expect(await openAbout(frame)).toContain('Ascensio System SIA'); + }); + + test("the About pane also offers this build's own source (Section 13)", async ({ page }) => { + const frame = await openBlankDocument(page); + await openAbout(frame); + + await expect + .poll(() => frame.evaluate(() => !!document.querySelector('#oo-source-notice')), { timeout: 30_000 }) + .toBe(true); + const notice = await frame.evaluate(() => { + const box = document.querySelector('#oo-source-notice') as HTMLElement; + return { + text: (box.textContent || '').replace(/\s+/g, ' '), + href: box.querySelector('a')?.getAttribute('href') ?? '', + height: box.getBoundingClientRect().height, + }; + }); + + expect(notice.height).toBeGreaterThan(0); + expect(notice.text).toContain('not an official ONLYOFFICE product'); + expect(notice.href).toBe('https://github.com/ranuts/document'); + }); +}); + +test.describe('trademark notice (AGPL-3.0 Section 7(e))', () => { + const PAGES = [ + ['/', 'ONLYOFFICE is a trademark of Ascensio System SIA'], + ['/zh-CN/', 'ONLYOFFICE 是 Ascensio System SIA 的商标'], + ['/help', 'ONLYOFFICE is a trademark of Ascensio System SIA'], + ] as const; + + for (const [route, expected] of PAGES) { + test(`${route} states whose mark ONLYOFFICE is`, async ({ page }) => { + await page.goto(route); + await expect(page.locator('.tm').first()).toContainText(expected); + }); + } +}); diff --git a/test/unit/branding-notice.test.ts b/test/unit/branding-notice.test.ts new file mode 100644 index 000000000..29e773e1f --- /dev/null +++ b/test/unit/branding-notice.test.ts @@ -0,0 +1,128 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { generate } from '../../bin/build-pages.mjs'; + +/** + * The ONLYOFFICE attribution, pinned. + * + * This site is a derivative work of ONLYOFFICE, whose AGPL-3.0 headers add two + * terms under Section 7 of that license: 7(b) requires the original product + * logo to be retained when the program is distributed, and 7(e) declines to + * grant any rights under trademark law. Both were being violated -- the header + * logo was hidden by an injected stylesheet and the About pane switched off in + * the DocEditor config, which left no product mark anywhere in the interface, + * and no trademark notice existed in the repository or on the site. + * + * What makes this worth a test rather than a comment is how it happened: both + * removals were deliberate UI tidy-ups ("strip the chrome a single-user local + * editor does not need", docs/explorations/2026-08-12-v9-pure-ui-and-issue-regression-sweep.md). + * The next tidy-up would do it again, and nothing else in the suite would go + * red. So: the two suppressions cannot come back, and the notices cannot be + * dropped. The runtime half -- that the logo and the About entry are really on + * screen -- is test/e2e/vendor-branding.spec.ts. + */ +const ROOT = resolve(__dirname, '../..'); +const read = (rel: string) => readFileSync(resolve(ROOT, rel), 'utf8'); + +/** The sentence that carries both terms, verbatim from the vendor's own headers. */ +const SECTION_7 = + 'Pursuant to Section 7(b) of the License you must retain the original Product\n' + + ' logo when distributing the program. Pursuant to Section 7(e) we decline to\n' + + ' grant you any rights under trademark law for use of our trademarks.'; + +const READMES = [ + 'readme.md', + 'readme.zh.md', + 'readme.ja.md', + 'readme.ko.md', + 'readme.de.md', + 'readme.es.md', + 'readme.pt.md', + 'readme.fa.md', +]; + +describe('ONLYOFFICE product logo (AGPL-3.0 Section 7(b))', () => { + it('is not hidden by the chrome stylesheet the guards inject', () => { + const guard = read('lib/onlyoffice/guards/chrome.ts'); + expect(guard, 'the header logo must not be hidden -- see NOTICE').not.toContain('#header-logo'); + // The guard still has a job: these two describe a collaboration session a + // serverless build cannot have, and hiding them is unrelated to branding. + expect(guard).toContain('.btn-current-user'); + expect(guard).toContain('#tlb-box-users'); + }); + + it('is not removed with the About pane by the DocEditor config', () => { + const editor = read('lib/onlyoffice-editor.ts'); + expect(editor, 'customization.about must stay at its default -- it is where the product logo lives').not.toMatch( + /\babout:\s*false/, + ); + }); + + it("is joined in the About pane by this build's own source offer (AGPL-3.0 Section 13)", () => { + const guard = read('lib/onlyoffice/guards/about-source.ts'); + expect(guard).toContain('about-menu-panel'); + expect(guard).toContain('https://github.com/ranuts/document'); + expect(guard).toMatch(/not an official ONLYOFFICE product/); + // Mounted, or it is a file nothing runs. + expect(read('lib/onlyoffice/iframe-guards.ts')).toContain('installAboutSourceNotice(doc)'); + }); +}); + +describe('trademark notice (AGPL-3.0 Section 7(e))', () => { + const notice = read('NOTICE'); + + it('reproduces the vendor terms verbatim, and the vendor build still carries them', () => { + expect(notice).toContain(SECTION_7); + // The source of that quote: an unminified vendor file that ships with the + // build. If an upgrade drops it, the quote above needs re-checking against + // whatever the new build carries. + const vendor = read('public/web-apps/apps/common/main/lib/util/fix-ie-compat.js'); + expect(vendor).toContain('Pursuant to Section 7(b) of the License you must retain the original Product'); + expect(vendor).toContain('Pursuant to Section 7(e) we decline to'); + }); + + it('names the mark, its owner, and that this project is neither', () => { + expect(notice).toContain('Ascensio System SIA'); + // The notice is hard-wrapped, so the phrases can straddle a line break. + expect(notice.replace(/\s+/g, ' ')).toMatch(/not an official ONLYOFFICE product/); + expect(notice.replace(/\s+/g, ' ')).toMatch(/not affiliated with/); + }); + + it('lists the changes made to the vendor tree (Section 5(a))', () => { + for (const changed of ['x2t_helper.js', 'x2t.wasm', 'locale/*.json', 'public/fonts/']) { + expect(notice, `NOTICE does not mention ${changed}`).toContain(changed); + } + }); + + it.each(READMES)('%s points at it and carries the disclaimer', (file) => { + const markdown = read(file); + expect(markdown).toContain('(NOTICE)'); + expect(markdown).toContain('Ascensio System SIA'); + expect(markdown).toContain('ONLYOFFICE'); + }); +}); + +describe('trademark notice on the site itself', () => { + const outputs = generate({ outDir: null }) as Array<{ route: string; kind: string; html: string }>; + + it("is on every generated page, in that page's language", () => { + expect(outputs.length).toBeGreaterThan(50); + for (const page of outputs) { + expect(page.html, `${page.route} has no trademark notice`).toContain('class="tm"'); + expect(page.html, `${page.route} does not name the trademark owner`).toContain('Ascensio System SIA'); + } + }); + + it('says it in Chinese on the Chinese pages, not in English', () => { + const zh = outputs.find((o) => o.route === '/zh-CN/')!; + expect(zh.html).toContain('ONLYOFFICE 是 Ascensio System SIA 的商标'); + const en = outputs.find((o) => o.route === '/')!; + expect(en.html).toContain('ONLYOFFICE is a trademark of Ascensio System SIA'); + }); + + it('is styled, or it is a paragraph of legalese in body copy', () => { + expect(read('public/landing.css')).toContain('.page-foot .tm'); + expect(read('public/home.css')).toContain('#landing-hero .foot .tm'); + }); +}); diff --git a/test/unit/hosting-contract.test.ts b/test/unit/hosting-contract.test.ts index 794e3c1d1..e3078cab2 100644 --- a/test/unit/hosting-contract.test.ts +++ b/test/unit/hosting-contract.test.ts @@ -146,6 +146,9 @@ describe('sws.toml (self-hosted Docker)', () => { describe('public/_redirects', () => { it('canonicalizes the localized landing directory', () => { expect(read('public/_redirects')).toMatch(/^\/zh-CN\s+\/zh-CN\/\s+308/m); + // /zh/ is the prefix people guess for the Chinese pages; the real one is + // the BCP-47 tag the hreflang set and the sitemap use. + expect(read('public/_redirects')).toMatch(/^\/zh\/\*\s+\/zh-CN\/:splat\s+301/m); }); });