Интеграция Битрикс24 с внешней системой без API
Ситуация
Например, так было у одного из наших клиентов, про которого мы рассказывали в статье про разработку чат-ботов. Нужно было передавать информацию о внесении платежа из Битрикс24 во внешнюю учетную систему, где они хранились, но при этом было несколько проблем.- Единственный способ работать с системой — через интерфейс в браузере. Никакого API в ней не предусмотрено.
- Разработчики системы в компании клиента уже давно не работают и обратиться с вопросом по доработке API не к кому.
- Мы могли бы доработать систему самостоятельно, но она написана на ASP.NET, с которым мы не работаем.
- При заполнении формы создания платежа система по какой-то неведомой логике дозаполняет часть полей перед отправкой. Чтобы разобраться, как это происходит — нужно реверсить минифицированный JS, потому что где лежат исходники тоже никто не знает.
- С учетом предыдущего пункта подделать все нужные http-запросы curl’ом тоже не представляется возможным: мы не знаем, как рассчитать некоторые требуемые поля.
Интеграция
В таком случае одним из вариантов решения проблемы будет полная имитация действий пользователя в браузере на сервере с Битрикс24. В качестве инструмента будем использовать Puppeteer, который является обёрткой над всем известным Google Chrome.Чтобы поставить библиотеку пишем так:
$ npm install --save puppeteerДля работы браузера нужен X-сервер. На линуксовых серверах без графического интерфейса X-сервера чаще всего нет. Нужно его поставить, список пакетов берём отсюда. Для веб-окружения Битрикс24 зависимости выглядят так:
$ yum -y install pango.x86_64 libXcomposite.x86_64 libXcursor.x86_64 libXdamage.x86_64 libXext.x86_64 libXi.x86_64 libXtst.x86_64 cups-libs.x86_64 libXScrnSaver.x86_64 libXrandr.x86_64 GConf2.x86_64 alsa-lib.x86_64 atk.x86_64 gtk3.x86_64 ipa-gothic-fonts xorg-x11-fonts-100dpi xorg-x11-fonts-75dpi xorg-x11-utils xorg-x11-fonts-cyrillic xorg-x11-fonts-Type1 xorg-x11-fonts-miscСразу после установки можно попробовать запустить тестовый скрипт из документации библиотеки:
const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto('https://example.com'); await page.screenshot({path: 'example.png'}); await browser.close(); })();Сохраняем скрипт в директории на сервере и запускаем на выполнение:
$ node example.jsЕсли всё получилось, в директории со скриптом должен появится файл example.png со скрином открытой в браузере страницы. Да, дебажить в дальнейшем придётся скриншотами :)
paybot.php
Нам нужно будет вызывать эту команду из Битрикс24, который написан на PHP. Сделаем это так:protected static function doCreatePayment($clientId, $amount, $comment = '', $recipientId = '') { // Передавать данные будем через переменные окружения $env = [ // Поля платежа 'CLIENT_ID' => $clientId, 'AMOUNT' => $amount, 'COMMENT' => Loc::getMessage('MAGNIFICO_PAYBOT_DEFAULT_COMMENT').($comment ? ' ('.$comment.')' : ''), 'RECIPIENT_ID' => $recipientId, // Пути к бинарникам хрома и NodeJS берём из настроек модуля 'CHROME_BIN' => \Bitrix\Main\Config\Option::get('magnifico.paybot', 'chrome_bin'), 'NODEJS_BIN' => \Bitrix\Main\Config\Option::get('magnifico.paybot', 'nodejs_bin'), // Логин и пароль для авторизации в внешней системе 'AUTH_USERNAME' => \Bitrix\Main\Config\Option::get('magnifico.paybot', 'auth_username'), 'AUTH_PASSWORD' => \Bitrix\Main\Config\Option::get('magnifico.paybot', 'auth_password'), ]; // Проверяем существование хрома if (!is_executable($env['CHROME_BIN'])) { throw new \Exception(Loc::getMessage('MAGNIFICO_PAYBOT_CHROME_ERROR')); } // То же самое для NodeJS if (!is_executable($env['NODEJS_BIN'])) { throw new \Exception(Loc::getMessage('MAGNIFICO_PAYBOT_NODEJS_ERROR')); } // Объявляем дескрипторы для чтения результатов вызова с stdout/stderr $descriptors = [ 1 => ['pipe', 'w'], 2 => ['pipe', 'w'], ]; // Запускаем подготовленный скрипт $process = proc_open($env['NODEJS_BIN'] . ' ' . 'paybot.js', $descriptors, $pipes, Loader::getLocal('/modules/'.static::MODULE_ID.'/browser'), $env); // Выходим, если не получилось запустит if (false === $process) { throw new \Exception(Loc::getMessage('MAGNIFICO_PAYBOT_PROCESS_ERROR')); } // Читаем содержимое объявленных ранее дескрипторов list($stdout, $stderr) = [stream_get_contents($pipes[1]), stream_get_contents($pipes[2])]; AddMessage2Log(['stdout' => $stdout, 'stderr' => $stderr], static::MODULE_ID); // Если код возврата ненулевой - была ошибка, и в stderr будет её текст if (0 !== proc_close($process)) { throw new \Exception(':!: '.$stderr); } // Тотальный успех return true; }Хотелось бы, чтобы в один момент времени на сервере было запущено не более, чем один инстанс хрома. В противном случае, если одновременно придёт много запросов на создание платежа - может закончиться оперативная память и сервер ляжет. Чтобы этого не случилось - реализуем простейший мутекс через механизм блокировок в MySQL.
public static function createPayment($clientId, $amount, $comment = '', $recipientId = '') { $connection = \Bitrix\Main\Application::getInstance()->getConnection(); $connection->queryExecute('SELECT GET_LOCK("'.__CLASS__.'", 60)'); $success = static::doCreatePayment($clientId, $amount, $comment, $recipientId); $connection->queryExecute('SELECT RELEASE_LOCK("'.__CLASS__.'")'); return $success; }Теперь вызовом createPayment можно создавать платежи. Со стороны PHP — остается только добавить в админке страницу настроек модуля:

paybot.js
Весь скрипт умещается в одном файле. Нам достаточно одного try .. catch, чтобы любая возникшая ошибка просто уходила бы в stderr и дальше обрабатывалась в PHP:const puppeteer = require('puppeteer'); (async () => { try { // here be dragons } catch (err) { console.error('[ERROR]', err); process.exit(1); } process.exit(0); })();Перед началом работы нужно запустить браузер:
console.log('[DEBUG] Launching browser...'); const browser = await puppeteer.launch({executablePath: process.env['CHROME_BIN'], args: ['--no-sandbox', '--disable-setuid-sandbox']});После чего открыть вкладку в созданном браузере:
console.log('[DEBUG] Creating new page...'); const page = await browser.newPage();Когда в системе возникает какая-либо ошибка, она по старинке выдаст её алертом и скрипт наглухо зависнет, поэтому молча соглашаемся со всеми алертами:
console.log('[DEBUG] Disabling javascript alerts...'); page.on('dialog', async (dialog) => { await dialog.accept(); });Авторизуемся в системе, вводя полученный из PHP логин и пароль. Наличие ошибки авторизации определяем по специфической строке в теле страницы.
console.log('[DEBUG] Opening authorization page...'); await page.goto('http://example.com/office/login.asp'); console.log('[DEBUG] Enter credentials...'); await page.evaluate((env) => { document.querySelector('input[name="CODE"]').value = env['AUTH_USERNAME']; document.querySelector('input[name="PASSWORD"]').value = env['AUTH_PASSWORD']; document.querySelector('input[type="submit"]').click(); }, process.env); console.log('[DEBUG] Waiting for authorization...'); await page.waitForNavigation({waitUntil: 'load'}); if ((await page.content()).search('Неправильные пароль или код') > -1) { throw new Error('Ошибка авторизации'); }Переходим на форму создания платежа и также заполняем её данными из PHP:
console.log('[DEBUG] Opening payment form...'); await page.goto('http://example.com/office/operation.asp?CID=' + process.env['CLIENT_ID']); console.log('[DEBUG] Checking if client exists...'); if ((await page.content()).search('НЕТ ДОСТУПА ИЛИ НЕ ОПОЗНАН') > -1) { throw new Error('Клиент не существует'); } console.log('[DEBUG] Enter payment fields...'); await page.evaluate((env) => { if (env['AMOUNT']) { const realsum = document.querySelector('input[name="realsum"]'); realsum.value = env['AMOUNT']; ('function' === typeof realsum.onchange) && realsum.onchange(); } if (env['COMMENT']) { const comment = document.querySelector('input[name="comment"]'); comment.value = env['COMMENT']; ('function' === typeof comment.onchange) && comment.onchange(); } if (env['RECIPIENT_ID']) { const credit = document.querySelector('select[name="Credit"]'); credit.value = env['RECIPIENT_ID']; ('function' === typeof credit.onchange) && credit.onchange(); } document.querySelector('input[name="OK"]').click(); }, process.env); console.log('[DEBUG] Waiting for payment creation...'); await page.waitForNavigation({waitUntil: 'load'});И не забываем закрыть браузер:
console.log('[DEBUG] Closing browser...'); await browser.close();
Выводы
Нет ничего невозможного. Есть недостаточная степень необходимости.Подписывайтесь на наш Телеграмм канал Максимум из Битрикс24.
Статьи по теме
«1C-Битрикс24» — универсальная и многофункциональная система управления данными, разработанная с высоким уровнем программирования относительно систем хранения данных, что делает ее требовательной к хостингу, на котором должен работать портал.
Расскажем, как автоматизировать рутину и сэкономить время сотрудников с помощью специализированного чат-бота, заменяющего ручную работу.