# Валидация и санитизация ввода 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-тегов | ` 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()` перед бизнес-логикой |