Интеграция Битрикс24 с внешней системой без API
Битрикс24
Битрикс24

Интеграция Битрикс24 с внешней системой без API

Время от времени к нам обращаются за помощью по интеграции Битрикс24 с внешними системами. Чаще всего разработка таких интеграций не вызывает особых трудностей, если система известная, имеет известный API и на всякий случай техподдержку.

Становится интереснее, когда внешняя система — какой-нибудь самописный
ERP-софт, разработчиков которого в последний раз видели 10 лет назад. Как правило, в таких случаях про API и документацию речи не идёт. Отказаться от интеграции тоже нельзя, потому часть критически важных для бизнеса процессов оказываются завязаны на этой внешней системе.

Ситуация

Например, так было у одного из наших клиентов, про которого мы рассказывали в статье про разработку чат-ботов. Нужно было передавать информацию о внесении платежа из Битрикс24 во внешнюю учетную систему, где они хранились, но при этом было несколько проблем.

  1. Единственный способ работать с системой — через интерфейс в браузере. Никакого API в ней не предусмотрено.
  2. Разработчики системы в компании клиента уже давно не работают и обратиться с вопросом по доработке API не к кому.
  3. Мы могли бы доработать систему самостоятельно, но она написана на ASP.NET, с которым мы не работаем.
  4. При заполнении формы создания платежа система по какой-то неведомой логике дозаполняет часть полей перед отправкой. Чтобы разобраться, как это происходит — нужно реверсить минифицированный JS, потому что где лежат исходники тоже никто не знает.
  5. С учетом предыдущего пункта подделать все нужные 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.

Игорь Денисенко
Игорь Денисенко
Технический директор БизнесПрофи

Статьи по теме

Как выбрать хостинг для Битрикс24
«1C-Битрикс24» — универсальная и многофункциональная система управления данными, разработанная с высоким уровнем программирования относительно систем хранения данных, что делает ее требовательной к хостингу, на котором должен работать портал.
Подробнее
Разработка чат-бота для Битрикс24 — снижаем рутину
Расскажем, как автоматизировать рутину и сэкономить время сотрудников с помощью специализированного чат-бота, заменяющего ручную работу.
Подробнее