docs: remove implemented arbitrary-html-embed plan
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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/`
|
||||
Reference in New Issue
Block a user