Joywork/engine/helpers/contacts.php
2026-05-22 21:21:54 +03:00

381 lines
18 KiB
PHP
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.

<?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-кавычками "&laquo; … &raquo;"
// (см. 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;
}
}