381 lines
18 KiB
PHP
381 lines
18 KiB
PHP
<?php
|
||
/**
|
||
* Helper-функции для маскирования контактных данных клиента.
|
||
*
|
||
* Применяются во всех точках выдачи phone/email клиента из бэкенда, если
|
||
* у текущего пользователя включено право `user_permissions.hide_client_contacts`.
|
||
*
|
||
* Подключается из config_common.php после engine/functions.php.
|
||
*/
|
||
|
||
/**
|
||
* FEATURE FLAG: список users.company_id, для которых фича "скрытие контактов" активна.
|
||
*
|
||
* - массив целых ID → фича работает ТОЛЬКО для юзеров с company_id из списка
|
||
* - null → фича работает для ВСЕХ агентств (полная раскатка / GA)
|
||
*
|
||
* Чтобы перейти в GA — поменять на null и сбросить opcache. БД и история
|
||
* не затрагиваются: у агентств вне allowlist маска просто не активируется.
|
||
*/
|
||
if (!defined('HIDE_CLIENT_CONTACTS_ALLOWED_COMPANIES')) {
|
||
define('HIDE_CLIENT_CONTACTS_ALLOWED_COMPANIES', [4, 117, 23015]);
|
||
}
|
||
|
||
if (!function_exists('hide_client_contacts_company_allowed')) {
|
||
/**
|
||
* Проверяет, разрешена ли фича hide_client_contacts для company_id.
|
||
* Если allowlist === null — разрешено всем (GA). Иначе — только если company_id в списке.
|
||
*
|
||
* @param int|string|null $companyId
|
||
* @return bool
|
||
*/
|
||
function hide_client_contacts_company_allowed($companyId) {
|
||
$allowlist = defined('HIDE_CLIENT_CONTACTS_ALLOWED_COMPANIES')
|
||
? HIDE_CLIENT_CONTACTS_ALLOWED_COMPANIES
|
||
: null;
|
||
if ($allowlist === null) return true; // GA / константа не определена → открыто
|
||
if (empty($companyId)) return false; // company_id неизвестен → закрыто (fail-closed)
|
||
return in_array((int)$companyId, (array)$allowlist, true);
|
||
}
|
||
}
|
||
|
||
if (!function_exists('mask_client_phone')) {
|
||
/**
|
||
* Маскирует телефон клиента, сохраняя ПЕРВУЮ цифру после "+" (код страны/первая цифра кода).
|
||
* Примеры:
|
||
* "79991234567" → "+7 *** *** ** **" (российский)
|
||
* "+12025551234" → "+1 *** *** ** **" (США/Канада)
|
||
* "+491701234567" → "+4 *** *** ** **" (Германия)
|
||
* "+861812345678" → "+8 *** *** ** **" (Китай)
|
||
* Если в строке вообще нет цифр — возвращаем "+ *** *** ** **".
|
||
*
|
||
* @param string|null $phone
|
||
* @return string|null Маска, либо исходное значение если оно пустое.
|
||
*/
|
||
function mask_client_phone($phone) {
|
||
if (empty($phone)) return $phone;
|
||
$digits = preg_replace('/[^0-9]/', '', $phone);
|
||
if ($digits === '') return '+ *** *** ** **';
|
||
return '+' . substr($digits, 0, 1) . ' *** *** ** **';
|
||
}
|
||
}
|
||
|
||
if (!function_exists('mask_client_email')) {
|
||
/**
|
||
* Маскирует email клиента.
|
||
* Пример: "ivan@mail.ru" → "i***@m***.ru".
|
||
* Если email невалидный — возвращает "***@***".
|
||
*
|
||
* @param string|null $email
|
||
* @return string|null
|
||
*/
|
||
function mask_client_email($email) {
|
||
if (empty($email)) return $email;
|
||
if (!preg_match('/^(.)[^@]*@(.)[^.]*(\..+)$/u', $email, $m)) {
|
||
return '***@***';
|
||
}
|
||
return $m[1] . '***@' . $m[2] . '***' . $m[3];
|
||
}
|
||
}
|
||
|
||
if (!function_exists('should_hide_contacts_for_current_user')) {
|
||
/**
|
||
* Должны ли скрываться контакты клиентов для текущего пользователя (из сессии).
|
||
* Кэшируется через static в рамках одного PHP-запроса.
|
||
*
|
||
* Логика (две проверки идут И):
|
||
* 1) Гейт раскатки: company_id текущего юзера должен быть в HIDE_CLIENT_CONTACTS_ALLOWED_COMPANIES
|
||
* (либо allowlist = null → пропускаем всех).
|
||
* 2) Право: user_permissions.hide_client_contacts = 1.
|
||
*
|
||
* Если первое условие не выполнено — даже при включённом праве маска не активируется.
|
||
*
|
||
* @return bool
|
||
*/
|
||
function should_hide_contacts_for_current_user() {
|
||
static $cached = null;
|
||
if ($cached === null) {
|
||
if (empty($_SESSION['id'])) {
|
||
$cached = false;
|
||
} else {
|
||
// Гейт раскатки + право берём одним запросом, чтобы не делать два хождения в БД.
|
||
$userId = (int)$_SESSION['id'];
|
||
$sql = "SELECT u.company_id, p.hide_client_contacts
|
||
FROM users u
|
||
LEFT JOIN user_permissions p ON p.user_id = u.id
|
||
WHERE u.id = $userId
|
||
LIMIT 1";
|
||
$res = mysql_query($sql);
|
||
$row = $res ? mysql_fetch_assoc($res) : null;
|
||
if (!$row) {
|
||
$cached = false;
|
||
} elseif (!hide_client_contacts_company_allowed($row['company_id'])) {
|
||
// Агентство не в allowlist — фича для него выключена даже если право взведено в БД.
|
||
$cached = false;
|
||
} else {
|
||
$cached = !empty($row['hide_client_contacts']);
|
||
}
|
||
}
|
||
}
|
||
return $cached;
|
||
}
|
||
}
|
||
|
||
if (!function_exists('should_hide_contacts_for_user_id')) {
|
||
/**
|
||
* Должны ли скрываться контакты для указанного user_id.
|
||
* Для использования в cron-скриптах, где нет сессии.
|
||
* Кэшируется per-userId в рамках запроса.
|
||
*
|
||
* Логика та же что и в should_hide_contacts_for_current_user — гейт по
|
||
* users.company_id + проверка user_permissions.hide_client_contacts.
|
||
*
|
||
* @param int|string|null $userId
|
||
* @return bool
|
||
*/
|
||
function should_hide_contacts_for_user_id($userId) {
|
||
if (empty($userId)) return false;
|
||
static $cache = [];
|
||
$userId = (int)$userId;
|
||
if (!isset($cache[$userId])) {
|
||
$sql = "SELECT u.company_id, p.hide_client_contacts
|
||
FROM users u
|
||
LEFT JOIN user_permissions p ON p.user_id = u.id
|
||
WHERE u.id = $userId
|
||
LIMIT 1";
|
||
$res = mysql_query($sql);
|
||
$row = $res ? mysql_fetch_assoc($res) : null;
|
||
if (!$row) {
|
||
$cache[$userId] = false;
|
||
} elseif (!hide_client_contacts_company_allowed($row['company_id'])) {
|
||
$cache[$userId] = false;
|
||
} else {
|
||
$cache[$userId] = !empty($row['hide_client_contacts']);
|
||
}
|
||
}
|
||
return $cache[$userId];
|
||
}
|
||
}
|
||
|
||
if (!function_exists('mask_fio_if_contains_phone')) {
|
||
/**
|
||
* Если fio имеет вид "Новый клиент …+7…" (с возможным оператором между,
|
||
* напр. "Новый клиент Мегафон +79...") и у текущего пользователя включено
|
||
* скрытие контактов — возвращает "Новый клиент" (без номера и оператора).
|
||
*
|
||
* Если в fio есть произвольный embedded-номер (6+ подряд цифр) — тоже
|
||
* вырезаем его, оставляя текст до числа.
|
||
*/
|
||
function mask_fio_if_contains_phone($fio) {
|
||
if (empty($fio)) return $fio;
|
||
if (!should_hide_contacts_for_current_user()) return $fio;
|
||
// Базовый кейс из CallEvents: "Новый клиент [оператор] +79..."
|
||
if (preg_match('/^Новый\s+клиент\b.*?\d{6,}/u', $fio)) {
|
||
return 'Новый клиент';
|
||
}
|
||
// Универсальный: вырезаем длинный набор цифр (телефон) из произвольного fio.
|
||
if (preg_match('/\d{6,}/', $fio)) {
|
||
$cleaned = preg_replace('/[+\(\)\-\s]*\d[\d\-\(\)\s]{5,}/u', '', $fio);
|
||
$cleaned = trim($cleaned);
|
||
if ($cleaned !== '') return $cleaned;
|
||
}
|
||
return $fio;
|
||
}
|
||
}
|
||
|
||
if (!function_exists('mask_fio_for_user_id')) {
|
||
/**
|
||
* Per-user-id вариант mask_fio_if_contains_phone() — для cron-скриптов,
|
||
* где сессии нет, а получатель уведомления известен по user_id.
|
||
*
|
||
* Если у указанного $userId включено скрытие контактов и в строке fio
|
||
* есть встроенный телефон (e.g. "Новый клиент +79..." или вообще
|
||
* любой 6+ значный набор цифр) — возвращает fio без номера.
|
||
*/
|
||
function mask_fio_for_user_id($fio, $userId) {
|
||
if (empty($fio)) return $fio;
|
||
if (!should_hide_contacts_for_user_id($userId)) return $fio;
|
||
if (preg_match('/^Новый\s+клиент\b.*?\d{6,}/u', $fio)) {
|
||
return 'Новый клиент';
|
||
}
|
||
if (preg_match('/\d{6,}/', $fio)) {
|
||
$cleaned = preg_replace('/[+\(\)\-\s]*\d[\d\-\(\)\s]{5,}/u', '', $fio);
|
||
$cleaned = trim($cleaned);
|
||
if ($cleaned !== '') return $cleaned;
|
||
}
|
||
return $fio;
|
||
}
|
||
}
|
||
|
||
if (!function_exists('mask_client_contacts_inplace')) {
|
||
/**
|
||
* Маскирует контактные данные клиента в assoc-массиве (по ссылке).
|
||
* Обрабатывает: phone, email, phones (массив), phones_dop (массив),
|
||
* phone_email (склейка для tooltip), fio (если содержит номер).
|
||
* Если права на скрытие нет — ничего не делает.
|
||
*
|
||
* @param array $client Массив клиента, изменяется по ссылке.
|
||
*/
|
||
function mask_client_contacts_inplace(array &$client) {
|
||
if (!should_hide_contacts_for_current_user()) return;
|
||
|
||
if (isset($client['phone'])) {
|
||
$client['phone'] = mask_client_phone($client['phone']);
|
||
}
|
||
if (isset($client['email'])) {
|
||
$client['email'] = mask_client_email($client['email']);
|
||
}
|
||
// Признак «зарубежный телефон» — это метаданные о номере; раз сам номер
|
||
// скрыт, не отдаём и его «национальность». Чтобы переключатель в формах
|
||
// не показывал реальное состояние, форсим 0.
|
||
if (isset($client['national_phone'])) {
|
||
$client['national_phone'] = 0;
|
||
}
|
||
if (isset($client['phones']) && is_array($client['phones'])) {
|
||
foreach ($client['phones'] as &$p) {
|
||
$p = mask_client_phone($p);
|
||
}
|
||
unset($p);
|
||
}
|
||
if (isset($client['phones_dop']) && is_array($client['phones_dop'])) {
|
||
foreach ($client['phones_dop'] as &$p) {
|
||
$p = mask_client_phone($p);
|
||
}
|
||
unset($p);
|
||
}
|
||
// ПК хранит дополнительные телефоны в поле clients.phone2 как JSON-строку
|
||
// (например, ["+79991234567","+12025551234"]). Маскируем каждый элемент,
|
||
// сохраняя ту же JSON-строку для совместимости с JS, который делает JSON.parse.
|
||
if (isset($client['phone2']) && is_string($client['phone2'])
|
||
&& $client['phone2'] !== '' && $client['phone2'] !== 'null'
|
||
) {
|
||
$decoded = json_decode($client['phone2'], true);
|
||
if (is_array($decoded) && !empty($decoded)) {
|
||
$masked = array_map('mask_client_phone', $decoded);
|
||
$client['phone2'] = json_encode($masked);
|
||
}
|
||
}
|
||
if (isset($client['phone_email'])) {
|
||
$parts = [];
|
||
if (!empty($client['phone'])) $parts[] = $client['phone'];
|
||
if (!empty($client['email'])) $parts[] = $client['email'];
|
||
$client['phone_email'] = implode(', ', $parts);
|
||
}
|
||
if (isset($client['fio'])) {
|
||
$client['fio'] = mask_fio_if_contains_phone($client['fio']);
|
||
}
|
||
// В API клиент часто отдаётся с ключом 'name' (алиас fio) — маскируем тоже.
|
||
if (isset($client['name'])) {
|
||
$client['name'] = mask_fio_if_contains_phone($client['name']);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!function_exists('mask_text_for_hide_user')) {
|
||
/**
|
||
* Маскирует исторические события (events_clients.dop_text), которые содержат
|
||
* фразы вида "Изменен телефон клиента с «+79...» на «+79...»" или
|
||
* "Изменен email клиента с «mail@x.ru» на «...»".
|
||
*
|
||
* Контекстная маскировка: ищет в строке маркеры "телефон"/"email"/"почта" и
|
||
* маскирует значения внутри «...» только для тех строк. Бюджет/ФИО/прочие
|
||
* поля с цифрами остаются нетронутыми.
|
||
*
|
||
* Безопасно при повторном применении: уже замаскированные значения (со
|
||
* звёздочками внутри) пропускаются без изменений.
|
||
*
|
||
* @param string $text
|
||
* @return string
|
||
*/
|
||
function mask_text_for_hide_user($text) {
|
||
if (empty($text)) return $text;
|
||
if (!should_hide_contacts_for_current_user()) return $text;
|
||
|
||
// dop_text в БД пишется с HTML-entity-кавычками "« … »"
|
||
// (см. Clients::addEdit). В тексте «телефон» и «email» — наоборот,
|
||
// обычные UTF-8 символы. Декодируем entities в самом начале, чтобы
|
||
// regex ниже работал и с прежними записями (entity-форма), и с
|
||
// потенциальными будущими (с обычными « »).
|
||
$text = html_entity_decode($text, ENT_QUOTES, 'UTF-8');
|
||
|
||
$lines = preg_split('/(<br\s*\/?>)/i', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
|
||
foreach ($lines as &$line) {
|
||
if ($line === '' || preg_match('/^<br/i', $line)) continue;
|
||
$lower = mb_strtolower($line, 'UTF-8');
|
||
$isPhoneLine = (mb_strpos($lower, 'телефон') !== false);
|
||
$isEmailLine = (mb_strpos($lower, 'email') !== false) || (mb_strpos($lower, 'почта') !== false);
|
||
if ($isPhoneLine) {
|
||
$line = preg_replace_callback(
|
||
'/«([^»]*\d[^»]*)»/u',
|
||
function($m) {
|
||
if (strpos($m[1], '*') !== false) return $m[0];
|
||
return '«' . mask_client_phone($m[1]) . '»';
|
||
},
|
||
$line
|
||
);
|
||
}
|
||
if ($isEmailLine) {
|
||
$line = preg_replace_callback(
|
||
'/«([^»]*@[^»]*)»/u',
|
||
function($m) {
|
||
if (strpos($m[1], '*') !== false) return $m[0];
|
||
return '«' . mask_client_email($m[1]) . '»';
|
||
},
|
||
$line
|
||
);
|
||
}
|
||
}
|
||
unset($line);
|
||
return implode('', $lines);
|
||
}
|
||
}
|
||
|
||
if (!function_exists('mask_phone_value_if_needed')) {
|
||
/**
|
||
* Условная маскировка одного телефона (для скаляров вне массивов).
|
||
* Применяется в местах, где phone клиента приходит без обёртки в массив.
|
||
*/
|
||
function mask_phone_value_if_needed($phone) {
|
||
if (should_hide_contacts_for_current_user()) {
|
||
return mask_client_phone($phone);
|
||
}
|
||
return $phone;
|
||
}
|
||
}
|
||
|
||
if (!function_exists('mask_email_value_if_needed')) {
|
||
/**
|
||
* Условная маскировка одного email (для скаляров вне массивов).
|
||
*/
|
||
function mask_email_value_if_needed($email) {
|
||
if (should_hide_contacts_for_current_user()) {
|
||
return mask_client_email($email);
|
||
}
|
||
return $email;
|
||
}
|
||
}
|
||
|
||
if (!function_exists('mask_phone_for_user_id')) {
|
||
/**
|
||
* Маскировка телефона для конкретного user_id (для cron-скриптов).
|
||
*/
|
||
function mask_phone_for_user_id($phone, $userId) {
|
||
if (should_hide_contacts_for_user_id($userId)) {
|
||
return mask_client_phone($phone);
|
||
}
|
||
return $phone;
|
||
}
|
||
}
|
||
|
||
if (!function_exists('mask_email_for_user_id')) {
|
||
/**
|
||
* Маскировка email для конкретного user_id (для cron-скриптов).
|
||
*/
|
||
function mask_email_for_user_id($email, $userId) {
|
||
if (should_hide_contacts_for_user_id($userId)) {
|
||
return mask_client_email($email);
|
||
}
|
||
return $email;
|
||
}
|
||
}
|