#441 SMS sending

This commit is contained in:
Roland Gruber 2025-05-27 17:36:17 +02:00
parent 41f7de9866
commit 6e1ceabba6
8 changed files with 366 additions and 2 deletions

View file

@ -355,6 +355,18 @@ $helpArray = [
"295" => ["Headline" => _("Show deleted entries"), "295" => ["Headline" => _("Show deleted entries"),
"Text" => _("This enables to show deleted entries in \"CN=Deleted Objects\" for Active Directory.") "Text" => _("This enables to show deleted entries in \"CN=Deleted Objects\" for Active Directory.")
], ],
"296" => ["Headline" => _("SMS provider"),
"Text" => _("Please select the SMS provider that should be used for password and reset link sending.")
],
"297" => ["Headline" => _("SMS API key"),
"Text" => _("Please enter the API key of your SMS provider.")
],
"298" => ["Headline" => _("SMS API token"),
"Text" => _("Please enter the API token of your SMS provider.")
],
"299" => ["Headline" => _("Mobile phone attributes"),
"Text" => _("Please enter the LDAP attributes that should be checked to identify the user's mobile phone number.") . ' ' . _("Multiple values are separated by semicolon.")
],
// 300 - 399 // 300 - 399
// profile/PDF editor, file upload // profile/PDF editor, file upload
"301" => ["Headline" => _("RDN identifier"), "301" => ["Headline" => _("RDN identifier"),

View file

@ -2961,6 +2961,8 @@ class LAMCfgMain {
public const MAIL_ATTRIBUTE_DEFAULT = 'mail'; public const MAIL_ATTRIBUTE_DEFAULT = 'mail';
public const MAIL_BACKUP_ATTRIBUTE_DEFAULT = 'passwordselfresetbackupmail'; public const MAIL_BACKUP_ATTRIBUTE_DEFAULT = 'passwordselfresetbackupmail';
public const SMS_ATTRIBUTES_DEFAULT = 'mobileTelephoneNumber;mobile;pager';
/** Default profile */ /** Default profile */
public $default; public $default;
@ -3066,6 +3068,18 @@ class LAMCfgMain {
*/ */
public string $mailBackupAttribute = self::MAIL_BACKUP_ATTRIBUTE_DEFAULT; public string $mailBackupAttribute = self::MAIL_BACKUP_ATTRIBUTE_DEFAULT;
/** @var string SMS provider ID */
public string $smsProvider = '';
/** @var string SMS API key */
public string $smsApiKey = '';
/** @var string SMS token */
public string $smsToken = '';
/** @var string SMS number attributes */
public string $smsAttributes = self::SMS_ATTRIBUTES_DEFAULT;
/** database type */ /** database type */
public $configDatabaseType = self::DATABASE_FILE_SYSTEM; public $configDatabaseType = self::DATABASE_FILE_SYSTEM;
@ -3103,7 +3117,8 @@ class LAMCfgMain {
'mailAttribute', 'mailBackupAttribute', 'mailAttribute', 'mailBackupAttribute',
'configDatabaseType', 'configDatabaseType',
'configDatabaseServer', 'configDatabasePort', 'configDatabaseName', 'configDatabaseUser', 'configDatabaseServer', 'configDatabasePort', 'configDatabaseName', 'configDatabaseUser',
'configDatabasePassword', 'configDatabaseSSLCA', 'moduleSettings' 'configDatabasePassword', 'configDatabaseSSLCA', 'moduleSettings',
'smsProvider', 'smsApiKey', 'smsToken', 'smsAttributes',
]; ];
/** persistence settings are always stored on local file system */ /** persistence settings are always stored on local file system */
@ -3744,6 +3759,18 @@ class LAMCfgMain {
return self::MAIL_BACKUP_ATTRIBUTE_DEFAULT; return self::MAIL_BACKUP_ATTRIBUTE_DEFAULT;
} }
/**
* Returns the SMS attributes.
*
* @return string[] attribute names
*/
public function getSmsAttributes(): array {
if (!empty($this->smsAttributes)) {
return preg_split('/;[ ]*/', $this->smsAttributes);
}
return preg_split('/;[ ]*/', self::SMS_ATTRIBUTES_DEFAULT);
}
/** /**
* Returns a list of module settings. * Returns a list of module settings.
* *

View file

@ -1565,8 +1565,9 @@ class htmlSelect extends htmlElement {
if (!empty($this->tableRowsToShow)) { if (!empty($this->tableRowsToShow)) {
$values = array_merge($values, array_keys($this->tableRowsToShow)); $values = array_merge($values, array_keys($this->tableRowsToShow));
} }
$values = array_unique($values);
$selector = $this->getShowHideSelector(); $selector = $this->getShowHideSelector();
// build Java script to show/hide depending fields // build JavaScript to show/hide depending on fields
foreach ($values as $val) { foreach ($values as $val) {
// build onChange listener // build onChange listener
$onChange .= 'if (document.getElementById(\'' . $this->name . '\').value == \'' . $val . '\') {'; $onChange .= 'if (document.getElementById(\'' . $this->name . '\').value == \'' . $val . '\') {';

View file

@ -0,0 +1,116 @@
<?php
namespace LAM\PLUGINS\SMS;
/*
This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/)
Copyright (C) 2025 Roland Gruber
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
/**
* SMS providers.
*
* @author Roland Gruber
*/
/**
* Provides SMS services.
*
* @package LAM\PLUGINS\SMS
*/
class SmsService {
/**
* Includes all plugin files.
*/
private function includeFiles(): void {
$pluginDir = dir(__DIR__);
while ($entry = $pluginDir->read()) {
if ((str_starts_with($entry, '.')) || ($entry === basename(__FILE__))) {
continue;
}
include_once(__DIR__ . '/' . $entry);
}
}
/**
* Returns a list of SmsProvider objects.
*
* @return SmsProvider[] providers (id => provider object)
*/
public function findProviders(): array {
$this->includeFiles();
$providers = [];
foreach (get_declared_classes() as $declaredClass) {
if (in_array('LAM\PLUGINS\SMS\SmsProvider', class_implements($declaredClass))) {
$provider = new $declaredClass();
$providers[$provider->getId()] = $provider;
}
}
return $providers;
}
}
/**
* Interface for providers of SMS services.
*
* @package LAM\PLUGINS\SMS
*/
interface SmsProvider {
/**
* Returns the label of the service
*
* @return string label
*/
public function getLabel(): string;
/**
* Returns the id of the service
*
* @return string id
*/
public function getId(): string;
/**
* Returns if the provider requires a token.
*
* @return bool token required
*/
public function usesApiToken(): bool;
/**
* Returns if the provider requires an API key.
*
* @return bool key required
*/
public function usesApiKey(): bool;
/**
* Returns the connection string for the SMS service.
*
* @param string|null $apiKey API key
* @param string|null $apiToken API token
* @return string connection string
*/
public function getConnectionString(?string $apiKey = '', ?string $apiToken = ''): string;
}

View file

@ -0,0 +1,74 @@
<?php
namespace LAM\PLUGINS\SMS;
/*
This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/)
Copyright (C) 2025 Roland Gruber
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
/**
* Sweego SMS provider.
*
* @author Roland Gruber
*/
/**
* Sweego SMS provider.
*
* @package LAM\PLUGINS\CAPTCHA
*/
class SweegoSms implements SmsProvider {
/**
* @inheritDoc
*/
public function getLabel(): string {
return 'Sweego';
}
/**
* @inheritDoc
*/
public function getId(): string {
return 'sweego';
}
/**
* @inheritDoc
*/
public function getConnectionString(?string $apiKey = '', ?string $apiToken = ''): string {
return "sweego://$apiKey@default";
}
/**
* @inheritDoc
*/
public function usesApiToken(): bool {
return false;
}
/**
* @inheritDoc
*/
public function usesApiKey(): bool {
return true;
}
}

View file

@ -6,6 +6,7 @@ use htmlJavaScript;
use htmlResponsiveTable; use htmlResponsiveTable;
use LAM\LOGIN\WEBAUTHN\WebauthnManager; use LAM\LOGIN\WEBAUTHN\WebauthnManager;
use LAM\PERSISTENCE\ConfigurationDatabase; use LAM\PERSISTENCE\ConfigurationDatabase;
use LAM\PLUGINS\SMS\SmsService;
use LAMCfgMain; use LAMCfgMain;
use htmlTable; use htmlTable;
use htmlTitle; use htmlTitle;
@ -68,6 +69,8 @@ include_once(__DIR__ . '/../../lib/config.inc');
include_once(__DIR__ . '/../../lib/status.inc'); include_once(__DIR__ . '/../../lib/status.inc');
/** LAM Pro */ /** LAM Pro */
include_once(__DIR__ . '/../../lib/selfService.inc'); include_once(__DIR__ . '/../../lib/selfService.inc');
/** SMS */
include_once __DIR__ . '/../../lib/plugins/sms/SmsProvider.inc';
// start session // start session
if (isFileBasedSession()) { if (isFileBasedSession()) {
@ -408,6 +411,13 @@ if (isset($_POST['submitFormData'])) {
$errors[] = _('The mail attributes are invalid.'); $errors[] = _('The mail attributes are invalid.');
} }
} }
// SMS settings
if (isLAMProVersion()) {
$cfg->smsProvider = $_POST['smsProvider'];
$cfg->smsAttributes = $_POST['smsAttributes'];
$cfg->smsApiKey = $_POST['smsApiKey'];
$cfg->smsToken = $_POST['smsApiToken'];
}
$cfg->errorReporting = $_POST['errorReporting']; $cfg->errorReporting = $_POST['errorReporting'];
// module settings // module settings
$allModules = getAllModules(); $allModules = getAllModules();
@ -723,6 +733,59 @@ if (isset($_POST['submitFormData'])) {
$row->add($testDialogDiv); $row->add($testDialogDiv);
} }
// SMS options
if (isLAMProVersion()) {
$row->add(new htmlSubTitle(_('SMS options')));
$smsService = new SmsService();
$smsProviders = $smsService->findProviders();
$smsProviderOptions = [
'-' => '',
];
$smsOptionsToHide = [
'' => ['smsApiKey', 'smsApiToken', 'smsAttributes', 'btn_testSms']
];
$smsOptionsToShow = [];
foreach ($smsProviders as $provider) {
$smsProviderOptions[$provider->getLabel()] = $provider->getId();
$smsOptionsToShow[$provider->getId()][] = 'smsAttributes';
$smsOptionsToShow[$provider->getId()][] = 'btn_testSms';
if ($provider->usesApiToken()) {
$smsOptionsToShow[$provider->getId()][] = 'smsApiToken';
}
else {
$smsOptionsToHide[$provider->getId()][] = 'smsApiToken';
}
if ($provider->usesApiKey()) {
$smsOptionsToShow[$provider->getId()][] = 'smsApiKey';
}
else {
$smsOptionsToHide[$provider->getId()][] = 'smsApiKey';
}
}
$selectedSmsProvider = empty($cfg->smsProvider) ? '' : $cfg->smsProvider;
$smsProviderSelect = new htmlResponsiveSelect('smsProvider', $smsProviderOptions, [$selectedSmsProvider], _('SMS provider'), '296');
$smsProviderSelect->setHasDescriptiveElements(true);
$smsProviderSelect->setTableRowsToHide($smsOptionsToHide);
$smsProviderSelect->setTableRowsToShow($smsOptionsToShow);
$row->add($smsProviderSelect);
$row->add(new htmlResponsiveInputField(_('SMS API key'), 'smsApiKey', $cfg->smsApiKey, '297'));
$row->add(new htmlResponsiveInputField(_('SMS API token'), 'smsApiToken', $cfg->smsToken, '298'));
$row->add(new htmlResponsiveInputField(_("Mobile phone attributes"), 'smsAttributes', implode(';', $cfg->getSmsAttributes()), '299'));
$smsTestButtonRow = new htmlResponsiveRow();
$smsTestButton = new htmlButton('testSms', _('Test settings'));
$smsTestButton->setOnClick("window.lam.sms.test(event, '" . getSecurityTokenName()
. "', '" . getSecurityTokenValue() . "', '" . _('Ok') . "', '" . _('Cancel') . "', '" . _('Test settings') . "')");
$smsTestButtonRow->addLabel(new htmlOutputText("&nbsp;", false));
$smsTestButtonRow->addField($smsTestButton);
$row->add($smsTestButtonRow);
$smsTestDialogDivContent = new htmlResponsiveRow();
$smsTestNumber = new htmlResponsiveInputField(_('Mobile number'), 'testSmsNumber', null, null, true);
$smsTestNumber->setType('tel');
$smsTestDialogDivContent->add($smsTestNumber);
$smsTestDialogDiv = new htmlDiv('smsTestDialogDiv', $smsTestDialogDivContent, ['hidden']);
$row->add($smsTestDialogDiv);
}
// webauthn management // webauthn management
if (extension_loaded('PDO') if (extension_loaded('PDO')
&& in_array('sqlite', PDO::getAvailableDrivers())) { && in_array('sqlite', PDO::getAvailableDrivers())) {

View file

@ -3495,6 +3495,56 @@ window.lam.smtp.test = function(event, tokenName, tokenValue, okText, cancelText
window.lam.dialog.showConfirmation(title, okText, cancelText, 'smtpTestDialogDiv', runTestCallback, runTestPreCallback); window.lam.dialog.showConfirmation(title, okText, cancelText, 'smtpTestDialogDiv', runTestCallback, runTestPreCallback);
} }
window.lam.sms = window.lam.sms || {};
/**
* Tests the SMS settings.
*
* @param event event
* @param tokenName security token name
* @param tokenValue security token value
* @param okText text to close dialog
* @param cancelText text to cancel test
* @param title dialog title
*/
window.lam.sms.test = function(event, tokenName, tokenValue, okText, cancelText, title) {
event.preventDefault();
const runTestPreCallback = function() {
const number = document.getElementById('testSmsNumber').value;
if (!number) {
return false;
}
return {
number: number
}
}
const runTestCallback = function(formData) {
document.getElementById('btn_testSms').disabled = true;
let data = new FormData();
data.append(tokenName, tokenValue);
data.append('provider', document.getElementById('smsProvider').value);
data.append('apiKey', document.getElementById('smsApiKey').value);
data.append('apiToken', document.getElementById('smsApiToken').value);
data.append('number', formData.number);
const url = '../misc/ajax.php?function=testSms';
fetch(url, {
method: 'POST',
body: data
})
.then(async response => {
const jsonData = await response.json();
if (jsonData.info) {
window.lam.dialog.showInfo(jsonData.info, okText);
}
else if (jsonData.error) {
window.lam.dialog.showError(jsonData.error, jsonData.details, okText);
}
document.getElementById('btn_testSms').disabled = false;
});
}
window.lam.dialog.showConfirmation(title, okText, cancelText, 'smsTestDialogDiv', runTestCallback, runTestPreCallback);
}
window.lam.config = window.lam.config || {}; window.lam.config = window.lam.config || {};
window.lam.config.updateModuleFilter = function(inputField) { window.lam.config.updateModuleFilter = function(inputField) {

View file

@ -80,6 +80,27 @@ class LAMCfgMainTest extends TestCase {
$this->assertEquals('test2', $this->conf->getMailBackupAttribute()); $this->assertEquals('test2', $this->conf->getMailBackupAttribute());
} }
/**
* SMS related settings
* @throws LAMException error saving config
*/
public function testSms() {
$this->assertEquals(explode(';', LAMCfgMain::SMS_ATTRIBUTES_DEFAULT), $this->conf->getSmsAttributes());
$this->conf->smsAttributes = 'mobile;pager';
$this->conf->smsProvider = 'sweego';
$this->conf->smsToken = 'token';
$this->conf->smsApiKey = 'key';
$this->conf->save();
$this->conf = new LAMCfgMain($this->file);
$this->assertEquals('sweego', $this->conf->smsProvider);
$this->assertEquals('token', $this->conf->smsToken);
$this->assertEquals('key', $this->conf->smsApiKey);
$this->assertEquals(['mobile', 'pager'], $this->conf->getSmsAttributes());
}
/** /**
* License related settings. * License related settings.
* @throws LAMException error saving config * @throws LAMException error saving config