Пишем обработчик для запрета на изменение поля в компании Коробка Битрикс24

Начнем с того, что все кастомные обработчики находятся в папке /local/php_interface/[название папки с обработчиками], можно назвать include или custom-init. В папке /local/php_interface/ должен быть файл init.php, если его там нет, создайте. В этом файле нужно написать следующий код.

<?php

if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die();
foreach (glob("{$_SERVER["DOCUMENT_ROOT"]}/local/php_interface/custom-init/*.php") as $customInit) { // путь до ваших обработчиков
    require_once $customInit;
    
}

/*.php — этим значением мы говорим, что будут запускаться все обработчики в папке custom-init. Итого мы создали универсальный init и теперь нам нужно просто писать обработчики и складывать их в папку custom-init для их запуска.

Теперь давайте напишем код обработчика который будет запрещать редактировать конкретное поле всем пользователям, кроме указанных, как админы и так же администраторам портала. Также обработчик будет писать логи и сохранять их в папку /upload. Это позволит нам отследить все изменения, кто и когда менял или пытался поменять это поле. Ниже будет представлен полный код, можете его скопировать себе в коробку, отредактировать Константы класса и пользоваться.

<?php
// Подключение необходимых классов Bitrix
use Bitrix\Main\EventManager; // Для работы с событиями
use Bitrix\Main\Localization\Loc; // Для локализации сообщений
use Bitrix\Main\IO\Directory; // Для работы с директориями
use Bitrix\Main\IO\File; // Для работы с файлами
use Bitrix\Crm\CompanyTable; // Для работы с таблицей компаний CRM
use Bitrix\Main\Loader; // Для подключения модулей
use Bitrix\Main\UserTable; // Для работы с таблицей пользователей

// Загрузка языковых сообщений из текущего файла
Loc::loadMessages(__FILE__);

class CompanyFieldEditRestriction
{
    // Константы класса
    const ALLOWED_USER_IDS = [1, 5]; // ID пользователей с правами редактирования
    const PROTECTED_FIELD = 'COMPANY_TYPE'; // Защищаемое поле
    const LOG_DIR = '/upload/'; // Директория для логов
    const LOG_FILE = 'restriction.log'; // Файл логов
    const ERROR_MESSAGE = 'Изменение поля "%s" запрещено. Для изменения обратитесь к администратору.'; // Шаблон сообщения об ошибке
    const ERROR_TITLE = 'Ограничение прав'; // Заголовок ошибки

    /**
     * Инициализация обработчиков событий
     */
    public static function init()
    {
        $eventManager = EventManager::getInstance();
        
        // Обработчик перед обновлением компании
        $eventManager->addEventHandlerCompatible(
            'crm', // Модуль CRM
            'OnBeforeCrmCompanyUpdate', // Событие перед обновлением
            [__CLASS__, 'onBeforeCompanyUpdate'] // Метод-обработчик
        );
        
        // Обработчик после добавления кнопок формы
        $eventManager->addEventHandlerCompatible(
            'crm', // Модуль CRM
            'OnAfterCrmCompanyFormButtonsAdd', // Событие после добавления кнопок
            [__CLASS__, 'onAfterFormButtonsAdd'] // Метод-обработчик
        );
    }

    /**
     * Логирование сообщений в файл
     * @param string $message - сообщение для записи в лог
     */
    protected static function log($message)
    {
        try {
            $logPath = $_SERVER['DOCUMENT_ROOT'].self::LOG_DIR;
            $logFile = $logPath.self::LOG_FILE;
            
            // Создаем директорию, если не существует
            if (!Directory::isDirectoryExists($logPath)) {
                Directory::createDirectory($logPath);
            }
            
            // Записываем сообщение с временной меткой
            $timestamp = date('Y-m-d H:i:s');
            File::putFileContents(
                $logFile,
                "[{$timestamp}] {$message}\n",
                FILE_APPEND
            );
        } catch (Exception $e) {
            // В случае ошибки пишем в системный лог Bitrix
            AddMessage2Log($e->getMessage(), __CLASS__);
        }
    }

    /**
     * Получение читаемого названия поля
     * @param string $fieldName - код поля
     * @return string - название поля
     */
    protected static function getFieldTitle($fieldName)
    {
        static $fieldTitles = null;
        
        if ($fieldTitles === null) {
            Loader::includeModule('crm'); // Подключаем модуль CRM
            
            // Массив соответствий кодов полей и их названий
            $fieldTitles = [
                'COMPANY_TYPE' => Loc::getMessage('COMPANY_TYPE_FIELD_TITLE') ?: 'Тип компании',
            ];
        }
        
        return $fieldTitles[$fieldName] ?? $fieldName; // Возвращаем название или код, если название не найдено
    }

    /**
     * Получение информации о пользователе (ФИО или логин)
     * @param int $userId - ID пользователя
     * @return string - информация о пользователе
     */
    protected static function getUserInfo($userId)
    {
        static $usersCache = []; // Кеш пользователей
        
        if (!isset($usersCache[$userId])) {
            // Получаем данные пользователя из БД
            $user = UserTable::getById($userId)->fetch();
            if ($user) {
                // Формируем ФИО или используем логин
                $name = trim($user['NAME'] . ' ' . $user['LAST_NAME']);
                $usersCache[$userId] = $name ?: $user['LOGIN'];
            } else {
                $usersCache[$userId] = "ID{$userId}";
            }
        }
        
        return $usersCache[$userId];
    }

    /**
     * Обработчик перед обновлением компании
     * @param array &$fields - массив полей компании
     * @return bool - разрешить (true) или запретить (false) обновление
     */
    public static function onBeforeCompanyUpdate(&$fields)
    {
        global $USER, $APPLICATION;
        
        // Получаем ID компании из разных возможных источников
        $id = $fields['ID'] ?? ($GLOBALS['ID'] ?? 0);
        if ($id <= 0) {
            return true; // Если ID невалидный, пропускаем
        }

        $userId = (int)$USER->GetID();
        $userInfo = self::getUserInfo($userId);
        $fieldTitle = self::getFieldTitle(self::PROTECTED_FIELD);
        
        // Логируем попытку изменения
        self::log("Попытка обновления компании ID {$id} пользователем {$userInfo}");

        // Проверяем права пользователя
        if (in_array($userId, self::ALLOWED_USER_IDS)) {
            return true; // Разрешаем для определенных пользователей
        }

        // Если защищаемое поле не изменяется
        if (!isset($fields[self::PROTECTED_FIELD])) {
            return true;
        }

        // Получаем текущие данные компании
        $company = CompanyTable::getById($id)->fetch();
        if (!$company || !isset($company[self::PROTECTED_FIELD])) {
            self::log("Ошибка: компания ID {$id} не найдена или поле '{$fieldTitle}' отсутствует");
            return true;
        }

        // Сравниваем старые и новые значения
        $oldValue = $company[self::PROTECTED_FIELD];
        $newValue = $fields[self::PROTECTED_FIELD];

        if ($oldValue == $newValue) {
            return true; // Если значение не изменилось
        }

        // Логируем попытку изменения
        self::log("Попытка изменения поля '{$fieldTitle}' в компании {$id}: '{$oldValue}' → '{$newValue}' (пользователь: {$userInfo})");

        // Формируем и выводим сообщение об ошибке
        $errorMessage = sprintf(self::ERROR_MESSAGE, $fieldTitle);
        $fields['RESULT_MESSAGE'] = $errorMessage; // Сообщение для формы
        $APPLICATION->ThrowException($errorMessage, self::ERROR_TITLE); // Исключение
        
        return false; // Запрещаем сохранение
    }

    /**
     * Обработчик формы редактирования компании
     * Делает защищаемое поле недоступным для редактирования
     */
    public static function onAfterFormButtonsAdd($guid, &$buttons, &$form, &$card)
    {
        global $USER;
        
        $userId = (int)$USER->GetID();
        $userInfo = self::getUserInfo($userId);
        
        // Проверяем права пользователя
        if (!in_array($userId, self::ALLOWED_USER_IDS)) {
            if (isset($form->arFields[self::PROTECTED_FIELD])) {
                $fieldTitle = self::getFieldTitle(self::PROTECTED_FIELD);
                $errorMessage = sprintf(self::ERROR_MESSAGE, $fieldTitle);
                
                // Модифицируем поле формы:
                $form->arFields[self::PROTECTED_FIELD]['SETTINGS']['READONLY'] = true; // Только чтение
                $form->arFields[self::PROTECTED_FIELD]['SETTINGS']['DISABLED'] = true; // Отключено
                $form->arFields[self::PROTECTED_FIELD]['ATTRIBUTES']['title'] = $errorMessage; // Подсказка
                $form->arFields[self::PROTECTED_FIELD]['ATTRIBUTES']['style'] = 'background-color:#f9f9f9; color:#888; cursor:not-allowed;'; // Стили
                $form->arFields[self::PROTECTED_FIELD]['ATTRIBUTES']['data-restricted'] = 'true'; // Маркер
                
                // Логируем открытие формы
                self::log("Пользователь {$userInfo} открыл форму с ограниченными правами (поле '{$fieldTitle}')");
            }
        }
    }
}

// Языковые сообщения (можно вынести в отдельный файл)
Loc::loadMessages(__FILE__);
$MESS['COMPANY_TYPE_FIELD_TITLE'] = 'Тип компании';

// Инициализация обработчиков событий
CompanyFieldEditRestriction::init();

Основные блоки и их функции:

  1. Константы класса:
    • Хранят настройки: ID разрешенных пользователей, защищаемое поле, пути для логов, тексты сообщений
  2. Метод init():
    • Регистрирует обработчики событий Bitrix
    • Связывает события с методами класса
  3. Метод log():
    • Записывает сообщения в лог-файл
    • Автоматически создает директорию для логов при необходимости
    • Обрабатывает ошибки записи
  4. Метод getFieldTitle():
    • Преобразует код поля в читаемое название
    • Использует локализацию через Loc::getMessage()
    • Кеширует результаты для производительности
  5. Метод getUserInfo():
    • Получает ФИО пользователя по ID
    • Использует кеширование для минимизации запросов к БД
    • Возвращает логин, если ФИО не указано
  6. Метод onBeforeCompanyUpdate():
    • Проверяет попытки изменения защищаемого поля
    • Сравнивает старые и новые значения
    • Логирует все попытки изменений
    • Блокирует сохранение для неавторизованных пользователей
    • Выводит сообщение об ошибке
  7. Метод onAfterFormButtonsAdd():
    • Модифицирует форму редактирования компании
    • Делает защищаемое поле недоступным для редактирования
    • Добавляет подсказку при наведении
    • Применяет CSS-стили для визуального выделения
  8. Языковые сообщения:
    • Определяют читаемые названия полей
    • Могут быть вынесены в отдельный файл локализации
  9. Инициализация:
    • Запускает весь механизм через вызов CompanyFieldEditRestriction::init()

Теперь давайте посмотрим, как это работает…

При попытке сменить значение в поле «Тип компании» система вывела мне сообщение «Изменение поля «Тип компании» запрещено. Для изменения обратитесь к администратору.«