diff --git a/.gitignore b/.gitignore index 93730408..272a8cdf 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ logs/* .well-known .idea *.iml +img/ !templates/Froxlor/ !templates/Sparkle/ diff --git a/actions/admin/settings/100.panel.php b/actions/admin/settings/100.panel.php index 3485bbdf..e73c5183 100644 --- a/actions/admin/settings/100.panel.php +++ b/actions/admin/settings/100.panel.php @@ -296,6 +296,24 @@ return array( 'default' => '', 'save_method' => 'storeSettingField' ), + 'panel_logo_image_header' => array( + 'label' => $lng['serversettings']['logo_image_header'], + 'settinggroup' => 'panel', + 'varname' => 'logo_image_header', + 'type' => 'image', + 'image_name' => 'logo_header', + 'default' => '', + 'save_method' => 'storeSettingImage' + ), + 'panel_logo_image_login' => array( + 'label' => $lng['serversettings']['logo_image_login'], + 'settinggroup' => 'panel', + 'varname' => 'logo_image_login', + 'type' => 'image', + 'image_name' => 'logo_login', + 'default' => '', + 'save_method' => 'storeSettingImage' + ), ) ) ) diff --git a/actions/admin/settings/131.ssl.php b/actions/admin/settings/131.ssl.php index 973dbf2b..50a0b96e 100644 --- a/actions/admin/settings/131.ssl.php +++ b/actions/admin/settings/131.ssl.php @@ -142,6 +142,9 @@ return array( 'default' => '/etc/apache2/conf-enabled/acme.conf', 'save_method' => 'storeSettingField' ), + /** + * currently the only option anyway + * 'system_leapiversion' => array( 'label' => $lng['serversettings']['leapiversion'], 'settinggroup' => 'system', @@ -154,16 +157,18 @@ return array( ), 'save_method' => 'storeSettingField' ), + */ 'system_letsencryptca' => array( 'label' => $lng['serversettings']['letsencryptca'], 'settinggroup' => 'system', 'varname' => 'letsencryptca', 'type' => 'option', - 'default' => 'production', + 'default' => 'letsencrypt', 'option_mode' => 'one', 'option_options' => array( - 'testing' => 'https://acme-staging-v0' . \Froxlor\Settings::Get('system.leapiversion') . '.api.letsencrypt.org (Test)', - 'production' => 'https://acme-v0' . \Froxlor\Settings::Get('system.leapiversion') . '.api.letsencrypt.org (Live)' + 'letsencrypt_test' => 'Let\'s Encrypt (Test / Staging)', + 'letsencrypt' => 'Let\'s Encrypt (Live)', + 'zerossl' => 'ZeroSSL (Live)' ), 'save_method' => 'storeSettingField' ), diff --git a/composer.json b/composer.json index d5feaf8c..82326ce4 100644 --- a/composer.json +++ b/composer.json @@ -43,12 +43,13 @@ "ext-curl": "*", "ext-json": "*", "ext-openssl": "*", + "ext-fileinfo": "*", "phpmailer/phpmailer": "~6.0", "monolog/monolog": "^1.24", "robthree/twofactorauth": "^1.6", "froxlor/idna-convert-legacy": "^2.1", "voku/anti-xss": "^4.1" - }, + }, "require-dev": { "phpunit/phpunit": "^9", "php": ">=7.3", diff --git a/composer.lock b/composer.lock index f66deb47..364abc0b 100644 --- a/composer.lock +++ b/composer.lock @@ -150,16 +150,16 @@ }, { "name": "phpmailer/phpmailer", - "version": "v6.4.1", + "version": "v6.5.0", "source": { "type": "git", "url": "https://github.com/PHPMailer/PHPMailer.git", - "reference": "9256f12d8fb0cd0500f93b19e18c356906cbed3d" + "reference": "a5b5c43e50b7fba655f793ad27303cd74c57363c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/9256f12d8fb0cd0500f93b19e18c356906cbed3d", - "reference": "9256f12d8fb0cd0500f93b19e18c356906cbed3d", + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/a5b5c43e50b7fba655f793ad27303cd74c57363c", + "reference": "a5b5c43e50b7fba655f793ad27303cd74c57363c", "shasum": "" }, "require": { @@ -214,7 +214,7 @@ "description": "PHPMailer is a full-featured email creation and transfer class for PHP", "support": { "issues": "https://github.com/PHPMailer/PHPMailer/issues", - "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.4.1" + "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.5.0" }, "funding": [ { @@ -222,7 +222,7 @@ "type": "github" } ], - "time": "2021-04-29T12:25:04+00:00" + "time": "2021-06-16T14:33:43+00:00" }, { "name": "psr/log", @@ -4133,5 +4133,5 @@ "php": ">=7.3", "ext-pcntl": "*" }, - "plugin-api-version": "2.0.0" + "plugin-api-version": "2.1.0" } diff --git a/install/froxlor.sql b/install/froxlor.sql index de3e8cf6..763a1fec 100644 --- a/install/froxlor.sql +++ b/install/froxlor.sql @@ -628,7 +628,7 @@ opcache.interned_strings_buffer'), ('system', 'apacheitksupport', '0'), ('system', 'leprivatekey', 'unset'), ('system', 'lepublickey', 'unset'), - ('system', 'letsencryptca', 'production'), + ('system', 'letsencryptca', 'letsencrypt'), ('system', 'letsencryptcountrycode', 'DE'), ('system', 'letsencryptstate', 'Hessen'), ('system', 'letsencryptchallengepath', '/var/www/froxlor'), @@ -715,8 +715,10 @@ opcache.interned_strings_buffer'), ('panel', 'imprint_url', ''), ('panel', 'terms_url', ''), ('panel', 'privacy_url', ''), + ('panel', 'logo_image_header', ''), + ('panel', 'logo_image_login', ''), ('panel', 'version', '0.10.26'), - ('panel', 'db_version', '202106160'); + ('panel', 'db_version', '202107070'); DROP TABLE IF EXISTS `panel_tasks`; @@ -933,7 +935,7 @@ CREATE TABLE IF NOT EXISTS `ftp_quotalimits` ( -INSERT INTO `ftp_quotalimits` (`name`, `quota_type`, `per_session`, `limit_type`, `bytes_in_avail`, `bytes_out_avail`, `bytes_xfer_avail`, `files_in_avail`, `files_out_avail`, `files_xfer_avail`) VALUES +INSERT INTO `ftp_quotalimits` (`name`, `quota_type`, `per_session`, `limit_type`, `bytes_in_avail`, `bytes_out_avail`, `bytes_xfer_avail`, `files_in_avail`, `files_out_avail`, `files_xfer_avail`) VALUES ('froxlor', 'user', 'false', 'hard', 0, 0, 0, 0, 0, 0); diff --git a/install/lib/class.FroxlorInstall.php b/install/lib/class.FroxlorInstall.php index aa9977e8..6b27e62a 100644 --- a/install/lib/class.FroxlorInstall.php +++ b/install/lib/class.FroxlorInstall.php @@ -28,7 +28,7 @@ * @author Froxlor team (2010-) * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt * @package Install - * + * */ class FroxlorInstall { @@ -784,7 +784,7 @@ class FroxlorInstall } // language selection $language_options = ''; - foreach ($this->_languages as $language_name => $language_file) { + foreach ($this->_languages as $language_file => $language_name) { $language_options .= \Froxlor\UI\HTML::makeoption($language_name, $language_file, $this->_activelng, true, true); } // get language-form-template @@ -867,19 +867,24 @@ class FroxlorInstall } // show list of available distro's + $distributions_select_data = []; $distros = glob(\Froxlor\FileDir::makeCorrectDir(\Froxlor\Froxlor::getInstallDir() . '/lib/configfiles/') . '*.xml'); foreach ($distros as $_distribution) { $dist = new \Froxlor\Config\ConfigParser($_distribution); $dist_display = $dist->distributionName . " " . $dist->distributionCodename . " (" . $dist->distributionVersion . ")"; + if (!array_key_exists($dist_display, $distributions_select_data)) { + $distributions_select_data[$dist_display] = ''; + } $distributions_select_data[$dist_display] .= str_replace(".xml", "", strtolower(basename($_distribution))); } // sort by distribution name ksort($distributions_select_data); + $distributions_select = ''; foreach ($distributions_select_data as $dist_display => $dist_index) { // create select-box-option - $distributions_select .= \Froxlor\UI\HTML::makeoption($dist_display, $dist_index, $this->_data['distribution']); + $distributions_select .= \Froxlor\UI\HTML::makeoption($dist_display, $dist_index, $this->_data['distribution'] ?? ''); // $this->_data['distribution'] } @@ -947,7 +952,7 @@ class FroxlorInstall * optional css * @param string $type * optional type of input-box (default: text) - * + * * @return string */ private function _getSectionItemString($fieldname = null, $required = false, $style = "", $type = 'text') @@ -994,7 +999,6 @@ class FroxlorInstall */ private function _getSectionItemSelectbox($fieldname = null, $options = null, $style = "") { - $groupname = $this->_lng['install'][$groupname]; $fieldlabel = $this->_lng['install'][$fieldname]; $sectionitem = ""; @@ -1239,7 +1243,7 @@ class FroxlorInstall * * @param string $template * name of the template including subdirectory - * + * * @return string */ private function _getTemplate($template = null) diff --git a/install/updates/froxlor/0.10/update_0.10.inc.php b/install/updates/froxlor/0.10/update_0.10.inc.php index 190123be..1f2d360e 100644 --- a/install/updates/froxlor/0.10/update_0.10.inc.php +++ b/install/updates/froxlor/0.10/update_0.10.inc.php @@ -14,7 +14,7 @@ use Froxlor\Settings; * @author Froxlor team (2010-) * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt * @package Install - * + * */ if (! defined('_CRON_UPDATE')) { if (! defined('AREA') || (defined('AREA') && AREA != 'admin') || ! isset($userinfo['loginname']) || (isset($userinfo['loginname']) && $userinfo['loginname'] == '')) { @@ -812,3 +812,53 @@ if (\Froxlor\Froxlor::isDatabaseVersion('202103240')) { \Froxlor\Froxlor::updateToDbVersion('202106160'); } + +if (\Froxlor\Froxlor::isDatabaseVersion('202106160')) { + + showUpdateStep("Adjusting Let's Encrypt endpoint configuration to support ZeroSSL", true); + if (Settings::Get('system.letsencryptca') == 'testing') { + Settings::Set("system.letsencryptca", 'letsencrypt_test'); + } else { + Settings::Set("system.letsencryptca", 'letsencrypt'); + } + lastStepStatus(0); + + \Froxlor\Froxlor::updateToDbVersion('202106270'); +} + +if (\Froxlor\Froxlor::isDatabaseVersion('202106270')) { + showUpdateStep("Adding custom logo image settings", true); + Settings::AddNew("panel.logo_image_header", ''); + Settings::AddNew("panel.logo_image_login", ''); + lastStepStatus(0); + + // Migrating old custom logo over, if exists + $custom_logo_file_old = \Froxlor\Froxlor::getInstallDir() . '/templates/Sparkle/assets/img/logo_custom.png'; + if (file_exists($custom_logo_file_old)) { + showUpdateStep("Migrating existing custom logo to new settings", true); + + $path = \Froxlor\Froxlor::getInstallDir().'/img/'; + if (!is_dir($path) && !mkdir($path, 0775)) { + throw new \Exception("img directory does not exist and cannot be created"); + } + if (!is_writable($path)) { + if (!chmod($path, '0775')) { + throw new \Exception("Cannot write to img directory"); + } + } + + // Save as new custom logo header + $save_to = 'logo_header.png'; + copy($custom_logo_file_old, $path.$save_to); + Settings::Set("panel.logo_image_header", "img/{$save_to}?v=".time()); + + // Save as new custom logo login + $save_to = 'logo_login.png'; + copy($custom_logo_file_old, $path.$save_to); + Settings::Set("panel.logo_image_login", "img/{$save_to}?v=".time()); + + lastStepStatus(0); + } + + \Froxlor\Froxlor::updateToDbVersion('202107070'); +} diff --git a/lib/Froxlor/Cron/Http/LetsEncrypt/AcmeSh.php b/lib/Froxlor/Cron/Http/LetsEncrypt/AcmeSh.php index 5a473b19..1b2786ce 100644 --- a/lib/Froxlor/Cron/Http/LetsEncrypt/AcmeSh.php +++ b/lib/Froxlor/Cron/Http/LetsEncrypt/AcmeSh.php @@ -21,70 +21,76 @@ use Froxlor\FileDir; * @author Froxlor team (2016-) * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt * @package Cron - * + * * @since 0.9.35 - * + * */ class AcmeSh extends \Froxlor\Cron\FroxlorCron { - private static $apiserver = ""; + const ACME_PROVIDER = [ + 'letsencrypt' => "https://acme-v02.api.letsencrypt.org/directory", + 'letsencrypt_test' => "https://acme-staging-v02.api.letsencrypt.org/directory", + 'zerossl' => "https://acme.zerossl.com/v2/DV90" + ]; - private static $acmesh = "/root/.acme.sh/acme.sh"; + private static $apiserver = ""; - /** - * - * @var \PDOStatement - */ - private static $updcert_stmt = null; + private static $acmesh = "/root/.acme.sh/acme.sh"; - /** - * - * @var \PDOStatement - */ - private static $upddom_stmt = null; + /** + * + * @var \PDOStatement + */ + private static $updcert_stmt = null; - public static $no_inserttask = false; + /** + * + * @var \PDOStatement + */ + private static $upddom_stmt = null; - /** - * run the task - * - * @param boolean $internal - * @return number - */ - public static function run($internal = false) - { - // usually, this is action is called from within the tasks-jobs - if (! defined('CRON_IS_FORCED') && ! defined('CRON_DEBUG_FLAG') && $internal == false) { - // Let's Encrypt cronjob is combined with regeneration of webserver configuration files. - // For debugging purposes you can use the --debug switch and the --force switch to run the cron manually. - // check whether we MIGHT need to run although there is no task to regenerate config-files - $issue_froxlor = self::issueFroxlorVhost(); - $issue_domains = self::issueDomains(); - $renew_froxlor = self::renewFroxlorVhost(); - $renew_domains = self::renewDomains(true); - if ($issue_froxlor || !empty($issue_domains) || !empty($renew_froxlor) || $renew_domains) { - // insert task to generate certificates and vhost-configs - \Froxlor\System\Cronjob::inserttask(1); - } - return 0; - } + public static $no_inserttask = false; - // set server according to settings - self::$apiserver = 'https://acme-' . (Settings::Get('system.letsencryptca') == 'testing' ? 'staging-' : '') . 'v0' . \Froxlor\Settings::Get('system.leapiversion') . '.api.letsencrypt.org/directory'; + /** + * run the task + * + * @param boolean $internal + * @return number + */ + public static function run($internal = false) + { + // usually, this is action is called from within the tasks-jobs + if (! defined('CRON_IS_FORCED') && ! defined('CRON_DEBUG_FLAG') && $internal == false) { + // Let's Encrypt cronjob is combined with regeneration of webserver configuration files. + // For debugging purposes you can use the --debug switch and the --force switch to run the cron manually. + // check whether we MIGHT need to run although there is no task to regenerate config-files + $issue_froxlor = self::issueFroxlorVhost(); + $issue_domains = self::issueDomains(); + $renew_froxlor = self::renewFroxlorVhost(); + $renew_domains = self::renewDomains(true); + if ($issue_froxlor || ! empty($issue_domains) || ! empty($renew_froxlor) || $renew_domains) { + // insert task to generate certificates and vhost-configs + \Froxlor\System\Cronjob::inserttask(1); + } + return 0; + } - // validate acme.sh installation - if (! self::checkInstall()) { - return - 1; - } + // set server according to settings + self::$apiserver = self::ACME_PROVIDER[Settings::Get('system.letsencryptca')]; - self::checkUpgrade(); + // validate acme.sh installation + if (! self::checkInstall()) { + return - 1; + } - // flag for re-generation of vhost files - $changedetected = 0; + self::checkUpgrade(); - // prepare update sql - self::$updcert_stmt = Database::prepare(" + // flag for re-generation of vhost files + $changedetected = 0; + + // prepare update sql + self::$updcert_stmt = Database::prepare(" REPLACE INTO `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` SET @@ -99,99 +105,99 @@ class AcmeSh extends \Froxlor\Cron\FroxlorCron `expirationdate` = :expirationdate "); - // prepare domain update sql - self::$upddom_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_DOMAINS . "` SET `ssl_redirect` = '1' WHERE `id` = :domainid"); + // prepare domain update sql + self::$upddom_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_DOMAINS . "` SET `ssl_redirect` = '1' WHERE `id` = :domainid"); - // check whether there are certificates to issue - $issue_froxlor = self::issueFroxlorVhost(); - $issue_domains = self::issueDomains(); + // check whether there are certificates to issue + $issue_froxlor = self::issueFroxlorVhost(); + $issue_domains = self::issueDomains(); - // first - generate LE for system-vhost if enabled - if ($issue_froxlor) { - // build row - $certrow = array( - 'loginname' => 'froxlor.panel', - 'domain' => Settings::Get('system.hostname'), - 'domainid' => 0, - 'documentroot' => \Froxlor\Froxlor::getInstallDir(), - 'leprivatekey' => Settings::Get('system.leprivatekey'), - 'lepublickey' => Settings::Get('system.lepublickey'), - 'leregistered' => Settings::Get('system.leregistered'), - 'ssl_redirect' => Settings::Get('system.le_froxlor_redirect'), - 'expirationdate' => null, - 'ssl_cert_file' => null, - 'ssl_key_file' => null, - 'ssl_ca_file' => null, - 'ssl_csr_file' => null, - 'id' => null - ); + // first - generate LE for system-vhost if enabled + if ($issue_froxlor) { + // build row + $certrow = array( + 'loginname' => 'froxlor.panel', + 'domain' => Settings::Get('system.hostname'), + 'domainid' => 0, + 'documentroot' => \Froxlor\Froxlor::getInstallDir(), + 'leprivatekey' => Settings::Get('system.leprivatekey'), + 'lepublickey' => Settings::Get('system.lepublickey'), + 'leregistered' => Settings::Get('system.leregistered'), + 'ssl_redirect' => Settings::Get('system.le_froxlor_redirect'), + 'expirationdate' => null, + 'ssl_cert_file' => null, + 'ssl_key_file' => null, + 'ssl_ca_file' => null, + 'ssl_csr_file' => null, + 'id' => null + ); - // add to queue - $issue_domains[] = $certrow; - } + // add to queue + $issue_domains[] = $certrow; + } - if (count($issue_domains)) { - FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Requesting " . count($issue_domains) . " new Let's Encrypt certificates"); - self::runIssueFor($issue_domains); - $changedetected = 1; - } + if (count($issue_domains)) { + FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Requesting " . count($issue_domains) . " new Let's Encrypt certificates"); + self::runIssueFor($issue_domains); + $changedetected = 1; + } - // compare file-system certificates with the ones in our database - // and update if needed - $renew_froxlor = self::renewFroxlorVhost(); - $renew_domains = self::renewDomains(); + // compare file-system certificates with the ones in our database + // and update if needed + $renew_froxlor = self::renewFroxlorVhost(); + $renew_domains = self::renewDomains(); - if ($renew_froxlor) { - // build row - $certrow = array( - 'loginname' => 'froxlor.panel', - 'domain' => Settings::Get('system.hostname'), - 'domainid' => 0, - 'documentroot' => \Froxlor\Froxlor::getInstallDir(), - 'leprivatekey' => Settings::Get('system.leprivatekey'), - 'lepublickey' => Settings::Get('system.lepublickey'), - 'leregistered' => Settings::Get('system.leregistered'), - 'ssl_redirect' => Settings::Get('system.le_froxlor_redirect'), - 'expirationdate' => is_array($renew_froxlor) ? $renew_froxlor['expirationdate'] : date('Y-m-d H:i:s', 0), - 'ssl_cert_file' => is_array($renew_froxlor) ? $renew_froxlor['ssl_cert_file'] : null, - 'ssl_key_file' => is_array($renew_froxlor) ? $renew_froxlor['ssl_key_file'] : null, - 'ssl_ca_file' => is_array($renew_froxlor) ? $renew_froxlor['ssl_ca_file'] : null, - 'ssl_csr_file' => is_array($renew_froxlor) ? $renew_froxlor['ssl_csr_file'] : null, - 'id' => is_array($renew_froxlor) ? $renew_froxlor['id'] : null - ); - $renew_domains[] = $certrow; - } + if ($renew_froxlor) { + // build row + $certrow = array( + 'loginname' => 'froxlor.panel', + 'domain' => Settings::Get('system.hostname'), + 'domainid' => 0, + 'documentroot' => \Froxlor\Froxlor::getInstallDir(), + 'leprivatekey' => Settings::Get('system.leprivatekey'), + 'lepublickey' => Settings::Get('system.lepublickey'), + 'leregistered' => Settings::Get('system.leregistered'), + 'ssl_redirect' => Settings::Get('system.le_froxlor_redirect'), + 'expirationdate' => is_array($renew_froxlor) ? $renew_froxlor['expirationdate'] : date('Y-m-d H:i:s', 0), + 'ssl_cert_file' => is_array($renew_froxlor) ? $renew_froxlor['ssl_cert_file'] : null, + 'ssl_key_file' => is_array($renew_froxlor) ? $renew_froxlor['ssl_key_file'] : null, + 'ssl_ca_file' => is_array($renew_froxlor) ? $renew_froxlor['ssl_ca_file'] : null, + 'ssl_csr_file' => is_array($renew_froxlor) ? $renew_froxlor['ssl_csr_file'] : null, + 'id' => is_array($renew_froxlor) ? $renew_froxlor['id'] : null + ); + $renew_domains[] = $certrow; + } - foreach ($renew_domains as $domain) { - $cronlog = FroxlorLogger::getInstanceOf(array( - 'loginname' => $domain['loginname'], - 'adminsession' => 0 - )); - if (defined('CRON_IS_FORCED') || self::checkFsFilesAreNewer($domain['domain'], $domain['expirationdate'])) { - self::certToDb($domain, $cronlog, array()); - $changedetected = 1; - } - } + foreach ($renew_domains as $domain) { + $cronlog = FroxlorLogger::getInstanceOf(array( + 'loginname' => $domain['loginname'], + 'adminsession' => 0 + )); + if (defined('CRON_IS_FORCED') || self::checkFsFilesAreNewer($domain['domain'], $domain['expirationdate'])) { + self::certToDb($domain, $cronlog, array()); + $changedetected = 1; + } + } - // If we have a change in a certificate, we need to update the webserver - configs - // This is easiest done by just creating a new task ;) - if ($changedetected) { - if (self::$no_inserttask == false) { - \Froxlor\System\Cronjob::inserttask(1); - } - FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Let's Encrypt certificates have been updated"); - } else { - FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "No new certificates or certificate updates found"); - } - } + // If we have a change in a certificate, we need to update the webserver - configs + // This is easiest done by just creating a new task ;) + if ($changedetected) { + if (self::$no_inserttask == false) { + \Froxlor\System\Cronjob::inserttask(1); + } + FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Let's Encrypt certificates have been updated"); + } else { + FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "No new certificates or certificate updates found"); + } + } - /** - * issue certificates for a list of domains - */ - private static function runIssueFor($certrows = array()) - { - // prepare aliasdomain-check - $aliasdomains_stmt = Database::prepare(" + /** + * issue certificates for a list of domains + */ + private static function runIssueFor($certrows = array()) + { + // prepare aliasdomain-check + $aliasdomains_stmt = Database::prepare(" SELECT dom.`id` as domainid, dom.`domain`, @@ -202,216 +208,216 @@ class AcmeSh extends \Froxlor\Cron\FroxlorCron AND dom.`letsencrypt` = 1 AND dom.`iswildcarddomain` = 0 "); - // iterate through all domains - foreach ($certrows as $certrow) { - // set logger to corresponding loginname for the log to appear in the users system-log - $cronlog = FroxlorLogger::getInstanceOf(array( - 'loginname' => $certrow['loginname'], - 'adminsession' => 0 - )); - // Only issue let's encrypt certificate if no broken ssl_redirect is enabled - if ($certrow['ssl_redirect'] != 2) { - $do_force = false; - if (! empty($certrow['ssl_cert_file']) && empty($certrow['expirationdate'])) { - // domain changed (SAN or similar) - $do_force = true; - $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Re-creating certificate for " . $certrow['domain']); - } else { - $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Creating certificate for " . $certrow['domain']); - } - $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Adding common-name: " . $certrow['domain']); - $domains = array( - strtolower($certrow['domain']) - ); - // add www. to SAN list - if ($certrow['wwwserveralias'] == 1) { - $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Adding SAN entry: www." . $certrow['domain']); - $domains[] = strtolower('www.' . $certrow['domain']); - } - if ($certrow['domainid'] == 0) { - $froxlor_aliases = Settings::Get('system.froxloraliases'); - if (! empty($froxlor_aliases)) { - $froxlor_aliases = explode(",", $froxlor_aliases); - foreach ($froxlor_aliases as $falias) { - if (\Froxlor\Validate\Validate::validateDomain(trim($falias))) { - $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Adding SAN entry: " . strtolower(trim($falias))); - $domains[] = strtolower(trim($falias)); - } - } - } - } else { - // add alias domains (and possibly www.) to SAN list - Database::pexecute($aliasdomains_stmt, array( - 'id' => $certrow['domainid'] - )); - $aliasdomains = $aliasdomains_stmt->fetchAll(\PDO::FETCH_ASSOC); - foreach ($aliasdomains as $aliasdomain) { - $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Adding SAN entry: " . $aliasdomain['domain']); - $domains[] = strtolower($aliasdomain['domain']); - if ($aliasdomain['wwwserveralias'] == 1) { - $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Adding SAN entry: www." . $aliasdomain['domain']); - $domains[] = strtolower('www.' . $aliasdomain['domain']); - } - } - } + // iterate through all domains + foreach ($certrows as $certrow) { + // set logger to corresponding loginname for the log to appear in the users system-log + $cronlog = FroxlorLogger::getInstanceOf(array( + 'loginname' => $certrow['loginname'], + 'adminsession' => 0 + )); + // Only issue let's encrypt certificate if no broken ssl_redirect is enabled + if ($certrow['ssl_redirect'] != 2) { + $do_force = false; + if (! empty($certrow['ssl_cert_file']) && empty($certrow['expirationdate'])) { + // domain changed (SAN or similar) + $do_force = true; + $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Re-creating certificate for " . $certrow['domain']); + } else { + $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Creating certificate for " . $certrow['domain']); + } + $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Adding common-name: " . $certrow['domain']); + $domains = array( + strtolower($certrow['domain']) + ); + // add www. to SAN list + if ($certrow['wwwserveralias'] == 1) { + $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Adding SAN entry: www." . $certrow['domain']); + $domains[] = strtolower('www.' . $certrow['domain']); + } + if ($certrow['domainid'] == 0) { + $froxlor_aliases = Settings::Get('system.froxloraliases'); + if (! empty($froxlor_aliases)) { + $froxlor_aliases = explode(",", $froxlor_aliases); + foreach ($froxlor_aliases as $falias) { + if (\Froxlor\Validate\Validate::validateDomain(trim($falias))) { + $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Adding SAN entry: " . strtolower(trim($falias))); + $domains[] = strtolower(trim($falias)); + } + } + } + } else { + // add alias domains (and possibly www.) to SAN list + Database::pexecute($aliasdomains_stmt, array( + 'id' => $certrow['domainid'] + )); + $aliasdomains = $aliasdomains_stmt->fetchAll(\PDO::FETCH_ASSOC); + foreach ($aliasdomains as $aliasdomain) { + $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Adding SAN entry: " . $aliasdomain['domain']); + $domains[] = strtolower($aliasdomain['domain']); + if ($aliasdomain['wwwserveralias'] == 1) { + $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Adding SAN entry: www." . $aliasdomain['domain']); + $domains[] = strtolower('www.' . $aliasdomain['domain']); + } + } + } - self::validateDns($domains, $certrow['domainid'], $cronlog); + self::validateDns($domains, $certrow['domainid'], $cronlog); - self::runAcmeSh($certrow, $domains, $cronlog, $do_force); - } else { - $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_WARNING, "Skipping Let's Encrypt generation for " . $certrow['domain'] . " due to an enabled ssl_redirect"); - } - } - } + self::runAcmeSh($certrow, $domains, $cronlog, $do_force); + } else { + $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_WARNING, "Skipping Let's Encrypt generation for " . $certrow['domain'] . " due to an enabled ssl_redirect"); + } + } + } - /** - * validate dns (A / AAAA record) of domain against known system ips - * - * @param array $domains - * @param int $domain_id - * @param FroxlorLogger $cronlog - */ - private static function validateDns(array &$domains, $domain_id, &$cronlog) - { - if (Settings::Get('system.le_domain_dnscheck') == '1' && ! empty($domains)) { - $loop_domains = $domains; - // ips according to our system - $our_ips = Domain::getIpsOfDomain($domain_id); - foreach ($loop_domains as $idx => $domain) { - $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Validating DNS of " . $domain); - // ips accordint to NS - $domain_ips = PhpHelper::gethostbynamel6($domain); - if ($domain_ips == false || count(array_intersect($our_ips, $domain_ips)) <= 0) { - // no common ips... - $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_WARNING, "Skipping Let's Encrypt generation for " . $domain . " due to no system known IP address via DNS check"); - unset($domains[$idx]); - } - } - } - } + /** + * validate dns (A / AAAA record) of domain against known system ips + * + * @param array $domains + * @param int $domain_id + * @param FroxlorLogger $cronlog + */ + private static function validateDns(array &$domains, $domain_id, &$cronlog) + { + if (Settings::Get('system.le_domain_dnscheck') == '1' && ! empty($domains)) { + $loop_domains = $domains; + // ips according to our system + $our_ips = Domain::getIpsOfDomain($domain_id); + foreach ($loop_domains as $idx => $domain) { + $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Validating DNS of " . $domain); + // ips accordint to NS + $domain_ips = PhpHelper::gethostbynamel6($domain); + if ($domain_ips == false || count(array_intersect($our_ips, $domain_ips)) <= 0) { + // no common ips... + $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_WARNING, "Skipping Let's Encrypt generation for " . $domain . " due to no system known IP address via DNS check"); + unset($domains[$idx]); + } + } + } + } - private static function runAcmeSh(array $certrow, array $domains, &$cronlog = null, $force = false) - { - if (! empty($domains)) { + private static function runAcmeSh(array $certrow, array $domains, &$cronlog = null, $force = false) + { + if (! empty($domains)) { - $acmesh_cmd = self::$acmesh . " --server " . self::$apiserver . " --issue -d " . implode(" -d ", $domains); - // challenge path - $acmesh_cmd .= " -w " . Settings::Get('system.letsencryptchallengepath'); - if (Settings::Get('system.leecc') > 0) { - // ecc certificate - $acmesh_cmd .= " --keylength ec-" . Settings::Get('system.leecc'); - } else { - $acmesh_cmd .= " --keylength " . Settings::Get('system.letsencryptkeysize'); - } - if (Settings::Get('system.letsencryptreuseold') != '1') { - $acmesh_cmd .= " --always-force-new-domain-key"; - } - if (Settings::Get('system.letsencryptca') == 'testing') { - $acmesh_cmd .= " --staging"; - } - if ($force) { - $acmesh_cmd .= " --force"; - } - if (defined('CRON_DEBUG_FLAG')) { - $acmesh_cmd .= " --debug"; - } + $acmesh_cmd = self::$acmesh . " --server " . self::$apiserver . " --issue -d " . implode(" -d ", $domains); + // challenge path + $acmesh_cmd .= " -w " . Settings::Get('system.letsencryptchallengepath'); + if (Settings::Get('system.leecc') > 0) { + // ecc certificate + $acmesh_cmd .= " --keylength ec-" . Settings::Get('system.leecc'); + } else { + $acmesh_cmd .= " --keylength " . Settings::Get('system.letsencryptkeysize'); + } + if (Settings::Get('system.letsencryptreuseold') != '1') { + $acmesh_cmd .= " --always-force-new-domain-key"; + } + if (Settings::Get('system.letsencryptca') == 'letsencrypt_test') { + $acmesh_cmd .= " --staging"; + } + if ($force) { + $acmesh_cmd .= " --force"; + } + if (defined('CRON_DEBUG_FLAG')) { + $acmesh_cmd .= " --debug"; + } - $acme_result = \Froxlor\FileDir::safe_exec($acmesh_cmd); - // debug output of acme.sh run - $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, implode("\n", $acme_result)); + $acme_result = \Froxlor\FileDir::safe_exec($acmesh_cmd); + // debug output of acme.sh run + $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, implode("\n", $acme_result)); - self::certToDb($certrow, $cronlog, $acme_result); - } - } + self::certToDb($certrow, $cronlog, $acme_result); + } + } - private static function certToDb($certrow, &$cronlog, $acme_result) - { - $return = array(); - self::readCertificateToVar(strtolower($certrow['domain']), $return, $cronlog); + private static function certToDb($certrow, &$cronlog, $acme_result) + { + $return = array(); + self::readCertificateToVar(strtolower($certrow['domain']), $return, $cronlog); - if (! empty($return['crt'])) { + if (! empty($return['crt'])) { - $newcert = openssl_x509_parse($return['crt']); + $newcert = openssl_x509_parse($return['crt']); - if ($newcert) { - // Store the new data - Database::pexecute(self::$updcert_stmt, array( - 'id' => $certrow['id'], - 'domainid' => $certrow['domainid'], - 'crt' => $return['crt'], - 'key' => $return['key'], - 'ca' => $return['chain'], - 'chain' => $return['chain'], - 'csr' => $return['csr'], - 'fullchain' => $return['fullchain'], - 'expirationdate' => date('Y-m-d H:i:s', $newcert['validTo_time_t']) - )); + if ($newcert) { + // Store the new data + Database::pexecute(self::$updcert_stmt, array( + 'id' => $certrow['id'], + 'domainid' => $certrow['domainid'], + 'crt' => $return['crt'], + 'key' => $return['key'], + 'ca' => $return['chain'], + 'chain' => $return['chain'], + 'csr' => $return['csr'], + 'fullchain' => $return['fullchain'], + 'expirationdate' => date('Y-m-d H:i:s', $newcert['validTo_time_t']) + )); - if ($certrow['ssl_redirect'] == 3) { - Database::pexecute(self::$upddom_stmt, array( - 'domainid' => $certrow['domainid'] - )); - } + if ($certrow['ssl_redirect'] == 3) { + Database::pexecute(self::$upddom_stmt, array( + 'domainid' => $certrow['domainid'] + )); + } - $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Updated Let's Encrypt certificate for " . $certrow['domain']); - } else { - $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, "Got non-successful Let's Encrypt response for " . $certrow['domain'] . ":\n" . implode("\n", $acme_result)); - } - } else { - $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, "Could not get Let's Encrypt certificate for " . $certrow['domain'] . ":\n" . implode("\n", $acme_result)); - } - } + $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Updated Let's Encrypt certificate for " . $certrow['domain']); + } else { + $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, "Got non-successful Let's Encrypt response for " . $certrow['domain'] . ":\n" . implode("\n", $acme_result)); + } + } else { + $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, "Could not get Let's Encrypt certificate for " . $certrow['domain'] . ":\n" . implode("\n", $acme_result)); + } + } - /** - * check whether we need to issue a new certificate for froxlor itself - * - * @return boolean - */ - private static function issueFroxlorVhost() - { - if (Settings::Get('system.le_froxlor_enabled') == '1') { - // let's encrypt is enabled, now check whether we have a certificate - $froxlor_ssl_settings_stmt = Database::prepare(" + /** + * check whether we need to issue a new certificate for froxlor itself + * + * @return boolean + */ + private static function issueFroxlorVhost() + { + if (Settings::Get('system.le_froxlor_enabled') == '1') { + // let's encrypt is enabled, now check whether we have a certificate + $froxlor_ssl_settings_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` WHERE `domainid` = '0' "); - $froxlor_ssl = Database::pexecute_first($froxlor_ssl_settings_stmt); - // also check for possible existing certificate - if (! $froxlor_ssl && ! self::checkFsFilesAreNewer(Settings::Get('system.hostname'), date('Y-m-d H:i:s'))) { - return true; - } - } - return false; - } + $froxlor_ssl = Database::pexecute_first($froxlor_ssl_settings_stmt); + // also check for possible existing certificate + if (! $froxlor_ssl && ! self::checkFsFilesAreNewer(Settings::Get('system.hostname'), date('Y-m-d H:i:s'))) { + return true; + } + } + return false; + } - /** - * check whether we need to renew-check the certificate for froxlor itself - * - * @return boolean - */ - private static function renewFroxlorVhost() - { - if (Settings::Get('system.le_froxlor_enabled') == '1') { - // let's encrypt is enabled, now check whether we have a certificate - $froxlor_ssl_settings_stmt = Database::prepare(" + /** + * check whether we need to renew-check the certificate for froxlor itself + * + * @return boolean + */ + private static function renewFroxlorVhost() + { + if (Settings::Get('system.le_froxlor_enabled') == '1') { + // let's encrypt is enabled, now check whether we have a certificate + $froxlor_ssl_settings_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` WHERE `domainid` = '0' "); - $froxlor_ssl = Database::pexecute_first($froxlor_ssl_settings_stmt); - // also check for possible existing certificate - if ($froxlor_ssl && self::checkFsFilesAreNewer(Settings::Get('system.hostname'), $froxlor_ssl['expirationdate'])) { - return $froxlor_ssl; - } - } - return false; - } + $froxlor_ssl = Database::pexecute_first($froxlor_ssl_settings_stmt); + // also check for possible existing certificate + if ($froxlor_ssl && self::checkFsFilesAreNewer(Settings::Get('system.hostname'), $froxlor_ssl['expirationdate'])) { + return $froxlor_ssl; + } + } + return false; + } - /** - * get a list of domains that have a lets encrypt certificate (possible renew) - */ - private static function renewDomains($check = false) - { - $certificates_stmt = Database::query(" + /** + * get a list of domains that have a lets encrypt certificate (possible renew) + */ + private static function renewDomains($check = false) + { + $certificates_stmt = Database::query(" SELECT domssl.`id`, domssl.`domainid`, @@ -435,27 +441,27 @@ class AcmeSh extends \Froxlor\Cron\FroxlorCron AND dom.`aliasdomain` IS NULL AND dom.`iswildcarddomain` = 0 "); - $renew_certs = $certificates_stmt->fetchAll(\PDO::FETCH_ASSOC); - if ($renew_certs) { - if ($check) { - foreach ($renew_certs as $cert) { - if (self::checkFsFilesAreNewer($cert['domain'], $cert['expirationdate'])) { - return true; - } - } - return false; - } - return $renew_certs; - } - return array(); - } + $renew_certs = $certificates_stmt->fetchAll(\PDO::FETCH_ASSOC); + if ($renew_certs) { + if ($check) { + foreach ($renew_certs as $cert) { + if (self::checkFsFilesAreNewer($cert['domain'], $cert['expirationdate'])) { + return true; + } + } + return false; + } + return $renew_certs; + } + return array(); + } - /** - * get a list of domains that require a new certificate (issue) - */ - private static function issueDomains() - { - $certificates_stmt = Database::query(" + /** + * get a list of domains that require a new certificate (issue) + */ + private static function issueDomains() + { + $certificates_stmt = Database::query(" SELECT domssl.`id`, domssl.`domainid`, @@ -488,125 +494,125 @@ class AcmeSh extends \Froxlor\Cron\FroxlorCron AND dom.`iswildcarddomain` = 0 AND domssl.`expirationdate` IS NULL "); - $customer_ssl = $certificates_stmt->fetchAll(\PDO::FETCH_ASSOC); - if ($customer_ssl) { - return $customer_ssl; - } - return array(); - } + $customer_ssl = $certificates_stmt->fetchAll(\PDO::FETCH_ASSOC); + if ($customer_ssl) { + return $customer_ssl; + } + return array(); + } - private static function checkFsFilesAreNewer($domain, $cert_date = 0) - { - $certificate_folder = self::getWorkingDirFromEnv(strtolower($domain)); - $ssl_file = \Froxlor\FileDir::makeCorrectFile($certificate_folder . '/' . strtolower($domain) . '.cer'); + private static function checkFsFilesAreNewer($domain, $cert_date = 0) + { + $certificate_folder = self::getWorkingDirFromEnv(strtolower($domain)); + $ssl_file = \Froxlor\FileDir::makeCorrectFile($certificate_folder . '/' . strtolower($domain) . '.cer'); - if (is_dir($certificate_folder) && file_exists($ssl_file) && is_readable($ssl_file)) { - $cert_data = openssl_x509_parse(file_get_contents($ssl_file)); - if ($cert_data && $cert_data['validTo_time_t'] > strtotime($cert_date)) { - return true; - } - } - return false; - } + if (is_dir($certificate_folder) && file_exists($ssl_file) && is_readable($ssl_file)) { + $cert_data = openssl_x509_parse(file_get_contents($ssl_file)); + if ($cert_data && $cert_data['validTo_time_t'] > strtotime($cert_date)) { + return true; + } + } + return false; + } - public static function getWorkingDirFromEnv($domain = "", $forced_noecc = false) - { - if (Settings::Get('system.leecc') > 0 && ! $forced_noecc) { - $domain .= "_ecc"; - } - $env_file = FileDir::makeCorrectFile(dirname(self::$acmesh) . '/acme.sh.env'); - if (file_exists($env_file)) { - $output = []; - $cut = << 0 && ! $forced_noecc) { + $domain .= "_ecc"; + } + $env_file = FileDir::makeCorrectFile(dirname(self::$acmesh) . '/acme.sh.env'); + if (file_exists($env_file)) { + $output = []; + $cut = << 0) { - $certificate_folder_noecc = self::getWorkingDirFromEnv($domain, true); - } - $certificate_folder = \Froxlor\FileDir::makeCorrectDir($certificate_folder); + /** + * get certificate files from filesystem and store in $return array + * + * @param string $domain + * @param array $return + * @param object $cronlog + */ + private static function readCertificateToVar($domain, &$return, &$cronlog) + { + $certificate_folder = self::getWorkingDirFromEnv($domain); + $certificate_folder_noecc = null; + if (Settings::Get('system.leecc') > 0) { + $certificate_folder_noecc = self::getWorkingDirFromEnv($domain, true); + } + $certificate_folder = \Froxlor\FileDir::makeCorrectDir($certificate_folder); - if (is_dir($certificate_folder) || is_dir($certificate_folder_noecc)) { - foreach ([ - 'crt' => $domain . '.cer', - 'key' => $domain . '.key', - 'chain' => 'ca.cer', - 'fullchain' => 'fullchain.cer', - 'csr' => $domain . '.csr' - ] as $index => $sslfile) { - $ssl_file = \Froxlor\FileDir::makeCorrectFile($certificate_folder . '/' . $sslfile); - if (file_exists($ssl_file)) { - $return[$index] = file_get_contents($ssl_file); - } else { - if (! empty($certificate_folder_noecc)) { - $ssl_file_fb = \Froxlor\FileDir::makeCorrectFile($certificate_folder_noecc . '/' . $sslfile); - if (file_exists($ssl_file_fb)) { - $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_WARNING, "ECC certificates activated but found only non-ecc file"); - $return[$index] = file_get_contents($ssl_file_fb); - continue; - } - } - $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, "Could not find file '" . $sslfile . "' in '" . $certificate_folder . "'"); - $return[$index] = null; - } - } - } else { - $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, "Could not find certificate-folder '" . $certificate_folder . "'"); - } - } + if (is_dir($certificate_folder) || is_dir($certificate_folder_noecc)) { + foreach ([ + 'crt' => $domain . '.cer', + 'key' => $domain . '.key', + 'chain' => 'ca.cer', + 'fullchain' => 'fullchain.cer', + 'csr' => $domain . '.csr' + ] as $index => $sslfile) { + $ssl_file = \Froxlor\FileDir::makeCorrectFile($certificate_folder . '/' . $sslfile); + if (file_exists($ssl_file)) { + $return[$index] = file_get_contents($ssl_file); + } else { + if (! empty($certificate_folder_noecc)) { + $ssl_file_fb = \Froxlor\FileDir::makeCorrectFile($certificate_folder_noecc . '/' . $sslfile); + if (file_exists($ssl_file_fb)) { + $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_WARNING, "ECC certificates activated but found only non-ecc file"); + $return[$index] = file_get_contents($ssl_file_fb); + continue; + } + } + $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, "Could not find file '" . $sslfile . "' in '" . $certificate_folder . "'"); + $return[$index] = null; + } + } + } else { + $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, "Could not find certificate-folder '" . $certificate_folder . "'"); + } + } - /** - * install acme.sh if not found yet - */ - private static function checkInstall($tries = 0) - { - if (! file_exists(self::$acmesh) && $tries > 0) { - FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, "Download/installation of acme.sh seems to have failed. Re-run cronjob to try again or install manually to '" . self::$acmesh . "'"); - echo PHP_EOL . "Download/installation of acme.sh seems to have failed. Re-run cronjob to try again or install manually to '" . self::$acmesh . "'" . PHP_EOL; - return false; - } else if (! file_exists(self::$acmesh)) { - FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Could not find acme.sh - installing it to /root/.acme.sh/"); - $return = false; - \Froxlor\FileDir::safe_exec("wget -O - https://get.acme.sh | sh", $return, array( - '|' - )); - // check whether the installation worked - return self::checkInstall(++ $tries); - } - return true; - } + /** + * install acme.sh if not found yet + */ + private static function checkInstall($tries = 0) + { + if (! file_exists(self::$acmesh) && $tries > 0) { + FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, "Download/installation of acme.sh seems to have failed. Re-run cronjob to try again or install manually to '" . self::$acmesh . "'"); + echo PHP_EOL . "Download/installation of acme.sh seems to have failed. Re-run cronjob to try again or install manually to '" . self::$acmesh . "'" . PHP_EOL; + return false; + } else if (! file_exists(self::$acmesh)) { + FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Could not find acme.sh - installing it to /root/.acme.sh/"); + $return = false; + \Froxlor\FileDir::safe_exec("wget -O - https://get.acme.sh | sh", $return, array( + '|' + )); + // check whether the installation worked + return self::checkInstall(++ $tries); + } + return true; + } - /** - * run upgrade - */ - private static function checkUpgrade() - { - $acmesh_result = \Froxlor\FileDir::safe_exec(self::$acmesh . " --upgrade --auto-upgrade 0"); - // check for activated cron - $acmesh_result2 = \Froxlor\FileDir::safe_exec(self::$acmesh . " --install-cronjob"); - FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Checking for LetsEncrypt client upgrades before renewing certificates:\n" . implode("\n", $acmesh_result) . "\n" . implode("\n", $acmesh_result2)); - } + /** + * run upgrade + */ + private static function checkUpgrade() + { + $acmesh_result = \Froxlor\FileDir::safe_exec(self::$acmesh . " --upgrade --auto-upgrade 0"); + // check for activated cron + $acmesh_result2 = \Froxlor\FileDir::safe_exec(self::$acmesh . " --install-cronjob"); + FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Checking for LetsEncrypt client upgrades before renewing certificates:\n" . implode("\n", $acmesh_result) . "\n" . implode("\n", $acmesh_result2)); + } } diff --git a/lib/Froxlor/Cron/Http/Php/Fpm.php b/lib/Froxlor/Cron/Http/Php/Fpm.php index c74cace8..cac90aa8 100644 --- a/lib/Froxlor/Cron/Http/Php/Fpm.php +++ b/lib/Froxlor/Cron/Http/Php/Fpm.php @@ -218,7 +218,7 @@ class Fpm $openbasedir .= $_phpappendopenbasedir; } } - $fpm_config .= 'php_admin_value[session.save_path] = ' . \Froxlor\FileDir::makeCorrectDir(Settings::Get('phpfpm.tmpdir') . '/' . $this->domain['loginname'] . '/') . "\n"; + $fpm_config .= 'php_admin_value[upload_tmp_dir] = ' . \Froxlor\FileDir::makeCorrectDir(Settings::Get('phpfpm.tmpdir') . '/' . $this->domain['loginname'] . '/') . "\n"; $admin = $this->getAdminData($this->domain['adminid']); @@ -261,6 +261,11 @@ class Fpm $fpm_config .= 'php_admin_value[sendmail_path] = /usr/sbin/sendmail -t -i -f ' . $this->domain['email'] . "\n"; } + // check for session.save_path, whether it has been specified by the user, if not, set a default + if (strpos($fpm_config, 'php_value[session.save_path]') === false && strpos($fpm_config, 'php_admin_value[session.save_path]') === false) { + $fpm_config .= 'php_admin_value[session.save_path] = ' . $this->getTempDir() . "\n"; + } + // append custom phpfpm configuration if (! empty($fpm_custom_config)) { $fpm_config .= "\n; Custom Configuration\n"; diff --git a/lib/Froxlor/Dns/Dns.php b/lib/Froxlor/Dns/Dns.php index fa95586e..f333c3d1 100644 --- a/lib/Froxlor/Dns/Dns.php +++ b/lib/Froxlor/Dns/Dns.php @@ -53,7 +53,7 @@ class Dns $domain = $domain_id; } - if ($domain['isbinddomain'] != '1') { + if (!isset($domain['isbinddomain']) || $domain['isbinddomain'] != '1') { return; } @@ -190,12 +190,26 @@ class Dns '@', 'www', '*' - ] as $crceord) { - if ($entry['type'] == 'CNAME' && $entry['record'] == '@' && (array_key_exists(md5($crceord), $required_entries['A']) || array_key_exists(md5($crceord), $required_entries['AAAA']))) { - unset($required_entries['A'][md5($crceord)]); - unset($required_entries['AAAA'][md5($crceord)]); + ] as $crecord) { + if ($entry['type'] == 'CNAME' && $entry['record'] == '@' && (array_key_exists(md5($crecord), $required_entries['A']) || array_key_exists(md5($crecord), $required_entries['AAAA']))) { + unset($required_entries['A'][md5($crecord)]); + unset($required_entries['AAAA'][md5($crecord)]); } } + // also allow overriding of auto-generated values (imap,pop3,mail,smtp) if enabled in the settings + if (Settings::Get('system.dns_createmailentry')) { + foreach (array( + 'imap', + 'pop3', + 'mail', + 'smtp' + ) as $crecord) { + if ($entry['type'] == 'CNAME' && $entry['record'] == $crecord && (array_key_exists(md5($crecord), $required_entries['A']) || array_key_exists(md5($crecord), $required_entries['AAAA']))) { + unset($required_entries['A'][md5($crecord)]); + unset($required_entries['AAAA'][md5($crecord)]); + } + } + } $zonerecords[] = new DnsEntry($entry['record'], $entry['type'], $entry['content'], $entry['prio'], $entry['ttl']); } diff --git a/lib/Froxlor/Froxlor.php b/lib/Froxlor/Froxlor.php index 97277643..9e9fb3f2 100644 --- a/lib/Froxlor/Froxlor.php +++ b/lib/Froxlor/Froxlor.php @@ -10,7 +10,7 @@ final class Froxlor const VERSION = '0.10.26'; // Database version (YYYYMMDDC where C is a daily counter) - const DBVERSION = '202106160'; + const DBVERSION = '202107070'; // Distribution branding-tag (used for Debian etc.) const BRANDING = ''; @@ -63,7 +63,7 @@ final class Froxlor * * @param string $to_check * version to check, if empty current version is used - * + * * @return bool true if version to check does not match, else false */ public static function hasUpdates($to_check = null) @@ -84,7 +84,7 @@ final class Froxlor * * @param int $to_check * version to check, if empty current dbversion is used - * + * * @return bool true if version to check does not match, else false */ public static function hasDbUpdates($to_check = null) @@ -105,7 +105,7 @@ final class Froxlor * * @param int $to_check * version to check - * + * * @return bool true if version to check matches, else false */ public static function isDatabaseVersion($to_check = null) @@ -124,7 +124,7 @@ final class Froxlor * * @param string $new_version * new-version - * + * * @return bool true on success, else false */ public static function updateToDbVersion($new_version = null) @@ -150,7 +150,7 @@ final class Froxlor * * @param string $new_version * new-version - * + * * @return bool true on success, else false */ public static function updateToVersion($new_version = null) @@ -191,7 +191,7 @@ final class Froxlor * * @param string $to_check * version to check - * + * * @return bool true if version to check matches, else false */ public static function isFroxlorVersion($to_check = null) diff --git a/lib/Froxlor/SImExporter.php b/lib/Froxlor/SImExporter.php index 7120aaad..5a549f97 100644 --- a/lib/Froxlor/SImExporter.php +++ b/lib/Froxlor/SImExporter.php @@ -16,9 +16,9 @@ use Froxlor\Database\Database; * @author Froxlor team (2018-) * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt * @package Classes - * + * * @since 0.9.39 - * + * */ /** @@ -60,6 +60,13 @@ class SImExporter public static function export() { + $settings_definitions = []; + foreach (\Froxlor\PhpHelper::loadConfigArrayDir('./actions/admin/settings/')['groups'] AS $group) { + foreach ($group['fields'] AS $field) { + $settings_definitions[$field['settinggroup']][$field['varname']] = $field; + } + } + $result_stmt = Database::query(" SELECT * FROM `" . TABLE_PANEL_SETTINGS . "` ORDER BY `settingid` ASC "); @@ -69,13 +76,26 @@ class SImExporter if (! in_array($index, self::$no_export)) { $_data[$index] = $row['value']; } + + if (array_key_exists($row['settinggroup'], $settings_definitions) && array_key_exists($row['varname'], $settings_definitions[$row['settinggroup']])) { + // Export image file + if ($settings_definitions[$row['settinggroup']][$row['varname']]['type'] === "image") { + if ($row['value'] === "") { + continue; + } + + $_data[$index.'.image_data'] = base64_encode(file_get_contents(explode('?', $row['value'], 2)[0])); + } + } } + // add checksum for validation $_data['_sha'] = sha1(var_export($_data, true)); $_export = json_encode($_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); if (! $_export) { throw new \Exception("Error exporting settings: " . json_last_error_msg()); } + return $_export; } @@ -120,6 +140,26 @@ class SImExporter } // store new data foreach ($_data as $index => $value) { + $index_split = explode('.', $index, 3); + + // Catch image_data and save it + if (isset($index_split[2]) && $index_split[2] === 'image_data' && !empty($_data[$index_split[0].'.'.$index_split[1]])) { + $path = \Froxlor\Froxlor::getInstallDir().'/img/'; + if (!is_dir($path) && !mkdir($path, '0775')) { + throw new \Exception("img directory does not exist and cannot be created"); + } + + // Make sure we can write to the upload directory + if (!is_writable($path)) { + if (!chmod($path, '0775')) { + throw new \Exception("Cannot write to img directory"); + } + } + + file_put_contents(\Froxlor\Froxlor::getInstallDir() . '/' . explode('?', $_data[$index_split[0].'.'.$index_split[1]], 2)[0], base64_decode($value)); + continue; + } + Settings::Set($index, $value); } // save to DB diff --git a/lib/Froxlor/Settings/Store.php b/lib/Froxlor/Settings/Store.php index e4a8035e..69e365fa 100644 --- a/lib/Froxlor/Settings/Store.php +++ b/lib/Froxlor/Settings/Store.php @@ -367,4 +367,67 @@ class Store return $returnvalue; } + + public static function storeSettingImage($fieldname, $fielddata) + { + if (isset($fielddata['settinggroup'], $fielddata['varname']) && is_array($fielddata) && $fielddata['settinggroup'] !== '' && $fielddata['varname'] !== '') { + $save_to = null; + $path = \Froxlor\Froxlor::getInstallDir().'/img/'; + + // New file? + if ($_FILES[$fieldname]['tmp_name']) { + // Make sure upload directory exists + if (!is_dir($path) && !mkdir($path, '0775')) { + throw new \Exception("img directory does not exist and cannot be created"); + } + + // Make sure we can write to the upload directory + if (!is_writable($path)) { + if (!chmod($path, '0775')) { + throw new \Exception("Cannot write to img directory"); + } + } + + // Make sure mime-type matches an image + if (!in_array(mime_content_type($_FILES[$fieldname]['tmp_name']), ['image/jpeg','image/jpg','image/png','image/gif'])) { + throw new \Exception("Uploaded file not a valid image"); + } + + // Determine file extension + $spl = explode('.', $_FILES[$fieldname]['name']); + $file_extension = strtolower(array_pop($spl)); + unset($spl); + + // Move file + if (!move_uploaded_file($_FILES[$fieldname]['tmp_name'], $path.$fielddata['image_name'].'.'.$file_extension)) { + throw new \Exception("Unable to save image to img folder"); + } + + $save_to = 'img/'.$fielddata['image_name'].'.'.$file_extension.'?v='.time(); + } + + // Delete file? + if ($fielddata['value'] !== "" && array_key_exists($fieldname.'_delete', $_POST) && $_POST[$fieldname.'_delete']) { + @unlink(\Froxlor\Froxlor::getInstallDir() . '/' . explode('?', $fielddata['value'], 2)[0]); + $save_to = ''; + } + + // Nothing changed + if ($save_to === null) { + return array( + $fielddata['settinggroup'] . '.' . $fielddata['varname'] => $fielddata['value'] + ); + } + + if (Settings::Set($fielddata['settinggroup'] . '.' . $fielddata['varname'], $save_to) === false) { + return false; + } + + return array( + $fielddata['settinggroup'] . '.' . $fielddata['varname'] => $save_to + ); + } + + return false; + } } diff --git a/lib/Froxlor/UI/Data.php b/lib/Froxlor/UI/Data.php index a32ffa31..ee34f012 100644 --- a/lib/Froxlor/UI/Data.php +++ b/lib/Froxlor/UI/Data.php @@ -52,6 +52,12 @@ class Data return $newfieldvalue; } + public static function getFormFieldDataImage($fieldname, $fielddata, $input) + { + // We always make the system think we have new data to trigger the save function where we actually check everything + return time(); + } + public static function manipulateFormFieldDataDate($fieldname, $fielddata, $newfieldvalue) { if (isset($fielddata['date_timestamp']) && $fielddata['date_timestamp'] === true) { diff --git a/lib/Froxlor/UI/Fields.php b/lib/Froxlor/UI/Fields.php index c138538a..681e701c 100644 --- a/lib/Froxlor/UI/Fields.php +++ b/lib/Froxlor/UI/Fields.php @@ -89,6 +89,15 @@ class Fields return $returnvalue; } + public static function getFormFieldOutputImage($fieldname, $fielddata, $do_show = true) + { + global $lng; + $label = $fielddata['label']; + $value = htmlentities($fielddata['value']); + eval("\$returnvalue = \"" . \Froxlor\UI\Template::getTemplate("formfields/image", true) . "\";"); + return $returnvalue; + } + public static function getFormFieldOutputDate($fieldname, $fielddata, $do_show = true) { if (isset($fielddata['date_timestamp']) && $fielddata['date_timestamp'] === true) { diff --git a/lib/configfiles/focal.xml b/lib/configfiles/focal.xml index 588f0e4d..0a060b63 100644 --- a/lib/configfiles/focal.xml +++ b/lib/configfiles/focal.xml @@ -74,7 +74,7 @@ Alias "/.well-known/acme-challenge" "{{settings.system.letsencryptchallengepath} ]]> - + @@ -138,7 +138,7 @@ include_shell "/usr/share/lighttpd/include-conf-enabled.pl" - + @@ -354,7 +354,6 @@ exit "$RETVAL" - @@ -366,7 +365,7 @@ exit "$RETVAL" - + @@ -908,7 +907,7 @@ gmysql-password= ]]> - + @@ -1455,7 +1454,7 @@ bind-check-interval=180 ]]> - + @@ -1578,7 +1577,7 @@ root: root@ - + @@ -3299,7 +3298,7 @@ plugin { - + @@ -3722,7 +3721,7 @@ TLSVerifyClient off ]]> - + @@ -3948,7 +3947,7 @@ UPLOADGID= ]]> - + @@ -4088,7 +4087,7 @@ aliases: files {{settings.system.webserver}} - + diff --git a/lib/init.php b/lib/init.php index a0a8a431..f34565c1 100644 --- a/lib/init.php +++ b/lib/init.php @@ -380,11 +380,8 @@ if (! array_key_exists('variants', $_themeoptions) || ! array_key_exists($themev // check for custom header-graphic $hl_path = 'templates/' . $theme . '/assets/img'; -$header_logo = $hl_path . '/logo.png'; - -if (file_exists($hl_path . '/logo_custom.png')) { - $header_logo = $hl_path . '/logo_custom.png'; -} +$header_logo = Settings::Get('panel.logo_image_header') ?: $hl_path . '/logo.png'; +$header_logo_login = Settings::Get('panel.logo_image_login') ?: $hl_path . '/logo.png'; /** * Redirects to index.php (login page) if no session exists diff --git a/lng/english.lng.php b/lng/english.lng.php index 1283c6f4..afb00a0a 100644 --- a/lng/english.lng.php +++ b/lng/english.lng.php @@ -1839,8 +1839,8 @@ $lng['error']['sslredirectonlypossiblewithsslipport'] = 'Using Let\'s Encrypt is $lng['error']['nowildcardwithletsencrypt'] = 'Let\'s Encrypt cannot handle wildcard-domains using ACME in froxlor (requires dns-challenge), sorry. Please set the ServerAlias to WWW or disable it completely'; $lng['panel']['letsencrypt'] = 'Using Let\'s encrypt'; $lng['crondesc']['cron_letsencrypt'] = 'updating Let\'s Encrypt certificates'; -$lng['serversettings']['letsencryptca']['title'] = "Let's Encrypt environment"; -$lng['serversettings']['letsencryptca']['description'] = "Environment to be used for Let's Encrypt certificates."; +$lng['serversettings']['letsencryptca']['title'] = "ACME environment"; +$lng['serversettings']['letsencryptca']['description'] = "Environment to be used for Let's Encrypt / ZeroSSL certificates."; $lng['serversettings']['letsencryptcountrycode']['title'] = "Let's Encrypt country code"; $lng['serversettings']['letsencryptcountrycode']['description'] = "2 letter country code used to generate Let's Encrypt certificates."; $lng['serversettings']['letsencryptstate']['title'] = "Let's Encrypt state"; @@ -2117,3 +2117,9 @@ $lng['privacy'] = 'Privacy policy'; $lng['serversettings']['privacy_url']['title'] = 'URL to privacy policy'; $lng['serversettings']['privacy_url']['description'] = 'Specify an URL to your privacy policy site / imprint site. The link will be visible on the login screen and on the footer when logged in.'; $lng['admin']['domaindefaultalias'] = 'Default ServerAlias value for new domains'; + +$lng['serversettings']['logo_image_header']['title'] = 'Logo Image (Header)'; +$lng['serversettings']['logo_image_header']['description'] = 'Upload your own logo image to be shown in the header after login (recommended height 30px)'; +$lng['serversettings']['logo_image_login']['title'] = 'Logo Image (Login)'; +$lng['serversettings']['logo_image_login']['description'] = 'Upload your own logo image to be shown during login'; +$lng['panel']['image_field_delete'] = 'Delete the existing current image'; diff --git a/lng/german.lng.php b/lng/german.lng.php index 13cdcb4d..c9cf309c 100644 --- a/lng/german.lng.php +++ b/lng/german.lng.php @@ -1490,8 +1490,8 @@ $lng['error']['sslredirectonlypossiblewithsslipport'] = 'Die Nutzung von Let\'s $lng['error']['nowildcardwithletsencrypt'] = 'Let\'s Encrypt kann mittels ACME Wildcard-Domains nur via DNS validieren, sorry. Bitte den ServerAlias auf WWW setzen oder deaktivieren'; $lng['panel']['letsencrypt'] = 'Benutzt Let\'s encrypt'; $lng['crondesc']['cron_letsencrypt'] = 'Aktualisierung der Let\'s Encrypt Zertifikate'; -$lng['serversettings']['letsencryptca']['title'] = "Let's Encrypt Umgebung"; -$lng['serversettings']['letsencryptca']['description'] = "Let's Encrypt - Umgebung, welche genutzt wird um Zertifikate zu bestellen."; +$lng['serversettings']['letsencryptca']['title'] = "ACME Umgebung"; +$lng['serversettings']['letsencryptca']['description'] = "Umgebung, welche genutzt wird um Zertifikate zu bestellen."; $lng['serversettings']['letsencryptcountrycode']['title'] = "Let's Encrypt Ländercode"; $lng['serversettings']['letsencryptcountrycode']['description'] = "2 - stelliger Ländercode, welcher benutzt wird um Let's Encrypt - Zertifikate zu bestellen."; $lng['serversettings']['letsencryptstate']['title'] = "Let's Encrypt Bundesland"; @@ -1763,3 +1763,9 @@ $lng['privacy'] = 'Datenschutzerklärung'; $lng['serversettings']['privacy_url']['title'] = 'URL zur Datenschutzerklärung'; $lng['serversettings']['privacy_url']['description'] = 'Die URL zur Datenschutzerklärungs-Seite. Der Link ist auf der Login-Seite und wenn eingeloggt, in der Fußzeile sichtbar.'; $lng['admin']['domaindefaultalias'] = 'Standard ServerAlias-Angabe für neue Domains'; + +$lng['serversettings']['logo_image_header']['title'] = 'Logo Bild (Header)'; +$lng['serversettings']['logo_image_header']['description'] = 'Das hochgeladene Bild wird als Logo oben links nach dem Login angezeigt (empfohlene Höhe sind 30px)'; +$lng['serversettings']['logo_image_login']['title'] = 'Logo Bild (Login)'; +$lng['serversettings']['logo_image_login']['description'] = 'Das hochgeladene Bild wird als Logo während des Logins angezeigt'; +$lng['panel']['image_field_delete'] = 'Das momentan vorhandene Bild löschen'; diff --git a/templates/Sparkle/2fa/entercode.tpl b/templates/Sparkle/2fa/entercode.tpl index 2f2d84f6..b79090e6 100644 --- a/templates/Sparkle/2fa/entercode.tpl +++ b/templates/Sparkle/2fa/entercode.tpl @@ -1,7 +1,7 @@ $header