Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c3bb5fb
Translated using Weblate (Japanese)
Mai-Shinano Aug 26, 2025
d793f88
Added translation using Weblate (Hebrew)
weblate Sep 29, 2025
c62230a
Added translation using Weblate (Persian)
weblate Sep 30, 2025
ed799a6
Add new field for Models
Oct 5, 2025
9c84036
Displaying the 'Expired' mark in the table
Oct 5, 2025
bda297a
Add new field for Models - settings
Oct 6, 2025
0c205ba
Readme - add new field
Oct 6, 2025
5e4130c
Rename field expired => created
Oct 6, 2025
26d44c7
Меняем action для сохранения настроек. Исправляем обработку ошибок пр…
Oct 6, 2025
e767ec3
Code style
Oct 6, 2025
ecbfd65
Add new field for settings form
Oct 6, 2025
83cb2eb
Опечатка в переводе ru
Oct 6, 2025
1031055
Add new field for settings form 2
Oct 6, 2025
6f43d73
Add new field for settings form 3
Oct 6, 2025
ea3257a
Save new settings
Oct 6, 2025
b4193d7
Проверка для поля phoneBookLifeTime
Oct 6, 2025
c9725e6
Основной функционал - поиск имени по Api, если нет в справочнике
Oct 6, 2025
8632126
Для модели PhoneBook добаим вспомогательный метод для заполнения полей
Oct 6, 2025
53587ac
Мелкие правки
Oct 6, 2025
2041cba
Оптимизируем изменение записей - используем setPhonebookRecord
Oct 6, 2025
8c1f1ec
Баг при редактировании позиций - не меняется id кнопки Удалить
Oct 6, 2025
1441cdc
Readme - add new method for create new contact
Oct 6, 2025
cad75a6
Меняем curl на GuzzleHttp
Oct 7, 2025
4c5cc4c
Предупреждение Codacy Static Code Analysis - функция передана в HTML код
Oct 7, 2025
8537e40
Предупреждение Codacy Static Code Analysis - функция передана в HTML …
Oct 7, 2025
798c2e0
Code style
Oct 7, 2025
d90c81f
Предупреждение Codacy Static Code Analysis - функция передана в HTML …
Oct 7, 2025
1b0117c
Предупреждение Codacy Static Code Analysis - функция передана в HTML …
Oct 7, 2025
c9661cb
Добавляем очистку call_id перед записью в базу
Oct 7, 2025
7fd4417
Выносим функционал поиска в отдельный класс PhoneBookFind
Oct 8, 2025
83f3589
fix: improve API caller ID lookup security and stability
jorikfon Dec 6, 2025
4ac0ed9
fix: resolve code review issues in phonebook module
jorikfon Dec 6, 2025
ba42613
docs: update README and add Russian translation
jorikfon Dec 6, 2025
56cb440
docs: add language cross-references and API examples
jorikfon Dec 6, 2025
d01412d
docs: update README files for Babel compilation instructions
jorikfon Dec 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 30 additions & 42 deletions App/Controllers/ModulePhoneBookController.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ public function getNewRecordsAction(): void
$parameters['columns'] = [
'call_id',
'number' => 'number_rep',
'created' => 'created',
'DT_RowId' => 'id',
];
$parameters['order'] = ['call_id desc'];
Expand Down Expand Up @@ -156,20 +157,18 @@ public function saveAction(): void

$dataId = $this->request->getPost('id', ['string', 'trim']);
$callId = $this->request->getPost('call_id', ['string', 'trim']);
$number = $this->request->getPost('number', ['alnum']);
$numberRep = $this->request->getPost('number_rep', ['string', 'trim'], $number);
$numberRep = $this->request->getPost('number_rep', ['string', 'trim']);
$number = PhoneBook::cleanPhoneNumber($numberRep, TRUE);

if (empty($callId) || empty($number)) {
return;
}

// If we are unable to change the primary field, delete the old record and recreate it
$oldId = null;
$record = null;
if (stripos($dataId, 'new') === false) {
$record = PhoneBook::findFirstById($dataId);
if ($record->number !== $number) {
$oldId = $record->id;
if ($record !== null && $record->number !== $number) {
$record->delete();
$record = null;
}
Expand All @@ -179,39 +178,17 @@ public function saveAction(): void
$record = new PhoneBook();
}

foreach ($record as $key => $value) {
switch ($key) {
case 'id':
break;
case 'number':
$record->number = $number;
break;
case 'number_rep':
$record->number_rep = $numberRep;
break;
case 'call_id':
$record->call_id = $callId;
break;
case 'search_index':
// Collect data for the search index
$username = mb_strtolower($callId);
// Combine all fields into a single string
$record->search_index = $username . $number . $numberRep;
break;
default:
break;
}
}
$record->setPhonebookRecord($callId, $numberRep);

if ($record->save() === false) {
$errors = $record->getMessages();
$this->flash->error(implode('<br>', $errors));
$this->view->success = false;

$this->response->setStatusCode(500);
return;
}

$this->view->data = ['oldId' => $oldId, 'newId' => $record->id];
$this->view->data = ['oldId' => $dataId, 'newId' => $record->id];
$this->view->success = true;
}

Expand All @@ -236,22 +213,24 @@ public function deleteAction(?string $id = null): void
*/
public function deleteAllRecordsAction(): void
{
$records = PhoneBook::find();
foreach ($records as $record) {
if (!$record->delete()) {
$this->flash->error(implode('<br>', $record->getMessages()));
$this->view->result = false;
return;
}
$phoneBook = new PhoneBook();
$connection = $phoneBook->getWriteConnection();
$tableName = $phoneBook->getSource();

try {
$connection->execute("DELETE FROM {$tableName}");
$this->view->result = true;
$this->view->reload = 'module-phone-book/module-phone-book/index';
} catch (\Throwable $e) {
$this->flash->error($e->getMessage());
$this->view->result = false;
}
$this->view->result = true;
$this->view->reload = 'module-phone-book/module-phone-book/index';
}

/**
* Toggle input mask feature.
* Save settings
*/
public function toggleDisableInputMaskAction(): void
public function saveSettingsAction(): void
{
if (!$this->request->isPost()) {
return;
Expand All @@ -262,10 +241,19 @@ public function toggleDisableInputMaskAction(): void
$settings = new Settings();
}

$settings->disableInputMask = $this->request->getPost('disableInputMask') === 'true' ? '1' : '0';
if ($this->request->hasPost('disableInputMask')) {
$settings->disableInputMask = $this->request->getPost('disableInputMask') === 'true' ? '1' : '0';
}

if ($this->request->hasPost('phoneBookApiUrl')) {
$settings->phoneBookApiUrl = empty($this->request->getPost('phoneBookApiUrl')) ? NULL : $this->request->getPost('phoneBookApiUrl', 'trim');
$settings->phoneBookLifeTime = empty($this->request->getPost('phoneBookLifeTime')) ? 0 : $this->request->getPost('phoneBookLifeTime', 'int!');
}

if (!$settings->save()) {
$this->flash->error(implode('<br>', $settings->getMessages()));
$this->view->success = false;
$this->response->setStatusCode(500);
return;
}
$this->view->success = true;
Expand Down
18 changes: 17 additions & 1 deletion App/Forms/ModuleConfigForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
namespace Modules\ModulePhoneBook\App\Forms;

use MikoPBX\AdminCabinet\Forms\BaseForm;
use Phalcon\Forms\Element\Text;
use Phalcon\Forms\Element\Numeric;
use Phalcon\Forms\Element\Check;
use Phalcon\Forms\Element\File;

Expand All @@ -31,6 +33,20 @@ public function initialize($entity = null, $options = null): void
// DisableInputMask
$this->addCheckBox('disableInputMask', intval($entity->disableInputMask) === 1);

// phoneBookApiUrl Text field
$this->add(
new Text('phoneBookApiUrl', [
'placeholder' => 'https://',
])
);

// phoneBookLifeTime Text field
$this->add(
new Numeric('phoneBookLifeTime', [
'min' => 0
])
);

// Excel file
$excelFile = new File('excelFile');
$this->add($excelFile);
Expand All @@ -49,7 +65,7 @@ public function addCheckBox(string $fieldName, bool $checked, string $checkedVal
{
$checkAr = ['value' => null];
if ($checked) {
$checkAr = ['checked' => $checkedValue,'value' => $checkedValue];
$checkAr = ['checked' => $checkedValue, 'value' => $checkedValue];
}
$this->add(new Check($fieldName, $checkAr));
}
Expand Down
4 changes: 2 additions & 2 deletions App/Views/ModulePhoneBook/Tabs/phonebookTab.volt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<div class="ui nine wide column">
<div class="ui search right action left icon fluid input" id="search-extensions-input">
<i class="search link icon" id="search-icon"></i>
<input type="search" id="global-search" name="global-search" placeholder="{{ t._('ex_EnterSearchPhrase') }}"
<input type="search" id="global-search" name="global-search" placeholder="{{ t._('module_phnbk_Search') }}"
aria-controls="KeysTable" class="prompt">
<div class="results"></div>
<div class="ui basic floating search dropdown button" id="page-length-select">
Expand Down Expand Up @@ -36,4 +36,4 @@
</tr>
</thead>
<tbody>
</table>
</table>
15 changes: 15 additions & 0 deletions App/Views/ModulePhoneBook/Tabs/settingsTab.volt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@
<label for="disableInputMask">{{ t._('module_phnbk_disableInputMask') }}</label>
</div>
</div>
<div class="ui segment">
<div class="wide field">
<label for="phoneBookApiUrl">{{ t._('module_phnbk_ApiUrl') }}</label>
{{ form.render('phoneBookApiUrl') }}
<div>{{ t._('module_phnbk_ApiUrlDescription', {'repesent': '%number%'}) }}</div>
</div>
<div class="wide field">
<label for="phoneBookLifeTime">{{ t._('module_phnbk_CacheLifetime') }}</label>
{{ form.render('phoneBookLifeTime') }}
<div>{{ t._('module_phnbk_CacheLifetimeDescription') }}</div>
</div>
<div class="field">
<div class="ui labeled icon positive button" id="btn-save-settings-api"><i class="save icon"></i>{{ t._('module_phnbk_SaveBtn') }}</div>
</div>
</div>
</div>
<div class="field">
<div class="ui labeled icon basic button" id="delete-all-records"><i class="red trash icon"></i>{{ t._('module_phnbk_DeleteAllRecords') }}</div>
Expand Down
85 changes: 85 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Overview

ModulePhoneBook is a MikoPBX extension module that provides caller ID management and contact storage. It integrates with Asterisk PBX for real-time caller identification on inbound and outbound calls.

## Build Commands

### JavaScript Compilation
```bash
docker run --rm -v /Users/nb/PhpstormProjects/mikopbx:/workspace ghcr.io/mikopbx/babel-compiler:latest /workspace/Extensions/[module]/public/assets/js/src/[file] extension`
```

### PHP Syntax Check
```bash
php -l Lib/PhoneBookConf.php
```

### Code Quality (PHPStan)
Run phpstan after creating new PHP code to validate quality.

### Dependencies
```bash
composer install
```

## Architecture

### Directory Structure
- `App/Controllers/` - Phalcon MVC controllers (ModulePhoneBookController)
- `App/Forms/` - Phalcon form definitions
- `Lib/` - Core business logic
- `PhoneBookConf.php` - PBX integration, REST API callbacks, Asterisk dialplan generation
- `PhoneBookAgi.php` - Asterisk AGI handler for real-time caller ID lookup
- `PhoneBookImport.php` - Excel import processor using PhpSpreadsheet
- `MikoPBXVersion.php` - Version compatibility helpers
- `Models/` - Phalcon ORM models (PhoneBook, Settings)
- `Setup/` - Module installation logic (PbxExtensionSetup)
- `agi-bin/` - Asterisk AGI scripts
- `Messages/` - i18n translation files (26 languages)
- `public/assets/js/src/` - Source JavaScript files (ES6)
- `public/assets/js/` - Compiled JavaScript files

### Data Flow
1. **Inbound calls**: Asterisk dialplan → `agi_phone_book.php` → `PhoneBookAgi::setCallerId('in')` → Sets CALLERID(name)
2. **Outbound calls**: CONNECTED_LINE_SEND_SUB → `PhoneBookAgi::setCallerId('out')` → Sets CONNECTEDLINE(name)
3. **Web UI**: DataTable with server-side processing via AJAX to `ModulePhoneBookController::getNewRecordsAction()`

### Phone Number Storage Format
Numbers are normalized for consistent storage and fast lookups:
- Strip all non-digit characters
- Keep last 9 digits only
- Prepend "1"
- Example: `+7 (906) 555-43-43` → `1065554343`

### Database
SQLite database at runtime: `/storage/usbdisk1/mikopbx/custom_modules/ModulePhoneBook/db/module.db`

Tables:
- `m_PhoneBook` - contacts (id, number, number_rep, call_id, search_index)
- `m_ModulePhoneBook` - settings (disableInputMask)

### Key Integration Points
- `PhoneBookConf::moduleRestAPICallback()` - REST API entry point for Excel import
- `PhoneBookConf::generateIncomingRoutBeforeDial()` - Injects AGI into inbound routes
- `PhoneBookConf::generateOutRoutContext()` - Injects connected line handling for outbound
- `PhoneBookConf::extensionGenContexts()` - Generates `[phone-book-out]` Asterisk context

### Frontend
- Uses Semantic UI components and DataTables
- Input masking for phone numbers (toggleable via settings)
- State persistence in localStorage for page length and search
- Files in `public/assets/js/src/` must be compiled with Babel to `public/assets/js/`

## Module Configuration

`module.json` defines module metadata including:
- `moduleUniqueID`: "ModulePhoneBook"
- `min_pbx_version`: "2024.1.114"

## CI/CD

GitHub Actions workflow (`.github/workflows/build.yml`) uses shared MikoPBX workflow for building and publishing releases.
13 changes: 13 additions & 0 deletions Lib/MikoPBXVersion.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,17 @@ public static function getLoggerClass(): string
return \Phalcon\Logger::class;
}
}

/**
* Return validator Callback class for the current version of PBX
* @return class-string<\Phalcon\Filter\Validation\Validator\Callback>|class-string<\Phalcon\Validation\Validator\Callback>
*/
public static function getValidatorCallbackClass(): string
{
if (self::isPhalcon5Version()) {
return \Phalcon\Filter\Validation\Validator\Callback::class;
} else {
return \Phalcon\Validation\Validator\Callback::class;
}
}
}
21 changes: 17 additions & 4 deletions Lib/PhoneBookAgi.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use MikoPBX\Core\Asterisk\AGI;
use MikoPBX\Core\System\Util;
use Modules\ModulePhoneBook\Models\PhoneBook;
use Modules\ModulePhoneBook\Models\Settings;
use Phalcon\Di\Injectable;

/**
Expand All @@ -48,15 +49,27 @@ public static function setCallerID(string $type): void
} else {
$number = $agi->request['agi_extension'];
}

$number_orig = $number;
// Normalize the phone number to match the expected format (last 9 digits)
$number = '1' . substr($number, -9);
$number = PhoneBook::cleanPhoneNumber($number, TRUE);

// Find the corresponding phonebook entry by the number
$result = PhoneBook::findFirstByNumber($number);

$settings = Settings::findFirst();
$lifeTime = ($settings !== null) ? ($settings->phoneBookLifeTime ?? 0) : 0;
$apiUrl = ($settings !== null) ? ($settings->phoneBookApiUrl ?? '') : '';

if ($result === null || empty($result->call_id) || ($lifeTime > 0 && $result->created > 0 && $result->created + $lifeTime < time())) {
// The record was not found or expired - search through the API if configured
if (!empty($apiUrl)) {
$searcher = new PhoneBookFind();
$result = $searcher->findApiByNumber($number_orig, $result);
}
}

// If a matching record is found and the call_id is not empty, set the appropriate caller ID
if ($result !== null && !empty($result->call_id)) {
if ($result !== NULL && !empty($result->call_id)) {
if ($type === 'in') {
$agi->set_variable('CALLERID(name)', $result->call_id);
} else {
Expand All @@ -68,4 +81,4 @@ public static function setCallerID(string $type): void
Util::sysLogMsg('PhoneBookAGI', $e->getMessage(), LOG_ERR);
}
}
}
}
Loading