Skip to content

Commit 1c2121c

Browse files
Alexey PortnovAlexey Portnov
authored andcommitted
Исправлены уязвимости виджета, WebSocket reconnect и cross-origin iframe
Widget (mpbx-connector.js, script.js): - Исправлен критический баг: contact ID вместо company ID - Устранены XSS-уязвимости в отображении имени контакта - Добавлена очистка ресурсов в destroy(), debounce на resize - Исправлен race condition при создании контакта - Добавлен encodeURIComponent для параметров поиска - Кнопка toggle tab: серый стиль, квадратная форма 32x32 WebRTC phone (sites/webrtc-phone/): - WebSocket reconnect с exponential backoff (1s→60s) - Исправлен checkConnection: канал 'pbx-events' вместо 'calls' - Cross-origin fallback chain: mikoPbxHost → frameElement.title → location.host - Защита saveSettings от undefined pbxHost AMI worker: - Null safety для массивов calls и activeChannels
1 parent 0b0184b commit 1c2121c

File tree

9 files changed

+496
-109
lines changed

9 files changed

+496
-109
lines changed

CLAUDE.md

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## КРИТИЧЕСКИ ВАЖНО: Запреты при разработке
6+
7+
**КАТЕГОРИЧЕСКИ ЗАПРЕЩЕНО:**
8+
1. **Изменять файлы вне директории модуля** - никогда не модифицировать файлы в `/offload/`, `/usr/www/src/`, или других системных директориях MikoPBX
9+
2. **Использовать rsync, cp -r или tar для установки модуля** - это может перезаписать системные файлы MikoPBX
10+
3. **Удалять директорию модуля целиком** (`rm -rf /storage/.../ModuleAmoCrm`) - это удалит базу данных модуля
11+
4. **Затирать `Lib/AmoCrmMainBase.php` при установке модуля** — этот файл содержит реальные OAuth-credentials (`CLIENT_ID`, `CLIENT_SECRET`, `REDIRECT_URL`), которые подставляются CI при сборке. В репозитории хранятся только плейсхолдеры (`%CLIENT_ID%` и т.д.). **Перед обновлением/установкой модуля на сервере необходимо сделать бэкап этого файла и восстановить его после установки.**
12+
13+
## Требования к коду
14+
15+
**Совместимость:**
16+
- Код должен быть совместим с **PHP 7.4** и **PHP 8.x**
17+
- Код должен работать с **Phalcon 4.x** и **Phalcon 5.x**
18+
19+
**Избегайте (только PHP 8+):**
20+
- `match` выражения → используйте `switch`
21+
- Union types `function foo(): int|string` → используйте PHPDoc
22+
- Named arguments `foo(name: $value)`
23+
- Constructor property promotion
24+
- Nullsafe operator `?->`
25+
- `str_contains()`, `str_starts_with()`, `str_ends_with()` → используйте `strpos() !== false`
26+
27+
## Project Overview
28+
29+
ModuleAmoCrm — модуль-расширение для MikoPBX, интегрирующий телефонную систему с AmoCRM. Синхронизирует звонки (CDR), контакты, сделки, компании и задачи между АТС и CRM.
30+
31+
- **Namespace**: `Modules\ModuleAmoCrm\` (PSR-4 от корня)
32+
- **Стек:** PHP 7.4+, Phalcon MVC, Beanstalk (очереди), Asterisk AMI
33+
- **Module ID**: `ModuleAmoCrm`
34+
35+
## Команды
36+
37+
```bash
38+
# Установка PHP-зависимостей
39+
composer install
40+
41+
# Проверка синтаксиса
42+
php -l <file.php>
43+
44+
# Фоновые воркеры (запускаются на PBX-системе)
45+
php bin/WorkerAmoCrmAMI.php # AMI-слушатель событий Asterisk
46+
php bin/AmoCdrDaemon.php # Синхронизация CDR
47+
php bin/ConnectorDb.php # IPC-диспетчер, все операции с БД
48+
php bin/WorkerAmoHTTP.php # HTTP-запросы к AmoCRM API
49+
php bin/SyncDaemon.php # Синхронизация контактов/компаний/сделок
50+
```
51+
52+
## Build & CI
53+
54+
GitHub Actions workflow (`.github/workflows/build.yml`) срабатывает на push в `master`/`develop` и использует reusable workflow из `mikopbx/.github-workflows`:
55+
```yaml
56+
jobs:
57+
build:
58+
uses: mikopbx/.github-workflows/.github/workflows/extension-publish.yml@master
59+
with:
60+
initial_version: "1.84"
61+
secrets: inherit
62+
```
63+
64+
Тестов в проекте нет (нет phpunit.xml, нет каталога tests/).
65+
66+
## Сборка и установка модуля
67+
68+
### Сборка архива (локально)
69+
70+
```bash
71+
cd /Volumes/DevDisk/apor/Developement/MikoPBX/Extensions/ModuleAmoCrm
72+
zip -r ../ModuleAmoCrm.zip . -x "*.git*" -x "*tasks.md*" -x "*.DS_Store*" -x "*CLAUDE.md*"
73+
```
74+
75+
### Установка через WorkerModuleInstaller (ЕДИНСТВЕННЫЙ РАЗРЕШЁННЫЙ СПОСОБ)
76+
77+
```bash
78+
# На сервере: создать settings.json
79+
cat > /tmp/settings.json << 'EOF'
80+
{
81+
"currentModuleDir": "/storage/usbdisk1/mikopbx/custom_modules/ModuleAmoCrm",
82+
"filePath": "/home/user/ModuleAmoCrm.zip",
83+
"uniqid": "ModuleAmoCrm"
84+
}
85+
EOF
86+
87+
# Установить модуль (сохраняет БД!)
88+
php -f /usr/www/src/PBXCoreREST/Workers/WorkerModuleInstaller.php start /tmp/settings.json
89+
```
90+
91+
## Инициализация в скриптах
92+
93+
Все PHP скрипты (bin/) должны начинаться с:
94+
95+
```php
96+
#!/usr/bin/php
97+
<?php
98+
require_once('Globals.php');
99+
```
100+
101+
**Важно:** `Globals.php` должен быть симлинком на `/usr/www/src/Core/Config/Globals.php`
102+
103+
## Сборка JavaScript
104+
105+
Исходники: `public/assets/js/src/`**редактировать только файлы в `src/`**.
106+
Скомпилированные файлы в `public/assets/js/*.js` — автогенерируемые, не редактировать вручную.
107+
108+
Сборка через Babel (пресет `airbnb`, source maps включены).
109+
PHPStorm File Watcher: https://docs.mikopbx.com/mikopbx-development/prepare-ide-tools/mac#phpstorm-setup-babel
110+
111+
Ручная сборка:
112+
```bash
113+
cd /Users/apor/Developement/MikoPBX/MikoPBXUtils && \
114+
cp babel.config.json babel.config.json.bak && \
115+
echo '{"presets":[["@babel/preset-env",{"targets":{"chrome":50,"ie":11,"firefox":45}}]]}' > babel.config.json && \
116+
./node_modules/.bin/babel \
117+
/Volumes/DevDisk/apor/Developement/MikoPBX/Extensions/ModuleAmoCrm/public/assets/js/src/module-amo-crm-index.js \
118+
--out-dir /Volumes/DevDisk/apor/Developement/MikoPBX/Extensions/ModuleAmoCrm/public/assets/js/ \
119+
--source-maps && \
120+
mv babel.config.json.bak babel.config.json
121+
```
122+
123+
## Architecture
124+
125+
### Worker-Based Async System
126+
127+
Модуль работает через набор фоновых демонов (workers), взаимодействующих через Beanstalk-очереди:
128+
129+
| Worker | Файл | Тип проверки | Назначение |
130+
|--------|------|-------------|------------|
131+
| `WorkerAmoCrmAMI` | `bin/WorkerAmoCrmAMI.php` | AMI | Слушает события Asterisk (перехват, звонки) |
132+
| `AmoCdrDaemon` | `bin/AmoCdrDaemon.php` | PID | Синхронизация CDR → AmoCRM (батчами по 50) |
133+
| `ConnectorDb` | `bin/ConnectorDb.php` | PID | RPC-сервис для операций с БД через Beanstalk |
134+
| `WorkerAmoHTTP` | `bin/WorkerAmoHTTP.php` | Beanstalk | HTTP-запросы к API AmoCRM (throttle: 7 req/s) |
135+
| `SyncDaemon` | `bin/SyncDaemon.php` | PID | Синхронизация контактов/компаний/сделок |
136+
137+
Все workers наследуют `WorkerBase` из MikoPBX. `ConnectorDb::invoke()` — универсальный паттерн RPC-вызова к БД-воркеру из других процессов.
138+
139+
### Inter-Process Communication Flow
140+
141+
```
142+
AMI Events → WorkerAmoCrmAMI → Beanstalk → ConnectorDb (DB ops)
143+
→ WorkerAmoHTTP (API calls)
144+
CDR records → AmoCdrDaemon → ConnectorDb → WorkerAmoHTTP → AmoCRM API
145+
Webhooks → ApiController → ConnectorDb → обработка
146+
```
147+
148+
### MVC Layer (Phalcon)
149+
150+
- **Controller**: `App/Controllers/ModuleAmoCrmController.php` — CRUD для настроек модуля
151+
- **Models** (`Models/`): 6 моделей, все наследуют `ModulesModelsBase`. Таблицы имеют префикс `m_ModuleAmo*`
152+
- **Forms** (`App/Forms/`): формы для настроек и правил обработки звонков
153+
- **Views** (`App/Views/`): Volt-шаблоны (`index.volt`, `modify.volt`)
154+
155+
### Module Configuration (`Lib/AmoCrmConf.php`)
156+
157+
Центральный класс конфигурации, наследует `ConfigClass`. Отвечает за:
158+
- Регистрацию workers в `getModuleWorkers()`
159+
- REST-маршруты в `getPBXCoreRESTAdditionalRoutes()`
160+
- Генерацию dialplan-контекстов (extensions.conf) для перехвата звонков
161+
- Nginx-локации для WebRTC-телефона и воспроизведения записей
162+
- Cron-задачи: очистка tmp (каждую минуту), начальная синхронизация (01:00 ежедневно)
163+
- Реакцию на изменения в БД через `modelsEventChangeData()`
164+
165+
### REST API
166+
167+
Маршруты зарегистрированы в `AmoCrmConf::getPBXCoreRESTAdditionalRoutes()`. Контроллер: `Lib/RestAPI/Controllers/ApiController.php`.
168+
169+
Базовый путь: `/pbxcore/api/amo-crm/v1/`
170+
171+
| Endpoint | Метод | Назначение |
172+
|----------|-------|------------|
173+
| `/callback` | POST | Инициация звонка из AmoCRM |
174+
| `/listener` | GET/POST | OAuth2 авторизация |
175+
| `/command` | POST | Команды управления (hangup и др.) |
176+
| `/change-settings` | POST | Сохранение настроек из виджета |
177+
| `/find-contact` | POST | Поиск контакта по телефону |
178+
| `/panel-enable` | GET | Проверка статуса панели |
179+
| `/entity-update` | POST | Webhook от AmoCRM при изменении сущностей |
180+
181+
Авторизация API — через файл-токен в `/var/etc/auth/`.
182+
183+
### Call Processing Rules
184+
185+
Модель `ModuleAmoEntitySettings` определяет правила для 7 типов звонков:
186+
`INCOMING_UNKNOWN`, `MISSING_UNKNOWN`, `INCOMING_KNOWN`, `MISSING_KNOWN`, `OUTGOING_UNKNOWN`, `OUTGOING_KNOWN`, `OUTGOING_KNOWN_FAIL`
187+
188+
Каждое правило задаёт: ответственного, создание контакта/сделки/задачи/unsorted, шаблоны имён, воронку и статус. Defaults хранятся в `db/default-entity-settings.json`.
189+
190+
### OAuth2 & AmoCRM API
191+
192+
- `Lib/AmoCrmMain.php` — основной API-клиент AmoCRM (v4 API)
193+
- `Lib/AuthToken.php` — управление OAuth2 токенами (refresh с буфером 1 час)
194+
- `Lib/ClientHTTP.php` — HTTP-обёртка (POST, PATCH, GET)
195+
- Поддержка приватного виджета с отдельными client_id/secret
196+
197+
### Widget (`widget/`)
198+
199+
AmoCRM-виджет для встраивания в интерфейс CRM: WebRTC-телефон через iframe, event-driven коммуникация (PubSub), настройки в `manifest.json`.
200+
201+
### Database Models
202+
203+
- `ModuleAmoCrm` — главные настройки (OAuth-токены, домен, параметры интеграции)
204+
- `ModuleAmoEntitySettings` — правила обработки звонков per DID/type
205+
- `ModuleAmoUsers` — маппинг PBX extension ↔ AmoCRM user
206+
- `ModuleAmoPhones` — маппинг телефонных номеров
207+
- `ModuleAmoLeads` — кеш сделок
208+
- `ModuleAmoPipeLines` — кеш воронок
209+
210+
## Пути на сервере
211+
212+
| Путь | Описание |
213+
|------|----------|
214+
| `/storage/usbdisk1/mikopbx/custom_modules/ModuleAmoCrm/` | Директория модуля |
215+
| `/storage/usbdisk1/mikopbx/custom_modules/ModuleAmoCrm/db/` | Персистентные данные (БД) |
216+
| `/usr/www/src/Core/Config/Globals.php` | MikoPBX bootstrap |
217+
218+
## Key Conventions
219+
220+
- Модели: наследовать `ModulesModelsBase`, таблицы с префиксом `m_Module*`
221+
- Workers: наследовать `WorkerBase`, регистрировать в `AmoCrmConf::getModuleWorkers()`
222+
- REST-маршруты: добавлять в `AmoCrmConf::getPBXCoreRESTAdditionalRoutes()`
223+
- Логи: через `Lib/Logger.php` с ротацией (cesargb/php-log-rotation)
224+
- Локализация: файлы переводов в `Messages/` (31 язык, управляется через Weblate)
225+
- Схема БД создаётся из аннотаций Phalcon-моделей при `PbxExtensionSetup::installDB()`
226+
- CSS-фреймворк — Semantic UI

bin/WorkerAmoCrmAMI.php

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,9 @@ private function cloneCdr($data, &$transferCall, $call):void
501501
*/
502502
private function actionDialCreateChan($data):void{
503503
$uid = $data['transfer_UNIQUEID']??$data['UNIQUEID'];
504+
if(empty($this->calls[$data['linkedid']])){
505+
return;
506+
}
504507
foreach ($this->calls[$data['linkedid']] as &$call){
505508
if($uid !== $call['uid']){
506509
continue;
@@ -541,6 +544,9 @@ private function actionDialCreateChan($data):void{
541544
private function actionDialAnswer($params):void
542545
{
543546
$channel = $params['agi_channel'];
547+
if(empty($this->calls[$params['linkedid']])){
548+
return;
549+
}
544550
foreach ($this->calls[$params['linkedid']] as &$call){
545551
if(isset($call['answer'])){
546552
continue;
@@ -568,7 +574,7 @@ private function actionDialAnswer($params):void
568574
*/
569575
private function actionDialEnd($data):void
570576
{
571-
$src_num = $this->activeChannels[$data['src_chan']];
577+
$src_num = $this->activeChannels[$data['src_chan']]??'';
572578
if (isset($this->users[$src_num])) {
573579
// Это исходящий вызов.
574580
$USER_ID = $this->users[$src_num];
@@ -607,9 +613,10 @@ private function actionCompleteCdr($data):void
607613
$uid = $data['UNIQUEID'];
608614
$endTime = date(\DateTimeInterface::ATOM, strtotime($data['endtime']));
609615
$start = date(\DateTimeInterface::ATOM, strtotime($data['start']));
610-
foreach ( $this->calls[$data['linkedid']] as $index => $callData){
616+
$callsById = $this->calls[$data['linkedid']]??[];
617+
foreach ($callsById as $index => $callData){
611618
if($callData['src'] === $data['src_num'] && $callData['dst'] === $data['dst_num']
612-
&& $callData['date'] === $start && $callData['end'] === $endTime){
619+
&& $callData['date'] === $start && ($callData['end']??'') === $endTime){
613620
$uid = $callData['uid'];
614621
unset($this->calls[$data['linkedid']][$index]);
615622
break;
@@ -631,11 +638,13 @@ private function actionCompleteCdr($data):void
631638
ClientHTTP::sendHttpPostRequest(self::getChannelUrl(), $call);
632639

633640
// Чистим мусор.
634-
unset(
635-
$this->activeChannels[$data['src_chan']],
636-
$this->activeChannels[$data['dst_chan']],
637-
$this->channelCounter[$data['UNIQUEID']]
638-
);
641+
if(isset($data['src_chan'])){
642+
unset($this->activeChannels[$data['src_chan']]);
643+
}
644+
if(isset($data['dst_chan'])){
645+
unset($this->activeChannels[$data['dst_chan']]);
646+
}
647+
unset($this->channelCounter[$data['UNIQUEID']]);
639648
if(empty($this->calls[$data['linkedid']])){
640649
unset($this->calls[$data['linkedid']]);
641650
}

sites/webrtc-phone/index.html

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,18 @@
111111
<audio id="sipRemoteAudio"></audio>
112112
<script>
113113
window.onload = function() {
114-
let iframe = window.parent.document.getElementById('miko-pbx-phone');
115-
console.log(iframe.getAttribute('title'));
116-
let apiUrl = window.location.protocol + '//' + iframe.getAttribute('title') + '/pbxcore/api/amo-crm/v1/panel-enable';
114+
let pbxHost = window.mikoPbxHost || '';
115+
if (!pbxHost) {
116+
try {
117+
let iframe = window.parent.document.getElementById('miko-pbx-phone');
118+
pbxHost = iframe.getAttribute('title');
119+
} catch(e) {}
120+
}
121+
if (!pbxHost) {
122+
pbxHost = window.location.host;
123+
}
124+
console.log(pbxHost);
125+
let apiUrl = window.location.protocol + '//' + pbxHost + '/pbxcore/api/amo-crm/v1/panel-enable';
117126
// Выполняем запрос
118127
fetch(apiUrl)
119128
.then(function(response) {
@@ -123,7 +132,7 @@
123132
document.querySelector('html').style = "";
124133
} else {
125134
console.log('FALSE');
126-
window.parent.postMessage(JSON.stringify({action: 'hide-panel', pbxHost: iframe.getAttribute('title')}), '*');
135+
window.parent.postMessage(JSON.stringify({action: 'hide-panel', pbxHost: pbxHost}), '*');
127136
}
128137
});
129138
};

sites/webrtc-phone/js/app-entrypoint.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@
1919
let baseUrl = 'js';
2020
if(window.self !== window.top){
2121
// This is iframe.
22-
baseUrl = `${window.location.protocol}//${window.frameElement.title}/webrtc-phone/js`;
22+
let pbxHost = window.mikoPbxHost || '';
23+
if (!pbxHost) {
24+
try { pbxHost = window.frameElement.title; } catch(e) {}
25+
}
26+
if (pbxHost) {
27+
baseUrl = `${window.location.protocol}//${pbxHost}/webrtc-phone/js`;
28+
}
2329
}
2430
requirejs.config({
2531
baseUrl: baseUrl,

0 commit comments

Comments
 (0)