Skip to content

Commit dc65c91

Browse files
committed
Merge firmware repository feature into develop
2 parents d4f0ae0 + 41821d2 commit dc65c91

27 files changed

Lines changed: 3025 additions & 5 deletions

App/Controllers/ModuleAutoprovisionController.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,15 @@ public function indexAction(): void
7171
->addJs('js/vendor/semantic/modal.min.js', true);
7272

7373
$footerJs = $this->assets->collection(AssetProvider::FOOTER_JS);
74+
// Resumable.js powers the chunked firmware upload. FilesAPI and the
75+
// FileUploadEventHandler globals are already in the default authenticated
76+
// bundle (see AssetProvider::makeDefaultAssets); only Resumable itself
77+
// is opt-in per page.
7478
$footerJs
79+
->addJs('js/vendor/resumable.js', true)
7580
->addJs('js/pbx/main/form.js', true)
76-
->addJs('js/cache/' . self::MODULE_UNIQUE_ID . '/module-autoprovision.js', true);
81+
->addJs('js/cache/' . self::MODULE_UNIQUE_ID . '/module-autoprovision.js', true)
82+
->addJs('js/cache/' . self::MODULE_UNIQUE_ID . '/module-autoprovision-firmware.js', true);
7783

7884
$settings = ModuleAutoprovision::findFirst() ?? new ModuleAutoprovision();
7985
$this->view->form = new ModuleAutoprovisionForm($settings);

App/Views/ModuleAutoprovision/index.volt

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
<a class="item" data-tab="general-settings">{{ t._('mod_Autoprovision_general_settings') }}</a>
77
<a class="item" data-tab="other-pbx">{{ t._('mod_Autoprovision_other_pbx') }}</a>
88
<a class="item" data-tab="pnp">{{ t._('mod_Autoprovision_pnp') }}</a>
9+
<a class="item" data-tab="firmware">{{ t._('mod_Autoprovision_firmware') }}</a>
910
</div>
1011

1112
<div class="ui bottom attached tab segment" data-tab="other-pbx">
@@ -259,5 +260,107 @@
259260
</table>
260261
</div>
261262

263+
<div class="ui bottom attached tab segment" data-tab="firmware">
264+
<div class="ui message">{{ t._('mod_Autoprovision_firmware_header') }}</div>
265+
266+
<div id="firmware-dropzone" class="ui placeholder segment" style="cursor: pointer;">
267+
<div class="ui icon header">
268+
<i class="upload icon"></i>
269+
{{ t._('mod_Autoprovision_firmware_drop_hint') }}
270+
</div>
271+
<div class="ui primary button" id="firmware-browse">
272+
<i class="folder open icon"></i>
273+
{{ t._('mod_Autoprovision_firmware_browse') }}
274+
</div>
275+
</div>
276+
277+
<div class="ui form" style="margin-top: 1em;">
278+
<div class="four fields">
279+
<div class="field">
280+
<label>{{ t._('mod_Autoprovision_firmware_vendor') }}</label>
281+
<select id="firmware-vendor" class="ui dropdown">
282+
<option value="yealink">Yealink</option>
283+
<option value="snom">Snom</option>
284+
<option value="fanvil">Fanvil</option>
285+
<option value="grandstream">Grandstream</option>
286+
<option value="htek">Htek</option>
287+
</select>
288+
</div>
289+
<div class="field">
290+
<label>{{ t._('mod_Autoprovision_firmware_model') }}</label>
291+
<input id="firmware-model" type="text" placeholder="T46S">
292+
</div>
293+
<div class="field">
294+
<label>{{ t._('mod_Autoprovision_firmware_version') }}</label>
295+
<input id="firmware-version" type="text" placeholder="66.86.0.15">
296+
</div>
297+
<div class="field">
298+
<label>{{ t._('mod_Autoprovision_firmware_notes') }}</label>
299+
<input id="firmware-notes" type="text">
300+
</div>
301+
</div>
302+
</div>
303+
304+
<div id="firmware-progress" class="ui small indicating progress" data-percent="0" style="margin: 1em 0;">
305+
<div class="bar"><div class="progress"></div></div>
306+
<div class="label"></div>
307+
</div>
308+
309+
<table id="firmware-table" class="ui celled table">
310+
<thead><tr>
311+
<th>{{ t._('mod_Autoprovision_firmware_vendor') }}</th>
312+
<th>{{ t._('mod_Autoprovision_firmware_model') }}</th>
313+
<th>{{ t._('mod_Autoprovision_firmware_filename') }}</th>
314+
<th>{{ t._('mod_Autoprovision_firmware_version') }}</th>
315+
<th>{{ t._('mod_Autoprovision_firmware_size') }}</th>
316+
<th>{{ t._('mod_Autoprovision_firmware_sha256') }}</th>
317+
<th class="collapsing right aligned"></th>
318+
</tr></thead>
319+
<tbody></tbody>
320+
</table>
321+
<div id="firmware-totals" class="ui small text"></div>
322+
323+
<div id="firmware-edit-modal" class="ui modal">
324+
<i class="close icon"></i>
325+
<div class="header">{{ t._('mod_Autoprovision_firmware_edit_title') }}</div>
326+
<div class="content">
327+
<div class="ui form">
328+
<input type="hidden" id="firmware-edit-id" value="">
329+
<div class="two fields">
330+
<div class="field">
331+
<label>{{ t._('mod_Autoprovision_firmware_vendor') }}</label>
332+
<select id="firmware-edit-vendor" class="ui dropdown">
333+
<option value="yealink">Yealink</option>
334+
<option value="snom">Snom</option>
335+
<option value="fanvil">Fanvil</option>
336+
<option value="grandstream">Grandstream</option>
337+
<option value="htek">Htek</option>
338+
</select>
339+
</div>
340+
<div class="field">
341+
<label>{{ t._('mod_Autoprovision_firmware_model') }}</label>
342+
<input id="firmware-edit-model" type="text">
343+
</div>
344+
</div>
345+
<div class="two fields">
346+
<div class="field">
347+
<label>{{ t._('mod_Autoprovision_firmware_version') }}</label>
348+
<input id="firmware-edit-version" type="text">
349+
</div>
350+
<div class="field">
351+
<label>{{ t._('mod_Autoprovision_firmware_notes') }}</label>
352+
<input id="firmware-edit-notes" type="text">
353+
</div>
354+
</div>
355+
<div id="firmware-edit-error" class="ui red message" style="display: none;"></div>
356+
</div>
357+
</div>
358+
<div class="actions">
359+
<div class="ui cancel button">{{ t._('mod_Autoprovision_firmware_cancel') }}</div>
360+
<div id="firmware-edit-save" class="ui positive button">{{ t._('mod_Autoprovision_firmware_save') }}</div>
361+
</div>
362+
</div>
363+
</div>
364+
262365
{{ partial("partials/submitbutton",['indexurl':'pbx-extension-modules/index/']) }}
263366
</form>

Lib/Autoprovision.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use MikoPBX\Core\System\MikoPBXConfig;
1717
use MikoPBX\Core\System\Network;
1818
use MikoPBX\Core\System\Util;
19+
use Modules\ModuleAutoprovision\Lib\RestAPI\Firmware\Repository as FirmwareRepository;
1920
use Modules\ModuleAutoprovision\Models\ModuleAutoprovisionDevice;
2021
use Modules\ModuleAutoprovision\Models\ModuleAutoprovisionUsers;
2122
use Phalcon\Di\Injectable;
@@ -95,6 +96,17 @@ public function generateConfigPhone(array $req_data): string
9596
}
9697
}
9798

99+
// Resolve {FIRMWARE_URL} once and surface it to every vendor generator
100+
// through $req_data so the per-vendor classes emit the correct config
101+
// key ('firmware.url' for Yealink, '<AUTOUPDATE>FirmwareUpgrade' for
102+
// Fanvil, P192/P237 for Grandstream, 'auto_image_url' for Htek, etc.).
103+
// Empty string when no matching firmware row exists — generators must
104+
// suppress the line in that case.
105+
$req_data['firmware_url'] = FirmwareRepository::resolveFirmwareUrl(
106+
(string)($req_data['vendor'] ?? ''),
107+
isset($req_data['model']) ? (string)$req_data['model'] : null
108+
);
109+
98110
$confManager = match ($req_data['vendor'] ?? '') {
99111
'yealink' => new AutoprovisionYealink(),
100112
'fanvil' => new AutoprovisionFanvil(),

Lib/AutoprovisionConf.php

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -361,13 +361,18 @@ public function onAfterModuleEnable(): void
361361
*/
362362
public function createNginxServers(): string
363363
{
364-
$port = self::getHttpPort();
365-
$baseUri = self::BASE_URI;
364+
$port = self::getHttpPort();
365+
$baseUri = self::BASE_URI;
366+
$firmwareDir = $this->moduleDir . '/firmware';
366367

367368
// Server-block is isolated (no shared `~ \.php$` handler), so we route
368369
// each accepted URI to a single internal location that talks to PHP-FPM
369370
// directly. Explicit `last` flag re-runs location matching after the
370371
// rewrite, landing on the `= /pbxcore/index.php` internal location.
372+
//
373+
// The /firmware/ alias serves phone firmware blobs straight from disk;
374+
// proxying them through PHP would OOM the worker on a 40 MB Yealink T5x
375+
// .rom file. Read-only by design — uploads go through the v3 REST path.
371376
$content = <<<NGINX
372377
location ^~ {$baseUri}/ {
373378
rewrite ^/pbxcore/(.*)\$ /pbxcore/index.php?_url=/\$1 last;
@@ -377,6 +382,17 @@ public function createNginxServers(): string
377382
rewrite ^/pbxcore/(.*)\$ /pbxcore/index.php?_url=/\$1 last;
378383
}
379384
385+
location ^~ /firmware/ {
386+
alias {$firmwareDir}/;
387+
autoindex off;
388+
add_header Cache-Control "public, max-age=3600";
389+
# Log every phone fetch through the OS syslog under the autoprovision-firmware
390+
# tag — the file is served by nginx without touching PHP, so PHP-side
391+
# Util::sysLogMsg() can't see these requests. Requires nginx ≥ 1.7.1.
392+
access_log syslog:server=unix:/dev/log,tag=autoprovision-firmware combined;
393+
limit_except GET HEAD { deny all; }
394+
}
395+
380396
location = /pbxcore/index.php {
381397
internal;
382398
fastcgi_pass unix:/var/run/php-fpm.sock;

Lib/AutoprovisionFanvil.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,14 @@ public function generateConfig($req_data, $sip_peers): string
7575
$cfg_arr[] = 'PNP Port :5060';
7676
$cfg_arr[] = 'PNP Transport :0';
7777
$cfg_arr[] = 'PNP Interval :1';
78+
// Fanvil firmware URL key. Two-image phones split between application
79+
// image and configuration; we only set the firmware ('app') URL —
80+
// admins can override via additional_params if they need both.
81+
$firmwareUrl = (string)($req_data['firmware_url'] ?? '');
82+
if ($firmwareUrl !== '') {
83+
$cfg_arr[] = 'Upgrade Mode :2';
84+
$cfg_arr[] = 'App URL :' . $firmwareUrl;
85+
}
7886
$cfg_arr[] = ($s['fanvil-autoupdate'] ?? '') . "\n";
7987
$cfg_arr[] = '<<END OF FILE>>';
8088

Lib/AutoprovisionGrandstream.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,15 @@ public function generateConfig($req_data, $sip_peers): string
6161
// переподписывался на чужой провижн-сервер после первой настройки.
6262
$cfg .= " <P1359>0</P1359>\n";
6363

64+
// Grandstream firmware: P192 is the firmware server URL, P237 forces
65+
// HTTP (1) instead of TFTP (0). Only set when a row exists; otherwise
66+
// the phone keeps using its current upgrade settings.
67+
$firmwareUrl = (string)($req_data['firmware_url'] ?? '');
68+
if ($firmwareUrl !== '') {
69+
$cfg .= " <P192>" . htmlspecialchars($firmwareUrl, ENT_XML1) . "</P192>\n";
70+
$cfg .= " <P237>1</P237>\n";
71+
}
72+
6473
$extra = trim($s['grandstream'] ?? '');
6574
if ($extra !== '') {
6675
$cfg .= $extra . "\n";

Lib/AutoprovisionHtek.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ public function generateConfig($req_data, $sip_peers): string
6565
$featureAttendedTransfer = $this->mikoPBXConfig->getGeneralSettings('PBXFeatureAttendedTransfer');
6666
$cfg .= "features.dtmf.transfer = {$featureAttendedTransfer}\r\n";
6767

68+
// Htek auto-image URL — the Htek firmware server config key.
69+
$firmwareUrl = (string)($req_data['firmware_url'] ?? '');
70+
if ($firmwareUrl !== '') {
71+
$cfg .= "auto_image_url = {$firmwareUrl}\r\n";
72+
}
73+
6874
$cfg .= ($s['htek'] ?? '') . "\n";
6975
file_put_contents($filename, $cfg);
7076

Lib/AutoprovisionSnom.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ public function generateConfig($req_data, $sip_peers): string
5050
}
5151
$cfg .= " <answer_after_policy perm=\"RW\">idle</answer_after_policy>\n";
5252

53+
// Snom firmware override — empty when no firmware row registered.
54+
$firmwareUrl = (string)($req_data['firmware_url'] ?? '');
55+
if ($firmwareUrl !== '') {
56+
$cfg .= " <firmware perm=\"RW\">"
57+
. htmlspecialchars($firmwareUrl, ENT_XML1)
58+
. "</firmware>\n";
59+
}
60+
5361
$cfg .= ($s['snom-phone-settings'] ?? '') . "\n";
5462
$cfg .= ' </phone-settings>' . "\n";
5563
$cfg .= '</settings>' . "\n";

Lib/AutoprovisionYealink.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,13 @@ public function generateConfig($req_data, $sip_peers): string
9494
// Enable or disable the headset prior feature; 0-Disabled (default), 1-Enabled;
9595
$cfg .= "features.headset_prior = 1\r\n";
9696

97+
// Vendor firmware URL — emitted only when a matching firmware row was
98+
// registered by the admin. Yealink's reference key is `firmware.url`.
99+
$firmwareUrl = (string)($req_data['firmware_url'] ?? '');
100+
if ($firmwareUrl !== '') {
101+
$cfg .= "firmware.url = {$firmwareUrl}\r\n";
102+
}
103+
97104
$cfg .= ($s['yealink'] ?? '') . "\n";
98105
file_put_contents($filename, $cfg);
99106

Lib/RestAPI/Controllers/GetController.php

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
use MikoPBX\Modules\PbxExtensionUtils;
1818
use MikoPBX\PBXCoreREST\Controllers\Modules\ModulesControllerBase;
1919
use MikoPBX\Common\Library\Text;
20+
use Modules\ModuleAutoprovision\Lib\RestAPI\Firmware\Repository as FirmwareRepository;
2021
use Modules\ModuleAutoprovision\Lib\Transliterate;
22+
use Modules\ModuleAutoprovision\Models\ModuleAutoprovisionDevice;
2123
use Modules\ModuleAutoprovision\Models\OtherPBX;
2224
use Modules\ModuleAutoprovision\Models\Templates;
2325
use Modules\ModuleAutoprovision\Models\TemplatesUri;
@@ -160,6 +162,25 @@ public function getConfigStatic(...$args):void
160162
if(!empty($resultSip)){
161163
$data = $resultSip[0];
162164
}
165+
166+
// Firmware URL placeholder. Vendor is sniffed from the User-Agent
167+
// (every supported phone family includes its brand there) and the
168+
// exact model comes from the device row keyed by MAC. Missing data
169+
// resolves to empty string, which lets templates wrap the line in
170+
// a vendor-specific conditional if needed.
171+
$vendor = $this->detectVendorFromUserAgent((string)$userAgent);
172+
$model = null;
173+
if ($mac !== '') {
174+
$device = ModuleAutoprovisionDevice::findFirst([
175+
'mac = :mac:',
176+
'bind' => ['mac' => $mac],
177+
]);
178+
if ($device !== null && !empty($device->manufacturer_model)) {
179+
$model = $this->extractModel((string)$device->manufacturer_model);
180+
}
181+
}
182+
$data['{FIRMWARE_URL}'] = FirmwareRepository::resolveFirmwareUrl($vendor, $model);
183+
163184
$config = str_replace(array_keys($data), array_values($data), $result[0]['template']);
164185
$this->response->setHeader('Content-Description', "config file");
165186
$this->response->setHeader('Content-Disposition', "attachment; filename=".basename($uri));
@@ -372,6 +393,57 @@ private function echoPhoneBookYealink($uri):void
372393
echo $phoneBook;
373394
}
374395

396+
/**
397+
* Best-effort vendor key derived from the User-Agent string sent by the phone.
398+
*
399+
* Every supported phone family identifies itself in User-Agent (Yealink, Snom,
400+
* Fanvil, Grandstream, Htek). Returns '' if no known brand matches — callers
401+
* then resolve {FIRMWARE_URL} to an empty string instead of guessing.
402+
*/
403+
private function detectVendorFromUserAgent(string $userAgent): string
404+
{
405+
$ua = strtolower($userAgent);
406+
foreach (['yealink', 'snom', 'fanvil', 'grandstream', 'htek'] as $vendor) {
407+
if (str_contains($ua, $vendor)) {
408+
return $vendor;
409+
}
410+
}
411+
return '';
412+
}
413+
414+
/**
415+
* Extracts the model name from ModuleAutoprovisionDevice.manufacturer_model.
416+
*
417+
* PnP populates the field as "Vendor / Model" (e.g. "Fanvil / X3SP V2") and
418+
* some models contain spaces, so we can't just take the last whitespace token
419+
* — that would shrink "X3SP V2" to "V2". When a "/" separator is present we
420+
* take everything after it; otherwise we drop a leading vendor word that
421+
* matches one of the known vendors and keep the rest. Returns null when the
422+
* field is empty.
423+
*/
424+
private function extractModel(string $manufacturerModel): ?string
425+
{
426+
$raw = trim($manufacturerModel);
427+
if ($raw === '') {
428+
return null;
429+
}
430+
431+
$slash = strpos($raw, '/');
432+
if ($slash !== false) {
433+
$tail = trim(substr($raw, $slash + 1));
434+
return $tail === '' ? null : $tail;
435+
}
436+
437+
// No separator: strip a leading known-vendor token if present.
438+
$parts = preg_split('/\s+/', $raw) ?: [];
439+
if ($parts !== [] && in_array(strtolower($parts[0]), ['yealink', 'snom', 'fanvil', 'grandstream', 'htek'], true)) {
440+
array_shift($parts);
441+
}
442+
$tail = implode(' ', $parts);
443+
$tail = trim($tail);
444+
return $tail === '' ? null : $tail;
445+
}
446+
375447
private function camelize(string $inputString): string
376448
{
377449
$inputString = preg_replace('/\s+/', '-', $inputString);

0 commit comments

Comments
 (0)