docs: remove implemented arbitrary-html-embed plan

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
claude code agent 227
2026-06-20 08:54:54 +03:00
parent bd28dbfe2b
commit 393bca4dab

View File

@@ -1,95 +0,0 @@
# Вставка произвольного HTML/CSS/JS в страницы — анализ и подходы
> Статус: **черновик / обсуждение**. Решение по модели изоляции ещё не принято — см. раздел «Развилка».
> Исходный кейс: нужно вставлять трекер (счётчик аналитики) на вики-страницы.
## 1. Почему «из коробки» произвольный HTML вставить нельзя
Контент страницы в Docmost хранится не как HTML, а как **ProseMirror JSON** (документ TipTap, синхронизируется через Yjs). Любой путь, которым контент попадает в страницу — ручной ввод, вставка из буфера (paste), импорт Markdown/HTML — проходит парсинг строго по схеме редактора:
`apps/server/src/common/helpers/prosemirror/html/generateJSON.ts:45`
```ts
PMDOMParser.fromSchema(schema).parse(doc.body, options)
```
`PMDOMParser.fromSchema` оставляет только те теги, для которых в схеме есть нода/марк с правилом `parseHTML` (`p`, `h1–h6`, списки, `blockquote`, `code`/`pre`, `a`, `strong`/`em`, таблицы, картинки, callout и т.п.). Всё остальное — `<div>`, `<style>`, `<script>`, инлайн-стили, кастомные теги — **молча отбрасывается**, выживает максимум текст внутри.
- Ноды «сырой HTML» в схеме нет (`rawHtml`/`htmlNode` в `packages/editor-ext/src/lib` отсутствуют).
- `marked` сам по себе HTML пропускает насквозь (санитайзер не подключён в `packages/editor-ext/src/lib/markdown/utils/marked.utils.ts`), но это неважно — финальным фильтром выступает схема ProseMirror на следующем шаге.
- Единственное, что отдалённо похоже на «вставку HTML» — embed-нода (`packages/editor-ext/src/lib/embed.ts`), но это `<iframe>` на URL известных провайдеров с санитизацией ссылки, а не произвольная разметка.
**Вывод:** чтобы получить произвольный HTML на странице, нужно добавлять в схему редактора отдельную ноду со своим `parseHTML`/`renderHTML` и собственным рендерингом.
## 2. Механика: как добавить такую ноду (одинаково для любого варианта)
По образцу `packages/editor-ext/src/lib/excalidraw.ts`:
1. **Новая нода** в `packages/editor-ext/src/lib/html-embed/`:
`Node.create({ name: 'htmlEmbed', group: 'block', atom: true, isolating: true })`.
Атрибут `source` (сырой HTML/CSS/JS строкой) с `parseHTML`/`renderHTML` через `data-`-атрибут или base64, чтобы корректно гонялось туда-обратно через HTML↔JSON. Экспорт добавить в `packages/editor-ext/src/index.ts`.
2. **Регистрация в ДВУХ схемах** (иначе сервер вырежет ноду при сохранении/коллаборации):
- клиент: `apps/client/src/features/editor/extensions/extensions.ts`
- сервер: `tiptapExtensions` в `apps/server/src/collaboration/collaboration.util.ts:58`
3. **React NodeView** на клиенте — то, что реально показывает контент. Здесь и зарыта безопасность (см. развилку).
4. **Markdown-сериализация** (turndown/marked в `packages/editor-ext/src/lib/markdown`) — если нода должна выживать при импорте/экспорте Markdown, иначе там она потеряется.
5. **UI вставки** — slash-команда/кнопка тулбара + модалка с редактором кода.
## 3. Развилка: модель изоляции (ключевое решение)
«Произвольный JS» в многопользовательской вики — это не фича рендеринга, а **модель доверия**. От выбора зависит весь NodeView и безопасность всего инстанса.
### Вариант A — Sandboxed iframe
Контент кладётся в `<iframe sandbox="allow-scripts" srcdoc="...">`.
- JS/CSS работают, но изолированы: нет доступа к DOM вики, кукам, токену сессии, localStorage.
- Безопасно, stored-XSS закрыт. Так делают HTML-эмбеды в Notion/Confluence.
- Минусы: скрипт не может управлять самой страницей; авто-высоту приходится решать через `postMessage`.
### Вариант B — Raw-инъекция в DOM страницы
`dangerouslySetInnerHTML` + выполнение `<script>`.
- Полная власть: скрипт выполняется в origin вики, может всё.
- Это **stored-XSS by design**: скрипт любого автора выполняется в браузере каждого читателя с его сессией → кража токенов, захват аккаунтов.
- Допустимо только на доверенном/одно-пользовательском инстансе.
### Вариант C — Raw-инъекция, но admin-only
Полная мощь raw-инъекции, но вставка такой ноды разрешена только админам/доверенным ролям; обычные авторы её добавлять не могут. Компромисс между мощью и риском.
## 4. Заработает ли трекер в песочнице? — НЕТ (для настоящего трекера)
Без `allow-same-origin` у iframe **opaque origin** (`null`). Из этого следуют ограничения, ломающие именно трекеры:
| Что делает трекер | В sandbox (`allow-scripts`) |
|---|---|
| Загрузить внешний `<script src>` и выполнить | ✅ работает |
| Отправить запрос/пиксель/`sendBeacon` на свой сервер | ✅ работает (fire-and-forget) |
| Поставить куку (`document.cookie`) | ❌ блокируется/кидает ошибку |
| `localStorage` / `sessionStorage` | ❌ `SecurityError` в opaque origin |
| Прочитать URL / referrer / title **самой вики-страницы** | ❌ видит только `about:srcdoc`, не родителя |
| Достучаться до DOM страницы (`window.parent`) | ❌ запрещено sandbox'ом |
**Итог:** GA4 / Яндекс.Метрика / Matomo внутри песочницы либо упадут на попытке поставить `_ga`/`_ym`, либо отправят «хит», где страница = `about:srcdoc`, а уникальный посетитель не сохраняется → данные мусорные. Песочница и «считать саму страницу» — взаимоисключающие вещи by design.
Добавлять `allow-same-origin` вместе с `allow-scripts` как «компромисс» нельзя: при одинаковом origin это снимает песочницу полностью (предупреждение MDN) — то есть это та же raw-инъекция окольным путём.
## 5. Что это значит для выбора
- **Цель — аналитика самих вики-страниц** (посещения, поведение, уники) → нужен скрипт в origin вики = **raw-инъекция**. Песочница тут бесполезна в принципе. Риск — stored-XSS, поэтому разумно держать это под **admin-only** (вариант C).
- **Цель — самодостаточный встроенный виджет** (калькулятор, демка, виджет без кук и без доступа к родителю) → песочницы (вариант A) хватает.
## 6. Возможные направления решения (выбрать позже)
1. **Admin-only raw-инъекция** — нода `htmlEmbed` с полным выполнением скрипта в origin вики; вставка только для админов/доверенных ролей. Трекер работает полноценно (куки, уники, URL страницы). Компромисс мощь/риск.
2. **Raw-инъекция без ограничений** — любой автор может вставить произвольный JS. Максимум гибкости, но stored-XSS для всех читателей. ОК только если все редакторы полностью доверенные.
3. **Узкая фича «только трекер», без произвольного JS** — вместо универсальной HTML-ноды поле в настройках для ID счётчика (GA/Метрика), сниппет вставляется в шаблон страницы. Безопасно и решает именно задачу трекинга.
4. **Sandboxed iframe (вариант A)** — для встраиваемых виджетов; для аналитики самих страниц не годится.
---
### Ссылки на код
- Парсинг по схеме (фильтр HTML): `apps/server/src/common/helpers/prosemirror/html/generateJSON.ts`
- Серверный список расширений: `apps/server/src/collaboration/collaboration.util.ts:58`
- Клиентский список расширений: `apps/client/src/features/editor/extensions/extensions.ts`
- Реестр нод editor-ext: `packages/editor-ext/src/index.ts`
- Образец кастомной ноды: `packages/editor-ext/src/lib/excalidraw.ts`
- Образец iframe-ноды: `packages/editor-ext/src/lib/embed.ts`
- Markdown ↔ HTML: `packages/editor-ext/src/lib/markdown/`