<!--
 * MD.Viewer — Документация
 * Version: 2.11.3
-->

# MD.Viewer

[![PHP](https://img.shields.io/badge/PHP-8.3%2B-777BB4?logo=php&logoColor=white)](https://php.net)
[![md.php](https://img.shields.io/badge/md.php-v2.18.0-success)](https://github.com/paulmann/MD.Viewer)
[![updater.php](https://img.shields.io/badge/updater.php-v3.14.0-4f46e5)](https://github.com/paulmann/MD.Viewer)

<!-- md-i18n:switcher -->
**На других языках:** [English](README.md) · [Deutsch](README.de.md) · [中文](README.zh.md) · [हिन्दी](README.hi.md) · [Español](README.es.md) · Русский

---

## Обзор

MD.Viewer построен вокруг **`md.php`** — единственной точки входа на PHP, которая отдаёт совпадающий файл Markdown, а если совпадающего файла нет, переключается на рекурсивный браузер файлов Markdown. Проект рассчитан на простое развёртывание: скопируйте файлы на PHP-хостинг, положите рядом свои документы `.md` и откройте `md.php` в браузере.

Просмотрщик рассчитан на самохостящуюся документацию без инструментальной цепочки. Он умеет читаемую типографику, нумерацию заголовков, автоматическое построение оглавления, отрисовку Mermaid с автопочинкой и диагностикой в консоли на английском, общие кнопки копирования кода и цитат, всплывающие подсказки глоссария, тёмную тему, элементы управления под мобильные экраны и панель настроек, которая хранит предпочтения просмотрщика локально.

**`updater.php`** — сопутствующий движок самообновления и обслуживания. Он сверяет сырые файлы на GitHub по ETag и SHA-256, делает версионные резервные копии перед заменой, восстанавливает прежние версии из панели настроек или прямо по URL, управляет необязательной жёсткой ссылкой `index.php`, принимает загрузку файлов `.md` и поддерживает обновление и откат прямо из браузера.

Поведение на стороне сервера задаётся файлом конфигурации **`.md.ini`**, который кладётся рядом с `md.php`. Этот файл жёстко фиксирует флаги возможностей, которые нельзя переопределить из браузера или из панели настроек.

---

## Быстрый старт

```bash
# Clone into your web root or any subdirectory
git clone https://github.com/paulmann/MD.Viewer.git /var/www/html/docs

# Create a Markdown file that matches the script name
echo "# Hello, World\n\nThis is my documentation." > /var/www/html/docs/md.md

# Open in a browser
# https://your-domain.com/docs/md.php
```

Минимальная установка:

- Положите **`md.php`** (вместе с каталогами `js/md/` и `css/md/`) рядом с **`md.md`**.
- Откройте **`md.php`** в браузере.
- Скрипт сам ищет файл Markdown с тем же базовым именем.

Необязательно:

- Добавьте **`updater.php`**, чтобы включить проверку обновлений внутри приложения, применение обновлений, резервные копии, восстановление, загрузку файлов и управление ссылкой `index.php`.
- Задайте **`ALLOW_UPDATE = true`** в `.md.ini`, чтобы включить систему обновлений (по умолчанию выключена).

### Установка из одного файла (бутстрап)

Самый быстрый способ установки — загрузить на сервер **только `updater.php`**, а затем открыть его в браузере с `?update=true`. Он:

1. Автоматически создаст `.md.ini` с безопасными значениями по умолчанию.
2. Скачает с GitHub все отслеживаемые файлы (`md.php`, `js/md/*`, `css/md/*`, `README.md`, `LICENSE`).
3. Создаст все нужные подкаталоги (`js/md/`, `css/md/`) с правами `0755`, если их нет.
4. Поставит себя в итоговый список и покажет страницу с полным результатом.

**Требование:** для режима браузера перед запуском в `.md.ini` должно стоять `ALLOW_UPDATE = true` (либо добавьте его вручную в автоматически созданный `.md.ini` и перезагрузите страницу). Для машинного API удостоверением вместо него служит `API_KEY` из `.md.ini` — см. [Машинный API](#4-машинный-api-json-для-внешних-клиентов).

```bash
# Upload only updater.php, then:
curl -o updater.php https://raw.githubusercontent.com/paulmann/MD.Viewer/main/updater.php
# Edit .md.ini (auto-created on first hit) and set ALLOW_UPDATE = true
# Then open: https://your-domain.com/updater.php?update=true
#
# Or drive it over the machine API (no ALLOW_UPDATE needed, key from .md.ini):
curl "https://your-domain.com/updater.php?api_key=<key>&action=conflicts&format=json"
curl -X POST "https://your-domain.com/updater.php?api_key=<key>&action=install&format=json&dry_run=1"
```

---

## Требования

- PHP **8.3+**.
- Расширения PHP: `mbstring` и `curl` (нужны для обновлятора и для работы с буфером обмена).
- Веб-сервер, например Apache, Nginx, Caddy или встроенный сервер PHP.
- Доступ в интернет со стороны браузера для ресурсов CDN (Mermaid, подсветка синтаксиса, Tailwind), если они не заменены локальными копиями.

---

## Установка

### Клонирование через git

```bash
git clone https://github.com/paulmann/MD.Viewer.git
cd MD.Viewer
```

### Ручная установка

1. Скачайте репозиторий или архив выпуска.
2. Скопируйте `md.php`, `updater.php`, `js/`, `css/` и, при желании, `README.md` в целевой каталог.
3. Добавьте свои файлы Markdown в то же дерево каталогов.
4. Откройте `md.php` в браузере. `.md.ini` создаётся автоматически при первой загрузке.

### Установка одним файлом через обновлятор

1. Загрузите на сервер в нужный каталог только `updater.php`.
2. Откройте `updater.php` один раз в браузере — он сам создаст `.md.ini` с `ALLOW_UPDATE = false`.
3. Отредактируйте `.md.ini` на сервере: задайте `ALLOW_UPDATE = true`.
4. Откройте `https://your-domain.com/updater.php?update=true`.
5. Обновлятор скачает все файлы, при необходимости создаст каталоги `js/md/` и `css/md/` и покажет страницу результата.
6. Откройте `md.php` — ваш MD.Viewer готов.
7. Необязательно: установите, не трогая `ALLOW_UPDATE`, — вызовом машинного API с
   `API_KEY` из `.md.ini` (см. [Машинный API](#4-машинный-api-json-для-внешних-клиентов)).

### Встроенный сервер PHP

```bash
cd /path/to/MD.Viewer
php -S localhost:8080
# then open http://localhost:8080/md.php
```

### Пример Nginx

```nginx
server {
    listen 80;
    server_name docs.example.com;
    root /var/www/html/docs;
    index index.php md.php;

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}
```

---

## Состав файлов

```text
MD.Viewer/
├── md.php                     # Main viewer / browser script       v2.18.0
├── updater.php                # Self-updater, backup, upload        v3.14.0
├── .md.ini                    # Server-side config (auto-created)
├── md.md                      # Default Markdown file for md.php
├── uploads.md/                # Created automatically for uploaded/saved files
├── md.backup/                 # Created automatically when updates/restores run
│   ├── 2.8.2/
│   ├── 2.8.2-pre-restore/     # Auto-created before each rollback
│   └── .state/                # ETag + SHA-256 per-file cache
├── css/
│   └── md/
│       ├── md.css             # v2.2.2
│       ├── settings.css       # v2.9.0
│       └── tooltips.css       # v2.4.5
├── js/
│   └── md/
│       ├── md.js              # v2.7.0
│       ├── settings.js        # v2.11.0
│       ├── tooltips.js        # v2.4.5
│       └── upload.js          # v2.9.1
├── LICENSE                    # v1.0.0
└── README.md                  # v2.11.3
```

Главное правило именования: `md.php` ищет `md.md`, `docs.php` ищет `docs.md`. Несколько независимых экземпляров просмотрщика могут делить одно дерево каталогов без дополнительной маршрутизации.

---

## Конфигурация на стороне сервера — `.md.ini`

При первом запросе `md.php` (или `updater.php`) создаёт `.md.ini` в том же каталоге. Этот файл жёстко фиксирует флаги возможностей, так что их **нельзя** изменить из браузера или из панели настроек. Отсутствующие ключи автоматически дописываются с безопасными значениями по умолчанию при каждой загрузке.

```ini
; MD.Viewer server-side configuration

; Disable the "Upload .md" button in the file browser (recommended: true)
DISABLE_UPLOAD    = true

; Disable the "Preview Clipboard" button in the file browser
DISABLE_CLIPBOARD = false

; Disable the "Save to File" button in clipboard preview
DISABLE_SAVE_CLIPBOARD_TO_FILE = true

; Allow updating files via updater.php?update=true or the Settings panel
ALLOW_UPDATE = false

; Allow restoring a backup via updater.php?restore=latest or ?restore=[version]
ALLOW_RESTORE = false

; Allow creating/removing the index.php hard link from the Settings panel
ALLOW_CREATE_INDEX_PHP_LINK = true

; Machine API key used by external clients (a control panel, an installer)
; Generated automatically on first run. Send it as ?api_key=<value>.
API_KEY = mdv_your_generated_key_here
```

| Ключ | По умолчанию | Действие |
|---|---|---|
| `DISABLE_UPLOAD` | `true` | Скрывает и отключает кнопку загрузки |
| `DISABLE_CLIPBOARD` | `false` | Скрывает и отключает кнопку просмотра буфера обмена |
| `API_KEY` | создаётся автоматически | Секретный ключ для машинного API (`?api_key=`) ; требуется для JSON-установок |
| `DISABLE_SAVE_CLIPBOARD_TO_FILE` | `true` | Скрывает сохранение в файл в просмотре буфера обмена |
| `ALLOW_UPDATE` | `false` | Включает систему обновлений (панель настроек + `?update=true`) |
| `ALLOW_RESTORE` | `false` | Включает раздел резервных копий и восстановления и режим URL `?restore=` |
| `ALLOW_CREATE_INDEX_PHP_LINK` | `true` | Включает раздел файла index в панели настроек |

**Все ключи дописываются автоматически** (со своими значениями по умолчанию) в существующие файлы `.md.ini` при следующей загрузке — обновление никогда не требует ручной правки `.md.ini`.

### `.md.ini` в отдельном каталоге (v2.12.0 / v3.12.0)

`.md.ini` рядом с `md.php` сохраняет роль файла уровня установки
(`BROWSE_DIR`, `API_KEY`, `ALLOW_*` и значения по умолчанию для флагов `DISABLE_*`).
Поверх него **любой каталог внутри корня просмотра может нести собственный
`.md.ini`**. Настройки, применимые к документу, — это базовый файл, поверх которого
наложены все `.md.ini`, найденные на пути от корня просмотра вниз до каталога
документа: более глубокий файл всегда важнее более верхнего:

```text
ext/mdviewer/.md.ini     base file: installation-level keys, API key
share/.md.ini            applies to everything below share/
share/sub/.md.ini        applies to share/sub/ and below, and wins
```

В файле каталога учитываются только эти три ключа:

| Ключ | По умолчанию | Действие в этом каталоге |
|---|---|---|
| `DISABLE_UPLOAD` | `true` | Разрешена ли загрузка **.md** и куда попадает загруженный файл |
| `DISABLE_CLIPBOARD` | `false` | Активна ли кнопка просмотра буфера обмена |
| `DISABLE_SAVE_CLIPBOARD_TO_FILE` | `true` | Разрешено ли **сохранение в файл** и куда попадает файл |

Правила:

- Ключи `BROWSE_DIR`, `API_KEY`, `ALLOW_UPDATE`, `ALLOW_RESTORE` и
  `ALLOW_CREATE_INDEX_PHP_LINK` относятся к уровню установки. В файле каталога они
  игнорируются, а в журнал ошибок PHP пишется пометка.
- Значения разбираются так же строго, как в базовом файле (пробелы по краям
  обрезаются, парные кавычки, если они есть, снимаются; значение, содержащее нулевой
  байт, перевод строки или больше 255 байт, отбрасывается). Нечитаемый файл или
  непригодное значение пропускается с записью в журнал ошибок — это никогда не ломает
  страницу и никогда не откатывается к привилегированному значению по умолчанию.
- `.md.ini` каталога читается только тогда, когда его реальный путь находится внутри
  корня просмотра, поэтому путь, который пытается выйти наружу через `..`,
  отклоняется.
- В режиме просмотра (`?file=sub/doc.md`) действует каталог документа; в режиме
  браузера файлов действует корень просмотра.
- **Загрузить .md** и **Сохранить в файл** пишут в просматриваемый каталог. Запрос
  передаёт его параметром `dir` (относительно корня просмотра); на значение `dir` с
  сегментами обхода, абсолютным путём или нулевым байтом отвечают `400`, и выйти из
  каталога каким-либо другим способом тоже нельзя.
- Файлы `.md.ini` в каталогах необязательны. Без них поведение ровно такое же, как
  раньше.

---

## Режимы работы

### Режим просмотра

Когда совпадающий файл Markdown существует, MD.Viewer отрисовывает его как оформленную страницу документа. Заголовок страницы берётся из первого заголовка верхнего уровня, описание строится из первого абзаца, строится оглавление и применяются локальные предпочтения просмотрщика.

### Режим браузера файлов

Когда совпадающего файла Markdown нет, MD.Viewer переключается на рекурсивный браузер Markdown. Браузер просматривает текущее дерево каталогов, перечисляет файлы Markdown в таблице с поиском и открывает выбранный файл по `?file=relative/path.md`.

### Режим просмотра из буфера обмена

В браузере файлов есть кнопка **Просмотр буфера обмена**. Вставьте любой текст Markdown в поле и нажмите «Просмотр» — содержимое отрисуется в режиме просмотра. Если `DISABLE_SAVE_CLIPBOARD_TO_FILE = false`, появляется полоса **Сохранить в файл** с полем имени файла и кнопкой сохранения, которая пишет в `BROWSE_DIR` (а если этот ключ не задан — в `uploads.md/` рядом с `md.php`).

### Параметры URL

| Параметр | Значение | Пример |
|---|---|---|
| `?file=path/to/doc.md` | Отрисовать указанный файл Markdown | `md.php?file=docs/api.md` |

---

## Возможности

### Отрисовка Markdown

- Заголовки, абзацы, курсив, полужирный текст, встроенный код, цитаты, горизонтальные линии, блоки кода в ограждении.
- Автопочинка Mermaid преобразует заметки классов в стиле PlantUML, литеральные `\n`, незакрытые `<br>`, `return` из PlantUML и скобки без кавычек; диагностика выводится только на английском.
- Упорядоченные и неупорядоченные списки, включая вложенные.
- Таблицы с оформленным выводом.
- Списки задач, сноски, ссылки в справочном стиле.
- Изображения и обычные ссылки.
- Поддержка коротких кодов эмодзи.
- Универсальные встроенные шаблоны для аккуратного оформления документа.

### Навигация и структура

- Автоматически строящееся оглавление.
- Необязательная автоматическая нумерация заголовков.
- Разделение заголовка по двоеточию для более чистого заголовка страницы.
- Режим браузера файлов с поиском.
- Копирование и скачивание исходного `.md` читаемого документа, а также каждого файла в
  браузере (оба переключателя по умолчанию включены).

### Код, диаграммы и насыщенное содержимое

- Блоки кода в ограждении с подсветкой синтаксиса и кнопкой копирования.
- Диаграммы Mermaid.
- Поддержка верхних и нижних индексов.

### Всплывающие подсказки глоссария

Встроенные подсказки глоссария через `js/md/tooltips.js` и `css/md/tooltips.css`. При наведении на термин показывается подсказка; движок сводит варианты написания к одному определению. Есть поддержка касаний, двухсекундная задержка и разумное позиционирование с учётом границ окна.

### Панель настроек — подсказки к возможностям

Каждый переключатель в разделе **Возможности** панели настроек показывает подробную подсказку при наведении: что делает эта возможность и как она влияет на отрисовку. Подсказки используют тот же пузырь `.g-tooltip` и тот же движок позиционирования, что и подсказки глоссария: тот же внешний вид, та же поддержка тёмной темы, то же поведение при касании и с клавиатуры. Дополнительный CSS не требуется.

### Внешний вид и управление чтением

- Тёмная тема.
- Регулируемые ширина страницы, размер шрифта и высота строки.
- Настройки видимости панели инструментов в шапке (отдельно для компьютера и мобильных).
- Сохраняемые предпочтения в cookie браузера.

### Загрузка файлов

Если в `.md.ini` задано `DISABLE_UPLOAD = false`, браузер файлов показывает кнопку **Загрузить .md**. Загруженные файлы сохраняются в `BROWSE_DIR` или, если этот ключ не задан, в `uploads.md/` рядом с `md.php`. При настройках для отдельных каталогов файл попадает вместо этого в просматриваемый каталог, и решает, допустимо ли действие, собственный `.md.ini` этого каталога. Защита загрузки трёхуровневая:

1. Кнопка в интерфейсе скрыта или отключена при `DISABLE_UPLOAD = true`.
2. Обработчик клика в JS сразу выходит, если `MDV_CONFIG.disableUpload` истинно.
3. PHP `updater.php`: независимо читает `.md.ini` и возвращает `403`, если загрузка отключена.

### Копирование и скачивание исходного `.md`

Три необязательных элемента управления отдают читателю сам файл вместо отрисованной страницы.
Все три по умолчанию включены, все три — переключатели возможностей на стороне PHP (см. панель
настроек и таблицу ключей `.md.ini`), и то и другое — **скорее ограничение интерфейса, чем
контроль доступа**: запрос того же файла в любом случае остаётся выполнимым, потому что в
просмотрщике нет аутентификации и никогда не было.

| Переключатель | Где появляется | По умолчанию |
|---|---|---|
| `FEATURE_DOWNLOAD_ICON` | Значок скачивания в каждой строке браузера файлов | вкл. |
| `FEATURE_DOWNLOAD_BUTTON` | Кнопка скачивания на панели документа, слева от элементов ширины и шрифта | вкл. |
| `FEATURE_COPY_BUTTON` | Кнопка копирования на панели документа, слева от кнопки скачивания | вкл. |

Все три ведут к одному запросу — `md.php?file=<relative path>&download=1` — и этот
запрос проверяется ровно той же функцией, что и страница просмотра, поэтому путь,
отклонённый для показа, отклоняется и здесь. Ответ отдаёт байты файла как
`application/octet-stream` с `X-Content-Type-Options: nosniff` и
`Content-Disposition: attachment`: `.md` — это текст, а объявить его текстом под именем
файла, выбранным самим автором документа, — это ровно тот способ, которым он в глазах
браузера стал бы скриптом. Кнопка копирования запрашивает этот адрес по клику, а не
встраивает документ в страницу второй раз, и затем кладёт результат в буфер обмена. За
просмотром из буфера обмена файла нет, поэтому он не получает ни одного из элементов
управления панели.

### Просмотр из буфера обмена и сохранение в файл
Вставьте текст Markdown и отрисуйте его сразу, без сохранения файла. Если `DISABLE_SAVE_CLIPBOARD_TO_FILE = false`, появляется полоса «Сохранить в файл» с полем имени файла, кнопкой сохранения и встроенным сообщением о состоянии. Исходный текст сохраняется в `sessionStorage`, когда просмотр открывается. Файл записывается в просматриваемый каталог, а `.md.ini` этого каталога может запретить или снова разрешить сохранение там.

### Панель настроек

Панель настроек (открывается значком шестерёнки в шапке) включает:

- Переключатели предпочтений просмотрщика (тема, ширина, шрифт, высота строки, оглавление, нумерация).
- Управление видимостью панели инструментов в шапке для каждого типа устройства.
- Переключатели возможностей на стороне PHP с описательными подсказками при наведении (применяются при следующей загрузке страницы через cookie).
- Элементы проверки и применения обновлений (видны только при `ALLOW_UPDATE = true`).
- Список резервных копий и элементы восстановления (видны только при `ALLOW_RESTORE = true`).
- Управление жёсткой ссылкой `index.php` (видно только при `ALLOW_CREATE_INDEX_PHP_LINK = true`).

---

## Встроенный обновлятор

`updater.php` даёт четыре способа обновлять файлы и управлять ими.

### Стартовая страница обновлятора

Открытие `updater.php` без параметров показывает **информационную страницу** с:

- Состоянием «есть / отсутствует» для каждого отслеживаемого файла с номерами версий.
- Каждое имя файла — ссылка на сырой файл на GitHub (открывается в новой вкладке).
- Состоянием `.md.ini`: показывается предупреждение, если `ALLOW_UPDATE = false`, с указаниями по правке.
- Кнопками действий (**Проверить и применить обновления**, **↺ Переустановить всё принудительно**, **⟲ Восстановить последнюю копию**) — появляются только тогда, когда соответствующий флаг включён в `.md.ini`.

### 1. Панель настроек (AJAX)

- Проверка обновлений по всем отслеживаемым файлам с показом версии каждого файла.
- Применение обновлений — с резервным копированием текущих файлов и скачиванием новых версий.
- Принудительная переустановка всех файлов независимо от версии (в обход кеша ETag/SHA-256).
- Просмотр и восстановление версий из резервных копий (видно только при `ALLOW_RESTORE = true`).

### 2. Прямое обновление (`?update=true`)

Требует `ALLOW_UPDATE = true` в `.md.ini`.

```
https://your-domain.com/updater.php?update=true
https://your-domain.com/updater.php?update=true&force=true   ← force reinstall
```

**Двухфазный бутстрап:**

- **Фаза 1** — сначала обновляет сам `updater.php`. Если файл изменился, перенаправляет на `?_phase=2` (сохраняя `&force=true`, если он был задан), чтобы остальным занимался **новый** обновлятор.
- **Фаза 2** — обновляет все остальные отслеживаемые файлы с показом разницы версий.

**Флаг `&force=true`:**

- Полностью пропускает кеш ETag и SHA-256 — каждый файл всегда скачивается с GitHub.
- Полезен после неудачного частичного обновления или когда локальные файлы повреждены.
- Автоматически передаётся из фазы 1 в фазу 2 через URL перенаправления.
- В подвале страницы результата появляется янтарная кнопка **↺ Переустановить всё принудительно**.

**Автосоздание каталогов:**

Обновлятор создаёт недостающие подкаталоги (`js/md/`, `css/md/`) с правами `0755` перед записью файлов. Именно поэтому `?update=true` и машинный API работают при установке из одного файла.

**Страница результата:**

- Значки состояния по каждому файлу: `updated` / `force-updated` / `created` / `current` / `error`
- Разница версий: `2.7.0 → 2.8.2`
- Каждое имя файла — кликабельная ссылка на сырой файл на GitHub (открывается в новой вкладке)
- Подвал: кнопка **↺ Переустановить всё принудительно** + кнопка **← Назад**

### 3. Прямой откат (`?restore=`)

Требует `ALLOW_RESTORE = true` в `.md.ini`.

```
https://your-domain.com/updater.php?restore=latest
https://your-domain.com/updater.php?restore=2.7.0
```

| URL | Поведение |
|---|---|
| `?restore=latest` | Просматривает `md.backup/` и автоматически выбирает наивысшую версию semver |
| `?restore=X.Y.Z` | Восстанавливает точно указанную версию резервной копии |

**Процесс отката:**

1. Проверяет `ALLOW_RESTORE = true` — при false показывает страницу с ошибкой.
2. `?restore=latest` просматривает `md.backup/`, сортирует по semver и берёт самую новую.
3. Проверяет строку версии (только буквы, цифры, точки и дефисы).
4. Копирует текущие файлы в `md.backup/[version]-pre-restore/` перед перезаписью — откат отката возможен всегда.
5. Сбрасывает кеш ETag по файлам (`.state/`), чтобы следующая проверка обновлений скачивала всё целиком.
6. Выводит HTML-страницу результата со значками `restored` / `skipped` / `error`, разницей версий и кликабельными ссылками на файлы.

### 4. Машинный API (JSON, для внешних клиентов)

Защищённый ключом JSON-API позволяет другой программе (панели управления, установщику) управлять установкой и
показывать её ход шаг за шагом. Он никогда не возвращает HTML, а замечания и предупреждения PHP никогда не
попадают в тело ответа.

`API_KEY` создаётся при первом запросе и хранится в `.md.ini`. Задайте там своё значение
перед развёртыванием, если хотите автоматизировать бутстрап, не читая файл потом.

```
GET  updater.php?api_key=<key>&action=status&format=json
GET  updater.php?api_key=<key>&action=conflicts&format=json
POST updater.php?api_key=<key>&action=install&format=json[&dry_run=1][&force=1]
```

| Действие | Метод | Назначение |
|---|---|---|
| `status` | GET | Состояние локальной установки по каждому файлу (есть / отсутствует / версия). Без сетевого ввода-вывода. |
| `conflicts` | GET | Отчёт о конфликтах в режиме сухого прогона. Ничего не пишет. |
| `install` | POST | Скачивает и записывает файлы через защитную проверку перед записью. |

Обёртка ответа:

```json
{
  "ok": true,
  "action": "install",
  "version": "2.16.0",
  "steps":     [{"step": "install:css/md/md.css", "status": "ok", "ms": 143, "detail": "2.2.2 → 2.2.2"}],
  "files":     [{"path": "css/md/md.css", "status": "current", "version": "2.2.2"}],
  "conflicts": [{"path": "css/md/settings.css", "status": "same_name_elsewhere",
                 "existing_path": "legacy/components/settings.css", "size_bytes": 1932}],
  "errors":    []
}
```

- `conflicts[].status`: `ok` | `exists` (точный путь уже существует) | `same_name_elsewhere`
- `files[].status`: `present` | `missing` | `updated` | `created` | `force-updated` | `current` | `skipped` | `would_write` | `error`
- `steps[].status`: `ok` | `dry-run` | `skip` | `warn` | `error`
- HTTP-коды: `200` — выполнено (при сбоях по отдельным файлам в теле остаётся `"ok": false`), `400` — неизвестное действие, `403` — `unauthorized` или `api_key_unavailable`, `405` — `install` без POST

`dry_run=1` останавливается после отчёта о конфликтах: ничего не скачивается и ничего не записывается.
`force=1` дополнительно заменяет файл по отслеживаемому пути, который не выглядит как файл MD.Viewer
(иначе файл пропускается и попадает в `conflicts`), и обходит кеш ETag/SHA-256.
Режимы браузера, описанные выше, по-прежнему подчиняются `ALLOW_UPDATE` / `ALLOW_RESTORE`; для действий
JSON собственным удостоверением служит ключ API.
---

## Отслеживаемые файлы

Все файлы, которыми управляет обновлятор (проверяются, скачиваются, копируются):

```
md.php                    v2.18.0
updater.php               v3.14.0
js/md/md.js               v2.7.0
js/md/settings.js         v2.11.0
js/md/tooltips.js         v2.4.5
js/md/upload.js           v2.9.1
css/md/md.css             v2.2.2
css/md/settings.css       v2.9.0
css/md/tooltips.css       v2.4.5
README.md                 v2.11.3
LICENSE                   v1.0.0
```

---

## Резервные копии и восстановление

Перед каждой заменой файла (обновление или принудительная переустановка) текущий локальный файл копируется в `md.backup/[version]/`. Панель настроек перечисляет версии резервных копий, даты и число файлов (видно только при `ALLOW_RESTORE = true`). Восстановление сначала копирует текущую версию (как `[version]-pre-restore`), затем записывает обратно выбранные файлы. `README.md` и `LICENSE` скачиваются и обновляются, но **не** копируются в резерв (это документация, а не код).

---

## Безопасность

Все действия, важные для безопасности, защищены на **трёх независимых уровнях**: флаг в серверном `.md.ini`, защитная проверка в PHP и подавление в интерфейсе на стороне клиента.

| Возможность | Флаг в `.md.ini` | Защита в PHP | Подавление в интерфейсе |
|---|---|---|---|
| Обновление (применение и проверка) | `ALLOW_UPDATE` | `requireAllowUpdate()` | Скрыто при false |
| Восстановление резервной копии | `ALLOW_RESTORE` | `requireAllowRestore()` | Скрыто при false |
| Список резервных копий | `ALLOW_RESTORE` | `requireAllowRestore()` | Не вызывается при false |
| Создание ссылки index | `ALLOW_CREATE_INDEX_PHP_LINK` | `requireAllowIndexLink()` | Скрыто при false |
| Удаление ссылки index | `ALLOW_CREATE_INDEX_PHP_LINK` | `requireAllowIndexLink()` | Скрыто при false |
| Состояние index | `ALLOW_CREATE_INDEX_PHP_LINK` | Возвращает `{disabled:true}` | Не вызывается при false |
| Загрузка файла | `DISABLE_UPLOAD` | 403 при true | Кнопка скрыта |
| Сохранение из буфера обмена | `DISABLE_SAVE_CLIPBOARD_TO_FILE` | 403 при true | Кнопка скрыта |
| Машинный API (`status`, `conflicts`, `install`) | `API_KEY` | 403 `unauthorized` | Не вызывается без ключа |

Дополнительные меры:

- **`.md.ini`** никогда не отдаётся браузеру — он читается только на стороне сервера.
- **`.md.ini` отдельных каталогов** читаются только из каталогов внутри корня просмотра, и из них берутся только ключи `DISABLE_*`; ключи уровня установки игнорируются с записью в журнал.
- **Обход путей** предотвращён во всех операциях отдачи файлов, загрузки и сохранения.
- **Загрузка и сохранение в файл** проверяют имена файлов на стороне сервера (только расширение `.md`, без косых черт, без нулевых байтов).
- **Защита CORS** в `updater.php` отклоняет запросы с других источников.
- **Все изменяющие действия** требуют `POST` (кроме прямых режимов по URL, которым нужны свои флаги `ALLOW_*`).
- **Строки версий** в `?restore=` проверяются только по набору `[a-zA-Z0-9.\-]`.
- **Машинный API** отвечает только JSON (`format=json`), требует `API_KEY` и сообщает о каждом
  пропущенном чужом файле вместо его перезаписи.

---

## Лицензия

Распространяется по лицензии PolyForm Noncommercial License 1.0.0. См. [LICENSE](https://github.com/paulmann/MD.Viewer/blob/main/LICENSE).
