mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-10-04 04:02:30 +02:00
Overhaul the Docsify documentation (English + Russian) so it matches the current codebase and follows one consistent pattern. Content accuracy (post-migration): - Rewrite development/autoloader.md to PSR-4 / Composer (the old XC_Autoloader scanner, igbinary tmp/cache/autoload_map and registerDirectories are gone). - PascalCase every source path (src/core -> src/Core, domain/Stream, cli/Commands, public/Controllers, Infrastructure/Redis, ...) across all docs. - Replace the removed autoload.php references with vendor/autoload.php (build_system, bootstrap-contexts, error-handling, modules). - ssl-generation: note that the installer now auto-generates a unique self-signed certificate before Nginx starts. Common pattern (Clean & uniform): - Strip emoji from headings; remove the in-page Navigation blocks (the Docsify sidebar already provides navigation). - One H1 + intro per doc; uniform "Related files" / "Связанные файлы" section, added to the code-centric docs that lacked it. Structure: - Remove the empty stray docs/api/; move updates_checklist.md into builds/; link the previously-orphaned ucs-integration.md. - Regroup the sidebars (split the oversized guides group into Developer Guides / Security & Access / Integrations; fold builds into Build & Release). Augment: - dev-workflow: Local Setup (make dev-tools) + Quality Checks (phpstan, cs, gates). - build_system: Composer Dependencies section (committed prod-only vendor, committed lock, dev tools via composer install, no build-time vendor step). en/ru parity: - Apply the same structure, fixes and pattern to docs/ru/ (translated), including a new Russian ucs-integration.md. The en and ru file sets are now identical.
321 lines
17 KiB
Markdown
321 lines
17 KiB
Markdown
# Валидация и санитизация ввода
|
||
|
||
XC_VM использует двухуровневую защиту входящих данных запроса. Сначала **глобальная санитизация** удаляет опасный контент из всех PHP-суперглобалов во время bootstrap, до того как заработает прикладной код. Затем **валидация на уровне действий** проверяет наличие обязательных полей перед выполнением бизнес-логики.
|
||
|
||
Оба слоя реализованы в `src/Core/Validation/InputValidator.php`.
|
||
|
||
---
|
||
|
||
## Поток глобальной санитизации
|
||
|
||
Санитизация запускается автоматически при bootstrap. Когда вызывается `LegacyInitializer::initCore()` (в `src/Core/Init/LegacyInitializer.php`), он выполняет следующие шаги до любого контроллера или сервиса:
|
||
|
||
```
|
||
LegacyInitializer::initCore()
|
||
|
|
||
+-- InputValidator::cleanGlobals($_GET)
|
||
+-- InputValidator::cleanGlobals($_POST)
|
||
+-- InputValidator::cleanGlobals($_SESSION)
|
||
+-- InputValidator::cleanGlobals($_COOKIE)
|
||
|
|
||
+-- $input = InputValidator::parseIncomingRecursively($_GET)
|
||
+-- RequestManager::set(InputValidator::parseIncomingRecursively($_POST, $input))
|
||
```
|
||
|
||
После этой последовательности все сырые суперглобалы санитизированы in-place, а объединённые/очищенные данные GET+POST доступны через `RequestManager`.
|
||
|
||
Стриминговый контекст (`LegacyInitializer::initStreaming()`) выполняет ту же последовательность санитизации, используя класс `Request`, который предоставляет эквивалентные методы для bootstrap-пути стриминга.
|
||
|
||
### cleanGlobals(&$rData, $rIteration = 0)
|
||
|
||
Рекурсивно обходит заданный массив суперглобала и удаляет опасный контент. Применяется к `$_GET`, `$_POST`, `$_SESSION` и `$_COOKIE`.
|
||
|
||
Удаляет следующее из каждого скалярного значения:
|
||
|
||
| Угроза | Удаляемый паттерн | Замена |
|
||
| --- | --- | --- |
|
||
| Null byte injection | `\0` (chr 0) | удаляется |
|
||
| Path traversal | `../` | `../` (HTML-encoded) |
|
||
| RTL override (UI spoofing) | `‮` | удаляется |
|
||
|
||
Рекурсия ограничена 10 уровнями для предотвращения исчерпания стека при глубоко вложенном вводе.
|
||
|
||
### parseIncomingRecursively(&$rData, $rInput, $rIteration = 0)
|
||
|
||
Рекурсивно обходит данные GET и POST, применяя санитизацию ключей и значений к каждому листу. Для массивов рекурсирует до 20 уровней. Для скалярных значений применяет `parseCleanKey()` к ключу и `parseCleanValue()` к значению.
|
||
|
||
Объединённый результат (сначала GET, затем POST поверх него) сохраняется в `RequestManager` для использования в течение жизненного цикла запроса.
|
||
|
||
### parseCleanKey($rKey)
|
||
|
||
Санитизирует ключи массивов для предотвращения инъекций через имена ключей:
|
||
|
||
1. URL-декодирует и HTML-экранирует ключ (`htmlspecialchars(urldecode(...))`)
|
||
2. Удаляет последовательности двойных точек (`..` -> `''`)
|
||
3. Удаляет маркеры в стиле `__dunder__` через regex
|
||
4. Валидирует по разрешённому набору символов: словесные символы, точки, дефисы, подчёркивания
|
||
|
||
### parseCleanValue($rValue)
|
||
|
||
Санитизирует скалярные значения в несколько проходов:
|
||
|
||
| Шаг | Что делает |
|
||
| --- | --- |
|
||
| Unescape | `stripslashes()` и восстановление ` ` в пробел |
|
||
| Нормализация переводов строк | Конвертирует `\r\n`, `\n\r`, `\r` в `\n` |
|
||
| Защита HTML-комментариев | `<!--` становится `<!--`, `-->` становится `-->` |
|
||
| Нейтрализация script-тегов | `<script` (регистронезависимо) становится `<script` |
|
||
| Нормализация сущностей | Исправление двойного-кодирования и некорректных числовых сущностей |
|
||
| Trim | Удаление ведущих/завершающих пробелов |
|
||
|
||
---
|
||
|
||
## Валидация на уровне действий
|
||
|
||
### validate()
|
||
|
||
```php
|
||
InputValidator::validate(string $rAction, array $rData): bool
|
||
```
|
||
|
||
Проверяет наличие минимально требуемых полей для заданного действия. Возвращает `true`, если данные приемлемы, и `false`, если обязательные поля отсутствуют или некорректны. Контроллеры должны вызывать её перед передачей данных в слой сервисов/репозиториев.
|
||
|
||
```php
|
||
if (!InputValidator::validate($action, $data)) {
|
||
// отклонить с ошибкой валидации
|
||
}
|
||
```
|
||
|
||
### validateOrFail()
|
||
|
||
```php
|
||
InputValidator::validateOrFail(string $rAction, array $rData): ?array
|
||
```
|
||
|
||
Удобная обёртка над `validate()`. Возвращает `null`, если данные валидны, или массив с ошибкой, если валидация не прошла:
|
||
|
||
```php
|
||
$error = InputValidator::validateOrFail($action, $data);
|
||
if ($error !== null) {
|
||
// $error = ['status' => STATUS_INVALID_INPUT, 'data' => $data]
|
||
return $error;
|
||
}
|
||
```
|
||
|
||
### confirmIDs($ids)
|
||
|
||
```php
|
||
InputValidator::confirmIDs(array $ids): array
|
||
```
|
||
|
||
Фильтрует массив, оставляя только положительные целочисленные ID. Любое значение, где `intval($id) <= 0`, отбрасывается. Используется широко по кодовой базе (30+ мест вызова) везде, где пользовательские списки ID должны быть санитизированы перед запросами к базе.
|
||
|
||
```php
|
||
$safeIds = InputValidator::confirmIDs($userSuppliedIds);
|
||
// [1, 42, 7] — отрицательные, ноль и нечисловые значения удалены
|
||
```
|
||
|
||
---
|
||
|
||
## Справочник действий валидации
|
||
|
||
Метод `validate()` использует `switch` по имени действия. Действия сгруппированы по функциональным областям ниже.
|
||
|
||
### Управление контентом
|
||
|
||
#### Потоки и каналы
|
||
|
||
| Действие | Обязательные поля | Заметки |
|
||
| --- | --- | --- |
|
||
| `processStream` | `stream_display_name` ИЛИ флаг `review` ИЛИ `$_FILES['m3u_file']` | Любое из трёх удовлетворяет валидации |
|
||
| `processChannel` | `stream_display_name` ИЛИ флаг `review` ИЛИ `$_FILES['m3u_file']` | Те же правила, что у processStream |
|
||
| `processRadio` | `stream_display_name` ИЛИ флаг `review` ИЛИ `$_FILES['m3u_file']` | Те же правила, что у processStream |
|
||
|
||
#### Movies / VOD
|
||
|
||
| Действие | Обязательные поля | Заметки |
|
||
| --- | --- | --- |
|
||
| `processMovie` | `stream_display_name` ИЛИ флаг `review` ИЛИ `$_FILES['m3u_file']` | Те же правила, что у processStream |
|
||
|
||
#### Сериалы и эпизоды
|
||
|
||
| Действие | Обязательные поля | Заметки |
|
||
| --- | --- | --- |
|
||
| `processSeries` | `title` | Имя сериала обязательно |
|
||
| `processEpisode` | `series` (непустое) И `season_num` (numeric) И (флаг `multi` ИЛИ `episode` numeric) | Сложная мульти-путевая валидация |
|
||
|
||
### Организация
|
||
|
||
#### Букеты
|
||
|
||
| Действие | Обязательные поля | Заметки |
|
||
| --- | --- | --- |
|
||
| `processBouquet` | `bouquet_name` | Обязательный скаляр |
|
||
| `reorderBouquet` | `stream_order_array` | Должен декодироваться в JSON-массив |
|
||
| `sortBouquets` | `bouquet_order_array` | Должен декодироваться в JSON-массив |
|
||
|
||
#### Категории
|
||
|
||
| Действие | Обязательные поля | Заметки |
|
||
| --- | --- | --- |
|
||
| `processCategory` | `category_name`, `category_type` | Оба обязательны |
|
||
| `orderCategories` | `categories` | Должен декодироваться в JSON-массив |
|
||
|
||
#### Группы и коды
|
||
|
||
| Действие | Обязательные поля | Заметки |
|
||
| --- | --- | --- |
|
||
| `processGroup` | `group_name` | Обязательный скаляр |
|
||
| `processGroupLegacy` | `group_name` | То же, что processGroup |
|
||
| `processCode` | `code` | Обязательный скаляр |
|
||
| `processPackage` | `package_name` | Обязательный скаляр |
|
||
|
||
### EPG
|
||
|
||
| Действие | Обязательные поля | Заметки |
|
||
| --- | --- | --- |
|
||
| `processEPG` | `epg_name`, `epg_file` | Оба обязательны |
|
||
|
||
### Устройства и линии
|
||
|
||
| Действие | Обязательные поля | Заметки |
|
||
| --- | --- | --- |
|
||
| `processMAG` | `mac` | MAC-адрес обязателен |
|
||
| `processEnigma` | `mac` | MAC-адрес обязателен |
|
||
| `setChannelOrder` | `stream_order_array` | Должен декодироваться в JSON-массив |
|
||
|
||
### Профили
|
||
|
||
| Действие | Обязательные поля | Заметки |
|
||
| --- | --- | --- |
|
||
| `processProfile` | `profile_name` | Обязательный скаляр |
|
||
|
||
### Провайдеры
|
||
|
||
| Действие | Обязательные поля | Заметки |
|
||
| --- | --- | --- |
|
||
| `processProvider` | `ip`, `port`, `username`, `password`, `name` | Все пять обязательны |
|
||
| `processISP` | `isp` | Обязательный скаляр |
|
||
| `processUA` | `user_agent` | Обязательный скаляр |
|
||
|
||
### Безопасность
|
||
|
||
| Действие | Обязательные поля | Заметки |
|
||
| --- | --- | --- |
|
||
| `blockIP` | `ip` | IP-адрес обязателен |
|
||
| `processRTMPIP` | `ip` | IP-адрес обязателен |
|
||
|
||
### Управление серверами
|
||
|
||
| Действие | Обязательные поля | Заметки |
|
||
| --- | --- | --- |
|
||
| `processServer` | `server_name`, `server_ip` | Оба обязательны |
|
||
| `processProxy` | `server_name`, `server_ip` | То же, что processServer |
|
||
| `installServer` | `ssh_port`, `root_password` | Оба обязательны |
|
||
| `moveStreams` | `content_type`, `source_server`, `replacement_server` | Все три обязательны |
|
||
| `replaceDNS` | `old_dns`, `new_dns` | Оба обязательны |
|
||
| `orderServers` | `server_order` | Должен декодироваться в JSON-массив |
|
||
|
||
### Записи и Watch Folders
|
||
|
||
| Действие | Обязательные поля | Заметки |
|
||
| --- | --- | --- |
|
||
| `scheduleRecording` | `title`, `source_id` | Оба обязательны |
|
||
| `processWatchFolder` | `folder_type`, `selected_path`, `server_id` | Все три обязательны |
|
||
|
||
### Массовые операции (JSON-массивы)
|
||
|
||
Все массовые операции требуют JSON-encoded массив в указанном поле. Поле должно декодироваться в валидный PHP-массив.
|
||
|
||
| Действие | JSON-поле |
|
||
| --- | --- |
|
||
| `massEditEpisodes` | `streams` |
|
||
| `massEditMovies` | `streams` |
|
||
| `massEditRadios` | `streams` |
|
||
| `massEditStreams` | `streams` |
|
||
| `massEditChannels` | `streams` |
|
||
| `massDeleteStreams` | `streams` |
|
||
| `massEditSeries` | `series` |
|
||
| `massDeleteSeries` | `series` |
|
||
| `massEditLines` | `users_selected` |
|
||
| `massEditUsers` | `users_selected` |
|
||
| `massEditMags` | `devices_selected` |
|
||
| `massEditEnigmas` | `devices_selected` |
|
||
| `massDeleteMovies` | `movies` |
|
||
| `massDeleteLines` | `lines` |
|
||
| `massDeleteUsers` | `users` |
|
||
| `massDeleteStations` | `radios` |
|
||
| `massDeleteMags` | `mags` |
|
||
| `massDeleteEnigmas` | `enigmas` |
|
||
| `massDeleteEpisodes` | `episodes` |
|
||
|
||
---
|
||
|
||
## Поведение по умолчанию (fallthrough)
|
||
|
||
Действия, явно не перечисленные в `switch`, проваливаются на `return true`, то есть всегда проходят валидацию. Это намеренно — у этих действий либо нет обязательных полей на уровне гейта, либо они выполняют собственную валидацию глубже в слое бизнес-логики.
|
||
|
||
**Действия с явным пропуском** (в switch с `return true`):
|
||
|
||
- `processUser`
|
||
- `processLine`
|
||
- `processHMAC`
|
||
- `editAdminProfile`
|
||
- `editSettings`
|
||
- `editBackupSettings`
|
||
- `editCacheCron`
|
||
- `editPlexSettings`
|
||
- `editWatchSettings`
|
||
- `processPlexSync`
|
||
- `processLogin`
|
||
- `submitTicket`
|
||
|
||
**Действия с неявным пропуском** (вообще не в switch, попадают в default `return true`):
|
||
|
||
Любая строка действия, не совпадающая с `case`, также вернёт `true`. Если новому действию нужен input-гейт, case должен быть добавлен явно.
|
||
|
||
---
|
||
|
||
## Используемые паттерны валидации
|
||
|
||
Метод `validate()` использует небольшой набор паттернов последовательно:
|
||
|
||
| Паттерн | Назначение | Пример |
|
||
| --- | --- | --- |
|
||
| `!empty($rData['field'])` | Обязательное скалярное поле (не null, не пустое, не ноль) | `!empty($rData['bouquet_name'])` |
|
||
| `is_numeric($rData['field'] ?? null)` | Числовая валидация с null-safe fallback | `is_numeric($rData['season_num'] ?? null)` |
|
||
| `is_array(json_decode($rData['field'] ?? '', true))` | JSON-строка, декодируемая в массив | `is_array(json_decode($rData['streams'] ?? '', true))` |
|
||
| `isset($rData['field'])` | Проверка наличия поля (значение может быть пустым/falsy) | `isset($rData['review'])` |
|
||
| `isset($_FILES['field'])` | Проверка наличия загруженного файла | `isset($_FILES['m3u_file'])` |
|
||
| OR-условия | Мульти-путевая валидация (любой путь удовлетворяет) | `!empty($rData['stream_display_name']) \|\| isset($rData['review']) \|\| isset($_FILES['m3u_file'])` |
|
||
|
||
---
|
||
|
||
## Добавление валидации для нового действия
|
||
|
||
Добавьте `case` в `switch` в `src/Core/Validation/InputValidator.php`:
|
||
|
||
```php
|
||
case 'myNewAction':
|
||
return !empty($rData['required_field'])
|
||
&& is_numeric($rData['numeric_field'] ?? null);
|
||
```
|
||
|
||
Рекомендации:
|
||
|
||
- Валидируйте только минимально необходимые входные данные на этом слое. Доменные правила (валидация формата, бизнес-ограничения, проверки уникальности) держите в сервисном слое.
|
||
- Используйте `!empty()` для обязательных скаляров, `is_numeric()` для числовых полей и `is_array(json_decode(..., true))` для JSON-массивов.
|
||
- Для действий, принимающих загрузку файлов как альтернативу полям формы, включите `isset($_FILES['field'])` как OR-условие.
|
||
- Если действию не нужна валидация на уровне гейта, добавьте его в блок явного пропуска с `return true`, чтобы будущим мейнтейнерам было понятно, что пропуск намеренный, а не случайный.
|
||
|
||
---
|
||
|
||
## Связанные файлы
|
||
|
||
| Файл | Назначение |
|
||
| --- | --- |
|
||
| `src/Core/Validation/InputValidator.php` | Вся логика санитизации и валидации |
|
||
| `src/Core/Init/LegacyInitializer.php` | Bootstrap-вызыватель, запускающий санитизацию через `initCore()` |
|
||
| `src/Core/Http/RequestManager.php` | Хранит санитизированные, объединённые данные GET+POST |
|
||
| `src/Public/Controllers/` | Контроллеры, вызывающие `validate()` / `validateOrFail()` перед бизнес-логикой |
|