<?php
namespace LAM\CONFIG;

use Exception;
use htmlJavaScript;
use htmlResponsiveTable;
use LAM\LOGIN\WEBAUTHN\WebauthnManager;
use LAM\PERSISTENCE\ConfigurationDatabase;
use LAM\PLUGINS\SMS\SmsService;
use LAMCfgMain;
use htmlTable;
use htmlTitle;
use htmlStatusMessage;
use htmlSubTitle;
use htmlSpacer;
use htmlOutputText;
use htmlLink;
use htmlGroup;
use htmlButton;
use htmlHelpLink;
use htmlInputField;
use htmlInputFileUpload;
use DateTime;
use DateTimeZone;
use htmlResponsiveRow;
use htmlResponsiveInputTextarea;
use htmlResponsiveSelect;
use htmlResponsiveInputCheckbox;
use htmlResponsiveInputField;
use htmlDiv;
use htmlHiddenInput;
use LAMConfig;
use LAMException;
use LamTemporaryFilesManager;
use PDO;

/*

  This code is part of LDAP Account Manager (http://www.ldap-account-manager.org/)
  Copyright (C) 2003 - 2026  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

*/


/**
 * Manages the main configuration options.
 *
 * @package configuration
 * @author Roland Gruber
 */


/** Access to config functions */
include_once(__DIR__ . '/../../lib/config.inc');
/** Used to print status messages */
include_once(__DIR__ . '/../../lib/status.inc');
/** LAM Pro */
include_once(__DIR__ . '/../../lib/selfService.inc');
/** SMS */
include_once __DIR__ . '/../../lib/plugins/sms/SmsService.inc';

// start session
if (isFileBasedSession()) {
	session_save_path("../../sess");
}
lam_start_session();

setlanguage();

if (!isset($_SESSION['cfgMain'])) {
	$cfg = new LAMCfgMain();
	$_SESSION['cfgMain'] = $cfg;
}
$cfg = &$_SESSION['cfgMain'];
if (!($cfg instanceof LAMCfgMain)) {
    die();
}

// check if user is logged in
if (!isset($_SESSION["mainconf_password"]) || (!$cfg->checkPassword($_SESSION["mainconf_password"]))) {
	require(__DIR__ . '/mainlogin.php');
	exit();
}

if (isset($_POST['cancel'])) {
	// back to login
	metaRefresh('../login.php');
	exit();
}

if (isset($_SESSION['header'])) {
	echo $_SESSION['header'];
}
printHeaderContents(_("Edit general settings"), '../..');
?>
</head>
<body>
<div id="lam-topnav" class="lam-header">
    <div class="lam-header-left lam-menu-stay">
        <a href="https://www.ldap-account-manager.org/" target="new_window">
            <img class="align-middle" width="24" height="24" alt="help" src="../../graphics/logo.svg">
            <span class="hide-on-mobile">
                        <?php
						echo getLAMVersionText();
						?>
                    </span>
        </a>
    </div>
	<?php
	if (is_dir(__DIR__ . '/../../docs/manual')) {
		?>
        <a class="lam-header-right lam-menu-icon hide-on-tablet" href="javascript:void(0);" class="icon"
           onclick="window.lam.topmenu.toggle();">
            <img class="align-middle" width="16" height="16" alt="menu" src="../../graphics/menu.svg">
            <span class="padding0">&nbsp;</span>
        </a>
        <a class="lam-header-right lam-menu-entry" target="_blank" href="../../docs/manual/index.html">
            <span class="padding0"><?php echo _("Help") ?></span>
        </a>
		<?php
	}
	?>
</div>
<br>
<?php
// include all JavaScript files
printJsIncludes('../..');

$errors = [];
$messages = [];
// check if submit button was pressed
if (isset($_POST['submitFormData'])) {
	if (extension_loaded('PDO')) {
		// set database
		$cfg->configDatabaseType = $_POST['configDatabaseType'];
		$cfg->configDatabaseServer = $_POST['configDatabaseServer'];
		$cfg->configDatabaseSSLCA = $_POST['configDatabaseSSLCA'];
		$cfg->configDatabasePort = $_POST['configDatabasePort'];
		$cfg->configDatabaseName = $_POST['configDatabaseName'];
		$cfg->configDatabaseUser = $_POST['configDatabaseUser'];
		$cfg->configDatabasePassword = $_POST['configDatabasePassword'];
		if ($cfg->configDatabaseType === LAMCfgMain::DATABASE_MYSQL) {
            $mySqlConfigOk = true;
			if (empty($cfg->configDatabaseServer) || !get_preg($cfg->configDatabaseServer, 'hostname')) {
				$errors[] = _('Please enter a valid database host name.');
                $mySqlConfigOk = false;
			}
			if (empty($cfg->configDatabaseName)) {
				$errors[] = _('Please enter a valid database name.');
				$mySqlConfigOk = false;
			}
			if (empty($cfg->configDatabaseUser)) {
				$errors[] = _('Please enter a valid database user.');
				$mySqlConfigOk = false;
			}
			if (empty($cfg->configDatabasePassword)) {
				$errors[] = _('Please enter a valid database password.');
				$mySqlConfigOk = false;
			}
            if ($mySqlConfigOk) {
				try {
					$configurationDatabase = new ConfigurationDatabase($cfg);
					$configurationDatabase->checkConnection();
				}
				catch (Exception $e) {
					logNewMessage(LOG_ERR, 'Unable to connect to configuration database: ' . $e->getMessage());
					$errors[] = _('Unable to connect to configuration database.');
				}
            }
		}
	}
	// set master password
	if (!empty($_POST['masterpassword'])) {
		if (($_POST['masterpassword'] !== $_POST['masterpassword2'])) {
			$errors[] = _("Master passwords are different.");
		}
        elseif (!isValidConfigurationPassword($_POST['masterpassword'])) {
            $errors[] = _('Please enter at least 8 characters including letters, a number and a symbol.');
        }
		else {
			$cfg->setPassword($_POST['masterpassword']);
			$msg = _("New master password set successfully.");
			unset($_SESSION["mainconf_password"]);
		}
	}
	// set license
	if (isLAMProVersion()) {
		$licenseLines = explode("\n", $_POST['license']);
		$licenseLines = array_map(trim(...), $licenseLines);
		$cfg->setLicenseLines($licenseLines);
		$cfg->licenseWarningType = $_POST['licenseWarningType'];
		$cfg->licenseEmailFrom = $_POST['licenseEmailFrom'];
		$cfg->licenseEmailTo = $_POST['licenseEmailTo'];
		if ((($cfg->licenseWarningType === LAMCfgMain::LICENSE_WARNING_EMAIL) || ($cfg->licenseWarningType === LAMCfgMain::LICENSE_WARNING_ALL))
			&& !get_preg($cfg->licenseEmailFrom, 'email')) {
			$errors[] = _('Licence') . ': ' . _('From address') . ' - ' . _('Please enter a valid email address!');
		}
		if (($cfg->licenseWarningType === LAMCfgMain::LICENSE_WARNING_EMAIL) || ($cfg->licenseWarningType === LAMCfgMain::LICENSE_WARNING_ALL)) {
			$toEmails = preg_split('/;[ ]*/', $cfg->licenseEmailTo);
			if ($toEmails !== false) {
				foreach ($toEmails as $toEmail) {
					if (!get_preg($toEmail, 'email')) {
						$errors[] = _('Licence') . ': ' . _('To address') . ' - ' . _('Please enter a valid email address!');
						break;
					}
				}
			}
		}
	}
	// set session timeout
	$cfg->sessionTimeout = intval($_POST['sessionTimeout']);
	// set hide login error details
	$cfg->hideLoginErrorDetails = (isset($_POST['hideLoginErrorDetails']) && ($_POST['hideLoginErrorDetails'] === 'on')) ? 'true' : 'false';
	// set allowed hosts
	if (isset($_POST['allowedHosts'])) {
		$allowedHosts = $_POST['allowedHosts'];
		$allowedHostsList = explode("\n", $allowedHosts);
		$allowedHostsListNew = [];
		foreach ($allowedHostsList as $allowedHostEntry) {
			$allowedHostEntry = trim($allowedHostEntry);
			// ignore empty lines
			if ($allowedHostEntry === '') {
				continue;
			}
			// check each line
			$ipRegex = '/^[0-9a-f\\.:\\*]+$/i';
			if (!preg_match($ipRegex, $allowedHostEntry) || (strlen($allowedHostEntry) > 45)) {
				$errors[] = sprintf(_("The IP address %s is invalid!"), htmlspecialchars(str_replace('%', '%%', $allowedHostEntry)));
			}
            $allowedHostsListNew[] = $allowedHostEntry;
		}
		$allowedHostsList = array_unique($allowedHostsListNew);
		$allowedHosts = implode(",", $allowedHostsList);
	}
	else {
		$allowedHosts = "";
	}
	$cfg->allowedHosts = $allowedHosts;
	// set allowed hosts for self-service
	if (isLAMProVersion()) {
		if (isset($_POST['allowedHostsSelfService'])) {
			$allowedHostsSelfService = $_POST['allowedHostsSelfService'];
			$allowedHostsSelfServiceList = explode("\r\n", $allowedHostsSelfService);
			$allowedHostsSelfServiceListNew = [];
			foreach ($allowedHostsSelfServiceList as $allowedHostsSelfServiceEntry) {
				$allowedHostsSelfServiceEntry = trim($allowedHostsSelfServiceEntry);
				// ignore empty lines
				if ($allowedHostsSelfServiceEntry === '') {
					continue;
				}
				// check each line
				$ipRegex = '/^[0-9a-f\\.:\\*]+$/i';
				if (!preg_match($ipRegex, $allowedHostsSelfServiceEntry) || (strlen($allowedHostsSelfServiceEntry) > 45)) {
					$errors[] = sprintf(_("The IP address %s is invalid!"), htmlspecialchars(str_replace('%', '%%', $allowedHostsSelfServiceEntry)));
				}
                $allowedHostsSelfServiceListNew[] = $allowedHostsSelfServiceEntry;
			}
			$allowedHostsSelfServiceList = array_unique($allowedHostsSelfServiceListNew);
			$allowedHostsSelfService = implode(",", $allowedHostsSelfServiceList);
		}
		else {
			$allowedHostsSelfService = "";
		}
		$cfg->allowedHostsSelfService = $allowedHostsSelfService;
	}
	// set log level
	$cfg->logLevel = intval($_POST['logLevel']);
	// set log destination
	if ($_POST['logDestination'] == "none") {
		$cfg->logDestination = "NONE";
	}
    elseif ($_POST['logDestination'] == "syslog") {
		$cfg->logDestination = "SYSLOG";
	}
    elseif ($_POST['logDestination'] == "remote") {
		$cfg->logDestination = "REMOTE:" . $_POST['logRemote'];
		$remoteParts = explode(':', $_POST['logRemote']);
		if ((count($remoteParts) !== 2) || !get_preg($remoteParts[0], 'DNSname') || !get_preg($remoteParts[1], 'digit')) {
			$errors[] = _("Please enter a valid remote server in format \"server:port\".");
		}
	}
	else {
		$isValidLogFile = isset($_POST['logFile']) && LAMCfgMain::isValidLogFilename($_POST['logFile']);
		$blockedPrefixes = ['/usr', '/etc', '/dev', '/boot', '/lib', '/proc', '/root', '/run', '/sys', '/snap'];
		if (!empty($_SERVER['DOCUMENT_ROOT'])) {
			$blockedPrefixes[] = $_SERVER['DOCUMENT_ROOT'];
		}
		foreach ($blockedPrefixes as $blockedPrefix) {
			if (!$isValidLogFile) {
				break;
			}
			if (str_starts_with($_POST['logFile'], $blockedPrefix)) {
				$isValidLogFile = false;
			}
		}
		if ($isValidLogFile) {
			$cfg->logDestination = $_POST['logFile'];
		}
		else {
			$errors[] = _("The log file is empty or contains invalid characters! Valid characters are: a-z, A-Z, 0-9, /, ., _ and -. The file must end with '.log' or '.txt'.");
		}
	}
	// password policies
	$cfg->passwordMinLength = intval($_POST['passwordMinLength']);
	$cfg->passwordMinLower = intval($_POST['passwordMinLower']);
	$cfg->passwordMinUpper = intval($_POST['passwordMinUpper']);
	$cfg->passwordMinNumeric = intval($_POST['passwordMinNumeric']);
	$cfg->passwordMinSymbol = intval($_POST['passwordMinSymbol']);
	$cfg->passwordMinClasses = intval($_POST['passwordMinClasses']);
	$cfg->checkedRulesCount = intval($_POST['passwordRulesCount']);
	$cfg->passwordMustNotContain3Chars = isset($_POST['passwordMustNotContain3Chars']) && ($_POST['passwordMustNotContain3Chars'] == 'on') ? 'true' : 'false';
	$cfg->passwordMustNotContainUser = isset($_POST['passwordMustNotContainUser']) && ($_POST['passwordMustNotContainUser'] == 'on') ? 'true' : 'false';
	if (function_exists('curl_init')) {
		$cfg->externalPwdCheckUrl = $_POST['externalPwdCheckUrl'];
		if (!empty($cfg->externalPwdCheckUrl) && (!str_contains($cfg->externalPwdCheckUrl, '{SHA1PREFIX}'))) {
			$errors[] = _('The URL for the external password check is invalid.');
		}
	}
	if (isset($_POST['sslCaCertUpload'])) {
		if (!isset($_FILES['sslCaCert']) || ($_FILES['sslCaCert']['size'] == 0)) {
			$errors[] = _('No file selected.');
		}
		else {
			$handle = fopen($_FILES['sslCaCert']['tmp_name'], "r");
			if ($handle === false) {
				$errors[] = _('Unable to create temporary file.');
			}
			else {
				$data = fread($handle, 10_000_000);
				if ($data === false) {
					$errors[] = _('Unable to create temporary file.');
				}
				else {
					fclose($handle);
					$sslReturn = $cfg->uploadSSLCaCert($data);
					if ($sslReturn !== true) {
						$errors[] = $sslReturn;
					}
					else {
						$messages[] = _('You might need to restart your webserver for changes to take effect.');
					}
				}
			}
		}
	}
	if (isset($_POST['sslCaCertDelete'])) {
		$cfg->deleteSSLCaCert();
		$messages[] = _('You might need to restart your webserver for changes to take effect.');
	}
	if (isset($_POST['sslCaCertImport'])) {
		$matches = [];
		if (preg_match('/^ldaps:\/\/([a-zA-Z0-9_.-]+)(:(\d+))?$/', $_POST['serverurl'], $matches)) {
			$port = '636';
			if (!empty($matches[3])) {
				$port = $matches[3];
			}
			$pemResult = getLDAPSSLCertificate($matches[1], $port);
			if ($pemResult !== false) {
				$messages[] = _('Imported certificate from server.');
				$messages[] = _('You might need to restart your webserver for changes to take effect.');
				$cfg->uploadSSLCaCert($pemResult);
			}
			else {
				$errors[] = _('Unable to import server certificate. Please use the upload function.');
			}
		}
		else {
			$errors[] = _('Invalid server name. Please enter "server" or "server:port".');
		}
	}
	foreach (array_keys($_POST) as $key) {
		if (str_starts_with($key, 'deleteCert_')) {
			$index = (int) substr($key, strlen('deleteCert_'));
			$cfg->deleteSSLCaCert($index);
		}
	}
	// mail settings
	if (isLAMProVersion()) {
		$cfg->mailUser = $_POST['mailUser'];
		$cfg->mailPassword = $_POST['mailPassword'];
		$cfg->mailEncryption = $_POST['mailEncryption'];
		$cfg->mailServer = $_POST['mailServer'];
		if (!empty($cfg->mailServer) && !get_preg($cfg->mailServer, 'hostAndPort')) {
			$errors[] = _('Please enter the mail server with host name and port.');
		}
		$mailAttribute = strtolower($_POST['mailAttribute']);
		$mailBackupAttribute = strtolower($_POST['mailBackupAttribute']);
		if (empty($mailAttribute)) {
			$cfg->mailAttribute = LAMCfgMain::MAIL_ATTRIBUTE_DEFAULT;
		}
        elseif (preg_match('/^[a-z0-9_-]+$/', $mailAttribute)) {
			$cfg->mailAttribute = $mailAttribute;
		}
		else {
			$errors[] = _('The mail attributes are invalid.');
		}
		if (empty($mailBackupAttribute)) {
			$cfg->mailBackupAttribute = LAMCfgMain::MAIL_BACKUP_ATTRIBUTE_DEFAULT;
		}
        elseif (preg_match('/^[a-z0-9_-]+$/', $mailBackupAttribute)) {
			$cfg->mailBackupAttribute = $mailBackupAttribute;
		}
		else {
			$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->smsAccountId = $_POST['smsAccountId'];
        $cfg->smsRegion = $_POST['smsRegion'];
        $cfg->smsFrom = $_POST['smsFrom'];
        $cfg->smsSubject = $_POST['smsSubject'];
        $cfg->smsDefaultCountryPrefix = $_POST['smsDefaultCountryPrefix'];
    }
	$cfg->errorReporting = $_POST['errorReporting'];
	// module settings
	$allModules = getAllModules();
	$moduleSettings = $cfg->getModuleSettings();
	foreach ($allModules as $module) {
		$module->checkGlobalConfigOptions($moduleSettings, $messages, $errors);
	}
	$cfg->setModuleSettings($moduleSettings);
	// save settings
	if (isset($_POST['submit']) && empty($errors)) {
		try {
				$cfg->save();
				$scriptTag = new htmlJavaScript('window.lam.dialog.showSuccessMessageAndRedirect("' . _("Your settings were successfully saved.") . '", "", "' . _('Ok') . '", "../login.php")');
				parseHtml(null, $scriptTag, [], false);
				echo '</body></html>';
				exit();
			}
			catch (LAMException $e) {
				$errors[] = $e->getTitle();
			}
	}
}

?>
<form class="text-center" enctype="multipart/form-data" action="mainmanage.php" method="post" novalidate="novalidate">

	<?php
	$row = new htmlResponsiveRow();
	$row->add(new htmlTitle(_('General settings')));

	// print messages
	foreach ($errors as $error) {
		$row->add(new htmlStatusMessage("ERROR", $error));
	}
	foreach ($messages as $message) {
		$row->add(new htmlStatusMessage("INFO", $message));
	}

	// check if config file is writable
	if (!$cfg->isWritable()) {
		$row->add(new htmlStatusMessage('WARN', _('The config file is not writable.'), _('Your changes cannot be saved until you make the file writable for the webserver user.')));
	}

	// database
	if (extension_loaded('PDO')) {
		$row->add(new htmlSubTitle(_('Configuration storage')));
		$storageProviders = [
			_('Local file system') => LAMCfgMain::DATABASE_FILE_SYSTEM
		];
		if (in_array('mysql', PDO::getAvailableDrivers())) {
			$storageProviders['MySQL'] = LAMCfgMain::DATABASE_MYSQL;
		}
		$storageProviderSelect = new htmlResponsiveSelect('configDatabaseType', $storageProviders, [$cfg->configDatabaseType], _('Database type'), '293');
		$storageProviderSelect->setHasDescriptiveElements(true);
		$dbRowsToShow = [
			LAMCfgMain::DATABASE_FILE_SYSTEM => [],
			LAMCfgMain::DATABASE_MYSQL => ['configDatabaseServer', 'configDatabasePort', 'configDatabaseName', 'configDatabaseUser', 'configDatabasePassword', 'configDatabaseSSLCA']
		];
		$storageProviderSelect->setTableRowsToShow($dbRowsToShow);
		$dbRowsToHide = [
			LAMCfgMain::DATABASE_FILE_SYSTEM => ['configDatabaseServer', 'configDatabasePort', 'configDatabaseName', 'configDatabaseUser', 'configDatabasePassword', 'configDatabaseSSLCA'],
			LAMCfgMain::DATABASE_MYSQL => []
		];
		$storageProviderSelect->setTableRowsToHide($dbRowsToHide);
		$row->add($storageProviderSelect);
		$dbHost = new htmlResponsiveInputField(_('Database host'), 'configDatabaseServer', $cfg->configDatabaseServer, '273');
		$dbHost->setRequired(true);
		$row->add($dbHost);
		$dbPort = new htmlResponsiveInputField(_('Database port'), 'configDatabasePort', $cfg->configDatabasePort, '274');
		$row->add($dbPort);
		$dbHostSSLCA = new htmlResponsiveInputField(_('CA certificate path'), 'configDatabaseSSLCA', $cfg->configDatabaseSSLCA, '277');
		$row->add($dbHostSSLCA);
		$dbName = new htmlResponsiveInputField(_('Database name'), 'configDatabaseName', $cfg->configDatabaseName, '276');
		$dbName->setRequired(true);
		$row->add($dbName);
		$dbUser = new htmlResponsiveInputField(_('Database user'), 'configDatabaseUser', $cfg->configDatabaseUser, '275');
		$dbUser->setRequired(true);
		$row->add($dbUser);
		$dbPassword = new htmlResponsiveInputField(_('Database password'), 'configDatabasePassword', deobfuscateText($cfg->configDatabasePassword), '275');
		$dbPassword->setRequired(true);
		$dbPassword->setIsPassword(true);
		$row->add($dbPassword);
	}

	// license
	if (isLAMProVersion()) {
		$row->add(new htmlSubTitle(_('Licence')));
		$row->add(new htmlResponsiveInputTextarea('license', implode("\n", $cfg->getLicenseLines()), 30, 10, _('Licence'), '287'));
		$warningOptions = [
			_('Screen') => LAMCfgMain::LICENSE_WARNING_SCREEN,
			_('Email') => LAMCfgMain::LICENSE_WARNING_EMAIL,
			_('Both') => LAMCfgMain::LICENSE_WARNING_ALL,
			_('None') => LAMCfgMain::LICENSE_WARNING_NONE
		];
		$warningTypeSelect = new htmlResponsiveSelect('licenseWarningType', $warningOptions, [$cfg->getLicenseWarningType()], _('Expiration warning'), '288');
		$warningTypeSelect->setHasDescriptiveElements(true);
		$warningTypeSelect->setSortElements(false);
		$warningTypeSelect->setTableRowsToHide([
			LAMCfgMain::LICENSE_WARNING_SCREEN => ['licenseEmailFrom', 'licenseEmailTo'],
			LAMCfgMain::LICENSE_WARNING_NONE => ['licenseEmailFrom', 'licenseEmailTo']
		]);
		$warningTypeSelect->setTableRowsToShow([
			LAMCfgMain::LICENSE_WARNING_EMAIL => ['licenseEmailFrom', 'licenseEmailTo'],
			LAMCfgMain::LICENSE_WARNING_ALL => ['licenseEmailFrom', 'licenseEmailTo']
		]);
		$row->add($warningTypeSelect);
		$licenseFrom = new htmlResponsiveInputField(_('From address'), 'licenseEmailFrom', $cfg->licenseEmailFrom, '289');
		$licenseFrom->setRequired(true);
		$row->add($licenseFrom);
		$licenseTo = new htmlResponsiveInputField(_('To address'), 'licenseEmailTo', $cfg->licenseEmailTo, '290');
		$licenseTo->setRequired(true);
		$row->add($licenseTo);

		$row->add(new htmlSpacer(null, '1rem'));
	}

	// security settings
	$row->add(new htmlSubTitle(_("Security settings")));
	$options = ['5', '10', '20', '30', '60', '90', '120', '240'];
	$row->add(new htmlResponsiveSelect('sessionTimeout', $options, [(string) $cfg->sessionTimeout], _("Session timeout"), '238'));
	$hideLoginErrorDetails = ($cfg->hideLoginErrorDetails === 'true');
	$row->add(new htmlResponsiveInputCheckbox('hideLoginErrorDetails', $hideLoginErrorDetails, _('Hide LDAP details on failed login'), '257'));
	$row->add(new htmlResponsiveInputTextarea('allowedHosts', implode("\n", explode(",", $cfg->allowedHosts)), 30, 7, _("Allowed hosts"), '241'));
	if (isLAMProVersion()) {
		$row->add(new htmlResponsiveInputTextarea('allowedHostsSelfService', implode("\n", explode(",", $cfg->allowedHostsSelfService)), 30, 7, _("Allowed hosts (self-service)"), '241'));
	}
	// SSL certificate
	$row->addVerticalSpacer('1rem');
	$row->addLabel(new htmlOutputText(_('SSL certificates')));
	$sslMethod = _('use system certificates');
	$sslFileName = $cfg->getSSLCaCertTempFileName();
	if ($sslFileName != null) {
		$sslMethod = _('use custom CA certificates');
	}
	$sslDelSaveGroup = new htmlGroup();
	$sslDelSaveGroup->addElement(new htmlOutputText($sslMethod));
	$sslDelSaveGroup->addElement(new htmlSpacer('5px', null));
	// delete+download button
	if ($sslFileName != null) {
		$temporaryFilesManager = new LamTemporaryFilesManager();
		$sslDownloadBtn = new htmlLink('', $temporaryFilesManager->getDownloadLink($sslFileName), '../../graphics/save.svg');
		$sslDownloadBtn->setTargetWindow('_blank');
		$sslDownloadBtn->setTitle(_('Download CA certificates'));
		$sslDownloadBtn->setCSSClasses(['icon']);
		$sslDelSaveGroup->addElement($sslDownloadBtn);
		$sslDeleteBtn = new htmlButton('sslCaCertDelete', 'del.svg', true);
		$sslDeleteBtn->setTitle(_('Delete all CA certificates'));
		$sslDelSaveGroup->addElement($sslDeleteBtn);
	}
	$sslDelSaveGroup->addElement(new htmlHelpLink('204'));
	$row->addField($sslDelSaveGroup);
	$row->addLabel(new htmlInputFileUpload('sslCaCert'));
	$sslUploadBtn = new htmlButton('sslCaCertUpload', _('Upload'));
	$sslUploadBtn->setTitle(_('Upload CA certificate in DER/PEM format.'));
	$row->addField($sslUploadBtn);
	if (function_exists('stream_socket_client') && function_exists('stream_context_get_params')) {
		$sslImportServerUrl = empty($_POST['serverurl']) ? 'ldaps://' : (string) $_POST['serverurl'];
		$serverUrlUpload = new htmlInputField('serverurl', $sslImportServerUrl);
		$row->addLabel($serverUrlUpload);
		$sslImportBtn = new htmlButton('sslCaCertImport', _('Import from server'));
		$sslImportBtn->setTitle(_('Imports the certificate directly from your LDAP server.'));
		$row->addField($sslImportBtn);
	}

	$sslCerts = $cfg->getSSLCaCertificates();
	if (count($sslCerts) > 0) {
		$certsTitles = [_('Common name'), _('Valid to'), _('Serial number'), _('Delete')];
		$certsData = [];
		foreach ($sslCerts as $i => $sslCert) {
			$serial = $sslCert['serialNumber'] ?? '';
			$validTo = $sslCert['validTo_time_t'] ?? '';
			if (get_preg($validTo, 'digit')) {
				$date = DateTime::createFromFormat('U', $validTo, new DateTimeZone('UTC'));
				if ($date !== false) {
					$validTo = $date->format('Y-m-d');
				}
			}
			$cn = $sslCert['subject']['CN'] ?? '';
			$delBtn = new htmlButton('deleteCert_' . $i, 'del.svg', true);
			$certsData[] = [
				new htmlOutputText($cn),
				new htmlDiv(null, new htmlOutputText($validTo), ['nowrap']),
				new htmlOutputText($serial),
				$delBtn
			];
		}
		$certsTable = new htmlResponsiveTable($certsTitles, $certsData);
		$certsTable->setCSSClasses(['text-left']);
		$row->add($certsTable);
	}

	// password policy
	$row->add(new htmlSubTitle(_("Password policy")));
	$optionsPwdLength = [];
	for ($i = 0; $i <= 50; $i++) {
		$optionsPwdLength[] = (string) $i;
	}
	$options4 = ['0', '1', '2', '3', '4'];
	$row->add(new htmlResponsiveSelect('passwordMinLength', $optionsPwdLength, [(string) $cfg->passwordMinLength], _('Minimum password length'), '242'));
	$row->addVerticalSpacer('1rem');
	$row->add(new htmlResponsiveSelect('passwordMinLower', $optionsPwdLength, [(string) $cfg->passwordMinLower], _('Minimum lowercase characters'), '242'));
	$row->add(new htmlResponsiveSelect('passwordMinUpper', $optionsPwdLength, [(string) $cfg->passwordMinUpper], _('Minimum uppercase characters'), '242'));
	$row->add(new htmlResponsiveSelect('passwordMinNumeric', $optionsPwdLength, [(string) $cfg->passwordMinNumeric], _('Minimum numeric characters'), '242'));
	$row->add(new htmlResponsiveSelect('passwordMinSymbol', $optionsPwdLength, [(string) $cfg->passwordMinSymbol], _('Minimum symbolic characters'), '242'));
	$row->add(new htmlResponsiveSelect('passwordMinClasses', $options4, [(string) $cfg->passwordMinClasses], _('Minimum character classes'), '242'));
	$row->addVerticalSpacer('1rem');
	$rulesCountOptions = [_('all') => '-1', '3' => '3', '4' => '4'];
	$rulesCountSelect = new htmlResponsiveSelect('passwordRulesCount', $rulesCountOptions, [(string) $cfg->checkedRulesCount], _('Number of rules that must match'), '246');
	$rulesCountSelect->setHasDescriptiveElements(true);
	$row->add($rulesCountSelect);
	$passwordMustNotContainUser = ($cfg->passwordMustNotContainUser === 'true');
	$row->add(new htmlResponsiveInputCheckbox('passwordMustNotContainUser', $passwordMustNotContainUser, _('Password must not contain user name'), '247'));
	$passwordMustNotContain3Chars = ($cfg->passwordMustNotContain3Chars === 'true');
	$row->add(new htmlResponsiveInputCheckbox('passwordMustNotContain3Chars', $passwordMustNotContain3Chars, _('Password must not contain part of user/first/last name'), '248'));
	if (function_exists('curl_init')) {
		$row->addVerticalSpacer('1rem');
		$row->add(new htmlResponsiveInputField(_('External password check'), 'externalPwdCheckUrl', $cfg->externalPwdCheckUrl, '249'));
	}

	// logging
	$row->add(new htmlSubTitle(_("Logging")));
	$levelOptions = [
		_("Debug") => (string) LOG_DEBUG,
		_("Notice") => (string) LOG_NOTICE,
		_("Warning") => (string) LOG_WARNING,
		_("Error") => (string) LOG_ERR
	];
	$levelSelect = new htmlResponsiveSelect('logLevel', $levelOptions, [(string) $cfg->logLevel], _("Log level"), '239');
	$levelSelect->setHasDescriptiveElements(true);
	$row->add($levelSelect);
	$destinationOptions = [
		_("No logging") => "none",
		_("System logging") => "syslog",
		_("File") => 'file',
		_("Remote") => 'remote'
	];
	$destinationSelected = 'file';
	$destinationPath = $cfg->logDestination;
	$destinationRemote = '';
	if ($cfg->logDestination == 'NONE') {
		$destinationSelected = 'none';
		$destinationPath = '';
	}
    elseif ($cfg->logDestination == 'SYSLOG') {
		$destinationSelected = 'syslog';
		$destinationPath = '';
	}
    elseif (str_starts_with($cfg->logDestination, 'REMOTE')) {
		$destinationSelected = 'remote';
		$remoteParts = explode(':', $cfg->logDestination, 2);
		$destinationRemote = empty($remoteParts[1]) ? '' : $remoteParts[1];
		$destinationPath = '';
	}
	$logDestinationSelect = new htmlResponsiveSelect('logDestination', $destinationOptions, [$destinationSelected], _("Log destination"), '240');
	$logDestinationSelect->setTableRowsToHide([
		'none' => ['logFile', 'logRemote'],
		'syslog' => ['logFile', 'logRemote'],
		'remote' => ['logFile'],
		'file' => ['logRemote']
	]);
	$logDestinationSelect->setTableRowsToShow([
		'file' => ['logFile'],
		'remote' => ['logRemote']
	]);
	$logDestinationSelect->setHasDescriptiveElements(true);
	$row->add($logDestinationSelect);
	$row->add(new htmlResponsiveInputField(_('File'), 'logFile', $destinationPath));
	$row->add(new htmlResponsiveInputField(_('Remote server'), 'logRemote', $destinationRemote, '251'));
	$errorLogOptions = [
		_('PHP system setting') => LAMCfgMain::ERROR_REPORTING_SYSTEM,
		_('default') => LAMCfgMain::ERROR_REPORTING_DEFAULT,
		_('all') => LAMCfgMain::ERROR_REPORTING_ALL
	];
	$errorLogSelect = new htmlResponsiveSelect('errorReporting', $errorLogOptions, [$cfg->errorReporting], _('PHP error reporting'), '244');
	$errorLogSelect->setHasDescriptiveElements(true);
	$row->add($errorLogSelect);

	// mail options
	if (isLAMProVersion()) {
		$row->add(new htmlSubTitle(_('Mail options')));
		$mailServer = new htmlResponsiveInputField(_("Mail server"), 'mailServer', $cfg->mailServer, '253');
		$row->add($mailServer);
		$mailUser = new htmlResponsiveInputField(_("User name"), 'mailUser', $cfg->mailUser, '254');
		$row->add($mailUser);
		$mailPassword = new htmlResponsiveInputField(_("Password"), 'mailPassword', $cfg->mailPassword, '255');
		$mailPassword->setIsPassword(true);
		$row->add($mailPassword);
		$mailEncryptionOptions = [
			'TLS' => LAMCfgMain::SMTP_TLS,
			'SSL' => LAMCfgMain::SMTP_SSL,
			_('None') => LAMCfgMain::SMTP_NONE
		];
		$selectedMailEncryption = empty($cfg->mailEncryption) ? LAMCfgMain::SMTP_TLS : $cfg->mailEncryption;
		$mailEncryptionSelect = new htmlResponsiveSelect('mailEncryption', $mailEncryptionOptions, [$selectedMailEncryption], _('Encryption protocol'), '256');
		$mailEncryptionSelect->setHasDescriptiveElements(true);
		$row->add($mailEncryptionSelect);
		$row->add(new htmlResponsiveInputField(_("Mail attribute"), 'mailAttribute', $cfg->getMailAttribute(), '258'));
		$row->add(new htmlResponsiveInputField(_("Secondary mail attribute"), 'mailBackupAttribute', $cfg->getMailBackupAttribute(), '259'));
		addSecurityTokenToSession(false);
		$mailTestButton = new htmlButton('testSmtp', _('Test settings'));
		$mailTestButton->setOnClick("window.lam.smtp.test(event, '" . getSecurityTokenName()
			. "', '" . getSecurityTokenValue() . "', '" . _('Ok') . "', '" . _('Cancel') . "', '" . _('Test settings') . "')");
		$row->addLabel(new htmlOutputText("&nbsp;", false));
		$row->addField($mailTestButton);
		$testDialogDivContent = new htmlResponsiveRow();
		$fromAddressInput = new htmlResponsiveInputField(_('From address'), 'testSmtpFrom', null, null, true);
		$fromAddressInput->setType('email');
		$testDialogDivContent->add($fromAddressInput);
		$toAddressInput = new htmlResponsiveInputField(_('To address'), 'testSmtpTo', null, null, true);
		$toAddressInput->setType('email');
		$testDialogDivContent->add($toAddressInput);
		$testDialogDiv = new htmlDiv('smtpTestDialogDiv', $testDialogDivContent, ['hidden']);
		$row->add($testDialogDiv);
	}

	// SMS options
	if (isLAMProVersion()) {
		$row->add(new htmlSubTitle(_('SMS options')));
        $smsService = new SmsService();
        $smsProviders = $smsService->findProviders();
		$smsProviderOptions = [
			'-' => '',
		];
        $smsOptionsToHide = [
            '' => ['smsApiKey', 'smsApiToken', 'smsAccountId', 'smsRegion', 'smsFrom', 'smsSubject', 'smsAttributes',
                'smsDefaultCountryPrefix', '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';
			}
			if ($provider->usesAccountId()) {
				$smsOptionsToShow[$provider->getId()][] = 'smsAccountId';
			}
			else {
				$smsOptionsToHide[$provider->getId()][] = 'smsAccountId';
			}
			if ($provider->usesRegion()) {
				$smsOptionsToShow[$provider->getId()][] = 'smsRegion';
			}
			else {
				$smsOptionsToHide[$provider->getId()][] = 'smsRegion';
			}
			if ($provider->usesFrom()) {
				$smsOptionsToShow[$provider->getId()][] = 'smsFrom';
			}
			else {
				$smsOptionsToHide[$provider->getId()][] = 'smsFrom';
			}
			if ($provider->usesSubject()) {
				$smsOptionsToShow[$provider->getId()][] = 'smsSubject';
			}
			else {
				$smsOptionsToHide[$provider->getId()][] = 'smsSubject';
			}
        }
		$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(_('Region'), 'smsRegion', $cfg->smsRegion, '299c'));
		$row->add(new htmlResponsiveInputField(_('Account id'), 'smsAccountId', $cfg->smsAccountId, '298a'));
		$row->add(new htmlResponsiveInputField(_('API key'), 'smsApiKey', $cfg->smsApiKey, '297'));
		$row->add(new htmlResponsiveInputField(_('Token'), 'smsApiToken', $cfg->smsToken, '298'));
		$row->add(new htmlResponsiveInputField(_('From'), 'smsFrom', $cfg->smsFrom, '299b'));
		$row->add(new htmlResponsiveInputField(_('Subject'), 'smsSubject', $cfg->smsSubject, '299d'));
		$row->add(new htmlResponsiveInputField(_('Default country prefix'), 'smsDefaultCountryPrefix', $cfg->smsDefaultCountryPrefix, '299a'));
		$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
	if (extension_loaded('PDO')
		&& in_array('sqlite', PDO::getAvailableDrivers())) {
		include_once __DIR__ . '/../../lib/webauthn.inc';
		$webAuthnManager = new WebauthnManager();
		try {
			$database = $webAuthnManager->getDatabase();
			if ($database->hasRegisteredCredentials()) {
				$row->add(new htmlSubTitle(_('WebAuthn devices')));
				$webauthnSearchField = new htmlResponsiveInputField(_('User DN'), 'webauthn_searchTerm', null, '252');
				$row->add($webauthnSearchField);
				$row->addVerticalSpacer('0.5rem');
				$row->add(new htmlButton('webauthn_search', _('Search')), 12, 12, 12, 'text-center');
				$resultDiv = new htmlDiv('webauthn_results', new htmlOutputText(''), ['lam-webauthn-results', 'text-left']);
				addSecurityTokenToSession(false);
				$resultDiv->addDataAttribute('sec_token_value', getSecurityTokenValue());
				$row->add($resultDiv);
				$confirmationDiv = new htmlDiv('webauthnDeleteConfirm', new htmlOutputText(_('Do you really want to remove this device?')), ['hidden']);
				$row->add($confirmationDiv);
			}
		}
		catch (LAMException $e) {
			logNewMessage(LOG_ERR, 'Webauthn error: ' . $e->getTitle() . ' ' . $e->getMessage());
			$row->add(new htmlStatusMessage('ERROR', $e->getTitle()));
		}
	}

	// module settings
	$modules = getAllModules();
	$supportsGlobalCronJob = false;
	foreach ($modules as $module) {
		$supportsGlobalCronJob = $supportsGlobalCronJob || $module->supportsGlobalCronJob();
		$moduleOptions = $module->getGlobalConfigOptions($cfg->getModuleSettings());
		if (empty($moduleOptions)) {
			continue;
		}
		$row->add(new htmlSubTitle($module->get_alias()));
		foreach ($moduleOptions as $moduleOption) {
			$row->add($moduleOption);
		}
		$row->addVerticalSpacer('3rem');
	}

	// global cron job
	if ($supportsGlobalCronJob) {
		$row->add(new htmlSubTitle(_('Global cron job')));
		$cronCommand = dirname(__FILE__, 3) . '/lib/runCronJobs.sh all';
		$row->addLabel(new htmlOutputText('Cron command'));
		$cmdGroup = new htmlGroup();
		$cmdGroup->addElement(new htmlOutputText('0 0 * * * ' . $cronCommand));
		$cmdGroup->addElement(new htmlSpacer('2rem', null));
		$cmdGroup->addElement(new htmlHelpLink('294'));
		$row->addField($cmdGroup);
	}

	// change master password
	$row->add(new htmlSubTitle(_("Change master password")));
	$pwd1 = new htmlResponsiveInputField(_("New master password"), 'masterpassword', '', '235');
	$pwd1->setIsPassword(true, false, true);
	$row->add($pwd1);
	$pwd2 = new htmlResponsiveInputField(_("Reenter password"), 'masterpassword2', '');
	$pwd2->setIsPassword(true, false, true);
	$pwd2->setSameValueFieldID('masterpassword');
	$row->add($pwd2);
	$row->addVerticalSpacer('3rem');

	// buttons
	if ($cfg->isWritable()) {
		$buttonTable = new htmlTable();
		$saveButton = new htmlButton('submit', _("Save"));
		$saveButton->setCSSClasses(['lam-primary']);
		$buttonTable->addElement($saveButton);
		$buttonTable->addElement(new htmlSpacer('0.5rem', null));
		$buttonTable->addElement(new htmlButton('cancel', _("Cancel")));
		$row->add($buttonTable);
		$row->add(new htmlHiddenInput('submitFormData', '1'));
	}

	$box = new htmlDiv(null, $row);
	$box->setCSSClasses(['roundedShadowBox']);
	parseHtml(null, $box, [], false);


	/**
	 * Formats an LDAP time string (e.g. from createTimestamp).
	 *
	 * @param String $time LDAP time value
	 * @return String formatted time
	 */
	function formatSSLTimestamp($time) {
		if (!empty($time)) {
			$timeZone = 'UTC';
			$sysTimeZone = @date_default_timezone_get();
			if (!empty($sysTimeZone)) {
				$timeZone = $sysTimeZone;
			}
			$date = new DateTime('@' . $time, new DateTimeZone($timeZone));
			return $date->format('d.m.Y');
		}
		return '';
	}


	?>

</form>
<p><br></p>

</body>
</html>

