Files
XC_VM/docs/ru/guides/input-validation.md
T
Divarion-D 76844fef11 docs: restructure, fix PSR-4 drift, and unify en/ru
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.
2026-06-26 15:56:15 +03:00

17 KiB
Raw Blame History

Валидация и санитизация ввода

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()

InputValidator::validate(string $rAction, array $rData): bool

Проверяет наличие минимально требуемых полей для заданного действия. Возвращает true, если данные приемлемы, и false, если обязательные поля отсутствуют или некорректны. Контроллеры должны вызывать её перед передачей данных в слой сервисов/репозиториев.

if (!InputValidator::validate($action, $data)) {
    // отклонить с ошибкой валидации
}

validateOrFail()

InputValidator::validateOrFail(string $rAction, array $rData): ?array

Удобная обёртка над validate(). Возвращает null, если данные валидны, или массив с ошибкой, если валидация не прошла:

$error = InputValidator::validateOrFail($action, $data);
if ($error !== null) {
    // $error = ['status' => STATUS_INVALID_INPUT, 'data' => $data]
    return $error;
}

confirmIDs($ids)

InputValidator::confirmIDs(array $ids): array

Фильтрует массив, оставляя только положительные целочисленные ID. Любое значение, где intval($id) <= 0, отбрасывается. Используется широко по кодовой базе (30+ мест вызова) везде, где пользовательские списки ID должны быть санитизированы перед запросами к базе.

$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:

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() перед бизнес-логикой