From 6e1ceabba6e88d99c79a6d81181bb54bd2fe5b40 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Tue, 27 May 2025 17:36:17 +0200 Subject: [PATCH] #441 SMS sending --- lam/help/help.inc | 12 +++ lam/lib/config.inc | 29 ++++++- lam/lib/html.inc | 3 +- lam/lib/plugins/sms/SmsProvider.inc | 116 ++++++++++++++++++++++++++++ lam/lib/plugins/sms/SweegoSms.inc | 74 ++++++++++++++++++ lam/templates/config/mainmanage.php | 63 +++++++++++++++ lam/templates/lib/500_lam.js | 50 ++++++++++++ lam/tests/lib/LAMCfgMainTest.php | 21 +++++ 8 files changed, 366 insertions(+), 2 deletions(-) create mode 100644 lam/lib/plugins/sms/SmsProvider.inc create mode 100644 lam/lib/plugins/sms/SweegoSms.inc diff --git a/lam/help/help.inc b/lam/help/help.inc index d9938dccd..0d356cfb3 100644 --- a/lam/help/help.inc +++ b/lam/help/help.inc @@ -355,6 +355,18 @@ $helpArray = [ "295" => ["Headline" => _("Show deleted entries"), "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 // profile/PDF editor, file upload "301" => ["Headline" => _("RDN identifier"), diff --git a/lam/lib/config.inc b/lam/lib/config.inc index 42e588b19..422b5f8af 100644 --- a/lam/lib/config.inc +++ b/lam/lib/config.inc @@ -2961,6 +2961,8 @@ class LAMCfgMain { public const MAIL_ATTRIBUTE_DEFAULT = 'mail'; public const MAIL_BACKUP_ATTRIBUTE_DEFAULT = 'passwordselfresetbackupmail'; + public const SMS_ATTRIBUTES_DEFAULT = 'mobileTelephoneNumber;mobile;pager'; + /** Default profile */ public $default; @@ -3066,6 +3068,18 @@ class LAMCfgMain { */ 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 */ public $configDatabaseType = self::DATABASE_FILE_SYSTEM; @@ -3103,7 +3117,8 @@ class LAMCfgMain { 'mailAttribute', 'mailBackupAttribute', 'configDatabaseType', 'configDatabaseServer', 'configDatabasePort', 'configDatabaseName', 'configDatabaseUser', - 'configDatabasePassword', 'configDatabaseSSLCA', 'moduleSettings' + 'configDatabasePassword', 'configDatabaseSSLCA', 'moduleSettings', + 'smsProvider', 'smsApiKey', 'smsToken', 'smsAttributes', ]; /** persistence settings are always stored on local file system */ @@ -3744,6 +3759,18 @@ class LAMCfgMain { 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. * diff --git a/lam/lib/html.inc b/lam/lib/html.inc index a3c973055..49c384e5e 100644 --- a/lam/lib/html.inc +++ b/lam/lib/html.inc @@ -1565,8 +1565,9 @@ class htmlSelect extends htmlElement { if (!empty($this->tableRowsToShow)) { $values = array_merge($values, array_keys($this->tableRowsToShow)); } + $values = array_unique($values); $selector = $this->getShowHideSelector(); - // build Java script to show/hide depending fields + // build JavaScript to show/hide depending on fields foreach ($values as $val) { // build onChange listener $onChange .= 'if (document.getElementById(\'' . $this->name . '\').value == \'' . $val . '\') {'; diff --git a/lam/lib/plugins/sms/SmsProvider.inc b/lam/lib/plugins/sms/SmsProvider.inc new file mode 100644 index 000000000..2724e7ae6 --- /dev/null +++ b/lam/lib/plugins/sms/SmsProvider.inc @@ -0,0 +1,116 @@ +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; + +} + diff --git a/lam/lib/plugins/sms/SweegoSms.inc b/lam/lib/plugins/sms/SweegoSms.inc new file mode 100644 index 000000000..055a9245b --- /dev/null +++ b/lam/lib/plugins/sms/SweegoSms.inc @@ -0,0 +1,74 @@ +smsProvider = $_POST['smsProvider']; + $cfg->smsAttributes = $_POST['smsAttributes']; + $cfg->smsApiKey = $_POST['smsApiKey']; + $cfg->smsToken = $_POST['smsApiToken']; + } $cfg->errorReporting = $_POST['errorReporting']; // module settings $allModules = getAllModules(); @@ -723,6 +733,59 @@ if (isset($_POST['submitFormData'])) { $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(" ", 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 if (extension_loaded('PDO') && in_array('sqlite', PDO::getAvailableDrivers())) { diff --git a/lam/templates/lib/500_lam.js b/lam/templates/lib/500_lam.js index b780f9ad4..57254048f 100644 --- a/lam/templates/lib/500_lam.js +++ b/lam/templates/lib/500_lam.js @@ -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.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.updateModuleFilter = function(inputField) { diff --git a/lam/tests/lib/LAMCfgMainTest.php b/lam/tests/lib/LAMCfgMainTest.php index dc2d4b0c3..e8be2370b 100644 --- a/lam/tests/lib/LAMCfgMainTest.php +++ b/lam/tests/lib/LAMCfgMainTest.php @@ -80,6 +80,27 @@ class LAMCfgMainTest extends TestCase { $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. * @throws LAMException error saving config