Files
XC_VM/docs/ru/guides/input-validation.md
T
Divarion-D f4555e7943 docs: reorganize structure, sync with current code, add missing framework reference
Structure:
- Split development/ (20 files) into development/ (framework reference) and guides/ (how-to)
- Moved 13 how-to files into new guides/ category (auth, cli, error-handling, etc.)
- Moved navbar-rendering.md from system/ into development/
- Deleted empty system/README.md files
- Deleted documentation_gaps.md (all gaps resolved) and redundant info/update.md

Sidebar / navbar:
- Removed all emojis from _sidebar.md (EN + RU) — fixes tree hierarchy rendering
- Removed flag emojis from _navbar.md language switcher
- Removed duplicate entries from the old "System Documentation" section
- Renamed section headers: "Development" -> "Framework Reference" + "Developer Guides"

Content fixes (modules.md EN + RU):
- Updated "Class naming" section: now documents XcVm\Module\{Pascal} namespaces
  (was "global PHP namespace — Until PHP namespaces are introduced")
- Updated main module class example to use namespace declaration + use statements
- Updated class naming in ModuleLoader description to FQN
- Replaced installCrontab() manual crontab instruction with getCronEntries() API
- Updated enable/disable section: added ModuleState enum table, updated config examples
- Fixed FAQ answer for disabling a module

Content fixes (bootstrap-contexts.md EN + RU):
- Replaced CONTEXT_* string constants with BootContext enum cases throughout
- Updated boot() signature: string $context -> BootContext $context
- Updated getContext() return type: ?string -> ?BootContext

New framework reference docs (EN + RU):
- docs/{en,ru}/development/event-system.md: EventDispatcher singleton bridge,
  #[ListensTo] attribute, getEventSubscribers() array API, stoppable events,
  built-in event catalog, custom event authoring, ListensTo attribute reference
- docs/{en,ru}/development/exception-hierarchy.md: full XcVmException tree,
  container vs module subtrees, PSR-11 compliance notes, catch-by-subsystem examples
2026-06-15 18:27:23 +03:00

321 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Валидация и санитизация ввода
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-комментариев | `<!--` становится `&#60;&#33;--`, `-->` становится `--&#62;` |
| Нейтрализация script-тегов | `<script` (регистронезависимо) становится `&#60;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()` перед бизнес-логикой |