From 0f3e8bdf70d99cf5bdf70cc82bf601879e0ecb6a Mon Sep 17 00:00:00 2001 From: Alexey Portnov Date: Thu, 19 Feb 2026 21:54:44 +0800 Subject: [PATCH 1/2] fix: restore return instead of Goto for forbidden outgoing routes Revert regression from f1cf2bc that replaced `return` with `Goto(users-group-forbidden)` in generateOutRoutContext. The Goto breaks route iteration by jumping out of the Gosub, preventing the outgoing context from trying subsequent allowed routes. --- Lib/UsersGroupsConf.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/UsersGroupsConf.php b/Lib/UsersGroupsConf.php index 1f28523..c56c6b2 100644 --- a/Lib/UsersGroupsConf.php +++ b/Lib/UsersGroupsConf.php @@ -214,7 +214,7 @@ public function generateOutRoutContext(array $rout): string $conf .= 'same => n,Set(EFFECTIVE_FROM_PEER=${IF($["${FW_SOURCE_PEER}x" != "x"]?${FW_SOURCE_PEER}:${FROM_PEER})})' . " \n\t"; $conf .= 'same => n,Set(GR_VARS=${DB(UsersGroups/${EFFECTIVE_FROM_PEER})})' . " \n\t"; $conf .= 'same => n,ExecIf($["${GR_VARS}x" != "x"]?Exec(Set(${GR_VARS})))' . " \n\t"; - $conf .= 'same => n,ExecIf($["${GR_PERM_ENABLE}" == "1" && "${GR_ID_' . $rout['id'] . '}" != "1"]?Goto(users-group-forbidden,${EXTEN},1))' . " \n\t"; + $conf .= 'same => n,ExecIf($["${GR_PERM_ENABLE}" == "1" && "${GR_ID_' . $rout['id'] . '}" != "1"]?return)' . " \n\t"; $conf .= 'same => n,ExecIf($["${GR_PERM_ENABLE}" == "1" && "${GR_CID_' . $rout['id'] . '}x" != "x"]?MSet(GR_OLD_CALLERID=${CALLERID(num)},OUTGOING_CID=${GR_CID_' . $rout['id'] . '}))' . "\n\t"; $conf .= 'same => n,ExecIf($["${OUTGOING_CID}x" != "x"]?Set(DOPTIONS=${DOPTIONS}f(${OUTGOING_CID})))' . " \n\t"; $conf .= 'same => n,GosubIf($["${DIALPLAN_EXISTS(SIP-${CUT(CONTEXT,-,2)}-outgoing-ug-custom,${EXTEN},1)}" == "1"]?SIP-${CUT(CONTEXT,-,2)}-outgoing-ug-custom,${EXTEN},1)'; From 7d1da209dc1196868d0c43f41734ddb6f8857301 Mon Sep 17 00:00:00 2001 From: Alexey Portnov Date: Fri, 20 Feb 2026 17:55:27 +0800 Subject: [PATCH 2/2] fix: use intval() for type-safe comparisons in AstDB group matching Strict === comparisons between GroupMembers (Column type="string") and Extensions/AllowedOutboundRules (Column type="integer") caused type mismatch, resulting in GR_PERM_ENABLE=0 for all extensions and outbound route restrictions never being applied. Also fix extra closing paren in disabled-path AstDB value. --- CLAUDE.md | 69 +++++++++++++++++++++++++++++++++++++++++++++ Lib/UsersGroups.php | 6 ++-- 2 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f1c9152 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,69 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +ModuleUsersGroups — модуль расширения для MikoPBX, реализующий управление группами сотрудников с контролем прав на звонки, изоляцией вызовов и управлением исходящими маршрутами. Модуль генерирует конфигурацию Asterisk dialplan и управляет данными через AstDB. + +## Architecture + +### Framework & Runtime +- **PHP 7.4+** с Phalcon (поддержка v4 и v5+ через `Lib/MikoPBXVersion.php`) +- Наследует базовые классы MikoPBX: `ConfigClass`, `PbxExtensionBase`, `ModulesModelsBase`, `BaseController`, `BaseForm` +- Namespace: `Modules\ModuleUsersGroups\` (PSR-4 от корня) +- Frontend: Semantic UI + DataTables, JS на ES6+ + +### Core Components + +**`Lib/UsersGroupsConf.php`** — центральный класс, наследует `ConfigClass`. Реализует хуки MikoPBX для генерации dialplan: +- `extensionGenAllPeersContext()` — правила изоляции в контексте `[all_peers]` +- `extensionGenContexts()` — контексты `users-group-isolate`, `users-group-dst-*`, `users-group-forbidden` +- `generateOutRoutContext()` / `generateOutRoutAfterDialContext()` — ограничения исходящих маршрутов, подмена CallerID +- `overridePJSIPOptions()` — назначение `named_call_group`/`named_pickup_group` для изоляции pickup +- `moduleRestAPICallback()` — точка входа REST API +- `onAfterExecuteRestAPIRoute()` — перехват сохранения сотрудника (API v2 и v3) для привязки к группе + +**`Lib/UsersGroups.php`** — сервисный класс, заполняет AstDB (`UsersGroups/{extension}`) переменными канала: `GR_PERM_ENABLE`, `GR_ID_{routeId}`, `GR_CID_{routeId}`. + +**`Lib/RestAPI/`** — action-based REST API: +- `UsersGroupsManagementProcessor.php` — роутер действий +- Actions: `GetUserGroupAction`, `UpdateUserGroupAction`, `SetDefaultGroupAction`, `GetDefaultGroupAction`, `GetGroupsStatsAction`, `CleanupOrphanedMembersAction` + +### Data Model (SQLite, ORM через аннотации Phalcon) +- `UsersGroups` — группа (name, description, patterns, isolate, isolatePickUp, defaultGroup) +- `GroupMembers` — связь группа↔пользователь (group_id, user_id). Один пользователь = одна группа +- `AllowedOutboundRules` — связь группа↔маршрут (group_id, rule_id, caller_id) + +**Важно:** `group_id` в коде используется со сдвигом `+1` при маппинге на named_call_group Asterisk (см. `initUserList()` и `getSettings()`). + +### Dialplan Logic +Изоляция работает через контексты Asterisk: +1. В `[all_peers]` проверяются флаги `srcIsolate` и `dstIsolate` через `DIALPLAN_EXISTS` +2. При запрете — `Goto(users-group-forbidden)` с воспроизведением звукового файла +3. Исходящие маршруты: переменные из AstDB (`GR_VARS`) определяют доступ к маршруту и подмену CallerID +4. Поддержка `FW_SOURCE_PEER` для переадресованных вызовов + +### Frontend Structure +- `App/Controllers/ModuleUsersGroupsController.php` — единый контроллер (index/modify) +- `App/Views/ModuleUsersGroups/` — Volt-шаблоны с табами (groups, users, rules) +- `public/assets/js/src/` — исходники JS, `public/assets/js/` — собранные файлы + +## Build & CI + +Сборка и публикация через GitHub Actions (`.github/workflows/build.yml`), использует shared workflow `mikopbx/.github-workflows/.github/workflows/extension-publish.yml@master`. + +Зависимости PHP: `composer install` (минимальные — только `mikopbx/core`). + +Тесты и линтеры не настроены в репозитории. + +## Localization + +32 языковых файла в `Messages/`. Звуковые файлы `Sounds/{lang}/forbidden.mp3` для голосовых уведомлений при запрете вызова. + +## Key Conventions + +- PHP 7.4 совместимость обязательна (нет `str_starts_with`, `match`, union types и т.д.) +- Модуль-специфичные POST-поля имеют префикс `mod_usrgr_` +- Модуль встраивается в карточку сотрудника через `onVoltBlockCompile` и `onBeforeFormInitialize` +- При изменении моделей `AllowedOutboundRules`, `GroupMembers`, `UsersGroups` автоматически вызывается `reloadConfigs()` (SIP + dialplan reload) diff --git a/Lib/UsersGroups.php b/Lib/UsersGroups.php index 7c0d670..012312d 100644 --- a/Lib/UsersGroups.php +++ b/Lib/UsersGroups.php @@ -102,7 +102,7 @@ public function fillAsteriskDatabase(): void $db = new AstDB(); $extension = Extensions::find("type='SIP'")->toArray(); if ($enabled === false) { - $cmd = "ARRAY(GR_PERM_ENABLE)=0)"; + $cmd = "ARRAY(GR_PERM_ENABLE)=0"; // Loop through each extension and disable group permissions foreach ($extension as $extensionData) { @@ -137,7 +137,7 @@ private function initGroupID(array $extensionData, array $groupMembers): array $groupId = null; $number = $extensionData['number']; foreach ($groupMembers as $memberData) { - if ($memberData['user_id'] === $extensionData['userid']) { + if (intval($memberData['user_id']) === intval($extensionData['userid'])) { $groupId = $memberData['group_id']; break; } @@ -160,7 +160,7 @@ private function initChannelVariables($group_id, array $allowedRules): string // Find all routes allowed in the group foreach ($allowedRules as $ruleData) { - if ($ruleData['group_id'] !== $group_id) { + if (intval($ruleData['group_id']) !== intval($group_id)) { continue; }