'right', 'launcher_label' => 'Chat with us', 'desktop_offset' => 24, 'mobile_offset' => 16, 'include_pages' => '', 'exclude_pages' => '', 'devices' => 'all', ]; public static function normalize(array $input): array { $out = array_intersect_key($input, self::DEFAULTS) + self::DEFAULTS; if (!in_array($out['side'], ['left', 'right'], true) || !in_array($out['devices'], ['all', 'desktop', 'mobile'], true)) { throw new \InvalidArgumentException('Invalid display rules.'); } $label = trim((string) $out['launcher_label']); if ($label === '' || !mb_check_encoding($label, 'UTF-8') || mb_strlen($label) > 60 || preg_match('/[<>\x00-\x1f]/', $label)) { throw new \InvalidArgumentException('Launcher label must be plain text, 1–60 characters.'); } $out['launcher_label'] = $label; foreach (['desktop_offset', 'mobile_offset'] as $key) { $value = filter_var($out[$key], FILTER_VALIDATE_INT); if ($value === false || $value < 8 || $value > 160) { throw new \InvalidArgumentException('Offsets must be 8–160 pixels.'); } $out[$key] = $value; } foreach (['include_pages', 'exclude_pages'] as $key) { $value = trim((string) $out[$key]); if (strlen($value) > 1000 || ($value !== '' && !preg_match('/^[1-9][0-9]*(\s*,\s*[1-9][0-9]*)*$/D', $value))) { throw new \InvalidArgumentException('Use comma-separated positive page IDs.'); } $out[$key] = $value === '' ? '' : implode(',', array_unique(array_map('intval', preg_split('/\s*,\s*/', $value)))); } return $out; } public static function eligible(array $bot, int $pageId): bool { if ($bot['status'] !== 'active' || !$bot['is_public']) { return false; } $look = BotRepository::appearance($bot); if ($look['placement'] !== 'floating') { return false; } $includes = $look['include_pages'] === '' ? [] : explode(',', $look['include_pages']); $excludes = $look['exclude_pages'] === '' ? [] : explode(',', $look['exclude_pages']); return !in_array((string) $pageId, $excludes, true) && ($includes === [] || in_array((string) $pageId, $includes, true)); } }