Repository: froxlor/Froxlor Branch: main Commit: 070e537744f2 Files: 443 Total size: 4.7 MB Directory structure: gitextract_fhz65cs7/ ├── .editorconfig ├── .github/ │ ├── CONTRIBUTING.md │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── ISSUE_TEMPLATE.md │ ├── LICENSE_HEADER │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows/ │ ├── build-docs.yml │ ├── build-mariadb.yml │ └── build-mysql.yml ├── .gitignore ├── 2fa.php ├── COPYING ├── README.md ├── SECURITY.md ├── actions/ │ ├── admin/ │ │ ├── index.html │ │ └── settings/ │ │ ├── 100.panel.php │ │ ├── 110.accounts.php │ │ ├── 120.system.php │ │ ├── 122.froxlorvhost.php │ │ ├── 125.cronjob.php │ │ ├── 130.webserver.php │ │ ├── 131.ssl.php │ │ ├── 135.fcgid.php │ │ ├── 136.phpfpm.php │ │ ├── 137.perl.php │ │ ├── 140.statistics.php │ │ ├── 150.mail.php │ │ ├── 155.ftpserver.php │ │ ├── 160.nameserver.php │ │ ├── 170.logger.php │ │ ├── 180.antispam.php │ │ ├── 210.security.php │ │ ├── 220.quota.php │ │ └── index.html │ └── index.html ├── admin_admins.php ├── admin_apcuinfo.php ├── admin_autoupdate.php ├── admin_configfiles.php ├── admin_cronjobs.php ├── admin_customers.php ├── admin_domains.php ├── admin_index.php ├── admin_ipsandports.php ├── admin_logger.php ├── admin_message.php ├── admin_mysqlserver.php ├── admin_opcacheinfo.php ├── admin_phpsettings.php ├── admin_plans.php ├── admin_settings.php ├── admin_templates.php ├── admin_traffic.php ├── admin_updates.php ├── api.php ├── api_keys.php ├── bin/ │ └── froxlor-cli ├── build.xml ├── composer.json ├── customer_domains.php ├── customer_email.php ├── customer_extras.php ├── customer_ftp.php ├── customer_index.php ├── customer_logger.php ├── customer_mysql.php ├── customer_traffic.php ├── dns_editor.php ├── doc/ │ ├── example/ │ │ ├── FroxlorAPI.php │ │ ├── create_customer.php │ │ ├── index.html │ │ └── list_functions.php │ └── index.html ├── error_report.php ├── index.php ├── install/ │ ├── froxlor.sql.php │ ├── index.html │ ├── install.php │ ├── updates/ │ │ ├── froxlor/ │ │ │ ├── index.html │ │ │ ├── update_2.0.inc.php │ │ │ ├── update_2.1.inc.php │ │ │ ├── update_2.2.inc.php │ │ │ └── update_2.3.inc.php │ │ ├── index.html │ │ └── preconfig/ │ │ ├── index.html │ │ ├── preconfig_2.0.inc.php │ │ ├── preconfig_2.1.inc.php │ │ ├── preconfig_2.2.inc.php │ │ └── preconfig_2.3.inc.php │ └── updatesql.php ├── lib/ │ ├── Froxlor/ │ │ ├── Ajax/ │ │ │ ├── Ajax.php │ │ │ ├── GlobalSearch.php │ │ │ └── index.html │ │ ├── Api/ │ │ │ ├── Api.php │ │ │ ├── ApiCommand.php │ │ │ ├── ApiParameter.php │ │ │ ├── Commands/ │ │ │ │ ├── Admins.php │ │ │ │ ├── Certificates.php │ │ │ │ ├── Cronjobs.php │ │ │ │ ├── Customers.php │ │ │ │ ├── DataDump.php │ │ │ │ ├── DirOptions.php │ │ │ │ ├── DirProtections.php │ │ │ │ ├── DomainZones.php │ │ │ │ ├── Domains.php │ │ │ │ ├── EmailAccounts.php │ │ │ │ ├── EmailDomains.php │ │ │ │ ├── EmailForwarders.php │ │ │ │ ├── EmailSender.php │ │ │ │ ├── Emails.php │ │ │ │ ├── FpmDaemons.php │ │ │ │ ├── Froxlor.php │ │ │ │ ├── Ftps.php │ │ │ │ ├── HostingPlans.php │ │ │ │ ├── IpsAndPorts.php │ │ │ │ ├── MysqlServer.php │ │ │ │ ├── Mysqls.php │ │ │ │ ├── PhpSettings.php │ │ │ │ ├── SshKeys.php │ │ │ │ ├── SubDomains.php │ │ │ │ ├── SysLog.php │ │ │ │ ├── Traffic.php │ │ │ │ └── index.html │ │ │ ├── FroxlorRPC.php │ │ │ ├── ResourceEntity.php │ │ │ ├── Response.php │ │ │ └── index.html │ │ ├── Bulk/ │ │ │ ├── BulkAction.php │ │ │ ├── DomainBulkAction.php │ │ │ └── index.html │ │ ├── Cli/ │ │ │ ├── CliCommand.php │ │ │ ├── ConfigDiff.php │ │ │ ├── ConfigServices.php │ │ │ ├── InstallCommand.php │ │ │ ├── MasterCron.php │ │ │ ├── PhpSessionclean.php │ │ │ ├── RunApiCommand.php │ │ │ ├── SwitchServerIp.php │ │ │ ├── UpdateCommand.php │ │ │ ├── UserCommand.php │ │ │ ├── ValidateAcmeWebroot.php │ │ │ ├── index.html │ │ │ └── install.functions.php │ │ ├── Config/ │ │ │ ├── ConfigDaemon.php │ │ │ ├── ConfigDisplay.php │ │ │ ├── ConfigParser.php │ │ │ ├── ConfigService.php │ │ │ └── index.html │ │ ├── Cron/ │ │ │ ├── CronConfig.php │ │ │ ├── Dns/ │ │ │ │ ├── Bind.php │ │ │ │ ├── DnsBase.php │ │ │ │ ├── PowerDNS.php │ │ │ │ └── index.html │ │ │ ├── Forkable.php │ │ │ ├── FroxlorCron.php │ │ │ ├── Http/ │ │ │ │ ├── Apache.php │ │ │ │ ├── ApacheFcgi.php │ │ │ │ ├── ConfigIO.php │ │ │ │ ├── DomainSSL.php │ │ │ │ ├── HttpConfigBase.php │ │ │ │ ├── LetsEncrypt/ │ │ │ │ │ ├── AcmeSh.php │ │ │ │ │ └── index.html │ │ │ │ ├── Nginx.php │ │ │ │ ├── NginxFcgi.php │ │ │ │ ├── Php/ │ │ │ │ │ ├── Fcgid.php │ │ │ │ │ ├── Fpm.php │ │ │ │ │ ├── PhpInterface.php │ │ │ │ │ └── index.html │ │ │ │ ├── WebserverBase.php │ │ │ │ └── index.html │ │ │ ├── Mail/ │ │ │ │ └── Rspamd.php │ │ │ ├── System/ │ │ │ │ ├── ExportCron.php │ │ │ │ ├── Extrausers.php │ │ │ │ ├── MailboxsizeCron.php │ │ │ │ ├── SshKeys.php │ │ │ │ ├── TasksCron.php │ │ │ │ └── index.html │ │ │ ├── TaskId.php │ │ │ ├── Traffic/ │ │ │ │ ├── ReportsCron.php │ │ │ │ ├── TrafficCron.php │ │ │ │ └── index.html │ │ │ └── index.html │ │ ├── CurrentUser.php │ │ ├── Customer/ │ │ │ ├── Customer.php │ │ │ └── index.html │ │ ├── Database/ │ │ │ ├── Database.php │ │ │ ├── DbManager.php │ │ │ ├── IntegrityCheck.php │ │ │ ├── Manager/ │ │ │ │ ├── DbManagerMySQL.php │ │ │ │ └── index.html │ │ │ └── index.html │ │ ├── Dns/ │ │ │ ├── Dns.php │ │ │ ├── DnsEntry.php │ │ │ ├── DnsZone.php │ │ │ ├── PowerDNS.php │ │ │ └── index.html │ │ ├── Domain/ │ │ │ ├── Domain.php │ │ │ ├── IpAddr.php │ │ │ └── index.html │ │ ├── ErrorBag.php │ │ ├── FileDir.php │ │ ├── Froxlor.php │ │ ├── FroxlorLogger.php │ │ ├── FroxlorTwoFactorAuth.php │ │ ├── Http/ │ │ │ ├── Directory.php │ │ │ ├── HttpClient.php │ │ │ ├── PhpConfig.php │ │ │ ├── RateLimiter.php │ │ │ ├── Statistics.php │ │ │ └── index.html │ │ ├── Idna/ │ │ │ ├── IdnaWrapper.php │ │ │ └── index.html │ │ ├── Install/ │ │ │ ├── AutoUpdate.php │ │ │ ├── Install/ │ │ │ │ └── Core.php │ │ │ ├── Install.php │ │ │ ├── Preconfig.php │ │ │ ├── Requirements.php │ │ │ └── Update.php │ │ ├── Language.php │ │ ├── MailLogParser.php │ │ ├── PhpHelper.php │ │ ├── SImExporter.php │ │ ├── Settings/ │ │ │ ├── FroxlorVhostSettings.php │ │ │ ├── Store.php │ │ │ └── index.html │ │ ├── Settings.php │ │ ├── System/ │ │ │ ├── Cronjob.php │ │ │ ├── Crypt.php │ │ │ ├── IPTools.php │ │ │ ├── Mailer.php │ │ │ ├── Markdown.php │ │ │ ├── MysqlHandler.php │ │ │ └── index.html │ │ ├── Traffic/ │ │ │ ├── Traffic.php │ │ │ └── index.html │ │ ├── UI/ │ │ │ ├── Callbacks/ │ │ │ │ ├── Admin.php │ │ │ │ ├── Customer.php │ │ │ │ ├── Dns.php │ │ │ │ ├── Domain.php │ │ │ │ ├── Email.php │ │ │ │ ├── Ftp.php │ │ │ │ ├── Impersonate.php │ │ │ │ ├── Mysql.php │ │ │ │ ├── PHPConf.php │ │ │ │ ├── ProgressBar.php │ │ │ │ ├── SSLCertificate.php │ │ │ │ ├── Style.php │ │ │ │ ├── SysLog.php │ │ │ │ ├── Text.php │ │ │ │ └── index.html │ │ │ ├── Collection.php │ │ │ ├── Data.php │ │ │ ├── Form.php │ │ │ ├── HTML.php │ │ │ ├── Linker.php │ │ │ ├── Listing.php │ │ │ ├── Pagination.php │ │ │ ├── Panel/ │ │ │ │ ├── CustomReflection.php │ │ │ │ ├── FroxlorTwig.php │ │ │ │ ├── UI.php │ │ │ │ └── index.html │ │ │ ├── Request.php │ │ │ ├── Response.php │ │ │ └── index.html │ │ ├── User.php │ │ ├── Validate/ │ │ │ ├── Check.php │ │ │ ├── Form/ │ │ │ │ ├── Data.php │ │ │ │ └── index.html │ │ │ ├── Form.php │ │ │ ├── Validate.php │ │ │ └── index.html │ │ └── index.html │ ├── ajax.php │ ├── config.example.inc.php │ ├── configfiles/ │ │ ├── bookworm.xml │ │ ├── bullseye.xml │ │ ├── focal.xml │ │ ├── index.html │ │ ├── jammy.xml │ │ ├── noble.xml │ │ └── trixie.xml │ ├── formfields/ │ │ ├── admin/ │ │ │ ├── admin/ │ │ │ │ ├── formfield.admin_add.php │ │ │ │ ├── formfield.admin_edit.php │ │ │ │ └── index.html │ │ │ ├── cronjobs/ │ │ │ │ ├── formfield.cronjobs_edit.php │ │ │ │ └── index.html │ │ │ ├── customer/ │ │ │ │ ├── formfield.customer_add.php │ │ │ │ ├── formfield.customer_edit.php │ │ │ │ └── index.html │ │ │ ├── domains/ │ │ │ │ ├── formfield.domains_add.php │ │ │ │ ├── formfield.domains_duplicate.php │ │ │ │ ├── formfield.domains_edit.php │ │ │ │ ├── formfield.domains_import.php │ │ │ │ └── index.html │ │ │ ├── index.html │ │ │ ├── ipsandports/ │ │ │ │ ├── formfield.ipsandports_add.php │ │ │ │ ├── formfield.ipsandports_edit.php │ │ │ │ └── index.html │ │ │ ├── messages/ │ │ │ │ ├── formfield.messages_add.php │ │ │ │ └── index.html │ │ │ ├── mysqlserver/ │ │ │ │ ├── formfield.mysqlserver_add.php │ │ │ │ ├── formfield.mysqlserver_edit.php │ │ │ │ └── index.html │ │ │ ├── phpconfig/ │ │ │ │ ├── formfield.fpmconfig_add.php │ │ │ │ ├── formfield.fpmconfig_edit.php │ │ │ │ ├── formfield.phpconfig_add.php │ │ │ │ ├── formfield.phpconfig_edit.php │ │ │ │ └── index.html │ │ │ ├── plans/ │ │ │ │ ├── formfield.plans_add.php │ │ │ │ ├── formfield.plans_edit.php │ │ │ │ └── index.html │ │ │ ├── settings/ │ │ │ │ ├── formfield.settings_import.php │ │ │ │ ├── formfield.settings_mailtest.php │ │ │ │ └── index.html │ │ │ └── templates/ │ │ │ ├── formfield.filetemplate_add.php │ │ │ ├── formfield.filetemplate_edit.php │ │ │ ├── formfield.template_add.php │ │ │ ├── formfield.template_edit.php │ │ │ ├── index.html │ │ │ └── template.replacers.php │ │ ├── customer/ │ │ │ ├── domains/ │ │ │ │ ├── formfield.domains_add.php │ │ │ │ ├── formfield.domains_edit.php │ │ │ │ └── index.html │ │ │ ├── email/ │ │ │ │ ├── formfield.emails_accountchangepasswd.php │ │ │ │ ├── formfield.emails_accountchangequota.php │ │ │ │ ├── formfield.emails_add.php │ │ │ │ ├── formfield.emails_addaccount.php │ │ │ │ ├── formfield.emails_addforwarder.php │ │ │ │ ├── formfield.emails_addsender.php │ │ │ │ ├── formfield.emails_edit.php │ │ │ │ └── index.html │ │ │ ├── extras/ │ │ │ │ ├── formfield.export.php │ │ │ │ ├── formfield.htaccess_add.php │ │ │ │ ├── formfield.htaccess_edit.php │ │ │ │ ├── formfield.htpasswd_add.php │ │ │ │ ├── formfield.htpasswd_edit.php │ │ │ │ └── index.html │ │ │ ├── ftp/ │ │ │ │ ├── formfield.ftp_add.php │ │ │ │ ├── formfield.ftp_edit.php │ │ │ │ ├── formfield.ftp_ssh_add.php │ │ │ │ ├── formfield.ftp_ssh_edit.php │ │ │ │ └── index.html │ │ │ ├── index.html │ │ │ └── mysql/ │ │ │ ├── formfield.mysql_add.php │ │ │ ├── formfield.mysql_edit.php │ │ │ ├── formfield.mysql_global_user.php │ │ │ └── index.html │ │ ├── formfield.api_key.php │ │ ├── formfield.dns_add.php │ │ ├── formfield.domain_ssleditor.php │ │ ├── index.html │ │ └── install/ │ │ ├── formfield.install.php │ │ └── index.html │ ├── functions.php │ ├── index.html │ ├── init.php │ ├── navigation/ │ │ ├── 00.froxlor.main.php │ │ └── index.html │ ├── tablelisting/ │ │ ├── admin/ │ │ │ ├── index.html │ │ │ ├── tablelisting.admins.php │ │ │ ├── tablelisting.cronjobs.php │ │ │ ├── tablelisting.customers.php │ │ │ ├── tablelisting.domains.php │ │ │ ├── tablelisting.filetemplates.php │ │ │ ├── tablelisting.fpmconfigs.php │ │ │ ├── tablelisting.integrity.php │ │ │ ├── tablelisting.ipsandports.php │ │ │ ├── tablelisting.mailtemplates.php │ │ │ ├── tablelisting.mysqlserver.php │ │ │ ├── tablelisting.phpconfigs.php │ │ │ └── tablelisting.plans.php │ │ ├── customer/ │ │ │ ├── index.html │ │ │ ├── tablelisting.domains.php │ │ │ ├── tablelisting.emails.php │ │ │ ├── tablelisting.emails_overview.php │ │ │ ├── tablelisting.export.php │ │ │ ├── tablelisting.ftps.php │ │ │ ├── tablelisting.htaccess.php │ │ │ ├── tablelisting.htpasswd.php │ │ │ ├── tablelisting.mysqls.php │ │ │ └── tablelisting.sshkeys.php │ │ ├── index.html │ │ ├── tablelisting.apikeys.php │ │ ├── tablelisting.dns.php │ │ ├── tablelisting.sslcertificates.php │ │ └── tablelisting.syslog.php │ └── tables.inc.php ├── lng/ │ ├── ca.lng.php │ ├── cz.lng.php │ ├── de.lng.php │ ├── en.lng.php │ ├── es.lng.php │ ├── fr.lng.php │ ├── hu.lng.php │ ├── index.html │ ├── it.lng.php │ ├── nl.lng.php │ ├── pt.lng.php │ ├── se.lng.php │ ├── sk.lng.php │ └── zh_CN.lng.php ├── logfiles_viewer.php ├── logs/ │ └── index.html ├── package.json ├── phpcs.xml ├── phpdox.xml ├── phpmd.xml ├── phpunit.xml ├── ssl_certificates.php ├── ssl_editor.php ├── templates/ │ └── index.html ├── tests/ │ ├── Admins/ │ │ └── AdminsTest.php │ ├── Backup/ │ │ └── DataDumpTest.php │ ├── Bulk/ │ │ └── DomainBulkTest.php │ ├── Certificates/ │ │ └── CertificatesTest.php │ ├── Cron/ │ │ └── TaskIdTest.php │ ├── Cronjobs/ │ │ └── CronjobsTest.php │ ├── Customers/ │ │ ├── CustomersTest.php │ │ └── HostingPlansTest.php │ ├── DomainZones/ │ │ └── DomainZonesTest.php │ ├── Domains/ │ │ └── DomainsTest.php │ ├── Emails/ │ │ └── EmailsTest.php │ ├── Extras/ │ │ ├── DirOptionsTest.php │ │ └── DirProtectionsTest.php │ ├── Froxlor/ │ │ ├── FroxlorTest.php │ │ ├── IPToolsTest.php │ │ ├── SettingsTest.php │ │ ├── StoreTest.php │ │ └── ValidateTest.php │ ├── Ftps/ │ │ └── FtpsTest.php │ ├── Global/ │ │ ├── ApiParameterTest.php │ │ └── FroxlorRpcTest.php │ ├── IpsAndPorts/ │ │ └── IpsAndPortsTest.php │ ├── Mysqls/ │ │ ├── MysqlServerTest.php │ │ └── MysqlsTest.php │ ├── PhpAndFpm/ │ │ ├── FpmDaemonsTest.php │ │ └── PhpSettingsTest.php │ ├── SubDomains/ │ │ └── SubDomainsTest.php │ ├── Traffic/ │ │ └── TrafficTest.php │ └── bootstrap.php └── vite.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true indent_style = tab indent_size = 4 trim_trailing_whitespace = true [*.md] trim_trailing_whitespace = false [*.{yml,yaml}] indent_style = space indent_size = 2 [docker-compose.yml] indent_size = 4 ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Contribution Before you start working on a PR, contact us via [Discord](https://discord.froxlor.org) or the forum at [https://forum.froxlor.org](https://forum.froxlor.org) to get a clue whether someone else isn't already working on it or if we don not want/need this certain change. Of course, bugfixes are always welcome. Please always focus on the **main** branch of our [Github repository](https://github.com/Froxlor/Froxlor). ## Checklist General rules for PRs are: * Please save us all some trouble and unnecessary round-trips by _testing_ your changes. * Re-write your commit history to provide a CLEAN history! * i.e. do not provide PRs which contain a commit that changes something, the next changes it back, a third one changes it again, only a little differently... Thanks! ### Service changes If you make changes to the functionality of service configurations, please make sure your implementation covers all supported services and distributions. ### l10n If you add new language strings, please make sure you add the english fallback strings in `lng/en.php`. ### New settings and database-layout changes If you add new settings or implement database-changes, please make sure you add these to * `install/froxlor.sql.php` * handle the update (see [`install/updates/froxlor/update_2.x.inc.php`](https://github.com/Froxlor/Froxlor/blob/main/install/updates/froxlor/update_2.x.inc.php)) * if you have any question on how update-process works, please contact us ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: d00p custom: ['https://paypal.me/Froxlor'] ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **As a rule of thumb: before reporting an issue** * see if it hasn't been [reported](https://github.com/Froxlor/froxlor/issues) (and possibly already been [fixed](https://github.com/Froxlor/froxlor/issues?utf8=✓&q=is:issue%20is:closed)) first * try with the git master **Describe the bug** A clear and concise description of what the bug is. **System information** * Froxlor version: \$version/\$gitSHA1 * PHP sapi & version: php-fpm 8.3 / fcgid 8.0 / etc. * Web server: apache2/nginx * DNS server: Bind/PowerDNS (standalone)/PowerDNS (Bind-backend) * POP/IMAP server: Courier/Dovecot * SMTP server: postfix/exim * FTP server: proftpd/pureftpd * OS/Version: ... **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Logfiles** If applicable, add log-entries to help explain your problem. **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ # Bug report vs. support request If you're unsure of whether your problem is a bug or a configuration error * contact us via IRC in #froxlor on irc.libera.chat * or post a thread in our forum at https://forum.froxlor.org As a rule of thumb: before reporting an issue * see if it hasn't been [reported](https://github.com/Froxlor/froxlor/issues) (and possibly already been [fixed](https://github.com/Froxlor/froxlor/issues?utf8=✓&q=is:issue%20is:closed)) first * try with the git master # Summary Please provide a concise summary of the problem you're experiencing... # System information * Froxlor version: $version/$gitSHA1 * PHP sapi & version: php-fpm 8.3 / fcgid 8.0 / etc. * Web server: apache2/nginx * DNS server: Bind/PowerDNS (standalone)/PowerDNS (Bind-backend) * POP/IMAP server: Courier/Dovecot * SMTP server: postfix/exim * FTP server: proftpd/pureftpd * OS/Version: ... # Steps to reproduce 1. 2. 3. # Expected behavior 1. 2. 3. # Actual behavior 1. 2. 3. # Log files/log entries syslog:
example
================================================ FILE: .github/LICENSE_HEADER ================================================ /** * This file is part of the froxlor project. * Copyright (c) 2010 the froxlor Team (see authors). * * 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, you can also view it online at * https://files.froxlor.org/misc/COPYING.txt * * @copyright the authors * @author froxlor team * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ # Description Please include a summary of the change and which issue is fixed if any. Please also include relevant motivation and context. List any dependencies that are required for this change. Fixes # (issue) ## Type of change Please delete options that are not relevant. - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] This change requires a documentation update # How Has This Been Tested? Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration - [ ] Test A - [ ] Test B **Test Configuration**: * Distribution: * Webserver: * PHP: * etc.etc.: # Checklist: - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes ================================================ FILE: .github/workflows/build-docs.yml ================================================ name: build-documentation on: release: # only run for stable releases types: [released] jobs: build_docs: runs-on: ubuntu-latest steps: - env: GITHUB_TOKEN: ${{ secrets.ORG_GITHUB_TOKEN }} run: | gh workflow run --repo Froxlor/Documentation build-and-deploy.yml -f type=tags -f ref=${{github.ref_name}} ================================================ FILE: .github/workflows/build-mariadb.yml ================================================ name: Froxlor-CI-MariaDB on: [ 'push', 'pull_request', 'create' ] jobs: froxlor: name: Froxlor (PHP ${{ matrix.php-versions }}, MariaDB ${{ matrix.mariadb-version }}) runs-on: ubuntu-latest strategy: fail-fast: false matrix: php-versions: [ '7.4', '8.4' ] mariadb-version: [ 10.11, 11.7 ] steps: - name: Checkout uses: actions/checkout@v4 - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} tools: composer:v2 extensions: mbstring, xml, ctype, pdo_mysql, mysql, curl, json, zip, session, filter, posix, openssl, fileinfo, bcmath, gmp - name: Install tools run: sudo apt-get install -y ant - name: Adjust firewall run: | sudo ufw allow out 3306/tcp sudo ufw allow in 3306/tcp - name: Setup MariaDB uses: shogo82148/actions-setup-mysql@v1.47.0 with: distribution: "mariadb" mysql-version: ${{ matrix.mariadb-version }} root-password: 'fr0xl0r.TravisCI' user: 'froxlor010' password: 'fr0xl0r.TravisCI' - name: Wait for database run: sleep 15 - name: Setup databases run: | mysql -h 127.0.0.1 --protocol=TCP -u root -pfr0xl0r.TravisCI -e "CREATE DATABASE froxlor010;" php -r "echo include('install/froxlor.sql.php');" > /tmp/froxlor.sql mysql -h 127.0.0.1 --protocol=TCP -u root -pfr0xl0r.TravisCI froxlor010 < /tmp/froxlor.sql - name: Run testing run: ant quick-build nightly: name: Create nightly/testing tarball runs-on: ubuntu-latest needs: froxlor if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} steps: - name: Checkout uses: actions/checkout@v4 - name: Setup PHP with PECL extension uses: shivammathur/setup-php@v2 with: php-version: '7.4' tools: composer:v2 extensions: mbstring, xml, ctype, pdo_mysql, mysql, curl, json, zip, session, filter, posix, openssl, fileinfo, bcmath, gmp, gnupg - name: Install composer dependencies run: composer install --no-dev - name: Install Node.js uses: actions/setup-node@v5 with: node-version: '22.x' - name: Install npm dependencies run: npm install - name: Build assets run: npm run build working-directory: . - name: Setting file/directory permissions run: | find -exec chmod ugo+r,u+w,go-w {} \; find -type f -exec chmod ugo-x {} \; find -type d -exec chmod ugo+x {} \; chmod 0755 bin/froxlor-cli - name: Remove vcs and unneeded files run: | rm .gitignore rm .editorconfig rm -rf node_modules rm composer.json rm composer.lock rm package.json rm package-lock.json rm *.xml rm vite.config.js - name: Create empty index.html in built assets directory run: | touch templates/Froxlor/build/index.html touch templates/Froxlor/build/assets/index.html - name: Set outputs id: vars run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT - name: Set nightly branding run: | sed -i "s/const BRANDING = '';/const BRANDING = '+nightly.${{steps.vars.outputs.sha_short}}';/" lib/Froxlor/Froxlor.php zip -r froxlor-nightly.${{steps.vars.outputs.sha_short}}.zip . -x "*.git*" sha256sum froxlor-nightly.${{steps.vars.outputs.sha_short}}.zip > froxlor-nightly.${{steps.vars.outputs.sha_short}}.zip.sha256 mkdir dist mv froxlor-nightly.${{steps.vars.outputs.sha_short}}.zip dist/ mv froxlor-nightly.${{steps.vars.outputs.sha_short}}.zip.sha256 dist/ - name: Deploy nightly to server uses: easingthemes/ssh-deploy@main with: ARGS: "-rltDzvO --chown=${{ secrets.WEB_USER }}:${{ secrets.WEB_USER }}" SOURCE: "dist/" SSH_PRIVATE_KEY: ${{ secrets.SERVER_SSH_KEY }} REMOTE_HOST: ${{ secrets.REMOTE_HOST }} REMOTE_USER: ${{ secrets.REMOTE_USER }} TARGET: "${{ secrets.REMOTE_TARGET }}" ================================================ FILE: .github/workflows/build-mysql.yml ================================================ name: Froxlor-CI-MySQL on: ['push', 'pull_request', 'create'] jobs: froxlor: name: Froxlor (PHP ${{ matrix.php-versions }}, MySQL ${{ matrix.mysql-version }}) runs-on: ubuntu-latest strategy: fail-fast: false matrix: php-versions: ['7.4', '8.4'] mysql-version: [8.4, 5.7] steps: - name: Checkout uses: actions/checkout@v4 - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} tools: composer:v2 extensions: mbstring, xml, ctype, pdo_mysql, mysql, curl, json, zip, session, filter, posix, openssl, fileinfo, bcmath, gmp - name: Install tools run: sudo apt-get install -y ant - name: Adjust firewall run: | sudo ufw allow out 3306/tcp sudo ufw allow in 3306/tcp - name: Setup MySQL uses: shogo82148/actions-setup-mysql@v1.47.0 with: mysql-version: ${{ matrix.mysql-version }} root-password: 'fr0xl0r.TravisCI' user: 'froxlor010' password: 'fr0xl0r.TravisCI' - name: Wait for database run: sleep 15 - name: Setup database run: | mysql -h 127.0.0.1 --protocol=TCP -u root -pfr0xl0r.TravisCI -e "CREATE DATABASE froxlor010;" php -r "echo include('install/froxlor.sql.php');" > /tmp/froxlor.sql mysql -h 127.0.0.1 --protocol=TCP -u root -pfr0xl0r.TravisCI froxlor010 < /tmp/froxlor.sql - name: Run testing run: ant quick-build ================================================ FILE: .gitignore ================================================ install/update.log install/*.json lib/userdata.inc.php lib/userdata.inc.php.bak lib/config.inc.php logs/* !logs/index.html .buildpath .project .settings/ .test/ *.diff *.patch *~ .well-known .idea .DS_Store *.iml img/ vendor/ node_modules/ fonts/ templates/* !templates/index.html !templates/Froxlor/ templates/Froxlor/build/ templates/Froxlor/hot !templates/misc/ ================================================ FILE: 2fa.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ if (!defined('AREA')) { header("Location: index.php"); exit(); } use Froxlor\Database\Database; use Froxlor\FroxlorLogger; use Froxlor\FroxlorTwoFactorAuth; use Froxlor\Settings; use Froxlor\UI\Panel\UI; use Froxlor\UI\Request; use Froxlor\UI\Response; use Froxlor\PhpHelper; use Froxlor\User; if (Settings::Get('2fa.enabled') != '1') { Response::dynamicError('2fa.2fa_not_activated'); } // This file is being included in admin_index and customer_index // and therefore does not need to require lib/init.php if (AREA == 'admin') { $upd_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_ADMINS . "` SET `type_2fa` = :t2fa, `data_2fa` = :d2fa WHERE adminid = :id"); $uid = $userinfo['adminid']; } elseif (AREA == 'customer') { $upd_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `type_2fa` = :t2fa, `data_2fa` = :d2fa WHERE customerid = :id"); $uid = $userinfo['customerid']; } $success_message = ""; $tfa = new FroxlorTwoFactorAuth('Froxlor ' . Settings::Get('system.hostname')); // do the delete and then just show a success-message if ($action == 'delete') { Database::pexecute($upd_stmt, [ 't2fa' => 0, 'd2fa' => "", 'id' => $uid ]); Response::standardSuccess('2fa.2fa_removed'); } elseif ($action == 'preadd') { $type = Request::post('type_2fa', '0'); $data = ""; if ($type > 0) { // generate secret for TOTP $data = $tfa->createSecret(); $userinfo['type_2fa'] = $type; $userinfo['data_2fa'] = $data; $userinfo['2fa_unsaved'] = true; // if type = email, send a code there for confirmation if ($type == 1) { $code = $tfa->getCode($data); $_mailerror = false; $mailerr_msg = ""; $replace_arr = [ 'CODE' => $code ]; $mail_body = html_entity_decode(PhpHelper::replaceVariables(lng('mails.2fa.mailbody'), $replace_arr)); try { $mail->Subject = lng('mails.2fa.subject'); $mail->AltBody = $mail_body; $mail->MsgHTML(str_replace("\n", "
", $mail_body)); $mail->AddAddress($userinfo['email'], User::getCorrectUserSalutation($userinfo)); $mail->Send(); } catch (\PHPMailer\PHPMailer\Exception $e) { $mailerr_msg = $e->errorMessage(); $_mailerror = true; } catch (Exception $e) { $mailerr_msg = $e->getMessage(); $_mailerror = true; } if ($_mailerror) { Response::dynamicError($mailerr_msg); } } UI::twig()->addGlobal('userinfo', $userinfo); } else { Response::dynamicError('Select one of the possible values for 2FA'); } } elseif ($action == 'add') { $type = Request::post('type_2fa', '0'); $data = Request::post('data_2fa', ''); $code = Request::post('codevalidation', ''); // validate $result = $tfa->verifyCode($data, $code, 3); if ($result) { if ($type == 0 || $type == 1) { // no fixed secret for email validation, the validation code will be set on the fly $data = ""; } Database::pexecute($upd_stmt, [ 't2fa' => $type, 'd2fa' => $data, 'id' => $uid ]); Response::standardSuccess('2fa.2fa_added', $filename); } Response::dynamicError('Invalid/wrong code'); } $log->logAction(FroxlorLogger::USR_ACTION, LOG_NOTICE, "viewed 2fa::overview"); $type_select_values = []; $ga_qrcode = ''; if ($userinfo['type_2fa'] == '0') { // available types $type_select_values = [ 0 => '-', 1 => 'E-Mail', 2 => 'Authenticator' ]; asort($type_select_values); } elseif ($userinfo['type_2fa'] == '1') { // email 2fa enabled } elseif ($userinfo['type_2fa'] == '2') { // authenticator 2fa enabled $ga_qrcode = $tfa->getQRCodeImageAsDataUri($userinfo['loginname'], $userinfo['data_2fa']); } UI::view('user/2fa.html.twig', [ 'type_select_values' => $type_select_values, 'ga_qrcode' => $ga_qrcode ]); ================================================ FILE: COPYING ================================================ GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS ================================================ FILE: README.md ================================================ [![Froxlor-CI](https://github.com/Froxlor/Froxlor/actions/workflows/build-mariadb.yml/badge.svg?branch=main)](https://github.com/Froxlor/Froxlor/actions/workflows/build-mariadb.yml) [![Froxlor-CI](https://github.com/Froxlor/Froxlor/actions/workflows/build-mysql.yml/badge.svg?branch=main)](https://github.com/Froxlor/Froxlor/actions/workflows/build-mysql.yml) [![Discord](https://badgen.net/badge/icon/discord?icon=discord&label)](https://discord.froxlor.org) # Froxlor The server administration software for your needs. Developed by experienced server administrators, this panel simplifies the effort of managing your hosting platform. ## Installation ### Fast install 1. Ensure that your webserver serves /var/www/html 2. Extract froxlor into /var/www/html 3. Point your browser to http://[ip-of-webserver]/froxlor 4. Follow the installer 5. Login as administrator 6. Have fun! If you have chosen to do the configuration by hand during the installation, you have to complete some more steps: 1. Adjust "System > Settings" according to your needs 2. Choose your distribution under "System > Configuration" 3. Follow the steps for your services ### Detailed installation https://docs.froxlor.org/latest/general/installation/ ## Help You may find help in the following places: ### Discord The froxlor community discord server can be found here: https://discord.froxlor.org ### Forum The community is located on https://forum.froxlor.org/ ### Documentation The documentation may be found at https://docs.froxlor.org/ ## License May be found in [COPYING](COPYING) ## Downloads ### Tarball https://files.froxlor.org/releases/froxlor-latest.tar.gz [MD5](https://files.froxlor.org/releases/froxlor-latest.tar.gz.md5) [SHA1](https://files.froxlor.org/releases/froxlor-latest.tar.gz.sha1) ### Debian / Ubuntu repository [HowTo](https://docs.froxlor.org/latest/general/installation/apt-package.html) #### Debian ``` apt -y install apt-transport-https lsb-release ca-certificates curl gnupg curl -sSLo /usr/share/keyrings/deb.froxlor.org-froxlor.gpg https://deb.froxlor.org/froxlor.gpg sh -c 'echo "deb [signed-by=/usr/share/keyrings/deb.froxlor.org-froxlor.gpg] https://deb.froxlor.org/debian $(lsb_release -sc) main" > /etc/apt/sources.list.d/froxlor.list' ``` #### Ubuntu ``` apt -y install apt-transport-https lsb-release ca-certificates curl gnupg curl -sSLo /usr/share/keyrings/deb.froxlor.org-froxlor.gpg https://deb.froxlor.org/froxlor.gpg sh -c 'echo "deb [signed-by=/usr/share/keyrings/deb.froxlor.org-froxlor.gpg] https://deb.froxlor.org/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/froxlor.list' ``` ## Contributing [see here](.github/CONTRIBUTING.md) ================================================ FILE: SECURITY.md ================================================ # froxlor's Security Policy Welcome and thanks for taking interest in [froxlor](https://www.froxlor.org)! We are mostly interested in reports by actual froxlor users but all high quality contributions are welcome. Please try your best to describe a clear and realistic impact for your report and please don't open any public issues on GitHub or social media, we're doing our best to respond through huntr as quickly as we can. With that, good luck hacking us ;) ## Supported versions - ️✅ **2.3.x** (`main` and `v2.3` git-branch) - ️❌ <=2.2.x - ❌ other git-branches ## Qualifying Vulnerabilities ### Vulnerabilities we really care about - SQL injection bugs - server-side code execution bugs - cross-site scripting vulnerabilities - cross-site request forgery vulnerabilities - authentication and authorization flaws - sensitive information disclosure ### Vulnerabilities we accept Only reproducible issues on a default/clean setup from the latest stable release of a supported version will be accepted. ## Non-Qualifying Vulnerabilities - Reports from automated tools or scanners - Theoretical attacks without proof of exploitability - Attacks that are the result of a third party library should be reported to the library maintainers - Social engineering - Attacks that require disabling security features or reducing the security level of the environment - Exploits by an admin user itself (privileged user and implicitly trusted) - Reflected file download - Physical attacks - Weak SSL/TLS/SSH algorithms or protocols - Attacks involving physical access to a user’s device, or involving a device or network that’s already seriously compromised (eg man-in-the-middle). - The user attacks themselves - anything in `/doc` - anything in `/tests` ## Reporting a Vulnerability If you think you have found a vulnerability in froxlor, please head over to [https://github.com/Froxlor/Froxlor/security/advisories](https://github.com/Froxlor/Froxlor/security/advisories/new) and use the reporting possibilities there. Also, please give us appropriate time to fix the issue and build update-packages before publishing anything into the wild. Alternatively you can email us to [team@froxlor.org](team@froxlor.org). ================================================ FILE: actions/admin/index.html ================================================ ================================================ FILE: actions/admin/settings/100.panel.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ return [ 'groups' => [ 'panel' => [ 'title' => lng('admin.panelsettings'), 'icon' => 'fa-solid fa-chalkboard-user', 'fields' => [ 'panel_standardlanguage' => [ 'label' => [ 'title' => lng('login.language'), 'description' => lng('serversettings.language.description') ], 'settinggroup' => 'panel', 'varname' => 'standardlanguage', 'type' => 'select', 'default' => 'en', 'option_options_method' => [ '\\Froxlor\\Language', 'getLanguages' ], 'save_method' => 'storeSettingField' ], 'panel_default_theme' => [ 'label' => [ 'title' => lng('panel.theme'), 'description' => lng('serversettings.default_theme') ], 'settinggroup' => 'panel', 'varname' => 'default_theme', 'type' => 'select', 'default' => 'Froxlor', 'option_options_method' => [ '\\Froxlor\\UI\\Panel\\UI', 'getThemes' ], 'save_method' => 'storeSettingDefaultTheme' ], 'panel_allow_theme_change_customer' => [ 'label' => lng('serversettings.panel_allow_theme_change_customer'), 'settinggroup' => 'panel', 'varname' => 'allow_theme_change_customer', 'type' => 'checkbox', 'default' => true, 'save_method' => 'storeSettingField' ], 'panel_allow_theme_change_admin' => [ 'label' => lng('serversettings.panel_allow_theme_change_admin'), 'settinggroup' => 'panel', 'varname' => 'allow_theme_change_admin', 'type' => 'checkbox', 'default' => true, 'save_method' => 'storeSettingField' ], 'panel_natsorting' => [ 'label' => lng('serversettings.natsorting'), 'settinggroup' => 'panel', 'varname' => 'natsorting', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'panel_paging' => [ 'label' => lng('serversettings.paging'), 'settinggroup' => 'panel', 'varname' => 'paging', 'type' => 'number', 'min' => 0, 'default' => 0, 'save_method' => 'storeSettingField' ], 'panel_pathedit' => [ 'label' => lng('serversettings.pathedit'), 'settinggroup' => 'panel', 'varname' => 'pathedit', 'type' => 'select', 'default' => 'Manual', 'select_var' => [ 'Manual' => lng('serversettings.manual'), 'Dropdown' => lng('serversettings.dropdown') ], 'save_method' => 'storeSettingField' ], 'panel_adminmail' => [ 'label' => lng('serversettings.adminmail'), 'settinggroup' => 'panel', 'varname' => 'adminmail', 'type' => 'email', 'string_emptyallowed' => true, 'default' => '', 'save_method' => 'storeSettingField' ], 'panel_adminmail_defname' => [ 'label' => lng('serversettings.adminmail_defname'), 'settinggroup' => 'panel', 'varname' => 'adminmail_defname', 'type' => 'text', 'default' => 'Froxlor Administrator', 'save_method' => 'storeSettingField' ], 'panel_adminmail_return' => [ 'label' => lng('serversettings.adminmail_return'), 'settinggroup' => 'panel', 'varname' => 'adminmail_return', 'type' => 'email', 'string_emptyallowed' => true, 'default' => '', 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'panel_decimal_places' => [ 'label' => lng('serversettings.decimal_places'), 'settinggroup' => 'panel', 'varname' => 'decimal_places', 'type' => 'number', 'min' => 0, 'max' => 15, 'default' => 4, 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'panel_phpmyadmin_url' => [ 'label' => lng('serversettings.phpmyadmin_url'), 'settinggroup' => 'panel', 'varname' => 'phpmyadmin_url', 'type' => 'url', 'string_emptyallowed' => true, 'default' => '', 'save_method' => 'storeSettingField' ], 'panel_webmail_url' => [ 'label' => lng('serversettings.webmail_url'), 'settinggroup' => 'panel', 'varname' => 'webmail_url', 'type' => 'url', 'string_emptyallowed' => true, 'default' => '', 'save_method' => 'storeSettingField' ], 'panel_webftp_url' => [ 'label' => lng('serversettings.webftp_url'), 'settinggroup' => 'panel', 'varname' => 'webftp_url', 'type' => 'url', 'string_emptyallowed' => true, 'default' => '', 'save_method' => 'storeSettingField' ], 'admin_show_version_login' => [ 'label' => lng('admin.show_version_login'), 'settinggroup' => 'admin', 'varname' => 'show_version_login', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'admin_show_version_footer' => [ 'label' => lng('admin.show_version_footer'), 'settinggroup' => 'admin', 'varname' => 'show_version_footer', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField' ], 'admin_show_news_feed' => [ 'label' => lng('admin.show_news_feed'), 'settinggroup' => 'admin', 'varname' => 'show_news_feed', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField' ], 'customer_show_news_feed' => [ 'label' => lng('admin.customer_show_news_feed'), 'settinggroup' => 'customer', 'varname' => 'show_news_feed', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField' ], 'customer_news_feed_url' => [ 'label' => lng('admin.customer_news_feed_url'), 'settinggroup' => 'customer', 'varname' => 'news_feed_url', 'type' => 'url', 'string_emptyallowed' => true, 'default' => '', 'save_method' => 'storeSettingField' ], 'panel_allow_domain_change_admin' => [ 'label' => lng('serversettings.panel_allow_domain_change_admin'), 'settinggroup' => 'panel', 'varname' => 'allow_domain_change_admin', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'panel_allow_domain_change_customer' => [ 'label' => lng('serversettings.panel_allow_domain_change_customer'), 'settinggroup' => 'panel', 'varname' => 'allow_domain_change_customer', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'panel_phpconfigs_hidesubdomains' => [ 'label' => lng('serversettings.panel_phpconfigs_hidesubdomains'), 'settinggroup' => 'panel', 'varname' => 'phpconfigs_hidesubdomains', 'type' => 'checkbox', 'default' => true, 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'panel_phpconfigs_hidestdsubdomain' => [ 'label' => lng('serversettings.panel_phpconfigs_hidestdsubdomain'), 'settinggroup' => 'panel', 'varname' => 'phpconfigs_hidestdsubdomain', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'panel_customer_hide_options' => [ 'label' => lng('serversettings.panel_customer_hide_options'), 'settinggroup' => 'panel', 'varname' => 'customer_hide_options', 'type' => 'select', 'default' => '', 'select_mode' => 'multiple', 'option_emptyallowed' => true, 'select_var' => [ 'email' => lng('menue.email.email'), 'mysql' => lng('menue.mysql.mysql'), 'domains' => lng('menue.domains.domains'), 'ftp' => lng('menue.ftp.ftp'), 'extras' => lng('menue.extras.extras'), 'extras.directoryprotection' => lng('menue.extras.extras') . " / " . lng('menue.extras.directoryprotection'), 'extras.pathoptions' => lng('menue.extras.extras') . " / " . lng('menue.extras.pathoptions'), 'extras.logger' => lng('menue.extras.extras') . " / " . lng('menue.logger.logger'), 'extras.export' => lng('menue.extras.extras') . " / " . lng('menue.extras.export'), 'traffic' => lng('menue.traffic.traffic'), 'traffic.http' => lng('menue.traffic.traffic') . " / HTTP", 'traffic.ftp' => lng('menue.traffic.traffic') . " / FTP", 'traffic.mail' => lng('menue.traffic.traffic') . " / Mail", 'misc.documentation' => lng('admin.documentation'), ], 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'panel_imprint_url' => [ 'label' => lng('serversettings.imprint_url'), 'settinggroup' => 'panel', 'varname' => 'imprint_url', 'type' => 'url', 'string_emptyallowed' => true, 'default' => '', 'save_method' => 'storeSettingField' ], 'panel_terms_url' => [ 'label' => lng('serversettings.terms_url'), 'settinggroup' => 'panel', 'varname' => 'terms_url', 'type' => 'url', 'string_emptyallowed' => true, 'default' => '', 'save_method' => 'storeSettingField' ], 'panel_privacy_url' => [ 'label' => lng('serversettings.privacy_url'), 'settinggroup' => 'panel', 'varname' => 'privacy_url', 'type' => 'url', 'string_emptyallowed' => true, 'default' => '', 'save_method' => 'storeSettingField' ], 'panel_logo_overridetheme' => [ 'label' => lng('serversettings.logo_overridetheme'), 'settinggroup' => 'panel', 'varname' => 'logo_overridetheme', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField' ], 'panel_logo_overridecustom' => [ 'label' => lng('serversettings.logo_overridecustom'), 'settinggroup' => 'panel', 'varname' => 'logo_overridecustom', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField' ], 'panel_logo_image_header' => [ 'label' => lng('serversettings.logo_image_header'), 'settinggroup' => 'panel', 'varname' => 'logo_image_header', 'type' => 'image', 'accept' => 'image/jpeg, image/jpg, image/png, image/gif', 'image_name' => 'logo_header', 'default' => '', 'save_method' => 'storeSettingImage' ], 'panel_logo_image_login' => [ 'label' => lng('serversettings.logo_image_login'), 'settinggroup' => 'panel', 'varname' => 'logo_image_login', 'type' => 'image', 'accept' => 'image/jpeg, image/jpg, image/png, image/gif', 'image_name' => 'logo_login', 'default' => '', 'save_method' => 'storeSettingImage' ], 'panel_menu_collapsed' => [ 'label' => lng('serversettings.panel_menu_collapsed'), 'settinggroup' => 'panel', 'varname' => 'menu_collapsed', 'type' => 'checkbox', 'default' => true, 'save_method' => 'storeSettingField', ], ] ] ] ]; ================================================ FILE: actions/admin/settings/110.accounts.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ return [ 'groups' => [ 'accounts' => [ 'title' => lng('admin.accountsettings'), 'icon' => 'fa-solid fa-users-gear', 'fields' => [ 'session_sessiontimeout' => [ 'label' => lng('serversettings.session_timeout'), 'settinggroup' => 'session', 'varname' => 'sessiontimeout', 'type' => 'number', 'min' => 60, 'max' => 31536000, 'default' => 600, 'save_method' => 'storeSettingField' ], 'session_allow_multiple_login' => [ 'label' => lng('serversettings.session_allow_multiple_login'), 'settinggroup' => 'session', 'varname' => 'allow_multiple_login', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField' ], 'login_domain_login' => [ 'label' => lng('serversettings.login_domain_login'), 'settinggroup' => 'login', 'varname' => 'domain_login', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField' ], 'login_maxloginattempts' => [ 'label' => lng('serversettings.maxloginattempts'), 'settinggroup' => 'login', 'varname' => 'maxloginattempts', 'type' => 'number', 'min' => 1, 'default' => 3, 'save_method' => 'storeSettingField' ], 'login_deactivatetime' => [ 'label' => lng('serversettings.deactivatetime'), 'settinggroup' => 'login', 'varname' => 'deactivatetime', 'type' => 'number', 'min' => 0, 'default' => 900, 'save_method' => 'storeSettingField' ], '2fa_enabled' => [ 'label' => lng('2fa.2fa_enabled'), 'settinggroup' => '2fa', 'varname' => 'enabled', 'type' => 'checkbox', 'default' => true, 'save_method' => 'storeSettingField' ], 'panel_password_min_length' => [ 'label' => lng('serversettings.panel_password_min_length'), 'settinggroup' => 'panel', 'varname' => 'password_min_length', 'type' => 'number', 'min' => 0, 'default' => 0, 'save_method' => 'storeSettingField' ], 'panel_password_alpha_lower' => [ 'label' => lng('serversettings.panel_password_alpha_lower'), 'settinggroup' => 'panel', 'varname' => 'password_alpha_lower', 'type' => 'checkbox', 'default' => true, 'save_method' => 'storeSettingField' ], 'panel_password_alpha_upper' => [ 'label' => lng('serversettings.panel_password_alpha_upper'), 'settinggroup' => 'panel', 'varname' => 'password_alpha_upper', 'type' => 'checkbox', 'default' => true, 'save_method' => 'storeSettingField' ], 'panel_password_numeric' => [ 'label' => lng('serversettings.panel_password_numeric'), 'settinggroup' => 'panel', 'varname' => 'password_numeric', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField' ], 'panel_password_special_char_required' => [ 'label' => lng('serversettings.panel_password_special_char_required'), 'settinggroup' => 'panel', 'varname' => 'password_special_char_required', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField' ], 'panel_password_special_char' => [ 'label' => lng('serversettings.panel_password_special_char'), 'settinggroup' => 'panel', 'varname' => 'password_special_char', 'type' => 'text', 'default' => '!?<>§$%+#=@', 'save_method' => 'storeSettingField' ], 'panel_password_regex' => [ 'label' => lng('serversettings.panel_password_regex'), 'settinggroup' => 'panel', 'varname' => 'password_regex', 'type' => 'text', 'default' => '', 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'system_req_limit_per_interval' => [ 'label' => lng('serversettings.req_limit_per_interval'), 'settinggroup' => 'system', 'varname' => 'req_limit_per_interval', 'type' => 'number', 'min' => 30, 'default' => 60, 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'system_req_limit_interval' => [ 'label' => lng('serversettings.req_limit_interval'), 'settinggroup' => 'system', 'varname' => 'req_limit_interval', 'type' => 'number', 'min' => 5, 'default' => 60, 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'customer_accountprefix' => [ 'label' => lng('serversettings.accountprefix'), 'settinggroup' => 'customer', 'varname' => 'accountprefix', 'type' => 'text', 'default' => '', 'plausibility_check_method' => [ '\\Froxlor\\Validate\\Check', 'checkUsername' ], 'save_method' => 'storeSettingField' ], 'customer_mysqlprefix' => [ 'label' => lng('serversettings.mysqlprefix'), 'settinggroup' => 'customer', 'varname' => 'mysqlprefix', 'type' => 'text', 'default' => '', 'plausibility_check_method' => [ '\\Froxlor\\Validate\\Check', 'checkUsername' ], 'save_method' => 'storeSettingField' ], 'customer_ftpprefix' => [ 'label' => lng('serversettings.ftpprefix'), 'settinggroup' => 'customer', 'varname' => 'ftpprefix', 'type' => 'text', 'default' => '', 'save_method' => 'storeSettingField' ], 'customer_ftpatdomain' => [ 'label' => lng('serversettings.ftpdomain'), 'settinggroup' => 'customer', 'varname' => 'ftpatdomain', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField' ], 'panel_allow_preset' => [ 'label' => lng('serversettings.allow_password_reset'), 'settinggroup' => 'panel', 'varname' => 'allow_preset', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField', 'dependency' => [ 'fieldname' => 'panel_allow_preset_admin', 'fielddata' => [ 'settinggroup' => 'panel', 'varname' => 'allow_preset_admin' ], 'onlyif' => 0 ] ], 'panel_allow_preset_admin' => [ 'label' => lng('serversettings.allow_password_reset_admin'), 'settinggroup' => 'panel', 'varname' => 'allow_preset_admin', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField', 'dependency' => [ 'fieldname' => 'panel_allow_preset', 'fielddata' => [ 'settinggroup' => 'panel', 'varname' => 'allow_preset' ], 'onlyif' => 1 ] ], 'system_exportenabled' => [ 'label' => lng('serversettings.exportenabled'), 'settinggroup' => 'system', 'varname' => 'exportenabled', 'type' => 'checkbox', 'default' => false, 'cronmodule' => 'froxlor/export', 'save_method' => 'storeSettingField' ], 'system_createstdsubdom_default' => [ 'label' => lng('serversettings.createstdsubdom_default'), 'settinggroup' => 'system', 'varname' => 'createstdsubdom_default', 'type' => 'select', 'default' => '1', 'select_var' => [ '0' => lng('panel.no'), '1' => lng('panel.yes') ], 'save_method' => 'storeSettingField' ], ] ] ] ]; ================================================ FILE: actions/admin/settings/120.system.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ return [ 'groups' => [ 'system' => [ 'title' => lng('admin.systemsettings'), 'icon' => 'fa-solid fa-gears', 'fields' => [ 'system_documentroot_prefix' => [ 'label' => lng('serversettings.documentroot_prefix'), 'settinggroup' => 'system', 'varname' => 'documentroot_prefix', 'type' => 'text', 'string_type' => 'dir', 'default' => '/var/customers/webs/', 'save_method' => 'storeSettingField', 'plausibility_check_method' => [ '\\Froxlor\\Validate\\Check', 'checkPathConflicts' ], 'requires_reconf' => ['http'] ], 'system_documentroot_use_default_value' => [ 'label' => lng('serversettings.documentroot_use_default_value'), 'settinggroup' => 'system', 'varname' => 'documentroot_use_default_value', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField' ], 'system_ipaddress' => [ 'label' => lng('serversettings.ipaddress'), 'settinggroup' => 'system', 'varname' => 'ipaddress', 'type' => 'select', 'option_options_method' => [ '\\Froxlor\\Domain\\IpAddr', 'getIpAddresses' ], 'default' => '', 'save_method' => 'storeSettingIpAddress' ], 'system_defaultip' => [ 'label' => lng('serversettings.defaultip'), 'settinggroup' => 'system', 'varname' => 'defaultip', 'type' => 'select', 'select_mode' => 'multiple', 'option_options_method' => [ '\\Froxlor\\Domain\\IpAddr', 'getIpPortCombinations' ], 'default' => '', 'save_method' => 'storeSettingDefaultIp' ], 'system_defaultsslip' => [ 'label' => lng('serversettings.defaultsslip'), 'settinggroup' => 'system', 'varname' => 'defaultsslip', 'type' => 'select', 'select_mode' => 'multiple', 'option_options_method' => [ '\\Froxlor\\Domain\\IpAddr', 'getSslIpPortCombinations' ], 'default' => '', 'save_method' => 'storeSettingDefaultSslIp' ], 'system_hostname' => [ 'label' => lng('serversettings.hostname'), 'settinggroup' => 'system', 'varname' => 'hostname', 'type' => 'text', 'default' => '', 'save_method' => 'storeSettingHostname', 'plausibility_check_method' => [ '\\Froxlor\\Validate\\Check', 'checkHostname' ] ], 'api_enabled' => [ 'label' => lng('serversettings.enable_api'), 'settinggroup' => 'api', 'varname' => 'enabled', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField', 'required_otp' => true ], 'api_customer_default' => [ 'label' => lng('serversettings.api_customer_default'), 'settinggroup' => 'api', 'varname' => 'customer_default', 'type' => 'select', 'default' => 1, 'select_var' => [ 1 => lng('panel.yes'), 0 => lng('panel.no') ], 'save_method' => 'storeSettingField' ], 'system_update_channel' => [ 'label' => lng('serversettings.update_channel'), 'settinggroup' => 'system', 'varname' => 'update_channel', 'type' => 'select', 'default' => 'stable', 'select_var' => [ 'stable' => lng('serversettings.uc_stable'), 'testing' => lng('serversettings.uc_testing'), 'nightly' => lng('serversettings.uc_nightly') ], 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'system_validate_domain' => [ 'label' => lng('serversettings.validate_domain'), 'settinggroup' => 'system', 'varname' => 'validate_domain', 'type' => 'checkbox', 'default' => true, 'save_method' => 'storeSettingField' ], 'system_stdsubdomain' => [ 'label' => lng('serversettings.stdsubdomainhost'), 'settinggroup' => 'system', 'varname' => 'stdsubdomain', 'type' => 'text', 'default' => '', 'save_method' => 'storeSettingHostname' ], 'system_mysql_access_host' => [ 'label' => lng('serversettings.mysql_access_host'), 'settinggroup' => 'system', 'varname' => 'mysql_access_host', 'type' => 'text', 'default' => '127.0.0.1,localhost', 'plausibility_check_method' => [ '\\Froxlor\\Validate\\Check', 'checkMysqlAccessHost' ], 'save_method' => 'storeSettingMysqlAccessHost' ], 'system_nssextrausers' => [ 'label' => lng('serversettings.nssextrausers'), 'settinggroup' => 'system', 'varname' => 'nssextrausers', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField' ], 'system_store_index_file_subs' => [ 'label' => lng('serversettings.system_store_index_file_subs'), 'settinggroup' => 'system', 'varname' => 'store_index_file_subs', 'type' => 'checkbox', 'default' => true, 'save_method' => 'storeSettingField' ], 'system_report_enable' => [ 'label' => lng('serversettings.report.report'), 'settinggroup' => 'system', 'varname' => 'report_enable', 'type' => 'checkbox', 'default' => true, 'cronmodule' => 'froxlor/reports', 'save_method' => 'storeSettingField' ], 'system_report_webmax' => [ 'label' => lng('serversettings.report.webmax'), 'settinggroup' => 'system', 'varname' => 'report_webmax', 'type' => 'number', 'min' => 0, 'max' => 150, 'default' => 90, 'save_method' => 'storeSettingField' ], 'system_report_trafficmax' => [ 'label' => lng('serversettings.report.trafficmax'), 'settinggroup' => 'system', 'varname' => 'report_trafficmax', 'type' => 'number', 'min' => 0, 'max' => 150, 'default' => 90, 'save_method' => 'storeSettingField' ], 'system_report_web_bccadmin' => [ 'label' => lng('serversettings.report.report_web_bccadmin'), 'settinggroup' => 'system', 'varname' => 'report_web_bccadmin', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField' ], 'system_mail_use_smtp' => [ 'label' => lng('serversettings.mail_use_smtp'), 'settinggroup' => 'system', 'varname' => 'mail_use_smtp', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField' ], 'system_mail_smtp_host' => [ 'label' => lng('serversettings.mail_smtp_host'), 'settinggroup' => 'system', 'varname' => 'mail_smtp_host', 'type' => 'text', 'default' => 'localhost', 'save_method' => 'storeSettingField' ], 'system_mail_smtp_port' => [ 'label' => lng('serversettings.mail_smtp_port'), 'settinggroup' => 'system', 'varname' => 'mail_smtp_port', 'type' => 'number', 'min' => 1, 'max' => 65535, 'default' => 25, 'save_method' => 'storeSettingField' ], 'system_mail_smtp_usetls' => [ 'label' => lng('serversettings.mail_smtp_usetls'), 'settinggroup' => 'system', 'varname' => 'mail_smtp_usetls', 'type' => 'checkbox', 'default' => true, 'save_method' => 'storeSettingField' ], 'system_mail_smtp_auth' => [ 'label' => lng('serversettings.mail_smtp_auth'), 'settinggroup' => 'system', 'varname' => 'mail_smtp_auth', 'type' => 'checkbox', 'default' => true, 'save_method' => 'storeSettingField' ], 'system_mail_smtp_user' => [ 'label' => lng('serversettings.mail_smtp_user'), 'settinggroup' => 'system', 'varname' => 'mail_smtp_user', 'type' => 'text', 'default' => '', 'save_method' => 'storeSettingField', 'autocomplete' => 'off' ], 'system_mail_smtp_passwd' => [ 'label' => lng('serversettings.mail_smtp_passwd'), 'settinggroup' => 'system', 'varname' => 'mail_smtp_passwd', 'type' => 'password', 'default' => '', 'save_method' => 'storeSettingField', 'autocomplete' => 'new-password' ], 'system_apply_specialsettings_default' => [ 'label' => lng('serversettings.apply_specialsettings_default'), 'settinggroup' => 'system', 'varname' => 'apply_specialsettings_default', 'type' => 'checkbox', 'default' => true, 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'system_apply_phpconfigs_default' => [ 'label' => lng('serversettings.apply_phpconfigs_default'), 'settinggroup' => 'system', 'varname' => 'apply_phpconfigs_default', 'type' => 'checkbox', 'default' => true, 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'system_domaindefaultalias' => [ 'label' => lng('admin.domaindefaultalias'), 'settinggroup' => 'system', 'varname' => 'domaindefaultalias', 'type' => 'select', 'default' => '0', 'select_var' => [ '0' => lng('domains.serveraliasoption_wildcard'), '1' => lng('domains.serveraliasoption_www'), '2' => lng('domains.serveraliasoption_none') ], 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'system_hide_incompatible_settings' => [ 'label' => lng('serversettings.hide_incompatible_settings'), 'settinggroup' => 'system', 'varname' => 'hide_incompatible_settings', 'type' => 'checkbox', 'default' => true, 'save_method' => 'storeSettingField', 'advanced_mode' => true ], ] ] ] ]; ================================================ FILE: actions/admin/settings/122.froxlorvhost.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ use Froxlor\Settings; return [ 'groups' => [ 'froxlorvhost' => [ 'title' => lng('admin.froxlorvhost') . (call_user_func([ '\Froxlor\Settings\FroxlorVhostSettings', 'hasVhostContainerEnabled' ]) == false ? lng('admin.novhostcontainer') : ''), 'icon' => 'fa-solid fa-wrench', 'fields' => [ /** * Webserver-Vhost */ 'system_froxlordirectlyviahostname' => [ 'label' => lng('serversettings.froxlordirectlyviahostname'), 'settinggroup' => 'system', 'varname' => 'froxlordirectlyviahostname', 'type' => 'checkbox', 'default' => true, 'save_method' => 'storeSettingField' ], 'system_froxloraliases' => [ 'label' => lng('serversettings.froxloraliases'), 'settinggroup' => 'system', 'varname' => 'froxloraliases', 'type' => 'text', 'string_regexp' => '/^(([a-z0-9\-\._]+, ?)*[a-z0-9\-\._]+)?$/i', 'string_emptyallowed' => true, 'default' => '', 'save_method' => 'storeSettingClearCertificates', 'advanced_mode' => true ], /** * SSL / Let's Encrypt */ 'system_le_froxlor_enabled' => [ 'label' => lng('serversettings.le_froxlor_enabled'), 'settinggroup' => 'system', 'varname' => 'le_froxlor_enabled', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingClearCertificates', 'visible' => Settings::Get('system.leenabled') && call_user_func([ '\Froxlor\Settings\FroxlorVhostSettings', 'hasVhostContainerEnabled' ], true), 'requires_reconf' => ['http'] ], 'system_le_froxlor_redirect' => [ 'label' => lng('serversettings.le_froxlor_redirect'), 'settinggroup' => 'system', 'varname' => 'le_froxlor_redirect', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField', 'visible' => Settings::Get('system.use_ssl') && call_user_func([ '\Froxlor\Settings\FroxlorVhostSettings', 'hasVhostContainerEnabled' ], true) ], 'system_hsts_maxage' => [ 'label' => lng('admin.domain_hsts_maxage'), 'settinggroup' => 'system', 'varname' => 'hsts_maxage', 'type' => 'number', 'min' => 0, 'max' => 94608000, // 3-years 'default' => 10368000, 'save_method' => 'storeSettingField', 'visible' => Settings::Get('system.use_ssl') && call_user_func([ '\Froxlor\Settings\FroxlorVhostSettings', 'hasVhostContainerEnabled' ], true), 'advanced_mode' => true ], 'system_hsts_incsub' => [ 'label' => lng('admin.domain_hsts_incsub'), 'settinggroup' => 'system', 'varname' => 'hsts_incsub', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField', 'visible' => Settings::Get('system.use_ssl') && call_user_func([ '\Froxlor\Settings\FroxlorVhostSettings', 'hasVhostContainerEnabled' ], true), 'advanced_mode' => true ], 'system_hsts_preload' => [ 'label' => lng('admin.domain_hsts_preload'), 'settinggroup' => 'system', 'varname' => 'hsts_preload', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField', 'visible' => Settings::Get('system.use_ssl') && call_user_func([ '\Froxlor\Settings\FroxlorVhostSettings', 'hasVhostContainerEnabled' ], true), 'advanced_mode' => true ], 'system_honorcipherorder' => [ 'label' => lng('admin.domain_honorcipherorder'), 'settinggroup' => 'system', 'varname' => 'honorcipherorder', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField', 'visible' => Settings::Get('system.use_ssl') && call_user_func([ '\Froxlor\Settings\FroxlorVhostSettings', 'hasVhostContainerEnabled' ], true), 'advanced_mode' => true ], 'system_sessiontickets' => [ 'label' => lng('admin.domain_sessiontickets'), 'settinggroup' => 'system', 'varname' => 'sessiontickets', 'type' => 'checkbox', 'default' => true, 'save_method' => 'storeSettingField', 'visible' => Settings::Get('system.use_ssl') && call_user_func([ '\Froxlor\Settings\FroxlorVhostSettings', 'hasVhostContainerEnabled' ], true), 'advanced_mode' => true ], /** * FCGID */ 'system_mod_fcgid_ownvhost' => [ 'label' => lng('serversettings.mod_fcgid_ownvhost'), 'settinggroup' => 'system', 'varname' => 'mod_fcgid_ownvhost', 'type' => 'checkbox', 'default' => true, 'save_method' => 'storeSettingField', 'websrv_avail' => [ 'apache2' ], 'visible' => Settings::Get('system.mod_fcgid') && call_user_func([ '\Froxlor\Settings\FroxlorVhostSettings', 'hasVhostContainerEnabled' ]), 'requires_reconf' => ['system:fcgid'] ], 'system_mod_fcgid_httpuser' => [ 'label' => lng('admin.mod_fcgid_user'), 'settinggroup' => 'system', 'varname' => 'mod_fcgid_httpuser', 'type' => 'text', 'default' => 'froxlorlocal', 'string_emptyallowed' => false, 'plausibility_check_method' => [ '\\Froxlor\\Validate\\Check', 'checkSystemUsername' ], 'save_method' => 'storeSettingWebserverFcgidFpmUser', 'websrv_avail' => [ 'apache2' ], 'visible' => Settings::Get('system.mod_fcgid') && call_user_func([ '\Froxlor\Settings\FroxlorVhostSettings', 'hasVhostContainerEnabled' ]), 'requires_reconf' => ['system:fcgid'] ], 'system_mod_fcgid_httpgroup' => [ 'label' => lng('admin.mod_fcgid_group'), 'settinggroup' => 'system', 'varname' => 'mod_fcgid_httpgroup', 'type' => 'text', 'default' => 'froxlorlocal', 'save_method' => 'storeSettingField', 'string_emptyallowed' => false, 'websrv_avail' => [ 'apache2' ], 'visible' => Settings::Get('system.mod_fcgid') && call_user_func([ '\Froxlor\Settings\FroxlorVhostSettings', 'hasVhostContainerEnabled' ]), 'requires_reconf' => ['system:fcgid'] ], 'system_mod_fcgid_defaultini_ownvhost' => [ 'label' => lng('serversettings.mod_fcgid.defaultini_ownvhost'), 'settinggroup' => 'system', 'varname' => 'mod_fcgid_defaultini_ownvhost', 'type' => 'select', 'default' => '2', 'option_options_method' => [ '\\Froxlor\\Http\\PhpConfig', 'getPhpConfigs' ], 'save_method' => 'storeSettingField', 'websrv_avail' => [ 'apache2' ], 'visible' => Settings::Get('system.mod_fcgid') && call_user_func([ '\Froxlor\Settings\FroxlorVhostSettings', 'hasVhostContainerEnabled' ]) ], /** * php-fpm */ 'phpfpm_enabled_ownvhost' => [ 'label' => lng('phpfpm.ownvhost'), 'settinggroup' => 'phpfpm', 'varname' => 'enabled_ownvhost', 'type' => 'checkbox', 'default' => true, 'save_method' => 'storeSettingField', 'visible' => Settings::Get('phpfpm.enabled') && call_user_func([ '\Froxlor\Settings\FroxlorVhostSettings', 'hasVhostContainerEnabled' ]), 'requires_reconf' => ['system:php-fpm'] ], 'phpfpm_vhost_httpuser' => [ 'label' => lng('phpfpm.vhost_httpuser'), 'settinggroup' => 'phpfpm', 'varname' => 'vhost_httpuser', 'type' => 'text', 'default' => 'froxlorlocal', 'string_emptyallowed' => false, 'plausibility_check_method' => [ '\\Froxlor\\Validate\\Check', 'checkSystemUsername' ], 'save_method' => 'storeSettingWebserverFcgidFpmUser', 'visible' => Settings::Get('phpfpm.enabled') && call_user_func([ '\Froxlor\Settings\FroxlorVhostSettings', 'hasVhostContainerEnabled' ]), 'requires_reconf' => ['system:php-fpm'] ], 'phpfpm_vhost_httpgroup' => [ 'label' => lng('phpfpm.vhost_httpgroup'), 'settinggroup' => 'phpfpm', 'varname' => 'vhost_httpgroup', 'type' => 'text', 'default' => 'froxlorlocal', 'string_emptyallowed' => false, 'save_method' => 'storeSettingField', 'visible' => Settings::Get('phpfpm.enabled') && call_user_func([ '\Froxlor\Settings\FroxlorVhostSettings', 'hasVhostContainerEnabled' ]), 'requires_reconf' => ['system:php-fpm'] ], 'phpfpm_vhost_defaultini' => [ 'label' => lng('serversettings.mod_fcgid.defaultini_ownvhost'), 'settinggroup' => 'phpfpm', 'varname' => 'vhost_defaultini', 'type' => 'select', 'default' => '2', 'option_options_method' => [ '\\Froxlor\\Http\\PhpConfig', 'getPhpConfigs' ], 'save_method' => 'storeSettingField', 'visible' => Settings::Get('phpfpm.enabled') && call_user_func([ '\Froxlor\Settings\FroxlorVhostSettings', 'hasVhostContainerEnabled' ]) ], /** * DNS */ 'system_dns_createhostnameentry' => [ 'label' => lng('serversettings.dns_createhostnameentry'), 'settinggroup' => 'system', 'varname' => 'dns_createhostnameentry', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField', 'visible' => Settings::Get('system.bind_enable') ] ] ] ] ]; ================================================ FILE: actions/admin/settings/125.cronjob.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ return [ 'groups' => [ 'crond' => [ 'title' => lng('admin.cronsettings'), 'icon' => 'fa-solid fa-clock-rotate-left', 'advanced_mode' => true, 'fields' => [ 'system_cronconfig' => [ 'label' => lng('serversettings.system_cronconfig'), 'settinggroup' => 'system', 'varname' => 'cronconfig', 'type' => 'text', 'string_type' => 'file', 'default' => '/etc/cron.d/froxlor', 'save_method' => 'storeSettingField' ], 'system_croncmdline' => [ 'label' => lng('serversettings.system_croncmdline'), 'settinggroup' => 'system', 'varname' => 'croncmdline', 'type' => 'text', 'string_regexp' => '/^[a-z0-9\/\._\- ]+$/i', 'default' => '/usr/bin/nice -n 5 /usr/bin/php -q', 'save_method' => 'storeSettingField', 'required_otp' => true ], 'system_crondreload' => [ 'label' => lng('serversettings.system_crondreload'), 'settinggroup' => 'system', 'varname' => 'crondreload', 'type' => 'text', 'string_regexp' => '/^[a-z0-9\/\._\- ]+$/i', 'default' => '/etc/init.d/cron reload', 'save_method' => 'storeSettingField', 'required_otp' => true ], 'system_cron_allowautoupdate' => [ 'label' => lng('serversettings.system_cron_allowautoupdate'), 'settinggroup' => 'system', 'varname' => 'cron_allowautoupdate', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField', 'required_otp' => true ] ] ] ] ]; ================================================ FILE: actions/admin/settings/130.webserver.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ use Froxlor\Settings; return [ 'groups' => [ 'webserver' => [ 'title' => lng('admin.webserversettings'), 'icon' => 'fa-solid fa-server', 'fields' => [ 'system_webserver' => [ 'label' => lng('admin.webserver'), 'settinggroup' => 'system', 'varname' => 'webserver', 'type' => 'select', 'default' => 'apache2', 'select_var' => [ 'apache2' => 'Apache 2', 'nginx' => 'Nginx' ], 'save_method' => 'storeSettingField', 'plausibility_check_method' => [ '\\Froxlor\\Validate\\Check', 'checkPhpInterfaceSetting' ], 'requires_reconf' => ['http'] ], 'system_apache24' => [ 'label' => lng('serversettings.apache_24'), 'settinggroup' => 'system', 'varname' => 'apache24', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField', 'websrv_avail' => [ 'apache2' ] ], 'system_apacheitksupport' => [ 'label' => lng('serversettings.apache_itksupport'), 'settinggroup' => 'system', 'varname' => 'apacheitksupport', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField', 'visible' => (Settings::Get('system.mod_fcgid') == 0 && Settings::Get('phpfpm.enabled') == 0), 'websrv_avail' => [ 'apache2' ], 'advanced_mode' => true ], 'system_http2_support' => [ 'label' => lng('serversettings.http2_support'), 'settinggroup' => 'system', 'varname' => 'http2_support', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField', 'websrv_avail' => [ 'apache2', 'nginx' ], 'visible' => Settings::Get('system.use_ssl') ], 'system_http3_support' => [ 'label' => lng('serversettings.http3_support'), 'settinggroup' => 'system', 'varname' => 'http3_support', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField', 'websrv_avail' => [ 'nginx' ], 'visible' => Settings::Get('system.use_ssl') ], 'system_dhparams_file' => [ 'label' => lng('serversettings.dhparams_file'), 'settinggroup' => 'system', 'varname' => 'dhparams_file', 'type' => 'text', 'string_type' => 'file', 'string_emptyallowed' => true, 'default' => '', 'save_method' => 'storeSettingField', 'visible' => Settings::Get('system.use_ssl'), 'advanced_mode' => true ], 'system_httpuser' => [ 'label' => lng('admin.webserver_user'), 'settinggroup' => 'system', 'varname' => 'httpuser', 'type' => 'text', 'default' => 'www-data', 'plausibility_check_method' => [ '\\Froxlor\\Validate\\Check', 'checkSystemUsername' ], 'save_method' => 'storeSettingWebserverFcgidFpmUser' ], 'system_httpgroup' => [ 'label' => lng('admin.webserver_group'), 'settinggroup' => 'system', 'varname' => 'httpgroup', 'type' => 'text', 'default' => 'www-data', 'save_method' => 'storeSettingField' ], 'system_apacheconf_vhost' => [ 'label' => lng('serversettings.apacheconf_vhost'), 'settinggroup' => 'system', 'varname' => 'apacheconf_vhost', 'type' => 'text', 'string_type' => 'filedir', 'default' => '/etc/apache2/sites-enabled/', 'save_method' => 'storeSettingField', 'requires_reconf' => ['http'] ], 'system_apacheconf_diroptions' => [ 'label' => lng('serversettings.apacheconf_diroptions'), 'settinggroup' => 'system', 'varname' => 'apacheconf_diroptions', 'type' => 'text', 'string_type' => 'filedir', 'default' => '/etc/apache2/sites-enabled/', 'save_method' => 'storeSettingField', 'requires_reconf' => ['http'] ], 'system_apacheconf_htpasswddir' => [ 'label' => lng('serversettings.apacheconf_htpasswddir'), 'settinggroup' => 'system', 'varname' => 'apacheconf_htpasswddir', 'type' => 'text', 'string_type' => 'confdir', 'default' => '/etc/apache2/htpasswd/', 'save_method' => 'storeSettingField' ], 'system_logfiles_directory' => [ 'label' => lng('serversettings.logfiles_directory'), 'settinggroup' => 'system', 'varname' => 'logfiles_directory', 'type' => 'text', 'string_type' => 'dir', 'default' => '/var/customers/logs/', 'save_method' => 'storeSettingField', 'requires_reconf' => ['http'] ], 'system_logfiles_script' => [ 'label' => lng('serversettings.logfiles_script'), 'settinggroup' => 'system', 'varname' => 'logfiles_script', 'type' => 'text', 'default' => '', 'save_method' => 'storeSettingField', 'websrv_avail' => [ 'apache2' ], 'advanced_mode' => true ], 'system_logfiles_piped' => [ 'label' => lng('serversettings.logfiles_piped'), 'settinggroup' => 'system', 'varname' => 'logfiles_piped', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField', 'websrv_avail' => [ 'apache2' ], 'advanced_mode' => true ], 'system_logfiles_format' => [ 'label' => lng('serversettings.logfiles_format'), 'settinggroup' => 'system', 'varname' => 'logfiles_format', 'type' => (strpos(Settings::Get('system.logfiles_format'), '"') !== false ? 'textarea' : 'text'), 'string_regexp' => '/^[^\0\r\n<>]*$/i', 'default' => '', 'string_emptyallowed' => true, 'save_method' => 'storeSettingField', 'websrv_avail' => [ 'apache2', 'nginx' ], 'visible' => Settings::Get('system.traffictool') != 'webalizer', 'advanced_mode' => true ], 'system_logfiles_type' => [ 'label' => lng('serversettings.logfiles_type'), 'settinggroup' => 'system', 'varname' => 'logfiles_type', 'type' => 'select', 'default' => '1', 'select_var' => [ '1' => 'combined', '2' => 'vhost_combined' ], 'save_method' => 'storeSettingField', 'websrv_avail' => [ 'apache2' ] ], 'system_errorlog_level' => [ 'label' => lng('serversettings.errorlog_level'), 'settinggroup' => 'system', 'varname' => 'errorlog_level', 'type' => 'select', 'default' => (Settings::Get('system.webserver') == 'nginx' ? 'error' : 'warn'), 'select_var' => [ 'emerg' => 'emerg', 'alert' => 'alert', 'crit' => 'crit', 'error' => 'error', 'warn' => 'warn', 'notice' => 'notice', 'info' => 'info', 'debug' => 'debug' ], 'save_method' => 'storeSettingField', 'websrv_avail' => [ 'apache2', 'nginx' ] ], 'system_customer_ssl_path' => [ 'label' => lng('serversettings.customerssl_directory'), 'settinggroup' => 'system', 'varname' => 'customer_ssl_path', 'type' => 'text', 'string_type' => 'confdir', 'default' => '/etc/ssl/froxlor-custom/', 'save_method' => 'storeSettingField' ], 'system_phpappendopenbasedir' => [ 'label' => lng('serversettings.phpappendopenbasedir'), 'settinggroup' => 'system', 'varname' => 'phpappendopenbasedir', 'type' => 'text', 'string_emptyallowed' => true, 'default' => '', 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'system_deactivateddocroot' => [ 'label' => lng('serversettings.deactivateddocroot'), 'settinggroup' => 'system', 'varname' => 'deactivateddocroot', 'type' => 'text', 'string_type' => 'dir', 'string_emptyallowed' => true, 'default' => '', 'save_method' => 'storeSettingField', 'requires_reconf' => ['http'] ], 'system_default_vhostconf' => [ 'label' => lng('serversettings.default_vhostconf'), 'settinggroup' => 'system', 'varname' => 'default_vhostconf', 'type' => 'textarea', 'default' => '', 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'system_default_sslvhostconf' => [ 'label' => lng('serversettings.default_sslvhostconf'), 'settinggroup' => 'system', 'varname' => 'default_sslvhostconf', 'type' => 'textarea', 'default' => '', 'save_method' => 'storeSettingField', 'visible' => Settings::Get('system.use_ssl') == 1, 'advanced_mode' => true ], 'system_include_default_vhostconf' => [ 'label' => lng('serversettings.includedefault_sslvhostconf'), 'settinggroup' => 'system', 'varname' => 'include_default_vhostconf', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'system_apacheglobaldiropt' => [ 'label' => lng('serversettings.apache_globaldiropt'), 'settinggroup' => 'system', 'varname' => 'apacheglobaldiropt', 'type' => 'textarea', 'default' => '', 'save_method' => 'storeSettingField', 'visible' => (Settings::Get('system.mod_fcgid') == 0 && Settings::Get('phpfpm.enabled') == 0), 'websrv_avail' => [ 'apache2' ], 'advanced_mode' => true ], 'system_apachereload_command' => [ 'label' => lng('serversettings.apachereload_command'), 'settinggroup' => 'system', 'varname' => 'apachereload_command', 'type' => 'text', 'string_regexp' => '/^[a-z0-9\/\._\- ]+$/i', 'default' => '/etc/init.d/apache2 reload', 'save_method' => 'storeSettingField', 'required_otp' => true ], 'system_phpreload_command' => [ 'label' => lng('serversettings.phpreload_command'), 'settinggroup' => 'system', 'varname' => 'phpreload_command', 'type' => 'text', 'string_regexp' => '/^[a-z0-9\/\._\- ]+$/i', 'default' => '', 'save_method' => 'storeSettingField', 'websrv_avail' => [ 'nginx' ], 'required_otp' => true ], 'system_nginx_php_backend' => [ 'label' => lng('serversettings.nginx_php_backend'), 'settinggroup' => 'system', 'varname' => 'nginx_php_backend', 'type' => 'text', 'default' => '127.0.0.1:8888', 'save_method' => 'storeSettingField', 'websrv_avail' => [ 'nginx' ] ], 'nginx_fastcgiparams' => [ 'label' => lng('serversettings.nginx_fastcgiparams'), 'settinggroup' => 'nginx', 'varname' => 'fastcgiparams', 'type' => 'text', 'string_type' => 'file', 'default' => '/etc/nginx/fastcgi_params', 'save_method' => 'storeSettingField', 'websrv_avail' => [ 'nginx' ] ], 'defaultwebsrverrhandler_enabled' => [ 'label' => lng('serversettings.defaultwebsrverrhandler_enabled'), 'settinggroup' => 'defaultwebsrverrhandler', 'varname' => 'enabled', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'defaultwebsrverrhandler_err401' => [ 'label' => lng('serversettings.defaultwebsrverrhandler_err401'), 'settinggroup' => 'defaultwebsrverrhandler', 'varname' => 'err401', 'type' => 'text', 'default' => '', 'save_method' => 'storeSettingField', 'websrv_avail' => [ 'apache2', 'nginx' ], 'advanced_mode' => true ], 'defaultwebsrverrhandler_err403' => [ 'label' => lng('serversettings.defaultwebsrverrhandler_err403'), 'settinggroup' => 'defaultwebsrverrhandler', 'varname' => 'err403', 'type' => 'text', 'default' => '', 'save_method' => 'storeSettingField', 'websrv_avail' => [ 'apache2', 'nginx' ], 'advanced_mode' => true ], 'defaultwebsrverrhandler_err404' => [ 'label' => lng('serversettings.defaultwebsrverrhandler_err404'), 'settinggroup' => 'defaultwebsrverrhandler', 'varname' => 'err404', 'type' => 'text', 'default' => '', 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'defaultwebsrverrhandler_err500' => [ 'label' => lng('serversettings.defaultwebsrverrhandler_err500'), 'settinggroup' => 'defaultwebsrverrhandler', 'varname' => 'err500', 'type' => 'text', 'default' => '', 'save_method' => 'storeSettingField', 'websrv_avail' => [ 'apache2', 'nginx' ], 'advanced_mode' => true ], 'customredirect_enabled' => [ 'label' => lng('serversettings.customredirect_enabled'), 'settinggroup' => 'customredirect', 'varname' => 'enabled', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField' ], 'customredirect_default' => [ 'label' => lng('serversettings.customredirect_default'), 'settinggroup' => 'customredirect', 'varname' => 'default', 'type' => 'select', 'default' => '1', 'option_options_method' => ['\\Froxlor\\Domain\\Domain', 'getRedirectCodes'], 'save_method' => 'storeSettingField' ], 'system_webserver_serveradmin' => [ 'label' => lng('admin.webserver_serveradmin.setting'), 'settinggroup' => 'system', 'varname' => 'webserver_serveradmin', 'type' => 'select', 'default' => 'customer', 'select_var' => [ 'customer' => lng('admin.webserver_serveradmin.customer'), 'admin' => lng('admin.webserver_serveradmin.admin'), 'global' => lng('admin.webserver_serveradmin.global'), 'none' => lng('admin.webserver_serveradmin.none') ], 'save_method' => 'storeSettingField', 'websrv_avail' => [ 'apache2', ], ], ] ] ] ]; ================================================ FILE: actions/admin/settings/131.ssl.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ use Froxlor\Froxlor; use Froxlor\Settings; return [ 'groups' => [ 'ssl' => [ 'title' => lng('admin.sslsettings'), 'icon' => 'fa-solid fa-shield', 'fields' => [ 'system_use_ssl' => [ 'label' => lng('serversettings.ssl.use_ssl'), 'settinggroup' => 'system', 'varname' => 'use_ssl', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField', 'overview_option' => true, 'requires_reconf' => ['http'] ], 'system_ssl_protocols' => [ 'label' => lng('serversettings.ssl.ssl_protocols'), 'settinggroup' => 'system', 'varname' => 'ssl_protocols', 'type' => 'select', 'default' => 'TLSv1.2', 'select_mode' => 'multiple', 'select_var' => [ 'TLSv1' => 'TLSv1', 'TLSv1.1' => 'TLSv1.1', 'TLSv1.2' => 'TLSv1.2', 'TLSv1.3' => 'TLSv1.3' ], 'save_method' => 'storeSettingField' ], 'system_ssl_cipher_list' => [ 'label' => lng('serversettings.ssl.ssl_cipher_list'), 'settinggroup' => 'system', 'varname' => 'ssl_cipher_list', 'type' => 'text', 'string_emptyallowed' => false, 'default' => 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305', 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'system_tlsv13_cipher_list' => [ 'label' => lng('serversettings.ssl.tlsv13_cipher_list'), 'settinggroup' => 'system', 'varname' => 'tlsv13_cipher_list', 'type' => 'text', 'string_emptyallowed' => true, 'default' => '', 'visible' => Settings::Get('system.webserver') == "apache2" && Settings::Get('system.apache24') == 1, 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'system_ssl_cert_file' => [ 'label' => lng('serversettings.ssl.ssl_cert_file'), 'settinggroup' => 'system', 'varname' => 'ssl_cert_file', 'type' => 'text', 'string_type' => 'file', 'string_emptyallowed' => true, 'default' => '/etc/ssl/froxlor_selfsigned.pem', 'save_method' => 'storeSettingField' ], 'system_ssl_key_file' => [ 'label' => lng('serversettings.ssl.ssl_key_file'), 'settinggroup' => 'system', 'varname' => 'ssl_key_file', 'type' => 'text', 'string_type' => 'file', 'string_emptyallowed' => true, 'default' => '/etc/ssl/froxlor_selfsigned.key', 'save_method' => 'storeSettingField' ], 'system_ssl_cert_chainfile' => [ 'label' => lng('admin.ipsandports.ssl_cert_chainfile'), 'settinggroup' => 'system', 'varname' => 'ssl_cert_chainfile', 'type' => 'text', 'string_type' => 'file', 'string_emptyallowed' => true, 'default' => '', 'save_method' => 'storeSettingField' ], 'system_ssl_ca_file' => [ 'label' => lng('serversettings.ssl.ssl_ca_file'), 'settinggroup' => 'system', 'varname' => 'ssl_ca_file', 'type' => 'text', 'string_type' => 'file', 'string_emptyallowed' => true, 'default' => '', 'save_method' => 'storeSettingField' ], 'system_apache24_ocsp_cache_path' => [ 'label' => lng('serversettings.ssl.apache24_ocsp_cache_path'), 'settinggroup' => 'system', 'varname' => 'apache24_ocsp_cache_path', 'type' => 'text', 'string_emptyallowed' => false, 'default' => 'shmcb:/var/run/apache2/ocsp-stapling.cache(131072)', 'visible' => Settings::Get('system.webserver') == "apache2" && Settings::Get('system.apache24') == 1, 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'system_sessionticketsenabled' => [ 'label' => lng('admin.domain_sessionticketsenabled'), 'settinggroup' => 'system', 'varname' => 'sessionticketsenabled', 'type' => 'checkbox', 'default' => true, 'save_method' => 'storeSettingField', 'visible' => Settings::Get('system.use_ssl') && (Settings::Get('system.webserver') == "nginx" || (Settings::Get('system.webserver') == "apache2" && Settings::Get('system.apache24') == 1)), 'advanced_mode' => true ], 'system_leenabled' => [ 'label' => lng('serversettings.leenabled'), 'settinggroup' => 'system', 'varname' => 'leenabled', 'type' => 'checkbox', 'default' => false, 'cronmodule' => 'froxlor/letsencrypt', 'save_method' => 'storeSettingField', 'requires_reconf' => ['http'] ], 'system_acmeshpath' => [ 'label' => lng('serversettings.acmeshpath'), 'settinggroup' => 'system', 'varname' => 'acmeshpath', 'type' => 'text', 'string_type' => 'file', 'default' => '/root/.acme.sh/acme.sh', 'save_method' => 'storeSettingField', 'advanced_mode' => true, 'required_otp' => true ], 'system_letsencryptacmeconf' => [ 'label' => lng('serversettings.letsencryptacmeconf'), 'settinggroup' => 'system', 'varname' => 'letsencryptacmeconf', 'type' => 'text', 'string_type' => 'file', 'default' => '/etc/apache2/conf-enabled/acme.conf', 'save_method' => 'storeSettingField', 'requires_reconf' => ['http'] ], 'system_letsencryptca' => [ 'label' => lng('serversettings.letsencryptca'), 'settinggroup' => 'system', 'varname' => 'letsencryptca', 'type' => 'select', 'default' => 'letsencrypt', 'select_var' => [ 'letsencrypt_test' => 'Let\'s Encrypt (Test / Staging)', 'letsencrypt' => 'Let\'s Encrypt (Live)', 'buypass_test' => 'Buypass (Test / Staging)', 'buypass' => 'Buypass (Live)', 'zerossl' => 'ZeroSSL (Live)', 'google' => 'Google (Live)', 'google_test' => 'Google (Test / Staging)', ], 'save_method' => 'storeSettingField' ], 'system_letsencryptchallengepath' => [ 'label' => lng('serversettings.letsencryptchallengepath'), 'settinggroup' => 'system', 'varname' => 'letsencryptchallengepath', 'type' => 'text', 'string_emptyallowed' => false, 'default' => Froxlor::getInstallDir(), 'save_method' => 'storeSettingField', 'advanced_mode' => true, 'requires_reconf' => ['http'] ], 'system_letsencryptkeysize' => [ 'label' => lng('serversettings.letsencryptkeysize'), 'settinggroup' => 'system', 'varname' => 'letsencryptkeysize', 'type' => 'select', 'default' => '2048', 'select_var' => [ '2048' => '2048', '3072' => '3072', '4096' => '4096', '8192' => '8192' ], 'save_method' => 'storeSettingField' ], 'system_leecc' => [ 'label' => lng('serversettings.letsencryptecc'), 'settinggroup' => 'system', 'varname' => 'leecc', 'type' => 'select', 'default' => '0', 'select_var' => [ '0' => '-', '256' => 'ec-256', '384' => 'ec-384' ], 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'system_letsencryptreuseold' => [ 'label' => lng('serversettings.letsencryptreuseold'), 'settinggroup' => 'system', 'varname' => 'letsencryptreuseold', 'type' => 'checkbox', 'default' => true, 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'system_le_domain_dnscheck' => [ 'label' => lng('serversettings.le_domain_dnscheck'), 'settinggroup' => 'system', 'varname' => 'le_domain_dnscheck', 'type' => 'checkbox', 'default' => true, 'save_method' => 'storeSettingField' ], 'system_le_domain_dnscheck_resolver' => [ 'label' => lng('serversettings.le_domain_dnscheck_resolver'), 'settinggroup' => 'system', 'varname' => 'le_domain_dnscheck_resolver', 'type' => 'text', 'string_type' => 'validate_ip', 'string_emptyallowed' => true, 'default' => '', 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'system_le_renew_services' => [ 'label' => lng('serversettings.le_renew_services'), 'settinggroup' => 'system', 'varname' => 'le_renew_services', 'type' => 'select', 'default' => '', 'select_mode' => 'multiple', 'option_emptyallowed' => true, 'select_var' => [ '' => lng('panel.none_value'), 'postfix' => 'postfix (smtp)', 'dovecot' => 'dovecot <2.4 (imap/pop3)', 'dovecot24' => 'dovecot >=2.4 (imap/pop3)', 'proftpd' => 'proftpd (ftp)', ], 'save_method' => 'storeSettingFieldInsertUpdateServicesTask', 'advanced_mode' => true ], 'system_le_renew_hook' => [ 'label' => lng('serversettings.le_renew_hook'), 'settinggroup' => 'system', 'varname' => 'le_renew_hook', 'type' => 'text', 'string_regexp' => '/^[a-z0-9\/\._\- ]+$/i', 'default' => 'systemctl restart postfix dovecot proftpd', 'save_method' => 'storeSettingFieldInsertUpdateServicesTask', 'advanced_mode' => true, 'required_otp' => true ], ] ] ] ]; ================================================ FILE: actions/admin/settings/135.fcgid.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ return [ 'groups' => [ 'fcgid' => [ 'title' => lng('admin.fcgid_settings'), 'icon' => 'fa-brands fa-php', 'websrv_avail' => [ 'apache2', ], 'fields' => [ 'system_mod_fcgid' => [ 'label' => lng('serversettings.mod_fcgid'), 'settinggroup' => 'system', 'varname' => 'mod_fcgid', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField', 'plausibility_check_method' => [ '\\Froxlor\\Validate\\Check', 'checkFcgidPhpFpm' ], 'overview_option' => true, 'requires_reconf' => ['http', 'system:fcgid'] ], 'system_mod_fcgid_configdir' => [ 'label' => lng('serversettings.mod_fcgid.configdir'), 'settinggroup' => 'system', 'varname' => 'mod_fcgid_configdir', 'type' => 'text', 'string_type' => 'confdir', 'default' => '/var/www/php-fcgi-scripts/', 'plausibility_check_method' => [ '\\Froxlor\\Validate\\Check', 'checkPathConflicts' ], 'save_method' => 'storeSettingField', 'requires_reconf' => ['system:fcgid'] ], 'system_mod_fcgid_tmpdir' => [ 'label' => lng('serversettings.mod_fcgid.tmpdir'), 'settinggroup' => 'system', 'varname' => 'mod_fcgid_tmpdir', 'type' => 'text', 'string_type' => 'dir', 'default' => '/var/customers/tmp/', 'save_method' => 'storeSettingField', 'requires_reconf' => ['http'] ], 'system_mod_fcgid_peardir' => [ 'label' => lng('serversettings.mod_fcgid.peardir'), 'settinggroup' => 'system', 'varname' => 'mod_fcgid_peardir', 'type' => 'text', 'string_type' => 'dir', 'string_delimiter' => ':', 'string_emptyallowed' => true, 'default' => '/usr/share/php/:/usr/share/php5/', 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'system_mod_fcgid_wrapper' => [ 'label' => lng('serversettings.mod_fcgid.wrapper'), 'settinggroup' => 'system', 'varname' => 'mod_fcgid_wrapper', 'type' => 'select', 'select_var' => [ 0 => 'ScriptAlias', 1 => 'FcgidWrapper' ], 'default' => 1, 'save_method' => 'storeSettingField', 'websrv_avail' => [ 'apache2' ], 'advanced_mode' => true ], 'system_mod_fcgid_starter' => [ 'label' => lng('serversettings.mod_fcgid.starter'), 'settinggroup' => 'system', 'varname' => 'mod_fcgid_starter', 'type' => 'number', 'min' => 0, 'default' => 0, 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'system_mod_fcgid_maxrequests' => [ 'label' => lng('serversettings.mod_fcgid.maxrequests'), 'settinggroup' => 'system', 'varname' => 'mod_fcgid_maxrequests', 'type' => 'number', 'default' => 250, 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'system_mod_fcgid_defaultini' => [ 'label' => lng('serversettings.mod_fcgid.defaultini'), 'settinggroup' => 'system', 'varname' => 'mod_fcgid_defaultini', 'type' => 'select', 'default' => '1', 'option_options_method' => [ '\\Froxlor\\Http\\PhpConfig', 'getPhpConfigs' ], 'save_method' => 'storeSettingField' ], 'system_mod_fcgid_idle_timeout' => [ 'label' => lng('serversettings.mod_fcgid.idle_timeout'), 'settinggroup' => 'system', 'varname' => 'mod_fcgid_idle_timeout', 'type' => 'number', 'default' => 30, 'save_method' => 'storeSettingField', 'advanced_mode' => true ] ] ] ] ]; ================================================ FILE: actions/admin/settings/136.phpfpm.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ use Froxlor\Settings; return [ 'groups' => [ 'phpfpm' => [ 'title' => lng('admin.phpfpm_settings'), 'icon' => 'fa-brands fa-php', 'fields' => [ 'phpfpm_enabled' => [ 'label' => lng('serversettings.phpfpm'), 'settinggroup' => 'phpfpm', 'varname' => 'enabled', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField', 'plausibility_check_method' => [ '\\Froxlor\\Validate\\Check', 'checkFcgidPhpFpm' ], 'overview_option' => true, 'requires_reconf' => ['http', 'system:php-fpm'] ], 'phpfpm_defaultini' => [ 'label' => lng('serversettings.mod_fcgid.defaultini'), 'settinggroup' => 'phpfpm', 'varname' => 'defaultini', 'type' => 'select', 'default' => '1', 'option_options_method' => [ '\\Froxlor\\Http\\PhpConfig', 'getPhpConfigs' ], 'save_method' => 'storeSettingField' ], 'phpfpm_aliasconfigdir' => [ 'label' => lng('serversettings.phpfpm_settings.aliasconfigdir'), 'settinggroup' => 'phpfpm', 'varname' => 'aliasconfigdir', 'type' => 'text', 'string_type' => 'confdir', 'default' => '/var/www/php-fpm/', 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'phpfpm_tmpdir' => [ 'label' => lng('serversettings.mod_fcgid.tmpdir'), 'settinggroup' => 'phpfpm', 'varname' => 'tmpdir', 'type' => 'text', 'string_type' => 'dir', 'default' => '/var/customers/tmp/', 'save_method' => 'storeSettingField' ], 'phpfpm_peardir' => [ 'label' => lng('serversettings.mod_fcgid.peardir'), 'settinggroup' => 'phpfpm', 'varname' => 'peardir', 'type' => 'text', 'string_type' => 'dir', 'string_delimiter' => ':', 'string_emptyallowed' => true, 'default' => '/usr/share/php/:/usr/share/php5/', 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'phpfpm_envpath' => [ 'label' => lng('serversettings.phpfpm_settings.envpath'), 'settinggroup' => 'phpfpm', 'varname' => 'envpath', 'type' => 'text', 'string_type' => 'dir', 'string_delimiter' => ':', 'string_emptyallowed' => true, 'default' => '/usr/local/bin:/usr/bin:/bin', 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'phpfpm_fastcgi_ipcdir' => [ 'label' => lng('serversettings.phpfpm_settings.ipcdir'), 'settinggroup' => 'phpfpm', 'varname' => 'fastcgi_ipcdir', 'type' => 'text', 'string_type' => 'dir', 'default' => '/var/lib/apache2/fastcgi/', 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'phpfpm_use_mod_proxy' => [ 'label' => lng('phpfpm.use_mod_proxy'), 'settinggroup' => 'phpfpm', 'varname' => 'use_mod_proxy', 'type' => 'checkbox', 'default' => true, 'visible' => Settings::Get('system.apache24'), 'save_method' => 'storeSettingField' ], 'phpfpm_ini_flags' => [ 'label' => lng('phpfpm.ini_flags'), 'settinggroup' => 'phpfpm', 'varname' => 'ini_flags', 'type' => 'textarea', 'default' => '', 'save_method' => 'storeSettingField', 'advanced_mode' => true, 'required_otp' => true ], 'phpfpm_ini_values' => [ 'label' => lng('phpfpm.ini_values'), 'settinggroup' => 'phpfpm', 'varname' => 'ini_values', 'type' => 'textarea', 'default' => '', 'save_method' => 'storeSettingField', 'advanced_mode' => true, 'required_otp' => true ], 'phpfpm_ini_admin_flags' => [ 'label' => lng('phpfpm.ini_admin_flags'), 'settinggroup' => 'phpfpm', 'varname' => 'ini_admin_flags', 'type' => 'textarea', 'default' => '', 'save_method' => 'storeSettingField', 'advanced_mode' => true, 'required_otp' => true ], 'phpfpm_ini_admin_values' => [ 'label' => lng('phpfpm.ini_admin_values'), 'settinggroup' => 'phpfpm', 'varname' => 'ini_admin_values', 'type' => 'textarea', 'default' => '', 'save_method' => 'storeSettingField', 'advanced_mode' => true, 'required_otp' => true ] ] ] ] ]; ================================================ FILE: actions/admin/settings/137.perl.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ return [ 'groups' => [ 'perl' => [ 'title' => lng('admin.perl_settings'), 'icon' => 'fa-solid fa-code', 'fields' => [ 'perl_suexecworkaround' => [ 'label' => lng('serversettings.perl.suexecworkaround'), 'settinggroup' => 'perl', 'varname' => 'suexecworkaround', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField', 'websrv_avail' => [ 'apache2' ] ], 'perl_suexecpath' => [ 'label' => lng('serversettings.perl.suexeccgipath'), 'settinggroup' => 'perl', 'varname' => 'suexecpath', 'type' => 'text', 'string_type' => 'dir', 'default' => '/var/www/cgi-bin/', 'save_method' => 'storeSettingField', 'websrv_avail' => [ 'apache2' ] ], 'serversettings_perl_server' => [ 'label' => lng('serversettings.perl_server'), 'settinggroup' => 'serversettings', 'varname' => 'perl_server', 'type' => 'text', 'default' => 'unix:/var/run/nginx/cgiwrap-dispatch.sock', 'save_method' => 'storeSettingField', 'websrv_avail' => [ 'nginx' ] ] ] ] ] ]; ================================================ FILE: actions/admin/settings/140.statistics.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ use Froxlor\Settings; return [ 'groups' => [ 'statistics' => [ 'title' => lng('admin.statisticsettings'), 'icon' => 'fa-solid fa-chart-area', 'fields' => [ 'system_traffictool' => [ 'label' => lng('serversettings.traffictool.toolselect'), 'settinggroup' => 'system', 'varname' => 'traffictool', 'type' => 'select', 'default' => 'goaccess', 'select_var' => [ 'webalizer' => lng('serversettings.traffictool.webalizer'), 'awstats' => lng('serversettings.traffictool.awstats'), 'goaccess' => lng('serversettings.traffictool.goaccess') ], 'save_method' => 'storeSettingUpdateTrafficTool', 'requires_reconf' => ['system'] ], 'system_webalizer_quiet' => [ 'label' => lng('serversettings.webalizer_quiet'), 'settinggroup' => 'system', 'varname' => 'webalizer_quiet', 'type' => 'select', 'default' => 2, 'select_var' => [ 0 => lng('admin.webalizer.normal'), 1 => lng('admin.webalizer.quiet'), 2 => lng('admin.webalizer.veryquiet') ], 'save_method' => 'storeSettingField', 'visible' => Settings::Get('system.traffictool') == 'webalizer' ], 'system_awstats_path' => [ 'label' => lng('serversettings.awstats_path'), 'settinggroup' => 'system', 'varname' => 'awstats_path', 'type' => 'text', 'string_type' => 'dir', 'default' => '/usr/share/awstats/tools/', 'save_method' => 'storeSettingField', 'visible' => Settings::Get('system.traffictool') == 'awstats' ], 'system_awstats_awstatspath' => [ 'label' => lng('serversettings.awstats_awstatspath'), 'settinggroup' => 'system', 'varname' => 'awstats_awstatspath', 'type' => 'text', 'string_type' => 'dir', 'default' => '/usr/lib/cgi-bin/', 'save_method' => 'storeSettingField', 'visible' => Settings::Get('system.traffictool') == 'awstats' ], 'system_awstats_conf' => [ 'label' => lng('serversettings.awstats_conf'), 'settinggroup' => 'system', 'varname' => 'awstats_conf', 'type' => 'text', 'string_type' => 'dir', 'default' => '/etc/awstats/', 'save_method' => 'storeSettingField', 'visible' => Settings::Get('system.traffictool') == 'awstats', 'requires_reconf' => ['system:awstats'] ], 'system_awstats_icons' => [ 'label' => lng('serversettings.awstats_icons'), 'settinggroup' => 'system', 'varname' => 'awstats_icons', 'type' => 'text', 'string_type' => 'dir', 'default' => '/usr/share/awstats/icon/', 'save_method' => 'storeSettingField', 'visible' => Settings::Get('system.traffictool') == 'awstats' ], 'system_awstats_logformat' => [ 'label' => lng('serversettings.awstats.logformat'), 'settinggroup' => 'system', 'varname' => 'awstats_logformat', 'type' => 'text', 'default' => '1', 'save_method' => 'storeSettingField', 'visible' => Settings::Get('system.traffictool') == 'awstats', 'advanced_mode' => true ] ] ] ] ]; ================================================ FILE: actions/admin/settings/150.mail.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ return [ 'groups' => [ 'mail' => [ 'title' => lng('admin.mailserversettings'), 'icon' => 'fa-solid fa-envelope', 'fields' => [ 'system_vmail_uid' => [ 'label' => lng('serversettings.vmail_uid'), 'settinggroup' => 'system', 'varname' => 'vmail_uid', 'type' => 'number', 'default' => 2000, 'min' => 2, 'max' => 65535, 'save_method' => 'storeSettingField', 'advanced_mode' => true, 'requires_reconf' => ['smtp'] ], 'system_vmail_gid' => [ 'label' => lng('serversettings.vmail_gid'), 'settinggroup' => 'system', 'varname' => 'vmail_gid', 'type' => 'number', 'default' => 2000, 'min' => 2, 'max' => 65535, 'save_method' => 'storeSettingField', 'advanced_mode' => true, 'requires_reconf' => ['smtp'] ], 'system_vmail_homedir' => [ 'label' => lng('serversettings.vmail_homedir'), 'settinggroup' => 'system', 'varname' => 'vmail_homedir', 'type' => 'text', 'string_type' => 'dir', 'default' => '/var/customers/mail/', 'save_method' => 'storeSettingField', 'requires_reconf' => ['smtp'] ], 'system_vmail_maildirname' => [ 'label' => lng('serversettings.vmail_maildirname'), 'settinggroup' => 'system', 'varname' => 'vmail_maildirname', 'type' => 'text', 'string_type' => 'dir', 'default' => 'Maildir', 'string_emptyallowed' => true, 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'panel_sendalternativemail' => [ 'label' => lng('serversettings.sendalternativemail'), 'settinggroup' => 'panel', 'varname' => 'sendalternativemail', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField' ], 'system_mail_quota_enabled' => [ 'label' => lng('serversettings.mail_quota_enabled'), 'settinggroup' => 'system', 'varname' => 'mail_quota_enabled', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField' ], 'system_mail_quota' => [ 'label' => lng('serversettings.mail_quota'), 'settinggroup' => 'system', 'varname' => 'mail_quota', 'type' => 'number', 'default' => 100, 'save_method' => 'storeSettingField' ], 'catchall_catchall_enabled' => [ 'label' => lng('serversettings.catchall_enabled'), 'settinggroup' => 'catchall', 'varname' => 'catchall_enabled', 'type' => 'checkbox', 'default' => true, 'save_method' => 'storeSettingResetCatchall' ], 'system_mailtraffic_enabled' => [ 'label' => lng('serversettings.mailtraffic_enabled'), 'settinggroup' => 'system', 'varname' => 'mailtraffic_enabled', 'type' => 'checkbox', 'default' => true, 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'system_mdaserver' => [ 'label' => lng('serversettings.mdaserver'), 'settinggroup' => 'system', 'varname' => 'mdaserver', 'type' => 'select', 'default' => 'dovecot', 'select_var' => [ 'courier' => 'Courier', 'dovecot' => 'Dovecot' ], 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'system_mdalog' => [ 'label' => lng('serversettings.mdalog'), 'settinggroup' => 'system', 'varname' => 'mdalog', 'type' => 'text', 'string_type' => 'file', 'default' => '/var/log/mail.log', 'string_emptyallowed' => true, 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'system_mtaserver' => [ 'label' => lng('serversettings.mtaserver'), 'settinggroup' => 'system', 'varname' => 'mtaserver', 'type' => 'select', 'default' => 'postfix', 'select_var' => [ 'exim4' => 'Exim4', 'postfix' => 'Postfix' ], 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'system_mtalog' => [ 'label' => lng('serversettings.mtalog'), 'settinggroup' => 'system', 'varname' => 'mtalog', 'type' => 'text', 'string_type' => 'file', 'default' => '/var/log/mail.log', 'string_emptyallowed' => true, 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'mail_enable_allow_sender' => [ 'label' => lng('serversettings.mail_enable_allow_sender'), 'settinggroup' => 'mail', 'varname' => 'enable_allow_sender', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField' ], 'mail_allow_external_domains' => [ 'label' => lng('serversettings.mail_allow_external_domains'), 'settinggroup' => 'mail', 'varname' => 'allow_external_domains', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField' ], ] ] ] ]; ================================================ FILE: actions/admin/settings/155.ftpserver.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ return [ 'groups' => [ 'ftpserver' => [ 'title' => lng('admin.ftpserversettings'), 'icon' => 'fa-solid fa-arrow-right-arrow-left', 'fields' => [ 'system_ftpserver' => [ 'label' => lng('admin.ftpserver'), 'settinggroup' => 'system', 'varname' => 'ftpserver', 'type' => 'select', 'default' => 'proftpd', 'select_var' => [ 'proftpd' => 'Proftpd', 'pureftpd' => 'Pureftpd' ], 'save_method' => 'storeSettingField' ] ] ] ] ]; ================================================ FILE: actions/admin/settings/160.nameserver.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ use Froxlor\Settings; return [ 'groups' => [ 'nameserver' => [ 'title' => lng('admin.nameserversettings'), 'icon' => 'fa-solid fa-globe', 'fields' => [ 'system_bind_enable' => [ 'label' => lng('serversettings.bindenable'), 'settinggroup' => 'system', 'varname' => 'bind_enable', 'type' => 'checkbox', 'default' => true, 'save_method' => 'storeSettingField', 'overview_option' => true, 'requires_reconf' => ['dns'] ], 'system_dnsenabled' => [ 'label' => lng('serversettings.dnseditorenable'), 'settinggroup' => 'system', 'varname' => 'dnsenabled', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField' ], 'system_dns_server' => [ 'label' => lng('serversettings.dns_server'), 'settinggroup' => 'system', 'varname' => 'dns_server', 'type' => 'select', 'default' => 'Bind', 'select_var' => [ 'Bind' => 'Bind9', 'PowerDNS' => 'PowerDNS' ], 'save_method' => 'storeSettingField', 'requires_reconf' => ['dns'] ], 'system_bindconf_directory' => [ 'label' => lng('serversettings.bindconf_directory'), 'settinggroup' => 'system', 'varname' => 'bindconf_directory', 'type' => 'text', 'string_type' => 'dir', 'default' => '/etc/bind/', 'save_method' => 'storeSettingField', 'visible' => Settings::Get('system.dns_server') == 'Bind', 'requires_reconf' => ['dns:bind'] ], 'system_bindreload_command' => [ 'label' => lng('serversettings.bindreload_command'), 'settinggroup' => 'system', 'varname' => 'bindreload_command', 'type' => 'text', 'string_regexp' => '/^[a-z0-9\/\._\- ]+$/i', 'default' => '/etc/init.d/bind9 reload', 'save_method' => 'storeSettingField', 'required_otp' => true ], 'system_nameservers' => [ 'label' => lng('serversettings.nameservers'), 'settinggroup' => 'system', 'varname' => 'nameservers', 'type' => 'text', 'string_regexp' => '/^(([a-z0-9\-\._]+, ?)*[a-z0-9\-\._]+)?$/i', 'string_emptyallowed' => true, 'default' => '', 'save_method' => 'storeSettingFieldInsertBindTask' ], 'system_mxservers' => [ 'label' => lng('serversettings.mxservers'), 'settinggroup' => 'system', 'varname' => 'mxservers', 'type' => 'text', 'string_regexp' => '/^(([0-9]+ [a-z0-9\-\._]+, ?)*[0-9]+ [a-z0-9\-\._]+)?$/i', 'string_emptyallowed' => true, 'default' => '', 'save_method' => 'storeSettingField' ], 'system_axfrservers' => [ 'label' => lng('serversettings.axfrservers'), 'settinggroup' => 'system', 'varname' => 'axfrservers', 'type' => 'text', 'string_type' => 'validate_ip_incl_private', 'string_delimiter' => ',', 'string_emptyallowed' => true, 'default' => '', 'save_method' => 'storeSettingField', 'required_otp' => true ], 'system_powerdns_mode' => [ 'label' => lng('serversettings.powerdns_mode'), 'settinggroup' => 'system', 'varname' => 'powerdns_mode', 'type' => 'select', 'default' => 'Native', 'select_var' => [ 'Native' => 'Native', 'Master' => 'Master' ], 'save_method' => 'storeSettingField', 'advanced_mode' => true, 'visible' => Settings::Get('system.dns_server') == 'PowerDNS', ], 'system_dns_createmailentry' => [ 'label' => lng('serversettings.mail_also_with_mxservers'), 'settinggroup' => 'system', 'varname' => 'dns_createmailentry', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField' ], 'system_dns_createcaaentry' => [ 'label' => lng('serversettings.caa_entry'), 'settinggroup' => 'system', 'varname' => 'dns_createcaaentry', 'type' => 'checkbox', 'default' => true, 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'caa_caa_entry' => [ 'label' => lng('serversettings.caa_entry_custom'), 'settinggroup' => 'caa', 'varname' => 'caa_entry', 'type' => 'textarea', 'default' => '', 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'system_defaultttl' => [ 'label' => lng('serversettings.defaultttl'), 'settinggroup' => 'system', 'varname' => 'defaultttl', 'type' => 'number', 'default' => 604800, /* 1 week */ 'min' => 3600, /* 1 hour */ 'max' => 2147483647, /* integer max */ 'save_method' => 'storeSettingField' ], 'system_soaemail' => [ 'label' => lng('serversettings.soaemail'), 'settinggroup' => 'system', 'varname' => 'soaemail', 'type' => 'email', 'string_emptyallowed' => true, 'default' => '', 'save_method' => 'storeSettingField' ] ] ] ] ]; ================================================ FILE: actions/admin/settings/170.logger.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ return [ 'groups' => [ 'logging' => [ 'title' => lng('admin.loggersettings'), 'icon' => 'fa-solid fa-file-lines', 'fields' => [ 'logger_enabled' => [ 'label' => lng('serversettings.logger.enable'), 'settinggroup' => 'logger', 'varname' => 'enabled', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField', 'overview_option' => true ], 'logger_severity' => [ 'label' => lng('serversettings.logger.severity'), 'settinggroup' => 'logger', 'varname' => 'severity', 'type' => 'select', 'default' => 1, 'select_var' => [ 1 => lng('admin.logger.normal'), 2 => lng('admin.logger.paranoid') ], 'save_method' => 'storeSettingField' ], 'logger_logtypes' => [ 'label' => lng('serversettings.logger.types'), 'settinggroup' => 'logger', 'varname' => 'logtypes', 'type' => 'select', 'default' => 'syslog,mysql', 'select_mode' => 'multiple', 'select_var' => [ 'syslog' => 'syslog', 'file' => 'file', 'mysql' => 'mysql' ], 'save_method' => 'storeSettingField' ], 'logger_logfile' => [ 'label' => lng('serversettings.logger.logfile'), 'settinggroup' => 'logger', 'varname' => 'logfile', 'type' => 'text', 'string_type' => 'file', 'string_emptyallowed' => true, 'default' => '', 'save_method' => 'storeSettingField' ], 'logger_log_cron' => [ 'label' => lng('serversettings.logger.logcron'), 'settinggroup' => 'logger', 'varname' => 'log_cron', 'type' => 'select', 'default' => 0, 'select_var' => [ 0 => lng('serversettings.logger.logcronoption.never'), 1 => lng('serversettings.logger.logcronoption.once'), 2 => lng('serversettings.logger.logcronoption.always') ], 'save_method' => 'storeSettingField' ] ] ] ] ]; ================================================ FILE: actions/admin/settings/180.antispam.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ return [ 'groups' => [ 'antispam' => [ 'title' => lng('admin.antispam_settings'), 'icon' => 'fa-solid fa-clipboard-check', 'fields' => [ 'antispam_activated' => [ 'label' => lng('antispam.activated'), 'settinggroup' => 'antispam', 'varname' => 'activated', 'type' => 'checkbox', 'default' => true, 'overview_option' => true, 'save_method' => 'storeSettingFieldInsertAntispamTask', ], 'antispam_config_file' => [ 'label' => lng('antispam.config_file'), 'settinggroup' => 'antispam', 'varname' => 'config_file', 'type' => 'text', 'string_type' => 'file', 'default' => '/etc/rspamd/local.d/froxlor_settings.conf', 'save_method' => 'storeSettingFieldInsertAntispamTask', 'requires_reconf' => ['antispam'] ], 'antispam_reload_command' => [ 'label' => lng('antispam.reload_command'), 'settinggroup' => 'antispam', 'varname' => 'reload_command', 'type' => 'text', 'string_regexp' => '/^[a-z0-9\/\._\- ]+$/i', 'default' => 'service rspamd restart', 'save_method' => 'storeSettingField', 'required_otp' => true ], 'antispam_default_bypass_spam' => [ 'label' => lng('antispam.default_bypass_spam'), 'settinggroup' => 'antispam', 'varname' => 'default_bypass_spam', 'type' => 'select', 'default' => 2, 'select_var' => [ 1 => lng('antispam.default_select.on_changeable'), 2 => lng('antispam.default_select.off_changeable'), 3 => lng('antispam.default_select.on_unchangeable'), 4 => lng('antispam.default_select.off_unchangeable'), ], 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'antispam_default_spam_rewrite_subject' => [ 'label' => lng('antispam.default_spam_rewrite_subject'), 'settinggroup' => 'antispam', 'varname' => 'default_spam_rewrite_subject', 'type' => 'select', 'default' => 1, 'select_var' => [ 1 => lng('antispam.default_select.on_changeable'), 2 => lng('antispam.default_select.off_changeable'), 3 => lng('antispam.default_select.on_unchangeable'), 4 => lng('antispam.default_select.off_unchangeable'), ], 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'antispam_default_policy_greylist' => [ 'label' => lng('antispam.default_policy_greylist'), 'settinggroup' => 'antispam', 'varname' => 'default_policy_greylist', 'type' => 'select', 'default' => 1, 'select_var' => [ 1 => lng('antispam.default_select.on_changeable'), 2 => lng('antispam.default_select.off_changeable'), 3 => lng('antispam.default_select.on_unchangeable'), 4 => lng('antispam.default_select.off_unchangeable'), ], 'save_method' => 'storeSettingField', 'advanced_mode' => true ], 'antispam_dkim_keylength' => [ 'label' => lng('antispam.dkim_keylength'), 'settinggroup' => 'antispam', 'varname' => 'dkim_keylength', 'type' => 'select', 'default' => '1024', 'select_var' => [ '1024' => '1024 Bit', '2048' => '2048 Bit' ], 'save_method' => 'storeSettingFieldInsertBindTask', 'advanced_mode' => true, ], 'spf_use_spf' => [ 'label' => lng('spf.use_spf'), 'settinggroup' => 'spf', 'varname' => 'use_spf', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField', ], 'spf_spf_entry' => [ 'label' => lng('spf.spf_entry'), 'settinggroup' => 'spf', 'varname' => 'spf_entry', 'type' => 'text', 'string_regexp' => '/^v=spf[a-z0-9:~?\s\.\-\/]+$/i', 'default' => 'v=spf1 a mx -all', 'save_method' => 'storeSettingField' ], 'dmarc_use_dmarc' => [ 'label' => lng('dmarc.use_dmarc'), 'settinggroup' => 'dmarc', 'varname' => 'use_dmarc', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField', ], 'dmarc_dmarc_entry' => [ 'label' => lng('dmarc.dmarc_entry'), 'settinggroup' => 'dmarc', 'varname' => 'dmarc_entry', 'type' => 'text', 'string_regexp' => '/^v=dmarc1(.+)$/i', 'default' => 'v=DMARC1; p=none;', 'save_method' => 'storeSettingField' ] ] ] ] ]; ================================================ FILE: actions/admin/settings/210.security.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ use Froxlor\Settings; return [ 'groups' => [ 'security' => [ 'title' => lng('admin.security_settings'), 'icon' => 'fa-solid fa-user-lock', 'fields' => [ 'panel_unix_names' => [ 'label' => lng('serversettings.unix_names'), 'settinggroup' => 'panel', 'varname' => 'unix_names', 'type' => 'checkbox', 'default' => true, 'save_method' => 'storeSettingField', 'required_otp' => true ], 'system_mailpwcleartext' => [ 'label' => lng('serversettings.mailpwcleartext'), 'settinggroup' => 'system', 'varname' => 'mailpwcleartext', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField', 'advanced_mode' => true, 'required_otp' => true ], 'system_passwordcryptfunc' => [ 'label' => lng('serversettings.passwordcryptfunc'), 'settinggroup' => 'system', 'varname' => 'passwordcryptfunc', 'type' => 'select', 'default' => PASSWORD_DEFAULT, 'option_options_method' => [ '\\Froxlor\\System\\Crypt', 'getAvailablePasswordHashes' ], 'save_method' => 'storeSettingField', 'advanced_mode' => true, 'required_otp' => true ], 'system_allow_error_report_admin' => [ 'label' => lng('serversettings.allow_error_report_admin'), 'settinggroup' => 'system', 'varname' => 'allow_error_report_admin', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField', 'required_otp' => true ], 'system_allow_error_report_customer' => [ 'label' => lng('serversettings.allow_error_report_customer'), 'settinggroup' => 'system', 'varname' => 'allow_error_report_customer', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField', 'required_otp' => true ], 'system_allow_customer_shell' => [ 'label' => lng('serversettings.allow_allow_customer_shell'), 'settinggroup' => 'system', 'varname' => 'allow_customer_shell', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField', 'advanced_mode' => true, 'required_otp' => true ], 'system_available_shells' => [ 'label' => lng('serversettings.available_shells'), 'settinggroup' => 'system', 'varname' => 'available_shells', 'type' => 'text', 'string_emptyallowed' => true, 'default' => '', 'save_method' => 'storeSettingField', 'advanced_mode' => true, 'required_otp' => true ], 'system_froxlorusergroup' => [ 'label' => lng('serversettings.froxlorusergroup'), 'settinggroup' => 'system', 'varname' => 'froxlorusergroup', 'type' => 'text', 'default' => '', 'save_method' => 'storeSettingField', 'plausibility_check_method' => [ '\\Froxlor\\Validate\\Check', 'checkLocalGroup' ], 'visible' => Settings::Get('system.nssextrausers'), 'advanced_mode' => true, 'required_otp' => true ], ] ] ] ]; ================================================ FILE: actions/admin/settings/220.quota.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ return [ 'groups' => [ 'diskquota' => [ 'title' => lng('diskquota'), 'icon' => 'fa-solid fa-sliders', 'advanced_mode' => true, 'fields' => [ 'system_diskquota_enabled' => [ 'label' => lng('serversettings.diskquota_enabled'), 'settinggroup' => 'system', 'varname' => 'diskquota_enabled', 'type' => 'checkbox', 'default' => false, 'save_method' => 'storeSettingField', 'overview_option' => true ], 'system_diskquota_repquota_path' => [ 'label' => lng('serversettings.diskquota_repquota_path.description'), 'settinggroup' => 'system', 'varname' => 'diskquota_repquota_path', 'type' => 'text', 'string_type' => 'file', 'default' => '/usr/sbin/repquota', 'save_method' => 'storeSettingField', 'required_otp' => true ], 'system_diskquota_quotatool_path' => [ 'label' => lng('serversettings.diskquota_quotatool_path.description'), 'settinggroup' => 'system', 'varname' => 'diskquota_quotatool_path', 'type' => 'text', 'string_type' => 'file', 'default' => '/usr/bin/quotatool', 'save_method' => 'storeSettingField', 'required_otp' => true ], 'system_diskquota_customer_partition' => [ 'label' => lng('serversettings.diskquota_customer_partition.description'), 'settinggroup' => 'system', 'varname' => 'diskquota_customer_partition', 'type' => 'text', 'string_type' => 'file', 'default' => '/dev/root', 'save_method' => 'storeSettingField', 'required_otp' => true ] ] ] ] ]; ================================================ FILE: actions/admin/settings/index.html ================================================ ================================================ FILE: actions/index.html ================================================ ================================================ FILE: admin_admins.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ const AREA = 'admin'; require __DIR__ . '/lib/init.php'; use Froxlor\Api\Commands\Admins; use Froxlor\CurrentUser; use Froxlor\Database\Database; use Froxlor\FroxlorLogger; use Froxlor\PhpHelper; use Froxlor\Settings; use Froxlor\UI\Collection; use Froxlor\UI\HTML; use Froxlor\UI\Listing; use Froxlor\UI\Panel\UI; use Froxlor\UI\Request; use Froxlor\UI\Response; $id = (int)Request::any('id'); if (($page == 'admins' || $page == 'overview') && $userinfo['change_serversettings'] == '1') { if ($action == '') { $log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "viewed admin_admins"); try { $admin_list_data = include_once dirname(__FILE__) . '/lib/tablelisting/admin/tablelisting.admins.php'; $collection = (new Collection(Admins::class, $userinfo)) ->withPagination($admin_list_data['admin_list']['columns'], $admin_list_data['admin_list']['default_sorting']); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } UI::view('user/table.html.twig', [ 'listing' => Listing::format($collection, $admin_list_data, 'admin_list'), 'actions_links' => [ [ 'href' => $linker->getLink(['section' => 'admins', 'page' => $page, 'action' => 'add']), 'label' => lng('admin.admin_add') ] ] ]); } elseif ($action == 'su') { try { $json_result = Admins::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; $destination_admin = $result['loginname']; if ($destination_admin != '' && $result['adminid'] != $userinfo['userid']) { $result['switched_user'] = CurrentUser::getData(); $result['adminsession'] = 1; $result['userid'] = $result['adminid']; session_regenerate_id(true); CurrentUser::setData($result); $log->logAction( FroxlorLogger::ADM_ACTION, LOG_INFO, "switched adminuser and is now '" . $destination_admin . "'" ); Response::redirectTo('admin_index.php'); } else { Response::redirectTo('index.php', [ 'action' => 'login' ]); } } elseif ($action == 'delete' && $id != 0) { try { $json_result = Admins::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if ($result['loginname'] != '') { if ($result['adminid'] == $userinfo['userid']) { Response::standardError('youcantdeleteyourself'); } if (Request::post('send') == 'send') { Admins::getLocal($userinfo, [ 'id' => $id ])->delete(); Response::redirectTo($filename, [ 'page' => $page ]); } else { HTML::askYesNo('admin_admin_reallydelete', $filename, [ 'id' => $id, 'page' => $page, 'action' => $action ], $result['loginname']); } } } elseif ($action == 'add') { if (Request::post('send') == 'send') { try { Admins::getLocal($userinfo, Request::postAll())->add(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { $ipaddress = []; $ipaddress[-1] = lng('admin.allips'); $ipsandports_stmt = Database::query(" SELECT `id`, `ip` FROM `" . TABLE_PANEL_IPSANDPORTS . "` GROUP BY `ip` ORDER BY `ip` ASC "); while ($row = $ipsandports_stmt->fetch(PDO::FETCH_ASSOC)) { $ipaddress[$row['id']] = $row['ip']; } $admin_add_data = include_once dirname(__FILE__) . '/lib/formfields/admin/admin/formfield.admin_add.php'; UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'admins']), 'formdata' => $admin_add_data['admin_add'] ]); } } elseif ($action == 'edit' && $id != 0) { try { $json_result = Admins::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if ($result['loginname'] != '') { if (Request::post('send') == 'send') { try { Admins::getLocal($userinfo, Request::postAll())->update(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { $dec_places = Settings::Get('panel.decimal_places'); $result['traffic'] = round($result['traffic'] / (1024 * 1024), $dec_places); $result['diskspace'] = round($result['diskspace'] / 1024, $dec_places); $result['email'] = $idna_convert->decode($result['email']); $ipaddress = []; $ipaddress[-1] = lng('admin.allips'); $ipsandports_stmt = Database::query(" SELECT `id`, `ip` FROM `" . TABLE_PANEL_IPSANDPORTS . "` GROUP BY `ip` ORDER BY `ip` ASC "); while ($row = $ipsandports_stmt->fetch(PDO::FETCH_ASSOC)) { $ipaddress[$row['id']] = $row['ip']; } $result = PhpHelper::htmlentitiesArray($result); $admin_edit_data = include_once dirname(__FILE__) . '/lib/formfields/admin/admin/formfield.admin_edit.php'; UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'admins', 'id' => $id]), 'formdata' => $admin_edit_data['admin_edit'], 'editid' => $id ]); } } } } ================================================ FILE: admin_apcuinfo.php ================================================ * @author Janos Muzsi * @author Ralf Becker * @author Rasmus Lerdorf * @author Ilia Alshanetsky * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 * * Based on https://github.com/krakjoe/apcu/blob/master/apc.php, which is * licensed under the PHP licence (version 3.01), which can be viewed * online at https://www.php.net/license/3_01.txt */ use Froxlor\FroxlorLogger; use Froxlor\UI\Panel\UI; use Froxlor\UI\Request; use Froxlor\UI\Response; use Froxlor\UI\HTML; const AREA = 'admin'; require __DIR__ . '/lib/init.php'; $horizontal_bar_size = 950; // 1280px window width if ($action == 'delete' && function_exists('apcu_clear_cache') && $userinfo['change_serversettings'] == '1') { if (Request::post('send') == 'send') { apcu_clear_cache(); $log->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "cleared APCu cache"); header('Location: ' . $linker->getLink([ 'section' => 'apcuinfo', 'page' => 'showinfo' ])); exit(); } else { HTML::askYesNo('cache_reallydelete', $filename, [ 'page' => $page, 'action' => 'delete', ], '', [ 'section' => 'apcuinfo', 'page' => 'showinfo' ]); } } if (!function_exists('apcu_cache_info') || !function_exists('apcu_sma_info')) { Response::standardError('no_apcuinfo'); } if ($page == 'showinfo' && $userinfo['change_serversettings'] == '1') { $cache = apcu_cache_info(); $mem = apcu_sma_info(); $time = time(); $log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "viewed admin_apcuinfo"); // check for possible empty values that are used in the templates if (!isset($cache['file_upload_progress'])) { $cache['file_upload_progress'] = lng('logger.unknown'); } if (!isset($cache['num_expunges'])) { $cache['num_expunges'] = lng('logger.unknown'); } $overview = [ 'mem_size' => $mem['num_seg'] * $mem['seg_size'], 'mem_avail' => $mem['avail_mem'], 'mem_used' => ($mem['num_seg'] * $mem['seg_size']) - $mem['avail_mem'], 'seg_size' => bsize($mem['seg_size']), 'num_hits' => $cache['num_hits'], 'num_misses' => $cache['num_misses'], 'num_inserts' => $cache['num_inserts'], 'req_rate_user' => sprintf("%.2f", $cache['num_hits'] ? (($cache['num_hits'] + $cache['num_misses']) / ($time - $cache['start_time'])) : 0), 'hit_rate_user' => sprintf("%.2f", $cache['num_hits'] ? (($cache['num_hits']) / ($time - $cache['start_time'])) : 0), 'miss_rate_user' => sprintf("%.2f", $cache['num_misses'] ? (($cache['num_misses']) / ($time - $cache['start_time'])) : 0), 'insert_rate_user' => sprintf("%.2f", $cache['num_inserts'] ? (($cache['num_inserts']) / ($time - $cache['start_time'])) : 0), 'apcversion' => phpversion('apcu'), 'phpversion' => phpversion(), 'number_vars' => $cache['num_entries'], 'size_vars' => bsize($cache['mem_size']), 'num_hits_and_misses' => 0 >= ($cache['num_hits'] + $cache['num_misses']) ? 1 : ($cache['num_hits'] + $cache['num_misses']), 'file_upload_progress' => $cache['file_upload_progress'], 'num_expunges' => $cache['num_expunges'], 'host' => (function_exists('gethostname') ? gethostname() : (php_uname('n') ?: (empty($_SERVER['SERVER_NAME']) ? $_SERVER['HOST_NAME'] : $_SERVER['SERVER_NAME'] ) ) ), 'server' => $_SERVER['SERVER_SOFTWARE'] ?: '', 'start_time' => $cache['start_time'], 'uptime' => duration($cache['start_time']) ]; $overview['mem_used_percentage'] = number_format(($overview['mem_used'] / $overview['mem_size']) * 100, 1); $overview['num_hits_percentage'] = number_format(($overview['num_hits'] / $overview['num_hits_and_misses']) * 100, 1); $overview['num_misses_percentage'] = number_format(($overview['num_misses'] / $overview['num_hits_and_misses']) * 100, 1); $overview['readable'] = [ 'mem_size' => bsize($overview['mem_size']), 'mem_avail' => bsize($overview['mem_avail']), 'mem_used' => bsize($overview['mem_used']), 'num_hits' => number_format($overview['num_hits']), 'num_misses' => number_format($overview['num_misses']), 'number_vars' => number_format($overview['number_vars']), ]; $overview['runtimelines'] = []; foreach (ini_get_all('apcu') as $name => $v) { $value = $v['local_value']; $overview['runtimelines'][$name] = $value; } // Fragementation: (freeseg - 1) / total_seg $nseg = $freeseg = $fragsize = $freetotal = 0; for ($i = 0; $i < $mem['num_seg']; $i++) { $ptr = 0; foreach ($mem['block_lists'][$i] as $block) { if ($block['offset'] != $ptr) { ++$nseg; } $ptr = $block['offset'] + $block['size']; /* Only consider blocks <5M for the fragmentation % */ if ($block['size'] < (5 * 1024 * 1024)) { $fragsize += $block['size']; } $freetotal += $block['size']; } $freeseg += count($mem['block_lists'][$i]); } $overview['fragmentation'] = []; if ($freeseg > 1) { $overview['fragmentation']['used_percentage'] = number_format(($fragsize / $freetotal) * 100, 1); $overview['fragmentation']['used_bytes'] = $fragsize; $overview['fragmentation']['total_bytes'] = $freetotal; $overview['fragmentation']['num_frags'] = $freeseg; $overview['fragmentation']['readable'] = [ 'used_bytes' => bsize($fragsize), 'total_bytes' => bsize($freetotal), 'num_frags' => number_format($freeseg) ]; } else { $overview['fragmentation'] = 0; } UI::view('settings/apcuinfo.html.twig', [ 'apcuinfo' => $overview ]); } // pretty printer for byte values function bsize($size) { $i = 0; $val = ['b', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; while (($size / 1024) > 1) { $size /= 1024; ++$i; } return sprintf( '%.2f%s%s', $size, '', $val[$i] ); } function duration($ts) { global $time; $years = (int)((($time - $ts) / (7 * 86400)) / 52.177457); $rem = (int)(($time - $ts) - ($years * 52.177457 * 7 * 86400)); $weeks = (int)(($rem) / (7 * 86400)); $days = (int)(($rem) / 86400) - $weeks * 7; $hours = (int)(($rem) / 3600) - $days * 24 - $weeks * 7 * 24; $mins = (int)(($rem) / 60) - $hours * 60 - $days * 24 * 60 - $weeks * 7 * 24 * 60; $str = ''; if ($years == 1) { $str .= "$years year, "; } if ($years > 1) { $str .= "$years years, "; } if ($weeks == 1) { $str .= "$weeks week, "; } if ($weeks > 1) { $str .= "$weeks weeks, "; } if ($days == 1) { $str .= "$days day,"; } if ($days > 1) { $str .= "$days days,"; } if ($hours == 1) { $str .= " $hours hour and"; } if ($hours > 1) { $str .= " $hours hours and"; } if ($mins == 1) { $str .= " 1 minute"; } else { $str .= " $mins minutes"; } return $str; } ================================================ FILE: admin_autoupdate.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ const AREA = 'admin'; require __DIR__ . '/lib/init.php'; use Froxlor\Froxlor; use Froxlor\FroxlorLogger; use Froxlor\FileDir; use Froxlor\Install\AutoUpdate; use Froxlor\Settings; use Froxlor\UI\Panel\UI; use Froxlor\UI\Request; use Froxlor\UI\Response; if ($page != 'error') { // check for webupdate to be enabled if (Settings::Config('enable_webupdate') != true) { Response::redirectTo($filename, [ 'page' => 'error', 'errno' => 11 ]); } } // display initial version check if ($page == 'overview') { // log our actions $log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "checking auto-update"); // check for new version try { $result = AutoUpdate::checkVersion(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } if ($result == 1) { // anzeige über version-status mit ggfls. formular // zum update schritt #1 -> download $text = lng('admin.newerversionavailable') . ' ' . lng('admin.newerversiondetails', [AutoUpdate::getFromResult('version'), Froxlor::VERSION]); $upd_formfield = [ 'updates' => [ 'title' => lng('update.update'), 'image' => 'fa-solid fa-download', 'sections' => [ 'section_autoupd' => [ 'fields' => [ 'newversion' => ['type' => 'hidden', 'value' => AutoUpdate::getFromResult('version')] ] ] ], 'buttons' => [ [ 'class' => 'btn-outline-secondary', 'label' => lng('panel.cancel'), 'type' => 'reset' ], [ 'label' => lng('update.proceed') ] ] ] ]; UI::view('user/form-note.html.twig', [ 'formaction' => $linker->getLink(['section' => 'autoupdate', 'page' => 'getdownload']), 'formdata' => $upd_formfield['updates'], // alert 'type' => 'warning', 'alert_msg' => $text ]); } else if ($result < 0 || $result > 1) { // remote errors if ($result < 0) { Response::dynamicError(AutoUpdate::getLastError()); } else { Response::redirectTo($filename, [ 'page' => 'error', 'errno' => $result ]); } } else { // no new version Response::standardSuccess('update.noupdatesavail', (Settings::Get('system.update_channel') == 'testing' ? lng('serversettings.uc_testing') . ' ' : '')); } } // download the new archive elseif ($page == 'getdownload') { // retrieve the new version from the form $newversion = Request::post('newversion'); $result = 6; // valid? if ($newversion !== null) { $result = AutoUpdate::downloadZip($newversion); if (!is_numeric($result)) { // to the next step Response::redirectTo($filename, [ 'page' => 'extract', 'archive' => $result ]); } } Response::redirectTo($filename, [ 'page' => 'error', 'errno' => $result ]); } // extract and install new version elseif ($page == 'extract') { if (Request::post('send') == 'send') { $toExtract = Request::post('archive'); $localArchive = FileDir::makeCorrectFile(Froxlor::getInstallDir() . '/updates/' . $toExtract); $log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "Extracting " . $localArchive . " to " . Froxlor::getInstallDir()); $result = AutoUpdate::extractZip($localArchive); if ($result > 0) { // error Response::redirectTo($filename, [ 'page' => 'error', 'errno' => $result ]); } if (function_exists('opcache_reset')) { @opcache_reset(); } // redirect to update-page Response::redirectTo('admin_updates.php'); } else { $toExtract = Request::get('archive'); $localArchive = FileDir::makeCorrectFile(Froxlor::getInstallDir() . '/updates/' . $toExtract); } if (!file_exists($localArchive)) { Response::redirectTo($filename, [ 'page' => 'error', 'errno' => 7 ]); } $text = lng('admin.extractdownloadedzip', [$toExtract]); $upd_formfield = [ 'updates' => [ 'title' => lng('update.update'), 'image' => 'fa-solid fa-download', 'sections' => [ 'section_autoupd' => [ 'fields' => [ 'archive' => ['type' => 'hidden', 'value' => $toExtract] ] ] ], 'buttons' => [ [ 'class' => 'btn-outline-secondary', 'label' => lng('panel.cancel'), 'type' => 'reset' ], [ 'label' => lng('update.proceed') ] ] ] ]; UI::view('user/form-note.html.twig', [ 'formaction' => $linker->getLink(['section' => 'autoupdate', 'page' => 'extract']), 'formdata' => $upd_formfield['updates'], // alert 'type' => 'warning', 'alert_msg' => $text ]); } // display error elseif ($page == 'error') { // retrieve error-number via url-parameter $errno = Request::get('errno', 0); // 2 = no Zlib // 3 = custom version detected // 4 = could not store archive to local hdd // 5 = some weird value came from version.froxlor.org // 6 = download without valid version // 7 = local archive does not exist // 8 = could not extract archive // 9 = checksum mismatch // 10 = * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ const AREA = 'admin'; require __DIR__ . '/lib/init.php'; use Froxlor\Config\ConfigParser; use Froxlor\FileDir; use Froxlor\Froxlor; use Froxlor\Settings; use Froxlor\UI\Panel\UI; use Froxlor\UI\Request; use Froxlor\UI\Response; use Froxlor\Validate\Validate; if ($userinfo['change_serversettings'] == '1') { if ($action == 'setconfigured') { Settings::Set('panel.is_configured', '1', true); Response::redirectTo('admin_configfiles.php'); } // get distro from URL param $distribution = Request::any('distribution'); $reselect = Request::any('reselect', 0); // check for possible setting if (empty($distribution)) { $distribution = Settings::Get('system.distribution') ?? ""; } if ($reselect == 2) { Settings::Set('system.distro_mismatch', '2'); $reselect = 1; } if ($reselect == 1) { $distribution = ''; } $distributions_select = []; $services = []; $config_dir = FileDir::makeCorrectDir(Froxlor::getInstallDir() . '/lib/configfiles/'); if (!empty($distribution)) { if (!file_exists($config_dir . '/' . $distribution . ".xml")) { // unknown distribution -> redirect to select a valid distribution for config-templates Settings::Set('system.distribution', ''); Response::redirectTo('admin_configfiles.php', ['reselect' => 1]); } // update setting if different if ($distribution != Settings::Get('system.distribution')) { Settings::Set('system.distribution', $distribution); Settings::Set('system.distro_mismatch', '0'); } // create configparser object $configfiles = new ConfigParser($config_dir . '/' . $distribution . ".xml"); // get distro-info $dist_display = $configfiles->getCompleteDistroName(); // get all the services from the distro $services = $configfiles->getServices(); } else { // show list of available distro's $distros = glob($config_dir . '*.xml'); // read in all the distros foreach ($distros as $_distribution) { // get configparser object $dist = new ConfigParser($_distribution); // store in tmp array $distributions_select[str_replace(".xml", "", strtolower(basename($_distribution)))] = $dist->getCompleteDistroName(); } // sort by distribution name asort($distributions_select); } if ($distribution != "" && !empty(Request::post('finish'))) { $valid_keys = ['http', 'dns', 'smtp', 'mail', 'antispam', 'ftp', 'system', 'distro']; unset($_POST['finish']); unset($_POST['csrf_token']); $params = Request::postAll(); $params['distro'] = $distribution; $params['system'] = []; foreach (Request::post('system', []) as $sysdaemon) { $params['system'][] = $sysdaemon; } // validate params foreach ($params as $key => $value) { if (!in_array($key, $valid_keys)) { unset($params[$key]); continue; } if (!is_array($value)) { $params[$key] = Validate::validate($value, $key); } else { foreach ($value as $subkey => $subvalue) { $params[$key][$subkey] = Validate::validate($subvalue, $key.'.'.$subkey); } } } $params_content = json_encode($params); $params_filename = FileDir::makeCorrectFile(Froxlor::getInstallDir() . 'install/' . Froxlor::genSessionId() . '.json'); file_put_contents($params_filename, $params_content); UI::twigBuffer('settings/configuration-final.html.twig', [ 'distribution' => $distribution, // alert 'type' => 'info', 'alert_msg' => lng('admin.configfiles.finishnote'), 'basedir' => Froxlor::getInstallDir(), 'params_filename' => $params_filename ]); } else { if (!empty($distribution)) { // show available services to configure $fields = $services; $link_params = ['section' => 'configfiles', 'distribution' => $distribution]; UI::twigBuffer('settings/configuration.html.twig', [ 'action' => $linker->getLink($link_params), 'fields' => $fields, 'distribution' => $distribution ]); } else { $cfg_formfield = [ 'config' => [ 'title' => lng('admin.configfiles.serverconfiguration'), 'image' => 'fa-solid fa-wrench', 'description' => lng('admin.configfiles.description'), 'sections' => [ 'section_config' => [ 'fields' => [ 'distribution' => [ 'type' => 'select', 'select_var' => $distributions_select, 'label' => lng('admin.configfiles.distribution'), 'selected' => Settings::Get('system.distribution') ?? '' ] ] ] ], 'buttons' => [ [ 'class' => 'btn-outline-secondary', 'label' => lng('panel.cancel'), 'type' => 'reset' ], [ 'label' => lng('update.proceed') ] ] ] ]; UI::twigBuffer('user/form-note.html.twig', [ 'formaction' => $linker->getLink(['section' => 'configfiles']), 'formdata' => $cfg_formfield['config'], 'actions_links' => (int)Settings::Get('panel.is_configured') == 0 ? [ [ 'href' => $linker->getLink([ 'section' => 'configfiles', 'page' => 'overview', 'action' => 'setconfigured' ]), 'label' => lng('panel.ihave_configured'), 'class' => 'btn-outline-warning', 'icon' => 'fa-solid fa-circle-check' ] ] : [], // alert 'type' => 'warning', 'alert_msg' => lng('panel.settings_before_configuration') . ((int)Settings::Get('panel.is_configured') == 1 ? '

' . lng('panel.system_is_configured') : '') ]); } } UI::twigOutputBuffer(); } else { Response::redirectTo('admin_index.php'); } ================================================ FILE: admin_cronjobs.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ const AREA = 'admin'; require __DIR__ . '/lib/init.php'; use Froxlor\Api\Commands\Cronjobs; use Froxlor\FroxlorLogger; use Froxlor\UI\Collection; use Froxlor\UI\Listing; use Froxlor\UI\Panel\UI; use Froxlor\UI\Request; use Froxlor\UI\Response; $id = (int)Request::any('id'); if (($page == 'cronjobs' || $page == 'overview') && $userinfo['change_serversettings'] == '1') { if ($action == '') { $log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, 'viewed admin_cronjobs'); try { $cron_list_data = include_once dirname(__FILE__) . '/lib/tablelisting/admin/tablelisting.cronjobs.php'; $collection = (new Collection(Cronjobs::class, $userinfo)) ->withPagination($cron_list_data['cron_list']['columns'], $cron_list_data['cron_list']['default_sorting']); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } UI::view('user/table-note.html.twig', [ 'listing' => Listing::format($collection, $cron_list_data, 'cron_list'), // alert-box 'type' => 'warning', 'alert_msg' => lng('cron.changewarning') ]); } elseif ($action == 'new') { /* * @TODO later */ } elseif ($action == 'edit' && $id != 0) { try { $json_result = Cronjobs::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if ($result['cronfile'] != '') { if (Request::post('send') == 'send') { try { Cronjobs::getLocal($userinfo, Request::postAll())->update(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { $cronjobs_edit_data = include_once dirname(__FILE__) . '/lib/formfields/admin/cronjobs/formfield.cronjobs_edit.php'; UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'cronjobs', 'id' => $id]), 'formdata' => $cronjobs_edit_data['cronjobs_edit'], 'editid' => $id ]); } } } elseif ($action == 'delete' && $id != 0) { /* * @TODO later */ } } ================================================ FILE: admin_customers.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ const AREA = 'admin'; require __DIR__ . '/lib/init.php'; use Froxlor\Api\Commands\Admins; use Froxlor\Api\Commands\Customers; use Froxlor\Api\Commands\MysqlServer; use Froxlor\CurrentUser; use Froxlor\Database\Database; use Froxlor\Froxlor; use Froxlor\FroxlorLogger; use Froxlor\PhpHelper; use Froxlor\Settings; use Froxlor\UI\Collection; use Froxlor\UI\HTML; use Froxlor\UI\Listing; use Froxlor\UI\Panel\UI; use Froxlor\UI\Request; use Froxlor\UI\Response; $id = (int)Request::any('id'); if (($page == 'customers' || $page == 'overview') && $userinfo['customers'] != '0') { if ($action == '') { $log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "viewed admin_customers"); try { $customer_list_data = include_once dirname(__FILE__) . '/lib/tablelisting/admin/tablelisting.customers.php'; $collection = (new Collection(Customers::class, $userinfo, ['show_usages' => true])) ->withPagination($customer_list_data['customer_list']['columns'], $customer_list_data['customer_list']['default_sorting']); if ($userinfo['change_serversettings']) { $collection->has('admin', Admins::class, 'adminid', 'adminid'); } } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $actions_links = false; if (CurrentUser::canAddResource('customers')) { $actions_links = [ [ 'href' => $linker->getLink(['section' => 'customers', 'page' => $page, 'action' => 'add']), 'label' => lng('admin.customer_add') ] ]; } UI::view('user/table.html.twig', [ 'listing' => Listing::format($collection, $customer_list_data, 'customer_list'), 'actions_links' => $actions_links ]); } elseif ($action == 'su' && $id != 0) { try { $json_result = Customers::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; $destination_user = $result['loginname']; if ($destination_user != '') { if ($result['deactivated'] == '1') { Response::standardError("usercurrentlydeactivated", $destination_user); } $result['switched_user'] = CurrentUser::getData(); $result['adminsession'] = 0; $result['userid'] = $result['customerid']; session_regenerate_id(true); CurrentUser::setData($result); $log->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "switched user and is now '" . $destination_user . "'"); $target = Request::get('target', 'index'); $redirect = "customer_" . $target . ".php"; if (!file_exists(Froxlor::getInstallDir() . "/" . $redirect)) { $redirect = "customer_index.php"; } Response::redirectTo($redirect, null, true); } else { Response::redirectTo('index.php', [ 'action' => 'login' ]); } } elseif ($action == 'unlock' && $id != 0) { try { $json_result = Customers::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if (Request::post('send') == 'send') { try { $json_result = Customers::getLocal($userinfo, [ 'id' => $id ])->unlock(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { HTML::askYesNo('customer_reallyunlock', $filename, [ 'id' => $id, 'page' => $page, 'action' => $action ], $result['loginname']); } } elseif ($action == 'delete' && $id != 0) { try { $json_result = Customers::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if (Request::post('send') == 'send') { try { $json_result = Customers::getLocal($userinfo, [ 'id' => $id, 'delete_userfiles' => Request::post('delete_userfiles', 0) ])->delete(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { HTML::askYesNoWithCheckbox('admin_customer_reallydelete', 'admin_customer_alsoremovefiles', $filename, [ 'id' => $id, 'page' => $page, 'action' => $action ], $result['loginname']); } } elseif ($action == 'add') { if (Request::post('send') == 'send') { try { Customers::getLocal($userinfo, Request::postAll())->add(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { $mysql_servers = []; try { $result_json = MysqlServer::getLocal($userinfo)->listing(); $result_decoded = json_decode($result_json, true)['data']['list']; foreach ($result_decoded as $dbserver => $dbdata) { $mysql_servers[] = [ 'label' => $dbdata['caption'], 'value' => $dbserver ]; } } catch (Exception $e) { /* just none */ } $phpconfigs = []; $configs = Database::query(" SELECT c.*, fc.description as interpreter FROM `" . TABLE_PANEL_PHPCONFIGS . "` c LEFT JOIN `" . TABLE_PANEL_FPMDAEMONS . "` fc ON fc.id = c.fpmsettingid "); while ($row = $configs->fetch(PDO::FETCH_ASSOC)) { if ((int)Settings::Get('phpfpm.enabled') == 1) { $phpconfigs[] = [ 'label' => $row['description'] . " [" . $row['interpreter'] . "]", 'value' => $row['id'] ]; } else { $phpconfigs[] = [ 'label' => $row['description'], 'value' => $row['id'] ]; } } // hosting plans $hosting_plans = []; $plans = Database::query(" SELECT * FROM `" . TABLE_PANEL_PLANS . "` ORDER BY name ASC "); $hosting_plans = [ 0 => "---" ]; while ($row = $plans->fetch(PDO::FETCH_ASSOC)) { $hosting_plans[$row['id']] = $row['name']; } $customer_add_data = include_once dirname(__FILE__) . '/lib/formfields/admin/customer/formfield.customer_add.php'; UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'customers']), 'formdata' => $customer_add_data['customer_add'] ]); } } elseif ($action == 'edit' && $id != 0) { try { $json_result = Customers::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if ($result['loginname'] != '') { if (Request::post('send') == 'send') { try { Customers::getLocal($userinfo, Request::postAll())->update(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { $dec_places = Settings::Get('panel.decimal_places'); $result['traffic'] = round($result['traffic'] / (1024 * 1024), $dec_places); $result['diskspace'] = round($result['diskspace'] / 1024, $dec_places); $result['email'] = $idna_convert->decode($result['email']); $result = PhpHelper::htmlentitiesArray($result); $mysql_servers = []; try { $result_json = MysqlServer::getLocal($userinfo)->listing(); $result_decoded = json_decode($result_json, true)['data']['list']; foreach ($result_decoded as $dbserver => $dbdata) { $mysql_servers[] = [ 'label' => $dbdata['caption'], 'value' => $dbserver ]; } } catch (Exception $e) { /* just none */ } $phpconfigs = []; $configs = Database::query(" SELECT c.*, fc.description as interpreter FROM `" . TABLE_PANEL_PHPCONFIGS . "` c LEFT JOIN `" . TABLE_PANEL_FPMDAEMONS . "` fc ON fc.id = c.fpmsettingid "); while ($row = $configs->fetch(PDO::FETCH_ASSOC)) { if ((int)Settings::Get('phpfpm.enabled') == 1) { $phpconfigs[] = [ 'label' => $row['description'] . " [" . $row['interpreter'] . "]", 'value' => $row['id'] ]; } else { $phpconfigs[] = [ 'label' => $row['description'], 'value' => $row['id'] ]; } } // hosting plans $plans = Database::query(" SELECT * FROM `" . TABLE_PANEL_PLANS . "` ORDER BY name ASC "); $hosting_plans = [ 0 => "---" ]; while ($row = $plans->fetch(PDO::FETCH_ASSOC)) { $hosting_plans[$row['id']] = $row['name']; } $admin_select = []; if ($userinfo['customers_see_all'] == '1') { $available_admins_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_ADMINS . "` WHERE (`customers` = '-1' OR `customers` > `customers_used`) AND adminid <> :currentadmin "); Database::pexecute($available_admins_stmt, ['currentadmin' => $result['adminid']]); $admin_select = [ 0 => "---" ]; while ($available_admin = $available_admins_stmt->fetch()) { $admin_select[$available_admin['adminid']] = $available_admin['name'] . " (" . $available_admin['loginname'] . ")"; } } $customer_edit_data = include_once dirname(__FILE__) . '/lib/formfields/admin/customer/formfield.customer_edit.php'; UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'customers', 'id' => $id]), 'formdata' => $customer_edit_data['customer_edit'], 'editid' => $id ]); } } } } ================================================ FILE: admin_domains.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ const AREA = 'admin'; require __DIR__ . '/lib/init.php'; use Froxlor\Api\Commands\Customers as Customers; use Froxlor\Api\Commands\Domains as Domains; use Froxlor\Bulk\DomainBulkAction; use Froxlor\Cron\TaskId; use Froxlor\CurrentUser; use Froxlor\Customer\Customer; use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\FroxlorLogger; use Froxlor\Settings; use Froxlor\System\Cronjob; use Froxlor\UI\Collection; use Froxlor\UI\HTML; use Froxlor\UI\Listing; use Froxlor\UI\Panel\UI; use Froxlor\UI\Request; use Froxlor\UI\Response; use Froxlor\User; use Froxlor\Validate\Validate; $id = (int)Request::any('id'); if ($page == 'domains' || $page == 'overview') { if ($action == '') { $log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "viewed admin_domains"); try { $customerCollection = (new Collection(Customers::class, $userinfo)); $domain_list_data = include_once dirname(__FILE__) . '/lib/tablelisting/admin/tablelisting.domains.php'; $collection = (new Collection(Domains::class, $userinfo)) ->has('customer', Customers::class, 'customerid', 'customerid') ->withPagination($domain_list_data['domain_list']['columns'], $domain_list_data['domain_list']['default_sorting']); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $actions_links = false; if (CurrentUser::canAddResource('domains')) { $actions_links = []; $actions_links[] = [ 'href' => $linker->getLink(['section' => 'domains', 'page' => $page, 'action' => 'add']), 'label' => lng('admin.domain_add') ]; $actions_links[] = [ 'href' => $linker->getLink(['section' => 'domains', 'page' => $page, 'action' => 'import']), 'label' => lng('domains.domain_import'), 'icon' => 'fa-solid fa-file-import', 'class' => 'btn-outline-secondary' ]; } UI::view('user/table.html.twig', [ 'listing' => Listing::format($collection, $domain_list_data, 'domain_list'), 'actions_links' => $actions_links ]); } elseif ($action == 'delete' && $id != 0) { try { $json_result = Domains::getLocal($userinfo, [ 'id' => $id, 'no_std_subdomain' => true ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; $alias_check_stmt = Database::prepare(" SELECT COUNT(`id`) AS `count` FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `aliasdomain`= :id"); $alias_check = Database::pexecute_first($alias_check_stmt, [ 'id' => $id ]); if ($result['domain'] != '') { if (Request::post('send') == 'send' && $alias_check['count'] == 0) { try { Domains::getLocal($userinfo, Request::postAll())->delete(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } elseif ($alias_check['count'] > 0) { Response::standardError('domains_cantdeletedomainwithaliases'); } else { HTML::askYesNoWithCheckbox('admin_domain_reallydelete', 'admin_customer_alsoremovemail', $filename, [ 'id' => $id, 'page' => $page, 'action' => $action ], $idna_convert->decode($result['domain'])); } } } elseif ($action == 'add') { if (Request::post('send') == 'send') { try { Domains::getLocal($userinfo, Request::postAll())->add(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { $customers = [ 0 => lng('panel.please_choose') ]; $result_customers_stmt = Database::prepare(" SELECT `customerid`, `loginname`, `name`, `firstname`, `company` FROM `" . TABLE_PANEL_CUSTOMERS . "` " . ($userinfo['customers_see_all'] ? '' : " WHERE `adminid` = :adminid ") . " ORDER BY COALESCE(NULLIF(`name`,''), `company`) ASC"); $params = []; if ($userinfo['customers_see_all'] == '0') { $params['adminid'] = $userinfo['adminid']; } Database::pexecute($result_customers_stmt, $params); while ($row_customer = $result_customers_stmt->fetch(PDO::FETCH_ASSOC)) { $customers[$row_customer['customerid']] = User::getCorrectFullUserDetails($row_customer) . ' (' . $row_customer['loginname'] . ')'; } $admins = []; if ($userinfo['customers_see_all'] == '1') { $result_admins_stmt = Database::query(" SELECT `adminid`, `loginname`, `name` FROM `" . TABLE_PANEL_ADMINS . "` WHERE `domains_used` < `domains` OR `domains` = '-1' ORDER BY `name` ASC"); while ($row_admin = $result_admins_stmt->fetch(PDO::FETCH_ASSOC)) { $admins[$row_admin['adminid']] = User::getCorrectFullUserDetails($row_admin) . ' (' . $row_admin['loginname'] . ')'; } } if ($userinfo['ip'] == "-1") { $result_ipsandports_stmt = Database::query(" SELECT `id`, `ip`, `port` FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE `ssl`='0' ORDER BY `ip`, `port` ASC "); $result_ssl_ipsandports_stmt = Database::query(" SELECT `id`, `ip`, `port` FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE `ssl`='1' ORDER BY `ip`, `port` ASC "); } else { $admin_ip_stmt = Database::prepare(" SELECT `id`, `ip`, `port` FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE `id` = :ipid ORDER BY `ip`, `port` ASC "); $admin_ip = Database::pexecute_first($admin_ip_stmt, [ 'ipid' => $userinfo['ip'] ]); $result_ipsandports_stmt = Database::prepare(" SELECT `id`, `ip`, `port` FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE `ssl`='0' AND `ip` = :ipid ORDER BY `ip`, `port` ASC "); Database::pexecute($result_ipsandports_stmt, [ 'ipid' => $admin_ip['ip'] ]); $result_ssl_ipsandports_stmt = Database::prepare(" SELECT `id`, `ip`, `port` FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE `ssl`='1' AND `ip` = :ipid ORDER BY `ip`, `port` ASC "); Database::pexecute($result_ssl_ipsandports_stmt, [ 'ipid' => $admin_ip['ip'] ]); } // Build array holding all IPs and Ports available to this admin $ipsandports = []; while ($row_ipandport = $result_ipsandports_stmt->fetch(PDO::FETCH_ASSOC)) { if (filter_var($row_ipandport['ip'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $row_ipandport['ip'] = '[' . $row_ipandport['ip'] . ']'; } $ipsandports[] = [ 'label' => $row_ipandport['ip'] . ':' . $row_ipandport['port'], 'value' => $row_ipandport['id'] ]; } $ssl_ipsandports = []; while ($row_ssl_ipandport = $result_ssl_ipsandports_stmt->fetch(PDO::FETCH_ASSOC)) { if (filter_var($row_ssl_ipandport['ip'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $row_ssl_ipandport['ip'] = '[' . $row_ssl_ipandport['ip'] . ']'; } $ssl_ipsandports[] = [ 'label' => $row_ssl_ipandport['ip'] . ':' . $row_ssl_ipandport['port'], 'value' => $row_ssl_ipandport['id'] ]; } $standardsubdomains = []; $result_standardsubdomains_stmt = Database::query(" SELECT `id` FROM `" . TABLE_PANEL_DOMAINS . "` `d`, `" . TABLE_PANEL_CUSTOMERS . "` `c` WHERE `d`.`id` = `c`.`standardsubdomain` "); while ($row_standardsubdomain = $result_standardsubdomains_stmt->fetch(PDO::FETCH_ASSOC)) { $standardsubdomains[$row_standardsubdomain['id']] = $row_standardsubdomain['id']; } if (count($standardsubdomains) > 0) { $standardsubdomains = " AND `d`.`id` NOT IN (" . join(',', $standardsubdomains) . ") "; } else { $standardsubdomains = ''; } $domains = [ 0 => lng('domains.noaliasdomain') ]; $result_domains_stmt = Database::prepare(" SELECT `d`.`id`, `d`.`domain`, `c`.`loginname` FROM `" . TABLE_PANEL_DOMAINS . "` `d`, `" . TABLE_PANEL_CUSTOMERS . "` `c` WHERE `d`.`aliasdomain` IS NULL AND `d`.`parentdomainid` = 0" . $standardsubdomains . ($userinfo['customers_see_all'] ? '' : " AND `d`.`adminid` = :adminid") . " AND `d`.`customerid`=`c`.`customerid` ORDER BY `loginname`, `domain` ASC "); $params = []; if ($userinfo['customers_see_all'] == '0') { $params['adminid'] = $userinfo['adminid']; } Database::pexecute($result_domains_stmt, $params); while ($row_domain = $result_domains_stmt->fetch(PDO::FETCH_ASSOC)) { $domains[$row_domain['id']] = $idna_convert->decode($row_domain['domain']) . ' (' . $row_domain['loginname'] . ')'; } $phpconfigs = []; $configs = Database::query(" SELECT c.*, fc.description as interpreter FROM `" . TABLE_PANEL_PHPCONFIGS . "` c LEFT JOIN `" . TABLE_PANEL_FPMDAEMONS . "` fc ON fc.id = c.fpmsettingid "); while ($row = $configs->fetch(PDO::FETCH_ASSOC)) { if ((int)Settings::Get('phpfpm.enabled') == 1) { $phpconfigs[$row['id']] = $row['description'] . " [" . $row['interpreter'] . "]"; } else { $phpconfigs[$row['id']] = $row['description']; } } $openbasedir = [ 0 => lng('domain.docroot'), 1 => lng('domain.homedir'), 2 => lng('domain.docparent') ]; // create serveralias options $serveraliasoptions = [ 0 => lng('domains.serveraliasoption_wildcard'), 1 => lng('domains.serveraliasoption_www'), 2 => lng('domains.serveraliasoption_none') ]; $subcanemaildomain = [ 0 => lng('admin.subcanemaildomain.never'), 1 => lng('admin.subcanemaildomain.choosableno'), 2 => lng('admin.subcanemaildomain.choosableyes'), 3 => lng('admin.subcanemaildomain.always') ]; $domain_add_data = include_once dirname(__FILE__) . '/lib/formfields/admin/domains/formfield.domains_add.php'; UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'domains']), 'formdata' => $domain_add_data['domain_add'] ]); } } elseif ($action == 'edit' && $id != 0) { try { $json_result = Domains::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if ($result['domain'] != '') { $subdomains_stmt = Database::prepare(" SELECT COUNT(`id`) AS count FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `parentdomainid` = :resultid "); $subdomains = Database::pexecute_first($subdomains_stmt, [ 'resultid' => $result['id'] ]); $subdomains = $subdomains['count']; $alias_check_stmt = Database::prepare(" SELECT COUNT(`id`) AS count FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `aliasdomain` = :resultid "); $alias_check = Database::pexecute_first($alias_check_stmt, [ 'resultid' => $result['id'] ]); $alias_check = $alias_check['count']; $domain_emails_result_stmt = Database::prepare(" SELECT `email`, `email_full`, `destination`, `popaccountid` FROM `" . TABLE_MAIL_VIRTUAL . "` WHERE `customerid` = :customerid AND `domainid` = :id "); Database::pexecute($domain_emails_result_stmt, [ 'customerid' => $result['customerid'], 'id' => $result['id'] ]); $emails = Database::num_rows(); $email_forwarders = 0; $email_accounts = 0; while ($domain_emails_row = $domain_emails_result_stmt->fetch(PDO::FETCH_ASSOC)) { if ($domain_emails_row['destination'] != '') { $domain_emails_row['destination'] = explode(' ', FileDir::makeCorrectDestination($domain_emails_row['destination'])); $email_forwarders += count($domain_emails_row['destination']); if (in_array($domain_emails_row['email_full'], $domain_emails_row['destination'])) { $email_forwarders -= 1; $email_accounts++; } } } $ipsresult_stmt = Database::prepare(" SELECT `id_ipandports` FROM `" . TABLE_DOMAINTOIP . "` WHERE `id_domain` = :id "); Database::pexecute($ipsresult_stmt, [ 'id' => $result['id'] ]); $usedips = []; while ($ipsresultrow = $ipsresult_stmt->fetch(PDO::FETCH_ASSOC)) { $usedips[] = $ipsresultrow['id_ipandports']; } if (Request::post('send') == 'send') { try { // remove ssl ip/ports if set is empty if (empty(Request::post('ssl_ipandport'))) { $_POST['remove_ssl_ipandport'] = true; } Domains::getLocal($userinfo, Request::postAll())->update(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { if (Settings::Get('panel.allow_domain_change_customer') == '1') { $customers = []; $result_customers_stmt = Database::prepare(" SELECT `customerid`, `loginname`, `name`, `firstname`, `company` FROM `" . TABLE_PANEL_CUSTOMERS . "` WHERE ( (`subdomains_used` + :subdomains <= `subdomains` OR `subdomains` = '-1' ) AND (`emails_used` + :emails <= `emails` OR `emails` = '-1' ) AND (`email_forwarders_used` + :forwarders <= `email_forwarders` OR `email_forwarders` = '-1' ) AND (`email_accounts_used` + :accounts <= `email_accounts` OR `email_accounts` = '-1' ) " . ($userinfo['customers_see_all'] ? '' : " AND `adminid` = :adminid ") . ") OR `customerid` = :customerid ORDER BY `name` ASC "); $params = [ 'subdomains' => $subdomains, 'emails' => $emails, 'forwarders' => $email_forwarders, 'accounts' => $email_accounts, 'customerid' => $result['customerid'] ]; if ($userinfo['customers_see_all'] == '0') { $params['adminid'] = $userinfo['adminid']; } Database::pexecute($result_customers_stmt, $params); while ($row_customer = $result_customers_stmt->fetch(PDO::FETCH_ASSOC)) { $customers[$row_customer['customerid']] = User::getCorrectFullUserDetails($row_customer) . ' (' . $row_customer['loginname'] . ')'; } } else { $customer_stmt = Database::prepare(" SELECT `customerid`, `loginname`, `name`, `firstname`, `company` FROM `" . TABLE_PANEL_CUSTOMERS . "` WHERE `customerid` = :customerid "); $customer = Database::pexecute_first($customer_stmt, [ 'customerid' => $result['customerid'] ]); $result['customername'] = User::getCorrectFullUserDetails($customer); } if ($userinfo['customers_see_all'] == '1') { if (Settings::Get('panel.allow_domain_change_admin') == '1') { $admins = []; $result_admins_stmt = Database::prepare(" SELECT `adminid`, `loginname`, `name` FROM `" . TABLE_PANEL_ADMINS . "` WHERE (`domains_used` < `domains` OR `domains` = '-1') OR `adminid` = :adminid ORDER BY `name` ASC "); Database::pexecute($result_admins_stmt, [ 'adminid' => $result['adminid'] ]); while ($row_admin = $result_admins_stmt->fetch(PDO::FETCH_ASSOC)) { $admins[$row_admin['adminid']] = User::getCorrectFullUserDetails($row_admin) . ' (' . $row_admin['loginname'] . ')'; } } else { $admin_stmt = Database::prepare(" SELECT `adminid`, `loginname`, `name` FROM `" . TABLE_PANEL_ADMINS . "` WHERE `adminid` = :adminid "); $admin = Database::pexecute_first($admin_stmt, [ 'adminid' => $result['adminid'] ]); $result['adminname'] = User::getCorrectFullUserDetails($admin) . ' (' . $admin['loginname'] . ')'; } } $domains = [ 0 => lng('domains.noaliasdomain') ]; $result_domains_stmt = Database::prepare(" SELECT `d`.`id`, `d`.`domain` FROM `" . TABLE_PANEL_DOMAINS . "` `d`, `" . TABLE_PANEL_CUSTOMERS . "` `c` WHERE `d`.`aliasdomain` IS NULL AND `d`.`parentdomainid` = '0' AND `d`.`id` <> :id AND `c`.`standardsubdomain`<>`d`.`id` AND `d`.`customerid` = :customerid AND `c`.`customerid`=`d`.`customerid` ORDER BY `d`.`domain` ASC "); Database::pexecute($result_domains_stmt, [ 'id' => $result['id'], 'customerid' => $result['customerid'] ]); while ($row_domain = $result_domains_stmt->fetch(PDO::FETCH_ASSOC)) { $domains[$row_domain['id']] = $idna_convert->decode($row_domain['domain']); } if ($userinfo['ip'] == "-1") { $result_ipsandports_stmt = Database::query(" SELECT `id`, `ip`, `port` FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE `ssl`='0' ORDER BY `ip`, `port` ASC "); $result_ssl_ipsandports_stmt = Database::query(" SELECT `id`, `ip`, `port` FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE `ssl`='1' ORDER BY `ip`, `port` ASC "); } else { $admin_ip_stmt = Database::prepare(" SELECT `id`, `ip`, `port` FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE `id` = :ipid ORDER BY `ip`, `port` ASC "); $admin_ip = Database::pexecute_first($admin_ip_stmt, [ 'ipid' => $userinfo['ip'] ]); $result_ipsandports_stmt = Database::prepare(" SELECT `id`, `ip`, `port` FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE `ssl`='0' AND `ip` = :ipid ORDER BY `ip`, `port` ASC "); Database::pexecute($result_ipsandports_stmt, [ 'ipid' => $admin_ip['ip'] ]); $result_ssl_ipsandports_stmt = Database::prepare(" SELECT `id`, `ip`, `port` FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE `ssl`='1' AND `ip` = :ipid ORDER BY `ip`, `port` ASC "); Database::pexecute($result_ssl_ipsandports_stmt, [ 'ipid' => $admin_ip['ip'] ]); } $ipsandports = []; while ($row_ipandport = $result_ipsandports_stmt->fetch(PDO::FETCH_ASSOC)) { if (filter_var($row_ipandport['ip'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $row_ipandport['ip'] = '[' . $row_ipandport['ip'] . ']'; } $ipsandports[] = [ 'label' => $row_ipandport['ip'] . ':' . $row_ipandport['port'], 'value' => $row_ipandport['id'] ]; } $ssl_ipsandports = []; while ($row_ssl_ipandport = $result_ssl_ipsandports_stmt->fetch(PDO::FETCH_ASSOC)) { if (filter_var($row_ssl_ipandport['ip'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $row_ssl_ipandport['ip'] = '[' . $row_ssl_ipandport['ip'] . ']'; } $ssl_ipsandports[] = [ 'label' => $row_ssl_ipandport['ip'] . ':' . $row_ssl_ipandport['port'], 'value' => $row_ssl_ipandport['id'] ]; } // check that letsencrypt is not activated for wildcard domain if ($result['iswildcarddomain'] == '1') { $letsencrypt = 0; } // Fudge the result for ssl_redirect to hide the Let's Encrypt steps $result['temporary_ssl_redirect'] = $result['ssl_redirect']; $result['ssl_redirect'] = ($result['ssl_redirect'] == 0 ? 0 : 1); $openbasedir = [ 0 => lng('domain.docroot'), 1 => lng('domain.homedir'), 2 => lng('domain.docparent') ]; $serveraliasoptions = [ 0 => lng('domains.serveraliasoption_wildcard'), 1 => lng('domains.serveraliasoption_www'), 2 => lng('domains.serveraliasoption_none') ]; $subcanemaildomain = [ 0 => lng('admin.subcanemaildomain.never'), 1 => lng('admin.subcanemaildomain.choosableno'), 2 => lng('admin.subcanemaildomain.choosableyes'), 3 => lng('admin.subcanemaildomain.always') ]; $phpconfigs = []; $phpconfigs_result_stmt = Database::query(" SELECT c.*, fc.description as interpreter FROM `" . TABLE_PANEL_PHPCONFIGS . "` c LEFT JOIN `" . TABLE_PANEL_FPMDAEMONS . "` fc ON fc.id = c.fpmsettingid "); $c_allowed_configs = Customer::getCustomerDetail($result['customerid'], 'allowed_phpconfigs'); if (!empty($c_allowed_configs)) { $c_allowed_configs = json_decode($c_allowed_configs, true); } else { $c_allowed_configs = []; } while ($phpconfigs_row = $phpconfigs_result_stmt->fetch(PDO::FETCH_ASSOC)) { $disabled = !empty($c_allowed_configs) && !in_array($phpconfigs_row['id'], $c_allowed_configs); if (!$disabled) { if ((int)Settings::Get('phpfpm.enabled') == 1) { $phpconfigs[$phpconfigs_row['id']] = $phpconfigs_row['description'] . " [" . $phpconfigs_row['interpreter'] . "]"; } else { $phpconfigs[$phpconfigs_row['id']] = $phpconfigs_row['description']; } } } if (Settings::Get('panel.allow_domain_change_customer') != '1') { $result['customername'] .= ' (' . $customer['loginname'] . ')'; } $domain_edit_data = include_once dirname(__FILE__) . '/lib/formfields/admin/domains/formfield.domains_edit.php'; UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'domains', 'id' => $id]), 'formdata' => $domain_edit_data['domain_edit'], 'editid' => $id ]); } } } elseif ($action == 'jqGetCustomerPHPConfigs') { $customerid = intval(Request::post('customerid')); $allowed_phpconfigs = Customer::getCustomerDetail($customerid, 'allowed_phpconfigs'); echo !empty($allowed_phpconfigs) ? $allowed_phpconfigs : json_encode([]); exit(); } elseif ($action == 'jqSpeciallogfileNote') { $domainid = intval(Request::post('id')); $newval = intval(Request::post('newval')); try { $json_result = Domains::getLocal($userinfo, [ 'id' => $domainid ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if ($newval != $result['speciallogfile']) { echo json_encode(['changed' => true, 'info' => lng('admin.speciallogwarning')]); exit(); } echo 0; exit(); } elseif ($action == 'jqEmaildomainNote') { $domainid = intval(Request::post('id')); $newval = intval(Request::post('newval')); try { $json_result = Domains::getLocal($userinfo, [ 'id' => $domainid ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if ((int)$newval == 0 && $newval != $result['isemaildomain']) { echo json_encode(['changed' => true, 'info' => lng('admin.emaildomainwarning')]); exit(); } echo 0; exit(); } elseif ($action == 'import') { if (Request::post('send') == 'send') { $separator = Validate::validate(Request::post('separator'), 'separator'); $offset = (int)Validate::validate(Request::post('offset'), 'offset', "/[0-9]/i"); $file_name = $_FILES['file']['tmp_name']; $result = []; try { $bulk = new DomainBulkAction($file_name, $userinfo); $result = $bulk->doImport($separator, $offset); } catch (Exception $e) { Response::standardError('domain_import_error', $e->getMessage()); } if (!empty($bulk->getErrors())) { Response::dynamicError(implode("
", $bulk->getErrors())); } // update customer/admin counters User::updateCounters(false); Cronjob::inserttask(TaskId::REBUILD_VHOST); Cronjob::inserttask(TaskId::REBUILD_DNS); $result_str = $result['imported'] . ' / ' . $result['all'] . (!empty($result['note']) ? ' (' . $result['note'] . ')' : ''); Response::standardSuccess('domain_import_successfully', $result_str, [ 'filename' => $filename, 'action' => '', 'page' => 'domains' ]); } else { $domain_import_data = include_once dirname(__FILE__) . '/lib/formfields/admin/domains/formfield.domains_import.php'; UI::view('user/form-note.html.twig', [ 'formaction' => $linker->getLink(['section' => 'domains', 'page' => $page]), 'formdata' => $domain_import_data['domain_import'], // alert-box 'type' => 'info', 'alert_msg' => lng('domains.import_description') ]); } } elseif ($action == 'duplicate') { if (Request::post('send') == 'send') { try { Domains::getLocal($userinfo, Request::postAll())->duplicate(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page, 'searchfield' => 'd.domain_ace', 'searchtext' => Request::post('domain', "") ]); } else { Response::redirectTo($filename, [ 'page' => 'overview' ]); } } } elseif ($page == 'domainssleditor') { require_once __DIR__ . '/ssl_editor.php'; } elseif ($page == 'domaindnseditor' && Settings::Get('system.dnsenabled') == '1') { require_once __DIR__ . '/dns_editor.php'; } elseif ($page == 'sslcertificates') { require_once __DIR__ . '/ssl_certificates.php'; } elseif ($page == 'logfiles') { require_once __DIR__ . '/logfiles_viewer.php'; } ================================================ FILE: admin_index.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ const AREA = 'admin'; require __DIR__ . '/lib/init.php'; use Froxlor\Api\Commands\Admins as Admins; use Froxlor\Api\Commands\Froxlor as Froxlor; use Froxlor\CurrentUser; use Froxlor\Database\Database; use Froxlor\FroxlorLogger; use Froxlor\Language; use Froxlor\Settings; use Froxlor\System\Cronjob; use Froxlor\System\Crypt; use Froxlor\UI\Panel\UI; use Froxlor\UI\Request; use Froxlor\UI\Response; use Froxlor\Validate\Validate; $id = (int)Request::any('id'); if ($action == 'logout') { $log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "logged out"); unset($_SESSION['userinfo']); CurrentUser::setData(); session_destroy(); Response::redirectTo('index.php'); } elseif ($action == 'suback') { if (is_array(CurrentUser::getField('switched_user'))) { $result = CurrentUser::getData(); $result = $result['switched_user']; session_regenerate_id(true); CurrentUser::setData($result); $target = Request::get('target', 'index'); $redirect = "admin_" . $target . ".php"; if (!file_exists(\Froxlor\Froxlor::getInstallDir() . "/" . $redirect)) { $redirect = "admin_index.php"; } Response::redirectTo($redirect, null, true); } else { Response::dynamicError("Cannot change back - You've never switched to another user :-)"); } } if ($page == 'overview') { $log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "viewed admin_index"); $params = []; if ($userinfo['customers_see_all'] == '0') { $params = [ 'adminid' => $userinfo['adminid'] ]; } $overview_stmt = Database::prepare("SELECT COUNT(*) AS `number_customers`, SUM(case when `diskspace` > 0 then `diskspace` else 0 end) AS `diskspace_assigned`, SUM(`diskspace_used`) AS `diskspace_used`, SUM(case when `mysqls` > 0 then `mysqls` else 0 end) AS `mysqls_assigned`, SUM(`mysqls_used`) AS `mysqls_used`, SUM(case when `emails` > 0 then `emails` else 0 end) AS `emails_assigned`, SUM(`emails_used`) AS `emails_used`, SUM(case when `email_accounts` > 0 then `email_accounts` else 0 end) AS `email_accounts_assigned`, SUM(`email_accounts_used`) AS `email_accounts_used`, SUM(case when `email_forwarders` > 0 then `email_forwarders` else 0 end) AS `email_forwarders_assigned`, SUM(`email_forwarders_used`) AS `email_forwarders_used`, SUM(case when `email_quota` > 0 then `email_quota` else 0 end) AS `email_quota_assigned`, SUM(`email_quota_used`) AS `email_quota_used`, SUM(case when `ftps` > 0 then `ftps` else 0 end) AS `ftps_assigned`, SUM(`ftps_used`) AS `ftps_used`, SUM(case when `subdomains` > 0 then `subdomains` else 0 end) AS `subdomains_assigned`, SUM(`subdomains_used`) AS `subdomains_used`, SUM(case when `traffic` > 0 then `traffic` else 0 end) AS `traffic_assigned`, SUM(`traffic_used`) AS `traffic_used` FROM `" . TABLE_PANEL_CUSTOMERS . "`" . ($userinfo['customers_see_all'] ? '' : " WHERE `adminid` = :adminid ")); $overview = Database::pexecute_first($overview_stmt, $params); $userinfo['diskspace_bytes'] = ($userinfo['diskspace'] > -1) ? $userinfo['diskspace'] * 1024 : -1; $overview['diskspace_bytes'] = $overview['diskspace_assigned'] * 1024; $overview['diskspace_bytes_used'] = $overview['diskspace_used'] * 1024; $userinfo['traffic_bytes'] = ($userinfo['traffic'] > -1) ? $userinfo['traffic'] * 1024 : -1; $overview['traffic_bytes'] = $overview['traffic_assigned'] * 1024; $overview['traffic_bytes_used'] = $overview['traffic_used'] * 1024; $number_domains_stmt = Database::prepare(" SELECT COUNT(*) AS `number_domains` FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `parentdomainid`='0'" . ($userinfo['customers_see_all'] ? '' : " AND `adminid` = :adminid")); $number_domains = Database::pexecute_first($number_domains_stmt, $params); $overview['number_domains'] = $number_domains['number_domains']; if (Request::get('lookfornewversion') == 'yes' || (isset($lookfornewversion) && $lookfornewversion == 'yes')) { try { $json_result = Froxlor::getLocal($userinfo)->checkUpdate(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; $lookfornewversion_lable = $result['version']; $lookfornewversion_link = $result['link']; $lookfornewversion_message = $result['message']; $lookfornewversion_addinfo = $result['additional_info']; $isnewerversion = $result['isnewerversion']; } else { $lookfornewversion_lable = lng('admin.lookfornewversion.clickhere'); $lookfornewversion_link = htmlspecialchars($filename . '?page=' . urlencode($page) . '&lookfornewversion=yes'); $lookfornewversion_message = ''; $lookfornewversion_addinfo = ''; $isnewerversion = 0; } $cron_last_runs = Cronjob::getCronjobsLastRun(); $outstanding_tasks = Cronjob::getOutstandingTasks(); // additional sys-infos $meminfo = explode("\n", @file_get_contents("/proc/meminfo")); $memory = ""; for ($i = 0; $i < count($meminfo); ++$i) { if (substr($meminfo[$i], 0, 3) === "Mem") { $memory .= $meminfo[$i] . PHP_EOL; } } if (function_exists('sys_getloadavg')) { $loadArray = sys_getloadavg(); $load = number_format($loadArray[0], 2, '.', '') . " / " . number_format($loadArray[1], 2, '.', '') . " / " . number_format($loadArray[2], 2, '.', ''); } else { $load = @file_get_contents('/proc/loadavg'); if (!$load) { $load = lng('admin.noloadavailable'); } } $kernel = ''; if (function_exists('posix_uname')) { $kernel_nfo = posix_uname(); $kernel = $kernel_nfo['release'] . ' (' . $kernel_nfo['machine'] . ')'; } // Try to get the uptime // First: With exec (let's hope it's enabled for the Froxlor - vHost) $uptime_array = explode(" ", @file_get_contents("/proc/uptime")); $uptime = ''; if (is_array($uptime_array) && isset($uptime_array[0]) && is_numeric($uptime_array[0])) { // Some calculatioon to get a nicly formatted display $seconds = round($uptime_array[0], 0); $minutes = $seconds / 60; $hours = $minutes / 60; $days = floor($hours / 24); $hours = floor($hours - ($days * 24)); $minutes = floor($minutes - ($days * 24 * 60) - ($hours * 60)); $seconds = floor($seconds - ($days * 24 * 60 * 60) - ($hours * 60 * 60) - ($minutes * 60)); $uptime = "{$days}d, {$hours}h, {$minutes}m, {$seconds}s"; // Just cleanup unset($uptime_array, $seconds, $minutes, $hours, $days); } $sysinfo = [ 'webserver' => $_SERVER['SERVER_SOFTWARE'] ?? 'unknown', 'phpversion' => phpversion(), 'mysqlserverversion' => Database::getAttribute(PDO::ATTR_SERVER_VERSION), 'phpsapi' => strtoupper(@php_sapi_name()), 'hostname' => gethostname(), 'memory' => $memory, 'load' => $load, 'kernel' => $kernel, 'uptime' => $uptime ]; UI::twig()->addGlobal('userinfo', $userinfo); UI::view('user/index.html.twig', [ 'sysinfo' => $sysinfo, 'overview' => $overview, 'outstanding_tasks' => $outstanding_tasks, 'cron_last_runs' => $cron_last_runs ]); } elseif ($page == 'profile') { $languages = Language::getLanguages(); if (!empty($_POST)) { if (Request::post('send') == 'changepassword') { $old_password = Validate::validate(Request::post('old_password'), 'old password'); if (!Crypt::validatePasswordLogin($userinfo, $old_password, TABLE_PANEL_ADMINS, 'adminid')) { Response::standardError('oldpasswordnotcorrect'); } try { $new_password = Crypt::validatePassword(Request::post('new_password'), 'new password'); $new_password_confirm = Crypt::validatePassword(Request::post('new_password_confirm'), 'new password confirm'); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } if ($old_password == '') { Response::standardError([ 'stringisempty', 'changepassword.old_password' ]); } elseif ($new_password == '') { Response::standardError([ 'stringisempty', 'changepassword.new_password' ]); } elseif ($new_password_confirm == '') { Response::standardError([ 'stringisempty', 'changepassword.new_password_confirm' ]); } elseif ($new_password != $new_password_confirm) { Response::standardError('newpasswordconfirmerror'); } else { try { Admins::getLocal($userinfo, [ 'id' => $userinfo['adminid'], 'admin_password' => $new_password ])->update(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, 'changed password'); Response::redirectTo($filename); } } elseif (Request::post('send') == 'changetheme') { if (Settings::Get('panel.allow_theme_change_admin') == 1) { $theme = Validate::validate(Request::post('theme'), 'theme'); try { Admins::getLocal($userinfo, [ 'id' => $userinfo['adminid'], 'theme' => $theme ])->update(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "changed his/her theme to '" . $theme . "'"); } Response::redirectTo($filename); } elseif (Request::post('send') == 'changelanguage') { $def_language = Validate::validate(Request::post('def_language'), 'default language'); if (isset($languages[$def_language])) { try { Admins::getLocal($userinfo, [ 'id' => $userinfo['adminid'], 'def_language' => $def_language ])->update(); CurrentUser::setField('language', $def_language); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } } $log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "changed his/her default language to '" . $def_language . "'"); Response::redirectTo($filename); } } else { // change theme $default_theme = Settings::Get('panel.default_theme'); if ($userinfo['theme'] != '') { $default_theme = $userinfo['theme']; } $themes_avail = UI::getThemes(); // change language $default_lang = Settings::Get('panel.standardlanguage'); if ($userinfo['def_language'] != '') { $default_lang = $userinfo['def_language']; } UI::view('user/profile.html.twig', [ 'themes' => $themes_avail, 'default_theme' => $default_theme, 'languages' => $languages, 'default_lang' => $default_lang, ]); } } elseif ($page == 'send_error_report' && Settings::Get('system.allow_error_report_admin') == '1') { require_once __DIR__ . '/error_report.php'; } elseif ($page == 'apikeys' && Settings::Get('api.enabled') == 1) { require_once __DIR__ . '/api_keys.php'; } elseif ($page == '2fa' && Settings::Get('2fa.enabled') == 1) { require_once __DIR__ . '/2fa.php'; } ================================================ FILE: admin_ipsandports.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ const AREA = 'admin'; require __DIR__ . '/lib/init.php'; use Froxlor\Api\Commands\IpsAndPorts; use Froxlor\FroxlorLogger; use Froxlor\PhpHelper; use Froxlor\UI\Collection; use Froxlor\UI\HTML; use Froxlor\UI\Listing; use Froxlor\UI\Panel\UI; use Froxlor\UI\Request; use Froxlor\UI\Response; $id = (int)Request::any('id'); if (($page == 'ipsandports' || $page == 'overview') && $userinfo['change_serversettings'] == '1') { if ($action == '') { $log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "viewed admin_ipsandports"); try { $ipsandports_list_data = include_once dirname(__FILE__) . '/lib/tablelisting/admin/tablelisting.ipsandports.php'; $collection = (new Collection(IpsAndPorts::class, $userinfo)) ->withPagination($ipsandports_list_data['ipsandports_list']['columns'], $ipsandports_list_data['ipsandports_list']['default_sorting']); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } UI::view('user/table.html.twig', [ 'listing' => Listing::format($collection, $ipsandports_list_data, 'ipsandports_list'), 'actions_links' => [ [ 'href' => $linker->getLink(['section' => 'ipsandports', 'page' => $page, 'action' => 'add']), 'label' => lng('admin.ipsandports.add') ] ] ]); } elseif ($action == 'delete' && $id != 0) { try { $json_result = IpsAndPorts::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if (isset($result['id']) && $result['id'] == $id) { if (Request::post('send') == 'send') { try { IpsAndPorts::getLocal($userinfo, [ 'id' => $id ])->delete(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { HTML::askYesNo('admin_ip_reallydelete', $filename, [ 'id' => $id, 'page' => $page, 'action' => $action ], $result['ip'] . ':' . $result['port']); } } } elseif ($action == 'add') { if (Request::post('send') == 'send') { try { IpsAndPorts::getLocal($userinfo, Request::postAll())->add(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { $ipsandports_add_data = include_once dirname(__FILE__) . '/lib/formfields/admin/ipsandports/formfield.ipsandports_add.php'; UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'ipsandports']), 'formdata' => $ipsandports_add_data['ipsandports_add'] ]); } } elseif ($action == 'edit' && $id != 0) { try { $json_result = IpsAndPorts::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if ($result['ip'] != '') { if (Request::post('send') == 'send') { try { IpsAndPorts::getLocal($userinfo, Request::postAll())->update(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { $result = PhpHelper::htmlentitiesArray($result); $ipsandports_edit_data = include_once dirname(__FILE__) . '/lib/formfields/admin/ipsandports/formfield.ipsandports_edit.php'; UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'ipsandports', 'id' => $id]), 'formdata' => $ipsandports_edit_data['ipsandports_edit'], 'editid' => $id ]); } } } elseif ($action == 'jqCheckIP') { $ip = Request::post('ip', ''); if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6)) { echo json_encode('
'.lng('error.invalidip', [$ip]).'
'); } elseif (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE | FILTER_FLAG_NO_PRIV_RANGE)) { // returns notice if private network detected, so we can display it echo json_encode(lng('admin.ipsandports.ipnote')); } else { echo 0; } exit(); } } ================================================ FILE: admin_logger.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ const AREA = 'admin'; require __DIR__ . '/lib/init.php'; use Froxlor\Api\Commands\SysLog; use Froxlor\UI\Collection; use Froxlor\UI\HTML; use Froxlor\UI\Listing; use Froxlor\UI\Panel\UI; use Froxlor\UI\Request; use Froxlor\UI\Response; if ($page == 'log') { if ($action == '') { try { $syslog_list_data = include_once dirname(__FILE__) . '/lib/tablelisting/tablelisting.syslog.php'; $collection = (new Collection(SysLog::class, $userinfo)) ->withPagination($syslog_list_data['syslog_list']['columns'], $syslog_list_data['syslog_list']['default_sorting']); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } UI::view('user/table.html.twig', [ 'listing' => Listing::format($collection, $syslog_list_data, 'syslog_list'), 'actions_links' => ($userinfo['change_serversettings'] == '1' ? [ [ 'href' => $linker->getLink(['section' => 'logger', 'page' => 'log', 'action' => 'truncate']), 'label' => lng('logger.truncate'), 'icon' => 'fa-solid fa-recycle', 'class' => 'btn-warning' ] ] : []) ]); } elseif ($action == 'truncate' && $userinfo['change_serversettings'] == '1') { if (Request::post('send') == 'send') { try { SysLog::getLocal($userinfo, [ 'min_to_keep' => 10 ])->delete(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { HTML::askYesNo('logger_reallytruncate', $filename, [ 'page' => $page, 'action' => $action ], TABLE_PANEL_LOG); } } } ================================================ FILE: admin_message.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ const AREA = 'admin'; require __DIR__ . '/lib/init.php'; use Froxlor\Database\Database; use Froxlor\FroxlorLogger; use Froxlor\UI\Panel\UI; use Froxlor\UI\Request; use Froxlor\UI\Response; use Froxlor\User; $id = (int)Request::any('id'); $note_type = null; $note_msg = null; if ($page == 'message') { if ($action == '') { $log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, 'viewed panel_message'); if (Request::post('send') == 'send') { if (Request::post('recipient', -1) == 0 && $userinfo['customers_see_all'] == '1') { $log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, 'sending messages to admins'); $result = Database::query('SELECT `name`, `email` FROM `' . TABLE_PANEL_ADMINS . "`"); } elseif (Request::post('recipient', -1) == 1) { if ($userinfo['customers_see_all'] == '1') { $log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, 'sending messages to ALL customers'); $result = Database::query('SELECT `firstname`, `name`, `company`, `email` FROM `' . TABLE_PANEL_CUSTOMERS . "`"); } else { $log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, 'sending messages to customers'); $result = Database::prepare(' SELECT `firstname`, `name`, `company`, `email` FROM `' . TABLE_PANEL_CUSTOMERS . "` WHERE `adminid` = :adminid"); Database::pexecute($result, [ 'adminid' => $userinfo['adminid'] ]); } } else { Response::standardError('norecipientsgiven'); } $subject = Request::post('subject'); $message = wordwrap(Request::post('message'), 70); if (!empty($message)) { $mailcounter = 0; $mail->Body = $message; $mail->Subject = $subject; while ($row = $result->fetch(PDO::FETCH_ASSOC)) { $row['firstname'] = isset($row['firstname']) ? $row['firstname'] : ''; $row['company'] = isset($row['company']) ? $row['company'] : ''; $mail->AddAddress($row['email'], User::getCorrectUserSalutation([ 'firstname' => $row['firstname'], 'name' => $row['name'], 'company' => $row['company'] ])); $mail->From = $userinfo['email']; $mail->FromName = (isset($userinfo['firstname']) ? $userinfo['firstname'] . ' ' : '') . $userinfo['name']; if (!$mail->Send()) { if ($mail->ErrorInfo != '') { $mailerr_msg = $mail->ErrorInfo; } else { $mailerr_msg = $row['email']; } $log->logAction(FroxlorLogger::ADM_ACTION, LOG_ERR, 'Error sending mail: ' . $mailerr_msg); Response::standardError('errorsendingmail', $row['email']); } $mailcounter++; $mail->ClearAddresses(); } Response::redirectTo($filename, [ 'page' => $page, 'action' => 'showsuccess', 'sentitems' => $mailcounter ]); } else { Response::standardError('nomessagetosend'); } } } elseif ($action == 'showsuccess') { $sentitems = Request::get('sentitems', 0); if ($sentitems == 0) { $note_type = 'info'; $note_msg = lng('message.norecipients'); } else { $note_type = 'success'; $note_msg = lng('message.success', [$sentitems]); } } $recipients = []; if ($userinfo['customers_see_all'] == '1') { $recipients[0] = lng('panel.reseller'); } $recipients[1] = lng('panel.customer'); $messages_add_data = include_once dirname(__FILE__) . '/lib/formfields/admin/messages/formfield.messages_add.php'; UI::view('user/form-note.html.twig', [ 'formaction' => $linker->getLink(['section' => 'message', 'action' => '']), 'formdata' => $messages_add_data['messages_add'], 'actions_links' => ($userinfo['change_serversettings'] == '1' ? [ [ 'href' => $linker->getLink([ 'section' => 'settings', 'page' => 'overview', 'part' => 'system', 'em' => 'system_mail_use_smtp' ]), 'label' => lng('admin.smtpsettings'), 'icon' => 'fa-solid fa-gears', 'class' => 'btn-outline-secondary' ] ] : []), // alert-box 'type' => $note_type, 'alert_msg' => $note_msg ]); } ================================================ FILE: admin_mysqlserver.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ const AREA = 'admin'; require __DIR__ . '/lib/init.php'; use Froxlor\Api\Commands\MysqlServer; use Froxlor\FroxlorLogger; use Froxlor\PhpHelper; use Froxlor\UI\Collection; use Froxlor\UI\HTML; use Froxlor\UI\Listing; use Froxlor\UI\Panel\UI; use Froxlor\UI\Request; use Froxlor\UI\Response; $id = (int)Request::any('id'); if (($page == 'mysqlserver' || $page == 'overview') && $userinfo['change_serversettings'] == '1') { if ($action == '') { $log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "viewed admin_mysqlserver"); try { $mysqlserver_list_data = include_once dirname(__FILE__) . '/lib/tablelisting/admin/tablelisting.mysqlserver.php'; $collection = (new Collection(MysqlServer::class, $userinfo)) ->withPagination($mysqlserver_list_data['mysqlserver_list']['columns'], $mysqlserver_list_data['mysqlserver_list']['default_sorting']); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } UI::view('user/table.html.twig', [ 'listing' => Listing::format($collection, $mysqlserver_list_data, 'mysqlserver_list'), 'actions_links' => [ [ 'href' => $linker->getLink(['section' => 'mysqlserver', 'page' => $page, 'action' => 'add']), 'label' => lng('admin.mysqlserver.add') ] ] ]); } elseif ($action == 'delete' && $id != 0) { try { $json_result = MysqlServer::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if (isset($result['id']) && $result['id'] == $id) { if (Request::post('send') == 'send') { try { MysqlServer::getLocal($userinfo, [ 'id' => $id ])->delete(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { HTML::askYesNo('admin_mysqlserver_reallydelete', $filename, [ 'id' => $id, 'page' => $page, 'action' => $action ], $result['caption'] . ' (' . $result['host'] . ')'); } } } elseif ($action == 'add') { if (Request::post('send') == 'send') { try { MysqlServer::getLocal($userinfo, Request::postAll())->add(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { $mysqlserver_add_data = include_once dirname(__FILE__) . '/lib/formfields/admin/mysqlserver/formfield.mysqlserver_add.php'; UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'mysqlserver']), 'formdata' => $mysqlserver_add_data['mysqlserver_add'] ]); } } elseif ($action == 'edit' && $id >= 0) { try { $json_result = MysqlServer::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if (isset($result['id']) && $result['id'] == $id) { if (Request::post('send') == 'send') { try { MysqlServer::getLocal($userinfo, Request::postAll())->update(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { $result = PhpHelper::htmlentitiesArray($result); $mysqlserver_edit_data = include_once dirname(__FILE__) . '/lib/formfields/admin/mysqlserver/formfield.mysqlserver_edit.php'; UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'mysqlserver', 'id' => $id]), 'formdata' => $mysqlserver_edit_data['mysqlserver_edit'], 'editid' => $id ]); } } } } ================================================ FILE: admin_opcacheinfo.php ================================================ * @author Janos Muzsi * @author Andrew Collington * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 * * Based on https://github.com/amnuts/opcache-gui, which is * licensed under the MIT licence, which can be viewed * online at https://acollington.mit-license.org/ */ const AREA = 'admin'; require __DIR__ . '/lib/init.php'; use Froxlor\FroxlorLogger; use Froxlor\UI\HTML; use Froxlor\UI\Panel\UI; use Froxlor\UI\Request; use Froxlor\UI\Response; if ($action == 'reset' && function_exists('opcache_reset') && $userinfo['change_serversettings'] == '1') { if (Request::post('send') == 'send') { opcache_reset(); $log->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "reset OPcache"); header('Location: ' . $linker->getLink([ 'section' => 'opcacheinfo', 'page' => 'showinfo' ])); exit(); } else { HTML::askYesNo('cache_reallydelete', $filename, [ 'page' => $page, 'action' => 'reset', ], '', [ 'section' => 'opcacheinfo', 'page' => 'showinfo' ]); } } if (!extension_loaded('Zend OPcache')) { Response::standardError('no_opcacheinfo'); } $ocEnabled = ini_get('opcache.enable'); if (empty($ocEnabled)) { Response::standardError('inactive_opcacheinfo'); } if ($page == 'showinfo' && $userinfo['change_serversettings'] == '1') { $time = time(); $log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "viewed OPcache info"); $opcache = (new \Amnuts\Opcache\Service())->getData(); UI::view('settings/opcacheinfo.html.twig', [ 'opcacheinfo' => [ 'version' => $opcache['version'], 'overview' => $opcache['overview'], 'files' => $opcache['files'], 'preload' => $opcache['preload'], 'directives' => $opcache['directives'], 'blacklist' => $opcache['blacklist'], 'functions' => $opcache['functions'], ] ]); } ================================================ FILE: admin_phpsettings.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ const AREA = 'admin'; require __DIR__ . '/lib/init.php'; use Froxlor\Api\Commands\FpmDaemons; use Froxlor\Api\Commands\PhpSettings; use Froxlor\Database\Database; use Froxlor\Froxlor; use Froxlor\UI\Collection; use Froxlor\UI\HTML; use Froxlor\UI\Listing; use Froxlor\UI\Panel\UI; use Froxlor\UI\Request; use Froxlor\UI\Response; $id = (int)Request::any('id'); if ($page == 'overview') { if ($action == '') { try { $phpconf_list_data = include_once dirname(__FILE__) . '/lib/tablelisting/admin/tablelisting.phpconfigs.php'; $collection = (new Collection(PhpSettings::class, $userinfo, ['with_subdomains' => true])) ->withPagination($phpconf_list_data['phpconf_list']['columns'], $phpconf_list_data['phpconf_list']['default_sorting']); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } UI::view('user/table.html.twig', [ 'listing' => Listing::format($collection, $phpconf_list_data, 'phpconf_list'), 'actions_links' => (bool)$userinfo['change_serversettings'] ? [ [ 'href' => $linker->getLink(['section' => 'phpsettings', 'page' => $page, 'action' => 'add']), 'label' => lng('admin.phpsettings.addnew') ] ] : [] ]); } if ($action == 'add') { if ((int)$userinfo['change_serversettings'] == 1) { if (Request::post('send') == 'send') { try { PhpSettings::getLocal($userinfo, Request::postAll())->add(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { if (file_exists(Froxlor::getInstallDir() . '/templates/misc/php/default.ini.php')) { include Froxlor::getInstallDir() . '/templates/misc/php/default.ini.php'; $result = [ 'phpsettings' => $phpini ]; } else { // use first php-config as fallback $result_stmt = Database::query("SELECT * FROM `" . TABLE_PANEL_PHPCONFIGS . "` WHERE `id` = 1"); $result = $result_stmt->fetch(PDO::FETCH_ASSOC); } $fpmconfigs = []; $configs = Database::query("SELECT * FROM `" . TABLE_PANEL_FPMDAEMONS . "` ORDER BY `description` ASC"); while ($row = $configs->fetch(PDO::FETCH_ASSOC)) { $fpmconfigs[$row['id']] = $row['description']; } $phpconfig_add_data = include_once dirname(__FILE__) . '/lib/formfields/admin/phpconfig/formfield.phpconfig_add.php'; UI::view('user/form-replacers.html.twig', [ 'formaction' => $linker->getLink(['section' => 'phpsettings']), 'formdata' => $phpconfig_add_data['phpconfig_add'], 'replacers' => $phpconfig_add_data['phpconfig_replacers'] ]); } } else { Response::standardError('nopermissionsorinvalidid'); } } if ($action == 'delete') { try { $json_result = PhpSettings::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if ($result['id'] != 0 && $result['id'] == $id && (int)$userinfo['change_serversettings'] == 1 && $id != 1) // cannot delete the default php.config { if (Request::post('send') == 'send') { try { PhpSettings::getLocal($userinfo, [ 'id' => $id ])->delete(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { HTML::askYesNo('phpsetting_reallydelete', $filename, [ 'id' => $id, 'page' => $page, 'action' => $action ], $result['description']); } } else { Response::standardError('nopermissionsorinvalidid'); } } if ($action == 'edit') { try { $json_result = PhpSettings::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if ($result['id'] != 0 && $result['id'] == $id && (int)$userinfo['change_serversettings'] == 1) { if (Request::post('send') == 'send') { try { PhpSettings::getLocal($userinfo, Request::postAll())->update(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { $fpmconfigs = []; $configs = Database::query("SELECT * FROM `" . TABLE_PANEL_FPMDAEMONS . "` ORDER BY `description` ASC"); while ($row = $configs->fetch(PDO::FETCH_ASSOC)) { $fpmconfigs[$row['id']] = $row['description']; } $phpconfig_edit_data = include_once dirname(__FILE__) . '/lib/formfields/admin/phpconfig/formfield.phpconfig_edit.php'; UI::view('user/form-replacers.html.twig', [ 'formaction' => $linker->getLink(['section' => 'phpsettings', 'id' => $id]), 'formdata' => $phpconfig_edit_data['phpconfig_edit'], 'replacers' => $phpconfig_edit_data['phpconfig_replacers'], 'editid' => $id ]); } } else { Response::standardError('nopermissionsorinvalidid'); } } } elseif ($page == 'fpmdaemons') { if ($action == '') { try { $fpmconf_list_data = include_once dirname(__FILE__) . '/lib/tablelisting/admin/tablelisting.fpmconfigs.php'; $collection = (new Collection(FpmDaemons::class, $userinfo)) ->withPagination($fpmconf_list_data['fpmconf_list']['columns'], $fpmconf_list_data['fpmconf_list']['default_sorting']); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } UI::view('user/table.html.twig', [ 'listing' => Listing::format($collection, $fpmconf_list_data, 'fpmconf_list'), 'actions_links' => (bool)$userinfo['change_serversettings'] ? [ [ 'href' => $linker->getLink(['section' => 'phpsettings', 'page' => $page, 'action' => 'add']), 'label' => lng('admin.fpmsettings.addnew') ] ] : [] ]); } if ($action == 'add') { if ((int)$userinfo['change_serversettings'] == 1) { if (Request::post('send') == 'send') { try { FpmDaemons::getLocal($userinfo, Request::postAll())->add(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { $fpmconfig_add_data = include_once dirname(__FILE__) . '/lib/formfields/admin/phpconfig/formfield.fpmconfig_add.php'; UI::view('user/form-replacers.html.twig', [ 'formaction' => $linker->getLink(['section' => 'phpsettings', 'page' => 'fpmdaemons']), 'formdata' => $fpmconfig_add_data['fpmconfig_add'], 'replacers' => $fpmconfig_add_data['fpmconfig_replacers'] ]); } } else { Response::standardError('nopermissionsorinvalidid'); } } if ($action == 'delete') { try { $json_result = FpmDaemons::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if ($id == 1) { Response::standardError('cannotdeletedefaultphpconfig'); } if ($result['id'] != 0 && $result['id'] == $id && (int)$userinfo['change_serversettings'] == 1 && $id != 1) // cannot delete the default php.config { if (Request::post('send') == 'send') { try { FpmDaemons::getLocal($userinfo, Request::postAll())->delete(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { HTML::askYesNo('fpmsetting_reallydelete', $filename, [ 'id' => $id, 'page' => $page, 'action' => $action ], $result['description']); } } else { Response::standardError('nopermissionsorinvalidid'); } } if ($action == 'edit') { try { $json_result = FpmDaemons::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if ($result['id'] != 0 && $result['id'] == $id && (int)$userinfo['change_serversettings'] == 1) { if (Request::post('send') == 'send') { try { FpmDaemons::getLocal($userinfo, Request::postAll())->update(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { $fpmconfig_edit_data = include_once dirname(__FILE__) . '/lib/formfields/admin/phpconfig/formfield.fpmconfig_edit.php'; UI::view('user/form-replacers.html.twig', [ 'formaction' => $linker->getLink(['section' => 'phpsettings', 'page' => 'fpmdaemons', 'id' => $id]), 'formdata' => $fpmconfig_edit_data['fpmconfig_edit'], 'replacers' => $fpmconfig_edit_data['fpmconfig_replacers'], 'editid' => $id ]); } } else { Response::standardError('nopermissionsorinvalidid'); } } } ================================================ FILE: admin_plans.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ const AREA = 'admin'; require __DIR__ . '/lib/init.php'; use Froxlor\Api\Commands\HostingPlans; use Froxlor\Api\Commands\MysqlServer; use Froxlor\Database\Database; use Froxlor\FroxlorLogger; use Froxlor\PhpHelper; use Froxlor\Settings; use Froxlor\UI\Collection; use Froxlor\UI\HTML; use Froxlor\UI\Listing; use Froxlor\UI\Panel\UI; use Froxlor\UI\Request; use Froxlor\UI\Response; $id = (int)Request::any('id'); if ($page == '' || $page == 'overview') { if ($action == '') { $log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "viewed admin_plans"); try { $plan_list_data = include_once dirname(__FILE__) . '/lib/tablelisting/admin/tablelisting.plans.php'; $collection = (new Collection(HostingPlans::class, $userinfo)) ->withPagination($plan_list_data['plan_list']['columns'], $plan_list_data['plan_list']['default_sorting']); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } UI::view('user/table.html.twig', [ 'listing' => Listing::format($collection, $plan_list_data, 'plan_list'), 'actions_links' => [ [ 'href' => $linker->getLink(['section' => 'plans', 'page' => $page, 'action' => 'add']), 'label' => lng('admin.plans.add') ] ] ]); } elseif ($action == 'delete' && $id != 0) { try { $json_result = HostingPlans::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if ($result['id'] != 0 && $result['id'] == $id && (int)$userinfo['adminid'] == $result['adminid']) { if (Request::post('send') == 'send') { try { HostingPlans::getLocal($userinfo, [ 'id' => $id ])->delete(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { HTML::askYesNo('plan_reallydelete', $filename, [ 'id' => $id, 'page' => $page, 'action' => $action ], $result['name']); } } else { Response::standardError('nopermissionsorinvalidid'); } } elseif ($action == 'add') { if (Request::post('send') == 'send') { try { HostingPlans::getLocal($userinfo, Request::postAll())->add(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { $mysql_servers = []; try { $result_json = MysqlServer::getLocal($userinfo)->listing(); $result_decoded = json_decode($result_json, true)['data']['list']; foreach ($result_decoded as $dbserver => $dbdata) { $mysql_servers[] = [ 'label' => $dbdata['caption'], 'value' => $dbserver ]; } } catch (Exception $e) { /* just none */ } $phpconfigs = []; $configs = Database::query(" SELECT c.*, fc.description as interpreter FROM `" . TABLE_PANEL_PHPCONFIGS . "` c LEFT JOIN `" . TABLE_PANEL_FPMDAEMONS . "` fc ON fc.id = c.fpmsettingid "); while ($row = $configs->fetch(PDO::FETCH_ASSOC)) { if ((int)Settings::Get('phpfpm.enabled') == 1) { $phpconfigs[] = [ 'label' => $row['description'] . " [" . $row['interpreter'] . "]", 'value' => $row['id'] ]; } else { $phpconfigs[] = [ 'label' => $row['description'], 'value' => $row['id'] ]; } } // dummy to avoid unknown variables $hosting_plans = null; $plans_add_data = include_once __DIR__ . '/lib/formfields/admin/plans/formfield.plans_add.php'; $cust_add_data = include_once __DIR__ . '/lib/formfields/admin/customer/formfield.customer_add.php'; // unset unneeded stuff unset($cust_add_data['customer_add']['sections']['section_a']); unset($cust_add_data['customer_add']['sections']['section_b']); unset($cust_add_data['customer_add']['sections']['section_cpre']); // merge $plans_add_data['plans_add']['sections'] = array_merge($plans_add_data['plans_add']['sections'], $cust_add_data['customer_add']['sections']); UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'plans']), 'formdata' => $plans_add_data['plans_add'] ]); } } elseif ($action == 'edit' && $id != 0) { try { $json_result = HostingPlans::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if ($result['name'] != '') { $result['value'] = json_decode($result['value'], true); $result = PhpHelper::htmlentitiesArray($result); foreach ($result['value'] as $index => $value) { $result[$index] = $value; } $result['allowed_phpconfigs'] = json_encode($result['allowed_phpconfigs']); if (Request::post('send') == 'send') { try { HostingPlans::getLocal($userinfo, Request::postAll())->update(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { $mysql_servers = []; try { $result_json = MysqlServer::getLocal($userinfo)->listing(); $result_decoded = json_decode($result_json, true)['data']['list']; foreach ($result_decoded as $dbserver => $dbdata) { $mysql_servers[] = [ 'label' => $dbdata['caption'], 'value' => $dbserver ]; } } catch (Exception $e) { /* just none */ } $phpconfigs = []; $configs = Database::query(" SELECT c.*, fc.description as interpreter FROM `" . TABLE_PANEL_PHPCONFIGS . "` c LEFT JOIN `" . TABLE_PANEL_FPMDAEMONS . "` fc ON fc.id = c.fpmsettingid "); while ($row = $configs->fetch(PDO::FETCH_ASSOC)) { if ((int)Settings::Get('phpfpm.enabled') == 1) { $phpconfigs[] = [ 'label' => $row['description'] . " [" . $row['interpreter'] . "]", 'value' => $row['id'] ]; } else { $phpconfigs[] = [ 'label' => $row['description'], 'value' => $row['id'] ]; } } $result['imap'] = $result['email_imap']; $result['pop3'] = $result['email_pop3']; // dummy to avoid unknown variables $result['loginname'] = null; $result['documentroot'] = null; $result['standardsubdomain'] = null; $result['deactivated'] = null; $result['def_language'] = null; $result['firstname'] = null; $result['gender'] = null; $result['company'] = null; $result['street'] = null; $result['zipcode'] = null; $result['city'] = null; $result['phone'] = null; $result['fax'] = null; $result['email'] = null; $result['customernumber'] = null; $result['custom_notes'] = null; $result['custom_notes_show'] = null; $result['api_allowed'] = null; $hosting_plans = null; $admin_select = []; $plans_edit_data = include_once __DIR__ . '/lib/formfields/admin/plans/formfield.plans_edit.php'; $cust_edit_data = include_once __DIR__ . '/lib/formfields/admin/customer/formfield.customer_edit.php'; // unset unneeded stuff unset($cust_edit_data['customer_edit']['sections']['section_a']); unset($cust_edit_data['customer_edit']['sections']['section_b']); unset($cust_edit_data['customer_edit']['sections']['section_cpre']); unset($cust_edit_data['customer_edit']['sections']['section_d']); // merge $plans_edit_data['plans_edit']['sections'] = array_merge($plans_edit_data['plans_edit']['sections'], $cust_edit_data['customer_edit']['sections']); UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'plans', 'id' => $id]), 'formdata' => $plans_edit_data['plans_edit'], 'editid' => $id ]); } } } elseif ($action == 'jqGetPlanValues') { $planid = (int)Request::any('planid', 0); try { $json_result = HostingPlans::getLocal($userinfo, [ 'id' => $planid ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; echo $result['value']; exit(); } } ================================================ FILE: admin_settings.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ use Froxlor\Api\Commands\Froxlor; use Froxlor\Cron\TaskId; use Froxlor\Database\Database; use Froxlor\Database\IntegrityCheck; use Froxlor\FroxlorLogger; use Froxlor\PhpHelper; use Froxlor\Settings; use Froxlor\System\Cronjob; use Froxlor\UI\Form; use Froxlor\UI\HTML; use Froxlor\UI\Listing; use Froxlor\UI\Panel\UI; use Froxlor\UI\Request; use Froxlor\UI\Response; use Froxlor\User; use PHPMailer\PHPMailer\PHPMailer; const AREA = 'admin'; require __DIR__ . '/lib/init.php'; if ($page == 'overview' && $userinfo['change_serversettings'] == '1') { $settings_data = PhpHelper::loadConfigArrayDir('./actions/admin/settings/'); Settings::loadSettingsInto($settings_data); if (Request::post('send') == 'send') { $_part = Request::get('part', ''); if ($_part == '') { $_part = Request::post('part', ''); } if ($_part != '') { if ($_part == 'all') { $settings_all = true; $settings_part = false; } else { $settings_all = false; $settings_part = true; } $only_enabledisable = false; } else { $settings_all = false; $settings_part = false; $only_enabledisable = true; } // check if the session timeout is too low #815 if (!empty(Request::post('session_sessiontimeout')) && intval(Request::post('session_sessiontimeout', 0)) < 60) { Response::standardError(['session_timeout', 'session_timeout_desc']); } try { if (Form::processForm($settings_data, Request::postAll(), [ 'filename' => $filename, 'action' => $action, 'page' => $page, 'part' => $_part, ], $_part, $settings_all, $settings_part, $only_enabledisable)) { $log->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "rebuild configfiles due to changed setting"); Cronjob::inserttask(TaskId::REBUILD_VHOST); // Using nameserver, insert a task which rebuilds the server config Cronjob::inserttask(TaskId::REBUILD_DNS); // cron.d file Cronjob::inserttask(TaskId::REBUILD_CRON); Response::standardSuccess('settingssaved', '', [ 'filename' => $filename, 'action' => $action, 'page' => $page ]); } } catch (Exception $e) { Response::dynamicError($e->getMessage(), $e->getCode()); } } else { $_part = Request::get('part', ''); if ($_part == '') { $_part = Request::post('part', ''); } $fields = Form::buildForm($settings_data, $_part); if ($_part == '' || $_part == 'all') { UI::view('settings/index.html.twig', ['fields' => $fields]); } else { $em = Request::any('em', ''); UI::view('settings/detailpart.html.twig', [ 'fields' => $fields, 'em' => $em, 'part' => $_part, // alert-box 'type' => 'warning', 'heading' => lng('dns.nis2note.title'), 'alert_msg' => lng('dns.nis2note.content') ]); } } } elseif ($page == 'phpinfo' && $userinfo['change_serversettings'] == '1') { ob_start(); phpinfo(); $phpinfo = [ 'phpinfo' => [] ]; if (preg_match_all('#(?:

(?:)?(.*?)(?:)?

)|(?:(.*?)\s*(?:(.*?)\s*(?:(.*?)\s*)?)?)#s', ob_get_clean(), $matches, PREG_SET_ORDER)) { foreach ($matches as $match) { $end = array_keys($phpinfo); $end = end($end); if (strlen($match[1])) { $phpinfo[$match[1]] = []; } elseif (isset($match[3])) { $phpinfo[$end][$match[2]] = isset($match[4]) ? [ $match[3], $match[4] ] : $match[3]; } else { $phpinfo[$end][] = $match[2]; } } } else { Response::standardError('error.no_phpinfo'); } UI::view('settings/phpinfo.html.twig', [ 'phpversion' => PHP_VERSION, 'phpinfo' => $phpinfo ]); } elseif ($page == 'rebuildconfigs' && $userinfo['change_serversettings'] == '1') { if (Request::post('send') == 'send') { $log->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "rebuild configfiles"); Cronjob::inserttask(TaskId::REBUILD_VHOST); Cronjob::inserttask(TaskId::CREATE_QUOTA); // Using nameserver, insert a task which rebuilds the server config Cronjob::inserttask(TaskId::REBUILD_DNS); // cron.d file Cronjob::inserttask(TaskId::REBUILD_CRON); Response::standardSuccess('rebuildingconfigs', '', [ 'filename' => 'admin_index.php' ]); } else { HTML::askYesNo('admin_configs_reallyrebuild', $filename, [ 'page' => $page ]); } } elseif ($page == 'updatecounters' && $userinfo['change_serversettings'] == '1') { if (Request::post('send') == 'send') { $log->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "updated resource-counters"); $updatecounters = User::updateCounters(true); UI::view('user/resource-counter.html.twig', [ 'counters' => $updatecounters ]); } else { HTML::askYesNo('admin_counters_reallyupdate', $filename, [ 'page' => $page ]); } } elseif ($page == 'wipecleartextmailpws' && $userinfo['change_serversettings'] == '1') { if (Request::post('send') == 'send') { $log->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "wiped all cleartext mail passwords"); Database::query("UPDATE `" . TABLE_MAIL_USERS . "` SET `password` = '';"); Database::query("UPDATE `" . TABLE_PANEL_SETTINGS . "` SET `value` = '0' WHERE `settinggroup` = 'system' AND `varname` = 'mailpwcleartext'"); Response::redirectTo($filename); } else { HTML::askYesNo('admin_cleartextmailpws_reallywipe', $filename, [ 'page' => $page ]); } } elseif ($page == 'wipequotas' && $userinfo['change_serversettings'] == '1') { if (Request::post('send') == 'send') { $log->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "wiped all mailquotas"); // Set the quota to 0 which means unlimited Database::query("UPDATE `" . TABLE_MAIL_USERS . "` SET `quota` = '0';"); Database::query("UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `email_quota_used` = '0'"); Response::redirectTo($filename); } else { HTML::askYesNo('admin_quotas_reallywipe', $filename, [ 'page' => $page ]); } } elseif ($page == 'enforcequotas' && $userinfo['change_serversettings'] == '1') { if (Request::post('send') == 'send') { // Fetch all accounts $result_stmt = Database::query("SELECT `quota`, `customerid` FROM `" . TABLE_MAIL_USERS . "`"); if (Database::num_rows() > 0) { $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `email_quota_used` = `email_quota_used` + :diff WHERE `customerid` = :customerid "); while ($array = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $difference = Settings::Get('system.mail_quota') - $array['quota']; Database::pexecute($upd_stmt, [ 'diff' => $difference, 'customerid' => $customerid ]); } } // Set the new quota $upd_stmt = Database::prepare(" UPDATE `" . TABLE_MAIL_USERS . "` SET `quota` = :quota "); Database::pexecute($upd_stmt, [ 'quota' => Settings::Get('system.mail_quota') ]); // Update the Customer, if the used quota is bigger than the allowed quota Database::query("UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `email_quota` = `email_quota_used` WHERE `email_quota` < `email_quota_used`"); $log->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, 'enforcing mailquota to all customers: ' . Settings::Get('system.mail_quota') . ' MB'); Response::redirectTo($filename); } else { HTML::askYesNo('admin_quotas_reallyenforce', $filename, [ 'page' => $page ]); } } elseif ($page == 'integritycheck' && $userinfo['change_serversettings'] == '1') { $integrity = new IntegrityCheck(); if (Request::post('send') == 'send') { $integrity->fixAll(); } elseif (Request::get('action') == "fix") { HTML::askYesNo('admin_integritycheck_reallyfix', $filename, [ 'page' => $page ]); } $integritycheck = []; foreach ($integrity->available as $id => $check) { $integritycheck[] = [ 'displayid' => $id + 1, 'result' => $integrity->$check(), 'checkdesc' => lng('integrity_check.' . $check) ]; } $integrity_list_data = include_once dirname(__FILE__) . '/lib/tablelisting/admin/tablelisting.integrity.php'; $collection = [ 'data' => $integritycheck, 'pagination' => [] ]; UI::view('user/table.html.twig', [ 'listing' => Listing::formatFromArray($collection, $integrity_list_data['integrity_list'], 'integrity_list'), 'actions_links' => [ [ 'href' => $linker->getLink(['section' => 'settings', 'page' => $page, 'action' => 'fix']), 'label' => lng('admin.integrityfix'), 'icon' => 'fa-solid fa-screwdriver-wrench', 'class' => 'btn-warning' ] ] ]); } elseif ($page == 'importexport' && $userinfo['change_serversettings'] == '1') { // check for json-stuff if (!extension_loaded('json')) { Response::standardError('jsonextensionnotfound'); } if (Request::get('action') == "export") { // export try { $json_result = Froxlor::getLocal($userinfo)->exportSettings(); $json_export = json_decode($json_result, true)['data']; } catch (Exception $e) { Response::dynamicError($e->getMessage()); } header('Content-disposition: attachment; filename=Froxlor_settings-' . \Froxlor\Froxlor::VERSION . '-' . \Froxlor\Froxlor::DBVERSION . '_' . date('d.m.Y') . '.json'); header('Content-type: application/json'); echo $json_export; exit(); } elseif (Request::get('action') == "import") { // import if (Request::post('send') == 'send') { // get uploaded file if (isset($_FILES["import_file"]["tmp_name"])) { $imp_content = file_get_contents($_FILES["import_file"]["tmp_name"]); try { Froxlor::getLocal($userinfo, [ 'json_str' => $imp_content ])->importSettings(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::standardSuccess('settingsimported', '', [ 'filename' => 'admin_settings.php' ]); } Response::dynamicError("Upload failed"); } } else { $settings_data = include_once dirname(__FILE__) . '/lib/formfields/admin/settings/formfield.settings_import.php'; UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'settings', 'page' => $page, 'action' => 'import']), 'formdata' => $settings_data['settings_import'], 'actions_links' => [ [ 'class' => 'btn-outline-primary', 'href' => $linker->getLink(['section' => 'settings', 'page' => 'overview']), 'label' => lng('admin.configfiles.overview'), 'icon' => 'fa-solid fa-grip' ], [ 'class' => 'btn-outline-secondary', 'href' => $linker->getLink(['section' => 'settings', 'page' => $page, 'action' => 'export']), 'label' => 'Download/export ' . lng('admin.serversettings'), 'icon' => 'fa-solid fa-file-import' ] ] ]); } } elseif ($page == 'testmail') { $note_type = 'info'; $note_msg = lng('admin.smtptestnote'); if (Request::post('send') == 'send') { $test_addr = Request::post('test_addr'); // Initialize the mailingsystem $testmail = new PHPMailer(true); $testmail->CharSet = "UTF-8"; if (Settings::Get('system.mail_use_smtp')) { $testmail->isSMTP(); $testmail->Host = Settings::Get('system.mail_smtp_host'); $testmail->SMTPAuth = Settings::Get('system.mail_smtp_auth') == '1'; $testmail->Username = Settings::Get('system.mail_smtp_user'); $testmail->Password = Settings::Get('system.mail_smtp_passwd'); if (Settings::Get('system.mail_smtp_usetls')) { $testmail->SMTPSecure = 'tls'; } else { $testmail->SMTPAutoTLS = false; } $testmail->Port = Settings::Get('system.mail_smtp_port'); } $_mailerror = false; if (PHPMailer::ValidateAddress(Settings::Get('panel.adminmail')) !== false) { // set return-to address and custom sender-name, see #76 $testmail->SetFrom(Settings::Get('panel.adminmail'), Settings::Get('panel.adminmail_defname')); if (Settings::Get('panel.adminmail_return') != '') { $testmail->AddReplyTo(Settings::Get('panel.adminmail_return'), Settings::Get('panel.adminmail_defname')); } try { $testmail->Subject = "Froxlor Test-Mail"; $mail_body = "Yay, this worked :)"; $testmail->AltBody = $mail_body; $testmail->MsgHTML(str_replace("\n", "
", $mail_body)); $testmail->AddAddress($test_addr); $testmail->Send(); } catch (\PHPMailer\PHPMailer\Exception $e) { $note_type = 'danger'; $note_msg = $e->getMessage(); $_mailerror = true; } catch (Exception $e) { $note_type = 'danger'; $note_msg = $e->getMessage(); $_mailerror = true; } if (!$_mailerror) { // success $mail->ClearAddresses(); Response::standardSuccess('testmailsent', '', [ 'filename' => 'admin_settings.php', 'page' => 'testmail' ]); } } else { // invalid sender e-mail $note_type = 'warning'; $note_msg = "Invalid sender e-mail address: " . Settings::Get('panel.adminmail'); } } $mailtest_add_data = include_once dirname(__FILE__) . '/lib/formfields/admin/settings/formfield.settings_mailtest.php'; UI::view('user/form-note.html.twig', [ 'formaction' => $linker->getLink(['section' => 'settings']), 'formdata' => $mailtest_add_data['mailtest'], 'actions_links' => ($userinfo['change_serversettings'] == '1' ? [ [ 'href' => $linker->getLink([ 'section' => 'settings', 'page' => 'overview', 'part' => 'system', 'em' => 'system_mail_use_smtp' ]), 'label' => lng('admin.smtpsettings'), 'icon' => 'fa-solid fa-gears', 'class' => 'btn-outline-secondary' ] ] : []), // alert-box 'type' => $note_type, 'alert_msg' => $note_msg ]); } elseif ($page == 'toggleSettingsMode') { if ($userinfo['change_serversettings'] == '1') { $cmode = Settings::Get('panel.settings_mode'); Settings::Set('panel.settings_mode', (int)(!(bool)$cmode)); } Response::redirectTo($filename); } ================================================ FILE: admin_templates.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ const AREA = 'admin'; require __DIR__ . '/lib/init.php'; use Froxlor\Database\Database; use Froxlor\FroxlorLogger; use Froxlor\Language; use Froxlor\PhpHelper; use Froxlor\Settings; use Froxlor\UI\HTML; use Froxlor\UI\Listing; use Froxlor\UI\Panel\UI; use Froxlor\UI\Request; use Froxlor\UI\Response; use Froxlor\Validate\Validate; use Froxlor\CurrentUser; $id = (int)Request::any('id'); $subjectid = intval(Request::any('subjectid')); $mailbodyid = intval(Request::any('mailbodyid')); $available_templates = [ 'createcustomer', 'pop_success', 'new_database_by_customer', 'new_ftpaccount_by_customer', 'password_reset' ]; // only show templates of features that are enabled #1191 if ((int)Settings::Get('system.report_enable') == 1) { array_push($available_templates, 'trafficmaxpercent', 'diskmaxpercent'); } if (Settings::Get('panel.sendalternativemail') == 1) { array_push($available_templates, 'pop_success_alternative'); } $file_templates = [ 'index_html', 'unconfigured_html' ]; $languages = Language::getLanguages(); if ($action == '') { // email templates $log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "viewed admin_templates"); $templates_array = []; $result_stmt = Database::prepare(" SELECT `id`, `language`, `varname` FROM `" . TABLE_PANEL_TEMPLATES . "` WHERE `adminid` = :adminid AND `templategroup`='mails' ORDER BY `language`, `varname` "); Database::pexecute($result_stmt, [ 'adminid' => $userinfo['adminid'] ]); while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $parts = []; preg_match('/^([a-z]([a-z_]+[a-z])*)_(mailbody|subject)$/', $row['varname'], $parts); $templates_array[$row['language']][$parts[1]][$parts[3]] = $row['id']; } $templates = []; foreach ($templates_array as $language => $template_defs) { foreach ($template_defs as $action => $email) { $templates[] = [ 'subjectid' => $email['subject'], 'mailbodyid' => $email['mailbody'], 'template' => lng('admin.templates.' . $action), 'language' => $language ]; } } $mail_actions_links = false; foreach ($languages as $language_file => $language_name) { $templates_done = []; $result_stmt = Database::prepare(" SELECT `varname` FROM `" . TABLE_PANEL_TEMPLATES . "` WHERE `adminid` = :adminid AND `language`= :lang AND `templategroup` = 'mails' AND `varname` LIKE '%_subject' "); Database::pexecute($result_stmt, [ 'adminid' => $userinfo['adminid'], 'lang' => $language_name ]); while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $templates_done[] = str_replace('_subject', '', $row['varname']); } if (count(array_diff($available_templates, $templates_done)) > 0) { $mail_actions_links = [ [ 'href' => $linker->getLink(['section' => 'templates', 'page' => $page, 'action' => 'add']), 'label' => lng('admin.templates.template_add') ] ]; } } $mailtpl_list_data = include_once dirname(__FILE__) . '/lib/tablelisting/admin/tablelisting.mailtemplates.php'; $collection_mail = [ 'data' => $templates, 'pagination' => [] ]; // filetemplates $result_stmt = Database::prepare(" SELECT `id`, `varname` FROM `" . TABLE_PANEL_TEMPLATES . "` WHERE `adminid` = :adminid AND `templategroup`='files'"); Database::pexecute($result_stmt, [ 'adminid' => $userinfo['adminid'] ]); $filetemplates = []; while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $filetemplates[] = [ 'id' => $row['id'], 'template' => lng('admin.templates.' . $row['varname']) ]; } $file_actions_links = false; if (Database::num_rows() != count($file_templates)) { $file_actions_links = [ [ 'href' => $linker->getLink([ 'section' => 'templates', 'page' => $page, 'action' => 'add', 'files' => 'files' ]), 'label' => lng('admin.templates.template_fileadd') ] ]; } $filetpl_list_data = include_once dirname(__FILE__) . '/lib/tablelisting/admin/tablelisting.filetemplates.php'; $collection_file = [ 'data' => $filetemplates, 'pagination' => [] ]; if ($mail_actions_links === false) { $mail_actions_links = []; } if ($file_actions_links === false) { $file_actions_links = []; } UI::view('user/table-tpl.html.twig', [ 'maillisting' => Listing::formatFromArray($collection_mail, $mailtpl_list_data['mailtpl_list'], 'mailtpl_list'), 'filelisting' => Listing::formatFromArray($collection_file, $filetpl_list_data['filetpl_list'], 'filetpl_list'), 'actions_links' => array_merge($mail_actions_links, $file_actions_links) ]); } elseif ($action == 'delete' && $subjectid != 0 && $mailbodyid != 0) { // email templates $result_stmt = Database::prepare(" SELECT `language`, `varname` FROM `" . TABLE_PANEL_TEMPLATES . "` WHERE `adminid` = :adminid AND `id` = :id"); Database::pexecute($result_stmt, [ 'adminid' => $userinfo['adminid'], 'id' => $subjectid ]); $result = $result_stmt->fetch(PDO::FETCH_ASSOC); if ($result['varname'] != '') { if (Request::post('send') == 'send') { $del_stmt = Database::prepare(" DELETE FROM `" . TABLE_PANEL_TEMPLATES . "` WHERE `adminid` = :adminid AND (`id` = :ida OR `id` = :idb)"); Database::pexecute($del_stmt, [ 'adminid' => $userinfo['adminid'], 'ida' => $subjectid, 'idb' => $mailbodyid ]); $log->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "deleted template '" . $result['language'] . ' - ' . lng('admin.templates.' . str_replace('_subject', '', $result['varname'])) . "'"); Response::redirectTo($filename, [ 'page' => $page ]); } else { HTML::askYesNo('admin_template_reallydelete', $filename, [ 'subjectid' => $subjectid, 'mailbodyid' => $mailbodyid, 'page' => $page, 'action' => $action ], $result['language'] . ' - ' . lng('admin.templates.' . str_replace('_subject', '', $result['varname']))); } } } elseif ($action == 'deletef' && $id != 0) { // file templates $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_TEMPLATES . "` WHERE `adminid` = :adminid AND `id` = :id"); Database::pexecute($result_stmt, [ 'adminid' => $userinfo['adminid'], 'id' => $id ]); if (Database::num_rows() > 0) { $row = $result_stmt->fetch(PDO::FETCH_ASSOC); if (Request::post('send') == 'send') { $del_stmt = Database::prepare(" DELETE FROM `" . TABLE_PANEL_TEMPLATES . "` WHERE `adminid` = :adminid AND `id` = :id"); Database::pexecute($del_stmt, [ 'adminid' => $userinfo['adminid'], 'id' => $id ]); $log->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "deleted template '" . lng('admin.templates.' . $row['varname']) . "'"); Response::redirectTo($filename, [ 'page' => $page ]); } else { HTML::askYesNo('admin_template_reallydelete', $filename, [ 'id' => $id, 'page' => $page, 'action' => $action ], lng('admin.templates.' . $row['varname'])); } } else { Response::standardError('templatenotfound'); } } elseif ($action == 'add') { if (Request::post('prepare') == 'prepare') { // email templates $language = htmlentities(Validate::validate(Request::post('language'), 'language', '/^[^\r\n\0"\']+$/', 'nolanguageselect')); if (!array_key_exists($language, $languages)) { Response::standardError('templatelanguageinvalid'); } $template = Validate::validate(Request::post('template'), 'template'); $result_stmt = Database::prepare(" SELECT COUNT(*) as def FROM `" . TABLE_PANEL_TEMPLATES . "` WHERE `adminid` = :adminid AND `language` = :lang AND `templategroup` = 'mails' AND `varname` LIKE :template "); $result = Database::pexecute_first($result_stmt, [ 'adminid' => $userinfo['adminid'], 'lang' => $language, 'template' => $template . '%' ]); if ($result && $result['def'] > 0) { Response::standardError('templatelanguagecombodefined'); } // set target language Language::setLanguage($language); $subject = lng('mails.' . $template . '.subject'); $body = str_replace('\n', "\n", lng('mails.' . $template . '.mailbody')); // re set language to user Language::setLanguage(CurrentUser::getField('def_language')); $template_add_data = include_once dirname(__FILE__) . '/lib/formfields/admin/templates/formfield.template_add.php'; UI::view('user/form-replacers.html.twig', [ 'formaction' => $linker->getLink(['section' => 'templates']), 'formdata' => $template_add_data['template_add'], 'replacers' => $template_add_data['template_replacers'] ]); } elseif (Request::post('send') == 'send' && empty(Request::post('filesend'))) { // email templates $language = htmlentities(Validate::validate(Request::post('language'), 'language', '/^[^\r\n\0"\']+$/', 'nolanguageselect')); if (!array_key_exists($language, $languages)) { Response::standardError('templatelanguageinvalid'); } $template = Validate::validate(Request::post('template'), 'template'); $subject = Validate::validate(Request::post('subject'), 'subject', '/^[^\r\n\0]+$/', 'nosubjectcreate'); $mailbody = Validate::validate(Request::post('mailbody'), 'mailbody', '/^[^\0]+$/', 'nomailbodycreate'); $templates = []; $result_stmt = Database::prepare(" SELECT `varname` FROM `" . TABLE_PANEL_TEMPLATES . "` WHERE `adminid` = :adminid AND `language` = :lang AND `templategroup` = 'mails' AND `varname` LIKE '%_subject'"); Database::pexecute($result_stmt, [ 'adminid' => $userinfo['adminid'], 'lang' => $language ]); while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $templates[] = str_replace('_subject', '', $row['varname']); } $templates = array_diff($available_templates, $templates); if (!in_array($template, $templates)) { Response::standardError('templatenotfound'); } else { $ins_stmt = Database::prepare(" INSERT INTO `" . TABLE_PANEL_TEMPLATES . "` SET `adminid` = :adminid, `language` = :lang, `templategroup` = 'mails', `varname` = :var, `value` = :value"); // mail-subject $ins_data = [ 'adminid' => $userinfo['adminid'], 'lang' => $language, 'var' => $template . '_subject', 'value' => $subject ]; Database::pexecute($ins_stmt, $ins_data); // mail-body $ins_data = [ 'adminid' => $userinfo['adminid'], 'lang' => $language, 'var' => $template . '_mailbody', 'value' => $mailbody ]; Database::pexecute($ins_stmt, $ins_data); $log->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "added template '" . $language . ' - ' . $template . "'"); Response::redirectTo($filename, [ 'page' => $page ]); } } elseif (Request::post('filesend') == 'filesend') { // file templates $template = Validate::validate(Request::post('template'), 'template'); $filecontent = Validate::validate(Request::post('filecontent'), 'filecontent', '/^[^\0]+$/', 'filecontentnotset'); $ins_stmt = Database::prepare(" INSERT INTO `" . TABLE_PANEL_TEMPLATES . "` SET `adminid` = :adminid, `language` = '', `templategroup` = 'files', `varname` = :var, `value` = :value"); $ins_data = [ 'adminid' => $userinfo['adminid'], 'var' => $template, 'value' => $filecontent ]; Database::pexecute($ins_stmt, $ins_data); $log->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "added template '" . $template . "'"); Response::redirectTo($filename, [ 'page' => $page ]); } elseif (empty(Request::get('files'))) { // email templates $add = false; $language_options = []; $template_options = []; foreach ($languages as $language_file => $language_name) { $templates = []; $result_stmt = Database::prepare(" SELECT `varname` FROM `" . TABLE_PANEL_TEMPLATES . "` WHERE `adminid` = :adminid AND `language` = :lang AND `templategroup` = 'mails' AND `varname` LIKE '%_subject'"); Database::pexecute($result_stmt, [ 'adminid' => $userinfo['adminid'], 'lang' => $language_name ]); while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $templates[] = str_replace('_subject', '', $row['varname']); } if (count(array_diff($available_templates, $templates)) > 0) { $add = true; $language_options[$language_file] = $language_name; $templates = array_diff($available_templates, $templates); foreach ($templates as $template) { $template_options[$template] = lng('admin.templates.' . $template); } } } if ($add) { UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'templates']), 'formdata' => [ 'title' => lng('admin.templates.template_add'), 'image' => 'fa-solid fa-plus', 'self_overview' => ['section' => 'templates', 'page' => 'email'], 'sections' => [ 'section_a' => [ 'title' => lng('admin.templates.template_add'), 'fields' => [ 'language' => [ 'label' => lng('login.language'), 'type' => 'select', 'select_var' => $language_options, 'selected' => $userinfo['language'] ], 'template' => [ 'label' => lng('admin.templates.action'), 'type' => 'select', 'select_var' => $template_options ], 'prepare' => [ 'type' => 'hidden', 'value' => 'prepare' ] ] ] ] ], 'editid' => $id ]); } else { Response::standardError('alltemplatesdefined'); } } else { // filetemplates $result_stmt = Database::prepare(" SELECT `id`, `varname` FROM `" . TABLE_PANEL_TEMPLATES . "` WHERE `adminid` = :adminid AND `templategroup`='files'"); Database::pexecute($result_stmt, [ 'adminid' => $userinfo['adminid'] ]); if (Database::num_rows() == count($file_templates)) { Response::standardError('alltemplatesdefined'); } else { $templatesdefined = []; $free_templates = []; while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $templatesdefined[] = $row['varname']; } foreach (array_diff($file_templates, $templatesdefined) as $template) { $free_templates[$template] = lng('admin.templates.' . $template); } $filetemplate_add_data = include_once dirname(__FILE__) . '/lib/formfields/admin/templates/formfield.filetemplate_add.php'; UI::view('user/form-replacers.html.twig', [ 'formaction' => $linker->getLink(['section' => 'templates']), 'formdata' => $filetemplate_add_data['filetemplate_add'], 'replacers' => $filetemplate_add_data['filetemplate_replacers'] ]); } } } elseif ($action == 'edit' && $subjectid != 0 && $mailbodyid != 0) { // email templates $result_stmt = Database::prepare(" SELECT `language`, `varname`, `value` FROM `" . TABLE_PANEL_TEMPLATES . "` WHERE `adminid` = :adminid AND `id` = :subjectid"); Database::pexecute($result_stmt, [ 'adminid' => $userinfo['adminid'], 'subjectid' => $subjectid ]); $result = $result_stmt->fetch(PDO::FETCH_ASSOC); if ($result['varname'] != '') { if (Request::post('send') == 'send') { $subject = Validate::validate(Request::post('subject'), 'subject', '/^[^\r\n\0]+$/', 'nosubjectcreate'); $mailbody = Validate::validate(Request::post('mailbody'), 'mailbody', '/^[^\0]+$/', 'nomailbodycreate'); $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_TEMPLATES . "` SET `value` = :value WHERE `adminid` = :adminid AND `id` = :id"); // subject Database::pexecute($upd_stmt, [ 'value' => $subject, 'adminid' => $userinfo['adminid'], 'id' => $subjectid ]); // same query but mailbody Database::pexecute($upd_stmt, [ 'value' => $mailbody, 'adminid' => $userinfo['adminid'], 'id' => $mailbodyid ]); $log->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "edited template '" . $result['varname'] . "'"); Response::redirectTo($filename, [ 'page' => $page ]); } else { $result = PhpHelper::htmlentitiesArray($result); $template_name = lng('admin.templates.' . str_replace('_subject', '', $result['varname'])); $subject = $result['value']; $result_stmt = Database::prepare(" SELECT `language`, `varname`, `value` FROM `" . TABLE_PANEL_TEMPLATES . "` WHERE `id` = :id"); Database::pexecute($result_stmt, [ 'id' => $mailbodyid ]); $result = $result_stmt->fetch(PDO::FETCH_ASSOC); $template = str_replace('_mailbody', '', $result['varname']); // don't escape the already escaped language-string so save up before htmlentities() $language = $result['language']; $result = PhpHelper::htmlentitiesArray($result); $mailbody = $result['value']; $template_edit_data = include_once dirname(__FILE__) . '/lib/formfields/admin/templates/formfield.template_edit.php'; UI::view('user/form-replacers.html.twig', [ 'formaction' => $linker->getLink(['section' => 'templates']), 'formdata' => $template_edit_data['template_edit'], 'replacers' => $template_edit_data['template_replacers'] ]); } } } elseif ($action == 'editf' && $id != 0) { // file templates $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_TEMPLATES . "` WHERE `adminid` = :adminid AND `id` = :id"); Database::pexecute($result_stmt, [ 'adminid' => $userinfo['adminid'], 'id' => $id ]); if (Database::num_rows() > 0) { $row = $result_stmt->fetch(PDO::FETCH_ASSOC); // filetemplates if (Request::post('filesend') == 'filesend') { $filecontent = Validate::validate(Request::post('filecontent'), 'filecontent', '/^[^\0]+$/', 'filecontentnotset'); $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_TEMPLATES . "` SET `value` = :value WHERE `adminid` = :adminid AND `id` = :id"); Database::pexecute($upd_stmt, [ 'value' => $filecontent, 'adminid' => $userinfo['adminid'], 'id' => $id ]); $log->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "edited template '" . $row['varname'] . "'"); Response::redirectTo($filename, [ 'page' => $page ]); } else { $row = PhpHelper::htmlentitiesArray($row); $filetemplate_edit_data = include_once dirname(__FILE__) . '/lib/formfields/admin/templates/formfield.filetemplate_edit.php'; UI::view('user/form-replacers.html.twig', [ 'formaction' => $linker->getLink(['section' => 'templates']), 'formdata' => $filetemplate_edit_data['filetemplate_edit'], 'replacers' => $filetemplate_edit_data['filetemplate_replacers'], 'editid' => $id ]); } } else { Response::standardError('templatenotfound'); } } ================================================ FILE: admin_traffic.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ const AREA = 'admin'; require __DIR__ . '/lib/init.php'; use Froxlor\Traffic\Traffic; use Froxlor\UI\Panel\UI; use Froxlor\UI\Request; use Froxlor\UI\Response; $range = Request::any('range', 'currentmonth'); if ($page == 'overview' || $page == 'customers') { try { $context = Traffic::getCustomerStats($userinfo, $range); } catch (Exception $e) { if ($e->getCode() === 405) { Response::dynamicError(lng('traffic.nocustomers')); } Response::dynamicError($e->getMessage()); } // pass metrics to the view UI::view('user/traffic.html.twig', $context); } ================================================ FILE: admin_updates.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ const AREA = 'admin'; require __DIR__ . '/lib/init.php'; use Froxlor\Cron\TaskId; use Froxlor\Froxlor; use Froxlor\FroxlorLogger; use Froxlor\Install\Preconfig; use Froxlor\Install\Update; use Froxlor\Settings; use Froxlor\System\Cronjob; use Froxlor\UI\Panel\UI; use Froxlor\UI\Request; use Froxlor\UI\Response; use Froxlor\User; if ($page == 'overview') { $log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "viewed admin_updates"); if (!Froxlor::isFroxlor()) { throw new Exception('SysCP/customized upgrades are not supported'); } if (Froxlor::hasDbUpdates() || Froxlor::hasUpdates()) { $successful_update = false; $message = ''; if (Request::post('send') == 'send') { if ((!empty(Request::post('update_preconfig')) && intval(Request::post('update_changesagreed', 0)) != 0) || empty(Request::post('update_preconfig'))) { include_once Froxlor::getInstallDir() . 'install/updatesql.php'; User::updateCounters(); Cronjob::inserttask(TaskId::REBUILD_VHOST); @chmod(Froxlor::getInstallDir() . '/lib/userdata.inc.php', 0400); UI::view('install/update.html.twig', [ 'checks' => Update::getUpdateTasks() ]); exit; } else { $message = '

You have to agree that you have read the update notifications.'; } } $current_version = Settings::Get('panel.version'); $current_db_version = Settings::Get('panel.db_version'); if (empty($current_db_version)) { $current_db_version = "0"; } $new_version = Froxlor::VERSION; $new_db_version = Froxlor::DBVERSION; if (Froxlor::VERSION != $current_version) { $replacer_currentversion = $current_version; $replacer_newversion = $new_version; } else { // show db version $replacer_currentversion = $current_db_version; $replacer_newversion = $new_db_version; } $ui_text = lng('update.update_information.part_a', [$replacer_newversion, $replacer_currentversion]); $ui_text .= lng('update.update_information.part_b'); $upd_formfield = [ 'updates' => [ 'title' => lng('update.update'), 'image' => 'fa-solid fa-download', 'description' => lng('update.description'), 'sections' => [], 'buttons' => [ [ 'label' => lng('update.proceed') ] ] ] ]; $preconfig = Preconfig::getPreConfig(); if (!empty($preconfig)) { $upd_formfield['updates']['sections'] = $preconfig; } UI::view('user/form-note.html.twig', [ 'formaction' => $linker->getLink(['section' => 'updates']), 'formdata' => $upd_formfield['updates'], // alert 'type' => !empty($message) ? 'danger' : 'info', 'alert_msg' => $ui_text . $message ]); } else { Response::standardSuccess('update.noupdatesavail', Settings::Get('system.update_channel') == 'testing' ? lng('serversettings.uc_testing') . ' ' : ''); } } ================================================ FILE: api.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ use Froxlor\Api\Api; use Froxlor\Api\Response; require __DIR__ . '/vendor/autoload.php'; require __DIR__ . '/lib/functions.php'; require __DIR__ . '/lib/tables.inc.php'; // set error-handler @set_error_handler([ '\\Froxlor\\Api\\Api', 'phpErrHandler' ]); // Return response try { echo (new Api)->formatMiddleware(@file_get_contents('php://input'))->handle(); } catch (Exception $e) { echo Response::jsonErrorResponse($e->getMessage(), $e->getCode()); } ================================================ FILE: api_keys.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ if (!defined('AREA')) { header("Location: index.php"); exit(); } use Froxlor\Database\Database; use Froxlor\FroxlorLogger; use Froxlor\UI\HTML; use Froxlor\UI\Listing; use Froxlor\UI\Panel\UI; use Froxlor\UI\Request; use Froxlor\UI\Response; // redirect if this customer has no permission for API usage if ($userinfo['adminsession'] == 0 && $userinfo['api_allowed'] == 0) { Response::redirectTo('customer_index.php'); } // redirect if this admin has no permission for API usage if ($userinfo['adminsession'] == 1 && $userinfo['api_allowed'] == 0) { Response::redirectTo('admin_index.php'); } // This file is being included in admin_index and customer_index // and therefore does not need to require lib/init.php $del_stmt = Database::prepare("DELETE FROM `" . TABLE_API_KEYS . "` WHERE id = :id"); $id = (int)Request::any('id'); // do the delete and then just show a success-message and the apikeys list again if ($action == 'delete' && $id > 0) { HTML::askYesNo('apikey_reallydelete', $filename, [ 'id' => $id, 'page' => $page, 'action' => 'deletesure' ], '', [ 'section' => 'index', 'page' => $page ]); } elseif (Request::post('send') == 'send' && $action == 'deletesure' && $id > 0) { $chk = (AREA == 'admin' && $userinfo['customers_see_all'] == '1') ? true : false; if (AREA == 'customer') { $chk_stmt = Database::prepare(" SELECT c.customerid FROM `" . TABLE_PANEL_CUSTOMERS . "` c LEFT JOIN `" . TABLE_API_KEYS . "` ak ON ak.customerid = c.customerid WHERE ak.`id` = :id AND c.`customerid` = :cid "); $chk = Database::pexecute_first($chk_stmt, [ 'id' => $id, 'cid' => $userinfo['customerid'] ]); } elseif (AREA == 'admin' && $userinfo['customers_see_all'] == '0') { $chk_stmt = Database::prepare(" SELECT a.adminid FROM `" . TABLE_PANEL_ADMINS . "` a LEFT JOIN `" . TABLE_API_KEYS . "` ak ON ak.adminid = a.adminid WHERE ak.`id` = :id AND a.`adminid` = :aid "); $chk = Database::pexecute_first($chk_stmt, [ 'id' => $id, 'aid' => $userinfo['adminid'] ]); } if ($chk !== false) { Database::pexecute($del_stmt, [ 'id' => $id ]); Response::standardSuccess('apikeys.apikey_removed', $id, [ 'filename' => $filename, 'page' => $page ]); } } elseif ($action == 'add') { if (Request::post('send') == 'send') { $user_passwd = Request::post('user_password'); if (empty($user_passwd)) { Response::dynamicError(lng('panel.noauthentication')); } if ($userinfo['adminsession']) { $table = "`" . TABLE_PANEL_ADMINS . "`"; $uid = 'adminid'; } else { $table = "`" . TABLE_PANEL_CUSTOMERS . "`"; $uid = 'customerid'; } if (\Froxlor\System\Crypt::validatePasswordLogin($userinfo, $user_passwd, $table, $uid)) { $ins_stmt = Database::prepare(" INSERT INTO `" . TABLE_API_KEYS . "` SET `apikey` = :key, `secret` = :secret, `adminid` = :aid, `customerid` = :cid, `valid_until` = '-1', `allowed_from` = '' "); // customer generates for himself, admins will see a customer-select-box later if (AREA == 'admin') { $cid = 0; } elseif (AREA == 'customer') { $cid = $userinfo['customerid']; } $key = hash('sha256', openssl_random_pseudo_bytes(64 * 64)); $secret = hash('sha512', openssl_random_pseudo_bytes(64 * 64 * 4)); Database::pexecute($ins_stmt, [ 'key' => $key, 'secret' => $secret, 'aid' => $userinfo['adminid'], 'cid' => $cid ]); Response::standardSuccess('apikeys.apikey_added', '', [ 'filename' => $filename, 'page' => $page ]); } else { Response::dynamicError(lng('panel.authenticationfailed')); } } HTML::askUserPasswd('apikey_reallyadd', $filename, [ 'id' => $id, 'page' => $page, 'action' => $action ], '', [ 'section' => 'index', 'page' => $page ]); exit; } $log->logAction(FroxlorLogger::USR_ACTION, LOG_NOTICE, "viewed api::api_keys"); // select all my (accessible) api-keys $keys_stmt_query = "SELECT ak.*, c.loginname, a.loginname as adminname FROM `" . TABLE_API_KEYS . "` ak LEFT JOIN `" . TABLE_PANEL_CUSTOMERS . "` c ON `c`.`customerid` = `ak`.`customerid` LEFT JOIN `" . TABLE_PANEL_ADMINS . "` a ON `a`.`adminid` = `ak`.`adminid` WHERE "; $qry_params = []; if (AREA == 'admin' && $userinfo['customers_see_all'] == '0') { // admin with only customer-specific permissions $keys_stmt_query .= "ak.adminid = :adminid "; $qry_params['adminid'] = $userinfo['adminid']; $fields = [ 'a.loginname' => lng('login.username') ]; } elseif (AREA == 'customer') { // customer-area $keys_stmt_query .= "ak.customerid = :cid "; $qry_params['cid'] = $userinfo['customerid']; $fields = [ 'c.loginname' => lng('login.username') ]; } else { // admin who can see all customers / reseller / admins $keys_stmt_query .= "1 "; $fields = [ 'a.loginname' => lng('login.username') ]; } //$keys_stmt_query .= $paging->getSqlWhere(true) . " " . $paging->getSqlOrderBy() . " " . $paging->getSqlLimit(); $keys_stmt = Database::prepare($keys_stmt_query); Database::pexecute($keys_stmt, $qry_params); $all_keys = $keys_stmt->fetchAll(PDO::FETCH_ASSOC); $apikeys_list_data = include_once dirname(__FILE__) . '/lib/tablelisting/tablelisting.apikeys.php'; $collection = [ 'data' => $all_keys, 'pagination' => [] ]; $tpl = 'user/table.html.twig'; UI::view($tpl, [ 'listing' => Listing::formatFromArray($collection, $apikeys_list_data['apikeys_list'], 'apikeys_list'), 'actions_links' => (int)$userinfo['api_allowed'] == 1 ? [ [ 'href' => $linker->getLink(['section' => 'index', 'page' => $page, 'action' => 'add']), 'label' => lng('apikeys.key_add') ] ] : null, ]); ================================================ FILE: bin/froxlor-cli ================================================ #!/usr/bin/env php * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ use Froxlor\Froxlor; use Symfony\Component\Console\Application; // validate correct php version if (version_compare("7.4.0", PHP_VERSION, ">=")) { die('Froxlor requires at least php-7.4. Please validate that your php-cli version is suitable.'); } // ensure that default timezone is set if (function_exists("date_default_timezone_set") && function_exists("date_default_timezone_get")) { @date_default_timezone_set(@date_default_timezone_get()); } require dirname(__DIR__) . '/vendor/autoload.php'; require dirname(__DIR__) . '/lib/tables.inc.php'; $application = new Application('froxlor-cli', Froxlor::getFullVersion()); // files that are no commands $fileIgnoreList = [ // Current non-command files 'CliCommand.php', 'index.html', 'install.functions.php', ]; // directory of commands to include $cmd_files = glob(Froxlor::getInstallDir() . '/lib/Froxlor/Cli/*.php'); // include and add commands foreach ($cmd_files as $cmdFile) { // check ignore-list if (!in_array(basename($cmdFile), $fileIgnoreList)) { // include class-file require $cmdFile; // create class-name including namespace $cmdClass = "\\Froxlor\\Cli\\" . substr(basename($cmdFile), 0, -4); // check whether it exists if (class_exists($cmdClass) && is_subclass_of($cmdClass, '\Symfony\Component\Console\Command\Command')) { // add to cli application $application->add(new $cmdClass()); } } } $application->run(); ================================================ FILE: build.xml ================================================ ================================================ FILE: composer.json ================================================ { "name": "froxlor/froxlor", "description": "The server administration software for your needs. Developed by experienced server administrators, this panel simplifies the effort of managing your hosting platform.", "keywords": [ "server", "administration", "php" ], "homepage": "https://www.froxlor.org", "license": "GPL-2.0-or-later", "authors": [ { "name": "Michael Kaufmann", "email": "team@froxlor.org", "role": "Lead Developer" } ], "support": { "email": "team@froxlor.org", "issues": "https://github.com/Froxlor/Froxlor/issues", "forum": "https://forum.froxlor.org/", "source": "https://github.com/Froxlor/Froxlor", "docs": "https://docs.froxlor.org/", "chat": "https://discord.froxlor.org/" }, "funding": [ { "type": "github", "url": "https://github.com/sponsors/d00p" } ], "require": { "php": "^7.4 || ^8.0", "ext-session": "*", "ext-ctype": "*", "ext-pdo": "*", "ext-pdo_mysql": "*", "ext-simplexml": "*", "ext-xml": "*", "ext-filter": "*", "ext-posix": "*", "ext-mbstring": "*", "ext-curl": "*", "ext-json": "*", "ext-openssl": "*", "ext-fileinfo": "*", "ext-gmp": "*", "ext-gd": "*", "phpmailer/phpmailer": "~6.0", "monolog/monolog": "^1.24", "robthree/twofactorauth": "^1.6", "froxlor/idna-convert-legacy": "^2.1", "voku/anti-xss": "^4.1", "twig/twig": "^3.3", "symfony/console": "^5.4", "pear/net_dns2": "^1.5", "amnuts/opcache-gui": "^3.4", "league/commonmark": "^2.4", "phpseclib/phpseclib": "~3.0" }, "require-dev": { "phpunit/phpunit": "^9", "ext-pcntl": "*", "phpcompatibility/php-compatibility": "*", "squizlabs/php_codesniffer": "*", "pdepend/pdepend": "^2.9", "sebastian/phpcpd": "^6.0", "phploc/phploc": "^7.0", "phpmd/phpmd": "^2.10", "phpunit/php-timer" : "^5", "phpstan/phpstan": "^1.8" }, "suggest": { "ext-bcmath": "*", "ext-zip": "*", "ext-gnupg": "*", "ext-apcu": "*", "ext-readline": "*" }, "config": { "platform": { "php": "7.4" } }, "autoload": { "psr-4": { "Froxlor\\": [ "lib/Froxlor" ] } }, "scripts": { "dev": [ "Composer\\Config::disableProcessTimeout", "npx concurrently -c \"#93c5fd,#fdba74\" \"php -S 127.0.0.1:8000\" \"npm run dev\" --names=server,vite" ], "post-install-cmd": "if [ -f ./vendor/bin/phpcs ]; then \"vendor/bin/phpcs\" --config-set installed_paths vendor/phpcompatibility/php-compatibility ; fi", "post-update-cmd" : "if [ -f ./vendor/bin/phpcs ]; then \"vendor/bin/phpcs\" --config-set installed_paths vendor/phpcompatibility/php-compatibility ; fi" } } ================================================ FILE: customer_domains.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ const AREA = 'customer'; require __DIR__ . '/lib/init.php'; use Froxlor\Api\Commands\SubDomains; use Froxlor\CurrentUser; use Froxlor\Database\Database; use Froxlor\Domain\Domain; use Froxlor\FileDir; use Froxlor\FroxlorLogger; use Froxlor\PhpHelper; use Froxlor\Settings; use Froxlor\UI\Collection; use Froxlor\UI\HTML; use Froxlor\UI\Listing; use Froxlor\UI\Panel\UI; use Froxlor\UI\Request; use Froxlor\UI\Response; use Froxlor\Validate\Validate; // redirect if this customer page is hidden via settings if (Settings::IsInList('panel.customer_hide_options', 'domains')) { Response::redirectTo('customer_index.php'); } $id = (int)Request::any('id'); if ($page == 'overview' || $page == 'domains') { if ($action == '') { $log->logAction(FroxlorLogger::USR_ACTION, LOG_INFO, "viewed customer_domains::domains"); $parentdomain_id = (int)Request::any('pid', '0'); try { $domain_list_data = include_once dirname(__FILE__) . '/lib/tablelisting/customer/tablelisting.domains.php'; $collection = (new Collection(SubDomains::class, $userinfo)) ->withPagination($domain_list_data['domain_list']['columns'], $domain_list_data['domain_list']['default_sorting']); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $actions_links = []; if (CurrentUser::canAddResource('subdomains')) { $actions_links[] = [ 'href' => $linker->getLink(['section' => 'domains', 'page' => 'domains', 'action' => 'add']), 'label' => lng('domains.subdomain_add') ]; } $actions_links[] = [ 'href' => \Froxlor\Froxlor::getDocsUrl() . 'user-guide/domains/', 'target' => '_blank', 'icon' => 'fa-solid fa-circle-info', 'class' => 'btn-outline-secondary' ]; $table_tpl = 'table.html.twig'; if ($collection->count() == 0) { $table_tpl = 'table-note.html.twig'; } UI::view('user/' . $table_tpl, [ 'listing' => Listing::format($collection, $domain_list_data, 'domain_list'), 'actions_links' => $actions_links, 'entity_info' => lng('domains.description'), // alert-box 'type' => 'warning', 'alert_msg' => lng('domains.nodomainsassignedbyadmin') ]); } elseif ($action == 'delete' && $id != 0) { try { $json_result = SubDomains::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; $alias_stmt = Database::prepare("SELECT COUNT(`id`) AS `count` FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `aliasdomain` = :aliasdomain"); $alias_check = Database::pexecute_first($alias_stmt, [ "aliasdomain" => $id ]); if (isset($result['parentdomainid']) && $result['parentdomainid'] != '0' && $alias_check['count'] == 0) { if (Request::post('send') == 'send') { try { SubDomains::getLocal($userinfo, Request::postAll())->delete(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { HTML::askYesNo('domains_reallydelete', $filename, [ 'id' => $id, 'page' => $page, 'action' => $action ], $idna_convert->decode($result['domain'])); } } else { Response::standardError('domains_cantdeletemaindomain'); } } elseif ($action == 'add') { if ($userinfo['subdomains_used'] < $userinfo['subdomains'] || $userinfo['subdomains'] == '-1') { if (Request::post('send') == 'send') { try { SubDomains::getLocal($userinfo, Request::postAll())->add(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { $stmt = Database::prepare("SELECT `id`, `domain`, `documentroot`, `ssl_redirect`,`isemaildomain`,`letsencrypt` FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `customerid` = :customerid AND `parentdomainid` = '0' AND `email_only` = '0' AND `deactivated` = '0' ORDER BY `domain` ASC"); Database::pexecute($stmt, [ "customerid" => $userinfo['customerid'] ]); $domains = []; while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $domains[$row['domain']] = $idna_convert->decode($row['domain']); } // check of there are any domains to be used if (count($domains) <= 0) { // no, possible direct URL access, redirect to overview Response::redirectTo($filename, [ 'page' => $page ]); } $aliasdomains[0] = lng('domains.noaliasdomain'); $domains_stmt = Database::prepare("SELECT `d`.`id`, `d`.`domain` FROM `" . TABLE_PANEL_DOMAINS . "` `d`, `" . TABLE_PANEL_CUSTOMERS . "` `c` WHERE `d`.`aliasdomain` IS NULL AND `d`.`id` <> `c`.`standardsubdomain` AND `d`.`parentdomainid` = '0' AND `d`.`customerid`=`c`.`customerid` AND `d`.`email_only`='0' AND `d`.`customerid`= :customerid ORDER BY `d`.`domain` ASC"); Database::pexecute($domains_stmt, [ "customerid" => $userinfo['customerid'] ]); while ($row_domain = $domains_stmt->fetch(PDO::FETCH_ASSOC)) { $aliasdomains[$row_domain['id']] = $idna_convert->decode($row_domain['domain']); } $redirectcode = []; if (Settings::Get('customredirect.enabled') == '1') { $codes = Domain::getRedirectCodesArray(); foreach ($codes as $rc) { $redirectcode[$rc['id']] = $rc['code'] . ' (' . lng('redirect_desc.' . $rc['desc']) . ')'; } } // check if we at least have one ssl-ip/port, #1179 $ssl_ipsandports = false; $ssl_ip_stmt = Database::prepare(" SELECT COUNT(*) as countSSL FROM `" . TABLE_PANEL_IPSANDPORTS . "` pip LEFT JOIN `" . TABLE_DOMAINTOIP . "` dti ON dti.id_ipandports = pip.id WHERE pip.`ssl`='1' "); Database::pexecute($ssl_ip_stmt); $resultX = $ssl_ip_stmt->fetch(PDO::FETCH_ASSOC); if (isset($resultX['countSSL']) && (int)$resultX['countSSL'] > 0) { $ssl_ipsandports = true; } $openbasedir = [ 0 => lng('domain.docroot'), 1 => lng('domain.homedir'), 2 => lng('domain.docparent') ]; $pathSelect = FileDir::makePathfield($userinfo['documentroot'], $userinfo['guid'], $userinfo['guid']); $phpconfigs = []; if (isset($userinfo['allowed_phpconfigs']) && !empty($userinfo['allowed_phpconfigs'])) { $allowed_cfg = json_decode($userinfo['allowed_phpconfigs'], JSON_OBJECT_AS_ARRAY); $phpconfigs_result_stmt = Database::query(" SELECT c.*, fc.description as interpreter FROM `" . TABLE_PANEL_PHPCONFIGS . "` c LEFT JOIN `" . TABLE_PANEL_FPMDAEMONS . "` fc ON fc.id = c.fpmsettingid WHERE c.id IN (" . implode(", ", $allowed_cfg) . ") "); while ($phpconfigs_row = $phpconfigs_result_stmt->fetch(PDO::FETCH_ASSOC)) { if ((int)Settings::Get('phpfpm.enabled') == 1) { $phpconfigs[$phpconfigs_row['id']] = $phpconfigs_row['description'] . " [" . $phpconfigs_row['interpreter'] . "]"; } else { $phpconfigs[$phpconfigs_row['id']] = $phpconfigs_row['description']; } } } $subdomain_add_data = include_once dirname(__FILE__) . '/lib/formfields/customer/domains/formfield.domains_add.php'; UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'domains']), 'formdata' => $subdomain_add_data['domain_add'] ]); } } } elseif ($action == 'edit' && $id != 0) { try { $json_result = SubDomains::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if (isset($result['customerid']) && $result['customerid'] == $userinfo['customerid']) { if ((int)$result['caneditdomain'] == 0) { Response::standardError('domaincannotbeedited', $result['domain']); } if (Request::post('send') == 'send') { try { SubDomains::getLocal($userinfo, Request::postAll())->update(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { $result['domain'] = $idna_convert->decode($result['domain']); $domains[0] = lng('domains.noaliasdomain'); // also check ip/port combination to be the same, #176 $domains_stmt = Database::prepare("SELECT `d`.`id`, `d`.`domain` FROM `" . TABLE_PANEL_DOMAINS . "` `d` , `" . TABLE_PANEL_CUSTOMERS . "` `c` , `" . TABLE_DOMAINTOIP . "` `dip` WHERE `d`.`aliasdomain` IS NULL AND `d`.`id` <> :id AND `c`.`standardsubdomain` <> `d`.`id` AND `d`.`parentdomainid` = '0' AND `d`.`customerid` = :customerid AND `c`.`customerid` = `d`.`customerid` AND `d`.`id` = `dip`.`id_domain` AND `dip`.`id_ipandports` IN (SELECT `id_ipandports` FROM `" . TABLE_DOMAINTOIP . "` WHERE `id_domain` = :id) GROUP BY `d`.`id`, `d`.`domain` ORDER BY `d`.`domain` ASC"); Database::pexecute($domains_stmt, [ "id" => $result['id'], "customerid" => $userinfo['customerid'] ]); while ($row_domain = $domains_stmt->fetch(PDO::FETCH_ASSOC)) { $domains[$row_domain['id']] = $idna_convert->decode($row_domain['domain']); } if (preg_match('/^https?\:\/\//', $result['documentroot']) && Validate::validateUrl($result['documentroot'])) { if (Settings::Get('panel.pathedit') == 'Dropdown') { $urlvalue = $result['documentroot']; $pathSelect = FileDir::makePathfield($userinfo['documentroot'], $userinfo['guid'], $userinfo['guid']); } else { $urlvalue = ''; $pathSelect = FileDir::makePathfield($userinfo['documentroot'], $userinfo['guid'], $userinfo['guid'], $result['documentroot'], true); } } else { $urlvalue = ''; $pathSelect = FileDir::makePathfield($userinfo['documentroot'], $userinfo['guid'], $userinfo['guid'], $result['documentroot']); } $redirectcode = []; if (Settings::Get('customredirect.enabled') == '1') { $def_code = Domain::getDomainRedirectId($id); $codes = Domain::getRedirectCodesArray(); foreach ($codes as $rc) { $redirectcode[$rc['id']] = $rc['code'] . ' (' . lng('redirect_desc.' . $rc['desc']) . ')'; } } // check if we at least have one ssl-ip/port, #1179 $ssl_ipsandports = false; $ssl_ip_stmt = Database::prepare(" SELECT COUNT(*) as countSSL FROM `" . TABLE_PANEL_IPSANDPORTS . "` pip LEFT JOIN `" . TABLE_DOMAINTOIP . "` dti ON dti.id_ipandports = pip.id WHERE `dti`.`id_domain` = :id_domain AND pip.`ssl`='1' "); Database::pexecute($ssl_ip_stmt, [ "id_domain" => $result['id'] ]); $resultX = $ssl_ip_stmt->fetch(PDO::FETCH_ASSOC); if (isset($resultX['countSSL']) && (int)$resultX['countSSL'] > 0) { $ssl_ipsandports = true; } // Fudge the result for ssl_redirect to hide the Let's Encrypt steps $result['temporary_ssl_redirect'] = $result['ssl_redirect']; $result['ssl_redirect'] = ($result['ssl_redirect'] == 0 ? 0 : 1); $openbasedir = [ 0 => lng('domain.docroot'), 1 => lng('domain.homedir'), 2 => lng('domain.docparent') ]; // create serveralias options $serveraliasoptions = []; $serveraliasoptions_selected = '2'; if ($result['iswildcarddomain'] == '1') { $serveraliasoptions_selected = '0'; } elseif ($result['wwwserveralias'] == '1') { $serveraliasoptions_selected = '1'; } $serveraliasoptions[0] = lng('domains.serveraliasoption_wildcard'); $serveraliasoptions[1] = lng('domains.serveraliasoption_www'); $serveraliasoptions[2] = lng('domains.serveraliasoption_none'); $ips_stmt = Database::prepare("SELECT `p`.`ip` AS `ip` FROM `" . TABLE_PANEL_IPSANDPORTS . "` `p` LEFT JOIN `" . TABLE_DOMAINTOIP . "` `dip` ON ( `dip`.`id_ipandports` = `p`.`id` ) WHERE `dip`.`id_domain` = :id_domain GROUP BY `p`.`ip`"); Database::pexecute($ips_stmt, [ "id_domain" => $result['id'] ]); $domainips = []; while ($rowip = $ips_stmt->fetch(PDO::FETCH_ASSOC)) { $domainips[] = ['item' => $rowip['ip']]; } $phpconfigs = []; if (isset($userinfo['allowed_phpconfigs']) && !empty($userinfo['allowed_phpconfigs'])) { $allowed_cfg = json_decode($userinfo['allowed_phpconfigs'], JSON_OBJECT_AS_ARRAY); $phpconfigs_result_stmt = Database::query(" SELECT c.*, fc.description as interpreter FROM `" . TABLE_PANEL_PHPCONFIGS . "` c LEFT JOIN `" . TABLE_PANEL_FPMDAEMONS . "` fc ON fc.id = c.fpmsettingid WHERE c.id IN (" . implode(", ", $allowed_cfg) . ") "); while ($phpconfigs_row = $phpconfigs_result_stmt->fetch(PDO::FETCH_ASSOC)) { if ((int)Settings::Get('phpfpm.enabled') == 1) { $phpconfigs[$phpconfigs_row['id']] = $phpconfigs_row['description'] . " [" . $phpconfigs_row['interpreter'] . "]"; } else { $phpconfigs[$phpconfigs_row['id']] = $phpconfigs_row['description']; } } } $alias_stmt = Database::prepare("SELECT COUNT(`id`) AS count FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `aliasdomain`= :aliasdomain"); $alias_check = Database::pexecute_first($alias_stmt, [ "aliasdomain" => $result['id'] ]); $alias_check = $alias_check['count']; $subdomain_edit_data = include_once dirname(__FILE__) . '/lib/formfields/customer/domains/formfield.domains_edit.php'; UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'domains', 'id' => $id]), 'formdata' => $subdomain_edit_data['domain_edit'], 'editid' => $id ]); } } else { Response::standardError('domains_canteditdomain'); } } elseif ($action == 'jqSpeciallogfileNote') { $domainid = intval(Request::post('id')); $newval = intval(Request::post('newval')); try { $json_result = SubDomains::getLocal($userinfo, [ 'id' => $domainid ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if ($newval != $result['speciallogfile']) { echo json_encode(['changed' => true, 'info' => lng('admin.speciallogwarning')]); exit(); } echo 0; exit(); } } elseif ($page == 'domainssleditor') { require_once __DIR__ . '/ssl_editor.php'; } elseif ($page == 'domaindnseditor' && $userinfo['dnsenabled'] == '1' && Settings::Get('system.dnsenabled') == '1') { require_once __DIR__ . '/dns_editor.php'; } elseif ($page == 'sslcertificates') { require_once __DIR__ . '/ssl_certificates.php'; } elseif ($page == 'logfiles') { require_once __DIR__ . '/logfiles_viewer.php'; } ================================================ FILE: customer_email.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ const AREA = 'customer'; require __DIR__ . '/lib/init.php'; use Froxlor\Api\Commands\EmailAccounts; use Froxlor\Api\Commands\EmailDomains; use Froxlor\Api\Commands\EmailForwarders; use Froxlor\Api\Commands\Emails; use Froxlor\Api\Commands\EmailSender; use Froxlor\CurrentUser; use Froxlor\Database\Database; use Froxlor\Froxlor; use Froxlor\FroxlorLogger; use Froxlor\PhpHelper; use Froxlor\Settings; use Froxlor\UI\Collection; use Froxlor\UI\HTML; use Froxlor\UI\Listing; use Froxlor\UI\Panel\UI; use Froxlor\UI\Request; use Froxlor\UI\Response; use Froxlor\Validate\Check; // redirect if this customer page is hidden via settings if (Settings::IsInList('panel.customer_hide_options', 'email') || $userinfo['emails'] == 0) { Response::redirectTo('customer_index.php'); } $id = (int)Request::any('id'); if ($page == 'overview' || $page == 'emails') { $result_stmt = Database::prepare(" SELECT COUNT(DISTINCT `domainid`) as maildomains FROM `" . TABLE_MAIL_VIRTUAL . "` WHERE `customerid`= :cid "); $domain_count = Database::pexecute_first($result_stmt, [ "cid" => $userinfo['customerid'] ]); if ($domain_count['maildomains'] && $domain_count['maildomains'] > 1) { try { $emaildomain_list_data = include_once dirname(__FILE__) . '/lib/tablelisting/customer/tablelisting.emails_overview.php'; $collection = (new Collection(EmailDomains::class, $userinfo)) ->withPagination($emaildomain_list_data['emaildomain_list']['columns'], $emaildomain_list_data['emaildomain_list']['default_sorting']); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $actions_links = []; if (CurrentUser::canAddResource('emails')) { $actions_links[] = [ 'href' => $linker->getLink(['section' => 'email', 'page' => 'email_domain', 'action' => 'add']), 'label' => lng('emails.emails_add') ]; } $actions_links[] = [ 'href' => Froxlor::getDocsUrl() . 'user-guide/emails/', 'target' => '_blank', 'icon' => 'fa-solid fa-circle-info', 'class' => 'btn-outline-secondary' ]; UI::view('user/table.html.twig', [ 'listing' => Listing::format($collection, $emaildomain_list_data, 'emaildomain_list'), 'actions_links' => $actions_links, ]); } else { // only emails for one domain -> show email address listing directly $page = 'email_domain'; } } if ($page == 'email_domain') { $email_domainid = Request::any('domainid', 0); if ($action == '') { $log->logAction(FroxlorLogger::USR_ACTION, LOG_INFO, "viewed customer_email::emails"); $sql_search = []; if ($email_domainid > 0) { $sql_search = ['sql_search' => ['m.domainid' => ['op' => '=', 'value' => $email_domainid]]]; } try { $email_list_data = include_once dirname(__FILE__) . '/lib/tablelisting/customer/tablelisting.emails.php'; $collection = (new Collection(Emails::class, $userinfo, $sql_search)) ->withPagination($email_list_data['email_list']['columns'], $email_list_data['email_list']['default_sorting'], ['domainid=' . $email_domainid]); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result_stmt = Database::prepare(" SELECT COUNT(`id`) as emaildomains FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `customerid`= :cid AND `isemaildomain` = '1' "); $result2 = Database::pexecute_first($result_stmt, [ "cid" => $userinfo['customerid'] ]); $emaildomains_count = $result2['emaildomains']; $actions_links = []; if ($email_domainid > 0) { $actions_links[] = [ 'class' => 'btn-outline-primary', 'href' => $linker->getLink([ 'section' => 'email', 'page' => 'emails', ]), 'label' => lng('emails.back_to_overview'), 'icon' => 'fa-solid fa-reply' ]; } if (CurrentUser::canAddResource('emails')) { $actions_links[] = [ 'href' => $linker->getLink(['section' => 'email', 'page' => 'email_domain', 'action' => 'add', 'domainid' => $email_domainid]), 'label' => lng('emails.emails_add') ]; } $actions_links[] = [ 'href' => Froxlor::getDocsUrl() . 'user-guide/emails/', 'target' => '_blank', 'icon' => 'fa-solid fa-circle-info', 'class' => 'btn-outline-secondary' ]; UI::view('user/table.html.twig', [ 'listing' => Listing::format($collection, $email_list_data, 'email_list'), 'actions_links' => $actions_links, 'entity_info' => lng('emails.description') ]); } elseif ($action == 'delete' && $id != 0) { try { $json_result = Emails::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if (isset($result['email']) && $result['email'] != '') { if (Request::post('send') == 'send') { try { Emails::getLocal($userinfo, [ 'id' => $id, 'delete_userfiles' => Request::post('delete_userfiles', 0) ])->delete(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { if ($result['popaccountid'] != '0') { $show_checkbox = true; } else { $show_checkbox = false; } HTML::askYesNoWithCheckbox('email_reallydelete', 'admin_customer_alsoremovemail', $filename, [ 'id' => $id, 'page' => $page, 'action' => $action ], $idna_convert->decode($result['email_full']), $show_checkbox); } } } elseif ($action == 'add') { if ($userinfo['emails_used'] < $userinfo['emails'] || $userinfo['emails'] == '-1') { if (Request::post('send') == 'send') { try { $json_result = Emails::getLocal($userinfo, Request::postAll())->add(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; Response::redirectTo($filename, [ 'page' => $page, 'action' => 'edit', 'id' => $result['id'] ]); } else { $result_stmt = Database::prepare("SELECT `id`, `domain`, `customerid` FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `customerid`= :cid AND `isemaildomain`='1' ORDER BY `domain_ace` ASC"); Database::pexecute($result_stmt, [ "cid" => $userinfo['customerid'] ]); $domains = []; $selected_domain = ""; while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { if ($email_domainid == $row['id']) { $selected_domain = $row['domain']; } $domains[$row['domain']] = $idna_convert->decode($row['domain']); } if (count($domains) > 0) { $email_add_data = include_once dirname(__FILE__) . '/lib/formfields/customer/email/formfield.emails_add.php'; if (Settings::Get('catchall.catchall_enabled') != '1') { unset($email_add_data['emails_add']['sections']['section_a']['fields']['iscatchall']); } UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'email']), 'formdata' => $email_add_data['emails_add'] ]); } else { Response::standardError('emails.noemaildomainaddedyet'); } } } else { Response::standardError('allresourcesused'); } } elseif ($action == 'edit' && $id != 0) { try { $json_result = Emails::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if (isset($result['email']) && $result['email'] != '') { if (Request::post('send') == 'send') { try { Emails::getLocal($userinfo, Request::postAll())->update(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } $result['email'] = $idna_convert->decode($result['email']); $result['email_full'] = $idna_convert->decode($result['email_full']); $result['destination'] = explode(' ', $result['destination']); uasort($result['destination'], 'strcasecmp'); $forwarders = []; $forwarders_count = 0; foreach ($result['destination'] as $dest_id => $destination) { $destination = $idna_convert->decode($destination); if ($destination != $result['email_full'] && $destination != '') { $forwarders[] = [ 'item' => $destination, 'href' => $linker->getLink([ 'section' => 'email', 'page' => 'forwarders', 'action' => 'delete', 'id' => $id, 'forwarderid' => $dest_id ]), 'label' => lng('panel.delete'), 'classes' => 'btn btn-sm btn-danger' ]; $forwarders_count++; } $result['destination'][$dest_id] = $destination; } $destinations_count = count($result['destination']); // allowed senders listing $senders = []; $senders_count = 0; if (Settings::Get('mail.enable_allow_sender') == '1') { try { $json_result = EmailSender::getLocal($userinfo, [ 'id' => $id ])->listing(); $sender_listing = json_decode($json_result, true)['data']; if ($sender_listing['count'] > 0) { foreach ($sender_listing['list'] as $sender) { $senders[] = [ 'item' => $sender['allowed_sender'], 'href' => $linker->getLink([ 'section' => 'email', 'page' => 'senders', 'action' => 'delete', 'id' => $id, 'senderid' => $sender['id'] ]), 'label' => lng('panel.delete'), 'classes' => 'btn btn-sm btn-danger' ]; $senders_count++; } } } catch (Exception $e) { // nothing } } $result = PhpHelper::htmlentitiesArray($result); $email_edit_data = include_once dirname(__FILE__) . '/lib/formfields/customer/email/formfield.emails_edit.php'; UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'email']), 'formdata' => $email_edit_data['emails_edit'], 'editid' => $id ]); } } } elseif ($page == 'accounts') { $email_domainid = Request::any('domainid', 0); if ($action == 'add' && $id != 0) { if ($userinfo['email_accounts'] == '-1' || ($userinfo['email_accounts_used'] < $userinfo['email_accounts'])) { try { $json_result = Emails::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if (Request::post('send') == 'send') { try { EmailAccounts::getLocal($userinfo, Request::postAll())->add(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => 'email_domain', 'domainid' => $email_domainid, 'action' => 'edit', 'id' => $id ]); } else { if (Check::checkMailAccDeletionState($result['email_full'])) { Response::standardError([ 'mailaccistobedeleted' ], $result['email_full']); } $result['email_full'] = $idna_convert->decode($result['email_full']); $result = PhpHelper::htmlentitiesArray($result); $quota = Settings::Get('system.mail_quota'); $account_add_data = include_once dirname(__FILE__) . '/lib/formfields/customer/email/formfield.emails_addaccount.php'; UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'email', 'id' => $id]), 'formdata' => $account_add_data['emails_addaccount'], 'actions_links' => [ [ 'class' => 'btn-secondary', 'href' => $linker->getLink([ 'section' => 'email', 'page' => 'email_domain', 'domainid' => $email_domainid, 'action' => 'edit', 'id' => $id ]), 'label' => lng('emails.emails_edit'), 'icon' => 'fa-solid fa-pen' ], [ 'class' => 'btn-secondary', 'href' => $linker->getLink([ 'section' => 'email', 'page' => 'email_domain', 'domainid' => $email_domainid ]), 'label' => lng('menue.email.emails'), 'icon' => 'fa-solid fa-envelope' ] ], ]); } } else { Response::standardError([ 'allresourcesused', 'allocatetoomuchquota' ], $quota); } } elseif ($action == 'changepw' && $id != 0) { try { $json_result = Emails::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if (isset($result['popaccountid']) && $result['popaccountid'] != '') { if (Request::post('send') == 'send') { try { EmailAccounts::getLocal($userinfo, Request::postAll())->update(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => 'email_domain', 'domainid' => $email_domainid, 'action' => 'edit', 'id' => $id ]); } else { $result['email_full'] = $idna_convert->decode($result['email_full']); $result = PhpHelper::htmlentitiesArray($result); $account_changepw_data = include_once dirname(__FILE__) . '/lib/formfields/customer/email/formfield.emails_accountchangepasswd.php'; UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'email', 'id' => $id]), 'formdata' => $account_changepw_data['emails_accountchangepasswd'], 'actions_links' => [ [ 'class' => 'btn-secondary', 'href' => $linker->getLink([ 'section' => 'email', 'page' => 'email_domain', 'domainid' => $email_domainid, 'action' => 'edit', 'id' => $id ]), 'label' => lng('emails.emails_edit'), 'icon' => 'fa-solid fa-pen' ], [ 'class' => 'btn-secondary', 'href' => $linker->getLink([ 'section' => 'email', 'page' => 'email_domain', 'domainid' => $email_domainid ]), 'label' => lng('menue.email.emails'), 'icon' => 'fa-solid fa-envelope' ] ], ]); } } } elseif ($action == 'changequota' && Settings::Get('system.mail_quota_enabled') == '1' && $id != 0) { try { $json_result = Emails::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if (isset($result['popaccountid']) && $result['popaccountid'] != '') { if (Request::post('send') == 'send') { try { EmailAccounts::getLocal($userinfo, Request::postAll())->update(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => 'email_domain', 'domainid' => $email_domainid, 'action' => 'edit', 'id' => $id ]); } else { $result['email_full'] = $idna_convert->decode($result['email_full']); $result = PhpHelper::htmlentitiesArray($result); $quota_edit_data = include_once dirname(__FILE__) . '/lib/formfields/customer/email/formfield.emails_accountchangequota.php'; UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'email', 'id' => $id]), 'formdata' => $quota_edit_data['emails_accountchangequota'], 'actions_links' => [ [ 'class' => 'btn-secondary', 'href' => $linker->getLink([ 'section' => 'email', 'page' => 'email_domain', 'domainid' => $email_domainid, 'action' => 'edit', 'id' => $id ]), 'label' => lng('emails.emails_edit'), 'icon' => 'fa-solid fa-pen' ], [ 'class' => 'btn-secondary', 'href' => $linker->getLink([ 'section' => 'email', 'page' => 'email_domain', 'domainid' => $email_domainid ]), 'label' => lng('menue.email.emails'), 'icon' => 'fa-solid fa-envelope' ] ], ]); } } } elseif ($action == 'delete' && $id != 0) { try { $json_result = Emails::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if (isset($result['popaccountid']) && $result['popaccountid'] != '') { if (Request::post('send') == 'send') { try { EmailAccounts::getLocal($userinfo, Request::postAll())->delete(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => 'email_domain', 'domainid' => $email_domainid, 'action' => 'edit', 'id' => $id ]); } else { HTML::askYesNoWithCheckbox('email_reallydelete_account', 'admin_customer_alsoremovemail', $filename, [ 'id' => $id, 'page' => $page, 'domainid' => $email_domainid, 'action' => $action ], $idna_convert->decode($result['email_full'])); } } } } elseif ($page == 'forwarders') { $email_domainid = Request::any('domainid', 0); if ($action == 'add' && $id != 0) { if ($userinfo['email_forwarders_used'] < $userinfo['email_forwarders'] || $userinfo['email_forwarders'] == '-1') { try { $json_result = Emails::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if (isset($result['email']) && $result['email'] != '') { if (Request::post('send') == 'send') { try { EmailForwarders::getLocal($userinfo, Request::postAll())->add(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => 'email_domain', 'domainid' => $email_domainid, 'action' => 'edit', 'id' => $id ]); } else { $result['email_full'] = $idna_convert->decode($result['email_full']); $result = PhpHelper::htmlentitiesArray($result); $forwarder_add_data = include_once dirname(__FILE__) . '/lib/formfields/customer/email/formfield.emails_addforwarder.php'; UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'email', 'id' => $id]), 'formdata' => $forwarder_add_data['emails_addforwarder'], 'actions_links' => [ [ 'class' => 'btn-secondary', 'href' => $linker->getLink([ 'section' => 'email', 'page' => 'email_domain', 'domainid' => $email_domainid, 'action' => 'edit', 'id' => $id ]), 'label' => lng('emails.emails_edit'), 'icon' => 'fa-solid fa-pen' ], [ 'class' => 'btn-secondary', 'href' => $linker->getLink([ 'section' => 'email', 'page' => 'email_domain', 'domainid' => $email_domainid ]), 'label' => lng('menue.email.emails'), 'icon' => 'fa-solid fa-envelope' ] ], ]); } } } else { Response::standardError('allresourcesused'); } } elseif ($action == 'delete' && $id != 0) { try { $json_result = Emails::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if (isset($result['destination']) && $result['destination'] != '') { $forwarderid = Request::any('forwarderid', 0); $result['destination'] = explode(' ', $result['destination']); if (isset($result['destination'][$forwarderid]) && $result['email'] != $result['destination'][$forwarderid]) { $forwarder = $result['destination'][$forwarderid]; if (Request::post('send') == 'send') { try { EmailForwarders::getLocal($userinfo, Request::postAll())->delete(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => 'email_domain', 'domainid' => $email_domainid, 'action' => 'edit', 'id' => $id ]); } else { HTML::askYesNo('email_reallydelete_forwarder', $filename, [ 'id' => $id, 'forwarderid' => $forwarderid, 'page' => $page, 'domainid' => $email_domainid, 'action' => $action ], $idna_convert->decode($result['email_full']) . ' -> ' . $idna_convert->decode($forwarder)); } } } } } elseif ($page == 'senders' && Settings::Get('mail.enable_allow_sender') == '1') { $email_domainid = Request::any('domainid', 0); if ($action == 'add' && $id != 0) { try { $json_result = Emails::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if (isset($result['email']) && $result['email'] != '') { if (Request::post('send') == 'send') { try { // build target-sender $allowed_sender = Request::post('allowed_sender', ''); $allowed_sender .= '@' . Request::post('allowed_domain', ''); $postdata = [ 'id' => $id, 'allowed_sender' => $allowed_sender ]; EmailSender::getLocal($userinfo, $postdata)->add(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => 'email_domain', 'domainid' => $email_domainid, 'action' => 'edit', 'id' => $id ]); } else { $result['email_full'] = $idna_convert->decode($result['email_full']); $result = PhpHelper::htmlentitiesArray($result); if (Settings::Get('mail.allow_external_domains') == '0') { $result_stmt = Database::prepare("SELECT `id`, `domain`, `customerid` FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `customerid`= :cid AND `isemaildomain`='1' ORDER BY `domain_ace` ASC "); Database::pexecute($result_stmt, [ "cid" => $userinfo['customerid'] ]); $domains = []; $selected_domain = ""; while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { if ($email_domainid == $row['id']) { $selected_domain = $row['domain']; } $domains[$row['domain']] = $idna_convert->decode($row['domain']); } if (count($domains) == 0) { Response::standardError('emails.noemaildomainaddedyet'); } } $sender_add_data = include_once dirname(__FILE__) . '/lib/formfields/customer/email/formfield.emails_addsender.php'; UI::view('user/form-note.html.twig', [ 'formaction' => $linker->getLink(['section' => 'email', 'id' => $id]), 'formdata' => $sender_add_data['emails_addsender'], 'actions_links' => [ [ 'class' => 'btn-secondary', 'href' => $linker->getLink([ 'section' => 'email', 'page' => 'email_domain', 'domainid' => $email_domainid, 'action' => 'edit', 'id' => $id ]), 'label' => lng('emails.emails_edit'), 'icon' => 'fa-solid fa-pen' ], [ 'class' => 'btn-secondary', 'href' => $linker->getLink([ 'section' => 'email', 'page' => 'email_domain', 'domainid' => $email_domainid ]), 'label' => lng('menue.email.emails'), 'icon' => 'fa-solid fa-envelope' ] ], // alert-box 'type' => 'info', 'alert_msg' => lng('emails.allowed_sender_info') ]); } } } elseif ($action == 'delete' && $id != 0) { try { $json_result = Emails::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if (!empty($result['popaccountid'])) { $senderid = Request::any('senderid', 0); if (Request::post('send') == 'send') { try { EmailSender::getLocal($userinfo, Request::postAll())->delete(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => 'email_domain', 'domainid' => $email_domainid, 'action' => 'edit', 'id' => $id ]); } else { $sel_stmt = Database::prepare(" SELECT s.`allowed_sender` FROM `" . TABLE_MAIL_SENDER_ALIAS . "` s LEFT JOIN `" . TABLE_MAIL_USERS . "` u ON u.username = s.email WHERE u.id = :popaccountid AND u.customerid = :cid AND s.id = :senderid "); $sender_data = Database::pexecute_first($sel_stmt, ['popaccountid' => $result['popaccountid'], 'cid' => $userinfo['customerid'], 'senderid' => (int)$senderid]); if ($sender_data) { HTML::askYesNo('email_reallydelete_sender', $filename, [ 'id' => $id, 'senderid' => $senderid, 'page' => $page, 'domainid' => $email_domainid, 'action' => $action ], $idna_convert->decode($result['email_full']) . ' -> ' . $sender_data['allowed_sender']); } Response::dynamicError('No such entity'); } } } } ================================================ FILE: customer_extras.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ const AREA = 'customer'; require __DIR__ . '/lib/init.php'; use Froxlor\Api\Commands\DataDump as DataDump; use Froxlor\Api\Commands\DirOptions as DirOptions; use Froxlor\Api\Commands\DirProtections as DirProtections; use Froxlor\Customer\Customer; use Froxlor\FileDir; use Froxlor\FroxlorLogger; use Froxlor\PhpHelper; use Froxlor\Settings; use Froxlor\UI\Collection; use Froxlor\UI\HTML; use Froxlor\UI\Listing; use Froxlor\UI\Panel\UI; use Froxlor\UI\Request; use Froxlor\UI\Response; // redirect if this customer page is hidden via settings if (Settings::IsInList('panel.customer_hide_options', 'extras')) { Response::redirectTo('customer_index.php'); } $id = (int)Request::any('id'); if ($page == 'overview' || $page == 'htpasswds') { // redirect if this customer sub-page is hidden via settings if (Settings::IsInList('panel.customer_hide_options', 'extras.directoryprotection')) { Response::redirectTo('customer_index.php'); } if ($action == '') { $log->logAction(FroxlorLogger::USR_ACTION, LOG_NOTICE, "viewed customer_extras::htpasswds"); $fields = [ 'username' => lng('login.username'), 'path' => lng('panel.path') ]; try { $htpasswd_list_data = include_once dirname(__FILE__) . '/lib/tablelisting/customer/tablelisting.htpasswd.php'; $collection = (new Collection(DirProtections::class, $userinfo)) ->withPagination($htpasswd_list_data['htpasswd_list']['columns'], $htpasswd_list_data['htpasswd_list']['default_sorting']); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $actions_links = []; $actions_links[] = [ 'href' => $linker->getLink(['section' => 'extras', 'page' => 'htpasswds', 'action' => 'add']), 'label' => lng('extras.directoryprotection_add') ]; $actions_links[] = [ 'href' => \Froxlor\Froxlor::getDocsUrl() . 'user-guide/extras/', 'target' => '_blank', 'icon' => 'fa-solid fa-circle-info', 'class' => 'btn-outline-secondary' ]; UI::view('user/table.html.twig', [ 'listing' => Listing::format($collection, $htpasswd_list_data, 'htpasswd_list'), 'actions_links' => $actions_links, 'entity_info' => lng('extras.description') ]); } elseif ($action == 'delete' && $id != 0) { try { $json_result = DirProtections::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if (isset($result['username']) && $result['username'] != '') { if (Request::post('send') == 'send') { try { DirProtections::getLocal($userinfo, Request::postAll())->delete(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { if (strpos($result['path'], $userinfo['documentroot']) === 0) { $result['path'] = str_replace($userinfo['documentroot'], "/", $result['path']); } HTML::askYesNo('extras_reallydelete', $filename, [ 'id' => $id, 'page' => $page, 'action' => $action ], $result['username'] . ' (' . $result['path'] . ')'); } } } elseif ($action == 'add') { if (Request::post('send') == 'send') { try { DirProtections::getLocal($userinfo, Request::postAll())->add(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { $pathSelect = FileDir::makePathfield($userinfo['documentroot'], $userinfo['guid'], $userinfo['guid']); $htpasswd_add_data = include_once dirname(__FILE__) . '/lib/formfields/customer/extras/formfield.htpasswd_add.php'; UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'extras']), 'formdata' => $htpasswd_add_data['htpasswd_add'] ]); } } elseif ($action == 'edit' && $id != 0) { try { $json_result = DirProtections::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if (isset($result['username']) && $result['username'] != '') { if (Request::post('send') == 'send') { try { DirProtections::getLocal($userinfo, Request::postAll())->update(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { if (strpos($result['path'], $userinfo['documentroot']) === 0) { $result['path'] = str_replace($userinfo['documentroot'], "/", $result['path']); } $result = PhpHelper::htmlentitiesArray($result); $htpasswd_edit_data = include_once dirname(__FILE__) . '/lib/formfields/customer/extras/formfield.htpasswd_edit.php'; UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'extras', 'id' => $id]), 'formdata' => $htpasswd_edit_data['htpasswd_edit'], 'editid' => $id ]); } } } } elseif ($page == 'htaccess') { // redirect if this customer sub-page is hidden via settings if (Settings::IsInList('panel.customer_hide_options', 'extras.pathoptions')) { Response::redirectTo('customer_index.php'); } if ($action == '') { $log->logAction(FroxlorLogger::USR_ACTION, LOG_NOTICE, "viewed customer_extras::htaccess"); $cperlenabled = Customer::customerHasPerlEnabled($userinfo['customerid']); try { $htaccess_list_data = include_once dirname(__FILE__) . '/lib/tablelisting/customer/tablelisting.htaccess.php'; $collection = (new Collection(DirOptions::class, $userinfo)) ->withPagination($htaccess_list_data['htaccess_list']['columns'], $htaccess_list_data['htaccess_list']['default_sorting']); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $actions_links = []; $actions_links[] = [ 'href' => $linker->getLink(['section' => 'extras', 'page' => 'htaccess', 'action' => 'add']), 'label' => lng('extras.pathoptions_add') ]; $actions_links[] = [ 'href' => \Froxlor\Froxlor::getDocsUrl() . 'user-guide/extras/', 'target' => '_blank', 'icon' => 'fa-solid fa-circle-info', 'class' => 'btn-outline-secondary' ]; UI::view('user/table.html.twig', [ 'listing' => Listing::format($collection, $htaccess_list_data, 'htaccess_list'), 'actions_links' => $actions_links, 'entity_info' => lng('extras.description') ]); } elseif ($action == 'delete' && $id != 0) { try { $json_result = DirOptions::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if (isset($result['customerid']) && $result['customerid'] != '' && $result['customerid'] == $userinfo['customerid']) { if (Request::post('send') == 'send') { try { DirOptions::getLocal($userinfo, Request::postAll())->delete(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { HTML::askYesNo('extras_reallydelete_pathoptions', $filename, [ 'id' => $id, 'page' => $page, 'action' => $action ], str_replace($userinfo['documentroot'], '/', $result['path'])); } } } elseif ($action == 'add') { if (Request::post('send') == 'send') { try { DirOptions::getLocal($userinfo, Request::postAll())->add(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { $pathSelect = FileDir::makePathfield($userinfo['documentroot'], $userinfo['guid'], $userinfo['guid']); $cperlenabled = Customer::customerHasPerlEnabled($userinfo['customerid']); $htaccess_add_data = include_once dirname(__FILE__) . '/lib/formfields/customer/extras/formfield.htaccess_add.php'; UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'extras']), 'formdata' => $htaccess_add_data['htaccess_add'] ]); } } elseif (($action == 'edit') && ($id != 0)) { try { $json_result = DirOptions::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if ((isset($result['customerid'])) && ($result['customerid'] != '') && ($result['customerid'] == $userinfo['customerid'])) { if (Request::post('send') == 'send') { try { DirOptions::getLocal($userinfo, Request::postAll())->update(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { if (strpos($result['path'], $userinfo['documentroot']) === 0) { $result['path'] = str_replace($userinfo['documentroot'], "/", $result['path']); } $cperlenabled = Customer::customerHasPerlEnabled($userinfo['customerid']); $result = PhpHelper::htmlentitiesArray($result); $htaccess_edit_data = include_once dirname(__FILE__) . '/lib/formfields/customer/extras/formfield.htaccess_edit.php'; UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'extras', 'id' => $id]), 'formdata' => $htaccess_edit_data['htaccess_edit'], 'editid' => $id ]); } } } } elseif ($page == 'export') { // redirect if this customer sub-page is hidden via settings if (Settings::IsInList('panel.customer_hide_options', 'extras.export')) { Response::redirectTo('customer_index.php'); } if (Settings::Get('system.exportenabled') == 1) { if ($action == 'abort') { if (Request::post('send') == 'send') { $log->logAction(FroxlorLogger::USR_ACTION, LOG_NOTICE, "customer_extras::export - aborted scheduled data export job"); try { DataDump::getLocal($userinfo, Request::postAll())->delete(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page, 'action' => '' ]); } else { HTML::askYesNo('extras_reallydelete_export', $filename, [ 'job_entry' => $id, 'section' => 'extras', 'page' => $page, 'action' => $action ]); } } elseif ($action == '') { $log->logAction(FroxlorLogger::USR_ACTION, LOG_INFO, "viewed customer_extras::export"); // check whether there is a backup-job for this customer try { $export_list_data = include_once dirname(__FILE__) . '/lib/tablelisting/customer/tablelisting.export.php'; $collection = (new Collection(DataDump::class, $userinfo)); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } if (Request::post('send') == 'send') { try { DataDump::getLocal($userinfo, Request::postAll())->add(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::standardSuccess('exportscheduled'); } else { $pathSelect = FileDir::makePathfield($userinfo['documentroot'], $userinfo['guid'], $userinfo['guid']); $export_data = include_once dirname(__FILE__) . '/lib/formfields/customer/extras/formfield.export.php'; $actions_links = [ [ 'href' => \Froxlor\Froxlor::getDocsUrl() . 'user-guide/extras/', 'target' => '_blank', 'icon' => 'fa-solid fa-circle-info', 'class' => 'btn-outline-secondary' ] ]; UI::view('user/form-datatable.html.twig', [ 'formaction' => $linker->getLink(['section' => 'extras']), 'formdata' => $export_data['export'], 'actions_links' => $actions_links, 'tabledata' => Listing::format($collection, $export_list_data, 'export_list'), ]); } } } else { Response::standardError('exportfunctionnotenabled'); } } ================================================ FILE: customer_ftp.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ const AREA = 'customer'; require __DIR__ . '/lib/init.php'; use Froxlor\Api\Commands\Ftps; use Froxlor\Api\Commands\SshKeys; use Froxlor\CurrentUser; use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\Froxlor; use Froxlor\FroxlorLogger; use Froxlor\Settings; use Froxlor\UI\Collection; use Froxlor\UI\HTML; use Froxlor\UI\Listing; use Froxlor\UI\Panel\UI; use Froxlor\UI\Request; use Froxlor\UI\Response; // redirect if this customer page is hidden via settings if (Settings::IsInList('panel.customer_hide_options', 'ftp')) { Response::redirectTo('customer_index.php'); } $id = (int)Request::any('id', 0); if ($page == 'overview' || $page == 'accounts') { if ($action == '') { $log->logAction(FroxlorLogger::USR_ACTION, LOG_NOTICE, "viewed customer_ftp::accounts"); try { $ftp_list_data = include_once dirname(__FILE__) . '/lib/tablelisting/customer/tablelisting.ftps.php'; $collection = (new Collection(Ftps::class, $userinfo)) ->withPagination($ftp_list_data['ftp_list']['columns'], $ftp_list_data['ftp_list']['default_sorting']); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $actions_links = []; if (CurrentUser::canAddResource('ftps')) { $actions_links[] = [ 'href' => $linker->getLink(['section' => 'ftp', 'page' => 'accounts', 'action' => 'add']), 'label' => lng('ftp.account_add') ]; } $actions_links[] = [ 'href' => Froxlor::getDocsUrl() . 'user-guide/ftp-accounts/', 'target' => '_blank', 'icon' => 'fa-solid fa-circle-info', 'class' => 'btn-outline-secondary' ]; UI::view('user/table.html.twig', [ 'listing' => Listing::format($collection, $ftp_list_data, 'ftp_list'), 'actions_links' => $actions_links, 'entity_info' => lng('ftp.description') ]); } elseif ($action == 'delete' && $id != 0) { try { $json_result = Ftps::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if (isset($result['username']) && $result['username'] != $userinfo['loginname']) { if (Request::post('send') == 'send') { try { Ftps::getLocal($userinfo, Request::postAll())->delete(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { HTML::askYesNoWithCheckbox('ftp_reallydelete', 'admin_customer_alsoremoveftphomedir', $filename, [ 'id' => $id, 'page' => $page, 'action' => $action ], $result['username']); } } else { Response::standardError('ftp_cantdeletemainaccount'); } } elseif ($action == 'add') { if ($userinfo['ftps_used'] < $userinfo['ftps'] || $userinfo['ftps'] == '-1') { if (Request::post('send') == 'send') { try { Ftps::getLocal($userinfo, Request::postAll())->add(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { $pathSelect = FileDir::makePathfield($userinfo['documentroot'], $userinfo['guid'], $userinfo['guid'], '/'); if (Settings::Get('customer.ftpatdomain') == '1') { $domainlist = []; $result_domains_stmt = Database::prepare("SELECT `domain` FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `customerid`= :customerid ORDER BY `domain` ASC"); Database::pexecute($result_domains_stmt, [ "customerid" => $userinfo['customerid'] ]); while ($row_domain = $result_domains_stmt->fetch(PDO::FETCH_ASSOC)) { $domainlist[$row_domain['domain']] = $idna_convert->decode($row_domain['domain']); } } $user_shell_allowed = intval($userinfo['shell_allowed']) == 1; if (Settings::Get('system.allow_customer_shell') == '1') { $shells['/bin/false'] = "/bin/false"; $shells_avail = Settings::Get('system.available_shells'); if (!empty($shells_avail)) { $shells_avail_arr = explode(",", $shells_avail); $shells_avail_arr = array_map("trim", $shells_avail_arr); foreach ($shells_avail_arr as $shell) { $shells[$shell] = $shell; } } } $ftp_add_data = include_once dirname(__FILE__) . '/lib/formfields/customer/ftp/formfield.ftp_add.php'; UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'ftp']), 'formdata' => $ftp_add_data['ftp_add'] ]); } } } elseif ($action == 'edit' && $id != 0) { try { $json_result = Ftps::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if (isset($result['username']) && $result['username'] != '') { if (Request::post('send') == 'send') { try { Ftps::getLocal($userinfo, Request::postAll())->update(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { if (strpos($result['homedir'], $userinfo['documentroot']) === 0) { $homedir = str_replace($userinfo['documentroot'], "/", $result['homedir']); } else { $homedir = $result['homedir']; } $homedir = FileDir::makeCorrectDir($homedir); $pathSelect = FileDir::makePathfield($userinfo['documentroot'], $userinfo['guid'], $userinfo['guid'], $homedir); $user_shell_allowed = intval($userinfo['shell_allowed']) == 1; if (Settings::Get('system.allow_customer_shell') == '1') { $shells['/bin/false'] = "/bin/false"; $shells_avail = Settings::Get('system.available_shells'); if (!empty($shells_avail)) { $shells_avail_arr = explode(",", $shells_avail); $shells_avail_arr = array_map("trim", $shells_avail_arr); foreach ($shells_avail_arr as $shell) { $shells[$shell] = $shell; } } } $ftp_edit_data = include_once dirname(__FILE__) . '/lib/formfields/customer/ftp/formfield.ftp_edit.php'; UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'ftp', 'id' => $id]), 'formdata' => $ftp_edit_data['ftp_edit'], 'editid' => $id ]); } } } } elseif ($page == 'sshkeys') { // redirect if this customer has no permission for API usage if ($userinfo['adminsession'] == 0 && (intval(Settings::Get('system.allow_customer_shell')) == 0 || $userinfo['shell_allowed'] == 0)) { Response::redirectTo('customer_index.php'); } if ($action == '') { $log->logAction(FroxlorLogger::USR_ACTION, LOG_NOTICE, "viewed customer_ftp::sshkeys"); try { $sshkeys_list_data = include_once dirname(__FILE__) . '/lib/tablelisting/customer/tablelisting.sshkeys.php'; $collection = (new Collection(SshKeys::class, $userinfo)) ->withPagination($sshkeys_list_data['sshkeys_list']['columns'], $sshkeys_list_data['sshkeys_list']['default_sorting']); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $actions_links = []; if (/* User-has-ftp-users-with-active-shell */ true) { $actions_links[] = [ 'href' => $linker->getLink(['section' => 'ftp', 'page' => 'sshkeys', 'action' => 'add']), 'label' => lng('ftp.sshkey_add') ]; } $actions_links[] = [ 'href' => Froxlor::getDocsUrl() . 'user-guide/ssh-keys/', 'target' => '_blank', 'icon' => 'fa-solid fa-circle-info', 'class' => 'btn-outline-secondary' ]; UI::view('user/table.html.twig', [ 'listing' => Listing::format($collection, $sshkeys_list_data, 'sshkeys_list'), 'actions_links' => $actions_links, 'entity_info' => lng('sshkeys.description') ]); } elseif ($action == 'add') { if (Request::post('send') == 'send') { try { SshKeys::getLocal($userinfo, Request::postAll())->add(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { $userList = []; $result_ftpusers_stmt = Database::prepare(" SELECT `id`, `username`, `shell` FROM `" . TABLE_FTP_USERS . "` WHERE `customerid` = :customerid ORDER BY `username` ASC "); Database::pexecute($result_ftpusers_stmt, [ "customerid" => $userinfo['customerid'] ]); while ($row_ftpusers = $result_ftpusers_stmt->fetch(PDO::FETCH_ASSOC)) { $userList[$row_ftpusers['id']] = $row_ftpusers['username'] . ' (Shell: ' . $row_ftpusers['shell'] . ')'; } $sshkey_add_data = include_once dirname(__FILE__) . '/lib/formfields/customer/ftp/formfield.ftp_ssh_add.php'; UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'ftp', 'page' => 'sshkeys']), 'formdata' => $sshkey_add_data['sshkey_add'] ]); } } elseif ($action == 'edit' && $id != 0) { try { $json_result = SshKeys::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if (isset($result['ssh_pubkey']) && $result['ssh_pubkey'] != '') { if (Request::post('send') == 'send') { try { SshKeys::getLocal($userinfo, Request::postAll())->update(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { $sshkey_edit_data = include_once dirname(__FILE__) . '/lib/formfields/customer/ftp/formfield.ftp_ssh_edit.php'; UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'ftp', 'page' => 'sshkeys', 'id' => $id]), 'formdata' => $sshkey_edit_data['sshkey_edit'], 'editid' => $id ]); } } } elseif ($action == 'delete' && $id != 0) { try { $json_result = SshKeys::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if (isset($result['ssh_pubkey']) && $result['ssh_pubkey'] != '') { if (Request::post('send') == 'send') { try { SshKeys::getLocal($userinfo, Request::postAll())->delete(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { HTML::askYesNo('sshkey_reallydelete', $filename, [ 'id' => $id, 'page' => $page, 'action' => $action ], $result['fingerprint']); } } } } ================================================ FILE: customer_index.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ const AREA = 'customer'; require __DIR__ . '/lib/init.php'; use Froxlor\Api\Commands\Customers as Customers; use Froxlor\Cron\TaskId; use Froxlor\CurrentUser; use Froxlor\Database\Database; use Froxlor\Database\DbManager; use Froxlor\Froxlor; use Froxlor\FroxlorLogger; use Froxlor\Language; use Froxlor\Settings; use Froxlor\System\Cronjob; use Froxlor\System\Crypt; use Froxlor\UI\Panel\UI; use Froxlor\UI\Request; use Froxlor\UI\Response; use Froxlor\Validate\Validate; if ($action == 'logout') { $log->logAction(FroxlorLogger::USR_ACTION, LOG_INFO, 'logged out'); unset($_SESSION['userinfo']); CurrentUser::setData(); session_destroy(); Response::redirectTo('index.php'); } elseif ($action == 'suback') { if (is_array(CurrentUser::getField('switched_user'))) { $result = CurrentUser::getData(); $result = $result['switched_user']; session_regenerate_id(true); CurrentUser::setData($result); $target = Request::get('target', 'index'); $redirect = "admin_" . $target . ".php"; if (!file_exists(Froxlor::getInstallDir() . "/" . $redirect)) { $redirect = "admin_index.php"; } Response::redirectTo($redirect, null, true); } else { Response::dynamicError("Cannot change back - You've never switched to another user :-)"); } } if ($page == 'overview') { $log->logAction(FroxlorLogger::USR_ACTION, LOG_INFO, "viewed customer_index"); $domain_stmt = Database::prepare("SELECT `domain` FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `customerid` = :customerid AND `parentdomainid` = '0' AND `id` <> :standardsubdomain "); Database::pexecute($domain_stmt, [ "customerid" => $userinfo['customerid'], "standardsubdomain" => $userinfo['standardsubdomain'] ]); $domainArray = []; while ($row = $domain_stmt->fetch(PDO::FETCH_ASSOC)) { $domainArray[] = $idna_convert->decode($row['domain']); } natsort($domainArray); // standard-subdomain $stdsubdomain = ''; if ($userinfo['standardsubdomain'] != '0') { $std_domain_stmt = Database::prepare(" SELECT `domain` FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `customerid` = :customerid AND `id` = :standardsubdomain "); $std_domain = Database::pexecute_first($std_domain_stmt, [ "customerid" => $userinfo['customerid'], "standardsubdomain" => $userinfo['standardsubdomain'] ]); $stdsubdomain = $std_domain['domain']; } $userinfo['email'] = $idna_convert->decode($userinfo['email']); $yesterday = time() - (60 * 60 * 24); $month = date('M Y', $yesterday); // get disk-space usages for web, mysql and mail $usages_stmt = Database::prepare("SELECT * FROM `" . TABLE_PANEL_DISKSPACE . "` WHERE `customerid` = :cid ORDER BY `stamp` DESC LIMIT 1"); $usages = Database::pexecute_first($usages_stmt, [ 'cid' => $userinfo['customerid'] ]); // get everything in bytes for the percentage calculation on the dashboard $userinfo['diskspace_bytes'] = ($userinfo['diskspace'] > -1) ? $userinfo['diskspace'] * 1024 : -1; $userinfo['traffic_bytes'] = ($userinfo['traffic'] > -1) ? $userinfo['traffic'] * 1024 : -1; $userinfo['traffic_bytes_used'] = $userinfo['traffic_used'] * 1024; if (Settings::Get('system.mail_quota_enabled')) { $userinfo['email_quota_bytes'] = ($userinfo['email_quota'] > -1) ? $userinfo['email_quota'] * 1024 * 1024 : -1; $userinfo['email_quota_bytes_used'] = $userinfo['email_quota_used'] * 1024 * 1024; } if ($usages) { $userinfo['diskspace_bytes_used'] = $usages['webspace'] * 1024; $userinfo['mailspace_used'] = $usages['mail'] * 1024; $userinfo['dbspace_used'] = $usages['mysql'] * 1024; $userinfo['total_bytes_used'] = ($usages['webspace'] + $usages['mail'] + $usages['mysql']) * 1024; } else { $userinfo['diskspace_bytes_used'] = 0; $userinfo['total_bytes_used'] = 0; $userinfo['mailspace_used'] = 0; $userinfo['dbspace_used'] = 0; } UI::twig()->addGlobal('userinfo', $userinfo); UI::view('user/index.html.twig', [ 'domains' => $domainArray, 'stdsubdomain' => $stdsubdomain ]); } elseif ($page == 'profile') { $languages = Language::getLanguages(); if (!empty($_POST)) { if (Request::post('send') == 'changepassword') { $old_password = Validate::validate(Request::post('old_password'), 'old password'); if (!Crypt::validatePasswordLogin($userinfo, $old_password, TABLE_PANEL_CUSTOMERS, 'customerid')) { Response::standardError('oldpasswordnotcorrect'); } try { $new_password = Crypt::validatePassword(Request::post('new_password'), 'new password'); $new_password_confirm = Crypt::validatePassword(Request::post('new_password_confirm'), 'new password confirm'); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } if ($old_password == '') { Response::standardError([ 'stringisempty', 'changepassword.old_password' ]); } elseif ($new_password == '') { Response::standardError([ 'stringisempty', 'changepassword.new_password' ]); } elseif ($new_password_confirm == '') { Response::standardError([ 'stringisempty', 'changepassword.new_password_confirm' ]); } elseif ($new_password != $new_password_confirm) { Response::standardError('newpasswordconfirmerror'); } else { // Update user password try { Customers::getLocal($userinfo, [ 'id' => $userinfo['customerid'], 'new_customer_password' => $new_password ])->update(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $log->logAction(FroxlorLogger::USR_ACTION, LOG_NOTICE, 'changed password'); // Update ftp password if (Request::post('change_main_ftp') == 'true') { $cryptPassword = Crypt::makeCryptPassword($new_password); $stmt = Database::prepare("UPDATE `" . TABLE_FTP_USERS . "` SET `password` = :password WHERE `customerid` = :customerid AND `username` = :username"); $params = [ "password" => $cryptPassword, "customerid" => $userinfo['customerid'], "username" => $userinfo['loginname'] ]; Database::pexecute($stmt, $params); $log->logAction(FroxlorLogger::USR_ACTION, LOG_NOTICE, 'changed main ftp password'); } // Update statistics password if (Request::post('change_stats') == 'true') { $new_stats_password = Crypt::makeCryptPassword($new_password, true); $stmt = Database::prepare("UPDATE `" . TABLE_PANEL_HTPASSWDS . "` SET `password` = :password WHERE `customerid` = :customerid AND `username` = :username"); $params = [ "password" => $new_stats_password, "customerid" => $userinfo['customerid'], "username" => $userinfo['loginname'] ]; Database::pexecute($stmt, $params); Cronjob::inserttask(TaskId::REBUILD_VHOST); } // Update global myqsl user password if ($userinfo['mysqls'] != 0 && Request::post('change_global_mysql') == 'true') { $allowed_mysqlservers = json_decode($userinfo['allowed_mysqlserver'] ?? '[]', true); foreach ($allowed_mysqlservers as $dbserver) { // require privileged access for target db-server Database::needRoot(true, $dbserver, false); // get DbManager $dbm = new DbManager($log); // give permission to the user on every access-host we have foreach (array_map('trim', explode(',', Settings::Get('system.mysql_access_host'))) as $mysql_access_host) { if ($dbm->getManager()->userExistsOnHost($userinfo['loginname'], $mysql_access_host)) { $dbm->getManager()->grantPrivilegesTo($userinfo['loginname'], $new_password, $mysql_access_host, false, true); } else { // create global mysql user if not exists $dbm->getManager()->grantPrivilegesTo($userinfo['loginname'], $new_password, $mysql_access_host, false, false, true); } } $dbm->getManager()->flushPrivileges(); } } Response::redirectTo($filename); } } elseif (Request::post('send') == 'changetheme') { if (Settings::Get('panel.allow_theme_change_customer') == 1) { $theme = Validate::validate(Request::post('theme'), 'theme'); try { Customers::getLocal($userinfo, [ 'id' => $userinfo['customerid'], 'theme' => $theme ])->update(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $log->logAction(FroxlorLogger::USR_ACTION, LOG_NOTICE, "changed default theme to '" . $theme . "'"); } Response::redirectTo($filename); } elseif (Request::post('send') == 'changelanguage') { $def_language = Validate::validate(Request::post('def_language'), 'default language'); if (isset($languages[$def_language])) { try { Customers::getLocal($userinfo, [ 'id' => $userinfo['customerid'], 'def_language' => $def_language ])->update(); CurrentUser::setField('language', $def_language); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } } $log->logAction(FroxlorLogger::USR_ACTION, LOG_NOTICE, "changed default language to '" . $def_language . "'"); Response::redirectTo($filename); } } else { // change theme $default_theme = Settings::Get('panel.default_theme'); if ($userinfo['theme'] != '') { $default_theme = $userinfo['theme']; } $themes_avail = UI::getThemes(); // change language $default_lang = Settings::Get('panel.standardlanguage'); if ($userinfo['def_language'] != '') { $default_lang = $userinfo['def_language']; } UI::view('user/profile.html.twig', [ 'themes' => $themes_avail, 'default_theme' => $default_theme, 'languages' => $languages, 'default_lang' => $default_lang, ]); } } elseif ($page == 'send_error_report' && Settings::Get('system.allow_error_report_customer') == '1') { require_once __DIR__ . '/error_report.php'; } elseif ($page == 'apikeys' && Settings::Get('api.enabled') == 1) { require_once __DIR__ . '/api_keys.php'; } elseif ($page == '2fa' && Settings::Get('2fa.enabled') == 1) { require_once __DIR__ . '/2fa.php'; } ================================================ FILE: customer_logger.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ const AREA = 'customer'; require __DIR__ . '/lib/init.php'; use Froxlor\Api\Commands\SysLog; use Froxlor\Settings; use Froxlor\UI\Collection; use Froxlor\UI\Listing; use Froxlor\UI\Panel\UI; use Froxlor\UI\Response; // redirect if this customer page is hidden via settings if (Settings::IsInList('panel.customer_hide_options', 'extras.logger')) { Response::redirectTo('customer_index.php'); } if ($page == 'log') { if ($action == '') { try { $syslog_list_data = include_once dirname(__FILE__) . '/lib/tablelisting/tablelisting.syslog.php'; $collection = (new Collection(SysLog::class, $userinfo)) ->withPagination($syslog_list_data['syslog_list']['columns'], $syslog_list_data['syslog_list']['default_sorting']); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } UI::view('user/table.html.twig', [ 'listing' => Listing::format($collection, $syslog_list_data, 'syslog_list') ]); } } ================================================ FILE: customer_mysql.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ const AREA = 'customer'; require __DIR__ . '/lib/init.php'; use Froxlor\Api\Commands\Mysqls; use Froxlor\Api\Commands\MysqlServer; use Froxlor\CurrentUser; use Froxlor\Database\Database; use Froxlor\Database\DbManager; use Froxlor\FroxlorLogger; use Froxlor\Settings; use Froxlor\System\Crypt; use Froxlor\UI\Collection; use Froxlor\UI\HTML; use Froxlor\UI\Listing; use Froxlor\UI\Panel\UI; use Froxlor\UI\Request; use Froxlor\UI\Response; // redirect if this customer page is hidden via settings or no resources given if (Settings::IsInList('panel.customer_hide_options', 'mysql') || $userinfo['mysqls'] == 0) { Response::redirectTo('customer_index.php'); } // get sql-root access data Database::needRoot(true); Database::needSqlData(); $sql_root = Database::getSqlData(); Database::needRoot(false); $id = (int)Request::any('id'); if ($page == 'overview' || $page == 'mysqls') { if ($action == '') { $log->logAction(FroxlorLogger::USR_ACTION, LOG_NOTICE, "viewed customer_mysql::mysqls"); $multiple_mysqlservers = count(json_decode($userinfo['allowed_mysqlserver'] ?? '[]', true)) > 1; try { $mysql_list_data = include_once dirname(__FILE__) . '/lib/tablelisting/customer/tablelisting.mysqls.php'; $collection = (new Collection(Mysqls::class, $userinfo)) ->withPagination($mysql_list_data['mysql_list']['columns'], $mysql_list_data['mysql_list']['default_sorting']); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $actions_links = []; if (CurrentUser::canAddResource('mysqls')) { $actions_links[] = [ 'href' => $linker->getLink(['section' => 'mysql', 'page' => 'mysqls', 'action' => 'add']), 'label' => lng('mysql.database_create') ]; } $view = 'user/table.html.twig'; if ($collection->count() > 0) { $view = 'user/table-note.html.twig'; $actions_links[] = [ 'href' => $linker->getLink(['section' => 'mysql', 'page' => 'mysqls', 'action' => 'global_user']), 'label' => lng('mysql.edit_global_user'), 'icon' => 'fa-solid fa-user-tie', 'class' => 'btn-outline-secondary' ]; } $actions_links[] = [ 'href' => \Froxlor\Froxlor::getDocsUrl() . 'user-guide/databases/', 'target' => '_blank', 'icon' => 'fa-solid fa-circle-info', 'class' => 'btn-outline-secondary' ]; UI::view($view, [ 'listing' => Listing::format($collection, $mysql_list_data, 'mysql_list'), 'actions_links' => $actions_links, 'entity_info' => lng('mysql.description'), // alert-box 'type' => 'info', 'alert_msg' => lng('mysql.globaluserinfo', [$userinfo['loginname']]), ]); } elseif ($action == 'delete' && $id != 0) { try { $json_result = Mysqls::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if (isset($result['databasename']) && $result['databasename'] != '') { Database::needRoot(true, $result['dbserver'], false); Database::needSqlData(); $sql_root = Database::getSqlData(); Database::needRoot(false); if (!isset($sql_root[$result['dbserver']]) || !is_array($sql_root[$result['dbserver']])) { $result['dbserver'] = 0; } if (Request::post('send') == 'send') { try { Mysqls::getLocal($userinfo, Request::postAll())->delete(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { $dbnamedesc = $result['databasename']; if (isset($result['description']) && $result['description'] != '') { $dbnamedesc .= ' (' . $result['description'] . ')'; } HTML::askYesNo('mysql_reallydelete', $filename, [ 'id' => $id, 'page' => $page, 'action' => $action ], $dbnamedesc); } } } elseif ($action == 'add') { if ($userinfo['mysqls_used'] < $userinfo['mysqls'] || $userinfo['mysqls'] == '-1') { if (Request::post('send') == 'send') { try { Mysqls::getLocal($userinfo, Request::postAll())->add(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { $mysql_servers = []; try { $result_json = MysqlServer::getLocal($userinfo)->listing(); $result_decoded = json_decode($result_json, true)['data']['list']; foreach ($result_decoded as $dbserver => $dbdata) { $mysql_servers[$dbserver] = $dbdata['caption']; } } catch (Exception $e) { /* just none */ } $mysql_add_data = include_once dirname(__FILE__) . '/lib/formfields/customer/mysql/formfield.mysql_add.php'; UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'mysql']), 'formdata' => $mysql_add_data['mysql_add'] ]); } } } elseif ($action == 'edit' && $id != 0) { try { $json_result = Mysqls::getLocal($userinfo, [ 'id' => $id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; if (isset($result['databasename']) && $result['databasename'] != '') { if (Request::post('send') == 'send') { try { $json_result = Mysqls::getLocal($userinfo, Request::postAll())->update(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } Response::redirectTo($filename, [ 'page' => $page ]); } else { $mysql_servers = []; try { $result_json = MysqlServer::getLocal($userinfo)->listing(); $result_decoded = json_decode($result_json, true)['data']['list']; foreach ($result_decoded as $dbserver => $dbdata) { $mysql_servers[$dbserver] = $dbdata['caption'] . ' (' . $dbdata['host'] . (isset($dbdata['port']) && !empty($dbdata['port']) ? ':' . $dbdata['port'] : '') . ')'; } } catch (Exception $e) { /* just none */ } $mysql_edit_data = include_once dirname(__FILE__) . '/lib/formfields/customer/mysql/formfield.mysql_edit.php'; UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'mysql', 'id' => $id]), 'formdata' => $mysql_edit_data['mysql_edit'], 'editid' => $id ]); } } } elseif ($action == 'global_user') { $allowed_mysqlservers = json_decode($userinfo['allowed_mysqlserver'] ?? '[]', true); if ($userinfo['mysqls'] == 0 || empty($allowed_mysqlservers)) { Response::dynamicError('No permission'); } if (Request::post('send') == 'send') { $new_password = Crypt::validatePassword(Request::post('mysql_password')); foreach ($allowed_mysqlservers as $dbserver) { // require privileged access for target db-server Database::needRoot(true, $dbserver, true); // get DbManager $dbm = new DbManager($log); // give permission to the user on every access-host we have foreach (array_map('trim', explode(',', Settings::Get('system.mysql_access_host'))) as $mysql_access_host) { if ($dbm->getManager()->userExistsOnHost($userinfo['loginname'], $mysql_access_host)) { // update password $dbm->getManager()->grantPrivilegesTo($userinfo['loginname'], $new_password, $mysql_access_host, false, true, true); } else { // create missing user $dbm->getManager()->grantPrivilegesTo($userinfo['loginname'], $new_password, $mysql_access_host, false, false, true); } } $dbm->getManager()->flushPrivileges(); } Response::redirectTo($filename, [ 'page' => 'overview' ]); } else { $mysql_global_user_data = include_once dirname(__FILE__) . '/lib/formfields/customer/mysql/formfield.mysql_global_user.php'; UI::view('user/form.html.twig', [ 'formaction' => $linker->getLink(['section' => 'mysql', 'page' => 'mysqls', 'action' => 'global_user']), 'formdata' => $mysql_global_user_data['mysql_global_user'], 'editid' => $id ]); } } } ================================================ FILE: customer_traffic.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ const AREA = 'customer'; require __DIR__ . '/lib/init.php'; use Froxlor\Traffic\Traffic; use Froxlor\Settings; use Froxlor\UI\Panel\UI; use Froxlor\UI\Request; use Froxlor\UI\Response; // redirect if this customer page is hidden via settings if (Settings::IsInList('panel.customer_hide_options', 'traffic')) { Response::redirectTo('customer_index.php'); } $range = Request::any('range', 'currentyear'); if ($page == 'current') { $range = 'currentmonth'; } try { $context = Traffic::getCustomerStats($userinfo, $range); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } // pass metrics to the view UI::view('user/traffic.html.twig', $context); ================================================ FILE: dns_editor.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ if (!defined('AREA')) { header("Location: index.php"); exit(); } use Froxlor\Api\Commands\DomainZones; use Froxlor\Dns\Dns; use Froxlor\Settings; use Froxlor\UI\Collection; use Froxlor\UI\HTML; use Froxlor\UI\Listing; use Froxlor\UI\Panel\UI; use Froxlor\UI\Request; use Froxlor\UI\Response; // This file is being included in admin_domains and customer_domains // and therefore does not need to require lib/init.php $domain_id = (int)Request::any('domain_id'); $record = Request::post('dns_record'); $type = Request::post('dns_type', 'A'); $prio = Request::post('dns_mxp'); $content = Request::post('dns_content'); $ttl = (int)Request::post('dns_ttl', Settings::get('system.defaultttl')); // get domain-name $domain = Dns::getAllowedDomainEntry($domain_id, AREA, $userinfo); $errors = ""; $success_message = ""; // action for adding a new entry if ($action == 'add_record' && !empty($_POST)) { try { DomainZones::getLocal($userinfo, [ 'id' => $domain_id, 'record' => $record, 'type' => $type, 'prio' => $prio, 'content' => $content, 'ttl' => $ttl ])->add(); $success_message = lng('success.dns_record_added'); $record = $prio = $content = ""; } catch (Exception $e) { $errors = str_replace("\n", "
", $e->getMessage()); } } elseif ($action == 'delete') { $entry_id = (int)Request::get('id', 0); HTML::askYesNo('dnsentry_reallydelete', $filename, [ 'id' => $entry_id, 'domain_id' => $domain_id, 'page' => $page, 'action' => 'deletesure' ], '', [ 'section' => 'domains', 'page' => $page, 'domain_id' => $domain_id ]); } elseif (Request::post('send') == 'send' && $action == 'deletesure' && !empty($_POST)) { $entry_id = (int)Request::post('id', 0); $domain_id = (int)Request::post('domain_id', 0); // remove entry if ($entry_id > 0 && $domain_id > 0) { try { DomainZones::getLocal($userinfo, [ 'entry_id' => $entry_id, 'id' => $domain_id ])->delete(); // success message (inline) $success_message = lng('success.dns_record_deleted'); } catch (Exception $e) { $errors = str_replace("\n", "
", $e->getMessage()); } } } // select all entries try { $dns_list_data = include_once dirname(__FILE__) . '/lib/tablelisting/tablelisting.dns.php'; $collection = (new Collection(DomainZones::class, $userinfo, ['id' => $domain_id])) ->withPagination($dns_list_data['dns_list']['columns'], $dns_list_data['dns_list']['default_sorting'], ['domain_id='.$domain_id]); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } try { $json_result = DomainZones::getLocal($userinfo, [ 'id' => $domain_id ])->get(); } catch (Exception $e) { Response::dynamicError($e->getMessage()); } $result = json_decode($json_result, true)['data']; $zonefile = implode("\n", $result); $dns_add_data = include_once dirname(__FILE__) . '/lib/formfields/formfield.dns_add.php'; UI::view('user/dns-editor.html.twig', [ 'listing' => Listing::format($collection, $dns_list_data, 'dns_list', ['domain_id' => $domain_id]), 'actions_links' => [ [ 'href' => $linker->getLink([ 'section' => 'domains', 'page' => 'domains', 'action' => 'edit', 'id' => $domain_id ]), 'label' => lng('admin.domain_edit'), 'icon' => 'fa-solid fa-pen' ], [ 'href' => $linker->getLink(['section' => 'domains', 'page' => 'domains']), 'label' => lng('panel.backtooverview'), 'icon' => 'fa-solid fa-reply' ] ], 'formaction' => $linker->getLink(['section' => 'domains', 'action' => 'add_record', 'domain_id' => $domain_id]), 'formdata' => $dns_add_data['dns_add'], // alert-box 'type' => (!empty($errors) ? 'danger' : (!empty($success_message) ? 'success' : 'warning')), 'alert_msg' => (!empty($errors) ? $errors : (!empty($success_message) ? $success_message : lng('dns.howitworks'))), 'zonefile' => $zonefile, ]); ================================================ FILE: doc/example/FroxlorAPI.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ class FroxlorAPI { private string $url; private string $key; private string $secret; private ?array $lastError = null; private ?string $lastStatusCode = null; public function __construct($url, $key, $secret) { $this->url = $url; $this->key = $key; $this->secret = $secret; } public function request($command, array $data = []) { $payload = [ 'command' => $command, 'params' => $data ]; $ch = curl_init($this->url); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); curl_setopt($ch, CURLOPT_HEADER, 0); curl_setopt($ch, CURLOPT_USERPWD, $this->key . ":" . $this->secret); curl_setopt($ch, CURLOPT_TIMEOUT, 30); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $result = curl_exec($ch); $this->lastStatusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); return json_decode($result ?? curl_error($ch), true); } public function getLastStatusCode(): ?string { return $this->lastStatusCode; } } ================================================ FILE: doc/example/create_customer.php ================================================ 'test', 'email' => 'test@froxlor.org', 'firstname' => 'Test', 'name' => 'Testman', 'customernumber' => 1337, 'new_customer_password' => 's0mEcRypt1cpassword' . uniqid() ]; // send request $response = $fapi->request('Customers.add', $data); // check for error if ($fapi->getLastStatusCode() != 200) { echo "HTTP-STATUS: " . $fapi->getLastStatusCode() . PHP_EOL; echo "Description: " . $response['message'] . PHP_EOL; exit(); } // view response data var_dump($response); /* array(60) { ["customerid"]=> string(1) "1" ["loginname"]=> string(4) "test" ["password"]=> string(63) "$5$asdasdasd.asdasd" ["adminid"]=> string(1) "1" ["name"]=> string(7) "Testman" ["firstname"]=> string(4) "Test" [...] */ ================================================ FILE: doc/example/index.html ================================================ ================================================ FILE: doc/example/list_functions.php ================================================ request('Froxlor.listFunctions'); // check for error if ($fapi->getLastStatusCode() != 200) { echo "HTTP-STATUS: " . $fapi->getLastStatusCode() . PHP_EOL; echo "Description: " . $response['message'] . PHP_EOL; exit(); } // view response data var_dump($response); ================================================ FILE: doc/index.html ================================================ ================================================ FILE: error_report.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ if (!defined('AREA')) { header("Location: index.php"); exit(); } use Froxlor\FileDir; use Froxlor\Froxlor; use Froxlor\UI\Panel\UI; use Froxlor\UI\Request; use Froxlor\UI\Response; use Froxlor\Database\Database; // This file is being included in admin_domains and customer_domains // and therefore does not need to require lib/init.php $errid = Request::any('errorid'); if (!empty($errid)) { // read error file $err_dir = FileDir::makeCorrectDir(Froxlor::getInstallDir() . "/logs/"); $err_file = FileDir::makeCorrectFile($err_dir . "/" . $errid . "_sql-error.log"); if (file_exists($err_file)) { $error_content = file_get_contents($err_file); $error = explode("|", $error_content); $_error = [ 'code' => str_replace("\n", "", substr($error[1], 5)), 'message' => str_replace("\n", "", substr($error[2], 4)), 'file' => str_replace("\n", "", substr($error[3], 5 + strlen(Froxlor::getInstallDir()))), 'line' => str_replace("\n", "", substr($error[4], 5)), 'trace' => str_replace(Froxlor::getInstallDir(), "", substr($error[5], 6)) ]; // build mail-content $mail_body = "Dear froxlor-team,\n\n"; $mail_body .= "the following error has been reported by a user:\n\n"; $mail_body .= "-------------------------------------------------------------\n"; $mail_body .= $_error['code'] . ' ' . $_error['message'] . "\n\n"; $mail_body .= "File: " . $_error['file'] . ':' . $_error['line'] . "\n\n"; $mail_body .= "Trace:\n" . trim($_error['trace']) . "\n\n"; $mail_body .= "-------------------------------------------------------------\n\n"; $mail_body .= "User-Area: " . AREA . "\n"; $mail_body .= "Froxlor-version: " . Froxlor::VERSION . "\n"; $mail_body .= "DB-version: " . Froxlor::DBVERSION . "\n\n"; try { $mail_body .= "Database: " . Database::getAttribute(PDO::ATTR_SERVER_VERSION); } catch (\Exception $e) { /* ignore */ } $mail_body .= "End of report"; $mail_html = nl2br($mail_body); // send actual report to dev-team if (Request::post('send') == 'send') { // send mail and say thanks $_mailerror = false; try { $mail->Subject = '[Froxlor] Error report by user'; $mail->AltBody = $mail_body; $mail->MsgHTML($mail_html); $mail->AddAddress('error-reports@froxlor.org', 'Froxlor Developer Team'); $mail->Send(); } catch (\PHPMailer\PHPMailer\Exception $e) { $mailerr_msg = $e->errorMessage(); $_mailerror = true; } catch (Exception $e) { $mailerr_msg = $e->getMessage(); $_mailerror = true; } if ($_mailerror) { // error when reporting an error...LOLFUQ Response::standardError('send_report_error', $mailerr_msg); } // finally remove error from fs @unlink($err_file); Response::standardSuccess('sent_error_report', '', ['filename' => 'index.php']); } // show a nice summary of the error-report // before actually sending anything UI::view('user/error_report.html.twig', [ 'mail_html' => $mail_body, 'errorid' => $errid ]); } else { Response::redirectTo($filename); } } else { Response::redirectTo($filename); } ================================================ FILE: index.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ const AREA = 'login'; require __DIR__ . '/lib/init.php'; use Froxlor\Api\FroxlorRPC; use Froxlor\CurrentUser; use Froxlor\Customer\Customer; use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\Froxlor; use Froxlor\FroxlorLogger; use Froxlor\FroxlorTwoFactorAuth; use Froxlor\PhpHelper; use Froxlor\Settings; use Froxlor\System\Crypt; use Froxlor\UI\Panel\UI; use Froxlor\UI\Request; use Froxlor\UI\Response; use Froxlor\User; use Froxlor\Validate\Validate; if ($action == '') { $action = 'login'; } if ($action == '2fa_entercode') { // page for entering the 2FA code after successful login if (!isset($_SESSION) || !isset($_SESSION['secret_2fa'])) { // no session - redirect to index Response::redirectTo('index.php'); exit(); } $smessage = (int)Request::get('showmessage', 0); $message = ""; if ($smessage > 0) { $message = lng('error.2fa_wrongcode'); } // show template to enter code UI::view('login/enter2fa.html.twig', [ 'pagetitle' => lng('login.2fa'), 'remember_me' => (Settings::Get('panel.db_version') >= 202407200) ? true : false, 'message' => $message ]); } elseif ($action == '2fa_verify') { // verify code from 2fa code-enter form if (!isset($_SESSION) || !isset($_SESSION['secret_2fa'])) { // no session - redirect to index Response::redirectTo('index.php'); exit(); } $code = Request::post('2fa_code'); $remember = Request::post('2fa_remember'); // verify entered code $tfa = new FroxlorTwoFactorAuth('Froxlor ' . Settings::Get('system.hostname')); // get user-data $table = $_SESSION['uidtable_2fa']; $field = $_SESSION['uidfield_2fa']; $uid = $_SESSION['uid_2fa']; $isadmin = $_SESSION['unfo_2fa']; if ($_SESSION['secret_2fa'] == 'email') { // verify code set to user's data_2fa field $sel_stmt = Database::prepare("SELECT `data_2fa` FROM " . $table . " WHERE `" . $field . "` = :uid"); $userinfo_code = Database::pexecute_first($sel_stmt, ['uid' => $uid]); // 60sec discrepancy (possible slow email delivery) $result = $tfa->verifyCode($userinfo_code['data_2fa'], $code, 60); } else { $result = $tfa->verifyCode($_SESSION['secret_2fa'], $code, 3); } // either the code is valid when using authenticator-app, or we will select userdata by id and entered code // which is temporarily stored for the customer when using email-2fa if ($result) { $sel_param = [ 'uid' => $uid ]; $sel_stmt = Database::prepare("SELECT * FROM " . $table . " WHERE `" . $field . "` = :uid"); $userinfo = Database::pexecute_first($sel_stmt, $sel_param); // whoops, no (valid) user? Start again if (empty($userinfo)) { Response::redirectTo('index.php', [ 'showmessage' => '2' ]); } // set fields in $userinfo required for finishLogin() $userinfo['adminsession'] = $isadmin; $userinfo['userid'] = $uid; // when using email-2fa, remove the one-time-code if ($userinfo['type_2fa'] == '1') { $del_stmt = Database::prepare("UPDATE " . $table . " SET `data_2fa` = '' WHERE `" . $field . "` = :uid"); Database::pexecute_first($del_stmt, [ 'uid' => $uid ]); } // when remember is activated, set the cookie if ($remember) { $selector = base64_encode(Froxlor::genSessionId(9)); $authenticator = Froxlor::genSessionId(33); $valid_until = time()+60*60*24*30; $ins_stmt = Database::prepare(" INSERT INTO `".TABLE_PANEL_2FA_TOKENS."` SET `selector` = :selector, `token` = :authenticator, `userid` = :userid, `valid_until` = :valid_until "); Database::pexecute($ins_stmt, [ 'selector' => $selector, 'authenticator' => hash('sha256', $authenticator), 'userid' => $uid, 'valid_until' => $valid_until ]); $cookie_params = [ 'expires' => $valid_until, // 30 days 'path' => '/', 'domain' => UI::getCookieHost(), 'secure' => UI::requestIsHttps(), 'httponly' => true, 'samesite' => 'Strict' ]; setcookie('frx_2fa_remember', $selector.':'.base64_encode($authenticator), $cookie_params); } // if not successful somehow - start again if (!finishLogin($userinfo)) { Response::redirectTo('index.php', [ 'showmessage' => '2' ]); } exit(); } // wrong 2fa code - treat like "wrong password" $stmt = Database::prepare(" UPDATE " . $table . " SET `lastlogin_fail`= :lastlogin_fail, `loginfail_count`=`loginfail_count`+1 WHERE `" . $field . "`= :uid "); Database::pexecute($stmt, [ "lastlogin_fail" => time(), "uid" => $uid ]); // get data for processing further $stmt = Database::prepare(" SELECT `loginname`, `loginfail_count`, `lastlogin_fail` FROM " . $table . " WHERE `" . $field . "`= :uid "); $fail_user = Database::pexecute_first($stmt, [ "uid" => $uid ]); if ($fail_user['loginfail_count'] >= Settings::Get('login.maxloginattempts') && $fail_user['lastlogin_fail'] > (time() - Settings::Get('login.deactivatetime'))) { // Log failed login $rstlog = FroxlorLogger::getInstanceOf([ 'loginname' => $_SERVER['REMOTE_ADDR'] ]); $rstlog->logAction(FroxlorLogger::LOGIN_ACTION, LOG_WARNING, "User '" . $fail_user['loginname'] . "' entered wrong 2fa code too often."); unset($fail_user); Response::redirectTo('index.php', [ 'showmessage' => '3' ]); exit(); } unset($fail_user); // back to form Response::redirectTo('index.php', [ 'action' => '2fa_entercode', 'showmessage' => '1' ]); exit(); } elseif ($action == 'login') { if (!empty($_POST)) { $loginname = Validate::validate(Request::post('loginname'), 'loginname'); $password = Validate::validate(Request::post('password'), 'password'); $select_additional = ''; if (Settings::Get('panel.db_version') >= 202312230) { $select_additional = ' AND `gui_access` = 1'; } $stmt = Database::prepare(" SELECT `loginname` AS `customer` FROM `" . TABLE_PANEL_CUSTOMERS . "` WHERE `loginname`= :loginname" . $select_additional ); Database::pexecute($stmt, [ "loginname" => $loginname ]); $row = $stmt->fetch(PDO::FETCH_ASSOC); $is_admin = false; $table = ""; if ($row && $row['customer'] == $loginname) { $table = "`" . TABLE_PANEL_CUSTOMERS . "`"; $uid = 'customerid'; $adminsession = '0'; } else { if ((int)Settings::Get('login.domain_login') == 1) { $domainname = $idna_convert->encode(preg_replace([ '/\:(\d)+$/', '/^https?\:\/\//' ], '', $loginname)); $stmt = Database::prepare(" SELECT `customerid` FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `domain` = :domain "); Database::pexecute($stmt, [ "domain" => $domainname ]); $row2 = $stmt->fetch(PDO::FETCH_ASSOC); if (isset($row2['customerid']) && $row2['customerid'] > 0) { $loginname = Customer::getCustomerDetail($row2['customerid'], 'loginname'); if ($loginname !== false) { $stmt = Database::prepare(" SELECT `loginname` AS `customer` FROM `" . TABLE_PANEL_CUSTOMERS . "` WHERE `loginname`= :loginname "); Database::pexecute($stmt, [ "loginname" => $loginname ]); $row3 = $stmt->fetch(PDO::FETCH_ASSOC); if ($row3 && $row3['customer'] == $loginname) { $table = "`" . TABLE_PANEL_CUSTOMERS . "`"; $uid = 'customerid'; $adminsession = '0'; } } } } } if (empty($table)) { // try login as admin of no customer-login method worked $is_admin = true; } if ((Froxlor::hasUpdates() || Froxlor::hasDbUpdates()) && $is_admin == false) { Response::redirectTo('index.php'); exit(); } if ($is_admin) { if (Froxlor::hasUpdates() || Froxlor::hasDbUpdates()) { $stmt = Database::prepare(" SELECT `loginname` AS `admin` FROM `" . TABLE_PANEL_ADMINS . "` WHERE `loginname`= :loginname AND `change_serversettings` = '1' "); Database::pexecute($stmt, [ "loginname" => $loginname ]); $row = $stmt->fetch(PDO::FETCH_ASSOC); if (!isset($row['admin'])) { // not an admin who can see updates Response::redirectTo('index.php'); exit(); } } else { $select_additional = ''; if (Settings::Get('panel.db_version') >= 202312230) { $select_additional = ' AND `gui_access` = 1'; } $stmt = Database::prepare(" SELECT `loginname` AS `admin` FROM `" . TABLE_PANEL_ADMINS . "` WHERE `loginname`= :loginname" . $select_additional ); Database::pexecute($stmt, [ "loginname" => $loginname ]); $row = $stmt->fetch(PDO::FETCH_ASSOC); } if ($row && $row['admin'] == $loginname) { $table = "`" . TABLE_PANEL_ADMINS . "`"; $uid = 'adminid'; $adminsession = '1'; } else { // Log failed login $rstlog = FroxlorLogger::getInstanceOf([ 'loginname' => $_SERVER['REMOTE_ADDR'] ]); $rstlog->logAction(FroxlorLogger::LOGIN_ACTION, LOG_WARNING, "Unknown user tried to login."); Response::redirectTo('index.php', [ 'showmessage' => '2' ]); exit(); } } $userinfo_stmt = Database::prepare(" SELECT * FROM $table WHERE `loginname`= :loginname "); Database::pexecute($userinfo_stmt, [ "loginname" => $loginname ]); $userinfo = $userinfo_stmt->fetch(PDO::FETCH_ASSOC); if ($userinfo['loginfail_count'] >= Settings::Get('login.maxloginattempts') && $userinfo['lastlogin_fail'] > (time() - Settings::Get('login.deactivatetime'))) { Response::redirectTo('index.php', [ 'showmessage' => '3' ]); exit(); } elseif (Crypt::validatePasswordLogin($userinfo, $password, $table, $uid)) { // only show "you're banned" if the login was successful // because we don't want to publish that the user does exist if ($userinfo['deactivated']) { unset($userinfo); Response::redirectTo('index.php', [ 'showmessage' => '5' ]); exit(); } else { // login correct // reset loginfail_counter, set lastlogin_succ $stmt = Database::prepare(" UPDATE $table SET `lastlogin_succ`= :lastlogin_succ, `loginfail_count`='0' WHERE `$uid`= :uid "); Database::pexecute($stmt, [ "lastlogin_succ" => time(), "uid" => $userinfo[$uid] ]); $userinfo['userid'] = $userinfo[$uid]; $userinfo['adminsession'] = $adminsession; } } else { // login incorrect $stmt = Database::prepare(" UPDATE $table SET `lastlogin_fail`= :lastlogin_fail, `loginfail_count`=`loginfail_count`+1 WHERE `$uid`= :uid "); Database::pexecute($stmt, [ "lastlogin_fail" => time(), "uid" => $userinfo[$uid] ]); // Log failed login $rstlog = FroxlorLogger::getInstanceOf([ 'loginname' => $_SERVER['REMOTE_ADDR'] ]); $rstlog->logAction(FroxlorLogger::LOGIN_ACTION, LOG_WARNING, "User tried to login with wrong password."); unset($userinfo); Response::redirectTo('index.php', [ 'showmessage' => '2' ]); exit(); } // 2FA activated if (Settings::Get('2fa.enabled') == '1' && $userinfo['type_2fa'] > 0) { // check for remember cookie if (!empty($_COOKIE['frx_2fa_remember'])) { list($selector, $authenticator) = explode(':', $_COOKIE['frx_2fa_remember']); $sel_stmt = Database::prepare("SELECT `token` FROM `".TABLE_PANEL_2FA_TOKENS."` WHERE `selector` = :selector AND `userid` = :uid AND `valid_until` >= UNIX_TIMESTAMP()"); $token_check = Database::pexecute_first($sel_stmt, ['selector' => $selector, 'uid' => $userinfo[$uid]]); if ($token_check && hash_equals($token_check['token'], hash('sha256', base64_decode($authenticator)))) { if (!finishLogin($userinfo)) { Response::redirectTo('index.php', [ 'showmessage' => '2' ]); } exit(); } // not found or invalid, this cookie is useless, get rid of it unset($_COOKIE['frx_2fa_remember']); setcookie('frx_2fa_remember', "", time()-3600); } // redirect to code-enter-page $_SESSION['secret_2fa'] = ($userinfo['type_2fa'] == 2 ? $userinfo['data_2fa'] : 'email'); $_SESSION['uid_2fa'] = $userinfo[$uid]; $_SESSION['uidfield_2fa'] = $uid; $_SESSION['uidtable_2fa'] = $table; $_SESSION['unfo_2fa'] = $is_admin; // send mail if type_2fa = 1 (email) if ($userinfo['type_2fa'] == 1) { // generate code $tfa = new FroxlorTwoFactorAuth('Froxlor ' . Settings::Get('system.hostname')); $secret = $tfa->createSecret(); $code = $tfa->getCode($secret); // set code for user $stmt = Database::prepare("UPDATE $table SET `data_2fa` = :d2fa WHERE `$uid` = :uid"); Database::pexecute($stmt, [ "d2fa" => $secret, "uid" => $userinfo[$uid] ]); // build up & send email $_mailerror = false; $mailerr_msg = ""; $replace_arr = [ 'CODE' => $code ]; $mail_body = html_entity_decode(PhpHelper::replaceVariables(lng('mails.2fa.mailbody'), $replace_arr)); try { $mail->Subject = lng('mails.2fa.subject'); $mail->AltBody = $mail_body; $mail->MsgHTML(str_replace("\n", "
", $mail_body)); $mail->AddAddress($userinfo['email'], User::getCorrectUserSalutation($userinfo)); $mail->Send(); } catch (\PHPMailer\PHPMailer\Exception $e) { $mailerr_msg = $e->errorMessage(); $_mailerror = true; } catch (Exception $e) { $mailerr_msg = $e->getMessage(); $_mailerror = true; } if ($_mailerror) { $rstlog = FroxlorLogger::getInstanceOf([ 'loginname' => '2fa code-sending' ]); $rstlog->logAction(FroxlorLogger::ADM_ACTION, LOG_ERR, "Error sending mail: " . $mailerr_msg); Response::redirectTo('index.php', [ 'showmessage' => '4', 'customermail' => $userinfo['email'] ]); exit(); } $mail->ClearAddresses(); } Response::redirectTo('index.php', [ 'action' => '2fa_entercode' ]); exit(); } if (!finishLogin($userinfo)) { Response::redirectTo('index.php', [ 'showmessage' => '2' ]); } exit(); } else { $smessage = (int)Request::get('showmessage', 0); $message = ''; $successmessage = ''; switch ($smessage) { case 1: $successmessage = lng('pwdreminder.success'); break; case 2: $message = lng('error.login'); break; case 3: $message = lng('error.login_blocked', [Settings::Get('login.deactivatetime')]); break; case 4: $message = lng('error.errorsendingmailpub'); break; case 5: $message = lng('error.user_banned'); break; case 6: $successmessage = lng('pwdreminder.changed'); break; case 7: $message = lng('pwdreminder.wrongcode'); break; case 8: $message = lng('pwdreminder.notallowed'); break; } $update_in_progress = false; if (Froxlor::hasUpdates() || Froxlor::hasDbUpdates()) { $update_in_progress = true; } // Pass the last used page if needed $lastscript = Request::any('script', ''); if (!empty($lastscript)) { $lastscript = str_replace("..", "", $lastscript); $lastscript = htmlspecialchars($lastscript, ENT_QUOTES); if (file_exists(__DIR__ . "/" . $lastscript)) { $_SESSION['lastscript'] = $lastscript; } else { $lastscript = ""; } } $lastqrystr = Request::any('qrystr', ''); if (!empty($lastqrystr)) { $lastqrystr = urlencode($lastqrystr); $_SESSION['lastqrystr'] = $lastqrystr; } UI::view('login/login.html.twig', [ 'pagetitle' => 'Login', 'upd_in_progress' => $update_in_progress, 'message' => $message, 'successmsg' => $successmessage ]); } } if ($action == 'forgotpwd') { $adminchecked = false; $message = ''; if (!empty($_POST)) { $loginname = Validate::validate(Request::post('loginname'), 'loginname'); $email = Validate::validateEmail(Request::post('loginemail')); $result_stmt = Database::prepare("SELECT `adminid`, `customerid`, `customernumber`, `firstname`, `name`, `company`, `email`, `loginname`, `def_language`, `deactivated` FROM `" . TABLE_PANEL_CUSTOMERS . "` WHERE `loginname`= :loginname AND `email`= :email"); Database::pexecute($result_stmt, [ "loginname" => $loginname, "email" => $email ]); if (Database::num_rows() == 0) { $result_stmt = Database::prepare("SELECT `adminid`, `name`, `email`, `loginname`, `def_language`, `deactivated` FROM `" . TABLE_PANEL_ADMINS . "` WHERE `loginname`= :loginname AND `email`= :email"); Database::pexecute($result_stmt, [ "loginname" => $loginname, "email" => $email ]); if (Database::num_rows() > 0) { $adminchecked = true; } else { $result_stmt = null; } } if ($adminchecked) { if (Settings::Get('panel.allow_preset_admin') != '1') { $message = lng('pwdreminder.notallowed'); unset($adminchecked); } } else { if (Settings::Get('panel.allow_preset') != '1') { $message = lng('pwdreminder.notallowed'); } } if (empty($message)) { if ($result_stmt !== null) { $user = $result_stmt->fetch(PDO::FETCH_ASSOC); /* Check whether user is banned */ if ($user['deactivated']) { $message = lng('pwdreminder.notallowed'); } else { if (($adminchecked && Settings::Get('panel.allow_preset_admin') == '1') || $adminchecked == false) { if ($user !== false) { // build a activation code $timestamp = time(); $first = substr(md5($user['loginname'] . $timestamp . PhpHelper::randomStr(16)), 0, 15); $third = substr(md5($user['email'] . $timestamp . PhpHelper::randomStr(16)), -15); $activationcode = $first . $timestamp . $third . substr(md5($third . $timestamp), 0, 10); // Drop all existing activation codes for this user $stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_ACTIVATION . "` WHERE `userid` = :userid AND `admin` = :admin"); $params = [ "userid" => $adminchecked ? $user['adminid'] : $user['customerid'], "admin" => $adminchecked ? 1 : 0 ]; Database::pexecute($stmt, $params); // Add new activation code to database $stmt = Database::prepare("INSERT INTO `" . TABLE_PANEL_ACTIVATION . "` (userid, admin, creation, activationcode) VALUES (:userid, :admin, :creation, :activationcode)"); $params = [ "userid" => $adminchecked ? $user['adminid'] : $user['customerid'], "admin" => $adminchecked ? 1 : 0, "creation" => $timestamp, "activationcode" => $activationcode ]; Database::pexecute($stmt, $params); $rstlog = FroxlorLogger::getInstanceOf([ 'loginname' => 'password_reset' ]); $rstlog->logAction(FroxlorLogger::USR_ACTION, LOG_WARNING, "User '" . $user['loginname'] . "' requested a link for setting a new password."); // Set together our activation link $protocol = empty($_SERVER['HTTPS']) ? 'http' : 'https'; // this can be a fixed value to avoid potential exploiting by modifying headers $host = Settings::Get('system.hostname'); // $_SERVER['HTTP_HOST']; $port = $_SERVER['SERVER_PORT'] != 80 ? ':' . $_SERVER['SERVER_PORT'] : ''; // don't add :443 when https is used, as it is default (and just looks weird!) if ($protocol == 'https' && $_SERVER['SERVER_PORT'] == '443') { $port = ''; } // there can be only one script to handle this so we can use a fixed value here $script = "/index.php"; // $_SERVER['SCRIPT_NAME']; if (Settings::Get('system.froxlordirectlyviahostname') == 0) { $script = FileDir::makeCorrectFile("/" . basename(__DIR__) . "/" . $script); } $activationlink = $protocol . '://' . $host . $port . $script . '?action=resetpwd&resetcode=' . $activationcode; $replace_arr = [ 'SALUTATION' => User::getCorrectUserSalutation($user), 'NAME' => $user['name'], 'FIRSTNAME' => $user['firstname'] ?? "", 'COMPANY' => $user['company'] ?? "", 'CUSTOMER_NO' => $user['customernumber'] ?? 0, 'USERNAME' => $loginname, 'LINK' => $activationlink ]; $def_language = ($user['def_language'] != '') ? $user['def_language'] : Settings::Get('panel.standardlanguage'); $result_stmt = Database::prepare('SELECT `value` FROM `' . TABLE_PANEL_TEMPLATES . '` WHERE `adminid`= :adminid AND `language`= :lang AND `templategroup`=\'mails\' AND `varname`=\'password_reset_subject\''); Database::pexecute($result_stmt, [ "adminid" => $user['adminid'], "lang" => $def_language ]); $result = $result_stmt->fetch(PDO::FETCH_ASSOC); $mail_subject = html_entity_decode(PhpHelper::replaceVariables((($result['value'] != '') ? $result['value'] : lng('mails.password_reset.subject')), $replace_arr)); $result_stmt = Database::prepare('SELECT `value` FROM `' . TABLE_PANEL_TEMPLATES . '` WHERE `adminid`= :adminid AND `language`= :lang AND `templategroup`=\'mails\' AND `varname`=\'password_reset_mailbody\''); Database::pexecute($result_stmt, [ "adminid" => $user['adminid'], "lang" => $def_language ]); $result = $result_stmt->fetch(PDO::FETCH_ASSOC); $mail_body = html_entity_decode(PhpHelper::replaceVariables((($result['value'] != '') ? $result['value'] : lng('mails.password_reset.mailbody')), $replace_arr)); $_mailerror = false; $mailerr_msg = ""; try { $mail->Subject = $mail_subject; $mail->AltBody = $mail_body; $mail->MsgHTML(str_replace("\n", "
", $mail_body)); $mail->AddAddress($user['email'], User::getCorrectUserSalutation($user)); $mail->Send(); } catch (\PHPMailer\PHPMailer\Exception $e) { $mailerr_msg = $e->errorMessage(); $_mailerror = true; } catch (Exception $e) { $mailerr_msg = $e->getMessage(); $_mailerror = true; } if ($_mailerror) { $rstlog = FroxlorLogger::getInstanceOf([ 'loginname' => 'password_reset' ]); $rstlog->logAction(FroxlorLogger::ADM_ACTION, LOG_ERR, "Error sending mail: " . $mailerr_msg); Response::redirectTo('index.php', [ 'showmessage' => '4', 'customermail' => $user['email'] ]); exit(); } $mail->ClearAddresses(); Response::redirectTo('index.php', [ 'showmessage' => '1' ]); exit(); } else { $rstlog = FroxlorLogger::getInstanceOf([ 'loginname' => 'password_reset' ]); $rstlog->logAction(FroxlorLogger::USR_ACTION, LOG_WARNING, "Unknown user requested to set a new password, but was not found in database!"); $message = lng('login.usernotfound'); } unset($user); } } } else { $message = lng('pwdreminder.notallowed'); } } } UI::view('login/fpwd.html.twig', [ 'pagetitle' => lng('login.presend'), 'formaction' => 'index.php?action=' . $action, 'message' => $message, ]); } if ($action == 'resetpwd') { $message = ''; // Remove old activation codes $stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_ACTIVATION . "` WHERE creation < :oldest"); Database::pexecute($stmt, [ "oldest" => time() - 86400 ]); $activationcode = Request::get('resetcode'); if (!empty($activationcode) && strlen($activationcode) == 50) { // Check if activation code is valid $timestamp = substr($activationcode, 15, 10); $third = substr($activationcode, 25, 15); $check = substr($activationcode, 40, 10); if (substr(md5($third . $timestamp), 0, 10) == $check && $timestamp >= time() - 86400) { if (!empty($_POST)) { $stmt = Database::prepare("SELECT `userid`, `admin` FROM `" . TABLE_PANEL_ACTIVATION . "` WHERE `activationcode` = :activationcode"); $result = Database::pexecute_first($stmt, [ "activationcode" => $activationcode ]); if ($result !== false) { try { $new_password = Crypt::validatePassword(Request::post('new_password'), true); $new_password_confirm = Crypt::validatePassword(Request::post('new_password_confirm'), true); } catch (Exception $e) { $message = $e->getMessage(); } if (empty($message) && (empty($new_password) || $new_password != $new_password_confirm)) { $message = lng('error.newpasswordconfirmerror'); } if (empty($message)) { // Update user password if ($result['admin'] == 1) { $stmt = Database::prepare("UPDATE `" . TABLE_PANEL_ADMINS . "` SET `password` = :newpassword WHERE `adminid` = :userid"); } else { $stmt = Database::prepare("UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `password` = :newpassword WHERE `customerid` = :userid"); } Database::pexecute($stmt, [ "newpassword" => Crypt::makeCryptPassword($new_password), "userid" => $result['userid'] ]); $rstlog = FroxlorLogger::getInstanceOf([ 'loginname' => 'password_reset' ]); $rstlog->logAction(FroxlorLogger::USR_ACTION, LOG_NOTICE, "changed password using password reset."); // Remove activation code from DB $stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_ACTIVATION . "` WHERE `activationcode` = :activationcode AND `userid` = :userid"); Database::pexecute($stmt, [ "activationcode" => $activationcode, "userid" => $result['userid'] ]); Response::redirectTo('index.php', [ "showmessage" => '6' ]); } } else { Response::redirectTo('index.php', [ "showmessage" => '7' ]); } } UI::view('login/rpwd.html.twig', [ 'pagetitle' => lng('pwdreminder.choosenew'), 'formaction' => 'index.php?action=resetpwd&resetcode=' . $activationcode, 'message' => $message, ]); } else { Response::redirectTo('index.php', [ "showmessage" => '7' ]); } } else { Response::redirectTo('index.php'); } } // one-time link login if ($action == 'll') { if (!Froxlor::hasUpdates() && !Froxlor::hasDbUpdates()) { $loginname = Request::get('ln'); $hash = Request::get('h'); if ($loginname && $hash) { $sel_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_LOGINLINKS . "` WHERE `loginname` = :loginname AND `hash` = :hash "); try { $entry = Database::pexecute_first($sel_stmt, ['loginname' => $loginname, 'hash' => $hash]); } catch (Exception $e) { $entry = false; } if ($entry) { // delete entry $del_stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_LOGINLINKS . "` WHERE `loginname` = :loginname AND `hash` = :hash"); Database::pexecute($del_stmt, ['loginname' => $loginname, 'hash' => $hash]); if (time() <= $entry['valid_until']) { $valid = true; // validate source ip if specified if (!empty($entry['allowed_from'])) { $valid = false; $ip_list = explode(",", $entry['allowed_from']); if (FroxlorRPC::validateAllowedFrom($ip_list, $_SERVER['REMOTE_ADDR'])) { $valid = true; } } if ($valid) { // login user / select only non-deactivated (in case the user got deactivated after generating the link) $userinfo_stmt = Database::prepare("SELECT * FROM `" . TABLE_PANEL_CUSTOMERS . "` WHERE `loginname`= :loginname AND `deactivated` = 0"); try { $userinfo = Database::pexecute_first($userinfo_stmt, [ "loginname" => $loginname ]); } catch (Exception $e) { $userinfo = false; } if ($userinfo) { $userinfo['userid'] = $userinfo['customerid']; $userinfo['adminsession'] = 0; finishLogin($userinfo); } } } } } } Response::redirectTo('index.php'); } function finishLogin($userinfo) { if (isset($userinfo['userid']) && $userinfo['userid'] != '') { session_regenerate_id(true); CurrentUser::setData($userinfo); $language = $userinfo['def_language'] ?? Settings::Get('panel.standardlanguage'); CurrentUser::setField('language', $language); if (isset($userinfo['theme']) && $userinfo['theme'] != '') { $theme = $userinfo['theme']; } else { $theme = Settings::Get('panel.default_theme'); } CurrentUser::setField('theme', $theme); $qryparams = []; if (!empty($_SESSION['lastqrystr'])) { parse_str(urldecode($_SESSION['lastqrystr']), $qryparams); unset($_SESSION['lastqrystr']); } if ($userinfo['adminsession'] == '1') { if (Froxlor::hasUpdates() || Froxlor::hasDbUpdates()) { Response::redirectTo('admin_updates.php?page=overview'); } else { if (!empty($_SESSION['lastscript'])) { $lastscript = $_SESSION['lastscript']; unset($_SESSION['lastscript']); if (preg_match("/customer\_/", $lastscript) === 1) { Response::redirectTo('admin_customers.php', [ "page" => "customers" ]); } else { Response::redirectTo($lastscript, $qryparams); } } else { Response::redirectTo('admin_index.php', $qryparams); } } } else { if (!empty($_SESSION['lastscript'])) { $lastscript = $_SESSION['lastscript']; unset($_SESSION['lastscript']); Response::redirectTo($lastscript, $qryparams); } else { Response::redirectTo('customer_index.php', $qryparams); } } } return false; } ================================================ FILE: install/froxlor.sql.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ return <<<'FROXLORSQL' DROP TABLE IF EXISTS `ftp_groups`; CREATE TABLE `ftp_groups` ( `id` int(20) NOT NULL auto_increment, `groupname` varchar(60) NOT NULL default '', `gid` int(5) NOT NULL default '0', `members` longtext NOT NULL, `customerid` int(11) NOT NULL default '0', PRIMARY KEY (`id`), UNIQUE KEY `groupname` (`groupname`), KEY `customerid` (`customerid`) ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; DROP TABLE IF EXISTS `ftp_users`; CREATE TABLE `ftp_users` ( `id` int(20) NOT NULL auto_increment, `username` varchar(255) NOT NULL, `uid` int(5) NOT NULL default '0', `gid` int(5) NOT NULL default '0', `password` varchar(255) NOT NULL, `homedir` varchar(255) NOT NULL default '', `shell` varchar(255) NOT NULL default '/bin/false', `login_enabled` enum('N','Y') NOT NULL default 'N', `login_count` int(15) NOT NULL default '0', `last_login` datetime default NULL, `up_count` int(15) NOT NULL default '0', `up_bytes` bigint(30) NOT NULL default '0', `down_count` int(15) NOT NULL default '0', `down_bytes` bigint(30) NOT NULL default '0', `customerid` int(11) NOT NULL default '0', `description` varchar(255) NOT NULL DEFAULT '', PRIMARY KEY (`id`), UNIQUE KEY `username` (`username`), KEY `customerid` (`customerid`) ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; DROP TABLE IF EXISTS `mail_users`; CREATE TABLE `mail_users` ( `id` int(11) NOT NULL auto_increment, `email` varchar(255) NOT NULL default '', `username` varchar(255) NOT NULL default '', `password` varchar(255) NOT NULL default '', `password_enc` varchar(255) NOT NULL default '', `uid` int(11) NOT NULL default '0', `gid` int(11) NOT NULL default '0', `homedir` varchar(255) NOT NULL default '', `maildir` varchar(255) NOT NULL default '', `postfix` enum('Y','N') NOT NULL default 'Y', `domainid` int(11) NOT NULL default '0', `customerid` int(11) NOT NULL default '0', `quota` bigint(13) NOT NULL default '0', `pop3` tinyint(1) NOT NULL default '1', `imap` tinyint(1) NOT NULL default '1', `mboxsize` bigint(30) NOT NULL default '0', PRIMARY KEY (`id`), UNIQUE KEY `email` (`email`) ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; DROP TABLE IF EXISTS `mail_virtual`; CREATE TABLE `mail_virtual` ( `id` int(11) NOT NULL auto_increment, `email` varchar(255) NOT NULL default '', `email_full` varchar(255) NOT NULL default '', `destination` text, `domainid` int(11) NOT NULL default '0', `customerid` int(11) NOT NULL default '0', `popaccountid` int(11) NOT NULL default '0', `iscatchall` tinyint(1) unsigned NOT NULL default '0', `description` varchar(255) NOT NULL DEFAULT '', `spam_tag_level` float(4,1) NOT NULL DEFAULT 7.0, `rewrite_subject` tinyint(1) NOT NULL default '1', `spam_kill_level` float(4,1) NOT NULL DEFAULT 14.0, `bypass_spam` tinyint(1) NOT NULL default '0', `policy_greylist` tinyint(1) NOT NULL default '1', PRIMARY KEY (`id`), KEY `email` (`email`) ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; DROP TABLE IF EXISTS `mail_sender_aliases`; CREATE TABLE `mail_sender_aliases` ( `id` int(11) NOT NULL auto_increment, `email` varchar(255) NOT NULL, `allowed_sender` varchar(255) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `email_sender` (`email`, `allowed_sender`) ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; DROP TABLE IF EXISTS `panel_activation`; CREATE TABLE `panel_activation` ( `id` int(11) unsigned NOT NULL auto_increment, `userid` int(11) unsigned NOT NULL default '0', `admin` tinyint(1) unsigned NOT NULL default '0', `creation` int(11) unsigned NOT NULL default '0', `activationcode` varchar(50) default NULL, PRIMARY KEY (id) ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; DROP TABLE IF EXISTS `panel_admins`; CREATE TABLE `panel_admins` ( `adminid` int(11) unsigned NOT NULL auto_increment, `loginname` varchar(50) NOT NULL, `password` varchar(255) NOT NULL, `name` varchar(255) NOT NULL default '', `email` varchar(255) NOT NULL default '', `def_language` varchar(100) NOT NULL default '', `ip` varchar(500) NOT NULL default '-1', `customers` int(15) NOT NULL default '0', `customers_used` int(15) NOT NULL default '0', `customers_see_all` tinyint(1) NOT NULL default '0', `domains` int(15) NOT NULL default '0', `domains_used` int(15) NOT NULL default '0', `caneditphpsettings` tinyint(1) NOT NULL default '0', `change_serversettings` tinyint(1) NOT NULL default '0', `diskspace` int(15) NOT NULL default '0', `diskspace_used` int(15) NOT NULL default '0', `mysqls` int(15) NOT NULL default '0', `mysqls_used` int(15) NOT NULL default '0', `emails` int(15) NOT NULL default '0', `emails_used` int(15) NOT NULL default '0', `email_accounts` int(15) NOT NULL default '0', `email_accounts_used` int(15) NOT NULL default '0', `email_forwarders` int(15) NOT NULL default '0', `email_forwarders_used` int(15) NOT NULL default '0', `email_quota` bigint(13) NOT NULL default '0', `email_quota_used` bigint(13) NOT NULL default '0', `ftps` int(15) NOT NULL default '0', `ftps_used` int(15) NOT NULL default '0', `subdomains` int(15) NOT NULL default '0', `subdomains_used` int(15) NOT NULL default '0', `traffic` bigint(30) NOT NULL default '0', `traffic_used` bigint(30) NOT NULL default '0', `deactivated` tinyint(1) NOT NULL default '0', `lastlogin_succ` int(11) unsigned NOT NULL default '0', `lastlogin_fail` int(11) unsigned NOT NULL default '0', `loginfail_count` int(11) unsigned NOT NULL default '0', `reportsent` tinyint(4) unsigned NOT NULL default '0', `theme` varchar(50) NOT NULL default 'Froxlor', `custom_notes` text, `custom_notes_show` tinyint(1) NOT NULL default '0', `type_2fa` tinyint(1) NOT NULL default '0', `data_2fa` varchar(25) NOT NULL default '', `api_allowed` tinyint(1) NOT NULL default '1', `gui_access` tinyint(1) NOT NULL default '1', PRIMARY KEY (`adminid`), UNIQUE KEY `loginname` (`loginname`) ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci ROW_FORMAT=DYNAMIC; DROP TABLE IF EXISTS `panel_customers`; CREATE TABLE `panel_customers` ( `customerid` int(11) unsigned NOT NULL auto_increment, `loginname` varchar(50) NOT NULL, `password` varchar(255) NOT NULL default '', `adminid` int(11) unsigned NOT NULL default '0', `name` varchar(255) NOT NULL default '', `firstname` varchar(255) NOT NULL default '', `gender` int(1) NOT NULL DEFAULT '0', `company` varchar(255) NOT NULL default '', `street` varchar(255) NOT NULL default '', `zipcode` varchar(25) NOT NULL default '', `city` varchar(255) NOT NULL default '', `phone` varchar(50) NOT NULL default '', `fax` varchar(50) NOT NULL default '', `email` varchar(255) NOT NULL default '', `customernumber` varchar(100) NOT NULL default '', `def_language` varchar(100) NOT NULL default '', `diskspace` bigint(30) NOT NULL default '0', `diskspace_used` bigint(30) NOT NULL default '0', `mysqls` int(15) NOT NULL default '0', `mysqls_used` int(15) NOT NULL default '0', `emails` int(15) NOT NULL default '0', `emails_used` int(15) NOT NULL default '0', `email_accounts` int(15) NOT NULL default '0', `email_accounts_used` int(15) NOT NULL default '0', `email_forwarders` int(15) NOT NULL default '0', `email_forwarders_used` int(15) NOT NULL default '0', `email_quota` bigint(13) NOT NULL default '0', `email_quota_used` bigint(13) NOT NULL default '0', `ftps` int(15) NOT NULL default '0', `ftps_used` int(15) NOT NULL default '0', `subdomains` int(15) NOT NULL default '0', `subdomains_used` int(15) NOT NULL default '0', `traffic` bigint(30) NOT NULL default '0', `traffic_used` bigint(30) NOT NULL default '0', `documentroot` varchar(255) NOT NULL default '', `standardsubdomain` int(11) NOT NULL default '0', `guid` int(5) NOT NULL default '0', `ftp_lastaccountnumber` int(11) NOT NULL default '0', `mysql_lastaccountnumber` int(11) NOT NULL default '0', `deactivated` tinyint(1) NOT NULL default '0', `phpenabled` tinyint(1) NOT NULL default '1', `lastlogin_succ` int(11) unsigned NOT NULL default '0', `lastlogin_fail` int(11) unsigned NOT NULL default '0', `loginfail_count` int(11) unsigned NOT NULL default '0', `reportsent` tinyint(4) unsigned NOT NULL default '0', `pop3` tinyint(1) NOT NULL default '1', `imap` tinyint(1) NOT NULL default '1', `perlenabled` tinyint(1) NOT NULL default '0', `dnsenabled` tinyint(1) NOT NULL default '0', `theme` varchar(50) NOT NULL default 'Froxlor', `custom_notes` text, `custom_notes_show` tinyint(1) NOT NULL default '0', `lepublickey` mediumtext default NULL, `leprivatekey` mediumtext default NULL, `leregistered` tinyint(1) NOT NULL default '0', `allowed_phpconfigs` text NOT NULL, `type_2fa` tinyint(1) NOT NULL default '0', `data_2fa` varchar(25) NOT NULL default '', `api_allowed` tinyint(1) NOT NULL default '1', `shell_allowed` tinyint(1) NOT NULL default '0', `logviewenabled` tinyint(1) NOT NULL default '0', `allowed_mysqlserver` text NOT NULL, `gui_access` tinyint(1) NOT NULL default '1', PRIMARY KEY (`customerid`), UNIQUE KEY `loginname` (`loginname`) ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci ROW_FORMAT=DYNAMIC; DROP TABLE IF EXISTS `panel_databases`; CREATE TABLE `panel_databases` ( `id` int(11) unsigned NOT NULL auto_increment, `customerid` int(11) NOT NULL default '0', `databasename` varchar(255) NOT NULL default '', `description` varchar(255) NOT NULL default '', `dbserver` int(11) unsigned NOT NULL default '0', PRIMARY KEY (`id`), KEY `customerid` (`customerid`) ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; DROP TABLE IF EXISTS `panel_domains`; CREATE TABLE `panel_domains` ( `id` int(11) unsigned NOT NULL auto_increment, `domain` varchar(255) NOT NULL, `domain_ace` varchar(255) NOT NULL default '', `adminid` int(11) unsigned NOT NULL default '0', `customerid` int(11) unsigned NOT NULL default '0', `aliasdomain` int(11) unsigned NULL, `documentroot` varchar(255) NOT NULL default '', `isbinddomain` tinyint(1) NOT NULL default '0', `isemaildomain` tinyint(1) NOT NULL default '0', `email_only` tinyint(1) NOT NULL default '0', `iswildcarddomain` tinyint(1) NOT NULL default '1', `subcanemaildomain` tinyint(1) NOT NULL default '0', `caneditdomain` tinyint(1) NOT NULL default '1', `zonefile` varchar(255) NOT NULL default '', `dkim` tinyint(1) NOT NULL default '0', `dkim_id` int(11) unsigned NOT NULL default '0', `dkim_privkey` text, `dkim_pubkey` text, `wwwserveralias` tinyint(1) NOT NULL default '1', `parentdomainid` int(11) NOT NULL default '0', `phpenabled` tinyint(1) NOT NULL default '0', `openbasedir` tinyint(1) NOT NULL default '0', `openbasedir_path` tinyint(1) NOT NULL default '0', `speciallogfile` tinyint(1) NOT NULL default '0', `ssl_redirect` tinyint(4) NOT NULL default '0', `specialsettings` text, `ssl_specialsettings` text, `include_specialsettings` tinyint(1) NOT NULL default '0', `deactivated` tinyint(1) NOT NULL default '0', `bindserial` varchar(10) NOT NULL default '2000010100', `add_date` int( 11 ) NOT NULL default '0', `registration_date` date DEFAULT NULL, `termination_date` date DEFAULT NULL, `phpsettingid` INT( 11 ) UNSIGNED NOT NULL DEFAULT '1', `mod_fcgid_starter` int(4) default '-1', `mod_fcgid_maxrequests` int(4) default '-1', `letsencrypt` tinyint(1) NOT NULL default '0', `hsts` varchar(10) NOT NULL default '0', `hsts_sub` tinyint(1) NOT NULL default '0', `hsts_preload` tinyint(1) NOT NULL default '0', `ocsp_stapling` tinyint(1) DEFAULT '0', `http2` tinyint(1) DEFAULT '0', `http3` tinyint(1) DEFAULT '0', `notryfiles` tinyint(1) DEFAULT '0', `writeaccesslog` tinyint(1) DEFAULT '1', `writeerrorlog` tinyint(1) DEFAULT '1', `override_tls` tinyint(1) DEFAULT '0', `ssl_protocols` varchar(255) NOT NULL DEFAULT '', `ssl_cipher_list` varchar(500) NOT NULL DEFAULT '', `tlsv13_cipher_list` varchar(500) NOT NULL DEFAULT '', `ssl_enabled` tinyint(1) DEFAULT '1', `ssl_honorcipherorder` tinyint(1) DEFAULT '0', `ssl_sessiontickets` tinyint(1) DEFAULT '1', `description` varchar(255) NOT NULL DEFAULT '', PRIMARY KEY (`id`), KEY `customerid` (`customerid`), KEY `parentdomain` (`parentdomainid`), KEY `domain` (`domain`) ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci ROW_FORMAT=DYNAMIC; DROP TABLE IF EXISTS `panel_ipsandports`; CREATE TABLE `panel_ipsandports` ( `id` int(11) unsigned NOT NULL auto_increment, `ip` varchar(39) NOT NULL, `port` int(5) NOT NULL default '80', `listen_statement` tinyint(1) NOT NULL default '0', `namevirtualhost_statement` tinyint(1) NOT NULL default '0', `vhostcontainer` tinyint(1) NOT NULL default '0', `vhostcontainer_servername_statement` tinyint(1) NOT NULL default '0', `specialsettings` text, `ssl` tinyint(4) NOT NULL default '0', `ssl_cert_file` varchar(255) NOT NULL default '', `ssl_key_file` varchar(255) NOT NULL default '', `ssl_ca_file` varchar(255) NOT NULL default '', `default_vhostconf_domain` text, `ssl_cert_chainfile` varchar(255) NOT NULL default '', `docroot` varchar(255) NOT NULL default '', `ssl_specialsettings` text, `include_specialsettings` tinyint(1) NOT NULL default '0', `ssl_default_vhostconf_domain` text, `include_default_vhostconf_domain` tinyint(1) NOT NULL default '0', PRIMARY KEY (`id`), UNIQUE KEY `ip_port` (`ip`,`port`) ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; DROP TABLE IF EXISTS `panel_htaccess`; CREATE TABLE `panel_htaccess` ( `id` int(11) unsigned NOT NULL auto_increment, `customerid` int(11) unsigned NOT NULL default '0', `path` varchar(255) NOT NULL default '', `options_indexes` tinyint(1) NOT NULL default '0', `error404path` varchar(255) NOT NULL default '', `error403path` varchar(255) NOT NULL default '', `error500path` varchar(255) NOT NULL default '', `error401path` varchar(255) NOT NULL default '', `options_cgi` tinyint(1) NOT NULL default '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; DROP TABLE IF EXISTS `panel_htpasswds`; CREATE TABLE `panel_htpasswds` ( `id` int(11) unsigned NOT NULL auto_increment, `customerid` int(11) unsigned NOT NULL default '0', `path` varchar(255) NOT NULL default '', `username` varchar(255) NOT NULL default '', `password` varchar(255) NOT NULL default '', `authname` varchar(255) NOT NULL default 'Restricted Area', PRIMARY KEY (`id`), KEY `customerid` (`customerid`) ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; DROP TABLE IF EXISTS `panel_settings`; CREATE TABLE `panel_settings` ( `settingid` int(11) unsigned NOT NULL auto_increment, `settinggroup` varchar(255) NOT NULL default '', `varname` varchar(255) NOT NULL default '', `value` text NOT NULL, PRIMARY KEY (`settingid`) ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; INSERT INTO `panel_settings` (`settinggroup`, `varname`, `value`) VALUES ('catchall', 'catchall_enabled', '1'), ('session', 'allow_multiple_login', '0'), ('session', 'sessiontimeout', '600'), ('customer', 'accountprefix', 'web'), ('customer', 'ftpprefix', 'ftp'), ('customer', 'mysqlprefix', 'sql'), ('customer', 'ftpatdomain', '0'), ('customer', 'show_news_feed', '0'), ('customer', 'news_feed_url', ''), ('logger', 'enabled', '1'), ('logger', 'log_cron', '0'), ('logger', 'logfile', ''), ('logger', 'logtypes', 'syslog,mysql'), ('logger', 'severity', '1'), ('antispam', 'activated', '0'), ('antispam', 'config_file', '/etc/rspamd/local.d/froxlor_settings.conf'), ('antispam', 'reload_command', 'service rspamd restart'), ('antispam', 'dkim_keylength', '1024'), ('antispam', 'default_bypass_spam', '2'), ('antispam', 'default_spam_rewrite_subject', '1'), ('antispam', 'default_policy_greylist', '1'), ('admin', 'show_news_feed', '0'), ('admin', 'show_version_login', '0'), ('admin', 'show_version_footer', '0'), ('caa', 'caa_entry', ''), ('spf', 'use_spf', '0'), ('spf', 'spf_entry', 'v=spf1 a mx -all'), ('dmarc', 'use_dmarc', '0'), ('dmarc', 'dmarc_entry', 'v=DMARC1; p=none;'), ('defaultwebsrverrhandler', 'enabled', '0'), ('defaultwebsrverrhandler', 'err401', ''), ('defaultwebsrverrhandler', 'err403', ''), ('defaultwebsrverrhandler', 'err404', ''), ('defaultwebsrverrhandler', 'err500', ''), ('customredirect', 'enabled', '1'), ('customredirect', 'default', '1'), ('perl', 'suexecworkaround', '0'), ('perl', 'suexecpath', '/var/www/cgi-bin/'), ('login', 'domain_login', '0'), ('login', 'maxloginattempts', '3'), ('login', 'deactivatetime', '900'), ('phpfpm', 'enabled', '0'), ('phpfpm', 'tmpdir', '/var/customers/tmp/'), ('phpfpm', 'peardir', '/usr/share/php/:/usr/share/php5/'), ('phpfpm', 'envpath', '/usr/local/bin:/usr/bin:/bin'), ('phpfpm', 'enabled_ownvhost', '0'), ('phpfpm', 'vhost_httpuser', 'froxlorlocal'), ('phpfpm', 'vhost_httpgroup', 'froxlorlocal'), ('phpfpm', 'aliasconfigdir', '/var/www/php-fpm/'), ('phpfpm', 'defaultini', '1'), ('phpfpm', 'vhost_defaultini', '2'), ('phpfpm', 'fastcgi_ipcdir', '/var/lib/apache2/fastcgi/'), ('phpfpm', 'use_mod_proxy', '1'), ('phpfpm', 'ini_flags', 'asp_tags display_errors display_startup_errors html_errors log_errors magic_quotes_gpc magic_quotes_runtime magic_quotes_sybase mail.add_x_header session.cookie_secure session.use_cookies short_open_tag track_errors xmlrpc_errors suhosin.simulation suhosin.session.encrypt suhosin.session.cryptua suhosin.session.cryptdocroot suhosin.cookie.encrypt suhosin.cookie.cryptua suhosin.cookie.cryptdocroot suhosin.executor.disable_eval mbstring.func_overload'), ('phpfpm', 'ini_values', 'auto_append_file auto_prepend_file date.timezone default_charset error_reporting include_path log_errors_max_len mail.log max_execution_time session.cookie_domain session.cookie_lifetime session.cookie_path session.name session.serialize_handler upload_max_filesize xmlrpc_error_number session.auto_start always_populate_raw_post_data suhosin.session.cryptkey suhosin.session.cryptraddr suhosin.session.checkraddr suhosin.cookie.cryptkey suhosin.cookie.plainlist suhosin.cookie.cryptraddr suhosin.cookie.checkraddr suhosin.executor.func.blacklist suhosin.executor.eval.whitelist'), ('phpfpm', 'ini_admin_flags', 'allow_call_time_pass_reference allow_url_fopen allow_url_include auto_detect_line_endings cgi.fix_pathinfo cgi.force_redirect enable_dl expose_php file_uploads ignore_repeated_errors ignore_repeated_source log_errors register_argc_argv report_memleaks opcache.enable opcache.consistency_checks opcache.dups_fix opcache.load_comments opcache.revalidate_path opcache.save_comments opcache.use_cwd opcache.fast_shutdown'), ('phpfpm', 'ini_admin_values', 'cgi.redirect_status_env disable_classes disable_functions error_log gpc_order max_input_time max_input_vars memory_limit open_basedir output_buffering post_max_size precision sendmail_path session.gc_divisor session.gc_probability variables_order opcache.log_verbosity_level opcache.restrict_api opcache.revalidate_freq opcache.max_accelerated_files opcache.memory_consumption opcache.interned_strings_buffer opcache.validate_timestamps'), ('nginx', 'fastcgiparams', '/etc/nginx/fastcgi_params'), ('system', 'lastaccountnumber', '0'), ('system', 'lastguid', '9999'), ('system', 'documentroot_prefix', '/var/customers/webs/'), ('system', 'logfiles_directory', '/var/customers/logs/'), ('system', 'ipaddress', 'SERVERIP'), ('system', 'apachereload_command', 'service apache2 reload'), ('system', 'last_traffic_run', '000000'), ('system', 'vmail_uid', '2000'), ('system', 'vmail_gid', '2000'), ('system', 'vmail_homedir', '/var/customers/mail/'), ('system', 'vmail_maildirname', 'Maildir'), ('system', 'bind_enable', '0'), ('system', 'bindconf_directory', '/etc/bind/'), ('system', 'bindreload_command', 'service bind9 reload'), ('system', 'hostname', 'SERVERNAME'), ('system', 'mysql_access_host', 'localhost'), ('system', 'lastcronrun', ''), ('system', 'defaultip', '1'), ('system', 'defaultsslip', ''), ('system', 'phpappendopenbasedir', '/tmp/'), ('system', 'deactivateddocroot', '/var/www/html/froxlor/templates/misc/deactivated/'), ('system', 'mailpwcleartext', '0'), ('system', 'last_tasks_run', '000000'), ('system', 'nameservers', ''), ('system', 'mxservers', ''), ('system', 'mod_fcgid', '0'), ('system', 'apacheconf_vhost', '/etc/apache2/sites-enabled/'), ('system', 'apacheconf_diroptions', '/etc/apache2/sites-enabled/'), ('system', 'apacheconf_htpasswddir', '/etc/apache2/froxlor-htpasswd/'), ('system', 'webalizer_quiet', '2'), ('system', 'last_archive_run', '000000'), ('system', 'mod_fcgid_configdir', '/var/www/php-fcgi-scripts'), ('system', 'mod_fcgid_tmpdir', '/var/customers/tmp'), ('system', 'ssl_cert_file', '/etc/ssl/froxlor_selfsigned.pem'), ('system', 'use_ssl', '0'), ('system', 'default_vhostconf', ''), ('system', 'default_sslvhostconf', ''), ('system', 'mail_quota_enabled', '0'), ('system', 'mail_quota', '100'), ('system', 'httpuser', 'www-data'), ('system', 'httpgroup', 'www-data'), ('system', 'webserver', 'apache2'), ('system', 'mod_fcgid_wrapper', '1'), ('system', 'mod_fcgid_starter', '0'), ('system', 'mod_fcgid_peardir', '/usr/share/php/:/usr/share/php5/'), ('system', 'mod_fcgid_maxrequests', '250'), ('system', 'ssl_key_file','/etc/ssl/froxlor_selfsigned.key'), ('system', 'ssl_ca_file', ''), ('system', 'debug_cron', '0'), ('system', 'store_index_file_subs', '1'), ('system', 'stdsubdomain', ''), ('system', 'awstats_path', '/usr/share/awstats/tools/'), ('system', 'awstats_conf', '/etc/awstats/'), ('system', 'awstats_logformat', '1'), ('system', 'defaultttl', '604800'), ('system', 'mod_fcgid_defaultini', '1'), ('system', 'ftpserver', 'proftpd'), ('system', 'dns_createmailentry', '0'), ('system', 'dns_createcaaentry', '1'), ('system', 'froxlordirectlyviahostname', '1'), ('system', 'report_enable', '1'), ('system', 'report_webmax', '90'), ('system', 'report_trafficmax', '90'), ('system', 'validate_domain', '1'), ('system', 'diskquota_enabled', '0'), ('system', 'diskquota_repquota_path', '/usr/sbin/repquota'), ('system', 'diskquota_quotatool_path', '/usr/bin/quotatool'), ('system', 'diskquota_customer_partition', '/dev/root'), ('system', 'mod_fcgid_idle_timeout', '30'), ('system', 'mod_fcgid_ownvhost', '0'), ('system', 'mod_fcgid_httpuser', 'froxlorlocal'), ('system', 'mod_fcgid_httpgroup', 'froxlorlocal'), ('system', 'awstats_awstatspath', '/usr/lib/cgi-bin/'), ('system', 'mod_fcgid_defaultini_ownvhost', '2'), ('system', 'awstats_icons', '/usr/share/awstats/icon/'), ('system', 'ssl_cert_chainfile', ''), ('system', 'ssl_cipher_list', 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305'), ('system', 'nginx_php_backend', '127.0.0.1:8888'), ('system', 'http2_support', '0'), ('system', 'http3_support', '0'), ('system', 'perl_server', 'unix:/var/run/nginx/cgiwrap-dispatch.sock'), ('system', 'phpreload_command', ''), ('system', 'apache24', '1'), ('system', 'apache24_ocsp_cache_path', 'shmcb:/var/run/apache2/ocsp-stapling.cache(131072)'), ('system', 'documentroot_use_default_value', '0'), ('system', 'passwordcryptfunc', '2y'), ('system', 'axfrservers', ''), ('system', 'powerdns_mode', 'Native'), ('system', 'customer_ssl_path', '/etc/ssl/froxlor-custom/'), ('system', 'allow_error_report_admin', '1'), ('system', 'allow_error_report_customer', '0'), ('system', 'mdalog', '/var/log/mail.log'), ('system', 'mtalog', '/var/log/mail.log'), ('system', 'mdaserver', 'dovecot'), ('system', 'mtaserver', 'postfix'), ('system', 'mailtraffic_enabled', '1'), ('system', 'cronconfig', '/etc/cron.d/froxlor'), ('system', 'crondreload', 'service cron reload'), ('system', 'croncmdline', '/usr/bin/nice -n 5 /usr/bin/php -q'), ('system', 'cron_allowautoupdate', '0'), ('system', 'dns_createhostnameentry', '0'), ('system', 'send_cron_errors', '0'), ('system', 'apacheitksupport', '0'), ('system', 'leprivatekey', 'unset'), ('system', 'lepublickey', 'unset'), ('system', 'letsencryptca', 'letsencrypt'), ('system', 'letsencryptchallengepath', '/var/www/html/froxlor'), ('system', 'letsencryptkeysize', '4096'), ('system', 'letsencryptreuseold', 0), ('system', 'leenabled', '0'), ('system', 'leapiversion', '2'), ('system', 'exportenabled', '0'), ('system', 'dnsenabled', '0'), ('system', 'dns_server', 'Bind'), ('system', 'apacheglobaldiropt', ''), ('system', 'allow_customer_shell', '0'), ('system', 'available_shells', ''), ('system', 'le_froxlor_enabled', '0'), ('system', 'le_froxlor_redirect', '0'), ('system', 'le_renew_hook', 'systemctl restart postfix dovecot proftpd'), ('system', 'le_renew_services', ''), ('system', 'letsencryptacmeconf', '/etc/apache2/conf-enabled/acme.conf'), ('system', 'mail_use_smtp', '0'), ('system', 'mail_smtp_host', 'localhost'), ('system', 'mail_smtp_port', '25'), ('system', 'mail_smtp_usetls', '1'), ('system', 'mail_smtp_auth', '1'), ('system', 'mail_smtp_user', ''), ('system', 'mail_smtp_passwd', ''), ('system', 'hsts_maxage', '10368000'), ('system', 'hsts_incsub', '0'), ('system', 'hsts_preload', '0'), ('system', 'leregistered', '0'), ('system', 'leaccount', ''), ('system', 'nssextrausers', '1'), ('system', 'le_domain_dnscheck', '1'), ('system', 'le_domain_dnscheck_resolver', '1.1.1.1'), ('system', 'ssl_protocols', 'TLSv1.2'), ('system', 'tlsv13_cipher_list', ''), ('system', 'honorcipherorder', '0'), ('system', 'sessiontickets', '1'), ('system', 'sessionticketsenabled', '1'), ('system', 'logfiles_format', ''), ('system', 'logfiles_type', '1'), ('system', 'logfiles_piped', '0'), ('system', 'logfiles_script', ''), ('system', 'dhparams_file', ''), ('system', 'errorlog_level', 'warn'), ('system', 'leecc', '0'), ('system', 'froxloraliases', ''), ('system', 'apply_specialsettings_default', '1'), ('system', 'apply_phpconfigs_default', '1'), ('system', 'hide_incompatible_settings', '1'), ('system', 'include_default_vhostconf', '0'), ('system', 'soaemail', ''), ('system', 'domaindefaultalias', '0'), ('system', 'createstdsubdom_default', '1'), ('system', 'froxlorusergroup', ''), ('system', 'froxlorusergroup_gid', ''), ('system', 'acmeshpath', '/root/.acme.sh/acme.sh'), ('system', 'distribution', ''), ('system', 'distro_mismatch', '0'), ('system', 'update_channel', 'stable'), ('system', 'updatecheck_data', ''), ('system', 'update_notify_last', ''), ('system', 'traffictool', 'goaccess'), ('system', 'req_limit_per_interval', 60), ('system', 'req_limit_interval', 60), ('system', 'report_web_bccadmin', '0'), ('system', 'webserver_serveradmin', 'customer'), ('api', 'enabled', '0'), ('api', 'customer_default', '1'), ('2fa', 'enabled', '1'), ('mail', 'enable_allow_sender', '0'), ('mail', 'allow_external_domains', '0'), ('panel', 'decimal_places', '4'), ('panel', 'adminmail', 'ADMIN_MAIL'), ('panel', 'phpmyadmin_url', ''), ('panel', 'webmail_url', ''), ('panel', 'webftp_url', ''), ('panel', 'standardlanguage', 'en'), ('panel', 'pathedit', 'Manual'), ('panel', 'paging', '20'), ('panel', 'natsorting', '1'), ('panel', 'sendalternativemail', '0'), ('panel', 'allow_domain_change_admin', '0'), ('panel', 'allow_domain_change_customer', '0'), ('panel', 'frontend', 'froxlor'), ('panel', 'default_theme', 'Froxlor'), ('panel', 'password_min_length', '0'), ('panel', 'adminmail_defname', 'Froxlor Administrator'), ('panel', 'adminmail_return', ''), ('panel', 'unix_names', '1'), ('panel', 'allow_preset', '1'), ('panel', 'allow_preset_admin', '0'), ('panel', 'password_regex', ''), ('panel', 'phpconfigs_hidestdsubdomain', '0'), ('panel', 'phpconfigs_hidesubdomains', '1'), ('panel', 'allow_theme_change_admin', '1'), ('panel', 'allow_theme_change_customer', '1'), ('panel', 'password_alpha_lower', '1'), ('panel', 'password_alpha_upper', '1'), ('panel', 'password_numeric', '0'), ('panel', 'password_special_char_required', '0'), ('panel', 'password_special_char', '!?<>§$%+#=@'), ('panel', 'customer_hide_options', ''), ('panel', 'is_configured', '0'), ('panel', 'imprint_url', ''), ('panel', 'terms_url', ''), ('panel', 'privacy_url', ''), ('panel', 'logo_image_header', ''), ('panel', 'logo_image_login', ''), ('panel', 'logo_overridetheme', '0'), ('panel', 'logo_overridecustom', '0'), ('panel', 'settings_mode', '0'), ('panel', 'menu_collapsed', '1'), ('panel', 'version', '2.3.7'), ('panel', 'db_version', '202603100'); DROP TABLE IF EXISTS `panel_tasks`; CREATE TABLE `panel_tasks` ( `id` int(11) unsigned NOT NULL auto_increment, `type` int(11) NOT NULL default '0', `data` text, PRIMARY KEY (`id`) ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; INSERT INTO `panel_tasks` (`type`) VALUES ('99'); DROP TABLE IF EXISTS `panel_templates`; CREATE TABLE `panel_templates` ( `id` int(11) NOT NULL auto_increment, `adminid` int(11) NOT NULL default '0', `language` varchar(255) NOT NULL default '', `templategroup` varchar(255) NOT NULL default '', `varname` varchar(255) NOT NULL default '', `value` longtext NOT NULL, `file_extension` varchar(50) NOT NULL default 'html', PRIMARY KEY (id), KEY adminid (adminid) ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; DROP TABLE IF EXISTS `panel_traffic`; CREATE TABLE `panel_traffic` ( `id` int(11) unsigned NOT NULL auto_increment, `customerid` int(11) unsigned NOT NULL default '0', `year` int(4) unsigned zerofill NOT NULL default '0000', `month` int(2) unsigned zerofill NOT NULL default '00', `day` int(2) unsigned zerofill NOT NULL default '00', `stamp` int(11) unsigned NOT NULL default '0', `http` bigint(30) unsigned NOT NULL default '0', `ftp_up` bigint(30) unsigned NOT NULL default '0', `ftp_down` bigint(30) unsigned NOT NULL default '0', `mail` bigint(30) unsigned NOT NULL default '0', PRIMARY KEY (`id`), KEY `customerid` (`customerid`) ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; DROP TABLE IF EXISTS `panel_traffic_admins`; CREATE TABLE `panel_traffic_admins` ( `id` int(11) unsigned NOT NULL auto_increment, `adminid` int(11) unsigned NOT NULL default '0', `year` int(4) unsigned zerofill NOT NULL default '0000', `month` int(2) unsigned zerofill NOT NULL default '00', `day` int(2) unsigned zerofill NOT NULL default '00', `stamp` int(11) unsigned NOT NULL default '0', `http` bigint(30) unsigned NOT NULL default '0', `ftp_up` bigint(30) unsigned NOT NULL default '0', `ftp_down` bigint(30) unsigned NOT NULL default '0', `mail` bigint(30) unsigned NOT NULL default '0', PRIMARY KEY (`id`), KEY `adminid` (`adminid`) ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; DROP TABLE IF EXISTS `panel_diskspace`; CREATE TABLE `panel_diskspace` ( `id` int(11) unsigned NOT NULL auto_increment, `customerid` int(11) unsigned NOT NULL default '0', `year` int(4) unsigned zerofill NOT NULL default '0000', `month` int(2) unsigned zerofill NOT NULL default '00', `day` int(2) unsigned zerofill NOT NULL default '00', `stamp` int(11) unsigned NOT NULL default '0', `webspace` bigint(30) unsigned NOT NULL default '0', `mail` bigint(30) unsigned NOT NULL default '0', `mysql` bigint(30) unsigned NOT NULL default '0', PRIMARY KEY (`id`), KEY `customerid` (`customerid`) ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; DROP TABLE IF EXISTS `panel_syslog`; CREATE TABLE IF NOT EXISTS `panel_syslog` ( `logid` bigint(20) NOT NULL auto_increment, `action` int(5) NOT NULL default '10', `type` int(5) NOT NULL default '0', `date` int(15) NOT NULL, `user` varchar(50) NOT NULL, `text` text NOT NULL, PRIMARY KEY (`logid`) ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; DROP TABLE IF EXISTS `panel_fpmdaemons`; CREATE TABLE `panel_fpmdaemons` ( `id` int(11) unsigned NOT NULL auto_increment, `description` varchar(50) NOT NULL, `reload_cmd` varchar(255) NOT NULL, `config_dir` varchar(255) NOT NULL, `pm` varchar(15) NOT NULL DEFAULT 'dynamic', `max_children` int(4) NOT NULL DEFAULT '5', `start_servers` int(4) NOT NULL DEFAULT '2', `min_spare_servers` int(4) NOT NULL DEFAULT '1', `max_spare_servers` int(4) NOT NULL DEFAULT '3', `max_requests` int(4) NOT NULL DEFAULT '0', `idle_timeout` int(4) NOT NULL DEFAULT '10', `limit_extensions` varchar(255) NOT NULL default '.php', `custom_config` text, PRIMARY KEY (`id`), UNIQUE KEY `reload` (`reload_cmd`), UNIQUE KEY `config` (`config_dir`) ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; INSERT INTO `panel_fpmdaemons` (`id`, `description`, `reload_cmd`, `config_dir`) VALUES (1, 'System default', 'service php7.4-fpm restart', '/etc/php/7.4/fpm/pool.d/'); DROP TABLE IF EXISTS `panel_phpconfigs`; CREATE TABLE `panel_phpconfigs` ( `id` int(11) unsigned NOT NULL auto_increment, `description` varchar(50) NOT NULL, `binary` varchar(255) NOT NULL, `file_extensions` varchar(255) NOT NULL, `mod_fcgid_starter` int(4) NOT NULL DEFAULT '-1', `mod_fcgid_maxrequests` int(4) NOT NULL DEFAULT '-1', `mod_fcgid_umask` varchar(15) NOT NULL DEFAULT '022', `fpm_slowlog` tinyint(1) NOT NULL default '0', `fpm_reqterm` varchar(15) NOT NULL default '60s', `fpm_reqslow` varchar(15) NOT NULL default '5s', `phpsettings` text NOT NULL, `fpmsettingid` int(11) NOT NULL DEFAULT '1', `pass_authorizationheader` tinyint(1) NOT NULL default '0', `override_fpmconfig` tinyint(1) NOT NULL DEFAULT '0', `pm` varchar(15) NOT NULL DEFAULT 'dynamic', `max_children` int(4) NOT NULL DEFAULT '5', `start_servers` int(4) NOT NULL DEFAULT '2', `min_spare_servers` int(4) NOT NULL DEFAULT '1', `max_spare_servers` int(4) NOT NULL DEFAULT '3', `max_requests` int(4) NOT NULL DEFAULT '0', `idle_timeout` int(4) NOT NULL DEFAULT '10', `limit_extensions` varchar(255) NOT NULL default '.php', PRIMARY KEY (`id`), KEY `fpmsettingid` (`fpmsettingid`) ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; INSERT INTO `panel_phpconfigs` (`id`, `description`, `binary`, `file_extensions`, `mod_fcgid_starter`, `mod_fcgid_maxrequests`, `pass_authorizationheader`, `phpsettings`) VALUES (1, 'Default Config', '/usr/bin/php-cgi', 'php', '-1', '-1', '1', 'allow_url_fopen = Off\r\nallow_url_include = Off\r\nauto_append_file =\r\nauto_globals_jit = On\r\nauto_prepend_file =\r\nbcmath.scale = 0\r\ncli_server.color = On\r\ndefault_charset = "UTF-8"\r\ndefault_mimetype = "text/html"\r\ndefault_socket_timeout = 60\r\nasp_tags = Off\r\ndisable_functions = pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,curl_exec,curl_multi_exec,exec,parse_ini_file,passthru,popen,proc_close,proc_get_status,proc_nice,proc_open,proc_terminate,shell_exec,show_source,system\r\ndisplay_errors = Off\r\ndisplay_startup_errors = Off\r\ndoc_root =\r\nenable_dl = Off\r\nerror_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT & ~E_NOTICE\r\nexpose_php = Off\r\nfile_uploads = On\r\nhtml_errors = On\r\nignore_repeated_errors = Off\r\nignore_repeated_source = Off\r\ninclude_path = ".:{PEAR_DIR}"\r\nimplicit_flush = Off\r\nldap.max_links = -1\r\nlog_errors = On\r\nlog_errors_max_len = 1024\r\nmail.add_x_header = Off\r\nmax_execution_time = 30\r\nmax_file_uploads = 20\r\nmax_input_time = 60\r\nmemory_limit = 128M\r\n{OPEN_BASEDIR_C}open_basedir = "{OPEN_BASEDIR}"\r\noutput_buffering = 4096\r\npost_max_size = 16M\r\nprecision = 14\r\nregister_argc_argv = Off\r\nreport_memleaks = On\r\nrequest_order = "GP"\r\nsendmail_path = "/usr/sbin/sendmail -t -i -f postmaster@{DOMAIN}"\r\nserialize_precision = -1\r\nsession.auto_start = 0\r\nsession.cache_expire = 180\r\nsession.cache_limiter = nocache\r\nsession.cookie_domain =\r\nsession.cookie_httponly =\r\nsession.cookie_lifetime = 0\r\nsession.cookie_path = /\r\nsession.cookie_samesite =\r\nsession.gc_divisor = 1000\r\nsession.gc_maxlifetime = 1440\r\nsession.gc_probability = 0\r\nsession.name = PHPSESSID\r\nsession.referer_check =\r\nsession.save_handler = files\r\nsession.save_path = "{TMP_DIR}"\r\nsession.serialize_handler = php\r\nsession.sid_bits_per_character = 5\r\nsession.sid_length = 26\r\nsession.trans_sid_tags = "a=href,area=href,frame=src,form="\r\nsession.use_cookies = 1\r\nsession.use_only_cookies = 1\r\nsession.use_strict_mode = 0\r\nsession.use_trans_sid = 0\r\nshort_open_tag = On\r\nupload_max_filesize = 32M\r\nupload_tmp_dir = "{TMP_DIR}"\r\nvariables_order = "GPCS"\r\nopcache.restrict_api = "{DOCUMENT_ROOT}"\r\n'), (2, 'Froxlor Vhost Config', '/usr/bin/php-cgi', 'php', '-1', '-1', '1', 'allow_url_fopen = On\r\nallow_url_include = Off\r\nauto_append_file =\r\nauto_globals_jit = On\r\nauto_prepend_file =\r\nbcmath.scale = 0\r\ncli_server.color = On\r\ndefault_charset = "UTF-8"\r\ndefault_mimetype = "text/html"\r\ndefault_socket_timeout = 60\r\nasp_tags = Off\r\ndisable_functions = pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,curl_multi_exec,parse_ini_file,passthru,popen,proc_close,proc_get_status,proc_nice,proc_open,proc_terminate,shell_exec,show_source,system\r\ndisplay_errors = Off\r\ndisplay_startup_errors = Off\r\ndoc_root =\r\nenable_dl = Off\r\nerror_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT & ~E_NOTICE\r\nexpose_php = Off\r\nfile_uploads = On\r\nhtml_errors = On\r\nignore_repeated_errors = Off\r\nignore_repeated_source = Off\r\ninclude_path = ".:{PEAR_DIR}"\r\nimplicit_flush = Off\r\nldap.max_links = -1\r\nlog_errors = On\r\nlog_errors_max_len = 1024\r\nmail.add_x_header = Off\r\nmax_execution_time = 60\r\nmax_file_uploads = 20\r\nmax_input_time = 60\r\nmemory_limit = 128M\r\noutput_buffering = 4096\r\npost_max_size = 16M\r\nprecision = 14\r\nregister_argc_argv = Off\r\nreport_memleaks = On\r\nrequest_order = "GP"\r\nsendmail_path = "/usr/sbin/sendmail -t -i -f postmaster@{DOMAIN}"\r\nserialize_precision = -1\r\nsession.auto_start = 0\r\nsession.cache_expire = 180\r\nsession.cache_limiter = nocache\r\nsession.cookie_domain =\r\nsession.cookie_httponly =\r\nsession.cookie_lifetime = 0\r\nsession.cookie_path = /\r\nsession.cookie_samesite =\r\nsession.gc_divisor = 1000\r\nsession.gc_maxlifetime = 1440\r\nsession.gc_probability = 0\r\nsession.name = PHPSESSID\r\nsession.referer_check =\r\nsession.save_handler = files\r\nsession.save_path = "{TMP_DIR}"\r\nsession.serialize_handler = php\r\nsession.sid_bits_per_character = 5\r\nsession.sid_length = 26\r\nsession.trans_sid_tags = "a=href,area=href,frame=src,form="\r\nsession.use_cookies = 1\r\nsession.use_only_cookies = 1\r\nsession.use_strict_mode = 0\r\nsession.use_trans_sid = 0\r\nshort_open_tag = On\r\nupload_max_filesize = 32M\r\nupload_tmp_dir = "{TMP_DIR}"\r\nvariables_order = "GPCS"\r\nopcache.restrict_api = ""\r\n'); DROP TABLE IF EXISTS `cronjobs_run`; CREATE TABLE IF NOT EXISTS `cronjobs_run` ( `id` bigint(20) NOT NULL auto_increment, `module` varchar(250) NOT NULL, `cronfile` varchar(250) NOT NULL, `cronclass` varchar(500) NOT NULL, `lastrun` int(15) NOT NULL DEFAULT '0', `interval` varchar(100) NOT NULL DEFAULT '5 MINUTE', `isactive` tinyint(1) DEFAULT '1', `desc_lng_key` varchar(100) NOT NULL DEFAULT 'cron_unknown_desc', PRIMARY KEY (`id`) ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; INSERT INTO `cronjobs_run` (`id`, `module`, `cronfile`, `cronclass`, `interval`, `isactive`, `desc_lng_key`) VALUES (1, 'froxlor/core', 'tasks', '\\Froxlor\\Cron\\System\\TasksCron', '5 MINUTE', '1', 'cron_tasks'), (2, 'froxlor/core', 'traffic', '\\Froxlor\\Cron\\Traffic\\TrafficCron', '1 DAY', '1', 'cron_traffic'), (3, 'froxlor/reports', 'usage_report', '\\Froxlor\\Cron\\Traffic\\ReportsCron', '1 DAY', '1', 'cron_usage_report'), (4, 'froxlor/core', 'mailboxsize', '\\Froxlor\\Cron\\System\\MailboxsizeCron', '6 HOUR', '1', 'cron_mailboxsize'), (5, 'froxlor/letsencrypt', 'letsencrypt', '\\Froxlor\\Cron\\Http\\LetsEncrypt\\AcmeSh', '5 MINUTE', '0', 'cron_letsencrypt'), (6, 'froxlor/export', 'export', '\\Froxlor\\Cron\\System\\ExportCron', '1 HOUR', '0', 'cron_export'); DROP TABLE IF EXISTS `ftp_quotalimits`; CREATE TABLE IF NOT EXISTS `ftp_quotalimits` ( `name` varchar(255) default NULL, `quota_type` enum('user','group','class','all') NOT NULL default 'user', `per_session` enum('false','true') NOT NULL default 'false', `limit_type` enum('soft','hard') NOT NULL default 'hard', `bytes_in_avail` float NOT NULL, `bytes_out_avail` float NOT NULL, `bytes_xfer_avail` float NOT NULL, `files_in_avail` int(10) unsigned NOT NULL, `files_out_avail` int(10) unsigned NOT NULL, `files_xfer_avail` int(10) unsigned NOT NULL ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; 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); DROP TABLE IF EXISTS `ftp_quotatallies`; CREATE TABLE IF NOT EXISTS `ftp_quotatallies` ( `name` varchar(255) NOT NULL, `quota_type` enum('user','group','class','all') NOT NULL, `bytes_in_used` float NOT NULL, `bytes_out_used` float NOT NULL, `bytes_xfer_used` float NOT NULL, `files_in_used` int(10) unsigned NOT NULL, `files_out_used` int(10) unsigned NOT NULL, `files_xfer_used` int(10) unsigned NOT NULL ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; DROP TABLE IF EXISTS `redirect_codes`; CREATE TABLE IF NOT EXISTS `redirect_codes` ( `id` int(5) NOT NULL auto_increment, `code` varchar(3) NOT NULL, `desc` varchar(200) NOT NULL, `enabled` tinyint(1) DEFAULT '1', PRIMARY KEY (`id`) ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; INSERT INTO `redirect_codes` (`id`, `code`, `desc`, `enabled`) VALUES (1, '---', 'rc_default', 1), (2, '301', 'rc_movedperm', 1), (3, '302', 'rc_found', 1), (4, '303', 'rc_seeother', 1), (5, '307', 'rc_tempred', 1); DROP TABLE IF EXISTS `domain_redirect_codes`; CREATE TABLE IF NOT EXISTS `domain_redirect_codes` ( `rid` int(5) NOT NULL, `did` int(11) unsigned NOT NULL, UNIQUE KEY `rc` (`rid`, `did`) ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; DROP TABLE IF EXISTS `domain_ssl_settings`; CREATE TABLE IF NOT EXISTS `domain_ssl_settings` ( `id` int(5) NOT NULL auto_increment, `domainid` int(11) NOT NULL, `ssl_cert_file` mediumtext, `ssl_key_file` mediumtext, `ssl_ca_file` mediumtext, `ssl_cert_chainfile` mediumtext, `ssl_csr_file` mediumtext, `ssl_fullchain_file` mediumtext, `validfromdate` datetime DEFAULT NULL, `validtodate` datetime DEFAULT NULL, `issuer` varchar(255) NOT NULL default '', PRIMARY KEY (`id`), UNIQUE KEY (`domainid`) ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; DROP TABLE IF EXISTS `panel_domaintoip`; CREATE TABLE IF NOT EXISTS `panel_domaintoip` ( `id_domain` int(11) unsigned NOT NULL, `id_ipandports` int(11) unsigned NOT NULL, PRIMARY KEY (`id_domain`,`id_ipandports`) ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; DROP TABLE IF EXISTS `domain_dns_entries`; CREATE TABLE `domain_dns_entries` ( `id` int(20) NOT NULL auto_increment, `domain_id` int(15) NOT NULL, `record` varchar(255) NOT NULL, `type` varchar(10) NOT NULL DEFAULT 'A', `content` text NOT NULL, `ttl` int(11) NOT NULL DEFAULT '18000', `prio` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; DROP TABLE IF EXISTS `panel_plans`; CREATE TABLE `panel_plans` ( `id` int(11) NOT NULL auto_increment, `adminid` int(11) NOT NULL default '0', `name` varchar(255) NOT NULL default '', `description` text NOT NULL, `value` longtext NOT NULL, `ts` int(15) NOT NULL default '0', PRIMARY KEY (id), KEY adminid (adminid) ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; DROP TABLE IF EXISTS `api_keys`; CREATE TABLE `api_keys` ( `id` int(11) NOT NULL auto_increment, `adminid` int(11) NOT NULL default '0', `customerid` int(11) NOT NULL default '0', `apikey` varchar(500) NOT NULL default '', `secret` varchar(500) NOT NULL default '', `allowed_from` text NOT NULL, `valid_until` int(15) NOT NULL default '0', PRIMARY KEY (id), KEY adminid (adminid), KEY customerid (customerid) ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; DROP TABLE IF EXISTS `panel_usercolumns`; CREATE TABLE `panel_usercolumns` ( `adminid` int(11) NOT NULL default '0', `customerid` int(11) NOT NULL default '0', `section` varchar(500) NOT NULL default '', `columns` text NOT NULL, UNIQUE KEY `user_section` (`adminid`, `customerid`, `section`), KEY adminid (adminid), KEY customerid (customerid) ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci; DROP TABLE IF EXISTS `panel_loginlinks`; CREATE TABLE `panel_loginlinks` ( `hash` varchar(500) NOT NULL, `loginname` varchar(50) NOT NULL, `valid_until` int(15) NOT NULL, `allowed_from` text NOT NULL, UNIQUE KEY `loginname` (`loginname`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci; DROP TABLE IF EXISTS `panel_2fa_tokens`; CREATE TABLE `panel_2fa_tokens` ( `id` int(11) NOT NULL auto_increment, `selector` varchar(200) NOT NULL, `token` varchar(200) NOT NULL, `userid` int(11) NOT NULL default '0', `valid_until` int(15) NOT NULL, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci; DROP TABLE IF EXISTS `panel_sshkeys`; CREATE TABLE `panel_sshkeys` ( `id` int(11) NOT NULL auto_increment, `customerid` int(11) NOT NULL, `ftp_user_id` int(20) NOT NULL, `ssh_pubkey` text NOT NULL, `description` varchar(255) NOT NULL DEFAULT '', PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci; FROXLORSQL; ================================================ FILE: install/index.html ================================================ ================================================ FILE: install/install.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ use Froxlor\Http\RateLimiter; use Froxlor\UI\Panel\UI; use Froxlor\Install\Install; require dirname(__DIR__) . '/lib/functions.php'; // define default theme for configurehint, etc. $_deftheme = 'Froxlor'; // validate correct php version if (version_compare("7.4.0", PHP_VERSION, ">=")) { die(view($_deftheme . '/misc/phprequirementfailed.html.twig', [ '{{ basehref }}' => '../', '{{ froxlor_min_version }}' => '7.4.0', '{{ current_version }}' => PHP_VERSION, '{{ current_year }}' => date('Y', time()), ])); } // validate vendor autoloader if (!file_exists(dirname(__DIR__) . '/vendor/autoload.php')) { die(view($_deftheme . '/misc/vendormissinghint.html.twig', [ '{{ basehref }}' => '../', '{{ froxlor_install_dir }}' => dirname(__DIR__), '{{ current_year }}' => date('Y', time()), ])); } // check installation status if (file_exists(dirname(__DIR__) . '/lib/userdata.inc.php')) { header("Location: ../"); exit; } require dirname(__DIR__) . '/vendor/autoload.php'; require dirname(__DIR__) . '/lib/tables.inc.php'; // init twig UI::initTwig(true); UI::sendHeaders(); RateLimiter::run(true); $installer = new Install(); $installer->handle(); ================================================ FILE: install/updates/froxlor/index.html ================================================ ================================================ FILE: install/updates/froxlor/update_2.0.inc.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\Froxlor; use Froxlor\Install\Update; use Froxlor\Settings; if (!defined('_CRON_UPDATE')) { if (!defined('AREA') || (defined('AREA') && AREA != 'admin') || !isset($userinfo['loginname']) || (isset($userinfo['loginname']) && $userinfo['loginname'] == '')) { header('Location: ../../../../index.php'); exit(); } } // last 0.10.x release if (Froxlor::isFroxlorVersion('0.10.38.3')) { $update_to = '2.0.0-beta1'; Update::showUpdateStep("Updating from 0.10.38.3 to " . $update_to, false); Update::showUpdateStep("Removing unused table"); Database::query("DROP TABLE IF EXISTS `panel_sessions`;"); Database::query("DROP TABLE IF EXISTS `panel_languages`;"); Update::lastStepStatus(0); Update::showUpdateStep("Updating froxlor - theme"); Database::query("UPDATE `" . TABLE_PANEL_ADMINS . "` SET `theme` = 'Froxlor' WHERE `theme` <> 'Froxlor';"); Database::query("UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `theme` = 'Froxlor' WHERE `theme` <> 'Froxlor';"); Settings::Set('panel.default_theme', 'Froxlor'); Update::lastStepStatus(0); Update::showUpdateStep("Creating new tables and fields"); Database::query("DROP TABLE IF EXISTS `panel_usercolumns`;"); $sql = "CREATE TABLE `panel_usercolumns` ( `adminid` int(11) NOT NULL default '0', `customerid` int(11) NOT NULL default '0', `section` varchar(500) NOT NULL default '', `columns` text NOT NULL, UNIQUE KEY `user_section` (`adminid`, `customerid`, `section`), KEY adminid (adminid), KEY customerid (customerid) ) ENGINE=InnoDB CHARSET=utf8 COLLATE=utf8_general_ci;"; Database::query($sql); // new customer allowed_mysqlserver field Database::query("ALTER TABLE `" . TABLE_PANEL_CUSTOMERS . "` ROW_FORMAT=DYNAMIC;"); Database::query("ALTER TABLE `" . TABLE_PANEL_CUSTOMERS . "` CHANGE COLUMN `customernumber` `customernumber` varchar(100) NOT NULL default '';"); Database::query("ALTER TABLE `" . TABLE_PANEL_CUSTOMERS . "` CHANGE COLUMN `allowed_phpconfigs` `allowed_phpconfigs` text NOT NULL;"); Database::query("ALTER TABLE `" . TABLE_PANEL_CUSTOMERS . "` ADD `allowed_mysqlserver` text NOT NULL;"); $has_customer_table_update_200 = true; // ftp_users adjustments Database::query("ALTER TABLE `" . TABLE_FTP_USERS . "` CHANGE COLUMN `password` `password` varchar(255) NOT NULL default '';"); Database::query("ALTER TABLE `" . TABLE_FTP_QUOTALIMITS . "` CHANGE COLUMN `name` `name` varchar(255) default NULL;"); Database::query("ALTER TABLE `" . TABLE_FTP_QUOTATALLIES . "` CHANGE COLUMN `name` `name` varchar(255) default NULL;"); // mail_users adjustments Database::query("ALTER TABLE `" . TABLE_MAIL_USERS . "` CHANGE COLUMN `password` `password` varchar(255) NOT NULL default '';"); Database::query("ALTER TABLE `" . TABLE_MAIL_USERS . "` CHANGE COLUMN `password_enc` `password_enc` varchar(255) NOT NULL default '';"); // drop domains_see_all field from panel_admins Database::query("ALTER TABLE `" . TABLE_PANEL_ADMINS . "` DROP COLUMN `domains_see_all`;"); Update::lastStepStatus(0); Update::showUpdateStep("Checking for multiple mysql-servers to allow access to customers for existing databases"); $dbservers_stmt = Database::query(" SELECT `customerid`, GROUP_CONCAT(DISTINCT `dbserver` SEPARATOR ',') as allowed_mysqlserver FROM `" . TABLE_PANEL_DATABASES . "` GROUP BY `customerid`; "); $upd_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `allowed_mysqlserver` = :allowed_mysqlserver WHERE `customerid` = :customerid"); while ($dbserver = $dbservers_stmt->fetch(PDO::FETCH_ASSOC)) { if (isset($dbserver['allowed_mysqlserver']) && !empty($dbserver['allowed_mysqlserver'])) { $allowed_mysqlserver = json_encode(explode(",", $dbserver['allowed_mysqlserver'])); Database::pexecute($upd_stmt, ['allowed_mysql_server' => $allowed_mysqlserver, 'customerid' => $dbserver['customerid']]); } } Update::lastStepStatus(0); $to_clean = array( "install/lib", "install/lng", "install/updates/froxlor/0.9", "install/updates/froxlor/0.10", "install/updates/preconfig/0.9", "install/updates/preconfig/0.10", "install/updates/preconfig.php", "templates/Sparkle", "lib/version.inc.php", "lng/czech.lng.php", "lng/dutch.lng.php", "lng/english.lng.php", "lng/french.lng.php", "lng/german.lng.php", "lng/italian.lng.php", "lng/lng_references.php", "lng/portugues.lng.php", "lng/swedish.lng.php", "scripts", ); Update::cleanOldFiles($to_clean); Update::showUpdateStep("Adding new settings"); $panel_settings_mode = isset($_POST['panel_settings_mode']) ? (int)$_POST['panel_settings_mode'] : 0; Settings::AddNew("panel.settings_mode", $panel_settings_mode); $system_distribution = isset($_POST['system_distribution']) ? $_POST['system_distribution'] : 'bullseye'; Settings::AddNew("system.distribution", $system_distribution); Settings::AddNew("system.update_channel", 'stable'); Settings::AddNew("system.updatecheck_data", ''); Settings::AddNew("system.update_notify_last", $update_to); Settings::AddNew("panel.phpconfigs_hidesubdomains", '1'); Update::lastStepStatus(0); Update::showUpdateStep("Adjusting existing settings"); Settings::Set('system.passwordcryptfunc', PASSWORD_DEFAULT); // remap default-language $lang_map = [ 'Deutsch' => 'de', 'English' => 'en', 'Français' => 'fr', 'Português' => 'pt', 'Italiano' => 'it', 'Nederlands' => 'nl', 'Svenska' => 'se', 'Česká republika' => 'cz' ]; // update user default languages $upd_adm_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_ADMINS . "` SET `def_language` = :nv WHERE `def_language` = :ov"); $upd_cus_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `def_language` = :nv WHERE `def_language` = :ov"); foreach ($lang_map as $old_val => $new_val) { Database::pexecute($upd_adm_stmt, ['nv' => $new_val, 'ov' => $old_val]); Database::pexecute($upd_cus_stmt, ['nv' => $new_val, 'ov' => $old_val]); } Settings::Set('panel.standardlanguage', $lang_map[Settings::Get('panel_standardlanguage')] ?? 'en'); Database::query("DELETE FROM `" . TABLE_PANEL_SETTINGS . "` WHERE `settinggroup` = 'system' AND `varname` = 'debug_cron'"); Database::query("DELETE FROM `" . TABLE_PANEL_SETTINGS . "` WHERE `settinggroup` = 'system' AND `varname` = 'letsencryptcountrycode'"); Database::query("DELETE FROM `" . TABLE_PANEL_SETTINGS . "` WHERE `settinggroup` = 'system' AND `varname` = 'letsencryptstate'"); Update::lastStepStatus(0); Update::showUpdateStep("Updating email account password-hashes"); Database::query("UPDATE `" . TABLE_MAIL_USERS . "` SET `password_enc` = REPLACE(`password_enc`, '$1$', '{MD5-CRYPT}$1$') WHERE SUBSTRING(`password_enc`, 1, 3) = '$1$'"); Database::query("UPDATE `" . TABLE_MAIL_USERS . "` SET `password_enc` = REPLACE(`password_enc`, '$5$', '{SHA256-CRYPT}$5$') WHERE SUBSTRING(`password_enc`, 1, 3) = '$5$'"); Database::query("UPDATE `" . TABLE_MAIL_USERS . "` SET `password_enc` = REPLACE(`password_enc`, '$6$', '{SHA512-CRYPT}$6$') WHERE SUBSTRING(`password_enc`, 1, 3) = '$6$'"); Database::query("UPDATE `" . TABLE_MAIL_USERS . "` SET `password_enc` = REPLACE(`password_enc`, '$2y$', '{BLF-CRYPT}$2y$') WHERE SUBSTRING(`password_enc`, 1, 4) = '$2y$'"); Update::lastStepStatus(0); Froxlor::updateToVersion($update_to); } if (Froxlor::isDatabaseVersion('202112310')) { Update::showUpdateStep("Adjusting traffic tool settings"); $traffic_tool = Settings::Get('system.awstats_enabled') == 1 ? 'awstats' : 'webalizer'; Settings::AddNew("system.traffictool", $traffic_tool); Database::query("DELETE FROM `" . TABLE_PANEL_SETTINGS . "` WHERE `settinggroup` = 'system' AND `varname` = 'awstats_enabled'"); Update::lastStepStatus(0); Froxlor::updateToDbVersion('202211030'); } if (Froxlor::isDatabaseVersion('202211030')) { Update::showUpdateStep("Creating backward compatibility for cronjob"); $disabled = explode(',', ini_get('disable_functions')); $exec_allowed = !in_array('exec', $disabled); // check whether old files could be deleted in previous updates and if not, // user should run cron to regenerate cron.d-file manually as he will run // the other commands manually only after the update so this file would be deleted too if ($exec_allowed) { $complete_filedir = Froxlor::getInstallDir() . '/scripts'; mkdir($complete_filedir, 0750, true); $newCronBin = Froxlor::getInstallDir() . '/bin/froxlor-cli'; $compCron = <<
' . $cron_run_cmd . '
'); } Froxlor::updateToDbVersion('202212060'); } if (Froxlor::isFroxlorVersion('2.0.0-beta1')) { Update::showUpdateStep("Updating from 2.0.0-beta1 to 2.0.0", false); Froxlor::updateToVersion('2.0.0'); } if (Froxlor::isFroxlorVersion('2.0.0')) { Update::showUpdateStep("Updating from 2.0.0 to 2.0.1", false); if (!isset($has_customer_table_update_200)) { Update::showUpdateStep("Creating new tables and fields"); // new customer allowed_mysqlserver field Database::query("ALTER TABLE `" . TABLE_PANEL_CUSTOMERS . "` CHANGE COLUMN `allowed_mysqlserver` `allowed_mysqlserver` text NOT NULL;"); Database::query("ALTER TABLE `" . TABLE_PANEL_CUSTOMERS . "` CHANGE COLUMN `allowed_phpconfigs` `allowed_phpconfigs` text NOT NULL;"); Database::query("ALTER TABLE `" . TABLE_PANEL_CUSTOMERS . "` CHANGE COLUMN `customernumber` `customernumber` varchar(100) NOT NULL default '';"); Update::lastStepStatus(0); } Froxlor::updateToVersion('2.0.1'); } if (Froxlor::isFroxlorVersion('2.0.1')) { Update::showUpdateStep("Updating from 2.0.1 to 2.0.2", false); Froxlor::updateToVersion('2.0.2'); } if (Froxlor::isFroxlorVersion('2.0.2')) { Update::showUpdateStep("Updating from 2.0.2 to 2.0.3", false); Froxlor::updateToVersion('2.0.3'); } if (Froxlor::isFroxlorVersion('2.0.3')) { Update::showUpdateStep("Updating from 2.0.3 to 2.0.4", false); $complete_filedir = Froxlor::getInstallDir() . '/scripts'; // check if compat. cronjob still exists (most likely didn't run successfully b/c of error from former 2.0 release) if (@file_exists($complete_filedir . '/froxlor_master_cronjob.php')) { Update::showUpdateStep("Adjusting backward compatibility for cronjob"); $disabled = explode(',', ini_get('disable_functions')); $exec_allowed = !in_array('exec', $disabled); if ($exec_allowed) { $newCronBin = Froxlor::getInstallDir() . '/bin/froxlor-cli'; $compCron = <<
' . $cron_run_cmd . '
'); } } Froxlor::updateToVersion('2.0.4'); } if (Froxlor::isFroxlorVersion('2.0.4')) { Update::showUpdateStep("Updating from 2.0.4 to 2.0.5", false); Froxlor::updateToVersion('2.0.5'); } if (Froxlor::isFroxlorVersion('2.0.5')) { Update::showUpdateStep("Updating from 2.0.5 to 2.0.6", false); Update::showUpdateStep("Updating possible missing email account password-hashes"); Database::query("UPDATE `" . TABLE_MAIL_USERS . "` SET `password_enc` = REPLACE(`password_enc`, '$1$', '{MD5-CRYPT}$1$') WHERE SUBSTRING(`password_enc`, 1, 3) = '$1$'"); Database::query("UPDATE `" . TABLE_MAIL_USERS . "` SET `password_enc` = REPLACE(`password_enc`, '$5$', '{SHA256-CRYPT}$5$') WHERE SUBSTRING(`password_enc`, 1, 3) = '$5$'"); Database::query("UPDATE `" . TABLE_MAIL_USERS . "` SET `password_enc` = REPLACE(`password_enc`, '$6$', '{SHA512-CRYPT}$6$') WHERE SUBSTRING(`password_enc`, 1, 3) = '$6$'"); Database::query("UPDATE `" . TABLE_MAIL_USERS . "` SET `password_enc` = REPLACE(`password_enc`, '$2y$', '{BLF-CRYPT}$2y$') WHERE SUBSTRING(`password_enc`, 1, 4) = '$2y$'"); Update::lastStepStatus(0); Froxlor::updateToVersion('2.0.6'); } if (Froxlor::isFroxlorVersion('2.0.6')) { Update::showUpdateStep("Updating from 2.0.6 to 2.0.7", false); Update::showUpdateStep("Correcting allowed_mysqlserver for customers"); Database::query("UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `allowed_mysqlserver` = '[0]' WHERE `allowed_mysqlserver` = ''"); Update::lastStepStatus(0); Froxlor::updateToVersion('2.0.7'); } if (Froxlor::isDatabaseVersion('202212060')) { Update::showUpdateStep("Validating acme.sh challenge path"); $acmesh_challenge_dir = Settings::Get('system.letsencryptchallengepath'); $system_letsencryptchallengepath_upd = isset($_POST['system_letsencryptchallengepath_upd']) ? $_POST['system_letsencryptchallengepath_upd'] : $acmesh_challenge_dir; if ($acmesh_challenge_dir != $system_letsencryptchallengepath_upd) { Settings::Set('system.letsencryptchallengepath', $system_letsencryptchallengepath_upd); if ((int)Settings::Get('system.leenabled') == 1) { // create JSON string for --apply $dist = Settings::Get('system.distribution'); $webserver = Settings::Get('system.webserver'); if ($webserver == 'apache2') { $webserver = 'apache22'; if (Settings::Get('system.apache24')) { $webserver = 'apache24'; } } $apply_json = '{"http":"' . $webserver . '","dns":"x","smtp":"x","mail":"x","ftp":"x","distro":"' . $dist . '","system":[]}'; Update::lastStepStatus(1, 'manual commands needed', "Please reconfigure webserver service using
bin/froxlor-cli froxlor:config-services --apply='" . $apply_json . "'
" . '
or adjust the path manually in
' . Settings::Get('system.letsencryptacmeconf') . '
' . '

In case you already have certificates issued, run the following command to validate and correct the webroot used for renewal:
' . '
bin/froxlor-cli froxlor:validate-acme-webroot

' ); } else { Update::lastStepStatus(0); } } else { Update::lastStepStatus(0); } Froxlor::updateToDbVersion('202301120'); } if (Froxlor::isFroxlorVersion('2.0.7')) { Update::showUpdateStep("Updating from 2.0.7 to 2.0.8", false); // adjust file-logging to be set to froxlor/logs/ $logtypes = explode(',', Settings::Get('logger.logtypes')); if (in_array('file', $logtypes)) { Update::showUpdateStep("Adjusting froxlor logfile for system-logging to be stored in logs/froxlor.log"); Settings::Set('logger.logfile', 'froxlor.log'); Update::lastStepStatus(0); } Froxlor::updateToVersion('2.0.8'); } if (Froxlor::isDatabaseVersion('202301120')) { Update::showUpdateStep("Adding new setting for DNS resolver when using Let's Encrypt"); $system_le_domain_dnscheck_resolver = isset($_POST['system_le_domain_dnscheck_resolver']) ? $_POST['system_le_domain_dnscheck_resolver'] : '1.1.1.1'; Settings::AddNew("system.le_domain_dnscheck_resolver", $system_le_domain_dnscheck_resolver); Update::lastStepStatus(0); Froxlor::updateToDbVersion('202301180'); } if (Froxlor::isFroxlorVersion('2.0.8')) { Update::showUpdateStep("Updating from 2.0.8 to 2.0.9", false); Froxlor::updateToVersion('2.0.9'); } if (Froxlor::isFroxlorVersion('2.0.9')) { Update::showUpdateStep("Updating from 2.0.9 to 2.0.10", false); Froxlor::updateToVersion('2.0.10'); } if (Froxlor::isDatabaseVersion('202301180')) { Update::showUpdateStep("Adding new setting for 'Allow API access' default value for new customers"); Settings::AddNew("api.customer_default", "1"); Update::lastStepStatus(0); Froxlor::updateToDbVersion('202302030'); } if (Froxlor::isFroxlorVersion('2.0.10')) { Update::showUpdateStep("Updating from 2.0.10 to 2.0.11", false); Froxlor::updateToVersion('2.0.11'); } if (Froxlor::isFroxlorVersion('2.0.11')) { Update::showUpdateStep("Updating from 2.0.11 to 2.0.12", false); Froxlor::updateToVersion('2.0.12'); } if (Froxlor::isFroxlorVersion('2.0.12')) { Update::showUpdateStep("Updating from 2.0.12 to 2.0.13", false); Froxlor::updateToVersion('2.0.13'); } if (Froxlor::isDatabaseVersion('202302030')) { Update::showUpdateStep("Correcting language mapping of templates created pre 2.0.x"); // languages from 0.10.x $language_mapping_comp = [ 'de' => 'Deutsch', 'en' => 'English', 'fr' => 'Français', 'pt' => 'Português', 'it' => 'Italiano', 'nl' => 'Nederlands', 'se' => 'Svenska', 'cz' => 'Česká republika' ]; $upd_tpl_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_TEMPLATES . "` SET `language` = :iso WHERE `language` = :lng"); foreach ($language_mapping_comp as $iso => $lang) { Database::pexecute($upd_tpl_stmt, ['iso' => $iso, 'lng' => $lang]); } Update::lastStepStatus(0); Update::showUpdateStep("Enhancing ssl data table"); Database::query("ALTER TABLE `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` CHANGE `expirationdate` `validtodate` datetime DEFAULT NULL;"); Database::query("ALTER TABLE `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` ADD `validfromdate` datetime DEFAULT NULL AFTER `ssl_fullchain_file`;"); Database::query("ALTER TABLE `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` ADD `issuer` varchar(255) NOT NULL default '' AFTER `validtodate`;"); Update::lastStepStatus(0); Update::showUpdateStep("Filling new ssl data fields with existing certificate data"); $crt_upd_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` SET `validfromdate` = :validfromdate, `issuer` = :issuer WHERE `id` = :id"); $crt_stmt = Database::prepare("SELECT * FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "`"); Database::pexecute($crt_stmt); while ($cert = $crt_stmt->fetch(\PDO::FETCH_ASSOC)) { $cert_content = openssl_x509_parse($cert['ssl_cert_file']); if (is_array($cert_content)) { $validfromdate = empty($cert_content['validFrom_time_t']) ? null : date("Y-m-d H:i:s", $cert_content['validFrom_time_t']); $issuer = $cert_content['issuer']['O'] ?? ""; Database::pexecute($crt_upd_stmt, ['validfromdate' => $validfromdate, 'issuer' => $issuer, 'id' => $cert['id']]); } } // clear possible user customized columns Database::query("DELETE FROM `" . TABLE_PANEL_USERCOLUMNS . "` WHERE `section` = 'sslcertificates_list'"); Update::lastStepStatus(0); Froxlor::updateToDbVersion('202303150'); } if (Froxlor::isFroxlorVersion('2.0.13')) { Update::showUpdateStep("Updating from 2.0.13 to 2.0.14", false); Froxlor::updateToVersion('2.0.14'); } if (Froxlor::isFroxlorVersion('2.0.14')) { Update::showUpdateStep("Updating from 2.0.14 to 2.0.15", false); Froxlor::updateToVersion('2.0.15'); } if (Froxlor::isDatabaseVersion('202303150')) { Update::showUpdateStep("Adding new request rate limit settings"); Settings::AddNew("system.req_limit_per_interval", "60"); Settings::AddNew("system.req_limit_interval", "60"); Update::lastStepStatus(0); Froxlor::updateToDbVersion('202304260'); } if (Froxlor::isFroxlorVersion('2.0.15')) { Update::showUpdateStep("Updating from 2.0.15 to 2.0.16", false); Froxlor::updateToVersion('2.0.16'); } if (Froxlor::isFroxlorVersion('2.0.16')) { Update::showUpdateStep("Updating from 2.0.16 to 2.0.17", false); Froxlor::updateToVersion('2.0.17'); } if (Froxlor::isFroxlorVersion('2.0.17')) { Update::showUpdateStep("Updating from 2.0.17 to 2.0.18", false); Froxlor::updateToVersion('2.0.18'); } if (Froxlor::isFroxlorVersion('2.0.18')) { Update::showUpdateStep("Updating from 2.0.18 to 2.0.19", false); Froxlor::updateToVersion('2.0.19'); } if (Froxlor::isFroxlorVersion('2.0.19')) { Update::showUpdateStep("Updating from 2.0.19 to 2.0.20", false); Froxlor::updateToVersion('2.0.20'); } if (Froxlor::isFroxlorVersion('2.0.20')) { Update::showUpdateStep("Updating from 2.0.20 to 2.0.21", false); Froxlor::updateToVersion('2.0.21'); } if (Froxlor::isFroxlorVersion('2.0.21')) { Update::showUpdateStep("Updating from 2.0.21 to 2.0.22", false); Froxlor::updateToVersion('2.0.22'); } if (Froxlor::isFroxlorVersion('2.0.22')) { Update::showUpdateStep("Updating from 2.0.22 to 2.0.23", false); Froxlor::updateToVersion('2.0.23'); } if (Froxlor::isFroxlorVersion('2.0.23')) { Update::showUpdateStep("Updating from 2.0.23 to 2.0.24", false); Froxlor::updateToVersion('2.0.24'); } ================================================ FILE: install/updates/froxlor/update_2.1.inc.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\Froxlor; use Froxlor\Install\Update; use Froxlor\Settings; if (!defined('_CRON_UPDATE')) { if (!defined('AREA') || (defined('AREA') && AREA != 'admin') || !isset($userinfo['loginname']) || (isset($userinfo['loginname']) && $userinfo['loginname'] == '')) { header('Location: ../../../../index.php'); exit(); } } if (Froxlor::isFroxlorVersion('2.0.24')) { Update::showUpdateStep("Cleaning domains table"); Database::query("ALTER TABLE `" . TABLE_PANEL_DOMAINS . "` ROW_FORMAT=DYNAMIC;"); Database::query("ALTER TABLE `" . TABLE_PANEL_DOMAINS . "` DROP COLUMN `ismainbutsubto`;"); Update::lastStepStatus(0); Update::showUpdateStep("Creating new tables and fields"); Database::query("DROP TABLE IF EXISTS `panel_loginlinks`;"); $sql = "CREATE TABLE `panel_loginlinks` ( `hash` varchar(500) NOT NULL, `loginname` varchar(50) NOT NULL, `valid_until` int(15) NOT NULL, `allowed_from` text NOT NULL, UNIQUE KEY `loginname` (`loginname`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;"; Database::query($sql); Update::lastStepStatus(0); Update::showUpdateStep("Adding new settings"); Settings::AddNew('panel.menu_collapsed', 1); Update::lastStepStatus(0); Update::showUpdateStep("Adjusting setting for deactivated webroot"); $current_deactivated_webroot = Settings::Get('system.deactivateddocroot'); if (empty($current_deactivated_webroot)) { Settings::Set('system.deactivateddocroot', FileDir::makeCorrectDir(Froxlor::getInstallDir() . '/templates/misc/deactivated/')); Update::lastStepStatus(0); } else { Update::lastStepStatus(1, 'Customized setting, not changing'); } Update::showUpdateStep("Adjusting cronjobs"); $cfupd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_CRONRUNS . "` SET `module`= 'froxlor/export', `cronfile` = 'export', `cronclass` = :cc, `interval` = '1 HOUR', `desc_lng_key` = 'cron_export' WHERE `module` = 'froxlor/backup' "); Database::pexecute($cfupd_stmt, [ 'cc' => '\\Froxlor\\Cron\\System\\ExportCron' ]); Update::lastStepStatus(0); Update::showUpdateStep("Adjusting system for data-export function"); Database::query("UPDATE `" . TABLE_PANEL_SETTINGS . "`SET `varname` = 'exportenabled' WHERE `settinggroup`= 'system' AND `varname`= 'backupenabled'"); Database::query("UPDATE `" . TABLE_PANEL_SETTINGS . "`SET `value` = REPLACE(`value`, 'extras.backup', 'extras.export') WHERE `settinggroup` = 'panel' AND `varname` = 'customer_hide_options'"); Database::query("DELETE FROM `" . TABLE_PANEL_USERCOLUMNS . "` WHERE `section` = 'backup_list'"); Database::query("DELETE FROM `" . TABLE_PANEL_TASKS . "` WHERE `type` = '20'"); Update::lastStepStatus(0); Froxlor::updateToDbVersion('202305240'); Froxlor::updateToVersion('2.1.0-dev1'); } if (Froxlor::isFroxlorVersion('2.1.0-dev1')) { Update::showUpdateStep("Updating from 2.1.0-dev1 to 2.1.0-beta1", false); Froxlor::updateToVersion('2.1.0-beta1'); } if (Froxlor::isFroxlorVersion('2.1.0-beta1')) { Update::showUpdateStep("Updating from 2.1.0-beta1 to 2.1.0-beta2", false); Update::showUpdateStep("Removing unused table"); Database::query("DROP TABLE IF EXISTS `panel_sessions`;"); Update::lastStepStatus(0); Froxlor::updateToVersion('2.1.0-beta2'); } if (Froxlor::isFroxlorVersion('2.1.0-beta2')) { Update::showUpdateStep("Updating from 2.1.0-beta2 to 2.1.0-rc1", false); Froxlor::updateToVersion('2.1.0-rc1'); } if (Froxlor::isFroxlorVersion('2.1.0-rc1')) { Update::showUpdateStep("Updating from 2.1.0-rc1 to 2.1.0-rc2", false); Update::showUpdateStep("Adjusting setting spf_entry"); $spf_entry = Settings::Get('spf.spf_entry'); if (!preg_match('/^v=spf[a-z0-9:~?\s.-]+$/i', $spf_entry)) { Settings::Set('spf.spf_entry', 'v=spf1 a mx -all'); Update::lastStepStatus(1, 'corrected'); } else { Update::lastStepStatus(0); } Froxlor::updateToVersion('2.1.0-rc2'); } if (Froxlor::isDatabaseVersion('202305240')) { Update::showUpdateStep("Adjusting file-template file extension setttings"); $current_fileextension = Settings::Get('system.index_file_extension'); Database::query("DELETE FROM `" . TABLE_PANEL_SETTINGS . "` WHERE `settinggroup`= 'system' AND `varname`= 'index_file_extension'"); Database::query("ALTER TABLE `" . TABLE_PANEL_TEMPLATES . "` ADD `file_extension` varchar(50) NOT NULL default 'html';"); if (!empty(trim($current_fileextension)) && strtolower(trim($current_fileextension)) != 'html') { $stmt = Database::prepare("UPDATE `" . TABLE_PANEL_TEMPLATES . "` SET `file_extension` = :ext WHERE `templategroup` = 'files'"); Database::pexecute($stmt, ['ext' => strtolower(trim($current_fileextension))]); } Update::lastStepStatus(0); Froxlor::updateToDbVersion('202311260'); } if (Froxlor::isFroxlorVersion('2.1.0-rc2')) { Update::showUpdateStep("Updating from 2.1.0-rc2 to 2.1.0-rc3", false); Froxlor::updateToVersion('2.1.0-rc3'); } if (Froxlor::isDatabaseVersion('202311260')) { $to_clean = array( "install/updates/froxlor/update_2.x.inc.php", "install/updates/preconfig/preconfig_2.x.inc.php", "lib/Froxlor/Api/Commands/CustomerBackups.php", "lib/Froxlor/Cli/Action", "lib/Froxlor/Cli/Action.php", "lib/Froxlor/Cli/CmdLineHandler.php", "lib/Froxlor/Cli/ConfigServicesCmd.php", "lib/Froxlor/Cli/PhpSessioncleanCmd.php", "lib/Froxlor/Cli/SwitchServerIpCmd.php", "lib/Froxlor/Cli/UpdateCliCmd.php", "lib/Froxlor/Cron/System/BackupCron.php", "lib/formfields/customer/extras/formfield.backup.php", "lib/tablelisting/customer/tablelisting.backups.php", "templates/Froxlor/assets/mix-manifest.json", "templates/Froxlor/assets/css", "templates/Froxlor/assets/webfonts", "templates/Froxlor/assets/js/main.js", "templates/Froxlor/assets/js/main.js.LICENSE.txt", "templates/Froxlor/src", "templates/Froxlor/user/change_language.html.twig", "templates/Froxlor/user/change_password.html.twig", "templates/Froxlor/user/change_theme.html.twig", "tests/Backup/CustomerBackupsTest.php" ); Update::cleanOldFiles($to_clean); Froxlor::updateToDbVersion('202312050'); } if (Froxlor::isFroxlorVersion('2.1.0-rc3')) { Update::showUpdateStep("Updating from 2.1.0-rc3 to 2.1.0 stable", false); Froxlor::updateToVersion('2.1.0'); } if (Froxlor::isFroxlorVersion('2.1.0')) { Update::showUpdateStep("Updating from 2.1.0 to 2.1.1", false); Froxlor::updateToVersion('2.1.1'); } if (Froxlor::isDatabaseVersion('202312050')) { $to_clean = array( "lib/configfiles/centos7.xml", "lib/configfiles/centos8.xml", "lib/configfiles/stretch.xml", "lib/configfiles/xenial.xml", "lib/configfiles/buster.xml", "lib/configfiles/bionic.xml", ); Update::cleanOldFiles($to_clean); Froxlor::updateToDbVersion('202312100'); } if (Froxlor::isDatabaseVersion('202312100')) { Update::showUpdateStep("Adjusting table row format of larger tables"); Database::query("ALTER TABLE `" . TABLE_PANEL_ADMINS . "` ROW_FORMAT=DYNAMIC;"); Database::query("ALTER TABLE `" . TABLE_PANEL_DOMAINS . "` ROW_FORMAT=DYNAMIC;"); Update::lastStepStatus(0); Froxlor::updateToDbVersion('202312120'); } if (Froxlor::isFroxlorVersion('2.1.1')) { Update::showUpdateStep("Updating from 2.1.1 to 2.1.2", false); Froxlor::updateToVersion('2.1.2'); } if (Froxlor::isFroxlorVersion('2.1.2')) { Update::showUpdateStep("Updating from 2.1.2 to 2.1.3", false); Froxlor::updateToVersion('2.1.3'); } if (Froxlor::isFroxlorVersion('2.1.3')) { Update::showUpdateStep("Updating from 2.1.3 to 2.1.4", false); Froxlor::updateToVersion('2.1.4'); } if (Froxlor::isFroxlorVersion('2.1.4')) { Update::showUpdateStep("Updating from 2.1.4 to 2.1.5", false); Froxlor::updateToVersion('2.1.5'); } if (Froxlor::isFroxlorVersion('2.1.5')) { Update::showUpdateStep("Updating from 2.1.5 to 2.1.6", false); Froxlor::updateToVersion('2.1.6'); } if (Froxlor::isFroxlorVersion('2.1.6')) { Update::showUpdateStep("Updating from 2.1.6 to 2.1.7", false); Froxlor::updateToVersion('2.1.7'); } if (Froxlor::isFroxlorVersion('2.1.7')) { Update::showUpdateStep("Updating from 2.1.7 to 2.1.8", false); Froxlor::updateToVersion('2.1.8'); } if (Froxlor::isFroxlorVersion('2.1.8')) { Update::showUpdateStep("Updating from 2.1.8 to 2.1.9", false); Froxlor::updateToVersion('2.1.9'); } ================================================ FILE: install/updates/froxlor/update_2.2.inc.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ use Froxlor\Database\Database; use Froxlor\Database\DbManager; use Froxlor\Froxlor; use Froxlor\FroxlorLogger; use Froxlor\Install\Update; use Froxlor\Settings; if (!defined('_CRON_UPDATE')) { if (!defined('AREA') || (defined('AREA') && AREA != 'admin') || !isset($userinfo['loginname']) || (isset($userinfo['loginname']) && $userinfo['loginname'] == '')) { header('Location: ../../../../index.php'); exit(); } } if (Froxlor::isFroxlorVersion('2.1.9')) { Update::showUpdateStep("Enhancing virtual email table"); Database::query("ALTER TABLE `" . TABLE_MAIL_VIRTUAL . "` ADD `spam_tag_level` float(4,1) NOT NULL DEFAULT 7.0;"); Database::query("ALTER TABLE `" . TABLE_MAIL_VIRTUAL . "` ADD `spam_kill_level` float(4,1) NOT NULL DEFAULT 14.0;"); Database::query("ALTER TABLE `" . TABLE_MAIL_VIRTUAL . "` ADD `bypass_spam` tinyint(1) NOT NULL default '0';"); Database::query("ALTER TABLE `" . TABLE_MAIL_VIRTUAL . "` ADD `policy_greylist` tinyint(1) NOT NULL default '1';"); Update::lastStepStatus(0); Update::showUpdateStep("Adjusting settings"); $antispam_activated = $_POST['antispam_activated'] ?? 0; Database::query("UPDATE `" . TABLE_PANEL_SETTINGS . "` SET `settinggroup` = 'antispam', `varname` = 'activated', `value` = '" . (int)$antispam_activated . "' WHERE `settinggroup` = 'dkim' AND `varname` = 'use_dkim';"); Database::query("UPDATE `" . TABLE_PANEL_SETTINGS . "` SET `settinggroup` = 'antispam', `varname` = 'reload_command', `value` = 'service rspamd restart' WHERE `settinggroup` = 'dkim' AND `varname` = 'dkimrestart_command';"); Database::query("UPDATE `" . TABLE_PANEL_SETTINGS . "` SET `settinggroup` = 'antispam', `varname` = 'config_file', `value` = '/etc/rspamd/local.d/froxlor_settings.conf' WHERE `settinggroup` = 'dkim' AND `varname` = 'dkim_prefix';"); Database::query("UPDATE `" . TABLE_PANEL_SETTINGS . "` SET `settinggroup` = 'antispam' WHERE `settinggroup` = 'dkim' AND `varname` = 'dkim_keylength';"); Settings::AddNew("dmarc.use_dmarc", "0"); Settings::AddNew("dmarc.dmarc_entry", "v=DMARC1; p=none;"); Database::query("DELETE FROM `" . TABLE_PANEL_SETTINGS . "` WHERE `settinggroup` = 'dkim' AND `varname` = 'privkeysuffix';"); Database::query("DELETE FROM `" . TABLE_PANEL_SETTINGS . "` WHERE `settinggroup` = 'dkim' AND `varname` = 'dkim_domains';"); Database::query("DELETE FROM `" . TABLE_PANEL_SETTINGS . "` WHERE `settinggroup` = 'dkim' AND `varname` = 'dkim_algorithm';"); Database::query("DELETE FROM `" . TABLE_PANEL_SETTINGS . "` WHERE `settinggroup` = 'dkim' AND `varname` = 'dkim_notes';"); Database::query("DELETE FROM `" . TABLE_PANEL_SETTINGS . "` WHERE `settinggroup` = 'dkim' AND `varname` = 'dkim_add_adsp';"); Database::query("DELETE FROM `" . TABLE_PANEL_SETTINGS . "` WHERE `settinggroup` = 'dkim' AND `varname` = 'dkim_dkimkeys';"); Database::query("DELETE FROM `" . TABLE_PANEL_SETTINGS . "` WHERE `settinggroup` = 'dkim' AND `varname` = 'dkim_servicetype';"); Database::query("DELETE FROM `" . TABLE_PANEL_SETTINGS . "` WHERE `settinggroup` = 'dkim' AND `varname` = 'dkim_add_adsppolicy';"); Update::lastStepStatus(0); if ($antispam_activated) { Update::showUpdateStep("Converting existing domainkeys"); $sel_stmt = Database::prepare("SELECT * FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `dkim` = '1' AND `dkim_pubkey` <> ''"); Database::pexecute($sel_stmt); $upd_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_DOMAINS . "` SET `dkim_pubkey` = :pkey WHERE `id` = :did"); while ($domain = $sel_stmt->fetch(\PDO::FETCH_ASSOC)) { $pubkey = trim(preg_replace( '/-----BEGIN PUBLIC KEY-----(.+)-----END PUBLIC KEY-----/s', '$1', str_replace("\n", '', $domain['dkim_pubkey']) )); Database::pexecute($upd_stmt, ['pkey' => $pubkey, 'did' => $domain['id']]); } Update::lastStepStatus(0); Update::showUpdateStep("Configure antispam services"); $froxlorCliBin = Froxlor::getInstallDir() . '/bin/froxlor-cli'; $currentDistro = Settings::Get('system.distribution'); $manual_command = <<
" . $manual_command . "
" ); } else { Update::showUpdateStep("Removing existing domainkeys because antispam is disabled"); Database::query("UPDATE `" . TABLE_PANEL_DOMAINS . "` SET `dkim` = '0', `dkim_id` = '0', `dkim_privkey` = '', `dkim_pubkey` = '' WHERE `dkim` = '1';"); Update::lastStepStatus(1, '!!!'); } Update::showUpdateStep("Enhancing admin and user table"); Database::query("ALTER TABLE `" . TABLE_PANEL_ADMINS . "` ADD `gui_access` tinyint(1) NOT NULL default '1';"); Database::query("ALTER TABLE `" . TABLE_PANEL_CUSTOMERS . "` ADD `gui_access` tinyint(1) NOT NULL default '1';"); Update::lastStepStatus(0); $to_clean = [ 'actions/admin/settings/180.dkim.php', 'actions/admin/settings/185.spf.php', ]; Update::cleanOldFiles($to_clean); Froxlor::updateToDbVersion('202312230'); Froxlor::updateToVersion('2.2.0-dev1'); } if (Froxlor::isDatabaseVersion('202312230')) { Update::showUpdateStep("Adding new settings"); Settings::AddNew("system.le_renew_services", ""); Settings::AddNew("system.le_renew_hook", "systemctl restart postfix dovecot proftpd"); Update::lastStepStatus(0); Froxlor::updateToDbVersion('202401090'); } if (Froxlor::isFroxlorVersion('2.2.0-dev1')) { Update::showUpdateStep("Updating from 2.2.0-dev1 to 2.2.0-rc1", false); Froxlor::updateToVersion('2.2.0-rc1'); } if (Froxlor::isDatabaseVersion('202401090')) { Update::showUpdateStep("Adding new table for 2fa tokens"); Database::query("DROP TABLE IF EXISTS `panel_2fa_tokens`;"); $sql = "CREATE TABLE `panel_2fa_tokens` ( `id` int(11) NOT NULL auto_increment, `selector` varchar(20) NOT NULL, `token` varchar(200) NOT NULL, `userid` int(11) NOT NULL default '0', `valid_until` int(15) NOT NULL, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;"; Database::query($sql); Update::lastStepStatus(0); Froxlor::updateToDbVersion('202407200'); } if (Froxlor::isFroxlorVersion('2.2.0-rc1')) { Update::showUpdateStep("Updating from 2.2.0-rc1 to 2.2.0-rc2", false); Froxlor::updateToVersion('2.2.0-rc2'); } if (Froxlor::isFroxlorVersion('2.2.0-rc2')) { Update::showUpdateStep("Updating from 2.2.0-rc2 to 2.2.0-rc3", false); Froxlor::updateToVersion('2.2.0-rc3'); } if (Froxlor::isDatabaseVersion('202407200')) { Update::showUpdateStep("Adjusting field in 2fa-token table"); Database::query("ALTER TABLE `panel_2fa_tokens` CHANGE COLUMN `selector` `selector` varchar(200) NOT NULL;"); Update::lastStepStatus(0); Froxlor::updateToDbVersion('202408140'); } if (Froxlor::isFroxlorVersion('2.2.0-rc3')) { Update::showUpdateStep("Updating from 2.2.0-rc3 to 2.2.0 stable", false); Froxlor::updateToVersion('2.2.0'); } if (Froxlor::isFroxlorVersion('2.2.0')) { Update::showUpdateStep("Updating from 2.2.0 to 2.2.1", false); Froxlor::updateToVersion('2.2.1'); } if (Froxlor::isDatabaseVersion('202408140')) { Update::showUpdateStep("Adding new rewrite-subject field to email table"); Database::query("ALTER TABLE `" . TABLE_MAIL_VIRTUAL . "` ADD `rewrite_subject` tinyint(1) NOT NULL default '1' AFTER `spam_tag_level`;"); Update::lastStepStatus(0); Froxlor::updateToDbVersion('202409280'); } if (Froxlor::isFroxlorVersion('2.2.1')) { Update::showUpdateStep("Updating from 2.2.1 to 2.2.2", false); Froxlor::updateToVersion('2.2.2'); } if (Froxlor::isFroxlorVersion('2.2.2')) { Update::showUpdateStep("Updating from 2.2.2 to 2.2.3", false); Froxlor::updateToVersion('2.2.3'); } if (Froxlor::isFroxlorVersion('2.2.3')) { Update::showUpdateStep("Updating from 2.2.3 to 2.2.4", false); Froxlor::updateToVersion('2.2.4'); } if (Froxlor::isFroxlorVersion('2.2.4')) { Update::showUpdateStep("Updating from 2.2.4 to 2.2.5", false); Froxlor::updateToVersion('2.2.5'); } if (Froxlor::isDatabaseVersion('202409280')) { Update::showUpdateStep("Adding new antispam settings"); Settings::AddNew("antispam.default_bypass_spam", "2"); Settings::AddNew("antispam.default_spam_rewrite_subject", "1"); Settings::AddNew("antispam.default_policy_greylist", "1"); Update::lastStepStatus(0); Froxlor::updateToDbVersion('202411200'); } if (Froxlor::isDatabaseVersion('202411200')) { Update::showUpdateStep("Adjusting customer mysql global user"); // get all customers that are not deactivated and that have at least one database (hence a global database-user) $customers = Database::query(" SELECT DISTINCT c.loginname, c.allowed_mysqlserver FROM `" . TABLE_PANEL_CUSTOMERS . "` c LEFT JOIN `" . TABLE_PANEL_DATABASES . "` d ON c.customerid = d.customerid WHERE c.deactivated = '0' AND d.id IS NOT NULL "); while ($customer = $customers->fetch(\PDO::FETCH_ASSOC)) { $current_allowed_mysqlserver = !empty($customer['allowed_mysqlserver']) ? json_decode($customer['allowed_mysqlserver'], true) : []; foreach ($current_allowed_mysqlserver as $dbserver) { // require privileged access for target db-server Database::needRoot(true, $dbserver, false); // get DbManager $dbm = new DbManager(FroxlorLogger::getInstanceOf()); foreach (array_map('trim', explode(',', Settings::Get('system.mysql_access_host'))) as $mysql_access_host) { try { if ($dbm->getManager()->userExistsOnHost($customer['loginname'], $mysql_access_host)) { // deactivate temporarily $dbm->getManager()->disableUser($customer['loginname'], $mysql_access_host); // re-enable $dbm->getManager()->enableUser($customer['loginname'], $mysql_access_host, true); } } catch (Exception $e) { // continue } } $dbm->getManager()->flushPrivileges(); Database::needRoot(); } } Update::lastStepStatus(0); Froxlor::updateToDbVersion('202412030'); } if (Froxlor::isFroxlorVersion('2.2.5')) { Update::showUpdateStep("Updating from 2.2.5 to 2.2.6", false); Froxlor::updateToVersion('2.2.6'); } if (Froxlor::isFroxlorVersion('2.2.6')) { Update::showUpdateStep("Updating from 2.2.6 to 2.2.7", false); Froxlor::updateToVersion('2.2.7'); } if (Froxlor::isFroxlorVersion('2.2.7')) { Update::showUpdateStep("Updating from 2.2.7 to 2.2.8", false); Froxlor::updateToVersion('2.2.8'); } ================================================ FILE: install/updates/froxlor/update_2.3.inc.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ use Froxlor\Database\Database; use Froxlor\Froxlor; use Froxlor\Install\Update; use Froxlor\Settings; if (!defined('_CRON_UPDATE')) { if (!defined('AREA') || (defined('AREA') && AREA != 'admin') || !isset($userinfo['loginname']) || (isset($userinfo['loginname']) && $userinfo['loginname'] == '')) { header('Location: ../../../../index.php'); exit(); } } if (Froxlor::isDatabaseVersion('202412030')) { Update::showUpdateStep("Enhancing customer table"); Database::query("ALTER TABLE `" . TABLE_PANEL_CUSTOMERS . "` ADD `shell_allowed` tinyint(1) NOT NULL DEFAULT 0;"); Update::lastStepStatus(0); if (Settings::Get('system.allow_customer_shell') == '1') { Update::showUpdateStep("Allowing shell-usage to current customers as setting is globally enabled"); Database::query("UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `shell_allowed` = '1';"); Update::lastStepStatus(0); } Froxlor::updateToDbVersion('202508310'); Update::showUpdateStep("Updating from 2.2.8 to 2.3.0-dev1", false); Froxlor::updateToVersion('2.3.0-dev1'); } if (Froxlor::isDatabaseVersion('202508310')) { Update::showUpdateStep("Remove old settings"); Database::query("DELETE FROM `" . TABLE_PANEL_SETTINGS . "` WHERE `settinggroup` = 'system' AND `varname` = 'perl_path'"); Update::lastStepStatus(0); if (Settings::Get('system.webserver') == 'lighttpd') { $system_alt_webserver = $_POST['system_alt_webserver'] ?? 'apache2'; Update::showUpdateStep("Switching from lighttpd to " . $system_alt_webserver); Settings::Set('system.webserver', $system_alt_webserver); Settings::Set('system.apache24', 1); Update::lastStepStatus(0); } Froxlor::updateToDbVersion('202509010'); } if (Froxlor::isDatabaseVersion('202509010')) { Update::showUpdateStep("Adding new table for user ssh-keys"); Database::query("DROP TABLE IF EXISTS `panel_sshkeys`;"); $sql = "CREATE TABLE `panel_sshkeys` ( `id` int(11) NOT NULL auto_increment, `customerid` int(11) NOT NULL, `ftp_user_id` int(20) NOT NULL, `ssh_pubkey` text NOT NULL, `description` varchar(255) NOT NULL DEFAULT '', PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;"; Database::query($sql); Update::lastStepStatus(0); Froxlor::updateToDbVersion('202509060'); } if (Froxlor::isDatabaseVersion('202509060')) { Update::showUpdateStep("Disabling OCSP for Let's Encrypt enabled domains, as service is EOL"); Database::query("UPDATE `" . TABLE_PANEL_DOMAINS . "` SET `ocsp_stapling` = '0' WHERE `letsencrypt` = '1';"); Update::lastStepStatus(0); // clear templates cache Update::cleanOldFiles([ 'cache/*' ]); Froxlor::updateToDbVersion('202509120'); } if (Froxlor::isDatabaseVersion('202509120')) { Update::showUpdateStep("Adding new settings"); Settings::AddNew("system.http3_support", "0"); Update::lastStepStatus(0); Update::showUpdateStep("Adding http3 field to domain table"); Database::query("ALTER TABLE `" . TABLE_PANEL_DOMAINS . "` ADD `http3` tinyint(1) NOT NULL default '0' AFTER `http2`;"); Update::lastStepStatus(0); Froxlor::updateToDbVersion('202509210'); } if (Froxlor::isDatabaseVersion('202509210')) { Update::showUpdateStep("Adding new table for email sender aliases"); Database::query("DROP TABLE IF EXISTS `mail_sender_aliases`;"); $sql = "CREATE TABLE `mail_sender_aliases` ( `id` int(11) NOT NULL auto_increment, `email` varchar(255) NOT NULL, `allowed_sender` varchar(255) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `email_sender` (`email`, `allowed_sender`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;"; Database::query($sql); $mail_enable_allow_sender = $_POST['mail_enable_allow_sender'] ?? 0; Settings::AddNew('mail.enable_allow_sender', $mail_enable_allow_sender); $mail_allow_external_domains = $_POST['mail_allow_external_domains'] ?? 0; Settings::AddNew('mail.allow_external_domains', $mail_allow_external_domains); Update::lastStepStatus(0); $to_clean = [ 'lib/configfiles/gentoo.xml', ]; Update::cleanOldFiles($to_clean); Froxlor::updateToDbVersion('202509270'); } if (Froxlor::isDatabaseVersion('202509270')) { Settings::AddNew('system.distro_mismatch', '0'); Froxlor::updateToDbVersion('202511020'); } if (Froxlor::isFroxlorVersion('2.3.0-dev1')) { Update::showUpdateStep("Updating from 2.3.0-dev1 to 2.3.0-rc1", false); Froxlor::updateToVersion('2.3.0-rc1'); } if (Froxlor::isFroxlorVersion('2.3.0-rc1')) { Update::showUpdateStep("Updating from 2.3.0-rc1 to 2.3.0", false); Froxlor::updateToVersion('2.3.0'); } if (Froxlor::isDatabaseVersion('202511020')) { Settings::AddNew('system.report_web_bccadmin', '0'); Froxlor::updateToDbVersion('202512090'); } if (Froxlor::isDatabaseVersion('202512090')) { $to_clean = [ 'install/updates/froxlor/update_0.10.inc.php', 'install/updates/preconfig/preconfig_0.10.inc.php', 'lib/Froxlor/Cron/Http/Lighttpd.php', 'lib/Froxlor/Cron/Http/LighttpdFcgi.php', ]; Update::cleanOldFiles($to_clean); Froxlor::updateToDbVersion('202512280'); } if (Froxlor::isFroxlorVersion('2.3.0')) { Update::showUpdateStep("Updating from 2.3.0 to 2.3.1", false); Froxlor::updateToVersion('2.3.1'); } if (Froxlor::isFroxlorVersion('2.3.1')) { Update::showUpdateStep("Updating from 2.3.1 to 2.3.2", false); Froxlor::updateToVersion('2.3.2'); } if (Froxlor::isFroxlorVersion('2.3.2')) { Update::showUpdateStep("Updating from 2.3.2 to 2.3.3", false); Froxlor::updateToVersion('2.3.3'); } if (Froxlor::isFroxlorVersion('2.3.3')) { Update::showUpdateStep("Updating from 2.3.3 to 2.3.4", false); Froxlor::updateToVersion('2.3.4'); } if (Froxlor::isFroxlorVersion('2.3.4')) { Update::showUpdateStep("Updating from 2.3.4 to 2.3.5", false); Froxlor::updateToVersion('2.3.5'); } if (Froxlor::isDatabaseVersion('202512280')) { Update::showUpdateStep("Adding new settings"); $system_webserver_serveradmin = $_POST['system_webserver_serveradmin'] ?? 'customer'; if (!in_array($system_webserver_serveradmin, ['customer', 'admin', 'global', 'none'])) { $system_webserver_serveradmin = 'customer'; } Settings::AddNew("system.webserver_serveradmin", $system_webserver_serveradmin); Update::lastStepStatus(0); Froxlor::updateToDbVersion('202603100'); } if (Froxlor::isFroxlorVersion('2.3.5')) { Update::showUpdateStep("Updating from 2.3.5 to 2.3.6", false); Froxlor::updateToVersion('2.3.6'); } if (Froxlor::isFroxlorVersion('2.3.6')) { Update::showUpdateStep("Updating from 2.3.6 to 2.3.7", false); Froxlor::updateToVersion('2.3.7'); } ================================================ FILE: install/updates/index.html ================================================ ================================================ FILE: install/updates/preconfig/index.html ================================================ ================================================ FILE: install/updates/preconfig/preconfig_2.0.inc.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ use Froxlor\Froxlor; use Froxlor\FileDir; use Froxlor\Config\ConfigParser; use Froxlor\Install\Update; use Froxlor\Settings; $preconfig = [ 'title' => '2.0.x updates', 'fields' => [] ]; $return = []; if (Update::versionInUpdate($current_version, '2.0.0-beta1')) { $description = 'We have rearranged the settings and split them into basic and advanced categories. This makes it easier for users who do not need all the detailed or very specific settings and options and gives a better overview of the basic/mostly used settings.'; $question = 'Chose settings mode (you can change that at any time)'; $return['panel_settings_mode'] = [ 'type' => 'select', 'select_var' => [ 0 => 'Basic', 1 => 'Advanced' ], 'selected' => 1, 'label' => $question, 'prior_infotext' => $description ]; $description = 'The configuration page now can preselect a distribution, please select your current distribution'; $question = 'Select distribution'; $config_dir = FileDir::makeCorrectDir(Froxlor::getInstallDir() . '/lib/configfiles/'); // show list of available distro's $distros = glob($config_dir . '*.xml'); // selection is required $distributions_select[''] = '-'; // read in all the distros foreach ($distros as $_distribution) { // get configparser object $dist = new ConfigParser($_distribution); // store in tmp array $distributions_select[str_replace(".xml", "", strtolower(basename($_distribution)))] = $dist->getCompleteDistroName(); } // sort by distribution name asort($distributions_select); $return['system_distribution'] = [ 'type' => 'select', 'select_var' => $distributions_select, 'selected' => '', 'label' => $question, 'prior_infotext' => $description ]; } if (Update::versionInUpdate($current_db_version, '202301120')) { $acmesh_challenge_dir = rtrim(FileDir::makeCorrectDir(Settings::Get('system.letsencryptchallengepath')), "/"); $recommended = rtrim(FileDir::makeCorrectDir(Froxlor::getInstallDir()), "/"); if ((int) Settings::Get('system.leenabled') == 1 && $acmesh_challenge_dir != $recommended) { $has_preconfig = true; $description = 'ACME challenge docroot from settings differs from the current installation directory.'; $question = 'Validate Let\'s Encrypt challenge path (recommended value: ' . $recommended . ')'; $return['system_letsencryptchallengepath_upd'] = [ 'type' => 'text', 'value' => $recommended, 'placeholder' => $acmesh_challenge_dir, 'label' => $question, 'prior_infotext' => $description, 'mandatory' => true, ]; } } if (Update::versionInUpdate($current_db_version, '202301180')) { if ((int) Settings::Get('system.leenabled') == 1) { $has_preconfig = true; $description = 'Froxlor now supports to set an external DNS resolver for the Let\'s Encrypt pre-check.'; $question = 'Specify a DNS resolver IP (recommended value: 1.1.1.1 or similar)'; $return['system_le_domain_dnscheck_resolver'] = [ 'type' => 'text', 'pattern' => '^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$|^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$|^\s*$', 'value' => '1.1.1.1', 'placeholder' => '1.1.1.1', 'label' => $question, 'prior_infotext' => $description, ]; } } $preconfig['fields'] = $return; return $preconfig; ================================================ FILE: install/updates/preconfig/preconfig_2.1.inc.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ use Froxlor\Install\Update; $preconfig = [ 'title' => '2.1.x updates', 'fields' => [] ]; $return = []; if (Update::versionInUpdate($current_version, '2.1.0-dev1')) { } $preconfig['fields'] = $return; return $preconfig; ================================================ FILE: install/updates/preconfig/preconfig_2.2.inc.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ use Froxlor\Install\Update; $preconfig = [ 'title' => '2.2.x updates', 'fields' => [] ]; $return = []; if (Update::versionInUpdate($current_version, '2.2.0-dev1')) { $has_preconfig = true; $description = 'Froxlor now features antispam configurations using rspamd. Would you like to enable the antispam feature (required re-configuration of services)?
ATTENTION: When not enabled and the former DomainKey feature was used, keep in mind that all existing domainkeys for all domain are being removed and the dkim-flag disabled for the domains.'; $question = 'Enable antispam (recommended) '; $return['antispam_activated'] = [ 'type' => 'checkbox', 'value' => 1, 'checked' => 0, 'label' => $question, 'prior_infotext' => $description ]; } $preconfig['fields'] = $return; return $preconfig; ================================================ FILE: install/updates/preconfig/preconfig_2.3.inc.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ use Froxlor\Install\Update; use Froxlor\Settings; $preconfig = [ 'title' => '2.3.x updates', 'fields' => [] ]; $return = []; if (Update::versionInUpdate($current_db_version, '202509010')) { if (Settings::Get('system.webserver') == 'lighttpd') { $has_preconfig = true; $description = 'You seem to be using "lighttpd" as webserver, froxlor 2.3 no longer supports this webserver. Please select an alternative one. Remember to configure the service after the update!'; $question = 'Switch webserver to: '; $return['system_alt_webserver'] = [ 'type' => 'select', 'select_var' => [ 'apache2' => 'Apache 2.4', 'nginx' => 'Nginx' ], 'selected' => 'apache2', 'label' => $question, 'prior_infotext' => $description ]; } } if (Update::versionInUpdate($current_db_version, '202509270')) { $has_preconfig = true; $description = 'It is now possible for customers to add "allowed sender" addresses for email-accounts to send from if enabled.'; $question = 'Enable "allowed sender" for customers? '; $return['mail_enable_allow_sender'] = [ 'type' => 'checkbox', 'value' => 1, 'checked' => 0, 'label' => $question, 'prior_infotext' => $description ]; $description = 'By default, a customer cannot use domains that are not added to the account for the "allowed sender" feature. You can specify if you would like to allow adding addresses from external domains not managed by your installation.'; $question = 'Allow external domains for "allowed sender"? '; $return['mail_allow_external_domains'] = [ 'type' => 'checkbox', 'value' => 1, 'checked' => 0, 'label' => $question, 'prior_infotext' => $description ]; } if (Update::versionInUpdate($current_db_version, '202603100')) { if (Settings::Get('system.webserver') == 'apache2') { $has_preconfig = true; $description = 'Select default value for the "ServerAdmin" value which is shown on webserver error pages (depending on ServerSignature setting).'; $question = 'Which email address should be shown in ServerAdmin directive? '; $return['system_webserver_serveradmin'] = [ 'type' => 'select', 'select_var' => [ 'customer' => 'Customer email address (default)', 'admin' => 'Admin email address', 'global' => 'Panel admin email address', 'none' => 'No ServerAdmin' ], 'selected' => 'customer', 'label' => $question, 'prior_infotext' => $description ]; } } $preconfig['fields'] = $return; return $preconfig; ================================================ FILE: install/updatesql.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ use Froxlor\Froxlor; use Froxlor\FileDir; use Froxlor\FroxlorLogger; use Froxlor\Settings; use Froxlor\UI\Response; use Froxlor\Database\IntegrityCheck; use Froxlor\Install\Update; if (!defined('_CRON_UPDATE')) { if (!defined('AREA') || (defined('AREA') && AREA != 'admin') || !isset($userinfo['loginname']) || (isset($userinfo['loginname']) && $userinfo['loginname'] == '')) { header('Location: ../index.php'); exit(); } } $filelog = FroxlorLogger::getInstanceOf(array( 'loginname' => 'updater' )); // if first writing does not work we'll stop, tell the user to fix it // and then let him try again. try { $filelog->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, '-------------- START LOG --------------'); } catch (Exception $e) { Response::standardError('exception', $e->getMessage()); } if (Froxlor::isFroxlor()) { include_once(FileDir::makeCorrectFile(dirname(__FILE__) . '/updates/froxlor/update_2.0.inc.php')); include_once(FileDir::makeCorrectFile(dirname(__FILE__) . '/updates/froxlor/update_2.1.inc.php')); include_once(FileDir::makeCorrectFile(dirname(__FILE__) . '/updates/froxlor/update_2.2.inc.php')); include_once(FileDir::makeCorrectFile(dirname(__FILE__) . '/updates/froxlor/update_2.3.inc.php')); // Check Froxlor - database integrity (only happens after all updates are done, so we know the db-layout is okay) Update::showUpdateStep("Checking database integrity"); $integrity = new IntegrityCheck(); if (!$integrity->checkAll()) { Update::lastStepStatus(1, 'Integrity could not be validated'); Update::showUpdateStep("Trying to automatically restore integrity"); if (!$integrity->fixAll()) { Update::lastStepStatus(2, 'failed', 'Check "database validation" as admin on the left-side menu to see where the problem is'); } else { Update::lastStepStatus(0, 'Integrity restored'); } } else { Update::lastStepStatus(0); } // reset cached version check Settings::Set('system.updatecheck_data', ''); $filelog->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, '--------------- END LOG ---------------'); unset($filelog); } ================================================ FILE: lib/Froxlor/Ajax/Ajax.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Ajax; use Exception; use DateTime; use Froxlor\Config\ConfigDisplay; use Froxlor\Config\ConfigParser; use Froxlor\CurrentUser; use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\Froxlor; use Froxlor\Http\HttpClient; use Froxlor\Install\Update; use Froxlor\Settings; use Froxlor\UI\Listing; use Froxlor\UI\Panel\UI; use Froxlor\UI\Request; use Froxlor\UI\Response; use Froxlor\Validate\Validate; class Ajax { protected string $action; protected string $theme; protected array $userinfo; /** * @throws Exception */ public function __construct() { $this->action = Request::any('action'); $this->theme = Request::any('theme', 'Froxlor'); UI::sendHeaders(); UI::sendSslHeaders(); } /** * @throws Exception */ public function handle() { $this->userinfo = $this->getValidatedSession(); switch ($this->action) { case 'newsfeed': return $this->getNewsfeed(); case 'updatecheck': return $this->getUpdateCheck(); case 'searchglobal': return $this->searchGlobal(); case 'updatetablelisting': return $this->updateTablelisting(); case 'resettablelisting': return $this->resetTablelisting(); case 'editapikey': return $this->editApiKey(); case 'getConfigDetails': return $this->getConfigDetails(); case 'getConfigJsonExport': return $this->getConfigJsonExport(); case 'loadLanguageString': return $this->loadLanguageString(); default: return $this->errorResponse('Action not found!'); } } /** * @throws Exception */ private function getValidatedSession(): array { if (CurrentUser::hasSession() == false) { throw new Exception("No valid session"); } return CurrentUser::getData(); } /** * @throws Exception */ private function getNewsfeed() { UI::initTwig(); $feed = "https://inside.froxlor.org/news/"; // Set custom feed if provided $role = Request::get('role'); if ($role == "customer") { $custom_feed = Settings::Get("customer.news_feed_url"); if (!empty(trim($custom_feed))) { $feed = $custom_feed; } } // Check for simplexml_load_file if (!function_exists("simplexml_load_file")) { return $this->errorResponse([ "Newsfeed not available due to missing php-simplexml extension", "Please install the php-simplexml extension in order to view our newsfeed." ]); } // Check for curl_version if (!function_exists('curl_version')) { return $this->errorResponse([ "Newsfeed not available due to missing php-curl extension", "Please install the php-curl extension in order to view our newsfeed." ]); } $output = HttpClient::urlGet($feed); $news = simplexml_load_string(trim($output)); if ($news === false) { $err = []; foreach (libxml_get_errors() as $error) { $err[] = $error->message; } return $this->errorResponse( $err ); } // Handle items if ($news) { $items = null; for ($i = 0; $i < 3; $i++) { $item = $news->channel->item[$i]; $title = (string)$item->title; $link = (string)$item->link; $date = date("d.m.Y", strtotime($item->pubDate)); $content = preg_replace("/[\r\n]+/", " ", strip_tags($item->description)); $content = substr($content, 0, 150) . "..."; $items .= UI::twig()->render(UI::validateThemeTemplate('/user/newsfeeditem.html.twig', $this->theme), [ 'link' => $link, 'title' => $title, 'date' => $date, 'content' => $content ]); } return $this->jsonResponse($items); } else { return $this->errorResponse('No Newsfeeds available at the moment.'); } } public function errorResponse($message, int $response_code = 500) { header("Content-Type: application/json"); return \Froxlor\Api\Response::jsonErrorResponse($message, $response_code); } public function jsonResponse($value, int $response_code = 200) { header("Content-Type: application/json"); return \Froxlor\Api\Response::jsonResponse($value, $response_code); } private function getUpdateCheck() { UI::initTwig(); try { $force = Request::get('force', 0); $json_result = \Froxlor\Api\Commands\Froxlor::getLocal($this->userinfo, ['force' => $force])->checkUpdate(); $result = json_decode($json_result, true)['data']; $result['full_version'] = Froxlor::getFullVersion(); $result['dbversion'] = Froxlor::DBVERSION; $uc_data = Update::getUpdateCheckData(); $result['last_update_check'] = $uc_data['ts']; $result['channel'] = Settings::Get('system.update_channel'); $result_rendered = UI::twig()->render(UI::validateThemeTemplate('/misc/version_top.html.twig', $this->theme), $result); return $this->jsonResponse($result_rendered); } catch (Exception $e) { // don't display anything if just not allowed due to permissions if ($e->getCode() != 403) { return $this->errorResponse($e->getMessage(), $e->getCode()); } } } /** * search globally in various resources */ private function searchGlobal() { $searchtext = Request::any('searchtext'); $result = []; // settings $result_settings = []; if (isset($this->userinfo['adminsession']) && $this->userinfo['adminsession'] == 1 && $this->userinfo['change_serversettings'] == 1) { $result_settings = GlobalSearch::searchSettings($searchtext, $this->userinfo); } // all searchable entities $result_entities = GlobalSearch::searchGlobal($searchtext, $this->userinfo); $result = array_merge($result_settings, $result_entities); return $this->jsonResponse($result); } private function updateTablelisting() { $columns = []; foreach ((Request::post('columns') ?? []) as $value) { $columns[] = $value; } if (!empty($columns)) { $columns = Listing::storeColumnListingForUser([Request::get('listing') => $columns]); return $this->jsonResponse($columns); } return $this->errorResponse('At least one column must be selected', 406); } private function resetTablelisting() { Listing::deleteColumnListingForUser([Request::get('listing') => []]); return $this->jsonResponse([]); } private function editApiKey() { $keyid = Request::post('id', 0); $allowed_from = Request::post('allowed_from', ""); $valid_until = Request::post('valid_until', ""); if (empty($keyid)) { return $this->errorResponse('Invalid call', 406); } // validate allowed_from if (!empty($allowed_from)) { $ip_list = array_map('trim', explode(",", $allowed_from)); $_check_list = $ip_list; foreach ($_check_list as $idx => $ip) { if (Validate::validate_ip2($ip, true, 'invalidip', true, true, true) == false) { return $this->errorResponse('Invalid ip address', 406); } // check for cidr if (strpos($ip, '/') !== false) { $ipparts = explode("/", $ip); // shorten IP $ip = inet_ntop(inet_pton($ipparts[0])); // re-add cidr $ip .= '/' . $ipparts[1]; } else { // shorten IP $ip = inet_ntop(inet_pton($ip)); } $ip_list[$idx] = $ip; } $allowed_from = implode(",", array_unique($ip_list)); } if (!empty($valid_until)) { $valid_until_db = DateTime::createFromFormat('Y-m-d\TH:i', $valid_until)->format('U'); } else { $valid_until_db = -1; } $upd_stmt = Database::prepare(" UPDATE `" . TABLE_API_KEYS . "` SET `valid_until` = :vu, `allowed_from` = :af WHERE `id` = :keyid AND `adminid` = :aid AND `customerid` = :cid "); if ((int)$this->userinfo['adminsession'] == 1) { $cid = 0; } else { $cid = $this->userinfo['customerid']; } Database::pexecute($upd_stmt, [ 'keyid' => $keyid, 'af' => $allowed_from, 'vu' => $valid_until_db, 'aid' => $this->userinfo['adminid'], 'cid' => $cid ]); return $this->jsonResponse(['allowed_from' => $allowed_from, 'valid_until' => $valid_until]); } /** * return parsed commands/files of configuration templates */ private function getConfigDetails() { if (isset($this->userinfo['adminsession']) && $this->userinfo['adminsession'] == 1 && $this->userinfo['change_serversettings'] == 1) { $distribution = Request::post('distro', ""); $section = Request::post('section', ""); $daemon = Request::post('daemon', ""); // validate distribution config-xml exists $config_dir = FileDir::makeCorrectDir(Froxlor::getInstallDir() . '/lib/configfiles/'); if (!file_exists($config_dir . "/" . $distribution . ".xml")) { return $this->errorResponse("Unknown distribution. The configuration could not be found."); } // read in all configurations $configfiles = new ConfigParser($config_dir . "/" . $distribution . ".xml"); // get the services $services = $configfiles->getServices(); // validate selected service exists for this distribution if (!isset($services[$section])) { return $this->errorResponse("Unknown category for selected distribution"); } // get the daemons $daemons = $services[$section]->getDaemons(); // validate selected daemon exists for this section if (!isset($daemons[$daemon])) { return $this->errorResponse("Unknown service for selected category"); } // finally the config-steps $confarr = $daemons[$daemon]->getConfig(); // get parsed content UI::initTwig(); $content = ConfigDisplay::fromConfigArr($confarr, $configfiles->distributionEditor, $this->theme); return $this->jsonResponse([ 'title' => $configfiles->getCompleteDistroName() . ' » ' . $services[$section]->title . ' » ' . $daemons[$daemon]->title, 'content' => $content ]); } return $this->errorResponse('Not allowed', 403); } /** * download JSON export of config-selection */ private function getConfigJsonExport() { if (isset($this->userinfo['adminsession']) && $this->userinfo['adminsession'] == 1 && $this->userinfo['change_serversettings'] == 1) { $params = $_GET; unset($params['action']); unset($params['finish']); unset($params['csrf_token']); header('Content-disposition: attachment; filename=froxlor-config-' . time() . '.json'); return $this->jsonResponse($params); } return $this->errorResponse('Not allowed', 403); } /** * loads a given language string by its identifier */ private function loadLanguageString() { $langid = Request::post('langid', ""); if (preg_match('/^([a-zA-Z\.]+)$/', $langid)) { return $this->jsonResponse(lng($langid)); } return $this->errorResponse('Invalid identifier: ' . $langid, 406); } } ================================================ FILE: lib/Froxlor/Ajax/GlobalSearch.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Ajax; use Froxlor\Api\Commands\Admins; use Froxlor\Api\Commands\Customers; use Froxlor\Api\Commands\Domains; use Froxlor\Api\Commands\EmailDomains; use Froxlor\Api\Commands\Emails; use Froxlor\Api\Commands\FpmDaemons; use Froxlor\Api\Commands\Ftps; use Froxlor\Api\Commands\HostingPlans; use Froxlor\Api\Commands\IpsAndPorts; use Froxlor\Api\Commands\Mysqls; use Froxlor\Api\Commands\PhpSettings; use Froxlor\Api\Commands\SubDomains; use Froxlor\Froxlor; use Froxlor\PhpHelper; use Froxlor\Settings; use Froxlor\UI\Collection; class GlobalSearch { protected array $userinfo; public static function searchSettings(string $searchtext, array $userinfo): array { $result = []; if ($searchtext && strlen(trim($searchtext)) > 2) { $processed = []; $stparts = explode(" ", $searchtext); foreach ($stparts as $searchtext) { $searchtext = trim($searchtext); if (preg_match('/^([a-z]+):$/', $searchtext, $matches)) { // only search settings if specific search is 'settings', else skip if ($matches[1] == 'settings') { continue; } else { break; } } $settings_data = PhpHelper::loadConfigArrayDir(Froxlor::getInstallDir() . '/actions/admin/settings/'); $results = []; if (!isset($processed['settings'])) { $processed['settings'] = []; } PhpHelper::recursive_array_search($searchtext, $settings_data, $results); foreach ($results as $pathkey) { $pk = explode(".", $pathkey); if (count($pk) > 4) { $settingkey = $pk[0] . '.' . $pk[1] . '.' . $pk[2] . '.' . $pk[3]; if (isset($settings_data[$pk[0]][$pk[1]]['advanced_mode']) && $settings_data[$pk[0]][$pk[1]]['advanced_mode'] && (int)Settings::Get('panel.settings_mode') == 0) { continue; } if (is_array($processed['settings']) && !array_key_exists($settingkey, $processed['settings'])) { $processed['settings'][$settingkey] = true; $sresult = $settings_data[$pk[0]][$pk[1]][$pk[2]][$pk[3]]; if (isset($sresult['advanced_mode']) && $sresult['advanced_mode'] && (int)Settings::Get('panel.settings_mode') == 0) { continue; } if ($sresult['type'] != 'hidden') { if (!isset($result['settings'])) { $result['settings'] = []; } $result['settings'][] = [ 'title' => (is_array($sresult['label']) ? $sresult['label']['title'] : $sresult['label']), 'href' => 'admin_settings.php?page=overview&part=' . $pk[1] . '&em=' . $pk[3] ]; } // not hidden } // if not processed } // correct settingkey } // foreach } // foreach } // searchtext min 3 chars return $result; } /** * */ public static function searchGlobal(string $searchtext, array $userinfo): array { $result = []; if ($searchtext && strlen(trim($searchtext)) > 2) { $processed = []; $stparts = explode(" ", $searchtext); $module = ""; foreach ($stparts as $searchtext) { $searchtext = trim($searchtext); if (preg_match('/^([a-z]+):$/', $searchtext, $matches)) { $module = $matches[1]; if ($matches[1] == 'settings') { break; } else { continue; } } // admin if (isset($userinfo['adminsession']) && $userinfo['adminsession'] == 1) { $toSearch = [ // customers 'customer' => [ 'class' => Customers::class, 'searchfields' => [ 'c.loginname', 'c.name', 'c.firstname', 'c.company', 'c.street', 'c.zipcode', 'c.city', 'c.email', 'c.customernumber', 'c.custom_notes' ], 'result_key' => 'loginname', 'result_format' => [ 'title' => ['\\Froxlor\\User', 'getCorrectFullUserDetails'], 'href' => 'admin_customers.php?page=customers&searchfield=c.loginname&searchtext=' ] ], // domains 'domains' => [ 'class' => Domains::class, 'searchfields' => [ 'd.domain', 'd.domain_ace', 'd.documentroot' ], 'result_key' => 'domain_ace', 'result_format' => [ 'title' => ['\\Froxlor\\Ajax\\GlobalSearch', 'getFieldFromResult'], 'title_args' => 'domain_ace', 'href' => 'admin_domains.php?page=domains&searchfield=d.domain_ace&searchtext=' ] ], // ips and ports 'ipsandports' => [ 'class' => IpsAndPorts::class, 'searchfields' => [ 'ip', 'vhostcontainer', 'specialsettings' ], 'result_key' => 'ip', 'result_groupkey' => 'ip', 'result_format' => [ 'title' => ['\\Froxlor\\Ajax\\GlobalSearch', 'getFieldFromResult'], 'title_args' => 'ip', 'href' => 'admin_ipsandports.php?page=ipsandports&searchfield=ip&searchtext=' ] ], // hosting-plans 'hostingplans' => [ 'class' => HostingPlans::class, 'searchfields' => [ 'p.name', 'p.description' ], 'result_key' => 'id', 'result_format' => [ 'title' => ['\\Froxlor\\Ajax\\GlobalSearch', 'getFieldFromResult'], 'title_args' => 'name', 'href' => 'admin_plans.php?page=overview&searchfield=id&searchtext=' ] ], // PHP configs 'phpconfigs' => [ 'class' => PhpSettings::class, 'searchfields' => [ 'c.description', 'fd.description', 'c.binary' ], 'result_key' => 'id', 'result_format' => [ 'title' => ['\\Froxlor\\Ajax\\GlobalSearch', 'getFieldFromResult'], 'title_args' => 'description', 'href' => 'admin_phpsettings.php?page=overview&searchfield=id&searchtext=' ] ], // FPM daemons 'fpmconfigs' => [ 'class' => FpmDaemons::class, 'searchfields' => [ 'description', 'reload_cmd' ], 'result_key' => 'id', 'result_format' => [ 'title' => ['\\Froxlor\\Ajax\\GlobalSearch', 'getFieldFromResult'], 'title_args' => 'description', 'href' => 'admin_phpsettings.php?page=fpmdaemons&searchfield=id&searchtext=' ] ] ]; if ((bool)$userinfo['change_serversettings']) { // admins $toSearch['admins'] = [ 'class' => Admins::class, 'searchfields' => [ 'loginname', 'name', 'email', 'custom_notes' ], 'result_key' => 'loginname', 'result_format' => [ 'title' => ['\\Froxlor\\Ajax\\GlobalSearch', 'getFieldFromResult'], 'title_args' => 'name', 'href' => 'admin_admins.php?page=admins&searchfield=loginname&searchtext=' ] ]; } } else { $toSearch = [ // (sub)domains 'domains' => [ 'class' => SubDomains::class, 'searchfields' => [ 'd.domain', 'd.domain_ace', 'd.documentroot' ], 'result_key' => 'domain_ace', 'result_format' => [ 'title' => ['\\Froxlor\\Ajax\\GlobalSearch', 'getFieldFromResult'], 'title_args' => 'domain_ace', 'href' => 'customer_domains.php?page=domains&searchfield=d.domain_ace&searchtext=' ] ], // email addresses 'emails' => [ 'class' => Emails::class, 'searchfields' => [ 'm.email', 'm.email_full' ], 'result_key' => 'email', 'result_format' => [ 'title' => ['\\Froxlor\\Ajax\\GlobalSearch', 'getFieldFromResult'], 'title_args' => 'email', 'href' => 'customer_email.php?page=email_domain&domainid={domainid}&searchfield=m.email&searchtext=' ] ], // email-domains 'email_domains' => [ 'class' => EmailDomains::class, 'searchfields' => [ 'd.domain', ], 'result_key' => 'domain', 'result_format' => [ 'title' => ['\\Froxlor\\Ajax\\GlobalSearch', 'getFieldFromResult'], 'title_args' => 'domain', 'href' => 'customer_email.php?page=emails&searchfield=d.domain&searchtext=' ] ], // databases 'databases' => [ 'class' => Mysqls::class, 'searchfields' => [ 'databasename', 'description' ], 'result_key' => 'databasename', 'result_format' => [ 'title' => ['\\Froxlor\\Ajax\\GlobalSearch', 'getFieldFromResult'], 'title_args' => 'databasename', 'href' => 'customer_mysql.php?page=mysqls&searchfield=databasename&searchtext=' ] ], // ftp user 'ftpuser' => [ 'class' => Ftps::class, 'searchfields' => [ 'username', 'description' ], 'result_key' => 'username', 'result_format' => [ 'title' => ['\\Froxlor\\Ajax\\GlobalSearch', 'getFieldFromResult'], 'title_args' => 'username', 'href' => 'customer_ftp.php?page=accounts&searchfield=username&searchtext=' ] ] ]; } // module specific search if (!empty($module)) { $modSearch = $toSearch[$module] ?? []; $toSearch = [$module => $modSearch]; } foreach ($toSearch as $entity => $edata) { $collection = (new Collection($edata['class'], $userinfo)) ->setInternal(true) ->addParam([ 'sql_search' => [ '_plainsql' => self::searchStringSql($edata['searchfields'], $searchtext) ] ]); if ($collection->count() > 0) { if (!isset($processed[$entity])) { $processed[$entity] = []; } $group_key = $edata['result_groupkey'] ?? $edata['result_key']; foreach ($collection->getList() as $cresult) { if (is_array($processed[$entity]) && !array_key_exists($cresult[$group_key], $processed[$entity])) { $processed[$entity][$cresult[$group_key]] = true; if (!isset($result[$entity])) { $result[$entity] = []; } // replacer from result in href $href_replacer = []; if (preg_match_all('/\{([a-z]+)\}/', $edata['result_format']['href'], $href_replacer) !== false) { foreach ($href_replacer[1] as $href_field) { $href_field_value = self::getFieldFromResult($cresult, $href_field); $edata['result_format']['href'] = str_replace('{' . $href_field . '}', $href_field_value, $edata['result_format']['href']); } } $result[$entity][] = [ 'title' => call_user_func($edata['result_format']['title'], $cresult, ($edata['result_format']['title_args'] ?? null)), 'href' => $edata['result_format']['href'] . $cresult[$edata['result_key']] ]; } } } } // foreach entity } // foreach split search-term } return $result; } private static function searchStringSql(array $searchfields, $searchtext) { $result = ['sql' => [], 'values' => []]; $result['sql'] = "("; foreach ($searchfields as $sf) { $result['sql'] .= $sf . " LIKE :searchtext OR "; } $result['sql'] = substr($result['sql'], 0, -3) . ")"; $result['values'] = ['searchtext' => '%' . $searchtext . '%']; return $result; } private static function getFieldFromResult(array $resultset, string $field = null) { return $resultset[$field] ?? ''; } } ================================================ FILE: lib/Froxlor/Ajax/index.html ================================================ ================================================ FILE: lib/Froxlor/Api/Api.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Api; use Exception; use Froxlor\Http\RateLimiter; use Froxlor\Settings; use voku\helper\AntiXSS; class Api { protected array $headers; protected $request = null; /** * Api constructor. * * @throws Exception */ public function __construct() { $this->headers = getallheaders(); // set header for the response header("Accept: application/json"); header("Content-Type: application/json"); // check whether API interface is enabled after all if (Settings::Get('api.enabled') != 1) { throw new Exception('API is not enabled. Please contact the administrator if you think this is wrong.', 400); } RateLimiter::run(); } /** * @param mixed $request * * @return Api */ public function formatMiddleware($request): Api { // check auf RESTful api call $this->request = $request; $uri = parse_url($_SERVER["REQUEST_URI"], PHP_URL_QUERY); // map /module/command to internal request array if match if (!empty($uri) && preg_match("/^\/([a-z]+)\/([a-z]+)\/?/", $uri, $matches)) { $request = []; $request['command'] = ucfirst($matches[1]) . '.' . $matches[2]; $request['params'] = !empty($this->request) ? json_decode($this->request, true) : null; $this->request = json_encode($request); } return $this; } /** * Handle incoming api request to our backend. * * @throws Exception */ public function handle() { $request = $this->request; // validate content $request = FroxlorRPC::validateRequest($request); $request = (new AntiXSS())->xss_clean( $this->stripcslashesDeep($request) ); // now actually do it $cls = "\\Froxlor\\Api\\Commands\\" . $request['command']['class']; $method = $request['command']['method']; $apiObj = new $cls([ 'apikey' => $_SERVER['PHP_AUTH_USER'], 'secret' => $_SERVER['PHP_AUTH_PW'] ], $request['params']); // call the method with the params if any return $apiObj->$method(); } /** * API PHP error handler to always return a valid JSON response * * @param mixed $errno * @param mixed $errstr * @param mixed $errfile * @param mixed $errline * @return never */ public static function phpErrHandler($errno, $errstr, $errfile, $errline) { throw new Exception('Internal PHP error: #' . $errno . ' ' . $errstr /* . ' in ' . $errfile . ':' . $errline */, 500); } private function stripcslashesDeep($value) { return is_array($value) ? array_map([$this, 'stripcslashesDeep'], $value) : (!empty($value) ? stripcslashes($value) : $value); } } ================================================ FILE: lib/Froxlor/Api/ApiCommand.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Api; use Exception; use Froxlor\Database\Database; use Froxlor\Froxlor; use Froxlor\FroxlorLogger; use Froxlor\Language; use Froxlor\PhpHelper; use Froxlor\Settings; use Froxlor\System\Mailer; abstract class ApiCommand extends ApiParameter { /** * froxlor version * * @var string */ protected $version = null; /** * froxlor dbversion * * @var int */ protected $dbversion = null; /** * froxlor version-branding * * @var string */ protected $branding = null; /** * debug flag * * @var boolean */ private $debug = false; /** * is admin flag * * @var boolean */ private $is_admin = false; /** * internal user data array * * @var array */ private $user_data = null; /** * logger interface * * @var FroxlorLogger */ private $logger = null; /** * mail interface * * @var Mailer */ private $mail = null; /** * whether the call is an internal one or not * * @var boolean */ private $internal_call = false; /** * * @param array $header * optional, passed via API * @param array $params * optional, array of parameters (var=>value) for the command * @param array $userinfo * optional, passed via WebInterface (instead of $header) * @param boolean $internal * optional whether called internally, default false * * @throws Exception */ final public function __construct($header = null, $params = null, $userinfo = null, $internal = false) { parent::__construct($params); $this->version = Froxlor::VERSION; $this->dbversion = Froxlor::DBVERSION; $this->branding = Froxlor::BRANDING; if (!empty($header)) { $this->readUserData($header); } elseif (!empty($userinfo)) { $this->user_data = $userinfo; $this->is_admin = (isset($userinfo['adminsession']) && $userinfo['adminsession'] == 1 && $userinfo['adminid'] > 0) ? true : false; } else { throw new Exception("Invalid user data", 500); } $this->logger = FroxlorLogger::getInstanceOf($this->user_data); // check whether the user is deactivated if ($this->getUserDetail('deactivated') == 1) { $this->logger()->logAction(FroxlorLogger::LOG_ERROR, LOG_INFO, "[API] User '" . $this->getUserDetail('loginnname') . "' tried to use API but is deactivated"); throw new Exception("Account suspended", 406); } $this->initLang(); /** * Initialize the mailingsystem */ $this->mail = new Mailer(true); if ($this->debug) { $this->logger()->logAction(FroxlorLogger::LOG_ERROR, LOG_DEBUG, "[API] " . get_called_class() . ": " . json_encode($params, JSON_UNESCAPED_SLASHES)); } // set internal call flag $this->internal_call = $internal; } /** * read user data from database by api-request-header fields * * @param array $header * api-request header * * @return boolean * @throws Exception */ private function readUserData($header = null) { $sel_stmt = Database::prepare("SELECT * FROM `api_keys` WHERE `apikey` = :ak AND `secret` = :as"); $result = Database::pexecute_first($sel_stmt, [ 'ak' => $header['apikey'], 'as' => $header['secret'] ], true, true); if ($result) { // admin or customer? if ($result['customerid'] == 0 && $result['adminid'] > 0) { $this->is_admin = true; $table = 'panel_admins'; $key = "adminid"; } elseif ($result['customerid'] > 0 && $result['adminid'] > 0) { $this->is_admin = false; $table = 'panel_customers'; $key = "customerid"; } else { // neither adminid is > 0 nor customerid is > 0 - sorry man, no way throw new Exception("Invalid API credentials", 400); } $sel_stmt = Database::prepare("SELECT * FROM `" . $table . "` WHERE `" . $key . "` = :id"); $this->user_data = Database::pexecute_first($sel_stmt, [ 'id' => ($this->is_admin ? $result['adminid'] : $result['customerid']) ], true, true); if ($this->is_admin) { $this->user_data['adminsession'] = 1; } return true; } throw new Exception("Invalid API credentials", 400); } /** * return field from user-table * * @param string $detail * * @return string|null */ protected function getUserDetail($detail = null) { return ($this->user_data[$detail] ?? null); } /** * return logger instance * * @return FroxlorLogger */ protected function logger() { return $this->logger; } /** * initialize language to have localized strings available for the ApiCommands */ private function initLang() { Language::setLanguage(Settings::Get('panel.standardlanguage')); if ($this->getUserDetail('language') !== null && isset(Language::getLanguages()[$this->getUserDetail('language')])) { Language::setLanguage($this->getUserDetail('language')); } elseif ($this->getUserDetail('def_language') !== null) { Language::setLanguage($this->getUserDetail('def_language')); } } /** * increase/decrease a resource field for customers/admins * * @param string $table * @param string $keyfield * @param int $key * @param string $operator * @param string $resource * @param string $extra * @param int $step */ protected static function updateResourceUsage($table = null, $keyfield = null, $key = null, $operator = '+', $resource = null, $extra = null, $step = 1) { $stmt = Database::prepare(" UPDATE `" . $table . "` SET `" . $resource . "` = `" . $resource . "` " . $operator . " " . (int)$step . " " . $extra . " WHERE `" . $keyfield . "` = :key "); Database::pexecute($stmt, [ 'key' => $key ], true, true); } /** * return SQL when parameter $sql_search is given via API * * @param array $sql_search * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), * LIKE is used if left empty and 'value' => searchvalue * @param array $query_fields * optional array of placeholders mapped to the actual value which is used in the API commands when * executing the statement [internal] * @param boolean $append * optional append to WHERE clause rather then create new one, default false [internal] * * @return string */ protected function getSearchWhere(&$query_fields = [], $append = false) { $search = $this->getParam('sql_search', true, []); $condition = ''; if (!empty($search)) { if ($append == true) { $condition = ' AND '; } else { $condition = ' WHERE '; } $ops = [ '<', '>', '=', '<>' ]; $first = true; foreach ($search as $field => $valoper) { if ($field == '_plainsql' && $this->internal_call) { if (isset($valoper['sql']) && isset($valoper['values']) && is_array($valoper['values'])) { if (preg_match('/^([a-z0-9\-\.,=\+_`\(\)\:\'\"\!\<\>\ ]+)$/i', $valoper['sql']) == false) { // skip continue; } $condition .= $valoper['sql']; foreach ($valoper['values'] as $var => $value) { $query_fields[':' . $var] = $value; } } } else { $cleanfield = str_replace(".", "", $field); $sortfield = explode('.', $field); foreach ($sortfield as $id => $sfield) { if (substr($sfield, -1, 1) != '`') { $sfield .= '`'; } if ($sfield[0] != '`') { $sfield = '`' . $sfield; } $sortfield[$id] = $sfield; } $field = implode('.', $sortfield); if (preg_match('/^([a-z0-9\-\._`]+)$/i', $field) == false) { // skip continue; } if (!$first) { $condition .= ' AND '; } if (!is_array($valoper) || !isset($valoper['op']) || empty($valoper['op'])) { $condition .= $field . ' LIKE :' . $cleanfield; if (!is_array($valoper)) { $query_fields[':' . $cleanfield] = '%' . $valoper . '%'; } else { $query_fields[':' . $cleanfield] = '%' . $valoper['value'] . '%'; } } elseif (in_array($valoper['op'], $ops)) { $condition .= $field . ' ' . $valoper['op'] . ':' . $cleanfield; $query_fields[':' . $cleanfield] = $valoper['value'] ?? ''; } elseif (strtolower($valoper['op']) == 'in' && is_array($valoper['value']) && count($valoper['value']) > 0) { $condition .= $field . ' ' . $valoper['op'] . ' ('; foreach ($valoper['value'] as $incnt => $invalue) { if (!is_numeric($incnt)) { // skip continue; } if (!empty($invalue) && preg_match('/^([a-z0-9\-\._`]+)$/i', $invalue) == false) { // skip continue; } $condition .= ":" . $cleanfield . $incnt . ", "; $query_fields[':' . $cleanfield . $incnt] = $invalue ?? ''; } $condition = substr($condition, 0, -2) . ')'; } else { continue; } if ($first) { $first = false; } } } } return $condition; } /** * return LIMIT clause when at least $sql_limit parameter is given via API * * @param int $sql_limit * optional, limit resultset, default 0 * @param int $sql_offset * optional, offset for limitation, default 0 * * @return string */ protected function getLimit() { $limit = $this->getParam('sql_limit', true, 0); $offset = $this->getParam('sql_offset', true, 0); if (!is_numeric($limit)) { $limit = 0; } if (!is_numeric($offset)) { $offset = 0; } if ($limit > 0) { return ' LIMIT ' . $offset . ',' . $limit; } return ''; } /** * return ORDER BY clause if parameter $sql_orderby parameter is given via API * * @param array $sql_orderby * optional array with index = fieldname and value = ASC|DESC * @param boolean $append * optional append to ORDER BY clause rather then create new one, default false [internal] * * @return string */ protected function getOrderBy($append = false) { $orderby = $this->getParam('sql_orderby', true, []); $order = ""; if (!empty($orderby)) { if ($append) { $order .= ", "; } else { $order .= " ORDER BY "; } $nat_fields = [ '`c`.`loginname`', '`c`.`name`', '`a`.`loginname`', '`adminname`', '`databasename`', '`username`' ]; foreach ($orderby as $field => $by) { $sortfield = explode('.', $field); foreach ($sortfield as $id => $sfield) { if (substr($sfield, -1, 1) != '`') { $sfield .= '`'; } if ($sfield[0] != '`') { $sfield = '`' . $sfield; } $sortfield[$id] = $sfield; } $field = implode('.', $sortfield); if (preg_match('/^([a-z0-9\-\._`]+)$/i', $field) == false) { // skip continue; } $by = strtoupper($by); if (!in_array($by, [ 'ASC', 'DESC' ])) { $by = 'ASC'; } if (Settings::Get('panel.natsorting') == 1 && in_array($field, $nat_fields)) { // Acts similar to php's natsort(), found in one comment at http://my.opera.com/cpr/blog/show.dml/160556 $order .= "CONCAT( IF( ASCII( LEFT( " . $field . ", 5 ) ) > 57, LEFT( " . $field . ", 1 ), 0 ), IF( ASCII( RIGHT( " . $field . ", 1 ) ) > 57, LPAD( " . $field . ", 255, '0' ), LPAD( CONCAT( " . $field . ", '-' ), 255, '0' ) )) " . $by . ", "; } else { $order .= $field . " " . $by . ", "; } } $order = substr($order, 0, -2); } return $order; } /** * return mailer instance * * @return Mailer */ protected function mailer() { return $this->mail; } /** * return api-compatible response in JSON format and send corresponding http-header * * @param mixed $data * @param int $response_code * @return string json-encoded response message */ protected function response($data = null, int $response_code = 200) { return Response::jsonDataResponse($data, $response_code); } /** * returns an array of customers the current user can access * * @param string $customer_hide_option * optional, when called as customer, some options might be hidden due to the * panel.customer_hide_options settings * * @return array * @throws Exception */ protected function getAllowedCustomerIds($customer_hide_option = '') { $customer_ids = []; if ($this->isAdmin()) { // if we're an admin, list all of the admins customers // or optionally for one specific customer identified by id or loginname $customerid = $this->getParam('customerid', true, 0); $loginname = $this->getParam('loginname', true, ''); if (!empty($customerid) || !empty($loginname)) { $_result = $this->apiCall('Customers.get', [ 'id' => $customerid, 'loginname' => $loginname ]); $custom_list_result = [ $_result ]; } else { $_custom_list_result = $this->apiCall('Customers.listing'); $custom_list_result = $_custom_list_result['list']; } foreach ($custom_list_result as $customer) { $customer_ids[] = $customer['customerid']; } } else { if (!$this->isInternal() && !empty($customer_hide_option) && Settings::IsInList('panel.customer_hide_options', $customer_hide_option)) { throw new Exception("You cannot access this resource", 405); } $customer_ids = [ $this->getUserDetail('customerid') ]; } if (empty($customer_ids)) { throw new Exception("Required resource unsatisfied.", 405); } return $customer_ids; } /** * admin flag * * @return boolean */ protected function isAdmin() { return $this->is_admin; } /** * call an api-command internally * * @param string $command * @param array|null $params * @param boolean $internal * optional whether called internally, default false * * * @return array */ protected function apiCall($command = null, $params = null, $internal = false) { $_command = explode(".", $command); $module = __NAMESPACE__ . "\Commands\\" . $_command[0]; $function = $_command[1]; $json_result = $module::getLocal($this->getUserData(), $params, $internal)->{$function}(); return json_decode($json_result, true)['data']; } /** * returns an instance of the wanted ApiCommand (e.g. * Customers, Domains, etc); * this is used widely in the WebInterface * * @param array $userinfo * array of user-data * @param array $params * array of parameters for the command * @param boolean $internal * optional whether called internally, default false * * @return static * @throws Exception */ public static function getLocal($userinfo = null, $params = null, $internal = false) { return new static(null, $params, $userinfo, $internal); } /** * return user-data array * * @return array */ protected function getUserData() { return $this->user_data; } /** * internal call flag * * @return boolean */ protected function isInternal() { return $this->internal_call; } /** * returns an array of customer data for customer, or by customer-id/loginname for admin/reseller * * @param int $customerid * optional, required if loginname is empty * @param string $loginname * optional, required of customerid is empty * @param string $customer_resource_check * optional, when called as admin, check the resources of the target customer * * @return array * @throws Exception */ protected function getCustomerData($customer_resource_check = '') { if ($this->isAdmin()) { $customerid = $this->getParam('customerid', true, 0); $loginname = $this->getParam('loginname', true, ''); $customer = $this->apiCall('Customers.get', [ 'id' => $customerid, 'loginname' => $loginname ]); // check whether the customer has enough resources if (!empty($customer_resource_check) && $customer[$customer_resource_check . '_used'] >= $customer[$customer_resource_check] && $customer[$customer_resource_check] != '-1') { throw new Exception("Customer has no more resources available", 406); } } else { $customer = $this->getUserData(); } return $customer; } /** * return email template content from database or global language file if not found in DB * * @param array $customerdata * @param string $group * @param string $varname * @param array $replace_arr * @param string $default * * @return string */ protected function getMailTemplate($customerdata = null, $group = null, $varname = null, $replace_arr = [], $default = "") { // get template $stmt = Database::prepare(" SELECT `value` FROM `" . TABLE_PANEL_TEMPLATES . "` WHERE `adminid`= :adminid AND `language`= :lang AND `templategroup`= :group AND `varname`= :var "); $result = Database::pexecute_first($stmt, [ "adminid" => $customerdata['adminid'], "lang" => $customerdata['def_language'], "group" => $group, "var" => $varname ], true, true); $content = $default; if ($result) { $content = $result['value'] ?? $default; } // @fixme html_entity_decode $content = html_entity_decode(PhpHelper::replaceVariables($content, $replace_arr)); return $content; } } ================================================ FILE: lib/Froxlor/Api/ApiParameter.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Api; use Exception; abstract class ApiParameter { /** * array of parameters passed to the command * * @var array */ private $cmd_params = null; /** * * @param array|null $params * optional, array of parameters (var=>value) for the command * * @throws Exception */ public function __construct(?array $params = null) { if (!is_null($params)) { $params = $this->trimArray($params); } $this->cmd_params = $params; } /** * run 'trim' function on an array recursively * * @param array $input * * @return string|array */ private function trimArray($input) { if ($input === '') { return ""; } if (is_numeric($input) || is_null($input)) { return $input; } if (!is_array($input)) { return trim($input); } return array_map([ $this, 'trimArray' ], $input); } /** * get specific parameter which also has and unlimited-field * * @param string|null $param * parameter to get out of the request-parameter list * @param string|null $ul_field * parameter to get out of the request-parameter list * @param bool $optional * default: false * @param mixed $default * value which is returned if optional=true and param is not set * * @return mixed * @throws Exception */ protected function getUlParam(?string $param = null, ?string $ul_field = null, bool $optional = false, $default = 0) { $param_value = (int)$this->getParam($param, $optional, $default); $ul_field_value = $this->getBoolParam($ul_field, true, 0); if ($ul_field_value != '0') { $param_value = -1; } return $param_value; } /** * get specific parameter from the parameter list; * check for existence and != empty if needed. * Maybe more in the future * * @param string|null $param * parameter to get out of the request-parameter list * @param bool $optional * default: false * @param mixed $default * value which is returned if optional=true and param is not set * * @return mixed * @throws Exception */ protected function getParam(?string $param = null, bool $optional = false, $default = '') { // does it exist? if (!isset($this->cmd_params[$param])) { if ($optional === false) { // get module + function for better error-messages $inmod = $this->getModFunctionString(); throw new Exception('Requested parameter "' . $param . '" could not be found for "' . $inmod . '"', 404); } return $default; } // is it empty? - test really on string, as value 0 is being seen as empty by php if (!is_array($this->cmd_params[$param]) && trim($this->cmd_params[$param]) === "") { if ($optional === false) { // get module + function for better error-messages $inmod = $this->getModFunctionString(); throw new Exception('Requested parameter "' . $param . '" is empty where it should not be for "' . $inmod . '"', 406); } return ''; } // everything else is fine return $this->cmd_params[$param]; } /** * returns "module::function()" for better error-messages (missing parameter etc.) * makes debugging a lot more comfortable * * @param int $level * depth of backtrace, default 2 * * @param int $max_level * @param array|null $trace * * @return string */ private function getModFunctionString(int $level = 1, int $max_level = 5, $trace = null) { // which class called us $_class = get_called_class(); if (empty($trace)) { // get backtrace $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); } // check class and function $class = $trace[$level]['class']; $func = $trace[$level]['function']; // is it the one we are looking for? if ($class != $_class && $level <= $max_level) { // check one level deeper return $this->getModFunctionString(++$level, $max_level, $trace); } return str_replace("Froxlor\\Api\\Commands\\", "", $class) . ':' . $func; } /** * getParam wrapper for boolean parameter * * @param string|null $param * parameter to get out of the request-parameter list * @param bool $optional * default: false * @param mixed $default * value which is returned if optional=true and param is not set * * @return string */ protected function getBoolParam(?string $param = null, bool $optional = false, $default = false) { $_default = '0'; if ($default) { $_default = '1'; } $param_value = $this->getParam($param, $optional, $_default); if ($param_value && intval($param_value) != 0) { return '1'; } return '0'; } /** * return list of all parameters * * @return array */ protected function getParamList() { return $this->cmd_params; } } ================================================ FILE: lib/Froxlor/Api/Commands/Admins.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Api\Commands; use Exception; use Froxlor\Api\ApiCommand; use Froxlor\Api\ResourceEntity; use Froxlor\Database\Database; use Froxlor\FroxlorLogger; use Froxlor\Idna\IdnaWrapper; use Froxlor\Language; use Froxlor\Settings; use Froxlor\System\Crypt; use Froxlor\UI\Response; use Froxlor\User; use Froxlor\Validate\Validate; use PDO; /** * @since 0.10.0 */ class Admins extends ApiCommand implements ResourceEntity { /** * increase resource-usage * * @param int $adminid * @param string $resource * @param string $extra * optional, default empty * @param int $increase_by * optional, default 1 */ public static function increaseUsage($adminid = 0, $resource = null, $extra = '', $increase_by = 1) { self::updateResourceUsage(TABLE_PANEL_ADMINS, 'adminid', $adminid, '+', $resource, $extra, $increase_by); } /** * decrease resource-usage * * @param int $adminid * @param string $resource * @param string $extra * optional, default empty * @param int $decrease_by * optional, default 1 */ public static function decreaseUsage($adminid = 0, $resource = null, $extra = '', $decrease_by = 1) { self::updateResourceUsage(TABLE_PANEL_ADMINS, 'adminid', $adminid, '-', $resource, $extra, $decrease_by); } /** * lists all admin entries * * @param array $sql_search * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), * LIKE is used if left empty and 'value' => searchvalue * @param int $sql_limit * optional specify number of results to be returned * @param int $sql_offset * optional specify offset for resultset * @param array $sql_orderby * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more * fields * * @access admin * @return string json-encoded array count|list * @throws Exception */ public function listing() { if ($this->isAdmin() && $this->getUserDetail('change_serversettings') == 1) { $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] list admins"); $query_fields = []; $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_ADMINS . "`" . $this->getSearchWhere($query_fields) . $this->getOrderBy() . $this->getLimit()); Database::pexecute($result_stmt, $query_fields, true, true); $result = []; while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $result[] = $row; } return $this->response([ 'count' => count($result), 'list' => $result ]); } throw new Exception("Not allowed to execute given command.", 403); } /** * returns the total number of admins for the given admin * * @access admin * @return string json-encoded response message * @throws Exception */ public function listingCount() { if ($this->isAdmin() && $this->getUserDetail('change_serversettings') == 1) { $query_fields = []; $result_stmt = Database::prepare(" SELECT COUNT(*) as num_admins FROM `" . TABLE_PANEL_ADMINS . "` " . $this->getSearchWhere($query_fields)); $result = Database::pexecute_first($result_stmt, $query_fields, true, true); if ($result) { return $this->response($result['num_admins']); } $this->response(0); } throw new Exception("Not allowed to execute given command.", 403); } /** * create a new admin user * * @param string $name * required, name of the adminstrator * @param string $email * required, email address of the administrator * @param string $new_loginname * required, loginname/username of the administrator * @param string $admin_password * optional, default auto-generated * @param string $def_language * optional, ISO 639-1 language code (e.g. 'en', 'de', see lng-folder for supported languages), * default is system-default language * @param bool $gui_access * optional, allow login via webui, if false ONLY the login via webui is disallowed; default true * @param bool $api_allowed * optional, default is true if system setting api.enabled is true, else false * @param string $custom_notes * optional, default empty * @param bool $custom_notes_show * optional, default false * @param int $diskspace * optional, default 0 * @param bool $diskspace_ul * optional, default false * @param int $traffic * optional, default 0 * @param bool $traffic_ul * optional, default false * @param int $customers * optional, default 0 * @param bool $customers_ul * optional, default false * @param int $domains * optional, default 0 * @param bool $domains_ul * optional, default false * @param int $subdomains * optional, default 0 * @param bool $subdomains_ul * optional, default false * @param int $emails * optional, default 0 * @param bool $emails_ul * optional, default false * @param int $email_accounts * optional, default 0 * @param bool $email_accounts_ul * optional, default false * @param int $email_forwarders * optional, default 0 * @param bool $email_forwarders_ul * optional, default false * @param int $email_quota * optional, default 0 * @param bool $email_quota_ul * optional, default false * @param int $ftps * optional, default 0 * @param bool $ftps_ul * optional, default false * @param int $mysqls * optional, default 0 * @param bool $mysqls_ul * optional, default false * @param bool $customers_see_all * optional, default false * @param bool $caneditphpsettings * optional, default false * @param bool $change_serversettings * optional, default false * @param array $ipaddress * optional, list of ip-address id's; default -1 (all IP's) * * @access admin * @return string json-encoded array * @throws Exception */ public function add() { if ($this->isAdmin() && $this->getUserDetail('change_serversettings') == 1) { // required parameters $name = $this->getParam('name'); $email = $this->getParam('email'); $loginname = $this->getParam('new_loginname'); // parameters $def_language = $this->getParam('def_language', true, Settings::Get('panel.standardlanguage')); $gui_access = $this->getBoolParam('gui_access', true, true); $api_allowed = $this->getBoolParam('api_allowed', true, Settings::Get('api.enabled')); $custom_notes = $this->getParam('custom_notes', true, ''); $custom_notes_show = $this->getBoolParam('custom_notes_show', true, 0); $password = $this->getParam('admin_password', true, ''); $diskspace = $this->getUlParam('diskspace', 'diskspace_ul', true, 0); $traffic = $this->getUlParam('traffic', 'traffic_ul', true, 0); $customers = $this->getUlParam('customers', 'customers_ul', true, 0); $domains = $this->getUlParam('domains', 'domains_ul', true, 0); $subdomains = $this->getUlParam('subdomains', 'subdomains_ul', true, 0); $emails = $this->getUlParam('emails', 'emails_ul', true, 0); $email_accounts = $this->getUlParam('email_accounts', 'email_accounts_ul', true, 0); $email_forwarders = $this->getUlParam('email_forwarders', 'email_forwarders_ul', true, 0); $email_quota = $this->getUlParam('email_quota', 'email_quota_ul', true, 0); $ftps = $this->getUlParam('ftps', 'ftps_ul', true, 0); $mysqls = $this->getUlParam('mysqls', 'mysqls_ul', true, 0); $customers_see_all = $this->getBoolParam('customers_see_all', true, 0); $caneditphpsettings = $this->getBoolParam('caneditphpsettings', true, 0); $change_serversettings = $this->getBoolParam('change_serversettings', true, 0); $ipaddress = $this->getParam('ipaddress', true, -1); // validation $name = Validate::validate($name, 'name', Validate::REGEX_DESC_TEXT, '', [], true); $idna_convert = new IdnaWrapper(); $email = $idna_convert->encode(Validate::validate($email, 'email', '', '', [], true)); $def_language = Validate::validate($def_language, 'default language', '', '', [], true); if (!empty($def_language) && !isset(Language::getLanguages()[$def_language])) { $def_language = Settings::Get('panel.standardlanguage'); } $custom_notes = Validate::validate(str_replace("\r\n", "\n", $custom_notes), 'custom_notes', Validate::REGEX_CONF_TEXT, '', [], true); if (Settings::Get('system.mail_quota_enabled') != '1') { $email_quota = -1; } $password = Validate::validate($password, 'password', '', '', [], true); // only check if not empty, // cause empty == generate password automatically if ($password != '') { $password = Crypt::validatePassword($password, true); } $diskspace *= 1024; $traffic *= 1024 * 1024; // Check if the account already exists // do not check via api as we skip any permission checks for this task $loginname_check_stmt = Database::prepare(" SELECT `loginname` FROM `" . TABLE_PANEL_CUSTOMERS . "` WHERE `loginname` = :login "); $loginname_check = Database::pexecute_first($loginname_check_stmt, [ 'login' => $loginname ], true, true); // Check if an admin with the loginname already exists // do not check via api as we skip any permission checks for this task $loginname_check_admin_stmt = Database::prepare(" SELECT `loginname` FROM `" . TABLE_PANEL_ADMINS . "` WHERE `loginname` = :login "); $loginname_check_admin = Database::pexecute_first($loginname_check_admin_stmt, [ 'login' => $loginname ], true, true); // Check for existing email address // do not check via api as we skip any permission checks for this task $email_check_admin_stmt = Database::prepare(" SELECT `email` FROM `" . TABLE_PANEL_ADMINS . "` WHERE `email` = :email "); $email_check_admin = Database::pexecute_first($email_check_admin_stmt, [ 'email' => $email ], true, true); if (($loginname_check && strtolower($loginname_check['loginname']) == strtolower($loginname)) || ($loginname_check_admin && strtolower($loginname_check_admin['loginname']) == strtolower($loginname))) { Response::standardError('loginnameexists', $loginname, true); } elseif (preg_match('/^' . preg_quote(Settings::Get('customer.accountprefix'), '/') . '([0-9]+)/', $loginname)) { // Accounts which match systemaccounts are not allowed, filtering them Response::standardError('loginnameisusingprefix', Settings::Get('customer.accountprefix'), true); } elseif (function_exists('posix_getpwnam') && !in_array("posix_getpwnam", explode(",", ini_get('disable_functions'))) && posix_getpwnam($loginname)) { Response::standardError('loginnameissystemaccount', $loginname, true); } elseif (!Validate::validateUsername($loginname)) { Response::standardError('loginnameiswrong', $loginname, true); } elseif (!Validate::validateEmail($email)) { Response::standardError('emailiswrong', $email, true); } elseif ($email_check_admin && strtolower($email_check_admin['email']) == strtolower($email)) { Response::standardError('emailexists', $email, true); } else { if ($customers_see_all != '1') { $customers_see_all = '0'; } if ($caneditphpsettings != '1') { $caneditphpsettings = '0'; } if ($change_serversettings != '1') { $change_serversettings = '0'; } if ($password == '') { $password = Crypt::generatePassword(); } $_theme = Settings::Get('panel.default_theme'); $ins_data = [ 'loginname' => $loginname, 'password' => Crypt::makeCryptPassword($password), 'name' => $name, 'email' => $email, 'lang' => $def_language, 'gui_access' => $gui_access, 'api_allowed' => $api_allowed, 'change_serversettings' => $change_serversettings, 'customers' => $customers, 'customers_see_all' => $customers_see_all, 'domains' => $domains, 'caneditphpsettings' => $caneditphpsettings, 'diskspace' => $diskspace, 'traffic' => $traffic, 'subdomains' => $subdomains, 'emails' => $emails, 'accounts' => $email_accounts, 'forwarders' => $email_forwarders, 'quota' => $email_quota, 'ftps' => $ftps, 'mysqls' => $mysqls, 'ip' => empty($ipaddress) ? "" : (is_array($ipaddress) && $ipaddress > 0 ? json_encode($ipaddress) : -1), 'theme' => $_theme, 'custom_notes' => $custom_notes, 'custom_notes_show' => $custom_notes_show ]; $ins_stmt = Database::prepare(" INSERT INTO `" . TABLE_PANEL_ADMINS . "` SET `loginname` = :loginname, `password` = :password, `name` = :name, `email` = :email, `def_language` = :lang, `gui_access` = :gui_access, `api_allowed` = :api_allowed, `change_serversettings` = :change_serversettings, `customers` = :customers, `customers_see_all` = :customers_see_all, `domains` = :domains, `caneditphpsettings` = :caneditphpsettings, `diskspace` = :diskspace, `traffic` = :traffic, `subdomains` = :subdomains, `emails` = :emails, `email_accounts` = :accounts, `email_forwarders` = :forwarders, `email_quota` = :quota, `ftps` = :ftps, `mysqls` = :mysqls, `ip` = :ip, `theme` = :theme, `custom_notes` = :custom_notes, `custom_notes_show` = :custom_notes_show "); Database::pexecute($ins_stmt, $ins_data, true, true); $adminid = Database::lastInsertId(); $ins_data['adminid'] = $adminid; $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] added admin '" . $loginname . "'"); // get all admin-data for return-array $result = $this->apiCall('Admins.get', [ 'id' => $adminid ]); return $this->response($result); } } throw new Exception("Not allowed to execute given command.", 403); } /** * return an admin entry by either id or loginname * * @param int $id * optional, the admin-id * @param string $loginname * optional, the loginname * * @access admin * @return string json-encoded array * @throws Exception */ public function get() { $id = $this->getParam('id', true, 0); $ln_optional = $id > 0; $loginname = $this->getParam('loginname', $ln_optional, ''); if ($this->isAdmin() && ($this->getUserDetail('change_serversettings') == 1 || ($this->getUserDetail('adminid') == $id || $this->getUserDetail('loginname') == $loginname))) { $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_ADMINS . "` WHERE " . ($id > 0 ? "`adminid` = :idln" : "`loginname` = :idln")); $params = [ 'idln' => ($id <= 0 ? $loginname : $id) ]; $result = Database::pexecute_first($result_stmt, $params, true, true); if ($result) { $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] get admin '" . $result['loginname'] . "'"); return $this->response($result); } $key = ($id > 0 ? "id #" . $id : "loginname '" . $loginname . "'"); throw new Exception("Admin with " . $key . " could not be found", 404); } throw new Exception("Not allowed to execute given command.", 403); } /** * update an admin user by given id or loginname * * @param int $id * optional, the admin-id * @param string $loginname * optional, the loginname * @param string $name * optional * @param string $email * optional * @param string $admin_password * optional, default auto-generated * @param string $def_language * optional, ISO 639-1 language code (e.g. 'en', 'de', see lng-folder for supported languages), * default is system-default language * @param bool $gui_access * optional, allow login via webui, if false ONLY the login via webui is disallowed; default true * @param bool $api_allowed * optional, default is true if system setting api.enabled is true, else false * @param string $custom_notes * optional, default empty * @param string $theme * optional * @param bool $deactivated * optional, default false * @param bool $custom_notes_show * optional, default false * @param int $diskspace * optional, default 0 * @param bool $diskspace_ul * optional, default false * @param int $traffic * optional, default 0 * @param bool $traffic_ul * optional, default false * @param int $customers * optional, default 0 * @param bool $customers_ul * optional, default false * @param int $domains * optional, default 0 * @param bool $domains_ul * optional, default false * @param int $subdomains * optional, default 0 * @param bool $subdomains_ul * optional, default false * @param int $emails * optional, default 0 * @param bool $emails_ul * optional, default false * @param int $email_accounts * optional, default 0 * @param bool $email_accounts_ul * optional, default false * @param int $email_forwarders * optional, default 0 * @param bool $email_forwarders_ul * optional, default false * @param int $email_quota * optional, default 0 * @param bool $email_quota_ul * optional, default false * @param int $ftps * optional, default 0 * @param bool $ftps_ul * optional, default false * @param int $mysqls * optional, default 0 * @param bool $mysqls_ul * optional, default false * @param bool $customers_see_all * optional, default false * @param bool $caneditphpsettings * optional, default false * @param bool $change_serversettings * optional, default false * @param array $ipaddress * optional, list of ip-address id's; default -1 (all IP's) * * @access admin * @return string json-encoded array * @throws Exception */ public function update() { if ($this->isAdmin()) { $id = $this->getParam('id', true, 0); $ln_optional = $id > 0; $loginname = $this->getParam('loginname', $ln_optional, ''); $result = $this->apiCall('Admins.get', [ 'id' => $id, 'loginname' => $loginname ]); $id = $result['adminid']; if ($this->getUserDetail('change_serversettings') == 1 || $result['adminid'] == $this->getUserDetail('adminid')) { // parameters $name = $this->getParam('name', true, $result['name']); $idna_convert = new IdnaWrapper(); $email = $this->getParam('email', true, $idna_convert->decode($result['email'])); $password = $this->getParam('admin_password', true, ''); $def_language = $this->getParam('def_language', true, $result['def_language']); $custom_notes = $this->getParam('custom_notes', true, ($result['custom_notes'] ?? "")); $custom_notes_show = $this->getBoolParam('custom_notes_show', true, $result['custom_notes_show']); $theme = $this->getParam('theme', true, $result['theme']); // you cannot edit some of the details of yourself if ($result['adminid'] == $this->getUserDetail('adminid')) { $gui_access = $result['gui_access']; $api_allowed = $result['api_allowed']; $deactivated = $result['deactivated']; $customers = $result['customers']; $domains = $result['domains']; $subdomains = $result['subdomains']; $emails = $result['emails']; $email_accounts = $result['email_accounts']; $email_forwarders = $result['email_forwarders']; $email_quota = $result['email_quota']; $ftps = $result['ftps']; $mysqls = $result['mysqls']; $customers_see_all = $result['customers_see_all']; $caneditphpsettings = $result['caneditphpsettings']; $change_serversettings = $result['change_serversettings']; $diskspace = $result['diskspace']; $traffic = $result['traffic']; $ipaddress = ($result['ip'] != -1 ? json_decode($result['ip'], true) : -1); } else { $gui_access = $this->getBoolParam('gui_access', true, $result['gui_access']); $api_allowed = $this->getBoolParam('api_allowed', true, $result['api_allowed']); $deactivated = $this->getBoolParam('deactivated', true, $result['deactivated']); $dec_places = Settings::Get('panel.decimal_places'); $diskspace = $this->getUlParam('diskspace', 'diskspace_ul', true, round($result['diskspace'] / 1024, $dec_places)); $traffic = $this->getUlParam('traffic', 'traffic_ul', true, round($result['traffic'] / (1024 * 1024), $dec_places)); $customers = $this->getUlParam('customers', 'customers_ul', true, $result['customers']); $domains = $this->getUlParam('domains', 'domains_ul', true, $result['domains']); $subdomains = $this->getUlParam('subdomains', 'subdomains_ul', true, $result['subdomains']); $emails = $this->getUlParam('emails', 'emails_ul', true, $result['emails']); $email_accounts = $this->getUlParam('email_accounts', 'email_accounts_ul', true, $result['email_accounts']); $email_forwarders = $this->getUlParam('email_forwarders', 'email_forwarders_ul', true, $result['email_forwarders']); $email_quota = $this->getUlParam('email_quota', 'email_quota_ul', true, $result['email_quota']); $ftps = $this->getUlParam('ftps', 'ftps_ul', true, $result['ftps']); $mysqls = $this->getUlParam('mysqls', 'mysqls_ul', true, $result['mysqls']); $customers_see_all = $this->getBoolParam('customers_see_all', true, $result['customers_see_all']); $caneditphpsettings = $this->getBoolParam('caneditphpsettings', true, $result['caneditphpsettings']); $change_serversettings = $this->getBoolParam('change_serversettings', true, $result['change_serversettings']); $ipaddress = $this->getParam('ipaddress', true, ($result['ip'] != -1 ? json_decode($result['ip'], true) : -1)); $diskspace *= 1024; $traffic *= 1024 * 1024; } // validation $name = Validate::validate($name, 'name', Validate::REGEX_DESC_TEXT, '', [], true); $idna_convert = new IdnaWrapper(); $email = $idna_convert->encode(Validate::validate($email, 'email', '', '', [], true)); $def_language = Validate::validate($def_language, 'default language', '', '', [], true); if (!empty($def_language) && !isset(Language::getLanguages()[$def_language])) { $def_language = Settings::Get('panel.standardlanguage'); } $custom_notes = Validate::validate(str_replace("\r\n", "\n", $custom_notes ?? ""), 'custom_notes', Validate::REGEX_CONF_TEXT, '', [], true); $theme = Validate::validate($theme, 'theme', '', '', [], true); $password = Validate::validate($password, 'password', '', '', [], true); if (Settings::Get('system.mail_quota_enabled') != '1') { $email_quota = -1; } if (empty($theme)) { $theme = Settings::Get('panel.default_theme'); } if (empty(trim($name))) { Response::standardError([ 'stringisempty', 'admin.name' ], '', true); } if (empty(trim($email))) { Response::standardError([ 'stringisempty', 'admin.email' ], '', true); } // Check for existing email address // do not check via api as we skip any permission checks for this task $email_check_admin_stmt = Database::prepare(" SELECT `email` FROM `" . TABLE_PANEL_ADMINS . "` WHERE `email` = :email and `adminid` <> :adminid "); $email_check_admin = Database::pexecute_first($email_check_admin_stmt, [ 'email' => $email, 'adminid' => $id, ], true, true); if (!Validate::validateEmail($email)) { Response::standardError('emailiswrong', $email, true); } elseif ($email_check_admin && strtolower($email_check_admin['email']) == strtolower($email)) { Response::standardError('emailexists', $email, true); } else { if ($deactivated != '1') { $deactivated = '0'; } if ($customers_see_all != '1') { $customers_see_all = '0'; } if ($caneditphpsettings != '1') { $caneditphpsettings = '0'; } if ($change_serversettings != '1') { $change_serversettings = '0'; } if ($password != '') { $password = Crypt::validatePassword($password, true); $password = Crypt::makeCryptPassword($password); } else { $password = $result['password']; } // check if a resource was set to something lower // than actually used by the admin/reseller $res_warning = ""; if ($customers != $result['customers'] && $customers != -1 && $customers < $result['customers_used']) { $res_warning .= lng('error.setlessthanalreadyused', ['customers']); } if ($domains != $result['domains'] && $domains != -1 && $domains < $result['domains_used']) { $res_warning .= lng('error.setlessthanalreadyused', ['domains']); } if ($diskspace != $result['diskspace'] && ($diskspace / 1024) != -1 && $diskspace < $result['diskspace_used']) { $res_warning .= lng('error.setlessthanalreadyused', ['diskspace']); } if ($traffic != $result['traffic'] && ($traffic / 1024 / 1024) != -1 && $traffic < $result['traffic_used']) { $res_warning .= lng('error.setlessthanalreadyused', ['traffic']); } if ($emails != $result['emails'] && $emails != -1 && $emails < $result['emails_used']) { $res_warning .= lng('error.setlessthanalreadyused', ['emails']); } if ($email_accounts != $result['email_accounts'] && $email_accounts != -1 && $email_accounts < $result['email_accounts_used']) { $res_warning .= lng('error.setlessthanalreadyused', ['email accounts']); } if ($email_forwarders != $result['email_forwarders'] && $email_forwarders != -1 && $email_forwarders < $result['email_forwarders_used']) { $res_warning .= lng('error.setlessthanalreadyused', ['email forwarders']); } if ($email_quota != $result['email_quota'] && $email_quota != -1 && $email_quota < $result['email_quota_used']) { $res_warning .= lng('error.setlessthanalreadyused', ['email quota']); } if ($ftps != $result['ftps'] && $ftps != -1 && $ftps < $result['ftps_used']) { $res_warning .= lng('error.setlessthanalreadyused', ['ftps']); } if ($mysqls != $result['mysqls'] && $mysqls != -1 && $mysqls < $result['mysqls_used']) { $res_warning .= lng('error.setlessthanalreadyused', ['mysqls']); } if (!empty($res_warning)) { throw new Exception($res_warning, 406); } $upd_data = [ 'password' => $password, 'name' => $name, 'email' => $email, 'lang' => $def_language, 'gui_access' => $gui_access, 'api_allowed' => $api_allowed, 'change_serversettings' => $change_serversettings, 'customers' => $customers, 'customers_see_all' => $customers_see_all, 'domains' => $domains, 'caneditphpsettings' => $caneditphpsettings, 'diskspace' => $diskspace, 'traffic' => $traffic, 'subdomains' => $subdomains, 'emails' => $emails, 'accounts' => $email_accounts, 'forwarders' => $email_forwarders, 'quota' => $email_quota, 'ftps' => $ftps, 'mysqls' => $mysqls, 'ip' => empty($ipaddress) ? "" : (is_array($ipaddress) && $ipaddress > 0 ? json_encode($ipaddress) : -1), 'deactivated' => $deactivated, 'custom_notes' => $custom_notes, 'custom_notes_show' => $custom_notes_show, 'theme' => $theme, 'adminid' => $id ]; $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_ADMINS . "` SET `password` = :password, `name` = :name, `email` = :email, `def_language` = :lang, `gui_access` = :gui_access, `api_allowed` = :api_allowed, `change_serversettings` = :change_serversettings, `customers` = :customers, `customers_see_all` = :customers_see_all, `domains` = :domains, `caneditphpsettings` = :caneditphpsettings, `diskspace` = :diskspace, `traffic` = :traffic, `subdomains` = :subdomains, `emails` = :emails, `email_accounts` = :accounts, `email_forwarders` = :forwarders, `email_quota` = :quota, `ftps` = :ftps, `mysqls` = :mysqls, `ip` = :ip, `deactivated` = :deactivated, `custom_notes` = :custom_notes, `custom_notes_show` = :custom_notes_show, `theme` = :theme WHERE `adminid` = :adminid "); Database::pexecute($upd_stmt, $upd_data, true, true); $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] edited admin '" . $result['loginname'] . "'"); // get all admin-data for return-array $result = $this->apiCall('Admins.get', [ 'id' => $result['adminid'] ]); return $this->response($result); } } } throw new Exception("Not allowed to execute given command.", 403); } /** * delete a admin entry by either id or loginname * * @param int $id * optional, the admin-id * @param string $loginname * optional, the loginname * * @access admin * @return string json-encoded array * @throws Exception */ public function delete() { if ($this->isAdmin() && $this->getUserDetail('change_serversettings') == 1) { $id = $this->getParam('id', true, 0); $ln_optional = $id > 0; $loginname = $this->getParam('loginname', $ln_optional, ''); $result = $this->apiCall('Admins.get', [ 'id' => $id, 'loginname' => $loginname ]); $id = $result['adminid']; // don't be stupid if ($id == $this->getUserDetail('adminid')) { Response::standardError('youcantdeleteyourself', '', true); } // can't delete the first superadmin if ($id == 1) { Response::standardError('cannotdeletesuperadmin', '', true); } // delete admin $del_stmt = Database::prepare(" DELETE FROM `" . TABLE_PANEL_ADMINS . "` WHERE `adminid` = :adminid "); Database::pexecute($del_stmt, [ 'adminid' => $id ], true, true); // delete the traffic-usage $del_stmt = Database::prepare(" DELETE FROM `" . TABLE_PANEL_TRAFFIC_ADMINS . "` WHERE `adminid` = :adminid "); Database::pexecute($del_stmt, [ 'adminid' => $id ], true, true); // set admin-id of the old admin's customer to current admins $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `adminid` = :userid WHERE `adminid` = :adminid "); Database::pexecute($upd_stmt, [ 'userid' => $this->getUserDetail('adminid'), 'adminid' => $id ], true, true); // set admin-id of the old admin's domains to current admins $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_DOMAINS . "` SET `adminid` = :userid WHERE `adminid` = :adminid "); Database::pexecute($upd_stmt, [ 'userid' => $this->getUserDetail('adminid'), 'adminid' => $id ], true, true); // delete old admin's api keys if exists (no customer keys) $upd_stmt = Database::prepare(" DELETE FROM `" . TABLE_API_KEYS . "` WHERE `adminid` = :adminid AND `customerid` = '0' "); Database::pexecute($upd_stmt, [ 'adminid' => $id ], true, true); // set admin-id of the old admin's api-keys to current admins $upd_stmt = Database::prepare(" UPDATE `" . TABLE_API_KEYS . "` SET `adminid` = :userid WHERE `adminid` = :adminid "); Database::pexecute($upd_stmt, [ 'userid' => $this->getUserDetail('adminid'), 'adminid' => $id ], true, true); $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] deleted admin '" . $result['loginname'] . "'"); User::updateCounters(); return $this->response($result); } throw new Exception("Not allowed to execute given command.", 403); } /** * unlock a locked admin by either id or loginname * * @param int $id * optional, the admin-id * @param string $loginname * optional, the loginname * * @access admin * @return string json-encoded array * @throws Exception */ public function unlock() { if ($this->isAdmin() && $this->getUserDetail('change_serversettings') == 1) { $id = $this->getParam('id', true, 0); $ln_optional = $id > 0; $loginname = $this->getParam('loginname', $ln_optional, ''); $result = $this->apiCall('Admins.get', [ 'id' => $id, 'loginname' => $loginname ]); $id = $result['adminid']; $result_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_ADMINS . "` SET `loginfail_count` = '0' WHERE `adminid`= :id "); Database::pexecute($result_stmt, [ 'id' => $id ], true, true); // set the new value for result-array $result['loginfail_count'] = 0; $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] unlocked admin '" . $result['loginname'] . "'"); return $this->response($result); } throw new Exception("Not allowed to execute given command.", 403); } } ================================================ FILE: lib/Froxlor/Api/Commands/Certificates.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Api\Commands; use Exception; use Froxlor\Api\ApiCommand; use Froxlor\Api\ResourceEntity; use Froxlor\Cron\TaskId; use Froxlor\Database\Database; use Froxlor\FroxlorLogger; use Froxlor\Settings; use Froxlor\System\Cronjob; use Froxlor\UI\Response; use PDO; /** * @since 0.10.0 */ class Certificates extends ApiCommand implements ResourceEntity { /** * add new ssl-certificate entry for given domain by either id or domainname * * @param int $id * optional, the domain-id * @param string $domainname * optional, the domainname * @param string $ssl_cert_file * @param string $ssl_key_file * @param string $ssl_ca_file * optional * @param string $ssl_cert_chainfile * optional * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function add() { $domainid = $this->getParam('domainid', true, 0); $dn_optional = $domainid > 0; $domainname = $this->getParam('domainname', $dn_optional, ''); if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'domains')) { throw new Exception("You cannot access this resource", 405); } $domain = $this->apiCall('SubDomains.get', [ 'id' => $domainid, 'domainname' => $domainname ]); $domainid = $domain['id']; // parameters $ssl_cert_file = $this->getParam('ssl_cert_file'); $ssl_key_file = $this->getParam('ssl_key_file'); $ssl_ca_file = $this->getParam('ssl_ca_file', true, ''); $ssl_cert_chainfile = $this->getParam('ssl_cert_chainfile', true, ''); // validate whether the domain does not already have an entry $has_cert = true; try { $this->apiCall('Certificates.get', [ 'id' => $domainid ]); } catch (Exception $e) { if ($e->getCode() == 412) { $has_cert = false; } else { throw $e; } } if (!$has_cert) { $this->addOrUpdateCertificate($domain['id'], $ssl_cert_file, $ssl_key_file, $ssl_ca_file, $ssl_cert_chainfile, true); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] added ssl-certificate for '" . $domain['domain'] . "'"); $result = $this->apiCall('Certificates.get', [ 'id' => $domain['id'] ]); return $this->response($result); } throw new Exception("Domain '" . $domain['domain'] . "' already has a certificate. Did you mean to call update?", 406); } /** * insert or update certificates entry * * @param int $domainid * @param string $ssl_cert_file * @param string $ssl_key_file * @param string $ssl_ca_file * @param string $ssl_cert_chainfile * @param boolean $do_insert * optional default false * * @return boolean * @throws Exception */ private function addOrUpdateCertificate($domainid = 0, $ssl_cert_file = '', $ssl_key_file = '', $ssl_ca_file = '', $ssl_cert_chainfile = '', $do_insert = false) { if ($ssl_cert_file != '' && $ssl_key_file == '') { Response::standardError('sslcertificateismissingprivatekey', '', true); } $do_verify = true; $validtodate = null; $validtodate = null; $issuer = ""; // no cert-file given -> forget everything if ($ssl_cert_file == '') { $ssl_key_file = ''; $ssl_ca_file = ''; $ssl_cert_chainfile = ''; $do_verify = false; } // verify certificate content if ($do_verify) { // array openssl_x509_parse ( mixed $x509cert [, bool $shortnames = true ] ) // openssl_x509_parse() returns information about the supplied x509cert, including fields such as // subject name, issuer name, purposes, valid from and valid to dates etc. $cert_content = openssl_x509_parse($ssl_cert_file); if (is_array($cert_content) && isset($cert_content['subject']) && isset($cert_content['subject']['CN'])) { // bool openssl_x509_check_private_key ( mixed $cert , mixed $key ) // Checks whether the given key is the private key that corresponds to cert. if (openssl_x509_check_private_key($ssl_cert_file, $ssl_key_file) === false) { Response::standardError('sslcertificateinvalidcertkeypair', '', true); } // check optional stuff if ($ssl_ca_file != '') { $ca_content = openssl_x509_parse($ssl_ca_file); if (!is_array($ca_content)) { // invalid Response::standardError('sslcertificateinvalidca', '', true); } } if ($ssl_cert_chainfile != '') { $chain_content = openssl_x509_parse($ssl_cert_chainfile); if (!is_array($chain_content)) { // invalid Response::standardError('sslcertificateinvalidchain', '', true); } } } else { Response::standardError('sslcertificateinvalidcert', '', true); } // get data from certificate to store in the table $validfromdate = empty($cert_content['validFrom_time_t']) ? null : date("Y-m-d H:i:s", $cert_content['validFrom_time_t']); $validtodate = empty($cert_content['validTo_time_t']) ? null : date("Y-m-d H:i:s", $cert_content['validTo_time_t']); $issuer = $cert_content['issuer']['O'] ?? ""; } // Add/Update database entry $qrystart = "UPDATE "; $qrywhere = "WHERE "; if ($do_insert) { $qrystart = "INSERT INTO "; $qrywhere = ", "; } $stmt = Database::prepare($qrystart . " `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` SET `ssl_cert_file` = :ssl_cert_file, `ssl_key_file` = :ssl_key_file, `ssl_ca_file` = :ssl_ca_file, `ssl_cert_chainfile` = :ssl_cert_chainfile, `validfromdate` = :validfromdate, `validtodate` = :validtodate, `issuer` = :issuer " . $qrywhere . " `domainid`= :domainid "); $params = [ "ssl_cert_file" => $ssl_cert_file, "ssl_key_file" => $ssl_key_file, "ssl_ca_file" => $ssl_ca_file, "ssl_cert_chainfile" => $ssl_cert_chainfile, "validfromdate" => $validfromdate, "validtodate" => $validtodate, "issuer" => $issuer, "domainid" => $domainid ]; Database::pexecute($stmt, $params, true, true); // insert task to re-generate webserver-configs (#1260) Cronjob::inserttask(TaskId::REBUILD_VHOST); return true; } /** * update ssl-certificate entry for given domain by either id or domainname * * @param int $id * optional, the domain-id * @param string $domainname * optional, the domainname * @param string $ssl_cert_file * @param string $ssl_key_file * @param string $ssl_ca_file * optional * @param string $ssl_cert_chainfile * optional * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function update() { $id = $this->getParam('id', true, 0); $dn_optional = $id > 0; $domainname = $this->getParam('domainname', $dn_optional, ''); if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'domains')) { throw new Exception("You cannot access this resource", 405); } $domain = $this->apiCall('SubDomains.get', [ 'id' => $id, 'domainname' => $domainname ]); // parameters $ssl_cert_file = $this->getParam('ssl_cert_file'); $ssl_key_file = $this->getParam('ssl_key_file'); $ssl_ca_file = $this->getParam('ssl_ca_file', true, ''); $ssl_cert_chainfile = $this->getParam('ssl_cert_chainfile', true, ''); $this->addOrUpdateCertificate($domain['id'], $ssl_cert_file, $ssl_key_file, $ssl_ca_file, $ssl_cert_chainfile, false); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] updated ssl-certificate for '" . $domain['domain'] . "'"); $result = $this->apiCall('Certificates.get', [ 'id' => $domain['id'] ]); return $this->response($result); } /** * lists all certificate entries * * @param array $sql_search * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), * LIKE is used if left empty and 'value' => searchvalue * @param int $sql_limit * optional specify number of results to be returned * @param int $sql_offset * optional specify offset for resultset * @param array $sql_orderby * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more * fields * * @access admin, customer * @return string json-encoded array count|list * @throws Exception */ public function listing() { // select all my (accessible) certificates $certs_stmt_query = "SELECT s.*, d.domain, d.letsencrypt, c.customerid, c.loginname FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` s LEFT JOIN `" . TABLE_PANEL_DOMAINS . "` d ON `d`.`id` = `s`.`domainid` LEFT JOIN `" . TABLE_PANEL_CUSTOMERS . "` c ON `c`.`customerid` = `d`.`customerid` WHERE "; $qry_params = []; $query_fields = []; if ($this->isAdmin() && $this->getUserDetail('customers_see_all') == '0') { // admin with only customer-specific permissions $certs_stmt_query .= "d.adminid = :adminid "; $qry_params['adminid'] = $this->getUserDetail('adminid'); } elseif ($this->isAdmin() == false) { // customer-area $certs_stmt_query .= "d.customerid = :cid "; $qry_params['cid'] = $this->getUserDetail('customerid'); } else { $certs_stmt_query .= "1 "; } $certs_stmt = Database::prepare($certs_stmt_query . $this->getSearchWhere($query_fields, true) . $this->getOrderBy() . $this->getLimit()); $qry_params = array_merge($qry_params, $query_fields); Database::pexecute($certs_stmt, $qry_params, true, true); $result = []; while ($cert = $certs_stmt->fetch(PDO::FETCH_ASSOC)) { // respect froxlor-hostname if ($cert['domainid'] == 0) { $cert['domain'] = Settings::Get('system.hostname'); $cert['letsencrypt'] = Settings::Get('system.le_froxlor_enabled'); $cert['loginname'] = 'froxlor.panel'; } // Set data from certificate $cert['isvalid'] = false; $cert['san'] = null; $cert_data = openssl_x509_parse($cert['ssl_cert_file']); if ($cert_data) { $cert['isvalid'] = (bool)$cert_data['validTo_time_t'] > time(); // Set subject alt names from certificate if (isset($cert_data['extensions']['subjectAltName']) && !empty($cert_data['extensions']['subjectAltName'])) { $SANs = explode(",", $cert_data['extensions']['subjectAltName']); $SANs = array_map('trim', $SANs); foreach ($SANs as $san) { $san = str_replace("DNS:", "", $san); if ($san != $cert_data['subject']['CN'] && strpos($san, "othername:") === false) { $cert['san'][] = $san; } } } } $result[] = $cert; } return $this->response([ 'count' => count($result), 'list' => $result ]); } /** * return ssl-certificate entry for given domain by either id or domainname * * @param int $id * optional, the domain-id * @param string $domainname * optional, the domainname * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function get() { $id = $this->getParam('id', true, 0); $dn_optional = $id > 0; $domainname = $this->getParam('domainname', $dn_optional, ''); if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'domains')) { throw new Exception("You cannot access this resource", 405); } $domain = $this->apiCall('SubDomains.get', [ 'id' => $id, 'domainname' => $domainname ]); $domainid = $domain['id']; $stmt = Database::prepare("SELECT * FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` WHERE `domainid`= :domainid"); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] get ssl-certificate for '" . $domain['domain'] . "'"); $result = Database::pexecute_first($stmt, [ "domainid" => $domainid ]); if (!$result) { throw new Exception("Domain '" . $domain['domain'] . "' does not have a certificate.", 412); } return $this->response($result); } /** * returns the total number of certificates for the given user * * @access admin, customer * @return string json-encoded response message * @throws Exception */ public function listingCount() { // select all my (accessible) certificates $certs_stmt_query = "SELECT COUNT(*) as num_certs FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` s LEFT JOIN `" . TABLE_PANEL_DOMAINS . "` d ON `d`.`id` = `s`.`domainid` LEFT JOIN `" . TABLE_PANEL_CUSTOMERS . "` c ON `c`.`customerid` = `d`.`customerid` WHERE "; $qry_params = []; $query_fields = []; if ($this->isAdmin() && $this->getUserDetail('customers_see_all') == '0') { // admin with only customer-specific permissions $certs_stmt_query .= "d.adminid = :adminid "; $qry_params['adminid'] = $this->getUserDetail('adminid'); } elseif ($this->isAdmin() == false) { // customer-area $certs_stmt_query .= "d.customerid = :cid "; $qry_params['cid'] = $this->getUserDetail('customerid'); } else { $certs_stmt_query .= "1 "; } $certs_stmt = Database::prepare($certs_stmt_query . $this->getSearchWhere($query_fields, true)); $qry_params = array_merge($qry_params, $query_fields); $result = Database::pexecute_first($certs_stmt, $qry_params, true, true); if ($result) { return $this->response($result['num_certs']); } return $this->response(0); } /** * delete certificates entry by id * * @param int $id * * @return string json-encoded array * @throws Exception */ public function delete() { $id = $this->getParam('id'); if ($this->isAdmin() == false) { $chk_stmt = Database::prepare(" SELECT d.domain, d.letsencrypt FROM `" . TABLE_PANEL_DOMAINS . "` d LEFT JOIN `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` s ON s.domainid = d.id WHERE s.`id` = :id AND d.`customerid` = :cid "); $chk = Database::pexecute_first($chk_stmt, [ 'id' => $id, 'cid' => $this->getUserDetail('customerid') ]); } elseif ($this->isAdmin()) { $chk_stmt = Database::prepare(" SELECT d.domain, d.letsencrypt FROM `" . TABLE_PANEL_DOMAINS . "` d LEFT JOIN `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` s ON s.domainid = d.id WHERE s.`id` = :id" . ($this->getUserDetail('customers_see_all') == '0' ? " AND d.`adminid` = :aid" : "")); $params = [ 'id' => $id ]; if ($this->getUserDetail('customers_see_all') == '0') { $params['aid'] = $this->getUserDetail('adminid'); } $chk = Database::pexecute_first($chk_stmt, $params); if ($chk == false && $this->getUserDetail('change_serversettings')) { // check whether it might be the froxlor-vhost certificate $chk_stmt = Database::prepare(" SELECT \"" . Settings::Get('system.hostname') . "\" as domain, \"" . Settings::Get('system.le_froxlor_enabled') . "\" as letsencrypt FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` WHERE `id` = :id AND `domainid` = '0'"); $params = [ 'id' => $id ]; $chk = Database::pexecute_first($chk_stmt, $params); $chk['isFroxlorVhost'] = true; } } if ($chk !== false) { // additional access check by trying to get the certificate if (isset($chk['isFroxlorVhost']) && $chk['isFroxlorVhost'] == true) { $result = $chk; } else { $result = $this->apiCall('Certificates.get', [ 'domainname' => $chk['domain'] ]); } $del_stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` WHERE id = :id"); Database::pexecute($del_stmt, [ 'id' => $id ]); // trigger removing of certificate from acme.sh if let's encrypt if ($chk['letsencrypt'] == '1') { Cronjob::inserttask(TaskId::DELETE_DOMAIN_SSL, $chk['domain']); } $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] removed ssl-certificate for '" . $chk['domain'] . "'"); return $this->response($result); } throw new Exception("Unable to determine SSL certificate. Maybe no access?", 406); } } ================================================ FILE: lib/Froxlor/Api/Commands/Cronjobs.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Api\Commands; use Exception; use Froxlor\Api\ApiCommand; use Froxlor\Api\ResourceEntity; use Froxlor\Cron\TaskId; use Froxlor\Database\Database; use Froxlor\FroxlorLogger; use Froxlor\System\Cronjob; use Froxlor\UI\Response; use Froxlor\Validate\Validate; use PDO; /** * @since 0.10.0 */ class Cronjobs extends ApiCommand implements ResourceEntity { private array $allowed_intervals = [ 'MINUTE', 'HOUR', 'DAY', 'WEEK', 'MONTH' ]; /** * You cannot add new cronjobs yet. */ public function add() { throw new Exception('You cannot add new cronjobs yet.', 303); } /** * return a cronjob entry by id * * @param int $id * cronjob-id * * @access admin * @return string json-encoded array * @throws Exception */ public function get() { if ($this->isAdmin()) { $id = $this->getParam('id'); $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_CRONRUNS . "` WHERE `id` = :id "); $result = Database::pexecute_first($result_stmt, [ 'id' => $id ], true, true); if ($result) { return $this->response($result); } throw new Exception("cronjob with id #" . $id . " could not be found", 404); } throw new Exception("Not allowed to execute given command.", 403); } /** * update a cronjob entry by given id * * @param int $id * @param bool $isactive * optional whether the cronjob is active or not * @param int $interval_value * optional number of seconds/minutes/hours/etc. for the interval * @param string $interval_interval * optional interval for the cronjob (MINUTE, HOUR, DAY, WEEK or MONTH) * * @access admin * @return string json-encoded array * @throws Exception */ public function update() { if ($this->isAdmin() && $this->getUserDetail('change_serversettings') == 1) { // required parameter $id = $this->getParam('id'); $result = $this->apiCall('Cronjobs.get', [ 'id' => $id ]); // split interval $cur_int = explode(" ", $result['interval']); // parameter $isactive = $this->getBoolParam('isactive', true, $result['isactive']); $interval_value = $this->getParam('interval_value', true, $cur_int[0]); $interval_interval = $this->getParam('interval_interval', true, $cur_int[1]); // validation if ($isactive != 1) { $isactive = 0; } $interval_value = Validate::validate($interval_value, 'interval_value', '/^([0-9]+)$/Di', 'stringisempty', [], true); $interval_interval = Validate::validate($interval_interval, 'interval_interval', '', '', [], true); if (!in_array(strtoupper($interval_interval), $this->allowed_intervals)) { Response::standardError('invalidcronjobintervalvalue', implode(", ", $this->allowed_intervals), true); } // put together interval value $interval = $interval_value . ' ' . strtoupper($interval_interval); $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_CRONRUNS . "` SET `isactive` = :isactive, `interval` = :int WHERE `id` = :id "); Database::pexecute($upd_stmt, [ 'isactive' => $isactive, 'int' => $interval, 'id' => $id ], true, true); // insert task to re-generate the cron.d-file Cronjob::inserttask(TaskId::REBUILD_CRON); $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] cronjob with description '" . $result['module'] . '/' . $result['cronfile'] . "' has been updated by '" . $this->getUserDetail('loginname') . "'"); $result = $this->apiCall('Cronjobs.get', [ 'id' => $id ]); return $this->response($result); } throw new Exception("Not allowed to execute given command.", 403); } /** * lists all cronjob entries * * @param array $sql_search * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), * LIKE is used if left empty and 'value' => searchvalue * @param int $sql_limit * optional specify number of results to be returned * @param int $sql_offset * optional specify offset for resultset * @param array $sql_orderby * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more * fields * * @access admin * @return string json-encoded array count|list * @throws Exception */ public function listing() { if ($this->isAdmin()) { $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] list cronjobs"); $query_fields = []; $result_stmt = Database::prepare(" SELECT `c`.* FROM `" . TABLE_PANEL_CRONRUNS . "` `c` " . $this->getSearchWhere($query_fields) . $this->getOrderBy() . $this->getLimit()); Database::pexecute($result_stmt, $query_fields, true, true); $result = []; while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $result[] = $row; } return $this->response([ 'count' => count($result), 'list' => $result ]); } throw new Exception("Not allowed to execute given command.", 403); } /** * returns the total number of cronjobs * * @access admin * @return string json-encoded response message * @throws Exception */ public function listingCount() { if ($this->isAdmin()) { $query_fields = []; $result_stmt = Database::prepare(" SELECT COUNT(*) as num_crons FROM `" . TABLE_PANEL_CRONRUNS . "` `c` " . $this->getSearchWhere($query_fields)); $result = Database::pexecute_first($result_stmt, $query_fields, true, true); if ($result) { return $this->response($result['num_crons']); } return $this->response(0); } throw new Exception("Not allowed to execute given command.", 403); } /** * You cannot delete system cronjobs. */ public function delete() { throw new Exception('You cannot delete system cronjobs.', 303); } } ================================================ FILE: lib/Froxlor/Api/Commands/Customers.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Api\Commands; use Exception; use Froxlor\Api\ApiCommand; use Froxlor\Api\ResourceEntity; use Froxlor\Cron\TaskId; use Froxlor\Database\Database; use Froxlor\Database\DbManager; use Froxlor\FileDir; use Froxlor\FroxlorLogger; use Froxlor\Idna\IdnaWrapper; use Froxlor\Language; use Froxlor\Settings; use Froxlor\System\Cronjob; use Froxlor\System\Crypt; use Froxlor\UI\Response; use Froxlor\User; use Froxlor\Validate\Validate; use PDO; /** * @since 0.10.0 */ class Customers extends ApiCommand implements ResourceEntity { /** * lists all customer entries * * @param array $sql_search * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), * LIKE is used if left empty and 'value' => searchvalue * @param int $sql_limit * optional specify number of results to be returned * @param int $sql_offset * optional specify offset for resultset * @param array $sql_orderby * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more * fields * @param bool $show_usages * optional, default false * * @access admin * @return string json-encoded array count|list * @throws Exception */ public function listing() { if ($this->isAdmin()) { $show_usages = $this->getBoolParam('show_usages', true, false); $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] list customers"); $query_fields = []; $result_stmt = Database::prepare(" SELECT `c`.*, `a`.`loginname` AS `adminname` FROM `" . TABLE_PANEL_CUSTOMERS . "` `c`, `" . TABLE_PANEL_ADMINS . "` `a` WHERE " . ($this->getUserDetail('customers_see_all') ? '' : " `c`.`adminid` = :adminid AND ") . " `c`.`adminid` = `a`.`adminid`" . $this->getSearchWhere($query_fields, true) . $this->getOrderBy() . $this->getLimit()); $params = []; if ($this->getUserDetail('customers_see_all') == '0') { $params = [ 'adminid' => $this->getUserDetail('adminid') ]; } $params = array_merge($params, $query_fields); Database::pexecute($result_stmt, $params, true, true); $result = []; $domains_stmt = null; $usages_stmt = null; if ($show_usages) { $domains_stmt = Database::prepare(" SELECT COUNT(`id`) AS `domains` FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `customerid` = :cid AND `parentdomainid` = '0' AND `id`<> :stdd "); $usages_stmt = Database::prepare(" SELECT webspace, mail, mysql FROM `" . TABLE_PANEL_DISKSPACE . "` WHERE `customerid` = :cid ORDER BY `stamp` DESC LIMIT 1 "); } while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { if ($show_usages) { // get number of domains $domains = Database::pexecute_first($domains_stmt, [ 'cid' => $row['customerid'], 'stdd' => $row['standardsubdomain'] ]); $row['domains'] = intval($domains['domains']); // get disk-space usages for web, mysql and mail $usages = Database::pexecute_first($usages_stmt, [ 'cid' => $row['customerid'] ]); if ($usages) { $row['webspace_used'] = $usages['webspace']; $row['mailspace_used'] = $usages['mail']; $row['dbspace_used'] = $usages['mysql']; } else { $row['webspace_used'] = 0; $row['mailspace_used'] = 0; $row['dbspace_used'] = 0; } } $result[] = $row; } return $this->response([ 'count' => count($result), 'list' => $result ]); } throw new Exception("Not allowed to execute given command.", 403); } /** * returns the total number of customers for the given admin * * @access admin * @return string json-encoded response message * @throws Exception */ public function listingCount() { if ($this->isAdmin()) { $query_fields = []; $result_stmt = Database::prepare(" SELECT COUNT(*) as num_customers FROM `" . TABLE_PANEL_CUSTOMERS . "` `c`, `" . TABLE_PANEL_ADMINS . "` `a` WHERE `c`.`adminid` = `a`.`adminid` AND " . ($this->getUserDetail('customers_see_all') ? "1" : " `c`.`adminid` = :adminid ") . $this->getSearchWhere($query_fields, true)); $params = []; if ($this->getUserDetail('customers_see_all') == '0') { $params = [ 'adminid' => $this->getUserDetail('adminid') ]; } $params = array_merge($params, $query_fields); $result = Database::pexecute_first($result_stmt, $params, true, true); if ($result) { return $this->response($result['num_customers']); } return $this->response(0); } throw new Exception("Not allowed to execute given command.", 403); } /** * create a new customer with default ftp-user and standard-subdomain (if wanted) * * @param string $email * required, email address of new customer * @param string $name * optional if company is set, else required * @param string $firstname * optional if company is set, else required * @param string $company * optional but required if name/firstname empty * @param string $street * optional * @param string $zipcode * optional * @param string $city * optional * @param string $phone * optional * @param string $fax * optional * @param int $customernumber * optional * @param string $def_language * optional, ISO 639-1 language code (e.g. 'en', 'de', see lng-folder for supported languages), * default is system-default language * @param bool $gui_access * optional, allow login via webui, if false ONLY the login via webui is disallowed; default true * @param bool $api_allowed * optional, default is true if system setting api.enabled is true, else false * @param bool $shell_allowed * optional, default is true if system setting system.allow_customer_shell is true, else false * @param int $gender * optional, 0 = no-gender, 1 = male, 2 = female * @param string $custom_notes * optional notes * @param bool $custom_notes_show * optional, whether to show the content of custom_notes to the customer, default 0 * (false) * @param string $new_loginname * optional, if empty generated automatically using customer-prefix and increasing * number * @param string $new_customer_password * optional, if empty generated automatically and send to the customer's email if * $sendpassword is 1 * @param bool $sendpassword * optional, whether to send the password to the customer after creation, default 0 * (false) * @param int $diskspace * optional disk-space available for customer in MB, default 0 * @param bool $diskspace_ul * optional, whether customer should have unlimited diskspace, default 0 (false) * @param int $traffic * optional traffic available for customer in GB, default 0 * @param bool $traffic_ul * optional, whether customer should have unlimited traffic, default 0 (false) * @param int $subdomains * optional amount of subdomains available for customer, default 0 * @param bool $subdomains_ul * optional, whether customer should have unlimited subdomains, default 0 (false) * @param int $emails * optional amount of emails available for customer, default 0 * @param bool $emails_ul * optional, whether customer should have unlimited emails, default 0 (false) * @param int $email_accounts * optional amount of email-accounts available for customer, default 0 * @param bool $email_accounts_ul * optional, whether customer should have unlimited email-accounts, default 0 (false) * @param int $email_forwarders * optional amount of email-forwarders available for customer, default 0 * @param bool $email_forwarders_ul * optional, whether customer should have unlimited email-forwarders, default 0 (false) * @param int $email_quota * optional size of email-quota available for customer in MB, default is system-setting * mail_quota * @param bool $email_quota_ul * optional, whether customer should have unlimited email-quota, default 0 (false) * @param bool $email_imap * optional, whether to allow IMAP access, default 0 (false) * @param bool $email_pop3 * optional, whether to allow POP3 access, default 0 (false) * @param int $ftps * optional amount of ftp-accounts available for customer, default 0 * @param bool $ftps_ul * optional, whether customer should have unlimited ftp-accounts, default 0 (false) * @param int $mysqls * optional amount of mysql-databases available for customer, default 0 * @param bool $mysqls_ul * optional, whether customer should have unlimited mysql-databases, default 0 (false) * @param bool $createstdsubdomain * optional, whether to create a standard-subdomain ([loginname].froxlor-hostname.tld), * default [system.createstdsubdom_default] * @param bool $phpenabled * optional, whether to allow usage of PHP, default 0 (false) * @param array $allowed_phpconfigs * optional, array of IDs of php-config that the customer is allowed to use, default * empty (none) * @param bool $perlenabled * optional, whether to allow usage of Perl/CGI, default 0 (false) * @param bool $dnsenabled * optional, whether to allow usage of the DNS editor (requires activated nameserver in * settings), default 0 (false) * @param bool $logviewenabled * optional, whether to allow access to webserver access/error-logs, default 0 (false) * @param bool $store_defaultindex * optional, whether to store the default index file to customers homedir * @param int $hosting_plan_id * optional, specify a hosting-plan to set certain resource-values from the plan * instead of specifying them * @param array $allowed_mysqlserver * optional, array of IDs of defined mysql-servers the customer is allowed to use, * default is to allow the default dbserver (id=0) * * @access admin * @return string json-encoded array * @throws Exception */ public function add() { if ($this->isAdmin()) { if ($this->getUserDetail('customers_used') < $this->getUserDetail('customers') || $this->getUserDetail('customers') == '-1') { // required parameters $email = $this->getParam('email'); // parameters $name = $this->getParam('name', true, ''); $firstname = $this->getParam('firstname', true, ''); $company_required = (!empty($name) && empty($firstname)) || (empty($name) && !empty($firstname)) || (empty($name) && empty($firstname)); $company = $this->getParam('company', !$company_required, ''); $street = $this->getParam('street', true, ''); $zipcode = $this->getParam('zipcode', true, ''); $city = $this->getParam('city', true, ''); $phone = $this->getParam('phone', true, ''); $fax = $this->getParam('fax', true, ''); $customernumber = $this->getParam('customernumber', true, ''); $def_language = $this->getParam('def_language', true, Settings::Get('panel.standardlanguage')); $gui_access = $this->getBoolParam('gui_access', true, 1); $api_allowed = $this->getBoolParam('api_allowed', true, (Settings::Get('api.enabled') && Settings::Get('api.customer_default'))); $shell_allowed = $this->getBoolParam('shell_allowed', true, intval(Settings::Get('system.allow_customer_shell'))); $gender = (int)$this->getParam('gender', true, 0); $custom_notes = $this->getParam('custom_notes', true, ''); $custom_notes_show = $this->getBoolParam('custom_notes_show', true, 0); $createstdsubdomain = $this->getBoolParam('createstdsubdomain', true, Settings::Get('system.createstdsubdom_default')); $password = $this->getParam('new_customer_password', true, ''); $sendpassword = $this->getBoolParam('sendpassword', true, 0); $store_defaultindex = $this->getBoolParam('store_defaultindex', true, 0); $loginname = $this->getParam('new_loginname', true, ''); // hosting-plan values $hosting_plan_id = $this->getParam('hosting_plan_id', true, 0); if ($hosting_plan_id > 0) { $hp_result = $this->apiCall('HostingPlans.get', [ 'id' => $hosting_plan_id ]); $hp_result['value'] = json_decode($hp_result['value'], true); foreach ($hp_result['value'] as $index => $value) { $hp_result[$index] = $value; } $diskspace = $hp_result['diskspace'] ?? 0; $traffic = $hp_result['traffic'] ?? 0; $subdomains = $hp_result['subdomains'] ?? 0; $emails = $hp_result['emails'] ?? 0; $email_accounts = $hp_result['email_accounts'] ?? 0; $email_forwarders = $hp_result['email_forwarders'] ?? 0; $email_quota = $hp_result['email_quota'] ?? Settings::Get('system.mail_quota'); $email_imap = $hp_result['email_imap'] ?? 0; $email_pop3 = $hp_result['email_pop3'] ?? 0; $ftps = $hp_result['ftps'] ?? 0; $mysqls = $hp_result['mysqls'] ?? 0; $phpenabled = $hp_result['phpenabled'] ?? 0; $p_allowed_phpconfigs = $hp_result['allowed_phpconfigs'] ?? 0; $perlenabled = $hp_result['perlenabled'] ?? 0; $dnsenabled = $hp_result['dnsenabled'] ?? 0; $logviewenabled = $hp_result['logviewenabled'] ?? 0; } else { $diskspace = $this->getUlParam('diskspace', 'diskspace_ul', true, 0); $traffic = $this->getUlParam('traffic', 'traffic_ul', true, 0); $subdomains = $this->getUlParam('subdomains', 'subdomains_ul', true, 0); $emails = $this->getUlParam('emails', 'emails_ul', true, 0); $email_accounts = $this->getUlParam('email_accounts', 'email_accounts_ul', true, 0); $email_forwarders = $this->getUlParam('email_forwarders', 'email_forwarders_ul', true, 0); $email_quota = $this->getUlParam('email_quota', 'email_quota_ul', true, Settings::Get('system.mail_quota')); $email_imap = $this->getBoolParam('email_imap', true, 0); $email_pop3 = $this->getBoolParam('email_pop3', true, 0); $ftps = $this->getUlParam('ftps', 'ftps_ul', true, 0); $mysqls = $this->getUlParam('mysqls', 'mysqls_ul', true, 0); $phpenabled = $this->getBoolParam('phpenabled', true, 0); $p_allowed_phpconfigs = $this->getParam('allowed_phpconfigs', true, []); $perlenabled = $this->getBoolParam('perlenabled', true, 0); $dnsenabled = $this->getBoolParam('dnsenabled', true, 0); $logviewenabled = $this->getBoolParam('logviewenabled', true, 0); } if ($mysqls == -1 || $mysqls > 0) { $p_allowed_mysqlserver = $this->getParam('allowed_mysqlserver', true, [0]); } else { // mysql not allowed, so no mysql available for customer $p_allowed_mysqlserver = []; } // validation $name = Validate::validate($name, 'name', Validate::REGEX_DESC_TEXT, '', [], true); $firstname = Validate::validate($firstname, 'first name', Validate::REGEX_DESC_TEXT, '', [], true); $company = Validate::validate($company, 'company', Validate::REGEX_DESC_TEXT, '', [], true); $street = Validate::validate($street, 'street', Validate::REGEX_DESC_TEXT, '', [], true); $zipcode = Validate::validate($zipcode, 'zipcode', '/^[0-9 \-A-Z]*$/', '', [], true); $city = Validate::validate($city, 'city', Validate::REGEX_DESC_TEXT, '', [], true); $phone = Validate::validate($phone, 'phone', '/^[0-9\- \+\(\)\/]*$/', '', [], true); $fax = Validate::validate($fax, 'fax', '/^[0-9\- \+\(\)\/]*$/', '', [], true); $idna_convert = new IdnaWrapper(); $email = $idna_convert->encode(Validate::validate($email, 'email', '', '', [], true)); $customernumber = Validate::validate($customernumber, 'customer number', '/^[A-Za-z0-9 \-]*$/Di', '', [], true); $def_language = Validate::validate($def_language, 'default language', '', '', [], true); if (!empty($def_language) && !isset(Language::getLanguages()[$def_language])) { $def_language = Settings::Get('panel.standardlanguage'); } $custom_notes = Validate::validate(str_replace("\r\n", "\n", $custom_notes), 'custom_notes', Validate::REGEX_CONF_TEXT, '', [], true); if (Settings::Get('system.mail_quota_enabled') != '1') { $email_quota = -1; } $password = Validate::validate($password, 'password', '', '', [], true); // only check if not empty, // cause empty == generate password automatically if ($password != '') { $password = Crypt::validatePassword($password, true); } // gender out of range? [0,2] if ($gender < 0 || $gender > 2) { $gender = 0; } $allowed_phpconfigs = []; if (!empty($p_allowed_phpconfigs) && is_array($p_allowed_phpconfigs)) { foreach ($p_allowed_phpconfigs as $allowed_phpconfig) { $allowed_phpconfig = intval($allowed_phpconfig); $allowed_phpconfigs[] = $allowed_phpconfig; } } $allowed_phpconfigs = array_map('intval', $allowed_phpconfigs); if (empty($allowed_phpconfigs) && $phpenabled == 1) { // only required if not using mod_php if ((int)Settings::Get('system.mod_fcgid') == 1 || (int)Settings::Get('phpfpm.enabled') == 1) { Response::standardError('customerphpenabledbutnoconfig', '', true); } } $allowed_mysqlserver = array(); if (!empty($p_allowed_mysqlserver) && is_array($p_allowed_mysqlserver)) { foreach ($p_allowed_mysqlserver as $allowed_ms) { $allowed_ms = intval($allowed_ms); $allowed_mysqlserver[] = $allowed_ms; } } $allowed_mysqlserver = array_map('intval', $allowed_mysqlserver); $diskspace *= 1024; $traffic *= 1024 * 1024; if ( ($diskspace != 0 && (($this->getUserDetail('diskspace_used') + $diskspace) > $this->getUserDetail('diskspace')) && ($this->getUserDetail('diskspace') / 1024) != '-1') || ($mysqls != 0 && (($this->getUserDetail('mysqls_used') + $mysqls) > $this->getUserDetail('mysqls')) && $this->getUserDetail('mysqls') != '-1') || ($emails != 0 && (($this->getUserDetail('emails_used') + $emails) > $this->getUserDetail('emails')) && $this->getUserDetail('emails') != '-1') || ($email_accounts != 0 && (($this->getUserDetail('email_accounts_used') + $email_accounts) > $this->getUserDetail('email_accounts')) && $this->getUserDetail('email_accounts') != '-1') || ($email_forwarders != 0 && (($this->getUserDetail('email_forwarders_used') + $email_forwarders) > $this->getUserDetail('email_forwarders')) && $this->getUserDetail('email_forwarders') != '-1') || ($email_quota != 0 && (($this->getUserDetail('email_quota_used') + $email_quota) > $this->getUserDetail('email_quota')) && $this->getUserDetail('email_quota') != '-1' && Settings::Get('system.mail_quota_enabled') == '1') || ($ftps != 0 && (($this->getUserDetail('ftps_used') + $ftps) > $this->getUserDetail('ftps')) && $this->getUserDetail('ftps') != '-1') || ($subdomains != 0 && (($this->getUserDetail('subdomains_used') + $subdomains) > $this->getUserDetail('subdomains')) && $this->getUserDetail('subdomains') != '-1') || (($diskspace / 1024) == '-1' && ($this->getUserDetail('diskspace') / 1024) != '-1') || ($mysqls == '-1' && $this->getUserDetail('mysqls') != '-1') || ($emails == '-1' && $this->getUserDetail('emails') != '-1') || ($email_accounts == '-1' && $this->getUserDetail('email_accounts') != '-1') || ($email_forwarders == '-1' && $this->getUserDetail('email_forwarders') != '-1') || ($email_quota == '-1' && $this->getUserDetail('email_quota') != '-1' && Settings::Get('system.mail_quota_enabled') == '1') || ($ftps == '-1' && $this->getUserDetail('ftps') != '-1') || ($subdomains == '-1' && $this->getUserDetail('subdomains') != '-1') ) { Response::standardError('youcantallocatemorethanyouhave', '', true); } if (!Validate::validateEmail($email)) { Response::standardError('emailiswrong', $email, true); } else { if ($loginname != '') { $accountnumber = intval(Settings::Get('system.lastaccountnumber')); $loginname = Validate::validate($loginname, 'loginname', '/^[a-z][a-z0-9\-_]+$/i', '', [], true); // Accounts which match systemaccounts are not allowed, filtering them if (preg_match('/^' . preg_quote(Settings::Get('customer.accountprefix'), '/') . '([0-9]+)/', $loginname)) { Response::standardError('loginnameisusingprefix', Settings::Get('customer.accountprefix'), true); } // Additional filtering for Bug #962 if (function_exists('posix_getpwnam') && !in_array("posix_getpwnam", explode(",", ini_get('disable_functions'))) && posix_getpwnam($loginname)) { Response::standardError('loginnameissystemaccount', $loginname, true); } // blacklist some system-internal names that might lead to issues Database::needSqlData(); $sqldata = Database::getSqlData(); Database::needRoot(true); Database::needSqlData(); $sqlrdata = Database::getSqlData(); $login_blacklist = [ 'root', 'admin', 'froxroot', 'froxlor', $sqldata['user'], $sqldata['db'], $sqlrdata['user'], ]; unset($sqldata); unset($sqlrdata); $login_blacklist = array_unique($login_blacklist); if (in_array($loginname, $login_blacklist)) { Response::standardError('loginnameisreservedname', $loginname, true); } } else { $accountnumber = intval(Settings::Get('system.lastaccountnumber')) + 1; $loginname = Settings::Get('customer.accountprefix') . $accountnumber; } // Check if the account already exists // do not check via api as we skip any permission checks for this task $loginname_check_stmt = Database::prepare(" SELECT `loginname` FROM `" . TABLE_PANEL_CUSTOMERS . "` WHERE `loginname` = :login "); $loginname_check = Database::pexecute_first($loginname_check_stmt, [ 'login' => $loginname ], true, true); // Check if an admin with the loginname already exists // do not check via api as we skip any permission checks for this task $loginname_check_admin_stmt = Database::prepare(" SELECT `loginname` FROM `" . TABLE_PANEL_ADMINS . "` WHERE `loginname` = :login "); $loginname_check_admin = Database::pexecute_first($loginname_check_admin_stmt, [ 'login' => $loginname ], true, true); // Check for existing email address // do not check via api as we skip any permission checks for this task $email_check_admin_stmt = Database::prepare(" SELECT `email` FROM `" . TABLE_PANEL_ADMINS . "` WHERE `email` = :email "); $email_check_admin = Database::pexecute_first($email_check_admin_stmt, [ 'email' => $email ], true, true); $mysql_maxlen = Database::getSqlUsernameLength() - strlen(Settings::Get('customer.mysqlprefix')); if (($loginname_check && strtolower($loginname_check['loginname']) == strtolower($loginname)) || ($loginname_check_admin && strtolower($loginname_check_admin['loginname']) == strtolower($loginname))) { Response::standardError('loginnameexists', $loginname, true); } elseif (!Validate::validateUsername($loginname, Settings::Get('panel.unix_names'), $mysql_maxlen)) { if (strlen($loginname) > $mysql_maxlen) { Response::standardError('loginnameiswrong2', $mysql_maxlen, true); } else { Response::standardError('loginnameiswrong', $loginname, true); } } elseif ($email_check_admin && strtolower($email_check_admin['email']) == strtolower($email)) { Response::standardError('emailexistsanon', $email, true); } $guid = intval(Settings::Get('system.lastguid')) + 1; $documentroot = FileDir::makeCorrectDir(Settings::Get('system.documentroot_prefix') . '/' . $loginname); if (file_exists($documentroot)) { Response::standardError('documentrootexists', $documentroot, true); } if ($password == '') { $password = Crypt::generatePassword(); } $_theme = Settings::Get('panel.default_theme'); $ins_data = [ 'adminid' => $this->getUserDetail('adminid'), 'loginname' => $loginname, 'passwd' => Crypt::makeCryptPassword($password), 'name' => $name, 'firstname' => $firstname, 'gender' => $gender, 'company' => $company, 'street' => $street, 'zipcode' => $zipcode, 'city' => $city, 'phone' => $phone, 'fax' => $fax, 'email' => $email, 'customerno' => $customernumber, 'lang' => $def_language, 'gui_access' => $gui_access, 'api_allowed' => $api_allowed, 'shell_allowed' => $shell_allowed, 'docroot' => $documentroot, 'guid' => $guid, 'diskspace' => $diskspace, 'traffic' => $traffic, 'subdomains' => $subdomains, 'emails' => $emails, 'email_accounts' => $email_accounts, 'email_forwarders' => $email_forwarders, 'email_quota' => $email_quota, 'ftps' => $ftps, 'mysqls' => $mysqls, 'phpenabled' => $phpenabled, 'allowed_phpconfigs' => empty($allowed_phpconfigs) ? "" : json_encode($allowed_phpconfigs), 'imap' => $email_imap, 'pop3' => $email_pop3, 'perlenabled' => $perlenabled, 'dnsenabled' => $dnsenabled, 'logviewenabled' => $logviewenabled, 'theme' => $_theme, 'custom_notes' => $custom_notes, 'custom_notes_show' => $custom_notes_show, 'allowed_mysqlserver' => empty($allowed_mysqlserver) ? "" : json_encode($allowed_mysqlserver) ]; $ins_stmt = Database::prepare(" INSERT INTO `" . TABLE_PANEL_CUSTOMERS . "` SET `adminid` = :adminid, `loginname` = :loginname, `password` = :passwd, `name` = :name, `firstname` = :firstname, `gender` = :gender, `company` = :company, `street` = :street, `zipcode` = :zipcode, `city` = :city, `phone` = :phone, `fax` = :fax, `email` = :email, `customernumber` = :customerno, `def_language` = :lang, `gui_access` = :gui_access, `api_allowed` = :api_allowed, `shell_allowed` = :shell_allowed, `documentroot` = :docroot, `guid` = :guid, `diskspace` = :diskspace, `traffic` = :traffic, `subdomains` = :subdomains, `emails` = :emails, `email_accounts` = :email_accounts, `email_forwarders` = :email_forwarders, `email_quota` = :email_quota, `ftps` = :ftps, `mysqls` = :mysqls, `standardsubdomain` = '0', `phpenabled` = :phpenabled, `allowed_phpconfigs` = :allowed_phpconfigs, `imap` = :imap, `pop3` = :pop3, `perlenabled` = :perlenabled, `dnsenabled` = :dnsenabled, `logviewenabled` = :logviewenabled, `theme` = :theme, `custom_notes` = :custom_notes, `custom_notes_show` = :custom_notes_show, `allowed_mysqlserver`= :allowed_mysqlserver "); Database::pexecute($ins_stmt, $ins_data, true, true); $customerid = Database::lastInsertId(); $ins_data['customerid'] = $customerid; Admins::increaseUsage($this->getUserDetail('adminid'), 'customers_used'); // update admin resource-usage if ($mysqls != '-1') { Admins::increaseUsage($this->getUserDetail('adminid'), 'mysqls_used', '', (int)$mysqls); } if ($emails != '-1') { Admins::increaseUsage($this->getUserDetail('adminid'), 'emails_used', '', (int)$emails); } if ($email_accounts != '-1') { Admins::increaseUsage($this->getUserDetail('adminid'), 'email_accounts_used', '', (int)$email_accounts); } if ($email_forwarders != '-1') { Admins::increaseUsage($this->getUserDetail('adminid'), 'email_forwarders_used', '', (int)$email_forwarders); } if ($email_quota != '-1') { Admins::increaseUsage($this->getUserDetail('adminid'), 'email_quota_used', '', (int)$email_quota); } if ($subdomains != '-1') { Admins::increaseUsage($this->getUserDetail('adminid'), 'subdomains_used', '', (int)$subdomains); } if ($ftps != '-1') { Admins::increaseUsage($this->getUserDetail('adminid'), 'ftps_used', '', (int)$ftps); } if (($diskspace / 1024) != '-1') { Admins::increaseUsage($this->getUserDetail('adminid'), 'diskspace_used', '', (int)$diskspace); } // update last guid Settings::Set('system.lastguid', $guid, true); if ($accountnumber != intval(Settings::Get('system.lastaccountnumber'))) { // update last account number Settings::Set('system.lastaccountnumber', $accountnumber, true); } $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] added customer '" . $loginname . "'"); unset($ins_data); // insert task to create homedir etc. Cronjob::inserttask(TaskId::CREATE_HOME, $loginname, $guid, $guid, $store_defaultindex); // Using filesystem - quota, insert a task which cleans the filesystem - quota Cronjob::inserttask(TaskId::CREATE_QUOTA); // Add htpasswd for the stats-pages $htpasswdPassword = Crypt::makeCryptPassword($password, true); $ins_stmt = Database::prepare(" INSERT INTO `" . TABLE_PANEL_HTPASSWDS . "` SET `customerid` = :customerid, `username` = :username, `password` = :passwd, `path` = :path "); $ins_data = [ 'customerid' => $customerid, 'username' => $loginname, 'passwd' => $htpasswdPassword ]; $stats_folder = Settings::Get('system.traffictool'); $ins_data['path'] = FileDir::makeCorrectDir($documentroot . '/' . $stats_folder . '/'); $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] automatically added " . $stats_folder . " htpasswd for user '" . $loginname . "'"); Database::pexecute($ins_stmt, $ins_data, true, true); Cronjob::inserttask(TaskId::REBUILD_VHOST); // add default FTP-User // also, add froxlor-local user to ftp-group (if exists!) to // allow access to customer-directories from within the panel, which // is necessary when pathedit = Dropdown $local_users = [ Settings::Get('system.httpuser') ]; if ((int)Settings::Get('system.mod_fcgid_ownvhost') == 1 || (int)Settings::Get('phpfpm.enabled_ownvhost') == 1) { if ((int)Settings::Get('system.mod_fcgid') == 1) { $local_user = Settings::Get('system.mod_fcgid_httpuser'); } else { $local_user = Settings::Get('phpfpm.vhost_httpuser'); } // check froxlor-local user membership in ftp-group // without this check addition may duplicate user in list if httpuser == local_user if (in_array($local_user, $local_users) == false) { $local_users[] = $local_user; } } $this->apiCall('Ftps.add', [ 'customerid' => $customerid, 'path' => '/', 'ftp_password' => $password, 'ftp_description' => "Default", 'sendinfomail' => 0, 'ftp_username' => $loginname, 'additional_members' => $local_users, 'is_defaultuser' => 1 ]); $_stdsubdomain = ''; if ($createstdsubdomain == '1') { if (Settings::Get('system.stdsubdomain') !== null && Settings::Get('system.stdsubdomain') != '') { $_stdsubdomain = $loginname . '.' . Settings::Get('system.stdsubdomain'); } else { $_stdsubdomain = $loginname . '.' . Settings::Get('system.hostname'); } $ins_data = [ 'domain' => $_stdsubdomain, 'customerid' => $customerid, 'adminid' => $this->getUserDetail('adminid'), 'docroot' => $documentroot, 'phpenabled' => $phpenabled, 'openbasedir' => '1', 'is_stdsubdomain' => 1 ]; $domainid = -1; try { $std_domain = $this->apiCall('Domains.add', $ins_data, true); $domainid = $std_domain['id']; } catch (Exception $e) { $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_ERR, "[API] Unable to add standard-subdomain: " . $e->getMessage()); } if ($domainid > 0) { $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `standardsubdomain` = :domainid WHERE `customerid` = :customerid "); Database::pexecute($upd_stmt, [ 'domainid' => $domainid, 'customerid' => $customerid ], true, true); $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] automatically added standardsubdomain for user '" . $loginname . "'"); Cronjob::inserttask(TaskId::REBUILD_VHOST); } } // Create default mysql-user if enabled if ($mysqls != 0) { foreach ($allowed_mysqlserver as $dbserver) { // require privileged access for target db-server Database::needRoot(true, $dbserver, false); // get DbManager $dbm = new DbManager($this->logger()); // give permission to the user on every access-host we have foreach (array_map('trim', explode(',', Settings::Get('system.mysql_access_host'))) as $mysql_access_host) { $dbm->getManager()->grantPrivilegesTo($loginname, $password, $mysql_access_host, false, false, true); } $dbm->getManager()->flushPrivileges(); Database::needRoot(false); } } if ($sendpassword == '1') { $srv_hostname = Settings::Get('system.hostname'); if (Settings::Get('system.froxlordirectlyviahostname') == '0') { $srv_hostname .= '/' . basename(\Froxlor\Froxlor::getInstallDir()); } $srv_ip_stmt = Database::prepare(" SELECT ip, port FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE `id` = :defaultip "); $default_ips = Settings::Get('system.defaultip'); $default_ips = explode(',', $default_ips); $srv_ip = Database::pexecute_first($srv_ip_stmt, [ 'defaultip' => reset($default_ips) ], true, true); $replace_arr = [ 'FIRSTNAME' => $firstname, 'NAME' => $name, 'COMPANY' => $company, 'SALUTATION' => User::getCorrectUserSalutation([ 'firstname' => $firstname, 'name' => $name, 'company' => $company ]), 'CUSTOMER_NO' => $customernumber, 'USERNAME' => $loginname, 'PASSWORD' => $password, 'SERVER_HOSTNAME' => $srv_hostname, 'SERVER_IP' => $srv_ip['ip'] ?? '', 'SERVER_PORT' => $srv_ip['port'] ?? '', 'DOMAINNAME' => $_stdsubdomain ]; // get template for mail subject $mail_subject = $this->getMailTemplate([ 'adminid' => $this->getUserDetail('adminid'), 'def_language' => $def_language ], 'mails', 'createcustomer_subject', $replace_arr, lng('mails.createcustomer.subject')); // get template for mail body $mail_body = $this->getMailTemplate([ 'adminid' => $this->getUserDetail('adminid'), 'def_language' => $def_language ], 'mails', 'createcustomer_mailbody', $replace_arr, lng('mails.createcustomer.mailbody')); $_mailerror = false; $mailerr_msg = ""; try { $this->mailer()->Subject = $mail_subject; $this->mailer()->AltBody = $mail_body; $this->mailer()->Body = str_replace("\n", "
", $mail_body); $this->mailer()->addAddress($email, User::getCorrectUserSalutation([ 'firstname' => $firstname, 'name' => $name, 'company' => $company ])); $this->mailer()->send(); } catch (\PHPMailer\PHPMailer\Exception $e) { $mailerr_msg = $e->errorMessage(); $_mailerror = true; } catch (Exception $e) { $mailerr_msg = $e->getMessage(); $_mailerror = true; } if ($_mailerror) { $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_ERR, "[API] Error sending mail: " . $mailerr_msg); Response::standardError('errorsendingmail', $email, true); } $this->mailer()->clearAddresses(); $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] automatically sent password to user '" . $loginname . "'"); } } $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] added customer '" . $loginname . "'"); $result = $this->apiCall('Customers.get', [ 'loginname' => $loginname ]); return $this->response($result); } throw new Exception("No more resources available", 406); } throw new Exception("Not allowed to execute given command.", 403); } /** * return a customer entry by either id or loginname * * @param int $id * optional, the customer-id * @param string $loginname * optional, the loginname * @param bool $show_usages * optional, default false * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function get() { $id = $this->getParam('id', true, 0); $ln_optional = $id > 0; $loginname = $this->getParam('loginname', $ln_optional, ''); $show_usages = $this->getBoolParam('show_usages', true, false); if ($this->isAdmin()) { $result_stmt = Database::prepare(" SELECT `c`.*, `a`.`loginname` AS `adminname` FROM `" . TABLE_PANEL_CUSTOMERS . "` `c`, `" . TABLE_PANEL_ADMINS . "` `a` WHERE " . ($id > 0 ? "`c`.`customerid` = :idln" : "`c`.`loginname` = :idln") . ($this->getUserDetail('customers_see_all') ? '' : " AND `c`.`adminid` = :adminid") . " AND `c`.`adminid` = `a`.`adminid`"); $params = [ 'idln' => ($id <= 0 ? $loginname : $id) ]; if ($this->getUserDetail('customers_see_all') == '0') { $params['adminid'] = $this->getUserDetail('adminid'); } } else { if (($id > 0 && $id != $this->getUserDetail('customerid')) || !empty($loginname) && $loginname != $this->getUserDetail('loginname')) { throw new Exception("You cannot access data of other customers", 401); } $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_CUSTOMERS . "` WHERE " . ($id > 0 ? "`customerid` = :idln" : "`loginname` = :idln")); $params = [ 'idln' => ($id <= 0 ? $loginname : $id) ]; } $result = Database::pexecute_first($result_stmt, $params, true, true); if ($result) { // check whether the admin does not want the customer to see the notes if (!$this->isAdmin() && $result['custom_notes_show'] != 1) { $result['custom_notes'] = ""; } if ($show_usages) { // get number of domains $domains_stmt = Database::prepare(" SELECT COUNT(`id`) AS `domains` FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `customerid` = :cid AND `parentdomainid` = '0' AND `id`<> :stdd "); Database::pexecute($domains_stmt, [ 'cid' => $result['customerid'], 'stdd' => $result['standardsubdomain'] ]); $domains = $domains_stmt->fetch(PDO::FETCH_ASSOC); $result['domains'] = intval($domains['domains']); // get disk-space usages for web, mysql and mail $usages_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_DISKSPACE . "` WHERE `customerid` = :cid ORDER BY `stamp` DESC LIMIT 1 "); $usages = Database::pexecute_first($usages_stmt, [ 'cid' => $result['customerid'] ]); if ($usages) { $result['webspace_used'] = $usages['webspace']; $result['mailspace_used'] = $usages['mail']; $result['dbspace_used'] = $usages['mysql']; } else { $result['webspace_used'] = 0; $result['mailspace_used'] = 0; $result['dbspace_used'] = 0; } } $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] get customer '" . $result['loginname'] . "'"); return $this->response($result); } $key = ($id > 0 ? "id #" . $id : "loginname '" . $loginname . "'"); throw new Exception("Customer with " . $key . " could not be found", 404); } /** * increase resource-usage * * @param int $customerid * @param string $resource * @param string $extra * optional, default empty * @param int $increase_by * optional, default 1 */ public static function increaseUsage($customerid = 0, $resource = null, $extra = '', $increase_by = 1) { self::updateResourceUsage(TABLE_PANEL_CUSTOMERS, 'customerid', $customerid, '+', $resource, $extra, $increase_by); } /** * update customer entry by either id or loginname, customer can only change language, password and theme * * @param int $id * optional, the customer-id * @param string $loginname * optional, the loginname * @param string $email * optional * @param string $name * optional if company is set, else required * @param string $firstname * optional if company is set, else required * @param string $company * optional but required if name/firstname empty * @param string $street * optional * @param string $zipcode * optional * @param string $city * optional * @param string $phone * optional * @param string $fax * optional * @param int $customernumber * optional * @param string $def_language * optional, ISO 639-1 language code (e.g. 'en', 'de', see lng-folder for supported languages), * default is system-default language * @param bool $gui_access * optional, allow login via webui, if false ONLY the login via webui is disallowed; default true * @param bool $api_allowed * optional, default is true if system setting api.enabled is true, else false * @param bool $shell_allowed * optional, default is true if system setting system.allow_customer_shell is true, else false * @param int $gender * optional, 0 = no-gender, 1 = male, 2 = female * @param string $custom_notes * optional notes * @param bool $custom_notes_show * optional, whether to show the content of custom_notes to the customer, default 0 * (false) * @param string $new_customer_password * optional, set new password * @param bool $sendpassword * optional, whether to send the password to the customer after creation, default 0 * (false) * @param int $move_to_admin * optional, if valid admin-id is given here, the customer's admin/reseller can be * changed * @param bool $deactivated * optional, if 1 (true) the customer can be deactivated/suspended * @param int $diskspace * optional disk-space available for customer in MB, default 0 * @param bool $diskspace_ul * optional, whether customer should have unlimited diskspace, default 0 (false) * @param int $traffic * optional traffic available for customer in GB, default 0 * @param bool $traffic_ul * optional, whether customer should have unlimited traffic, default 0 (false) * @param int $subdomains * optional amount of subdomains available for customer, default 0 * @param bool $subdomains_ul * optional, whether customer should have unlimited subdomains, default 0 (false) * @param int $emails * optional amount of emails available for customer, default 0 * @param bool $emails_ul * optional, whether customer should have unlimited emails, default 0 (false) * @param int $email_accounts * optional amount of email-accounts available for customer, default 0 * @param bool $email_accounts_ul * optional, whether customer should have unlimited email-accounts, default 0 (false) * @param int $email_forwarders * optional amount of email-forwarders available for customer, default 0 * @param bool $email_forwarders_ul * optional, whether customer should have unlimited email-forwarders, default 0 (false) * @param int $email_quota * optional size of email-quota available for customer in MB, default is system-setting * mail_quota * @param bool $email_quota_ul * optional, whether customer should have unlimited email-quota, default 0 (false) * @param bool $email_imap * optional, whether to allow IMAP access, default 0 (false) * @param bool $email_pop3 * optional, whether to allow POP3 access, default 0 (false) * @param int $ftps * optional amount of ftp-accounts available for customer, default 0 * @param bool $ftps_ul * optional, whether customer should have unlimited ftp-accounts, default 0 (false) * @param int $mysqls * optional amount of mysql-databases available for customer, default 0 * @param bool $mysqls_ul * optional, whether customer should have unlimited mysql-databases, default 0 (false) * @param bool $createstdsubdomain * optional, whether to create a standard-subdomain ([loginname].froxlor-hostname.tld), * default 1 (if customer has std-subdomain) else 0 (false) * @param bool $phpenabled * optional, whether to allow usage of PHP, default 0 (false) * @param array $allowed_phpconfigs * optional, array of IDs of php-config that the customer is allowed to use, default * empty (none) * @param bool $perlenabled * optional, whether to allow usage of Perl/CGI, default 0 (false) * @param bool $dnsenabled * optional, whether to allow usage of the DNS editor (requires activated nameserver in * settings), default 0 (false) * @param bool $logviewenabled * optional, whether to allow access to webserver access/error-logs, default 0 (false) * @param string $theme * optional, change theme * @param array $allowed_mysqlserver * optional, array of IDs of defined mysql-servers the customer is allowed to use, * default is to allow the default dbserver (id=0) * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function update() { $id = $this->getParam('id', true, 0); $ln_optional = $id > 0; $loginname = $this->getParam('loginname', $ln_optional, ''); $result = $this->apiCall('Customers.get', [ 'id' => $id, 'loginname' => $loginname ]); $id = $result['customerid']; if ($this->isAdmin()) { // parameters $move_to_admin = (int)($this->getParam('move_to_admin', true, 0)); $idna_convert = new IdnaWrapper(); $email = $this->getParam('email', true, $idna_convert->decode($result['email'])); $name = $this->getParam('name', true, $result['name']); $firstname = $this->getParam('firstname', true, $result['firstname']); $company_required = ((!empty($name) && empty($firstname)) || (empty($name) && !empty($firstname)) || (empty($name) && empty($firstname))) && empty($result['company']); $company = $this->getParam('company', !$company_required, $result['company']); $street = $this->getParam('street', true, $result['street']); $zipcode = $this->getParam('zipcode', true, $result['zipcode']); $city = $this->getParam('city', true, $result['city']); $phone = $this->getParam('phone', true, $result['phone']); $fax = $this->getParam('fax', true, $result['fax']); $customernumber = $this->getParam('customernumber', true, $result['customernumber']); $def_language = $this->getParam('def_language', true, $result['def_language']); $gui_access = $this->getBoolParam('gui_access', true, $result['gui_access']); $api_allowed = $this->getBoolParam('api_allowed', true, $result['api_allowed']); $shell_allowed = $this->getBoolParam('shell_allowed', true, $result['shell_allowed']); $gender = (int)$this->getParam('gender', true, $result['gender']); $custom_notes = $this->getParam('custom_notes', true, $result['custom_notes']); $custom_notes_show = $this->getBoolParam('custom_notes_show', true, $result['custom_notes_show']); $dec_places = Settings::Get('panel.decimal_places'); $diskspace = $this->getUlParam('diskspace', 'diskspace_ul', true, round($result['diskspace'] / 1024, $dec_places)); $traffic = $this->getUlParam('traffic', 'traffic_ul', true, round($result['traffic'] / (1024 * 1024), $dec_places)); $subdomains = $this->getUlParam('subdomains', 'subdomains_ul', true, $result['subdomains']); $emails = $this->getUlParam('emails', 'emails_ul', true, $result['emails']); $email_accounts = $this->getUlParam('email_accounts', 'email_accounts_ul', true, $result['email_accounts']); $email_forwarders = $this->getUlParam('email_forwarders', 'email_forwarders_ul', true, $result['email_forwarders']); $email_quota = $this->getUlParam('email_quota', 'email_quota_ul', true, $result['email_quota']); $email_imap = $this->getParam('email_imap', true, $result['imap']); $email_pop3 = $this->getParam('email_pop3', true, $result['pop3']); $ftps = $this->getUlParam('ftps', 'ftps_ul', true, $result['ftps']); $mysqls = $this->getUlParam('mysqls', 'mysqls_ul', true, $result['mysqls']); $createstdsubdomain = $this->getBoolParam('createstdsubdomain', true, ($result['standardsubdomain'] != 0 ? 1 : 0)); $password = $this->getParam('new_customer_password', true, ''); $phpenabled = $this->getBoolParam('phpenabled', true, $result['phpenabled']); $allowed_phpconfigs = $this->getParam('allowed_phpconfigs', true, json_decode($result['allowed_phpconfigs'], true)); $perlenabled = $this->getBoolParam('perlenabled', true, $result['perlenabled']); $dnsenabled = $this->getBoolParam('dnsenabled', true, $result['dnsenabled']); $logviewenabled = $this->getBoolParam('logviewenabled', true, $result['logviewenabled']); $deactivated = $this->getBoolParam('deactivated', true, $result['deactivated']); $theme = $this->getParam('theme', true, $result['theme']); $allowed_mysqlserver = $this->getParam('allowed_mysqlserver', true, json_decode($result['allowed_mysqlserver'], true)); } else { // allowed parameters $def_language = $this->getParam('def_language', true, $result['def_language']); $password = $this->getParam('new_customer_password', true, ''); $theme = $this->getParam('theme', true, $result['theme']); } // validation if ($this->isAdmin()) { $idna_convert = new IdnaWrapper(); $name = Validate::validate($name, 'name', Validate::REGEX_DESC_TEXT, '', [], true); $firstname = Validate::validate($firstname, 'first name', Validate::REGEX_DESC_TEXT, '', [], true); $company = Validate::validate($company, 'company', Validate::REGEX_DESC_TEXT, '', [], true); $street = Validate::validate($street, 'street', Validate::REGEX_DESC_TEXT, '', [], true); $zipcode = Validate::validate($zipcode, 'zipcode', '/^[0-9 \-A-Z]*$/', '', [], true); $city = Validate::validate($city, 'city', Validate::REGEX_DESC_TEXT, '', [], true); $phone = Validate::validate($phone, 'phone', '/^[0-9\- \+\(\)\/]*$/', '', [], true); $fax = Validate::validate($fax, 'fax', '/^[0-9\- \+\(\)\/]*$/', '', [], true); $email = $idna_convert->encode(Validate::validate($email, 'email', '', '', [], true)); $customernumber = Validate::validate($customernumber, 'customer number', '/^[A-Za-z0-9 \-]*$/Di', '', [], true); $custom_notes = Validate::validate(str_replace("\r\n", "\n", $custom_notes), 'custom_notes', Validate::REGEX_CONF_TEXT, '', [], true); if (!empty($allowed_phpconfigs)) { $allowed_phpconfigs = array_map('intval', $allowed_phpconfigs); } if (empty($allowed_phpconfigs) && $phpenabled == 1) { // only required if not using mod_php if ((int)Settings::Get('system.mod_fcgid') == 1 || (int)Settings::Get('phpfpm.enabled') == 1) { Response::standardError('customerphpenabledbutnoconfig', '', true); } } // add permission for allowed mysql usage if customer was not allowed to use mysql prior if ($result['mysqls'] == 0 && ($mysqls == -1 || $mysqls > 0)) { $allowed_mysqlserver = $this->getParam('allowed_mysqlserver', true, [0]); } if (!empty($allowed_mysqlserver)) { $allowed_mysqlserver = array_map('intval', $allowed_mysqlserver); } } $def_language = Validate::validate($def_language, 'default language', '', '', [], true); if (!empty($def_language) && !isset(Language::getLanguages()[$def_language])) { $def_language = Settings::Get('panel.standardlanguage'); } $theme = Validate::validate($theme, 'theme', '', '', [], true); if (Settings::Get('system.mail_quota_enabled') != '1') { $email_quota = -1; } if (empty($theme)) { $theme = Settings::Get('panel.default_theme'); } if ($this->isAdmin()) { $diskspace *= 1024; $traffic *= 1024 * 1024; if ( ($diskspace != 0 && (($this->getUserDetail('diskspace_used') + $diskspace - $result['diskspace']) > $this->getUserDetail('diskspace')) && ($this->getUserDetail('diskspace') / 1024) != '-1') || ($mysqls != 0 && (($this->getUserDetail('mysqls_used') + $mysqls - $result['mysqls']) > $this->getUserDetail('mysqls')) && $this->getUserDetail('mysqls') != '-1') || ($emails != 0 && (($this->getUserDetail('emails_used') + $emails - $result['emails']) > $this->getUserDetail('emails')) && $this->getUserDetail('emails') != '-1') || ($email_accounts != 0 && (($this->getUserDetail('email_accounts_used') + $email_accounts - $result['email_accounts']) > $this->getUserDetail('email_accounts')) && $this->getUserDetail('email_accounts') != '-1') || ($email_forwarders != 0 && (($this->getUserDetail('email_forwarders_used') + $email_forwarders - $result['email_forwarders']) > $this->getUserDetail('email_forwarders')) && $this->getUserDetail('email_forwarders') != '-1') || ($email_quota != 0 && (($this->getUserDetail('email_quota_used') + $email_quota - $result['email_quota']) > $this->getUserDetail('email_quota')) && $this->getUserDetail('email_quota') != '-1' && Settings::Get('system.mail_quota_enabled') == '1') || ($ftps != 0 && (($this->getUserDetail('ftps_used') + $ftps - $result['ftps']) > $this->getUserDetail('ftps')) && $this->getUserDetail('ftps') != '-1') || ($subdomains != 0 && (($this->getUserDetail('subdomains_used') + $subdomains - $result['subdomains']) > $this->getUserDetail('subdomains')) && $this->getUserDetail('subdomains') != '-1') || (($diskspace / 1024) == '-1' && ($this->getUserDetail('diskspace') / 1024) != '-1') || ($mysqls == '-1' && $this->getUserDetail('mysqls') != '-1') || ($emails == '-1' && $this->getUserDetail('emails') != '-1') || ($email_accounts == '-1' && $this->getUserDetail('email_accounts') != '-1') || ($email_forwarders == '-1' && $this->getUserDetail('email_forwarders') != '-1') || ($email_quota == '-1' && $this->getUserDetail('email_quota') != '-1' && Settings::Get('system.mail_quota_enabled') == '1') || ($ftps == '-1' && $this->getUserDetail('ftps') != '-1') || ($subdomains == '-1' && $this->getUserDetail('subdomains') != '-1') ) { Response::standardError('youcantallocatemorethanyouhave', '', true); } // validate allowed_mysqls whether the customer has databases on a removed, now disallowed db-server and abort if true $former_allowed_mysqlserver = json_decode($result['allowed_mysqlserver'], true); if ($allowed_mysqlserver != $former_allowed_mysqlserver && !empty($former_allowed_mysqlserver)) { $to_remove_mysqlserver = array_diff($former_allowed_mysqlserver, $allowed_mysqlserver); if (count($to_remove_mysqlserver) > 0) { foreach ($to_remove_mysqlserver as $mysqlserver_check) { $result_ms = $this->apiCall('MysqlServer.databasesOnServer', [ 'mysql_server' => $mysqlserver_check, 'customerid' => $id ]); if ($result_ms['count'] > 0) { Response::standardError('mysqlserverstillhasdbs', '', true); } } } } if ($email == '') { Response::standardError([ 'stringisempty', 'customer.email' ], '', true); } elseif (!Validate::validateEmail($email)) { Response::standardError('emailiswrong', $email, true); } else { // Check for existing email address // do not check via api as we skip any permission checks for this task $email_check_admin_stmt = Database::prepare(" SELECT `email` FROM `" . TABLE_PANEL_ADMINS . "` WHERE `email` = :email "); $email_check_admin = Database::pexecute_first($email_check_admin_stmt, [ 'email' => $email ], true, true); if ($email_check_admin && strtolower($email_check_admin['email']) == strtolower($email)) { Response::standardError('emailexistsanon', $email, true); } } } if ($password != '') { $password = Crypt::validatePassword($password, true); $password = Crypt::makeCryptPassword($password); } else { $password = $result['password']; } if ($this->isAdmin()) { if ($createstdsubdomain != '1' || $deactivated) { $createstdsubdomain = '0'; } if ($createstdsubdomain == '1' && $result['standardsubdomain'] == '0') { if (Settings::Get('system.stdsubdomain') !== null && Settings::Get('system.stdsubdomain') != '') { $_stdsubdomain = $result['loginname'] . '.' . Settings::Get('system.stdsubdomain'); } else { $_stdsubdomain = $result['loginname'] . '.' . Settings::Get('system.hostname'); } $ins_data = [ 'domain' => $_stdsubdomain, 'customerid' => $result['customerid'], 'adminid' => $this->getUserDetail('adminid'), 'docroot' => $result['documentroot'], 'phpenabled' => $phpenabled, 'openbasedir' => '1' ]; $domainid = -1; try { $std_domain = $this->apiCall('Domains.add', $ins_data); $domainid = $std_domain['id']; } catch (Exception $e) { $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_ERR, "[API] Unable to add standard-subdomain: " . $e->getMessage()); } if ($domainid > 0) { $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `standardsubdomain` = :domainid WHERE `customerid` = :customerid "); Database::pexecute($upd_stmt, [ 'domainid' => $domainid, 'customerid' => $result['customerid'] ], true, true); $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] automatically added standardsubdomain for user '" . $result['loginname'] . "'"); Cronjob::inserttask(TaskId::REBUILD_VHOST); } } if ($createstdsubdomain == '0' && $result['standardsubdomain'] != '0') { try { $std_domain = $this->apiCall('Domains.delete', [ 'id' => $result['standardsubdomain'], 'is_stdsubdomain' => 1 ]); } catch (Exception $e) { $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_ERR, "[API] Unable to delete standard-subdomain: " . $e->getMessage()); } $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] automatically deleted standardsubdomain for user '" . $result['loginname'] . "'"); Cronjob::inserttask(TaskId::REBUILD_VHOST); } if ($phpenabled != $result['phpenabled'] || $perlenabled != $result['perlenabled'] || $email != $result['email']) { Cronjob::inserttask(TaskId::REBUILD_VHOST); } // activate/deactivate customer services if ($deactivated != $result['deactivated']) { $yesno = ($deactivated ? 'N' : 'Y'); $pop3 = ($deactivated ? '0' : (int)$result['pop3']); $imap = ($deactivated ? '0' : (int)$result['imap']); $upd_stmt = Database::prepare(" UPDATE `" . TABLE_MAIL_USERS . "` SET `postfix`= :yesno, `pop3` = :pop3, `imap` = :imap WHERE `customerid` = :customerid "); Database::pexecute($upd_stmt, [ 'yesno' => $yesno, 'pop3' => $pop3, 'imap' => $imap, 'customerid' => $id ]); $upd_stmt = Database::prepare(" UPDATE `" . TABLE_FTP_USERS . "` SET `login_enabled` = :yesno WHERE `customerid` = :customerid "); Database::pexecute($upd_stmt, [ 'yesno' => $yesno, 'customerid' => $id ]); $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_DOMAINS . "` SET `deactivated`= :deactivated WHERE `customerid` = :customerid "); Database::pexecute($upd_stmt, [ 'deactivated' => $deactivated, 'customerid' => $id ]); // enable/disable global mysql-user (loginname) $current_allowed_mysqlserver = isset($result['allowed_mysqlserver']) && !empty($result['allowed_mysqlserver']) ? json_decode($result['allowed_mysqlserver'], true) : []; foreach ($current_allowed_mysqlserver as $dbserver) { // require privileged access for target db-server Database::needRoot(true, $dbserver, true); // get DbManager $dbm = new DbManager($this->logger()); foreach (array_map('trim', explode(',', Settings::Get('system.mysql_access_host'))) as $mysql_access_host) { // Prevent access, if deactivated if ($deactivated) { // failsafe if user has been deleted manually (requires MySQL 4.1.2+) $dbm->getManager()->disableUser($result['loginname'], $mysql_access_host); } else { // Otherwise grant access $dbm->getManager()->enableUser($result['loginname'], $mysql_access_host, true); } } $dbm->getManager()->flushPrivileges(); Database::needRoot(false); } // Retrieve customer's databases $databases_stmt = Database::prepare("SELECT * FROM " . TABLE_PANEL_DATABASES . " WHERE customerid = :customerid ORDER BY `dbserver`"); Database::pexecute($databases_stmt, [ 'customerid' => $id ]); Database::needRoot(true); $last_dbserver = 0; $dbm = new DbManager($this->logger()); // For each of them $priv_changed = false; while ($row_database = $databases_stmt->fetch(PDO::FETCH_ASSOC)) { if ($last_dbserver != $row_database['dbserver']) { $dbm->getManager()->flushPrivileges(); Database::needRoot(true, $row_database['dbserver']); $last_dbserver = $row_database['dbserver']; } foreach (array_map('trim', explode(',', Settings::Get('system.mysql_access_host'))) as $mysql_access_host) { // Prevent access, if deactivated if ($deactivated) { // failsafe if user has been deleted manually (requires MySQL 4.1.2+) $dbm->getManager()->disableUser($row_database['databasename'], $mysql_access_host); } else { // Otherwise grant access $dbm->getManager()->enableUser($row_database['databasename'], $mysql_access_host); } } $priv_changed = true; } // At last flush the new privileges if ($priv_changed) { $dbm->getManager()->flushPrivileges(); } Database::needRoot(false); // reactivate/deactivate api-keys $valid_until = $deactivated ? 0 : -1; $stmt = Database::prepare("UPDATE `" . TABLE_API_KEYS . "` SET `valid_until` = :vu WHERE `customerid` = :id"); Database::pexecute($stmt, [ 'id' => $id, 'vu' => $valid_until ], true, true); $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] " . ($deactivated ? 'deactivated' : 'reactivated') . " user '" . $result['loginname'] . "'"); Cronjob::inserttask(TaskId::REBUILD_VHOST); } // Disable or enable POP3 Login for customers Mail Accounts if ($email_pop3 != $result['pop3']) { $upd_stmt = Database::prepare("UPDATE `" . TABLE_MAIL_USERS . "` SET `pop3` = :pop3 WHERE `customerid` = :customerid"); Database::pexecute($upd_stmt, [ 'pop3' => $email_pop3, 'customerid' => $id ]); } // Disable or enable IMAP Login for customers Mail Accounts if ($email_imap != $result['imap']) { $upd_stmt = Database::prepare("UPDATE `" . TABLE_MAIL_USERS . "` SET `imap` = :imap WHERE `customerid` = :customerid"); Database::pexecute($upd_stmt, [ 'imap' => $email_imap, 'customerid' => $id ]); } } $upd_data = [ 'customerid' => $id, 'passwd' => $password, 'lang' => $def_language, 'theme' => $theme ]; if ($this->isAdmin()) { $admin_upd_data = [ 'name' => $name, 'firstname' => $firstname, 'gender' => $gender, 'company' => $company, 'street' => $street, 'zipcode' => $zipcode, 'city' => $city, 'phone' => $phone, 'fax' => $fax, 'email' => $email, 'customerno' => $customernumber, 'diskspace' => $diskspace, 'traffic' => $traffic, 'subdomains' => $subdomains, 'emails' => $emails, 'email_accounts' => $email_accounts, 'email_forwarders' => $email_forwarders, 'email_quota' => $email_quota, 'ftps' => $ftps, 'mysqls' => $mysqls, 'deactivated' => $deactivated, 'phpenabled' => $phpenabled, 'allowed_phpconfigs' => empty($allowed_phpconfigs) ? "" : json_encode($allowed_phpconfigs), 'imap' => $email_imap, 'pop3' => $email_pop3, 'perlenabled' => $perlenabled, 'dnsenabled' => $dnsenabled, 'logviewenabled' => $logviewenabled, 'custom_notes' => $custom_notes, 'custom_notes_show' => $custom_notes_show, 'gui_access' => $gui_access, 'api_allowed' => $api_allowed, 'shell_allowed' => $shell_allowed, 'allowed_mysqlserver' => empty($allowed_mysqlserver) ? "" : json_encode($allowed_mysqlserver) ]; $upd_data += $admin_upd_data; } $upd_query = "UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `def_language` = :lang, `password` = :passwd, `theme` = :theme"; if ($this->isAdmin()) { $admin_upd_query = ", `name` = :name, `firstname` = :firstname, `gender` = :gender, `company` = :company, `street` = :street, `zipcode` = :zipcode, `city` = :city, `phone` = :phone, `fax` = :fax, `email` = :email, `customernumber` = :customerno, `diskspace` = :diskspace, `traffic` = :traffic, `subdomains` = :subdomains, `emails` = :emails, `email_accounts` = :email_accounts, `email_forwarders` = :email_forwarders, `ftps` = :ftps, `mysqls` = :mysqls, `deactivated` = :deactivated, `phpenabled` = :phpenabled, `allowed_phpconfigs` = :allowed_phpconfigs, `email_quota` = :email_quota, `imap` = :imap, `pop3` = :pop3, `perlenabled` = :perlenabled, `dnsenabled` = :dnsenabled, `logviewenabled` = :logviewenabled, `custom_notes` = :custom_notes, `custom_notes_show` = :custom_notes_show, `gui_access` = :gui_access, `api_allowed` = :api_allowed, `shell_allowed` = :shell_allowed, `allowed_mysqlserver` = :allowed_mysqlserver"; $upd_query .= $admin_upd_query; } $upd_query .= " WHERE `customerid` = :customerid"; $upd_stmt = Database::prepare($upd_query); Database::pexecute($upd_stmt, $upd_data); if ($this->isAdmin()) { // Using filesystem - quota, insert a task which cleans the filesystem - quota Cronjob::inserttask(TaskId::CREATE_QUOTA); $admin_update_query = "UPDATE `" . TABLE_PANEL_ADMINS . "` SET `customers_used` = `customers_used` "; if ($mysqls != '-1' || $result['mysqls'] != '-1') { $admin_update_query .= ", `mysqls_used` = `mysqls_used` "; if ($mysqls != '-1') { $admin_update_query .= " + 0" . (int)$mysqls . " "; } if ($result['mysqls'] != '-1') { $admin_update_query .= " - 0" . (int)$result['mysqls'] . " "; } } if ($emails != '-1' || $result['emails'] != '-1') { $admin_update_query .= ", `emails_used` = `emails_used` "; if ($emails != '-1') { $admin_update_query .= " + 0" . (int)$emails . " "; } if ($result['emails'] != '-1') { $admin_update_query .= " - 0" . (int)$result['emails'] . " "; } } if ($email_accounts != '-1' || $result['email_accounts'] != '-1') { $admin_update_query .= ", `email_accounts_used` = `email_accounts_used` "; if ($email_accounts != '-1') { $admin_update_query .= " + 0" . (int)$email_accounts . " "; } if ($result['email_accounts'] != '-1') { $admin_update_query .= " - 0" . (int)$result['email_accounts'] . " "; } } if ($email_forwarders != '-1' || $result['email_forwarders'] != '-1') { $admin_update_query .= ", `email_forwarders_used` = `email_forwarders_used` "; if ($email_forwarders != '-1') { $admin_update_query .= " + 0" . (int)$email_forwarders . " "; } if ($result['email_forwarders'] != '-1') { $admin_update_query .= " - 0" . (int)$result['email_forwarders'] . " "; } } if ($email_quota != '-1' || $result['email_quota'] != '-1') { $admin_update_query .= ", `email_quota_used` = `email_quota_used` "; if ($email_quota != '-1') { $admin_update_query .= " + 0" . (int)$email_quota . " "; } if ($result['email_quota'] != '-1') { $admin_update_query .= " - 0" . (int)$result['email_quota'] . " "; } } if ($subdomains != '-1' || $result['subdomains'] != '-1') { $admin_update_query .= ", `subdomains_used` = `subdomains_used` "; if ($subdomains != '-1') { $admin_update_query .= " + 0" . (int)$subdomains . " "; } if ($result['subdomains'] != '-1') { $admin_update_query .= " - 0" . (int)$result['subdomains'] . " "; } } if ($ftps != '-1' || $result['ftps'] != '-1') { $admin_update_query .= ", `ftps_used` = `ftps_used` "; if ($ftps != '-1') { $admin_update_query .= " + 0" . (int)$ftps . " "; } if ($result['ftps'] != '-1') { $admin_update_query .= " - 0" . (int)$result['ftps'] . " "; } } if (($diskspace / 1024) != '-1' || ($result['diskspace'] / 1024) != '-1') { $admin_update_query .= ", `diskspace_used` = `diskspace_used` "; if (($diskspace / 1024) != '-1') { $admin_update_query .= " + 0" . (int)$diskspace . " "; } if (($result['diskspace'] / 1024) != '-1') { $admin_update_query .= " - 0" . (int)$result['diskspace'] . " "; } } $admin_update_query .= " WHERE `adminid` = '" . (int)$result['adminid'] . "'"; Database::query($admin_update_query); } // shell allowance has changed if ($result['shell_allowed'] == '1' && $shell_allowed == '0') { // update all users with a valid shell to have /bin/false (disable shell) $ftp_upd_stmt = Database::prepare("UPDATE `" . TABLE_FTP_USERS . "` SET `shell` = '/bin/false' WHERE `customerid` = :cid"); Database::pexecute($ftp_upd_stmt, ['cid' => (int)$result['customerid']]); } $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] edited user '" . $result['loginname'] . "'"); /* * move customer to another admin/reseller; #1166 */ if ($this->isAdmin()) { if ($move_to_admin > 0 && $move_to_admin != $result['adminid']) { $move_result = $this->apiCall('Customers.move', [ 'id' => $result['customerid'], 'adminid' => $move_to_admin ]); if ($move_result != true) { Response::standardError('moveofcustomerfailed', $move_result, true); } } } $result = $this->apiCall('Customers.get', [ 'id' => $result['customerid'] ]); return $this->response($result); } /** * delete a customer entry by either id or loginname * * @param int $id * optional, the customer-id * @param string $loginname * optional, the loginname * @param bool $delete_userfiles * optional, default false * * @access admin * @return string json-encoded array * @throws Exception */ public function delete() { if ($this->isAdmin()) { $id = $this->getParam('id', true, 0); $ln_optional = $id > 0; $loginname = $this->getParam('loginname', $ln_optional, ''); $delete_userfiles = $this->getParam('delete_userfiles', true, 0); $result = $this->apiCall('Customers.get', [ 'id' => $id, 'loginname' => $loginname ]); $id = $result['customerid']; // remove global mysql-user (loginname) $current_allowed_mysqlserver = isset($result['allowed_mysqlserver']) && !empty($result['allowed_mysqlserver']) ? json_decode($result['allowed_mysqlserver'], true) : []; foreach ($current_allowed_mysqlserver as $dbserver) { // require privileged access for target db-server Database::needRoot(true, $dbserver, false); // get DbManager $dbm = new DbManager($this->logger()); foreach (array_map('trim', explode(',', Settings::Get('system.mysql_access_host'))) as $mysql_access_host) { $dbm->getManager()->deleteUser($result['loginname'], $mysql_access_host); } $dbm->getManager()->flushPrivileges(); Database::needRoot(false); } // remove all databases $databases_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_DATABASES . "` WHERE `customerid` = :id ORDER BY `dbserver` "); Database::pexecute($databases_stmt, [ 'id' => $id ]); Database::needRoot(true); $last_dbserver = 0; $dbm = new DbManager($this->logger()); $priv_changed = false; while ($row_database = $databases_stmt->fetch(PDO::FETCH_ASSOC)) { if ($last_dbserver != $row_database['dbserver']) { $dbm->getManager()->flushPrivileges(); Database::needRoot(true, $row_database['dbserver']); $last_dbserver = $row_database['dbserver']; } $dbm->getManager()->deleteDatabase($row_database['databasename']); $priv_changed = true; } if ($priv_changed) { $dbm->getManager()->flushPrivileges(); } Database::needRoot(false); // delete customer itself $stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_CUSTOMERS . "` WHERE `customerid` = :id"); Database::pexecute($stmt, [ 'id' => $id ], true, true); // delete customer databases $stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_DATABASES . "` WHERE `customerid` = :id"); Database::pexecute($stmt, [ 'id' => $id ], true, true); // first gather all domain-id's to clean up panel_domaintoip, dns-entries and certificates accordingly $did_stmt = Database::prepare("SELECT `id`, `domain` FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `customerid` = :id"); Database::pexecute($did_stmt, [ 'id' => $id ], true, true); while ($row = $did_stmt->fetch(PDO::FETCH_ASSOC)) { // remove domain->ip connection $stmt = Database::prepare("DELETE FROM `" . TABLE_DOMAINTOIP . "` WHERE `id_domain` = :did"); Database::pexecute($stmt, [ 'did' => $row['id'] ], true, true); // remove domain->dns entries $stmt = Database::prepare("DELETE FROM `" . TABLE_DOMAIN_DNS . "` WHERE `domain_id` = :did"); Database::pexecute($stmt, [ 'did' => $row['id'] ], true, true); // remove domain->certificates entries $stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` WHERE `domainid` = :did"); Database::pexecute($stmt, [ 'did' => $row['id'] ], true, true); // remove domains DNS from powerDNS if used, #581 Cronjob::inserttask(TaskId::DELETE_DOMAIN_PDNS, $row['domain']); // remove domain from acme.sh / lets encrypt if used Cronjob::inserttask(TaskId::DELETE_DOMAIN_SSL, $row['domain']); } // remove customer domains $stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `customerid` = :id"); Database::pexecute($stmt, [ 'id' => $id ], true, true); $domains_deleted = $stmt->rowCount(); // delete htpasswds $stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_HTPASSWDS . "` WHERE `customerid` = :id"); Database::pexecute($stmt, [ 'id' => $id ], true, true); // delete htaccess options $stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_HTACCESS . "` WHERE `customerid` = :id"); Database::pexecute($stmt, [ 'id' => $id ], true, true); // delete traffic information $stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_TRAFFIC . "` WHERE `customerid` = :id"); Database::pexecute($stmt, [ 'id' => $id ], true, true); // remove diskspace analysis $stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_DISKSPACE . "` WHERE `customerid` = :id"); Database::pexecute($stmt, [ 'id' => $id ], true, true); // delete mail-accounts $stmt = Database::prepare("DELETE FROM `" . TABLE_MAIL_USERS . "` WHERE `customerid` = :id"); Database::pexecute($stmt, [ 'id' => $id ], true, true); // delete mail-addresses $stmt = Database::prepare("DELETE FROM `" . TABLE_MAIL_VIRTUAL . "` WHERE `customerid` = :id"); Database::pexecute($stmt, [ 'id' => $id ], true, true); // gather ftp-user names $result2_stmt = Database::prepare("SELECT `username` FROM `" . TABLE_FTP_USERS . "` WHERE `customerid` = :id"); Database::pexecute($result2_stmt, [ 'id' => $id ], true, true); while ($row = $result2_stmt->fetch(PDO::FETCH_ASSOC)) { // delete ftp-quotatallies by username $stmt = Database::prepare("DELETE FROM `" . TABLE_FTP_QUOTATALLIES . "` WHERE `name` = :name"); Database::pexecute($stmt, [ 'name' => $row['username'] ], true, true); } // remove ftp-group $stmt = Database::prepare("DELETE FROM `" . TABLE_FTP_GROUPS . "` WHERE `customerid` = :id"); Database::pexecute($stmt, [ 'id' => $id ], true, true); // remove ftp-users $stmt = Database::prepare("DELETE FROM `" . TABLE_FTP_USERS . "` WHERE `customerid` = :id"); Database::pexecute($stmt, [ 'id' => $id ], true, true); // remove api-keys $stmt = Database::prepare("DELETE FROM `" . TABLE_API_KEYS . "` WHERE `customerid` = :id"); Database::pexecute($stmt, [ 'id' => $id ], true, true); // Delete all waiting "create user" -tasks for this user, #276 // Note: the WHERE selects part of a serialized array, but it should be safe this way $del_stmt = Database::prepare(" DELETE FROM `" . TABLE_PANEL_TASKS . "` WHERE `type` = '2' AND `data` LIKE :loginname "); Database::pexecute($del_stmt, [ 'loginname' => "%:{$result['loginname']};%" ], true, true); // update admin-resource-usage Admins::decreaseUsage($result['adminid'], 'customers_used'); Admins::decreaseUsage($result['adminid'], 'domains_used', '', (int)($domains_deleted - $result['subdomains_used'])); if ($result['mysqls'] != '-1') { Admins::decreaseUsage($result['adminid'], 'mysqls_used', '', (int)$result['mysqls']); } if ($result['emails'] != '-1') { Admins::decreaseUsage($result['adminid'], 'emails_used', '', (int)$result['emails']); } if ($result['email_accounts'] != '-1') { Admins::decreaseUsage($result['adminid'], 'email_accounts_used', '', (int)$result['email_accounts']); } if ($result['email_forwarders'] != '-1') { Admins::decreaseUsage($result['adminid'], 'email_forwarders_used', '', (int)$result['email_forwarders']); } if ($result['email_quota'] != '-1') { Admins::decreaseUsage($result['adminid'], 'email_quota_used', '', (int)$result['email_quota']); } if ($result['subdomains'] != '-1') { Admins::decreaseUsage($result['adminid'], 'subdomains_used', '', (int)$result['subdomains']); } if ($result['ftps'] != '-1') { Admins::decreaseUsage($result['adminid'], 'ftps_used', '', (int)$result['ftps']); } if (($result['diskspace'] / 1024) != '-1') { Admins::decreaseUsage($result['adminid'], 'diskspace_used', '', (int)$result['diskspace']); } // rebuild configs Cronjob::inserttask(TaskId::REBUILD_VHOST); // Using nameserver, insert a task which rebuilds the server config Cronjob::inserttask(TaskId::REBUILD_DNS); if ($delete_userfiles == 1) { // insert task to remove the customers files from the filesystem Cronjob::inserttask(TaskId::DELETE_CUSTOMER_FILES, $result['loginname']); } // Using filesystem - quota, insert a task which cleans the filesystem - quota Cronjob::inserttask(TaskId::CREATE_QUOTA); $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] deleted customer '" . $result['loginname'] . "'"); return $this->response($result); } throw new Exception("Not allowed to execute given command.", 403); } /** * decrease resource-usage * * @param int $customerid * @param string $resource * @param string $extra * optional, default empty * @param int $decrease_by * optional, default 1 */ public static function decreaseUsage($customerid = 0, $resource = null, $extra = '', $decrease_by = 1) { self::updateResourceUsage(TABLE_PANEL_CUSTOMERS, 'customerid', $customerid, '-', $resource, $extra, $decrease_by); } /** * unlock a locked customer by either id or loginname * * @param int $id * optional, the customer-id * @param string $loginname * optional, the loginname * * @access admin * @return string json-encoded array * @throws Exception */ public function unlock() { if ($this->isAdmin()) { $id = $this->getParam('id', true, 0); $ln_optional = $id > 0; $loginname = $this->getParam('loginname', $ln_optional, ''); $result = $this->apiCall('Customers.get', [ 'id' => $id, 'loginname' => $loginname ]); $id = $result['customerid']; $result_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `loginfail_count` = '0' WHERE `customerid`= :id "); Database::pexecute($result_stmt, [ 'id' => $id ], true, true); // set the new value for result-array $result['loginfail_count'] = 0; $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] unlocked customer '" . $result['loginname'] . "'"); return $this->response($result); } throw new Exception("Not allowed to execute given command.", 403); } /** * Function to move a given customer to a given admin/reseller * and update all its references accordingly * * @param int $id * optional, the customer-id * @param string $loginname * optional, the loginname * @param int $adminid * target-admin-id * * @access admin * @return string json-encoded array * @throws Exception */ public function move() { if ($this->isAdmin() && $this->getUserDetail('change_serversettings') == 1) { $adminid = $this->getParam('adminid'); $id = $this->getParam('id', true, 0); $ln_optional = $id > 0; $loginname = $this->getParam('loginname', $ln_optional, ''); $c_result = $this->apiCall('Customers.get', [ 'id' => $id, 'loginname' => $loginname ]); $id = $c_result['customerid']; // check if target-admin is the current admin if ($adminid == $c_result['adminid']) { throw new Exception("Cannot move customer to the same admin/reseller as he currently is assigned to", 406); } // get target admin $a_result = $this->apiCall('Admins.get', [ 'id' => $adminid ]); // Update customer entry $updCustomer_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `adminid` = :adminid WHERE `customerid` = :cid "); Database::pexecute($updCustomer_stmt, [ 'adminid' => $adminid, 'cid' => $id ], true, true); // Update customer-domains $updDomains_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_DOMAINS . "` SET `adminid` = :adminid WHERE `customerid` = :cid "); Database::pexecute($updDomains_stmt, [ 'adminid' => $adminid, 'cid' => $id ], true, true); // now, recalculate the resource-usage for the old and the new admin User::updateCounters(false); $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] moved user '" . $c_result['loginname'] . "' from admin/reseller '" . $c_result['adminname'] . " to admin/reseller '" . $a_result['loginname'] . "'"); $result = $this->apiCall('Customers.get', [ 'id' => $c_result['customerid'] ]); return $this->response($result); } throw new Exception("Not allowed to execute given command.", 403); } } ================================================ FILE: lib/Froxlor/Api/Commands/DataDump.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Api\Commands; use Exception; use Froxlor\Api\ApiCommand; use Froxlor\Api\ResourceEntity; use Froxlor\Cron\TaskId; use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\FroxlorLogger; use Froxlor\Settings; use Froxlor\System\Cronjob; use Froxlor\UI\Response; use Froxlor\Validate\Validate; use PDO; /** * @since 0.10.0 */ class DataDump extends ApiCommand implements ResourceEntity { /** * add a new data dump job * * @param string $path * path to store the dumped data to * @param string $pgp_public_key * optional pgp public key to encrypt the archive, default is empty * @param bool $dump_dbs * optional whether to include databases, default is 0 (false) * @param bool $dump_mail * optional whether to include mail-data, default is 0 (false) * @param bool $dump_web * optional whether to incoude web-data, default is 0 (false) * @param int $customerid * optional, required when called as admin (if $loginname is not specified) * @param string $loginname * optional, required when called as admin (if $customerid is not specified) * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function add() { $this->validateAccess(); // required parameter $path = $this->getParam('path'); // parameter $pgp_public_key = $this->getParam('pgp_public_key', true, ''); $dump_dbs = $this->getBoolParam('dump_dbs', true, 0); $dump_mail = $this->getBoolParam('dump_mail', true, 0); $dump_web = $this->getBoolParam('dump_web', true, 0); // get customer data $customer = $this->getCustomerData(); // validation $path = FileDir::makeCorrectDir(Validate::validate($path, 'path', '', '', [], true)); $userpath = $path; $path = FileDir::makeCorrectDir($customer['documentroot'] . '/' . $path, $customer['documentroot']); // path cannot be the customers docroot if ($path == FileDir::makeCorrectDir($customer['documentroot'])) { Response::standardError('dumpfoldercannotbedocroot', '', true); } // pgp public key validation if (!empty($pgp_public_key)) { // check if gnupg extension is loaded if (!extension_loaded('gnupg')) { Response::standardError('gnupgextensionnotavailable', '', true); } // check if the pgp public key is a valid key putenv('GNUPGHOME='.sys_get_temp_dir()); if (gnupg_import(gnupg_init(), $pgp_public_key) === false) { Response::standardError('invalidpgppublickey', '', true); } } if ($dump_dbs != '1') { $dump_dbs = '0'; } if ($dump_mail != '1') { $dump_mail = '0'; } if ($dump_web != '1') { $dump_web = '0'; } $task_data = [ 'customerid' => $customer['customerid'], 'uid' => $customer['guid'], 'gid' => $customer['guid'], 'loginname' => $customer['loginname'], 'destdir' => $path, 'pgp_public_key' => $pgp_public_key, 'dump_dbs' => $dump_dbs, 'dump_mail' => $dump_mail, 'dump_web' => $dump_web ]; // schedule export job Cronjob::inserttask(TaskId::CREATE_CUSTOMER_DATADUMP, $task_data); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] added customer data export job for '" . $customer['loginname'] . "'. Target directory: " . $userpath); return $this->response($task_data); } /** * check whether data dump is enabled systemwide and if accessible for customer (hide_options) * * @throws Exception */ private function validateAccess() { if (Settings::Get('system.exportenabled') != 1) { throw new Exception("You cannot access this resource", 405); } if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'extras')) { throw new Exception("You cannot access this resource", 405); } if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'extras.export')) { throw new Exception("You cannot access this resource", 405); } } /** * You cannot get a planned data export. * Try DataDump.listing() */ public function get() { throw new Exception('You cannot get a planned data export. Try DataDump.listing()', 303); } /** * You cannot update a planned data export. * You need to delete it and re-add it. */ public function update() { throw new Exception('You cannot update a planned data export. You need to delete it and re-add it.', 303); } /** * list all planned data export jobs, if called from an admin, list all planned data export jobs of all customers you are * allowed to view, or specify id or loginname for one specific customer * * @param int $customerid * optional, admin-only, select data export jobs of a specific customer by id * @param string $loginname * optional, admin-only, select data export jobs of a specific customer by loginname * @param array $sql_search * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), * LIKE is used if left empty and 'value' => searchvalue * @param int $sql_limit * optional specify number of results to be returned * @param int $sql_offset * optional specify offset for resultset * @param array $sql_orderby * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more * fields * * @access admin, customer * @return string json-encoded array count|list * @throws Exception */ public function listing() { $this->validateAccess(); $customer_ids = $this->getAllowedCustomerIds('extras.export'); // check whether there is a data export job for this customer $query_fields = []; $sel_stmt = Database::prepare("SELECT * FROM `" . TABLE_PANEL_TASKS . "` WHERE `type` = '20'" . $this->getSearchWhere($query_fields, true) . $this->getOrderBy() . $this->getLimit()); Database::pexecute($sel_stmt, $query_fields, true, true); $result = []; while ($entry = $sel_stmt->fetch(PDO::FETCH_ASSOC)) { $entry['data'] = json_decode($entry['data'], true); if (in_array($entry['data']['customerid'], $customer_ids)) { $result[] = $entry; } } $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] list customer data dump jobs"); return $this->response([ 'count' => count($result), 'list' => $result ]); } /** * returns the total number of planned data exports * * @param int $customerid * optional, admin-only, select data export jobs of a specific customer by id * @param string $loginname * optional, admin-only, select data export jobs of a specific customer by loginname * * @access admin, customer * @return string json-encoded response message * @throws Exception */ public function listingCount() { $this->validateAccess(); $customer_ids = $this->getAllowedCustomerIds('extras.export'); // check whether there is a data export job for this customer $result_count = 0; $query_fields = []; $sel_stmt = Database::prepare("SELECT * FROM `" . TABLE_PANEL_TASKS . "` WHERE `type` = '20' " . $this->getSearchWhere($query_fields, true)); Database::pexecute($sel_stmt, $query_fields, true, true); while ($entry = $sel_stmt->fetch(PDO::FETCH_ASSOC)) { $entry['data'] = json_decode($entry['data'], true); if (in_array($entry['data']['customerid'], $customer_ids)) { $result_count++; } } return $this->response($result_count); } /** * delete a planned data export jobs by id, if called from an admin you need to specify the customerid/loginname * * @param int $job_entry * id of data export job * @param int $customerid * optional, required when called as admin (if $loginname is not specified) * @param string $loginname * optional, required when called as admin (if $customerid is not specified) * * @access admin, customer * @return bool * @throws Exception */ public function delete() { // get planned exports $result = $this->apiCall('DataDump.listing', $this->getParamList()); $entry = $this->getParam('job_entry'); $customer_ids = $this->getAllowedCustomerIds('extras.export'); if ($result['count'] > 0 && $entry > 0) { // prepare statement $del_stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_TASKS . "` WHERE `id` = :tid"); // check for the correct job foreach ($result['list'] as $exportjob) { if ($exportjob['id'] == $entry && in_array($exportjob['data']['customerid'], $customer_ids)) { Database::pexecute($del_stmt, [ 'tid' => $entry ], true, true); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] deleted planned customer data export job #" . $entry); return $this->response(true); } } } throw new Exception('Data export job with id #' . $entry . ' could not be found', 404); } } ================================================ FILE: lib/Froxlor/Api/Commands/DirOptions.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Api\Commands; use Exception; use Froxlor\Api\ApiCommand; use Froxlor\Api\ResourceEntity; use Froxlor\Cron\TaskId; use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\FroxlorLogger; use Froxlor\Settings; use Froxlor\System\Cronjob; use Froxlor\UI\Response; use Froxlor\Validate\Validate; use PDO; /** * @since 0.10.0 */ class DirOptions extends ApiCommand implements ResourceEntity { /** * add options for a given directory * * @param int $customerid * optional, required when called as admin (if $loginname is not specified) * @param string $loginname * optional, required when called as admin (if $customerid is not specified) * @param string $path * path relative to the customer's home-Directory * @param bool $options_indexes * optional, activate directory-listing for this path, default 0 (false) * @param bool $options_cgi * optional, allow Perl/CGI execution, default 0 (false) * @param string $error404path * optional, custom 404 error string/file * @param string $error403path * optional, custom 403 error string/file * @param string $error500path * optional, custom 500 error string/file * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function add() { if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'extras')) { throw new Exception("You cannot access this resource", 405); } if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'extras.pathoptions')) { throw new Exception("You cannot access this resource", 405); } // get needed customer info to reduce the email-address-counter by one $customer = $this->getCustomerData(); // required parameters $path = $this->getParam('path'); // parameters $options_indexes = $this->getBoolParam('options_indexes', true, 0); $options_cgi = $this->getBoolParam('options_cgi', true, 0); $error404path = $this->getParam('error404path', true, ''); $error403path = $this->getParam('error403path', true, ''); $error500path = $this->getParam('error500path', true, ''); // validation $path = FileDir::makeCorrectDir(Validate::validate($path, 'path', Validate::REGEX_DIR, '', [], true)); $userpath = $path; $path = FileDir::makeCorrectDir($customer['documentroot'] . '/' . $path, $customer['documentroot']); if (!empty($error404path)) { $error404path = $this->correctErrorDocument($error404path, true); } if (!empty($error403path)) { $error403path = $this->correctErrorDocument($error403path, true); } if (!empty($error500path)) { $error500path = $this->correctErrorDocument($error500path, true); } // check for duplicate path $path_dupe_check_stmt = Database::prepare(" SELECT `id`, `path` FROM `" . TABLE_PANEL_HTACCESS . "` WHERE `path`= :path AND `customerid`= :customerid "); $path_dupe_check = Database::pexecute_first($path_dupe_check_stmt, [ "path" => $path, "customerid" => $customer['customerid'] ], true, true); // duplicate check if ($path_dupe_check && $path_dupe_check['path'] == $path) { Response::standardError('errordocpathdupe', $userpath, true); } // insert the entry $stmt = Database::prepare(' INSERT INTO `' . TABLE_PANEL_HTACCESS . '` SET `customerid` = :customerid, `path` = :path, `options_indexes` = :options_indexes, `error404path` = :error404path, `error403path` = :error403path, `error500path` = :error500path, `options_cgi` = :options_cgi '); $params = [ "customerid" => $customer['customerid'], "path" => $path, "options_indexes" => $options_indexes, "error403path" => $error403path, "error404path" => $error404path, "error500path" => $error500path, "options_cgi" => $options_cgi ]; Database::pexecute($stmt, $params, true, true); $id = Database::lastInsertId(); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] added directory-option for '" . $userpath . "'"); Cronjob::inserttask(TaskId::REBUILD_VHOST); $result = $this->apiCall('DirOptions.get', [ 'id' => $id ]); return $this->response($result); } /** * this functions validates a given value as ErrorDocument * refs #267 * * @param string $errdoc * @param bool $throw_exception * * @return string error-document-string * */ private function correctErrorDocument(string $errdoc, $throw_exception = false) { if (trim($errdoc) != '') { // not a URL if ((strtoupper(substr($errdoc, 0, 5)) != 'HTTP:' && strtoupper(substr($errdoc, 0, 6)) != 'HTTPS:') || !Validate::validateUrl($errdoc)) { // a file if (substr($errdoc, 0, 1) != '"') { $errdoc = FileDir::makeCorrectFile($errdoc); // apache needs a starting-slash (starting at the domains-docroot) if (!substr($errdoc, 0, 1) == '/') { $errdoc = '/' . $errdoc; } } elseif (preg_match('/^"([^\r\n\t\f\0"]+)"$/', $errdoc)) { // a string (check for ending ") } else { Response::standardError('invaliderrordocumentvalue', '', $throw_exception); } } } return trim($errdoc); } /** * return a directory-protection entry by id * * @param int $id * id of dir-protection entry * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function get() { if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'extras')) { throw new Exception("You cannot access this resource", 405); } $id = $this->getParam('id', true, 0); $params = []; if ($this->isAdmin()) { if ($this->getUserDetail('customers_see_all') == false) { // if it's a reseller or an admin who cannot see all customers, we need to check // whether the database belongs to one of his customers $_custom_list_result = $this->apiCall('Customers.listing'); $custom_list_result = $_custom_list_result['list']; $customer_ids = []; foreach ($custom_list_result as $customer) { $customer_ids[] = $customer['customerid']; } $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_HTACCESS . "` WHERE `customerid` IN (" . implode(", ", $customer_ids) . ") AND `id` = :id "); } else { $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_HTACCESS . "` WHERE `id` = :id "); } } else { if (Settings::IsInList('panel.customer_hide_options', 'extras.pathoptions')) { throw new Exception("You cannot access this resource", 405); } $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_HTACCESS . "` WHERE `customerid` = :customerid AND `id` = :id "); $params['customerid'] = $this->getUserDetail('customerid'); } $params['id'] = $id; $result = Database::pexecute_first($result_stmt, $params, true, true); if ($result) { $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] get directory options for '" . $result['path'] . "'"); return $this->response($result); } $key = "id #" . $id; throw new Exception("Directory option with " . $key . " could not be found", 404); } /** * update options for a given directory by id * * @param int $id * id of dir-protection entry * @param int $customerid * optional, required when called as admin (if $loginname is not specified) * @param string $loginname * optional, required when called as admin (if $customerid is not specified) * @param bool $options_indexes * optional, activate directory-listing for this path, default 0 (false) * @param bool $options_cgi * optional, allow Perl/CGI execution, default 0 (false) * @param string $error404path * optional, custom 404 error string/file * @param string $error403path * optional, custom 403 error string/file * @param string $error500path * optional, custom 500 error string/file * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function update() { $id = $this->getParam('id', true, 0); // validation $result = $this->apiCall('DirOptions.get', [ 'id' => $id ]); // get needed customer info to reduce the email-address-counter by one $customer = $this->getCustomerData(); // parameters $options_indexes = $this->getBoolParam('options_indexes', true, $result['options_indexes']); $options_cgi = $this->getBoolParam('options_cgi', true, $result['options_cgi']); $error404path = $this->getParam('error404path', true, $result['error404path']); $error403path = $this->getParam('error403path', true, $result['error403path']); $error500path = $this->getParam('error500path', true, $result['error500path']); if (!empty($error404path)) { $error404path = $this->correctErrorDocument($error404path, true); } if (!empty($error403path)) { $error403path = $this->correctErrorDocument($error403path, true); } if (!empty($error500path)) { $error500path = $this->correctErrorDocument($error500path, true); } if (($options_indexes != $result['options_indexes']) || ($error404path != $result['error404path']) || ($error403path != $result['error403path']) || ($error500path != $result['error500path']) || ($options_cgi != $result['options_cgi'])) { Cronjob::inserttask(TaskId::REBUILD_VHOST); $stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_HTACCESS . "` SET `options_indexes` = :options_indexes, `error404path` = :error404path, `error403path` = :error403path, `error500path` = :error500path, `options_cgi` = :options_cgi WHERE `customerid` = :customerid AND `id` = :id "); $params = [ "customerid" => $customer['customerid'], "options_indexes" => $options_indexes, "error403path" => $error403path, "error404path" => $error404path, "error500path" => $error500path, "options_cgi" => $options_cgi, "id" => $id ]; Database::pexecute($stmt, $params, true, true); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] edited directory options for '" . str_replace($customer['documentroot'], '/', $result['path']) . "'"); } $result = $this->apiCall('DirOptions.get', [ 'id' => $id ]); return $this->response($result); } /** * list all directory-options, if called from an admin, list all directory-options of all customers you are allowed * to view, or specify id or loginname for one specific customer * * @param int $customerid * optional, admin-only, select directory-protections of a specific customer by id * @param string $loginname * optional, admin-only, select directory-protections of a specific customer by loginname * @param array $sql_search * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), * LIKE is used if left empty and 'value' => searchvalue * @param int $sql_limit * optional specify number of results to be returned * @param int $sql_offset * optional specify offset for resultset * @param array $sql_orderby * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more * fields * * @access admin, customer * @return string json-encoded array count|list * @throws Exception */ public function listing() { if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'extras')) { throw new Exception("You cannot access this resource", 405); } $customer_ids = $this->getAllowedCustomerIds('extras.pathoptions'); $result = []; $query_fields = []; $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_HTACCESS . "` WHERE `customerid` IN (" . implode(', ', $customer_ids) . ")" . $this->getSearchWhere($query_fields, true) . $this->getOrderBy() . $this->getLimit()); Database::pexecute($result_stmt, $query_fields, true, true); while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $result[] = $row; } $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] list directory-options"); return $this->response([ 'count' => count($result), 'list' => $result ]); } /** * returns the total number of accessible directory options * * @param int $customerid * optional, admin-only, select directory-protections of a specific customer by id * @param string $loginname * optional, admin-only, select directory-protections of a specific customer by loginname * * @access admin, customer * @return string json-encoded response message * @throws Exception */ public function listingCount() { if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'extras')) { throw new Exception("You cannot access this resource", 405); } $customer_ids = $this->getAllowedCustomerIds('extras.pathoptions'); $result = []; $query_fields = []; $result_stmt = Database::prepare(" SELECT COUNT(*) as num_htaccess FROM `" . TABLE_PANEL_HTACCESS . "` WHERE `customerid` IN (" . implode(', ', $customer_ids) . ") " . $this->getSearchWhere($query_fields, true)); $result = Database::pexecute_first($result_stmt, $query_fields, true, true); if ($result) { return $this->response($result['num_htaccess']); } return $this->response(0); } /** * delete a directory-options by id * * @param int $id * id of dir-protection entry * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function delete() { if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'extras')) { throw new Exception("You cannot access this resource", 405); } $id = $this->getParam('id'); if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'extras.pathoptions')) { throw new Exception("You cannot access this resource", 405); } // get directory-option $result = $this->apiCall('DirOptions.get', [ 'id' => $id ]); if ($this->isAdmin()) { // get customer-data $customer_data = $this->apiCall('Customers.get', [ 'id' => $result['customerid'] ]); } else { $customer_data = $this->getUserData(); } // do we have to remove the symlink and folder in suexecpath? if ((int)Settings::Get('perl.suexecworkaround') == 1) { $loginname = $customer_data['loginname']; $suexecpath = FileDir::makeCorrectDir(Settings::Get('perl.suexecpath') . '/' . $loginname . '/' . md5($result['path']) . '/'); $perlsymlink = FileDir::makeCorrectFile($result['path'] . '/cgi-bin'); // remove symlink if (file_exists($perlsymlink)) { FileDir::safe_exec('rm -f ' . escapeshellarg($perlsymlink)); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_DEBUG, "[API] deleted suexecworkaround symlink '" . $perlsymlink . "'"); } // remove folder in suexec-path if (file_exists($suexecpath)) { FileDir::safe_exec('rm -rf ' . escapeshellarg($suexecpath)); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_DEBUG, "[API] deleted suexecworkaround path '" . $suexecpath . "'"); } } $stmt = Database::prepare(" DELETE FROM `" . TABLE_PANEL_HTACCESS . "` WHERE `customerid`= :customerid AND `id`= :id "); Database::pexecute($stmt, [ "customerid" => $customer_data['customerid'], "id" => $id ], true, true); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] deleted directory-option for '" . str_replace($customer_data['documentroot'], '/', $result['path']) . "'"); Cronjob::inserttask(TaskId::REBUILD_VHOST); return $this->response($result); } } ================================================ FILE: lib/Froxlor/Api/Commands/DirProtections.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Api\Commands; use Exception; use Froxlor\Api\ApiCommand; use Froxlor\Api\ResourceEntity; use Froxlor\Cron\TaskId; use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\FroxlorLogger; use Froxlor\Settings; use Froxlor\System\Cronjob; use Froxlor\System\Crypt; use Froxlor\UI\Response; use Froxlor\Validate\Validate; use PDO; /** * @since 0.10.0 */ class DirProtections extends ApiCommand implements ResourceEntity { /** * add htaccess protection to a given directory * * @param int $customerid * optional, required when called as admin (if $loginname is not specified) * @param string $loginname * optional, required when called as admin (if $customerid is not specified) * @param string $path * @param string $username * @param string $directory_password * @param string $directory_authname * optional name/description for the protection * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function add() { if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'extras')) { throw new Exception("You cannot access this resource", 405); } if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'extras.directoryprotection')) { throw new Exception("You cannot access this resource", 405); } // get needed customer info to reduce the email-address-counter by one $customer = $this->getCustomerData(); // required parameters $path = $this->getParam('path'); $username = $this->getParam('username'); $password = $this->getParam('directory_password'); // parameters $authname = $this->getParam('directory_authname', true, ''); // validation $path = FileDir::makeCorrectDir(Validate::validate($path, 'path', Validate::REGEX_DIR, '', [], true)); $path = FileDir::makeCorrectDir($customer['documentroot'] . '/' . $path, $customer['documentroot']); $username = Validate::validate($username, 'username', '/^[a-zA-Z0-9][a-zA-Z0-9\-_]+\$?$/', '', [], true); $authname = Validate::validate($authname, 'directory_authname', '/^[a-zA-Z0-9][a-zA-Z0-9\-_ ]+\$?$/', '', [], true); $password = Validate::validate($password, 'password', '', '', [], true); $password = Crypt::validatePassword($password, true); // check for duplicate usernames for the path $username_path_check_stmt = Database::prepare(" SELECT `id`, `username`, `path` FROM `" . TABLE_PANEL_HTPASSWDS . "` WHERE `username`= :username AND `path`= :path AND `customerid`= :customerid "); $params = [ "username" => $username, "path" => $path, "customerid" => $customer['customerid'] ]; $username_path_check = Database::pexecute_first($username_path_check_stmt, $params, true, true); $password_enc = Crypt::makeCryptPassword($password, true); // duplicate check if ($username_path_check && $username_path_check['username'] == $username && $username_path_check['path'] == $path) { Response::standardError('userpathcombinationdupe', '', true); } elseif ($password == $username) { Response::standardError('passwordshouldnotbeusername', '', true); } // insert the entry $stmt = Database::prepare(" INSERT INTO `" . TABLE_PANEL_HTPASSWDS . "` SET `customerid` = :customerid, `username` = :username, `password` = :password, `path` = :path, `authname` = :authname "); $params = [ "customerid" => $customer['customerid'], "username" => $username, "password" => $password_enc, "path" => $path, "authname" => $authname ]; Database::pexecute($stmt, $params, true, true); $id = Database::lastInsertId(); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] added directory-protection for '" . $username . " (" . $path . ")'"); Cronjob::inserttask(TaskId::REBUILD_VHOST); $result = $this->apiCall('DirProtections.get', [ 'id' => $id ]); return $this->response($result); } /** * return a directory-protection entry by either id or username * * @param int $id * optional, the directory-protection-id * @param string $username * optional, the username * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function get() { if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'extras')) { throw new Exception("You cannot access this resource", 405); } $id = $this->getParam('id', true, 0); $un_optional = $id > 0; $username = $this->getParam('username', $un_optional, ''); $params = []; if ($this->isAdmin()) { if ($this->getUserDetail('customers_see_all') == false) { // if it's a reseller or an admin who cannot see all customers, we need to check // whether the database belongs to one of his customers $_custom_list_result = $this->apiCall('Customers.listing'); $custom_list_result = $_custom_list_result['list']; $customer_ids = []; foreach ($custom_list_result as $customer) { $customer_ids[] = $customer['customerid']; } $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_HTPASSWDS . "` WHERE `customerid` IN (" . implode(", ", $customer_ids) . ") AND (`id` = :idun OR `username` = :idun) "); } else { $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_HTPASSWDS . "` WHERE (`id` = :idun OR `username` = :idun) "); } } else { if (Settings::IsInList('panel.customer_hide_options', 'extras.directoryprotection')) { throw new Exception("You cannot access this resource", 405); } $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_HTPASSWDS . "` WHERE `customerid` = :customerid AND (`id` = :idun OR `username` = :idun) "); $params['customerid'] = $this->getUserDetail('customerid'); } $params['idun'] = ($id <= 0 ? $username : $id); $result = Database::pexecute_first($result_stmt, $params, true, true); if ($result) { $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] get directory protection for '" . $result['path'] . "'"); return $this->response($result); } $key = ($id > 0 ? "id #" . $id : "username '" . $username . "'"); throw new Exception("Directory protection with " . $key . " could not be found", 404); } /** * update htaccess protection of a given directory * * @param int $id * optional the directory-protection-id * @param string $username * optional, the username * @param int $customerid * optional, required when called as admin (if $loginname is not specified) * @param string $loginname * optional, required when called as admin (if $customerid is not specified) * @param string $directory_password * optional, leave empty for no change * @param string $directory_authname * optional name/description for the protection * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function update() { $id = $this->getParam('id', true, 0); $un_optional = $id > 0; $username = $this->getParam('username', $un_optional, ''); // validation $result = $this->apiCall('DirProtections.get', [ 'id' => $id, 'username' => $username ]); $id = $result['id']; // parameters $password = $this->getParam('directory_password', true, ''); $authname = $this->getParam('directory_authname', true, $result['authname']); // get needed customer info $customer = $this->getCustomerData(); // validation $authname = Validate::validate($authname, 'directory_authname', '/^[a-zA-Z0-9][a-zA-Z0-9\-_ ]+\$?$/', '', [], true); $password = Validate::validate($password, 'password', '', '', [], true); $password = Crypt::validatePassword($password, true); $upd_query = ""; $upd_params = [ "id" => $result['id'], "cid" => $customer['customerid'] ]; if (!empty($password)) { if ($password == $result['username']) { Response::standardError('passwordshouldnotbeusername', '', true); } $password_enc = Crypt::makeCryptPassword($password, true); $upd_query .= "`password`= :password_enc"; $upd_params['password_enc'] = $password_enc; } if ($authname != $result['authname']) { if (!empty($upd_query)) { $upd_query .= ", "; } $upd_query .= "`authname` = :authname"; $upd_params['authname'] = $authname; } // build update query if (!empty($upd_query)) { $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_HTPASSWDS . "` SET " . $upd_query . " WHERE `id` = :id AND `customerid`= :cid "); Database::pexecute($upd_stmt, $upd_params, true, true); Cronjob::inserttask(TaskId::REBUILD_VHOST); } $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] updated directory-protection '" . $result['username'] . " (" . $result['path'] . ")'"); $result = $this->apiCall('DirProtections.get', [ 'id' => $result['id'] ]); return $this->response($result); } /** * list all directory-protections, if called from an admin, list all directory-protections of all customers you are * allowed to view, or specify id or loginname for one specific customer * * @param int $customerid * optional, admin-only, select directory-protections of a specific customer by id * @param string $loginname * optional, admin-only, select directory-protections of a specific customer by loginname * @param array $sql_search * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), * LIKE is used if left empty and 'value' => searchvalue * @param int $sql_limit * optional specify number of results to be returned * @param int $sql_offset * optional specify offset for resultset * @param array $sql_orderby * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more * fields * * @access admin, customer * @return string json-encoded array count|list * @throws Exception */ public function listing() { if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'extras')) { throw new Exception("You cannot access this resource", 405); } $customer_ids = $this->getAllowedCustomerIds('extras.directoryprotection'); $result = []; $query_fields = []; $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_HTPASSWDS . "` WHERE `customerid` IN (" . implode(', ', $customer_ids) . ")" . $this->getSearchWhere($query_fields, true) . $this->getOrderBy() . $this->getLimit()); Database::pexecute($result_stmt, $query_fields, true, true); while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $result[] = $row; } $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] list directory-protections"); return $this->response([ 'count' => count($result), 'list' => $result ]); } /** * returns the total number of accessible directory protections * * @param int $customerid * optional, admin-only, select directory-protections of a specific customer by id * @param string $loginname * optional, admin-only, select directory-protections of a specific customer by loginname * * @access admin, customer * @return string json-encoded response message * @throws Exception */ public function listingCount() { if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'extras')) { throw new Exception("You cannot access this resource", 405); } $customer_ids = $this->getAllowedCustomerIds('extras.directoryprotection'); $result = []; $query_fields = []; $result_stmt = Database::prepare(" SELECT COUNT(*) as num_htpasswd FROM `" . TABLE_PANEL_HTPASSWDS . "` WHERE `customerid` IN (" . implode(', ', $customer_ids) . ") " . $this->getSearchWhere($query_fields, true)); $result = Database::pexecute_first($result_stmt, $query_fields, true, true); if ($result) { return $this->response($result['num_htpasswd']); } return $this->response(0); } /** * delete a directory-protection by either id or username * * @param int $id * optional, the directory-protection-id * @param string $username * optional, the username * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function delete() { if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'extras')) { throw new Exception("You cannot access this resource", 405); } $id = $this->getParam('id', true, 0); $un_optional = $id > 0; $username = $this->getParam('username', $un_optional, ''); if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'extras.directoryprotection')) { throw new Exception("You cannot access this resource", 405); } // get directory protection $result = $this->apiCall('DirProtections.get', [ 'id' => $id, 'username' => $username ]); $id = $result['id']; if ($this->isAdmin()) { // get customer-data $customer_data = $this->apiCall('Customers.get', [ 'id' => $result['customerid'] ]); } else { $customer_data = $this->getUserData(); } $stmt = Database::prepare(" DELETE FROM `" . TABLE_PANEL_HTPASSWDS . "` WHERE `customerid`= :customerid AND `id`= :id "); Database::pexecute($stmt, [ "customerid" => $customer_data['customerid'], "id" => $id ]); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_WARNING, "[API] deleted htpasswd for '" . $result['username'] . " (" . $result['path'] . ")'"); Cronjob::inserttask(TaskId::REBUILD_VHOST); return $this->response($result); } } ================================================ FILE: lib/Froxlor/Api/Commands/DomainZones.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Api\Commands; use Exception; use Froxlor\Api\ApiCommand; use Froxlor\Api\ResourceEntity; use Froxlor\Cron\TaskId; use Froxlor\Database\Database; use Froxlor\Dns\Dns; use Froxlor\FroxlorLogger; use Froxlor\Idna\IdnaWrapper; use Froxlor\Settings; use Froxlor\System\Cronjob; use Froxlor\UI\Response; use Froxlor\Validate\Validate; use PDO; /** * @since 0.10.0 */ class DomainZones extends ApiCommand implements ResourceEntity { /** * add a new dns zone for a given domain by id or domainname * * @param int $id * optional domain id * @param string $domainname * optional domain name * @param string $record * optional, default empty * @param string $type * optional, zone-entry type (A, AAAA, TXT, etc.), default 'A' * @param int $prio * optional, priority, default empty * @param string $content * optional, default empty * @param int $ttl * optional, default 18000 * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function add() { if (Settings::Get('system.dnsenabled') != '1') { throw new Exception("DNS service not enabled on this system", 405); } if ($this->isAdmin() == false && $this->getUserDetail('dnsenabled') != '1') { throw new Exception("You cannot access this resource", 405); } $id = $this->getParam('id', true, 0); $dn_optional = $id > 0; $domainname = $this->getParam('domainname', $dn_optional, ''); // get requested domain $result = $this->apiCall('SubDomains.get', [ 'id' => $id, 'domainname' => $domainname ]); $id = $result['id']; // parameters $record = $this->getParam('record', true, null); $type = $this->getParam('type', true, 'A'); $prio = $this->getParam('prio', true, null); $content = $this->getParam('content', true, null); $ttl = $this->getParam('ttl', true, 18000); if ($result['parentdomainid'] != '0') { throw new Exception("DNS zones can only be generated for the main domain, not for subdomains", 406); } if ($result['subisbinddomain'] != '1') { Response::standardError('dns_domain_nodns', '', true); } $idna_convert = new IdnaWrapper(); $domain = $idna_convert->encode($result['domain']); // select all entries $sel_stmt = Database::prepare("SELECT * FROM `" . TABLE_DOMAIN_DNS . "` WHERE domain_id = :did"); Database::pexecute($sel_stmt, [ 'did' => $id ], true, true); $dom_entries = $sel_stmt->fetchAll(PDO::FETCH_ASSOC); // validation $errors = []; if (empty(trim($record))) { $record = "@"; } $record = trim(strtolower($record)); if ($record != '@' && $record != '*') { // validate record if (strpos($record, '--') !== false) { $errors[] = lng('error.domain_nopunycode'); } else { // check for wildcard-record $add_wildcard_again = false; if (substr($record, 0, 2) == '*.') { $record = substr($record, 2); $add_wildcard_again = true; } // convert entry $record = $idna_convert->encode($record); if ($add_wildcard_again) { $record = '*.' . $record; } if (strlen($record) > 63) { $errors[] = lng('error.dns_record_toolong'); } } } if ($ttl <= 0) { $ttl = 18000; } $content = trim($content); if (empty($content)) { $errors[] = lng('error.dns_content_empty'); } // remove invalid control characters (allow tab + printable ASCII) $content = preg_replace('/[^\x09\x20-\x7E]/', '', $content); // collapse excessive whitespace $content = preg_replace('/\s+/', ' ', $content); if ($type != 'CNAME') { // check whether there is a CNAME-record for the same resource foreach ($dom_entries as $existing_entries) { if ($existing_entries['type'] == 'CNAME' && $existing_entries['record'] == $record) { $errors[] = lng('error.dns_other_nomorerr'); break; } } } // types if ($type == 'A' && filter_var($content, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === false) { $errors[] = lng('error.dns_arec_noipv4'); } elseif ($type == 'AAAA' && filter_var($content, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) === false) { $errors[] = lng('error.dns_aaaarec_noipv6'); } elseif ($type == 'CAA' && !empty($content)) { $re = '/(?\'critical\'\d+)\h*(?\'type\'iodef|issue|issuewild)\h*(?\'value\'(?\'issuevalue\'"(?\'domain\'(?=.{3,128}$)(?>(?>[a-zA-Z0-9]+[a-zA-Z0-9-]*[a-zA-Z0-9]+|[a-zA-Z0-9]+)\.)*(?>[a-zA-Z]{2,}|[a-zA-Z0-9]{2,}\.[a-zA-Z]{2,}))[;\h]*(?\'parameters\'(?>[a-zA-Z0-9]{1,60}=[a-zA-Z0-9:\.\/\-]{1,60}\h*)+)?")|(?\'iodefvalue\'"(?\'url\'(mailto:.*|http:\/\/.*|https:\/\/.*))"))/'; preg_match($re, $content, $matches); if (empty($matches)) { $errors[] = lng('error.dns_content_invalid'); } elseif (($matches['type'] == 'issue' || $matches['type'] == 'issuewild') && !Validate::validateDomain($matches['domain'])) { $errors[] = lng('error.dns_content_invalid'); } elseif ($matches['type'] == 'iodef' && !Validate::validateUrl($matches['url'])) { $errors[] = lng('error.dns_content_invalid'); } else { $content = $matches[0]; } } elseif ($type == 'CNAME' || $type == 'DNAME') { // check for trailing dot if (substr($content, -1) == '.') { // remove it for checks $content = substr($content, 0, -1); } else { // add domain name $content .= '.' . $domain; } if (!Validate::validateDomain($content, true)) { $errors[] = lng('error.dns_cname_invaliddom'); } else { // check whether there are RR-records for the same resource foreach ($dom_entries as $existing_entries) { if ($existing_entries['record'] == $record) { $errors[] = lng('error.dns_cname_nomorerr'); break; } } // check www-alias setting if ($result['wwwserveralias'] == '1' && $result['iswildcarddomain'] == '0' && $record == 'www') { $errors[] = lng('error.no_wwwcnamae_ifwwwalias'); } } // append trailing dot (again) $content .= '.'; } elseif ($type == 'LOC' && !empty($content)) { if (!Validate::validateDnsLoc($content)) { $errors[] = lng('error.dns_loc_invalid'); } } elseif ($type == 'MX') { if ($prio === null || $prio < 0) { $errors[] = lng('error.dns_mx_prioempty'); } // check for trailing dot if (substr($content, -1) == '.') { // remove it for checks $content = substr($content, 0, -1); } if (!empty($content) && !Validate::validateDomain($content)) { $errors[] = lng('error.dns_mx_needdom'); } else { // check whether there is a CNAME-record for the same resource foreach ($dom_entries as $existing_entries) { $fqdn = $existing_entries['record'] . '.' . $domain; if ($existing_entries['type'] == 'CNAME' && $fqdn == $content) { $errors[] = lng('error.dns_mx_noalias'); break; } } } // append trailing dot (again) $content .= '.'; // if content is only ".", the prio needs to be 0 which results in a "null mx" entry if ($content == '.' && $prio != 0) { $prio = 0; } } elseif ($type == 'NAPTR' && !empty($content)) { if (!Validate::validateDnsNaptr($content)) { $errors[] = lng('error.dns_naptr_invalid'); } } elseif ($type == 'NS') { // check for trailing dot if (substr($content, -1) == '.') { // remove it for checks $content = substr($content, 0, -1); } if (!Validate::validateDomain($content)) { $errors[] = lng('error.dns_ns_invaliddom'); } // append trailing dot (again) $content .= '.'; } elseif ($type == 'RP' && !empty($content)) { if (!Validate::validateDnsRp($content)) { $errors[] = lng('error.dns_rp_invalid'); } } elseif ($type == 'SRV') { if ($prio === null || $prio < 0) { $errors[] = lng('error.dns_srv_prioempty'); } // check only last part of content, as it can look like: // _service._proto.name. TTL class SRV priority weight port target. $_split_content = explode(" ", $content); // SRV content must be [weight] [port] [target] if (count($_split_content) != 3) { $errors[] = lng('error.dns_srv_invalidcontent'); } $target = trim($_split_content[count($_split_content) - 1]); if ($target != '.') { // check for trailing dot if (substr($target, -1) == '.') { // remove it for checks $target = substr($target, 0, -1); } } if ($target != '.' && !Validate::validateDomain($target, true)) { $errors[] = lng('error.dns_srv_needdom'); } else { // check whether there is a CNAME-record for the same resource foreach ($dom_entries as $existing_entries) { $fqdn = $existing_entries['record'] . '.' . $domain; if ($existing_entries['type'] == 'CNAME' && $fqdn == $target) { $errors[] = lng('error.dns_srv_noalias'); break; } } } // append trailing dot if there's none if (substr($content, -1) != '.') { $content .= '.'; } } elseif ($type == 'SSHFP' && !empty($content)) { if (!Validate::validateDnsSshfp($content)) { $errors[] = lng('error.dns_sshfp_invalid'); } } elseif ($type == 'TLSA' && !empty($content)) { if (!Validate::validateDnsTlsa($content)) { $errors[] = lng('error.dns_tlsa_invalid'); } } elseif ($type == 'TXT' && !empty($content)) { // check that TXT content is enclosed in " " $content = Dns::encloseTXTContent($content); } $new_entry = [ 'record' => $record, 'type' => $type, 'prio' => (int)$prio, 'content' => $content, 'ttl' => (int)$ttl, 'domain_id' => (int)$id ]; ksort($new_entry); // check for duplicate foreach ($dom_entries as $existing_entry) { // compare json-encoded string of array $check_entry = $existing_entry; // new entry has no ID yet unset($check_entry['id']); // sort by key ksort($check_entry); // format integer fields to real integer (as they are read as string from the DB) $check_entry['prio'] = (int)$check_entry['prio']; $check_entry['ttl'] = (int)$check_entry['ttl']; $check_entry['domain_id'] = (int)$check_entry['domain_id']; // encode both $check_entry = json_encode($check_entry); $new = json_encode($new_entry); // compare if ($check_entry === $new) { $errors[] = lng('error.dns_duplicate_entry'); unset($check_entry); break; } } if (empty($errors)) { $ins_stmt = Database::prepare(" INSERT INTO `" . TABLE_DOMAIN_DNS . "` SET `record` = :record, `type` = :type, `prio` = :prio, `content` = :content, `ttl` = :ttl, `domain_id` = :domain_id "); Database::pexecute($ins_stmt, $new_entry, true, true); $new_entry_id = Database::lastInsertId(); // add temporary to the entries-array (no reread of DB necessary) $new_entry['id'] = $new_entry_id; $dom_entries[] = $new_entry; // re-generate bind configs Cronjob::inserttask(TaskId::REBUILD_DNS); $result = $this->apiCall('DomainZones.get', [ 'id' => $id ]); return $this->response($result); } // return $errors throw new Exception(implode("\n", $errors), 406); } /** * return a domain-dns entry by either id or domainname * * @param int $id * optional, the domain id * @param string $domainname * optional, the domain name * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function get() { if (Settings::Get('system.dnsenabled') != '1') { throw new Exception("DNS service not enabled on this system", 405); } if ($this->isAdmin() == false && $this->getUserDetail('dnsenabled') != '1') { throw new Exception("You cannot access this resource", 405); } $id = $this->getParam('id', true, 0); $dn_optional = $id > 0; $domainname = $this->getParam('domainname', $dn_optional, ''); // get requested domain $result = $this->apiCall('SubDomains.get', [ 'id' => $id, 'domainname' => $domainname ]); $id = $result['id']; if ($result['parentdomainid'] != '0') { throw new Exception("DNS zones can only be generated for the main domain, not for subdomains", 406); } if ($result['subisbinddomain'] != '1') { Response::standardError('dns_domain_nodns', '', true); } $zone = Dns::createDomainZone($id); $zonefile = (string)$zone; $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] get dns-zone for '" . $result['domain'] . "'"); return $this->response(explode("\n", $zonefile)); } /** * You cannot update a dns zone entry. * You need to delete it and re-add it. */ public function update() { throw new Exception('You cannot update a dns zone entry. You need to delete it and re-add it.', 303); } /** * List all entry records of a given domain by either id or domainname * * @param int $id * optional, the domain id * @param string $domainname * optional, the domain name * @param array $sql_search * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), * LIKE is used if left empty and 'value' => searchvalue * @param int $sql_limit * optional specify number of results to be returned * @param int $sql_offset * optional specify offset for resultset * @param array $sql_orderby * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more * fields * * @access admin, customer * @return bool * @throws Exception */ public function listing() { if (Settings::Get('system.dnsenabled') != '1') { throw new Exception("DNS service not enabled on this system", 405); } if ($this->isAdmin() == false && $this->getUserDetail('dnsenabled') != '1') { throw new Exception("You cannot access this resource", 405); } $id = $this->getParam('id', true, 0); $dn_optional = $id > 0; $domainname = $this->getParam('domainname', $dn_optional, ''); // get requested domain $result = $this->apiCall('SubDomains.get', [ 'id' => $id, 'domainname' => $domainname ]); $id = $result['id']; $query_fields = []; $sel_stmt = Database::prepare("SELECT * FROM `" . TABLE_DOMAIN_DNS . "` WHERE `domain_id` = :did" . $this->getSearchWhere($query_fields, true) . $this->getOrderBy() . $this->getLimit()); $query_fields['did'] = $id; Database::pexecute($sel_stmt, $query_fields, true, true); $result = []; while ($row = $sel_stmt->fetch(PDO::FETCH_ASSOC)) { $result[] = $row; } return $this->response([ 'count' => count($result), 'list' => $result ]); } /** * returns the total number of domainzone-entries for given domain * * @param int $id * optional, the domain id * @param string $domainname * optional, the domain name * * @access admin, customer * @return string json-encoded response message * @throws Exception */ public function listingCount() { if (Settings::Get('system.dnsenabled') != '1') { throw new Exception("DNS service not enabled on this system", 405); } if ($this->isAdmin() == false && $this->getUserDetail('dnsenabled') != '1') { throw new Exception("You cannot access this resource", 405); } $id = $this->getParam('id', true, 0); $dn_optional = $id > 0; $domainname = $this->getParam('domainname', $dn_optional, ''); // get requested domain $result = $this->apiCall('SubDomains.get', [ 'id' => $id, 'domainname' => $domainname ]); $id = $result['id']; $query_fields = []; $sel_stmt = Database::prepare("SELECT COUNT(*) as num_dns FROM `" . TABLE_DOMAIN_DNS . "` WHERE `domain_id` = :did" . $this->getSearchWhere($query_fields, true)); $params = array_merge(['did' => $id], $query_fields); $result = Database::pexecute_first($sel_stmt, $params, true, true); if ($result) { return $this->response($result['num_dns']); } return $this->response(0); } /** * deletes a domain-dns entry by id * * @param int $entry_id * @param int $id * optional, the domain id * @param string $domainname * optional, the domain name * * @access admin, customer * @return bool * @throws Exception */ public function delete() { if (Settings::Get('system.dnsenabled') != '1') { throw new Exception("DNS service not enabled on this system", 405); } if ($this->isAdmin() == false && $this->getUserDetail('dnsenabled') != '1') { throw new Exception("You cannot access this resource", 405); } $entry_id = $this->getParam('entry_id'); $id = $this->getParam('id', true, 0); $dn_optional = $id > 0; $domainname = $this->getParam('domainname', $dn_optional, ''); // get requested domain $result = $this->apiCall('SubDomains.get', [ 'id' => $id, 'domainname' => $domainname ]); $id = $result['id']; $del_stmt = Database::prepare("DELETE FROM `" . TABLE_DOMAIN_DNS . "` WHERE `id` = :id AND `domain_id` = :did"); Database::pexecute($del_stmt, [ 'id' => $entry_id, 'did' => $id ], true, true); if ($del_stmt->rowCount() > 0) { // re-generate bind configs Cronjob::inserttask(TaskId::REBUILD_DNS); return $this->response(true); } return $this->response(true, 304); } } ================================================ FILE: lib/Froxlor/Api/Commands/Domains.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Api\Commands; use Exception; use Froxlor\Api\ApiCommand; use Froxlor\Api\ResourceEntity; use Froxlor\Cron\TaskId; use Froxlor\Database\Database; use Froxlor\Domain\Domain; use Froxlor\FileDir; use Froxlor\FroxlorLogger; use Froxlor\Idna\IdnaWrapper; use Froxlor\PhpHelper; use Froxlor\Settings; use Froxlor\System\Cronjob; use Froxlor\UI\Response; use Froxlor\User; use Froxlor\Validate\Validate; use PDO; /** * @since 0.10.0 */ class Domains extends ApiCommand implements ResourceEntity { /** * lists all domain entries * * @param bool $with_ips * optional, default true * @param array $sql_search * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), * LIKE is used if left empty and 'value' => searchvalue * @param int $sql_limit * optional specify number of results to be returned * @param int $sql_offset * optional specify offset for resultset * @param array $sql_orderby * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more * fields * * @access admin * @return string json-encoded array count|list * @throws Exception */ public function listing() { if ($this->isAdmin()) { $with_ips = $this->getParam('with_ips', true, true); $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] list domains"); $query_fields = []; $result_stmt = Database::prepare(" SELECT `d`.*, `c`.`loginname`, `c`.`deactivated` as `customer_deactivated`, `c`.`name`, `c`.`firstname`, `c`.`company`, `c`.`standardsubdomain`, `c`.`adminid` as customeradmin, `ad`.`id` AS `aliasdomainid`, `ad`.`domain` AS `aliasdomain` FROM `" . TABLE_PANEL_DOMAINS . "` `d` LEFT JOIN `" . TABLE_PANEL_CUSTOMERS . "` `c` USING(`customerid`) LEFT JOIN `" . TABLE_PANEL_DOMAINS . "` `ad` ON `d`.`aliasdomain`=`ad`.`id` WHERE `d`.`parentdomainid`='0' " . ($this->getUserDetail('customers_see_all') ? '' : " AND `d`.`adminid` = :adminid ") . $this->getSearchWhere($query_fields, true) . $this->getOrderBy() . $this->getLimit()); $params = []; if ($this->getUserDetail('customers_see_all') == '0') { $params['adminid'] = $this->getUserDetail('adminid'); } $params = array_merge($params, $query_fields); Database::pexecute($result_stmt, $params, true, true); $result = []; while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $row['ipsandports'] = []; if ($with_ips) { $row['ipsandports'] = $this->getIpsForDomain($row['id']); } $row['domain_hascert'] = $this->getHasCertValueForDomain((int)$row['id'], (int)$row['parentdomainid']); $result[] = $row; } return $this->response([ 'count' => count($result), 'list' => $result ]); } throw new Exception("Not allowed to execute given command.", 403); } /** * get ips connected to given domain as array * * @param number $domain_id * @param bool $ssl_only * optional, return only ssl enabled ips, default false * @return array */ private function getIpsForDomain($domain_id = 0, $ssl_only = false) { $resultips_stmt = Database::prepare(" SELECT `ips`.* FROM `" . TABLE_DOMAINTOIP . "` AS `dti`, `" . TABLE_PANEL_IPSANDPORTS . "` AS `ips` WHERE `dti`.`id_ipandports` = `ips`.`id` AND `dti`.`id_domain` = :domainid " . ($ssl_only ? " AND `ips`.`ssl` = '1'" : "")); Database::pexecute($resultips_stmt, [ 'domainid' => $domain_id ]); $ipandports = []; while ($rowip = $resultips_stmt->fetch(PDO::FETCH_ASSOC)) { if (filter_var($rowip['ip'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $rowip['is_ipv6'] = true; } $ipandports[] = $rowip; } return $ipandports; } private function getHasCertValueForDomain(int $domainid, int $parentdomainid): int { // nothing (ssl_global) $domain_hascert = 0; $ssl_stmt = Database::prepare("SELECT * FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` WHERE `domainid` = :domainid"); Database::pexecute($ssl_stmt, array( "domainid" => $domainid )); $ssl_result = $ssl_stmt->fetch(PDO::FETCH_ASSOC); if (is_array($ssl_result) && isset($ssl_result['ssl_cert_file']) && $ssl_result['ssl_cert_file'] != '') { // own certificate (ssl_customer_green) $domain_hascert = 1; } else { // check if it's parent has one set (shared) if ($parentdomainid != 0) { $ssl_stmt = Database::prepare("SELECT * FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` WHERE `domainid` = :domainid"); Database::pexecute($ssl_stmt, array( "domainid" => $parentdomainid )); $ssl_result = $ssl_stmt->fetch(PDO::FETCH_ASSOC); if (is_array($ssl_result) && isset($ssl_result['ssl_cert_file']) && $ssl_result['ssl_cert_file'] != '') { // parent has a certificate (ssl_shared) $domain_hascert = 2; } } } return $domain_hascert; } /** * returns the total number of accessible domains * * @access admin * @return string json-encoded array count|list * @throws Exception */ public function listingCount() { if ($this->isAdmin()) { $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] list domains"); $query_fields = []; $result_stmt = Database::prepare(" SELECT COUNT(*) as num_domains FROM `" . TABLE_PANEL_DOMAINS . "` `d` LEFT JOIN `" . TABLE_PANEL_CUSTOMERS . "` `c` USING(`customerid`) LEFT JOIN `" . TABLE_PANEL_DOMAINS . "` `ad` ON `d`.`aliasdomain`=`ad`.`id` WHERE `d`.`parentdomainid`='0' " . ($this->getUserDetail('customers_see_all') ? '' : " AND `d`.`adminid` = :adminid ") . $this->getSearchWhere($query_fields, true)); $params = []; if ($this->getUserDetail('customers_see_all') == '0') { $params['adminid'] = $this->getUserDetail('adminid'); } $params = array_merge($params, $query_fields); $result = Database::pexecute_first($result_stmt, $params, true, true); if ($result) { return $this->response($result['num_domains']); } } throw new Exception("Not allowed to execute given command.", 403); } /** * add new domain entry * * @param string $domain * domain-name * @param int $customerid * optional, required when called as admin (if $loginname is not specified) * @param string $loginname * optional, required when called as admin (if $customerid is not specified) * @param int $adminid * optional, default is the calling admin's ID * @param array $ipandport * optional list of ip/ports to assign to domain, default is system-default-ips * @param int $subcanemaildomain * optional, allow subdomains of this domain as email domains, 1 = choosable (default no), 2 = choosable * (default yes), 3 = always, default 0 (never) * @param bool $isemaildomain * optional, allow email usage with this domain, default 0 (false) * @param bool $email_only * optional, restrict domain to email usage, default 0 (false) * @param int $selectserveralias * optional, 0 = wildcard, 1 = www-alias, 2 = none, default [system.domaindefaultalias] * @param bool $speciallogfile * optional, whether to create an exclusive web-logfile for this domain, default 0 (false) * @param int $alias * optional, domain-id of a domain that the new domain should be an alias of, default 0 (none) * @param string $registration_date * optional, date of domain registration in form of YYYY-MM-DD, default empty (none) * @param string $termination_date * optional, date of domain termination in form of YYYY-MM-DD, default empty (none) * @param bool $caneditdomain * optional, whether to allow the customer to edit domain settings, default 0 (false) * @param bool $isbinddomain * optional, whether to generate a dns-zone or not (only of nameserver is activated), default 0 (false) * @param string $zonefile * optional, custom dns zone filename (only of nameserver is activated), default empty (auto-generated) * @param bool $dkim * optional, whether this domain should use dkim if antispam is activated, default 0 (false) * @param string $specialsettings * optional, custom webserver vhost-content which is added to the generated vhost, default empty * @param string $ssl_specialsettings * optional, custom webserver vhost-content which is added to the generated ssl-vhost, default empty * @param bool $include_specialsettings * optional, whether to include non-ssl specialsettings in the generated ssl-vhost, default false * @param bool $notryfiles * optional, [nginx only] do not generate the default try-files directive, default 0 (false) * @param bool $writeaccesslog * optional, Enable writing an access-log file for this domain, default 1 (true) * @param bool $writeerrorlog * optional, Enable writing an error-log file for this domain, default 1 (true) * @param string $documentroot * optional, specify homedir of domain by specifying a directory (relative to customer-docroot), be * aware, if path starts with / it is considered a full path, not relative to customer-docroot. Also * specifying a URL is possible here (redirect), default empty (autogenerated) * @param bool $phpenabled * optional, whether php is enabled for this domain, default 0 (false) * @param bool $openbasedir * optional, whether to activate openbasedir restriction for this domain, default 0 (false) * @param int $openbasedir_path * optional, either 0 for domains-docroot, 1 for customers-homedir or 2 for parent-directory of domains-docroot * @param int $phpsettingid * optional, specify php-configuration that is being used by id, default 1 (system-default) * @param int $mod_fcgid_starter * optional number of fcgid-starters if FCGID is used, default is -1 * @param int $mod_fcgid_maxrequests * optional number of fcgid-maxrequests if FCGID is used, default is -1 * @param bool $ssl_redirect * optional, whether to generate a https-redirect or not, default false; requires SSL to be enabled * @param bool $letsencrypt * optional, whether to generate a Let's Encrypt certificate for this domain, default false; requires * SSL to be enabled * @param array $ssl_ipandport * optional, list of ssl-enabled ip/port id's to assign to this domain, default empty * @param bool $dont_use_default_ssl_ipandport_if_empty * optional, do NOT set the systems default ssl ip addresses if none are given via $ssl_ipandport * parameter * @param bool $sslenabled * optional, whether SSL is enabled for this domain, regardless of the assigned ssl-ips, default * 1 (true) * @param bool $http2 * optional, whether to enable http/2 for this domain (requires to be enabled in the settings), default * 0 (false) * @param bool $http3 * optional, whether to enable http/3 for this domain (requires to be enabled in the settings), default * 0 (false) * @param int $hsts_maxage * optional max-age value for HSTS header * @param bool $hsts_sub * optional whether to add subdomains to the HSTS header * @param bool $hsts_preload * optional whether to preload HSTS header value * @param bool $ocsp_stapling * optional whether to enable ocsp-stapling for this domain. default 0 (false), requires SSL * @param bool $honorcipherorder * optional whether to honor the (server) cipher order for this domain. default 0 (false), requires SSL * @param bool $sessiontickets * optional whether to enable or disable TLS sessiontickets (RFC 5077) for this domain. default 1 * (true), requires SSL * @param bool $override_tls * optional whether to override system-tls settings like protocol, ssl-ciphers and if applicable * tls-1.3 ciphers, requires change_serversettings flag for the admin, default false * @param array $ssl_protocols * optional list of allowed/used ssl/tls protocols, see system.ssl_protocols setting, only used/required * if $override_tls is true, default empty or system.ssl_protocols setting if $override_tls is true * @param string $ssl_cipher_list * optional list of allowed/used ssl/tls ciphers, see system.ssl_cipher_list setting, only used/required * if $override_tls is true, default empty or system.ssl_cipher_list setting if $override_tls is true * @param string $tlsv13_cipher_list * optional list of allowed/used tls-1.3 specific ciphers, see system.tlsv13_cipher_list setting, only * used/required if $override_tls is true, default empty or system.tlsv13_cipher_list setting if * $override_tls is true * @param string $description * optional custom description (currently not used/shown in the frontend), default empty * @param bool $is_stdsubdomain * (internally) optional whether this is a standard subdomain for a customer which is being added so no usage is decreased * @access admin * @return string json-encoded array * @throws Exception */ public function add() { if ($this->isAdmin()) { $is_stdsubdomain = $this->isInternal() ? $this->getBoolParam('is_stdsubdomain', true, 0) : false; if ($is_stdsubdomain || $this->getUserDetail('domains_used') < $this->getUserDetail('domains') || $this->getUserDetail('domains') == '-1') { // parameters $p_domain = $this->getParam('domain'); // optional parameters $p_ipandports = $this->getParam('ipandport', true, explode(',', Settings::Get('system.defaultip'))); $adminid = intval($this->getParam('adminid', true, $this->getUserDetail('adminid'))); $subcanemaildomain = $this->getParam('subcanemaildomain', true, 0); $isemaildomain = $this->getBoolParam('isemaildomain', true, 0); $email_only = $this->getBoolParam('email_only', true, 0); $serveraliasoption = $this->getParam('selectserveralias', true, Settings::Get('system.domaindefaultalias')); $speciallogfile = $this->getBoolParam('speciallogfile', true, 0); $aliasdomain = intval($this->getParam('alias', true, 0)); $registration_date = $this->getParam('registration_date', true, ''); $termination_date = $this->getParam('termination_date', true, ''); $caneditdomain = $this->getBoolParam('caneditdomain', true, 0); $isbinddomain = $this->getBoolParam('isbinddomain', true, 0); $zonefile = $this->getParam('zonefile', true, ''); $dkim = $this->getBoolParam('dkim', true, 0); $specialsettings = $this->getParam('specialsettings', true, ''); $ssl_specialsettings = $this->getParam('ssl_specialsettings', true, ''); $include_specialsettings = $this->getBoolParam('include_specialsettings', true, 0); $notryfiles = $this->getBoolParam('notryfiles', true, 0); $writeaccesslog = $this->getBoolParam('writeaccesslog', true, 1); $writeerrorlog = $this->getBoolParam('writeerrorlog', true, 1); $documentroot = $this->getParam('documentroot', true, ''); $phpenabled = $this->getBoolParam('phpenabled', true, 0); $openbasedir = $this->getBoolParam('openbasedir', true, 0); $openbasedir_path = $this->getParam('openbasedir_path', true, 0); $phpsettingid = $this->getParam('phpsettingid', true, 1); $mod_fcgid_starter = $this->getParam('mod_fcgid_starter', true, -1); $mod_fcgid_maxrequests = $this->getParam('mod_fcgid_maxrequests', true, -1); $ssl_redirect = $this->getBoolParam('ssl_redirect', true, 0); $letsencrypt = $this->getBoolParam('letsencrypt', true, 0); $sslenabled = $this->getBoolParam('sslenabled', true, 1); $dont_use_default_ssl_ipandport_if_empty = $this->getBoolParam('dont_use_default_ssl_ipandport_if_empty', true, 0); $p_ssl_ipandports = $this->getParam('ssl_ipandport', true, $dont_use_default_ssl_ipandport_if_empty ? [] : explode(',', Settings::Get('system.defaultsslip'))); $http2 = $this->getBoolParam('http2', true, 0); $http3 = $this->getBoolParam('http3', true, 0); $hsts_maxage = $this->getParam('hsts_maxage', true, 0); $hsts_sub = $this->getBoolParam('hsts_sub', true, 0); $hsts_preload = $this->getBoolParam('hsts_preload', true, 0); $ocsp_stapling = $this->getBoolParam('ocsp_stapling', true, 0); $honorcipherorder = $this->getBoolParam('honorcipherorder', true, 0); $sessiontickets = $this->getBoolParam('sessiontickets', true, 1); $override_tls = $this->getBoolParam('override_tls', true, 0); $p_ssl_protocols = []; $ssl_cipher_list = ""; $tlsv13_cipher_list = ""; if ($this->getUserDetail('change_serversettings') == '1') { if ($override_tls) { $p_ssl_protocols = $this->getParam('ssl_protocols', true, explode(',', Settings::Get('system.ssl_protocols'))); $ssl_cipher_list = $this->getParam('ssl_cipher_list', true, Settings::Get('system.ssl_cipher_list')); $tlsv13_cipher_list = $this->getParam('tlsv13_cipher_list', true, Settings::Get('system.tlsv13_cipher_list')); } } $description = $this->getParam('description', true, ''); // validation $p_domain = strtolower($p_domain); if ($p_domain == strtolower(Settings::Get('system.hostname'))) { Response::standardError('admin_domain_emailsystemhostname', '', true); } if (substr($p_domain, 0, 4) == 'xn--') { Response::standardError('domain_nopunycode', '', true); } elseif (Validate::validate_ip2($p_domain, true, '', true, true)) { Response::standardError('domain_noipaddress', '', true); } $idna_convert = new IdnaWrapper(); $domain = $idna_convert->encode(preg_replace([ '/\:(\d)+$/', '/^https?\:\/\//' ], '', Validate::validate($p_domain, 'domain'))); // Check whether domain validation is enabled and if, validate the domain if (Settings::Get('system.validate_domain') && !Validate::validateDomain($domain)) { Response::standardError([ 'stringiswrong', 'mydomain' ], '', true); } $customer = $this->getCustomerData(); $customerid = $customer['customerid']; if ($this->getUserDetail('customers_see_all') == '1' && $adminid != $this->getUserDetail('adminid')) { $admin_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_ADMINS . "` WHERE `adminid` = :adminid AND (`domains_used` < `domains` OR `domains` = '-1')"); $admin = Database::pexecute_first($admin_stmt, [ 'adminid' => $adminid ], true, true); if (empty($admin)) { Response::dynamicError("Selected admin cannot have any more domains or could not be found"); } unset($admin); } else { // Force adminid to the caller's own ID when they don't have customers_see_all $adminid = intval($this->getUserDetail('adminid')); } // set default path if admin/reseller has "change_serversettings == false" but we still // need to respect the documentroot_use_default_value - setting $path_suffix = ''; if (Settings::Get('system.documentroot_use_default_value') == 1) { $path_suffix = '/' . $domain; } $_documentroot = FileDir::makeCorrectDir($customer['documentroot'] . $path_suffix); $documentroot = Validate::validate($documentroot, 'documentroot', Validate::REGEX_DIR, '', [], true); // If path is empty and 'Use domain name as default value for DocumentRoot path' is enabled in settings, // set default path to subdomain or domain name if (!empty($documentroot)) { if (substr($documentroot, 0, 1) != '/' && !preg_match('/^https?\:\/\//', $documentroot)) { $documentroot = $_documentroot . '/' . $documentroot; } elseif (substr($documentroot, 0, 1) == '/' && $this->getUserDetail('change_serversettings') != '1') { Response::standardError('pathmustberelative', '', true); } } else { $documentroot = $_documentroot; } if (!is_null($registration_date)) { $registration_date = Validate::validate($registration_date, 'registration_date', Validate::REGEX_YYYY_MM_DD, '', [ '0000-00-00', '0', '' ], true); } if ($registration_date == '0000-00-00' || empty($registration_date)) { $registration_date = null; } if (!is_null($termination_date)) { $termination_date = Validate::validate($termination_date, 'termination_date', Validate::REGEX_YYYY_MM_DD, '', [ '0000-00-00', '0', '' ], true); } if ($termination_date == '0000-00-00' || empty($termination_date)) { $termination_date = null; } if ($this->getUserDetail('change_serversettings') == '1') { if (Settings::Get('system.bind_enable') == '1') { $zonefile = Validate::validate($zonefile, 'zonefile', '', '', [], true); } else { $isbinddomain = 0; $zonefile = ''; } $specialsettings = Validate::validate(str_replace("\r\n", "\n", $specialsettings), 'specialsettings', Validate::REGEX_CONF_TEXT, '', [], true); $ssl_protocols = []; if (!empty($p_ssl_protocols) && is_numeric($p_ssl_protocols)) { $p_ssl_protocols = [ $p_ssl_protocols ]; } if (!empty($p_ssl_protocols) && !is_array($p_ssl_protocols)) { $p_ssl_protocols = json_decode($p_ssl_protocols, true); } if (!empty($p_ssl_protocols) && is_array($p_ssl_protocols)) { $protocols_available = [ 'TLSv1', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3' ]; foreach ($p_ssl_protocols as $ssl_protocol) { if (!in_array(trim($ssl_protocol), $protocols_available)) { $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_DEBUG, "[API] unknown SSL protocol '" . trim($ssl_protocol) . "'"); continue; } $ssl_protocols[] = $ssl_protocol; } } if (empty($ssl_protocols)) { $override_tls = '0'; } // http/3 for nginx only works with TLSv1.3 enabled if ($http3 == '1') { // overwrite enabled? if (Settings::Get('system.webserver') != 'nginx') { $http3 = '0'; } else { if (($override_tls == '1' && !in_array('TLSv1.3', $ssl_protocols)) || ($override_tls == '0' && !in_array('TLSv1.3', explode(",", Settings::Get('system.ssl_protocols')))) ) { // no tlsv1.3 -> no http/3 Response::standardError('tls13requiredforhttp3', '', true); } } } } else { $isbinddomain = '0'; if (Settings::Get('system.bind_enable') == '1') { $isbinddomain = '1'; } $caneditdomain = '1'; $zonefile = ''; $specialsettings = ''; $ssl_specialsettings = ''; $include_specialsettings = 0; $notryfiles = '0'; $writeaccesslog = '1'; $writeerrorlog = '1'; $override_tls = '0'; $ssl_protocols = []; } if ($this->getUserDetail('caneditphpsettings') == '1' || $this->getUserDetail('change_serversettings') == '1') { if ((int)Settings::Get('system.mod_fcgid') == 1 || (int)Settings::Get('phpfpm.enabled') == 1) { $phpsettingid_check_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_PHPCONFIGS . "` WHERE `id` = :phpsettingid"); $phpsettingid_check = Database::pexecute_first($phpsettingid_check_stmt, [ 'phpsettingid' => $phpsettingid ], true, true); if (!isset($phpsettingid_check['id']) || $phpsettingid_check['id'] == '0' || $phpsettingid_check['id'] != $phpsettingid) { Response::standardError('phpsettingidwrong', '', true); } if ((int)Settings::Get('system.mod_fcgid') == 1) { $mod_fcgid_starter = Validate::validate($mod_fcgid_starter, 'mod_fcgid_starter', '/^[0-9]*$/', '', [ '-1', '' ], true); $mod_fcgid_maxrequests = Validate::validate($mod_fcgid_maxrequests, 'mod_fcgid_maxrequests', '/^[0-9]*$/', '', [ '-1', '' ], true); } else { $mod_fcgid_starter = '-1'; $mod_fcgid_maxrequests = '-1'; } } else { if ((int)Settings::Get('phpfpm.enabled') == 1) { $phpsettingid = Settings::Get('phpfpm.defaultini'); } else { $phpsettingid = Settings::Get('system.mod_fcgid_defaultini'); } $mod_fcgid_starter = '-1'; $mod_fcgid_maxrequests = '-1'; } } else { // set default to whether the customer has php enabled or not $phpenabled = $customer['phpenabled']; $openbasedir = '1'; if ((int)Settings::Get('phpfpm.enabled') == 1) { $phpsettingid = Settings::Get('phpfpm.defaultini'); } else { $phpsettingid = Settings::Get('system.mod_fcgid_defaultini'); } $mod_fcgid_starter = '-1'; $mod_fcgid_maxrequests = '-1'; } if ($openbasedir_path > 2 && $openbasedir_path < 0) { $openbasedir_path = 0; } // check non-ssl IP $ipandports = $this->validateIpAddresses($p_ipandports); // check ssl IP $ssl_ipandports = []; if (Settings::Get('system.use_ssl') == "1" && !empty($p_ssl_ipandports)) { $ssl_ipandports = $this->validateIpAddresses($p_ssl_ipandports, true); if ($this->getUserDetail('change_serversettings') == '1') { $ssl_specialsettings = Validate::validate(str_replace("\r\n", "\n", $ssl_specialsettings), 'ssl_specialsettings', '/^[^\0]*$/', '', [], true); } } if (Settings::Get('system.use_ssl') == "1" && $sslenabled == 1 && empty($ssl_ipandports)) { // if this is a customer standard-subdomain, we simply ignore this and disable ssl-related settings (see if-statement below) if (!$is_stdsubdomain) { // enabled ssl for the domain but no ssl ip/port is selected Response::standardError('nosslippportgiven', '', true); } } if (Settings::Get('system.use_ssl') == "0" || empty($ssl_ipandports)) { $ssl_redirect = 0; $letsencrypt = 0; $http2 = 0; $http3 = 0; // we need this for the json_encode // if ssl is disabled or no ssl-ip/port exists $ssl_ipandports[] = -1; // HSTS $hsts_maxage = 0; $hsts_sub = 0; $hsts_preload = 0; // OCSP stapling $ocsp_stapling = 0; // vhost container settings $ssl_specialsettings = ''; $include_specialsettings = 0; } // validate dns if lets encrypt is enabled to check whether we can use it at all if ($letsencrypt == '1' && Settings::Get('system.le_domain_dnscheck') == '1') { $domain_ips = PhpHelper::gethostbynamel6($domain, true, Settings::Get('system.le_domain_dnscheck_resolver')); $selected_ips = $this->getIpsFromIdArray($ssl_ipandports); if ($domain_ips == false || count(array_intersect($selected_ips, $domain_ips)) <= 0) { Response::standardError('invaliddnsforletsencrypt', '', true); } } // We can't enable let's encrypt for wildcard-domains if ($serveraliasoption == '0' && $letsencrypt == '1') { Response::standardError('nowildcardwithletsencrypt', '', true); } // Temporarily deactivate ssl_redirect until Let's Encrypt certificate was generated if ($ssl_redirect > 0 && $letsencrypt == 1) { $ssl_redirect = 2; } // Check if given documentroot is either a valid URL or a valid path if (preg_match('/^https?\:\/\//', $documentroot)) { $encoded = $idna_convert->encode($documentroot); if (!Validate::validateUrl($encoded, true)) { Response::standardError('invaliddocumentrooturl', '', true); } $documentroot = $encoded; } else { if (strpos($documentroot, ':') !== false) { Response::standardError('pathmaynotcontaincolon', '', true); } $documentroot = FileDir::makeCorrectDir($documentroot); } $domain_check_stmt = Database::prepare(" SELECT `id`, `domain` FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `domain` = :domain"); $domain_check = Database::pexecute_first($domain_check_stmt, [ 'domain' => strtolower($domain) ], true, true); $aliasdomain_check = [ 'id' => 0 ]; if ($aliasdomain != 0) { // Overwrite given ipandports with these of the "main" domain $ipandports = []; $ssl_ipandports = []; $origipresult_stmt = Database::prepare(" SELECT `id_ipandports` FROM `" . TABLE_DOMAINTOIP . "` WHERE `id_domain` = :id"); Database::pexecute($origipresult_stmt, [ 'id' => $aliasdomain ], true, true); $ipdata_stmt = Database::prepare("SELECT * FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE `id` = :ipid"); while ($origip = $origipresult_stmt->fetch(PDO::FETCH_ASSOC)) { $_origip_tmp = Database::pexecute_first($ipdata_stmt, [ 'ipid' => $origip['id_ipandports'] ], true, true); if ($_origip_tmp['ssl'] == 0) { $ipandports[] = $origip['id_ipandports']; } else { $ssl_ipandports[] = $origip['id_ipandports']; } } if (count($ssl_ipandports) == 0) { // we need this for the json_encode // if ssl is disabled or no ssl-ip/port exists $ssl_ipandports[] = -1; } $aliasdomain_check_stmt = Database::prepare(" SELECT `d`.`id` FROM `" . TABLE_PANEL_DOMAINS . "` `d`, `" . TABLE_PANEL_CUSTOMERS . "` `c` WHERE `d`.`customerid` = :customerid AND `d`.`aliasdomain` IS NULL AND `d`.`id` <> `c`.`standardsubdomain` AND `c`.`customerid` = :customerid AND `d`.`id` = :aliasdomainid"); $alias_params = [ 'customerid' => $customerid, 'aliasdomainid' => $aliasdomain ]; $aliasdomain_check = Database::pexecute_first($aliasdomain_check_stmt, $alias_params, true, true); } if (count($ipandports) == 0) { Response::standardError('noipportgiven', '', true); } if ($email_only == '1') { $isemaildomain = '1'; } else { $email_only = '0'; } if ($subcanemaildomain != '1' && $subcanemaildomain != '2' && $subcanemaildomain != '3') { $subcanemaildomain = '0'; } if ($serveraliasoption != '1' && $serveraliasoption != '2') { $serveraliasoption = '0'; } $idna_convert = new IdnaWrapper(); if ($domain == '') { Response::standardError([ 'stringisempty', 'mydomain' ], '', true); } elseif ($documentroot == '') { Response::standardError([ 'stringisempty', 'mydocumentroot' ], '', true); } elseif ($customerid == 0) { Response::standardError('adduserfirst', '', true); } elseif ($domain_check && strtolower($domain_check['domain']) == strtolower($domain)) { Response::standardError('domainalreadyexists', $idna_convert->decode($domain), true); } elseif ($aliasdomain_check && $aliasdomain_check['id'] != $aliasdomain) { Response::standardError('domainisaliasorothercustomer', '', true); } else { $wwwserveralias = ($serveraliasoption == '1') ? '1' : '0'; $iswildcarddomain = ($serveraliasoption == '0') ? '1' : '0'; $ins_data = [ 'domain' => $domain, 'domain_ace' => $idna_convert->decode($domain), 'customerid' => $customerid, 'adminid' => $adminid, 'documentroot' => $documentroot, 'aliasdomain' => ($aliasdomain != 0 ? $aliasdomain : null), 'zonefile' => $zonefile, 'dkim' => $dkim, 'wwwserveralias' => $wwwserveralias, 'iswildcarddomain' => $iswildcarddomain, 'isbinddomain' => $isbinddomain, 'isemaildomain' => $isemaildomain, 'email_only' => $email_only, 'subcanemaildomain' => $subcanemaildomain, 'caneditdomain' => $caneditdomain, 'phpenabled' => $phpenabled, 'openbasedir' => $openbasedir, 'openbasedir_path' => $openbasedir_path, 'speciallogfile' => $speciallogfile, 'specialsettings' => $specialsettings, 'ssl_specialsettings' => $ssl_specialsettings, 'include_specialsettings' => $include_specialsettings, 'notryfiles' => $notryfiles, 'writeaccesslog' => $writeaccesslog, 'writeerrorlog' => $writeerrorlog, 'ssl_redirect' => $ssl_redirect, 'add_date' => time(), 'registration_date' => $registration_date, 'termination_date' => $termination_date, 'phpsettingid' => $phpsettingid, 'mod_fcgid_starter' => $mod_fcgid_starter, 'mod_fcgid_maxrequests' => $mod_fcgid_maxrequests, 'letsencrypt' => $letsencrypt, 'http2' => $http2, 'http3' => $http3, 'hsts' => $hsts_maxage, 'hsts_sub' => $hsts_sub, 'hsts_preload' => $hsts_preload, 'ocsp_stapling' => $ocsp_stapling, 'override_tls' => $override_tls, 'ssl_protocols' => implode(",", $ssl_protocols), 'ssl_cipher_list' => $ssl_cipher_list, 'tlsv13_cipher_list' => $tlsv13_cipher_list, 'sslenabled' => $sslenabled, 'honorcipherorder' => $honorcipherorder, 'sessiontickets' => $sessiontickets, 'description' => $description ]; $ins_stmt = Database::prepare(" INSERT INTO `" . TABLE_PANEL_DOMAINS . "` SET `domain` = :domain, `domain_ace` = :domain_ace, `customerid` = :customerid, `adminid` = :adminid, `documentroot` = :documentroot, `aliasdomain` = :aliasdomain, `zonefile` = :zonefile, `dkim` = :dkim, `dkim_id` = '0', `dkim_privkey` = '', `dkim_pubkey` = '', `wwwserveralias` = :wwwserveralias, `iswildcarddomain` = :iswildcarddomain, `isbinddomain` = :isbinddomain, `isemaildomain` = :isemaildomain, `email_only` = :email_only, `subcanemaildomain` = :subcanemaildomain, `caneditdomain` = :caneditdomain, `phpenabled` = :phpenabled, `openbasedir` = :openbasedir, `openbasedir_path` = :openbasedir_path, `speciallogfile` = :speciallogfile, `specialsettings` = :specialsettings, `ssl_specialsettings` = :ssl_specialsettings, `include_specialsettings` = :include_specialsettings, `notryfiles` = :notryfiles, `writeaccesslog` = :writeaccesslog, `writeerrorlog` = :writeerrorlog, `ssl_redirect` = :ssl_redirect, `add_date` = :add_date, `registration_date` = :registration_date, `termination_date` = :termination_date, `phpsettingid` = :phpsettingid, `mod_fcgid_starter` = :mod_fcgid_starter, `mod_fcgid_maxrequests` = :mod_fcgid_maxrequests, `letsencrypt` = :letsencrypt, `http2` = :http2, `http3` = :http3, `hsts` = :hsts, `hsts_sub` = :hsts_sub, `hsts_preload` = :hsts_preload, `ocsp_stapling` = :ocsp_stapling, `override_tls` = :override_tls, `ssl_protocols` = :ssl_protocols, `ssl_cipher_list` = :ssl_cipher_list, `tlsv13_cipher_list` = :tlsv13_cipher_list, `ssl_enabled` = :sslenabled, `ssl_honorcipherorder` = :honorcipherorder, `ssl_sessiontickets` = :sessiontickets, `description` = :description "); Database::pexecute($ins_stmt, $ins_data, true, true); $domainid = Database::lastInsertId(); $ins_data['id'] = $domainid; unset($ins_data); if (!$is_stdsubdomain) { $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_ADMINS . "` SET `domains_used` = `domains_used` + 1 WHERE `adminid` = :adminid "); Database::pexecute($upd_stmt, [ 'adminid' => $adminid ], true, true); } $ins_stmt = Database::prepare(" INSERT INTO `" . TABLE_DOMAINTOIP . "` SET `id_domain` = :domainid, `id_ipandports` = :ipandportsid "); foreach ($ipandports as $ipportid) { $ins_data = [ 'domainid' => $domainid, 'ipandportsid' => $ipportid ]; Database::pexecute($ins_stmt, $ins_data, true, true); } foreach ($ssl_ipandports as $ssl_ipportid) { if ($ssl_ipportid > 0) { $ins_data = [ 'domainid' => $domainid, 'ipandportsid' => $ssl_ipportid ]; Database::pexecute($ins_stmt, $ins_data, true, true); } } Domain::triggerLetsEncryptCSRForAliasDestinationDomain($aliasdomain, $this->logger()); Cronjob::inserttask(TaskId::REBUILD_VHOST); // Using nameserver, insert a task which rebuilds the server config Cronjob::inserttask(TaskId::REBUILD_DNS); if ($dkim == '1') { Cronjob::inserttask(TaskId::REBUILD_RSPAMD); } $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] added domain '" . $domain . "'"); $result = $this->apiCall('Domains.get', [ 'domainname' => $domain ]); return $this->response($result); } } throw new Exception("No more resources available", 406); } throw new Exception("Not allowed to execute given command.", 403); } /** * return a domain entry by either id or domainname * * @param int $id * optional, the domain-id * @param string $domainname * optional, the domainname * @param bool $with_ips * optional, default true * @param bool $no_std_subdomain * optional, default false * * @access admin * @return string json-encoded array * @throws Exception */ public function get() { if ($this->isAdmin()) { $id = $this->getParam('id', true, 0); $dn_optional = $id > 0; $domainname = $this->getParam('domainname', $dn_optional, ''); $with_ips = $this->getParam('with_ips', true, true); $no_std_subdomain = $this->getParam('no_std_subdomain', true, false); // convert possible idn domain to punycode if (substr($domainname, 0, 4) != 'xn--') { $idna_convert = new IdnaWrapper(); $domainname = $idna_convert->encode($domainname); } $result_stmt = Database::prepare(" SELECT `d`.*, `c`.`customerid` FROM `" . TABLE_PANEL_DOMAINS . "` `d` LEFT JOIN `" . TABLE_PANEL_CUSTOMERS . "` `c` USING(`customerid`) WHERE `d`.`parentdomainid` = '0' AND " . ($id > 0 ? "`d`.`id` = :iddn" : "`d`.`domain` = :iddn") . ($no_std_subdomain ? ' AND `d`.`id` <> `c`.`standardsubdomain`' : '') . ($this->getUserDetail('customers_see_all') ? '' : " AND `d`.`adminid` = :adminid")); $params = [ 'iddn' => ($id <= 0 ? $domainname : $id) ]; if ($this->getUserDetail('customers_see_all') == '0') { $params['adminid'] = $this->getUserDetail('adminid'); } $result = Database::pexecute_first($result_stmt, $params, true, true); if ($result) { $result['ipsandports'] = []; if ($with_ips) { $result['ipsandports'] = $this->getIpsForDomain($result['id']); } $result['domain_hascert'] = $this->getHasCertValueForDomain((int)$result['id'], (int)$result['parentdomainid']); $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] get domain '" . $result['domain'] . "'"); return $this->response($result); } $key = ($id > 0 ? "id #" . $id : "domainname '" . $domainname . "'"); throw new Exception("Domain with " . $key . " could not be found", 404); } throw new Exception("Not allowed to execute given command.", 403); } /** * validate given ips * * @param int|string|array $p_ipandports * @param boolean $ssl * default false * @param int $edit_id * default 0 * * @return array * @throws Exception */ private function validateIpAddresses($p_ipandports = null, $ssl = false, $edit_id = 0) { // when adding a new domain and no ip is given, we try to use the // system-default, check here if there is none // this is not required for ssl-enabled ip's if ($edit_id <= 0 && !$ssl && empty($p_ipandports)) { throw new Exception("No IPs given, unable to add domain (no default IPs set?)", 406); } // convert given value(s) correctly $ipandports = []; if (!empty($p_ipandports) && is_numeric($p_ipandports)) { $p_ipandports = [ $p_ipandports ]; } if (!empty($p_ipandports) && !is_array($p_ipandports)) { $p_ipandports = json_decode($p_ipandports, true); } // check whether there are ip usage restrictions $additional_ip_condition = ''; $aip_param = []; if ($this->getUserDetail('ip') != "-1") { // handle multiple-ip-array $additional_ip_condition = " AND `ip` IN (" . implode(",", json_decode($this->getUserDetail('ip'), true)) . ") "; } if (!empty($p_ipandports) && is_array($p_ipandports)) { $ipandport_check_stmt = Database::prepare(" SELECT `id`, `ip`, `port` FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE `id` = :ipandport " . ($ssl ? " AND `ssl` = '1'" : "") . $additional_ip_condition); foreach ($p_ipandports as $ipandport) { if (trim($ipandport) == "") { continue; } // fix if no ip/port is checked if (trim($ipandport) < 1) { continue; } $ipandport = intval($ipandport); $ip_params = array_merge([ 'ipandport' => $ipandport ], $aip_param); $ipandport_check = Database::pexecute_first($ipandport_check_stmt, $ip_params, true, true); if (!isset($ipandport_check['id']) || $ipandport_check['id'] == '0' || $ipandport_check['id'] != $ipandport) { Response::standardError('ipportdoesntexist', '', true); } else { $ipandports[] = $ipandport; } } } elseif ($edit_id > 0) { // set currently used ip's $ipsresult_stmt = Database::prepare(" SELECT d2i.`id_ipandports` FROM `" . TABLE_DOMAINTOIP . "` d2i LEFT JOIN `" . TABLE_PANEL_IPSANDPORTS . "` i ON i.id = d2i.id_ipandports WHERE d2i.`id_domain` = :id AND i.`ssl` = " . ($ssl ? "'1'" : "'0'")); Database::pexecute($ipsresult_stmt, [ 'id' => $edit_id ], true, true); while ($ipsresultrow = $ipsresult_stmt->fetch(PDO::FETCH_ASSOC)) { $ipandports[] = $ipsresultrow['id_ipandports']; } } return $ipandports; } /** * get ips from array of id's * * @param array $ips * @return array */ private function getIpsFromIdArray(array $ids) { $resultips_stmt = Database::prepare(" SELECT `ip` FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE id = :id "); $result = []; foreach ($ids as $id) { $entry = Database::pexecute_first($resultips_stmt, [ 'id' => $id ]); $result[] = $entry['ip']; } return $result; } /** * update domain entry by either id or domainname * * @param int $id * optional, the domain-id * @param string $domainname * optional, the domainname * @param int $customerid * required (if $loginname is not specified) * @param string $loginname * required (if $customerid is not specified) * @param int $adminid * optional, default is the calling admin's ID * @param array $ipandport * optional list of ip/ports to assign to domain, default is system-default-ips * @param int $subcanemaildomain * optional, allow subdomains of this domain as email domains, 1 = choosable (default no), 2 = choosable * (default yes), 3 = always, default 0 (never) * @param bool $isemaildomain * optional, allow email usage with this domain, default 0 (false) * @param bool $emaildomainverified * optional, when setting $isemaildomain to false, this needs to be set to true to confirm the action in case email addresses exist for this domain, * default 0 (false) * @param bool $email_only * optional, restrict domain to email usage, default 0 (false) * @param int $selectserveralias * optional, 0 = wildcard, 1 = www-alias, 2 = none, default 0 * @param bool $speciallogfile * optional, whether to create an exclusive web-logfile for this domain, default 0 (false) * @param bool $speciallogverified * optional, when setting $speciallogfile to false, this needs to be set to true to confirm the action, * default 0 (false) * @param int $alias * optional, domain-id of a domain that the new domain should be an alias of, default 0 (none) * @param string $registration_date * optional, date of domain registration in form of YYYY-MM-DD, default empty (none) * @param string $termination_date * optional, date of domain termination in form of YYYY-MM-DD, default empty (none) * @param bool $caneditdomain * optional, whether to allow the customer to edit domain settings, default 0 (false) * @param bool $isbinddomain * optional, whether to generate a dns-zone or not (only of nameserver is activated), default 0 (false) * @param string $zonefile * optional, custom dns zone filename (only of nameserver is activated), default empty (auto-generated) * @param bool $dkim * optional, whether this domain should use dkim if antispam is activated, default 0 (false) * @param string $specialsettings * optional, custom webserver vhost-content which is added to the generated vhost, default empty * @param string $ssl_specialsettings * optional, custom webserver vhost-content which is added to the generated ssl-vhost, default empty * @param bool $include_specialsettings * optional, whether to include non-ssl specialsettings in the generated ssl-vhost, default false * @param bool $specialsettingsforsubdomains * optional, whether to apply specialsettings to all subdomains of this domain, default is read from * setting system.apply_specialsettings_default * @param bool $notryfiles * optional, [nginx only] do not generate the default try-files directive, default 0 (false) * @param bool $writeaccesslog * optional, Enable writing an access-log file for this domain, default 1 (true) * @param bool $writeerrorlog * optional, Enable writing an error-log file for this domain, default 1 (true) * @param string $documentroot * optional, specify homedir of domain by specifying a directory (relative to customer-docroot), be * aware, if path starts with / it is considered a full path, not relative to customer-docroot. Also * specifying a URL is possible here (redirect), default empty (autogenerated) * @param bool $phpenabled * optional, whether php is enabled for this domain, default 0 (false) * @param bool $phpsettingsforsubdomains * optional, whether to apply php-setting to apply to all subdomains of this domain, default is read * from setting system.apply_phpconfigs_default * @param bool $openbasedir * optional, whether to activate openbasedir restriction for this domain, default 0 (false) * @param int $openbasedir_path * optional, either 0 for domains-docroot, 1 for customers-homedir or 2 for parent-directory of domains-docroot * @param int $phpsettingid * optional, specify php-configuration that is being used by id, default 1 (system-default) * @param int $mod_fcgid_starter * optional number of fcgid-starters if FCGID is used, default is -1 * @param int $mod_fcgid_maxrequests * optional number of fcgid-maxrequests if FCGID is used, default is -1 * @param bool $ssl_redirect * optional, whether to generate a https-redirect or not, default false; requires SSL to be enabled * @param bool $letsencrypt * optional, whether to generate a Let's Encrypt certificate for this domain, default false; requires * SSL to be enabled * @param array $ssl_ipandport * optional, list of ssl-enabled ip/port id's to assign to this domain, if left empty, the current set * value is being used, to remove all ssl ips use $remove_ssl_ipandport * @param bool $remove_ssl_ipandport * optional, if set to true and no $ssl_ipandport value is given, the ip's get removed, otherwise, the * currently set value is used, default false * @param bool $sslenabled * optional, whether SSL is enabled for this domain, regardless of the assigned ssl-ips, default * 1 (true) * @param bool $http2 * optional, whether to enable http/2 for this domain (requires to be enabled in the settings), default * 0 (false) * @param bool $http3 * optional, whether to enable http/3 for this domain (requires to be enabled in the settings), default * 0 (false) * @param int $hsts_maxage * optional max-age value for HSTS header * @param bool $hsts_sub * optional whether to add subdomains to the HSTS header * @param bool $hsts_preload * optional whether to preload HSTS header value * @param bool $ocsp_stapling * optional whether to enable ocsp-stapling for this domain. default 0 (false), requires SSL * @param bool $honorcipherorder * optional whether to honor the (server) cipher order for this domain. default 0 (false), requires SSL * @param bool $sessiontickets * optional whether to enable or disable TLS sessiontickets (RFC 5077) for this domain. default 1 * (true), requires SSL * @param string $description * optional custom description (currently not used/shown in the frontend), default empty * @param bool $deactivated * optional, if 1 (true) the domain can be deactivated/suspended * * @access admin * @return string json-encoded array * @throws Exception */ public function update() { if ($this->isAdmin()) { // parameters $id = $this->getParam('id', true, 0); $dn_optional = $id > 0; $domainname = $this->getParam('domainname', $dn_optional, ''); // get requested domain $result = $this->apiCall('Domains.get', [ 'id' => $id, 'domainname' => $domainname ]); $id = $result['id']; // optional parameters $p_ipandports = $this->getParam('ipandport', true, []); $adminid = intval($this->getParam('adminid', true, $result['adminid'])); if ($this->getParam('customerid', true, 0) == 0 && $this->getParam('loginname', true, '') == '') { $customerid = $result['customerid']; $customer = $this->apiCall('Customers.get', [ 'id' => $customerid ]); } else { $customer = $this->getCustomerData(); $customerid = $customer['customerid']; } $subcanemaildomain = $this->getParam('subcanemaildomain', true, $result['subcanemaildomain']); $isemaildomain = $this->getBoolParam('isemaildomain', true, $result['isemaildomain']); $emaildomainverified = $this->getBoolParam('emaildomainverified', true, 0); $email_only = $this->getBoolParam('email_only', true, $result['email_only']); $p_serveraliasoption = $this->getParam('selectserveralias', true, -1); $speciallogfile = $this->getBoolParam('speciallogfile', true, $result['speciallogfile']); $speciallogverified = $this->getBoolParam('speciallogverified', true, 0); $aliasdomain = intval($this->getParam('alias', true, $result['aliasdomain'])); $registration_date = $this->getParam('registration_date', true, $result['registration_date']); $termination_date = $this->getParam('termination_date', true, $result['termination_date']); $caneditdomain = $this->getBoolParam('caneditdomain', true, $result['caneditdomain']); $isbinddomain = $this->getBoolParam('isbinddomain', true, $result['isbinddomain']); $zonefile = $this->getParam('zonefile', true, $result['zonefile']); $dkim = $this->getBoolParam('dkim', true, $result['dkim']); $specialsettings = $this->getParam('specialsettings', true, $result['specialsettings']); $ssl_specialsettings = $this->getParam('ssl_specialsettings', true, $result['ssl_specialsettings']); $include_specialsettings = $this->getBoolParam('include_specialsettings', true, $result['include_specialsettings']); $ssfs = $this->getBoolParam('specialsettingsforsubdomains', true, Settings::Get('system.apply_specialsettings_default')); $notryfiles = $this->getBoolParam('notryfiles', true, $result['notryfiles']); $writeaccesslog = $this->getBoolParam('writeaccesslog', true, $result['writeaccesslog']); $writeerrorlog = $this->getBoolParam('writeerrorlog', true, $result['writeerrorlog']); $documentroot = $this->getParam('documentroot', true, $result['documentroot']); $phpenabled = $this->getBoolParam('phpenabled', true, $result['phpenabled']); $phpfs = $this->getBoolParam('phpsettingsforsubdomains', true, Settings::Get('system.apply_phpconfigs_default')); $openbasedir = $this->getBoolParam('openbasedir', true, $result['openbasedir']); $openbasedir_path = $this->getParam('openbasedir_path', true, $result['openbasedir_path']); $phpsettingid = $this->getParam('phpsettingid', true, $result['phpsettingid']); $mod_fcgid_starter = $this->getParam('mod_fcgid_starter', true, $result['mod_fcgid_starter']); $mod_fcgid_maxrequests = $this->getParam('mod_fcgid_maxrequests', true, $result['mod_fcgid_maxrequests']); $ssl_redirect = $this->getBoolParam('ssl_redirect', true, $result['ssl_redirect']); $letsencrypt = $this->getBoolParam('letsencrypt', true, $result['letsencrypt']); $remove_ssl_ipandport = $this->getBoolParam('remove_ssl_ipandport', true, 0); $p_ssl_ipandports = $this->getParam('ssl_ipandport', true, $remove_ssl_ipandport ? [ -1 ] : null); $sslenabled = $remove_ssl_ipandport ? false : $this->getBoolParam('sslenabled', true, $result['ssl_enabled']); $http2 = $this->getBoolParam('http2', true, $result['http2']); $http3 = $this->getBoolParam('http3', true, $result['http3']); $hsts_maxage = $this->getParam('hsts_maxage', true, $result['hsts']); $hsts_sub = $this->getBoolParam('hsts_sub', true, $result['hsts_sub']); $hsts_preload = $this->getBoolParam('hsts_preload', true, $result['hsts_preload']); $ocsp_stapling = $this->getBoolParam('ocsp_stapling', true, $result['ocsp_stapling']); $honorcipherorder = $this->getBoolParam('honorcipherorder', true, $result['ssl_honorcipherorder']); $sessiontickets = $this->getBoolParam('sessiontickets', true, $result['ssl_sessiontickets']); $override_tls = $this->getBoolParam('override_tls', true, $result['override_tls']); if ($this->getUserDetail('change_serversettings') == '1') { if ($override_tls) { $p_ssl_protocols = $this->getParam('ssl_protocols', true, explode(',', $result['ssl_protocols'])); $ssl_cipher_list = $this->getParam('ssl_cipher_list', true, $result['ssl_cipher_list']); $tlsv13_cipher_list = $this->getParam('tlsv13_cipher_list', true, $result['tlsv13_cipher_list']); } else { $p_ssl_protocols = []; $ssl_cipher_list = ""; $tlsv13_cipher_list = ""; } } else { $p_ssl_protocols = explode(',', $result['ssl_protocols']); $ssl_cipher_list = $result['ssl_cipher_list']; $tlsv13_cipher_list = $result['tlsv13_cipher_list']; } $description = $this->getParam('description', true, $result['description']); $deactivated = $this->getBoolParam('deactivated', true, $result['deactivated']); // count subdomain usage of source-domain $subdomains_stmt = Database::prepare(" SELECT COUNT(`id`) AS count FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `parentdomainid` = :resultid "); $subdomains = Database::pexecute_first($subdomains_stmt, [ 'resultid' => $result['id'] ], true, true); $subdomains = $subdomains['count']; // count where this domain is alias domain $alias_check_stmt = Database::prepare(" SELECT COUNT(`id`) AS count FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `aliasdomain` = :resultid "); $alias_check = Database::pexecute_first($alias_check_stmt, [ 'resultid' => $result['id'] ], true, true); $alias_check = $alias_check['count']; // count where we are used in email-accounts $domain_emails_result_stmt = Database::prepare(" SELECT `email`, `email_full`, `destination`, `popaccountid` FROM `" . TABLE_MAIL_VIRTUAL . "` WHERE `customerid` = :customerid AND `domainid` = :id "); Database::pexecute($domain_emails_result_stmt, [ 'customerid' => $result['customerid'], 'id' => $result['id'] ], true, true); $emails = Database::num_rows(); $email_forwarders = 0; $email_accounts = 0; while ($domain_emails_row = $domain_emails_result_stmt->fetch(PDO::FETCH_ASSOC)) { if ($domain_emails_row['destination'] != '') { $domain_emails_row['destination'] = explode(' ', FileDir::makeCorrectDestination($domain_emails_row['destination'])); $email_forwarders += count($domain_emails_row['destination']); if (in_array($domain_emails_row['email_full'], $domain_emails_row['destination'])) { $email_forwarders -= 1; $email_accounts++; } } } if ($emails > 0 && (int)$isemaildomain == 0 && (int)$result['isemaildomain'] == 1 && (int)$emaildomainverified == 0) { Response::standardError('emaildomainstillhasaddresses', '', true); } // handle change of customer (move domain from customer to customer) if ($customerid > 0 && $customerid != $result['customerid'] && Settings::Get('panel.allow_domain_change_customer') == '1') { // check whether target customer has enough resources $customer_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_CUSTOMERS . "` WHERE `customerid` = :customerid AND (`subdomains_used` + :subdomains <= `subdomains` OR `subdomains` = '-1' ) AND (`emails_used` + :emails <= `emails` OR `emails` = '-1' ) AND (`email_forwarders_used` + :forwarders <= `email_forwarders` OR `email_forwarders` = '-1' ) AND (`email_accounts_used` + :accounts <= `email_accounts` OR `email_accounts` = '-1' ) " . ($this->getUserDetail('customers_see_all') ? '' : " AND `adminid` = :adminid")); $params = [ 'customerid' => $customerid, 'subdomains' => $subdomains, 'emails' => $emails, 'forwarders' => $email_forwarders, 'accounts' => $email_accounts ]; if ($this->getUserDetail('customers_see_all') == '0') { $params['adminid'] = $this->getUserDetail('adminid'); } $customer = Database::pexecute_first($customer_stmt, $params, true, true); if (empty($customer) || $customer['customerid'] != $customerid) { Response::standardError('customerdoesntexist', '', true); } } // handle change of admin (move domain from admin to admin) if ($this->getUserDetail('customers_see_all') == '1') { if ($adminid > 0 && $adminid != $result['adminid'] && Settings::Get('panel.allow_domain_change_admin') == '1') { $admin_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_ADMINS . "` WHERE `adminid` = :adminid AND ( `domains_used` < `domains` OR `domains` = '-1' ) "); $admin = Database::pexecute_first($admin_stmt, [ 'adminid' => $adminid ], true, true); if (empty($admin) || $admin['adminid'] != $adminid) { Response::standardError('admindoesntexist', '', true); } } else { $adminid = $result['adminid']; } } else { $adminid = $result['adminid']; } if (!is_null($registration_date)) { $registration_date = Validate::validate($registration_date, 'registration_date', Validate::REGEX_YYYY_MM_DD, '', [ '0000-00-00', '0', '' ], true); } if ($registration_date == '0000-00-00' || empty($registration_date)) { $registration_date = null; } if (!is_null($termination_date)) { $termination_date = Validate::validate($termination_date, 'termination_date', Validate::REGEX_YYYY_MM_DD, '', [ '0000-00-00', '0', '' ], true); } if ($termination_date == '0000-00-00' || empty($termination_date)) { $termination_date = null; } $serveraliasoption = '2'; if ($result['iswildcarddomain'] == '1') { $serveraliasoption = '0'; } elseif ($result['wwwserveralias'] == '1') { $serveraliasoption = '1'; } if ($p_serveraliasoption > -1) { $serveraliasoption = $p_serveraliasoption; } $documentroot = Validate::validate($documentroot, 'documentroot', Validate::REGEX_DIR, '', [], true); if (!empty($documentroot) && $documentroot != $result['documentroot'] && substr($documentroot, 0, 1) == '/' && substr($documentroot, 0, strlen($customer['documentroot'])) != $customer['documentroot'] && $this->getUserDetail('change_serversettings') != '1') { Response::standardError('pathmustberelative', '', true); } // when moving customer and no path is specified, update would normally reuse the current document-root // which would point to the wrong customer, therefore we will re-create that directory if (!empty($documentroot) && $customerid > 0 && $customerid != $result['customerid'] && Settings::Get('panel.allow_domain_change_customer') == '1') { if (Settings::Get('system.documentroot_use_default_value') == 1) { $_documentroot = FileDir::makeCorrectDir($customer['documentroot'] . '/' . $result['domain']); } else { $_documentroot = $customer['documentroot']; } // set the customers default docroot $documentroot = $_documentroot; } if ($documentroot == '') { // If path is empty and 'Use domain name as default value for DocumentRoot path' is enabled in settings, // set default path to subdomain or domain name if (Settings::Get('system.documentroot_use_default_value') == 1) { $documentroot = FileDir::makeCorrectDir($customer['documentroot'] . '/' . $result['domain']); } else { $documentroot = $customer['documentroot']; } } if ($this->getUserDetail('change_serversettings') == '1') { if (Settings::Get('system.bind_enable') == '1') { $zonefile = Validate::validate($zonefile, 'zonefile', '', '', [], true); } else { $isbinddomain = $result['isbinddomain']; $zonefile = $result['zonefile']; } if (Settings::Get('antispam.activated') != '1') { $dkim = $result['dkim']; } $specialsettings = Validate::validate(str_replace("\r\n", "\n", $specialsettings), 'specialsettings', Validate::REGEX_CONF_TEXT, '', [], true); $ssl_protocols = []; if (!empty($p_ssl_protocols) && is_numeric($p_ssl_protocols)) { $p_ssl_protocols = [ $p_ssl_protocols ]; } if (!empty($p_ssl_protocols) && !is_array($p_ssl_protocols)) { $p_ssl_protocols = json_decode($p_ssl_protocols, true); } if (!empty($p_ssl_protocols) && is_array($p_ssl_protocols)) { $protocols_available = [ 'TLSv1', 'TLSv1.1', 'TLSv1.2', 'TLSv1.3' ]; foreach ($p_ssl_protocols as $ssl_protocol) { if (!in_array(trim($ssl_protocol), $protocols_available)) { $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_DEBUG, "[API] unknown SSL protocol '" . trim($ssl_protocol) . "'"); continue; } $ssl_protocols[] = $ssl_protocol; } } if (empty($ssl_protocols)) { $override_tls = '0'; } // http/3 for nginx only works with TLSv1.3 enabled if ($http3 == '1') { // overwrite enabled? if (Settings::Get('system.webserver') != 'nginx') { $http3 = '0'; } else { if (($override_tls == '1' && !in_array('TLSv1.3', $ssl_protocols)) || ($override_tls == '0' && !in_array('TLSv1.3', explode(",", Settings::Get('system.ssl_protocols')))) ) { // no tlsv1.3 -> no http/3 Response::standardError('tls13requiredforhttp3', '', true); } } } } else { $isbinddomain = $result['isbinddomain']; $zonefile = $result['zonefile']; $specialsettings = $result['specialsettings']; $ssl_specialsettings = $result['ssl_specialsettings']; $include_specialsettings = $result['include_specialsettings']; $ssfs = (empty($specialsettings) ? 0 : 1); $notryfiles = $result['notryfiles']; $writeaccesslog = $result['writeaccesslog']; $writeerrorlog = $result['writeerrorlog']; $ssl_protocols = $p_ssl_protocols; $override_tls = $result['override_tls']; } if ($this->getUserDetail('caneditphpsettings') == '1' || $this->getUserDetail('change_serversettings') == '1') { if ((int)Settings::Get('system.mod_fcgid') == 1 || (int)Settings::Get('phpfpm.enabled') == 1) { $phpsettingid_check_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_PHPCONFIGS . "` WHERE `id` = :phpid "); $phpsettingid_check = Database::pexecute_first($phpsettingid_check_stmt, [ 'phpid' => $phpsettingid ], true, true); if (!isset($phpsettingid_check['id']) || $phpsettingid_check['id'] == '0' || $phpsettingid_check['id'] != $phpsettingid) { Response::standardError('phpsettingidwrong', '', true); } if ((int)Settings::Get('system.mod_fcgid') == 1) { $mod_fcgid_starter = Validate::validate($mod_fcgid_starter, 'mod_fcgid_starter', '/^[0-9]*$/', '', [ '-1', '' ], true); $mod_fcgid_maxrequests = Validate::validate($mod_fcgid_maxrequests, 'mod_fcgid_maxrequests', '/^[0-9]*$/', '', [ '-1', '' ], true); } else { $mod_fcgid_starter = $result['mod_fcgid_starter']; $mod_fcgid_maxrequests = $result['mod_fcgid_maxrequests']; } } else { $phpsettingid = $result['phpsettingid']; $phpfs = 1; $mod_fcgid_starter = $result['mod_fcgid_starter']; $mod_fcgid_maxrequests = $result['mod_fcgid_maxrequests']; } } else { $phpenabled = $result['phpenabled']; $openbasedir = $result['openbasedir']; $phpsettingid = $result['phpsettingid']; $phpfs = 1; $mod_fcgid_starter = $result['mod_fcgid_starter']; $mod_fcgid_maxrequests = $result['mod_fcgid_maxrequests']; } // check changes of openbasedir-path variable if ($openbasedir_path > 2 && $openbasedir_path < 0) { $openbasedir_path = 0; } // check non-ssl IP $ipandports = $this->validateIpAddresses($p_ipandports, false, $result['id']); // check ssl IP if (empty($p_ssl_ipandports) || (!is_array($p_ssl_ipandports) && is_null($p_ssl_ipandports))) { $p_ssl_ipandports = []; foreach ($result['ipsandports'] as $ip) { if ($ip['ssl'] == 1) { $p_ssl_ipandports[] = $ip['id']; } } } $ssl_ipandports = []; if (Settings::Get('system.use_ssl') == "1" && !empty($p_ssl_ipandports) && $p_ssl_ipandports[0] != -1) { $ssl_ipandports = $this->validateIpAddresses($p_ssl_ipandports, true, $result['id']); if ($this->getUserDetail('change_serversettings') == '1') { $ssl_specialsettings = Validate::validate(str_replace("\r\n", "\n", $ssl_specialsettings), 'ssl_specialsettings', '/^[^\0]*$/', '', [], true); } } if ($remove_ssl_ipandport || (!empty($p_ssl_ipandports) && $p_ssl_ipandports[0] == -1)) { $ssl_ipandports = []; } if (Settings::Get('system.use_ssl') == "1" && $sslenabled && empty($ssl_ipandports)) { // enabled ssl for the domain but no ssl ip/port is selected Response::standardError('nosslippportgiven', '', true); } if (Settings::Get('system.use_ssl') == "0" || empty($ssl_ipandports) || !$sslenabled) { $ssl_redirect = 0; $letsencrypt = 0; $http2 = 0; $http3 = 0; // act like $remove_ssl_ipandport $ssl_ipandports = []; // HSTS $hsts_maxage = 0; $hsts_sub = 0; $hsts_preload = 0; // OCSP stapling $ocsp_stapling = 0; // vhost container settings $ssl_specialsettings = ''; $include_specialsettings = 0; } // validate dns if lets encrypt is enabled to check whether we can use it at all if ($letsencrypt == '1' && Settings::Get('system.le_domain_dnscheck') == '1') { $domain_ips = PhpHelper::gethostbynamel6($result['domain'], true, Settings::Get('system.le_domain_dnscheck_resolver')); $selected_ips = $this->getIpsFromIdArray($ssl_ipandports); if ($domain_ips == false || count(array_intersect($selected_ips, $domain_ips)) <= 0) { Response::standardError('invaliddnsforletsencrypt', '', true); } } // We can't enable let's encrypt for wildcard-domains if ($serveraliasoption == '0' && $letsencrypt == '1') { Response::standardError('nowildcardwithletsencrypt', '', true); } // Temporarily deactivate ssl_redirect until Let's Encrypt certificate was generated if ($result['letsencrypt'] != $letsencrypt && $ssl_redirect > 0 && $letsencrypt == 1) { $ssl_redirect = 2; } $idna_convert = new IdnaWrapper(); if ($documentroot != $result['documentroot']) { if (preg_match('/^https?\:\/\//', $documentroot)) { $encoded = $idna_convert->encode($documentroot); if (!Validate::validateUrl($encoded, true)) { Response::standardError('invaliddocumentrooturl', '', true); } $documentroot = $encoded; } else { if (substr($documentroot, 0, 1) != "/") { $documentroot = $customer['documentroot'] . '/' . $documentroot; } if (strpos($documentroot, ':') !== false) { Response::standardError('pathmaynotcontaincolon', '', true); } $documentroot = FileDir::makeCorrectDir($documentroot); } } if ($email_only == '1') { $isemaildomain = '1'; } else { $email_only = '0'; } if ($subcanemaildomain != '1' && $subcanemaildomain != '2' && $subcanemaildomain != '3') { $subcanemaildomain = '0'; } $aliasdomain_check = [ 'id' => 0 ]; if ($aliasdomain != 0) { // Overwrite given ipandports with these of the "main" domain $ipandports = []; $ssl_ipandports = []; $origipresult_stmt = Database::prepare(" SELECT `id_ipandports` FROM `" . TABLE_DOMAINTOIP . "` WHERE `id_domain` = :aliasdomain "); Database::pexecute($origipresult_stmt, [ 'aliasdomain' => $aliasdomain ], true, true); $ipdata_stmt = Database::prepare("SELECT * FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE `id` = :ipid"); while ($origip = $origipresult_stmt->fetch(PDO::FETCH_ASSOC)) { $_origip_tmp = Database::pexecute_first($ipdata_stmt, [ 'ipid' => $origip['id_ipandports'] ], true, true); if ($_origip_tmp['ssl'] == 0) { $ipandports[] = $origip['id_ipandports']; } else { $ssl_ipandports[] = $origip['id_ipandports']; } } if (count($ssl_ipandports) == 0) { // we need this for the json_encode // if ssl is disabled or no ssl-ip/port exists $ssl_ipandports[] = -1; } $aliasdomain_check_stmt = Database::prepare(" SELECT `d`.`id` FROM `" . TABLE_PANEL_DOMAINS . "` `d`, `" . TABLE_PANEL_CUSTOMERS . "` `c` WHERE `d`.`customerid` = :customerid AND `d`.`aliasdomain` IS NULL AND `d`.`id` <> `c`.`standardsubdomain` AND `c`.`customerid` = :customerid AND `d`.`id` = :aliasdomain "); $aliasdomain_check = Database::pexecute_first($aliasdomain_check_stmt, [ 'customerid' => $customerid, 'aliasdomain' => $aliasdomain ], true, true); } if (count($ipandports) == 0) { Response::standardError('noipportgiven', '', true); } if ($aliasdomain_check['id'] != $aliasdomain) { Response::standardError('domainisaliasorothercustomer', '', true); } if ($serveraliasoption != '1' && $serveraliasoption != '2') { $serveraliasoption = '0'; } $wwwserveralias = ($serveraliasoption == '1') ? '1' : '0'; $iswildcarddomain = ($serveraliasoption == '0') ? '1' : '0'; if ($documentroot != $result['documentroot'] || $ssl_redirect != $result['ssl_redirect'] || $wwwserveralias != $result['wwwserveralias'] || $iswildcarddomain != $result['iswildcarddomain'] || $phpenabled != $result['phpenabled'] || $openbasedir != $result['openbasedir'] || $openbasedir_path != $result['openbasedir_path'] || $phpsettingid != $result['phpsettingid'] || $mod_fcgid_starter != $result['mod_fcgid_starter'] || $mod_fcgid_maxrequests != $result['mod_fcgid_maxrequests'] || $specialsettings != $result['specialsettings'] || $ssl_specialsettings != $result['ssl_specialsettings'] || $notryfiles != $result['notryfiles'] || $writeaccesslog != $result['writeaccesslog'] || $writeerrorlog != $result['writeerrorlog'] || $aliasdomain != $result['aliasdomain'] || $email_only != $result['email_only'] || ($speciallogfile != $result['speciallogfile'] && $speciallogverified == '1') || $letsencrypt != $result['letsencrypt'] || $http2 != $result['http2'] || $http3 != $result['http3'] || $hsts_maxage != $result['hsts'] || $hsts_sub != $result['hsts_sub'] || $hsts_preload != $result['hsts_preload'] || $ocsp_stapling != $result['ocsp_stapling'] || $sslenabled != $result['ssl_enabled'] || $override_tls != $result['override_tls'] || implode(",", $ssl_protocols) != $result['ssl_protocols'] ) { Cronjob::inserttask(TaskId::REBUILD_VHOST); } if ($dkim != $result['dkim']) { Cronjob::inserttask(TaskId::REBUILD_RSPAMD); } if ($speciallogfile != $result['speciallogfile'] && $speciallogverified != '1') { $speciallogfile = $result['speciallogfile']; } if ($isbinddomain != $result['isbinddomain'] || $zonefile != $result['zonefile'] || $dkim != $result['dkim'] || $isemaildomain != $result['isemaildomain']) { Cronjob::inserttask(TaskId::REBUILD_DNS); } // check whether nameserver has been disabled, #581 if ($isbinddomain != $result['isbinddomain'] && $isbinddomain == 0) { Cronjob::inserttask(TaskId::DELETE_DOMAIN_PDNS, $result['domain']); } if ($isemaildomain == '0' && $result['isemaildomain'] == '1') { $del_stmt = Database::prepare(" DELETE FROM `" . TABLE_MAIL_USERS . "` WHERE `domainid` = :id "); Database::pexecute($del_stmt, [ 'id' => $id ], true, true); $del_stmt = Database::prepare(" DELETE FROM `" . TABLE_MAIL_VIRTUAL . "` WHERE `domainid` = :id "); Database::pexecute($del_stmt, [ 'id' => $id ], true, true); $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] deleted domain #" . $id . " from mail-tables as is-email-domain was set to 0"); } // check whether LE has been disabled, so we remove the certificate if ($letsencrypt == '0' && $result['letsencrypt'] == '1') { $del_stmt = Database::prepare(" DELETE FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` WHERE `domainid` = :id "); Database::pexecute($del_stmt, [ 'id' => $id ], true, true); // remove domain from acme.sh / lets encrypt if used Cronjob::inserttask(TaskId::DELETE_DOMAIN_SSL, $result['domain']); } $updatechildren = ''; if ($subcanemaildomain == '0' && $result['subcanemaildomain'] != '0') { $updatechildren = ", `isemaildomain` = '0' "; } elseif ($subcanemaildomain == '3' && $result['subcanemaildomain'] != '3') { $updatechildren = ", `isemaildomain` = '1' "; } if ($customerid != $result['customerid'] && Settings::Get('panel.allow_domain_change_customer') == '1') { $upd_data = [ 'customerid' => $customerid, 'domainid' => $result['id'] ]; $upd_stmt = Database::prepare(" UPDATE `" . TABLE_MAIL_USERS . "` SET `customerid` = :customerid WHERE `domainid` = :domainid "); Database::pexecute($upd_stmt, $upd_data, true, true); $upd_stmt = Database::prepare(" UPDATE `" . TABLE_MAIL_VIRTUAL . "` SET `customerid` = :customerid WHERE `domainid` = :domainid "); Database::pexecute($upd_stmt, $upd_data, true, true); $upd_data = [ 'subdomains' => $subdomains, 'emails' => $emails, 'forwarders' => $email_forwarders, 'accounts' => $email_accounts ]; $upd_data['customerid'] = $customerid; $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `subdomains_used` = `subdomains_used` + :subdomains, `emails_used` = `emails_used` + :emails, `email_forwarders_used` = `email_forwarders_used` + :forwarders, `email_accounts_used` = `email_accounts_used` + :accounts WHERE `customerid` = :customerid "); Database::pexecute($upd_stmt, $upd_data, true, true); $upd_data['customerid'] = $result['customerid']; $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `subdomains_used` = `subdomains_used` - :subdomains, `emails_used` = `emails_used` - :emails, `email_forwarders_used` = `email_forwarders_used` - :forwarders, `email_accounts_used` = `email_accounts_used` - :accounts WHERE `customerid` = :customerid "); Database::pexecute($upd_stmt, $upd_data, true, true); } if ($adminid != $result['adminid'] && Settings::Get('panel.allow_domain_change_admin') == '1') { $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_ADMINS . "` SET `domains_used` = `domains_used` + 1 WHERE `adminid` = :adminid "); Database::pexecute($upd_stmt, [ 'adminid' => $adminid ], true, true); $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_ADMINS . "` SET `domains_used` = `domains_used` - 1 WHERE `adminid` = :adminid "); Database::pexecute($upd_stmt, [ 'adminid' => $result['adminid'] ], true, true); } $_update_data = []; if ($ssfs == 1) { $_update_data['specialsettings'] = $specialsettings; $_update_data['ssl_specialsettings'] = $ssl_specialsettings; $_update_data['include_specialsettings'] = $include_specialsettings; $upd_specialsettings = ", `specialsettings` = :specialsettings, `ssl_specialsettings` = :ssl_specialsettings, `include_specialsettings` = :include_specialsettings "; } else { $upd_specialsettings = ''; $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_DOMAINS . "` SET `specialsettings`='', `ssl_specialsettings`='', `include_specialsettings`='0' WHERE `parentdomainid` = :id "); Database::pexecute($upd_stmt, [ 'id' => $id ], true, true); $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] removed specialsettings on all subdomains of domain #" . $id); } $wwwserveralias = ($serveraliasoption == '1') ? '1' : '0'; $iswildcarddomain = ($serveraliasoption == '0') ? '1' : '0'; $update_data = []; $update_data['customerid'] = $customerid; $update_data['adminid'] = $adminid; $update_data['documentroot'] = $documentroot; $update_data['ssl_redirect'] = $ssl_redirect; $update_data['aliasdomain'] = ($aliasdomain != 0 && $alias_check == 0) ? $aliasdomain : null; $update_data['isbinddomain'] = $isbinddomain; $update_data['isemaildomain'] = $isemaildomain; $update_data['email_only'] = $email_only; $update_data['subcanemaildomain'] = $subcanemaildomain; $update_data['dkim'] = $dkim; $update_data['caneditdomain'] = $caneditdomain; $update_data['zonefile'] = $zonefile; $update_data['wwwserveralias'] = $wwwserveralias; $update_data['iswildcarddomain'] = $iswildcarddomain; $update_data['phpenabled'] = $phpenabled; $update_data['openbasedir'] = $openbasedir; $update_data['openbasedir_path'] = $openbasedir_path; $update_data['speciallogfile'] = $speciallogfile; $update_data['phpsettingid'] = $phpsettingid; $update_data['mod_fcgid_starter'] = $mod_fcgid_starter; $update_data['mod_fcgid_maxrequests'] = $mod_fcgid_maxrequests; $update_data['specialsettings'] = $specialsettings; $update_data['ssl_specialsettings'] = $ssl_specialsettings; $update_data['include_specialsettings'] = $include_specialsettings; $update_data['notryfiles'] = $notryfiles; $update_data['writeaccesslog'] = $writeaccesslog; $update_data['writeerrorlog'] = $writeerrorlog; $update_data['registration_date'] = $registration_date; $update_data['termination_date'] = $termination_date; $update_data['letsencrypt'] = $letsencrypt; $update_data['http2'] = $http2; $update_data['http3'] = $http3; $update_data['hsts'] = $hsts_maxage; $update_data['hsts_sub'] = $hsts_sub; $update_data['hsts_preload'] = $hsts_preload; $update_data['ocsp_stapling'] = $ocsp_stapling; $update_data['override_tls'] = $override_tls; $update_data['ssl_protocols'] = implode(",", $ssl_protocols); $update_data['ssl_cipher_list'] = $ssl_cipher_list; $update_data['tlsv13_cipher_list'] = $tlsv13_cipher_list; $update_data['sslenabled'] = $sslenabled; $update_data['honorcipherorder'] = $honorcipherorder; $update_data['sessiontickets'] = $sessiontickets; $update_data['description'] = $description; $update_data['deactivated'] = $deactivated; $update_data['id'] = $id; $update_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_DOMAINS . "` SET `customerid` = :customerid, `adminid` = :adminid, `documentroot` = :documentroot, `ssl_redirect` = :ssl_redirect, `aliasdomain` = :aliasdomain, `isbinddomain` = :isbinddomain, `isemaildomain` = :isemaildomain, `email_only` = :email_only, `subcanemaildomain` = :subcanemaildomain, `dkim` = :dkim, `caneditdomain` = :caneditdomain, `zonefile` = :zonefile, `wwwserveralias` = :wwwserveralias, `iswildcarddomain` = :iswildcarddomain, `phpenabled` = :phpenabled, `openbasedir` = :openbasedir, `openbasedir_path` = :openbasedir_path, `speciallogfile` = :speciallogfile, `phpsettingid` = :phpsettingid, `mod_fcgid_starter` = :mod_fcgid_starter, `mod_fcgid_maxrequests` = :mod_fcgid_maxrequests, `specialsettings` = :specialsettings, `ssl_specialsettings` = :ssl_specialsettings, `include_specialsettings` = :include_specialsettings, `notryfiles` = :notryfiles, `writeaccesslog` = :writeaccesslog, `writeerrorlog` = :writeerrorlog, `registration_date` = :registration_date, `termination_date` = :termination_date, `letsencrypt` = :letsencrypt, `http2` = :http2, `http3` = :http3, `hsts` = :hsts, `hsts_sub` = :hsts_sub, `hsts_preload` = :hsts_preload, `ocsp_stapling` = :ocsp_stapling, `override_tls` = :override_tls, `ssl_protocols` = :ssl_protocols, `ssl_cipher_list` = :ssl_cipher_list, `tlsv13_cipher_list` = :tlsv13_cipher_list, `ssl_enabled` = :sslenabled, `ssl_honorcipherorder` = :honorcipherorder, `ssl_sessiontickets` = :sessiontickets, `description` = :description, `deactivated` = :deactivated WHERE `id` = :id "); Database::pexecute($update_stmt, $update_data, true, true); // activate/deactivate domain-based services if ($deactivated != $result['deactivated']) { // deactivate email accounts $yesno = ($deactivated ? 'N' : 'Y'); $pop3 = ($deactivated ? '0' : (int)$customer['pop3']); $imap = ($deactivated ? '0' : (int)$customer['imap']); $upd_stmt = Database::prepare(" UPDATE `" . TABLE_MAIL_USERS . "` SET `postfix`= :yesno, `pop3` = :pop3, `imap` = :imap WHERE `customerid` = :customerid AND `domainid` = :domainid "); Database::pexecute($upd_stmt, [ 'yesno' => $yesno, 'pop3' => $pop3, 'imap' => $imap, 'customerid' => $customerid, 'domainid' => $id ]); $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] " . ($deactivated ? 'deactivated' : 'reactivated') . " domain '" . $result['domain'] . "'"); Cronjob::inserttask(TaskId::REBUILD_VHOST); } $_update_data['customerid'] = $customerid; $_update_data['adminid'] = $adminid; $_update_data['phpenabled'] = $phpenabled; $_update_data['openbasedir'] = $openbasedir; $_update_data['openbasedir_path'] = $openbasedir_path; $_update_data['mod_fcgid_starter'] = $mod_fcgid_starter; $_update_data['mod_fcgid_maxrequests'] = $mod_fcgid_maxrequests; $_update_data['notryfiles'] = $notryfiles; $_update_data['writeaccesslog'] = $writeaccesslog; $_update_data['writeerrorlog'] = $writeerrorlog; $_update_data['override_tls'] = $override_tls; $_update_data['ssl_protocols'] = implode(",", $ssl_protocols); $_update_data['ssl_cipher_list'] = $ssl_cipher_list; $_update_data['tlsv13_cipher_list'] = $tlsv13_cipher_list; $_update_data['honorcipherorder'] = $honorcipherorder; $_update_data['sessiontickets'] = $sessiontickets; $_update_data['parentdomainid'] = $id; $_update_data['deactivated'] = $deactivated; // if php config is to be set for all subdomains, check here $update_phpconfig = ''; if ($phpfs == 1) { $_update_data['phpsettingid'] = $phpsettingid; $update_phpconfig = ", `phpsettingid` = :phpsettingid"; } // if we have no more ssl-ip's for this domain, // all its subdomains must have "ssl-redirect = 0" // and disable let's encrypt $update_sslredirect = ''; if (count($ssl_ipandports) == 1 && $ssl_ipandports[0] == -1) { $update_sslredirect = ", `ssl_redirect` = '0', `letsencrypt` = '0' "; } $_update_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_DOMAINS . "` SET `customerid` = :customerid, `adminid` = :adminid, `phpenabled` = :phpenabled, `openbasedir` = :openbasedir, `openbasedir_path` = :openbasedir_path, `mod_fcgid_starter` = :mod_fcgid_starter, `mod_fcgid_maxrequests` = :mod_fcgid_maxrequests, `notryfiles` = :notryfiles, `writeaccesslog` = :writeaccesslog, `writeerrorlog` = :writeerrorlog, `override_tls` = :override_tls, `ssl_protocols` = :ssl_protocols, `ssl_cipher_list` = :ssl_cipher_list, `tlsv13_cipher_list` = :tlsv13_cipher_list, `ssl_honorcipherorder` = :honorcipherorder, `ssl_sessiontickets` = :sessiontickets, `deactivated` = :deactivated " . $update_phpconfig . $upd_specialsettings . $updatechildren . $update_sslredirect . " WHERE `parentdomainid` = :parentdomainid "); Database::pexecute($_update_stmt, $_update_data, true, true); // get current ip<>domain entries $ip_sel_stmt = Database::prepare(" SELECT id_ipandports FROM `" . TABLE_DOMAINTOIP . "` WHERE `id_domain` = :id "); Database::pexecute($ip_sel_stmt, [ 'id' => $id ], true, true); $current_ips = []; while ($cIP = $ip_sel_stmt->fetch(PDO::FETCH_ASSOC)) { $current_ips[] = $cIP['id_ipandports']; } // Cleanup domain <-> ip mapping $del_stmt = Database::prepare(" DELETE FROM `" . TABLE_DOMAINTOIP . "` WHERE `id_domain` = :id "); Database::pexecute($del_stmt, [ 'id' => $id ], true, true); $ins_stmt = Database::prepare(" INSERT INTO `" . TABLE_DOMAINTOIP . "` SET `id_domain` = :domainid, `id_ipandports` = :ipportid "); foreach ($ipandports as $ipportid) { Database::pexecute($ins_stmt, [ 'domainid' => $id, 'ipportid' => $ipportid ], true, true); } foreach ($ssl_ipandports as $ssl_ipportid) { if ($ssl_ipportid > 0) { Database::pexecute($ins_stmt, [ 'domainid' => $id, 'ipportid' => $ssl_ipportid ], true, true); } } // check ip changes $all_new_ips = array_merge($ipandports, $ssl_ipandports); if (count(array_diff($current_ips, $all_new_ips)) != 0 || count(array_diff($all_new_ips, $current_ips)) != 0) { Cronjob::inserttask(TaskId::REBUILD_VHOST); } // Cleanup domain <-> ip mapping for subdomains $domainidsresult_stmt = Database::prepare(" SELECT `id` FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `parentdomainid` = :id "); Database::pexecute($domainidsresult_stmt, [ 'id' => $id ], true, true); while ($row = $domainidsresult_stmt->fetch(PDO::FETCH_ASSOC)) { $del_stmt = Database::prepare(" DELETE FROM `" . TABLE_DOMAINTOIP . "` WHERE `id_domain` = :rowid "); Database::pexecute($del_stmt, [ 'rowid' => $row['id'] ], true, true); $ins_stmt = Database::prepare(" INSERT INTO `" . TABLE_DOMAINTOIP . "` SET `id_domain` = :rowid, `id_ipandports` = :ipportid "); foreach ($ipandports as $ipportid) { Database::pexecute($ins_stmt, [ 'rowid' => $row['id'], 'ipportid' => $ipportid ], true, true); } foreach ($ssl_ipandports as $ssl_ipportid) { if ($ssl_ipportid > 0) { Database::pexecute($ins_stmt, [ 'rowid' => $row['id'], 'ipportid' => $ssl_ipportid ], true, true); } } } if ($result['aliasdomain'] != $aliasdomain && is_numeric($result['aliasdomain'])) { // trigger when domain id for alias destination has changed: both for old and new destination Domain::triggerLetsEncryptCSRForAliasDestinationDomain($result['aliasdomain'], $this->logger()); Domain::triggerLetsEncryptCSRForAliasDestinationDomain($aliasdomain, $this->logger()); } if ($result['wwwserveralias'] != $wwwserveralias || $result['letsencrypt'] != $letsencrypt) { // or when wwwserveralias or letsencrypt was changed if ((int)$aliasdomain === 0) { // in case the wwwserveralias is set on a main domain, $aliasdomain is 0 Domain::triggerLetsEncryptCSRForAliasDestinationDomain($id, $this->logger()); } else { Domain::triggerLetsEncryptCSRForAliasDestinationDomain($aliasdomain, $this->logger()); } } $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] updated domain '" . $idna_convert->decode($result['domain']) . "'"); $result = $this->apiCall('Domains.get', [ 'domainname' => $result['domain'] ]); return $this->response($result); } throw new Exception("Not allowed to execute given command.", 403); } /** * delete a domain entry by either id or domainname * * @param int $id * optional, the domain-id * @param string $domainname * optional, the domainname * @param bool $is_stdsubdomain * optional, default false, specify whether it's a std-subdomain you are deleting as it does not count * as subdomain-resource * @param bool $delete_userfiles * optional, delete email account files on filesystem (if any), default false * * @access admin * @return string json-encoded array * @throws Exception */ public function delete() { if ($this->isAdmin()) { $id = $this->getParam('id', true, 0); $dn_optional = $id > 0; $domainname = $this->getParam('domainname', $dn_optional, ''); $is_stdsubdomain = $this->getBoolParam('is_stdsubdomain', true, 0); $delete_user_emailfiles = $this->getBoolParam('delete_userfiles', true, 0); $result = $this->apiCall('Domains.get', [ 'id' => $id, 'domainname' => $domainname ]); $id = $result['id']; $subresult_stmt = Database::prepare(" SELECT `id` FROM `" . TABLE_PANEL_DOMAINS . "` WHERE (`id` = :id OR `parentdomainid` = :id) "); Database::pexecute($subresult_stmt, [ 'id' => $id ], true, true); $idString = []; $paramString = []; while ($subRow = $subresult_stmt->fetch(PDO::FETCH_ASSOC)) { $idString[] = "`domainid` = :domain_" . (int)$subRow['id']; $paramString['domain_' . $subRow['id']] = $subRow['id']; } $idString = implode(' OR ', $idString); if ($idString != '') { if ($delete_user_emailfiles) { // determine all connected email-accounts $emailaccount_sel = Database::prepare("SELECT `email`, `homedir`, `maildir` FROM `" . TABLE_MAIL_USERS . "` WHERE " . $idString); Database::pexecute($emailaccount_sel, $paramString, true, true); while ($emailacc_row = $emailaccount_sel->fetch(PDO::FETCH_ASSOC)) { Cronjob::inserttask(TaskId::DELETE_EMAIL_DATA, $emailacc_row['email'], FileDir::makeCorrectDir($emailacc_row['homedir'] . '/' . $emailacc_row['maildir'])); } } $del_stmt = Database::prepare(" DELETE FROM `" . TABLE_MAIL_USERS . "` WHERE " . $idString); Database::pexecute($del_stmt, $paramString, true, true); $del_stmt = Database::prepare(" DELETE FROM `" . TABLE_MAIL_VIRTUAL . "` WHERE " . $idString); Database::pexecute($del_stmt, $paramString, true, true); $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] deleted domain/s from mail-tables"); } $del_stmt = Database::prepare(" DELETE FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `id` = :id OR `parentdomainid` = :id "); Database::pexecute($del_stmt, [ 'id' => $id ], true, true); $deleted_domains = $del_stmt->rowCount(); if ($is_stdsubdomain == 0) { $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `subdomains_used` = `subdomains_used` - :domaincount WHERE `customerid` = :customerid"); Database::pexecute($upd_stmt, [ 'domaincount' => ($deleted_domains - 1), 'customerid' => $result['customerid'] ], true, true); $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_ADMINS . "` SET `domains_used` = `domains_used` - 1 WHERE `adminid` = :adminid"); Database::pexecute($upd_stmt, [ 'adminid' => $this->getUserDetail('adminid') ], true, true); } $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `standardsubdomain` = '0' WHERE `standardsubdomain` = :id AND `customerid` = :customerid"); Database::pexecute($upd_stmt, [ 'id' => $result['id'], 'customerid' => $result['customerid'] ], true, true); $del_stmt = Database::prepare(" DELETE FROM `" . TABLE_DOMAINTOIP . "` WHERE `id_domain` = :domainid"); Database::pexecute($del_stmt, [ 'domainid' => $id ], true, true); $del_stmt = Database::prepare(" DELETE FROM `" . TABLE_PANEL_DOMAINREDIRECTS . "` WHERE `did` = :domainid"); Database::pexecute($del_stmt, [ 'domainid' => $id ], true, true); // remove certificate from domain_ssl_settings, fixes #1596 $del_stmt = Database::prepare(" DELETE FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` WHERE `domainid` = :domainid"); Database::pexecute($del_stmt, [ 'domainid' => $id ], true, true); // remove possible existing DNS entries $del_stmt = Database::prepare(" DELETE FROM `" . TABLE_DOMAIN_DNS . "` WHERE `domain_id` = :domainid "); Database::pexecute($del_stmt, [ 'domainid' => $id ], true, true); if ((int)$result['aliasdomain'] !== 0) { Domain::triggerLetsEncryptCSRForAliasDestinationDomain($result['aliasdomain'], $this->logger()); } // remove domains DNS from powerDNS if used, #581 Cronjob::inserttask(TaskId::DELETE_DOMAIN_PDNS, $result['domain']); // remove domain from acme.sh / lets encrypt if used Cronjob::inserttask(TaskId::DELETE_DOMAIN_SSL, $result['domain']); $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] deleted domain/subdomains (#" . $result['id'] . ")"); User::updateCounters(); Cronjob::inserttask(TaskId::REBUILD_VHOST); // Using nameserver, insert a task which rebuilds the server config Cronjob::inserttask(TaskId::REBUILD_DNS); return $this->response($result); } throw new Exception("Not allowed to execute given command.", 403); } /** * duplicate domain entry by either id or domainname. All parameters from Domains.add() can be used * to overwrite source entity values if necessary. * * @param int $id * optional, the domain-id * @param string $domainname * optional, the domainname * @param string $domain * required, name of the new domain to be added * * @access admin * @return string json-encoded array * @throws Exception */ public function duplicate() { if ($this->isAdmin()) { // parameters $id = $this->getParam('id', true, 0); $dn_optional = $id > 0; $domainname = $this->getParam('domainname', $dn_optional, ''); $p_domain = $this->getParam('domain'); // get requested domain $result = $this->apiCall('Domains.get', [ 'id' => $id, 'domainname' => $domainname, ]); // clear some defaults unset($result['domain_ace']); unset($result['adminid']); unset($result['documentroot']); unset($result['registration_date']); unset($result['termination_date']); unset($result['zonefile']); // clear auto-generated values unset($result['bindserial']); unset($result['dkim_privkey']); unset($result['dkim_pubkey']); // clear api-call generated fields unset($result['domain_hascert']); // set correct ip/port information $domain_ips = $result['ipsandports']; unset($result['ipsandports']); $result['ipandport'] = []; $result['ssl_ipandport'] = []; foreach ($domain_ips as $dip) { if ($dip['ssl'] == 1) { $result['ssl_ipandport'][] = $dip['id']; } else { $result['ipandport'][] = $dip['id']; } } // check whether we are changing the customer/owner if ($this->getParam('customerid', true, 0) == 0 && $this->getParam('loginname', true, '') == '') { $customerid = $result['customerid']; } else { $customer = $this->getCustomerData(); $customerid = $customer['customerid']; } // check for alias-domain and whether it belongs to the target user if (!empty($result['aliasdomain']) && $customerid == $result['customerid']) { // duplicate alias entry $result['alias'] = $result['aliasdomain']; } unset($result['aliasdomain']); // validate possible fpm configs and whether the customer is allowed to use them if ($customerid != $result['customerid']) { $allowed_phpconfigs = json_decode($customer['allowed_phpconfigs'] ?? '[]', true); if (empty($allowed_phpconfigs)) { // system defaults unset($result['phpsettingid']); } elseif (!in_array($result['phpsettingid'], $allowed_phpconfigs)) { // use the first customer allowed config $result['phpsettingid'] = array_shift($allowed_phpconfigs); } } // translate serveralias values $result['selectserveralias'] = 2; if ((int)$result['wwwserveralias'] == 1) { $result['selectserveralias'] = 1; } elseif ((int)$result['iswildcarddomain'] == 1) { $result['selectserveralias'] = 0; } unset($result['wwwserveralias']); unset($result['iswildcarddomain']); // translate sslenabled flag $result['sslenabled'] = $result['ssl_enabled']; unset($result['ssl_enabled']); $additional_params = $this->getParamList(); // unset unneeded params from this call unset($additional_params['id']); unset($additional_params['domainname']); unset($additional_params['domain']); // set new values and merge with optional add() parameters $new_domain = array_merge($result, $additional_params); $new_domain['domain'] = $p_domain; $result_new = $this->apiCall('Domains.add', $new_domain); return $this->response($result_new); } throw new Exception("Not allowed to execute given command.", 403); } } ================================================ FILE: lib/Froxlor/Api/Commands/EmailAccounts.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Api\Commands; use Exception; use Froxlor\Api\ApiCommand; use Froxlor\Api\ResourceEntity; use Froxlor\Cron\TaskId; use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\FroxlorLogger; use Froxlor\Idna\IdnaWrapper; use Froxlor\Settings; use Froxlor\System\Cronjob; use Froxlor\System\Crypt; use Froxlor\UI\Response; use Froxlor\User; use Froxlor\Validate\Check; use Froxlor\Validate\Validate; /** * @since 0.10.0 */ class EmailAccounts extends ApiCommand implements ResourceEntity { /** * add a new email account for a given email-address either by id or emailaddr * * @param int $id * optional email-address-id of email-address to add the account for * @param string $emailaddr * optional email-address to add the account for * @param int $customerid * optional, required when called as admin (if $loginname is not specified) * @param string $loginname * optional, required when called as admin (if $customerid is not specified) * @param string $email_password * password for the account * @param string $alternative_email * optional email address to send account information to, default is the account that is being created * @param int $email_quota * optional quota if enabled in MB, default setting: system.mail_quota * @param bool $sendinfomail * optional, sends the welcome message to the new account (needed for creation, without the user won't * be able to login before any mail is received), default 1 (true) * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function add() { if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'email')) { throw new Exception("You cannot access this resource", 405); } if ($this->getUserDetail('email_accounts_used') < $this->getUserDetail('email_accounts') || $this->getUserDetail('email_accounts') == '-1') { // parameter $id = $this->getParam('id', true, 0); $ea_optional = $id > 0; $emailaddr = $this->getParam('emailaddr', $ea_optional, ''); $email_password = $this->getParam('email_password'); $alternative_email = $this->getParam('alternative_email', true, ''); $quota = $this->getParam('email_quota', true, Settings::Get('system.mail_quota') ?? 0); $sendinfomail = $this->getBoolParam('sendinfomail', true, 1); // validation $quota = Validate::validate($quota, 'email_quota', '/^\d+$/', 'vmailquotawrong', [], true); // get needed customer info to reduce the email-account-counter by one $customer = $this->getCustomerData('email_accounts'); // check for imap||pop3 == 1, see #1298 // d00p, 6.5.2023 @revert this - if a customer has resources which allow email accounts // it implicitly allowed SMTP, e.g. sending of emails which also requires an account to exist /* if ($customer['imap'] != '1' && $customer['pop3'] != '1') { Response::standardError('notallowedtouseaccounts', '', true); } */ if (!empty($emailaddr)) { $idna_convert = new IdnaWrapper(); $emailaddr = $idna_convert->encode($emailaddr); } // get email address $result = $this->apiCall('Emails.get', [ 'id' => $id, 'emailaddr' => $emailaddr ]); $id = $result['id']; $idna_convert = new IdnaWrapper(); $email_full = $result['email_full']; $username = $email_full; $password = Validate::validate($email_password, 'password', '', '', [], true); $password = Crypt::validatePassword($password, true); if ($result['popaccountid'] != 0) { throw new Exception("Email address '" . $email_full . "' has already an account assigned.", 406); } if (Check::checkMailAccDeletionState($email_full)) { Response::standardError([ 'mailaccistobedeleted' ], $email_full, true); } // alternative email address to send info to if (Settings::Get('panel.sendalternativemail') == 1) { $alternative_email = $idna_convert->encode(Validate::validate($alternative_email, 'alternative_email', '', '', [], true)); if (!empty($alternative_email) && !Validate::validateEmail($alternative_email)) { Response::standardError('alternativeemailiswrong', $alternative_email, true); } } else { $alternative_email = ''; } // validate quota if enabled if (Settings::Get('system.mail_quota_enabled') == 1) { if ($customer['email_quota'] != '-1' && ($quota == 0 || ($quota + $customer['email_quota_used']) > $customer['email_quota'])) { Response::standardError('allocatetoomuchquota', $quota, true); } } else { // disable $quota = 0; } if ($password == $email_full) { Response::standardError('passwordshouldnotbeusername', '', true); } // prefix hash-algo switch (Settings::Get('system.passwordcryptfunc')) { case 'argon2i': $cpPrefix = '{ARGON2I}'; break; case 'argon2id': $cpPrefix = '{ARGON2ID}'; break; default: $cpPrefix = '{BLF-CRYPT}'; break; } // encrypt the password $cryptPassword = $cpPrefix . Crypt::makeCryptPassword($password); $email_user = substr($email_full, 0, strrpos($email_full, "@")); $email_domain = substr($email_full, strrpos($email_full, "@") + 1); $maildirname = trim(Settings::Get('system.vmail_maildirname')); // Add trailing slash to Maildir if needed $maildirpath = $maildirname; if (!empty($maildirname) && substr($maildirname, -1) != "/") { $maildirpath .= "/"; } // insert data $stmt = Database::prepare("INSERT INTO `" . TABLE_MAIL_USERS . "` SET `customerid` = :cid, `email` = :email, `username` = :username," . (Settings::Get('system.mailpwcleartext') == '1' ? '`password` = :password, ' : '') . " `password_enc` = :password_enc, `homedir` = :homedir, `maildir` = :maildir, `uid` = :uid, `gid` = :gid, `domainid` = :domainid, `postfix` = 'y', `quota` = :quota, `imap` = :imap, `pop3` = :pop3 "); $params = [ "cid" => $customer['customerid'], "email" => $email_full, "username" => $username, "password_enc" => $cryptPassword, "homedir" => Settings::Get('system.vmail_homedir'), "maildir" => $customer['loginname'] . '/' . $email_domain . "/" . $email_user . "/" . $maildirpath, "uid" => Settings::Get('system.vmail_uid'), "gid" => Settings::Get('system.vmail_gid'), "domainid" => $result['domainid'], "quota" => $quota, "imap" => $customer['imap'], "pop3" => $customer['pop3'] ]; if (Settings::Get('system.mailpwcleartext') == '1') { $params["password"] = $password; } Database::pexecute($stmt, $params, true, true); $popaccountid = Database::lastInsertId(); // add email address to its destination field $result['destination'] .= ' ' . $email_full; $stmt = Database::prepare(" UPDATE `" . TABLE_MAIL_VIRTUAL . "` SET `destination` = :destination, `popaccountid` = :popaccountid WHERE `customerid`= :cid AND `id`= :id "); $params = [ "destination" => FileDir::makeCorrectDestination($result['destination']), "popaccountid" => $popaccountid, "cid" => $customer['customerid'], "id" => $id ]; Database::pexecute($stmt, $params, true, true); // update customer usage Customers::increaseUsage($customer['customerid'], 'email_accounts_used'); Customers::increaseUsage($customer['customerid'], 'email_quota_used', '', $quota); if ($sendinfomail) { // replacer array for mail to create account on server $replace_arr = [ 'EMAIL' => $email_full, 'PASSWORD' => htmlentities(htmlentities($password)), 'SALUTATION' => User::getCorrectUserSalutation($customer), 'NAME' => $customer['name'], 'FIRSTNAME' => $customer['firstname'], 'COMPANY' => $customer['company'], 'USERNAME' => $customer['loginname'], 'CUSTOMER_NO' => $customer['customernumber'] ]; // get the customers admin $stmt = Database::prepare("SELECT `name`, `email` FROM `" . TABLE_PANEL_ADMINS . "` WHERE `adminid`= :adminid"); $admin = Database::pexecute_first($stmt, [ "adminid" => $customer['adminid'] ]); // get template for mail subject $mail_subject = $this->getMailTemplate($customer, 'mails', 'pop_success_subject', $replace_arr, lng('mails.pop_success.subject')); // get template for mail body $mail_body = $this->getMailTemplate($customer, 'mails', 'pop_success_mailbody', $replace_arr, lng('mails.pop_success.mailbody')); $_mailerror = false; $mailerr_msg = ""; try { $this->mailer()->setFrom(Settings::Get('panel.adminmail'), User::getCorrectUserSalutation($admin)); $this->mailer()->clearReplyTos(); $this->mailer()->addReplyTo($admin['email'], User::getCorrectUserSalutation($admin)); $this->mailer()->Subject = $mail_subject; $this->mailer()->AltBody = $mail_body; $this->mailer()->Body = str_replace("\n", "
", $mail_body); $this->mailer()->addAddress($email_full); $this->mailer()->send(); } catch (\PHPMailer\PHPMailer\Exception $e) { $mailerr_msg = $e->errorMessage(); $_mailerror = true; } catch (Exception $e) { $mailerr_msg = $e->getMessage(); $_mailerror = true; } if ($_mailerror) { $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_ERR, "[API] Error sending mail: " . $mailerr_msg); Response::standardError('errorsendingmail', $email_full, true); } $this->mailer()->clearAddresses(); // customer wants to send the e-mail to an alternative email address too if (Settings::Get('panel.sendalternativemail') == 1 && !empty($alternative_email)) { // get template for mail subject $mail_subject = $this->getMailTemplate($customer, 'mails', 'pop_success_alternative_subject', $replace_arr, lng('mails.pop_success_alternative.subject')); // get template for mail body $mail_body = $this->getMailTemplate($customer, 'mails', 'pop_success_alternative_mailbody', $replace_arr, lng('mails.pop_success_alternative.mailbody')); $_mailerror = false; try { $this->mailer()->setFrom(Settings::Get('panel.adminmail'), User::getCorrectUserSalutation($admin)); $this->mailer()->clearReplyTos(); $this->mailer()->addReplyTo($admin['email'], User::getCorrectUserSalutation($admin)); $this->mailer()->Subject = $mail_subject; $this->mailer()->AltBody = $mail_body; $this->mailer()->msgHTML(str_replace("\n", "
", $mail_body)); $this->mailer()->addAddress($idna_convert->encode($alternative_email), User::getCorrectUserSalutation($customer)); $this->mailer()->send(); } catch (\PHPMailer\PHPMailer\Exception $e) { $mailerr_msg = $e->errorMessage(); $_mailerror = true; } catch (Exception $e) { $mailerr_msg = $e->getMessage(); $_mailerror = true; } if ($_mailerror) { $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_ERR, "[API] Error sending mail: " . $mailerr_msg); Response::standardError([ 'errorsendingmail' ], $alternative_email, true); } $this->mailer()->clearAddresses(); } } $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] added email account for '" . $result['email_full'] . "'"); $result = $this->apiCall('Emails.get', [ 'emailaddr' => $result['email_full'] ]); return $this->response($result); } throw new Exception("No more resources available", 406); } /** * You cannot directly get an email account. * You need to call Emails.get() */ public function get() { throw new Exception('You cannot directly get an email account. You need to call Emails.get()', 303); } /** * update email-account entry for given email-address by either id or email-address * * @param int $id * optional, the email-address-id * @param string $emailaddr * optional, the email-address to update * @param int $customerid * optional, required when called as admin (if $loginname is not specified) * @param string $loginname * optional, required when called as admin (if $customerid is not specified) * @param int $email_quota * optional, update quota * @param string $email_password * optional, update password * @param bool $deactivated * optional, admin-only * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function update() { if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'email')) { throw new Exception("You cannot access this resource", 405); } // parameter $id = $this->getParam('id', true, 0); $ea_optional = $id > 0; $emailaddr = $this->getParam('emailaddr', $ea_optional, ''); if (!empty($emailaddr)) { $idna_convert = new IdnaWrapper(); $emailaddr = $idna_convert->encode($emailaddr); } // validation $result = $this->apiCall('Emails.get', [ 'id' => $id, 'emailaddr' => $emailaddr ]); $id = $result['id']; if (empty($result['popaccountid']) || $result['popaccountid'] == 0) { throw new Exception("Email address '" . $result['email_full'] . "' has no account assigned.", 406); } $password = $this->getParam('email_password', true, ''); $quota = $this->getParam('email_quota', true, $result['quota']); $deactivated = $this->getBoolParam('deactivated', true, strtolower($result['postfix']) == 'n'); // get needed customer info to reduce the email-account-counter by one $customer = $this->getCustomerData(); // validation $quota = Validate::validate($quota, 'email_quota', '/^\d+$/', 'vmailquotawrong', [], true); $upd_query = ""; $upd_params = [ "id" => $result['popaccountid'], "cid" => $customer['customerid'] ]; if (!empty($password)) { if ($password == $result['email_full']) { Response::standardError('passwordshouldnotbeusername', '', true); } $password = Crypt::validatePassword($password, true); // prefix hash-algo switch (Settings::Get('system.passwordcryptfunc')) { case 'argon2i': $cpPrefix = '{ARGON2I}'; break; case 'argon2id': $cpPrefix = '{ARGON2ID}'; break; default: $cpPrefix = '{BLF-CRYPT}'; break; } // encrypt the password $cryptPassword = $cpPrefix . Crypt::makeCryptPassword($password); $upd_query .= (Settings::Get('system.mailpwcleartext') == '1' ? "`password` = :password, " : '') . "`password_enc`= :password_enc"; $upd_params['password_enc'] = $cryptPassword; if (Settings::Get('system.mailpwcleartext') == '1') { $upd_params['password'] = $password; } } if (Settings::Get('system.mail_quota_enabled') == 1) { if ($quota != $result['quota']) { if ($customer['email_quota'] != '-1' && ($quota == 0 || ($quota + $customer['email_quota_used'] - $result['quota']) > $customer['email_quota'])) { Response::standardError('allocatetoomuchquota', $quota, true); } if (!empty($upd_query)) { $upd_query .= ", "; } $upd_query .= "`quota` = :quota"; $upd_params['quota'] = $quota; } } else { // disable $quota = 0; } if ($this->isAdmin()) { if (($deactivated == true && strtolower($result['postfix']) == 'y') || ($deactivated == false && strtolower($result['postfix']) == 'n')) { if (!empty($upd_query)) { $upd_query .= ", "; } $upd_query .= "`postfix` = :postfix, `imap` = :imap, `pop3` = :pop3"; $upd_params['postfix'] = $deactivated ? 'N' : 'Y'; $upd_params['imap'] = $deactivated ? '0' : '1'; $upd_params['pop3'] = $deactivated ? '0' : '1'; } } // build update query if (!empty($upd_query)) { $upd_stmt = Database::prepare(" UPDATE `" . TABLE_MAIL_USERS . "` SET " . $upd_query . " WHERE `id` = :id AND `customerid`= :cid "); Database::pexecute($upd_stmt, $upd_params, true, true); } if ($customer['email_quota'] != '-1') { Customers::increaseUsage($customer['customerid'], 'email_quota_used', '', ($quota - $result['quota'])); Admins::increaseUsage($customer['adminid'], 'email_quota_used', '', ($quota - $result['quota'])); } $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] updated email account '" . $result['email_full'] . "'"); $result = $this->apiCall('Emails.get', [ 'emailaddr' => $result['email_full'] ]); return $this->response($result); } /** * You cannot directly list email accounts. * You need to call Emails.listing() */ public function listing() { throw new Exception('You cannot directly list email accounts. You need to call Emails.listing()', 303); } /** * You cannot directly count email accounts. * You need to call Emails.listingCount() */ public function listingCount() { throw new Exception('You cannot directly count email accounts. You need to call Emails.listingCount()', 303); } /** * delete email-account entry for given email-address by either id or email-address * * @param int $id * optional, the email-address-id * @param string $emailaddr * optional, the email-address to delete the account for * @param int $customerid * optional, required when called as admin (if $loginname is not specified) * @param string $loginname * optional, required when called as admin (if $customerid is not specified) * @param bool $delete_userfiles * optional, default false * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function delete() { if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'email')) { throw new Exception("You cannot access this resource", 405); } // parameter $id = $this->getParam('id', true, 0); $ea_optional = $id > 0; $emailaddr = $this->getParam('emailaddr', $ea_optional, ''); $delete_userfiles = $this->getBoolParam('delete_userfiles', true, 0); // validation $result = $this->apiCall('Emails.get', [ 'id' => $id, 'emailaddr' => $emailaddr ], true); $id = $result['id']; if (empty($result['popaccountid']) || $result['popaccountid'] == 0) { throw new Exception("Email address '" . $result['email_full'] . "' has no account assigned.", 406); } // get needed customer info to reduce the email-account-counter by one $customer = $this->getCustomerData(); // delete entry $stmt = Database::prepare(" DELETE FROM `" . TABLE_MAIL_USERS . "` WHERE `customerid`= :cid AND `id`= :id "); Database::pexecute($stmt, [ "cid" => $customer['customerid'], "id" => $result['popaccountid'] ], true, true); // update mail-virtual entry $result['destination'] = str_replace($result['email_full'], '', $result['destination']); $stmt = Database::prepare(" UPDATE `" . TABLE_MAIL_VIRTUAL . "` SET `destination` = :dest, `popaccountid` = '0' WHERE `customerid`= :cid AND `id`= :id "); $params = [ "dest" => FileDir::makeCorrectDestination($result['destination']), "cid" => $customer['customerid'], "id" => $id ]; Database::pexecute($stmt, $params, true, true); $result['popaccountid'] = 0; if (Settings::Get('system.mail_quota_enabled') == '1' && $customer['email_quota'] != '-1') { $quota = (int)$result['quota']; } else { $quota = 0; } if ($delete_userfiles) { Cronjob::inserttask(TaskId::DELETE_EMAIL_DATA, $customer['loginname'], FileDir::makeCorrectDir($result['homedir'] . '/' . $result['maildir'])); } // decrease usage for customer Customers::decreaseUsage($customer['customerid'], 'email_accounts_used'); Customers::decreaseUsage($customer['customerid'], 'email_quota_used', '', $quota); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_WARNING, "[API] deleted email account for '" . $result['email_full'] . "'"); return $this->response($result); } } ================================================ FILE: lib/Froxlor/Api/Commands/EmailDomains.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Api\Commands; use Exception; use Froxlor\Api\ApiCommand; use Froxlor\Api\ResourceEntity; use Froxlor\Database\Database; use Froxlor\FroxlorLogger; use Froxlor\Settings; use PDO; /** * @since 2.0 */ class EmailDomains extends ApiCommand implements ResourceEntity { /** * list all domains with email addresses connected to it. * If called from an admin, list all domains with email addresses * connected to it from all customers you are allowed to view, or * specify id or loginname for one specific customer * * @param int $customerid * optional, admin-only, select email addresses of a specific customer by id * @param string $loginname * optional, admin-only, select email addresses of a specific customer by loginname * @param array $sql_search * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), * LIKE is used if left empty and 'value' => searchvalue * @param int $sql_limit * optional specify number of results to be returned * @param int $sql_offset * optional specify offset for resultset * @param array $sql_orderby * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more * fields * * @access admin, customer * @return string json-encoded array count|list * @throws Exception */ public function listing() { $customer_ids = $this->getAllowedCustomerIds('email'); $result = []; $query_fields = []; $result_stmt = Database::prepare(" SELECT DISTINCT d.domain, d.domain_ace, e.domainid, COUNT(e.email) as addresses, IFNULL(SUM(CASE WHEN e.popaccountid > 0 THEN 1 ELSE 0 END), 0) as accounts, IFNULL(SUM( CASE WHEN LENGTH(REPLACE(e.destination, CONCAT(e.email_full, ' '), '')) - LENGTH(REPLACE(REPLACE(e.destination, CONCAT(e.email_full, ' '), ''), ' ', '')) > 0 THEN LENGTH(REPLACE(e.destination, CONCAT(e.email_full, ' '), '')) - LENGTH(REPLACE(REPLACE(e.destination, CONCAT(e.email_full, ' '), ''), ' ', '')) WHEN e.destination <> e.email_full THEN 1 ELSE 0 END ), 0) as forwarder FROM `" . TABLE_MAIL_VIRTUAL . "` e LEFT JOIN `" . TABLE_PANEL_DOMAINS . "` d ON d.id = e.domainid WHERE e.customerid IN (" . implode(", ", $customer_ids) . ") AND d.domain IS NOT NULL " . $this->getSearchWhere($query_fields, true) . " GROUP BY e.domainid " . $this->getOrderBy() . $this->getLimit()); Database::pexecute($result_stmt, $query_fields, true, true); while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $result[] = $row; } $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] list email-domains"); return $this->response([ 'count' => count($result), 'list' => $result ]); } /** * returns the total number of accessible domains with email addresses connected to * * @param int $customerid * optional, admin-only, select email addresses of a specific customer by id * @param string $loginname * optional, admin-only, select email addresses of a specific customer by loginname * * @access admin, customer * @return string json-encoded response message * @throws Exception */ public function listingCount() { $customer_ids = $this->getAllowedCustomerIds('email'); $query_fields = []; $result_stmt = Database::prepare(" SELECT COUNT(DISTINCT d.domain) as num_emaildomains FROM `" . TABLE_MAIL_VIRTUAL . "` e LEFT JOIN `" . TABLE_PANEL_DOMAINS . "` d ON d.id = e.domainid WHERE e.customerid IN (" . implode(", ", $customer_ids) . ") AND d.domain IS NOT NULL " . $this->getSearchWhere($query_fields, true)); $result = Database::pexecute_first($result_stmt, $query_fields, true, true); if ($result) { return $this->response($result['num_emaildomains']); } return $this->response(0); } /** * You cannot directly access email-domains * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function get() { if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'email')) { throw new Exception("You cannot access this resource", 405); } throw new Exception('You cannot directly access this resource.', 303); } /** * You cannot directly add email-domains * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function add() { if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'email')) { throw new Exception("You cannot access this resource", 405); } throw new Exception('You cannot directly add this resource.', 303); } /** * toggle catchall flag of given email address either by id or email-address * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function update() { if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'email')) { throw new Exception("You cannot access this resource", 405); } throw new Exception('You cannot directly update this resource.', 303); } /** * You cannot directly delete email-domains * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function delete() { if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'email')) { throw new Exception("You cannot access this resource", 405); } throw new Exception('You cannot directly delete this resource.', 303); } } ================================================ FILE: lib/Froxlor/Api/Commands/EmailForwarders.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Api\Commands; use Exception; use Froxlor\Api\ApiCommand; use Froxlor\Api\ResourceEntity; use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\FroxlorLogger; use Froxlor\Idna\IdnaWrapper; use Froxlor\Settings; use Froxlor\UI\Response; use Froxlor\Validate\Validate; /** * @since 0.10.0 */ class EmailForwarders extends ApiCommand implements ResourceEntity { /** * add new email-forwarder entry for given email-address by either id or email-address * * @param int $id * optional, the email-address-id * @param string $emailaddr * optional, the email-address to add the forwarder for * @param int $customerid * optional, required when called as admin (if $loginname is not specified) * @param string $loginname * optional, required when called as admin (if $customerid is not specified) * @param string $destination * email-address to add as forwarder * * @access admin,customer * @return string json-encoded array * @throws Exception */ public function add() { if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'email')) { throw new Exception("You cannot access this resource", 405); } if ($this->getUserDetail('email_forwarders_used') < $this->getUserDetail('email_forwarders') || $this->getUserDetail('email_forwarders') == '-1') { // parameter $id = $this->getParam('id', true, 0); $ea_optional = $id > 0; $emailaddr = $this->getParam('emailaddr', $ea_optional, ''); $destination = $this->getParam('destination'); // validation $idna_convert = new IdnaWrapper(); $destination = $idna_convert->encode($destination); if (!empty($emailaddr)) { $idna_convert = new IdnaWrapper(); $emailaddr = $idna_convert->encode($emailaddr); } $result = $this->apiCall('Emails.get', [ 'id' => $id, 'emailaddr' => $emailaddr ]); $id = $result['id']; // current destination array $result['destination_array'] = explode(' ', ($result['destination'] ?? "")); // prepare destination $destination = trim($destination); if (!Validate::validateEmail($destination)) { Response::standardError('destinationiswrong', $destination, true); } elseif ($destination == $result['email']) { Response::standardError('destinationalreadyexistasmail', $destination, true); } elseif (in_array($destination, $result['destination_array'])) { Response::standardError('destinationalreadyexist', $destination, true); } // get needed customer info to reduce the email-forwarder-counter by one $customer = $this->getCustomerData('email_forwarders'); // add destination to address $result['destination'] .= ' ' . $destination; $stmt = Database::prepare(" UPDATE `" . TABLE_MAIL_VIRTUAL . "` SET `destination` = :dest WHERE `customerid`= :cid AND `id`= :id "); $params = [ "dest" => FileDir::makeCorrectDestination($result['destination']), "cid" => $customer['customerid'], "id" => $id ]; Database::pexecute($stmt, $params, true, true); // update customer usage Customers::increaseUsage($customer['customerid'], 'email_forwarders_used'); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] added email forwarder for '" . $result['email_full'] . "'"); $result = $this->apiCall('Emails.get', [ 'emailaddr' => $result['email_full'] ]); return $this->response($result); } throw new Exception("No more resources available", 406); } /** * You cannot directly get an email forwarder. * Try EmailForwarders.listing() */ public function get() { throw new Exception('You cannot directly get an email forwarder. Try EmailForwarders.listing()', 303); } /** * You cannot update an email forwarder. * You need to delete the entry and create a new one. */ public function update() { throw new Exception('You cannot update an email forwarder. You need to delete the entry and create a new one.', 303); } /** * List email forwarders for a given email address * * @param int $id * optional, the email-address-id * @param string $emailaddr * optional, the email-address to delete the forwarder from * @param int $customerid * optional, admin-only, the customer-id * @param string $loginname * optional, admin-only, the loginname * * @access admin,customer * @return string json-encoded array count|list * @throws Exception */ public function listing() { if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'email')) { throw new Exception("You cannot access this resource", 405); } // parameter $id = $this->getParam('id', true, 0); $ea_optional = $id > 0; $emailaddr = $this->getParam('emailaddr', $ea_optional, ''); // validation $result = $this->apiCall('Emails.get', [ 'id' => $id, 'emailaddr' => $emailaddr ]); $id = $result['id']; $result['destination'] = explode(' ', $result['destination']); $destination = []; foreach ($result['destination'] as $index => $address) { $destination[] = [ 'id' => $index, 'address' => $address ]; } return $this->response([ 'count' => count($destination), 'list' => $destination ]); } /** * count email forwarders for a given email address * * @param int $id * optional, the email-address-id * @param string $emailaddr * optional, the email-address to delete the forwarder from * @param int $customerid * optional, admin-only, the customer-id * @param string $loginname * optional, admin-only, the loginname * * @access admin,customer * @return string json-encoded response message * @throws Exception */ public function listingCount() { if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'email')) { throw new Exception("You cannot access this resource", 405); } // parameter $id = $this->getParam('id', true, 0); $ea_optional = $id > 0; $emailaddr = $this->getParam('emailaddr', $ea_optional, ''); // validation $result = $this->apiCall('Emails.get', [ 'id' => $id, 'emailaddr' => $emailaddr ]); $id = $result['id']; $result['destination'] = explode(' ', $result['destination']); return $this->response(count($result['destination'])); } /** * delete email-forwarder entry for given email-address by either id or email-address and forwarder-id * * @param int $id * optional, the email-address-id * @param string $emailaddr * optional, the email-address to delete the forwarder from * @param int $customerid * optional, required when called as admin (if $loginname is not specified) * @param string $loginname * optional, required when called as admin (if $customerid is not specified) * @param int $forwarderid * id of the forwarder to delete * * @access admin,customer * @return string json-encoded array * @throws Exception */ public function delete() { if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'email')) { throw new Exception("You cannot access this resource", 405); } // parameter $id = $this->getParam('id', true, 0); $ea_optional = $id > 0; $emailaddr = $this->getParam('emailaddr', $ea_optional, ''); $forwarderid = $this->getParam('forwarderid'); // validation $result = $this->apiCall('Emails.get', [ 'id' => $id, 'emailaddr' => $emailaddr ]); $id = $result['id']; $result['destination'] = explode(' ', $result['destination']); if (isset($result['destination'][$forwarderid]) && $result['email'] != $result['destination'][$forwarderid]) { // get needed customer info to reduce the email-forwarder-counter by one $customer = $this->getCustomerData(); // unset it from array unset($result['destination'][$forwarderid]); // rebuild destination-string $result['destination'] = implode(' ', $result['destination']); // update in DB $stmt = Database::prepare(" UPDATE `" . TABLE_MAIL_VIRTUAL . "` SET `destination` = :dest WHERE `customerid`= :cid AND `id`= :id "); $params = [ "dest" => FileDir::makeCorrectDestination($result['destination']), "cid" => $customer['customerid'], "id" => $id ]; Database::pexecute($stmt, $params, true, true); // update customer usage Customers::decreaseUsage($customer['customerid'], 'email_forwarders_used'); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] deleted email forwarder for '" . $result['email_full'] . "'"); $result = $this->apiCall('Emails.get', [ 'emailaddr' => $result['email_full'] ]); return $this->response($result); } throw new Exception("Unknown forwarder id", 404); } } ================================================ FILE: lib/Froxlor/Api/Commands/EmailSender.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Api\Commands; use Exception; use Froxlor\Api\ApiCommand; use Froxlor\Api\ResourceEntity; use Froxlor\CurrentUser; use Froxlor\Database\Database; use Froxlor\FroxlorLogger; use Froxlor\Idna\IdnaWrapper; use Froxlor\Settings; use Froxlor\UI\Response; use Froxlor\Validate\Validate; /** * @since 2.3.0 */ class EmailSender extends ApiCommand implements ResourceEntity { /** * add a new sender email address for a given email-address either by id or emailaddr * * @param int $id * optional id of email-address to add the allowed sender for (must have an account) * @param string $emailaddr * optional address of email-address to add the allowed sender for (must have an account) * @param int $customerid * optional, required when called as admin (if $loginname is not specified) * @param string $loginname * optional, required when called as admin (if $customerid is not specified) * @param string $allowed_sender * required email-address or @domain.tld notation (wildcard) of allowed sender entry for the given account * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function add() { if (Settings::Get('mail.enable_allow_sender') != '1') { throw new Exception("Allowed-sender not enabled on this system", 405); } if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'email')) { throw new Exception("You cannot access this resource", 405); } // parameter $id = $this->getParam('id', true, 0); $ea_optional = $id > 0; $emailaddr = $this->getParam('emailaddr', $ea_optional, ''); $allowed_sender = strtolower($this->getParam('allowed_sender')); // validation $idna_convert = new IdnaWrapper(); if (!empty($emailaddr)) { $emailaddr = $idna_convert->encode($emailaddr); } $result = $this->apiCall('Emails.get', [ 'id' => $id, 'emailaddr' => $emailaddr ]); $id = $result['id']; if (empty($result['popaccountid'])) { Response::standardError('emailhasnoaccount', $result['email_full'], true); } if (substr($allowed_sender, 0, 1) != '@') { if (!Validate::validateEmail($idna_convert->encode($allowed_sender))) { Response::standardError('emailiswrong', $allowed_sender, true); } self::validateLocalDomainOwnership(explode("@", $allowed_sender)[1] ?? ""); } else { if (!Validate::validateDomain($idna_convert->encode(substr($allowed_sender, 1)))) { Response::standardError('wildcardemailiswrong', substr($allowed_sender, 1), true); } self::validateLocalDomainOwnership(substr($allowed_sender, 1)); } // get needed customer info $customer = $this->getCustomerData(); // check whether account exists and if it belongs to the customer $sel_stmt = Database::prepare(" SELECT `username` FROM `" . TABLE_MAIL_USERS . "` WHERE `id` = :id AND `customerid` = :cid "); $emailaccount = Database::pexecute_first($sel_stmt, [ 'id' => (int)$result['popaccountid'], 'cid' => (int)$customer['customerid'] ]); if ($emailaccount && !empty($emailaccount['username'])) { // insert email sender $ins_stmt = Database::prepare(" INSERT IGNORE INTO `" . TABLE_MAIL_SENDER_ALIAS . "` SET `email` = :email, `allowed_sender` = :allowed_sender "); $result = [ 'email' => $emailaccount['username'], 'allowed_sender' => $allowed_sender ]; Database::pexecute($ins_stmt, $result); $result['id'] = Database::lastInsertId(); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] added email-sender alias '" . $result['email'] . "' for account '" . $result['allowed_sender'] . "'"); return $this->response($result); } throw new Exception("Email account for email-address " . $result['email_full'] . " could not be found", 404); } /** * You cannot update an email sender alias. * You need to delete the entry and create a new one. */ public function update() { throw new Exception('You cannot update an email sender alias. You need to delete the entry and create a new one.', 303); } /** * You cannot directly get an email sender alias. * Try EmailSender.listing() */ public function get() { throw new Exception('You cannot directly get an email sender alias. Try EmailSender.listing()', 303); } /** * List email senders for a given email address * * @param int $id * optional, the id of the email-address to list allowed senders from * @param string $emailaddr * optional, the email-address to list allowed senders from * @param int $customerid * optional, admin-only, the customer-id * @param string $loginname * optional, admin-only, the loginname * * @access admin,customer * @return string json-encoded array count|list * @throws Exception */ public function listing() { if (Settings::Get('mail.enable_allow_sender') != '1') { throw new Exception("Allowed-sender not enabled on this system", 405); } if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'email')) { throw new Exception("You cannot access this resource", 405); } // parameter $id = $this->getParam('id', true, 0); $ea_optional = $id > 0; $emailaddr = $this->getParam('emailaddr', $ea_optional, ''); // validation $result = $this->apiCall('Emails.get', [ 'id' => $id, 'emailaddr' => $emailaddr ]); $id = $result['id']; if (empty($result['popaccountid'])) { return $this->response([ 'count' => 0, 'list' => [] ]); } else { $sel_stmt = Database::prepare(" SELECT s.* FROM `" . TABLE_MAIL_SENDER_ALIAS . "` s LEFT JOIN `" . TABLE_MAIL_USERS . "` u ON u.username = s.email WHERE u.id = :popaccountid AND u.customerid = :cid "); Database::pexecute($sel_stmt, ['popaccountid' => (int)$result['popaccountid'], 'cid' => $result['customerid']]); $senders = []; while ($row = $sel_stmt->fetch(\PDO::FETCH_ASSOC)) { $senders[] = $row; } $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] list email-senders for '" . $result['email_full'] . "'"); return $this->response([ 'count' => count($senders), 'list' => $senders ]); } } /** * returns the total number of allowed sender addresses for a given email address * * @param int $id * optional, the id of the email-address to list allowed senders from * @param string $emailaddr * optional, the email-address to list allowed senders from * @param int $customerid * optional, admin-only, the customer-id * @param string $loginname * optional, admin-only, the loginname * * @access admin, customer * @return string json-encoded response message * @throws Exception */ public function listingCount() { if (Settings::Get('mail.enable_allow_sender') != '1') { throw new Exception("Allowed-sender not enabled on this system", 405); } if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'email')) { throw new Exception("You cannot access this resource", 405); } // parameter $id = $this->getParam('id', true, 0); $ea_optional = $id > 0; $emailaddr = $this->getParam('emailaddr', $ea_optional, ''); // validation $result = $this->apiCall('Emails.get', [ 'id' => $id, 'emailaddr' => $emailaddr ]); $id = $result['id']; if (empty($result['popaccountid'])) { return $this->response(0); } else { $sel_stmt = Database::prepare(" SELECT COUNT(*) as cnt FROM `" . TABLE_MAIL_SENDER_ALIAS . "` s LEFT JOIN `" . TABLE_MAIL_USERS . "` u ON u.username = s.email WHERE u.id = :popaccountid AND u.customerid = :cid "); $sender_cnt = Database::pexecute_first($sel_stmt, ['popaccountid' => (int)$result['popaccountid'], 'cid' => $result['customerid']]); return $this->response($sender_cnt['cnt']); } } /** * delete email-sender entry for given email-address by either id or email-address and sender-id * * @param int $id * optional, the email-address-id * @param string $emailaddr * optional, the email-address to delete the forwarder from * @param int $customerid * optional, required when called as admin (if $loginname is not specified) * @param string $loginname * optional, required when called as admin (if $customerid is not specified) * @param int $senderid * id of the sender to delete * * @access admin,customer * @return string json-encoded array * @throws Exception */ public function delete() { if (Settings::Get('mail.enable_allow_sender') != '1') { throw new Exception("Allowed-sender not enabled on this system", 405); } if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'email')) { throw new Exception("You cannot access this resource", 405); } // parameter $id = $this->getParam('id', true, 0); $ea_optional = $id > 0; $emailaddr = $this->getParam('emailaddr', $ea_optional, ''); $senderid = $this->getParam('senderid'); // validation $result = $this->apiCall('Emails.get', [ 'id' => $id, 'emailaddr' => $emailaddr ]); $id = $result['id']; if (!empty($result['popaccountid'])) { // get needed customer info $customer = $this->getCustomerData(); $sel_stmt = Database::prepare(" SELECT s.id FROM `" . TABLE_MAIL_SENDER_ALIAS . "` s LEFT JOIN `" . TABLE_MAIL_USERS . "` u ON u.username = s.email WHERE u.id = :popaccountid AND u.customerid = :cid AND s.id = :senderid "); $sender_result = Database::pexecute_first($sel_stmt, [ 'popaccountid' => (int)$result['popaccountid'], 'cid' => $customer['customerid'], 'senderid' => $senderid ]); if ($sender_result && $sender_result['id'] == $senderid) { $del_stmt = Database::prepare("DELETE FROM `" . TABLE_MAIL_SENDER_ALIAS . "` WHERE `id` = :senderid"); Database::pexecute($del_stmt, ['senderid' => $senderid]); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] deleted email sender for '" . $result['email_full'] . "'"); $result = $this->apiCall('Emails.get', [ 'emailaddr' => $result['email_full'] ]); return $this->response($result); } } throw new Exception("Unknown sender id", 404); } private static function validateLocalDomainOwnership(string $domain): void { // check whether the used domain belongs to the customer if it's a domain on this system $sel_stmt = Database::prepare("SELECT customerid FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `domain` = :domain"); $domain_result = Database::pexecute_first($sel_stmt, ['domain' => $domain]); if ($domain_result && $domain_result['customerid'] != CurrentUser::getField('customerid')) { // domain exists in our system but not owned by current user Response::standardError('senderdomainnotowned', $domain, true); } } } ================================================ FILE: lib/Froxlor/Api/Commands/Emails.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Api\Commands; use Exception; use Froxlor\Api\ApiCommand; use Froxlor\Api\ResourceEntity; use Froxlor\Cron\TaskId; use Froxlor\Database\Database; use Froxlor\FroxlorLogger; use Froxlor\Idna\IdnaWrapper; use Froxlor\Settings; use Froxlor\System\Cronjob; use Froxlor\UI\Response; use Froxlor\Validate\Validate; use PDO; /** * @since 0.10.0 */ class Emails extends ApiCommand implements ResourceEntity { /** * add a new email address * * @param string $email_part * name of the address before @ * @param string $domain * domain-name for the email-address * @param float $spam_tag_level * optional, score which is required to tag emails as spam, default: 7.0 * @param bool $rewrite_subject * optional, whether to add ***SPAM*** to the email's subject if applicable, default: [antispam.default_spam_rewrite_subject] * @param float $spam_kill_level * optional, score which is required to discard emails, default: 14.0 * @param boolean $bypass_spam * optional, disable spam-filter entirely, default: [antispam.default_bypass_spam] * @param boolean $policy_greylist * optional, enable grey-listing, default: [antispam.default_policy_greylist] * @param boolean $iscatchall * optional, make this address a catchall address, default: no * @param int $customerid * optional, required when called as admin (if $loginname is not specified) * @param string $loginname * optional, required when called as admin (if $customerid is not specified) * @param string $description * optional custom description (currently not used/shown in the frontend), default empty * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function add() { if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'email')) { throw new Exception("You cannot access this resource", 405); } if ($this->getUserDetail('emails_used') < $this->getUserDetail('emails') || $this->getUserDetail('emails') == '-1') { // required parameters $email_part = $this->getParam('email_part'); $domain = $this->getParam('domain'); // parameters $spam_tag_level = $this->getParam('spam_tag_level', true, '7.0'); $spam_kill_level = $this->getUlParam('spam_kill_level', 'spam_kill_level_ul', true, '14.0'); $iscatchall = $this->getBoolParam('iscatchall', true, 0); $description = $this->getParam('description', true, ''); if ((int)Settings::Get('antispam.default_spam_rewrite_subject') <= 2) { $rewrite_subject = $this->getBoolParam('rewrite_subject', true, (int)Settings::Get('antispam.default_spam_rewrite_subject') == 1 ? 1 : 0); } else { $rewrite_subject = (int)Settings::Get('antispam.default_spam_rewrite_subject') == 3 ? 1 : 0; } if ((int)Settings::Get('antispam.default_bypass_spam') <= 2) { $bypass_spam = $this->getBoolParam('bypass_spam', true, (int)Settings::Get('antispam.default_bypass_spam') == 1 ? 1 : 0); } else { $bypass_spam = (int)Settings::Get('antispam.default_bypass_spam') == 3 ? 1 : 0; } if ((int)Settings::Get('antispam.default_policy_greylist') <= 2) { $policy_greylist = $this->getBoolParam('policy_greylist', true, (int)Settings::Get('antispam.default_policy_greylist') == 1 ? 1 : 0); } else { $policy_greylist = (int)Settings::Get('antispam.default_policy_greylist') == 3 ? 1 : 0; } // validation $idna_convert = new IdnaWrapper(); if (substr($domain, 0, 4) != 'xn--') { $domain = $idna_convert->encode(Validate::validate($domain, 'domain', '', '', [], true)); } $email_part = $idna_convert->encode(strtolower($email_part)); // check domain and whether it's an email-enabled domain // use internal call because the customer might have 'domains' in customer_hide_options $domain_check = $this->apiCall('SubDomains.get', [ 'domainname' => $domain ], true); if ((int)$domain_check['isemaildomain'] == 0) { Response::standardError('maindomainnonexist', $idna_convert->decode($domain), true); } if ((int)$domain_check['deactivated'] == 1) { Response::standardError('maindomaindeactivated', $idna_convert->decode($domain), true); } if (Settings::Get('catchall.catchall_enabled') != '1') { $iscatchall = 0; } // check for catchall-flag if ($iscatchall) { $iscatchall = '1'; $email = '@' . $domain; } else { $iscatchall = '0'; $email = $email_part . '@' . $domain; } // full email value $email_full = $email_part . '@' . $domain; // validate it if (!Validate::validateEmail($email_full)) { Response::standardError('emailiswrong', $idna_convert->decode($email_full), true); } // get needed customer info to reduce the email-address-counter by one $customer = $this->getCustomerData('emails'); // duplicate check $stmt = Database::prepare(" SELECT `id`, `email`, `email_full`, `iscatchall`, `destination`, `customerid` FROM `" . TABLE_MAIL_VIRTUAL . "` WHERE (`email` = :email OR `email_full` = :emailfull ) AND `customerid`= :cid "); $params = [ "email" => $email, "emailfull" => $email_full, "cid" => $customer['customerid'] ]; $email_check = Database::pexecute_first($stmt, $params, true, true); if ($email_check) { if (strtolower($email_check['email_full']) == strtolower($email_full)) { Response::standardError('emailexistalready', $idna_convert->decode($email_full), true); } elseif ($email_check['email'] == $email) { Response::standardError('youhavealreadyacatchallforthisdomain', '', true); } } $spam_tag_level = Validate::validate($spam_tag_level, 'spam_tag_level', '/^\d{1,}(\.\d{1})?$/', '', [7.0], true); if ($spam_kill_level > -1) { $spam_kill_level = Validate::validate($spam_kill_level, 'spam_kill_level', '/^\d{1,}(\.\d{1})?$/', '', [14.0], true); } $description = Validate::validate(trim($description), 'description', Validate::REGEX_DESC_TEXT, '', [], true); $stmt = Database::prepare(" INSERT INTO `" . TABLE_MAIL_VIRTUAL . "` SET `customerid` = :cid, `email` = :email, `email_full` = :email_full, `spam_tag_level` = :spam_tag_level, `rewrite_subject` = :rewrite_subject, `spam_kill_level` = :spam_kill_level, `bypass_spam` = :bypass_spam, `policy_greylist` = :policy_greylist, `iscatchall` = :iscatchall, `domainid` = :domainid, `description` = :description "); $params = [ "cid" => $customer['customerid'], "email" => $email, "email_full" => $email_full, "spam_tag_level" => $spam_tag_level, "rewrite_subject" => $rewrite_subject, "spam_kill_level" => $spam_kill_level, "bypass_spam" => $bypass_spam, "policy_greylist" => $policy_greylist, "iscatchall" => $iscatchall, "domainid" => $domain_check['id'], "description" => $description ]; Database::pexecute($stmt, $params, true, true); // update customer usage Customers::increaseUsage($customer['customerid'], 'emails_used'); Cronjob::inserttask(TaskId::REBUILD_RSPAMD); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] added email address '" . $email_full . "'"); $result = $this->apiCall('Emails.get', [ 'emailaddr' => $email_full ]); return $this->response($result); } throw new Exception("No more resources available", 406); } /** * return a email-address entry by either id or email-address * * @param int $id * optional, the email-address-id * @param string $emailaddr * optional, the email-address * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function get() { $id = $this->getParam('id', true, 0); $ea_optional = $id > 0; $emailaddr = $this->getParam('emailaddr', $ea_optional, ''); $params = []; $customer_ids = $this->getAllowedCustomerIds('email'); $params['idea'] = ($id <= 0 ? $emailaddr : $id); $result_stmt = Database::prepare("SELECT v.*, u.`quota`, u.`imap`, u.`pop3`, u.`postfix`, u.`mboxsize` " . ($this->isInternal() ? ", `u`.`homedir`, `u`.`maildir`" : "") . " FROM `" . TABLE_MAIL_VIRTUAL . "` v LEFT JOIN `" . TABLE_MAIL_USERS . "` u ON v.`popaccountid` = u.`id` WHERE v.`customerid` IN (" . implode(", ", $customer_ids) . ") AND " . (is_numeric($params['idea']) ? "v.`id`= :idea" : "(v.`email` = :idea OR v.`email_full` = :idea)" )); $result = Database::pexecute_first($result_stmt, $params, true, true); if ($result) { $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] get email address '" . $result['email_full'] . "'"); return $this->response($result); } $key = ($id > 0 ? "id #" . $id : "emailaddr '" . $emailaddr . "'"); throw new Exception("Email address with " . $key . " could not be found", 404); } /** * toggle catchall flag of given email address either by id or email-address * * @param int $id * optional, the email-address-id * @param string $emailaddr * optional, the email-address * @param int $customerid * optional, required when called as admin (if $loginname is not specified) * @param string $loginname * optional, required when called as admin (if $customerid is not specified) * @param float $spam_tag_level * optional, score which is required to tag emails as spam, default: 7.0 * @param bool $rewrite_subject * optional, whether to add ***SPAM*** to the email's subject if applicable, default: [antispam.default_spam_rewrite_subject] * @param float $spam_kill_level * optional, score which is required to discard emails, default: 14.0 * @param boolean $bypass_spam * optional, disable spam-filter entirely, default: [antispam.default_bypass_spam] * @param boolean $policy_greylist * optional, enable grey-listing, default: [antispam.default_policy_greylist] * @param boolean $iscatchall * optional * @param string $description * optional custom description (currently not used/shown in the frontend), default empty * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function update() { if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'email')) { throw new Exception("You cannot access this resource", 405); } $id = $this->getParam('id', true, 0); $ea_optional = $id > 0; $emailaddr = $this->getParam('emailaddr', $ea_optional, ''); $result = $this->apiCall('Emails.get', [ 'id' => $id, 'emailaddr' => $emailaddr ]); $id = $result['id']; // parameters $spam_tag_level = $this->getParam('spam_tag_level', true, $result['spam_tag_level']); $spam_kill_level = $this->getUlParam('spam_kill_level', 'spam_kill_level_ul', true, $result['spam_kill_level']); $iscatchall = $this->getBoolParam('iscatchall', true, $result['iscatchall']); $description = $this->getParam('description', true, $result['description']); if ((int)Settings::Get('antispam.default_spam_rewrite_subject') <= 2) { $rewrite_subject = $this->getBoolParam('rewrite_subject', true, $result['rewrite_subject']); } else { $rewrite_subject = (int)Settings::Get('antispam.default_spam_rewrite_subject') == 3 ? 1 : 0; } if ((int)Settings::Get('antispam.default_bypass_spam') <= 2) { $bypass_spam = $this->getBoolParam('bypass_spam', true, $result['bypass_spam']); } else { $bypass_spam = (int)Settings::Get('antispam.default_bypass_spam') == 3 ? 1 : 0; } if ((int)Settings::Get('antispam.default_policy_greylist') <= 2) { $policy_greylist = $this->getBoolParam('policy_greylist', true, $result['policy_greylist']); } else { $policy_greylist = (int)Settings::Get('antispam.default_policy_greylist') == 3 ? 1 : 0; } // if enabling catchall is not allowed by settings, we do not need // to run update() if ($iscatchall && $result['iscatchall'] == 0 && Settings::Get('catchall.catchall_enabled') != '1') { Response::standardError([ 'operationnotpermitted', 'featureisdisabled' ], 'catchall', true); } // get needed customer info to reduce the email-address-counter by one $customer = $this->getCustomerData(); // check for catchall-flag $email = $result['email_full']; if ($iscatchall) { $iscatchall = '1'; $email = $result['email']; // update only required if it was not a catchall before if ($result['iscatchall'] == 0) { $email_parts = explode('@', $result['email_full']); $email = '@' . $email_parts[1]; // catchall check $stmt = Database::prepare(" SELECT `email_full` FROM `" . TABLE_MAIL_VIRTUAL . "` WHERE `email` = :email AND `customerid` = :cid AND `iscatchall` = '1' "); $params = [ "email" => $email, "cid" => $customer['customerid'] ]; $email_check = Database::pexecute_first($stmt, $params, true, true); if ($email_check) { Response::standardError('youhavealreadyacatchallforthisdomain', '', true); } } } $spam_tag_level = Validate::validate($spam_tag_level, 'spam_tag_level', '/^\d{1,}(\.\d{1,2})?$/', '', [7.0], true); if ($spam_kill_level > -1) { $spam_kill_level = Validate::validate($spam_kill_level, 'spam_kill_level', '/^\d{1,}(\.\d{1,2})?$/', '', [14.0], true); } $description = Validate::validate(trim($description), 'description', Validate::REGEX_DESC_TEXT, '', [], true); $stmt = Database::prepare(" UPDATE `" . TABLE_MAIL_VIRTUAL . "` SET `email` = :email , `spam_tag_level` = :spam_tag_level, `rewrite_subject` = :rewrite_subject, `spam_kill_level` = :spam_kill_level, `bypass_spam` = :bypass_spam, `policy_greylist` = :policy_greylist, `iscatchall` = :caflag, `description` = :description WHERE `customerid`= :cid AND `id`= :id "); $params = [ "email" => $email, "spam_tag_level" => $spam_tag_level, "rewrite_subject" => $rewrite_subject, "spam_kill_level" => $spam_kill_level, "bypass_spam" => $bypass_spam, "policy_greylist" => $policy_greylist, "caflag" => $iscatchall, "description" => $description, "cid" => $customer['customerid'], "id" => $id ]; Database::pexecute($stmt, $params, true, true); Cronjob::inserttask(TaskId::REBUILD_RSPAMD); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] toggled catchall-flag for email address '" . $result['email_full'] . "'"); $result = $this->apiCall('Emails.get', [ 'emailaddr' => $result['email_full'] ]); return $this->response($result); } /** * list all email addresses, if called from an admin, list all email addresses of all customers you are allowed to * view, or specify id or loginname for one specific customer * * @param int $customerid * optional, admin-only, select email addresses of a specific customer by id * @param string $loginname * optional, admin-only, select email addresses of a specific customer by loginname * @param array $sql_search * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), * LIKE is used if left empty and 'value' => searchvalue * @param int $sql_limit * optional specify number of results to be returned * @param int $sql_offset * optional specify offset for resultset * @param array $sql_orderby * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more * fields * * @access admin, customer * @return string json-encoded array count|list * @throws Exception */ public function listing() { $customer_ids = $this->getAllowedCustomerIds('email'); $result = []; $query_fields = []; $result_stmt = Database::prepare(" SELECT m.*, d.`domain`, u.`quota`, u.`imap`, u.`pop3`, u.`postfix`, u.`mboxsize` FROM `" . TABLE_MAIL_VIRTUAL . "` m LEFT JOIN `" . TABLE_PANEL_DOMAINS . "` d ON (m.`domainid` = d.`id`) LEFT JOIN `" . TABLE_MAIL_USERS . "` u ON (m.`popaccountid` = u.`id`) WHERE m.`customerid` IN (" . implode(", ", $customer_ids) . ")" . $this->getSearchWhere($query_fields, true) . $this->getOrderBy() . $this->getLimit()); Database::pexecute($result_stmt, $query_fields, true, true); $idna_convert = new IdnaWrapper(); while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $row['email'] = $idna_convert->decode($row['email']); $row['email_full'] = $idna_convert->decode($row['email_full']); $result[] = $row; } $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] list email-addresses"); return $this->response([ 'count' => count($result), 'list' => $result ]); } /** * returns the total number of accessible email addresses * * @param int $customerid * optional, admin-only, select email addresses of a specific customer by id * @param string $loginname * optional, admin-only, select email addresses of a specific customer by loginname * * @access admin, customer * @return string json-encoded response message * @throws Exception */ public function listingCount() { $customer_ids = $this->getAllowedCustomerIds('email'); $query_fields = []; $result_stmt = Database::prepare(" SELECT COUNT(*) as num_emails FROM `" . TABLE_MAIL_VIRTUAL . "` m LEFT JOIN `" . TABLE_PANEL_DOMAINS . "` d ON (m.`domainid` = d.`id`) LEFT JOIN `" . TABLE_MAIL_USERS . "` u ON (m.`popaccountid` = u.`id`) WHERE m.`customerid` IN (" . implode(", ", $customer_ids) . ") " . $this->getSearchWhere($query_fields, true)); $result = Database::pexecute_first($result_stmt, $query_fields, true, true); if ($result) { return $this->response($result['num_emails']); } return $this->response(0); } /** * delete an email address by either id or username * * @param int $id * optional, the email-address-id * @param string $emailaddr * optional, the email-address * @param int $customerid * optional, required when called as admin (if $loginname is not specified) * @param string $loginname * optional, required when called as admin (if $customerid is not specified) * @param boolean $delete_userfiles * optional, delete email data from filesystem, default: 0 (false) * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function delete() { if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'email')) { throw new Exception("You cannot access this resource", 405); } $id = $this->getParam('id', true, 0); $ea_optional = $id > 0; $emailaddr = $this->getParam('emailaddr', $ea_optional, ''); $result = $this->apiCall('Emails.get', [ 'id' => $id, 'emailaddr' => $emailaddr ]); $id = $result['id']; // parameters $delete_userfiles = $this->getBoolParam('delete_userfiles', true, 0); // get needed customer info to reduce the email-address-counter by one $customer = $this->getCustomerData(); // check for forwarders $number_forwarders = 0; if ($result['destination'] != '') { $result['destination'] = explode(' ', $result['destination']); $number_forwarders = count($result['destination']); } // check whether this address is an account if ($result['popaccountid'] != 0) { // use EmailAccounts.delete $this->apiCall('EmailAccounts.delete', [ 'id' => $result['id'], 'customerid' => $customer['customerid'], 'delete_userfiles' => $delete_userfiles ]); $number_forwarders--; } // decrease forwarder counter Customers::decreaseUsage($customer['customerid'], 'email_forwarders_used', '', $number_forwarders); Admins::decreaseUsage($customer['customerid'], 'email_forwarders_used', '', $number_forwarders); // delete address $stmt = Database::prepare("DELETE FROM `" . TABLE_MAIL_VIRTUAL . "` WHERE `customerid`= :customerid AND `id`= :id"); Database::pexecute($stmt, [ "customerid" => $customer['customerid'], "id" => $id ], true, true); Customers::decreaseUsage($customer['customerid'], 'emails_used'); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_WARNING, "[API] deleted email address '" . $result['email_full'] . "'"); return $this->response($result); } } ================================================ FILE: lib/Froxlor/Api/Commands/FpmDaemons.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Api\Commands; use Exception; use Froxlor\Api\ApiCommand; use Froxlor\Api\ResourceEntity; use Froxlor\Cron\TaskId; use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\FroxlorLogger; use Froxlor\System\Cronjob; use Froxlor\UI\Response; use Froxlor\Validate\Validate; use PDO; /** * @since 0.10.0 */ class FpmDaemons extends ApiCommand implements ResourceEntity { /** * lists all fpm-daemon entries * * @param array $sql_search * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), * LIKE is used if left empty and 'value' => searchvalue * @param int $sql_limit * optional specify number of results to be returned * @param int $sql_offset * optional specify offset for resultset * @param array $sql_orderby * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more * fields * * @access admin * @return string json-encoded array count|list * @throws Exception */ public function listing() { if ($this->isAdmin()) { $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] list fpm-daemons"); $query_fields = []; $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_FPMDAEMONS . "`" . $this->getSearchWhere($query_fields) . $this->getOrderBy() . $this->getLimit()); Database::pexecute($result_stmt, $query_fields, true, true); $fpmdaemons = []; while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $query_params = [ 'id' => $row['id'] ]; $configresult_stmt = Database::prepare("SELECT * FROM `" . TABLE_PANEL_PHPCONFIGS . "` WHERE `fpmsettingid` = :id"); Database::pexecute($configresult_stmt, $query_params, true, true); $configs = []; if (Database::num_rows() > 0) { while ($row2 = $configresult_stmt->fetch(PDO::FETCH_ASSOC)) { $configs[] = $row2['description']; } } if (empty($configs)) { $configs[] = lng('admin.phpsettings.notused'); } $row['configs'] = $configs; $fpmdaemons[] = $row; } return $this->response([ 'count' => count($fpmdaemons), 'list' => $fpmdaemons ]); } throw new Exception("Not allowed to execute given command.", 403); } /** * returns the total number of accessible fpm daemons * * @access admin * @return string json-encoded response message * @throws Exception */ public function listingCount() { if ($this->isAdmin()) { $query_fields = []; $result_stmt = Database::prepare(" SELECT COUNT(*) as num_fpms FROM `" . TABLE_PANEL_FPMDAEMONS . "` " . $this->getSearchWhere($query_fields)); $result = Database::pexecute_first($result_stmt, $query_fields, true, true); if ($result) { return $this->response($result['num_fpms']); } return $this->response(0); } throw new Exception("Not allowed to execute given command.", 403); } /** * return a fpm-daemon entry by id * * @param int $id * fpm-daemon-id * * @access admin * @return string json-encoded array * @throws Exception */ public function get() { if ($this->isAdmin()) { $id = $this->getParam('id'); $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_FPMDAEMONS . "` WHERE `id` = :id "); $result = Database::pexecute_first($result_stmt, [ 'id' => $id ], true, true); if ($result) { return $this->response($result); } throw new Exception("fpm-daemon with id #" . $id . " could not be found", 404); } throw new Exception("Not allowed to execute given command.", 403); } /** * create a new fpm-daemon entry * * @param string $description * @param string $reload_cmd * @param string $config_dir * @param string $pm * optional, process-manager, one of 'static', 'dynamic' or 'ondemand', default 'dynamic' * @param int $max_children * optional, default 5 * @param int $start_servers * optional, default 2 * @param int $min_spare_servers * optional, default 1 * @param int $max_spare_servers * optional, default 3 * @param int $max_requests * optional, default 0 * @param int $idle_timeout * optional, default 10 * @param string $limit_extensions * optional, limit execution to the following extensions, default '.php' * @param string $custom_config * optional, custom settings appended to phpfpm pool configuration * * @access admin * @return string json-encoded array * @throws Exception */ public function add() { if ($this->isAdmin() && $this->getUserDetail('change_serversettings') == 1) { // required parameter $description = $this->getParam('description'); $reload_cmd = $this->getParam('reload_cmd'); $config_dir = $this->getParam('config_dir'); // parameters $pmanager = $this->getParam('pm', true, 'dynamic'); $max_children = $this->getParam('max_children', true, 5); $start_servers = $this->getParam('start_servers', true, 2); $min_spare_servers = $this->getParam('min_spare_servers', true, 1); $max_spare_servers = $this->getParam('max_spare_servers', true, 3); $max_requests = $this->getParam('max_requests', true, 0); $idle_timeout = $this->getParam('idle_timeout', true, 10); $limit_extensions = $this->getParam('limit_extensions', true, '.php'); $custom_config = $this->getParam('custom_config', true, ''); // validation $description = Validate::validate($description, 'description', Validate::REGEX_DESC_TEXT, '', [], true); $reload_cmd = Validate::validate($reload_cmd, 'reload_cmd', '/^[a-z0-9\/\._\-@ ]+$/i', '', [], true); $sel_stmt = Database::prepare("SELECT `id` FROM `".TABLE_PANEL_FPMDAEMONS."` WHERE `reload_cmd` = :rc"); $dupcheck = Database::pexecute_first($sel_stmt, ['rc' => $reload_cmd]); if ($dupcheck && $dupcheck['id']) { throw new Exception("PHP-FPM version with the given restart command already exists", 406); } $config_dir = Validate::validate($config_dir, 'config_dir', Validate::REGEX_DIR, '', [], true); if (!in_array($pmanager, [ 'static', 'dynamic', 'ondemand' ])) { throw new Exception("Unknown process manager", 406); } if (empty($limit_extensions)) { $limit_extensions = '.php'; } $limit_extensions = Validate::validate($limit_extensions, 'limit_extensions', '/^(\.[a-z]([a-z0-9]+)\ ?)+$/', '', [], true); if (strlen($description) == 0 || strlen($description) > 50) { Response::standardError('descriptioninvalid', '', true); } $ins_stmt = Database::prepare(" INSERT INTO `" . TABLE_PANEL_FPMDAEMONS . "` SET `description` = :desc, `reload_cmd` = :reload_cmd, `config_dir` = :config_dir, `pm` = :pm, `max_children` = :max_children, `start_servers` = :start_servers, `min_spare_servers` = :min_spare_servers, `max_spare_servers` = :max_spare_servers, `max_requests` = :max_requests, `idle_timeout` = :idle_timeout, `limit_extensions` = :limit_extensions, `custom_config` = :custom_config "); $ins_data = [ 'desc' => $description, 'reload_cmd' => $reload_cmd, 'config_dir' => FileDir::makeCorrectDir($config_dir), 'pm' => $pmanager, 'max_children' => $max_children, 'start_servers' => $start_servers, 'min_spare_servers' => $min_spare_servers, 'max_spare_servers' => $max_spare_servers, 'max_requests' => $max_requests, 'idle_timeout' => $idle_timeout, 'limit_extensions' => $limit_extensions, 'custom_config' => $custom_config ]; Database::pexecute($ins_stmt, $ins_data); $id = Database::lastInsertId(); Cronjob::inserttask(TaskId::REBUILD_VHOST); $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] fpm-daemon with description '" . $description . "' has been created by '" . $this->getUserDetail('loginname') . "'"); $result = $this->apiCall('FpmDaemons.get', [ 'id' => $id ]); return $this->response($result); } throw new Exception("Not allowed to execute given command.", 403); } /** * update a fpm-daemon entry by given id * * @param int $id * fpm-daemon id * @param string $description * optional * @param string $reload_cmd * optional * @param string $config_dir * optional * @param string $pm * optional, process-manager, one of 'static', 'dynamic' or 'ondemand', default 'dynamic' * @param int $max_children * optional, default 5 * @param int $start_servers * optional, default 2 * @param int $min_spare_servers * optional, default 1 * @param int $max_spare_servers * optional, default 3 * @param int $max_requests * optional, default 0 * @param int $idle_timeout * optional, default 10 * @param string $limit_extensions * optional, limit execution to the following extensions, default '.php' * @param string $custom_config * optional, custom settings appended to phpfpm pool configuration * * @access admin * @return string json-encoded array * @throws Exception */ public function update() { if ($this->isAdmin() && $this->getUserDetail('change_serversettings') == 1) { // required parameter $id = $this->getParam('id'); $result = $this->apiCall('FpmDaemons.get', [ 'id' => $id ]); // parameters $description = $this->getParam('description', true, $result['description']); $reload_cmd = $this->getParam('reload_cmd', true, $result['reload_cmd']); $config_dir = $this->getParam('config_dir', true, $result['config_dir']); $pmanager = $this->getParam('pm', true, $result['pm']); $max_children = $this->getParam('max_children', true, $result['max_children']); $start_servers = $this->getParam('start_servers', true, $result['start_servers']); $min_spare_servers = $this->getParam('min_spare_servers', true, $result['min_spare_servers']); $max_spare_servers = $this->getParam('max_spare_servers', true, $result['max_spare_servers']); $max_requests = $this->getParam('max_requests', true, $result['max_requests']); $idle_timeout = $this->getParam('idle_timeout', true, $result['idle_timeout']); $limit_extensions = $this->getParam('limit_extensions', true, $result['limit_extensions']); $custom_config = $this->getParam('custom_config', true, $result['custom_config']); // validation $description = Validate::validate($description, 'description', Validate::REGEX_DESC_TEXT, '', [], true); $reload_cmd = Validate::validate($reload_cmd, 'reload_cmd', '/^[a-z0-9\/\._\-@ ]+$/i', '', [], true); $sel_stmt = Database::prepare("SELECT `id` FROM `".TABLE_PANEL_FPMDAEMONS."` WHERE `reload_cmd` = :rc"); $dupcheck = Database::pexecute_first($sel_stmt, ['rc' => $reload_cmd]); if ($dupcheck && $dupcheck['id'] != $id) { throw new Exception("PHP-FPM version with the given restart command already exists", 406); } $config_dir = Validate::validate($config_dir, 'config_dir', Validate::REGEX_DIR, '', [], true); if (!in_array($pmanager, [ 'static', 'dynamic', 'ondemand' ])) { throw new Exception("Unknown process manager", 406); } if (empty($limit_extensions)) { $limit_extensions = '.php'; } $limit_extensions = Validate::validate($limit_extensions, 'limit_extensions', '/^(\.[a-z]([a-z0-9]+)\ ?)+$/', '', [], true); if (strlen($description) == 0 || strlen($description) > 50) { Response::standardError('descriptioninvalid', '', true); } $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_FPMDAEMONS . "` SET `description` = :desc, `reload_cmd` = :reload_cmd, `config_dir` = :config_dir, `pm` = :pm, `max_children` = :max_children, `start_servers` = :start_servers, `min_spare_servers` = :min_spare_servers, `max_spare_servers` = :max_spare_servers, `max_requests` = :max_requests, `idle_timeout` = :idle_timeout, `limit_extensions` = :limit_extensions, `custom_config` = :custom_config WHERE `id` = :id "); $upd_data = [ 'desc' => $description, 'reload_cmd' => $reload_cmd, 'config_dir' => FileDir::makeCorrectDir($config_dir), 'pm' => $pmanager, 'max_children' => $max_children, 'start_servers' => $start_servers, 'min_spare_servers' => $min_spare_servers, 'max_spare_servers' => $max_spare_servers, 'max_requests' => $max_requests, 'idle_timeout' => $idle_timeout, 'limit_extensions' => $limit_extensions, 'custom_config' => $custom_config, 'id' => $id ]; Database::pexecute($upd_stmt, $upd_data, true, true); Cronjob::inserttask(TaskId::REBUILD_VHOST); $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] fpm-daemon with description '" . $description . "' has been updated by '" . $this->getUserDetail('loginname') . "'"); $result = $this->apiCall('FpmDaemons.get', [ 'id' => $id ]); return $this->response($result); } throw new Exception("Not allowed to execute given command.", 403); } /** * delete a fpm-daemon entry by id * * @param int $id * fpm-daemon-id * * @access admin * @return string json-encoded array * @throws Exception */ public function delete() { if ($this->isAdmin() && $this->getUserDetail('change_serversettings') == 1) { $id = $this->getParam('id'); if ($id == 1) { Response::standardError('cannotdeletedefaultphpconfig', '', true); } $result = $this->apiCall('FpmDaemons.get', [ 'id' => $id ]); // set default fpm daemon config for all php-config that use this config that is to be deleted $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_PHPCONFIGS . "` SET `fpmsettingid` = '1' WHERE `fpmsettingid` = :id "); Database::pexecute($upd_stmt, [ 'id' => $id ], true, true); $del_stmt = Database::prepare(" DELETE FROM `" . TABLE_PANEL_FPMDAEMONS . "` WHERE `id` = :id "); Database::pexecute($del_stmt, [ 'id' => $id ], true, true); Cronjob::inserttask(TaskId::REBUILD_VHOST); $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] fpm-daemon setting '" . $result['description'] . "' has been deleted by '" . $this->getUserDetail('loginname') . "'"); return $this->response($result); } throw new Exception("Not allowed to execute given command.", 403); } } ================================================ FILE: lib/Froxlor/Api/Commands/Froxlor.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Api\Commands; use Exception; use Froxlor\Api\ApiCommand; use Froxlor\Cron\TaskId; use Froxlor\Database\Database; use Froxlor\Database\IntegrityCheck; use Froxlor\FroxlorLogger; use Froxlor\Install\AutoUpdate; use Froxlor\Install\Update; use Froxlor\Settings; use Froxlor\SImExporter; use Froxlor\System\Cronjob; use Froxlor\System\Crypt; use Froxlor\Validate\Validate; use PDO; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use ReflectionClass; use ReflectionException; use ReflectionMethod; /** * @since 0.10.0 */ class Froxlor extends ApiCommand { const UPDATE_CHECK_INTERVAL = 21600; // 6 hrs /** * checks whether there is a newer version of froxlor available * * @param bool $force optional, force live update-check * * @access admin * @return string json-encoded array * @throws Exception */ public function checkUpdate() { if ($this->isAdmin() && $this->getUserDetail('change_serversettings')) { $uc_data = Update::getUpdateCheckData(); $force_ucheck = $this->getBoolParam('force', true, 0); $response = $uc_data['data'] ?? []; if (empty($uc_data) || empty($response) || $uc_data['ts'] + self::UPDATE_CHECK_INTERVAL < time() || $uc_data['channel'] != Settings::Get('system.update_channel') || $force_ucheck) { // log our actions $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] checking for updates"); // check for new version $aucheck = AutoUpdate::checkVersion(); $response = []; if ($aucheck == 1) { // anzeige über version-status mit ggfls. formular // zum update schritt #1 -> download $text = lng('update.uc_newinfo', [(Settings::Get('system.update_channel') != 'stable' ? Settings::Get('system.update_channel').' ' : ''), AutoUpdate::getFromResult('version'), $this->version]); $response = [ 'isnewerversion' => (int) !AutoUpdate::getFromResult('has_latest'), 'version' => $this->version, 'message' => $text, 'link' => AutoUpdate::getFromResult('url'), 'additional_info' => AutoUpdate::getFromResult('info'), 'aucheck' => $aucheck ]; } elseif ($aucheck < 0 || $aucheck > 1) { // errors if ($aucheck < 0) { $errmsg = AutoUpdate::getLastError(); } else { if ($aucheck == 3) { $errmsg = lng('error.customized_version'); } else { $errmsg = lng('error.autoupdate_' . $aucheck); } } $response = [ 'isnewerversion' => 0, 'version' => $this->version, 'message' => '', 'link' => '', 'additional_info' => $errmsg, 'aucheck' => $aucheck ]; } else { $response = [ 'isnewerversion' => 0, 'version' => $this->version, 'message' => '', 'link' => '', 'additional_info' => AutoUpdate::getFromResult('info'), 'aucheck' => $aucheck ]; } Update::storeUpdateCheckData($response); } return $this->response($response); } throw new Exception("Not allowed to execute given command.", 403); } /** * import settings * * @param string $json_str * content of exported froxlor-settings json file * * @access admin * @return string json-encoded bool * @throws Exception */ public function importSettings() { if ($this->isAdmin() && $this->getUserDetail('change_serversettings')) { $json_str = $this->getParam('json_str'); $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "User " . $this->getUserDetail('loginname') . " imported settings"); try { SImExporter::import($json_str); Cronjob::inserttask(TaskId::REBUILD_VHOST); Cronjob::inserttask(TaskId::CREATE_QUOTA); // Using nameserver, insert a task which rebuilds the server config Cronjob::inserttask(TaskId::REBUILD_DNS); // cron.d file Cronjob::inserttask(TaskId::REBUILD_CRON); return $this->response(true); } catch (Exception $e) { throw new Exception($e->getMessage(), 406); } } throw new Exception("Not allowed to execute given command.", 403); } /** * export settings * * @access admin * @return string json-string * @throws Exception */ public function exportSettings() { if ($this->isAdmin() && $this->getUserDetail('change_serversettings')) { $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "User " . $this->getUserDetail('loginname') . " exported settings"); $json_export = SImExporter::export(); return $this->response($json_export); } throw new Exception("Not allowed to execute given command.", 403); } /** * return a list of all settings * * @access admin * @return string json-encoded array count|list * @throws Exception */ public function listSettings() { if ($this->isAdmin() && $this->getUserDetail('change_serversettings')) { $sel_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_SETTINGS . "` ORDER BY settinggroup ASC, varname ASC "); Database::pexecute($sel_stmt, null, true, true); $result = []; while ($row = $sel_stmt->fetch(PDO::FETCH_ASSOC)) { $result[] = [ 'key' => $row['settinggroup'] . '.' . $row['varname'], 'value' => $row['value'] ]; } return $this->response([ 'count' => count($result), 'list' => $result ]); } throw new Exception("Not allowed to execute given command.", 403); } /** * return a setting by settinggroup.varname couple * * @param string $key * settinggroup.varname couple * * @access admin * @return string * @throws Exception */ public function getSetting() { if ($this->isAdmin() && $this->getUserDetail('change_serversettings')) { $setting = $this->getParam('key'); return $this->response(Settings::Get($setting)); } throw new Exception("Not allowed to execute given command.", 403); } /** * updates a setting * * @param string $key * settinggroup.varname couple * @param string $value * optional the new value, default is '' * * @access admin * @return string * @throws Exception */ public function updateSetting() { // currently not implemented as it requires validation too so no wrong settings are being stored via API throw new Exception("Not available yet.", 501); if ($this->isAdmin() && $this->getUserDetail('change_serversettings')) { $setting = $this->getParam('key'); $value = $this->getParam('value', true, ''); $oldvalue = Settings::Get($setting); if (is_null($oldvalue)) { throw new Exception("Setting '" . $setting . "' could not be found"); } $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] Changing setting '" . $setting . "' from '" . $oldvalue . "' to '" . $value . "'"); return $this->response(Settings::Set($setting, $value, true)); } throw new Exception("Not allowed to execute given command.", 403); } /** * returns a random password based on froxlor settings for min-length, included characters, etc. * * @param int $length * optional length of password, defaults to 0 (panel.password_min_length) * * @access admin, customer * @return string * @throws Exception */ public function generatePassword(): string { $length = $this->getParam('length', true, 0); return $this->response(Crypt::generatePassword($length)); } /** * return a one-time login link URL for a given user * * @param int $customerid optional, required if $loginname is not specified, user to create link for * @param string $loginname optional, required if $customerid is not specified, user to create link for * @param int $valid_time optional, value in seconds how long the link will be valid, default is 10 seconds, valid values are numbers from 10 to 120 * @param string $allowed_from optional, comma separated list of ip addresses or networks to allow login from via this link * * @access admin * @return string json-encoded array [base => domain, uri => relative link] * @throws Exception */ public function generateLoginLink() { if ($this->isAdmin()) { $customer = $this->getCustomerData(); // cannot create link for deactivated users if ((int)$customer['deactivated'] == 1) { throw new Exception("Cannot generate link for deactivated user", 406); } $valid_time = (int)$this->getParam('valid_time', true, 10); $allowed_from = $this->getParam('allowed_from', true, ''); $valid_time = Validate::validate($valid_time, 'valid time', '/^(1[0-1][0-9]|120|[1-9][0-9])$/', 'invalid_validtime', [10], true); // validate allowed_from if (!empty($allowed_from)) { $ip_list = array_map('trim', explode(",", $allowed_from)); $_check_list = $ip_list; foreach ($_check_list as $idx => $ip) { if (Validate::validate_ip2($ip, true, 'invalidip', true, true, true) == false) { throw new Exception('Invalid ip address', 406); } // check for cidr if (strpos($ip, '/') !== false) { $ipparts = explode("/", $ip); // shorten IP $ip = inet_ntop(inet_pton($ipparts[0])); // re-add cidr $ip .= '/' . $ipparts[1]; } else { // shorten IP $ip = inet_ntop(inet_pton($ip)); } $ip_list[$idx] = $ip; } $allowed_from = implode(",", array_unique($ip_list)); } $hash = hash('sha256', openssl_random_pseudo_bytes(64 * 64)); $ins_stmt = Database::prepare(" INSERT INTO `" . TABLE_PANEL_LOGINLINKS . "` SET `hash` = :hash, `loginname` = :loginname, `valid_until` = :validuntil, `allowed_from` = :allowedfrom ON DUPLICATE KEY UPDATE `hash` = :hash, `valid_until` = :validuntil, `allowed_from` = :allowedfrom "); Database::pexecute($ins_stmt, [ 'hash' => $hash, 'loginname' => $customer['loginname'], 'validuntil' => time() + $valid_time, 'allowedfrom' => $allowed_from ]); return $this->response([ 'base' => 'https://' . Settings::Get('system.hostname') . '/' . (Settings::Get('system.froxlordirectlyviahostname') != 1 ? basename(\Froxlor\Froxlor::getInstallDir()) . '/' : ''), 'uri' => 'index.php?action=ll&ln=' . $customer['loginname'] . '&h=' . $hash ]); } throw new Exception("Not allowed to execute given command.", 403); } /** * can be used to remotely run the integritiy checks froxlor implements * * @access admin * @return string * @throws Exception */ public function integrityCheck() { if ($this->isAdmin() && $this->getUserDetail('change_serversettings')) { $integrity = new IntegrityCheck(); $result = $integrity->checkAll(); if ($result) { return $this->response(null, 204); } throw new Exception("Some checks failed.", 406); } throw new Exception("Not allowed to execute given command.", 403); } /** * returns a list of all available api functions * * @param string $module * optional, return list of functions for a specific module * @param string $function * optional, return parameter information for a specific module and function * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function listFunctions() { $module = $this->getParam('module', true, ''); $function = $this->getParam('function', true, ''); $functions = []; if ($module != null) { // check existence $this->requireModules($module); // now get all static functions $reflection = new ReflectionClass(__NAMESPACE__ . '\\' . $module); $_functions = $reflection->getMethods(ReflectionMethod::IS_PUBLIC); foreach ($_functions as $func) { if (empty($function) || ($function != null && $func->name == $function)) { if ($func->class == __NAMESPACE__ . '\\' . $module && $func->isPublic()) { array_push($functions, array_merge([ 'module' => $module, 'function' => $func->name ], $this->getParamListFromDoc($module, $func->name))); } } } } else { // check all the modules $path = \Froxlor\Froxlor::getInstallDir() . '/lib/Froxlor/Api/Commands/'; // valid directory? if (is_dir($path)) { // create RecursiveIteratorIterator $its = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path)); // check every file foreach ($its as $it) { // does it match the Filename pattern? $matches = []; if (preg_match("/^(.+)\.php$/i", $it->getFilename(), $matches)) { // check for existence try { // set the module to be in our namespace $mod = $matches[1]; $this->requireModules($mod); } catch (Exception $e) { // @todo log? continue; } // now get all static functions $reflection = new ReflectionClass(__NAMESPACE__ . '\\' . $mod); $_functions = $reflection->getMethods(ReflectionMethod::IS_PUBLIC); foreach ($_functions as $func) { if ($func->class == __NAMESPACE__ . '\\' . $mod && $func->isPublic() && !$func->isStatic()) { array_push($functions, array_merge([ 'module' => $matches[1], 'function' => $func->name ], $this->getParamListFromDoc($matches[1], $func->name))); } } } } } else { // yikes - no valid directory to check throw new Exception("Cannot search directory '" . $path . "'. No such directory.", 500); } } // return the list return $this->response($functions); } /** * this functions is used to check the availability * of a given list of modules. * If either one of * them are not found, throw an Exception * * @param string|array $modules * * @throws Exception */ private function requireModules($modules = null) { if ($modules != null) { // no array -> create one if (!is_array($modules)) { $modules = [ $modules ]; } // check all the modules foreach ($modules as $module) { try { $module = __NAMESPACE__ . '\\' . $module; // can we use the class? if (class_exists($module)) { continue; } else { throw new Exception('The required class "' . $module . '" could not be found but the module-file exists', 404); } } catch (Exception $e) { // The autoloader will throw an Exception // that the required class could not be found // but we want a nicer error-message for this here throw new Exception('The required module "' . $module . '" could not be found', 404); } } } } /** * generate an api-response to list all parameters and the return-value of * a given module.function-combination * * @param string $module * @param string $function * * @return array|bool * @throws Exception */ private function getParamListFromDoc($module = null, $function = null) { try { // set the module $cls = new ReflectionMethod(__NAMESPACE__ . '\\' . $module, $function); $comment = $cls->getDocComment(); if ($comment == false) { return [ 'head' => 'There is no comment-block for "' . $module . '.' . $function . '"' ]; } $clines = explode("\n", $comment); $result = []; $result['params'] = []; $param_desc = false; $r = []; foreach ($clines as $c) { $c = trim($c); // check param-section if (strpos($c, '@param')) { preg_match('/^\*\s\@param\s(.+)\s(\$\w+)(\s.*)?/', $c, $r); // cut $ off the parameter-name as it is not wanted in the api-request $result['params'][] = [ 'parameter' => substr($r[2], 1), 'type' => $r[1], 'desc' => (isset($r[3]) ? trim($r['3']) : '') ]; $param_desc = true; } elseif (strpos($c, '@access')) { // check access-section preg_match('/^\*\s\@access\s(.*)/', $c, $r); if (!isset($r[0]) || empty($r[0])) { $r[1] = 'This function has no restrictions'; } $result['access'] = [ 'groups' => (isset($r[1]) ? trim($r[1]) : '') ]; } elseif (strpos($c, '@return')) { // check return-section preg_match('/^\*\s\@return\s(\w+)(\s.*)?/', $c, $r); if (!isset($r[0]) || empty($r[0])) { $r[1] = 'null'; $r[2] = 'This function has no return value given'; } $result['return'] = [ 'type' => $r[1], 'desc' => (isset($r[2]) ? trim($r[2]) : '') ]; } elseif (!empty($c) && strpos($c, '@throws') === false) { // check throws-section if (substr($c, 0, 3) == "/**") { continue; } if (substr($c, 0, 2) == "*/") { continue; } if (substr($c, 0, 1) == "*") { $c = trim(substr($c, 1)); if (empty($c)) { continue; } if ($param_desc) { $result['params'][count($result['params']) - 1]['desc'] .= $c; } else { if (!isset($result['head']) || empty($result['head'])) { $result['head'] = $c . " "; } else { $result['head'] .= $c . " "; } } } } } $result['head'] = trim($result['head']); return $result; } catch (ReflectionException $e) { return []; } } } ================================================ FILE: lib/Froxlor/Api/Commands/Ftps.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Api\Commands; use Exception; use Froxlor\Api\ApiCommand; use Froxlor\Api\ResourceEntity; use Froxlor\Cron\TaskId; use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\FroxlorLogger; use Froxlor\Idna\IdnaWrapper; use Froxlor\Settings; use Froxlor\System\Cronjob; use Froxlor\System\Crypt; use Froxlor\UI\Response; use Froxlor\User; use Froxlor\Validate\Validate; use PDO; /** * @since 0.10.0 */ class Ftps extends ApiCommand implements ResourceEntity { /** * add a new ftp-user * * @param string $ftp_password * password for the created database and database-user * @param string $path * destination path relative to the customers-homedir * @param string $ftp_description * optional, description for ftp-user * @param bool $sendinfomail * optional, send created resource-information to customer, default: false * @param string $shell * optional, default /bin/false (not changeable when deactivated) * @param string $ftp_username * optional if customer.ftpatdomain is allowed, specify an username * @param string $ftp_domain * optional if customer.ftpatdomain is allowed, specify a domain (customer must be owner) * @param int $customerid * optional, required when called as admin (if $loginname is not specified) * @param string $loginname * optional, required when called as admin (if $customerid is not specified) * @param array $additional_members * optional whether to add additional usernames to the group * @param bool $is_defaultuser * optional whether this is the standard default ftp user which is being added so no usage is decreased * @param bool $login_enabled * optional whether to allow login (default) or not * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function add() { if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'ftp')) { throw new Exception("You cannot access this resource", 405); } $is_defaultuser = $this->getBoolParam('is_defaultuser', true, 0); $login_enabled = $this->getBoolParam('login_enabled', true, 1); if (($this->getUserDetail('ftps_used') < $this->getUserDetail('ftps') || $this->getUserDetail('ftps') == '-1') || $this->isAdmin() && $is_defaultuser == 1) { // required parameters $path = $this->getParam('path'); $password = $this->getParam('ftp_password'); // parameters $description = $this->getParam('ftp_description', true, ''); $sendinfomail = $this->getBoolParam('sendinfomail', true, 0); $shell = $this->getParam('shell', true, '/bin/false'); $ftpusername = $this->getParam('ftp_username', true, ''); $ftpdomain = $this->getParam('ftp_domain', true, ''); $additional_members = $this->getParam('additional_members', true, []); // validation $password = Validate::validate($password, 'password', '', '', [], true); $password = Crypt::validatePassword($password, true); $description = Validate::validate(trim($description), 'description', Validate::REGEX_DESC_TEXT, '', [], true); // get needed customer info to reduce the ftp-user-counter by one if ($is_defaultuser) { // no resource check for default user $customer = $this->getCustomerData(); } else { $customer = $this->getCustomerData('ftps'); } if (Settings::Get('system.allow_customer_shell') == '1' && $customer['shell_allowed'] == '1') { $shell = Validate::validate(trim($shell), 'shell', '', '', [], true); $availableshells = explode(',', Settings::Get('system.available_shells')); if (!is_array($availableshells) || empty($availableshells) || !in_array($shell, $availableshells)) { $shell = "/bin/false"; } } else { $shell = "/bin/false"; } if (Settings::Get('customer.ftpatdomain') == '1') { $ftpusername = Validate::validate(trim($ftpusername), 'username', '/^[a-zA-Z0-9][a-zA-Z0-9\-_]+\$?$/', '', [], true); if (substr($ftpdomain, 0, 4) != 'xn--') { $idna_convert = new IdnaWrapper(); $ftpdomain = $idna_convert->encode(Validate::validate($ftpdomain, 'domain', '', '', [], true)); } } $params = []; if ($sendinfomail != 1) { $sendinfomail = 0; } if (Settings::Get('customer.ftpatdomain') == '1' && !$is_defaultuser) { if ($ftpusername == '') { Response::standardError([ 'stringisempty', 'username' ], '', true); } $ftpdomain_check_stmt = Database::prepare("SELECT `id`, `domain`, `customerid` FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `domain` = :domain AND `customerid` = :customerid"); $ftpdomain_check = Database::pexecute_first($ftpdomain_check_stmt, [ "domain" => $ftpdomain, "customerid" => $customer['customerid'] ], true, true); if ($ftpdomain_check && $ftpdomain_check['domain'] != $ftpdomain) { Response::standardError('maindomainnonexist', $ftpdomain, true); } $username = $ftpusername . "@" . $ftpdomain; } else { if ($is_defaultuser) { $username = $customer['loginname']; } else { $username = $customer['loginname'] . Settings::Get('customer.ftpprefix') . (intval($customer['ftp_lastaccountnumber']) + 1); } } $username_check_stmt = Database::prepare(" SELECT * FROM `" . TABLE_FTP_USERS . "` WHERE `username` = :username "); $username_check = Database::pexecute_first($username_check_stmt, [ "username" => $username ], true, true); if (!empty($username_check) && $username_check['username'] = $username) { Response::standardError('usernamealreadyexists', $username, true); } elseif ($username == $password) { Response::standardError('passwordshouldnotbeusername', '', true); } else { $path = FileDir::makeCorrectDir($customer['documentroot'] . '/' . $path, $customer['documentroot']); $cryptPassword = Crypt::makeCryptPassword($password, false, true); $stmt = Database::prepare("INSERT INTO `" . TABLE_FTP_USERS . "` (`customerid`, `username`, `description`, `password`, `homedir`, `login_enabled`, `uid`, `gid`, `shell`) VALUES (:customerid, :username, :description, :password, :homedir, :loginenabled, :guid, :guid, :shell)"); $params = [ "customerid" => $customer['customerid'], "username" => $username, "description" => $description, "password" => $cryptPassword, "homedir" => $path, "loginenabled" => $login_enabled ? 'Y' : 'N', "guid" => $customer['guid'], "shell" => $shell ]; Database::pexecute($stmt, $params, true, true); $result_stmt = Database::prepare(" SELECT `bytes_in_used` FROM `" . TABLE_FTP_QUOTATALLIES . "` WHERE `name` = :name "); Database::pexecute($result_stmt, [ "name" => $customer['loginname'] ], true, true); while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $stmt = Database::prepare("INSERT INTO `" . TABLE_FTP_QUOTATALLIES . "` (`name`, `quota_type`, `bytes_in_used`, `bytes_out_used`, `bytes_xfer_used`, `files_in_used`, `files_out_used`, `files_xfer_used`) VALUES (:name, 'user', :bytes_in_used, '0', '0', '0', '0', '0') "); Database::pexecute($stmt, [ "name" => $username, "bytes_in_used" => $row['bytes_in_used'] ], true, true); } // create quotatallies entry if it not exists, refs #885 if ($result_stmt->rowCount() == 0) { $stmt = Database::prepare("INSERT INTO `" . TABLE_FTP_QUOTATALLIES . "` (`name`, `quota_type`, `bytes_in_used`, `bytes_out_used`, `bytes_xfer_used`, `files_in_used`, `files_out_used`, `files_xfer_used`) VALUES (:name, 'user', '0', '0', '0', '0', '0', '0') "); Database::pexecute($stmt, [ "name" => $username ], true, true); } $group_upd_stmt = Database::prepare(" UPDATE `" . TABLE_FTP_GROUPS . "` SET `members` = CONCAT_WS(',',`members`, :username) WHERE `customerid`= :customerid AND `gid`= :guid "); $params = [ "username" => $username, "customerid" => $customer['customerid'], "guid" => $customer['guid'] ]; if ($is_defaultuser) { // add the new group $group_ins_stmt = Database::prepare(" INSERT INTO `" . TABLE_FTP_GROUPS . "` SET `customerid`= :customerid, `gid`= :guid, `groupname` = :username, `members` = :username "); Database::pexecute($group_ins_stmt, $params, true, true); } else { // just update Database::pexecute($group_upd_stmt, $params, true, true); } if (count($additional_members) > 0) { foreach ($additional_members as $add_member) { $params = [ "username" => $add_member, "customerid" => $customer['customerid'], "guid" => $customer['guid'] ]; Database::pexecute($group_upd_stmt, $params, true, true); } } if (!$is_defaultuser) { // update customer usage Customers::increaseUsage($customer['customerid'], 'ftps_used'); Customers::increaseUsage($customer['customerid'], 'ftp_lastaccountnumber'); Cronjob::inserttask(TaskId::REBUILD_NSSUSERS); } $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] added ftp-account '" . $username . " (" . $path . ")'"); Cronjob::inserttask(TaskId::CREATE_FTP); if ($sendinfomail == 1) { $replace_arr = [ 'SALUTATION' => User::getCorrectUserSalutation($customer), 'CUST_NAME' => User::getCorrectUserSalutation($customer), // < keep this for compatibility 'NAME' => $customer['name'], 'FIRSTNAME' => $customer['firstname'], 'COMPANY' => $customer['company'], 'USERNAME' => $customer['loginname'], 'CUSTOMER_NO' => $customer['customernumber'], 'USR_NAME' => $username, 'USR_PASS' => htmlentities(htmlentities($password)), 'USR_PATH' => FileDir::makeCorrectDir(str_replace($customer['documentroot'], "/", $path)) ]; // get template for mail subject $mail_subject = $this->getMailTemplate($customer, 'mails', 'new_ftpaccount_by_customer_subject', $replace_arr, lng('mails.new_ftpaccount_by_customer.subject')); // get template for mail body $mail_body = $this->getMailTemplate($customer, 'mails', 'new_ftpaccount_by_customer_mailbody', $replace_arr, lng('mails.new_ftpaccount_by_customer.mailbody')); $_mailerror = false; $mailerr_msg = ""; try { $this->mailer()->Subject = $mail_subject; $this->mailer()->AltBody = $mail_body; $this->mailer()->Body = str_replace("\n", "
", $mail_body); $this->mailer()->addAddress($customer['email'], User::getCorrectUserSalutation($customer)); $this->mailer()->send(); } catch (\PHPMailer\PHPMailer\Exception $e) { $mailerr_msg = $e->errorMessage(); $_mailerror = true; } catch (Exception $e) { $mailerr_msg = $e->getMessage(); $_mailerror = true; } if ($_mailerror) { $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_ERR, "[API] Error sending mail: " . $mailerr_msg); Response::standardError('errorsendingmail', $customer['email'], true); } $this->mailer()->clearAddresses(); } $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] added ftp-user '" . $username . "'"); $result = $this->apiCall('Ftps.get', [ 'username' => $username ]); return $this->response($result); } } throw new Exception("No more resources available", 406); } /** * return a ftp-user entry by either id or username * * @param int $id * optional, the customer-id * @param string $username * optional, the username * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function get() { $id = $this->getParam('id', true, 0); $un_optional = $id > 0; $username = $this->getParam('username', $un_optional, ''); $params = []; if ($this->isAdmin()) { if ($this->getUserDetail('customers_see_all') == false) { // if it's a reseller or an admin who cannot see all customers, we need to check // whether the database belongs to one of his customers $_custom_list_result = $this->apiCall('Customers.listing'); $custom_list_result = $_custom_list_result['list']; $customer_ids = []; foreach ($custom_list_result as $customer) { $customer_ids[] = $customer['customerid']; } $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_FTP_USERS . "` WHERE `customerid` IN (" . implode(", ", $customer_ids) . ") AND (`id` = :idun OR `username` = :idun) "); } else { $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_FTP_USERS . "` WHERE (`id` = :idun OR `username` = :idun) "); } } else { if (Settings::IsInList('panel.customer_hide_options', 'ftp')) { throw new Exception("You cannot access this resource", 405); } $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_FTP_USERS . "` WHERE `customerid` = :customerid AND (`id` = :idun OR `username` = :idun) "); $params['customerid'] = $this->getUserDetail('customerid'); } $params['idun'] = ($id <= 0 ? $username : $id); $result = Database::pexecute_first($result_stmt, $params, true, true); if ($result) { $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] get ftp-user '" . $result['username'] . "'"); return $this->response($result); } $key = ($id > 0 ? "id #" . $id : "username '" . $username . "'"); throw new Exception("FTP user with " . $key . " could not be found", 404); } /** * update a given ftp-user by id or username * * @param int $id * optional, the ftp-user-id * @param string $username * optional, the username * @param string $ftp_password * optional, update password if specified * @param string $path * destination path relative to the customers-homedir * @param string $ftp_description * optional, description for ftp-user * @param string $shell * optional, default /bin/false (not changeable when deactivated) * @param bool $login_enabled * optional whether to allow login (default) or not * @param int $customerid * optional, required when called as admin (if $loginname is not specified) * @param string $loginname * optional, required when called as admin (if $customerid is not specified) * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function update() { if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'ftp')) { throw new Exception("You cannot access this resource", 405); } $id = $this->getParam('id', true, 0); $un_optional = $id > 0; $username = $this->getParam('username', $un_optional, ''); $result = $this->apiCall('Ftps.get', [ 'id' => $id, 'username' => $username ]); $id = $result['id']; // parameters $path = $this->getParam('path', true, ''); $password = $this->getParam('ftp_password', true, ''); $description = $this->getParam('ftp_description', true, $result['description']); $shell = $this->getParam('shell', true, $result['shell']); $login_enabled = $this->getBoolParam('login_enabled', true, ($result['login_enabled'] == 'Y' ? 1 : 0)); // validation $password = Validate::validate($password, 'password', '', '', [], true); $description = Validate::validate(trim($description), 'description', Validate::REGEX_DESC_TEXT, '', [], true); // get needed customer info to reduce the ftp-user-counter by one $customer = $this->getCustomerData(); if (Settings::Get('system.allow_customer_shell') == '1' && $customer['shell_allowed'] == '1') { $shell = Validate::validate(trim($shell), 'shell', '', '', [], true); $availableshells = explode(',', Settings::Get('system.available_shells')); if (!is_array($availableshells) || empty($availableshells) || !in_array($shell, $availableshells)) { $shell = "/bin/false"; } } else { $shell = "/bin/false"; } if ($login_enabled != 1) { $login_enabled = 0; } // password update? if ($password != '') { // validate password $password = Crypt::validatePassword($password, true); if ($password == $result['username']) { Response::standardError('passwordshouldnotbeusername', '', true); } $cryptPassword = Crypt::makeCryptPassword($password, false, true); $stmt = Database::prepare("UPDATE `" . TABLE_FTP_USERS . "` SET `password` = :password WHERE `customerid` = :customerid AND `id` = :id "); Database::pexecute($stmt, [ "customerid" => $customer['customerid'], "id" => $id, "password" => $cryptPassword ], true, true); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] updated ftp-account password for '" . $result['username'] . "'"); } // path update? if ($path != '') { $path = FileDir::makeCorrectDir($customer['documentroot'] . '/' . $path, $customer['documentroot']); if ($path != $result['homedir']) { $stmt = Database::prepare("UPDATE `" . TABLE_FTP_USERS . "` SET `homedir` = :homedir WHERE `customerid` = :customerid AND `id` = :id "); Database::pexecute($stmt, [ "homedir" => $path, "customerid" => $customer['customerid'], "id" => $id ], true, true); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] updated ftp-account homdir for '" . $result['username'] . "'"); } } // it's the task for "new ftp" but that will // create all directories and correct their permissions Cronjob::inserttask(TaskId::CREATE_FTP); Cronjob::inserttask(TaskId::REBUILD_NSSUSERS); $stmt = Database::prepare(" UPDATE `" . TABLE_FTP_USERS . "` SET `description` = :desc, `shell` = :shell, `login_enabled` = :loginenabled WHERE `customerid` = :customerid AND `id` = :id "); Database::pexecute($stmt, [ "desc" => $description, "shell" => $shell, "loginenabled" => $login_enabled ? 'Y' : 'N', "customerid" => $customer['customerid'], "id" => $id ], true, true); $result = $this->apiCall('Ftps.get', [ 'username' => $result['username'] ]); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] updated ftp-user '" . $result['username'] . "'"); return $this->response($result); } /** * list all ftp-users, if called from an admin, list all ftp-users of all customers you are allowed to view, or * specify id or loginname for one specific customer * * @param int $customerid * optional, admin-only, select ftp-users of a specific customer by id * @param string $loginname * optional, admin-only, select ftp-users of a specific customer by loginname * @param array $sql_search * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), * LIKE is used if left empty and 'value' => searchvalue * @param int $sql_limit * optional specify number of results to be returned * @param int $sql_offset * optional specify offset for resultset * @param array $sql_orderby * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more * fields * * @access admin, customer * @return string json-encoded array count|list * @throws Exception */ public function listing() { $customer_ids = $this->getAllowedCustomerIds('ftp'); $result = []; $query_fields = []; $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_FTP_USERS . "` WHERE `customerid` IN (" . implode(", ", $customer_ids) . ")" . $this->getSearchWhere($query_fields, true) . $this->getOrderBy() . $this->getLimit()); Database::pexecute($result_stmt, $query_fields, true, true); while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $result[] = $row; } $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] list ftp-users"); return $this->response([ 'count' => count($result), 'list' => $result ]); } /** * returns the total number of accessible ftp accounts * * @param int $customerid * optional, admin-only, select ftp-users of a specific customer by id * @param string $loginname * optional, admin-only, select ftp-users of a specific customer by loginname * * @access admin, customer * @return string json-encoded response message * @throws Exception */ public function listingCount() { $customer_ids = $this->getAllowedCustomerIds('ftp'); $result = []; $query_fields = []; $result_stmt = Database::prepare(" SELECT COUNT(*) as num_ftps FROM `" . TABLE_FTP_USERS . "` WHERE `customerid` IN (" . implode(", ", $customer_ids) . ") " . $this->getSearchWhere($query_fields, true)); $result = Database::pexecute_first($result_stmt, $query_fields, true, true); if ($result) { return $this->response($result['num_ftps']); } return $this->response(0); } /** * delete a ftp-user by either id or username * * @param int $id * optional, the ftp-user-id * @param string $username * optional, the username * @param bool $delete_userfiles * optional, default false * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function delete() { $id = $this->getParam('id', true, 0); $un_optional = $id > 0; $username = $this->getParam('username', $un_optional, ''); $delete_userfiles = $this->getBoolParam('delete_userfiles', true, 0); if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'ftp')) { throw new Exception("You cannot access this resource", 405); } // get ftp-user $result = $this->apiCall('Ftps.get', [ 'id' => $id, 'username' => $username ]); $id = $result['id']; if ($this->isAdmin()) { // get customer-data $customer_data = $this->apiCall('Customers.get', [ 'id' => $result['customerid'] ]); } else { $customer_data = $this->getUserData(); } // add usage of this ftp-user to main-ftp user of customer if different if ($result['username'] != $customer_data['loginname']) { $stmt = Database::prepare("UPDATE `" . TABLE_FTP_USERS . "` SET `up_count` = `up_count` + :up_count, `up_bytes` = `up_bytes` + :up_bytes, `down_count` = `down_count` + :down_count, `down_bytes` = `down_bytes` + :down_bytes WHERE `username` = :username "); $params = [ "up_count" => $result['up_count'], "up_bytes" => $result['up_bytes'], "down_count" => $result['down_count'], "down_bytes" => $result['down_bytes'], "username" => $customer_data['loginname'] ]; Database::pexecute($stmt, $params, true, true); } else { // do not allow removing default ftp-account Response::standardError('ftp_cantdeletemainaccount', '', true); } // remove all quotatallies $stmt = Database::prepare("DELETE FROM `" . TABLE_FTP_QUOTATALLIES . "` WHERE `name` = :name"); Database::pexecute($stmt, [ "name" => $result['username'] ], true, true); // remove user itself $stmt = Database::prepare(" DELETE FROM `" . TABLE_FTP_USERS . "` WHERE `customerid` = :customerid AND `id` = :id "); Database::pexecute($stmt, [ "customerid" => $customer_data['customerid'], "id" => $id ], true, true); // update ftp-groups $stmt = Database::prepare(" UPDATE `" . TABLE_FTP_GROUPS . "` SET `members` = REPLACE(`members`, :username,'') WHERE `customerid` = :customerid "); Database::pexecute($stmt, [ "username" => "," . $result['username'], "customerid" => $customer_data['customerid'] ], true, true); // refs #293 if ($delete_userfiles == 1) { Cronjob::inserttask(TaskId::DELETE_FTP_DATA, $customer_data['loginname'], $result['homedir']); } else { if (Settings::Get('system.nssextrausers') == 1) { // this is used so that the libnss-extrausers cron is fired Cronjob::inserttask(TaskId::CREATE_FTP); } } Cronjob::inserttask(TaskId::REBUILD_NSSUSERS); // decrease ftp-user usage for customer $resetaccnumber = ($customer_data['ftps_used'] == '1') ? " , `ftp_lastaccountnumber`='0'" : ''; Customers::decreaseUsage($customer_data['customerid'], 'ftps_used', $resetaccnumber); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_WARNING, "[API] deleted ftp-user '" . $result['username'] . "'"); return $this->response($result); } } ================================================ FILE: lib/Froxlor/Api/Commands/HostingPlans.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Api\Commands; use Exception; use Froxlor\Api\ApiCommand; use Froxlor\Api\ResourceEntity; use Froxlor\Database\Database; use Froxlor\FroxlorLogger; use Froxlor\Settings; use Froxlor\Validate\Validate; use PDO; /** * @since 0.10.0 */ class HostingPlans extends ApiCommand implements ResourceEntity { /** * list all available hosting plans * * @param array $sql_search * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), * LIKE is used if left empty and 'value' => searchvalue * @param int $sql_limit * optional specify number of results to be returned * @param int $sql_offset * optional specify offset for resultset * @param array $sql_orderby * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more * fields * * @access admin * @return string json-encoded array count|list * @throws Exception */ public function listing() { if ($this->isAdmin()) { $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] list hosting-plans"); $query_fields = []; $result_stmt = Database::prepare(" SELECT p.*, a.loginname as adminname FROM `" . TABLE_PANEL_PLANS . "` p, `" . TABLE_PANEL_ADMINS . "` a WHERE `p`.`adminid` = `a`.`adminid`" . ($this->getUserDetail('customers_see_all') ? '' : " AND `p`.`adminid` = :adminid ") . $this->getSearchWhere($query_fields, true) . $this->getOrderBy() . $this->getLimit()); $params = []; if ($this->getUserDetail('customers_see_all') == '0') { $params['adminid'] = $this->getUserDetail('adminid'); } $params = array_merge($params, $query_fields); Database::pexecute($result_stmt, $params, true, true); $result = []; while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $result[] = $row; } return $this->response([ 'count' => count($result), 'list' => $result ]); } throw new Exception("Not allowed to execute given command.", 403); } /** * returns the total number of accessible hosting plans * * @access admin * @return string json-encoded response message * @throws Exception */ public function listingCount() { if ($this->isAdmin()) { $query_fields = []; $result_stmt = Database::prepare(" SELECT COUNT(*) as num_plans FROM `" . TABLE_PANEL_PLANS . "` p, `" . TABLE_PANEL_ADMINS . "` a WHERE `p`.`adminid` = `a`.`adminid`" . ($this->getUserDetail('customers_see_all') ? '' : " AND `p`.`adminid` = :adminid ") . $this->getSearchWhere($query_fields, true)); $params = []; if ($this->getUserDetail('customers_see_all') == '0') { $params['adminid'] = $this->getUserDetail('adminid'); } $params = array_merge($params, $query_fields); $result = Database::pexecute_first($result_stmt, $params, true, true); if ($result) { return $this->response($result['num_plans']); } return $this->response(0); } throw new Exception("Not allowed to execute given command.", 403); } /** * add new hosting-plan * * @param string $name * name of the plan * @param string $description * optional, description for hosting-plan * @param int $diskspace * optional disk-space available for customer in MB, default 0 * @param bool $diskspace_ul * optional, whether customer should have unlimited diskspace, default 0 (false) * @param int $traffic * optional traffic available for customer in GB, default 0 * @param bool $traffic_ul * optional, whether customer should have unlimited traffic, default 0 (false) * @param int $subdomains * optional amount of subdomains available for customer, default 0 * @param bool $subdomains_ul * optional, whether customer should have unlimited subdomains, default 0 (false) * @param int $emails * optional amount of emails available for customer, default 0 * @param bool $emails_ul * optional, whether customer should have unlimited emails, default 0 (false) * @param int $email_accounts * optional amount of email-accounts available for customer, default 0 * @param bool $email_accounts_ul * optional, whether customer should have unlimited email-accounts, default 0 (false) * @param int $email_forwarders * optional amount of email-forwarders available for customer, default 0 * @param bool $email_forwarders_ul * optional, whether customer should have unlimited email-forwarders, default 0 (false) * @param int $email_quota * optional size of email-quota available for customer in MB, default is system-setting mail_quota * @param bool $email_quota_ul * optional, whether customer should have unlimited email-quota, default 0 (false) * @param bool $email_imap * optional, whether to allow IMAP access, default 0 (false) * @param bool $email_pop3 * optional, whether to allow POP3 access, default 0 (false) * @param int $ftps * optional amount of ftp-accounts available for customer, default 0 * @param bool $ftps_ul * optional, whether customer should have unlimited ftp-accounts, default 0 (false) * @param int $mysqls * optional amount of mysql-databases available for customer, default 0 * @param bool $mysqls_ul * optional, whether customer should have unlimited mysql-databases, default 0 (false) * @param bool $phpenabled * optional, whether to allow usage of PHP, default 0 (false) * @param array $allowed_phpconfigs * optional, array of IDs of php-config that the customer is allowed to use, default empty (none) * @param bool $perlenabled * optional, whether to allow usage of Perl/CGI, default 0 (false) * @param bool $dnsenabled * optional, whether to allow usage of the DNS editor (requires activated nameserver in settings), * default 0 (false) * @param bool $logviewenabled * optional, whether to allow access to webserver access/error-logs, default 0 (false) * * @access admin * @return string json-encoded array * @throws Exception */ public function add() { if ($this->isAdmin()) { $name = $this->getParam('name'); $description = $this->getParam('description', true, ''); $value_arr = []; $value_arr['diskspace'] = $this->getUlParam('diskspace', 'diskspace_ul', true, 0); $value_arr['traffic'] = $this->getUlParam('traffic', 'traffic_ul', true, 0); $value_arr['subdomains'] = $this->getUlParam('subdomains', 'subdomains_ul', true, 0); $value_arr['emails'] = $this->getUlParam('emails', 'emails_ul', true, 0); $value_arr['email_accounts'] = $this->getUlParam('email_accounts', 'email_accounts_ul', true, 0); $value_arr['email_forwarders'] = $this->getUlParam('email_forwarders', 'email_forwarders_ul', true, 0); $value_arr['email_quota'] = $this->getUlParam('email_quota', 'email_quota_ul', true, Settings::Get('system.mail_quota')); $value_arr['email_imap'] = $this->getBoolParam('email_imap', true, 0); $value_arr['email_pop3'] = $this->getBoolParam('email_pop3', true, 0); $value_arr['ftps'] = $this->getUlParam('ftps', 'ftps_ul', true, 0); $value_arr['mysqls'] = $this->getUlParam('mysqls', 'mysqls_ul', true, 0); $value_arr['phpenabled'] = $this->getBoolParam('phpenabled', true, 0); $p_allowed_phpconfigs = $this->getParam('allowed_phpconfigs', true, []); $value_arr['perlenabled'] = $this->getBoolParam('perlenabled', true, 0); $value_arr['dnsenabled'] = $this->getBoolParam('dnsenabled', true, 0); $value_arr['logviewenabled'] = $this->getBoolParam('logviewenabled', true, 0); // validation $name = Validate::validate(trim($name), 'name', Validate::REGEX_DESC_TEXT, '', [], true); $description = Validate::validate(str_replace("\r\n", "\n", $description), 'description', Validate::REGEX_DESC_TEXT); if (Settings::Get('system.mail_quota_enabled') != '1') { $value_arr['email_quota'] = -1; } $value_arr['allowed_phpconfigs'] = []; if (!empty($p_allowed_phpconfigs) && is_array($p_allowed_phpconfigs)) { foreach ($p_allowed_phpconfigs as $allowed_phpconfig) { $allowed_phpconfig = intval($allowed_phpconfig); $value_arr['allowed_phpconfigs'][] = $allowed_phpconfig; } } $value_arr['allowed_phpconfigs'] = array_map('intval', $value_arr['allowed_phpconfigs']); $ins_stmt = Database::prepare(" INSERT INTO `" . TABLE_PANEL_PLANS . "` SET `adminid` = :adminid, `name` = :name, `description` = :desc, `value` = :valuearr, `ts` = UNIX_TIMESTAMP(); "); $ins_data = [ 'adminid' => $this->getUserDetail('adminid'), 'name' => $name, 'desc' => $description, 'valuearr' => json_encode($value_arr) ]; Database::pexecute($ins_stmt, $ins_data, true, true); $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] added hosting-plan '" . $name . "'"); $result = $this->apiCall('HostingPlans.get', [ 'planname' => $name ]); return $this->response($result); } throw new Exception("Not allowed to execute given command.", 403); } /** * return a hosting-plan entry by either id or plan-name * * @param int $id * optional, the hosting-plan-id * @param string $planname * optional, the hosting-plan-name * * @access admin * @return string json-encoded array * @throws Exception */ public function get() { if ($this->isAdmin()) { $id = $this->getParam('id', true, 0); $dn_optional = $id > 0; $planname = $this->getParam('planname', $dn_optional, ''); $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_PLANS . "` WHERE " . ($id > 0 ? "`id` = :iddn" : "`name` = :iddn") . ($this->getUserDetail('customers_see_all') ? '' : " AND `adminid` = :adminid")); $params = [ 'iddn' => ($id <= 0 ? $planname : $id) ]; if ($this->getUserDetail('customers_see_all') == '0') { $params['adminid'] = $this->getUserDetail('adminid'); } $result = Database::pexecute_first($result_stmt, $params, true, true); if ($result) { $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] get hosting-plan '" . $result['name'] . "'"); return $this->response($result); } $key = ($id > 0 ? "id #" . $id : "planname '" . $planname . "'"); throw new Exception("Hosting-plan with " . $key . " could not be found", 404); } throw new Exception("Not allowed to execute given command.", 403); } /** * update hosting-plan by either id or plan-name * * @param int $id * optional the hosting-plan-id * @param string $planname * optional the hosting-plan-name * @param string $name * optional name of the plan * @param string $description * optional description for hosting-plan * @param int $diskspace * optional disk-space available for customer in MB, default 0 * @param bool $diskspace_ul * optional, whether customer should have unlimited diskspace, default 0 (false) * @param int $traffic * optional traffic available for customer in GB, default 0 * @param bool $traffic_ul * optional, whether customer should have unlimited traffic, default 0 (false) * @param int $subdomains * optional amount of subdomains available for customer, default 0 * @param bool $subdomains_ul * optional, whether customer should have unlimited subdomains, default 0 (false) * @param int $emails * optional amount of emails available for customer, default 0 * @param bool $emails_ul * optional, whether customer should have unlimited emails, default 0 (false) * @param int $email_accounts * optional amount of email-accounts available for customer, default 0 * @param bool $email_accounts_ul * optional, whether customer should have unlimited email-accounts, default 0 (false) * @param int $email_forwarders * optional amount of email-forwarders available for customer, default 0 * @param bool $email_forwarders_ul * optional, whether customer should have unlimited email-forwarders, default 0 (false) * @param int $email_quota * optional size of email-quota available for customer in MB, default is system-setting mail_quota * @param bool $email_quota_ul * optional, whether customer should have unlimited email-quota, default 0 (false) * @param bool $email_imap * optional, whether to allow IMAP access, default 0 (false) * @param bool $email_pop3 * optional, whether to allow POP3 access, default 0 (false) * @param int $ftps * optional amount of ftp-accounts available for customer, default 0 * @param bool $ftps_ul * optional, whether customer should have unlimited ftp-accounts, default 0 (false) * @param int $mysqls * optional amount of mysql-databases available for customer, default 0 * @param bool $mysqls_ul * optional, whether customer should have unlimited mysql-databases, default 0 (false) * @param bool $phpenabled * optional, whether to allow usage of PHP, default 0 (false) * @param array $allowed_phpconfigs * optional, array of IDs of php-config that the customer is allowed to use, default empty (none) * @param bool $perlenabled * optional, whether to allow usage of Perl/CGI, default 0 (false) * @param bool $dnsenabled * optional, either to allow usage of the DNS editor (requires activated nameserver in settings), * default 0 (false) * @param bool $logviewenabled * optional, either to allow access to webserver access/error-logs, default 0 (false) * * @access admin * @return string json-encoded array * @throws Exception */ public function update() { if ($this->isAdmin()) { // parameters $id = $this->getParam('id', true, 0); $dn_optional = $id > 0; $planname = $this->getParam('planname', $dn_optional, ''); // get requested hosting-plan $result = $this->apiCall('HostingPlans.get', [ 'id' => $id, 'planname' => $planname ]); $id = $result['id']; $result['value'] = json_decode($result['value'], true); foreach ($result['value'] as $index => $value) { $result[$index] = $value; } $name = $this->getParam('name', true, $result['name']); $description = $this->getParam('description', true, $result['description']); $value_arr = []; $value_arr['diskspace'] = $this->getUlParam('diskspace', 'diskspace_ul', true, $result['diskspace']); $value_arr['traffic'] = $this->getUlParam('traffic', 'traffic_ul', true, $result['traffic']); $value_arr['subdomains'] = $this->getUlParam('subdomains', 'subdomains_ul', true, $result['subdomains']); $value_arr['emails'] = $this->getUlParam('emails', 'emails_ul', true, $result['emails']); $value_arr['email_accounts'] = $this->getUlParam('email_accounts', 'email_accounts_ul', true, $result['email_accounts']); $value_arr['email_forwarders'] = $this->getUlParam('email_forwarders', 'email_forwarders_ul', true, $result['email_forwarders']); $value_arr['email_quota'] = $this->getUlParam('email_quota', 'email_quota_ul', true, $result['email_quota']); $value_arr['email_imap'] = $this->getParam('email_imap', true, $result['email_imap']); $value_arr['email_pop3'] = $this->getParam('email_pop3', true, $result['email_pop3']); $value_arr['ftps'] = $this->getUlParam('ftps', 'ftps_ul', true, $result['ftps']); $value_arr['mysqls'] = $this->getUlParam('mysqls', 'mysqls_ul', true, $result['mysqls']); $value_arr['phpenabled'] = $this->getBoolParam('phpenabled', true, $result['phpenabled']); $p_allowed_phpconfigs = $this->getParam('allowed_phpconfigs', true, $result['allowed_phpconfigs']); $value_arr['perlenabled'] = $this->getBoolParam('perlenabled', true, $result['perlenabled']); $value_arr['dnsenabled'] = $this->getBoolParam('dnsenabled', true, $result['dnsenabled']); $value_arr['logviewenabled'] = $this->getBoolParam('logviewenabled', true, $result['logviewenabled']); // validation $name = Validate::validate(trim($name), 'name', Validate::REGEX_DESC_TEXT, '', [], true); $description = Validate::validate(str_replace("\r\n", "\n", $description), 'description', Validate::REGEX_DESC_TEXT); if (Settings::Get('system.mail_quota_enabled') != '1') { $value_arr['email_quota'] = -1; } if (empty($name)) { $name = $result['name']; } $value_arr['allowed_phpconfigs'] = []; if (!empty($p_allowed_phpconfigs) && is_array($p_allowed_phpconfigs)) { foreach ($p_allowed_phpconfigs as $allowed_phpconfig) { $allowed_phpconfig = intval($allowed_phpconfig); $value_arr['allowed_phpconfigs'][] = $allowed_phpconfig; } } $value_arr['allowed_phpconfigs'] = array_map('intval', $value_arr['allowed_phpconfigs']); $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_PLANS . "` SET `name` = :name, `description` = :desc, `value` = :valuearr, `ts` = UNIX_TIMESTAMP() WHERE `id` = :id "); $update_data = [ 'name' => $name, 'desc' => $description, 'valuearr' => json_encode($value_arr), 'id' => $id ]; Database::pexecute($upd_stmt, $update_data, true, true); $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] updated hosting-plan '" . $result['name'] . "'"); return $this->response($update_data); } throw new Exception("Not allowed to execute given command.", 403); } /** * delete hosting-plan by either id or plan-name * * @param int $id * optional the hosting-plan-id * @param string $planname * optional the hosting-plan-name * * @access admin * @return string json-encoded array * @throws Exception */ public function delete() { if ($this->isAdmin()) { $id = $this->getParam('id', true, 0); $dn_optional = $id > 0; $planname = $this->getParam('planname', $dn_optional, ''); // get requested hosting-plan $result = $this->apiCall('HostingPlans.get', [ 'id' => $id, 'planname' => $planname ]); $id = $result['id']; $del_stmt = Database::prepare(" DELETE FROM `" . TABLE_PANEL_PLANS . "` WHERE `id` = :id "); Database::pexecute($del_stmt, [ 'id' => $id ], true, true); $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] deleted hosting-plan '" . $result['name'] . "'"); return $this->response($result); } throw new Exception("Not allowed to execute given command.", 403); } } ================================================ FILE: lib/Froxlor/Api/Commands/IpsAndPorts.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Api\Commands; use Exception; use Froxlor\Api\ApiCommand; use Froxlor\Api\ResourceEntity; use Froxlor\Cron\TaskId; use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\FroxlorLogger; use Froxlor\Settings; use Froxlor\System\Cronjob; use Froxlor\UI\Response; use Froxlor\Validate\Validate; use PDO; /** * @since 0.10.0 */ class IpsAndPorts extends ApiCommand implements ResourceEntity { /** * lists all ip/port entries * * @param array $sql_search * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), * LIKE is used if left empty and 'value' => searchvalue * @param int $sql_limit * optional specify number of results to be returned * @param int $sql_offset * optional specify offset for resultset * @param array $sql_orderby * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more * fields * * @access admin * @return string json-encoded array count|list * @throws Exception */ public function listing() { if ($this->isAdmin() && ($this->getUserDetail('change_serversettings') || !empty($this->getUserDetail('ip')))) { $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] list ips and ports"); $ip_where = ""; $append_where = false; if (!empty($this->getUserDetail('ip')) && $this->getUserDetail('ip') != -1) { $ip_where = "WHERE `id` IN (" . implode(", ", json_decode($this->getUserDetail('ip'), true)) . ")"; $append_where = true; } $query_fields = []; $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_IPSANDPORTS . "` " . $ip_where . $this->getSearchWhere($query_fields, $append_where) . $this->getOrderBy() . $this->getLimit()); Database::pexecute($result_stmt, $query_fields, true, true); $result = []; while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $result[] = $row; } return $this->response([ 'count' => count($result), 'list' => $result ]); } throw new Exception("Not allowed to execute given command.", 403); } /** * returns the total number of accessible ip/port entries * * @access admin * @return string json-encoded response message * @throws Exception */ public function listingCount() { if ($this->isAdmin() && ($this->getUserDetail('change_serversettings') || !empty($this->getUserDetail('ip')))) { $ip_where = ""; $query_fields = []; if (!empty($this->getUserDetail('ip')) && $this->getUserDetail('ip') != -1) { $ip_where = "WHERE `id` IN (" . implode(", ", json_decode($this->getUserDetail('ip'), true)) . ") " . $this->getSearchWhere($query_fields, true); } else { $ip_where = $this->getSearchWhere($query_fields); } $result_stmt = Database::prepare(" SELECT COUNT(*) as num_ips FROM `" . TABLE_PANEL_IPSANDPORTS . "` " . $ip_where); $result = Database::pexecute_first($result_stmt, $query_fields, true, true); if ($result) { return $this->response($result['num_ips']); } return $this->response(0); } throw new Exception("Not allowed to execute given command.", 403); } /** * create a new ip/port entry * * @param string $ip * @param int $port * optional, default 80 * @param bool $listen_statement * optional, default 0 (false) * @param bool $namevirtualhost_statement * optional, default 0 (false) * @param bool $vhostcontainer * optional, default 0 (false) * @param string $specialsettings * optional, default empty * @param bool $vhostcontainer_servername_statement * optional, default 0 (false) * @param string $default_vhostconf_domain * optional, defatul empty * @param string $docroot * optional, default empty (point to froxlor) * @param bool $ssl * optional, default 0 (false) * @param string $ssl_cert_file * optional, requires $ssl = 1, default empty * @param string $ssl_key_file * optional, requires $ssl = 1, default empty * @param string $ssl_ca_file * optional, requires $ssl = 1, default empty * @param string $ssl_cert_chainfile * optional, requires $ssl = 1, default empty * @param string $ssl_specialsettings * optional, requires $ssl = 1, default empty * @param bool $include_specialsettings * optional, requires $ssl = 1, whether or not to include non-ssl specialsettings, default false * @param string $ssl_default_vhostconf_domain * optional, requires $ssl = 1, defatul empty * @param bool $include_default_vhostconf_domain * optional, requires $ssl = 1, whether or not to include non-ssl default_vhostconf_domain, default false * * @access admin * @return string json-encoded array * @throws Exception */ public function add() { if ($this->isAdmin() && $this->getUserDetail('change_serversettings')) { $ip = Validate::validate_ip2($this->getParam('ip'), false, 'invalidip', false, true, false, false, true); $port = Validate::validate($this->getParam('port', true, 80), 'port', Validate::REGEX_PORT, [ 'stringisempty', 'myport' ], [], true); $listen_statement = !empty($this->getBoolParam('listen_statement', true, 0)) ? 1 : 0; $namevirtualhost_statement = !empty($this->getBoolParam('namevirtualhost_statement', true, 0)) ? 1 : 0; $vhostcontainer = !empty($this->getBoolParam('vhostcontainer', true, 0)) ? 1 : 0; $ss = $this->getParam('specialsettings', true, ''); $specialsettings = Validate::validate(str_replace("\r\n", "\n", $ss ?? ""), 'specialsettings', Validate::REGEX_CONF_TEXT, '', [], true); $vhostcontainer_servername_statement = !empty($this->getBoolParam('vhostcontainer_servername_statement', true, 1)) ? 1 : 0; $dvd = $this->getParam('default_vhostconf_domain', true, ''); $default_vhostconf_domain = Validate::validate(str_replace("\r\n", "\n", $dvd), 'default_vhostconf_domain', Validate::REGEX_CONF_TEXT, '', [], true); $docroot = Validate::validate($this->getParam('docroot', true, ''), 'docroot', Validate::REGEX_DIR, '', [], true); if ((int)Settings::Get('system.use_ssl') == 1) { $ssl = (bool)$this->getBoolParam('ssl', true, 0); $cert_optional = !($ssl && empty(Settings::Get('system.ssl_cert_file'))); $ssl_cert_file = Validate::validate($this->getParam('ssl_cert_file', $cert_optional, ''), 'ssl_cert_file', '', '', [], true); $ssl_key_file = Validate::validate($this->getParam('ssl_key_file', $cert_optional, ''), 'ssl_key_file', '', '', [], true); $ssl_ca_file = Validate::validate($this->getParam('ssl_ca_file', true, ''), 'ssl_ca_file', '', '', [], true); $ssl_cert_chainfile = Validate::validate($this->getParam('ssl_cert_chainfile', true, ''), 'ssl_cert_chainfile', '', '', [], true); $sslss = $this->getParam('ssl_specialsettings', true, ''); $ssl_specialsettings = Validate::validate(str_replace("\r\n", "\n", $sslss ?? ""), 'ssl_specialsettings', Validate::REGEX_CONF_TEXT, '', [], true); $include_specialsettings = !empty($this->getBoolParam('include_specialsettings', true, 0)) ? 1 : 0; $ssldvd = $this->getParam('ssl_default_vhostconf_domain', true, ''); $ssl_default_vhostconf_domain = Validate::validate(str_replace("\r\n", "\n", $ssldvd ?? ""), 'ssl_default_vhostconf_domain', Validate::REGEX_CONF_TEXT, '', [], true); $include_default_vhostconf_domain = !empty($this->getBoolParam('include_default_vhostconf_domain', true, 0)) ? 1 : 0; } else { $ssl = 0; $ssl_cert_file = ''; $ssl_key_file = ''; $ssl_ca_file = ''; $ssl_cert_chainfile = ''; $ssl_specialsettings = ''; $include_specialsettings = 0; $ssl_default_vhostconf_domain = ''; $include_default_vhostconf_domain = 0; } if ($listen_statement != '1') { $listen_statement = '0'; } if ($namevirtualhost_statement != '1') { $namevirtualhost_statement = '0'; } if ($vhostcontainer != '1') { $vhostcontainer = '0'; } if ($vhostcontainer_servername_statement != '1') { $vhostcontainer_servername_statement = '0'; } if ($ssl != '1') { $ssl = '0'; } if ($ssl_cert_file != '') { $ssl_cert_file = FileDir::makeCorrectFile($ssl_cert_file); } if ($ssl_key_file != '') { $ssl_key_file = FileDir::makeCorrectFile($ssl_key_file); } if ($ssl_ca_file != '') { $ssl_ca_file = FileDir::makeCorrectFile($ssl_ca_file); } if ($ssl_cert_chainfile != '') { $ssl_cert_chainfile = FileDir::makeCorrectFile($ssl_cert_chainfile); } if (strlen(trim($docroot)) > 0) { $docroot = FileDir::makeCorrectDir($docroot); } else { $docroot = ''; } // always use compressed ipv6 format $ip = inet_ntop(inet_pton($ip)); $result_checkfordouble_stmt = Database::prepare(" SELECT `id` FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE `ip` = :ip AND `port` = :port"); $result_checkfordouble = Database::pexecute_first($result_checkfordouble_stmt, [ 'ip' => $ip, 'port' => $port ]); if ($result_checkfordouble && $result_checkfordouble['id'] != '') { Response::standardError('myipnotdouble', '', true); } $ins_stmt = Database::prepare(" INSERT INTO `" . TABLE_PANEL_IPSANDPORTS . "` SET `ip` = :ip, `port` = :port, `listen_statement` = :ls, `namevirtualhost_statement` = :nvhs, `vhostcontainer` = :vhc, `vhostcontainer_servername_statement` = :vhcss, `specialsettings` = :ss, `ssl` = :ssl, `ssl_cert_file` = :ssl_cert, `ssl_key_file` = :ssl_key, `ssl_ca_file` = :ssl_ca, `ssl_cert_chainfile` = :ssl_chain, `default_vhostconf_domain` = :dvhd, `docroot` = :docroot, `ssl_specialsettings` = :ssl_ss, `include_specialsettings` = :incss, `ssl_default_vhostconf_domain` = :ssl_dvhd, `include_default_vhostconf_domain` = :incdvhd; "); $ins_data = [ 'ip' => $ip, 'port' => $port, 'ls' => $listen_statement, 'nvhs' => $namevirtualhost_statement, 'vhc' => $vhostcontainer, 'vhcss' => $vhostcontainer_servername_statement, 'ss' => $specialsettings, 'ssl' => $ssl, 'ssl_cert' => $ssl_cert_file, 'ssl_key' => $ssl_key_file, 'ssl_ca' => $ssl_ca_file, 'ssl_chain' => $ssl_cert_chainfile, 'dvhd' => $default_vhostconf_domain, 'docroot' => $docroot, 'ssl_ss' => $ssl_specialsettings, 'incss' => $include_specialsettings, 'ssl_dvhd' => $ssl_default_vhostconf_domain, 'incdvhd' => $include_default_vhostconf_domain ]; Database::pexecute($ins_stmt, $ins_data); $ins_data['id'] = Database::lastInsertId(); Cronjob::inserttask(TaskId::REBUILD_VHOST); // Using nameserver, insert a task which rebuilds the server config Cronjob::inserttask(TaskId::REBUILD_DNS); if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $ip = '[' . $ip . ']'; } $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] added IP/port '" . $ip . ":" . $port . "'"); // get ip for return-array $result = $this->apiCall('IpsAndPorts.get', [ 'id' => $ins_data['id'] ]); return $this->response($result); } throw new Exception("Not allowed to execute given command.", 403); } /** * return an ip/port entry by id * * @param int $id * ip-port-id * * @access admin * @return string json-encoded array * @throws Exception */ public function get() { if ($this->isAdmin() && ($this->getUserDetail('change_serversettings') || !empty($this->getUserDetail('ip')))) { $id = $this->getParam('id'); if (!empty($this->getUserDetail('ip')) && $this->getUserDetail('ip') != -1) { $allowed_ips = json_decode($this->getUserDetail('ip'), true); if (!in_array($id, $allowed_ips)) { throw new Exception("You cannot access this resource", 405); } } $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE `id` = :id "); $result = Database::pexecute_first($result_stmt, [ 'id' => $id ], true, true); if ($result) { $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] get ip " . $result['ip'] . " " . $result['port']); return $this->response($result); } throw new Exception("IP/port with id #" . $id . " could not be found", 404); } throw new Exception("Not allowed to execute given command.", 403); } /** * update ip/port entry by given id * * @param int $id * @param string $ip * optional * @param int $port * optional, default 80 * @param bool $listen_statement * optional, default 0 (false) * @param bool $namevirtualhost_statement * optional, default 0 (false) * @param bool $vhostcontainer * optional, default 0 (false) * @param string $specialsettings * optional, default empty * @param bool $vhostcontainer_servername_statement * optional, default 0 (false) * @param string $default_vhostconf_domain * optional, defatul empty * @param string $docroot * optional, default empty (point to froxlor) * @param bool $ssl * optional, default 0 (false) * @param string $ssl_cert_file * optional, requires $ssl = 1, default empty * @param string $ssl_key_file * optional, requires $ssl = 1, default empty * @param string $ssl_ca_file * optional, requires $ssl = 1, default empty * @param string $ssl_cert_chainfile * optional, requires $ssl = 1, default empty * @param string $ssl_specialsettings * optional, requires $ssl = 1, default empty * @param bool $include_specialsettings * optional, requires $ssl = 1, whether or not to include non-ssl specialsettings, default false * @param string $ssl_default_vhostconf_domain * optional, requires $ssl = 1, defatul empty * @param bool $include_default_vhostconf_domain * optional, requires $ssl = 1, whether or not to include non-ssl default_vhostconf_domain, default false * * * @access admin * @return string json-encoded array * @throws Exception */ public function update() { if ($this->isAdmin() && $this->getUserDetail('change_serversettings')) { $id = $this->getParam('id'); $result = $this->apiCall('IpsAndPorts.get', [ 'id' => $id ]); $ip = Validate::validate_ip2($this->getParam('ip', true, $result['ip']), false, 'invalidip', false, true, false, false, true); $port = Validate::validate($this->getParam('port', true, $result['port']), 'port', Validate::REGEX_PORT, [ 'stringisempty', 'myport' ], [], true); $listen_statement = $this->getBoolParam('listen_statement', true, $result['listen_statement']); $namevirtualhost_statement = $this->getBoolParam('namevirtualhost_statement', true, $result['namevirtualhost_statement']); $vhostcontainer = $this->getBoolParam('vhostcontainer', true, $result['vhostcontainer']); $ss = $this->getParam('specialsettings', true, $result['specialsettings']); $specialsettings = Validate::validate(str_replace("\r\n", "\n", $ss ?? ""), 'specialsettings', Validate::REGEX_CONF_TEXT, '', [], true); $vhostcontainer_servername_statement = $this->getParam('vhostcontainer_servername_statement', true, $result['vhostcontainer_servername_statement']); $dvd = $this->getParam('default_vhostconf_domain', true, $result['default_vhostconf_domain']); $default_vhostconf_domain = Validate::validate(str_replace("\r\n", "\n", $dvd ?? ""), 'default_vhostconf_domain', Validate::REGEX_CONF_TEXT, '', [], true); $docroot = Validate::validate($this->getParam('docroot', true, $result['docroot']), 'docroot', Validate::REGEX_DIR, '', [], true); if ((int)Settings::Get('system.use_ssl') == 1) { $ssl = (bool)$this->getBoolParam('ssl', true, $result['ssl']); $cert_optional = !($ssl && empty(Settings::Get('system.ssl_cert_file'))); $ssl_cert_file = Validate::validate($this->getParam('ssl_cert_file', $cert_optional, $result['ssl_cert_file']), 'ssl_cert_file', '', '', [], true); $ssl_key_file = Validate::validate($this->getParam('ssl_key_file', $cert_optional, $result['ssl_key_file']), 'ssl_key_file', '', '', [], true); $ssl_ca_file = Validate::validate($this->getParam('ssl_ca_file', true, $result['ssl_ca_file']), 'ssl_ca_file', '', '', [], true); $ssl_cert_chainfile = Validate::validate($this->getParam('ssl_cert_chainfile', true, $result['ssl_cert_chainfile']), 'ssl_cert_chainfile', '', '', [], true); $sslss = $this->getParam('ssl_specialsettings', true, $result['ssl_specialsettings']); $ssl_specialsettings = Validate::validate(str_replace("\r\n", "\n", $sslss ?? ""), 'ssl_specialsettings', Validate::REGEX_CONF_TEXT, '', [], true); $include_specialsettings = $this->getBoolParam('include_specialsettings', true, $result['include_specialsettings']); $ssldvd = $this->getParam('ssl_default_vhostconf_domain', true, $result['ssl_default_vhostconf_domain']); $ssl_default_vhostconf_domain = Validate::validate(str_replace("\r\n", "\n", $ssldvd ?? ""), 'ssl_default_vhostconf_domain', Validate::REGEX_CONF_TEXT, '', [], true); $include_default_vhostconf_domain = $this->getBoolParam('include_default_vhostconf_domain', true, $result['include_default_vhostconf_domain']); } else { $ssl = 0; $ssl_cert_file = ''; $ssl_key_file = ''; $ssl_ca_file = ''; $ssl_cert_chainfile = ''; $ssl_specialsettings = ''; $include_specialsettings = 0; $ssl_default_vhostconf_domain = ''; $include_default_vhostconf_domain = 0; } $result_checkfordouble_stmt = Database::prepare(" SELECT `id` FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE `ip` = :ip AND `port` = :port "); $result_checkfordouble = Database::pexecute_first($result_checkfordouble_stmt, [ 'ip' => $ip, 'port' => $port ]); $result_sameipotherport_stmt = Database::prepare(" SELECT `id` FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE `ip` = :ip AND `id` <> :id "); $result_sameipotherport = Database::pexecute_first($result_sameipotherport_stmt, [ 'ip' => $ip, 'id' => $id ], true, true); if ($listen_statement != '1') { $listen_statement = '0'; } if ($namevirtualhost_statement != '1') { $namevirtualhost_statement = '0'; } if ($vhostcontainer != '1') { $vhostcontainer = '0'; } if ($vhostcontainer_servername_statement != '1') { $vhostcontainer_servername_statement = '0'; } if ($ssl != '1') { $ssl = '0'; } if ($ssl_cert_file != '') { $ssl_cert_file = FileDir::makeCorrectFile($ssl_cert_file); } if ($ssl_key_file != '') { $ssl_key_file = FileDir::makeCorrectFile($ssl_key_file); } if ($ssl_ca_file != '') { $ssl_ca_file = FileDir::makeCorrectFile($ssl_ca_file); } if ($ssl_cert_chainfile != '') { $ssl_cert_chainfile = FileDir::makeCorrectFile($ssl_cert_chainfile); } if (strlen(trim($docroot)) > 0) { $docroot = FileDir::makeCorrectDir($docroot); } else { $docroot = ''; } // always use compressed ipv6 format $ip = inet_ntop(inet_pton($ip)); if ($result['ip'] != $ip && $result['ip'] == Settings::Get('system.ipaddress') && $result_sameipotherport == false) { Response::standardError('cantchangesystemip', '', true); } elseif ($result_checkfordouble && $result_checkfordouble['id'] != '' && $result_checkfordouble['id'] != $id) { Response::standardError('myipnotdouble', '', true); } else { $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_IPSANDPORTS . "` SET `ip` = :ip, `port` = :port, `listen_statement` = :ls, `namevirtualhost_statement` = :nvhs, `vhostcontainer` = :vhc, `vhostcontainer_servername_statement` = :vhcss, `specialsettings` = :ss, `ssl` = :ssl, `ssl_cert_file` = :ssl_cert, `ssl_key_file` = :ssl_key, `ssl_ca_file` = :ssl_ca, `ssl_cert_chainfile` = :ssl_chain, `default_vhostconf_domain` = :dvhd, `docroot` = :docroot, `ssl_specialsettings` = :ssl_ss, `include_specialsettings` = :incss, `ssl_default_vhostconf_domain` = :ssl_dvhd, `include_default_vhostconf_domain` = :incdvhd WHERE `id` = :id; "); $upd_data = [ 'ip' => $ip, 'port' => $port, 'ls' => $listen_statement, 'nvhs' => $namevirtualhost_statement, 'vhc' => $vhostcontainer, 'vhcss' => $vhostcontainer_servername_statement, 'ss' => $specialsettings, 'ssl' => $ssl, 'ssl_cert' => $ssl_cert_file, 'ssl_key' => $ssl_key_file, 'ssl_ca' => $ssl_ca_file, 'ssl_chain' => $ssl_cert_chainfile, 'dvhd' => $default_vhostconf_domain, 'docroot' => $docroot, 'ssl_ss' => $ssl_specialsettings, 'incss' => $include_specialsettings, 'ssl_dvhd' => $ssl_default_vhostconf_domain, 'incdvhd' => $include_default_vhostconf_domain, 'id' => $id ]; Database::pexecute($upd_stmt, $upd_data); Cronjob::inserttask(TaskId::REBUILD_VHOST); // Using nameserver, insert a task which rebuilds the server config Cronjob::inserttask(TaskId::REBUILD_DNS); $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] changed IP/port from '" . $result['ip'] . ":" . $result['port'] . "' to '" . $ip . ":" . $port . "'"); $result = $this->apiCall('IpsAndPorts.get', [ 'id' => $result['id'] ]); return $this->response($result); } } throw new Exception("Not allowed to execute given command.", 403); } /** * delete an ip/port entry by id * * @param int $id * ip-port-id * * @access admin * @return string json-encoded array * @throws Exception */ public function delete() { if ($this->isAdmin() && $this->getUserDetail('change_serversettings')) { $id = $this->getParam('id'); $result = $this->apiCall('IpsAndPorts.get', [ 'id' => $id ]); $result_checkdomain_stmt = Database::prepare(" SELECT `id_domain` FROM `" . TABLE_DOMAINTOIP . "` WHERE `id_ipandports` = :id "); $result_checkdomain = Database::pexecute_first($result_checkdomain_stmt, [ 'id' => $id ], true, true); if (empty($result_checkdomain)) { if (!in_array($result['id'], explode(',', Settings::Get('system.defaultip'))) && !in_array($result['id'], explode(',', Settings::Get('system.defaultsslip')))) { // check whether there is the same IP with a different port // in case this ip-address is the system.ipaddress and therefore // when there is one - we have an alternative $result_sameipotherport_stmt = Database::prepare(" SELECT `id` FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE `ip` = :ip AND `id` <> :id"); $result_sameipotherport = Database::pexecute_first($result_sameipotherport_stmt, [ 'id' => $id, 'ip' => $result['ip'] ]); if (($result['ip'] != Settings::Get('system.ipaddress')) || ($result['ip'] == Settings::Get('system.ipaddress') && $result_sameipotherport != false)) { $del_stmt = Database::prepare(" DELETE FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE `id` = :id "); Database::pexecute($del_stmt, [ 'id' => $id ], true, true); // also, remove connections to domains (multi-stack) $del_stmt = Database::prepare(" DELETE FROM `" . TABLE_DOMAINTOIP . "` WHERE `id_ipandports` = :id "); Database::pexecute($del_stmt, [ 'id' => $id ], true, true); Cronjob::inserttask(TaskId::REBUILD_VHOST); // Using nameserver, insert a task which rebuilds the server config Cronjob::inserttask(TaskId::REBUILD_DNS); $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] deleted IP/port '" . $result['ip'] . ":" . $result['port'] . "'"); return $this->response($result); } else { Response::standardError('cantdeletesystemip', '', true); } } else { Response::standardError('cantdeletedefaultip', '', true); } } else { Response::standardError('ipstillhasdomains', '', true); } } throw new Exception("Not allowed to execute given command.", 403); } } ================================================ FILE: lib/Froxlor/Api/Commands/MysqlServer.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Api\Commands; use Exception; use Froxlor\Api\ApiCommand; use Froxlor\Api\ResourceEntity; use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\Froxlor; use Froxlor\FroxlorLogger; use Froxlor\PhpHelper; use Froxlor\Validate\Validate; use PDO; use PDOException; class MysqlServer extends ApiCommand implements ResourceEntity { /** * check whether the user is allowed * * @throws Exception */ private function validateAccess() { if ($this->isAdmin() == false || ($this->isAdmin() && $this->getUserDetail('change_serversettings') == 0)) { throw new Exception("You cannot access this resource", 405); } } /** * add a new mysql-server * * @param string $mysql_host * ip/hostname of mysql-server * @param string $mysql_port * optional, port to connect to * @param string $mysql_ca * optional, path to certificate file * @param string $mysql_verifycert * optional, verify server certificate * @param string $privileged_user * privileged user on the mysql-server (must have GRANT privileges) * @param string $privileged_password * password of privileged user * @param string $description * optional, description for server * @param bool $allow_all_customers * optional add this configuration to the list of every existing customer's allowed-mysqlserver-config list, default is false (no) * @param bool $test_connection * optional, test connection with given credentials, default is true (yes) * * @access admin * @return string json-encoded array * @throws Exception */ public function add() { $this->validateAccess(); $mysql_host = $this->getParam('mysql_host'); $mysql_port = $this->getParam('mysql_port', true, 3306); $mysql_ca = $this->getParam('mysql_ca', true, ''); $mysql_verifycert = $this->getBoolParam('mysql_verifycert', true, 0); $privileged_user = $this->getParam('privileged_user'); $privileged_password = $this->getParam('privileged_password'); $description = $this->getParam('description', true, ''); $allow_all_customers = $this->getParam('allow_all_customers', true, 0); $test_connection = $this->getParam('test_connection', true, 1); // validation $mysql_host_chk = Validate::validate_ip2($mysql_host, true, 'invalidip', true, true, false); if ($mysql_host_chk === false) { $mysql_host_chk = Validate::validateLocalHostname($mysql_host); if ($mysql_host_chk === false) { $mysql_host_chk = Validate::validateDomain($mysql_host); if ($mysql_host_chk === false) { throw new Exception("Invalid mysql server ip/hostname", 406); } } } $mysql_port = Validate::validate($mysql_port, 'port', Validate::REGEX_PORT, '', [3306], true); $mysql_ca = !empty($mysql_ca) ? FileDir::makeCorrectFile($mysql_ca) : ''; $privileged_user = Validate::validate($privileged_user, 'privileged_user', '/^[a-z][a-z0-9\-_]+$/i', '', [], true); $privileged_password = Validate::validate($privileged_password, 'password', '', '', [], true); $description = Validate::validate(trim($description), 'description', Validate::REGEX_DESC_TEXT, '', [], true); // testing connection with given credentials if ($test_connection) { $options = array( PDO::MYSQL_ATTR_INIT_COMMAND => 'SET names utf8' ); if (!empty($mysql_ca)) { $options[PDO::MYSQL_ATTR_SSL_CA] = $mysql_ca; $options[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = (bool)$mysql_verifycert; } $dsn = "mysql:host=" . $mysql_host . ";port=" . $mysql_port . ";"; try { $db_test = new \PDO($dsn, $privileged_user, $privileged_password, $options); unset($db_test); } catch (PDOException $e) { throw new Exception("Connection to given mysql database could not be established. Error-message: " . $e->getMessage(), $e->getCode()); } } $sql = []; $sql_root = []; // get all data from lib/userdata require Froxlor::getInstallDir() . "/lib/userdata.inc.php"; // le format if (isset($sql['root_user']) && isset($sql['root_password']) && !is_array($sql_root)) { $sql_root = array( 0 => array( 'caption' => 'Default', 'host' => $sql['host'], 'socket' => (isset($sql['socket']) ? $sql['socket'] : null), 'user' => $sql['root_user'], 'password' => $sql['root_password'] ) ); unset($sql['root_user']); unset($sql['root_password']); } // add new values to sql_root array $sql_root[] = [ 'caption' => $description, 'host' => $mysql_host, 'port' => $mysql_port, 'user' => $privileged_user, 'password' => $privileged_password, 'ssl' => [ 'caFile' => $mysql_ca ?? "", 'verifyServerCertificate' => $mysql_verifycert ] ]; $this->generateNewUserData($sql, $sql_root); // last added to array $newdbserver = array_key_last($sql_root); if ($allow_all_customers) { $this->addDatabaseFromCustomerAllowedList($newdbserver); } $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] added new database server '" . $description . "' (" . $mysql_host . ")"); return $this->response(['dbserver' => $newdbserver]); } /** * remove a mysql-server * * @param int $id * optional the number of the mysql server (either id or dbserver must be set) * @param int $dbserver * optional the number of the mysql server (either id or dbserver must be set) * * @access admin * @return string json-encoded array * @throws Exception */ public function delete() { $this->validateAccess(); $id = (int)$this->getParam('id', true, -1); $dn_optional = $id >= 0; $dbserver = (int)$this->getParam('dbserver', $dn_optional, -1); $dbserver = $id >= 0 ? $id : $dbserver; if ($dbserver == 0) { throw new Exception('Cannot delete first/default mysql-server', 406); } $sql_root = []; // get all data from lib/userdata require Froxlor::getInstallDir() . "/lib/userdata.inc.php"; if (!isset($sql_root[$dbserver])) { throw new Exception('Mysql server not found', 404); } // check whether the server is in use by any customer $result_ms = $this->databasesOnServer(true, $dbserver); if ($result_ms > 0) { throw new Exception(lng('error.mysqlserverstillhasdbs'), 406); } // when removing, remove from list of allowed_mysqlservers from any customers $this->removeDatabaseFromCustomerAllowedList($dbserver); $description = $sql_root[$dbserver]['caption'] ?? "unknown"; $mysql_host = $sql_root[$dbserver]['host'] ?? "unknown"; unset($sql_root[$dbserver]); $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] removed database server '" . $description . "' (" . $mysql_host . ")"); $this->generateNewUserData($sql, $sql_root); return $this->response(['true']); } /** * list available mysql-server * * @access admin, customer * @return string json-encoded array */ public function listing() { $sql = []; $sql_root = []; // get all data from lib/userdata require Froxlor::getInstallDir() . "/lib/userdata.inc.php"; // limit customer to its allowed servers $allowed_mysqls = []; if ($this->isAdmin() == false) { $allowed_mysqls = json_decode($this->getUserDetail('allowed_mysqlserver'), true); } $result = []; foreach ($sql_root as $index => $sqlrootdata) { if ($this->isAdmin() == false) { if ($allowed_mysqls === false || empty($allowed_mysqls)) { break; } elseif (!in_array($index, $allowed_mysqls)) { continue; } // no usernames required for non-admins unset($sqlrootdata['user']); } // passwords will not be returned in any case for security reasons unset($sqlrootdata['password']); $sqlrootdata['id'] = $index; $result[$index] = $sqlrootdata; } return $this->response(['list' => $result, 'count' => count($result)]); } /** * returns the total number of mysql servers * * @access admin, customer * @return string json-encoded response message */ public function listingCount() { if ($this->isAdmin() == false) { $allowed_mysqls = json_decode($this->getUserDetail('allowed_mysqlserver'), true); if ($allowed_mysqls) { return $this->response(count($allowed_mysqls)); } return $this->response(0); } $sql_root = []; // get all data from lib/userdata require Froxlor::getInstallDir() . "/lib/userdata.inc.php"; return $this->response(count($sql_root)); } /** * Return info about a specific mysql-server * * @param int $id * optional the number of the mysql server (either id or dbserver must be set) * @param int $dbserver * optional the number of the mysql server (either id or dbserver must be set) * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function get() { $id = (int)$this->getParam('id', true, -1); $dn_optional = $id >= 0; $dbserver = (int)$this->getParam('dbserver', $dn_optional, -1); $dbserver = $id >= 0 ? $id : $dbserver; $sql_root = []; // get all data from lib/userdata require Froxlor::getInstallDir() . "/lib/userdata.inc.php"; if (!isset($sql_root[$dbserver])) { throw new Exception('Mysql server not found', 404); } // limit customer to its allowed servers if ($this->isAdmin() == false) { $allowed_mysqls = json_decode($this->getUserDetail('allowed_mysqlserver'), true); if ($allowed_mysqls === false || empty($allowed_mysqls) || !in_array($dbserver, $allowed_mysqls)) { throw new Exception("You cannot access this resource", 405); } // no usernames required for non-admins unset($sql_root[$dbserver]['user']); } unset($sql_root[$dbserver]['password']); $sql_root[$dbserver]['id'] = $dbserver; $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] get database-server '" . $sql_root[$dbserver]['caption'] . "'"); return $this->response($sql_root[$dbserver]); } /** * update given mysql-server * * @param int $id * optional the number of the mysql server (either id or dbserver must be set) * @param int $dbserver * optional the number of the mysql server (either id or dbserver must be set) * @param string $mysql_host * ip/hostname of mysql-server * @param string $mysql_port * optional, port to connect to * @param string $mysql_ca * optional, path to certificate file * @param string $mysql_verifycert * optional, verify server certificate * @param string $privileged_user * privileged user on the mysql-server (must have GRANT privileges) * @param string $privileged_password * password of privileged user * @param string $description * optional, description for server * @param bool $allow_all_customers * optional add this configuration to the list of every existing customer's allowed-mysqlserver-config list, default is false (no) * @param bool $test_connection * optional, test connection with given credentials, default is true (yes) * * @access admin * @return string json-encoded array * @throws Exception */ public function update() { $this->validateAccess(); $id = (int)$this->getParam('id', true, -1); $dn_optional = $id >= 0; $dbserver = (int)$this->getParam('dbserver', $dn_optional, -1); $dbserver = $id >= 0 ? $id : $dbserver; $sql_root = []; require Froxlor::getInstallDir() . "/lib/userdata.inc.php"; if (!isset($sql_root[$dbserver])) { throw new Exception('Mysql server not found', 404); } $result = $sql_root[$dbserver]; if ($dbserver == 0) { $mysql_host = $result['host']; } else { $mysql_host = $this->getParam('mysql_host', true, $result['host']); } $mysql_port = $this->getParam('mysql_port', true, $result['port'] ?? 3306); $mysql_ca = $this->getParam('mysql_ca', true, $result['ssl']['caFile'] ?? ''); $mysql_verifycert = $this->getBoolParam('mysql_verifycert', true, $result['ssl']['verifyServerCertificate'] ?? 0); $privileged_user = $this->getParam('privileged_user', true, $result['user']); $privileged_password = $this->getParam('privileged_password', true, ''); $description = $this->getParam('description', true, $result['caption']); $allow_all_customers = $this->getParam('allow_all_customers', true, 0); $test_connection = $this->getParam('test_connection', true, 1); // validation $mysql_host_chk = Validate::validate_ip2($mysql_host, true, 'invalidip', true, true, false); if ($mysql_host_chk === false) { $mysql_host_chk = Validate::validateLocalHostname($mysql_host); if ($mysql_host_chk === false) { $mysql_host_chk = Validate::validateDomain($mysql_host); if ($mysql_host_chk === false) { throw new Exception("Invalid mysql server ip/hostname", 406); } } } $mysql_port = Validate::validate($mysql_port, 'port', Validate::REGEX_PORT, '', [3306], true); $mysql_ca = !empty($mysql_ca) ? FileDir::makeCorrectFile($mysql_ca) : ''; $privileged_user = Validate::validate($privileged_user, 'privileged_user', '/^[a-z][a-z0-9\-_]+$/i', '', [], true); $privileged_password = Validate::validate($privileged_password, 'password', '', '', [], true); $description = Validate::validate(trim($description), 'description', Validate::REGEX_DESC_TEXT, '', [], true); // keep old password? if (empty($privileged_password)) { $privileged_password = $result['password']; } if ($mysql_host != $result['host']) { // check whether the server is in use by any customer $result_ms = $this->databasesOnServer(true, $dbserver); if ($result_ms > 0) { throw new Exception("Unable to update mysql-host as there are still databases on it", 406); } } // testing connection with given credentials if ($test_connection) { $options = array( PDO::MYSQL_ATTR_INIT_COMMAND => 'SET names utf8' ); if (!empty($mysql_ca)) { $options[PDO::MYSQL_ATTR_SSL_CA] = $mysql_ca; $options[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = (bool)$mysql_verifycert; } $dsn = "mysql:host=" . $mysql_host . ";port=" . $mysql_port . ";"; try { $db_test = new \PDO($dsn, $privileged_user, $privileged_password, $options); unset($db_test); } catch (PDOException $e) { throw new Exception("Connection to given mysql database could not be established. Error-message: " . $e->getMessage(), $e->getCode()); } } // set new values to sql_root array $sql_root[$dbserver] = [ 'caption' => $description, 'host' => $mysql_host, 'port' => $mysql_port, 'user' => $privileged_user, 'password' => $privileged_password, 'ssl' => [ 'caFile' => $mysql_ca ?? "", 'verifyServerCertificate' => $mysql_verifycert ?? false ] ]; $this->generateNewUserData($sql, $sql_root); if ($allow_all_customers) { $this->addDatabaseFromCustomerAllowedList($dbserver); } $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] edited database server '" . $description . "' (" . $mysql_host . ")"); return $this->response(['true']); } /** * check whether a given customer / current user (as customer) has * databases on the given dbserver * * @param int $mysql_server * @param int $customerid * optional, admin-only, select ftp-users of a specific customer by id * @param string $loginname * optional, admin-only, select ftp-users of a specific customer by loginname * * @access admin, customer * @return string json-encoded array count */ public function databasesOnServer(bool $internal_all = false, int $dbserver = 0) { if ($internal_all) { $result_stmt = Database::prepare(" SELECT COUNT(*) num_dbs FROM `" . TABLE_PANEL_DATABASES . "` WHERE `dbserver` = :dbserver "); $result = Database::pexecute_first($result_stmt, ['dbserver' => $dbserver], true, true); return (int)$result['num_dbs']; } else { $dbserver = $this->getParam('mysql_server'); $customer_ids = $this->getAllowedCustomerIds(); $result_stmt = Database::prepare(" SELECT COUNT(*) num_dbs FROM `" . TABLE_PANEL_DATABASES . "` WHERE `customerid` IN (" . implode(", ", $customer_ids) . ") AND `dbserver` = :dbserver "); $result = Database::pexecute_first($result_stmt, ['dbserver' => $dbserver], true, true); return $this->response(['count' => $result['num_dbs']]); } } private function removeDatabaseFromCustomerAllowedList(int $dbserver) { $sel_stmt = Database::prepare(" SELECT customerid, allowed_mysqlserver FROM `" . TABLE_PANEL_CUSTOMERS . "` "); Database::pexecute($sel_stmt); $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `allowed_mysqlserver` = :am WHERE `customerid` = :cid "); while ($customer = $sel_stmt->fetch(PDO::FETCH_ASSOC)) { $allowed_mysqls = json_decode(($customer['allowed_mysqlserver'] ?? '[]'), true); if (($key = array_search($dbserver, $allowed_mysqls)) !== false) { unset($allowed_mysqls[$key]); $allowed_mysqls = json_encode($allowed_mysqls); Database::pexecute($upd_stmt, ['am' => $allowed_mysqls, 'cid' => $customer['customerid']]); } } } private function addDatabaseFromCustomerAllowedList(int $dbserver) { $sel_stmt = Database::prepare(" SELECT customerid, allowed_mysqlserver FROM `" . TABLE_PANEL_CUSTOMERS . "` "); Database::pexecute($sel_stmt); $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `allowed_mysqlserver` = :am WHERE `customerid` = :cid "); while ($customer = $sel_stmt->fetch(PDO::FETCH_ASSOC)) { $allowed_mysqls = json_decode(($customer['allowed_mysqlserver'] ?: '[]'), true); if (!in_array($dbserver, $allowed_mysqls)) { $allowed_mysqls[] = $dbserver; $allowed_mysqls = json_encode($allowed_mysqls); Database::pexecute($upd_stmt, ['am' => $allowed_mysqls, 'cid' => $customer['customerid']]); } } } /** * write new userdata.inc.php file */ private function generateNewUserData(array $sql, array $sql_root) { $content = PhpHelper::parseArrayToPhpFile( ['sql' => $sql, 'sql_root' => $sql_root], 'automatically generated userdata.inc.php for froxlor' ); chmod(Froxlor::getInstallDir() . "/lib/userdata.inc.php", 0700); file_put_contents(Froxlor::getInstallDir() . "/lib/userdata.inc.php", $content); chmod(Froxlor::getInstallDir() . "/lib/userdata.inc.php", 0400); clearstatcache(); if (function_exists('opcache_invalidate')) { @opcache_invalidate(Froxlor::getInstallDir() . "/lib/userdata.inc.php", true); } } } ================================================ FILE: lib/Froxlor/Api/Commands/Mysqls.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Api\Commands; use Exception; use Froxlor\Api\ApiCommand; use Froxlor\Api\ResourceEntity; use Froxlor\Database\Database; use Froxlor\Database\DbManager; use Froxlor\FroxlorLogger; use Froxlor\Settings; use Froxlor\System\Crypt; use Froxlor\UI\Response; use Froxlor\User; use Froxlor\Validate\Validate; use PDO; /** * @since 0.10.0 */ class Mysqls extends ApiCommand implements ResourceEntity { /** * add a new mysql-database * * @param string $mysql_password * password for the created database and database-user * @param int $mysql_server * optional, default is 0 * @param string $description * optional, description for database * @param string $custom_suffix * optional, name for database if customer.mysqlprefix setting is set to "DBNAME" * @param bool $sendinfomail * optional, send created resource-information to customer, default: false * @param int $customerid * optional, required when called as admin (if $loginname is not specified) * @param string $loginname * optional, required when called as admin (if $customerid is not specified) * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function add() { if (($this->getUserDetail('mysqls_used') < $this->getUserDetail('mysqls') || $this->getUserDetail('mysqls') == '-1') || $this->isAdmin()) { // required parameters $password = $this->getParam('mysql_password'); // parameters $databasedescription = $this->getParam('description', true, ''); $databasename = $this->getParam('custom_suffix', true, ''); $sendinfomail = $this->getBoolParam('sendinfomail', true, 0); // get needed customer info to reduce the mysql-usage-counter by one $customer = $this->getCustomerData('mysqls'); $dbserver = $this->getParam('mysql_server', true, $this->getDefaultMySqlServer($customer)); // validation $password = Validate::validate($password, 'password', '', '', [], true); $password = Crypt::validatePassword($password, true); $databasedescription = Validate::validate(trim($databasedescription), 'description', Validate::REGEX_DESC_TEXT, '', [], true); if (!empty($databasename)) { $databasename = Validate::validate(trim($databasename), 'database_name', '/^[A-Za-z0-9][A-Za-z0-9\-_]+$/i', '', [], true); } // validate whether the dbserver exists $dbserver = Validate::validate($dbserver, html_entity_decode(lng('mysql.mysql_server')), '/^[0-9]+$/', '', 0, true); // enforce per-customer allowed_mysqlserver allowlist if (!$this->isAdmin()) { $allowed = json_decode($customer['allowed_mysqlserver'] ?? '[]', true); if (!is_array($allowed) || empty($allowed) || !in_array((int)$dbserver, array_map('intval', $allowed), true)) { throw new Exception('You cannot access this resource', 405); } } Database::needRoot(true, $dbserver, false); Database::needSqlData(); $sql_root = Database::getSqlData(); Database::needRoot(false); if (!is_array($sql_root)) { throw new Exception("Database server with index #" . $dbserver . " is unknown", 404); } if ($sendinfomail != 1) { $sendinfomail = 0; } $newdb_params = [ 'loginname' => ($this->isAdmin() ? $customer['loginname'] : $this->getUserDetail('loginname')), 'mysql_lastaccountnumber' => ($this->isAdmin() ? $customer['mysql_lastaccountnumber'] : $this->getUserDetail('mysql_lastaccountnumber')) ]; // create database, user, set permissions, etc.pp. $dbm = new DbManager($this->logger()); if (strtoupper(Settings::Get('customer.mysqlprefix')) == 'DBNAME' && !empty($databasename)) { if (strlen($newdb_params['loginname'] . '_' . $databasename) > Database::getSqlUsernameLength()) { throw new Exception("Database name cannot be longer than " . (Database::getSqlUsernameLength() - strlen($newdb_params['loginname'] . '_')) . " characters.", 406); } $username = $dbm->createDatabase($newdb_params['loginname'] . '_' . $databasename, $password, $dbserver, 0, $newdb_params['loginname']); } else { $username = $dbm->createDatabase($newdb_params['loginname'], $password, $dbserver, $newdb_params['mysql_lastaccountnumber'], $newdb_params['loginname']); } // we've checked against the password in dbm->createDatabase if ($username == false) { Response::standardError('passwordshouldnotbeusername', '', true); } // add database info to froxlor $stmt = Database::prepare(" INSERT INTO `" . TABLE_PANEL_DATABASES . "` SET `customerid` = :customerid, `databasename` = :databasename, `description` = :description, `dbserver` = :dbserver "); $params = [ "customerid" => $customer['customerid'], "databasename" => $username, "description" => $databasedescription, "dbserver" => $dbserver ]; Database::pexecute($stmt, $params, true, true); $databaseid = Database::lastInsertId(); $params['id'] = $databaseid; // update customer usage Customers::increaseUsage($customer['customerid'], 'mysqls_used'); Customers::increaseUsage($customer['customerid'], 'mysql_lastaccountnumber'); // send info-mail? if ($sendinfomail == 1) { $pma = lng('admin.notgiven'); if (Settings::Get('panel.phpmyadmin_url') != '') { $pma = Settings::Get('panel.phpmyadmin_url'); } Database::needRoot(true, $dbserver, false); Database::needSqlData(); $sql_root = Database::getSqlData(); Database::needRoot(false); $userinfo = $customer; $replace_arr = [ 'SALUTATION' => User::getCorrectUserSalutation($userinfo), 'CUST_NAME' => User::getCorrectUserSalutation($userinfo), // < keep this for compatibility 'NAME' => $userinfo['name'], 'FIRSTNAME' => $userinfo['firstname'], 'COMPANY' => $userinfo['company'], 'USERNAME' => $userinfo['loginname'], 'CUSTOMER_NO' => $userinfo['customernumber'], 'DB_NAME' => $username, 'DB_PASS' => htmlentities(htmlentities($password)), 'DB_DESC' => $databasedescription, 'DB_SRV' => $sql_root['host'], 'PMA_URI' => $pma ]; // get template for mail subject $mail_subject = $this->getMailTemplate($userinfo, 'mails', 'new_database_by_customer_subject', $replace_arr, lng('mails.new_database_by_customer.subject')); // get template for mail body $mail_body = $this->getMailTemplate($userinfo, 'mails', 'new_database_by_customer_mailbody', $replace_arr, lng('mails.new_database_by_customer.mailbody')); $_mailerror = false; $mailerr_msg = ""; try { $this->mailer()->Subject = $mail_subject; $this->mailer()->AltBody = $mail_body; $this->mailer()->Body = str_replace("\n", "
", $mail_body); $this->mailer()->addAddress($userinfo['email'], User::getCorrectUserSalutation($userinfo)); $this->mailer()->send(); } catch (\PHPMailer\PHPMailer\Exception $e) { $mailerr_msg = $e->errorMessage(); $_mailerror = true; } catch (Exception $e) { $mailerr_msg = $e->getMessage(); $_mailerror = true; } if ($_mailerror) { $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_ERR, "[API] Error sending mail: " . $mailerr_msg); Response::standardError('errorsendingmail', $userinfo['email'], true); } $this->mailer()->clearAddresses(); } $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] added mysql-database '" . $username . "'"); $result = $this->apiCall('Mysqls.get', [ 'dbname' => $username, 'mysql_server' => $dbserver ]); return $this->response($result); } throw new Exception("No more resources available", 406); } /** * return a mysql database entry by either id or dbname * * @param int $id * optional, the database-id * @param string $dbname * optional, the databasename * @param int $mysql_server * optional, specify database-server, default is none * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function get() { $id = $this->getParam('id', true, 0); $dn_optional = $id > 0; $dbname = $this->getParam('dbname', $dn_optional, ''); $dbserver = $this->getParam('mysql_server', true, -1); if ($dbserver != -1) { $dbserver = Validate::validate($dbserver, html_entity_decode(lng('mysql.mysql_server')), '/^[0-9]+$/', '', 0, true); } if ($this->isAdmin()) { if ($this->getUserDetail('customers_see_all') != 1) { // if it's a reseller or an admin who cannot see all customers, we need to check // whether the database belongs to one of his customers $_custom_list_result = $this->apiCall('Customers.listing'); $custom_list_result = $_custom_list_result['list']; $customer_ids = []; foreach ($custom_list_result as $customer) { $customer_ids[] = $customer['customerid']; } if (count($customer_ids) > 0) { $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_DATABASES . "` WHERE " . ($id > 0 ? "`id` = :iddn" : "`databasename` = :iddn") . ($dbserver >= 0 ? " AND `dbserver` = :dbserver" : "") . " AND `customerid` IN (" . implode(", ", $customer_ids) . ") "); $params = [ 'iddn' => ($id <= 0 ? $dbname : $id) ]; if ($dbserver >= 0) { $params['dbserver'] = $dbserver; } } else { throw new Exception("You do not have any customers yet", 406); } } else { $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_DATABASES . "` WHERE " . ($id > 0 ? "`id` = :iddn" : "`databasename` = :iddn") . ($dbserver >= 0 ? " AND `dbserver` = :dbserver" : "")); $params = [ 'iddn' => ($id <= 0 ? $dbname : $id) ]; if ($dbserver >= 0) { $params['dbserver'] = $dbserver; } } } else { if (Settings::IsInList('panel.customer_hide_options', 'mysql')) { throw new Exception("You cannot access this resource", 405); } $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_DATABASES . "` WHERE `customerid`= :customerid AND " . ($id > 0 ? "`id` = :iddn" : "`databasename` = :iddn") . ($dbserver >= 0 ? " AND `dbserver` = :dbserver" : "")); $params = [ 'customerid' => $this->getUserDetail('customerid'), 'iddn' => ($id <= 0 ? $dbname : $id) ]; if ($dbserver >= 0) { $params['dbserver'] = $dbserver; } } $result = Database::pexecute_first($result_stmt, $params, true, true); if ($result) { Database::needRoot(true, $result['dbserver'], false); $mbdata_stmt = Database::prepare(" SELECT SUM(data_length + index_length) as MB FROM information_schema.TABLES WHERE table_schema = :table_schema GROUP BY table_schema "); Database::pexecute($mbdata_stmt, [ "table_schema" => $result['databasename'] ], true, true); $mbdata = $mbdata_stmt->fetch(PDO::FETCH_ASSOC); Database::needRoot(false); $result['size'] = $mbdata['MB'] ?? 0; $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] get database '" . $result['databasename'] . "'"); return $this->response($result); } $key = ($id > 0 ? "id #" . $id : "dbname '" . $dbname . "'"); throw new Exception("MySQL database with " . $key . " could not be found", 404); } /** * update a mysql database entry by either id or dbname * * @param int $id * optional, the database-id * @param string $dbname * optional, the databasename * @param int $mysql_server * optional, specify database-server, default is none * @param string $mysql_password * optional, update password for the database * @param string $description * optional, description for database * @param int $customerid * optional, required when called as admin (if $loginname is not specified) * @param string $loginname * optional, required when called as admin (if $customerid is not specified) * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function update() { $id = $this->getParam('id', true, 0); $dn_optional = $id > 0; $dbname = $this->getParam('dbname', $dn_optional, ''); $dbserver = $this->getParam('mysql_server', true, -1); $customer = $this->getCustomerData(); if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'mysql')) { throw new Exception("You cannot access this resource", 405); } $result = $this->apiCall('Mysqls.get', [ 'id' => $id, 'dbname' => $dbname, 'mysql_server' => $dbserver ]); $id = $result['id']; // parameters $password = $this->getParam('mysql_password', true, ''); $databasedescription = $this->getParam('description', true, $result['description']); // validation $password = Validate::validate($password, 'password', '', '', [], true); $databasedescription = Validate::validate(trim($databasedescription), 'description', Validate::REGEX_DESC_TEXT, '', [], true); if ($password != '') { // validate password $password = Crypt::validatePassword($password, true); if ($password == $result['databasename']) { Response::standardError('passwordshouldnotbeusername', '', true); } // Begin root-session Database::needRoot(true, $result['dbserver'], false); $dbmgr = new DbManager($this->logger()); foreach (array_map('trim', explode(',', Settings::Get('system.mysql_access_host'))) as $mysql_access_host) { $dbmgr->getManager()->grantPrivilegesTo($result['databasename'], $password, $mysql_access_host, false, true); } $stmt = Database::prepare("FLUSH PRIVILEGES"); Database::pexecute($stmt, null, true, true); Database::needRoot(false); // End root-session } $stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_DATABASES . "` SET `description` = :desc WHERE `customerid` = :customerid AND `id` = :id "); $params = [ "desc" => $databasedescription, "customerid" => $customer['customerid'], "id" => $id ]; Database::pexecute($stmt, $params, true, true); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] updated mysql-database '" . $result['databasename'] . "'"); $result = $this->apiCall('Mysqls.get', [ 'dbname' => $result['databasename'] ]); return $this->response($result); } /** * list all databases, if called from an admin, list all databases of all customers you are allowed to view, or * specify id or loginname for one specific customer * * @param int $mysql_server * optional, specify dbserver to select from, else use all available * @param int $customerid * optional, admin-only, select dbs of a specific customer by id * @param string $loginname * optional, admin-only, select dbs of a specific customer by loginname * @param array $sql_search * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), * LIKE is used if left empty and 'value' => searchvalue * @param int $sql_limit * optional specify number of results to be returned * @param int $sql_offset * optional specify offset for resultset * @param array $sql_orderby * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more * fields * * @access admin, customer * @return string json-encoded array count|list * @throws Exception */ public function listing() { $result = []; $dbserver = $this->getParam('mysql_server', true, -1); $customer_ids = $this->getAllowedCustomerIds('mysql'); $query_fields = []; $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_DATABASES . "` WHERE `customerid`= :customerid AND `dbserver` = :dbserver" . $this->getSearchWhere($query_fields, true) . $this->getOrderBy() . $this->getLimit()); if ($dbserver < 0) { // use all dbservers $dbservers_stmt = Database::query("SELECT DISTINCT `dbserver` FROM `" . TABLE_PANEL_DATABASES . "`"); $dbservers = $dbservers_stmt->fetchAll(PDO::FETCH_ASSOC); } else { // use specific dbserver $dbservers = [ [ 'dbserver' => $dbserver ] ]; } foreach ($customer_ids as $customer_id) { foreach ($dbservers as $_dbserver) { Database::pexecute($result_stmt, array_merge([ 'customerid' => $customer_id, 'dbserver' => $_dbserver['dbserver'] ], $query_fields), true, true); // Begin root-session Database::needRoot(true, $_dbserver['dbserver'], false); while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $mbdata_stmt = Database::prepare(" SELECT SUM(data_length + index_length) as MB FROM information_schema.TABLES WHERE table_schema = :table_schema GROUP BY table_schema "); Database::pexecute($mbdata_stmt, [ "table_schema" => $row['databasename'] ], true, true); $mbdata = $mbdata_stmt->fetch(PDO::FETCH_ASSOC); $row['size'] = $mbdata['MB'] ?? 0; $result[] = $row; } Database::needRoot(false); } } return $this->response([ 'count' => count($result), 'list' => $result ]); } /** * returns the total number of accessible databases * * @param int $customerid * optional, admin-only, select dbs of a specific customer by id * @param string $loginname * optional, admin-only, select dbs of a specific customer by loginname * * @access admin, customer * @return string json-encoded response message * @throws Exception */ public function listingCount() { $customer_ids = $this->getAllowedCustomerIds('mysql'); $query_fields = []; $result_stmt = Database::prepare(" SELECT COUNT(*) as num_dbs FROM `" . TABLE_PANEL_DATABASES . "` WHERE `customerid` IN (" . implode(", ", $customer_ids) . ") " . $this->getSearchWhere($query_fields, true)); $result = Database::pexecute_first($result_stmt, $query_fields, true, true); if ($result) { return $this->response($result['num_dbs']); } return $this->response(0); } /** * delete a mysql database by either id or dbname * * @param int $id * optional, the database-id * @param string $dbname * optional, the databasename * @param int $mysql_server * optional, specify database-server, default is none * @param int $customerid * optional, required when called as admin (if $loginname is not specified) * @param string $loginname * optional, required when called as admin (if $customerid is not specified) * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function delete() { $id = $this->getParam('id', true, 0); $dn_optional = $id > 0; $dbname = $this->getParam('dbname', $dn_optional, ''); $dbserver = $this->getParam('mysql_server', true, -1); $customer = $this->getCustomerData(); if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'mysql')) { throw new Exception("You cannot access this resource", 405); } $result = $this->apiCall('Mysqls.get', [ 'id' => $id, 'dbname' => $dbname, 'mysql_server' => $dbserver ]); $id = $result['id']; // Begin root-session Database::needRoot(true, $result['dbserver'], false); $dbm = new DbManager($this->logger()); $dbm->getManager()->deleteDatabase($result['databasename'], $customer['loginname']); Database::needRoot(false); // End root-session // delete from table $stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_DATABASES . "` WHERE `id` = :id"); Database::pexecute($stmt, [ "id" => $id ], true, true); // get needed customer info to reduce the mysql-usage-counter by one $mysql_used = $customer['mysqls_used']; // reduce mysql-usage-counter $resetaccnumber = ($mysql_used == '1') ? " , `mysql_lastaccountnumber` = '0' " : ''; Customers::decreaseUsage($customer['customerid'], 'mysqls_used', $resetaccnumber); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_WARNING, "[API] deleted database '" . $result['databasename'] . "'"); return $this->response($result); } private function getDefaultMySqlServer(array $customer) { $allowed_mysqlservers = json_decode($customer['allowed_mysqlserver'] ?? '[]', true); asort($allowed_mysqlservers, SORT_NUMERIC); if (count($allowed_mysqlservers) == 1 && $allowed_mysqlservers[0] != 0) { return (int) $allowed_mysqlservers[0]; } return (int) array_shift($allowed_mysqlservers); } } ================================================ FILE: lib/Froxlor/Api/Commands/PhpSettings.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Api\Commands; use Exception; use Froxlor\Api\ApiCommand; use Froxlor\Api\ResourceEntity; use Froxlor\Cron\TaskId; use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\FroxlorLogger; use Froxlor\Settings; use Froxlor\System\Cronjob; use Froxlor\UI\Response; use Froxlor\Validate\Validate; use PDO; /** * @since 0.10.0 */ class PhpSettings extends ApiCommand implements ResourceEntity { /** * lists all php-setting entries * * @param bool $with_subdomains * optional, also include subdomains to the list domains that use the config, default 0 (false) * @param array $sql_search * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), * LIKE is used if left empty and 'value' => searchvalue * @param int $sql_limit * optional specify number of results to be returned * @param int $sql_offset * optional specify offset for resultset * @param array $sql_orderby * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more * fields * * @access admin * @return string json-encoded array count|list * @throws Exception */ public function listing() { if ($this->isAdmin()) { $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] list php-configs"); $with_subdomains = $this->getBoolParam('with_subdomains', true, false); $query_fields = []; $result_stmt = Database::prepare(" SELECT c.*, fd.description as fpmdesc FROM `" . TABLE_PANEL_PHPCONFIGS . "` c LEFT JOIN `" . TABLE_PANEL_FPMDAEMONS . "` fd ON fd.id = c.fpmsettingid" . $this->getSearchWhere($query_fields) . $this->getOrderBy() . $this->getLimit()); Database::pexecute($result_stmt, $query_fields, true, true); $phpconfigs = []; while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $query_params = [ 'id' => $row['id'] ]; $query = "SELECT * FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `phpsettingid` = :id AND `email_only` = '0' AND `phpenabled` = '1'"; if (!$with_subdomains) { $query .= " AND `parentdomainid` = '0'"; } if ((int)$this->getUserDetail('customers_see_all') == 0) { $query .= " AND `adminid` = :adminid"; $query_params['adminid'] = $this->getUserDetail('adminid'); } if ((int)Settings::Get('panel.phpconfigs_hidestdsubdomain') == 1) { $ssdids_res = Database::query(" SELECT DISTINCT `standardsubdomain` FROM `" . TABLE_PANEL_CUSTOMERS . "` WHERE `standardsubdomain` > 0 ORDER BY `standardsubdomain` ASC;"); $ssdids = []; while ($ssd = $ssdids_res->fetch(PDO::FETCH_ASSOC)) { $ssdids[] = $ssd['standardsubdomain']; } if (count($ssdids) > 0) { $query .= " AND `id` NOT IN (" . implode(', ', $ssdids) . ")"; } } $domains = []; $subdomains = []; $domainresult_stmt = Database::prepare($query); Database::pexecute($domainresult_stmt, $query_params, true, true); if (Database::num_rows() > 0) { while ($row2 = $domainresult_stmt->fetch(PDO::FETCH_ASSOC)) { if ($row2['parentdomainid'] != 0) { $subdomains[] = $row2['domain']; } else { $domains[] = $row2['domain']; } } } // check whether we use that config as froxor-vhost config if ((Settings::Get('system.mod_fcgid') == '1' && Settings::Get('system.mod_fcgid_defaultini_ownvhost') == $row['id']) || (Settings::Get('phpfpm.enabled') == '1' && Settings::Get('phpfpm.vhost_defaultini') == $row['id'])) { $domains[] = Settings::Get('system.hostname'); } // check whether this is our default config if ((Settings::Get('system.mod_fcgid') == '1' && Settings::Get('system.mod_fcgid_defaultini') == $row['id']) || (Settings::Get('phpfpm.enabled') == '1' && Settings::Get('phpfpm.defaultini') == $row['id'])) { $row['is_default'] = true; } $row['domains'] = $domains; $row['subdomains'] = $subdomains; $phpconfigs[] = $row; } return $this->response([ 'count' => count($phpconfigs), 'list' => $phpconfigs ]); } throw new Exception("Not allowed to execute given command.", 403); } /** * return a php-setting entry by id * * @param int $id * php-settings-id * * @access admin * @return string json-encoded array * @throws Exception */ public function get() { if ($this->isAdmin()) { $id = $this->getParam('id'); $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_PHPCONFIGS . "` WHERE `id` = :id "); $result = Database::pexecute_first($result_stmt, [ 'id' => $id ], true, true); if ($result) { return $this->response($result); } throw new Exception("php-config with id #" . $id . " could not be found", 404); } throw new Exception("Not allowed to execute given command.", 403); } /** * returns the total number of accessible php-setting entries * * @access admin * @return string json-encoded response message * @throws Exception */ public function listingCount() { if ($this->isAdmin()) { $query_fields = []; $result_stmt = Database::prepare(" SELECT COUNT(*) as num_phps FROM `" . TABLE_PANEL_PHPCONFIGS . "` c LEFT JOIN `" . TABLE_PANEL_FPMDAEMONS . "` fd ON fd.id = c.fpmsettingid" . $this->getSearchWhere($query_fields)); $result = Database::pexecute_first($result_stmt, $query_fields, true, true); if ($result) { return $this->response($result['num_phps']); } return $this->response(0); } throw new Exception("Not allowed to execute given command.", 403); } /** * add new php-settings entry * * @param string $description * description of the php-config * @param string $phpsettings * the actual ini-settings * @param string $binary * optional the binary to php-cgi if FCGID is used * @param string $file_extensions * optional allowed php-file-extensions if FCGID is used, default is 'php' * @param int $mod_fcgid_starter * optional number of fcgid-starters if FCGID is used, default is -1 * @param int $mod_fcgid_maxrequests * optional number of fcgid-maxrequests if FCGID is used, default is -1 * @param string $mod_fcgid_umask * optional umask if FCGID is used, default is '022' * @param int $fpmconfig * optional id of the fpm-daemon-config if FPM is used * @param bool $phpfpm_enable_slowlog * optional whether to write a slowlog or not if FPM is used, default is 0 (false) * @param string $phpfpm_reqtermtimeout * optional request terminate timeout if FPM is used, default is '60s' * @param string $phpfpm_reqslowtimeout * optional request slowlog timeout if FPM is used, default is '5s' * @param bool $pass_authorizationheader * optional whether to pass authorization header to webserver if FPM/FCGID is used, default is 0 (false) * @param bool $override_fpmconfig * optional whether to override fpm-daemon-config value for the following settings if FPM is used, * default is 0 (false) * @param string $pm * optional process-manager to use if FPM is used (allowed values are 'static', 'dynamic' and * 'ondemand'), default is fpm-daemon-value * @param int $max_children * optional number of max children if FPM is used, default is the fpm-daemon-value * @param int $start_server * optional number of servers to start if FPM is used, default is fpm-daemon-value * @param int $min_spare_servers * optional number of minimum spare servers if FPM is used, default is fpm-daemon-value * @param int $max_spare_servers * optional number of maximum spare servers if FPM is used, default is fpm-daemon-value * @param int $max_requests * optional number of maximum requests if FPM is used, default is fpm-daemon-value * @param int $idle_timeout * optional number of seconds for idle-timeout if FPM is used, default is fpm-daemon-value * @param string $limit_extensions * optional limitation of php-file-extensions if FPM is used, default is fpm-daemon-value * @param bool $allow_all_customers * optional add this configuration to the list of every existing customer's allowed-fpm-config list, * default is false (no) * * @access admin * @return string json-encoded array * @throws Exception */ public function add() { if ($this->isAdmin() && $this->getUserDetail('change_serversettings') == 1) { // required parameter $description = $this->getParam('description'); $phpsettings = $this->getParam('phpsettings'); if (Settings::Get('system.mod_fcgid') == 1) { $binary = $this->getParam('binary'); $fpm_config_id = 1; } elseif (Settings::Get('phpfpm.enabled') == 1) { $fpm_config_id = intval($this->getParam('fpmconfig')); } else { $fpm_config_id = 1; } // parameters $file_extensions = $this->getParam('file_extensions', true, 'php'); $mod_fcgid_starter = $this->getParam('mod_fcgid_starter', true, -1); $mod_fcgid_maxrequests = $this->getParam('mod_fcgid_maxrequests', true, -1); $mod_fcgid_umask = $this->getParam('mod_fcgid_umask', true, "022"); $fpm_enableslowlog = $this->getBoolParam('phpfpm_enable_slowlog', true, 0); $fpm_reqtermtimeout = $this->getParam('phpfpm_reqtermtimeout', true, "60s"); $fpm_reqslowtimeout = $this->getParam('phpfpm_reqslowtimeout', true, "5s"); $pass_authorizationheader = $this->getBoolParam('pass_authorizationheader', true, 0); $override_fpmconfig = $this->getBoolParam('override_fpmconfig', true, 0); $def_fpmconfig = $this->apiCall('FpmDaemons.get', [ 'id' => $fpm_config_id ]); $pmanager = $this->getParam('pm', true, $def_fpmconfig['pm']); $max_children = $this->getParam('max_children', true, $def_fpmconfig['max_children']); $start_servers = $this->getParam('start_servers', true, $def_fpmconfig['start_servers']); $min_spare_servers = $this->getParam('min_spare_servers', true, $def_fpmconfig['min_spare_servers']); $max_spare_servers = $this->getParam('max_spare_servers', true, $def_fpmconfig['max_spare_servers']); $max_requests = $this->getParam('max_requests', true, $def_fpmconfig['max_requests']); $idle_timeout = $this->getParam('idle_timeout', true, $def_fpmconfig['idle_timeout']); $limit_extensions = $this->getParam('limit_extensions', true, $def_fpmconfig['limit_extensions']); $allow_all_customers = $this->getBoolParam('allow_all_customers', true, 0); // validation $description = Validate::validate($description, 'description', Validate::REGEX_DESC_TEXT, '', [], true); $phpsettings = Validate::validate(str_replace("\r\n", "\n", $phpsettings), 'phpsettings', '/^[^\0]*$/', '', [], true); if (Settings::Get('system.mod_fcgid') == 1) { $binary = FileDir::makeCorrectFile(Validate::validate($binary, 'binary', '', '', [], true)); $file_extensions = Validate::validate($file_extensions, 'file_extensions', '/^[a-zA-Z0-9\s]*$/', '', [], true); $mod_fcgid_starter = Validate::validate($mod_fcgid_starter, 'mod_fcgid_starter', '/^[0-9]*$/', '', [ '-1', '' ], true); $mod_fcgid_maxrequests = Validate::validate($mod_fcgid_maxrequests, 'mod_fcgid_maxrequests', '/^[0-9]*$/', '', [ '-1', '' ], true); $mod_fcgid_umask = Validate::validate($mod_fcgid_umask, 'mod_fcgid_umask', '/^[0-9]*$/', '', [], true); // disable fpm stuff $fpm_config_id = 1; $fpm_enableslowlog = 0; $fpm_reqtermtimeout = 0; $fpm_reqslowtimeout = 0; $override_fpmconfig = 0; } elseif (Settings::Get('phpfpm.enabled') == 1) { $fpm_reqtermtimeout = Validate::validate($fpm_reqtermtimeout, 'phpfpm_reqtermtimeout', '/^([0-9]+)(|s|m|h|d)$/', '', [], true); $fpm_reqslowtimeout = Validate::validate($fpm_reqslowtimeout, 'phpfpm_reqslowtimeout', '/^([0-9]+)(|s|m|h|d)$/', '', [], true); if (!in_array($pmanager, [ 'static', 'dynamic', 'ondemand' ])) { throw new Exception("Unknown process manager", 406); } if (empty($limit_extensions)) { $limit_extensions = '.php'; } $limit_extensions = Validate::validate($limit_extensions, 'limit_extensions', '/^(\.[a-z]([a-z0-9]+)\ ?)+$/', '', [], true); // disable fcgid stuff $binary = '/usr/bin/php-cgi'; $file_extensions = 'php'; $mod_fcgid_starter = 0; $mod_fcgid_maxrequests = 0; $mod_fcgid_umask = "022"; } if (strlen($description) == 0 || strlen($description) > 50) { Response::standardError('descriptioninvalid', '', true); } $ins_stmt = Database::prepare(" INSERT INTO `" . TABLE_PANEL_PHPCONFIGS . "` SET `description` = :desc, `binary` = :binary, `file_extensions` = :fext, `mod_fcgid_starter` = :starter, `mod_fcgid_maxrequests` = :mreq, `mod_fcgid_umask` = :umask, `fpm_slowlog` = :fpmslow, `fpm_reqterm` = :fpmreqterm, `fpm_reqslow` = :fpmreqslow, `phpsettings` = :phpsettings, `fpmsettingid` = :fpmsettingid, `pass_authorizationheader` = :fpmpassauth, `override_fpmconfig` = :ofc, `pm` = :pm, `max_children` = :max_children, `start_servers` = :start_servers, `min_spare_servers` = :min_spare_servers, `max_spare_servers` = :max_spare_servers, `max_requests` = :max_requests, `idle_timeout` = :idle_timeout, `limit_extensions` = :limit_extensions "); $ins_data = [ 'desc' => $description, 'binary' => $binary, 'fext' => $file_extensions, 'starter' => $mod_fcgid_starter, 'mreq' => $mod_fcgid_maxrequests, 'umask' => $mod_fcgid_umask, 'fpmslow' => $fpm_enableslowlog, 'fpmreqterm' => $fpm_reqtermtimeout, 'fpmreqslow' => $fpm_reqslowtimeout, 'phpsettings' => $phpsettings, 'fpmsettingid' => $fpm_config_id, 'fpmpassauth' => $pass_authorizationheader, 'ofc' => $override_fpmconfig, 'pm' => $pmanager, 'max_children' => $max_children, 'start_servers' => $start_servers, 'min_spare_servers' => $min_spare_servers, 'max_spare_servers' => $max_spare_servers, 'max_requests' => $max_requests, 'idle_timeout' => $idle_timeout, 'limit_extensions' => $limit_extensions ]; Database::pexecute($ins_stmt, $ins_data, true, true); $ins_data['id'] = Database::lastInsertId(); Cronjob::inserttask(TaskId::REBUILD_VHOST); $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] php setting with description '" . $description . "' has been created by '" . $this->getUserDetail('loginname') . "'"); $result = $this->apiCall('PhpSettings.get', [ 'id' => $ins_data['id'] ]); $this->addForAllCustomers($allow_all_customers, $ins_data['id']); return $this->response($result); } throw new Exception("Not allowed to execute given command.", 403); } /** * add given php-config id to the list of allowed php-config to all currently existing customers * if allow_all_customers parameter is true in PhpSettings::add() or PhpSettings::update() * * @param bool $allow_all_customers * @param int $config_id */ private function addForAllCustomers(bool $allow_all_customers, int $config_id) { // should this config be added to the allowed list of all existing customers? if ($allow_all_customers) { $sel_stmt = Database::prepare("SELECT customerid, allowed_phpconfigs FROM `" . TABLE_PANEL_CUSTOMERS . "`"); $upd_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET allowed_phpconfigs = :ap WHERE customerid = :cid"); Database::pexecute($sel_stmt); while ($cust = $sel_stmt->fetch(PDO::FETCH_ASSOC)) { // get existing entries of customer $ap = json_decode($cust['allowed_phpconfigs'], true); // initialize array if it's empty if (empty($ap)) { $ap = []; } // add this config $ap[] = $config_id; // check for duplicates and force value-type to be int $ap = array_map('intval', array_unique($ap)); // update customer-entry Database::pexecute($upd_stmt, [ 'ap' => json_encode($ap), 'cid' => $cust['customerid'] ]); } } } /** * update a php-setting entry by given id * * @param int $id * @param string $description * description of the php-config * @param string $phpsettings * the actual ini-settings * @param string $binary * optional the binary to php-cgi if FCGID is used * @param string $file_extensions * optional allowed php-file-extensions if FCGID is used, default is 'php' * @param int $mod_fcgid_starter * optional number of fcgid-starters if FCGID is used, default is -1 * @param int $mod_fcgid_maxrequests * optional number of fcgid-maxrequests if FCGID is used, default is -1 * @param string $mod_fcgid_umask * optional umask if FCGID is used, default is '022' * @param int $fpmconfig * optional id of the fpm-daemon-config if FPM is used * @param bool $phpfpm_enable_slowlog * optional whether to write a slowlog or not if FPM is used, default is 0 (false) * @param string $phpfpm_reqtermtimeout * optional request terminate timeout if FPM is used, default is '60s' * @param string $phpfpm_reqslowtimeout * optional request slowlog timeout if FPM is used, default is '5s' * @param bool $pass_authorizationheader * optional whether to pass authorization header to webserver if FPM is used, default is 0 (false) * @param bool $override_fpmconfig * optional whether to override fpm-daemon-config value for the following settings if FPM is used, * default is 0 (false) * @param string $pm * optional process-manager to use if FPM is used (allowed values are 'static', 'dynamic' and * 'ondemand'), default is fpm-daemon-value * @param int $max_children * optional number of max children if FPM is used, default is the fpm-daemon-value * @param int $start_server * optional number of servers to start if FPM is used, default is fpm-daemon-value * @param int $min_spare_servers * optional number of minimum spare servers if FPM is used, default is fpm-daemon-value * @param int $max_spare_servers * optional number of maximum spare servers if FPM is used, default is fpm-daemon-value * @param int $max_requests * optional number of maximum requests if FPM is used, default is fpm-daemon-value * @param int $idle_timeout * optional number of seconds for idle-timeout if FPM is used, default is fpm-daemon-value * @param string $limit_extensions * optional limitation of php-file-extensions if FPM is used, default is fpm-daemon-value * @param bool $allow_all_customers * optional add this configuration to the list of every existing customer's allowed-fpm-config list, * default is false (no) * * @access admin * @return string json-encoded array * @throws Exception */ public function update() { if ($this->isAdmin() && $this->getUserDetail('change_serversettings') == 1) { // required parameter $id = $this->getParam('id'); $result = $this->apiCall('PhpSettings.get', [ 'id' => $id ]); // parameters $description = $this->getParam('description', true, $result['description']); $phpsettings = $this->getParam('phpsettings', true, $result['phpsettings']); $binary = $this->getParam('binary', true, $result['binary']); $fpm_config_id = intval($this->getParam('fpmconfig', true, $result['fpmsettingid'])); $file_extensions = $this->getParam('file_extensions', true, $result['file_extensions']); $mod_fcgid_starter = $this->getParam('mod_fcgid_starter', true, $result['mod_fcgid_starter']); $mod_fcgid_maxrequests = $this->getParam('mod_fcgid_maxrequests', true, $result['mod_fcgid_maxrequests']); $mod_fcgid_umask = $this->getParam('mod_fcgid_umask', true, $result['mod_fcgid_umask']); $fpm_enableslowlog = $this->getBoolParam('phpfpm_enable_slowlog', true, $result['fpm_slowlog']); $fpm_reqtermtimeout = $this->getParam('phpfpm_reqtermtimeout', true, $result['fpm_reqterm']); $fpm_reqslowtimeout = $this->getParam('phpfpm_reqslowtimeout', true, $result['fpm_reqslow']); $pass_authorizationheader = $this->getBoolParam('pass_authorizationheader', true, $result['pass_authorizationheader']); $override_fpmconfig = $this->getBoolParam('override_fpmconfig', true, $result['override_fpmconfig']); $pmanager = $this->getParam('pm', true, $result['pm']); $max_children = $this->getParam('max_children', true, $result['max_children']); $start_servers = $this->getParam('start_servers', true, $result['start_servers']); $min_spare_servers = $this->getParam('min_spare_servers', true, $result['min_spare_servers']); $max_spare_servers = $this->getParam('max_spare_servers', true, $result['max_spare_servers']); $max_requests = $this->getParam('max_requests', true, $result['max_requests']); $idle_timeout = $this->getParam('idle_timeout', true, $result['idle_timeout']); $limit_extensions = $this->getParam('limit_extensions', true, $result['limit_extensions']); $allow_all_customers = $this->getBoolParam('allow_all_customers', true, 0); // validation $description = Validate::validate($description, 'description', Validate::REGEX_DESC_TEXT, '', [], true); $phpsettings = Validate::validate(str_replace("\r\n", "\n", $phpsettings), 'phpsettings', '/^[^\0]*$/', '', [], true); if (Settings::Get('system.mod_fcgid') == 1) { $binary = FileDir::makeCorrectFile(Validate::validate($binary, 'binary', '', '', [], true)); $file_extensions = Validate::validate($file_extensions, 'file_extensions', '/^[a-zA-Z0-9\s]*$/', '', [], true); $mod_fcgid_starter = Validate::validate($mod_fcgid_starter, 'mod_fcgid_starter', '/^[0-9]*$/', '', [ '-1', '' ], true); $mod_fcgid_maxrequests = Validate::validate($mod_fcgid_maxrequests, 'mod_fcgid_maxrequests', '/^[0-9]*$/', '', [ '-1', '' ], true); $mod_fcgid_umask = Validate::validate($mod_fcgid_umask, 'mod_fcgid_umask', '/^[0-9]*$/', '', [], true); // disable fpm stuff $fpm_config_id = 1; $fpm_enableslowlog = 0; $fpm_reqtermtimeout = 0; $fpm_reqslowtimeout = 0; $override_fpmconfig = 0; } elseif (Settings::Get('phpfpm.enabled') == 1) { $fpm_reqtermtimeout = Validate::validate($fpm_reqtermtimeout, 'phpfpm_reqtermtimeout', '/^([0-9]+)(|s|m|h|d)$/', '', [], true); $fpm_reqslowtimeout = Validate::validate($fpm_reqslowtimeout, 'phpfpm_reqslowtimeout', '/^([0-9]+)(|s|m|h|d)$/', '', [], true); if (!in_array($pmanager, [ 'static', 'dynamic', 'ondemand' ])) { throw new Exception("Unknown process manager", 406); } if (empty($limit_extensions)) { $limit_extensions = '.php'; } $limit_extensions = Validate::validate($limit_extensions, 'limit_extensions', '/^(\.[a-z]([a-z0-9]+)\ ?)+$/', '', [], true); // disable fcgid stuff $binary = '/usr/bin/php-cgi'; $file_extensions = 'php'; $mod_fcgid_starter = 0; $mod_fcgid_maxrequests = 0; $mod_fcgid_umask = "022"; } if (strlen($description) == 0 || strlen($description) > 50) { Response::standardError('descriptioninvalid', '', true); } $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_PHPCONFIGS . "` SET `description` = :desc, `binary` = :binary, `file_extensions` = :fext, `mod_fcgid_starter` = :starter, `mod_fcgid_maxrequests` = :mreq, `mod_fcgid_umask` = :umask, `fpm_slowlog` = :fpmslow, `fpm_reqterm` = :fpmreqterm, `fpm_reqslow` = :fpmreqslow, `phpsettings` = :phpsettings, `fpmsettingid` = :fpmsettingid, `pass_authorizationheader` = :fpmpassauth, `override_fpmconfig` = :ofc, `pm` = :pm, `max_children` = :max_children, `start_servers` = :start_servers, `min_spare_servers` = :min_spare_servers, `max_spare_servers` = :max_spare_servers, `max_requests` = :max_requests, `idle_timeout` = :idle_timeout, `limit_extensions` = :limit_extensions WHERE `id` = :id "); $upd_data = [ 'desc' => $description, 'binary' => $binary, 'fext' => $file_extensions, 'starter' => $mod_fcgid_starter, 'mreq' => $mod_fcgid_maxrequests, 'umask' => $mod_fcgid_umask, 'fpmslow' => $fpm_enableslowlog, 'fpmreqterm' => $fpm_reqtermtimeout, 'fpmreqslow' => $fpm_reqslowtimeout, 'phpsettings' => $phpsettings, 'fpmsettingid' => $fpm_config_id, 'fpmpassauth' => $pass_authorizationheader, 'ofc' => $override_fpmconfig, 'pm' => $pmanager, 'max_children' => $max_children, 'start_servers' => $start_servers, 'min_spare_servers' => $min_spare_servers, 'max_spare_servers' => $max_spare_servers, 'max_requests' => $max_requests, 'idle_timeout' => $idle_timeout, 'limit_extensions' => $limit_extensions, 'id' => $id ]; Database::pexecute($upd_stmt, $upd_data, true, true); Cronjob::inserttask(TaskId::REBUILD_VHOST); $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "[API] php setting with description '" . $description . "' has been updated by '" . $this->getUserDetail('loginname') . "'"); $result = $this->apiCall('PhpSettings.get', [ 'id' => $id ]); $this->addForAllCustomers($allow_all_customers, $id); return $this->response($result); } throw new Exception("Not allowed to execute given command.", 403); } /** * delete a php-setting entry by id * * @param int $id * php-settings-id * * @access admin * @return string json-encoded array * @throws Exception */ public function delete() { if ($this->isAdmin() && $this->getUserDetail('change_serversettings') == 1) { $id = $this->getParam('id'); $result = $this->apiCall('PhpSettings.get', [ 'id' => $id ]); if ((Settings::Get('system.mod_fcgid') == '1' && Settings::Get('system.mod_fcgid_defaultini_ownvhost') == $id) || (Settings::Get('phpfpm.enabled') == '1' && Settings::Get('phpfpm.vhost_defaultini') == $id)) { Response::standardError('cannotdeletehostnamephpconfig', '', true); } if ((Settings::Get('system.mod_fcgid') == '1' && Settings::Get('system.mod_fcgid_defaultini') == $id) || (Settings::Get('phpfpm.enabled') == '1' && Settings::Get('phpfpm.defaultini') == $id)) { Response::standardError('cannotdeletedefaultphpconfig', '', true); } // set php-config to default for all domains using the // config that is to be deleted $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_DOMAINS . "` SET `phpsettingid` = '1' WHERE `phpsettingid` = :id "); Database::pexecute($upd_stmt, [ 'id' => $id ], true, true); $del_stmt = Database::prepare(" DELETE FROM `" . TABLE_PANEL_PHPCONFIGS . "` WHERE `id` = :id "); Database::pexecute($del_stmt, [ 'id' => $id ], true, true); Cronjob::inserttask(TaskId::REBUILD_VHOST); $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] php setting '" . $result['description'] . "' has been deleted by '" . $this->getUserDetail('loginname') . "'"); return $this->response($result); } throw new Exception("Not allowed to execute given command.", 403); } } ================================================ FILE: lib/Froxlor/Api/Commands/SshKeys.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Api\Commands; use Exception; use Froxlor\Api\ApiCommand; use Froxlor\Api\ResourceEntity; use Froxlor\Cron\TaskId; use Froxlor\Database\Database; use Froxlor\FroxlorLogger; use Froxlor\Settings; use Froxlor\System\Cronjob; use Froxlor\Validate\Validate; use phpseclib3\Crypt\PublicKeyLoader; /** * @since 2.3.0 */ class SshKeys extends ApiCommand implements ResourceEntity { /** * add a new ssh-key * * @param int $id * optional id of ftp-user to add the ssh-key for, required if `ftpuser` is empty * @param string $ftpuser * optional loginname of ftp-user to add the ssh-key for, required if `id` is empty * @param int $customerid * optional, required when called as admin (if $loginname is not specified) * @param string $loginname * optional, required when called as admin (if $customerid is not specified) * @param string $ssh_pubkey * ssh public key to add for the given user * @param string $description * optional, description for ssh-key * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function add() { if ($this->isAdmin() == false && (Settings::IsInList('panel.customer_hide_options', 'ftp') || intval(Settings::Get('system.allow_customer_shell')) == 0 || intval($this->getUserDetail('shell_allowed')) == 0) ) { throw new Exception("You cannot access this resource", 405); } // get needed customer info $customer = $this->getCustomerData(); $id = $this->getParam('id', true, 0); $ea_optional = $id > 0; $ftpuser = $this->getParam('ftpuser', $ea_optional); // get ftp user $ftp_user = $this->apiCall('Ftps.get', [ 'id' => $id, 'username' => $ftpuser ]); $id = $ftp_user['id']; // parameters $ssh_pubkey = $this->getParam('ssh_pubkey'); $description = $this->getParam('description', true, ''); // validation if ($customer['customerid'] != $ftp_user['customerid']) { throw new Exception("ftp user not found", 404); } if (!$this->isValidSshPublicKey($ssh_pubkey)) { throw new Exception("Given SSH-key does not seem to be a valid public key", 406); } $key = PublicKeyLoader::loadPublicKey(trim($ssh_pubkey)); if (empty($description)) { $description = $key->getComment() ?? ''; } $description = Validate::validate(trim($description), 'description', Validate::REGEX_DESC_TEXT, '', [], true); // check for existing ssh-key for given user $check_stmt = Database::prepare(" SELECT `ssh_pubkey` FROM `" . TABLE_PANEL_USER_SSHKEYS . "` WHERE `ftp_user_id` = :fuid "); Database::pexecute($check_stmt, ['fuid' => $id]); while ($row = $check_stmt->fetch(\PDO::FETCH_ASSOC)) { $rowkey = PublicKeyLoader::loadPublicKey($row['ssh_pubkey']); if ($rowkey->getFingerprint('sha256') == $key->getFingerprint('sha256')) { throw new Exception("This SSH-key already exists for the given user", 406); } } // insert data $stmt = Database::prepare(" INSERT INTO `" . TABLE_PANEL_USER_SSHKEYS . "` SET `customerid` = :cid, `ftp_user_id` = :fid, `ssh_pubkey` = :sshpub, `description` = :desc "); $params = [ "cid" => $customer['customerid'], "fid" => $id, "sshpub" => trim($ssh_pubkey), "desc" => $description ]; Database::pexecute($stmt, $params, true, true); $sshkeyid = Database::lastInsertId(); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] added ssh key for '" . $ftp_user['username'] . "'"); $result = $this->apiCall('SshKeys.get', [ 'id' => $sshkeyid ]); if (Settings::Get('system.nssextrausers') == 1) { // this is used so that the libnss-extrausers cron is fired Cronjob::inserttask(TaskId::REBUILD_NSSUSERS); } return $this->response($result); } /** * return a ssh-key entry by id * * @param int $id * the ssh-key id * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function get() { $id = $this->getParam('id'); $params = []; if ($this->isAdmin()) { if ($this->getUserDetail('customers_see_all') == false) { // if it's a reseller or an admin who cannot see all customers, we need to check // whether the database belongs to one of his customers $_custom_list_result = $this->apiCall('Customers.listing'); $custom_list_result = $_custom_list_result['list']; $customer_ids = []; foreach ($custom_list_result as $customer) { $customer_ids[] = $customer['customerid']; } $result_stmt = Database::prepare(" SELECT s.*, f.username FROM `" . TABLE_PANEL_USER_SSHKEYS . "` s LEFT JOIN `" . TABLE_FTP_USERS . "` f ON f.id = s.ftp_user_id WHERE s.`customerid` IN (" . implode(", ", $customer_ids) . ") AND s.`id` = :id "); } else { $result_stmt = Database::prepare(" SELECT s.*, f.username FROM `" . TABLE_PANEL_USER_SSHKEYS . "` s LEFT JOIN `" . TABLE_FTP_USERS . "` f ON f.id = s.ftp_user_id WHERE s.`id` = :id "); } } else { if (Settings::IsInList('panel.customer_hide_options', 'ftp') || intval(Settings::Get('system.allow_customer_shell')) == 0 || intval($this->getUserDetail('shell_allowed')) == 0) { throw new Exception("You cannot access this resource", 405); } $result_stmt = Database::prepare(" SELECT s.*, f.username FROM `" . TABLE_PANEL_USER_SSHKEYS . "` s LEFT JOIN `" . TABLE_FTP_USERS . "` f ON f.id = s.ftp_user_id WHERE s.`customerid` = :customerid AND s.`id` = :id "); $params['customerid'] = $this->getUserDetail('customerid'); } $params['id'] = $id; $result = Database::pexecute_first($result_stmt, $params, true, true); if ($result) { $key = PublicKeyLoader::loadPublicKey($result['ssh_pubkey']); $result['fingerprint'] = 'SHA256:' . $key->getFingerprint('sha256'); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] get ssh-key for ftp-user '" . $result['ftp_user_id'] . "'"); return $this->response($result); } $key = "id #" . $id; throw new Exception("FTP user with " . $key . " could not be found", 404); } /** * update a given ftp-user by id or username * * @param int $id * the ssh-key id * @param string $description * optional, description for ssh-key * @param int $customerid * optional, required when called as admin (if $loginname is not specified) * @param string $loginname * optional, required when called as admin (if $customerid is not specified) * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function update() { if ($this->isAdmin() == false && (Settings::IsInList('panel.customer_hide_options', 'ftp') || intval(Settings::Get('system.allow_customer_shell')) == 0 || intval($this->getUserDetail('shell_allowed')) == 0) ) { throw new Exception("You cannot access this resource", 405); } $id = $this->getParam('id'); $result = $this->apiCall('SshKeys.get', [ 'id' => $id ]); $id = $result['id']; // parameters $description = $this->getParam('description', true, $result['description']); // validation $description = Validate::validate(trim($description), 'description', Validate::REGEX_DESC_TEXT, '', [], true); // get needed customer info $customer = $this->getCustomerData(); $stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_USER_SSHKEYS . "` SET `description` = :desc WHERE `customerid` = :customerid AND `id` = :id "); Database::pexecute($stmt, [ "desc" => $description, "customerid" => $customer['customerid'], "id" => $id ], true, true); $result = $this->apiCall('SshKeys.get', [ 'id' => $result['id'] ]); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] updated ssh-key '" . $result['id'] . "'"); if (Settings::Get('system.nssextrausers') == 1) { // this is used so that the libnss-extrausers cron is fired Cronjob::inserttask(TaskId::REBUILD_NSSUSERS); } return $this->response($result); } /** * list all ssh-keys, if called from an admin, list all ssh-keys of all customers you are allowed to view, or * specify id or loginname for one specific customer * * @param int $customerid * optional, admin-only, select ftp-users of a specific customer by id * @param string $loginname * optional, admin-only, select ftp-users of a specific customer by loginname * @param array $sql_search * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), * LIKE is used if left empty and 'value' => searchvalue * @param int $sql_limit * optional specify number of results to be returned * @param int $sql_offset * optional specify offset for resultset * @param array $sql_orderby * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more * fields * * @access admin, customer * @return string json-encoded array count|list * @throws Exception */ public function listing() { if ($this->isAdmin() == false && (Settings::IsInList('panel.customer_hide_options', 'ftp') || intval(Settings::Get('system.allow_customer_shell')) == 0 || intval($this->getUserDetail('shell_allowed')) == 0) ) { throw new Exception("You cannot access this resource", 405); } $customer_ids = $this->getAllowedCustomerIds('ftp'); $result = []; $query_fields = []; $result_stmt = Database::prepare(" SELECT s.*, f.username FROM `" . TABLE_PANEL_USER_SSHKEYS . "` s LEFT JOIN `" . TABLE_FTP_USERS . "` f ON f.id = s.ftp_user_id WHERE s.`customerid` IN (" . implode(", ", $customer_ids) . ")" . $this->getSearchWhere($query_fields, true) . $this->getOrderBy() . $this->getLimit()); Database::pexecute($result_stmt, $query_fields, true, true); while ($row = $result_stmt->fetch(\PDO::FETCH_ASSOC)) { $key = PublicKeyLoader::loadPublicKey($row['ssh_pubkey']); $row['fingerprint'] = 'SHA256:' . $key->getFingerprint('sha256'); $result[] = $row; } $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] list ssh-keys"); return $this->response([ 'count' => count($result), 'list' => $result ]); } /** * returns the total number of accessible ssh keys * * @param int $customerid * optional, admin-only, select ftp-users of a specific customer by id * @param string $loginname * optional, admin-only, select ftp-users of a specific customer by loginname * * @access admin, customer * @return string json-encoded response message * @throws Exception */ public function listingCount() { if ($this->isAdmin() == false && (Settings::IsInList('panel.customer_hide_options', 'ftp') || intval(Settings::Get('system.allow_customer_shell')) == 0 || intval($this->getUserDetail('shell_allowed')) == 0) ) { throw new Exception("You cannot access this resource", 405); } $customer_ids = $this->getAllowedCustomerIds('ftp'); $result = []; $query_fields = []; $result_stmt = Database::prepare(" SELECT COUNT(*) as num_sshkeys FROM `" . TABLE_PANEL_USER_SSHKEYS . "` WHERE `customerid` IN (" . implode(", ", $customer_ids) . ") " . $this->getSearchWhere($query_fields, true)); $result = Database::pexecute_first($result_stmt, $query_fields, true, true); if ($result) { return $this->response($result['num_sshkeys']); } return $this->response(0); } /** * delete a ftp-user by either id or username * * @param int $id * the ssh-key id * @param int $customerid * optional, required when called as admin (if $loginname is not specified) * @param string $loginname * optional, required when called as admin (if $customerid is not specified) * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function delete() { $id = $this->getParam('id'); if ($this->isAdmin() == false && (Settings::IsInList('panel.customer_hide_options', 'ftp') || intval(Settings::Get('system.allow_customer_shell')) == 0 || intval($this->getUserDetail('shell_allowed')) == 0) ) { throw new Exception("You cannot access this resource", 405); } // get ssh-key $result = $this->apiCall('SshKeys.get', [ 'id' => $id ]); $id = $result['id']; $customer = $this->getCustomerData(); // remove entry $stmt = Database::prepare(" DELETE FROM `" . TABLE_PANEL_USER_SSHKEYS . "` WHERE `customerid` = :customerid AND `id` = :id "); Database::pexecute($stmt, [ "customerid" => $customer['customerid'], "id" => $id ], true, true); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_WARNING, "[API] deleted ssh-key '" . $result['id'] . "'"); if (Settings::Get('system.nssextrausers') == 1) { // this is used so that the libnss-extrausers cron is fired Cronjob::inserttask(TaskId::REBUILD_NSSUSERS); } return $this->response($result); } private function isValidSshPublicKey(string $key): bool { try { $loaded = PublicKeyLoader::loadPublicKey($key); return $loaded !== null; } catch (\Exception $e) { return false; } } } ================================================ FILE: lib/Froxlor/Api/Commands/SubDomains.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Api\Commands; use Exception; use Froxlor\Api\ApiCommand; use Froxlor\Api\ResourceEntity; use Froxlor\Cron\TaskId; use Froxlor\Database\Database; use Froxlor\Domain\Domain; use Froxlor\FileDir; use Froxlor\FroxlorLogger; use Froxlor\Idna\IdnaWrapper; use Froxlor\PhpHelper; use Froxlor\Settings; use Froxlor\System\Cronjob; use Froxlor\UI\Response; use Froxlor\Validate\Validate; use PDO; /** * @since 0.10.0 */ class SubDomains extends ApiCommand implements ResourceEntity { /** * add a new subdomain * * @param string $subdomain * part before domain.tld to create as subdomain * @param string $domain * domainname of main-domain * @param int $alias * optional, domain-id of a domain that the new domain should be an alias of * @param string $path * optional, destination path relative to the customers-homedir, default is customers-homedir * @param string $url * optional, overwrites path value with an URL to generate a redirect, alternatively use the path * parameter also for URLs * @param int $openbasedir_path * optional, either 0 for domains-docroot [default], 1 for customers-homedir or 2 for parent-directory of domains-docroot * @param int $phpsettingid * optional, php-settings-id, if empty the $domain value is used * @param int $redirectcode * optional, redirect-code-id from TABLE_PANEL_REDIRECTCODES * @param int $speciallogfile * optional, whether to create an exclusive web-logfile for this domain (1) or not (0) or inherit value from parentdomain (2, default) * @param bool $sslenabled * optional, whether or not SSL is enabled for this domain, regardless of the assigned ssl-ips, default * 1 (true) * @param bool $ssl_redirect * optional, whether to generate a https-redirect or not, default false; requires SSL to be enabled * @param bool $letsencrypt * optional, whether to generate a Let's Encrypt certificate for this domain, default false; requires * SSL to be enabled * @param bool $http2 * optional, whether to enable http/2 for this subdomain (requires to be enabled in the settings), * default 0 (false) * @param bool $http3 * optional, whether to enable http/3 for this subdomain (requires to be enabled in the settings), * default 0 (false) * @param int $hsts_maxage * optional max-age value for HSTS header, default 0 * @param bool $hsts_sub * optional whether or not to add subdomains to the HSTS header, default 0 * @param bool $hsts_preload * optional whether or not to preload HSTS header value, default 0 * @param int $customerid * optional, required when called as admin (if $loginname is not specified) * @param string $loginname * optional, required when called as admin (if $customerid is not specified) * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function add() { if (($this->getUserDetail('subdomains_used') < $this->getUserDetail('subdomains') || $this->getUserDetail('subdomains') == '-1') || $this->isAdmin()) { // parameters $subdomain = $this->getParam('subdomain'); $domain = $this->getParam('domain'); // optional parameters $aliasdomain = $this->getParam('alias', true, 0); $path = $this->getParam('path', true, ''); $url = $this->getParam('url', true, ''); $openbasedir_path = $this->getParam('openbasedir_path', true, 0); $phpsettingid = $this->getParam('phpsettingid', true, 0); $redirectcode = $this->getParam('redirectcode', true, Settings::Get('customredirect.default')); $speciallogfile = intval($this->getParam('speciallogfile', true, 2)); $isemaildomain = $this->getParam('isemaildomain', true, 0); if (Settings::Get('system.use_ssl')) { $sslenabled = $this->getBoolParam('sslenabled', true, 1); $ssl_redirect = $this->getBoolParam('ssl_redirect', true, 0); $letsencrypt = $this->getBoolParam('letsencrypt', true, 0); $http2 = $this->getBoolParam('http2', true, 0); $http3 = $this->getBoolParam('http3', true, 0); $hsts_maxage = $this->getParam('hsts_maxage', true, 0); $hsts_sub = $this->getBoolParam('hsts_sub', true, 0); $hsts_preload = $this->getBoolParam('hsts_preload', true, 0); } else { $sslenabled = 0; $ssl_redirect = 0; $letsencrypt = 0; $http2 = 0; $http3 = 0; $hsts_maxage = 0; $hsts_sub = 0; $hsts_preload = 0; } // get needed customer info to reduce the subdomain-usage-counter by one $customer = $this->getCustomerData('subdomains'); // validation $subdomain = strtolower($subdomain); if (substr($subdomain, 0, 4) == 'xn--') { Response::standardError('domain_nopunycode', '', true); } $idna_convert = new IdnaWrapper(); $subdomain = $idna_convert->encode(preg_replace([ '/\:(\d)+$/', '/^https?\:\/\//' ], '', Validate::validate($subdomain, 'subdomain', '', 'subdomainiswrong', [], true))); // merge the two parts together $completedomain = $subdomain . '.' . $domain; if (Settings::Get('system.validate_domain') && !Validate::validateDomain($completedomain)) { Response::standardError([ 'stringiswrong', 'mydomain' ], '', true); } if ($completedomain == strtolower(Settings::Get('system.hostname'))) { Response::standardError('admin_domain_emailsystemhostname', '', true); } // check whether the domain already exists $completedomain_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `domain` = :domain AND `customerid` = :customerid AND `email_only` = '0' "); $completedomain_check = Database::pexecute_first($completedomain_stmt, [ "domain" => $completedomain, "customerid" => $customer['customerid'] ], true, true); if ($completedomain_check) { // no exception so far - domain exists Response::standardError('domainexistalready', $completedomain, true); } // alias domain checked? if ($aliasdomain != 0) { // also check ip/port combination to be the same, #176 $aliasdomain_stmt = Database::prepare(" SELECT `d`.`id` FROM `" . TABLE_PANEL_DOMAINS . "` `d` , `" . TABLE_PANEL_CUSTOMERS . "` `c` , `" . TABLE_DOMAINTOIP . "` `dip` WHERE `d`.`aliasdomain` IS NULL AND `d`.`id` = :id AND `c`.`standardsubdomain` <> `d`.`id` AND `d`.`customerid` = :customerid AND `c`.`customerid` = `d`.`customerid` AND `d`.`id` = `dip`.`id_domain` AND `dip`.`id_ipandports` IN (SELECT `id_ipandports` FROM `" . TABLE_DOMAINTOIP . "` WHERE `id_domain` = :id ) GROUP BY `d`.`domain` ORDER BY `d`.`domain` ASC "); $aliasdomain_check = Database::pexecute_first($aliasdomain_stmt, [ "id" => $aliasdomain, "customerid" => $customer['customerid'] ], true, true); if ($aliasdomain_check['id'] != $aliasdomain) { Response::standardError('domainisaliasorothercustomer', '', true); } Domain::triggerLetsEncryptCSRForAliasDestinationDomain($aliasdomain, $this->logger()); } // validate / correct path/url of domain $_doredirect = false; $path = $this->validateDomainDocumentRoot($path, $url, $customer, $completedomain, $_doredirect); if ($openbasedir_path > 2 && $openbasedir_path < 0) { $openbasedir_path = 0; } // get main domain for various checks $domain_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `domain` = :domain AND `customerid` = :customerid AND `parentdomainid` = '0' AND `email_only` = '0' "); $domain_check = Database::pexecute_first($domain_stmt, [ "domain" => $domain, "customerid" => $customer['customerid'] ], true, true); if (!$domain_check) { // the given main-domain Response::standardError('maindomainnonexist', $domain, true); } elseif ($subdomain == 'www' && $domain_check['wwwserveralias'] == '1') { // you cannot add 'www' as subdomain when the maindomain generates a www-alias Response::standardError('wwwnotallowed', '', true); } elseif ($completedomain_check && strtolower($completedomain_check['domain']) == strtolower($completedomain)) { // the domain does already exist as main-domain Response::standardError('domainexistalready', $completedomain, true); } elseif ((int)$domain_check['deactivated'] == 1) { // main domain is deactivated Response::standardError('maindomaindeactivated', $domain, true); } // if allowed, check for 'is email domain'-flag if ($domain_check['subcanemaildomain'] == '1' || $domain_check['subcanemaildomain'] == '2') { $isemaildomain = intval($isemaildomain); } else { $isemaildomain = $domain_check['subcanemaildomain'] == '3' ? 1 : 0; } if ($ssl_redirect != 0) { // a ssl-redirect only works if there actually is a // ssl ip/port assigned to the domain if (Domain::domainHasSslIpPort($domain_check['id']) == true) { $ssl_redirect = '1'; $_doredirect = true; } else { Response::standardError('sslredirectonlypossiblewithsslipport', '', true); } } if ($letsencrypt != 0) { // let's encrypt only works if there actually is a // ssl ip/port assigned to the domain if (Domain::domainHasSslIpPort($domain_check['id']) == true) { $letsencrypt = '1'; } else { Response::standardError('letsencryptonlypossiblewithsslipport', '', true); } } // validate dns if lets encrypt is enabled to check whether we can use it at all if ($letsencrypt == '1' && Settings::Get('system.le_domain_dnscheck') == '1') { $our_ips = Domain::getIpsOfDomain($domain_check['id']); $domain_ips = PhpHelper::gethostbynamel6($completedomain, true, Settings::Get('system.le_domain_dnscheck_resolver')); if ($domain_ips == false || count(array_intersect($our_ips, $domain_ips)) <= 0) { Response::standardError('invaliddnsforletsencrypt', '', true); } } // Temporarily deactivate ssl_redirect until Let's Encrypt certificate was generated if ($ssl_redirect > 0 && $letsencrypt == 1) { $ssl_redirect = 2; } // validate speciallogfile value if ($speciallogfile < 0 || $speciallogfile > 2) { $speciallogfile = 2; // inherit from parent-domain } // get the phpsettingid from parentdomain, #107 $phpsid_stmt = Database::prepare(" SELECT `phpsettingid` FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `id` = :id "); $phpsid_result = Database::pexecute_first($phpsid_stmt, [ "id" => $domain_check['id'] ], true, true); if (!isset($phpsid_result['phpsettingid']) || (int)$phpsid_result['phpsettingid'] <= 0) { // assign default config $phpsid_result['phpsettingid'] = 1; } if ($domain_check['phpenabled'] == 1) { // check whether the customer has chosen its own php-config if ($phpsettingid > 0 && $phpsettingid != $phpsid_result['phpsettingid']) { $phpsid_result['phpsettingid'] = intval($phpsettingid); } $allowed_phpconfigs = $customer['allowed_phpconfigs']; if (!empty($allowed_phpconfigs)) { $allowed_phpconfigs = json_decode($allowed_phpconfigs, true); } else { $allowed_phpconfigs = []; } // only with fcgid/fpm enabled will it be possible to select a php-setting if ((int)Settings::Get('system.mod_fcgid') == 1 || (int)Settings::Get('phpfpm.enabled') == 1) { if (!in_array($phpsid_result['phpsettingid'], $allowed_phpconfigs)) { Response::standardError('notallowedphpconfigused', '', true); } } } // actually insert domain $stmt = Database::prepare(" INSERT INTO `" . TABLE_PANEL_DOMAINS . "` SET `customerid` = :customerid, `adminid` = :adminid, `domain` = :domain, `domain_ace` = :domain_ace, `documentroot` = :documentroot, `aliasdomain` = :aliasdomain, `parentdomainid` = :parentdomainid, `wwwserveralias` = :wwwserveralias, `isemaildomain` = :isemaildomain, `iswildcarddomain` = :iswildcarddomain, `phpenabled` = :phpenabled, `openbasedir` = :openbasedir, `openbasedir_path` = :openbasedir_path, `speciallogfile` = :speciallogfile, `specialsettings` = :specialsettings, `ssl_specialsettings` = :ssl_specialsettings, `include_specialsettings` = :include_specialsettings, `ssl_redirect` = :ssl_redirect, `phpsettingid` = :phpsettingid, `letsencrypt` = :letsencrypt, `http2` = :http2, `http3` = :http3, `hsts` = :hsts, `hsts_sub` = :hsts_sub, `hsts_preload` = :hsts_preload, `ocsp_stapling` = :ocsp_stapling, `override_tls` = :override_tls, `ssl_protocols` = :ssl_protocols, `ssl_cipher_list` = :ssl_cipher_list, `tlsv13_cipher_list` = :tlsv13_cipher_list, `ssl_enabled` = :sslenabled, `dkim` = :dkim "); $params = [ "customerid" => $customer['customerid'], "adminid" => $customer['adminid'], "domain" => $completedomain, "domain_ace" => $idna_convert->decode($completedomain), "documentroot" => $path, "aliasdomain" => $aliasdomain != 0 ? $aliasdomain : null, "parentdomainid" => $domain_check['id'], "wwwserveralias" => $domain_check['wwwserveralias'] == '1' ? '1' : '0', "iswildcarddomain" => $domain_check['iswildcarddomain'] == '1' ? '1' : '0', "isemaildomain" => $isemaildomain, "openbasedir" => $domain_check['openbasedir'], "openbasedir_path" => $openbasedir_path, "phpenabled" => $domain_check['phpenabled'], "speciallogfile" => $speciallogfile == 2 ? $domain_check['speciallogfile'] : $speciallogfile, "specialsettings" => $domain_check['specialsettings'], "ssl_specialsettings" => $domain_check['ssl_specialsettings'], "include_specialsettings" => $domain_check['include_specialsettings'], "ssl_redirect" => $ssl_redirect, "phpsettingid" => $phpsid_result['phpsettingid'], "letsencrypt" => $letsencrypt, "http2" => $http2, "http3" => $http3, "hsts" => $hsts_maxage, "hsts_sub" => $hsts_sub, "hsts_preload" => $hsts_preload, "ocsp_stapling" => $domain_check['ocsp_stapling'], "override_tls" => $domain_check['override_tls'], "ssl_protocols" => $domain_check['ssl_protocols'], "ssl_cipher_list" => $domain_check['ssl_cipher_list'], "tlsv13_cipher_list" => $domain_check['tlsv13_cipher_list'], "sslenabled" => $sslenabled, "dkim" => $domain_check['dkim'], ]; Database::pexecute($stmt, $params, true, true); $subdomain_id = Database::lastInsertId(); $stmt = Database::prepare(" INSERT INTO `" . TABLE_DOMAINTOIP . "` (`id_domain`, `id_ipandports`) SELECT LAST_INSERT_ID(), `id_ipandports` FROM `" . TABLE_DOMAINTOIP . "` WHERE `id_domain` = :id_domain "); Database::pexecute($stmt, [ "id_domain" => $domain_check['id'] ]); if ($_doredirect) { Domain::addRedirectToDomain($subdomain_id, $redirectcode); } Cronjob::inserttask(TaskId::REBUILD_VHOST); // Using nameserver, insert a task which rebuilds the server config Cronjob::inserttask(TaskId::REBUILD_DNS); Customers::increaseUsage($customer['customerid'], 'subdomains_used'); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] added subdomain '" . $completedomain . "'"); $result = $this->apiCall('SubDomains.get', [ 'id' => $subdomain_id ]); return $this->response($result); } throw new Exception("No more resources available", 406); } /** * return a subdomain entry by either id or domainname * * @param int $id * optional, the domain-id * @param string $domainname * optional, the domainname * @param bool $with_ips * optional, default true * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function get() { $id = $this->getParam('id', true, 0); $dn_optional = $id > 0; $domainname = $this->getParam('domainname', $dn_optional, ''); $with_ips = $this->getParam('with_ips', true, true); // convert possible idn domain to punycode if (substr($domainname, 0, 4) != 'xn--') { $idna_convert = new IdnaWrapper(); $domainname = $idna_convert->encode($domainname); } if ($this->isAdmin()) { if ($this->getUserDetail('customers_see_all') != 1) { // if it's a reseller or an admin who cannot see all customers, we need to check // whether the database belongs to one of his customers $_custom_list_result = $this->apiCall('Customers.listing'); $custom_list_result = $_custom_list_result['list']; $customer_ids = []; foreach ($custom_list_result as $customer) { $customer_ids[] = $customer['customerid']; } if (count($customer_ids) > 0) { $result_stmt = Database::prepare(" SELECT d.*, pd.`subcanemaildomain`, pd.`isbinddomain` as subisbinddomain FROM `" . TABLE_PANEL_DOMAINS . "` d, `" . TABLE_PANEL_DOMAINS . "` pd WHERE " . ($id > 0 ? "d.`id` = :iddn" : "d.`domain` = :iddn") . " AND d.`customerid` IN (" . implode(", ", $customer_ids) . ") AND ((d.`parentdomainid`!='0' AND pd.`id` = d.`parentdomainid`) OR (d.`parentdomainid`='0' AND pd.`id` = d.`id`)) "); $params = [ 'iddn' => ($id <= 0 ? $domainname : $id) ]; } else { throw new Exception("You do not have any customers yet", 406); } } else { $result_stmt = Database::prepare(" SELECT d.*, pd.`subcanemaildomain`, pd.`isbinddomain` as subisbinddomain FROM `" . TABLE_PANEL_DOMAINS . "` d, `" . TABLE_PANEL_DOMAINS . "` pd WHERE " . ($id > 0 ? "d.`id` = :iddn" : "d.`domain` = :iddn") . " AND ((d.`parentdomainid`!='0' AND pd.`id` = d.`parentdomainid`) OR (d.`parentdomainid`='0' AND pd.`id` = d.`id`)) "); $params = [ 'iddn' => ($id <= 0 ? $domainname : $id) ]; } } else { if (!$this->isInternal() && Settings::IsInList('panel.customer_hide_options', 'domains')) { throw new Exception("You cannot access this resource", 405); } $result_stmt = Database::prepare(" SELECT d.*, pd.`subcanemaildomain`, pd.`isbinddomain` as subisbinddomain, pd.`domain` as parentdomain FROM `" . TABLE_PANEL_DOMAINS . "` d, `" . TABLE_PANEL_DOMAINS . "` pd WHERE d.`customerid`= :customerid AND " . ($id > 0 ? "d.`id` = :iddn" : "d.`domain` = :iddn") . " AND ((d.`parentdomainid`!='0' AND pd.`id` = d.`parentdomainid`) OR (d.`parentdomainid`='0' AND pd.`id` = d.`id`)) "); $params = [ 'customerid' => $this->getUserDetail('customerid'), 'iddn' => ($id <= 0 ? $domainname : $id) ]; } $result = Database::pexecute_first($result_stmt, $params, true, true); if ($result) { $result['ipsandports'] = []; if ($with_ips) { $result['ipsandports'] = $this->getIpsForDomain($result['id']); } $result['domain_hascert'] = $this->getHasCertValueForDomain((int)$result['id'], (int)$result['parentdomainid']); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] get subdomain '" . $result['domain'] . "'"); return $this->response($result); } throw new Exception("Requested subdomain could not be found", 404); } private function getHasCertValueForDomain(int $domainid, int $parentdomainid): int { // nothing (ssl_global) $domain_hascert = 0; $ssl_stmt = Database::prepare("SELECT * FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` WHERE `domainid` = :domainid"); Database::pexecute($ssl_stmt, array( "domainid" => $domainid )); $ssl_result = $ssl_stmt->fetch(PDO::FETCH_ASSOC); if (is_array($ssl_result) && isset($ssl_result['ssl_cert_file']) && $ssl_result['ssl_cert_file'] != '') { // own certificate (ssl_customer_green) $domain_hascert = 1; } else { // check if it's parent has one set (shared) if ($parentdomainid != 0) { $ssl_stmt = Database::prepare("SELECT * FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` WHERE `domainid` = :domainid"); Database::pexecute($ssl_stmt, array( "domainid" => $parentdomainid )); $ssl_result = $ssl_stmt->fetch(PDO::FETCH_ASSOC); if (is_array($ssl_result) && isset($ssl_result['ssl_cert_file']) && $ssl_result['ssl_cert_file'] != '') { // parent has a certificate (ssl_shared) $domain_hascert = 2; } } } return $domain_hascert; } /** * validate given path and replace with url if given and valid * * @param string $path * @param string $url * @param array $customer * @param string $completedomain * @param boolean $_doredirect * * @return string validated path * @throws Exception */ private function validateDomainDocumentRoot($path = null, $url = null, $customer = null, $completedomain = null, &$_doredirect = false) { $_doredirect = false; $idna = new IdnaWrapper(); // url mode: either $url or $path begins with http:// or https:// $maybeUrl = !empty($url) ? $url : (preg_match('/^https?\:\/\//', $path) ? $path : ''); if ($maybeUrl !== '') { $encoded = $idna->encode($maybeUrl); if (!Validate::validateUrl($encoded, true)) { Response::standardError('invaliddocumentrooturl', '', true); } $_doredirect = true; return $encoded; } // path mode: regular directory path $path = Validate::validate($path, 'path', Validate::REGEX_DIR, '', [], true); // default path if empty and setting active if (($path === '' || $path === '/') && Settings::Get('system.documentroot_use_default_value') == 1) { return FileDir::makeCorrectDir($customer['documentroot'] . '/' . $completedomain, $customer['documentroot']); } // check if path does not contain a colon if (strpos($path, ':') !== false) { Response::standardError('pathmaynotcontaincolon', '', true); } return FileDir::makeCorrectDir($customer['documentroot'] . '/' . $path, $customer['documentroot']); } /** * update subdomain entry by either id or domainname * * @param int $id * optional, the domain-id * @param string $domainname * optional, the domainname * @param int $alias * optional, domain-id of a domain that the new domain should be an alias of * @param string $path * optional, destination path relative to the customers-homedir, default is customers-homedir * @param string $url * optional, overwrites path value with an URL to generate a redirect, alternatively use the path * parameter also for URLs * @param int $selectserveralias * optional, 0 = wildcard, 1 = www-alias, 2 = none * @param bool $isemaildomain * optional * @param int $openbasedir_path * optional, either 0 for domains-docroot, 1 for customers-homedir or 2 for parent-directory of domains-docroot * @param int $phpsettingid * optional, php-settings-id, if empty the $domain value is used * @param int $redirectcode * optional, redirect-code-id from TABLE_PANEL_REDIRECTCODES * @param bool $speciallogfile * optional, whether to create an exclusive web-logfile for this domain * @param bool $speciallogverified * optional, when setting $speciallogfile to false, this needs to be set to true to confirm the action, * default 0 (false) * @param bool $sslenabled * optional, whether or not SSL is enabled for this domain, regardless of the assigned ssl-ips, default * 1 (true) * @param bool $ssl_redirect * optional, whether to generate a https-redirect or not, default false; requires SSL to be enabled * @param bool $letsencrypt * optional, whether to generate a Let's Encrypt certificate for this domain, default false; requires * SSL to be enabled * @param bool $http2 * optional, whether to enable http/2 for this domain (requires to be enabled in the settings), default * 0 (false) * @param bool $http3 * optional, whether to enable http/3 for this domain (requires to be enabled in the settings), default * 0 (false) * @param int $hsts_maxage * optional max-age value for HSTS header * @param bool $hsts_sub * optional whether or not to add subdomains to the HSTS header * @param bool $hsts_preload * optional whether or not to preload HSTS header value * @param int $customerid * optional, required when called as admin (if $loginname is not specified) * @param string $loginname * optional, required when called as admin (if $customerid is not specified) * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function update() { $id = $this->getParam('id', true, 0); $dn_optional = $id > 0; $domainname = $this->getParam('domainname', $dn_optional, ''); if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'domains')) { throw new Exception("You cannot access this resource", 405); } $result = $this->apiCall('SubDomains.get', [ 'id' => $id, 'domainname' => $domainname ]); $id = $result['id']; if ($this->isAdmin() == false && (int)$result['caneditdomain'] == 0) { throw new Exception(lng('error.domaincannotbeedited', [$result['domain']]), 406); } // parameters $aliasdomain = $this->getParam('alias', true, 0); $path = $this->getParam('path', true, $result['documentroot']); $url = $this->getParam('url', true, ''); // default: 0 = wildcard, 1 = www-alias, 2 = none $_serveraliasdefault = $result['iswildcarddomain'] == '1' ? 0 : ($result['wwwserveralias'] == '1' ? 1 : 2); $selectserveralias = $this->getParam('selectserveralias', true, $_serveraliasdefault); $isemaildomain = $this->getBoolParam('isemaildomain', true, $result['isemaildomain']); $openbasedir_path = $this->getParam('openbasedir_path', true, $result['openbasedir_path']); $phpsettingid = $this->getParam('phpsettingid', true, $result['phpsettingid']); $redirectcode = $this->getParam('redirectcode', true, Domain::getDomainRedirectId($id)); $speciallogfile = $this->getBoolParam('speciallogfile', true, $result['speciallogfile']); $speciallogverified = $this->getBoolParam('speciallogverified', true, 0); if (Settings::Get('system.use_ssl')) { $sslenabled = $this->getBoolParam('sslenabled', true, $result['ssl_enabled']); $ssl_redirect = $this->getBoolParam('ssl_redirect', true, $result['ssl_redirect']); $letsencrypt = $this->getBoolParam('letsencrypt', true, $result['letsencrypt']); $http2 = $this->getBoolParam('http2', true, $result['http2']); $http3 = $this->getBoolParam('http3', true, $result['http3']); $hsts_maxage = $this->getParam('hsts_maxage', true, $result['hsts']); $hsts_sub = $this->getBoolParam('hsts_sub', true, $result['hsts_sub']); $hsts_preload = $this->getBoolParam('hsts_preload', true, $result['hsts_preload']); } else { $sslenabled = 0; $ssl_redirect = 0; $letsencrypt = 0; $http2 = 0; $http3 = 0; $hsts_maxage = 0; $hsts_sub = 0; $hsts_preload = 0; } // get needed customer info to reduce the subdomain-usage-counter by one $customer = $this->getCustomerData(); $alias_stmt = Database::prepare("SELECT COUNT(`id`) AS count FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `aliasdomain`= :aliasdomain"); $alias_check = Database::pexecute_first($alias_stmt, [ "aliasdomain" => $result['id'] ]); $alias_check = $alias_check['count']; // alias domain checked? if ($aliasdomain != 0) { $aliasdomain_stmt = Database::prepare(" SELECT `id` FROM `" . TABLE_PANEL_DOMAINS . "` `d`,`" . TABLE_PANEL_CUSTOMERS . "` `c` WHERE `d`.`customerid`= :customerid AND `d`.`aliasdomain` IS NULL AND `d`.`id`<>`c`.`standardsubdomain` AND `c`.`customerid`= :customerid AND `d`.`id`= :id "); $aliasdomain_check = Database::pexecute_first($aliasdomain_stmt, [ "id" => $aliasdomain, "customerid" => $customer['customerid'] ], true, true); if ($aliasdomain_check['id'] != $aliasdomain) { Response::standardError('domainisaliasorothercustomer', '', true); } Domain::triggerLetsEncryptCSRForAliasDestinationDomain($aliasdomain, $this->logger()); } // validate / correct path/url of domain $_doredirect = false; $path = $this->validateDomainDocumentRoot($path, $url, $customer, $result['domain'], $_doredirect); // set alias-fields according to selected alias mode $iswildcarddomain = ($selectserveralias == '0') ? '1' : '0'; $wwwserveralias = ($selectserveralias == '1') ? '1' : '0'; // if allowed, check for 'is email domain'-flag if ($isemaildomain != $result['isemaildomain']) { if ($result['parentdomainid'] != '0' && ($result['subcanemaildomain'] == '1' || $result['subcanemaildomain'] == '2')) { $isemaildomain = intval($isemaildomain); } elseif ($result['parentdomainid'] != '0') { $isemaildomain = $result['subcanemaildomain'] == '3' ? 1 : 0; } } // check changes of openbasedir-path variable if ($openbasedir_path > 2 && $openbasedir_path < 0) { $openbasedir_path = 0; } if ($ssl_redirect != 0) { // a ssl-redirect only works if there actually is a // ssl ip/port assigned to the domain if (Domain::domainHasSslIpPort($result['id']) == true) { $ssl_redirect = '1'; $_doredirect = true; } else { Response::standardError('sslredirectonlypossiblewithsslipport', '', true); } } if ($letsencrypt != 0) { // let's encrypt only works if there actually is a // ssl ip/port assigned to the domain if (Domain::domainHasSslIpPort($result['id']) == true) { $letsencrypt = '1'; } else { Response::standardError('letsencryptonlypossiblewithsslipport', '', true); } } // validate dns if lets encrypt is enabled to check whether we can use it at all if ($result['letsencrypt'] != $letsencrypt && $letsencrypt == '1' && Settings::Get('system.le_domain_dnscheck') == '1') { $our_ips = Domain::getIpsOfDomain($result['parentdomainid']); $domain_ips = PhpHelper::gethostbynamel6($result['domain'], true, Settings::Get('system.le_domain_dnscheck_resolver')); if ($domain_ips == false || count(array_intersect($our_ips, $domain_ips)) <= 0) { Response::standardError('invaliddnsforletsencrypt', '', true); } } // We can't enable let's encrypt for wildcard-domains if ($iswildcarddomain == '1' && $letsencrypt == '1') { Response::standardError('nowildcardwithletsencrypt', '', true); } // Temporarily deactivate ssl_redirect until Let's Encrypt certificate was generated if ($ssl_redirect > 0 && $letsencrypt == 1 && $result['letsencrypt'] != $letsencrypt) { $ssl_redirect = 2; } if ($speciallogfile != $result['speciallogfile'] && $speciallogverified != '1') { $speciallogfile = $result['speciallogfile']; } // is-email-domain flag changed - remove mail accounts and mail-addresses if (($result['isemaildomain'] == '1') && $isemaildomain == '0') { $params = [ "customerid" => $customer['customerid'], "domainid" => $id ]; $stmt = Database::prepare("DELETE FROM `" . TABLE_MAIL_USERS . "` WHERE `customerid`= :customerid AND `domainid`= :domainid"); Database::pexecute($stmt, $params, true, true); $stmt = Database::prepare("DELETE FROM `" . TABLE_MAIL_VIRTUAL . "` WHERE `customerid`= :customerid AND `domainid`= :domainid"); Database::pexecute($stmt, $params, true, true); $idna_convert = new IdnaWrapper(); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] automatically deleted mail-table entries for '" . $idna_convert->decode($result['domain']) . "'"); } $allowed_phpconfigs = $customer['allowed_phpconfigs']; if (!empty($allowed_phpconfigs)) { $allowed_phpconfigs = json_decode($allowed_phpconfigs, true); } else { $allowed_phpconfigs = []; } // only with fcgid/fpm enabled will it be possible to select a php-setting if ((int)$result['phpenabled'] == 1 && ((int)Settings::Get('system.mod_fcgid') == 1 || (int)Settings::Get('phpfpm.enabled') == 1)) { if (!in_array($phpsettingid, $allowed_phpconfigs)) { Response::standardError('notallowedphpconfigused', '', true); } } // handle redirect if ($_doredirect) { Domain::updateRedirectOfDomain($id, $redirectcode); } if ($path != $result['documentroot'] || $isemaildomain != $result['isemaildomain'] || $wwwserveralias != $result['wwwserveralias'] || $iswildcarddomain != $result['iswildcarddomain'] || $aliasdomain != (int)$result['aliasdomain'] || $openbasedir_path != $result['openbasedir_path'] || $sslenabled != $result['ssl_enabled'] || $ssl_redirect != $result['ssl_redirect'] || $letsencrypt != $result['letsencrypt'] || $hsts_maxage != $result['hsts'] || $hsts_sub != $result['hsts_sub'] || $hsts_preload != $result['hsts_preload'] || $phpsettingid != $result['phpsettingid'] || $http2 != $result['http2'] || $http3 != $result['http3'] || ($speciallogfile != $result['speciallogfile'] && $speciallogverified == '1') ) { $stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_DOMAINS . "` SET `documentroot` = :documentroot, `isemaildomain` = :isemaildomain, `wwwserveralias` = :wwwserveralias, `iswildcarddomain` = :iswildcarddomain, `aliasdomain` = :aliasdomain, `openbasedir_path` = :openbasedir_path, `ssl_enabled` = :sslenabled, `ssl_redirect` = :ssl_redirect, `letsencrypt` = :letsencrypt, `http2` = :http2, `http3` = :http3, `hsts` = :hsts, `hsts_sub` = :hsts_sub, `hsts_preload` = :hsts_preload, `phpsettingid` = :phpsettingid, `speciallogfile` = :speciallogfile WHERE `customerid`= :customerid AND `id`= :id "); $params = [ "documentroot" => $path, "isemaildomain" => $isemaildomain, "wwwserveralias" => $wwwserveralias, "iswildcarddomain" => $iswildcarddomain, "aliasdomain" => ($aliasdomain != 0 && $alias_check == 0) ? $aliasdomain : null, "openbasedir_path" => $openbasedir_path, "sslenabled" => $sslenabled, "ssl_redirect" => $ssl_redirect, "letsencrypt" => $letsencrypt, "http2" => $http2, "http3" => $http3, "hsts" => $hsts_maxage, "hsts_sub" => $hsts_sub, "hsts_preload" => $hsts_preload, "phpsettingid" => $phpsettingid, "speciallogfile" => $speciallogfile, "customerid" => $customer['customerid'], "id" => $id ]; Database::pexecute($stmt, $params, true, true); if ($result['aliasdomain'] != $aliasdomain && is_numeric($result['aliasdomain'])) { // trigger when domain id for alias destination has changed: both for old and new destination Domain::triggerLetsEncryptCSRForAliasDestinationDomain($result['aliasdomain'], $this->logger()); Domain::triggerLetsEncryptCSRForAliasDestinationDomain($aliasdomain, $this->logger()); } if ($result['wwwserveralias'] != $wwwserveralias || $result['letsencrypt'] != $letsencrypt) { // or when wwwserveralias or letsencrypt was changed Domain::triggerLetsEncryptCSRForAliasDestinationDomain($aliasdomain, $this->logger()); if ((int)$aliasdomain === 0) { // in case the wwwserveralias is set on a main domain, $aliasdomain is 0 // --> the call just above to triggerLetsEncryptCSRForAliasDestinationDomain // is a noop...let's repeat it with the domain id of the main domain Domain::triggerLetsEncryptCSRForAliasDestinationDomain($id, $this->logger()); } } // check whether LE has been disabled, so we remove the certificate if ($letsencrypt == '0' && $result['letsencrypt'] == '1') { $del_stmt = Database::prepare(" DELETE FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` WHERE `domainid` = :id "); Database::pexecute($del_stmt, [ 'id' => $id ], true, true); // remove domain from acme.sh / lets encrypt if used Cronjob::inserttask(TaskId::DELETE_DOMAIN_SSL, $result['domain']); } Cronjob::inserttask(TaskId::REBUILD_VHOST); Cronjob::inserttask(TaskId::REBUILD_DNS); $idna_convert = new IdnaWrapper(); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_NOTICE, "[API] edited domain '" . $idna_convert->decode($result['domain']) . "'"); } $result = $this->apiCall('SubDomains.get', [ 'id' => $id ]); return $this->response($result); } /** * lists all customer domain/subdomain entries * * @param bool $with_ips * optional, default true * @param int $customerid * optional, admin-only, select (sub)domains of a specific customer by id * @param string $loginname * optional, admin-only, select (sub)domains of a specific customer by loginname * @param array $sql_search * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), * LIKE is used if left empty and 'value' => searchvalue * @param int $sql_limit * optional specify number of results to be returned * @param int $sql_offset * optional specify offset for resultset * @param array $sql_orderby * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more * fields * * @access admin, customer * @return string json-encoded array count|list * @throws Exception */ public function listing() { $with_ips = $this->getParam('with_ips', true, true); if ($this->isAdmin()) { // if we're an admin, list all subdomains of all the admins customers // or optionally for one specific customer identified by id or loginname $customerid = $this->getParam('customerid', true, 0); $loginname = $this->getParam('loginname', true, ''); if (!empty($customerid) || !empty($loginname)) { $result = $this->apiCall('Customers.get', [ 'id' => $customerid, 'loginname' => $loginname ]); $custom_list_result = [ $result ]; } else { $_custom_list_result = $this->apiCall('Customers.listing'); $custom_list_result = $_custom_list_result['list']; } $customer_ids = []; foreach ($custom_list_result as $customer) { $customer_ids[] = $customer['customerid']; } if (empty($customer_ids)) { throw new Exception("Required resource unsatisfied.", 405); } $select_fields = [ '`d`.*' ]; } else { if (Settings::IsInList('panel.customer_hide_options', 'domains')) { throw new Exception("You cannot access this resource", 405); } $customer_ids = [ $this->getUserDetail('customerid') ]; $select_fields = [ '`d`.`id`', '`d`.`customerid`', '`d`.`domain`', '`d`.`domain_ace`', '`d`.`documentroot`', '`d`.`isbinddomain`', '`d`.`isemaildomain`', '`d`.`caneditdomain`', '`d`.`iswildcarddomain`', '`d`.`parentdomainid`', '`d`.`letsencrypt`', '`d`.`registration_date`', '`d`.`termination_date`', '`d`.`deactivated`', '`d`.`email_only`', ]; } $query_fields = []; // prepare select statement $domains_stmt = Database::prepare(" SELECT " . implode(",", $select_fields) . ", IF(`d`.`parentdomainid` > 0, `pd`.`domain_ace`, `d`.`domain_ace`) AS `parentdomainname`, `ad`.`id` AS `aliasdomainid`, `ad`.`domain` AS `aliasdomain`, `da`.`id` AS `domainaliasid`, `da`.`domain` AS `domainalias` FROM `" . TABLE_PANEL_DOMAINS . "` `d` LEFT JOIN `" . TABLE_PANEL_DOMAINS . "` `ad` ON `d`.`aliasdomain`=`ad`.`id` LEFT JOIN `" . TABLE_PANEL_DOMAINS . "` `da` ON `da`.`aliasdomain`=`d`.`id` LEFT JOIN `" . TABLE_PANEL_DOMAINS . "` `pd` ON `pd`.`id`=`d`.`parentdomainid` WHERE `d`.`customerid` IN (" . implode(', ', $customer_ids) . ") " . $this->getSearchWhere($query_fields, true) . " GROUP BY `d`.`id` ORDER BY `parentdomainname` ASC, `d`.`parentdomainid` ASC " . $this->getOrderBy(true) . $this->getLimit()); $result = []; Database::pexecute($domains_stmt, $query_fields, true, true); while ($row = $domains_stmt->fetch(PDO::FETCH_ASSOC)) { $row['ipsandports'] = []; if ($with_ips) { $row['ipsandports'] = $this->getIpsForDomain($row['id']); } $row['domain_hascert'] = $this->getHasCertValueForDomain((int)$row['id'], (int)$row['parentdomainid']); $result[] = $row; } return $this->response([ 'count' => count($result), 'list' => $result ]); } /** * get ips connected to given domain as array * * @param number $domain_id * @param bool $ssl_only * optional, return only ssl enabled ip's, default false * @return array */ private function getIpsForDomain($domain_id = 0, $ssl_only = false) { $fields = '`ips`.ip, `ips`.port, `ips`.ssl'; if ($this->isAdmin()) { $fields = '`ips`.*'; } $resultips_stmt = Database::prepare(" SELECT " . $fields . " FROM `" . TABLE_DOMAINTOIP . "` AS `dti`, `" . TABLE_PANEL_IPSANDPORTS . "` AS `ips` WHERE `dti`.`id_ipandports` = `ips`.`id` AND `dti`.`id_domain` = :domainid " . ($ssl_only ? " AND `ips`.`ssl` = '1'" : "")); Database::pexecute($resultips_stmt, [ 'domainid' => $domain_id ]); $ipandports = []; while ($rowip = $resultips_stmt->fetch(PDO::FETCH_ASSOC)) { if (filter_var($rowip['ip'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $rowip['is_ipv6'] = true; } $ipandports[] = $rowip; } return $ipandports; } /** * returns the total number of accessible subdomain entries * * @param int $customerid * optional, admin-only, select (sub)domains of a specific customer by id * @param string $loginname * optional, admin-only, select (sub)domains of a specific customer by loginname * * @access admin, customer * @return string json-encoded response message * @throws Exception */ public function listingCount() { if ($this->isAdmin()) { // if we're an admin, list all databases of all the admins customers // or optionally for one specific customer identified by id or loginname $customerid = $this->getParam('customerid', true, 0); $loginname = $this->getParam('loginname', true, ''); if (!empty($customerid) || !empty($loginname)) { $result = $this->apiCall('Customers.get', [ 'id' => $customerid, 'loginname' => $loginname ]); $custom_list_result = [ $result ]; } else { $_custom_list_result = $this->apiCall('Customers.listing'); $custom_list_result = $_custom_list_result['list']; } $customer_ids = []; foreach ($custom_list_result as $customer) { $customer_ids[] = $customer['customerid']; } } else { if (Settings::IsInList('panel.customer_hide_options', 'domains')) { throw new Exception("You cannot access this resource", 405); } $customer_ids = [ $this->getUserDetail('customerid') ]; } if (!empty($customer_ids)) { $query_fields = []; // prepare select statement $domains_stmt = Database::prepare(" SELECT COUNT(*) as num_subdom FROM `" . TABLE_PANEL_DOMAINS . "` `d` WHERE `d`.`customerid` IN (" . implode(', ', $customer_ids) . ") " . $this->getSearchWhere($query_fields, true)); $result = Database::pexecute_first($domains_stmt, $query_fields, true, true); if ($result) { return $this->response($result['num_subdom']); } } return $this->response(0); } /** * delete a subdomain by either id or domainname * * @param int $id * optional, the domain-id * @param string $domainname * optional, the domainname * @param int $customerid * optional, required when called as admin (if $loginname is not specified) * @param string $loginname * optional, required when called as admin (if $customerid is not specified) * * @access admin, customer * @return string json-encoded array * @throws Exception */ public function delete() { $id = $this->getParam('id', true, 0); $dn_optional = $id > 0; $domainname = $this->getParam('domainname', $dn_optional, ''); if ($this->isAdmin() == false && Settings::IsInList('panel.customer_hide_options', 'domains')) { throw new Exception("You cannot access this resource", 405); } $result = $this->apiCall('SubDomains.get', [ 'id' => $id, 'domainname' => $domainname ]); $id = $result['id']; // get needed customer info to reduce the subdomain-usage-counter by one $customer = $this->getCustomerData(); if (!$this->isAdmin() && $result['caneditdomain'] == 0) { throw new Exception("You cannot edit this resource", 405); } if ($result['isemaildomain'] == '1') { // check for e-mail addresses $emails_stmt = Database::prepare(" SELECT COUNT(`id`) AS `count` FROM `" . TABLE_MAIL_VIRTUAL . "` WHERE `customerid` = :customerid AND `domainid` = :domainid "); $emails = Database::pexecute_first($emails_stmt, [ "customerid" => $customer['customerid'], "domainid" => $id ], true, true); if ($emails['count'] != '0') { Response::standardError('domains_cantdeletedomainwithemail', '', true); } } if ((int)$result['aliasdomain'] !== 0) { Domain::triggerLetsEncryptCSRForAliasDestinationDomain($result['aliasdomain'], $this->logger()); } // delete domain from table $stmt = Database::prepare(" DELETE FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `customerid` = :customerid AND `id` = :id "); Database::pexecute($stmt, [ "customerid" => $customer['customerid'], "id" => $id ], true, true); // remove connections to ips and domainredirects $del_stmt = Database::prepare(" DELETE FROM `" . TABLE_DOMAINTOIP . "` WHERE `id_domain` = :domainid "); Database::pexecute($del_stmt, [ 'domainid' => $id ], true, true); // remove redirect-codes $del_stmt = Database::prepare(" DELETE FROM `" . TABLE_PANEL_DOMAINREDIRECTS . "` WHERE `did` = :domainid "); Database::pexecute($del_stmt, [ 'domainid' => $id ], true, true); // remove certificate from domain_ssl_settings, fixes #1596 $del_stmt = Database::prepare(" DELETE FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` WHERE `domainid` = :domainid "); Database::pexecute($del_stmt, [ 'domainid' => $id ], true, true); // remove possible existing DNS entries $del_stmt = Database::prepare(" DELETE FROM `" . TABLE_DOMAIN_DNS . "` WHERE `domain_id` = :domainid "); Database::pexecute($del_stmt, [ 'domainid' => $id ], true, true); Cronjob::inserttask(TaskId::REBUILD_VHOST); // Using nameserver, insert a task which rebuilds the server config Cronjob::inserttask(TaskId::REBUILD_DNS); // remove domains DNS from powerDNS if used, #581 Cronjob::inserttask(TaskId::DELETE_DOMAIN_PDNS, $result['domain']); // remove domain from acme.sh / lets encrypt if used Cronjob::inserttask(TaskId::DELETE_DOMAIN_SSL, $result['domain']); // reduce subdomain-usage-counter Customers::decreaseUsage($customer['customerid'], 'subdomains_used'); $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_WARNING, "[API] deleted subdomain '" . $result['domain'] . "'"); return $this->response($result); } } ================================================ FILE: lib/Froxlor/Api/Commands/SysLog.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Api\Commands; use Exception; use Froxlor\Api\ApiCommand; use Froxlor\Api\ResourceEntity; use Froxlor\Database\Database; use Froxlor\FroxlorLogger; use PDO; /** * @since 0.10.6 */ class SysLog extends ApiCommand implements ResourceEntity { /** * list all log-entries * * @param array $sql_search * optional array with index = fieldname, and value = array with 'op' => operator (one of <, > or =), * LIKE is used if left empty and 'value' => searchvalue * @param int $sql_limit * optional specify number of results to be returned * @param int $sql_offset * optional specify offset for resultset * @param array $sql_orderby * optional array with index = fieldname and value = ASC|DESC to order the resultset by one or more * fields * * @access admin, customer * @return string json-encoded array count|list * @throws Exception */ public function listing() { $result = []; $query_fields = []; if ($this->isAdmin() && $this->getUserDetail('customers_see_all') == '1') { $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_LOG . "` " . $this->getSearchWhere($query_fields) . $this->getOrderBy() . $this->getLimit()); } elseif ($this->isAdmin()) { // get all admin customers $_custom_list_result = $this->apiCall('Customers.listing'); $custom_list_result = $_custom_list_result['list']; $customer_names = []; foreach ($custom_list_result as $customer) { $customer_names[] = $customer['loginname']; } if (count($customer_names) > 0) { $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_LOG . "` WHERE `user` = :loginname OR `user` IN ('" . implode("', '", $customer_names) . "')" . $this->getSearchWhere($query_fields, true) . $this->getOrderBy() . $this->getLimit()); } else { $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_LOG . "` WHERE `user` = :loginname" . $this->getSearchWhere($query_fields, true) . $this->getOrderBy() . $this->getLimit()); } $query_fields['loginname'] = $this->getUserDetail('loginname'); } else { // every one else just sees their logs $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_LOG . "` WHERE `user` = :loginname AND `action` <> 99 " . $this->getSearchWhere($query_fields, true) . $this->getOrderBy() . $this->getLimit()); $query_fields['loginname'] = $this->getUserDetail('loginname'); } Database::pexecute($result_stmt, $query_fields, true, true); while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { // clean log-text $row['text'] = preg_replace("/[^\w @#\"':.,()\[\]+\-_\/\\\!]/i", "_", $row['text']); $result[] = $row; } $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] list log-entries"); return $this->response([ 'count' => count($result), 'list' => $result ]); } /** * returns the total number of log-entries * * @access admin * @return string json-encoded response message * @throws Exception */ public function listingCount() { $params = []; $query_fields = []; if ($this->isAdmin() && $this->getUserDetail('customers_see_all') == '1') { $result_stmt = Database::prepare(" SELECT COUNT(*) as num_logs FROM `" . TABLE_PANEL_LOG . "` " . $this->getSearchWhere($query_fields)); } elseif ($this->isAdmin()) { // get all admin customers $_custom_list_result = $this->apiCall('Customers.listing'); $custom_list_result = $_custom_list_result['list']; $customer_names = []; foreach ($custom_list_result as $customer) { $customer_names[] = $customer['loginname']; } if (count($customer_names) > 0) { $result_stmt = Database::prepare(" SELECT COUNT(*) as num_logs FROM `" . TABLE_PANEL_LOG . "` WHERE `user` = :loginname OR `user` IN ('" . implode("', '", $customer_names) . "') " . $this->getSearchWhere($query_fields, true)); } else { $result_stmt = Database::prepare(" SELECT COUNT(*) as num_logs FROM `" . TABLE_PANEL_LOG . "` WHERE `user` = :loginname " . $this->getSearchWhere($query_fields, true)); } $params = [ 'loginname' => $this->getUserDetail('loginname') ]; } else { // every one else just sees their logs $result_stmt = Database::prepare(" SELECT COUNT(*) as num_logs FROM `" . TABLE_PANEL_LOG . "` WHERE `user` = :loginname AND `action` <> 99 " . $this->getSearchWhere($query_fields, true)); $params = [ 'loginname' => $this->getUserDetail('loginname') ]; } $params = array_merge($params, $query_fields); $result = Database::pexecute_first($result_stmt, $params, true, true); if ($result) { return $this->response($result['num_logs']); } return $this->response(0); } /** * You cannot get log entries */ public function get() { throw new Exception('You cannot get log entries', 303); } /** * You cannot add log entries */ public function add() { throw new Exception('You cannot add log entries', 303); } /** * You cannot update log entries */ public function update() { throw new Exception('You cannot update log entries', 303); } /** * delete log entries * * @param int $min_to_keep * optional minutes to keep, default is 10 * * @access admin * @return string json-encoded array * @throws Exception */ public function delete() { if ($this->isAdmin()) { $min_to_keep = self::getParam('min_to_keep', true, 10); if ($min_to_keep < 0) { $min_to_keep = 0; } $truncatedate = time() - (60 * $min_to_keep); $params = []; if ($this->getUserDetail('customers_see_all') == '1') { $result_stmt = Database::prepare(" DELETE FROM `" . TABLE_PANEL_LOG . "` WHERE `date` < :trunc "); } else { // get all admin customers $_custom_list_result = $this->apiCall('Customers.listing'); $custom_list_result = $_custom_list_result['list']; $customer_names = []; foreach ($custom_list_result as $customer) { $customer_names[] = $customer['loginname']; } if (count($customer_names) > 0) { $result_stmt = Database::prepare(" DELETE FROM `" . TABLE_PANEL_LOG . "` WHERE `date` < :trunc AND `user` = :loginname OR `user` IN ('" . implode("', '", $customer_names) . "') "); } else { $result_stmt = Database::prepare(" DELETE FROM `" . TABLE_PANEL_LOG . "` WHERE `date` < :trunc AND `user` = :loginname "); } $params = [ 'loginname' => $this->getUserDetail('loginname') ]; } $params['trunc'] = $truncatedate; Database::pexecute($result_stmt, $params, true, true); $this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "[API] truncated the froxlor syslog"); return $this->response(true); } throw new Exception("Not allowed to execute given command.", 403); } } ================================================ FILE: lib/Froxlor/Api/Commands/Traffic.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Api\Commands; use Exception; use Froxlor\Api\ApiCommand; use Froxlor\Api\ResourceEntity; use Froxlor\Database\Database; use Froxlor\FroxlorLogger; use PDO; /** * @since 0.10.0 */ class Traffic extends ApiCommand implements ResourceEntity { /** * You cannot add traffic data * * @throws Exception */ public function add() { throw new Exception('You cannot add traffic data', 303); } /** * to get specific traffic details use year, month and/or day parameter for Traffic.listing() * * @throws Exception */ public function get() { throw new Exception('To get specific traffic details use year, month and/or day parameter for Traffic.listing()', 303); } /** * You cannot update traffic data * * @throws Exception */ public function update() { throw new Exception('You cannot update traffic data', 303); } /** * list traffic information * * @param int $year * optional, default empty * @param int $month * optional, default empty * @param int $day * optional, default empty * @param int $date_from * optional timestamp, default empty, if specified, $year, $month and $day will be ignored * @param int $date_until * optional timestamp, default empty, if specified, $year, $month and $day will be ignored * @param bool $customer_traffic * optional, admin-only, whether to output ones own traffic or all of ones customers, default is 0 * (false) * @param int $customerid * optional, admin-only, select traffic of a specific customer by id * @param string $loginname * optional, admin-only, select traffic of a specific customer by loginname * * @access admin, customer * @return string json-encoded array count|list * @throws Exception */ public function listing() { $year = $this->getParam('year', true, ""); $month = $this->getParam('month', true, ""); $day = $this->getParam('day', true, ""); $date_from = $this->getParam('date_from', true, -1); $date_until = $this->getParam('date_until', true, -1); $customer_traffic = $this->getBoolParam('customer_traffic', true, 0); $customer_ids = $this->getAllowedCustomerIds(); $result = []; $params = []; // validate parameters if ($date_from >= 0 || $date_until >= 0) { $year = ""; $month = ""; $day = ""; if ($date_from == $date_until) { $date_until = -1; } if ($date_from >= 0 && $date_until >= 0 && $date_until < $date_from) { // switch $temp_ts = $date_from; $date_from = $date_until; $date_until = $temp_ts; } } // check for year/month/day $where_str = ""; if (!empty($year) && is_numeric($year)) { $where_str .= " AND `year` = :year"; $params['year'] = $year; } if (!empty($month) && is_numeric($month)) { $where_str .= " AND `month` = :month"; $params['month'] = $month; } if (!empty($day) && is_numeric($day)) { $where_str .= " AND `day` = :day"; $params['day'] = $day; } if ($date_from >= 0 && $date_until >= 0) { $where_str .= " AND `stamp` BETWEEN :df AND :du"; $params['df'] = $date_from; $params['du'] = $date_until; } elseif ($date_from >= 0 && $date_until < 0) { $where_str .= " AND `stamp` > :df"; $params['df'] = $date_from; } elseif ($date_from < 0 && $date_until >= 0) { $where_str .= " AND `stamp` < :du"; $params['du'] = $date_until; } if (!$this->isAdmin() || ($this->isAdmin() && $customer_traffic)) { $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_TRAFFIC . "` WHERE `customerid` IN (" . implode(", ", $customer_ids) . ")" . $where_str); } else { $params['adminid'] = $this->getUserDetail('adminid'); $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_TRAFFIC_ADMINS . "` WHERE `adminid` = :adminid" . $where_str); } Database::pexecute($result_stmt, $params, true, true); while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { // make Bytes from KB $row['http'] *= 1024; $row['ftp_up'] *= 1024; $row['ftp_down'] *= 1024; $row['mail'] *= 1024; $result[] = $row; } $this->logger()->logAction($this->isAdmin() ? FroxlorLogger::ADM_ACTION : FroxlorLogger::USR_ACTION, LOG_INFO, "[API] list traffic"); return $this->response([ 'count' => count($result), 'list' => $result ]); } /** * You cannot count the traffic data list * * @throws Exception */ public function listingCount() { throw new Exception('You cannot count the traffic data list', 303); } /** * You cannot delete traffic data * * @throws Exception */ public function delete() { throw new Exception('You cannot delete traffic data', 303); } } ================================================ FILE: lib/Froxlor/Api/Commands/index.html ================================================ ================================================ FILE: lib/Froxlor/Api/FroxlorRPC.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Api; use Exception; use Froxlor\Database\Database; use Froxlor\System\IPTools; class FroxlorRPC { /** * validate a given request * * @param $request * @return array * @throws Exception */ public static function validateRequest($request): array { // make basic authentication if (!isset($_SERVER['PHP_AUTH_USER']) || !self::validateAuth($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])) { if (@php_sapi_name() !== 'cli') { header('WWW-Authenticate: Basic realm="API"'); } throw new Exception('Unauthenticated. Please provide api user credentials.', 401); } // check if present if (empty($request)) { throw new Exception('Empty request body.', 400); } // decode json request $decoded_request = json_decode($request, true); // is it valid? if (is_null($decoded_request)) { throw new Exception('Invalid JSON Format.', 400); } return self::validateBody($decoded_request); } /** * validates the given api credentials * * @param string $key * @param string $secret * * @return bool */ private static function validateAuth(string $key, string $secret): bool { $sel_stmt = Database::prepare( " SELECT ak.*, a.api_allowed as admin_api_allowed, c.api_allowed as cust_api_allowed, c.deactivated FROM `api_keys` ak LEFT JOIN `panel_admins` a ON a.adminid = ak.adminid LEFT JOIN `panel_customers` c ON c.customerid = ak.customerid WHERE `apikey` = :ak AND `secret` = :as " ); $result = Database::pexecute_first($sel_stmt, [ 'ak' => $key, 'as' => $secret ], true, true); if ($result) { if ($result['apikey'] == $key && $result['secret'] == $secret && ($result['valid_until'] == -1 || $result['valid_until'] >= time()) && (($result['customerid'] == 0 && $result['admin_api_allowed'] == 1) || ($result['customerid'] > 0 && $result['cust_api_allowed'] == 1 && $result['deactivated'] == 0))) { // get user to check whether api call is allowed if (!empty($result['allowed_from'])) { // @todo allow specification and validating of whole subnets later $ip_list = explode(",", $result['allowed_from']); if (self::validateAllowedFrom($ip_list, $_SERVER['REMOTE_ADDR'])) { return true; } } else { return true; } } } throw new Exception('Invalid authorization credentials', 403); } /** * validate if given remote_addr is within the list of allowed ip/ip-ranges * * @param array $allowed_from * @param string $remote_addr * * @return bool */ public static function validateAllowedFrom(array $allowed_from, string $remote_addr): bool { // shorten IP for comparison $remote_addr = inet_ntop(inet_pton($remote_addr)); // check for direct matches if (in_array($remote_addr, $allowed_from)) { return true; } // check for possible cidr ranges foreach ($allowed_from as $ip) { $ip_cidr = explode("/", $ip); if (count($ip_cidr) == 2 && IPTools::ip_in_range($ip_cidr, $remote_addr)) { return true; } } return false; } /** * validates the given command * * @param array $request * * @return array * @throws Exception */ private static function validateBody($request) { // check command exists if (empty($request['command'])) { throw new Exception("Please provide a command.", 400); } $command = explode(".", $request['command']); if (count($command) != 2) { throw new Exception("The given command is invalid.", 400); } // simply check for file-existance, as we do not want to use our autoloader because this way // it will recognize non-api classes+methods as valid commands $apiclass = '\\Froxlor\\Api\\Commands\\' . $command[0]; if (!class_exists($apiclass) || !@method_exists($apiclass, $command[1])) { throw new Exception("Unknown command", 400); } return [ 'command' => [ 'class' => $command[0], 'method' => $command[1] ], 'params' => $request['params'] ?? null ]; } } ================================================ FILE: lib/Froxlor/Api/ResourceEntity.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Api; /** * @since 0.10.0 */ interface ResourceEntity { public function listing(); public function listingCount(); public function get(); public function add(); public function update(); public function delete(); } ================================================ FILE: lib/Froxlor/Api/Response.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Api; class Response { public static function jsonDataResponse($data = null, int $response_code = 200) { return self::jsonResponse(['data' => $data], $response_code); } public static function jsonResponse($data = null, int $response_code = 200) { if (!defined('TRAVIS_CI') || TRAVIS_CI == 0) { http_response_code($response_code); } return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); } public static function jsonErrorResponse($message = null, int $response_code = 400) { return self::jsonResponse(['message' => $message], $response_code); } } ================================================ FILE: lib/Froxlor/Api/index.html ================================================ ================================================ FILE: lib/Froxlor/Bulk/BulkAction.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Bulk; use Exception; use Froxlor\FileDir; /** * Abstract Class BulkAction to mass-import entities * * @author Michael Kaufmann (d00p) * */ abstract class BulkAction { /** * logged in user * * @var array */ protected $userinfo = []; /** * complete path including filename of file to be imported * * @var string */ private $impFile = null; /** * api-function to call for addingg entity * * @var string */ private $api_call = null; /** * api-function parameter names, read from import-file (first line) * * @var array */ private $api_params = null; /** * errors while importing * * @var array */ private $errors = []; /** * class constructor, optionally sets file and customer-id * * @param string $import_file * @param array $userinfo * * @return object BulkAction instance */ protected function __construct(string $import_file = null, array $userinfo = []) { if (!empty($import_file)) { $this->impFile = FileDir::makeCorrectFile($import_file); } $this->userinfo = $userinfo; } /** * import the parsed import file data with an optional separator other then semicolon * and offset (maybe for header-line in csv or similar) * * @param string $separator * @param int $offset * * @return array 'all' => amount of records processed, 'imported' => number of imported records */ abstract public function doImport(string $separator = ";", int $offset = 0); /** * setter for import-file * * @param string $import_file * * @return void */ public function setImportFile($import_file = null) { $this->impFile = FileDir::makeCorrectFile($import_file); } /** * return the list of errors * * @return array */ public function getErrors() { return $this->errors; } /** * setter for api_call * * @param string $api_call * * @return void */ protected function setApiCall($api_call = "") { $this->api_call = $api_call; } protected function importEntity($data_array = null) { if (empty($data_array)) { return null; } $module = '\\Froxlor\\Api\\Commands\\' . substr($this->api_call, 0, strpos($this->api_call, ".")); $function = substr($this->api_call, strpos($this->api_call, ".") + 1); $new_data = []; foreach ($this->api_params as $idx => $param) { if (isset($data_array[$idx])) { $new_data[$param] = $data_array[$idx]; } } $result = null; try { $json_result = $module::getLocal($this->userinfo, $new_data)->$function(); $result = json_decode($json_result, true)['data']; } catch (Exception $e) { $this->errors[] = $e->getMessage(); } return !empty($result); } /** * reads in the csv import file and returns an array with * all the entities to be imported * * @param string $separator * * @return array */ protected function parseImportFile($separator = ";") { if (empty($this->impFile)) { throw new Exception("No file was given for import"); } if (!file_exists($this->impFile)) { throw new Exception("The file '" . $this->impFile . "' could not be found"); } if (!is_readable($this->impFile)) { throw new Exception("Unable to read file '" . $this->impFile . "'"); } if (empty($separator) || strlen($separator) != 1) { throw new Exception("Invalid separator specified: '" . $separator . "'"); } $file_data = []; $is_params_line = true; $fh = @fopen($this->impFile, "r"); if ($fh) { while (($line = fgets($fh)) !== false) { $tmp_arr = explode($separator, $line); $data_arr = []; foreach ($tmp_arr as $idx => $data) { if ($is_params_line) { $this->api_params[$idx] = $data; } else { $data_arr[$idx] = $data; } } if ($is_params_line) { $is_params_line = false; continue; } $file_data[] = array_map("trim", $data_arr); } $this->api_params = array_map("trim", $this->api_params); } else { throw new Exception("Unable to open file '" . $this->impFile . "'"); } fclose($fh); return $file_data; } } ================================================ FILE: lib/Froxlor/Bulk/DomainBulkAction.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Bulk; use Exception; /** * Class DomainBulkAction to mass-import domains for a given customer */ class DomainBulkAction extends BulkAction { /** * @param string $import_file * @param array $userinfo * * @return DomainBulkAction */ public function __construct(string $import_file = null, array $userinfo = []) { parent::__construct($import_file, $userinfo); $this->setApiCall('Domains.add'); } /** * import the parsed import file data with an optional separator other then semicolon * and offset (maybe for header-line in csv or similar) * * @param string $separator * @param int $offset * * @return array 'all' => amount of records processed, 'imported' => number of imported records */ public function doImport(string $separator = ";", int $offset = 0) { if ($this->userinfo['domains'] == "-1") { $dom_unlimited = true; } else { $dom_unlimited = false; } $domains_used = (int)$this->userinfo['domains_used']; $domains_avail = (int)$this->userinfo['domains']; if (!is_int($offset) || $offset < 0) { throw new Exception("Invalid offset specified"); } try { $domain_array = $this->parseImportFile($separator); } catch (Exception $e) { throw $e; } if (count($domain_array) <= 0) { throw new Exception("No domains were read from the file."); } $global_counter = 0; $import_counter = 0; $note = ''; foreach ($domain_array as $idx => $dom) { if ($idx >= $offset) { if ($dom_unlimited || (!$dom_unlimited && $domains_used < $domains_avail)) { $result = $this->importEntity($dom); if ($result) { $import_counter++; $domains_used++; } } else { $note .= 'You have reached your maximum allocation of domains (' . $domains_avail . ').'; break; } } $global_counter++; } return [ 'all' => $global_counter, 'imported' => $import_counter, 'notice' => $note ]; } } ================================================ FILE: lib/Froxlor/Bulk/index.html ================================================ ================================================ FILE: lib/Froxlor/Cli/CliCommand.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cli; use Exception; use Froxlor\Database\Database; use Froxlor\Froxlor; use Froxlor\Settings; use PDO; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Output\OutputInterface; class CliCommand extends Command { protected function validateRequirements(OutputInterface $output, bool $ignore_has_updates = false): int { if (!file_exists(Froxlor::getInstallDir() . '/lib/userdata.inc.php')) { $output->writeln("Could not find froxlor's userdata.inc.php file. You should use this script only with an installed froxlor system."); return self::INVALID; } // try database connection try { Database::query("SELECT 1"); } catch (Exception $e) { // Do not proceed further if no database connection could be established $output->writeln("" . $e->getMessage() . ""); return self::INVALID; } if (!$ignore_has_updates && (Froxlor::hasUpdates() || Froxlor::hasDbUpdates())) { if ((int)Settings::Get('system.cron_allowautoupdate') == 1) { return $this->runUpdate($output); } else { $output->writeln("It seems that the froxlor files have been updated. Please login and finish the update procedure."); return self::INVALID; } } return self::SUCCESS; } protected function getUserByName(?string $loginname, bool $deactivated_check = true): array { if (empty($loginname)) { throw new Exception("Empty username"); } $stmt = Database::prepare(" SELECT `loginname` AS `customer` FROM `" . TABLE_PANEL_CUSTOMERS . "` WHERE `loginname`= :loginname "); Database::pexecute($stmt, [ "loginname" => $loginname ]); $row = $stmt->fetch(PDO::FETCH_ASSOC); if ($row && $row['customer'] == $loginname) { $table = "`" . TABLE_PANEL_CUSTOMERS . "`"; $adminsession = '0'; } else { $stmt = Database::prepare(" SELECT `loginname` AS `admin` FROM `" . TABLE_PANEL_ADMINS . "` WHERE `loginname`= :loginname "); Database::pexecute($stmt, [ "loginname" => $loginname ]); $row = $stmt->fetch(PDO::FETCH_ASSOC); if ($row && $row['admin'] == $loginname) { $table = "`" . TABLE_PANEL_ADMINS . "`"; $adminsession = '1'; } else { throw new Exception("Unknown user '" . $loginname . "'"); } } $userinfo_stmt = Database::prepare(" SELECT * FROM $table WHERE `loginname`= :loginname "); Database::pexecute($userinfo_stmt, [ "loginname" => $loginname ]); $userinfo = $userinfo_stmt->fetch(PDO::FETCH_ASSOC); $userinfo['adminsession'] = $adminsession; if ($deactivated_check && $userinfo['deactivated']) { throw new Exception("User '" . $loginname . "' is currently deactivated"); } return $userinfo; } protected function runUpdate(OutputInterface $output, bool $manual = false): int { if (!$manual) { $output->writeln('Automatic update is activated and we are going to proceed without any notices'); } include_once Froxlor::getInstallDir() . '/lib/tables.inc.php'; define('_CRON_UPDATE', 1); ob_start([ $this, 'cleanUpdateOutput' ]); include_once Froxlor::getInstallDir() . '/install/updatesql.php'; ob_end_flush(); $output->writeln('' . ($manual ? 'Database' : 'Automatic') . ' update done - you should check your settings to be sure everything is fine'); return self::SUCCESS; } private function cleanUpdateOutput($buffer): string { return strip_tags(preg_replace("//", "\n", $buffer)); } } ================================================ FILE: lib/Froxlor/Cli/ConfigDiff.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cli; use Froxlor\Config\ConfigParser; use Froxlor\FileDir; use Froxlor\Froxlor; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; final class ConfigDiff extends CliCommand { protected function configure(): void { $this->setName('froxlor:config-diff') ->setDescription('Shows differences in config templates between OS versions') ->addArgument('from', InputArgument::OPTIONAL, 'OS version to compare against') ->addArgument('to', InputArgument::OPTIONAL, 'OS version to compare from') ->addOption('list', 'l', InputOption::VALUE_NONE, 'List all possible OS versions') ->addOption('diff-params', '', InputOption::VALUE_REQUIRED, 'Additional parameters for `diff`, e.g. --diff-params="--color=always"'); } /** * @throws \Exception */ protected function execute(InputInterface $input, OutputInterface $output): int { require Froxlor::getInstallDir() . '/lib/functions.php'; $parsers = $versions = []; foreach (glob(Froxlor::getInstallDir() . '/lib/configfiles/*.xml') as $config) { $name = str_replace(".xml", "", strtolower(basename($config))); $parser = new ConfigParser($config); $versions[$name] = $parser->getCompleteDistroName(); $parsers[$name] = $parser; } asort($versions); if ($input->getOption('list') === true) { $output->writeln('The following OS version templates are available:'); foreach ($versions as $k => $v) { $output->writeln(str_pad($k, 20) . $v); } return self::SUCCESS; } if (!$input->hasArgument('from') || !array_key_exists($input->getArgument('from'), $versions)) { $output->writeln('Missing or invalid "from" argument.'); $output->writeln('Available versions: ' . implode(', ', array_keys($versions))); return self::INVALID; } if (!$input->hasArgument('to') || !array_key_exists($input->getArgument('to'), $versions)) { $output->writeln('Missing or invalid "to" argument.'); $output->writeln('Available versions: ' . implode(', ', array_keys($versions))); return self::INVALID; } // Make sure diff is installed $check_diff_installed = FileDir::safe_exec('which diff'); if (count($check_diff_installed) === 0) { $output->writeln('Unable to find "diff" installation on your system.'); return self::INVALID; } $parser_from = $parsers[$input->getArgument('from')]; $parser_to = $parsers[$input->getArgument('to')]; $tmp_from = tempnam(sys_get_temp_dir(), 'froxlor_config_diff_from'); $tmp_to = tempnam(sys_get_temp_dir(), 'froxlor_config_diff_to'); $files = []; $titles_by_key = []; // Aggregate content for each config file foreach ([[$parser_from, 'from'], [$parser_to, 'to']] as $todo) { foreach ($todo[0]->getServices() as $service_type => $service) { foreach ($service->getDaemons() as $daemon_name => $daemon) { foreach ($daemon->getConfig() as $instruction) { if ($instruction['type'] !== 'file') { continue; } if (isset($instruction['subcommands'])) { foreach ($instruction['subcommands'] as $subinstruction) { if ($subinstruction['type'] !== 'file') { continue; } $content = $subinstruction['content']; } } else { $content = $instruction['content']; } if (!isset($content)) { throw new \Exception("Cannot find content for {$instruction['name']}"); } $key = "{$service_type}_{$daemon_name}_{$instruction['name']}"; $titles_by_key[$key] = "{$service->title} : {$daemon->title} : {$instruction['name']}"; if (!isset($files[$key])) { $files[$key] = ['from' => '', 'to' => '']; } $files[$key][$todo[1]] = $this->filterContent($content); } } } } ksort($files); $diff_params = ''; if ($input->hasOption('diff-params') && trim($input->getOption('diff-params')) !== '') { $diff_params = trim($input->getOption('diff-params')); } // Run diff on each file and output, if anything changed foreach ($files as $file_key => $content) { file_put_contents($tmp_from, $content['from']); file_put_contents($tmp_to, $content['to']); $diff_output = FileDir::safe_exec("{$check_diff_installed[0]} {$diff_params} {$tmp_from} {$tmp_to}"); if (count($diff_output) === 0) { continue; } $output->writeln('# ' . $titles_by_key[$file_key] . ''); $output->writeln(implode("\n", $diff_output) . "\n"); unset($diff_output); } // Remove tmp files again unlink($tmp_from); unlink($tmp_to); return self::SUCCESS; } private function filterContent(string $content): string { $new_content = ''; foreach (explode("\n", $content) as $n) { $n = trim($n); if (!$n) { continue; } if (str_starts_with($n, '#')) { continue; } $new_content .= $n . "\n"; } return $new_content; } } ================================================ FILE: lib/Froxlor/Cli/ConfigServices.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cli; use Exception; use Froxlor\Config\ConfigParser; use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\Froxlor; use Froxlor\PhpHelper; use Froxlor\Settings; use Froxlor\SImExporter; use Froxlor\System\Crypt; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; final class ConfigServices extends CliCommand { private $yes_to_all_supported = [ 'trixie', 'bookworm', 'bullseye', 'focal', 'jammy', 'noble', ]; protected function configure() { $this->setName('froxlor:config-services'); $this->setDescription('Configure system services'); $this->addOption('create', 'c', InputOption::VALUE_NONE, 'Create a services list configuration for the --apply option.') ->addOption('apply', 'a', InputOption::VALUE_REQUIRED, 'Configure your services by given configuration file/string. To create one run the command with the --create option.') ->addOption('list', 'l', InputOption::VALUE_NONE, 'Output the services that are going to be configured using a given config file (--apply option). No services will be configured.') ->addOption('daemon', 'd', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'When used with --apply you can specify one or multiple daemons. These will be the only services that get configured.') ->addOption('import-settings', 'i', InputOption::VALUE_REQUIRED, 'Import settings from another froxlor installation. This can be done standalone or in addition to --apply.') ->addOption('yes-to-all', 'A', InputOption::VALUE_NONE, 'Install packages without asking questions (Debian/Ubuntu only currently)') ->addOption('delete-file', 'D', InputOption::VALUE_NONE, 'If --apply is called with a local file, remove it after successful configurations.'); } protected function execute(InputInterface $input, OutputInterface $output): int { $result = $this->validateRequirements($output); require Froxlor::getInstallDir() . '/lib/functions.php'; if ($result == self::SUCCESS && $input->getOption('import-settings') == false && $input->getOption('create') == false && $input->getOption('apply') == false) { $output->writeln('No option given to do something, exiting.'); return self::INVALID; } // import settings if given if ($result == self::SUCCESS && $input->getOption('import-settings')) { $result = $this->importSettings($input, $output); } if ($result == self::SUCCESS && $input->getOption('yes-to-all')) { if (in_array(Settings::Get('system.distribution'), $this->yes_to_all_supported)) { putenv("DEBIAN_FRONTEND=noninteractive"); exec("echo 'APT::Get::Assume-Yes \"true\";' > /tmp/_tmp_apt.conf"); putenv("APT_CONFIG=/tmp/_tmp_apt.conf"); } else { $output->writeln('--yes-to-all ignored, not configured for supported distribution'); } } if ($result == self::SUCCESS) { $io = new SymfonyStyle($input, $output); if ($input->getOption('create')) { $result = $this->createConfig($output, $io); } elseif ($input->getOption('apply')) { $result = $this->applyConfig($input, $output, $io); } elseif ($input->getOption('list') || $input->getOption('daemon')) { $output->writeln('Options --list and --daemon only work together with --apply.'); $result = self::INVALID; } } if ($input->getOption('yes-to-all') && in_array(Settings::Get('system.distribution'), $this->yes_to_all_supported)) { putenv("DEBIAN_FRONTEND"); unlink("/tmp/_tmp_apt.conf"); putenv("APT_CONFIG"); } return $result; } private function importSettings(InputInterface $input, OutputInterface $output) { $importFile = $input->getOption('import-settings'); if (strtoupper(substr($importFile, 0, 4)) == 'HTTP') { $output->writeln("Settings file seems to be an URL, trying to download"); $target = "/tmp/froxlor-import-settings-" . time() . ".json"; if (@file_exists($target)) { @unlink($target); } $this->downloadFile($importFile, $target); $importFile = $target; } if (!is_file($importFile)) { $output->writeln('Given settings file is not a file'); return self::INVALID; } elseif (!file_exists($importFile)) { $output->writeln('Given settings file cannot be found (' . $importFile . ')'); return self::INVALID; } elseif (!is_readable($importFile)) { $output->writeln('Given settings file cannot be read (' . $importFile . ')'); return self::INVALID; } $imp_content = file_get_contents($importFile); SImExporter::import($imp_content); $output->writeln("Successfully imported settings from '" . $input->getOption('import-settings') . "'"); return self::SUCCESS; } private function downloadFile($src, $dest) { set_time_limit(0); // This is the file where we save the information $fp = fopen($dest, 'w+'); // Here is the file we are downloading, replace spaces with %20 $ch = curl_init(str_replace(" ", "%20", $src)); curl_setopt($ch, CURLOPT_TIMEOUT, 50); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // write curl response to file curl_setopt($ch, CURLOPT_FILE, $fp); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // get curl response curl_exec($ch); fclose($fp); } /** * @throws Exception */ private function createConfig(OutputInterface $output, SymfonyStyle $io): int { $_daemons_config = [ 'distro' => "" ]; $config_dir = Froxlor::getInstallDir() . '/lib/configfiles/'; // show list of available distro's $distros = glob($config_dir . '*.xml'); // tmp array $distributions_select_data = []; //set default os. $os_dist = ['ID' => 'trixie']; $os_version = ['0' => '13']; $os_default = $os_dist['ID']; //read os-release if (file_exists('/etc/os-release')) { $os_dist = parse_ini_file('/etc/os-release', false); if (is_array($os_dist) && array_key_exists('ID', $os_dist) && array_key_exists('VERSION_ID', $os_dist)) { $os_version = explode('.', $os_dist['VERSION_ID'])[0]; } } // read in all the distros foreach ($distros as $_distribution) { // get configparser object $dist = new ConfigParser($_distribution); // get distro-info $dist_display = $dist->getCompleteDistroName(); // store in tmp array $distributions_select_data[$dist_display] = str_replace(".xml", "", strtolower(basename($_distribution))); //guess if this is the current distro. $ver = explode('.', $dist->distributionVersion)[0]; if (strtolower($os_dist['ID']) == strtolower($dist->distributionName) && $os_version == $ver) { $os_default = str_replace(".xml", "", strtolower(basename($_distribution))); } } // sort by distribution name ksort($distributions_select_data); // list all distributions $table_rows = []; $valid_dists = []; foreach ($distributions_select_data as $name => $filename) { $table_rows[] = [$filename, $name]; $valid_dists[] = $filename; } $io->table( ['ID', 'Distribution'], $table_rows ); $_daemons_config['distro'] = $io->choice('Choose distribution', $valid_dists, $os_default); // go through all services and let user check whether to include it or not if (empty($_daemons_config['distro']) || !file_exists($config_dir . '/' . $_daemons_config['distro'] . ".xml")) { $output->writeln('Empty or non-existing distribution given.'); return self::INVALID; } $configfiles = new ConfigParser($config_dir . '/' . $_daemons_config['distro'] . ".xml"); $services = $configfiles->getServices(); foreach ($services as $si => $service) { $output->writeln("--- " . strtoupper($si) . " ---"); $_daemons_config[$si] = ""; $daemons = $service->getDaemons(); $default_daemon = ""; $table_rows = []; $valid_options = []; if ($si != 'system') { $table_rows[] = ['x', 'No']; $valid_options[] = 'x'; } foreach ($daemons as $di => $dd) { $title = $dd->title; if ($dd->default) { $default_daemon = $di; $title .= " (default)"; } $table_rows[] = [$di, $title]; $valid_options[] = $di; } $io->table( ['Value', 'Name'], $table_rows ); $daemons['x'] = 'x'; if ($si == 'system') { $_daemons_config[$si] = []; // for the system/other services we need a multiple choice possibility $output->writeln("Select every service you need. Enter empty value when done"); $sysservice = ""; do { $sysservice = $io->ask('Choose service'); if (!empty($sysservice)) { $_daemons_config[$si][] = $sysservice; } } while (!empty($sysservice)); // add 'cron' as fixed part (doesn't hurt if it exists) if (!in_array('cron', $_daemons_config[$si])) { $_daemons_config[$si][] = 'cron'; } } else { // for all others -> only one value $_daemons_config[$si] = $io->choice('Choose service', $valid_options, $default_daemon); } } $daemons_config = json_encode($_daemons_config); $output_file = $io->ask("Choose output-filename", "/tmp/froxlor-config-" . date('Ymd') . ".json"); file_put_contents($output_file, $daemons_config); $output->writeln("Successfully generated service-configfile '" . $output_file . "'"); $output->writeln([ "", "You can now apply this config running:", "php " . Froxlor::getInstallDir() . "bin/froxlor-cli froxlor:config-services --apply=" . $output_file, "" ]); $proceed = $io->confirm("Do you want to apply the config now?", false); if ($proceed) { passthru("php " . Froxlor::getInstallDir() . "bin/froxlor-cli froxlor:config-services --apply=" . $output_file); } return self::SUCCESS; } /** * @throws Exception */ private function applyConfig(InputInterface $input, OutputInterface $output, SymfonyStyle $io): int { $applyFile = $input->getOption('apply'); // check if plain JSON $decoded_config = json_decode($applyFile, true); $skipFileCheck = false; if (json_last_error() == JSON_ERROR_NONE) { $skipFileCheck = true; } if (!$skipFileCheck) { if (strtoupper(substr($applyFile, 0, 4)) == 'HTTP') { $output->writeln("Config file seems to be an URL, trying to download"); $target = "/tmp/froxlor-config-" . time() . ".json"; if (@file_exists($target)) { @unlink($target); } $this->downloadFile($applyFile, $target); $applyFile = $target; } if (!is_file($applyFile)) { $output->writeln('Given config file is not a file'); return self::INVALID; } elseif (!file_exists($applyFile)) { $output->writeln('Given config file cannot be found (' . $applyFile . ')'); return self::INVALID; } elseif (!is_readable($applyFile)) { $output->writeln('Given config file cannot be read (' . $applyFile . ')'); return self::INVALID; } $config = file_get_contents($applyFile); $decoded_config = json_decode($config, true); } if ($input->getOption('list') != false) { $table_rows = []; foreach ($decoded_config as $service => $daemon) { if (is_array($daemon) && count($daemon) > 0) { foreach ($daemon as $sysdaemon) { $table_rows[] = [$service, $sysdaemon]; } } else { if ($daemon == 'x') { $daemon = '--- skipped ---'; } $table_rows[] = [$service, $daemon]; } } $io->table( ['Service', 'Selected daemon'], $table_rows ); return self::SUCCESS; } $only_daemon = []; if ($input->getOption('daemon') != false) { $only_daemon = $input->getOption('daemon'); } if (!empty($decoded_config)) { $config_dir = Froxlor::getInstallDir() . 'lib/configfiles/'; if (empty($decoded_config['distro']) || !file_exists($config_dir . '/' . $decoded_config['distro'] . ".xml")) { $output->writeln('Empty or non-existing distribution given. Please login with an admin, go to "System -> Configuration" and select your correct distribution in the top-right corner or specify valid distribution name for "distro" parameter.'); return self::INVALID; } $configfiles = new ConfigParser($config_dir . '/' . $decoded_config['distro'] . ".xml"); $services = $configfiles->getServices(); $replace_arr = $this->getReplacerArray(); $clean_replace_arr = array_map(function ($v) { return escapeshellarg((string)($v ?? '')); }, $replace_arr); // be sure the fallback certificate specified in the settings exists $certFile = Settings::Get('system.ssl_cert_file'); $keyFile = Settings::Get('system.ssl_key_file'); if (empty($certFile) || empty($keyFile) || !file_exists($certFile) || !file_exists($keyFile)) { $output->writeln('Creating missing certificate ' . $certFile . ''); Crypt::createSelfSignedCertificate(); } foreach ($services as $si => $service) { $output->writeln("--- Configuring: " . strtoupper($si) . " ---"); if (!isset($decoded_config[$si]) || $decoded_config[$si] == 'x') { $output->writeln('Skipping ' . strtoupper($si) . ' configuration as desired'); continue; } $daemons = $service->getDaemons(); foreach ($daemons as $di => $dd) { // check for desired service if (($si != 'system' && $decoded_config[$si] != $di) || (is_array($decoded_config[$si]) && !in_array($di, $decoded_config[$si]))) { continue; } $output->writeln("Configuring '" . $di . "'"); if (!empty($only_daemon) && !in_array($di, $only_daemon)) { $output->writeln('Skipping ' . $di . ' configuration as desired'); continue; } // run all cmds $confarr = $dd->getConfig(); foreach ($confarr as $action) { switch ($action['type']) { case "install": $output->writeln("Installing required packages"); $result = null; passthru(strtr($action['content'], $clean_replace_arr), $result); if (strlen($result) > 1) { echo $result; } break; case "command": exec(strtr($action['content'], $clean_replace_arr)); break; case "file": if (array_key_exists('content', $action)) { $output->writeln('Creating file "' . $action['name'] . '"'); file_put_contents($action['name'], trim(strtr($action['content'], $replace_arr)) . PHP_EOL); } elseif (array_key_exists('subcommands', $action)) { foreach ($action['subcommands'] as $fileaction) { if (array_key_exists('execute', $fileaction) && $fileaction['execute'] == "pre") { exec(strtr($fileaction['content'], $replace_arr)); } elseif (array_key_exists('execute', $fileaction) && $fileaction['execute'] == "post") { exec(strtr($fileaction['content'], $replace_arr)); } elseif ($fileaction['type'] == 'file') { $output->writeln('Creating file "' . $fileaction['name'] . '"'); file_put_contents($fileaction['name'], trim(strtr($fileaction['content'], $replace_arr)) . PHP_EOL); } } } break; } } } } // set is_configured flag Settings::Set('panel.is_configured', '1', true); // run cronjob at the end to ensure configs are all up to date exec('php ' . Froxlor::getInstallDir() . 'bin/froxlor-cli froxlor:cron --force'); // and done $output->writeln('All services have been configured'); if ($input->getOption('delete-file') && file_exists($applyFile)) { @unlink($applyFile); } return self::SUCCESS; } else { $output->writeln('Unable to decode given JSON file'); return self::INVALID; } } /** * @throws Exception */ private function getReplacerArray(): array { $customer_tmpdir = '/tmp/'; if (Settings::Get('system.mod_fcgid') == '1' && Settings::Get('system.mod_fcgid_tmpdir') != '') { $customer_tmpdir = Settings::Get('system.mod_fcgid_tmpdir'); } elseif (Settings::Get('phpfpm.enabled') == '1' && Settings::Get('phpfpm.tmpdir') != '') { $customer_tmpdir = Settings::Get('phpfpm.tmpdir'); } // try to convert nameserver hosts to ip's $ns_ips = ""; $known_ns_ips = []; if (Settings::Get('system.nameservers') != '') { $nameservers = explode(',', Settings::Get('system.nameservers')); foreach ($nameservers as $nameserver) { $nameserver = trim($nameserver); // DNS servers might be multi homed; allow transfer from all ip // addresses of the DNS server $nameserver_ips = PhpHelper::gethostbynamel6($nameserver); // append dot to hostname if (substr($nameserver, -1, 1) != '.') { $nameserver .= '.'; } // ignore invalid responses if (!is_array($nameserver_ips)) { // act like PhpHelper::gethostbynamel6() and return unmodified hostname on error $nameserver_ips = [ $nameserver ]; } else { $known_ns_ips = array_merge($known_ns_ips, $nameserver_ips); } if (!empty($ns_ips)) { $ns_ips .= ','; } $ns_ips .= implode(",", $nameserver_ips); } } // AXFR server if (Settings::Get('system.axfrservers') != '') { $axfrservers = explode(',', Settings::Get('system.axfrservers')); foreach ($axfrservers as $axfrserver) { if (!in_array(trim($axfrserver), $known_ns_ips)) { if (!empty($ns_ips)) { $ns_ips .= ','; } $ns_ips .= trim($axfrserver); } } } Database::needSqlData(); $sql = Database::getSqlData(); return [ '' => $sql['user'], '' => $sql['passwd'], '' => $sql['db'], '' => $sql['host'], '' => $sql['socket'] ?? null, '' => Settings::Get('system.hostname'), '' => Settings::Get('system.ipaddress'), '' => Settings::Get('system.nameservers'), '' => $ns_ips, '' => Settings::Get('system.vmail_homedir'), '' => Settings::Get('system.vmail_uid'), '' => Settings::Get('system.vmail_gid'), '' => (Settings::Get('system.use_ssl') == '1') ? 'imaps pop3s' : '', '' => FileDir::makeCorrectDir($customer_tmpdir), '' => Froxlor::getInstallDir(), '' => FileDir::makeCorrectDir(Settings::Get('system.bindconf_directory')), '' => Settings::Get('system.apachereload_command'), '' => FileDir::makeCorrectDir(Settings::Get('system.logfiles_directory')), '' => FileDir::makeCorrectDir(Settings::Get('phpfpm.fastcgi_ipcdir')), '' => Settings::Get('system.httpgroup'), '' => Settings::Get('system.ssl_cert_file'), '' => Settings::Get('system.ssl_key_file'), '' => Settings::Get('panel.adminmail'), ]; } } ================================================ FILE: lib/Froxlor/Cli/InstallCommand.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cli; use Exception; use Froxlor\Config\ConfigParser; use Froxlor\Database\Database; use Froxlor\Froxlor; use Froxlor\Install\Install; use Froxlor\Install\Install\Core; use Froxlor\Settings; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; final class InstallCommand extends Command { private $io = null; private $formfielddata = []; protected function configure() { $this->setName('froxlor:install'); $this->setDescription('Installation process to use instead of web-ui'); $this->addArgument('input-file', InputArgument::OPTIONAL, 'Optional JSON array file to use for unattended installations'); $this->addOption('print-example-file', 'p', InputOption::VALUE_NONE, 'Outputs an example JSON content to be used with the input file parameter') ->addOption('create-userdata-from-str', 'c', InputOption::VALUE_REQUIRED, 'Creates lib/userdata.inc.php file from string created by web-install process') ->addOption('show-sysinfo', 's', InputOption::VALUE_NONE, 'Outputs system information about your froxlor installation'); } /** * @throws Exception */ protected function execute(InputInterface $input, OutputInterface $output): int { $result = self::SUCCESS; if ($input->getOption('create-userdata-from-str') !== null) { $ud_str = $input->getOption('create-userdata-from-str'); $ud_dec = @json_decode(@base64_decode($ud_str), true); if (is_array($ud_dec) && !empty($ud_dec) && count($ud_dec) == 8) { $core = new Core($ud_dec); $core->createUserdataConf(); return $result; } $output->writeln("Invalid parameter value."); return self::INVALID; } if ($input->getOption('show-sysinfo') !== false) { if (!file_exists(Froxlor::getInstallDir() . '/lib/userdata.inc.php')) { $output->writeln("Could not find froxlor's userdata.inc.php file. You can use this parameter only with an installed froxlor system."); return self::INVALID; } $this->printSysInfo($output); return self::SUCCESS; } session_start(); require __DIR__ . '/install.functions.php'; // set a few defaults CLI cannot know $_SERVER['SERVER_SOFTWARE'] = 'apache'; $host = []; exec('hostname -f', $host); $_SERVER['SERVER_NAME'] = $host[0] ?? ''; $ips = []; exec('hostname -I', $ips); $ips = explode(" ", $ips[0] ?? ""); // ipv4 address? $_SERVER['SERVER_ADDR'] = filter_var($ips[0] ?? "", FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ? ($ips[0] ?? '') : ''; if (empty($_SERVER['SERVER_ADDR'])) { // possible ipv6 address? $_SERVER['SERVER_ADDR'] = filter_var($ips[0] ?? "", FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? ($ips[0] ?? '') : ''; } if ($input->getOption('print-example-file') !== false) { $this->printExampleFile($output); return self::SUCCESS; } if (file_exists(Froxlor::getInstallDir() . '/lib/userdata.inc.php')) { $output->writeln("froxlor seems to be installed already."); return self::INVALID; } $this->io = new SymfonyStyle($input, $output); $this->io->title('Froxlor installation'); if ($input->getArgument('input-file')) { $inputFile = $input->getArgument('input-file'); if (strtoupper(substr($inputFile, 0, 4)) == 'HTTP') { $output->writeln("Input file seems to be an URL, trying to download"); $target = "/tmp/froxlor-install-" . time() . ".json"; if (@file_exists($target)) { @unlink($target); } $this->downloadFile($inputFile, $target); $inputFile = $target; } if (!is_file($inputFile)) { $output->writeln('Given input file is not a file'); return self::INVALID; } elseif (!file_exists($inputFile)) { $output->writeln('Given input file cannot be found (' . $inputFile . ')'); return self::INVALID; } elseif (!is_readable($inputFile)) { $output->writeln('Given input file cannot be read (' . $inputFile . ')'); return self::INVALID; } $inputcontent = file_get_contents($inputFile); $decoded_input = json_decode($inputcontent, true) ?? []; $extended = true; if (empty($decoded_input)) { $output->writeln('Given input file seems to be invalid JSON'); return self::INVALID; } $this->io->info('Running unattended installation'); } else { $extended = $this->io->confirm('Use advanced installation mode?', false); $decoded_input = []; } return $this->showStep(0, $extended, $decoded_input); } /** * @throws Exception */ private function showStep(int $step = 0, bool $extended = false, array $decoded_input = []): int { $result = self::SUCCESS; $inst = new Install(['step' => $step, 'extended' => $extended]); switch ($step) { case 0: $this->io->section(lng('install.preflight')); $crresult = $inst->checkRequirements(); $this->io->info($crresult['text']); if (!empty($crresult['criticals'])) { foreach ($crresult['criticals'] as $ctype => $critical) { if (!empty($ctype) && $ctype == 'wrong_ownership') { $this->io->error(lng('install.errors.' . $ctype, [$critical['user'], $critical['group']])); } elseif (!empty($ctype) && $ctype == 'missing_extensions') { $this->io->error([ lng('install.errors.' . $ctype), implode("\n", $critical) ]); } else { $this->io->error($critical); } } $result = self::FAILURE; } if (!empty($crresult['suggestions'])) { foreach ($crresult['suggestions'] as $ctype => $suggestion) { if ($ctype == 'missing_extensions') { $this->io->warning([ lng('install.errors.suggestedextensions'), implode("\n", $suggestion) ]); } else { $this->io->warning($suggestion); } } } if ($result == self::SUCCESS) { return $this->showStep(++$step, $extended, $decoded_input); } break; case 1: case 2: case 3: $section = $inst->formfield['install']['sections']['step' . $step] ?? []; $this->io->section($section['title']); if (empty($decoded_input)) { $this->io->note($section['description']); } foreach ($section['fields'] as $fieldname => $fielddata) { if ($extended == false && isset($fielddata['advanced']) && $fielddata['advanced'] == true) { if ($fieldname == 'httpuser' || $fieldname == 'httpgroup') { // overwrite posix_getgrgid(posix_getgid())['name'] as it would result in 'root' $this->formfielddata[$fieldname] = 'www-data'; } else { $this->formfielddata[$fieldname] = $fielddata['value']; } continue; } $ask_field = true; // preset from input-file if (!empty($decoded_input) && isset($decoded_input[$fieldname])) { $this->formfielddata[$fieldname] = $decoded_input[$fieldname]; $ask_field = false; } $fielddata['value'] = $this->formfielddata[$fieldname] ?? ($fielddata['value'] ?? null); $fielddata['label'] = $this->cliTextFormat($fielddata['label'], " "); if ($ask_field) { if ($fielddata['type'] == 'password') { $this->formfielddata[$fieldname] = $this->io->askHidden($fielddata['label'], function ($value) use ($fielddata) { if (isset($fielddata['mandatory']) && $fielddata['mandatory'] && empty($value)) { throw new \RuntimeException('You must enter a value.'); } return $value; }); } elseif ($fielddata['type'] == 'checkbox') { $this->formfielddata[$fieldname] = $this->io->confirm($fielddata['label'], $fielddata['value'] ?? false); } elseif ($fielddata['type'] == 'select') { $this->formfielddata[$fieldname] = $this->io->choice($fielddata['label'], $fielddata['select_var'], $fielddata['selected'] ?? ''); } else { $this->formfielddata[$fieldname] = $this->io->ask($fielddata['label'], $fielddata['value'] ?? '', function ($value) use ($fielddata) { if (isset($fielddata['mandatory']) && $fielddata['mandatory'] && empty($value)) { throw new \RuntimeException('You must enter a value.'); } return $value; }); } } else { $this->io->text("Setting field '" . $fieldname . "' to value '" . ($fielddata['type'] == 'password' ? '*hidden*' : $fielddata['value']) . "'"); if (isset($fielddata['mandatory']) && $fielddata['mandatory'] && empty($fielddata['value'])) { $this->io->error("Mandatory field '" . $fieldname . "' not specified/empty value in input file"); return self::FAILURE; } } } try { if ($step == 1) { $inst->checkDatabase($this->formfielddata); } elseif ($step == 2) { $inst->checkAdminUser($this->formfielddata); } elseif ($step == 3) { $inst->checkSystem($this->formfielddata); } } catch (Exception $e) { $this->io->error($e->getMessage()); if ($this->io->confirm('Retry?', empty($decoded_input))) { return $this->showStep($step, $extended, $decoded_input); } return self::FAILURE; } if ($step == 3) { // do actual install with data from $this->formfielddata $core = new Core($this->formfielddata); $core->doInstall(false); $core->createUserdataConf(); } return $this->showStep(++$step, $extended, $decoded_input); break; case 4: $section = $inst->formfield['install']['sections']['step' . $step] ?? []; $this->io->section($section['title']); $this->io->note($this->cliTextFormat($section['description'])); $cmdfield = $section['fields']['system']; $this->io->success([ $cmdfield['label'], $cmdfield['value'] ]); if (!isset($decoded_input['manual_config']) || (bool)$decoded_input['manual_config'] === false) { if (!empty($decoded_input) || $this->io->confirm('Execute command now?', false)) { passthru($cmdfield['value']); } } break; } return $result; } private function printExampleFile(OutputInterface $output) { // show list of available distro's $distros = glob(dirname(__DIR__, 3) . '/lib/configfiles/*.xml'); // read in all the distros foreach ($distros as $distribution) { // get configparser object $dist = new ConfigParser($distribution); // store in tmp array $supportedOS[str_replace(".xml", "", strtolower(basename($distribution)))] = $dist->getCompleteDistroName(); } // sort by distribution name asort($supportedOS); $webserverBackend = [ 'php-fpm' => 'PHP-FPM', 'fcgid' => 'FCGID', 'mod_php' => 'mod_php (not recommended)', ]; $guessedDistribution = ""; $guessedWebserver = ""; $fields = include dirname(dirname(__DIR__)) . '/formfields/install/formfield.install.php'; $json_output = []; foreach ($fields['install']['sections'] as $section => $section_fields) { foreach ($section_fields['fields'] as $name => $field) { if ($name == 'system' || $name == 'target_servername') { continue; } if ($field['type'] == 'text' || $field['type'] == 'email') { if ($name == 'httpuser' || $name == 'httpgroup') { $fieldval = 'www-data'; } else { $fieldval = $field['value'] ?? ""; } } elseif ($field['type'] == 'password') { $fieldval = '******'; } elseif ($field['type'] == 'select') { $fieldval = implode("|", array_keys($field['select_var'])); } elseif ($field['type'] == 'checkbox') { $fieldval = "1|0"; } else { $fieldval = "?"; } $json_output[$name] = $fieldval; } } $output->writeln(json_encode($json_output, JSON_PRETTY_PRINT)); } private function downloadFile($src, $dest) { set_time_limit(0); // This is the file where we save the information $fp = fopen($dest, 'w+'); // Here is the file we are downloading, replace spaces with %20 $ch = curl_init(str_replace(" ", "%20", $src)); curl_setopt($ch, CURLOPT_TIMEOUT, 50); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // write curl response to file curl_setopt($ch, CURLOPT_FILE, $fp); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // get curl response curl_exec($ch); fclose($fp); } private function printSysInfo(OutputInterface $output) { $php_sapi = 'mod_php'; $php_version = phpversion(); if (Settings::Get('system.mod_fcgid') == '1') { $php_sapi = 'FCGID'; if (Settings::Get('system.mod_fcgid_ownvhost') == '1') { $php_sapi .= ' (+ froxlor)'; } } elseif (Settings::Get('phpfpm.enabled') == '1') { $php_sapi = 'PHP-FPM'; if (Settings::Get('phpfpm.enabled_ownvhost') == '1') { $php_sapi .= ' (+ froxlor)'; } } $kernel = 'unknown'; if (function_exists('posix_uname')) { $kernel_nfo = posix_uname(); $kernel = $kernel_nfo['release'] . ' (' . $kernel_nfo['machine'] . ')'; } $ips = []; $ips_stmt = Database::query("SELECT CONCAT(`ip`, ' (', `port`, ')') as ipaddr FROM `" . TABLE_PANEL_IPSANDPORTS . "` ORDER BY `id`"); while ($ip = $ips_stmt->fetch(\PDO::FETCH_ASSOC)) { $ips[] = $ip['ipaddr']; } $table = new Table($output); $table ->setHeaders([ 'Key', 'Value' ]) ->setRows([ ['Froxlor', Froxlor::getVersionString()], ['Update-channel', Settings::Get('system.update_channel')], ['Hostname', Settings::Get('system.hostname')], ['Install-dir', Froxlor::getInstallDir()], ['PHP CLI', $php_version], ['PHP SAPI', $php_sapi], ['Webserver', Settings::Get('system.webserver')], ['Kernel', $kernel], ['Database', Database::getAttribute(\PDO::ATTR_SERVER_VERSION)], ['Distro config', Settings::Get('system.distribution')], ['IP addresses', implode("\n", $ips)], ]); $table->setStyle('box'); $table->render(); } private function cliTextFormat(string $text, string $nl_char = "\n"): string { $text = str_replace(['
', '
', '
'], [$nl_char, $nl_char, $nl_char], $text); return strip_tags($text); } } ================================================ FILE: lib/Froxlor/Cli/MasterCron.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cli; use Exception; use Froxlor\Cron\CronConfig; use Froxlor\Cron\TaskId; use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\Froxlor; use Froxlor\FroxlorLogger; use Froxlor\Settings; use Froxlor\System\Cronjob; use PDO; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; final class MasterCron extends CliCommand { private $lockFile = null; private $cronLog = null; protected function configure() { $this->setName('froxlor:cron'); $this->setDescription('Regulary perform tasks created by froxlor'); $this->addArgument('job', InputArgument::IS_ARRAY, 'Job(s) to run'); $this->addOption('run-task', 'r', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Run a specific task [1 = re-generate configs, 4 = re-generate dns zones, 9 = re-generate rspamd configs, 10 = re-set quotas, 99 = re-create cron.d-file]') ->addOption('force', 'f', InputOption::VALUE_NONE, 'Forces given job or, if none given, forces re-generating of config-files (webserver, nameserver, etc.)') ->addOption('debug', 'd', InputOption::VALUE_NONE, 'Output debug information about what is going on to STDOUT.') ->addOption('no-fork', 'N', InputOption::VALUE_NONE, 'Do not fork to background (traffic cron only).'); } /** * @throws Exception */ protected function execute(InputInterface $input, OutputInterface $output): int { $result = $this->validateRequirements($output); if ($result != self::SUCCESS) { // requirements failed, exit return $result; } $jobs = $input->getArgument('job'); // handle force option if ($input->getOption('force')) { if (empty($jobs) || in_array('tasks', $jobs)) { Cronjob::inserttask(TaskId::REBUILD_VHOST); Cronjob::inserttask(TaskId::REBUILD_DNS); Cronjob::inserttask(TaskId::REBUILD_RSPAMD); Cronjob::inserttask(TaskId::CREATE_QUOTA); Cronjob::inserttask(TaskId::REBUILD_CRON); Cronjob::inserttask(TaskId::UPDATE_LE_SERVICES); Cronjob::inserttask(TaskId::REBUILD_NSSUSERS); $jobs[] = 'tasks'; } define('CRON_IS_FORCED', 1); } // handle debug option if ($input->getOption('debug')) { define('CRON_DEBUG_FLAG', 1); } // handle no-fork option if ($input->getOption('no-fork')) { define('CRON_NOFORK_FLAG', 1); } // handle run-task option if ($input->getOption('run-task')) { $tasks_to_run = $input->getOption('run-task'); foreach ($tasks_to_run as $ttr) { if (in_array($ttr, [TaskId::REBUILD_VHOST, TaskId::REBUILD_DNS, TaskId::REBUILD_RSPAMD, TaskId::CREATE_QUOTA, TaskId::REBUILD_CRON, TaskId::UPDATE_LE_SERVICES, TaskId::REBUILD_NSSUSERS])) { Cronjob::inserttask($ttr); $jobs[] = 'tasks'; } else { $output->writeln('Unknown task number "' . $ttr . '"'); } } } // unique job-array $jobs = array_unique($jobs); // check for given job(s) to execute and return if empty if (empty($jobs)) { $output->writeln('No job given. Nothing to do.'); return self::INVALID; } $this->validateOwnership($output); $this->cronLog = FroxlorLogger::getInstanceOf([ 'loginname' => 'cronjob' ]); $this->cronLog->setCronDebugFlag(defined('CRON_DEBUG_FLAG')); // check whether there are actual tasks to perform by 'tasks'-cron, so // we don't regenerate files unnecessarily $tasks_cnt_stmt = Database::query("SELECT COUNT(*) as jobcnt FROM `panel_tasks`"); $tasks_cnt = $tasks_cnt_stmt->fetch(PDO::FETCH_ASSOC); // iterate through all needed jobs foreach ($jobs as $job) { // lock the job if ($this->lockJob($job, $output)) { // get FQDN of cron-class $cronfile = $this->getCronModule($job, $output); // validate if ($cronfile && class_exists($cronfile)) { // info $output->writeln('Running "' . $job . '" job' . (defined('CRON_IS_FORCED') ? ' (forced)' : '') . (defined('CRON_DEBUG_FLAG') ? ' (debug)' : '') . (defined('CRON_NOFORK_FLAG') ? ' (not forking)' : '') . ''); // update time of last run Cronjob::updateLastRunOfCron($job); // set logger $cronfile::setCronlog($this->cronLog); // run the job $cronfile::run(); } // free the lockfile $this->unlockJob(); } } // possible long-running jobs disconnect from the database Settings::refreshState(); // we have to check the system's last guid with every cron run // in case the admin installed new software which added a new user //so users in the database don't conflict with system users $this->cronLog->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Checking system\'s last guid'); Cronjob::checkLastGuid(); $this->cronLog->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Checking system\'s OS version'); Cronjob::checkCurrentDistro(); // validate if we're on fcgid/php-fpm that the local froxlor user is in the http-group to access log files if ((int)Settings::Get('phpfpm.enabled') == 1 || (int)Settings::Get('system.mod_fcgid') == 1) { $this->cronLog->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Checking group membership of local user'); Cronjob::checkLocalUserGroupMembership(); } // check for cron.d-generation task and create it if necessary CronConfig::checkCrondConfigurationFile(); // check for old/compatibility cronjob file if (file_exists(Froxlor::getInstallDir() . '/scripts/froxlor_master_cronjob.php')) { @unlink(Froxlor::getInstallDir() . '/scripts/froxlor_master_cronjob.php'); @rmdir(Froxlor::getInstallDir() . '/scripts'); } // reset cronlog-flag if set to "once" if ((int)Settings::Get('logger.log_cron') == 1) { FroxlorLogger::getInstanceOf()->setCronLog(0); } // clean up possible old login-links and 2fa tokens Database::query("DELETE FROM `" . TABLE_PANEL_LOGINLINKS . "` WHERE `valid_until` < UNIX_TIMESTAMP()"); Database::query("DELETE FROM `" . TABLE_PANEL_2FA_TOKENS . "` WHERE `valid_until` < UNIX_TIMESTAMP()"); return $result; } /** * @throws Exception */ private function validateOwnership(OutputInterface $output) { // when using fcgid or fpm for froxlor-vhost itself, we have to check // whether the permission of the files are still correct $output->write('Checking froxlor file permissions...'); $_mypath = FileDir::makeCorrectDir(Froxlor::getInstallDir()); if (((int)Settings::Get('system.mod_fcgid') == 1 && (int)Settings::Get('system.mod_fcgid_ownvhost') == 1) || ((int)Settings::Get('phpfpm.enabled') == 1 && (int)Settings::Get('phpfpm.enabled_ownvhost') == 1)) { $user = Settings::Get('system.mod_fcgid_httpuser'); $group = Settings::Get('system.mod_fcgid_httpgroup'); if (Settings::Get('phpfpm.enabled') == 1) { $user = Settings::Get('phpfpm.vhost_httpuser'); $group = Settings::Get('phpfpm.vhost_httpgroup'); } // all the files and folders have to belong to the local user FileDir::safe_exec('chown -R ' . $user . ':' . $group . ' ' . escapeshellarg($_mypath)); } else { // back to webserver permission $user = Settings::Get('system.httpuser'); $group = Settings::Get('system.httpgroup'); FileDir::safe_exec('chown -R ' . $user . ':' . $group . ' ' . escapeshellarg($_mypath)); } $output->writeln('OK'); } private function lockJob(string $job, OutputInterface $output): bool { $this->lockFile = '/run/lock/froxlor_' . $job . '.lock'; if (file_exists($this->lockFile)) { $jobinfo = json_decode(file_get_contents($this->lockFile), true); if ($jobinfo === false || !is_array($jobinfo)) { // looks like an invalid lockfile $check_pid_return = 1; } else { $check_pid_return = null; // get status of process system("kill -CHLD " . (int)$jobinfo['pid'] . " 1> /dev/null 2> /dev/null", $check_pid_return); } if ($check_pid_return == 1) { // Process does not seem to run, most likely it has died $this->unlockJob(); } else { // cronjob still running, output info and stop $output->writeln([ 'Job "' . $jobinfo['job'] . '" is currently running.', 'Started: ' . date('d.m.Y H:i', (int)$jobinfo['startts']), 'PID: ' . $jobinfo['pid'] . '' ]); return false; } } $jobinfo = [ 'job' => $job, 'startts' => time(), 'pid' => getmypid() ]; return file_put_contents($this->lockFile, json_encode($jobinfo)) !== false; } private function unlockJob(): bool { return @unlink($this->lockFile); } private function getCronModule(string $cronname, OutputInterface $output) { $upd_stmt = Database::prepare(" SELECT `cronclass` FROM `" . TABLE_PANEL_CRONRUNS . "` WHERE `cronfile` = :cron; "); $cron = Database::pexecute_first($upd_stmt, [ 'cron' => $cronname ]); if ($cron) { return $cron['cronclass']; } $output->writeln("Requested cronjob '" . $cronname . "' could not be found."); return false; } } ================================================ FILE: lib/Froxlor/Cli/PhpSessionclean.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cli; use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\Settings; use PDO; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; final class PhpSessionclean extends CliCommand { protected function configure() { $this->setName('froxlor:php-sessionclean'); $this->setDescription('Cleans old php-session files from tmp folder'); $this->addArgument('max-lifetime', InputArgument::OPTIONAL, 'The number of seconds after which data will be seen as "garbage" and potentially cleaned up. Defaults to "1440"'); } protected function execute(InputInterface $input, OutputInterface $output): int { $result = $this->validateRequirements($output); if ($result == self::SUCCESS) { if ((int)Settings::Get('phpfpm.enabled') == 1) { if ($input->hasArgument('max-lifetime') && is_numeric($input->getArgument('max-lifetime')) && $input->getArgument('max-lifetime') > 0) { $this->cleanSessionfiles((int)$input->getArgument('max-lifetime')); } else { // use default max-lifetime value $this->cleanSessionfiles(); } $result = self::SUCCESS; } else { // php-fpm not enabled $output->writeln('PHP-FPM not enabled for this installation.'); $result = self::INVALID; } } return $result; } private function cleanSessionfiles(int $maxlifetime = 1440) { // store paths to clean up $paths_to_clean = []; // get all pool-config directories configured $sel_stmt = Database::prepare("SELECT DISTINCT `config_dir` FROM `" . TABLE_PANEL_FPMDAEMONS . "`"); Database::pexecute($sel_stmt); while ($fpmd = $sel_stmt->fetch(PDO::FETCH_ASSOC)) { $poolfiles = glob(FileDir::makeCorrectFile($fpmd['config_dir'] . '/*.conf')); foreach ($poolfiles as $cf) { $contents = file_get_contents($cf); $pattern = preg_quote('session.save_path', '/'); $pattern = "/" . $pattern . ".+?\=(.*)/"; if (preg_match_all($pattern, $contents, $matches)) { $session_path = trim($matches[1][0]); // Skip non-file-based session storage (Redis, Memcached, etc.) // These typically contain protocol indicators like :// or are not valid directories if (strpos($session_path, '://') !== false) { // Skip paths with protocol indicators (tcp://, redis://, memcached://, etc.) continue; } $paths_to_clean[] = FileDir::makeCorrectDir(trim($matches[1][0])); } } } // every path is just needed once $paths_to_clean = array_unique($paths_to_clean); if (count($paths_to_clean) > 0) { foreach ($paths_to_clean as $ptc) { // find all files older than maxlifetime and delete them FileDir::safe_exec("find -O3 \"" . $ptc . "\" -ignore_readdir_race -depth -mindepth 1 -name 'sess_*' -type f -cmin \"+" . $maxlifetime . "\" -delete"); } } } } ================================================ FILE: lib/Froxlor/Cli/RunApiCommand.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cli; use Exception; use Froxlor\Froxlor; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; final class RunApiCommand extends CliCommand { protected function configure() { $this->setName('froxlor:api-call'); $this->setDescription('Run an API command as given user'); $this->addArgument('user', InputArgument::REQUIRED, 'Loginname of the user you want to run the command as') ->addArgument('api-command', InputArgument::REQUIRED, 'The command to execute in the form "Module.function"') ->addArgument('parameters', InputArgument::OPTIONAL, 'Parameters to pass to the command as JSON array'); $this->addOption('show-params', 's', InputOption::VALUE_NONE, 'Show possible parameters for given api-command (given command will *not* be called)'); } protected function execute(InputInterface $input, OutputInterface $output): int { $result = $this->validateRequirements($output); require Froxlor::getInstallDir() . '/lib/functions.php'; // set error-handler @set_error_handler([ '\\Froxlor\\Api\\Api', 'phpErrHandler' ]); if ($result == self::SUCCESS) { try { $loginname = $input->getArgument('user'); $userinfo = $this->getUserByName($loginname); $command = $input->getArgument('api-command'); $apicmd = $this->validateCommand($command); $module = "\\Froxlor\\Api\\Commands\\" . $apicmd['class']; $function = $apicmd['function']; if ($input->getOption('show-params') !== false) { $json_result = \Froxlor\Api\Commands\Froxlor::getLocal($userinfo, ['module' => $apicmd['class'], 'function' => $function])->listFunctions(); $io = new SymfonyStyle($input, $output); $result = $this->outputParamsList($json_result, $io); } else { $params_json = $input->getArgument('parameters'); $params = json_decode($params_json ?? '', true); $json_result = $module::getLocal($userinfo, $params)->{$function}(); $output->write($json_result); $result = self::SUCCESS; } } catch (Exception $e) { $output->writeln('' . $e->getMessage() . ''); $result = self::FAILURE; } } return $result; } private function outputParamsList(string $json, SymfonyStyle $io): int { $docs = json_decode($json, true); $docs = array_shift($docs['data']); if (!isset($docs['params'])) { $io->warning(($docs['head'] ?? "unknown return")); return self::INVALID; } if (empty($docs['params'])) { $io->success("No parameters required"); } else { $rows = []; foreach ($docs['params'] as $param) { $rows[] = [$param['type'], '' . $param['parameter'] . '', $param['desc'] ?? ""]; } $io->table(['Type', 'Name', 'Description'], $rows); } return self::SUCCESS; } /** * @throws Exception */ private function validateCommand(string $command): array { $command = explode(".", $command); if (count($command) != 2) { throw new Exception("The given command is invalid."); } // simply check for file-existance, as we do not want to use our autoloader because this way // it will recognize non-api classes+methods as valid commands $apiclass = '\\Froxlor\\Api\\Commands\\' . $command[0]; if (!class_exists($apiclass) || !@method_exists($apiclass, $command[1])) { throw new Exception("Unknown command"); } return ['class' => $command[0], 'function' => $command[1]]; } } ================================================ FILE: lib/Froxlor/Cli/SwitchServerIp.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cli; use Froxlor\Database\Database; use PDO; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; final class SwitchServerIp extends CliCommand { protected function configure() { $this->setName('froxlor:switch-server-ip'); $this->setDescription('Easily switch IP addresses e.g. after server migration'); $this->addOption('switch', 's', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Switch IP-address pair. A pair is separated by comma. For example: --switch=A,B') ->addOption('list', 'l', InputOption::VALUE_NONE, 'List all IP addresses currently added for this server in froxlor'); } protected function execute(InputInterface $input, OutputInterface $output): int { $result = $this->validateRequirements($output); if ($result == self::SUCCESS && $input->getOption('list') == false && $input->getOption('switch') == false) { $output->writeln('Either --list or --switch option must be provided. Nothing to do, exiting.'); $result = self::INVALID; } $io = new SymfonyStyle($input, $output); if ($result == self::SUCCESS && $input->getOption('list')) { $sel_stmt = Database::prepare("SELECT * FROM panel_ipsandports ORDER BY ip ASC, port ASC"); Database::pexecute($sel_stmt); $ips = $sel_stmt->fetchAll(PDO::FETCH_ASSOC); $table_rows = []; foreach ($ips as $ipdata) { $table_rows[] = [$ipdata['id'], $ipdata['ip'], $ipdata['port']]; } $io->table( ['#', 'IP address', 'Port'], $table_rows ); } if ($result == self::SUCCESS && $input->getOption('switch')) { $result = $this->switchIPs($input, $output); } return $result; } private function switchIPs(InputInterface $input, OutputInterface $output): int { $ip_list = $input->getOption('switch'); $has_error = false; $ips_to_switch = []; foreach ($ip_list as $ips_combo) { $ip_pair = explode(",", $ips_combo); if (count($ip_pair) != 2) { $output->writeln('Invalid option parameter, not a valid IP address pair.'); $has_error = true; } else { if (filter_var($ip_pair[0], FILTER_VALIDATE_IP) == false) { $output->writeln('Invalid source ip address: ' . $ip_pair[0] . ''); $has_error = true; } if (filter_var($ip_pair[1], FILTER_VALIDATE_IP) == false) { $output->writeln('Invalid target ip address: ' . $ip_pair[1] . ''); $has_error = true; } if ($ip_pair[0] == $ip_pair[1] && !$has_error) { $output->writeln('Source and target ip address are equal'); $has_error = true; } } $ips_to_switch[] = $ip_pair; } if ($has_error) { return self::FAILURE; } if (count($ips_to_switch) > 0) { $check_stmt = Database::prepare("SELECT `id` FROM panel_ipsandports WHERE `ip` = :newip"); $upd_stmt = Database::prepare("UPDATE panel_ipsandports SET `ip` = :newip WHERE `ip` = :oldip"); // system.ipaddress $check_sysip_stmt = Database::prepare("SELECT `value` FROM `panel_settings` WHERE `settinggroup` = 'system' and `varname` = 'ipaddress'"); $check_sysip = Database::pexecute_first($check_sysip_stmt); // system.mysql_access_host $check_mysqlip_stmt = Database::prepare("SELECT `value` FROM `panel_settings` WHERE `settinggroup` = 'system' and `varname` = 'mysql_access_host'"); $check_mysqlip = Database::pexecute_first($check_mysqlip_stmt); // system.axfrservers $check_axfrip_stmt = Database::prepare("SELECT `value` FROM `panel_settings` WHERE `settinggroup` = 'system' and `varname` = 'axfrservers'"); $check_axfrip = Database::pexecute_first($check_axfrip_stmt); foreach ($ips_to_switch as $ip_pair) { $output->writeln('Switching IP ' . $ip_pair[0] . ' to IP ' . $ip_pair[1] . ''); $ip_check = Database::pexecute_first($check_stmt, [ 'newip' => $ip_pair[1] ]); if ($ip_check) { $output->writeln('Note: ' . $ip_pair[0] . ' not updated to ' . $ip_pair[1] . ' - IP already exists in froxlor\'s database'); continue; } Database::pexecute($upd_stmt, [ 'newip' => $ip_pair[1], 'oldip' => $ip_pair[0] ]); $rows_updated = $upd_stmt->rowCount(); if ($rows_updated == 0) { $output->writeln('Note: ' . $ip_pair[0] . ' not updated to ' . $ip_pair[1] . ' (possibly no entry found in froxlor database. Use --list to see what IP addresses are added in froxlor'); continue; } // check whether the system.ipaddress needs updating if ($check_sysip['value'] == $ip_pair[0]) { $upd2_stmt = Database::prepare("UPDATE `panel_settings` SET `value` = :newip WHERE `settinggroup` = 'system' and `varname` = 'ipaddress'"); Database::pexecute($upd2_stmt, [ 'newip' => $ip_pair[1] ]); $output->writeln('Updated system-ipaddress from ' . $ip_pair[0] . ' to ' . $ip_pair[1] . ''); } // check whether the system.mysql_access_host needs updating if (strstr($check_mysqlip['value'], $ip_pair[0]) !== false) { $new_mysqlip = str_replace($ip_pair[0], $ip_pair[1], $check_mysqlip['value']); $upd2_stmt = Database::prepare("UPDATE `panel_settings` SET `value` = :newmysql WHERE `settinggroup` = 'system' and `varname` = 'mysql_access_host'"); Database::pexecute($upd2_stmt, [ 'newmysql' => $new_mysqlip ]); $output->writeln('Updated mysql_access_host from ' . $check_mysqlip['value'] . ' to ' . $new_mysqlip . ''); } // check whether the system.axfrservers needs updating if (strstr($check_axfrip['value'], $ip_pair[0]) !== false) { $new_axfrip = str_replace($ip_pair[0], $ip_pair[1], $check_axfrip['value']); $upd2_stmt = Database::prepare("UPDATE `panel_settings` SET `value` = :newaxfr WHERE `settinggroup` = 'system' and `varname` = 'axfrservers'"); Database::pexecute($upd2_stmt, [ 'newaxfr' => $new_axfrip ]); $output->writeln('Updated axfr-servers from ' . $check_axfrip['value'] . ' to ' . $new_axfrip . ''); } } } $output->writeln(""); $output->writeln("*** ATTENTION *** Remember to replace IP addresses in configuration files if used anywhere."); $output->writeln("IP addresses updated"); return self::SUCCESS; } } ================================================ FILE: lib/Froxlor/Cli/UpdateCommand.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cli; use Exception; use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\Froxlor; use Froxlor\Install\AutoUpdate; use Froxlor\Install\Preconfig; use Froxlor\Install\Update; use Froxlor\Settings; use Froxlor\System\Mailer; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Question\Question; use Symfony\Component\Console\Style\SymfonyStyle; final class UpdateCommand extends CliCommand { protected function configure() { $this->setName('froxlor:update'); $this->setDescription('Check for newer version and update froxlor'); $this->addOption('check-only', 'c', InputOption::VALUE_NONE, 'Only check for newer version and exit') ->addOption('show-update-options', 'o', InputOption::VALUE_NONE, 'Show possible update option parameter for the update if any. Only usable in combination with "check-only".') ->addOption('update-options', 'O', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Parameter list of update options.') ->addOption('database', 'd', InputOption::VALUE_NONE, 'Only run database updates in case updates are done via apt or manually.') ->addOption('mail-notify', 'm', InputOption::VALUE_NONE, 'Additionally inform administrator via email if a newer version was found') ->addOption('yes-to-all', 'A', InputOption::VALUE_NONE, 'Do not ask for download, extract and database-update, just do it (if not --check-only is set)') ->addOption('integer-return', 'i', InputOption::VALUE_NONE, 'Return integer whether a new version is available or not (implies --check-only). Useful for programmatic use.'); } protected function execute(InputInterface $input, OutputInterface $output) { $result = self::SUCCESS; // database update only if ($input->getOption('database')) { $result = $this->validateRequirements($output, true); if ($result == self::SUCCESS) { require Froxlor::getInstallDir() . '/lib/functions.php'; if (Froxlor::hasUpdates() || Froxlor::hasDbUpdates()) { $output->writeln('' . lng('update.dbupdate_required') . ''); if ($input->getOption('check-only')) { $output->writeln('Doing nothing because of "check-only" flag.'); $this->askUpdateOptions($input, $output, null, false); } else { $yestoall = $input->getOption('yes-to-all') !== false; $helper = $this->getHelper('question'); $this->askUpdateOptions($input, $output, $helper, $yestoall); $question = new ConfirmationQuestion('Update database? [no] ', false, '/^(y|j)/i'); if ($yestoall || $helper->ask($input, $output, $question)) { $result = $this->runUpdate($output, true); } } return $result; } $output->writeln('' . lng('update.noupdatesavail', [(Settings::Get('system.update_channel') == 'testing' ? lng('serversettings.uc_testing') . ' ' : '')]) . ''); } return $result; } $result = $this->validateRequirements($output); if ($result != self::SUCCESS) { // requirements failed, exit return $result; } require Froxlor::getInstallDir() . '/lib/functions.php'; // version check $newversionavail = false; if ($result == self::SUCCESS) { try { $aucheck = AutoUpdate::checkVersion(); if ($aucheck == 1) { $this->mailNotify($input, $output); if ($input->getOption('integer-return')) { $output->write(1); return self::SUCCESS; } // there is a new version if ($input->getOption('check-only')) { $text = lng('update.uc_newinfo', [(Settings::Get('system.update_channel') != 'stable' ? Settings::Get('system.update_channel') . ' ' : ''), AutoUpdate::getFromResult('version'), Froxlor::VERSION]); } else { $text = lng('admin.newerversionavailable') . ' ' . lng('admin.newerversiondetails', [AutoUpdate::getFromResult('version'), Froxlor::VERSION]); } $text = str_replace("
", " ", $text); $text = str_replace("", "", $text); $text = str_replace("", "
", $text); $newversionavail = true; $output->writeln('' . $text . ''); $result = self::SUCCESS; } elseif ($aucheck < 0 || $aucheck > 1) { if ($input->getOption('integer-return')) { $output->write(-1); return self::INVALID; } // errors if ($aucheck < 0) { $output->writeln('' . AutoUpdate::getLastError() . ''); } else { $errmsg = 'error.autoupdate_' . $aucheck; if ($aucheck == 3) { $errmsg = 'error.customized_version'; } $output->writeln('' . lng($errmsg) . ''); } $result = self::INVALID; } else { if ($input->getOption('integer-return')) { $output->write(0); return self::SUCCESS; } // no new version $output->writeln('' . AutoUpdate::getFromResult('info') . ''); $result = self::SUCCESS; } } catch (Exception $e) { if ($input->getOption('integer-return')) { $output->write(-1); return self::FAILURE; } $output->writeln('' . $e->getMessage() . ''); $result = self::FAILURE; } } // if there's a newer version, proceed if ($result == self::SUCCESS && $newversionavail) { // check whether we only wanted to check if ($input->getOption('check-only')) { //$output->writeln('Not proceeding as "check-only" is specified'); $this->askUpdateOptions($input, $output, null, false); return $result; } else { $yestoall = $input->getOption('yes-to-all') !== false; $helper = $this->getHelper('question'); // ask download $question = new ConfirmationQuestion('Download newer version? [no] ', false, '/^(y|j)/i'); if ($yestoall || $helper->ask($input, $output, $question)) { // do download $output->writeln('Downloading...'); $audl = AutoUpdate::downloadZip(AutoUpdate::getFromResult('version')); if (!is_numeric($audl)) { // ask extract $question = new ConfirmationQuestion('Extract downloaded archive? [no] ', false, '/^(y|j)/i'); if ($yestoall || $helper->ask($input, $output, $question)) { // do extract $output->writeln('Extracting...'); $auex = AutoUpdate::extractZip(Froxlor::getInstallDir() . '/updates/' . $audl); if ($auex == 0) { $output->writeln("Froxlor files updated successfully."); $result = self::SUCCESS; // restart fpm if used to clear opcache if ((int)Settings::Get('phpfpm.enabled') == 1 && Settings::Get('phpfpm.enabled_ownvhost') == '1') { // get fpm restart cmd $startstop_sel = Database::prepare(" SELECT f.reload_cmd, f.config_dir FROM `" . TABLE_PANEL_FPMDAEMONS . "` f LEFT JOIN `" . TABLE_PANEL_PHPCONFIGS . "` p ON p.fpmsettingid = f.id WHERE p.id = :phpconfigid "); $restart_cmd = Database::pexecute_first($startstop_sel, [ 'phpconfigid' => Settings::Get('phpfpm.vhost_defaultini') ]); // restart php-fpm instance FileDir::safe_exec(escapeshellcmd($restart_cmd['reload_cmd'])); } $this->askUpdateOptions($input, $output, $helper, $yestoall); $question = new ConfirmationQuestion('Update database? [no] ', false, '/^(y|j)/i'); if ($yestoall || $helper->ask($input, $output, $question)) { // run in separate process to ensure the use of newly unpacked files passthru(Froxlor::getInstallDir() . '/bin/froxlor-cli froxlor:update -dA', $result); } } else { $errmsg = 'error.autoupdate_' . $auex; $output->writeln('' . lng($errmsg) . ''); $result = self::FAILURE; } } } else { $errmsg = 'error.autoupdate_' . $audl; $output->writeln('' . lng($errmsg) . ''); $result = self::FAILURE; } } } } return $result; } /** * @param InputInterface $input * @param OutputInterface $output * @param $helper * @param bool $yestoall * @return void */ private function askUpdateOptions(InputInterface $input, OutputInterface $output, $helper, bool $yestoall = false) { // check for preconfigs $preconfig = Preconfig::getPreConfig(true); $show_options_only = $input->getOption('show-update-options') !== false; if (!is_null($helper) && $show_options_only) { $output->writeln('Unsetting "show-update-options" due to not being called with "check-only".'); $show_options_only = false; } $update_options = []; // set parameters $uOptions = $input->getOption('update-options'); if (!empty($uOptions)) { $options_value = []; foreach ($uOptions as $givenOption) { $optVal = explode("=", $givenOption); if (count($optVal) == 2) { $options_value[$optVal[0]] = $optVal[1]; } } } if (!empty($preconfig)) { krsort($preconfig); foreach ($preconfig as $section) { if (!$show_options_only) { $output->writeln("Updater questions for " . $section['title'] . ""); } foreach ($section['fields'] as $update_field => $metainfo) { if (isset($options_value[$update_field])) { $output->writeln('Setting given parameter "' . $update_field . '" to "' . $options_value[$update_field] . '"'); $_POST[$update_field] = $options_value[$update_field]; continue; } $default = null; $question_text = html_entity_decode(strip_tags($metainfo['label']), ENT_QUOTES | ENT_IGNORE, "UTF-8"); if ($metainfo['type'] == 'checkbox') { $default = (int)$metainfo['checked']; if ($show_options_only) { $update_options[] = [ 'name' => $update_field, 'question' => $question_text, 'default' => $default, 'choices' => '0: No' . PHP_EOL . '1: Yes' . PHP_EOL ]; } else { $question = new ConfirmationQuestion($question_text . ' [' . ($metainfo['checked'] ? 'yes' : 'no') . '] ', (bool)$metainfo['checked'], '/^(y|j)/i'); } } elseif ($metainfo['type'] == 'select') { $default = $metainfo['selected']; $choices = ""; foreach (array_values($metainfo['select_var'] ?? []) as $index => $choice) { $choices .= $index . ': ' . $choice . PHP_EOL; } if ($show_options_only) { $update_options[] = [ 'name' => $update_field, 'question' => $question_text, 'default' => !empty($default) ? $default : '-', 'choices' => $choices ]; } else { $question = new ChoiceQuestion( $question_text, array_values($metainfo['select_var'] ?? []), $metainfo['selected'] ); $question->setValidator(function ($answer) use ($metainfo): string { $key = array_keys($metainfo['select_var'])[(int)$answer] ?? false; // Find the key based on the selected value if ($key === false) { throw new \RuntimeException('Invalid selection.'); } return $key; }); } } elseif ($metainfo['type'] == 'text') { $default = $metainfo['value'] ?? ''; if ($show_options_only) { $update_options[] = [ 'name' => $update_field, 'question' => $question_text, 'default' => $default, 'choices' => PHP_EOL ]; } else { $question = new Question($question_text . (!empty($metainfo['value']) ? ' [' . $metainfo['value'] . ']' : ''), $default); $question->setValidator(function (string $answer) use ($metainfo): string { if (($metainfo['mandatory'] ?? false) && empty($answer)) { throw new \RuntimeException( 'Answer cannot be empty' ); } if (!empty($metainfo['pattern'] ?? "") && !preg_match("/" . $metainfo['pattern'] . "/", $answer)) { throw new \RuntimeException('Answer does not seem to be in valid format'); } return $answer; }); } } else { $output->writeln("Unknown type " . $metainfo['type'] . ""); continue; } if (!$show_options_only) { if ($yestoall) { $_POST[$update_field] = $default; } else { $_POST[$update_field] = $helper->ask($input, $output, $question); } } } } if ($show_options_only) { $io = new SymfonyStyle($input, $output); $io->table( ['Parameter', 'Description', 'Default', 'Choices'], $update_options ); } } } private function mailNotify(InputInterface $input, OutputInterface $output) { if ($input->getOption('mail-notify')) { $last_check_version = Settings::Get('system.update_notify_last'); if (Update::versionInUpdate($last_check_version, AutoUpdate::getFromResult('version'))) { $text = lng('update.uc_newinfo', [(Settings::Get('system.update_channel') != 'stable' ? Settings::Get('system.update_channel') . ' ' : ''), AutoUpdate::getFromResult('version'), Froxlor::VERSION]); $mail = new Mailer(true); $mail->Body = $text; $mail->Subject = "[froxlor] " . lng('update.notify_subject'); $mail->AddAddress(Settings::Get('panel.adminmail'), Settings::Get('panel.adminmail_defname')); if (!$mail->Send() && $input->getOption('integer-return') == null) { $output->writeln('' . $mail->ErrorInfo . ''); } Settings::Set('system.update_notify_last', AutoUpdate::getFromResult('version')); } } } } ================================================ FILE: lib/Froxlor/Cli/UserCommand.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cli; use Exception; use Froxlor\Api\Commands\Admins; use Froxlor\Api\Commands\Customers; use Froxlor\Froxlor; use Froxlor\System\Crypt; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; final class UserCommand extends CliCommand { protected function configure() { $this->setName('froxlor:user'); $this->setDescription('Various user actions'); $this->addArgument('user', InputArgument::REQUIRED, 'Loginname of the target user') ->addArgument('admin', InputArgument::OPTIONAL, 'Loginname of the executing admin/reseller user', 'admin'); $this->addOption('unlock', 'u', InputOption::VALUE_NONE, 'Unlock user after too many failed login attempts') ->addOption('change-passwd', 'p', InputOption::VALUE_NONE, 'Set new password for given user') ->addOption('show-info', 's', InputOption::VALUE_NONE, 'Output information details of given user'); } protected function execute(InputInterface $input, OutputInterface $output): int { $result = self::SUCCESS; $result = $this->validateRequirements($output); require Froxlor::getInstallDir() . '/lib/functions.php'; // set error-handler @set_error_handler([ '\\Froxlor\\Api\\Api', 'phpErrHandler' ]); if ($result == self::SUCCESS) { try { $adminname = $input->getArgument('admin'); $admininfo = $this->getUserByName($adminname); $loginname = $input->getArgument('user'); $userinfo = $this->getUserByName($loginname, false); $do_unlock = $input->getOption('unlock'); $do_passwd = $input->getOption('change-passwd'); $do_show = $input->getOption('show-info'); if ($do_unlock === false && $do_passwd === false && $do_show === false) { $output->writeln('No option given, nothing to do.'); $result = self::INVALID; } if ($do_unlock !== false) { // unlock given user if ((int)$userinfo['adminsession'] == 1) { Admins::getLocal($admininfo, ['loginname' => $loginname])->unlock(); } else { Customers::getLocal($admininfo, ['loginname' => $loginname])->unlock(); } $output->writeln('User ' . $loginname . ' unlocked successfully'); $result = self::SUCCESS; } if ($result == self::SUCCESS && $do_passwd !== false) { $io = new SymfonyStyle($input, $output); $passwd = $io->askHidden("Enter new password", function ($value) { if (empty($value)) { throw new \RuntimeException('You must enter a value.'); } $value = Crypt::validatePassword($value, 'new password'); return $value; }); $passwd2 = $io->askHidden("Confirm new password", function ($value) use ($passwd) { if (empty($value)) { throw new \RuntimeException('You must enter a value.'); } elseif ($value != $passwd) { throw new \RuntimeException('Passwords do not match'); } return $value; }); if ((int)$userinfo['adminsession'] == 1) { Admins::getLocal($admininfo, ['id' => $userinfo['adminid'], 'admin_password' => $passwd])->update(); } else { Customers::getLocal($admininfo, ['id' => $userinfo['customerid'], 'new_customer_password' => $passwd])->update(); } $output->writeln('Changed password for ' . $loginname . ''); $result = self::SUCCESS; } if ($result == self::SUCCESS && $do_show !== false) { $userinfo['password'] = '[hidden]'; $userinfo['data_2fa'] = '[hidden]'; $io = new SymfonyStyle($input, $output); $io->horizontalTable( array_keys($userinfo), [array_values($userinfo)] ); } } catch (Exception $e) { $output->writeln('' . $e->getMessage() . ''); $result = self::FAILURE; } } return $result; } } ================================================ FILE: lib/Froxlor/Cli/ValidateAcmeWebroot.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cli; use Froxlor\Cron\TaskId; use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\Froxlor; use Froxlor\Settings; use Froxlor\System\Cronjob; use PDO; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Style\SymfonyStyle; final class ValidateAcmeWebroot extends CliCommand { protected function configure() { $this->setName('froxlor:validate-acme-webroot'); $this->setDescription('Validates the Le_Webroot value is correct for froxlor managed domains with Let\'s Encrypt certificate.'); $this->addOption('yes-to-all', 'A', InputOption::VALUE_NONE, 'Do not ask for confirmation, update files if necessary'); } /** * @throws \Exception */ protected function execute(InputInterface $input, OutputInterface $output): int { $result = $this->validateRequirements($output, true); $io = new SymfonyStyle($input, $output); if ((int)Settings::Get('system.leenabled') == 0) { $io->info("Let's Encrypt not activated in froxlor settings."); $result = self::INVALID; } if ($result == self::SUCCESS) { $yestoall = $input->getOption('yes-to-all') !== false; $helper = $this->getHelper('question'); $count_changes = 0; // get all Let's Encrypt enabled domains $sel_stmt = Database::prepare("SELECT id, domain FROM panel_domains WHERE `letsencrypt` = '1' AND aliasdomain IS NULL ORDER BY id ASC"); Database::pexecute($sel_stmt); $domains = $sel_stmt->fetchAll(PDO::FETCH_ASSOC); // check for froxlor-vhost if (Settings::Get('system.le_froxlor_enabled') == '1') { $domains[] = [ 'id' => 0, 'domain' => Settings::Get('system.hostname') ]; } $upd_stmt = Database::prepare("UPDATE domain_ssl_settings SET `validtodate`=NULL WHERE `domainid` = :did"); $acmesh_dir = dirname(Settings::Get('system.acmeshpath')); $acmesh_challenge_dir = rtrim(FileDir::makeCorrectDir(Settings::Get('system.letsencryptchallengepath')), "/"); $recommended = rtrim(FileDir::makeCorrectDir(Froxlor::getInstallDir()), "/"); if ($acmesh_challenge_dir != $recommended) { $io->warning([ "ACME challenge docroot from settings differs from the current installation directory.", "Settings: '" . $acmesh_challenge_dir . "'", "Default/recommended value: '" . $recommended . "'", ]); $question = new ConfirmationQuestion('Fix ACME challenge docroot setting? [yes] ', true, '/^(y|j)/i'); if ($yestoall || $helper->ask($input, $output, $question)) { Settings::Set('system.letsencryptchallengepath', $recommended); $former_value = $acmesh_challenge_dir; $acmesh_challenge_dir = $recommended; // need to update the corresponding acme-alias config-file $acme_alias_file = Settings::Get('system.letsencryptacmeconf'); $sed_params = "s@" . $former_value . "@" . $acmesh_challenge_dir . "@"; FileDir::safe_exec('sed -i -e "' . $sed_params . '" ' . escapeshellarg($acme_alias_file)); $count_changes++; } } foreach ($domains as $domain_arr) { $domain = $domain_arr['domain']; $acme_domain_conf = FileDir::makeCorrectFile($acmesh_dir . '/' . $domain . '/' . $domain . '.conf'); if (file_exists($acme_domain_conf)) { $io->text("Getting info from " . $acme_domain_conf); $conf_content = file_get_contents($acme_domain_conf); } else { $acme_domain_conf = FileDir::makeCorrectFile($acmesh_dir . '/' . $domain . '_ecc/' . $domain . '.conf'); if (file_exists($acme_domain_conf)) { $io->text("Getting info from " . $acme_domain_conf); $conf_content = file_get_contents($acme_domain_conf); } else { $io->info("No domain configuration file found in '" . $acmesh_dir . "'"); continue; } } if (!empty($conf_content)) { $lines = explode("\n", $conf_content); foreach ($lines as $line) { $val_key = explode("=", $line); if ($val_key[0] == 'Le_Webroot') { $domain_webroot = trim(trim($val_key[1], "'"), '"'); if ($domain_webroot != $acmesh_challenge_dir) { $io->warning("Domain '" . $domain . "' has old/wrong Le_Webroot setting: '" . $domain_webroot . ' <> ' . $acmesh_challenge_dir . "'"); $question = new ConfirmationQuestion('Fix Le_Webroot? [yes] ', true, '/^(y|j)/i'); if ($yestoall || $helper->ask($input, $output, $question)) { $sed_params = "s@Le_Webroot=.*@Le_Webroot='" . $acmesh_challenge_dir . "'@"; FileDir::safe_exec('sed -i -e "' . $sed_params . '" ' . escapeshellarg($acme_domain_conf)); Database::pexecute($upd_stmt, ['did' => $domain_arr['id']]); $io->success("Correction of Le_Webroot successful"); $count_changes++; } else { continue; } } else { $io->info("Domain '" . $domain . "' Le_Webroot value is correct"); } break; } } } } if ($count_changes > 0) { if (Froxlor::hasUpdates() || Froxlor::hasDbUpdates()) { $io->info("Changes detected but froxlor has been updated. Inserting task to rebuild vhosts after update."); Cronjob::inserttask(TaskId::REBUILD_VHOST); } else { $question = new ConfirmationQuestion('Changes detected. Force cronjob to refresh certificates? [yes] ', true, '/^(y|j)/i'); if ($yestoall || $helper->ask($input, $output, $question)) { passthru(FileDir::makeCorrectFile(Froxlor::getInstallDir() . '/bin/froxlor-cli') . ' froxlor:cron -f -d'); } } } else { $io->success("No changes necessary."); } } return $result; } } ================================================ FILE: lib/Froxlor/Cli/index.html ================================================ ================================================ FILE: lib/Froxlor/Cli/install.functions.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ use Froxlor\Language; function lng(string $identifier, array $arguments = []) { return Language::getTranslation($identifier, $arguments); } function old(string $identifier, string $default = null, string $session = null) { return $default; } ================================================ FILE: lib/Froxlor/Config/ConfigDaemon.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Config; use Exception; use Froxlor\Froxlor; use Froxlor\Settings; use SimpleXMLElement; /** * Class ConfigDaemon * * Parses a distributions XML - file and gives access to the configuration * Not to be used directly */ class ConfigDaemon { /** * Human - readable title of this service * * @var string */ public $title; /** * Whether this is the default daemon of the service-category * * @var boolean */ public $default; /** * Holding the commands for this daemon * * @var array */ private $orders = []; /** * Store the parsed SimpleXMLElement for usage * * @var SimpleXMLElement */ private $fullxml; /** * Memorize if we already parsed the XML * * @var bool */ private $isparsed = false; /** * Sub - area of the full - XML only holding the daemon - data we are interested in * * @var SimpleXMLElement */ private $daemon; /** * xpath leading to this daemon in the full XML * * @var string */ private $xpath; /** * cache of sql-data if used */ private $sqldata_cache = null; public function __construct($xml, $xpath) { $this->fullxml = $xml; $this->xpath = $xpath; $this->daemon = $this->fullxml->xpath($this->xpath); if (count($this->daemon) !== 1) { throw new Exception('XPath "' . $this->xpath . '" didn\'t return exactly one element'); } $attributes = $this->daemon[0]->attributes(); if ($attributes['title'] != '') { $this->title = $this->parseContent((string)$attributes['title']); } if (isset($attributes['default'])) { $this->default = ($attributes['default'] == true); } } /** * Replace placeholders with content * * @param string $content * @return string $content w/o placeholder */ private function parseContent($content) { $content = preg_replace_callback('/\{\{(.*)\}\}/Ui', function ($matches) { $match = null; if (preg_match('/^settings\.(.*)$/', $matches[1], $match)) { return Settings::Get($match[1]); } elseif (preg_match('/^lng\.(.*)(?:\.(.*)(?:\.(.*)))$/U', $matches[1], $match)) { if (isset($match[1]) && $match[1] != '' && isset($match[2]) && $match[2] != '' && isset($match[3]) && $match[3] != '') { return lng($match[1] . '.' . $match[2] . '.' . $match[3]); } elseif (isset($match[1]) && $match[1] != '' && isset($match[2]) && $match[2] != '') { return lng($match[1] . '.' . $match[2]); } elseif (isset($match[1]) && $match[1] != '') { return lng($match[1]); } return ''; } elseif (preg_match('/^const\.(.*)$/', $matches[1], $match)) { return $this->returnDynamic($match[1]); } elseif (preg_match('/^sql\.(.*)$/', $matches[1], $match)) { if (is_null($this->sqldata_cache)) { // read in sql-data (if exists) if (file_exists(Froxlor::getInstallDir() . "/lib/userdata.inc.php")) { $sql = []; $sql_root = []; require Froxlor::getInstallDir() . "/lib/userdata.inc.php"; unset($sql_root); $this->sqldata_cache = $sql; } } return isset($this->sqldata_cache[$match[1]]) ? $this->sqldata_cache[$match[1]] : ''; } }, $content); return $content; } private function returnDynamic($key = null) { $dynamics = [ 'install_dir' => Froxlor::getInstallDir() ]; return $dynamics[$key] ?? ''; } /** * Get config for this daemon * * The returned array will be an array of array, each sub-array looking like this: * array('type' => 'install|file|command', 'content' => '') * If the type is "file", an additional "name" - element will be added to the array * To configure a daemon, the steps in the array must be followed in order * * @return array */ public function getConfig() { $this->parse(); return $this->orders; } /** * Parse the XML and populate $this->orders * * @return bool */ private function parse() { // We only want to parse the stuff one time if ($this->isparsed == true) { return true; } $preparsed = []; // First: let's push everything into an array and expand all includes foreach ($this->daemon[0]->children() as $order) { switch ((string)$order->getName()) { case "install": case "file": case "command": // Normal stuff, just add it to the preparsed - array $preparsed[] = $order; break; case "include": // Includes, get the part we want via xpath $includes = $this->fullxml->xpath((string)$order); foreach ($includes[0] as $include) { // The "include" is also a child, so just skip it, would make a mess later if ((string)$include->getName() == 'include') { continue; } $preparsed[] = $include; } break; // The next 3 are groupings, MUST come first in this to work properly case "commands": case "files": case "installs": // Hold the results $visibility = 1; foreach ($order->children() as $child) { switch ((string)$child->getName()) { case "visibility": $visibility += $this->checkVisibility($child); break; case "install": case "file": case "command": if ($visibility > 0) { $preparsed[] = $child; } break; case "include": // Includes, get the part we want via xpath $includes = $this->fullxml->xpath((string)$child); foreach ($includes[0] as $include) { // The "include" is also a child, so just skip it, would make a mess later if ((string)$include->getName() == 'include') { continue; } $preparsed[] = $include; } break; } } break; } } // Go through every preparsed order and evaluate what should happen to it foreach ($preparsed as $order) { $parsedorder = $this->parseOrder($order); // We got an array (= valid order) and the array already has a type -> add to stack if (is_array($parsedorder) && array_key_exists('type', $parsedorder)) { $this->orders[] = $parsedorder; // We got an array, but no type, means we got multiple orders back, at them to the stack one at a time } elseif (is_array($parsedorder)) { foreach ($parsedorder as $neworder) { $this->orders[] = $neworder; } } } // Switch flag to indicate we parsed our data $this->isparsed = true; return true; } /** * Check if visibility should be changed * * @param SimpleXMLElement $order * @return int 0|-1 */ private function checkVisibility($order) { $attributes = []; foreach ($order->attributes() as $key => $value) { $attributes[(string)$key] = $this->parseContent(trim((string)$value)); } $order = $this->parseContent(trim((string)$order)); if (!array_key_exists('mode', $attributes)) { throw new Exception('"' . $order . '" is missing mode'); } $return = 0; switch ($attributes['mode']) { case "isfile": if (!is_file($order)) { $return = -1; } break; case "notisfile": if (is_file($order)) { $return = -1; } break; case "isdir": if (!is_dir($order)) { $return = -1; } break; case "notisdir": if (is_dir($order)) { $return = -1; } break; case "false": if ($order == true) { $return = -1; } break; case "true": if ($order == false) { $return = -1; } break; case "notempty": if ($order == "") { $return = -1; } break; case "userexists": if (posix_getpwuid($order) === false) { $return = -1; } break; case "groupexists": if (posix_getgrgid($order) === false) { $return = -1; } break; case "usernotexists": if (is_array(posix_getpwuid($order))) { $return = -1; } break; case "groupnotexists": if (is_array(posix_getgrgid($order))) { $return = -1; } break; case "usernamenotexists": if (is_array(posix_getpwnam($order))) { $return = -1; } break; case "equals": $return = (isset($attributes['value']) && $attributes['value'] == $order ? 0 : -1); break; } return $return; } /** * Parse a single order and return it in a format for easier usage * * @param * SimpleXMLElement object holding a single order from the distribution - XML * @return array|string */ private function parseOrder($order) { $attributes = []; foreach ($order->attributes() as $key => $value) { $attributes[(string)$key] = (string)$value; } $parsedorder = ''; switch ((string)$order->getName()) { case "file": $parsedorder = $this->parseFile($order, $attributes); break; case "command": $parsedorder = $this->parseCommand($order, $attributes); break; case "install": $parsedorder = $this->parseInstall($order, $attributes); break; default: throw new Exception('Invalid order: ' . (string)$order->getName()); } return $parsedorder; } /** * Parse a file - order and return it in a format for easier usage * * @param * SimpleXMLElement object holding a single file from the distribution - XML * @return array|string */ private function parseFile($order, $attributes) { $visibility = 1; // No sub - elements, so the content can be returned directly if ($order->count() == 0) { $content = (string)$order; } else { // Hold the results foreach ($order->children() as $child) { switch ((string)$child->getName()) { case "visibility": $visibility += $this->checkVisibility($child); break; case "content": $content = (string)$child; break; } } } $return = []; // Check if the original file should be backupped // @TODO: Maybe have a backup - location somewhere central? // @TODO: Use IO - class if (array_key_exists('backup', $attributes)) { if (array_key_exists('mode', $attributes) && $attributes['mode'] == 'append') { $cmd = 'cp'; } else { $cmd = 'mv'; } $return[] = [ 'type' => 'command', 'content' => '[ -f ' . $this->parseContent($attributes['name']) . ' ] && ' . $cmd . ' "' . $this->parseContent($attributes['name']) . '" "' . $this->parseContent($attributes['name']) . '.frx.bak"', 'execute' => "pre" ]; } // Now the content of the file can be written if (isset($attributes['mode'])) { $return[] = [ 'type' => 'file', 'content' => $this->parseContent($content), 'name' => $this->parseContent($attributes['name']), 'mode' => $this->parseContent($attributes['mode']) ]; } else { $return[] = [ 'type' => 'file', 'content' => $this->parseContent($content), 'name' => $this->parseContent($attributes['name']) ]; } // Let's check if the mode of the file should be changed if (array_key_exists('chmod', $attributes)) { $return[] = [ 'type' => 'command', 'content' => 'chmod ' . $attributes['chmod'] . ' "' . $this->parseContent($attributes['name']) . '"', 'execute' => "post" ]; } // Let's check if the owner of the file should be changed if (array_key_exists('chown', $attributes)) { $return[] = [ 'type' => 'command', 'content' => 'chown ' . $attributes['chown'] . ' "' . $this->parseContent($attributes['name']) . '"', 'execute' => "post" ]; } // If we have more than 1 element, we want to group this stuff for easier processing later if (count($return) > 1) { $return = [ 'type' => 'file', 'subcommands' => $return, 'name' => $this->parseContent($attributes['name']) ]; } if ($visibility > 0) { return $return; } else { return ''; } } /** * Parse a command - order and return it in a format for easier usage * * @param * SimpleXMLElement object holding a single command from the distribution - XML * @return array|string */ private function parseCommand($order, $attributes) { // No sub - elements, so the content can be returned directly if ($order->count() == 0) { return [ 'type' => 'command', 'content' => $this->parseContent(trim((string)$order)) ]; } // Hold the results $visibility = 1; $content = ''; foreach ($order->children() as $child) { switch ((string)$child->getName()) { case "visibility": $visibility += $this->checkVisibility($child); break; case "content": $content = trim((string)$child); break; } } if ($visibility > 0) { return [ 'type' => 'command', 'content' => $this->parseContent($content) ]; } else { return ''; } } /** * Parse a install - order and return it in a format for easier usage * * @param * SimpleXMLElement object holding a single install from the distribution - XML * @return array|string */ private function parseInstall($order, $attributes) { // No sub - elements, so the content can be returned directly if ($order->count() == 0) { return [ 'type' => 'install', 'content' => $this->parseContent(trim((string)$order)) ]; } // Hold the results $visibility = 1; $content = ''; foreach ($order->children() as $child) { switch ((string)$child->getName()) { case "visibility": $visibility += $this->checkVisibility($child); break; case "content": $content = trim((string)$child); break; } } if ($visibility > 0) { return [ 'type' => 'install', 'content' => $this->parseContent($content) ]; } else { return ''; } } } ================================================ FILE: lib/Froxlor/Config/ConfigDisplay.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Config; use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\Froxlor; use Froxlor\PhpHelper; use Froxlor\Settings; use Froxlor\UI\Panel\UI; class ConfigDisplay { /** * @var array */ private static $replace_arr; /** * @var string */ private static $editor; /** * @var string */ private static $theme; /** * @param array $confarr * @param string $editor * @param string $theme */ public static function fromConfigArr(array $confarr, string $editor, string $theme) { self::$editor = $editor; self::$theme = $theme; $customer_tmpdir = '/tmp/'; if (Settings::Get('system.mod_fcgid') == '1' && Settings::Get('system.mod_fcgid_tmpdir') != '') { $customer_tmpdir = Settings::Get('system.mod_fcgid_tmpdir'); } elseif (Settings::Get('phpfpm.enabled') == '1' && Settings::Get('phpfpm.tmpdir') != '') { $customer_tmpdir = Settings::Get('phpfpm.tmpdir'); } // try to convert namserver hosts to ip's $ns_ips = ""; $known_ns_ips = []; if (Settings::Get('system.nameservers') != '') { $nameservers = explode(',', Settings::Get('system.nameservers')); foreach ($nameservers as $nameserver) { $nameserver = trim($nameserver); // DNS servers might be multi homed; allow transfer from all ip // addresses of the DNS server $nameserver_ips = PhpHelper::gethostbynamel6($nameserver); // append dot to hostname if (substr($nameserver, -1, 1) != '.') { $nameserver .= '.'; } // ignore invalid responses if (!is_array($nameserver_ips)) { // act like \Froxlor\PhpHelper::gethostbynamel6() and return unmodified hostname on error $nameserver_ips = [ $nameserver ]; } else { $known_ns_ips = array_merge($known_ns_ips, $nameserver_ips); } if (!empty($ns_ips)) { $ns_ips .= ','; } $ns_ips .= implode(",", $nameserver_ips); } } // AXFR server if (Settings::Get('system.axfrservers') != '') { $axfrservers = explode(',', Settings::Get('system.axfrservers')); foreach ($axfrservers as $axfrserver) { if (!in_array(trim($axfrserver), $known_ns_ips)) { if (!empty($ns_ips)) { $ns_ips .= ','; } $ns_ips .= trim($axfrserver); } } } Database::needSqlData(); $sql = Database::getSqlData(); self::$replace_arr = [ '' => $sql['user'], '' => 'FROXLOR_MYSQL_PASSWORD', '' => $sql['db'], '' => $sql['host'], '' => $sql['socket'] ?? null, '' => Settings::Get('system.hostname'), '' => Settings::Get('system.ipaddress'), '' => Settings::Get('system.nameservers'), '' => $ns_ips, '' => Settings::Get('system.vmail_homedir'), '' => Settings::Get('system.vmail_uid'), '' => Settings::Get('system.vmail_gid'), '' => (Settings::Get('system.use_ssl') == '1') ? 'imaps pop3s' : '', '' => FileDir::makeCorrectDir($customer_tmpdir), '' => Froxlor::getInstallDir(), '' => FileDir::makeCorrectDir(Settings::Get('system.bindconf_directory')), '' => Settings::Get('system.apachereload_command'), '' => FileDir::makeCorrectDir(Settings::Get('system.logfiles_directory')), '' => FileDir::makeCorrectDir(Settings::Get('phpfpm.fastcgi_ipcdir')), '' => Settings::Get('system.httpgroup'), '' => Settings::Get('system.ssl_cert_file'), '' => Settings::Get('system.ssl_key_file'), '' => Settings::Get('panel.adminmail'), ]; $commands_pre = ""; $commands_file = ""; $commands_post = ""; $lasttype = ''; $commands = ''; $configpage = ""; foreach ($confarr as $_action) { if ($lasttype != '' && $lasttype != $_action['type']) { $commands = trim($commands); $numbrows = count(explode("\n", $commands)); $configpage .= UI::twig()->render(UI::validateThemeTemplate('/settings/conf/command.html.twig', self::$theme), [ 'commands' => $commands, 'numbrows' => $numbrows ]); $lasttype = ''; $commands = ''; } switch ($_action['type']) { case "install": $commands .= strtr($_action['content'], self::$replace_arr) . "\n"; $lasttype = "install"; break; case "command": $commands .= strtr($_action['content'], self::$replace_arr) . "\n"; $lasttype = "command"; break; case "file": if (array_key_exists('content', $_action)) { $commands_file = self::getFileContentContainer($_action['content'], $_action['name']); } elseif (array_key_exists('subcommands', $_action)) { foreach ($_action['subcommands'] as $fileaction) { if (array_key_exists('execute', $fileaction) && $fileaction['execute'] == "pre") { $commands_pre .= $fileaction['content'] . "\n"; } elseif (array_key_exists('execute', $fileaction) && $fileaction['execute'] == "post") { $commands_post .= $fileaction['content'] . "\n"; } elseif ($fileaction['type'] == 'file') { $commands_file = self::getFileContentContainer($fileaction['content'], $_action['name']); } } } $realname = $_action['name']; $commands = trim($commands_pre); if ($commands != "") { $numbrows = count(explode("\n", $commands)); $commands_pre = UI::twig()->render(UI::validateThemeTemplate('/settings/conf/command.html.twig', self::$theme), [ 'commands' => $commands, 'numbrows' => $numbrows ]); } $commands = trim($commands_post); if ($commands != "") { $numbrows = count(explode("\n", $commands)); $commands_post = UI::twig()->render(UI::validateThemeTemplate('/settings/conf/command.html.twig', self::$theme), [ 'commands' => $commands, 'numbrows' => $numbrows ]); } $configpage .= UI::twig()->render(UI::validateThemeTemplate('/settings/conf/fileblock.html.twig', self::$theme), [ 'realname' => $realname, 'commands_pre' => $commands_pre, 'commands_file' => $commands_file, 'commands_post' => $commands_post ]); $commands = ''; $commands_pre = ''; $commands_post = ''; break; } } $commands = trim($commands); if ($commands != '') { $numbrows = count(explode("\n", $commands)); $configpage .= UI::twig()->render(UI::validateThemeTemplate('/settings/conf/command.html.twig', self::$theme), [ 'commands' => $commands, 'numbrows' => $numbrows ]); } return $configpage; } /** * @param string $file_content * @param string $realname * * @return string */ private static function getFileContentContainer(string $file_content, string $realname): string { $files = ""; $file_content = trim($file_content); if ($file_content != '') { $file_content = strtr($file_content, self::$replace_arr); $file_content = htmlspecialchars($file_content); $numbrows = count(explode("\n", $file_content)); //eval("\$files=\"" . \Froxlor\UI\Template::getTemplate("configfiles/configfiles_file") . "\";"); $files = UI::twig()->render(UI::validateThemeTemplate('/settings/conf/file.html.twig', self::$theme), [ 'distro_editor' => self::$editor, 'realname' => $realname, 'numbrows' => $numbrows, 'file_content' => $file_content ]); } return $files; } } ================================================ FILE: lib/Froxlor/Config/ConfigParser.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Config; use Exception; use SimpleXMLElement; /** * Class ConfigParser * * Parses a distributions XML - file and gives access to the configuration */ class ConfigParser { /** * Name of the distribution this configuration is for * * @var string */ public $distributionName = ''; /** * Codename of the distribution this configuration is for * * @var string */ public $distributionCodename = ''; /** * Version of the distribution this configuration is for * * @var string */ public $distributionVersion = ''; /** * Recommended editor * * @var string */ public $distributionEditor = '/bin/nano'; /** * Show if this configuration is deprecated * * @var bool */ public $deprecated = false; /** * Holding the available services in the XML * * @var array */ private $services = []; /** * Holding the available defaults in the XML * * @var array */ private $defaults = []; /** * Store the parsed SimpleXMLElement for usage * * @var SimpleXMLElement */ private $xml; /** * Memorize if we already parsed the XML * * @var bool */ private $isparsed = false; /** * Constructor * * Initialize the XML - ConfigParser * * @param string $filename * filename of the froxlor - configurationfile * @return void */ public function __construct($filename) { if (!is_readable($filename)) { throw new Exception('File not readable'); } $this->xml = simplexml_load_file($filename); if ($this->xml === false) { $error = ''; foreach (libxml_get_errors() as $error) { $error .= "\t" . $error->message; } throw new Exception($error); } // Let's see if we can find a block in the XML $distribution = $this->xml->xpath('//distribution'); // No distribution found - can't use this file if (!is_array($distribution)) { throw new Exception('Invalid XML, no distribution found'); } // Search for attributes we understand foreach ($distribution[0]->attributes() as $key => $value) { switch ((string)$key) { case "name": $this->distributionName = (string)$value; break; case "version": $this->distributionVersion = (string)$value; break; case "codename": $this->distributionCodename = (string)$value; break; case "defaulteditor": $this->distributionEditor = (string)$value; break; case "deprecated": (string)$value == 'true' ? $this->deprecated = true : $this->deprecated = false; break; } } } /** * Return all services defined by the XML * * The array will hold ConfigService - Objects for further handling * * @return array */ public function getServices() { // Let's parse this shit(!) $this->parseServices(); // Return our carefully searched for services return $this->services; } /** * Parse the XML and populate $this->services * * @return bool */ private function parseServices() { // We only want to parse the stuff one time if ($this->isparsed == true) { return true; } // Get all services $services = $this->xml->xpath('//services/service'); foreach ($services as $service) { // We don't want comments if ($service->getName() == 'comment') { continue; } // Search the attributes for "type" foreach ($service->attributes() as $key => $value) { if ($key == 'type') { $this->services[(string)$value] = new ConfigService($this->xml, '//services/service[@type="' . (string)$value . '"]'); } } } // Switch flag to indicate we parsed our data $this->isparsed = true; return true; } /** * Return all defaults defined by the XML * * The array will hold ConfigDefaults - Objects for further handling * * @return array */ public function getDefaults() { // Let's parse this shit(!) $this->parseDefaults(); // Return our carefully searched for defaults return $this->defaults; } /** * Parse the XML and populate $this->services * * @return bool */ private function parseDefaults() { // We only want to parse the stuff one time if ($this->isparsed == true) { return true; } // Get all defaults $defaults = $this->xml->xpath('//defaults/default'); foreach ($defaults as $default) { $this->defaults[] = $default; } // Switch flag to indicate we parsed our data $this->isparsed = true; return true; } /** * return complete distribution string "Name [codename] [ (version)] [deprecated] * * @return string */ public function getCompleteDistroName(): string { // get distro-info $dist_display = $this->distributionName; if ($this->distributionCodename != '') { $dist_display .= " " . $this->distributionCodename; } if ($this->distributionVersion != '') { $dist_display .= " (" . $this->distributionVersion . ")"; } if ($this->deprecated) { $dist_display .= " [deprecated]"; } return $dist_display; } } ================================================ FILE: lib/Froxlor/Config/ConfigService.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Config; use Exception; use Froxlor\Settings; use SimpleXMLElement; /** * Class ConfigService * * Parses a distributions XML - file and gives access to the services within * Not to be used directly */ class ConfigService { /** * Human - readable title of this service * * @var string */ public $title; /** * Holding the available daemons in this service * * @var array */ private $daemons = []; /** * Store the parsed SimpleXMLElement for usage * * @var SimpleXMLElement */ private $fullxml; /** * Memorize if we already parsed the XML * * @var bool */ private $isparsed = false; /** * xpath leading to this service in the full XML * * @var string */ private $xpath; public function __construct($xml, $xpath) { $this->fullxml = $xml; $this->xpath = $xpath; $service = $this->fullxml->xpath($this->xpath); $attributes = $service[0]->attributes(); if ($attributes['title'] != '') { $this->title = $this->parseContent((string)$attributes['title']); } } /** * Replace placeholders with content * * @param string $content * @return string $content w/o placeholder */ private function parseContent($content) { $content = preg_replace_callback('/\{\{(.*)\}\}/Ui', function ($matches) { $match = null; if (preg_match('/^settings\.(.*)$/', $matches[1], $match)) { return Settings::Get($match[1]); } elseif (preg_match('/^lng\.(.*)(?:\.(.*)(?:\.(.*)))$/U', $matches[1], $match)) { if (isset($match[1]) && $match[1] != '' && isset($match[2]) && $match[2] != '' && isset($match[3]) && $match[3] != '') { return lng($match[1] . '.' . $match[2] . '.' . $match[3]); } elseif (isset($match[1]) && $match[1] != '' && isset($match[2]) && $match[2] != '') { return lng($match[1] . '.' . $match[2]); } elseif (isset($match[1]) && $match[1] != '') { return lng($match[1]); } return ''; } }, $content); return $content; } public function getDaemons() { $this->parse(); return $this->daemons; } /** * Parse the XML and populate $this->daemons * * @return bool */ private function parse() { // We only want to parse the stuff one time if ($this->isparsed == true) { return true; } $daemons = $this->fullxml->xpath($this->xpath . '/daemon'); foreach ($daemons as $daemon) { if ($daemon->getName() == 'comment') { continue; } $name = ''; $nametag = ''; $versiontag = ''; foreach ($daemon->attributes() as $key => $value) { if ($key == 'name' && $name == '') { $name = (string)$value; $nametag = "[@name='" . $value . "']"; } elseif ($key == 'name' && $name != '') { $name = (string)$value . '_' . $name; $nametag = "[@name='" . $value . "']"; } elseif ($key == 'version' && $name == '') { $name = str_replace('.', '', $value); $versiontag = "[@version='" . $value . "']"; } elseif ($key == 'version' && $name != '') { $name .= str_replace('.', '', $value); $versiontag = "[@version='" . $value . "']"; } } if ($name == '') { throw new Exception('No name attribute for daemon'); } $this->daemons[$name] = new ConfigDaemon($this->fullxml, $this->xpath . "/daemon" . $nametag . $versiontag); } // Switch flag to indicate we parsed our data $this->isparsed = true; return true; } } ================================================ FILE: lib/Froxlor/Config/index.html ================================================ ================================================ FILE: lib/Froxlor/Cron/CronConfig.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cron; use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\Froxlor; use Froxlor\Settings; use PDO; class CronConfig { /** * 1st: check for task of generation * 2nd: if task found, generate cron.d-file * 3rd: maybe restart cron? */ public static function checkCrondConfigurationFile() { // check for task Database::query(" SELECT * FROM `" . TABLE_PANEL_TASKS . "` WHERE `type` = '99' "); $num_results = Database::num_rows(); // is there a task for re-generating the cron.d-file? if ($num_results > 0) { // get all crons and their intervals if (FileDir::isFreeBSD()) { // FreeBSD does not need a header as we are writing directly to the crontab $cronfile = "\n"; } else { $cronfile = "# automatically generated cron-configuration by froxlor\n"; $cronfile .= "# do not manually edit this file as it will be re-generated periodically.\n"; $cronfile .= "PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin\n#\n"; } // get all the crons $result_stmt = Database::query(" SELECT * FROM `" . TABLE_PANEL_CRONRUNS . "` WHERE `isactive` = '1' "); $binpath = Settings::Get("system.croncmdline"); // fallback as it is important if ($binpath === null) { $binpath = "/usr/bin/nice -n 5 /usr/bin/php -q"; } $hour_delay = 0; $day_delay = 5; $month_delay = 7; while ($row_cronentry = $result_stmt->fetch(PDO::FETCH_ASSOC)) { // create cron.d-entry $matches = []; if (preg_match("/(\d+) (MINUTE|HOUR|DAY|WEEK|MONTH)/", $row_cronentry['interval'], $matches)) { if ($matches[1] == 1) { $minvalue = "*"; } else { $minvalue = "*/" . $matches[1]; } switch ($matches[2]) { case "MINUTE": $cronfile .= $minvalue . " * * * * "; break; case "HOUR": $cronfile .= $hour_delay . " " . $minvalue . " * * * "; $hour_delay += 3; break; case "DAY": if ($row_cronentry['cronfile'] == 'traffic') { // traffic at exactly 0:00 o'clock $cronfile .= "0 0 " . $minvalue . " * * "; } else { $cronfile .= $day_delay . " 0 " . $minvalue . " * * "; $day_delay += 5; } break; case "MONTH": $cronfile .= $month_delay . " 0 1 " . $minvalue . " * "; $month_delay += 7; break; case "WEEK": $cronfile .= $day_delay . " 0 " . ($matches[1] * 7) . " * * "; $day_delay += 5; break; } // create entry-line $cronfile .= "root " . $binpath . " " . FileDir::makeCorrectFile(Froxlor::getInstallDir() . "/bin/froxlor-cli") . " froxlor:cron " . escapeshellarg($row_cronentry['cronfile']) . " -q 1> /dev/null\n"; } } // php sessionclean if enabled if ((int)Settings::Get('phpfpm.enabled') == 1) { $cronfile .= "# Look for and purge old sessions every 30 minutes" . PHP_EOL; $cronfile .= "09,39 * * * * root " . $binpath . " " . FileDir::makeCorrectFile(Froxlor::getInstallDir() . "/bin/froxlor-cli") . " froxlor:php-sessionclean 1> /dev/null" . PHP_EOL; } if (FileDir::isFreeBSD()) { // FreeBSD handles the cron-stuff in another way. We need to directly // write to the crontab file as there is not cron.d/froxlor file // (settings for system.cronconfig should be set correctly of course) $crontab = file_get_contents(Settings::Get("system.cronconfig")); if ($crontab === false) { die("Oh snap, we cannot read the crontab file. This should not happen.\nPlease check the path and permissions, the cron will keep trying if you don't stop the cron-service.\n\n"); } // now parse out / replace our entries $crontablines = explode("\n", $crontab); $newcrontab = ""; foreach ($crontablines as $ctl) { $ctl = trim($ctl); if (!empty($ctl) && !preg_match("/(.*)froxlor\:cron(.*)/", $ctl)) { $newcrontab .= $ctl . "\n"; } } // re-assemble old-content + new froxlor-content $newcrontab .= $cronfile; // now continue with writing the file $cronfile = $newcrontab; } // write the file if (file_put_contents(Settings::Get("system.cronconfig"), $cronfile) === false) { // oh snap cannot create new crond-file die("Oh snap, we cannot create the cron-file. This should not happen.\nPlease check the path and permissions, the cron will keep trying if you don't stop the cron-service.\n\n"); } // correct permissions chmod(Settings::Get("system.cronconfig"), 0640); // remove all re-generation tasks Database::query("DELETE FROM `" . TABLE_PANEL_TASKS . "` WHERE `type` = '99'"); // run reload command FileDir::safe_exec(escapeshellcmd(Settings::Get('system.crondreload'))); } return true; } } ================================================ FILE: lib/Froxlor/Cron/Dns/Bind.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cron\Dns; use Froxlor\Dns\Dns; use Froxlor\FileDir; use Froxlor\FroxlorLogger; use Froxlor\Settings; use Froxlor\Validate\Validate; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; class Bind extends DnsBase { private $bindconf_file = ""; public function writeConfigs() { // tell the world what we are doing $this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'Task4 started - Rebuilding froxlor_bind.conf'); // clean up $this->cleanZonefiles(); // check for subfolder in bind-config-directory if (!file_exists(FileDir::makeCorrectDir(Settings::Get('system.bindconf_directory') . '/domains/'))) { $this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'mkdir ' . escapeshellarg(FileDir::makeCorrectDir(Settings::Get('system.bindconf_directory') . '/domains/'))); FileDir::safe_exec('mkdir -p ' . escapeshellarg(FileDir::makeCorrectDir(Settings::Get('system.bindconf_directory') . '/domains/'))); } $domains = $this->getDomainList(); if (empty($domains)) { $this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'No domains found for nameserver-config, not creating any zones...'); $this->bindconf_file = ''; } else { $this->bindconf_file = '# ' . Settings::Get('system.bindconf_directory') . 'froxlor_bind.conf' . "\n" . '# Created ' . date('d.m.Y H:i') . "\n" . '# Do NOT manually edit this file, all changes will be deleted after the next domain change at the panel.' . "\n\n"; foreach ($domains as $domain) { if ($domain['is_child']) { // domains that are subdomains to other main domains are handled by recursion within walkDomainList() continue; } $this->walkDomainList($domain, $domains); } } $bindconf_file_handler = fopen(FileDir::makeCorrectFile(Settings::Get('system.bindconf_directory') . '/froxlor_bind.conf'), 'w'); fwrite($bindconf_file_handler, $this->bindconf_file); fclose($bindconf_file_handler); $this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'froxlor_bind.conf written'); $this->reloadDaemon(); $this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'Task4 finished'); } private function cleanZonefiles() { $config_dir = FileDir::makeCorrectFile(Settings::Get('system.bindconf_directory') . '/domains/'); $this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'Cleaning dns zone files from ' . $config_dir); // check directory if (@is_dir($config_dir)) { // create directory iterator $its = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($config_dir)); // iterate through all subdirs, look for zone files and delete them foreach ($its as $it) { if ($it->isFile()) { // remove file FileDir::safe_exec('rm -f ' . escapeshellarg(FileDir::makeCorrectFile($its->getPathname()))); } } } } private function walkDomainList($domain, $domains) { $zoneContent = ''; $subzones = ''; foreach ($domain['children'] as $child_domain_id) { $subzones .= $this->walkDomainList($domains[$child_domain_id], $domains); } if ($domain['zonefile'] == '') { // check for system-hostname $isFroxlorHostname = false; if (isset($domain['froxlorhost']) && $domain['froxlorhost'] == 1) { $isFroxlorHostname = true; } if (!$domain['is_child']) { $zoneContent = (string)Dns::createDomainZone(($domain['id'] == 'none') ? $domain : $domain['id'], $isFroxlorHostname); $domain['zonefile'] = 'domains/' . $domain['domain'] . '.zone'; $zonefile_name = FileDir::makeCorrectFile(Settings::Get('system.bindconf_directory') . '/' . $domain['zonefile']); $zonefile_handler = fopen($zonefile_name, 'w'); fwrite($zonefile_handler, $zoneContent . $subzones); fclose($zonefile_handler); $this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, '`' . $zonefile_name . '` written'); $this->bindconf_file .= $this->generateDomainConfig($domain); } else { return (string)Dns::createDomainZone(($domain['id'] == 'none') ? $domain : $domain['id'], $isFroxlorHostname, true); } } else { $this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'Added zonefile ' . $domain['zonefile'] . ' for domain ' . $domain['domain'] . ' - Note that you will also have to handle ALL records for ALL subdomains.'); $this->bindconf_file .= $this->generateDomainConfig($domain); } } private function generateDomainConfig($domain = []) { $this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'Generating dns config for ' . $domain['domain']); $bindconf_file = '# Domain ID: ' . $domain['id'] . ' - CustomerID: ' . $domain['customerid'] . ' - CustomerLogin: ' . $domain['loginname'] . "\n"; $bindconf_file .= 'zone "' . $domain['domain'] . '" in {' . "\n"; $bindconf_file .= ' type master;' . "\n"; $bindconf_file .= ' file "' . FileDir::makeCorrectFile(Settings::Get('system.bindconf_directory') . '/' . $domain['zonefile']) . '";' . "\n"; $bindconf_file .= ' allow-query { any; };' . "\n"; if (count($this->ns) > 0 || count($this->axfr) > 0) { // open allow-transfer $bindconf_file .= ' allow-transfer {' . "\n"; // put nameservers in allow-transfer if (count($this->ns) > 0) { foreach ($this->ns as $ns) { foreach ($ns["ips"] as $ip) { $ip = Validate::validate_ip2($ip, true, 'invalidip', true, true, true); if ($ip) { $bindconf_file .= ' ' . $ip . ";\n"; } } } } // AXFR server #100 if (count($this->axfr) > 0) { foreach ($this->axfr as $axfrserver) { $bindconf_file .= ' ' . $axfrserver . ';' . "\n"; } } // close allow-transfer $bindconf_file .= ' };' . "\n"; } $bindconf_file .= '};' . "\n"; $bindconf_file .= "\n"; return $bindconf_file; } } ================================================ FILE: lib/Froxlor/Cron/Dns/DnsBase.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cron\Dns; use Froxlor\Database\Database; use Froxlor\Domain\Domain; use Froxlor\FileDir; use Froxlor\FroxlorLogger; use Froxlor\PhpHelper; use Froxlor\Settings; use PDO; /** * Class DnsBase * * Base class for all DNS server configs */ abstract class DnsBase { protected $logger = false; protected $ns = []; protected $mx = []; protected $axfr = []; public function __construct($logger) { $this->logger = $logger; $known_ns_ips = []; if (Settings::Get('system.nameservers') != '') { $nameservers = explode(',', Settings::Get('system.nameservers')); foreach ($nameservers as $nameserver) { $nameserver = trim($nameserver); // DNS servers might be multi homed; allow transfer from all ip // addresses of the DNS server $nameserver_ips = PhpHelper::gethostbynamel6($nameserver); // append dot to hostname if (substr($nameserver, -1, 1) != '.') { $nameserver .= '.'; } // ignore invalid responses if (!is_array($nameserver_ips)) { // act like \Froxlor\PhpHelper::gethostbynamel6() and return unmodified hostname on error $nameserver_ips = [ $nameserver ]; } else { $known_ns_ips = array_merge($known_ns_ips, $nameserver_ips); } $this->ns[] = [ 'hostname' => $nameserver, 'ips' => $nameserver_ips ]; } } if (Settings::Get('system.mxservers') != '') { $mxservers = explode(',', Settings::Get('system.mxservers')); foreach ($mxservers as $mxserver) { if (substr($mxserver, -1, 1) != '.') { $mxserver .= '.'; } $this->mx[] = $mxserver; } } // AXFR server #100 if (Settings::Get('system.axfrservers') != '') { $axfrservers = explode(',', Settings::Get('system.axfrservers')); foreach ($axfrservers as $axfrserver) { if (!in_array(trim($axfrserver), $known_ns_ips)) { $this->axfr[] = trim($axfrserver); } } } } abstract public function writeConfigs(); public function reloadDaemon() { // reload DNS daemon $cmd = Settings::Get('system.bindreload_command'); $cmdStatus = 1; FileDir::safe_exec(escapeshellcmd($cmd), $cmdStatus); if ($cmdStatus === 0) { $this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, Settings::Get('system.dns_server') . ' daemon reloaded'); } else { $this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, 'Error while running `' . $cmd . '`: exit code (' . $cmdStatus . ') - please check your system logs'); } } protected function getDomainList() { $result_domains_stmt = Database::query(" SELECT `d`.`id`, `d`.`domain`, `d`.`isemaildomain`, `d`.`iswildcarddomain`, `d`.`wwwserveralias`, `d`.`customerid`, `d`.`zonefile`, `d`.`bindserial`, `d`.`dkim`, `d`.`dkim_id`, `d`.`dkim_pubkey`, `c`.`loginname`, `c`.`guid` FROM `" . TABLE_PANEL_DOMAINS . "` `d` LEFT JOIN `" . TABLE_PANEL_CUSTOMERS . "` `c` USING(`customerid`) WHERE `d`.`isbinddomain` = '1' ORDER BY LENGTH(`d`.`domain`), `d`.`domain` ASC "); $domains = []; // don't use fetchall() to be able to set the first column to the domain id and use it later on to set the rows' // array of direct children without having to search the outer array while ($domain = $result_domains_stmt->fetch(PDO::FETCH_ASSOC)) { $domains[$domain["id"]] = $domain; } // frolxor-hostname (#1090) if (Settings::get('system.dns_createhostnameentry') == 1) { $hostname_arr = [ 'id' => 'none', 'domain' => Settings::Get('system.hostname'), 'isbinddomain' => '1', 'isemaildomain' => Settings::Get('system.dns_createmailentry'), 'email_only' => '0', 'customerid' => 'none', 'loginname' => 'froxlor.panel', 'bindserial' => date('Ymd') . '00', 'dkim' => '0', 'iswildcarddomain' => '1', 'zonefile' => '', 'froxlorhost' => '1' ]; $domains[0] = $hostname_arr; } if (empty($domains)) { return null; } // collect domain IDs of direct child domains as arrays in ['children'] column foreach (array_keys($domains) as $key) { if (!isset($domains[$key]['children'])) { $domains[$key]['children'] = []; } if (!isset($domains[$key]['is_child'])) { $domains[$key]['is_child'] = false; } $children = Domain::getMainSubdomainIds($key); if (count($children) > 0) { foreach ($children as $child) { if (isset($domains[$child])) { $domains[$key]['children'][] = $domains[$child]['id']; $domains[$child]['is_child'] = true; } } } } $this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, str_pad('domId', 9, ' ') . str_pad('domain', 40, ' ') . "list of child domain ids"); foreach ($domains as $domain) { $logLine = str_pad($domain['id'], 9, ' ') . str_pad($domain['domain'], 40, ' ') . join(', ', $domain['children']); $this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, $logLine); } return $domains; } } ================================================ FILE: lib/Froxlor/Cron/Dns/PowerDNS.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cron\Dns; use Froxlor\Dns\Dns; use Froxlor\Dns\DnsZone; use Froxlor\FroxlorLogger; use Froxlor\Settings; use PDO; class PowerDNS extends DnsBase { public function writeConfigs() { // tell the world what we are doing $this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'Task4 started - Refreshing DNS database'); $domains = $this->getDomainList(); // clean up $this->clearZoneTables($domains); if (empty($domains)) { $this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'No domains found for nameserver-config, not creating any zones...'); } else { foreach ($domains as $domain) { if ($domain['is_child']) { // domains that are subdomains to other main domains are handled by recursion within walkDomainList() continue; } $this->walkDomainList($domain, $domains); } } $this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'PowerDNS database updated'); $this->reloadDaemon(); $this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'Task4 finished'); } private function clearZoneTables($domains = null) { $this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'Cleaning dns zone entries from database'); $pdns_domains_stmt = \Froxlor\Dns\PowerDNS::getDB()->prepare("SELECT `id`, `name` FROM `domains` WHERE `name` = :domain"); $del_rec_stmt = \Froxlor\Dns\PowerDNS::getDB()->prepare("DELETE FROM `records` WHERE `domain_id` = :did"); $del_meta_stmt = \Froxlor\Dns\PowerDNS::getDB()->prepare("DELETE FROM `domainmetadata` WHERE `domain_id` = :did"); $del_dom_stmt = \Froxlor\Dns\PowerDNS::getDB()->prepare("DELETE FROM `domains` WHERE `id` = :did"); foreach ($domains as $domain) { $pdns_domains_stmt->execute([ 'domain' => $domain['domain'] ]); $pdns_domain = $pdns_domains_stmt->fetch(PDO::FETCH_ASSOC); if ($pdns_domain && !empty($pdns_domain['id'])) { $del_rec_stmt->execute([ 'did' => $pdns_domain['id'] ]); $del_meta_stmt->execute([ 'did' => $pdns_domain['id'] ]); $del_dom_stmt->execute([ 'did' => $pdns_domain['id'] ]); } } } private function walkDomainList($domain, $domains) { $zoneContent = ''; $subzones = []; foreach ($domain['children'] as $child_domain_id) { $subzones[] = $this->walkDomainList($domains[$child_domain_id], $domains); } if ($domain['zonefile'] == '') { // check for system-hostname $isFroxlorHostname = false; if (isset($domain['froxlorhost']) && $domain['froxlorhost'] == 1) { $isFroxlorHostname = true; } if (!$domain['is_child']) { $zoneContent = Dns::createDomainZone(($domain['id'] == 'none') ? $domain : $domain['id'], $isFroxlorHostname); if (count($subzones)) { foreach ($subzones as $subzone) { $zoneContent->records[] = $subzone; } } $pdnsDomId = $this->insertZone($zoneContent->origin, $zoneContent->serial); $this->insertRecords($pdnsDomId, $zoneContent->records, $zoneContent->origin); $this->insertAllowedTransfers($pdnsDomId); $this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'DB entries stored for zone `' . $domain['domain'] . '`'); } else { return Dns::createDomainZone(($domain['id'] == 'none') ? $domain : $domain['id'], $isFroxlorHostname, true); } } else { $this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, 'Custom zonefiles are NOT supported when PowerDNS is selected as DNS daemon (triggered by: ' . $domain['domain'] . ')'); } } private function insertZone($domainname, $serial = 0) { $ins_stmt = \Froxlor\Dns\PowerDNS::getDB()->prepare(" INSERT INTO domains set `name` = :domainname, `notified_serial` = :serial, `type` = :type "); $ins_stmt->execute([ 'domainname' => $domainname, 'serial' => $serial, 'type' => strtoupper(Settings::Get('system.powerdns_mode')) ]); $lastid = \Froxlor\Dns\PowerDNS::getDB()->lastInsertId(); return $lastid; } private function insertRecords($domainid = 0, $records = [], $origin = "") { $ins_stmt = \Froxlor\Dns\PowerDNS::getDB()->prepare(" INSERT INTO records set `domain_id` = :did, `name` = :rec, `type` = :type, `content` = :content, `ttl` = :ttl, `prio` = :prio, `disabled` = '0' "); foreach ($records as $record) { if ($record instanceof DnsZone) { $this->insertRecords($domainid, $record->records, $record->origin); continue; } if ($record->record == '@') { $_record = $origin; } else { $_record = $record->record . "." . $origin; } $ins_data = [ 'did' => $domainid, 'rec' => $_record, 'type' => $record->type, 'content' => $record->content, 'ttl' => $record->ttl, 'prio' => $record->priority ]; $ins_stmt->execute($ins_data); } } private function insertAllowedTransfers($domainid) { $ins_stmt = \Froxlor\Dns\PowerDNS::getDB()->prepare(" INSERT INTO domainmetadata set `domain_id` = :did, `kind` = 'ALLOW-AXFR-FROM', `content` = :value "); $ins_data = [ 'did' => $domainid ]; if (count($this->ns) > 0 || count($this->axfr) > 0) { // put nameservers in allow-transfer if (count($this->ns) > 0) { foreach ($this->ns as $ns) { foreach ($ns["ips"] as $ip) { $ins_data['value'] = $ip; $ins_stmt->execute($ins_data); } } } // AXFR server #100 if (count($this->axfr) > 0) { foreach ($this->axfr as $axfrserver) { $ins_data['value'] = $axfrserver; $ins_stmt->execute($ins_data); } } } } } ================================================ FILE: lib/Froxlor/Cron/Dns/index.html ================================================ ================================================ FILE: lib/Froxlor/Cron/Forkable.php ================================================ = $concurrentChildren) { foreach ($childrenPids as $key => $pid) { $res = pcntl_waitpid($pid, $status, WNOHANG); // If the process has already exited if ($res == -1 || $res > 0) { unset($childrenPids[$key]); } } sleep(1); } } } while (pcntl_waitpid(0, $status) != -1); } else { if (!defined('CRON_NOFORK_FLAG')) { if (extension_loaded('pcntl')) { $msg = "PHP compiled with pcntl but pcntl_fork function is not available."; } else { $msg = "PHP compiled without pcntl."; } FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_WARNING, $msg . " Not forking " . self::class . ", this may take a long time!"); } foreach ($attributes as $closureAttributes) { $closure($closureAttributes); } } } } ================================================ FILE: lib/Froxlor/Cron/FroxlorCron.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cron; abstract class FroxlorCron { protected static $cronlog = null; protected static $lockfile = null; abstract public static function run(); public static function getLockfile() { return static::$lockfile; } public static function setLockfile($lockfile = null) { static::$lockfile = $lockfile; } public static function setCronlog($cronlog = null) { static::$cronlog = $cronlog; } } ================================================ FILE: lib/Froxlor/Cron/Http/Apache.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cron\Http; use Froxlor\Cron\Http\Php\PhpInterface; use Froxlor\Cron\TaskId; use Froxlor\Customer\Customer; use Froxlor\Database\Database; use Froxlor\Domain\Domain; use Froxlor\FileDir; use Froxlor\FroxlorLogger; use Froxlor\Http\Directory; use Froxlor\Http\Statistics; use Froxlor\PhpHelper; use Froxlor\Settings; use Froxlor\System\Cronjob; use Froxlor\System\Crypt; use Froxlor\Validate\Validate; use PDO; class Apache extends HttpConfigBase { // protected protected $known_diroptionsfilenames = []; protected $known_htpasswdsfilenames = []; protected $virtualhosts_data = []; protected $diroptions_data = []; protected $htpasswds_data = []; /** * indicator whether a customer is deactivated or not * if yes, only the webroot will be generated * * @var bool */ private $deactivated = false; public function createIpPort() { $result_ipsandports_stmt = Database::query("SELECT * FROM `" . TABLE_PANEL_IPSANDPORTS . "` ORDER BY `ip` ASC, `port` ASC"); while ($row_ipsandports = $result_ipsandports_stmt->fetch(PDO::FETCH_ASSOC)) { if (filter_var($row_ipsandports['ip'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $ipport = '[' . $row_ipsandports['ip'] . ']:' . $row_ipsandports['port']; } else { $ipport = $row_ipsandports['ip'] . ':' . $row_ipsandports['port']; } FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'apache::createIpPort: creating ip/port settings for ' . $ipport); $vhosts_filename = FileDir::makeCorrectFile(Settings::Get('system.apacheconf_vhost') . '/10_froxlor_ipandport_' . trim(str_replace(':', '.', $row_ipsandports['ip']), '.') . '.' . $row_ipsandports['port'] . '.conf'); if (!isset($this->virtualhosts_data[$vhosts_filename])) { $this->virtualhosts_data[$vhosts_filename] = ''; } if ($row_ipsandports['listen_statement'] == '1') { $this->virtualhosts_data[$vhosts_filename] .= 'Listen ' . $ipport . "\n"; FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, $ipport . ' :: inserted listen-statement'); } if ($row_ipsandports['namevirtualhost_statement'] == '1') { // >=apache-2.4 enabled? if (Settings::Get('system.apache24') == '1') { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, $ipport . ' :: namevirtualhost-statement no longer needed for apache-2.4'); } else { $this->virtualhosts_data[$vhosts_filename] .= 'NameVirtualHost ' . $ipport . "\n"; FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, $ipport . ' :: inserted namevirtualhost-statement'); } } if ($row_ipsandports['vhostcontainer'] == '1') { $without_vhost = $this->virtualhosts_data[$vhosts_filename]; $close_vhost = true; $this->virtualhosts_data[$vhosts_filename] .= '' . "\n"; $mypath = $this->getMyPath($row_ipsandports); $this->virtualhosts_data[$vhosts_filename] .= 'DocumentRoot "' . rtrim($mypath, "/") . '"' . "\n"; if ($row_ipsandports['vhostcontainer_servername_statement'] == '1') { $this->virtualhosts_data[$vhosts_filename] .= ' ServerName ' . Settings::Get('system.hostname') . "\n"; $froxlor_aliases = Settings::Get('system.froxloraliases'); if (!empty($froxlor_aliases)) { $froxlor_aliases = explode(",", $froxlor_aliases); $aliases = ""; foreach ($froxlor_aliases as $falias) { if (Validate::validateDomain(trim($falias))) { $aliases .= trim($falias) . " "; } } $aliases = trim($aliases); if (!empty($aliases)) { $this->virtualhosts_data[$vhosts_filename] .= ' ServerAlias ' . $aliases . "\n"; } } } $is_redirect = false; // check for SSL redirect if ($row_ipsandports['ssl'] == '0' && Settings::Get('system.le_froxlor_redirect') == '1') { $is_redirect = true; // check whether froxlor uses Let's Encrypt and not cert is being generated yet // or a renewal is ongoing - disable redirect if (Settings::Get('system.leenabled') == '1' && Settings::Get('system.le_froxlor_enabled') && ($this->froxlorVhostHasLetsEncryptCert() == false || $this->froxlorVhostLetsEncryptNeedsRenew())) { $this->virtualhosts_data[$vhosts_filename] .= '# temp. disabled ssl-redirect due to Let\'s Encrypt certificate generation.' . PHP_EOL; $is_redirect = false; Cronjob::inserttask(TaskId::REBUILD_VHOST); } else { $_sslport = $this->checkAlternativeSslPort(); $mypath = 'https://' . Settings::Get('system.hostname') . $_sslport . '/'; $code = '301'; $modrew_red = ' [R=' . $code . ';L,NE]'; // redirect everything, not only root-directory, #541 $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n"; $this->virtualhosts_data[$vhosts_filename] .= ' RewriteEngine On' . "\n"; $this->virtualhosts_data[$vhosts_filename] .= ' RewriteCond %{HTTPS} off' . "\n"; if (Settings::Get('system.leenabled') == '1' && Settings::Get('system.le_froxlor_enabled') == '1') { $this->virtualhosts_data[$vhosts_filename] .= ' RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge' . "\n"; } $this->virtualhosts_data[$vhosts_filename] .= ' RewriteRule ^/(.*) ' . $mypath . '$1' . $modrew_red . "\n"; $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n"; $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n"; $this->virtualhosts_data[$vhosts_filename] .= ' Redirect ' . $code . ' / ' . $mypath . "\n"; $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n"; } } if (!$is_redirect) { // protect lib/userdata.inc.php $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n"; $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n"; if (Settings::Get('system.apache24') == '1') { $this->virtualhosts_data[$vhosts_filename] .= ' Require all denied' . "\n"; } else { $this->virtualhosts_data[$vhosts_filename] .= ' Order deny,allow' . "\n"; $this->virtualhosts_data[$vhosts_filename] .= ' deny from all' . "\n"; } $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n"; $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n"; // protect bin/ $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n"; if (Settings::Get('system.apache24') == '1') { $this->virtualhosts_data[$vhosts_filename] .= ' Require all denied' . "\n"; } else { $this->virtualhosts_data[$vhosts_filename] .= ' Order deny,allow' . "\n"; $this->virtualhosts_data[$vhosts_filename] .= ' deny from all' . "\n"; } $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n"; // create fcgid -Part (starter is created in apache_fcgid) if (Settings::Get('system.mod_fcgid_ownvhost') == '1' && Settings::Get('system.mod_fcgid') == '1') { $configdir = FileDir::makeCorrectDir(Settings::Get('system.mod_fcgid_configdir') . '/froxlor.panel/' . Settings::Get('system.hostname')); $this->virtualhosts_data[$vhosts_filename] .= ' FcgidIdleTimeout ' . Settings::Get('system.mod_fcgid_idle_timeout') . "\n"; if ((int)Settings::Get('system.mod_fcgid_wrapper') == 0) { $this->virtualhosts_data[$vhosts_filename] .= ' SuexecUserGroup "' . Settings::Get('system.mod_fcgid_httpuser') . '" "' . Settings::Get('system.mod_fcgid_httpgroup') . '"' . "\n"; $this->virtualhosts_data[$vhosts_filename] .= ' ScriptAlias /php/ ' . $configdir . "\n"; } else { $domain = [ 'id' => 'none', 'domain' => Settings::Get('system.hostname'), 'adminid' => 1, /* first admin-user (superadmin) */ 'mod_fcgid_starter' => -1, 'mod_fcgid_maxrequests' => -1, 'guid' => Settings::Get('system.mod_fcgid_httpuser'), 'openbasedir' => 0, 'email' => Settings::Get('panel.adminmail'), 'loginname' => 'froxlor.panel', 'documentroot' => $mypath, 'customerroot' => $mypath ]; $php = new PhpInterface($domain); $phpconfig = $php->getPhpConfig(Settings::Get('system.mod_fcgid_defaultini_ownvhost')); if ($phpconfig['pass_authorizationheader'] == '1') { $this->virtualhosts_data[$vhosts_filename] .= ' FcgidPassHeader Authorization' . "\n"; } $starter_filename = FileDir::makeCorrectFile($configdir . '/php-fcgi-starter'); $this->virtualhosts_data[$vhosts_filename] .= ' SuexecUserGroup "' . Settings::Get('system.mod_fcgid_httpuser') . '" "' . Settings::Get('system.mod_fcgid_httpgroup') . '"' . "\n"; $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n"; $file_extensions = explode(' ', $phpconfig['file_extensions']); $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n"; $this->virtualhosts_data[$vhosts_filename] .= ' SetHandler fcgid-script' . "\n"; foreach ($file_extensions as $file_extension) { $this->virtualhosts_data[$vhosts_filename] .= ' FcgidWrapper ' . $starter_filename . ' .' . $file_extension . "\n"; } $this->virtualhosts_data[$vhosts_filename] .= ' Options +ExecCGI' . "\n"; $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n"; // >=apache-2.4 enabled? if (Settings::Get('system.apache24') == '1') { $mypath_dir = new Directory($mypath); // only create the require all granted if there is not active directory-protection // for this path, as this would be the first require and therefore grant all access if ($mypath_dir->isUserProtected() == false) { $this->virtualhosts_data[$vhosts_filename] .= ' Require all granted' . "\n"; $this->virtualhosts_data[$vhosts_filename] .= ' AllowOverride All' . "\n"; } } else { $this->virtualhosts_data[$vhosts_filename] .= ' Order allow,deny' . "\n"; $this->virtualhosts_data[$vhosts_filename] .= ' allow from all' . "\n"; } $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n"; } } elseif (Settings::Get('phpfpm.enabled') == '1' && (int)Settings::Get('phpfpm.enabled_ownvhost') == 1) { // get fpm config $fpm_sel_stmt = Database::prepare(" SELECT f.id FROM `" . TABLE_PANEL_FPMDAEMONS . "` f LEFT JOIN `" . TABLE_PANEL_PHPCONFIGS . "` p ON p.fpmsettingid = f.id WHERE p.id = :phpconfigid "); $fpm_config = Database::pexecute_first($fpm_sel_stmt, [ 'phpconfigid' => Settings::Get('phpfpm.vhost_defaultini') ]); // create php-fpm -Part (config is created in apache_fcgid) $domain = [ 'id' => 'none', 'domain' => Settings::Get('system.hostname'), 'adminid' => 1, /* first admin-user (superadmin) */ 'mod_fcgid_starter' => -1, 'mod_fcgid_maxrequests' => -1, 'guid' => Settings::Get('phpfpm.vhost_httpuser'), 'openbasedir' => 0, 'email' => Settings::Get('panel.adminmail'), 'loginname' => 'froxlor.panel', 'documentroot' => $mypath, 'customerroot' => $mypath, 'fpm_config_id' => isset($fpm_config['id']) ? $fpm_config['id'] : 1 ]; $php = new phpinterface($domain); $phpconfig = $php->getPhpConfig(Settings::Get('phpfpm.vhost_defaultini')); $srvName = substr(md5($ipport), 0, 4) . '.fpm.external'; if ($row_ipsandports['ssl']) { $srvName = substr(md5($ipport), 0, 4) . '.ssl-fpm.external'; } $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n"; // mod_proxy stuff for apache-2.4 if (Settings::Get('system.apache24') == '1' && Settings::Get('phpfpm.use_mod_proxy') == '1') { $filesmatch = $phpconfig['fpm_settings']['limit_extensions']; $extensions = explode(" ", $filesmatch); $filesmatch = ""; foreach ($extensions as $ext) { $filesmatch .= substr($ext, 1) . '|'; } // start block, cut off last pipe and close block $filesmatch = '(' . str_replace(".", "\.", substr($filesmatch, 0, -1)) . ')'; $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n"; $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n"; $this->virtualhosts_data[$vhosts_filename] .= ' SetHandler proxy:unix:' . $php->getInterface()->getSocketFile() . '|fcgi://localhost' . "\n"; $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n"; $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n"; if ($phpconfig['pass_authorizationheader'] == '1') { $this->virtualhosts_data[$vhosts_filename] .= ' CGIPassAuth On' . "\n"; } } else { $addheader = ""; if ($phpconfig['pass_authorizationheader'] == '1') { $addheader = " -pass-header Authorization"; } $this->virtualhosts_data[$vhosts_filename] .= ' FastCgiExternalServer ' . $php->getInterface()->getAliasConfigDir() . $srvName . ' -socket ' . $php->getInterface()->getSocketFile() . ' -idle-timeout ' . $phpconfig['fpm_settings']['idle_timeout'] . $addheader . "\n"; $filesmatch = $phpconfig['fpm_settings']['limit_extensions']; $extensions = explode(" ", $filesmatch); $filesmatch = ""; foreach ($extensions as $ext) { $filesmatch .= substr($ext, 1) . '|'; } // start block, cut off last pipe and close block $filesmatch = '(' . str_replace(".", "\.", substr($filesmatch, 0, -1)) . ')'; $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n"; $this->virtualhosts_data[$vhosts_filename] .= ' AddHandler php-fastcgi .php' . "\n"; $this->virtualhosts_data[$vhosts_filename] .= ' Action php-fastcgi /fastcgiphp' . "\n"; $this->virtualhosts_data[$vhosts_filename] .= ' Options +ExecCGI' . "\n"; $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n"; $this->virtualhosts_data[$vhosts_filename] .= ' Alias /fastcgiphp ' . $php->getInterface()->getAliasConfigDir() . $srvName . "\n"; } // >=apache-2.4 enabled? if (Settings::Get('system.apache24') == '1') { $this->virtualhosts_data[$vhosts_filename] .= ' Require all granted' . "\n"; $this->virtualhosts_data[$vhosts_filename] .= ' AllowOverride All' . "\n"; } else { $this->virtualhosts_data[$vhosts_filename] .= ' Order allow,deny' . "\n"; $this->virtualhosts_data[$vhosts_filename] .= ' allow from all' . "\n"; } $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n"; } else { // mod_php $domain = [ 'id' => 'none', 'domain' => Settings::Get('system.hostname'), 'adminid' => 1, /* first admin-user (superadmin) */ 'guid' => Settings::Get('system.httpuser'), 'openbasedir' => 0, 'email' => Settings::Get('panel.adminmail'), 'loginname' => 'froxlor.panel', 'documentroot' => $mypath, 'customerroot' => $mypath ]; } // end of ssl-redirect check } else { // fallback of froxlor domain-data for processSpecialConfigTemplate() $domain = [ 'domain' => Settings::Get('system.hostname'), 'loginname' => 'froxlor.panel', 'documentroot' => $mypath, 'customerroot' => $mypath ]; } /** * dirprotection, see #72 * * @todo deferred until 0.9.5, needs more testing * $this->virtualhosts_data[$vhosts_filename] .= "\t\n"; * $this->virtualhosts_data[$vhosts_filename] .= "\t\tAllow from all\n"; * $this->virtualhosts_data[$vhosts_filename] .= "\t\tOptions -Indexes\n"; * $this->virtualhosts_data[$vhosts_filename] .= "\t\n"; * * $this->virtualhosts_data[$vhosts_filename] .= "\t\n"; * $this->virtualhosts_data[$vhosts_filename] .= "\t\tOrder Deny,Allow\n"; * $this->virtualhosts_data[$vhosts_filename] .= "\t\tDeny from All\n"; * $this->virtualhosts_data[$vhosts_filename] .= "\t\n"; * end of dirprotection */ if ($row_ipsandports['specialsettings'] != '' && ($row_ipsandports['ssl'] == '0' || ($row_ipsandports['ssl'] == '1' && Settings::Get('system.use_ssl') == '1' && $row_ipsandports['include_specialsettings'] == '1'))) { $this->virtualhosts_data[$vhosts_filename] .= $this->processSpecialConfigTemplate($row_ipsandports['specialsettings'], $domain, $row_ipsandports['ip'], $row_ipsandports['port'], $row_ipsandports['ssl'] == '1') . "\n"; } if ($row_ipsandports['ssl'] == '1' && Settings::Get('system.use_ssl') == '1') { if ($row_ipsandports['ssl_specialsettings'] != '') { $this->virtualhosts_data[$vhosts_filename] .= $this->processSpecialConfigTemplate($row_ipsandports['ssl_specialsettings'], $domain, $row_ipsandports['ip'], $row_ipsandports['port'], $row_ipsandports['ssl'] == '1') . "\n"; } // check for required fallback if (($row_ipsandports['ssl_cert_file'] == '' || !file_exists($row_ipsandports['ssl_cert_file'])) && (Settings::Get('system.le_froxlor_enabled') == '0' || $this->froxlorVhostHasLetsEncryptCert() == false)) { $row_ipsandports['ssl_cert_file'] = Settings::Get('system.ssl_cert_file'); if (!file_exists($row_ipsandports['ssl_cert_file'])) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'System certificate file "' . Settings::Get('system.ssl_cert_file') . '" does not seem to exist. Creating self-signed certificate...'); Crypt::createSelfSignedCertificate(); } } if ($row_ipsandports['ssl_key_file'] == '') { $row_ipsandports['ssl_key_file'] = Settings::Get('system.ssl_key_file'); if (!file_exists($row_ipsandports['ssl_key_file'])) { // explicitly disable ssl for this vhost $row_ipsandports['ssl_cert_file'] = ""; FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'System certificate key-file "' . Settings::Get('system.ssl_key_file') . '" does not seem to exist. Disabling SSL-vhost for "' . Settings::Get('system.hostname') . '"'); } } if ($row_ipsandports['ssl_ca_file'] == '') { $row_ipsandports['ssl_ca_file'] = Settings::Get('system.ssl_ca_file'); } // #418 if ($row_ipsandports['ssl_cert_chainfile'] == '') { $row_ipsandports['ssl_cert_chainfile'] = Settings::Get('system.ssl_cert_chainfile'); } $domain = [ 'id' => 0, 'domain' => Settings::Get('system.hostname'), 'adminid' => 1, /* first admin-user (superadmin) */ 'loginname' => 'froxlor.panel', 'documentroot' => $mypath, 'customerroot' => $mypath, 'parentdomainid' => 0, 'ssl_honorcipherorder' => Settings::Get('system.honorcipherorder'), 'ssl_sessiontickets' => Settings::Get('system.sessiontickets') ]; // override corresponding array values $domain['ssl_cert_file'] = $row_ipsandports['ssl_cert_file']; $domain['ssl_key_file'] = $row_ipsandports['ssl_key_file']; $domain['ssl_ca_file'] = $row_ipsandports['ssl_ca_file']; $domain['ssl_cert_chainfile'] = $row_ipsandports['ssl_cert_chainfile']; // SSL STUFF $dssl = new DomainSSL(); // this sets the ssl-related array-indices in the $domain array // if the domain has customer-defined ssl-certificates $dssl->setDomainSSLFilesArray($domain); if ($domain['ssl_cert_file'] != '') { // check for existence, #1485 if (!file_exists($domain['ssl_cert_file'])) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, $ipport . ' :: certificate file "' . $domain['ssl_cert_file'] . '" does not exist! Cannot create ssl-directives'); } else { $this->virtualhosts_data[$vhosts_filename] .= ' SSLEngine On' . "\n"; $this->virtualhosts_data[$vhosts_filename] .= ' SSLProtocol -ALL +' . str_replace(",", " +", Settings::Get('system.ssl_protocols')) . "\n"; if (Settings::Get('system.apache24') == '1') { if (Settings::Get('system.http2_support') == '1') { $this->virtualhosts_data[$vhosts_filename] .= ' Protocols h2 http/1.1' . "\n"; } if (!empty(Settings::Get('system.dhparams_file'))) { $dhparams = FileDir::makeCorrectFile(Settings::Get('system.dhparams_file')); if (!file_exists($dhparams)) { file_put_contents($dhparams, self::FFDHE4096); } $this->virtualhosts_data[$vhosts_filename] .= ' SSLOpenSSLConfCmd DHParameters "' . $dhparams . '"' . "\n"; } $this->virtualhosts_data[$vhosts_filename] .= ' SSLCompression Off' . "\n"; if (Settings::Get('system.sessionticketsenabled') == '1') { $this->virtualhosts_data[$vhosts_filename] .= ' SSLSessionTickets ' . ($domain['ssl_sessiontickets'] == '1' ? 'on' : 'off') . "\n"; } } $this->virtualhosts_data[$vhosts_filename] .= ' SSLHonorCipherOrder ' . ($domain['ssl_honorcipherorder'] == '1' ? 'on' : 'off') . "\n"; $this->virtualhosts_data[$vhosts_filename] .= ' SSLCipherSuite ' . Settings::Get('system.ssl_cipher_list') . "\n"; $protocols = array_map('trim', explode(",", Settings::Get('system.ssl_protocols'))); if (in_array("TLSv1.3", $protocols) && !empty(Settings::Get('system.tlsv13_cipher_list')) && Settings::Get('system.apache24') == 1) { $this->virtualhosts_data[$vhosts_filename] .= ' SSLCipherSuite TLSv1.3 ' . Settings::Get('system.tlsv13_cipher_list') . "\n"; } $this->virtualhosts_data[$vhosts_filename] .= ' SSLVerifyDepth 10' . "\n"; $this->virtualhosts_data[$vhosts_filename] .= ' SSLCertificateFile ' . FileDir::makeCorrectFile($domain['ssl_cert_file']) . "\n"; if ($domain['ssl_key_file'] != '') { // check for existence, #1485 if (!file_exists($domain['ssl_key_file'])) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, $ipport . ' :: certificate key file "' . $domain['ssl_key_file'] . '" does not exist! Cannot create ssl-directives'); } else { $this->virtualhosts_data[$vhosts_filename] .= ' SSLCertificateKeyFile ' . FileDir::makeCorrectFile($domain['ssl_key_file']) . "\n"; } } if ($domain['ssl_ca_file'] != '') { // check for existence, #1485 if (!file_exists($domain['ssl_ca_file'])) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, $ipport . ' :: certificate CA file "' . $domain['ssl_ca_file'] . '" does not exist! Cannot create ssl-directives'); } else { $this->virtualhosts_data[$vhosts_filename] .= ' SSLCACertificateFile ' . FileDir::makeCorrectFile($domain['ssl_ca_file']) . "\n"; } } // #418 if ($domain['ssl_cert_chainfile'] != '') { // check for existence, #1485 if (!file_exists($domain['ssl_cert_chainfile'])) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, $ipport . ' :: certificate chain file "' . $domain['ssl_cert_chainfile'] . '" does not exist! Cannot create ssl-directives'); } else { $this->virtualhosts_data[$vhosts_filename] .= ' SSLCertificateChainFile ' . FileDir::makeCorrectFile($domain['ssl_cert_chainfile']) . "\n"; } } } } else { // if there is no cert-file specified but we are generating a ssl-vhost, // we should return an empty string because this vhost would suck dick, ref #1583 FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, $domain['domain'] . ' :: empty certificate file! Cannot create ssl-directives'); $this->virtualhosts_data[$vhosts_filename] = $without_vhost; $this->virtualhosts_data[$vhosts_filename] .= '# no ssl-certificate was specified for this domain, therefore no explicit vhost-container is being generated'; $close_vhost = false; } } if ($close_vhost) { $this->virtualhosts_data[$vhosts_filename] .= '' . "\n"; } FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, $ipport . ' :: inserted vhostcontainer'); } unset($vhosts_filename); } /** * bug #32 */ $this->createStandardDirectoryEntry(); /** * bug #unknown-yet */ $this->createStandardErrorHandler(); } /** * define a standard -statement, bug #32 */ private function createStandardDirectoryEntry() { $vhosts_filename = $this->getCustomVhostFilename('05_froxlor_dirfix_nofcgid.conf'); if (!isset($this->virtualhosts_data[$vhosts_filename])) { $this->virtualhosts_data[$vhosts_filename] = ''; } $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n"; // check for custom values, see #1638 $custom_opts = Settings::Get('system.apacheglobaldiropt'); if (!empty($custom_opts)) { $this->virtualhosts_data[$vhosts_filename] .= $custom_opts . "\n"; } else { // >=apache-2.4 enabled? if (Settings::Get('system.apache24') == '1') { $this->virtualhosts_data[$vhosts_filename] .= ' Require all granted' . "\n"; $this->virtualhosts_data[$vhosts_filename] .= ' AllowOverride All' . "\n"; } else { $this->virtualhosts_data[$vhosts_filename] .= ' Order allow,deny' . "\n"; $this->virtualhosts_data[$vhosts_filename] .= ' allow from all' . "\n"; } } $this->virtualhosts_data[$vhosts_filename] .= ' ' . "\n"; $ocsp_cache_filename = $this->getCustomVhostFilename('03_froxlor_ocsp_cache.conf'); if (Settings::Get('system.use_ssl') == '1' && Settings::Get('system.apache24') == 1) { $this->virtualhosts_data[$ocsp_cache_filename] = 'SSLStaplingCache ' . Settings::Get('system.apache24_ocsp_cache_path') . "\n"; } else { if (file_exists($ocsp_cache_filename)) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'apache::_createStandardDirectoryEntry: unlinking ' . basename($ocsp_cache_filename)); unlink(FileDir::makeCorrectFile($ocsp_cache_filename)); } } } /** * define a default ErrorDocument-statement, bug #unknown-yet */ private function createStandardErrorHandler() { if (Settings::Get('defaultwebsrverrhandler.enabled') == '1' && (Settings::Get('defaultwebsrverrhandler.err401') != '' || Settings::Get('defaultwebsrverrhandler.err403') != '' || Settings::Get('defaultwebsrverrhandler.err404') != '' || Settings::Get('defaultwebsrverrhandler.err500') != '')) { $vhosts_filename = $this->getCustomVhostFilename('05_froxlor_default_errorhandler.conf'); if (!isset($this->virtualhosts_data[$vhosts_filename])) { $this->virtualhosts_data[$vhosts_filename] = ''; } $statusCodes = [ '401', '403', '404', '500' ]; foreach ($statusCodes as $statusCode) { if (Settings::Get('defaultwebsrverrhandler.err' . $statusCode) != '') { $defhandler = Settings::Get('defaultwebsrverrhandler.err' . $statusCode); if (!Validate::validateUrl($defhandler)) { if (substr($defhandler, 0, 1) != '"' && substr($defhandler, -1, 1) != '"') { $defhandler = '"' . FileDir::makeCorrectFile($defhandler) . '"'; } } $this->virtualhosts_data[$vhosts_filename] .= 'ErrorDocument ' . $statusCode . ' ' . $defhandler . "\n"; } } } } public function createOwnVhostStarter() { return; } /** * We compose the virtualhost entries for the domains */ public function createVirtualHosts() { $domains = WebserverBase::getVhostsToCreate(); foreach ($domains as $domain) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'apache::createVirtualHosts: creating vhost container for domain ' . $domain['id'] . ', customer ' . $domain['loginname']); $vhosts_filename = $this->getVhostFilename($domain); // Apply header $this->virtualhosts_data[$vhosts_filename] = '# Domain ID: ' . $domain['id'] . ' - CustomerID: ' . $domain['customerid'] . ' - CustomerLogin: ' . $domain['loginname'] . "\n"; $ddr = Settings::Get('system.deactivateddocroot'); if (($domain['deactivated'] == '1' || $domain['customer_deactivated'] == '1') && empty($ddr)) { $this->virtualhosts_data[$vhosts_filename] .= '# Customer/domain deactivated and a docroot for deactivated users hasn\'t been set.' . "\n"; } else { // Create vhost without ssl $this->virtualhosts_data[$vhosts_filename] .= $this->getVhostContent($domain, false); if ($domain['ssl_enabled'] == '1' && ($domain['ssl'] == '1' || $domain['ssl_redirect'] == '1')) { // Adding ssl stuff if enabled $vhosts_filename_ssl = $this->getVhostFilename($domain, true); $this->virtualhosts_data[$vhosts_filename_ssl] = '# Domain ID: ' . $domain['id'] . ' (SSL) - CustomerID: ' . $domain['customerid'] . ' - CustomerLogin: ' . $domain['loginname'] . "\n"; $this->virtualhosts_data[$vhosts_filename_ssl] .= $this->getVhostContent($domain, true); } } } } /** * We compose the virtualhost entry for one domain */ protected function getVhostContent($domain, $ssl_vhost = false) { if ($ssl_vhost === true && ($domain['ssl_redirect'] != '1' && $domain['ssl'] != '1')) { return ''; } $query = "SELECT * FROM `" . TABLE_PANEL_IPSANDPORTS . "` `i`, `" . TABLE_DOMAINTOIP . "` `dip` WHERE dip.id_domain = :domainid AND i.id = dip.id_ipandports "; if ($ssl_vhost === true && ($domain['ssl'] == '1' || $domain['ssl_redirect'] == '1')) { // by ordering by cert-file the row with filled out SSL-Fields will be shown last, thus it is enough to fill out 1 set of SSL-Fields $query .= "AND i.ssl = '1' ORDER BY i.ssl_cert_file ASC;"; } else { $query .= "AND i.ssl = '0';"; } $vhost_content = ''; $result_stmt = Database::prepare($query); Database::pexecute($result_stmt, [ 'domainid' => $domain['id'] ]); $ipportlist = ''; $_vhost_content = ''; while ($ipandport = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $ipport = ''; $domain['ip'] = $ipandport['ip']; $domain['port'] = $ipandport['port']; if ($domain['ssl'] == '1') { $domain['ssl_cert_file'] = $ipandport['ssl_cert_file']; $domain['ssl_key_file'] = $ipandport['ssl_key_file']; $domain['ssl_ca_file'] = $ipandport['ssl_ca_file']; $domain['ssl_cert_chainfile'] = $ipandport['ssl_cert_chainfile']; // SSL STUFF $dssl = new DomainSSL(); // this sets the ssl-related array-indices in the $domain array // if the domain has customer-defined ssl-certificates $dssl->setDomainSSLFilesArray($domain); } if (filter_var($domain['ip'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $ipport = '[' . $domain['ip'] . ']:' . $domain['port'] . ' '; } else { $ipport = $domain['ip'] . ':' . $domain['port'] . ' '; } if ($ipandport['default_vhostconf_domain'] != '' && ($ssl_vhost == false || ($ssl_vhost == true && $ipandport['include_default_vhostconf_domain'] == '1'))) { $_vhost_content .= $this->processSpecialConfigTemplate($ipandport['default_vhostconf_domain'], $domain, $domain['ip'], $domain['port'], $ssl_vhost) . "\n"; } if ($ipandport['ssl_default_vhostconf_domain'] != '' && $ssl_vhost == true) { $_vhost_content .= $this->processSpecialConfigTemplate($ipandport['ssl_default_vhostconf_domain'], $domain, $domain['ip'], $domain['port'], $ssl_vhost) . "\n"; } $ipportlist .= $ipport; } $vhost_content .= '' . "\n"; $vhost_content .= $this->getServerNames($domain); $domain['documentroot_norewrite'] = $domain['documentroot']; if (($ssl_vhost == false && $domain['ssl'] == '1' && $domain['ssl_redirect'] == '1')) { // We must not check if our port differs from port 443, // but if there is a destination-port != 443 $_sslport = ''; // This returns the first port that is != 443 with ssl enabled, if any // ordered by ssl-certificate (if any) so that the ip/port combo // with certificate is used $ssldestport_stmt = Database::prepare(" SELECT `ip`.`port` FROM " . TABLE_PANEL_IPSANDPORTS . " `ip` LEFT JOIN `" . TABLE_DOMAINTOIP . "` `dip` ON (`ip`.`id` = `dip`.`id_ipandports`) WHERE `dip`.`id_domain` = :domainid AND `ip`.`ssl` = '1' AND `ip`.`port` != 443 ORDER BY `ip`.`ssl_cert_file` DESC, `ip`.`port` LIMIT 1; "); $ssldestport = Database::pexecute_first($ssldestport_stmt, [ 'domainid' => $domain['id'] ]); if ($ssldestport && $ssldestport['port'] != '') { $_sslport = ":" . $ssldestport['port']; } $domain['documentroot'] = 'https://%{HTTP_HOST}' . $_sslport . '/'; $domain['documentroot_norewrite'] = 'https://' . $domain['domain'] . $_sslport . '/'; } if ($ssl_vhost === true && $domain['ssl'] == '1' && Settings::Get('system.use_ssl') == '1') { if ($domain['ssl_cert_file'] == '' || !file_exists($domain['ssl_cert_file'])) { $domain['ssl_cert_file'] = Settings::Get('system.ssl_cert_file'); if (!file_exists($domain['ssl_cert_file'])) { // explicitly disable ssl for this vhost $domain['ssl_cert_file'] = ""; FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'System certificate file "' . Settings::Get('system.ssl_cert_file') . '" does not seem to exist. Disabling SSL-vhost for "' . $domain['domain'] . '"'); } } if ($domain['ssl_key_file'] == '' || !file_exists($domain['ssl_key_file'])) { $domain['ssl_key_file'] = Settings::Get('system.ssl_key_file'); if (!file_exists($domain['ssl_key_file'])) { // explicitly disable ssl for this vhost $domain['ssl_cert_file'] = ""; FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'System certificate key-file "' . Settings::Get('system.ssl_key_file') . '" does not seem to exist. Disabling SSL-vhost for "' . $domain['domain'] . '"'); } } if ($domain['ssl_ca_file'] == '') { $domain['ssl_ca_file'] = Settings::Get('system.ssl_ca_file'); } if ($domain['ssl_cert_chainfile'] == '') { $domain['ssl_cert_chainfile'] = Settings::Get('system.ssl_cert_chainfile'); } if ($domain['ssl_cert_file'] != '') { $ssl_protocols = ($domain['override_tls'] == '1' && !empty($domain['ssl_protocols'])) ? $domain['ssl_protocols'] : Settings::Get('system.ssl_protocols'); $ssl_cipher_list = ($domain['override_tls'] == '1' && !empty($domain['ssl_cipher_list'])) ? $domain['ssl_cipher_list'] : Settings::Get('system.ssl_cipher_list'); $tlsv13_cipher_list = ($domain['override_tls'] == '1' && !empty($domain['tlsv13_cipher_list'])) ? $domain['tlsv13_cipher_list'] : Settings::Get('system.tlsv13_cipher_list'); $vhost_content .= ' SSLEngine On' . "\n"; $vhost_content .= ' SSLProtocol -ALL +' . str_replace(",", " +", $ssl_protocols) . "\n"; if (Settings::Get('system.apache24') == '1') { if (isset($domain['http2']) && $domain['http2'] == '1' && Settings::Get('system.http2_support') == '1') { $vhost_content .= ' Protocols h2 http/1.1' . "\n"; } if (!empty(Settings::Get('system.dhparams_file'))) { $dhparams = FileDir::makeCorrectFile(Settings::Get('system.dhparams_file')); if (!file_exists($dhparams)) { file_put_contents($dhparams, self::FFDHE4096); } $vhost_content .= ' SSLOpenSSLConfCmd DHParameters "' . $dhparams . '"' . "\n"; } $vhost_content .= ' SSLCompression Off' . "\n"; if (Settings::Get('system.sessionticketsenabled') == '1') { $vhost_content .= ' SSLSessionTickets ' . ($domain['ssl_sessiontickets'] == '1' ? 'on' : 'off') . "\n"; } } $vhost_content .= ' SSLHonorCipherOrder ' . ($domain['ssl_honorcipherorder'] == '1' ? 'on' : 'off') . "\n"; $vhost_content .= ' SSLCipherSuite ' . $ssl_cipher_list . "\n"; $protocols = array_map('trim', explode(",", $ssl_protocols)); if (in_array("TLSv1.3", $protocols) && !empty($tlsv13_cipher_list) && Settings::Get('system.apache24') == 1) { $vhost_content .= ' SSLCipherSuite TLSv1.3 ' . $tlsv13_cipher_list . "\n"; } $vhost_content .= ' SSLVerifyDepth 10' . "\n"; $vhost_content .= ' SSLCertificateFile ' . FileDir::makeCorrectFile($domain['ssl_cert_file']) . "\n"; if ($domain['ssl_key_file'] != '') { $vhost_content .= ' SSLCertificateKeyFile ' . FileDir::makeCorrectFile($domain['ssl_key_file']) . "\n"; } if ($domain['ssl_ca_file'] != '') { $vhost_content .= ' SSLCACertificateFile ' . FileDir::makeCorrectFile($domain['ssl_ca_file']) . "\n"; } if ($domain['ssl_cert_chainfile'] != '') { $vhost_content .= ' SSLCertificateChainFile ' . FileDir::makeCorrectFile($domain['ssl_cert_chainfile']) . "\n"; } if (Settings::Get('system.apache24') == '1' && isset($domain['ocsp_stapling']) && $domain['ocsp_stapling'] == '1') { $vhost_content .= ' SSLUseStapling on' . PHP_EOL; } if ($domain['hsts'] >= 0) { $vhost_content .= ' ' . "\n"; $vhost_content .= ' Header always set Strict-Transport-Security "max-age=' . $domain['hsts']; if ($domain['hsts_sub'] == 1) { $vhost_content .= '; includeSubDomains'; } if ($domain['hsts_preload'] == 1) { $vhost_content .= '; preload'; } $vhost_content .= '"' . "\n"; $vhost_content .= ' ' . "\n"; } } else { // if there is no cert-file specified but we are generating a ssl-vhost, // we should return an empty string because this vhost would suck dick, ref #1583 FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, $domain['domain'] . ' :: empty certificate file! Cannot create ssl-directives'); return '# no ssl-certificate was specified for this domain, therefore no explicit vhost is being generated'; } } // avoid using any whitespaces $domain['documentroot'] = trim($domain['documentroot']); if (preg_match('/^https?\:\/\//', $domain['documentroot'])) { $possible_deactivated_webroot = $this->getWebroot($domain); if ($this->deactivated == false) { $corrected_docroot = $domain['documentroot']; // Get domain's redirect code $code = Domain::getDomainRedirectCode($domain['id']); $modrew_red = ''; if ($code != '') { $modrew_red = ' [R=' . $code . ';L,NE]'; } $vhost_content .= $this->getLogfiles($domain); // redirect everything, not only root-directory, #541 $vhost_content .= ' ' . "\n"; $vhost_content .= ' RewriteEngine On' . "\n"; if (!$ssl_vhost) { $vhost_content .= ' RewriteCond %{HTTPS} off' . "\n"; } if ($domain['letsencrypt'] == '1') { $vhost_content .= ' RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge' . "\n"; } $vhost_content .= ' RewriteRule ^/(.*) ' . $corrected_docroot . '$1' . $modrew_red . "\n"; $vhost_content .= ' ' . "\n"; $vhost_content .= ' ' . "\n"; $vhost_content .= ' Redirect ' . $code . ' / ' . $domain['documentroot_norewrite'] . "\n"; $vhost_content .= ' ' . "\n"; } elseif (Settings::Get('system.deactivateddocroot') != '') { $vhost_content .= $possible_deactivated_webroot; } } else { FileDir::mkDirWithCorrectOwnership($domain['customerroot'], $domain['documentroot'], $domain['guid'], $domain['guid'], true, true); $vhost_content .= $this->getWebroot($domain); if ($this->deactivated == false) { $vhost_content .= $this->composePhpOptions($domain, $ssl_vhost); $vhost_content .= $this->getStats($domain); } $vhost_content .= $this->getLogfiles($domain); if ($this->deactivated == false) { if ($domain['specialsettings'] != '' && ($ssl_vhost == false || ($ssl_vhost == true && $domain['include_specialsettings'] == 1))) { $vhost_content .= $this->processSpecialConfigTemplate($domain['specialsettings'], $domain, $domain['ip'], $domain['port'], $ssl_vhost) . "\n"; } if ($domain['ssl_specialsettings'] != '' && $ssl_vhost == true) { $vhost_content .= $this->processSpecialConfigTemplate($domain['ssl_specialsettings'], $domain, $domain['ip'], $domain['port'], $ssl_vhost) . "\n"; } if ($_vhost_content != '') { $vhost_content .= $_vhost_content; } if (Settings::Get('system.default_vhostconf') != '' && ($ssl_vhost == false || ($ssl_vhost == true && Settings::Get('system.include_default_vhostconf') == 1))) { $vhost_content .= $this->processSpecialConfigTemplate(Settings::Get('system.default_vhostconf'), $domain, $domain['ip'], $domain['port'], $ssl_vhost) . "\n"; } if (Settings::Get('system.default_sslvhostconf') != '' && $ssl_vhost == true) { $vhost_content .= $this->processSpecialConfigTemplate(Settings::Get('system.default_sslvhostconf'), $domain, $domain['ip'], $domain['port'], $ssl_vhost) . "\n"; } } } $vhost_content .= '' . "\n"; return $vhost_content; } /** * We collect all servernames and Aliases */ protected function getServerNames($domain) { $servernames_text = ' ServerName ' . $domain['domain'] . "\n"; $server_alias = ''; if ($domain['iswildcarddomain'] == '1') { $server_alias = '*.' . $domain['domain']; } elseif ($domain['wwwserveralias'] == '1') { $server_alias = 'www.' . $domain['domain']; } if (trim($server_alias) != '') { $servernames_text .= ' ServerAlias ' . $server_alias . "\n"; } $alias_domains_stmt = Database::prepare(" SELECT `domain`, `iswildcarddomain`, `wwwserveralias` FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `aliasdomain`= :domainid "); Database::pexecute($alias_domains_stmt, [ 'domainid' => $domain['id'] ]); while (($alias_domain = $alias_domains_stmt->fetch(PDO::FETCH_ASSOC)) !== false) { $server_alias = ' ServerAlias ' . $alias_domain['domain']; if ($alias_domain['iswildcarddomain'] == '1') { $server_alias .= ' *.' . $alias_domain['domain']; } else { if ($alias_domain['wwwserveralias'] == '1') { $server_alias .= ' www.' . $alias_domain['domain']; } } $servernames_text .= $server_alias . "\n"; } switch (Settings::Get('system.webserver_serveradmin')) { case 'customer': $servernames_text .= ' ServerAdmin ' . $domain['email'] . "\n"; break; case 'admin': $servernames_text .= ' ServerAdmin ' . $domain['admin_email'] . "\n"; break; case 'global': $servernames_text .= ' ServerAdmin ' . Settings::Get('panel.adminmail') . "\n"; break; case 'none': default: // empty } return $servernames_text; } /** * Let's get the webroot */ protected function getWebroot($domain) { $webroot_text = ''; $domain['customerroot'] = FileDir::makeCorrectDir($domain['customerroot']); $domain['documentroot'] = FileDir::makeCorrectDir($domain['documentroot']); if (($domain['deactivated'] == '1' || $domain['customer_deactivated'] == '1') && Settings::Get('system.deactivateddocroot') != '') { $webroot_text .= ' # Using docroot for deactivated users/domains...' . "\n"; $webroot_text .= ' DocumentRoot "' . rtrim(FileDir::makeCorrectDir(Settings::Get('system.deactivateddocroot')), "/") . "\"\n"; $webroot_text .= ' ' . "\n"; // >=apache-2.4 enabled? if (Settings::Get('system.apache24') == '1') { $webroot_text .= ' Require all granted' . "\n"; $webroot_text .= ' AllowOverride All' . "\n"; } else { $webroot_text .= ' Order allow,deny' . "\n"; $webroot_text .= ' allow from all' . "\n"; } $webroot_text .= ' ' . "\n"; $this->deactivated = true; } else { $webroot_text .= ' DocumentRoot "' . rtrim($domain['documentroot'], "/") . "\"\n"; $this->deactivated = false; } return $webroot_text; } /** * We put together the needed php options in the virtualhost entries * * @param array $domain * @param bool $ssl_vhost * * @return string */ protected function composePhpOptions(&$domain, $ssl_vhost = false) { $php_options_text = ''; if ($domain['phpenabled_customer'] == 1 && $domain['phpenabled_vhost'] == '1') { // This vHost has PHP enabled and we are using the regular mod_php $cmail = Customer::getCustomerDetail($domain['customerid'], 'email'); $php_options_text .= ' php_admin_value sendmail_path "/usr/sbin/sendmail -t -f ' . $cmail . '"' . PHP_EOL; if ($domain['openbasedir'] == '1') { if ($domain['openbasedir_path'] == '1' || strstr($domain['documentroot'], ":") !== false) { $_phpappendopenbasedir = Domain::appendOpenBasedirPath($domain['customerroot'], true); } else if ($domain['openbasedir_path'] == '2' && strpos(dirname($domain['documentroot']) . '/', $domain['customerroot']) !== false) { $_phpappendopenbasedir = Domain::appendOpenBasedirPath(dirname($domain['documentroot']) . '/', true); } else { $_phpappendopenbasedir = Domain::appendOpenBasedirPath($domain['documentroot'], true); } $_custom_openbasedir = explode(':', Settings::Get('system.phpappendopenbasedir')); foreach ($_custom_openbasedir as $cobd) { $_phpappendopenbasedir .= Domain::appendOpenBasedirPath($cobd); } $php_options_text .= ' php_admin_value open_basedir "' . $_phpappendopenbasedir . '"' . "\n"; } } else { $php_options_text .= ' # PHP is disabled for this vHost' . "\n"; $php_options_text .= ' php_flag engine off' . "\n"; } /** * check for apache-itk-support, #1400 * why is this here? Because it only works with mod_php */ if (Settings::get('system.apacheitksupport') == 1) { $php_options_text .= ' ' . "\n"; $php_options_text .= ' AssignUserID ' . $domain['loginname'] . ' ' . $domain['loginname'] . "\n"; $php_options_text .= ' ' . "\n"; } return $php_options_text; } /** * Lets set the text part for the stats software */ protected function getStats($domain) { $stats_text = ''; $statTool = Settings::Get('system.traffictool'); $statDomain = ""; if ($statTool == 'awstats') { // awstats generates for each domain regardless of speciallogfile $statDomain = "/" . $domain['domain']; } if ($domain['speciallogfile'] == '1') { $statDomain = "/" . (($domain['parentdomainid'] == '0') ? $domain['domain'] : $domain['parentdomain']); } $statDocroot = FileDir::makeCorrectFile($domain['customerroot'] . '/' . $statTool . $statDomain); $stats_text .= ' Alias /' . $statTool . ' "' . $statDocroot . '"' . "\n"; // awstats special requirement for icons if ($statTool == 'awstats') { $stats_text .= ' Alias /awstats-icon "' . FileDir::makeCorrectDir(Settings::Get('system.awstats_icons')) . '"' . "\n"; } return $stats_text; } /** * Lets set the logfiles */ protected function getLogfiles($domain) { $logfiles_text = ''; if ($domain['speciallogfile'] == '1') { if ($domain['parentdomainid'] == '0') { $speciallogfile = '-' . $domain['domain']; } else { $speciallogfile = '-' . $domain['parentdomain']; } } else { $speciallogfile = ''; } if ($domain['writeerrorlog']) { // The normal access/error - logging is enabled $error_log = FileDir::makeCorrectFile(Settings::Get('system.logfiles_directory') . $domain['loginname'] . $speciallogfile . '-error.log'); // Create the logfile if it does not exist (fixes #46) touch($error_log); chmod($error_log, 0640); chown($error_log, Settings::Get('system.httpuser')); chgrp($error_log, Settings::Get('system.httpgroup')); // set error log log-level $logfiles_text .= ' LogLevel ' . Settings::Get('system.errorlog_level') . "\n"; } else { $error_log = '/dev/null'; } if ($domain['writeaccesslog']) { $access_log = FileDir::makeCorrectFile(Settings::Get('system.logfiles_directory') . $domain['loginname'] . $speciallogfile . '-access.log'); // Create the logfile if it does not exist (fixes #46) touch($access_log); chmod($access_log, 0640); chown($access_log, Settings::Get('system.httpuser')); chgrp($access_log, Settings::Get('system.httpgroup')); } else { $access_log = '/dev/null'; } $logtype = 'combined'; if (Settings::Get('system.logfiles_format') != '') { $logtype = 'frx_custom'; $logfiles_text .= ' LogFormat ' . Settings::Get('system.logfiles_format') . ' ' . $logtype . "\n"; } if (Settings::Get('system.logfiles_type') == '2' && Settings::Get('system.logfiles_format') == '') { $logtype = 'vhost_combined'; } if (Settings::Get('system.logfiles_piped') == '1' && Settings::Get('system.logfiles_script') != '') { if ($domain['writeerrorlog']) { // replace for error_log $command = PhpHelper::replaceVariables(Settings::Get('system.logfiles_script'), [ 'LOGFILE' => $error_log, 'DOMAIN' => $domain['domain'], 'CUSTOMER' => $domain['loginname'] ]); $logfiles_text .= ' ErrorLog "|' . $command . "\"\n"; } else { $logfiles_text .= ' ErrorLog "' . $error_log . '"' . "\n"; } if ($domain['writeaccesslog']) { // replace for access_log $command = PhpHelper::replaceVariables(Settings::Get('system.logfiles_script'), [ 'LOGFILE' => $access_log, 'DOMAIN' => $domain['domain'], 'CUSTOMER' => $domain['loginname'] ]); $logfiles_text .= ' CustomLog "|' . $command . '" ' . $logtype . "\n"; } else { $logfiles_text .= ' CustomLog "' . $access_log . '" ' . $logtype . "\n"; } } else { $logfiles_text .= ' ErrorLog "' . $error_log . '"' . "\n"; $logfiles_text .= ' CustomLog "' . $access_log . '" ' . $logtype . "\n"; } if (Settings::Get('system.traffictool') == 'awstats') { if ((int)$domain['parentdomainid'] == 0) { // prepare the aliases and subdomains for stats config files $server_alias = ''; $alias_domains_stmt = Database::prepare(" SELECT `domain`, `iswildcarddomain`, `wwwserveralias` FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `aliasdomain` = :domainid OR `parentdomainid` = :domainid "); Database::pexecute($alias_domains_stmt, [ 'domainid' => $domain['id'] ]); while (($alias_domain = $alias_domains_stmt->fetch(PDO::FETCH_ASSOC)) !== false) { $server_alias .= ' ' . $alias_domain['domain'] . ' '; if ($alias_domain['iswildcarddomain'] == '1') { $server_alias .= '*.' . $alias_domain['domain']; } elseif ($alias_domain['wwwserveralias'] == '1') { $server_alias .= 'www.' . $alias_domain['domain']; } } $alias = ''; if ($domain['iswildcarddomain'] == '1') { $alias = '*.' . $domain['domain']; } elseif ($domain['wwwserveralias'] == '1') { $alias = 'www.' . $domain['domain']; } // After inserting the AWStats information, // be sure to build the awstats conf file as well // and chown it using $awstats_params, #258 // Bug 960 + Bug 970 : Use full $domain instead of custom $awstats_params as following classes depend on the information Statistics::createAWStatsConf(Settings::Get('system.logfiles_directory') . $domain['loginname'] . $speciallogfile . '-access.log', $domain['domain'], $alias . $server_alias, $domain['customerroot'], $domain); } } return $logfiles_text; } /** * We compose the diroption entries for the paths */ public function createFileDirOptions() { $result_stmt = Database::query(" SELECT `htac`.*, `c`.`guid`, `c`.`documentroot` AS `customerroot` FROM `" . TABLE_PANEL_HTACCESS . "` `htac` LEFT JOIN `" . TABLE_PANEL_CUSTOMERS . "` `c` USING (`customerid`) ORDER BY `htac`.`path` "); $diroptions = []; while ($row_diroptions = $result_stmt->fetch(PDO::FETCH_ASSOC)) { if ($row_diroptions['customerid'] != 0 && isset($row_diroptions['customerroot']) && $row_diroptions['customerroot'] != '') { $diroptions[$row_diroptions['path']] = $row_diroptions; $diroptions[$row_diroptions['path']]['htpasswds'] = []; } } $result_stmt = Database::query(" SELECT `htpw`.*, `c`.`guid`, `c`.`documentroot` AS `customerroot` FROM `" . TABLE_PANEL_HTPASSWDS . "` `htpw` LEFT JOIN `" . TABLE_PANEL_CUSTOMERS . "` `c` USING (`customerid`) ORDER BY `htpw`.`path`, `htpw`.`username` "); while ($row_htpasswds = $result_stmt->fetch(PDO::FETCH_ASSOC)) { if ($row_htpasswds['customerid'] != 0 && isset($row_htpasswds['customerroot']) && $row_htpasswds['customerroot'] != '') { if (!isset($diroptions[$row_htpasswds['path']]) || !is_array($diroptions[$row_htpasswds['path']])) { $diroptions[$row_htpasswds['path']] = []; } $diroptions[$row_htpasswds['path']]['path'] = $row_htpasswds['path']; $diroptions[$row_htpasswds['path']]['guid'] = $row_htpasswds['guid']; $diroptions[$row_htpasswds['path']]['customerroot'] = $row_htpasswds['customerroot']; $diroptions[$row_htpasswds['path']]['customerid'] = $row_htpasswds['customerid']; $diroptions[$row_htpasswds['path']]['htpasswds'][] = $row_htpasswds; } } foreach ($diroptions as $row_diroptions) { $row_diroptions['path'] = FileDir::makeCorrectDir($row_diroptions['path']); FileDir::mkDirWithCorrectOwnership($row_diroptions['customerroot'], $row_diroptions['path'], $row_diroptions['guid'], $row_diroptions['guid']); $diroptions_filename = FileDir::makeCorrectFile(Settings::Get('system.apacheconf_diroptions') . '/40_froxlor_diroption_' . md5($row_diroptions['path']) . '.conf'); if (!isset($this->diroptions_data[$diroptions_filename])) { $this->diroptions_data[$diroptions_filename] = ''; } if (is_dir($row_diroptions['path'])) { $cperlenabled = Customer::customerHasPerlEnabled($row_diroptions['customerid']); $this->diroptions_data[$diroptions_filename] .= '' . "\n"; if (isset($row_diroptions['options_indexes']) && $row_diroptions['options_indexes'] == '1') { $this->diroptions_data[$diroptions_filename] .= ' Options +Indexes'; // add perl options if enabled if ($cperlenabled && isset($row_diroptions['options_cgi']) && $row_diroptions['options_cgi'] == '1') { $this->diroptions_data[$diroptions_filename] .= ' +ExecCGI -MultiViews +SymLinksIfOwnerMatch +FollowSymLinks' . "\n"; } else { $this->diroptions_data[$diroptions_filename] .= "\n"; } FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'Setting Options +Indexes for ' . $row_diroptions['path']); } if (isset($row_diroptions['options_indexes']) && $row_diroptions['options_indexes'] == '0') { $this->diroptions_data[$diroptions_filename] .= ' Options -Indexes'; // add perl options if enabled if ($cperlenabled && isset($row_diroptions['options_cgi']) && $row_diroptions['options_cgi'] == '1') { $this->diroptions_data[$diroptions_filename] .= ' +ExecCGI -MultiViews +SymLinksIfOwnerMatch +FollowSymLinks' . "\n"; } else { $this->diroptions_data[$diroptions_filename] .= "\n"; } FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'Setting Options -Indexes for ' . $row_diroptions['path']); } $statusCodes = [ '404', '403', '500' ]; foreach ($statusCodes as $statusCode) { if (isset($row_diroptions['error' . $statusCode . 'path']) && $row_diroptions['error' . $statusCode . 'path'] != '') { $defhandler = $row_diroptions['error' . $statusCode . 'path']; if (!Validate::validateUrl($defhandler)) { if (substr($defhandler, 0, 1) != '"' && substr($defhandler, -1, 1) != '"') { $defhandler = '"' . FileDir::makeCorrectFile($defhandler) . '"'; } } $this->diroptions_data[$diroptions_filename] .= ' ErrorDocument ' . $statusCode . ' ' . $defhandler . "\n"; } } if ($cperlenabled && isset($row_diroptions['options_cgi']) && $row_diroptions['options_cgi'] == '1') { $this->diroptions_data[$diroptions_filename] .= ' AllowOverride None' . "\n"; $this->diroptions_data[$diroptions_filename] .= ' AddHandler cgi-script .cgi .pl' . "\n"; // >=apache-2.4 enabled? if (Settings::Get('system.apache24') == '1') { $mypath_dir = new Directory($row_diroptions['path']); // only create the' require all granted' if there is no active directory-protection // for this path, as this would be the first require and therefore grant all access if ($mypath_dir->isUserProtected() == false) { $this->diroptions_data[$diroptions_filename] .= ' Require all granted' . "\n"; // $this->diroptions_data[$diroptions_filename] .= ' AllowOverride All' . "\n"; } } else { $this->diroptions_data[$diroptions_filename] .= ' Order allow,deny' . "\n"; $this->diroptions_data[$diroptions_filename] .= ' Allow from all' . "\n"; } FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'Enabling perl execution for ' . $row_diroptions['path']); // check for suexec-workaround, #319 if ((int)Settings::Get('perl.suexecworkaround') == 1) { // symlink this directory to suexec-safe-path $loginname = Customer::getCustomerDetail($row_diroptions['customerid'], 'loginname'); $suexecpath = FileDir::makeCorrectDir(Settings::Get('perl.suexecpath') . '/' . $loginname . '/' . md5($row_diroptions['path']) . '/'); if (!file_exists($suexecpath)) { FileDir::safe_exec('mkdir -p ' . escapeshellarg($suexecpath)); FileDir::safe_exec('chown -R ' . escapeshellarg($row_diroptions['guid']) . ':' . escapeshellarg($row_diroptions['guid']) . ' ' . escapeshellarg($suexecpath)); } // symlink to {$givenpath}/cgi-bin // NOTE: symlinks are FILES, so do not append a / here $perlsymlink = FileDir::makeCorrectFile($row_diroptions['path'] . '/cgi-bin'); if (!file_exists($perlsymlink)) { FileDir::safe_exec('ln -s ' . escapeshellarg($suexecpath) . ' ' . escapeshellarg($perlsymlink)); } FileDir::safe_exec('chown -h ' . escapeshellarg($row_diroptions['guid']) . ':' . escapeshellarg($row_diroptions['guid']) . ' ' . escapeshellarg($perlsymlink)); } } else { // if no perl-execution is enabled but the workaround is, // we have to remove the symlink and folder in suexecpath if ((int)Settings::Get('perl.suexecworkaround') == 1) { $loginname = Customer::getCustomerDetail($row_diroptions['customerid'], 'loginname'); $suexecpath = FileDir::makeCorrectDir(Settings::Get('perl.suexecpath') . '/' . $loginname . '/' . md5($row_diroptions['path']) . '/'); $perlsymlink = FileDir::makeCorrectFile($row_diroptions['path'] . '/cgi-bin'); // remove symlink if (file_exists($perlsymlink)) { FileDir::safe_exec('rm -f ' . escapeshellarg($perlsymlink)); } // remove folder in suexec-path if (file_exists($suexecpath)) { FileDir::safe_exec('rm -rf ' . escapeshellarg($suexecpath)); } } } if (count($row_diroptions['htpasswds']) > 0) { $htpasswd_filename = FileDir::makeCorrectFile(Settings::Get('system.apacheconf_htpasswddir') . '/' . $row_diroptions['customerid'] . '-' . md5($row_diroptions['path']) . '.htpasswd'); if (!isset($this->htpasswds_data[$htpasswd_filename])) { $this->htpasswds_data[$htpasswd_filename] = ''; } foreach ($row_diroptions['htpasswds'] as $row_htpasswd) { $this->htpasswds_data[$htpasswd_filename] .= $row_htpasswd['username'] . ':' . $row_htpasswd['password'] . "\n"; } $this->diroptions_data[$diroptions_filename] .= ' AuthType Basic' . "\n"; $this->diroptions_data[$diroptions_filename] .= ' AuthName "' . $row_htpasswd['authname'] . '"' . "\n"; $this->diroptions_data[$diroptions_filename] .= ' AuthUserFile ' . $htpasswd_filename . "\n"; $this->diroptions_data[$diroptions_filename] .= ' require valid-user' . "\n"; } $this->diroptions_data[$diroptions_filename] .= '' . "\n"; } } } /** * We write the configs */ public function writeConfigs() { // Write diroptions FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "apache::writeConfigs: rebuilding " . Settings::Get('system.apacheconf_diroptions')); if (count($this->diroptions_data) > 0) { $optsDir = new Directory(Settings::Get('system.apacheconf_diroptions')); if (!$optsDir->isConfigDir()) { // Save one big file $diroptions_file = ''; foreach ($this->diroptions_data as $diroptions_filename => $diroptions_content) { $diroptions_file .= $diroptions_content . "\n\n"; } $diroptions_filename = Settings::Get('system.apacheconf_diroptions'); // Apply header $diroptions_file = '# ' . basename($diroptions_filename) . "\n" . '# Created ' . date('d.m.Y H:i') . "\n" . '# Do NOT manually edit this file, all changes will be deleted after the next domain change at the panel.' . "\n" . "\n" . $diroptions_file; $diroptions_file_handler = fopen($diroptions_filename, 'w'); fwrite($diroptions_file_handler, $diroptions_file); fclose($diroptions_file_handler); } else { if (!file_exists(Settings::Get('system.apacheconf_diroptions'))) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'apache::writeConfigs: mkdir ' . escapeshellarg(FileDir::makeCorrectDir(Settings::Get('system.apacheconf_diroptions')))); FileDir::safe_exec('mkdir ' . escapeshellarg(FileDir::makeCorrectDir(Settings::Get('system.apacheconf_diroptions')))); } // Write a single file for every diroption foreach ($this->diroptions_data as $diroptions_filename => $diroptions_file) { $this->known_diroptionsfilenames[] = basename($diroptions_filename); // Apply header $diroptions_file = '# ' . basename($diroptions_filename) . "\n" . '# Created ' . date('d.m.Y H:i') . "\n" . '# Do NOT manually edit this file, all changes will be deleted after the next domain change at the panel.' . "\n" . "\n" . $diroptions_file; $diroptions_file_handler = fopen($diroptions_filename, 'w'); fwrite($diroptions_file_handler, $diroptions_file); fclose($diroptions_file_handler); } } } // Write htpasswds FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "apache::writeConfigs: rebuilding " . Settings::Get('system.apacheconf_htpasswddir')); if (count($this->htpasswds_data) > 0) { if (!file_exists(Settings::Get('system.apacheconf_htpasswddir'))) { $umask = umask(); umask(0000); mkdir(Settings::Get('system.apacheconf_htpasswddir'), 0751); umask($umask); } $htpasswdDir = new Directory(Settings::Get('system.apacheconf_htpasswddir')); if ($htpasswdDir->isConfigDir(true)) { foreach ($this->htpasswds_data as $htpasswd_filename => $htpasswd_file) { $this->known_htpasswdsfilenames[] = basename($htpasswd_filename); $htpasswd_file_handler = fopen($htpasswd_filename, 'w'); fwrite($htpasswd_file_handler, $htpasswd_file); fclose($htpasswd_file_handler); } } else { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_WARNING, 'WARNING!!! ' . Settings::Get('system.apacheconf_htpasswddir') . ' is not a directory. htpasswd directory protection is disabled!!!'); } } // Write virtualhosts FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "apache::writeConfigs: rebuilding " . Settings::Get('system.apacheconf_vhost')); if (count($this->virtualhosts_data) > 0) { $vhostDir = new Directory(Settings::Get('system.apacheconf_vhost')); if (!$vhostDir->isConfigDir()) { // Save one big file $vhosts_file = ''; // sort by filename so the order is: // 1. subdomains x-29 // 2. subdomains as main-domains 30 // 3. main-domains 35 // #437 ksort($this->virtualhosts_data); foreach ($this->virtualhosts_data as $vhosts_filename => $vhost_content) { $vhosts_file .= $vhost_content . "\n\n"; } // Include diroptions file in case it exists if (file_exists(Settings::Get('system.apacheconf_diroptions'))) { $vhosts_file .= "\n" . 'Include ' . Settings::Get('system.apacheconf_diroptions') . "\n\n"; } $vhosts_filename = Settings::Get('system.apacheconf_vhost'); // Apply header $vhosts_file = '# ' . basename($vhosts_filename) . "\n" . '# Created ' . date('d.m.Y H:i') . "\n" . '# Do NOT manually edit this file, all changes will be deleted after the next domain change at the panel.' . "\n" . "\n" . $vhosts_file; $vhosts_file_handler = fopen($vhosts_filename, 'w'); fwrite($vhosts_file_handler, $vhosts_file); fclose($vhosts_file_handler); } else { if (!file_exists(Settings::Get('system.apacheconf_vhost'))) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'apache::writeConfigs: mkdir ' . escapeshellarg(FileDir::makeCorrectDir(Settings::Get('system.apacheconf_vhost')))); FileDir::safe_exec('mkdir ' . escapeshellarg(FileDir::makeCorrectDir(Settings::Get('system.apacheconf_vhost')))); } // Write a single file for every vhost foreach ($this->virtualhosts_data as $vhosts_filename => $vhosts_file) { // Apply header $vhosts_file = '# ' . basename($vhosts_filename) . "\n" . '# Created ' . date('d.m.Y H:i') . "\n" . '# Do NOT manually edit this file, all changes will be deleted after the next domain change at the panel.' . "\n" . "\n" . $vhosts_file; $vhosts_file_handler = fopen($vhosts_filename, 'w'); fwrite($vhosts_file_handler, $vhosts_file); fclose($vhosts_file_handler); } } } } } ================================================ FILE: lib/Froxlor/Cron/Http/ApacheFcgi.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cron\Http; use Froxlor\Cron\Http\Php\PhpInterface; use Froxlor\Customer\Customer; use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\Froxlor; use Froxlor\Http\Directory; use Froxlor\Settings; /** * @author Florian Lippert (2003-2009) * @author Froxlor team (2010-) */ class ApacheFcgi extends Apache { public function createOwnVhostStarter() { if (Settings::Get('system.mod_fcgid_ownvhost') == '1' || (Settings::Get('phpfpm.enabled') == '1' && Settings::Get('phpfpm.enabled_ownvhost') == '1')) { $mypath = Froxlor::getInstallDir(); if (Settings::Get('system.mod_fcgid_ownvhost') == '1') { $user = Settings::Get('system.mod_fcgid_httpuser'); $group = Settings::Get('system.mod_fcgid_httpgroup'); } elseif (Settings::Get('phpfpm.enabled') == '1' && Settings::Get('phpfpm.enabled_ownvhost') == '1') { $user = Settings::Get('phpfpm.vhost_httpuser'); $group = Settings::Get('phpfpm.vhost_httpgroup'); // get fpm config $fpm_sel_stmt = Database::prepare(" SELECT f.id FROM `" . TABLE_PANEL_FPMDAEMONS . "` f LEFT JOIN `" . TABLE_PANEL_PHPCONFIGS . "` p ON p.fpmsettingid = f.id WHERE p.id = :phpconfigid "); $fpm_config = Database::pexecute_first($fpm_sel_stmt, [ 'phpconfigid' => Settings::Get('phpfpm.vhost_defaultini') ]); } $domain = [ 'id' => 'none', 'domain' => Settings::Get('system.hostname'), 'adminid' => 1, /* first admin-user (superadmin) */ 'mod_fcgid_starter' => -1, 'mod_fcgid_maxrequests' => -1, 'guid' => $user, 'openbasedir' => 0, 'email' => Settings::Get('panel.adminmail'), 'loginname' => 'froxlor.panel', 'documentroot' => $mypath, 'customerroot' => $mypath, 'fpm_config_id' => isset($fpm_config['id']) ? $fpm_config['id'] : 1 ]; // all the files and folders have to belong to the local user // now because we also use fcgid for our own vhost FileDir::safe_exec('chown -R ' . $user . ':' . $group . ' ' . escapeshellarg($mypath)); // get php.ini for our own vhost $php = new PhpInterface($domain); // get php-config if (Settings::Get('phpfpm.enabled') == '1') { // fpm $phpconfig = $php->getPhpConfig(Settings::Get('phpfpm.vhost_defaultini')); } else { // fcgid $phpconfig = $php->getPhpConfig(Settings::Get('system.mod_fcgid_defaultini_ownvhost')); } // create starter-file | config-file $php->getInterface()->createConfig($phpconfig); // create php.ini (fpm does nothing here, as it // defines ini-settings in its pool config) $php->getInterface()->createIniFile($phpconfig); } } protected function composePhpOptions(&$domain, $ssl_vhost = false) { $php_options_text = ''; if ($domain['phpenabled_customer'] == 1 && $domain['phpenabled_vhost'] == '1') { $php = new PhpInterface($domain); $phpconfig = $php->getPhpConfig((int)$domain['phpsettingid']); if ((int)Settings::Get('phpfpm.enabled') == 1) { $srvName = 'fpm.external'; if ($domain['ssl'] == 1 && $ssl_vhost) { $srvName = 'ssl-fpm.external'; } // #1317 - perl is executed via apache and therefore, when using fpm, does not know the user // which perl is supposed to run as, hence the need for Suexec need if (Customer::customerHasPerlEnabled($domain['customerid'])) { $php_options_text .= ' SuexecUserGroup "' . $domain['loginname'] . '" "' . $domain['loginname'] . '"' . "\n"; } $domain['fpm_socket'] = $php->getInterface()->getSocketFile(); // mod_proxy stuff for apache-2.4 if (Settings::Get('system.apache24') == '1' && Settings::Get('phpfpm.use_mod_proxy') == '1') { $php_options_text .= ' ' . "\n"; $filesmatch = $phpconfig['fpm_settings']['limit_extensions']; $extensions = explode(" ", $filesmatch); $filesmatch = ""; foreach ($extensions as $ext) { $filesmatch .= substr($ext, 1) . '|'; } // start block, cut off last pipe and close block $filesmatch = '(' . str_replace(".", "\.", substr($filesmatch, 0, -1)) . ')'; $php_options_text .= ' ' . "\n"; $php_options_text .= ' ' . "\n"; $php_options_text .= ' SetHandler proxy:unix:' . $domain['fpm_socket'] . '|fcgi://localhost' . "\n"; $php_options_text .= ' ' . "\n"; $php_options_text .= ' ' . "\n"; $mypath_dir = new Directory($domain['documentroot']); // only create the "require all granted" directive if there is no active directory-protection // for this path, as this would be the first require and therefore grant all access if ($mypath_dir->isUserProtected() == false) { if ($phpconfig['pass_authorizationheader'] == '1') { $php_options_text .= ' CGIPassAuth On' . "\n"; } $php_options_text .= ' Require all granted' . "\n"; $php_options_text .= ' AllowOverride All' . "\n"; } elseif ($phpconfig['pass_authorizationheader'] == '1') { // allow Pass of Authorization header $php_options_text .= ' CGIPassAuth On' . "\n"; } $php_options_text .= ' ' . "\n"; } else { $addheader = ""; if ($phpconfig['pass_authorizationheader'] == '1') { $addheader = " -pass-header Authorization"; } $php_options_text .= ' FastCgiExternalServer ' . $php->getInterface()->getAliasConfigDir() . $srvName . ' -socket ' . $domain['fpm_socket'] . ' -idle-timeout ' . $phpconfig['fpm_settings']['idle_timeout'] . $addheader . "\n"; $php_options_text .= ' ' . "\n"; $filesmatch = $phpconfig['fpm_settings']['limit_extensions']; $extensions = explode(" ", $filesmatch); $filesmatch = ""; foreach ($extensions as $ext) { $filesmatch .= substr($ext, 1) . '|'; } // start block, cut off last pipe and close block $filesmatch = '(' . str_replace(".", "\.", substr($filesmatch, 0, -1)) . ')'; $php_options_text .= ' ' . "\n"; $php_options_text .= ' SetHandler php-fastcgi' . "\n"; $php_options_text .= ' Action php-fastcgi /fastcgiphp' . "\n"; $php_options_text .= ' Options +ExecCGI' . "\n"; $php_options_text .= ' ' . "\n"; // >=apache-2.4 enabled? if (Settings::Get('system.apache24') == '1') { $mypath_dir = new Directory($domain['documentroot']); // only create the require all granted if there is not active directory-protection // for this path, as this would be the first require and therefore grant all access if ($mypath_dir->isUserProtected() == false) { $php_options_text .= ' Require all granted' . "\n"; $php_options_text .= ' AllowOverride All' . "\n"; } } else { $php_options_text .= ' Order allow,deny' . "\n"; $php_options_text .= ' allow from all' . "\n"; } $php_options_text .= ' ' . "\n"; $php_options_text .= ' Alias /fastcgiphp ' . $php->getInterface()->getAliasConfigDir() . $srvName . "\n"; } } else { $php_options_text .= ' FcgidIdleTimeout ' . Settings::Get('system.mod_fcgid_idle_timeout') . "\n"; if ($phpconfig['pass_authorizationheader'] == '1') { $php_options_text .= ' FcgidPassHeader Authorization' . "\n"; } if ((int)Settings::Get('system.mod_fcgid_wrapper') == 0) { $php_options_text .= ' SuexecUserGroup "' . $domain['loginname'] . '" "' . $domain['loginname'] . '"' . "\n"; $php_options_text .= ' ScriptAlias /php/ ' . $php->getInterface()->getConfigDir() . "\n"; } else { $php_options_text .= ' SuexecUserGroup "' . $domain['loginname'] . '" "' . $domain['loginname'] . '"' . "\n"; $php_options_text .= ' ' . "\n"; $file_extensions = explode(' ', $phpconfig['file_extensions']); $php_options_text .= ' ' . "\n"; $php_options_text .= ' SetHandler fcgid-script' . "\n"; foreach ($file_extensions as $file_extension) { $php_options_text .= ' FcgidWrapper ' . $php->getInterface()->getStarterFile() . ' .' . $file_extension . "\n"; } $php_options_text .= ' Options +ExecCGI' . "\n"; $php_options_text .= ' ' . "\n"; // >=apache-2.4 enabled? if (Settings::Get('system.apache24') == '1') { $mypath_dir = new Directory($domain['documentroot']); // only create the require all granted if there is not active directory-protection // for this path, as this would be the first require and therefore grant all access if ($mypath_dir->isUserProtected() == false) { $php_options_text .= ' Require all granted' . "\n"; $php_options_text .= ' AllowOverride All' . "\n"; } } else { $php_options_text .= ' Order allow,deny' . "\n"; $php_options_text .= ' allow from all' . "\n"; } $php_options_text .= ' ' . "\n"; } } // create starter-file | config-file $php->getInterface()->createConfig($phpconfig); // create php.ini (fpm does nothing here, as it // defines ini-settings in its pool config) $php->getInterface()->createIniFile($phpconfig); } else { $php_options_text .= ' # PHP is disabled for this vHost' . "\n"; } return $php_options_text; } } ================================================ FILE: lib/Froxlor/Cron/Http/ConfigIO.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cron\Http; use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\Froxlor; use Froxlor\Settings; use PDO; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; class ConfigIO { /** * clean up former created configs, including (if enabled) * awstats, fcgid, php-fpm and of course automatically created * webserver vhost and diroption files * * @return null */ public function cleanUp() { // old error logs $this->cleanErrLogs(); // awstats files $this->cleanAwstatsFiles(); // fcgid files $this->cleanFcgidFiles(); // php-fpm files $this->cleanFpmFiles(); // clean webserver-configs $this->cleanWebserverConfigs(); // old htpasswd files $this->cleanHtpasswdFiles(); // customer-specified ssl-certificates $this->cleanCustomerSslCerts(); } private function cleanErrLogs() { $err_dir = FileDir::makeCorrectDir(Froxlor::getInstallDir() . "/logs/"); if (@is_dir($err_dir)) { // now get rid of old stuff // (but append /*.log so we don't delete the directory) $err_dir .= '/*.log'; FileDir::safe_exec('rm -f ' . FileDir::makeCorrectFile($err_dir)); } } /** * remove awstats related configuration files before regeneration * * @return null */ private function cleanAwstatsFiles() { if (Settings::Get('system.traffictool') != 'awstats') { return; } // dhr: cleanout froxlor-generated awstats configs prior to re-creation $awstatsclean = []; $awstatsclean['header'] = "## GENERATED BY FROXLOR\n"; $awstatsclean['headerold'] = "## GENERATED BY SYSCP\n"; $awstatsclean['path'] = $this->getFile('system', 'awstats_conf'); /** * don't do anything if the directory does not exist * (e.g. * awstats not installed yet or whatever) * fixes #45 */ if ($awstatsclean['path'] !== false && is_dir($awstatsclean['path'])) { $awstatsclean['dir'] = dir($awstatsclean['path']); while ($awstatsclean['entry'] = $awstatsclean['dir']->read()) { $awstatsclean['fullentry'] = FileDir::makeCorrectFile($awstatsclean['path'] . '/' . $awstatsclean['entry']); /** * don't do anything if the file does not exist */ if (@file_exists($awstatsclean['fullentry']) && $awstatsclean['entry'] != '.' && $awstatsclean['entry'] != '..') { $awstatsclean['fh'] = fopen($awstatsclean['fullentry'], 'r'); $awstatsclean['headerRead'] = fgets($awstatsclean['fh'], strlen($awstatsclean['header']) + 1); fclose($awstatsclean['fh']); if ($awstatsclean['headerRead'] == $awstatsclean['header'] || $awstatsclean['headerRead'] == $awstatsclean['headerold']) { $awstats_conf_file = FileDir::makeCorrectFile($awstatsclean['fullentry']); @unlink($awstats_conf_file); } } } } unset($awstatsclean); // end dhr } /** * returns a file/directory from the settings and checks whether it exists * * @param string $group * settings-group * @param string $varname * var-name * @param boolean $check_exists * check if the file exists * * @return string|boolean complete path including filename if any or false on error */ private function getFile($group, $varname, $check_exists = true) { // read from settings $file = Settings::Get($group . '.' . $varname); // check whether it exists if ($check_exists && @file_exists($file) == false) { return false; } return $file; } /** * remove fcgid related configuration files before regeneration * * @return null */ private function cleanFcgidFiles() { if (Settings::Get('system.mod_fcgid') == '0') { return; } // get correct directory $configdir = $this->getFile('system', 'mod_fcgid_configdir'); if ($configdir !== false) { $configdir = FileDir::makeCorrectDir($configdir); if (@is_dir($configdir)) { // create directory iterator $its = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($configdir)); // iterate through all subdirs, // look for php-fcgi-starter files // and take immutable-flag away from them // so we can delete them :) foreach ($its as $it) { if ($it->isFile() && $it->getFilename() == 'php-fcgi-starter') { // set chattr -i FileDir::removeImmutable($its->getPathname()); } } // now get rid of old stuff // (but append /* so we don't delete the directory) $configdir .= '/*'; FileDir::safe_exec('rm -rf ' . FileDir::makeCorrectFile($configdir)); } } } /** * remove php-fpm related configuration files before regeneration * * @return null */ private function cleanFpmFiles() { if (Settings::Get('phpfpm.enabled') == '0') { return; } // get all fpm config paths $fpmconf_sel = Database::prepare("SELECT config_dir FROM `" . TABLE_PANEL_FPMDAEMONS . "`"); Database::pexecute($fpmconf_sel); $fpmconf_paths = $fpmconf_sel->fetchAll(PDO::FETCH_ASSOC); // clean all php-fpm config-dirs foreach ($fpmconf_paths as $configdir) { $configdir = FileDir::makeCorrectDir($configdir['config_dir']); if (@is_dir($configdir)) { // now get rid of old stuff // (but append /*.conf so we don't delete the directory) $configdir .= '/*.conf'; FileDir::safe_exec('rm -f ' . FileDir::makeCorrectFile($configdir)); } else { FileDir::safe_exec('mkdir -p ' . $configdir); } } // also remove aliasconfigdir #1273 $aliasconfigdir = $this->getFile('phpfpm', 'aliasconfigdir'); if ($aliasconfigdir !== false) { $aliasconfigdir = FileDir::makeCorrectDir($aliasconfigdir); if (@is_dir($aliasconfigdir)) { $aliasconfigdir .= '/*'; FileDir::safe_exec('rm -rf ' . FileDir::makeCorrectFile($aliasconfigdir)); } } } /** * remove webserver related configuration files before regeneration * * @return null */ private function cleanWebserverConfigs() { // get directories $configdirs = []; $dir = $this->getFile('system', 'apacheconf_vhost'); if ($dir !== false) { $configdirs[] = FileDir::makeCorrectDir($dir); } $dir = $this->getFile('system', 'apacheconf_diroptions'); if ($dir !== false) { $configdirs[] = FileDir::makeCorrectDir($dir); } // file pattern $pattern = "/^([0-9]){2}_(froxlor|syscp)_(.+)\.conf$/"; // check ALL the folders foreach ($configdirs as $config_dir) { // check directory if (@is_dir($config_dir)) { // create directory iterator $its = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($config_dir)); // iterate through all subdirs, // look for vhost/diroption files // and delete them foreach ($its as $it) { if ($it->isFile() && preg_match($pattern, $it->getFilename())) { // remove file FileDir::safe_exec('rm -f ' . escapeshellarg(FileDir::makeCorrectFile($its->getPathname()))); } } } } } /** * remove htpasswd files before regeneration * * @return null */ private function cleanHtpasswdFiles() { // get correct directory $configdir = $this->getFile('system', 'apacheconf_htpasswddir'); if ($configdir !== false) { $configdir = FileDir::makeCorrectDir($configdir); if (@is_dir($configdir)) { // now get rid of old stuff // (but append /* so we don't delete the directory) $configdir .= '/*'; FileDir::safe_exec('rm -f ' . FileDir::makeCorrectFile($configdir)); } } } /** * remove customer-specified auto-generated ssl-certificates * (they are being regenerated) * * @return null */ private function cleanCustomerSslCerts() { /* * only clean up if we're actually using SSL */ if (Settings::Get('system.use_ssl') == '1') { // get correct directory $configdir = $this->getFile('system', 'customer_ssl_path'); if ($configdir !== false) { $configdir = FileDir::makeCorrectDir($configdir); if (@is_dir($configdir)) { // now get rid of old stuff // (but append /* so we don't delete the directory) $configdir .= '/*'; FileDir::safe_exec('rm -f ' . FileDir::makeCorrectFile($configdir)); } } } } } ================================================ FILE: lib/Froxlor/Cron/Http/DomainSSL.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cron\Http; use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\FroxlorLogger; use Froxlor\Settings; class DomainSSL { /** * read domain-related (or if empty, parentdomain-related) ssl-certificates from the database * and (if not empty) set the corresponding array-indices (ssl_cert_file, ssl_key_file, * ssl_ca_file and ssl_cert_chainfile). * Hence the parameter as reference. * * @param array $domain * domain-array as reference so we can set the corresponding array-indices * * @return null * @throws \Exception */ public function setDomainSSLFilesArray(?array &$domain = null) { // check if the domain itself has a certificate defined $dom_certs_stmt = Database::prepare(" SELECT s.*, d.domain FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` s LEFT JOIN `" . TABLE_PANEL_DOMAINS . "` d ON d.id = s.domainid WHERE s.`domainid` = :domid "); $dom_certs = Database::pexecute_first($dom_certs_stmt, [ 'domid' => $domain['id'] ]); $parent_certificate = false; if (!is_array($dom_certs) || !isset($dom_certs['ssl_cert_file']) || $dom_certs['ssl_cert_file'] == '') { // maybe its parent? if (isset($domain['parentdomainid']) && $domain['parentdomainid'] != 0) { $dom_certs = Database::pexecute_first($dom_certs_stmt, [ 'domid' => $domain['parentdomainid'] ]); $parent_certificate = true; } } // check if it's an array and if the most important field is set if (is_array($dom_certs) && isset($dom_certs['ssl_cert_file']) && $dom_certs['ssl_cert_file'] != '') { // get destination path $sslcertpath = FileDir::makeCorrectDir(Settings::Get('system.customer_ssl_path')); // create path if it does not exist if (!file_exists($sslcertpath)) { FileDir::safe_exec('mkdir -p ' . escapeshellarg($sslcertpath)); } // make correct files for the certificates $ssl_files = [ 'ssl_cert_file' => FileDir::makeCorrectFile($sslcertpath . '/' . ($parent_certificate ? $dom_certs['domain'] : $domain['domain']) . '.crt'), 'ssl_key_file' => FileDir::makeCorrectFile($sslcertpath . '/' . ($parent_certificate ? $dom_certs['domain'] : $domain['domain']) . '.key') ]; if (!$this->validateCertificate($dom_certs)) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, 'Given SSL private key for ' . $domain['domain'] . ' does not seem to match the certificate. Cannot create ssl-directives'); return; } // initialize optional files $ssl_files['ssl_ca_file'] = ''; $ssl_files['ssl_cert_chainfile'] = ''; // set them if they are != empty if ($dom_certs['ssl_ca_file'] != '') { $ssl_files['ssl_ca_file'] = FileDir::makeCorrectFile($sslcertpath . '/' . ($parent_certificate ? $dom_certs['domain'] : $domain['domain']) . '_CA.pem'); } if ($dom_certs['ssl_cert_chainfile'] != '') { if (Settings::Get('system.webserver') == 'nginx') { // put ca.crt in my.crt, as nginx does not support a separate chain file. $dom_certs['ssl_cert_file'] = trim($dom_certs['ssl_cert_file']) . "\n" . trim($dom_certs['ssl_cert_chainfile']) . "\n"; } else { $ssl_files['ssl_cert_chainfile'] = FileDir::makeCorrectFile($sslcertpath . '/' . ($parent_certificate ? $dom_certs['domain'] : $domain['domain']) . '_chain.pem'); } } // will only be generated to be used externally, froxlor does not need this if ($dom_certs['ssl_fullchain_file'] != '') { $ssl_files['ssl_fullchain_file'] = FileDir::makeCorrectFile($sslcertpath . '/' . ($parent_certificate ? $dom_certs['domain'] : $domain['domain']) . '_fullchain.pem'); } // create them on the filesystem foreach ($ssl_files as $type => $filename) { if ($filename != '') { touch($filename); $_fh = fopen($filename, 'w'); fwrite($_fh, $dom_certs[$type]); fclose($_fh); if ($type == 'ssl_key_file') { chmod($filename, 0600); } else { chmod($filename, 0644); } } } // override corresponding array values $domain['ssl_cert_file'] = $ssl_files['ssl_cert_file']; $domain['ssl_key_file'] = $ssl_files['ssl_key_file']; $domain['ssl_ca_file'] = $ssl_files['ssl_ca_file']; $domain['ssl_cert_chainfile'] = $ssl_files['ssl_cert_chainfile']; } return; } private function validateCertificate($dom_certs = []): bool { return openssl_x509_check_private_key($dom_certs['ssl_cert_file'], $dom_certs['ssl_key_file']); } } ================================================ FILE: lib/Froxlor/Cron/Http/HttpConfigBase.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cron\Http; use Froxlor\Cron\Http\LetsEncrypt\AcmeSh; use Froxlor\Cron\Http\Php\Fpm; use Froxlor\Database\Database; use Froxlor\Domain\Domain; use Froxlor\FileDir; use Froxlor\Froxlor; use Froxlor\FroxlorLogger; use Froxlor\PhpHelper; use Froxlor\Settings; use Froxlor\System\Cronjob; use PDO; /** * Class HttpConfigBase * * Base class for all HTTP server configs */ class HttpConfigBase { /** * Pre-defined DHE groups to use as fallback if dhparams_file * is given, but non-existent, see also https://github.com/froxlor/Froxlor/issues/1270 */ const FFDHE4096 = <<logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'Running Let\'s Encrypt cronjob prior to regenerating webserver config files'); AcmeSh::$no_inserttask = true; AcmeSh::run(true); // set last run timestamp of cronjob Cronjob::updateLastRunOfCron('letsencrypt'); } } public function reload() { $called_class = get_called_class(); if ((int)Settings::Get('phpfpm.enabled') == 1) { // get all start/stop commands $startstop_sel = Database::prepare("SELECT reload_cmd, config_dir FROM `" . TABLE_PANEL_FPMDAEMONS . "`"); Database::pexecute($startstop_sel); $restart_cmds = $startstop_sel->fetchAll(PDO::FETCH_ASSOC); // restart all php-fpm instances foreach ($restart_cmds as $restart_cmd) { // check whether the config dir is empty (no domains uses this daemon) // so we need to create a dummy $_conffiles = glob(FileDir::makeCorrectFile($restart_cmd['config_dir'] . "/*.conf")); if ($_conffiles === false || empty($_conffiles)) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, $called_class . '::reload: fpm config directory "' . $restart_cmd['config_dir'] . '" is empty. Creating dummy.'); Fpm::createDummyPool($restart_cmd['config_dir']); } FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, $called_class . '::reload: running ' . $restart_cmd['reload_cmd']); FileDir::safe_exec(escapeshellcmd($restart_cmd['reload_cmd'])); } } FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, $called_class . '::reload: reloading ' . $called_class); FileDir::safe_exec(escapeshellcmd(Settings::Get('system.apachereload_command'))); /** * nginx does not auto-spawn fcgi-processes */ if (Settings::Get('system.webserver') == "nginx" && Settings::Get('system.phpreload_command') != '' && (int)Settings::Get('phpfpm.enabled') == 0) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, $called_class . '::reload: restarting php processes'); FileDir::safe_exec(Settings::Get('system.phpreload_command')); } } /** * process special config as template, by substituting {VARIABLE} with the * respective value. * * The following variables are known at the moment: * * {DOMAIN} - domain name * {IP} - IP for this domain * {PORT} - Port for this domain * {CUSTOMER} - customer name * {IS_SSL} - evaluates to 'ssl' if domain/ip is ssl, otherwise it is an empty string * {DOCROOT} - document root for this domain * * @param * $template * @return string */ protected function processSpecialConfigTemplate($template, $domain, $ip, $port, $is_ssl_vhost) { $templateVars = [ 'DOMAIN' => $domain['domain'], 'CUSTOMER' => $domain['loginname'], 'IP' => $ip, 'PORT' => $port, 'SCHEME' => ($is_ssl_vhost) ? 'https' : 'http', 'DOCROOT' => $domain['documentroot'], 'FPMSOCKET' => '' ]; if ((int)Settings::Get('phpfpm.enabled') == 1 && isset($domain['fpm_socket']) && !empty($domain['fpm_socket'])) { $templateVars['FPMSOCKET'] = $domain['fpm_socket']; } return PhpHelper::replaceVariables($template, $templateVars); } protected function getMyPath($ip_port = null) { if (!empty($ip_port) && $ip_port['docroot'] == '') { if (Settings::Get('system.froxlordirectlyviahostname')) { $mypath = FileDir::makeCorrectDir(Froxlor::getInstallDir()); } else { $mypath = FileDir::makeCorrectDir(dirname(Froxlor::getInstallDir())); } } else { // user-defined docroot, #417 $mypath = FileDir::makeCorrectDir($ip_port['docroot']); } return $mypath; } protected function checkAlternativeSslPort() { // We must not check if our port differs from port 443, // but if there is a destination-port != 443 $_sslport = ''; // This returns the first port that is != 443 with ssl enabled, // ordered by ssl-certificate (if any) so that the ip/port combo // with certificate is used $ssldestport_stmt = Database::prepare(" SELECT `ip`.`port` FROM " . TABLE_PANEL_IPSANDPORTS . " `ip` WHERE `ip`.`ssl` = '1' AND `ip`.`port` != 443 ORDER BY `ip`.`ssl_cert_file` DESC, `ip`.`port` LIMIT 1; "); $ssldestport = Database::pexecute_first($ssldestport_stmt); if ($ssldestport && $ssldestport['port'] != '') { $_sslport = ":" . $ssldestport['port']; } return $_sslport; } protected function froxlorVhostHasLetsEncryptCert() { // check whether we have an entry with valid certificates which just does not need // updating yet, so we need to skip this here $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); if ($froxlor_ssl && !empty($froxlor_ssl['ssl_cert_file'])) { return true; } return false; } protected function froxlorVhostLetsEncryptNeedsRenew() { $froxlor_ssl_settings_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` WHERE `domainid` = '0' AND (`validtodate` < DATE_ADD(NOW(), INTERVAL 30 DAY) OR `validtodate` IS NULL) "); $froxlor_ssl = Database::pexecute_first($froxlor_ssl_settings_stmt); if ($froxlor_ssl && !empty($froxlor_ssl['ssl_cert_file'])) { return true; } return false; } /** * Get the filename for the virtualhost */ protected function getVhostFilename(array $domain, bool $ssl_vhost = false, bool $filename_only = false) { // number of dots in a domain specifies its position (and depth of subdomain) starting at 35 going downwards on higher depth $vhost_no = (string)(35 - substr_count($domain['domain'], ".") + 1); $filename = $vhost_no . '_froxlor_' . ($ssl_vhost ? 'ssl' : 'normal') . '_vhost_' . $domain['domain'] . '.conf'; if ($filename_only) { return $filename; } return FileDir::makeCorrectFile(Settings::Get('system.apacheconf_vhost') . '/' . $filename); } protected function getCustomVhostFilename(string $name) { $vhosts_folder = FileDir::makeCorrectDir(dirname(Settings::Get('system.apacheconf_vhost'))); if (is_dir(Settings::Get('system.apacheconf_vhost'))) { $vhosts_folder = FileDir::makeCorrectDir(Settings::Get('system.apacheconf_vhost')); } return FileDir::makeCorrectFile($vhosts_folder . '/' . $name); } } ================================================ FILE: lib/Froxlor/Cron/Http/LetsEncrypt/AcmeSh.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cron\Http\LetsEncrypt; use Froxlor\Cron\FroxlorCron; use Froxlor\Cron\TaskId; use Froxlor\Database\Database; use Froxlor\Domain\Domain; use Froxlor\FileDir; use Froxlor\Froxlor; use Froxlor\FroxlorLogger; use Froxlor\PhpHelper; use Froxlor\Settings; use Froxlor\System\Cronjob; use Froxlor\Validate\Validate; use PDO; use PDOStatement; class AcmeSh extends FroxlorCron { const ACME_PROVIDER = [ 'letsencrypt' => "https://acme-v02.api.letsencrypt.org/directory", 'letsencrypt_test' => "https://acme-staging-v02.api.letsencrypt.org/directory", 'buypass' => "https://api.buypass.com/acme/directory", 'buypass_test' => "https://api.test4.buypass.no/acme/directory", 'zerossl' => "https://acme.zerossl.com/v2/DV90", 'google' => "https://dv.acme-v02.api.pki.goog/directory", 'google_test' => "https://dv.acme-v02.test-api.pki.goog/directory", ]; public static $no_inserttask = false; private static $apiserver = ""; private static $acmesh = "/root/.acme.sh/acme.sh"; /** * * @var PDOStatement */ private static $updcert_stmt = null; /** * * @var PDOStatement */ private static $upddom_stmt = null; /** * run the task * * @param bool $internal * @return int * @throws \Exception */ public static function run(bool $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 Cronjob::inserttask(TaskId::REBUILD_VHOST); if ($renew_froxlor) { Cronjob::inserttask(TaskId::UPDATE_LE_SERVICES); } } return 0; } // set server according to settings self::$apiserver = self::ACME_PROVIDER[Settings::Get('system.letsencryptca')]; // validate acme.sh installation if (!self::checkInstall()) { return -1; } self::checkUpgrade(); // 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 `id` = :id, `domainid` = :domainid, `ssl_cert_file` = :crt, `ssl_key_file` = :key, `ssl_ca_file` = :ca, `ssl_cert_chainfile` = :chain, `ssl_csr_file` = :csr, `ssl_fullchain_file` = :fullchain, `validfromdate` = :validfromdate, `validtodate` = :validtodate, `issuer` = :issuer "); // 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(); // first - generate LE for system-vhost if enabled if ($issue_froxlor) { // build row $certrow = [ 'loginname' => 'froxlor.panel', 'domain' => Settings::Get('system.hostname'), 'domainid' => 0, 'documentroot' => 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'), 'validfromdate' => null, 'validtodate' => null, 'issuer' => "", 'ssl_cert_file' => null, 'ssl_key_file' => null, 'ssl_ca_file' => null, 'ssl_csr_file' => null, 'id' => null, 'wwwserveralias' => 0 ]; // 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; } // 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 = [ 'loginname' => 'froxlor.panel', 'domain' => Settings::Get('system.hostname'), 'domainid' => 0, 'documentroot' => 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'), 'validfromdate' => is_array($renew_froxlor) ? $renew_froxlor['validfromdate'] : date('Y-m-d H:i:s', 0), 'validtodate' => is_array($renew_froxlor) ? $renew_froxlor['validtodate'] : date('Y-m-d H:i:s', 0), 'issuer' => is_array($renew_froxlor) ? $renew_froxlor['issuer'] : "", '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, 'wwwserveralias' => 0 ]; $renew_domains[] = $certrow; } foreach ($renew_domains as $domain) { $cronlog = FroxlorLogger::getInstanceOf([ 'loginname' => $domain['loginname'], 'adminsession' => 0 ]); if (defined('CRON_IS_FORCED') || self::checkFsFilesAreNewer($domain['domain'], $domain['validtodate'])) { self::certToDb($domain, $cronlog, []); $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) { Cronjob::inserttask(TaskId::REBUILD_VHOST); } 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"); } return 0; } /** * check whether we need to issue a new certificate for froxlor itself * * @return boolean * @throws \Exception */ 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 || empty($froxlor_ssl['validtodate'])) { return true; } } return false; } private static function checkFsFilesAreNewer($domain, $cert_date = 0): bool { $certificate_folder = self::getCertificateFolder(strtolower($domain)); if (empty($certificate_folder)) { return false; } $ssl_file = 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; } public static function getWorkingDirFromEnv($domain = "", $forced_ecc = false): string { // first try without _ecc either if it's enabled currently or not as // it might have been at some point so there is a chance we have certificates // with and without _ecc - the method getCertificateFolder() will check both // possibilities if ($forced_ecc) { $domain .= "_ecc"; } $env_file = FileDir::makeCorrectFile(dirname(self::getAcmeSh()) . '/acme.sh.env'); if (file_exists($env_file)) { $output = []; $cut = <<fetchAll(PDO::FETCH_ASSOC); if ($customer_ssl) { return $customer_ssl; } return []; } /** * check whether we need to renew-check the certificate for froxlor itself * * @return boolean * @throws \Exception */ 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['validtodate'])) { 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(" SELECT domssl.`id`, domssl.`domainid`, domssl.`validfromdate`, domssl.`validtodate`, domssl.`issuer`, domssl.`ssl_cert_file`, domssl.`ssl_key_file`, dom.`domain`, dom.`id` AS 'domainid', dom.`ssl_redirect`, cust.`loginname` FROM `" . TABLE_PANEL_CUSTOMERS . "` AS cust, `" . TABLE_PANEL_DOMAINS . "` AS dom LEFT JOIN `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` AS domssl ON dom.`id` = domssl.`domainid` WHERE dom.`customerid` = cust.`customerid` AND cust.deactivated = 0 AND dom.deactivated = 0 AND dom.`ssl_enabled` = 1 AND dom.`letsencrypt` = 1 AND dom.`aliasdomain` IS NULL AND dom.`iswildcarddomain` = 0 AND dom.`email_only` = 0 AND dom.`ssl_redirect` != 2 "); $renew_certs = $certificates_stmt->fetchAll(PDO::FETCH_ASSOC); if ($renew_certs) { if ($check) { foreach ($renew_certs as $cert) { if (self::checkFsFilesAreNewer($cert['domain'], $cert['validtodate'])) { return true; } } return false; } return $renew_certs; } return []; } /** * install acme.sh if not found yet */ private static function checkInstall($tries = 0) { if (!file_exists(self::getAcmeSh()) && $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::getAcmeSh() . "'"); echo PHP_EOL . "Download/installation of acme.sh seems to have failed. Re-run cronjob to try again or install manually to '" . self::getAcmeSh() . "'" . PHP_EOL; return false; } else { if (!file_exists(self::getAcmeSh())) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Could not find acme.sh - installing it to /root/.acme.sh/"); $return = false; FileDir::safe_exec("wget -O - https://get.acme.sh | sh -s email=" . escapeshellarg(Settings::Get('panel.adminmail')), $return, [ '|' ]); $set_path = self::getAcmeSh(); // after this, regardless of what the user specified, the acme.sh installation will be in /root/.acme.sh if ($set_path != '/root/.acme.sh/acme.sh') { Settings::Set('system.acmeshpath', '/root/.acme.sh/acme.sh', true); // let the user know FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_WARNING, "Acme.sh could not be found in '" . $set_path . "' so froxlor installed it to the default location, which is '/root/.acme.sh/'"); echo PHP_EOL . "Acme.sh could not be found in '" . $set_path . "' so froxlor installed it to the default location, which is '/root/.acme.sh/'" . PHP_EOL; } // check whether the installation worked return self::checkInstall(++$tries); } } return true; } /** * run upgrade */ private static function checkUpgrade() { $acmesh_result = FileDir::safe_exec(self::getAcmeSh() . " --upgrade --auto-upgrade 0"); // check for activated cron $acmesh_result2 = FileDir::safe_exec(self::getAcmeSh() . " --install-cronjob"); // set default CA $acmesh_result3 = FileDir::safe_exec(self::getAcmeSh() . " --set-default-ca --server " . self::$apiserver); // log messages 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) . "\n" . implode("\n", $acmesh_result3)); } /** * issue certificates for a list of domains */ private static function runIssueFor($certrows = []) { // prepare aliasdomain-check $aliasdomains_stmt = Database::prepare(" SELECT dom.`id` as domainid, dom.`domain`, dom.`wwwserveralias` FROM `" . TABLE_PANEL_DOMAINS . "` AS dom WHERE dom.`aliasdomain` = :id 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([ '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['validtodate'])) { // 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 = [ 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 (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, [ '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::runAcmeSh($certrow, $domains, $cronlog, $do_force, $certrow['domainid'] == 0); } else { $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_WARNING, "Skipping Let's Encrypt generation for " . $certrow['domain'] . " due to an enabled ssl_redirect"); // we need another reconfigure in order to get the certificate Cronjob::inserttask(TaskId::REBUILD_VHOST); } } } /** * validate dns (A / AAAA record) of domain against known system ips * * @param array $domains * @param int $domain_id * @param FroxlorLogger $cronlog * @throws \Exception */ 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 according to NS $domain_ips = PhpHelper::gethostbynamel6($domain, true, Settings::Get('system.le_domain_dnscheck_resolver')); 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]); // in order to avoid a cron-loop that tries to get a certificate every 5 minutes, we disable let's encrypt for this domain if ($domain_id > 0) { $upd_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_DOMAINS . "` SET `letsencrypt` = '0' WHERE `id` = :did"); Database::pexecute($upd_stmt, [ 'did' => $domain_id ]); } else { // froxlor's hostname Settings::Set('system.le_froxlor_enabled', 0); } $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_WARNING, "Let's Encrypt deactivated for domain " . $domain); if (!defined('CRON_IS_FORCED') && !defined('CRON_DEBUG_FLAG')) { // email info to admin that lets encrypt has been disabled for this domain Cronjob::notifyMailToAdmin("Let's Encrypt has been deactivated for domain '" . $domain . "' due to failed dns validation (wrong or no IP address)"); } } } } } private static function runAcmeSh(array $certrow, array $domains, &$cronlog = null, bool $force = false, bool $renew_hook = false) { if (!empty($domains)) { $acmesh_cmd = self::getAcmeSh() . " --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 (substr(Settings::Get('system.letsencryptca'), -5) == '_test') { $acmesh_cmd .= " --staging"; } if ($force) { $acmesh_cmd .= " --force"; } if ($renew_hook && !empty(trim(Settings::Get('system.le_renew_services') ?? "")) && !empty(trim(Settings::Get('system.le_renew_hook') ?? "")) ) { $acmesh_cmd .= " --renew-hook '" . Settings::Get('system.le_renew_hook') . "'"; } if (defined('CRON_DEBUG_FLAG')) { $acmesh_cmd .= " --debug"; } $exit_code = null; $acme_result = FileDir::safe_exec($acmesh_cmd, $exit_code); // debug output of acme.sh run $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, implode("\n", $acme_result)); if ($exit_code != 0) { $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, "Non-successful exit-code returned :("); if (!defined('CRON_IS_FORCED') && !defined('CRON_DEBUG_FLAG')) { Cronjob::notifyMailToAdmin("Let's Encrypt certificate could not be obtained for: " . implode(", ", $domains) . "\n\n" . implode("\n", $acme_result)); } } else { $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, "Successful exit-code returned - storing certificate"); $cert_stored = self::certToDb($certrow, $cronlog, $acme_result); if ($cert_stored && $renew_hook) { self::renewHookConfigs($cronlog); } } } } public static function renewHookConfigs($cronlog) { if (!empty(trim(Settings::Get('system.le_renew_services') ?? "")) && !empty(trim(Settings::Get('system.le_renew_hook') ?? "")) ) { $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, "Renew-hook is enabled - adjusting configurations"); $certificate_folder = self::getCertificateFolder(strtolower(Settings::Get('system.hostname'))); if (empty($certificate_folder)) { $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "No certificate folder for '" . Settings::Get('system.hostname') . "' found"); return; } $fullchain = FileDir::makeCorrectFile($certificate_folder . '/fullchain.cer'); $keyfile = FileDir::makeCorrectFile($certificate_folder . '/' . strtolower(Settings::Get('system.hostname')) . '.key'); $ca_file = FileDir::makeCorrectFile($certificate_folder . '/ca.cer'); if (!file_exists($fullchain) || !file_exists($keyfile) || !file_exists($ca_file)) { $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "At least one of the required certificate files for '" . Settings::Get('system.hostname') . "' could not be found"); return; } $dovecot_conf = '/etc/dovecot/conf.d/99-froxlor.ssl.conf'; // @fixme setting? if (Settings::IsInList('system.le_renew_services', 'postfix')) { // "postconf -e" for postfix FileDir::safe_exec('postconf -e smtpd_tls_cert_file=' . escapeshellarg($fullchain)); FileDir::safe_exec('postconf -e smtpd_tls_key_file=' . escapeshellarg($keyfile)); } if (Settings::IsInList('system.le_renew_services', 'dovecot')) { // custom config for dovecot $ssl_content = << $certrow['id'], 'domainid' => $certrow['domainid'], 'crt' => $return['crt'], 'key' => $return['key'], 'ca' => $return['chain'], 'chain' => $return['chain'], 'csr' => $return['csr'], 'fullchain' => $return['fullchain'], 'validfromdate' => date('Y-m-d H:i:s', $newcert['validFrom_time_t']), 'validtodate' => date('Y-m-d H:i:s', $newcert['validTo_time_t']), 'issuer' => $newcert['issuer']['O'] ?? "" ]); if ($certrow['ssl_redirect'] == 3) { Database::pexecute(self::$upddom_stmt, [ 'domainid' => $certrow['domainid'] ]); } $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Updated Let's Encrypt certificate for " . $certrow['domain']); return true; } 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)); } return false; } /** * 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::getCertificateFolder($domain); if (!empty($certificate_folder)) { $certificate_files = [ 'crt' => $domain . '.cer', 'key' => $domain . '.key', 'chain' => 'ca.cer', 'fullchain' => 'fullchain.cer', 'csr' => $domain . '.csr' ]; foreach ($certificate_files as $index => $sslfile) { $ssl_file = FileDir::makeCorrectFile($certificate_folder . '/' . $sslfile); if (file_exists($ssl_file)) { $return[$index] = file_get_contents($ssl_file); } else { $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 . "'"); } } private static function getCertificateFolder(string $domain): string { $certificate_folder = self::getWorkingDirFromEnv(strtolower($domain)); if (file_exists($certificate_folder)) { return $certificate_folder; } $certificate_folder_ecc = self::getWorkingDirFromEnv($domain, true); if (file_exists($certificate_folder_ecc)) { return $certificate_folder_ecc; } FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, "Could not find certificate-folder for domain '" . $domain . "'"); return ""; } } ================================================ FILE: lib/Froxlor/Cron/Http/LetsEncrypt/index.html ================================================ ================================================ FILE: lib/Froxlor/Cron/Http/Nginx.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cron\Http; use Froxlor\Cron\Http\Php\PhpInterface; use Froxlor\Cron\TaskId; use Froxlor\Customer\Customer; use Froxlor\Database\Database; use Froxlor\Domain\Domain; use Froxlor\Froxlor; use Froxlor\FileDir; use Froxlor\FroxlorLogger; use Froxlor\Http\Directory; use Froxlor\Http\Statistics; use Froxlor\Settings; use Froxlor\System\Cronjob; use Froxlor\Validate\Validate; use Froxlor\System\Crypt; use PDO; class Nginx extends HttpConfigBase { protected $nginx_data = []; // protected protected $needed_htpasswds = []; protected $http2_on_directive = false; protected $htpasswds_data = []; protected $known_htpasswdsfilenames = []; protected $vhost_root_autoindex = false; /** * indicator whether a customer is deactivated or not * if yes, only the webroot will be generated * * @var bool */ private $deactivated = false; public function __construct() { $nores = false; $res = FileDir::safe_exec('nginx -v 2>&1', $nores, ['>', '&']); $ver_str = array_shift($res); $cNginxVer = substr($ver_str, strrpos($ver_str, "/") + 1); if (version_compare($cNginxVer, '1.25.1', '>=')) { // at least 1.25.1 $this->http2_on_directive = true; } } public function createVirtualHosts() { return; } public function createFileDirOptions() { return; } public function createIpPort() { $this->createLogformatEntry(); $result_ipsandports_stmt = Database::query(" SELECT * FROM `" . TABLE_PANEL_IPSANDPORTS . "` ORDER BY `ip` ASC, `port` ASC "); while ($row_ipsandports = $result_ipsandports_stmt->fetch(PDO::FETCH_ASSOC)) { if (filter_var($row_ipsandports['ip'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $ip = '[' . $row_ipsandports['ip'] . ']'; } else { $ip = $row_ipsandports['ip']; } $port = $row_ipsandports['port']; FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'nginx::createIpPort: creating ip/port settings for ' . $ip . ":" . $port); $vhost_filename = FileDir::makeCorrectFile(Settings::Get('system.apacheconf_vhost') . '/10_froxlor_ipandport_' . trim(str_replace(':', '.', $row_ipsandports['ip']), '.') . '.' . $row_ipsandports['port'] . '.conf'); if (!isset($this->nginx_data[$vhost_filename])) { $this->nginx_data[$vhost_filename] = ''; } if ($row_ipsandports['vhostcontainer'] == '1') { $this->nginx_data[$vhost_filename] .= 'server { ' . "\n"; $mypath = $this->getMyPath($row_ipsandports); // check for ssl before anything else so // we know whether it's an ssl vhost or not $ssl_vhost = false; if ($row_ipsandports['ssl'] == '1') { // check for required fallback if (($row_ipsandports['ssl_cert_file'] == '' || !file_exists($row_ipsandports['ssl_cert_file'])) && (Settings::Get('system.le_froxlor_enabled') == '0' || $this->froxlorVhostHasLetsEncryptCert() == false)) { $row_ipsandports['ssl_cert_file'] = Settings::Get('system.ssl_cert_file'); if (!file_exists($row_ipsandports['ssl_cert_file'])) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'System certificate file "' . Settings::Get('system.ssl_cert_file') . '" does not seem to exist. Creating self-signed certificate...'); Crypt::createSelfSignedCertificate(); } } if ($row_ipsandports['ssl_key_file'] == '') { $row_ipsandports['ssl_key_file'] = Settings::Get('system.ssl_key_file'); if (!file_exists($row_ipsandports['ssl_key_file'])) { // explicitly disable ssl for this vhost $row_ipsandports['ssl_cert_file'] = ""; FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'System certificate key-file "' . Settings::Get('system.ssl_key_file') . '" does not seem to exist. Disabling SSL-vhost for "' . Settings::Get('system.hostname') . '"'); } } if ($row_ipsandports['ssl_ca_file'] == '') { $row_ipsandports['ssl_ca_file'] = Settings::Get('system.ssl_ca_file'); } if ($row_ipsandports['ssl_cert_file'] != '' && file_exists($row_ipsandports['ssl_cert_file'])) { $ssl_vhost = true; } $domain = [ 'id' => 0, 'domain' => Settings::Get('system.hostname'), 'adminid' => 1, /* first admin-user (superadmin) */ 'loginname' => 'froxlor.panel', 'documentroot' => $mypath, 'customerroot' => $mypath, 'parentdomainid' => 0 ]; // override corresponding array values $domain['ssl_cert_file'] = $row_ipsandports['ssl_cert_file']; $domain['ssl_key_file'] = $row_ipsandports['ssl_key_file']; $domain['ssl_ca_file'] = $row_ipsandports['ssl_ca_file']; $domain['ssl_cert_chainfile'] = $row_ipsandports['ssl_cert_chainfile']; // SSL STUFF $dssl = new DomainSSL(); // this sets the ssl-related array-indices in the $domain array // if the domain has customer-defined ssl-certificates $dssl->setDomainSSLFilesArray($domain); if ($domain['ssl_cert_file'] != '' && file_exists($domain['ssl_cert_file'])) { // override corresponding array values $row_ipsandports['ssl_cert_file'] = $domain['ssl_cert_file']; $row_ipsandports['ssl_key_file'] = $domain['ssl_key_file']; $row_ipsandports['ssl_ca_file'] = $domain['ssl_ca_file']; $row_ipsandports['ssl_cert_chainfile'] = $domain['ssl_cert_chainfile']; $ssl_vhost = true; } } $http2 = $ssl_vhost == true && Settings::Get('system.http2_support') == '1'; $http3 = $ssl_vhost == true && Settings::Get('system.http3_support') == '1'; /** * this HAS to be set for the default host in nginx or else no vhost will work */ $this->nginx_data[$vhost_filename] .= "\t" . 'listen ' . $ip . ':' . $port . ' default_server' . ($ssl_vhost == true ? ' ssl' : '') . ($http2 && !$this->http2_on_directive ? ' http2' : '') . ';' . "\n"; if ($http2 && $this->http2_on_directive) { $this->nginx_data[$vhost_filename] .= "\t" . 'http2 on;' . "\n"; } if ($http3) { $this->nginx_data[$vhost_filename] .= "\t" . 'listen ' . $ip . ':' . $port . ' default_server quic reuseport;' . "\n"; $this->nginx_data[$vhost_filename] .= "\t" . 'http3 on;' . "\n"; $this->nginx_data[$vhost_filename] .= "\t" . 'quic_gso on;' . "\n"; $this->nginx_data[$vhost_filename] .= "\t" . 'quic_retry on;' . "\n"; $this->nginx_data[$vhost_filename] .= "\t" . 'add_header Alt-Svc \'h3=":' . $port . '"; ma=86400\' always;' . "\n"; } $this->nginx_data[$vhost_filename] .= "\t" . '# Froxlor default vhost' . "\n"; $aliases = ""; $froxlor_aliases = Settings::Get('system.froxloraliases'); if (!empty($froxlor_aliases)) { $froxlor_aliases = explode(",", $froxlor_aliases); foreach ($froxlor_aliases as $falias) { if (Validate::validateDomain(trim($falias))) { $aliases .= trim($falias) . " "; } } $aliases = " " . trim($aliases); } $this->nginx_data[$vhost_filename] .= "\t" . 'server_name ' . Settings::Get('system.hostname') . $aliases . ';' . "\n"; $logtype = 'combined'; if (Settings::Get('system.logfiles_format') != '') { $logtype = 'frx_custom'; } $this->nginx_data[$vhost_filename] .= "\t" . 'access_log /var/log/nginx/access.log ' . $logtype . ';' . "\n"; if (Settings::Get('system.use_ssl') == '1' && Settings::Get('system.leenabled') == '1' && Settings::Get('system.le_froxlor_enabled') == '1') { $acmeConfFilename = Settings::Get('system.letsencryptacmeconf'); $this->nginx_data[$vhost_filename] .= "\t" . 'include ' . $acmeConfFilename . ';' . "\n"; } $is_redirect = false; // check for SSL redirect if ($row_ipsandports['ssl'] == '0' && Settings::Get('system.le_froxlor_redirect') == '1') { $is_redirect = true; // check whether froxlor uses Let's Encrypt and not cert is being generated yet // or a renewal is ongoing - disable redirect if (Settings::Get('system.leenabled') == '1' && Settings::Get('system.le_froxlor_enabled') && ($this->froxlorVhostHasLetsEncryptCert() == false || $this->froxlorVhostLetsEncryptNeedsRenew())) { $this->nginx_data[$vhost_filename] .= '# temp. disabled ssl-redirect due to Let\'s Encrypt certificate generation.' . PHP_EOL; $is_redirect = false; Cronjob::inserttask(TaskId::REBUILD_VHOST); } else { $_sslport = $this->checkAlternativeSslPort(); $mypath = 'https://' . Settings::Get('system.hostname') . $_sslport; $this->nginx_data[$vhost_filename] .= "\t" . 'location / {' . "\n"; $this->nginx_data[$vhost_filename] .= "\t\t" . 'return 301 ' . $mypath . '$request_uri;' . "\n"; $this->nginx_data[$vhost_filename] .= "\t" . '}' . "\n"; } } if (!$is_redirect) { $this->nginx_data[$vhost_filename] .= "\t" . 'root ' . $mypath . ';' . "\n"; $this->nginx_data[$vhost_filename] .= "\t" . 'index index.php index.html index.htm;' . "\n\n"; $this->nginx_data[$vhost_filename] .= "\t" . 'location / {' . "\n"; $this->nginx_data[$vhost_filename] .= "\t" . '}' . "\n"; if (Settings::Get('system.froxlordirectlyviahostname')) { $relpath = "/"; } else { $relpath = "/".basename(Froxlor::getInstallDir()); } // protect lib/userdata.inc.php $this->nginx_data[$vhost_filename] .= "\t" . 'location = ' . rtrim($relpath, "/") . '/lib/userdata.inc.php {' . "\n"; $this->nginx_data[$vhost_filename] .= "\t" . ' deny all;' . "\n"; $this->nginx_data[$vhost_filename] .= "\t" . '}' . "\n"; // protect bin/ $this->nginx_data[$vhost_filename] .= "\t" . 'location ~ ^' . rtrim($relpath, "/") . '/(bin|cache|logs|tests|vendor) {' . "\n"; $this->nginx_data[$vhost_filename] .= "\t" . ' deny all;' . "\n"; $this->nginx_data[$vhost_filename] .= "\t" . '}' . "\n"; } if ($row_ipsandports['specialsettings'] != '' && ($row_ipsandports['ssl'] == '0' || ($row_ipsandports['ssl'] == '1' && Settings::Get('system.use_ssl') == '1' && $row_ipsandports['include_specialsettings'] == '1'))) { $this->nginx_data[$vhost_filename] .= $this->processSpecialConfigTemplate($row_ipsandports['specialsettings'], [ 'domain' => Settings::Get('system.hostname'), 'loginname' => Settings::Get('phpfpm.vhost_httpuser'), 'documentroot' => $mypath, 'customerroot' => $mypath ], $row_ipsandports['ip'], $row_ipsandports['port'], $row_ipsandports['ssl'] == '1') . "\n"; } /** * SSL config options */ if ($row_ipsandports['ssl'] == '1') { $row_ipsandports['domain'] = Settings::Get('system.hostname'); $row_ipsandports['ssl_honorcipherorder'] = Settings::Get('system.honorcipherorder'); $row_ipsandports['ssl_sessiontickets'] = Settings::Get('system.sessiontickets'); $this->nginx_data[$vhost_filename] .= $this->composeSslSettings($row_ipsandports); if ($row_ipsandports['ssl_specialsettings'] != '') { $this->nginx_data[$vhost_filename] .= $this->processSpecialConfigTemplate($row_ipsandports['ssl_specialsettings'], [ 'domain' => Settings::Get('system.hostname'), 'loginname' => Settings::Get('phpfpm.vhost_httpuser'), 'documentroot' => $mypath, 'customerroot' => $mypath ], $row_ipsandports['ip'], $row_ipsandports['port'], $row_ipsandports['ssl'] == '1') . "\n"; } } if (!$is_redirect) { $this->nginx_data[$vhost_filename] .= "\tlocation ~ \.php {\n"; $this->nginx_data[$vhost_filename] .= "\t\tfastcgi_split_path_info ^(.+?\.php)(/.*)$;\n"; $this->nginx_data[$vhost_filename] .= "\t\tinclude " . Settings::Get('nginx.fastcgiparams') . ";\n"; $this->nginx_data[$vhost_filename] .= "\t\tfastcgi_param SCRIPT_FILENAME \$request_filename;\n"; $this->nginx_data[$vhost_filename] .= "\t\ttry_files \$fastcgi_script_name =404;\n"; $this->nginx_data[$vhost_filename] .= "\t\tset \$path_info \$fastcgi_path_info;\n"; $this->nginx_data[$vhost_filename] .= "\t\tfastcgi_param PATH_INFO \$path_info;\n"; if ($row_ipsandports['ssl'] == '1') { $this->nginx_data[$vhost_filename] .= "\t\tfastcgi_param HTTPS on;\n"; } if ((int)Settings::Get('phpfpm.enabled') == 1 && (int)Settings::Get('phpfpm.enabled_ownvhost') == 1) { // get fpm config $fpm_sel_stmt = Database::prepare(" SELECT f.id FROM `" . TABLE_PANEL_FPMDAEMONS . "` f LEFT JOIN `" . TABLE_PANEL_PHPCONFIGS . "` p ON p.fpmsettingid = f.id WHERE p.id = :phpconfigid "); $fpm_config = Database::pexecute_first($fpm_sel_stmt, [ 'phpconfigid' => Settings::Get('phpfpm.vhost_defaultini') ]); $domain = [ 'id' => 'none', 'domain' => Settings::Get('system.hostname'), 'adminid' => 1, /* first admin-user (superadmin) */ 'mod_fcgid_starter' => -1, 'mod_fcgid_maxrequests' => -1, 'guid' => Settings::Get('phpfpm.vhost_httpuser'), 'openbasedir' => 0, 'email' => Settings::Get('panel.adminmail'), 'loginname' => 'froxlor.panel', 'documentroot' => $mypath, 'customerroot' => $mypath, 'fpm_config_id' => isset($fpm_config['id']) ? $fpm_config['id'] : 1 ]; $php = new PhpInterface($domain); $this->nginx_data[$vhost_filename] .= "\t\tfastcgi_pass unix:" . $php->getInterface()->getSocketFile() . ";\n"; } else { $this->nginx_data[$vhost_filename] .= "\t\tfastcgi_pass " . Settings::Get('system.nginx_php_backend') . ";\n"; } $this->nginx_data[$vhost_filename] .= "\t\tfastcgi_index index.php;\n"; $this->nginx_data[$vhost_filename] .= "\t}\n"; } $this->nginx_data[$vhost_filename] .= "}\n\n"; // End of Froxlor server{}-part } } $this->createNginxHosts(); /** * standard error pages */ $this->createStandardErrorHandler(); } private function createLogformatEntry() { if (Settings::Get('system.logfiles_format') != '') { $vhosts_folder = ''; if (is_dir(Settings::Get('system.apacheconf_vhost'))) { $vhosts_folder = FileDir::makeCorrectDir(Settings::Get('system.apacheconf_vhost')); } else { $vhosts_folder = FileDir::makeCorrectDir(dirname(Settings::Get('system.apacheconf_vhost'))); } $vhosts_filename = FileDir::makeCorrectFile($vhosts_folder . '/02_froxlor_logfiles_format.conf'); if (!isset($this->nginx_data[$vhosts_filename])) { $this->nginx_data[$vhosts_filename] = ''; } $logtype = 'frx_custom'; $this->nginx_data[$vhosts_filename] = 'log_format ' . $logtype . ' ' . Settings::Get('system.logfiles_format') . ';' . "\n"; } } protected function composeSslSettings($domain_or_ip) { $sslsettings = ''; if ($domain_or_ip['ssl_cert_file'] == '' || !file_exists($domain_or_ip['ssl_cert_file'])) { $domain_or_ip['ssl_cert_file'] = Settings::Get('system.ssl_cert_file'); if (!file_exists($domain_or_ip['ssl_cert_file'])) { // explicitly disable ssl for this vhost $domain_or_ip['ssl_cert_file'] = ""; FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'System certificate file "' . Settings::Get('system.ssl_cert_file') . '" does not seem to exist. Disabling SSL-vhost for "' . $domain_or_ip['domain'] . '"'); } } if ($domain_or_ip['ssl_key_file'] == '' || !file_exists($domain_or_ip['ssl_key_file'])) { // use fallback $domain_or_ip['ssl_key_file'] = Settings::Get('system.ssl_key_file'); // check whether it exists if (!file_exists($domain_or_ip['ssl_key_file'])) { // explicitly disable ssl for this vhost $domain_or_ip['ssl_cert_file'] = ""; FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'System certificate key-file "' . Settings::Get('system.ssl_key_file') . '" does not seem to exist. Disabling SSL-vhost for "' . $domain_or_ip['domain'] . '"'); } } if ($domain_or_ip['ssl_ca_file'] == '') { $domain_or_ip['ssl_ca_file'] = Settings::Get('system.ssl_ca_file'); } // #418 if ($domain_or_ip['ssl_cert_chainfile'] == '') { $domain_or_ip['ssl_cert_chainfile'] = Settings::Get('system.ssl_cert_chainfile'); } if ($domain_or_ip['ssl_cert_file'] != '') { // check for existence, #1485 if (!file_exists($domain_or_ip['ssl_cert_file'])) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, $domain_or_ip['domain'] . ' :: certificate file "' . $domain_or_ip['ssl_cert_file'] . '" does not exist! Cannot create ssl-directives'); } else { $ssl_protocols = (isset($domain_or_ip['override_tls']) && $domain_or_ip['override_tls'] == '1' && !empty($domain_or_ip['ssl_protocols'])) ? $domain_or_ip['ssl_protocols'] : Settings::Get('system.ssl_protocols'); $ssl_cipher_list = (isset($domain_or_ip['override_tls']) && $domain_or_ip['override_tls'] == '1' && !empty($domain_or_ip['ssl_cipher_list'])) ? $domain_or_ip['ssl_cipher_list'] : Settings::Get('system.ssl_cipher_list'); // obsolete: ssl on now belongs to the listen block as 'ssl' at the end // $sslsettings .= "\t" . 'ssl on;' . "\n"; $sslsettings .= "\t" . 'ssl_protocols ' . str_replace(",", " ", $ssl_protocols) . ';' . "\n"; $sslsettings .= "\t" . 'ssl_ciphers ' . $ssl_cipher_list . ';' . "\n"; if (!empty(Settings::Get('system.dhparams_file'))) { $dhparams = FileDir::makeCorrectFile(Settings::Get('system.dhparams_file')); if (!file_exists($dhparams)) { file_put_contents($dhparams, self::FFDHE4096); } $sslsettings .= "\t" . 'ssl_dhparam ' . $dhparams . ';' . "\n"; } // When <1.11.0: Defaults to prime256v1, similar to first curve recommendation by Mozilla. // (When specifyng just one, there's no fallback when specific curve is not supported by client.) // When >1.11.0: Defaults to auto, using recommended curves provided by OpenSSL. // see https://github.com/Froxlor/Froxlor/issues/652 // $sslsettings .= "\t" . 'ssl_ecdh_curve secp384r1;' . "\n"; $sslsettings .= "\t" . 'ssl_prefer_server_ciphers ' . (isset($domain_or_ip['ssl_honorcipherorder']) && $domain_or_ip['ssl_honorcipherorder'] == '1' ? 'on' : 'off') . ';' . "\n"; if (Settings::Get('system.sessionticketsenabled') == '1') { $sslsettings .= "\t" . 'ssl_session_tickets ' . (isset($domain_or_ip['ssl_sessiontickets']) && $domain_or_ip['ssl_sessiontickets'] == '1' ? 'on' : 'off') . ';' . "\n"; } $sslsettings .= "\t" . 'ssl_session_cache shared:SSL:10m;' . "\n"; $sslsettings .= "\t" . 'ssl_certificate ' . FileDir::makeCorrectFile($domain_or_ip['ssl_cert_file']) . ';' . "\n"; if ($domain_or_ip['ssl_key_file'] != '') { // check for existence, #1485 if (!file_exists($domain_or_ip['ssl_key_file'])) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, $domain_or_ip['domain'] . ' :: certificate key file "' . $domain_or_ip['ssl_key_file'] . '" does not exist! Cannot create ssl-directives'); } else { $sslsettings .= "\t" . 'ssl_certificate_key ' . FileDir::makeCorrectFile($domain_or_ip['ssl_key_file']) . ';' . "\n"; } } if (isset($domain_or_ip['hsts']) && $domain_or_ip['hsts'] >= 0) { $sslsettings .= 'add_header Strict-Transport-Security "max-age=' . $domain_or_ip['hsts']; if ($domain_or_ip['hsts_sub'] == 1) { $sslsettings .= '; includeSubDomains'; } if ($domain_or_ip['hsts_preload'] == 1) { $sslsettings .= '; preload'; } $sslsettings .= '" always;' . "\n"; } if ((isset($domain_or_ip['ocsp_stapling']) && $domain_or_ip['ocsp_stapling'] == "1")) { $sslsettings .= "\t" . 'ssl_stapling on;' . "\n"; $sslsettings .= "\t" . 'ssl_stapling_verify on;' . "\n"; $sslsettings .= "\t" . 'ssl_trusted_certificate ' . FileDir::makeCorrectFile($domain_or_ip['ssl_cert_file']) . ';' . "\n"; } } } return $sslsettings; } /** * create vhosts */ protected function createNginxHosts() { $domains = WebserverBase::getVhostsToCreate(); foreach ($domains as $domain) { if (is_dir(Settings::Get('system.apacheconf_vhost'))) { FileDir::safe_exec('mkdir -p ' . escapeshellarg(FileDir::makeCorrectDir(Settings::Get('system.apacheconf_vhost')))); } $vhost_filename = $this->getVhostFilename($domain); if (!isset($this->nginx_data[$vhost_filename])) { $this->nginx_data[$vhost_filename] = ''; } if ((empty($this->nginx_data[$vhost_filename]) && !is_dir(Settings::Get('system.apacheconf_vhost'))) || is_dir(Settings::Get('system.apacheconf_vhost'))) { $domain['nonexistinguri'] = '/' . md5(uniqid(microtime(), 1)) . '.htm'; // Create non-ssl host $this->nginx_data[$vhost_filename] .= $this->getVhostContent($domain, false); if ($domain['ssl'] == '1' || $domain['ssl_redirect'] == '1') { $vhost_filename_ssl = $this->getVhostFilename($domain, true); if (!isset($this->nginx_data[$vhost_filename_ssl])) { $this->nginx_data[$vhost_filename_ssl] = ''; } // Now enable ssl stuff $this->nginx_data[$vhost_filename_ssl] .= $this->getVhostContent($domain, true); } } } } protected function getVhostContent($domain, $ssl_vhost = false) { if ($ssl_vhost === true && $domain['ssl'] != '1' && $domain['ssl_redirect'] != '1') { return ''; } // check whether the customer/domain is deactivated and NO docroot for deactivated users has been set# $ddr = Settings::Get('system.deactivateddocroot'); if (($domain['deactivated'] == '1' || $domain['customer_deactivated'] == '1') && empty($ddr)) { return '# Customer deactivated and a docroot for deactivated users/domains hasn\'t been set.' . "\n"; } $vhost_content = ''; $_vhost_content = ''; $has_http2_on = false; $query = "SELECT * FROM `" . TABLE_PANEL_IPSANDPORTS . "` `i`, `" . TABLE_DOMAINTOIP . "` `dip` WHERE dip.id_domain = :domainid AND i.id = dip.id_ipandports "; if ($ssl_vhost === true && ($domain['ssl'] == '1' || $domain['ssl_redirect'] == '1')) { // by ordering by cert-file the row with filled out SSL-Fields will be shown last, // thus it is enough to fill out 1 set of SSL-Fields $query .= "AND i.ssl = 1 ORDER BY i.ssl_cert_file ASC;"; } else { $query .= "AND i.ssl = '0';"; } // start vhost $vhost_content .= 'server { ' . "\n"; $result_stmt = Database::prepare($query); Database::pexecute($result_stmt, [ 'domainid' => $domain['id'] ]); $http3 = $ssl_vhost == true && (isset($domain['http3']) && $domain['http3'] == '1' && Settings::Get('system.http3_support') == '1'); while ($ipandport = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $domain['ip'] = $ipandport['ip']; $domain['port'] = $ipandport['port']; if ($domain['ssl'] == '1') { $domain['ssl_cert_file'] = $ipandport['ssl_cert_file']; $domain['ssl_key_file'] = $ipandport['ssl_key_file']; $domain['ssl_ca_file'] = $ipandport['ssl_ca_file']; $domain['ssl_cert_chainfile'] = $ipandport['ssl_cert_chainfile']; // SSL STUFF $dssl = new DomainSSL(); // this sets the ssl-related array-indices in the $domain array // if the domain has customer-defined ssl-certificates $dssl->setDomainSSLFilesArray($domain); } if (filter_var($domain['ip'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $ipport = '[' . $domain['ip'] . ']:' . $domain['port']; } else { $ipport = $domain['ip'] . ':' . $domain['port']; } if ($ipandport['default_vhostconf_domain'] != '' && ($ssl_vhost == false || ($ssl_vhost == true && $ipandport['include_default_vhostconf_domain'] == '1'))) { $_vhost_content .= $this->processSpecialConfigTemplate($ipandport['default_vhostconf_domain'], $domain, $domain['ip'], $domain['port'], $ssl_vhost) . "\n"; } if ($ipandport['ssl_default_vhostconf_domain'] != '' && $ssl_vhost == true) { $_vhost_content .= $this->processSpecialConfigTemplate($ipandport['ssl_default_vhostconf_domain'], $domain, $domain['ip'], $domain['port'], $ssl_vhost) . "\n"; } $http2 = $ssl_vhost == true && (isset($domain['http2']) && $domain['http2'] == '1' && Settings::Get('system.http2_support') == '1'); $vhost_content .= "\t" . 'listen ' . $ipport . ($ssl_vhost == true ? ' ssl' : '') . ($http2 && !$this->http2_on_directive ? ' http2' : '') . ';' . "\n"; if ($http2 && $this->http2_on_directive && !$has_http2_on) { $vhost_content .= "\t" . 'http2 on;' . "\n"; $has_http2_on = true; } if ($http3) { $vhost_content .= "\t" . 'listen ' . $ipport . ' quic;' . "\n"; } } if ($http3) { $vhost_content .= "\t" . 'add_header Alt-Svc \'h3=":' . $domain['port'] . '"; ma=86400\' always;' . "\n"; $vhost_content .= "\t" . 'http3 on;' . "\n"; $vhost_content .= "\t" . 'quic_gso on;' . "\n"; $vhost_content .= "\t" . 'quic_retry on;' . "\n"; } // get all server-names $vhost_content .= $this->getServerNames($domain); // respect ssl_redirect settings, #542 if ($ssl_vhost == false && $domain['ssl'] == '1' && $domain['ssl_redirect'] == '1') { // We must not check if our port differs from port 443, // but if there is a destination-port != 443 $_sslport = ''; // This returns the first port that is != 443 with ssl enabled, if any // ordered by ssl-certificate (if any) so that the ip/port combo // with certificate is used $ssldestport_stmt = Database::prepare("SELECT `ip`.`port` FROM " . TABLE_PANEL_IPSANDPORTS . " `ip` LEFT JOIN `" . TABLE_DOMAINTOIP . "` `dip` ON (`ip`.`id` = `dip`.`id_ipandports`) WHERE `dip`.`id_domain` = :domainid AND `ip`.`ssl` = '1' AND `ip`.`port` != 443 ORDER BY `ip`.`ssl_cert_file` DESC, `ip`.`port` LIMIT 1;"); $ssldestport = Database::pexecute_first($ssldestport_stmt, [ 'domainid' => $domain['id'] ]); if ($ssldestport && $ssldestport['port'] != '') { $_sslport = ":" . $ssldestport['port']; } $domain['documentroot'] = 'https://$host' . $_sslport . '/'; } // avoid using any whitespaces $domain['documentroot'] = trim($domain['documentroot']); // create ssl settings first since they are required for normal and redirect vhosts if ($ssl_vhost === true && $domain['ssl'] == '1' && Settings::Get('system.use_ssl') == '1') { $vhost_content .= "\n" . $this->composeSslSettings($domain) . "\n"; } if (Settings::Get('system.use_ssl') == '1' && Settings::Get('system.leenabled') == '1') { $acmeConfFilename = Settings::Get('system.letsencryptacmeconf'); $vhost_content .= "\t" . 'include ' . $acmeConfFilename . ';' . "\n"; } // if the documentroot is an URL we just redirect if (preg_match('/^https?\:\/\//', $domain['documentroot'])) { $possible_deactivated_webroot = $this->getWebroot($domain); if ($this->deactivated == false) { $uri = $domain['documentroot']; if (substr($uri, -1) == '/') { $uri = substr($uri, 0, -1); } // Get domain's redirect code $code = Domain::getDomainRedirectCode($domain['id']); $vhost_content .= $this->getLogFiles($domain); $vhost_content .= "\t" . 'location / {' . "\n"; $vhost_content .= "\t\t" . 'return ' . $code . ' ' . $uri . '$request_uri;' . "\n"; $vhost_content .= "\t" . '}' . "\n"; } elseif (Settings::Get('system.deactivateddocroot') != '') { $vhost_content .= $possible_deactivated_webroot; } } else { FileDir::mkDirWithCorrectOwnership($domain['customerroot'], $domain['documentroot'], $domain['guid'], $domain['guid'], true); $vhost_content .= $this->getLogFiles($domain); $vhost_content .= $this->getWebroot($domain); if ($this->deactivated == false) { $vhost_content = $this->mergeVhostCustom($vhost_content, $this->createPathOptions($domain)) . "\n"; $vhost_content .= $this->composePhpOptions($domain, $ssl_vhost); $vhost_content .= isset($this->needed_htpasswds[$domain['id']]) ? $this->needed_htpasswds[$domain['id']] . "\n" : ''; if ($domain['specialsettings'] != '' && ($ssl_vhost == false || ($ssl_vhost == true && $domain['include_specialsettings'] == 1))) { $vhost_content = $this->mergeVhostCustom($vhost_content, $this->processSpecialConfigTemplate($domain['specialsettings'], $domain, $domain['ip'], $domain['port'], $ssl_vhost)); } if ($domain['ssl_specialsettings'] != '' && $ssl_vhost == true) { $vhost_content = $this->mergeVhostCustom($vhost_content, $this->processSpecialConfigTemplate($domain['ssl_specialsettings'], $domain, $domain['ip'], $domain['port'], $ssl_vhost)); } if ($_vhost_content != '') { $vhost_content = $this->mergeVhostCustom($vhost_content, $_vhost_content); } if (Settings::Get('system.default_vhostconf') != '' && ($ssl_vhost == false || ($ssl_vhost == true && Settings::Get('system.include_default_vhostconf') == 1))) { $vhost_content = $this->mergeVhostCustom($vhost_content, $this->processSpecialConfigTemplate(Settings::Get('system.default_vhostconf'), $domain, $domain['ip'], $domain['port'], $ssl_vhost) . "\n"); } if (Settings::Get('system.default_sslvhostconf') != '' && $ssl_vhost == true) { $vhost_content = $this->mergeVhostCustom($vhost_content, $this->processSpecialConfigTemplate(Settings::Get('system.default_sslvhostconf'), $domain, $domain['ip'], $domain['port'], $ssl_vhost) . "\n"); } } } $vhost_content .= "\n}\n\n"; return $vhost_content; } protected function getServerNames($domain) { $server_alias = ''; if ($domain['iswildcarddomain'] == '1') { $server_alias = '*.' . $domain['domain']; } elseif ($domain['wwwserveralias'] == '1') { $server_alias = 'www.' . $domain['domain']; } $alias_domains_stmt = Database::prepare(" SELECT `domain`, `iswildcarddomain`, `wwwserveralias` FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `aliasdomain` = :domainid "); Database::pexecute($alias_domains_stmt, [ 'domainid' => $domain['id'] ]); while (($alias_domain = $alias_domains_stmt->fetch(PDO::FETCH_ASSOC)) !== false) { $server_alias .= ' ' . $alias_domain['domain']; if ($alias_domain['iswildcarddomain'] == '1') { $server_alias .= ' *.' . $alias_domain['domain']; } elseif ($alias_domain['wwwserveralias'] == '1') { $server_alias .= ' www.' . $alias_domain['domain']; } } $servernames_text = "\t" . 'server_name ' . $domain['domain']; if (trim($server_alias) != '') { $servernames_text .= ' ' . $server_alias; } $servernames_text .= ';' . "\n"; return $servernames_text; } protected function getLogFiles($domain) { $logfiles_text = ''; $speciallogfile = ''; if ($domain['speciallogfile'] == '1') { if ($domain['parentdomainid'] == '0') { $speciallogfile = '-' . $domain['domain']; } else { $speciallogfile = '-' . $domain['parentdomain']; } } if ($domain['writeerrorlog']) { // The normal access/error - logging is enabled $error_log = FileDir::makeCorrectFile(Settings::Get('system.logfiles_directory') . $domain['loginname'] . $speciallogfile . '-error.log'); // Create the logfile if it does not exist (fixes #46) touch($error_log); chmod($error_log, 0640); chown($error_log, Settings::Get('system.httpuser')); chgrp($error_log, Settings::Get('system.httpgroup')); } else { $error_log = '/dev/null'; } if ($domain['writeaccesslog']) { $access_log = FileDir::makeCorrectFile(Settings::Get('system.logfiles_directory') . $domain['loginname'] . $speciallogfile . '-access.log'); // Create the logfile if it does not exist (fixes #46) touch($access_log); chmod($access_log, 0640); chown($access_log, Settings::Get('system.httpuser')); chgrp($access_log, Settings::Get('system.httpgroup')); } else { $access_log = '/dev/null'; } $logtype = 'combined'; if (Settings::Get('system.logfiles_format') != '') { $logtype = 'frx_custom'; } $logfiles_text .= "\t" . 'access_log ' . $access_log . ' ' . $logtype . ';' . "\n"; $logfiles_text .= "\t" . 'error_log ' . $error_log . ' ' . Settings::Get('system.errorlog_level') . ';' . "\n"; if (Settings::Get('system.traffictool') == 'awstats') { if ((int)$domain['parentdomainid'] == 0) { // prepare the aliases and subdomains for stats config files $server_alias = ''; $alias_domains_stmt = Database::prepare(" SELECT `domain`, `iswildcarddomain`, `wwwserveralias` FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `aliasdomain` = :domainid OR `parentdomainid` = :domainid "); Database::pexecute($alias_domains_stmt, [ 'domainid' => $domain['id'] ]); while (($alias_domain = $alias_domains_stmt->fetch(PDO::FETCH_ASSOC)) !== false) { $server_alias .= ' ' . $alias_domain['domain'] . ' '; if ($alias_domain['iswildcarddomain'] == '1') { $server_alias .= '*.' . $domain['domain']; } else { if ($alias_domain['wwwserveralias'] == '1') { $server_alias .= 'www.' . $alias_domain['domain']; } else { $server_alias .= ''; } } } $alias = ''; if ($domain['iswildcarddomain'] == '1') { $alias = '*.' . $domain['domain']; } elseif ($domain['wwwserveralias'] == '1') { $alias = 'www.' . $domain['domain']; } // After inserting the AWStats information, // be sure to build the awstats conf file as well // and chown it using $awstats_params, #258 // Bug 960 + Bug 970 : Use full $domain instead of custom $awstats_params as following classes depend on the information Statistics::createAWStatsConf(Settings::Get('system.logfiles_directory') . $domain['loginname'] . $speciallogfile . '-access.log', $domain['domain'], $alias . $server_alias, $domain['customerroot'], $domain); } } return $logfiles_text; } protected function getWebroot($domain) { $webroot_text = ''; if (($domain['deactivated'] == '1' || $domain['customer_deactivated'] == '1' ) && Settings::Get('system.deactivateddocroot') != '') { $webroot_text .= "\t" . '# Using docroot for deactivated users/domains...' . "\n"; $webroot_text .= "\t" . 'root ' . FileDir::makeCorrectDir(Settings::Get('system.deactivateddocroot')) . ';' . "\n"; $this->deactivated = true; } else { $webroot_text .= "\t" . 'root ' . FileDir::makeCorrectDir($domain['documentroot']) . ';' . "\n"; $this->deactivated = false; } $webroot_text .= "\n\t" . 'location / {' . "\n"; if ($domain['phpenabled_customer'] == 1 && $domain['phpenabled_vhost'] == '1') { $webroot_text .= "\t" . 'index index.php index.html index.htm;' . "\n"; if ($domain['notryfiles'] != 1) { $webroot_text .= "\t\t" . 'try_files $uri $uri/ @rewrites;' . "\n"; } } else { $webroot_text .= "\t" . 'index index.html index.htm;' . "\n"; } if ($this->vhost_root_autoindex) { $webroot_text .= "\t\t" . 'autoindex on;' . "\n"; $this->vhost_root_autoindex = false; } $webroot_text .= "\t" . '}' . "\n\n"; if ($domain['phpenabled_customer'] == 1 && $domain['phpenabled_vhost'] == '1' && $domain['notryfiles'] != 1) { $webroot_text .= "\tlocation @rewrites {\n"; $webroot_text .= "\t\trewrite ^ /index.php last;\n"; $webroot_text .= "\t}\n\n"; } return $webroot_text; } protected function mergeVhostCustom($vhost_frx, $vhost_usr) { // Clean froxlor defined settings $vhost_frx = $this->cleanVhostStruct($vhost_frx); // Clean user defined settings $vhost_usr = $this->cleanVhostStruct($vhost_usr); // Cycle through the user defined settings $currentBlock = []; $blockLevel = 0; foreach ($vhost_usr as $line) { $line = trim($line); $currentBlock[] = $line; if (strpos($line, "{") !== false) { $blockLevel++; } if (strpos($line, "}") !== false && $blockLevel > 0) { $blockLevel--; } if ($line == "}" && $blockLevel == 0) { if (in_array($currentBlock[0], $vhost_frx)) { // Add to existing block $pos = array_search($currentBlock[0], $vhost_frx); do { $pos++; } while ($vhost_frx[$pos] != "}"); for ($i = 1; $i < count($currentBlock) - 1; $i++) { array_splice($vhost_frx, $pos + $i - 1, 0, $currentBlock[$i]); } } else { // Add to end array_splice($vhost_frx, count($vhost_frx), 0, $currentBlock); } $currentBlock = []; } elseif ($blockLevel == 0) { array_splice($vhost_frx, count($vhost_frx), 0, $currentBlock); $currentBlock = []; } } $nextLevel = 0; for ($i = 0; $i < count($vhost_frx); $i++) { if (substr_count($vhost_frx[$i], "}") != 0 && substr_count($vhost_frx[$i], "{") == 0) { $nextLevel -= 1; $vhost_frx[$i] .= "\n"; } if ($nextLevel > 0) { for ($j = 0; $j < $nextLevel; $j++) { $vhost_frx[$i] = " " . $vhost_frx[$i]; } } if (substr_count($vhost_frx[$i], "{") != 0 && substr_count($vhost_frx[$i], "}") == 0) { $nextLevel += 1; } } return implode("\n", $vhost_frx); } private function cleanVhostStruct($vhost = null) { // Remove windows linebreaks $vhost = str_replace("\r", "\n", $vhost); // remove comments $vhost = implode("\n", preg_replace('/^(\s+)?#(.*)$/', '', explode("\n", $vhost))); // Break blocks into lines $vhost = preg_replace("/^(\s+)?location(.+)\{(.+)\}$/misU", "location $2 {\n $3 \n}", $vhost); // Break into array items $vhost = explode("\n", preg_replace('/[ \t]+/', ' ', trim(preg_replace('/\t+/', '', $vhost)))); // Remove empty lines $vhost = array_filter($vhost, function ($a) { return preg_match("#\S#", $a); }); // remove unnecessary whitespaces $vhost = array_map("trim", $vhost); // re-number array keys $vhost = array_values($vhost); return $vhost; } protected function createPathOptions($domain) { $result_stmt = Database::prepare(" SELECT * FROM " . TABLE_PANEL_HTACCESS . " WHERE `path` LIKE :docroot "); Database::pexecute($result_stmt, [ 'docroot' => $domain['documentroot'] . '%' ]); $path_options = ''; $htpasswds = $this->getHtpasswds($domain); // for each entry in the htaccess table while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { if (!empty($row['error404path'])) { $defhandler = $row['error404path']; if (!Validate::validateUrl($defhandler)) { $defhandler = FileDir::makeCorrectFile($defhandler); } $path_options .= "\t" . 'error_page 404 ' . $defhandler . ';' . "\n"; } if (!empty($row['error403path'])) { $defhandler = $row['error403path']; if (!Validate::validateUrl($defhandler)) { $defhandler = FileDir::makeCorrectFile($defhandler); } $path_options .= "\t" . 'error_page 403 ' . $defhandler . ';' . "\n"; } if (!empty($row['error500path'])) { $defhandler = $row['error500path']; if (!Validate::validateUrl($defhandler)) { $defhandler = FileDir::makeCorrectFile($defhandler); } $path_options .= "\t" . 'error_page 500 502 503 504 ' . $defhandler . ';' . "\n"; } // if ($row['options_indexes'] != '0') { $path = FileDir::makeCorrectDir(substr($row['path'], strlen($domain['documentroot']) - 1)); FileDir::mkDirWithCorrectOwnership($domain['documentroot'], $row['path'], $domain['guid'], $domain['guid']); $path_options .= "\t" . '# ' . $path . "\n"; if ($path == '/') { if ($row['options_indexes'] != '0') { $this->vhost_root_autoindex = true; } $path_options .= "\t" . 'location ' . FileDir::makeCorrectDir($path) . ' {' . "\n"; if ($this->vhost_root_autoindex) { $path_options .= "\t\t" . 'autoindex on;' . "\n"; $this->vhost_root_autoindex = false; } // check if we have a htpasswd for this path // (damn nginx does not like more than one // 'location'-part with the same path) if (count($htpasswds) > 0) { foreach ($htpasswds as $idx => $single) { switch ($single['path']) { case '/awstats/': case '/webalizer/': case '/goaccess/': // no stats-alias in "location /"-context break; default: if ($single['path'] == '/') { $path_options .= "\t\t" . 'auth_basic "' . $single['authname'] . '";' . "\n"; $path_options .= "\t\t" . 'auth_basic_user_file ' . FileDir::makeCorrectFile($single['usrf']) . ';' . "\n"; if ($domain['phpenabled_customer'] == 1 && $domain['phpenabled_vhost'] == '1') { $path_options .= "\t\t" . 'index index.php index.html index.htm;' . "\n"; } else { $path_options .= "\t\t" . 'index index.html index.htm;' . "\n"; } // remove already used entries so we do not have doubles unset($htpasswds[$idx]); } } } } $path_options .= "\t" . '}' . "\n"; $this->vhost_root_autoindex = false; } else { $path_options .= "\t" . 'location ^~ ' . FileDir::makeCorrectFile($path) . ' {' . "\n"; if ($this->vhost_root_autoindex || $row['options_indexes'] != '0') { $path_options .= "\t\t" . 'autoindex on;' . "\n"; $this->vhost_root_autoindex = false; } $path_options .= "\t" . '} ' . "\n"; } // } /** * Perl support * required the fastCGI wrapper to be running to receive the CGI requests. */ if (Customer::customerHasPerlEnabled($domain['customerid']) && $row['options_cgi'] != '0') { $path = FileDir::makeCorrectDir(substr($row['path'], strlen($domain['documentroot']) - 1)); FileDir::mkDirWithCorrectOwnership($domain['documentroot'], $row['path'], $domain['guid'], $domain['guid']); // We need to remove the last slash, otherwise the regex wouldn't work if ($row['path'] != $domain['documentroot']) { $path = substr($path, 0, -1); } $path_options .= "\t" . 'location ~ \(.pl|.cgi)$ {' . "\n"; $path_options .= "\t\t" . 'gzip off; #gzip makes scripts feel slower since they have to complete before getting gzipped' . "\n"; $path_options .= "\t\t" . 'fastcgi_pass ' . Settings::Get('system.perl_server') . ';' . "\n"; $path_options .= "\t\t" . 'fastcgi_index index.cgi;' . "\n"; $path_options .= "\t\t" . 'include ' . Settings::Get('nginx.fastcgiparams') . ';' . "\n"; $path_options .= "\t" . '}' . "\n"; } } // now the rest of the htpasswds if (count($htpasswds) > 0) { foreach ($htpasswds as $idx => $single) { // if ($single['path'] != '/') { switch ($single['path']) { case '/awstats/': case '/webalizer/': case '/goaccess/': $path_options .= $this->getStats($domain, $single); unset($htpasswds[$idx]); break; default: if ($single['path'] == '/') { $path_options .= "\t" . 'location ' . FileDir::makeCorrectDir($single['path']) . ' {' . "\n"; } else { $path_options .= "\t" . 'location ^~ ' . FileDir::makeCorrectFile($single['path']) . ' {' . "\n"; } $path_options .= "\t\t" . 'auth_basic "' . $single['authname'] . '";' . "\n"; $path_options .= "\t\t" . 'auth_basic_user_file ' . FileDir::makeCorrectFile($single['usrf']) . ';' . "\n"; if ($domain['phpenabled_customer'] == 1 && $domain['phpenabled_vhost'] == '1') { $path_options .= "\t\t" . 'index index.php index.html index.htm;' . "\n"; if ($domain['notryfiles'] != 1) { $path_options .= "\t\t" . 'location ~ ^(.+?\.php)(/.*)?$ {' . "\n"; $path_options .= "\t\t\t" . 'try_files ' . $domain['nonexistinguri'] . ' @php;' . "\n"; $path_options .= "\t\t" . '}' . "\n\n"; } } else { $path_options .= "\t\t" . 'index index.html index.htm;' . "\n"; } $path_options .= "\t" . '}' . "\n"; } // } unset($htpasswds[$idx]); } } return $path_options; } protected function getHtpasswds($domain) { $result_stmt = Database::prepare(" SELECT a.* FROM `" . TABLE_PANEL_HTPASSWDS . "` AS a JOIN `" . TABLE_PANEL_DOMAINS . "` AS b USING (`customerid`) LEFT JOIN `" . TABLE_PANEL_CUSTOMERS . "` c ON c.customerid = b.customerid WHERE b.customerid = :customerid AND b.domain = :domain AND (a.path = CONCAT(c.documentroot, :ttool, '/') OR INSTR(a.path, b.documentroot)); "); Database::pexecute($result_stmt, [ 'customerid' => $domain['customerid'], 'domain' => $domain['domain'], 'ttool' => Settings::Get('system.traffictool') ]); $returnval = []; $x = 0; while ($row_htpasswds = $result_stmt->fetch(PDO::FETCH_ASSOC)) { if (count($row_htpasswds) > 0) { $htpasswd_filename = FileDir::makeCorrectFile(Settings::Get('system.apacheconf_htpasswddir') . '/' . $row_htpasswds['customerid'] . '-' . md5($row_htpasswds['path']) . '.htpasswd'); // ensure we can write to the array with index $htpasswd_filename if (!isset($this->htpasswds_data[$htpasswd_filename])) { $this->htpasswds_data[$htpasswd_filename] = ''; } $this->htpasswds_data[$htpasswd_filename] .= $row_htpasswds['username'] . ':' . $row_htpasswds['password'] . "\n"; // if the domains and their web contents are located in a subdirectory of // the nginx user, we have to evaluate the right path which is to protect if (stripos($row_htpasswds['path'], $domain['documentroot']) !== false) { // if the website contents is located in the user directory $path = FileDir::makeCorrectDir(substr($row_htpasswds['path'], strlen($domain['documentroot']) - 1)); } else { // if the website contents is located in a subdirectory of the user $matches = []; preg_match('/^([\/[:print:]]*\/)([[:print:]\/]+){1}$/i', $row_htpasswds['path'], $matches); $path = FileDir::makeCorrectDir(substr($row_htpasswds['path'], strlen($matches[1]) - 1)); } $returnval[$x]['path'] = $path; $returnval[$x]['root'] = FileDir::makeCorrectDir($domain['documentroot']); // Ensure there is only one auth name per password block, otherwise // the directives are inserted multiple times -> invalid config $authname = $row_htpasswds['authname']; for ($i = 0; $i < $x; $i++) { if ($returnval[$i]['usrf'] == $htpasswd_filename) { $authname = $returnval[$i]['authname']; break; } } $returnval[$x]['authname'] = $authname; $returnval[$x]['usrf'] = $htpasswd_filename; $x++; } } // Remove duplicate entries $returnval = array_map("unserialize", array_unique(array_map("serialize", $returnval))); return $returnval; } protected function getStats($domain, $single) { $stats_text = ''; $statTool = Settings::Get('system.traffictool'); $statDomain = ""; if ($statTool == 'awstats') { // awstats generates for each domain regardless of speciallogfile $statDomain = "/" . $domain['domain']; } if ($domain['speciallogfile'] == '1') { $statDomain = "/" . (($domain['parentdomainid'] == '0') ? $domain['domain'] : $domain['parentdomain']); } $statDocroot = FileDir::makeCorrectFile($domain['customerroot'] . '/' . $statTool . $statDomain); $stats_text .= "\t" . 'location ^~ /'.$statTool.' {' . "\n"; $stats_text .= "\t\t" . 'alias ' . $statDocroot . '/;' . "\n"; $stats_text .= "\t\t" . 'auth_basic "' . $single['authname'] . '";' . "\n"; $stats_text .= "\t\t" . 'auth_basic_user_file ' . FileDir::makeCorrectFile($single['usrf']) . ';' . "\n"; $stats_text .= "\t" . '}' . "\n\n"; // awstats special requirement for icons if ($statTool == 'awstats') { $stats_text .= "\t" . 'location ~ ^/awstats-icon/(.*)$ {' . "\n"; $stats_text .= "\t\t" . 'alias ' . FileDir::makeCorrectDir(Settings::Get('system.awstats_icons')) . '$1;' . "\n"; $stats_text .= "\t" . '}' . "\n\n"; } return $stats_text; } protected function composePhpOptions(&$domain, $ssl_vhost = false) { $phpopts = ''; if ($domain['phpenabled_customer'] == 1 && $domain['phpenabled_vhost'] == '1') { $phpopts = "\t" . 'location ~ ^(.+?\.php)(/.*)?$ {' . "\n"; if ($domain['notryfiles'] != 1) { $phpopts .= "\t\t" . 'try_files ' . $domain['nonexistinguri'] . ' @php;' . "\n"; $phpopts .= "\t" . '}' . "\n\n"; $phpopts .= "\tlocation @php {\n"; $phpopts .= "\t\t" . 'try_files $1 =404;' . "\n\n"; } $phpopts .= "\t\tfastcgi_split_path_info ^(.+?\.php)(/.*)$;\n"; $phpopts .= "\t\tinclude " . Settings::Get('nginx.fastcgiparams') . ";\n"; $phpopts .= "\t\tfastcgi_param SCRIPT_FILENAME \$request_filename;\n"; $phpopts .= "\t\tset \$path_info \$fastcgi_path_info;\n"; $phpopts .= "\t\tfastcgi_param PATH_INFO \$path_info;\n"; $phpopts .= "\t\tfastcgi_pass " . Settings::Get('system.nginx_php_backend') . ";\n"; $phpopts .= "\t\tfastcgi_index index.php;\n"; if ($domain['ssl'] == '1' && $ssl_vhost) { $phpopts .= "\t\tfastcgi_param HTTPS on;\n"; } $phpopts .= "\t}\n\n"; } return $phpopts; } /** * define a default ErrorDocument-statement, bug #unknown-yet */ private function createStandardErrorHandler() { if (Settings::Get('defaultwebsrverrhandler.enabled') == '1' && (Settings::Get('defaultwebsrverrhandler.err401') != '' || Settings::Get('defaultwebsrverrhandler.err403') != '' || Settings::Get('defaultwebsrverrhandler.err404') != '' || Settings::Get('defaultwebsrverrhandler.err500') != '')) { $vhosts_filename = $this->getCustomVhostFilename('05_froxlor_default_errorhandler.conf'); if (!isset($this->nginx_data[$vhosts_filename])) { $this->nginx_data[$vhosts_filename] = ''; } $statusCodes = [ '401', '403', '404', '500' ]; foreach ($statusCodes as $statusCode) { if (Settings::Get('defaultwebsrverrhandler.err' . $statusCode) != '') { $defhandler = Settings::Get('defaultwebsrverrhandler.err' . $statusCode); if (!Validate::validateUrl($defhandler)) { $defhandler = FileDir::makeCorrectFile($defhandler); } $this->nginx_data[$vhosts_filename] .= 'error_page ' . $statusCode . ' ' . $defhandler . ';' . "\n"; } } } } public function createOwnVhostStarter() { return; } public function writeConfigs() { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "nginx::writeConfigs: rebuilding " . Settings::Get('system.apacheconf_vhost')); $vhostDir = new Directory(Settings::Get('system.apacheconf_vhost')); if (!$vhostDir->isConfigDir()) { // Save one big file $vhosts_file = ''; // sort by filename so the order is: // 1. subdomains // 2. subdomains as main-domains // 3. main-domains ksort($this->nginx_data); foreach ($this->nginx_data as $vhosts_filename => $vhost_content) { $vhosts_file .= $vhost_content . "\n\n"; } $vhosts_filename = Settings::Get('system.apacheconf_vhost'); // Apply header $vhosts_file = '# ' . basename($vhosts_filename) . "\n" . '# Created ' . date('d.m.Y H:i') . "\n" . '# Do NOT manually edit this file, all changes will be deleted after the next domain change at the panel.' . "\n" . "\n" . $vhosts_file; $vhosts_file_handler = fopen($vhosts_filename, 'w'); fwrite($vhosts_file_handler, $vhosts_file); fclose($vhosts_file_handler); } else { if (!file_exists(Settings::Get('system.apacheconf_vhost'))) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'nginx::writeConfigs: mkdir ' . escapeshellarg(FileDir::makeCorrectDir(Settings::Get('system.apacheconf_vhost')))); FileDir::safe_exec('mkdir -p ' . escapeshellarg(FileDir::makeCorrectDir(Settings::Get('system.apacheconf_vhost')))); } // Write a single file for every vhost foreach ($this->nginx_data as $vhosts_filename => $vhosts_file) { // Apply header $vhosts_file = '# ' . basename($vhosts_filename) . "\n" . '# Created ' . date('d.m.Y H:i') . "\n" . '# Do NOT manually edit this file, all changes will be deleted after the next domain change at the panel.' . "\n" . "\n" . $vhosts_file; if (!empty($vhosts_filename)) { $vhosts_file_handler = fopen($vhosts_filename, 'w'); fwrite($vhosts_file_handler, $vhosts_file); fclose($vhosts_file_handler); } } } // htaccess stuff if (count($this->htpasswds_data) > 0) { if (!file_exists(Settings::Get('system.apacheconf_htpasswddir'))) { $umask = umask(); umask(0000); mkdir(Settings::Get('system.apacheconf_htpasswddir'), 0751); umask($umask); } elseif (!is_dir(Settings::Get('system.apacheconf_htpasswddir'))) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_WARNING, 'WARNING!!! ' . Settings::Get('system.apacheconf_htpasswddir') . ' is not a directory. htpasswd directory protection is disabled!!!'); } if (is_dir(Settings::Get('system.apacheconf_htpasswddir'))) { foreach ($this->htpasswds_data as $htpasswd_filename => $htpasswd_file) { $this->known_htpasswdsfilenames[] = basename($htpasswd_filename); $htpasswd_file_handler = fopen($htpasswd_filename, 'w'); // Filter duplicate pairs of username and password $htpasswd_file = implode("\n", array_unique(explode("\n", $htpasswd_file))); fwrite($htpasswd_file_handler, $htpasswd_file); fclose($htpasswd_file_handler); } } } } } ================================================ FILE: lib/Froxlor/Cron/Http/NginxFcgi.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cron\Http; use Froxlor\Cron\Http\Php\PhpInterface; use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\Froxlor; use Froxlor\Settings; class NginxFcgi extends Nginx { public function createOwnVhostStarter() { if (Settings::Get('phpfpm.enabled') == '1' && Settings::Get('phpfpm.enabled_ownvhost') == '1') { $mypath = Froxlor::getInstallDir(); $user = Settings::Get('phpfpm.vhost_httpuser'); $group = Settings::Get('phpfpm.vhost_httpgroup'); // get fpm config $fpm_sel_stmt = Database::prepare(" SELECT f.id FROM `" . TABLE_PANEL_FPMDAEMONS . "` f LEFT JOIN `" . TABLE_PANEL_PHPCONFIGS . "` p ON p.fpmsettingid = f.id WHERE p.id = :phpconfigid "); $fpm_config = Database::pexecute_first($fpm_sel_stmt, [ 'phpconfigid' => Settings::Get('phpfpm.vhost_defaultini') ]); $domain = [ 'id' => 'none', 'domain' => Settings::Get('system.hostname'), 'adminid' => 1, /* first admin-user (superadmin) */ 'mod_fcgid_starter' => -1, 'mod_fcgid_maxrequests' => -1, 'guid' => $user, 'openbasedir' => 0, 'email' => Settings::Get('panel.adminmail'), 'loginname' => 'froxlor.panel', 'documentroot' => $mypath, 'customerroot' => $mypath, 'fpm_config_id' => isset($fpm_config['id']) ? $fpm_config['id'] : 1 ]; // all the files and folders have to belong to the local user // now because we also use fcgid for our own vhost FileDir::safe_exec('chown -R ' . $user . ':' . $group . ' ' . escapeshellarg($mypath)); // get php.ini for our own vhost $php = new PhpInterface($domain); // get php-config if (Settings::Get('phpfpm.enabled') == '1') { // fpm $phpconfig = $php->getPhpConfig(Settings::Get('phpfpm.vhost_defaultini')); } else { // fcgid $phpconfig = $php->getPhpConfig(Settings::Get('system.mod_fcgid_defaultini_ownvhost')); } // create starter-file | config-file $php->getInterface()->createConfig($phpconfig); // create php.ini (fpm does nothing here, as it // defines ini-settings in its pool config) $php->getInterface()->createIniFile($phpconfig); } } protected function composePhpOptions(&$domain, $ssl_vhost = false) { $php_options_text = ''; if ($domain['phpenabled_customer'] == 1 && $domain['phpenabled_vhost'] == '1') { $php = new PhpInterface($domain); $phpconfig = $php->getPhpConfig((int)$domain['phpsettingid']); $php_options_text = "\t" . 'location ~ ^(.+?\.php)(/.*)?$ {' . "\n"; if ($domain['notryfiles'] != 1) { $php_options_text .= "\t\t" . 'try_files ' . $domain['nonexistinguri'] . ' @php;' . "\n"; $php_options_text .= "\t" . '}' . "\n\n"; $php_options_text .= "\t" . 'location @php {' . "\n"; $php_options_text .= "\t\t" . 'try_files $1 =404;' . "\n\n"; } $php_options_text .= "\t\t" . 'include ' . Settings::Get('nginx.fastcgiparams') . ";\n"; $php_options_text .= "\t\t" . 'fastcgi_split_path_info ^(.+?\.php)(/.*)$;' . "\n"; $php_options_text .= "\t\t" . 'fastcgi_param SCRIPT_FILENAME $request_filename;' . "\n"; $php_options_text .= "\t\t" . 'set $path_info $fastcgi_path_info;' . "\n"; $php_options_text .= "\t\t" . 'fastcgi_param PATH_INFO $path_info;' . "\n"; if ($domain['ssl'] == '1' && $ssl_vhost) { $php_options_text .= "\t\t" . 'fastcgi_param HTTPS on;' . "\n"; } $domain['fpm_socket'] = $php->getInterface()->getSocketFile(); $php_options_text .= "\t\t" . 'fastcgi_pass unix:' . $domain['fpm_socket'] . ";\n"; $php_options_text .= "\t\t" . 'fastcgi_index index.php;' . "\n"; $php_options_text .= "\t}\n\n"; // create starter-file | config-file $php->getInterface()->createConfig($phpconfig); // create php.ini (fpm does nothing here, as it // defines ini-settings in its pool config) $php->getInterface()->createIniFile($phpconfig); } else { $php_options_text .= ' # PHP is disabled for this vHost' . "\n"; } return $php_options_text; } } ================================================ FILE: lib/Froxlor/Cron/Http/Php/Fcgid.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cron\Http\Php; use Froxlor\Database\Database; use Froxlor\Domain\Domain; use Froxlor\FileDir; use Froxlor\PhpHelper; use Froxlor\Settings; class Fcgid { /** * Domain-Data array * * @var array */ private $domain = []; /** * Admin-Date cache array * * @var array */ private $admin_cache = []; /** * main constructor */ public function __construct($domain) { $this->domain = $domain; } /** * create fcgid-starter-file * * @param array $phpconfig */ public function createConfig($phpconfig) { // create starter $starter_file = "#!/bin/sh\n\n"; $starter_file .= "#\n"; $starter_file .= "# starter created/changed on " . date("Y.m.d H:i:s") . " for domain '" . $this->domain['domain'] . "' with id #" . $this->domain['id'] . " from php template '" . $phpconfig['description'] . "' with id #" . $phpconfig['id'] . "\n"; $starter_file .= "# Do not change anything in this file, it will be overwritten by the Froxlor Cronjob!\n"; $starter_file .= "#\n\n"; $starter_file .= "umask " . $phpconfig['mod_fcgid_umask'] . "\n"; $starter_file .= "PHPRC=" . escapeshellarg($this->getConfigDir()) . "\n"; $starter_file .= "export PHPRC\n"; // set number of processes for one domain if ((int)$this->domain['mod_fcgid_starter'] != -1) { $starter_file .= "PHP_FCGI_CHILDREN=" . (int)$this->domain['mod_fcgid_starter'] . "\n"; } else { if ((int)$phpconfig['mod_fcgid_starter'] != -1) { $starter_file .= "PHP_FCGI_CHILDREN=" . (int)$phpconfig['mod_fcgid_starter'] . "\n"; } else { $starter_file .= "PHP_FCGI_CHILDREN=" . (int)Settings::Get('system.mod_fcgid_starter') . "\n"; } } $starter_file .= "export PHP_FCGI_CHILDREN\n"; // set number of maximum requests for one domain if ((int)$this->domain['mod_fcgid_maxrequests'] != -1) { $starter_file .= "PHP_FCGI_MAX_REQUESTS=" . (int)$this->domain['mod_fcgid_maxrequests'] . "\n"; } else { if ((int)$phpconfig['mod_fcgid_maxrequests'] != -1) { $starter_file .= "PHP_FCGI_MAX_REQUESTS=" . (int)$phpconfig['mod_fcgid_maxrequests'] . "\n"; } else { $starter_file .= "PHP_FCGI_MAX_REQUESTS=" . (int)Settings::Get('system.mod_fcgid_maxrequests') . "\n"; } } $starter_file .= "export PHP_FCGI_MAX_REQUESTS\n"; // Set Binary $starter_file .= "exec " . $phpconfig['binary'] . " -c " . escapeshellarg($this->getConfigDir()) . "\n"; // remove +i attribute, so starter can be overwritten if (file_exists($this->getStarterFile())) { FileDir::removeImmutable($this->getStarterFile()); } $starter_file_handler = fopen($this->getStarterFile(), 'w'); fwrite($starter_file_handler, $starter_file); fclose($starter_file_handler); FileDir::safe_exec('chmod 750 ' . escapeshellarg($this->getStarterFile())); FileDir::safe_exec('chown ' . $this->domain['guid'] . ':' . $this->domain['guid'] . ' ' . escapeshellarg($this->getStarterFile())); FileDir::setImmutable($this->getStarterFile()); } /** * fcgid-config directory * * @param boolean $createifnotexists * create the directory if it does not exist * * @return string the directory */ public function getConfigDir($createifnotexists = true) { $configdir = FileDir::makeCorrectDir(Settings::Get('system.mod_fcgid_configdir') . '/' . $this->domain['loginname'] . '/' . $this->domain['domain'] . '/'); if (!is_dir($configdir) && $createifnotexists) { FileDir::safe_exec('mkdir -p ' . escapeshellarg($configdir)); FileDir::safe_exec('chmod 0755 ' . escapeshellarg(dirname($configdir))); FileDir::safe_exec('chmod 0755 ' . escapeshellarg($configdir)); FileDir::safe_exec('chown ' . $this->domain['guid'] . ':' . $this->domain['guid'] . ' ' . escapeshellarg($configdir)); } return $configdir; } /** * return path of php-starter file * * @return string the directory */ public function getStarterFile() { $starter_filename = FileDir::makeCorrectFile($this->getConfigDir() . '/php-fcgi-starter'); return $starter_filename; } /** * create customized php.ini * * @param array $phpconfig */ public function createIniFile($phpconfig) { $openbasedir = ''; $openbasedirc = ';'; if ($this->domain['openbasedir'] == '1') { $openbasedirc = ''; $_phpappendopenbasedir = ''; $_custom_openbasedir = explode(':', Settings::Get('system.mod_fcgid_peardir')); foreach ($_custom_openbasedir as $cobd) { $_phpappendopenbasedir .= Domain::appendOpenBasedirPath($cobd); } $_custom_openbasedir = explode(':', Settings::Get('system.phpappendopenbasedir')); foreach ($_custom_openbasedir as $cobd) { $_phpappendopenbasedir .= Domain::appendOpenBasedirPath($cobd); } if ($this->domain['openbasedir_path'] == '0' && strstr($this->domain['documentroot'], ":") === false) { $openbasedir = Domain::appendOpenBasedirPath($this->domain['documentroot'], true); } else if ($this->domain['openbasedir_path'] == '2' && strpos(dirname($this->domain['documentroot']).'/', $this->domain['customerroot']) !== false) { $openbasedir = Domain::appendOpenBasedirPath(dirname($this->domain['documentroot']).'/', true); } else { $openbasedir = Domain::appendOpenBasedirPath($this->domain['customerroot'], true); } $openbasedir .= Domain::appendOpenBasedirPath($this->getTempDir()); $openbasedir .= $_phpappendopenbasedir; } else { $openbasedir = 'none'; $openbasedirc = ';'; } $admin = $this->getAdminData($this->domain['adminid']); $php_ini_variables = [ 'SAFE_MODE' => 'Off', // keep this for compatibility, just in case 'PEAR_DIR' => Settings::Get('system.mod_fcgid_peardir'), 'TMP_DIR' => $this->getTempDir(), 'CUSTOMER_EMAIL' => $this->domain['email'], 'ADMIN_EMAIL' => $admin['email'], 'DOMAIN' => $this->domain['domain'], 'CUSTOMER' => $this->domain['loginname'], 'ADMIN' => $admin['loginname'], 'OPEN_BASEDIR' => $openbasedir, 'OPEN_BASEDIR_C' => $openbasedirc, 'OPEN_BASEDIR_GLOBAL' => Settings::Get('system.phpappendopenbasedir'), 'DOCUMENT_ROOT' => FileDir::makeCorrectDir($this->domain['documentroot']), 'CUSTOMER_HOMEDIR' => FileDir::makeCorrectDir($this->domain['customerroot']) ]; // insert a small header for the file $phpini_file = ";\n"; $phpini_file .= "; php.ini created/changed on " . date("Y.m.d H:i:s") . " for domain '" . $this->domain['domain'] . "' with id #" . $this->domain['id'] . " from php template '" . $phpconfig['description'] . "' with id #" . $phpconfig['id'] . "\n"; $phpini_file .= "; Do not change anything in this file, it will be overwritten by the Froxlor Cronjob!\n"; $phpini_file .= ";\n\n"; $phpini_file .= PhpHelper::replaceVariables($phpconfig['phpsettings'], $php_ini_variables); $phpini_file = str_replace('"none"', 'none', $phpini_file); // $phpini_file = preg_replace('/\"+/', '"', $phpini_file); $phpini_file_handler = fopen($this->getIniFile(), 'w'); fwrite($phpini_file_handler, $phpini_file); fclose($phpini_file_handler); FileDir::safe_exec('chown root:0 ' . escapeshellarg($this->getIniFile())); FileDir::safe_exec('chmod 0644 ' . escapeshellarg($this->getIniFile())); } /** * fcgid-temp directory * * @param boolean $createifnotexists * create the directory if it does not exist * * @return string the directory */ public function getTempDir($createifnotexists = true) { $tmpdir = FileDir::makeCorrectDir(Settings::Get('system.mod_fcgid_tmpdir') . '/' . $this->domain['loginname'] . '/'); if (!is_dir($tmpdir) && $createifnotexists) { FileDir::safe_exec('mkdir -p ' . escapeshellarg($tmpdir)); FileDir::safe_exec('chown -R ' . $this->domain['guid'] . ':' . $this->domain['guid'] . ' ' . escapeshellarg($tmpdir)); FileDir::safe_exec('chmod 0750 ' . escapeshellarg($tmpdir)); } return $tmpdir; } /** * return the admin-data of a specific admin * * @param int $adminid * id of the admin-user * * @return array */ private function getAdminData($adminid) { $adminid = intval($adminid); if (!isset($this->admin_cache[$adminid])) { $stmt = Database::prepare(" SELECT `email`, `loginname` FROM `" . TABLE_PANEL_ADMINS . "` WHERE `adminid` = :id"); $this->admin_cache[$adminid] = Database::pexecute_first($stmt, [ 'id' => $adminid ]); } return $this->admin_cache[$adminid]; } /** * return path of php.ini file * * @return string full with path file-name */ public function getIniFile() { $phpini_filename = FileDir::makeCorrectFile($this->getConfigDir() . '/php.ini'); return $phpini_filename; } } ================================================ FILE: lib/Froxlor/Cron/Http/Php/Fpm.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cron\Http\Php; use Froxlor\Database\Database; use Froxlor\Domain\Domain; use Froxlor\FileDir; use Froxlor\PhpHelper; use Froxlor\Settings; use InvalidArgumentException; class Fpm { /** * Domain-Data array * * @var array */ private $domain = []; /** * fpm config * * @var array */ private $fpm_cfg = []; /** * Admin-Date cache array * * @var array */ private $admin_cache = []; /** * defines what can be used for pool-config from php.ini * Mostly taken from http://php.net/manual/en/ini.list.php * * @var array */ private $ini = []; /** * main constructor */ public function __construct($domain) { if (!isset($domain['fpm_config_id']) || empty($domain['fpm_config_id'])) { $domain['fpm_config_id'] = 1; } $this->domain = $domain; $this->readFpmConfig($domain['fpm_config_id']); $this->buildIniMapping(); } private function readFpmConfig($fpm_config_id) { $stmt = Database::prepare("SELECT * FROM `" . TABLE_PANEL_FPMDAEMONS . "` WHERE `id` = :id"); $this->fpm_cfg = Database::pexecute_first($stmt, [ 'id' => $fpm_config_id ]); } private function buildIniMapping() { $this->ini = [ 'php_flag' => array_map('trim', explode("\n", Settings::Get('phpfpm.ini_flags'))), 'php_value' => array_map('trim', explode("\n", Settings::Get('phpfpm.ini_values'))), 'php_admin_flag' => array_map('trim', explode("\n", Settings::Get('phpfpm.ini_admin_flags'))), 'php_admin_value' => array_map('trim', explode("\n", Settings::Get('phpfpm.ini_admin_values'))) ]; } /** * create a dummy fpm pool config with minimal configuration * (this is used whenever a config directory is empty but needs at least one pool to startup/restart) * * @param string $configdir */ public static function createDummyPool($configdir) { if (!is_dir($configdir)) { FileDir::safe_exec('mkdir -p ' . escapeshellarg($configdir)); } $config = FileDir::makeCorrectFile($configdir . '/dummy.conf'); $dummy = "[dummy] user = " . Settings::Get('system.httpuser') . " listen = /run/" . md5($configdir) . "-fpm.sock pm = static pm.max_children = 1 "; file_put_contents($config, $dummy); } /** * create fpm-pool config * * @param array $phpconfig */ public function createConfig($phpconfig) { $fh = @fopen($this->getConfigFile(), 'w'); if ($fh) { if ($phpconfig['override_fpmconfig'] == 1) { $this->fpm_cfg['pm'] = $phpconfig['pm']; $this->fpm_cfg['max_children'] = $phpconfig['max_children']; $this->fpm_cfg['start_servers'] = $phpconfig['start_servers']; $this->fpm_cfg['min_spare_servers'] = $phpconfig['min_spare_servers']; $this->fpm_cfg['max_spare_servers'] = $phpconfig['max_spare_servers']; $this->fpm_cfg['max_requests'] = $phpconfig['max_requests']; $this->fpm_cfg['idle_timeout'] = $phpconfig['idle_timeout']; $this->fpm_cfg['limit_extensions'] = $phpconfig['limit_extensions']; } $fpm_pm = $this->fpm_cfg['pm']; $fpm_children = (int)$this->fpm_cfg['max_children']; $fpm_start_servers = (int)$this->fpm_cfg['start_servers']; $fpm_min_spare_servers = (int)$this->fpm_cfg['min_spare_servers']; $fpm_max_spare_servers = (int)$this->fpm_cfg['max_spare_servers']; $fpm_requests = (int)$this->fpm_cfg['max_requests']; $fpm_process_idle_timeout = (int)$this->fpm_cfg['idle_timeout']; $fpm_limit_extensions = $this->fpm_cfg['limit_extensions']; $fpm_custom_config = $this->fpm_cfg['custom_config']; if ($fpm_children == 0) { $fpm_children = 1; } $fpm_config = ';PHP-FPM configuration for "' . $this->domain['domain'] . '" created on ' . date("Y.m.d H:i:s") . "\n"; $fpm_config .= '[' . $this->domain['domain'] . ']' . "\n"; $fpm_config .= 'listen = ' . $this->getSocketFile() . "\n"; if ($this->domain['loginname'] == 'froxlor.panel') { $fpm_config .= 'listen.owner = ' . $this->domain['guid'] . "\n"; $fpm_config .= 'listen.group = ' . $this->domain['guid'] . "\n"; } else { $fpm_config .= 'listen.owner = ' . $this->domain['loginname'] . "\n"; $fpm_config .= 'listen.group = ' . $this->domain['loginname'] . "\n"; } // see #1418 why this is 0660 $fpm_config .= 'listen.mode = 0660' . "\n"; if ($this->domain['loginname'] == 'froxlor.panel') { $fpm_config .= 'user = ' . $this->domain['guid'] . "\n"; $fpm_config .= 'group = ' . $this->domain['guid'] . "\n"; } else { $fpm_config .= 'user = ' . $this->domain['loginname'] . "\n"; $fpm_config .= 'group = ' . $this->domain['loginname'] . "\n"; } $fpm_config .= 'pm = ' . $fpm_pm . "\n"; $fpm_config .= 'pm.max_children = ' . $fpm_children . "\n"; if ($fpm_pm == 'dynamic') { // honor max_children if ($fpm_children < $fpm_min_spare_servers) { $fpm_min_spare_servers = $fpm_children; } if ($fpm_children < $fpm_max_spare_servers) { $fpm_max_spare_servers = $fpm_children; } // failsafe, refs #955 if ($fpm_start_servers < $fpm_min_spare_servers) { $fpm_start_servers = $fpm_min_spare_servers; } if ($fpm_start_servers > $fpm_max_spare_servers) { $fpm_start_servers = $fpm_max_spare_servers; } $fpm_config .= 'pm.start_servers = ' . $fpm_start_servers . "\n"; $fpm_config .= 'pm.min_spare_servers = ' . $fpm_min_spare_servers . "\n"; $fpm_config .= 'pm.max_spare_servers = ' . $fpm_max_spare_servers . "\n"; } elseif ($fpm_pm == 'ondemand') { $fpm_config .= 'pm.process_idle_timeout = ' . $fpm_process_idle_timeout . "\n"; } $fpm_config .= 'pm.max_requests = ' . $fpm_requests . "\n"; $fpm_config .= 'request_terminate_timeout = ' . $phpconfig['fpm_reqterm'] . "\n"; // possible slowlog configs if ($phpconfig['fpm_slowlog'] == '1') { $this->durationCompare($phpconfig['fpm_reqterm'], $phpconfig['fpm_reqslow']); $fpm_config .= 'request_slowlog_timeout = ' . $phpconfig['fpm_reqslow'] . "\n"; $slowlog = FileDir::makeCorrectFile(Settings::Get('system.logfiles_directory') . '/' . $this->domain['loginname'] . '-php-slow.log'); $fpm_config .= 'slowlog = ' . $slowlog . "\n"; $fpm_config .= 'catch_workers_output = yes' . "\n"; } $fpm_config .= ';chroot = ' . FileDir::makeCorrectDir($this->domain['documentroot']) . "\n"; $fpm_config .= 'security.limit_extensions = ' . $fpm_limit_extensions . "\n"; $tmpdir = FileDir::makeCorrectDir(Settings::Get('phpfpm.tmpdir') . '/' . $this->domain['loginname'] . '/'); if (!is_dir($tmpdir)) { $this->getTempDir(); } $env_path = Settings::Get('phpfpm.envpath'); if (!empty($env_path)) { $fpm_config .= 'env[PATH] = ' . $env_path . "\n"; } $fpm_config .= 'env[TMP] = ' . $tmpdir . "\n"; $fpm_config .= 'env[TMPDIR] = ' . $tmpdir . "\n"; $fpm_config .= 'env[TEMP] = ' . $tmpdir . "\n"; $openbasedir = ''; if ($this->domain['loginname'] != 'froxlor.panel') { if ($this->domain['openbasedir'] == '1') { $_phpappendopenbasedir = ''; $_custom_openbasedir = explode(':', Settings::Get('phpfpm.peardir')); foreach ($_custom_openbasedir as $cobd) { $_phpappendopenbasedir .= Domain::appendOpenBasedirPath($cobd); } $_custom_openbasedir = explode(':', Settings::Get('system.phpappendopenbasedir')); foreach ($_custom_openbasedir as $cobd) { $_phpappendopenbasedir .= Domain::appendOpenBasedirPath($cobd); } if ($this->domain['openbasedir_path'] == '0' && strstr($this->domain['documentroot'], ":") === false) { $openbasedir = Domain::appendOpenBasedirPath($this->domain['documentroot'], true); } else if ($this->domain['openbasedir_path'] == '2' && strpos(dirname($this->domain['documentroot']) . '/', $this->domain['customerroot']) !== false) { $openbasedir = Domain::appendOpenBasedirPath(dirname($this->domain['documentroot']) . '/', true); } else { $openbasedir = Domain::appendOpenBasedirPath($this->domain['customerroot'], true); } $openbasedir .= Domain::appendOpenBasedirPath($this->getTempDir()); $openbasedir .= $_phpappendopenbasedir; } } $fpm_config .= 'php_admin_value[upload_tmp_dir] = ' . FileDir::makeCorrectDir(Settings::Get('phpfpm.tmpdir') . '/' . $this->domain['loginname'] . '/') . "\n"; $admin = $this->getAdminData($this->domain['adminid']); $php_ini_variables = [ 'SAFE_MODE' => 'Off', // keep this for compatibility, just in case 'PEAR_DIR' => Settings::Get('phpfpm.peardir'), 'TMP_DIR' => $this->getTempDir(), 'CUSTOMER_EMAIL' => $this->domain['email'], 'ADMIN_EMAIL' => $admin['email'], 'DOMAIN' => $this->domain['domain'], 'CUSTOMER' => $this->domain['loginname'], 'ADMIN' => $admin['loginname'], 'OPEN_BASEDIR' => $openbasedir, 'OPEN_BASEDIR_C' => '', 'OPEN_BASEDIR_GLOBAL' => Settings::Get('system.phpappendopenbasedir'), 'DOCUMENT_ROOT' => FileDir::makeCorrectDir($this->domain['documentroot']), 'CUSTOMER_HOMEDIR' => FileDir::makeCorrectDir($this->domain['customerroot']) ]; $phpini = PhpHelper::replaceVariables($phpconfig['phpsettings'], $php_ini_variables); $phpini_array = explode("\n", $phpini); $fpm_config .= "\n\n"; foreach ($phpini_array as $inisection) { $is = explode("=", trim($inisection), 2); if (empty($is[0])) { continue; } foreach ($this->ini as $sec => $possibles) { if (in_array(trim($is[0]), $possibles)) { // check explicitly for open_basedir if (trim($is[0]) == 'open_basedir' && $openbasedir == '') { continue; } $fpm_config .= $sec . '[' . trim($is[0]) . '] = ' . trim($is[1] ?? '') . "\n"; } } } // now check if 'sendmail_path' has not been set in the custom-php.ini // if not we use our fallback-default as usual if (strpos($fpm_config, 'php_admin_value[sendmail_path]') === false) { $fpm_config .= 'php_admin_value[sendmail_path] = /usr/sbin/sendmail -t -i -f postmaster@' . $this->domain['domain'] . "\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"; $fpm_config .= PhpHelper::replaceVariables($fpm_custom_config, $php_ini_variables); } fwrite($fh, $fpm_config, strlen($fpm_config)); fclose($fh); } } /** * fpm-config file * * @param boolean $createifnotexists * create the directory if it does not exist * * @return string the full path to the file */ public function getConfigFile($createifnotexists = true) { $configdir = $this->fpm_cfg['config_dir']; $config = FileDir::makeCorrectFile($configdir . '/' . $this->domain['domain'] . '.conf'); if (!is_dir($configdir) && $createifnotexists) { FileDir::safe_exec('mkdir -p ' . escapeshellarg($configdir)); } return $config; } /** * return path of fpm-socket file * * @param boolean $createifnotexists * create the directory if it does not exist * * @return string the full path to the socket */ public function getSocketFile($createifnotexists = true) { $socketdir = FileDir::makeCorrectDir(Settings::Get('phpfpm.fastcgi_ipcdir')); // add fpm-config-id to filename, so it's unique for the fpm-daemon and doesn't interfere with running configs when reuilding $socket_filename = $socketdir . '/' . $this->domain['fpm_config_id'] . '-' . $this->domain['loginname'] . '-' . $this->domain['domain'] . '-php-fpm.socket'; if (strlen($socket_filename) > 100) { // respect the unix socket-length limitation $socket_filename = $socketdir . '/' . $this->domain['fpm_config_id'] . '-' . $this->domain['loginname'] . '-' . $this->domain['id'] . '-php-fpm.socket'; if (strlen($socket_filename) > 100) { // even a long loginname it seems $socket_filename = $socketdir . '/' . $this->domain['fpm_config_id'] . '-' . $this->domain['guid'] . '-' . $this->domain['id'] . '-php-fpm.socket'; } } $socket = strtolower(FileDir::makeCorrectFile($socket_filename)); if (!is_dir($socketdir) && $createifnotexists) { FileDir::safe_exec('mkdir -p ' . escapeshellarg($socketdir)); FileDir::safe_exec('chown -R ' . Settings::Get('system.httpuser') . ':' . Settings::Get('system.httpgroup') . ' ' . escapeshellarg($socketdir)); } return $socket; } /** * fpm-temp directory * * @param boolean $createifnotexists * create the directory if it does not exist * * @return string the directory */ public function getTempDir($createifnotexists = true) { $tmpdir = FileDir::makeCorrectDir(Settings::Get('phpfpm.tmpdir') . '/' . $this->domain['loginname'] . '/'); if (!is_dir($tmpdir) && $createifnotexists) { FileDir::safe_exec('mkdir -p ' . escapeshellarg($tmpdir)); FileDir::safe_exec('chown -R ' . $this->domain['guid'] . ':' . $this->domain['guid'] . ' ' . escapeshellarg($tmpdir)); FileDir::safe_exec('chmod 0750 ' . escapeshellarg($tmpdir)); } return $tmpdir; } /** * return the admin-data of a specific admin * * @param int $adminid * id of the admin-user * * @return array */ private function getAdminData($adminid) { $adminid = intval($adminid); if (!isset($this->admin_cache[$adminid])) { $stmt = Database::prepare(" SELECT `email`, `loginname` FROM `" . TABLE_PANEL_ADMINS . "` WHERE `adminid` = :id"); $this->admin_cache[$adminid] = Database::pexecute_first($stmt, [ 'id' => $adminid ]); } return $this->admin_cache[$adminid]; } /** * this is done via createConfig as php-fpm defines * the ini-values/flags in its pool-config * * @param string $phpconfig */ public function createIniFile($phpconfig) { return; } /** * fastcgi-fakedirectory directory * * @param boolean $createifnotexists * create the directory if it does not exist * * @return string the directory */ public function getAliasConfigDir($createifnotexists = true) { // ensure default... if (Settings::Get('phpfpm.aliasconfigdir') == null) { Settings::Set('phpfpm.aliasconfigdir', '/var/www/php-fpm'); } $configdir = FileDir::makeCorrectDir(Settings::Get('phpfpm.aliasconfigdir') . '/' . $this->domain['loginname'] . '/' . $this->domain['domain'] . '/'); if (!is_dir($configdir) && $createifnotexists) { FileDir::safe_exec('mkdir -p ' . escapeshellarg($configdir)); FileDir::safe_exec('chown ' . $this->domain['guid'] . ':' . $this->domain['guid'] . ' ' . escapeshellarg($configdir)); } return $configdir; } /** * 'request_slowlog_timeout' can't be greater than 'request_terminate_timeout' * * @param $request_terminate_timeout * @param $request_slowlog_timeout * @return void */ private function durationCompare(&$request_terminate_timeout, &$request_slowlog_timeout) { $to_seconds = function ($str) { if (!preg_match('/^([0-9]+)([smhd])?$/', $str, $matches)) { throw new InvalidArgumentException("Invalid format: $str"); } $value = (int)$matches[1]; $unit = $matches[2] ?? 's'; switch ($unit) { case 'm': return $value * 60; case 'h': return $value * 3600; case 'd': return $value * 86400; default: return $value; } }; $aSec = $to_seconds($request_terminate_timeout); $bSec = $to_seconds($request_slowlog_timeout); if ($bSec > $aSec) { $request_slowlog_timeout = $request_terminate_timeout; } } } ================================================ FILE: lib/Froxlor/Cron/Http/Php/PhpInterface.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cron\Http\Php; use Froxlor\Database\Database; use Froxlor\Settings; class PhpInterface { /** * Domain-Data array * * @var array */ private $domain = []; /** * Interface object * * @var object */ private $interface = null; /** * PHP-Config data array * * @var array */ private $php_configs_cache = []; /** * main constructor */ public function __construct($domain) { $this->domain = $domain; $this->setInterface(); } /** * set interface-object by type of * php-interface: fcgid or php-fpm * sets private $_interface variable */ private function setInterface() { // php-fpm if ((int)Settings::Get('phpfpm.enabled') == 1) { $this->interface = new Fpm($this->domain); } elseif ((int)Settings::Get('system.mod_fcgid') == 1) { $this->interface = new Fcgid($this->domain); } } /** * returns the interface-object * from where we can control it */ public function getInterface() { return $this->interface; } /** * return the php-configuration from the database * * @param int $php_config_id * id of the php-configuration * * @return array */ public function getPhpConfig(int $php_config_id) { // If domain has no config, we will use the default one. if ($php_config_id == 0) { $php_config_id = 1; } if (!isset($this->php_configs_cache[$php_config_id])) { $stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_PHPCONFIGS . "` WHERE `id` = :id "); $this->php_configs_cache[$php_config_id] = Database::pexecute_first($stmt, [ 'id' => $php_config_id ]); if ((int)Settings::Get('phpfpm.enabled') == 1) { $stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_FPMDAEMONS . "` WHERE `id` = :id "); $this->php_configs_cache[$php_config_id]['fpm_settings'] = Database::pexecute_first($stmt, [ 'id' => $this->php_configs_cache[$php_config_id]['fpmsettingid'] ]); // override fpm daemon settings if set in php-config if ($this->php_configs_cache[$php_config_id]['override_fpmconfig'] == 1) { $this->php_configs_cache[$php_config_id]['fpm_settings']['limit_extensions'] = $this->php_configs_cache[$php_config_id]['limit_extensions']; $this->php_configs_cache[$php_config_id]['fpm_settings']['idle_timeout'] = $this->php_configs_cache[$php_config_id]['idle_timeout']; } } } return $this->php_configs_cache[$php_config_id]; } } ================================================ FILE: lib/Froxlor/Cron/Http/Php/index.html ================================================ ================================================ FILE: lib/Froxlor/Cron/Http/WebserverBase.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cron\Http; use Froxlor\Database\Database; use Froxlor\Domain\Domain; use Froxlor\Settings; use PDO; class WebserverBase { /** * returns an array with all entries required for all * webserver-vhost-configs * * @return array */ public static function getVhostsToCreate() { $query = "SELECT `d`.*, `pd`.`domain` AS `parentdomain`, `c`.`loginname`, `d`.`phpsettingid`, `c`.`adminid`, `c`.`guid`, `c`.`email`, `c`.`documentroot` AS `customerroot`, `c`.`deactivated` as `customer_deactivated`, `c`.`phpenabled` AS `phpenabled_customer`, `d`.`phpenabled` AS `phpenabled_vhost`, `a`.`email` as `admin_email` FROM `" . TABLE_PANEL_DOMAINS . "` `d` LEFT JOIN `" . TABLE_PANEL_CUSTOMERS . "` `c` USING(`customerid`) LEFT JOIN `" . TABLE_PANEL_DOMAINS . "` `pd` ON (`pd`.`id` = `d`.`parentdomainid`) LEFT JOIN `" . TABLE_PANEL_ADMINS . "` `a` ON (`a`.`adminid` = `c`.`adminid`) WHERE `d`.`aliasdomain` IS NULL AND `d`.`email_only` <> '1' ORDER BY `d`.`parentdomainid` DESC, `d`.`iswildcarddomain`, `d`.`domain` ASC; "; $result_domains_stmt = Database::query($query); // prepare IP statement $ip_stmt = Database::prepare(" SELECT `di`.`id_domain` , `p`.`ssl`, `p`.`ssl_cert_file`, `p`.`ssl_key_file`, `p`.`ssl_ca_file`, `p`.`ssl_cert_chainfile` FROM `" . TABLE_DOMAINTOIP . "` `di`, `" . TABLE_PANEL_IPSANDPORTS . "` `p` WHERE `p`.`id` = `di`.`id_ipandports` AND `di`.`id_domain` = :domainid AND `p`.`ssl` = '1' "); // prepare fpm-config select query $fpm_sel_stmt = Database::prepare(" SELECT f.id FROM `" . TABLE_PANEL_FPMDAEMONS . "` f LEFT JOIN `" . TABLE_PANEL_PHPCONFIGS . "` p ON p.fpmsettingid = f.id WHERE p.id = :phpconfigid "); $domains = []; while ($domain = $result_domains_stmt->fetch(PDO::FETCH_ASSOC)) { // set whole domain $domains[$domain['domain']] = $domain; // set empty-defaults for non-ssl $domains[$domain['domain']]['ssl'] = ''; $domains[$domain['domain']]['ssl_cert_file'] = ''; $domains[$domain['domain']]['ssl_key_file'] = ''; $domains[$domain['domain']]['ssl_ca_file'] = ''; $domains[$domain['domain']]['ssl_cert_chainfile'] = ''; // now, if the domain has an ssl ip/port assigned, get // the corresponding information from the db if (Domain::domainHasSslIpPort($domain['id'])) { $ssl_ip = Database::pexecute_first($ip_stmt, [ 'domainid' => $domain['id'] ]); // set ssl info for domain $domains[$domain['domain']]['ssl'] = '1'; $domains[$domain['domain']]['ssl_cert_file'] = $ssl_ip['ssl_cert_file']; $domains[$domain['domain']]['ssl_key_file'] = $ssl_ip['ssl_key_file']; $domains[$domain['domain']]['ssl_ca_file'] = $ssl_ip['ssl_ca_file']; $domains[$domain['domain']]['ssl_cert_chainfile'] = $ssl_ip['ssl_cert_chainfile']; } // read fpm-config-id if using fpm if ((int)Settings::Get('phpfpm.enabled') == 1) { $fpm_config = Database::pexecute_first($fpm_sel_stmt, [ 'phpconfigid' => $domain['phpsettingid'] ]); if ($fpm_config) { $domains[$domain['domain']]['fpm_config_id'] = $fpm_config['id']; } else { // fallback $domains[$domain['domain']]['fpm_config_id'] = 1; } } } return $domains; } } ================================================ FILE: lib/Froxlor/Cron/Http/index.html ================================================ ================================================ FILE: lib/Froxlor/Cron/Mail/Rspamd.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cron\Mail; use Exception; use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\FroxlorLogger; use Froxlor\Settings; class Rspamd { const DEFAULT_MARK_LVL = 7.0; const DEFAULT_REJECT_LVL = 14.0; private string $frx_settings_file = ""; protected FroxlorLogger $logger; public function __construct(FroxlorLogger $logger) { $this->logger = $logger; } /** * @throws Exception */ public function writeConfigs() { // tell the world what we are doing $this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'Task9 started - Rebuilding antispam configuration'); // get all email addresses $antispam_stmt = Database::prepare(" SELECT email, spam_tag_level, rewrite_subject, spam_kill_level, bypass_spam, policy_greylist, iscatchall FROM `" . TABLE_MAIL_VIRTUAL . "` ORDER BY email "); Database::pexecute($antispam_stmt); $this->frx_settings_file = "#\n# Automatically generated file by froxlor. DO NOT EDIT manually as it will be overwritten!\n# Generated: " . date('d.m.Y H:i') . "\n#\n\n"; while ($email = $antispam_stmt->fetch(\PDO::FETCH_ASSOC)) { $this->generateEmailAddrConfig($email); } $antispam_cfg_file = FileDir::makeCorrectFile(Settings::Get('antispam.config_file')); file_put_contents($antispam_cfg_file, $this->frx_settings_file); $this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, $antispam_cfg_file . ' written'); $this->writeDkimConfigs(); $this->reloadDaemon(); $this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'Task9 finished'); } /** * # local.d/dkim_signing.conf * try_fallback = true; * path = "/var/lib/rspamd/dkim/$domain.$selector.key"; * selector_map = "/etc/rspamd/dkim_selectors.map"; * * @return void * @throws Exception */ public function writeDkimConfigs() { $this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'Writing DKIM key-pairs'); $dkim_selector_map = ""; $result_domains_stmt = Database::query(" SELECT `id`, `domain`, `dkim`, `dkim_id`, `dkim_pubkey`, `dkim_privkey` FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `dkim` = '1' ORDER BY `id` ASC "); while ($domain = $result_domains_stmt->fetch(\PDO::FETCH_ASSOC)) { if ($domain['dkim_privkey'] == '' || $domain['dkim_pubkey'] == '') { $max_dkim_id_stmt = Database::query("SELECT MAX(`dkim_id`) as `max_dkim_id` FROM `" . TABLE_PANEL_DOMAINS . "`"); $max_dkim_id = $max_dkim_id_stmt->fetch(\PDO::FETCH_ASSOC); $domain['dkim_id'] = (int)$max_dkim_id['max_dkim_id'] + 1; $privkey_filename = FileDir::makeCorrectFile('/var/lib/rspamd/dkim/' . $domain['domain'] . '.dkim' . $domain['dkim_id'] . '.key'); $pubkey_filename = FileDir::makeCorrectFile('/var/lib/rspamd/dkim/' . $domain['domain'] . '.dkim' . $domain['dkim_id'] . '.txt'); $this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'Generating DKIM keys for "' . $domain['domain'] . '"'); $rsret = []; FileDir::safe_exec( 'rspamadm dkim_keygen -d ' . escapeshellarg($domain['domain']) . ' -k ' . $privkey_filename . ' -s dkim' . $domain['dkim_id'] . ' -b ' . Settings::Get('antispam.dkim_keylength') . ' -o plain > ' . escapeshellarg($pubkey_filename), $rsret, ['>'] ); if (!file_exists($privkey_filename) || !file_exists($pubkey_filename)) { $this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, 'DKIM Keypair for domain "' . $domain['domain'] . '" was not generated successfully.'); continue; } $domain['dkim_privkey'] = file_get_contents($privkey_filename); FileDir::safe_exec("chmod 0640 " . escapeshellarg($privkey_filename)); FileDir::safe_exec("chown _rspamd:_rspamd " . escapeshellarg($privkey_filename)); $domain['dkim_pubkey'] = file_get_contents($pubkey_filename); FileDir::safe_exec("chmod 0664 " . escapeshellarg($pubkey_filename)); FileDir::safe_exec("chown _rspamd:_rspamd " . escapeshellarg($pubkey_filename)); $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_DOMAINS . "` SET `dkim_id` = :dkimid, `dkim_privkey` = :privkey, `dkim_pubkey` = :pubkey WHERE `id` = :id "); $upd_data = [ 'dkimid' => $domain['dkim_id'], 'privkey' => $domain['dkim_privkey'], 'pubkey' => $domain['dkim_pubkey'], 'id' => $domain['id'] ]; Database::pexecute($upd_stmt, $upd_data); } else { $privkey_filename = FileDir::makeCorrectFile('/var/lib/rspamd/dkim/' . $domain['domain'] . '.dkim' . $domain['dkim_id'] . '.key'); $pubkey_filename = FileDir::makeCorrectFile('/var/lib/rspamd/dkim/' . $domain['domain'] . '.dkim' . $domain['dkim_id'] . '.txt'); } if (!file_exists($privkey_filename) && $domain['dkim_privkey'] != '') { file_put_contents($privkey_filename, $domain['dkim_privkey']); FileDir::safe_exec("chmod 0640 " . escapeshellarg($privkey_filename)); FileDir::safe_exec("chown _rspamd:_rspamd " . escapeshellarg($privkey_filename)); } if (!file_exists($pubkey_filename) && $domain['dkim_pubkey'] != '') { file_put_contents($pubkey_filename, $domain['dkim_pubkey']); FileDir::safe_exec("chmod 0644 " . escapeshellarg($pubkey_filename)); FileDir::safe_exec("chown _rspamd:_rspamd " . escapeshellarg($pubkey_filename)); } $dkim_selector_map .= $domain['domain'] . " dkim" . $domain['dkim_id'] . "\n"; } $dkim_selector_file = FileDir::makeCorrectFile('/etc/rspamd/dkim_selectors.map'); file_put_contents($dkim_selector_file, $dkim_selector_map); } private function generateEmailAddrConfig(array $email): void { $this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'Generating antispam config for ' . $email['email']); $email['spam_tag_level'] = floatval($email['spam_tag_level']); $email['spam_kill_level'] = $email['spam_kill_level'] == -1 ? "null" : floatval($email['spam_kill_level']); $email_id = md5($email['email']); $this->frx_settings_file .= '# Email: ' . $email['email'] . "\n"; foreach (['rcpt', 'from'] as $type) { $this->frx_settings_file .= 'frx_' . $email_id . '_' . $type . ' {' . "\n"; $this->frx_settings_file .= ' id = "frx_' . $email_id . '_' . $type . '";' . "\n"; if ($email['iscatchall']) { $this->frx_settings_file .= ' priority = low;' . "\n"; $this->frx_settings_file .= ' ' . $type . ' = "' . substr($email['email'], strpos($email['email'], '@')) . '";' . "\n"; } else { $this->frx_settings_file .= ' priority = medium;' . "\n"; $this->frx_settings_file .= ' ' . $type . ' = "' . $email['email'] . '";' . "\n"; } if ((int)$email['bypass_spam'] == 1) { $this->frx_settings_file .= ' want_spam = yes;' . "\n"; } else { $this->frx_settings_file .= ' apply {' . "\n"; $this->frx_settings_file .= ' actions {' . "\n"; $this->frx_settings_file .= ' "add header" = ' . $email['spam_tag_level'] . ';' . "\n"; if ((int)$email['rewrite_subject'] == 1) { $this->frx_settings_file .= ' rewrite_subject = ' . ($email['spam_tag_level'] + 0.01) . ';' . "\n"; } $this->frx_settings_file .= ' reject = ' . $email['spam_kill_level'] . ';' . "\n"; if ($type == 'rcpt' && (int)$email['policy_greylist'] == 0) { $this->frx_settings_file .= ' greylist = null;' . "\n"; } $this->frx_settings_file .= ' }' . "\n"; $this->frx_settings_file .= ' }' . "\n"; if ($type == 'rcpt' && (int)$email['policy_greylist'] == 0) { $this->frx_settings_file .= ' symbols [ "DONT_GREYLIST" ]' . "\n"; } } $this->frx_settings_file .= '}' . "\n"; } $this->frx_settings_file .= "\n"; } public function reloadDaemon() { // reload DNS daemon $cmd = Settings::Get('antispam.reload_command'); $cmdStatus = 1; FileDir::safe_exec(escapeshellcmd($cmd), $cmdStatus); if ($cmdStatus === 0) { $this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'Antispam daemon reloaded'); } else { $this->logger->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, 'Error while running `' . $cmd . '`: exit code (' . $cmdStatus . ') - please check your system logs'); } } } ================================================ FILE: lib/Froxlor/Cron/System/ExportCron.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cron\System; use Exception; use Froxlor\Cron\Forkable; use Froxlor\Cron\FroxlorCron; use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\FroxlorLogger; use Froxlor\Settings; class ExportCron extends FroxlorCron { use Forkable; public static function run() { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'ExportCron: started - creating customer data export'); $result_tasks_stmt = Database::query(" SELECT * FROM `" . TABLE_PANEL_TASKS . "` WHERE `type` = '20' ORDER BY `id` ASC "); $all_jobs = $result_tasks_stmt->fetchAll(); if (!empty($all_jobs)) { self::runFork([self::class, 'handle'], $all_jobs); } } public static function handle(array $row) { $del_stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_TASKS . "` WHERE `id` = :id"); $cronlog = FroxlorLogger::getInstanceOf(); if ($row['data'] != '') { $row['data'] = json_decode($row['data'], true); } if (is_array($row['data'])) { if (isset($row['data']['customerid']) && isset($row['data']['loginname']) && isset($row['data']['destdir'])) { $row['data']['destdir'] = FileDir::makeCorrectDir($row['data']['destdir']); $customerdocroot = FileDir::makeCorrectDir(Settings::Get('system.documentroot_prefix') . '/' . $row['data']['loginname'] . '/'); // create folder if not exists if (!file_exists($row['data']['destdir']) && $row['data']['destdir'] != '/' && $row['data']['destdir'] != Settings::Get('system.documentroot_prefix') && $row['data']['destdir'] != $customerdocroot) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'Creating data export destination path for customer: ' . escapeshellarg($row['data']['destdir'])); FileDir::safe_exec('mkdir -p ' . escapeshellarg($row['data']['destdir'])); } self::createCustomerExport($row['data'], $customerdocroot, $cronlog); } } // remove entry Database::pexecute($del_stmt, [ 'id' => $row['id'] ]); } /** * depending on the give choice, the customers web-data, email-data and databases are being exported * * @param array $data * * @return void * * @throws Exception */ private static function createCustomerExport($data = null, $customerdocroot = null, &$cronlog = null) { $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Creating data export for user "' . $data['loginname'] . '"'); // create tmp folder $tmpdir = FileDir::makeCorrectDir($data['destdir'] . '/.tmp/'); $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'Creating tmp-folder "' . $tmpdir . '"'); $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'shell> mkdir -p ' . escapeshellarg($tmpdir)); FileDir::safe_exec('mkdir -p ' . escapeshellarg($tmpdir)); $create_export_tar_data = ""; // MySQL databases if ($data['dump_dbs'] == 1) { $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'Creating mysql-folder "' . FileDir::makeCorrectDir($tmpdir . '/mysql') . '"'); $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'shell> mkdir -p ' . escapeshellarg(FileDir::makeCorrectDir($tmpdir . '/mysql'))); FileDir::safe_exec('mkdir -p ' . escapeshellarg(FileDir::makeCorrectDir($tmpdir . '/mysql'))); // get all customer database-names $sel_stmt = Database::prepare("SELECT `databasename`, `dbserver` FROM `" . TABLE_PANEL_DATABASES . "` WHERE `customerid` = :cid ORDER BY `dbserver`"); Database::pexecute($sel_stmt, [ 'cid' => $data['customerid'] ]); $has_dbs = false; $current_dbserver = -1; // look for mysqldump $section = 'mysqldump'; if (file_exists("/usr/bin/mysqldump")) { $mysql_dump = '/usr/bin/mysqldump'; } elseif (file_exists("/usr/local/bin/mysqldump")) { $mysql_dump = '/usr/local/bin/mysqldump'; } elseif (file_exists("/usr/bin/mariadb-dump")) { $mysql_dump = '/usr/bin/mariadb-dump'; $section = 'mariadb-dump'; } if (!isset($mysql_dump)) { $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, 'mysqldump/mariadb-dump executable could not be found. Please install mysql-client/mariadb-client package.'); } else { while ($row = $sel_stmt->fetch()) { // Get sql_root data for the specific database-server the database resides on if ($current_dbserver != $row['dbserver']) { Database::needRoot(true, $row['dbserver']); Database::needSqlData(); $sql_root = Database::getSqlData(); Database::needRoot(false); // create temporary mysql-defaults file for the connection-credentials/details $mysqlcnf_file = tempnam("/tmp", "frx"); $mysqlcnf = "[".$section."]\npassword=" . $sql_root['passwd'] . "\nhost=" . $sql_root['host'] . "\n"; if (!empty($sql_root['port'])) { $mysqlcnf .= "port=" . $sql_root['port'] . "\n"; } elseif (!empty($sql_root['socket'])) { $mysqlcnf .= "socket=" . $sql_root['socket'] . "\n"; } file_put_contents($mysqlcnf_file, $mysqlcnf); } $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'shell> '.basename($mysql_dump) . ' -u ' . escapeshellarg($sql_root['user']) . ' -pXXXXX ' . $row['databasename'] . ' > ' . FileDir::makeCorrectFile($tmpdir . '/mysql/' . $row['databasename'] . '_' . date('YmdHi', time()) . '.sql')); $bool_false = false; FileDir::safe_exec($mysql_dump . ' --defaults-file=' . escapeshellarg($mysqlcnf_file) . ' -u ' . escapeshellarg($sql_root['user']) . ' ' . $row['databasename'] . ' > ' . FileDir::makeCorrectFile($tmpdir . '/mysql/' . $row['databasename'] . '_' . date('YmdHi', time()) . '.sql'), $bool_false, [ '>' ]); $has_dbs = true; $current_dbserver = $row['dbserver']; } } if ($has_dbs) { $create_export_tar_data .= './mysql '; } if (file_exists($mysqlcnf_file)) { unlink($mysqlcnf_file); } unset($sql_root); } // E-mail data if ($data['dump_mail'] == 1) { $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'Creating mail-folder "' . FileDir::makeCorrectDir($tmpdir . '/mail') . '"'); FileDir::safe_exec('mkdir -p ' . escapeshellarg(FileDir::makeCorrectDir($tmpdir . '/mail'))); // get all customer mail-accounts $sel_stmt = Database::prepare("SELECT `homedir`, `maildir` FROM `" . TABLE_MAIL_USERS . "` WHERE `customerid` = :cid"); Database::pexecute($sel_stmt, [ 'cid' => $data['customerid'] ]); $tar_file_list = ""; $mail_homedir = ""; while ($row = $sel_stmt->fetch()) { $tar_file_list .= escapeshellarg("./" . $row['maildir']) . " "; $mail_homedir = $row['homedir']; } if (!empty($tar_file_list)) { $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'shell> tar cfvz ' . escapeshellarg(FileDir::makeCorrectFile($tmpdir . '/mail/' . $data['loginname'] . '-mail.tar.gz')) . ' -C ' . escapeshellarg($mail_homedir) . ' ' . trim($tar_file_list)); FileDir::safe_exec('tar cfz ' . escapeshellarg(FileDir::makeCorrectFile($tmpdir . '/mail/' . $data['loginname'] . '-mail.tar.gz')) . ' -C ' . escapeshellarg($mail_homedir) . ' ' . trim($tar_file_list)); $create_export_tar_data .= './mail '; } } // Web data if ($data['dump_web'] == 1) { $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'Creating web-folder "' . FileDir::makeCorrectDir($tmpdir . '/web') . '"'); FileDir::safe_exec('mkdir -p ' . escapeshellarg(FileDir::makeCorrectDir($tmpdir . '/web'))); $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'shell> tar cfz ' . escapeshellarg(FileDir::makeCorrectFile($tmpdir . '/web/' . $data['loginname'] . '-web.tar.gz')) . ' --exclude=' . escapeshellarg(str_replace($customerdocroot, "./", FileDir::makeCorrectFile($tmpdir . '/*'))) . ' --exclude=' . escapeshellarg(str_replace($customerdocroot, "./", substr(FileDir::makeCorrectDir($tmpdir), 0, -1))) . ' -C ' . escapeshellarg($customerdocroot) . ' .'); FileDir::safe_exec('tar cfz ' . escapeshellarg(FileDir::makeCorrectFile($tmpdir . '/web/' . $data['loginname'] . '-web.tar.gz')) . ' --exclude=' . escapeshellarg(str_replace($customerdocroot, "./", FileDir::makeCorrectFile($tmpdir . '/*'))) . ' --exclude=' . escapeshellarg(str_replace($customerdocroot, "./", substr(FileDir::makeCorrectFile($tmpdir), 0, -1))) . ' -C ' . escapeshellarg($customerdocroot) . ' .'); $create_export_tar_data .= './web '; } if (!empty($create_export_tar_data)) { // set owner to customer $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'shell> chown -R ' . (int)$data['uid'] . ':' . (int)$data['gid'] . ' ' . escapeshellarg($tmpdir)); FileDir::safe_exec('chown -R ' . (int)$data['uid'] . ':' . (int)$data['gid'] . ' ' . escapeshellarg($tmpdir)); // create tar-file $export_file = FileDir::makeCorrectFile($tmpdir . '/' . $data['loginname'] . '-export_' . date('YmdHi', time()) . '.tar.gz' . (!empty($data['pgp_public_key']) ? '.gpg' : '')); $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'Creating export-file "' . $export_file . '"'); if (!empty($data['pgp_public_key'])) { // pack all archives in tmp-dir to one archive and encrypt it with gpg $recipient_file = FileDir::makeCorrectFile($tmpdir . '/' . $data['loginname'] . '-recipients.gpg'); $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'Creating recipient-file "' . $recipient_file . '"'); file_put_contents($recipient_file, $data['pgp_public_key']); $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'shell> tar cfz - -C ' . escapeshellarg($tmpdir) . ' ' . trim($create_export_tar_data) . ' | gpg --encrypt --recipient-file ' . escapeshellarg($recipient_file) . ' --output ' . escapeshellarg($export_file) . ' --trust-model always --batch --yes'); FileDir::safe_exec('tar cfz - -C ' . escapeshellarg($tmpdir) . ' ' . trim($create_export_tar_data) . ' | gpg --encrypt --recipient-file ' . escapeshellarg($recipient_file) . ' --output ' . escapeshellarg($export_file) . ' --trust-model always --batch --yes', $return_value, ['|']); } else { // pack all archives in tmp-dir to one archive $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'shell> tar cfz ' . escapeshellarg($export_file) . ' -C ' . escapeshellarg($tmpdir) . ' ' . trim($create_export_tar_data)); FileDir::safe_exec('tar cfz ' . escapeshellarg($export_file) . ' -C ' . escapeshellarg($tmpdir) . ' ' . trim($create_export_tar_data)); } // move to destination directory $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'shell> mv ' . escapeshellarg($export_file) . ' ' . escapeshellarg($data['destdir'])); FileDir::safe_exec('mv ' . escapeshellarg($export_file) . ' ' . escapeshellarg($data['destdir'])); // remove tmp-files $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'shell> rm -rf ' . escapeshellarg($tmpdir)); FileDir::safe_exec('rm -rf ' . escapeshellarg($tmpdir)); // set owner to customer $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'shell> chown -R ' . (int)$data['uid'] . ':' . (int)$data['gid'] . ' ' . escapeshellarg($data['destdir'])); if (is_link(rtrim($data['destdir'], '/'))) { $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, 'Export destination is a symlink, skipping chown for security: ' . $data['destdir']); } else { FileDir::safe_exec('chown -R ' . (int)$data['uid'] . ':' . (int)$data['gid'] . ' ' . escapeshellarg($data['destdir'])); } } } } ================================================ FILE: lib/Froxlor/Cron/System/Extrausers.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cron\System; use Froxlor\Customer\Customer; use Froxlor\Database\Database; use Froxlor\FroxlorLogger; use Froxlor\Settings; use Froxlor\User; use PDO; class Extrausers { public static function generateFiles(&$cronlog) { // passwd $passwd = '/var/lib/extrausers/passwd'; $sql = "SELECT customerid,username,'x' as password,uid,gid,'Froxlor User' as comment,homedir,shell, login_enabled FROM ftp_users ORDER BY uid, LENGTH(username) ASC"; $users_list = []; self::generateFile($passwd, $sql, $cronlog, $users_list); // group $group = '/var/lib/extrausers/group'; $sql = "SELECT groupname,'x' as password,gid,members FROM ftp_groups ORDER BY gid ASC"; self::generateFile($group, $sql, $cronlog, $users_list); // shadow $shadow = '/var/lib/extrausers/shadow'; $sql = "SELECT username,password FROM ftp_users ORDER BY gid ASC"; self::generateFile($shadow, $sql, $cronlog); // set correct permissions @chmod('/var/lib/extrausers/', 0755); @chmod('/var/lib/extrausers/passwd', 0644); @chmod('/var/lib/extrausers/group', 0644); @chmod('/var/lib/extrausers/shadow', 0640); SshKeys::generateFiles($cronlog); } private static function generateFile($file, $query, &$cronlog, &$result_list = null) { $type = basename($file); $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Creating ' . $type . ' file'); if (!file_exists($file)) { $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, $type . ' file does not yet exist'); @mkdir(dirname($file), 0750, true); touch($file); } $data_sel_stmt = Database::query($query); $data_content = ""; $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Writing ' . $data_sel_stmt->rowCount() . ' entries to ' . $type . ' file'); while ($u = $data_sel_stmt->fetch(PDO::FETCH_ASSOC)) { switch ($type) { case 'passwd': // get user real name $salutation_array = [ 'firstname' => Customer::getCustomerDetail($u['customerid'], 'firstname'), 'name' => Customer::getCustomerDetail($u['customerid'], 'name'), 'company' => Customer::getCustomerDetail($u['customerid'], 'company') ]; $u['comment'] = self::cleanString(User::getCorrectUserSalutation($salutation_array)); if ($u['login_enabled'] != 'Y') { $u['password'] = '*'; $u['shell'] = '/bin/false'; $u['comment'] = 'Locked Froxlor User'; } if (Customer::getCustomerDetail($u['customerid'], 'shell_allowed') == '0') { $u['shell'] = '/bin/false'; } $line = $u['username'] . ':' . $u['password'] . ':' . $u['uid'] . ':' . $u['gid'] . ':' . $u['comment'] . ':' . $u['homedir'] . ':' . $u['shell'] . PHP_EOL; if (is_array($result_list)) { $result_list[] = $u['username']; } break; case 'group': $line = $u['groupname'] . ':' . $u['password'] . ':' . $u['gid'] . ':' . $u['members'] . PHP_EOL; break; case 'shadow': $line = $u['username'] . ':' . $u['password'] . ':' . floor(time() / 86400 - 1) . ':0:99999:7:::' . PHP_EOL; break; } $data_content .= $line; } // check for local group to generate if ($type == 'group' && Settings::Get('system.froxlorusergroup') != '') { $guid = intval(Settings::Get('system.froxlorusergroup_gid')); if (empty($guid)) { $guid = intval(Settings::Get('system.lastguid')) + 1; Settings::Set('system.lastguid', $guid, true); Settings::Set('system.froxlorusergroup_gid', $guid, true); } $line = Settings::Get('system.froxlorusergroup') . ':x:' . $guid . ':' . implode(',', $result_list) . PHP_EOL; $data_content .= $line; } if (file_put_contents($file, $data_content) !== false) { $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Succesfully wrote ' . $type . ' file'); } else { $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Error when writing ' . $type . ' file entries'); } } private static function cleanString($string = null) { $allowed = "/[^a-z0-9\\.\\-\\_\\ ]/i"; return preg_replace($allowed, "", $string); } } ================================================ FILE: lib/Froxlor/Cron/System/MailboxsizeCron.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cron\System; use Froxlor\Cron\FroxlorCron; use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\FroxlorLogger; use PDO; class MailboxsizeCron extends FroxlorCron { public static function run() { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'calculating mailspace usage'); $maildirs_stmt = Database::query(" SELECT `id`, CONCAT(`homedir`, `maildir`) AS `maildirpath` FROM `" . TABLE_MAIL_USERS . "` ORDER BY `id` "); $upd_stmt = Database::prepare(" UPDATE `" . TABLE_MAIL_USERS . "` SET `mboxsize` = :size WHERE `id` = :id "); while ($maildir = $maildirs_stmt->fetch(PDO::FETCH_ASSOC)) { $_maildir = FileDir::makeCorrectDir($maildir['maildirpath']); if (file_exists($_maildir) && is_dir($_maildir)) { // compute actual maildirsize with `du` // mail-address allows many special characters, see http://en.wikipedia.org/wiki/Email_address#Local_part $return = false; $back = FileDir::safe_exec('du -sk ' . escapeshellarg($_maildir), $return, [ '|', '&', '`', '$', '~', '?' ]); foreach ($back as $backrow) { $emailusage = explode(' ', $backrow); } $emailusage = floatval($emailusage['0']); // as freebsd does not have the -b flag for 'du' which gives // the size in bytes, we use "-sk" for all and calculate from KiB $emailusage *= 1024; unset($back); Database::pexecute($upd_stmt, [ 'size' => $emailusage, 'id' => $maildir['id'] ]); } else { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_WARNING, 'maildir ' . $_maildir . ' does not exist'); } } } } ================================================ FILE: lib/Froxlor/Cron/System/SshKeys.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cron\System; use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\FroxlorLogger; use Froxlor\Settings; use PDO; class SshKeys { /** * @throws \Exception */ public static function generateFiles(&$cronlog) { if (intval(Settings::Get('system.allow_customer_shell')) == 0) { return; } // authorized_keys $sel_stmt = Database::prepare(" SELECT id,customerid,username,homedir,uid,gid,shell,login_enabled FROM `" . TABLE_FTP_USERS . "` ORDER BY uid, LENGTH(username) ASC "); Database::pexecute($sel_stmt); $sshkeys_sel_stmt = Database::prepare(" SELECT `id`, `ssh_pubkey` FROM `" . TABLE_PANEL_USER_SSHKEYS . "` WHERE `ftp_user_id` = :fuid AND `customerid` = :cid "); while ($usr = $sel_stmt->fetch(PDO::FETCH_ASSOC)) { $userHomeDir = FileDir::makeCorrectDir($usr['homedir'] . '/.ssh', $usr['homedir']); $authkeysfile = FileDir::makeCorrectFile($userHomeDir . '/authorized_keys', $usr['homedir']); $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Creating file ' . $authkeysfile); // remove all entries with 'froxlor:id=...' self::removeFroxlorKeys($authkeysfile, $cronlog); // get keys Database::pexecute($sshkeys_sel_stmt, ['fuid' => $usr['id'], 'cid' => $usr['customerid']]); if ($sshkeys_sel_stmt->rowCount() > 0) { if (!file_exists(dirname($authkeysfile))) { @mkdir(dirname($authkeysfile), 0700); } if (!file_exists($authkeysfile)) { @touch($authkeysfile); } while ($sshkey = $sshkeys_sel_stmt->fetch(PDO::FETCH_ASSOC)) { // normalize: Algo + Base64 part (remove comment) $parts = preg_split('/\s+/', trim($sshkey['ssh_pubkey']), 3); if (count($parts) < 2) { // Invalid public key format continue; } $normalized = $parts[0] . ' ' . $parts[1]; // Datei lesen (falls sie schon existiert) $existing = []; if (file_exists($authkeysfile)) { $lines = file($authkeysfile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); foreach ($lines as $line) { $lineParts = preg_split('/\s+/', trim($line), 3); if (count($lineParts) >= 2) { $existing[] = $lineParts[0] . ' ' . $lineParts[1]; } } } // does the key exist already? if (in_array($normalized, $existing, true)) { // skip continue; } // add key $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Adding ssh-key for user ' . $usr['username']); file_put_contents($authkeysfile, trim($sshkey['ssh_pubkey']) . " froxlor:id=" . $sshkey['id'] . "\n", FILE_APPEND | LOCK_EX); } } @chmod(dirname($authkeysfile), 0700); @chown(dirname($authkeysfile), $usr['uid']); @chgrp(dirname($authkeysfile), $usr['gid']); @chmod($authkeysfile, 0600); @chown($authkeysfile, $usr['uid']); @chgrp($authkeysfile, $usr['gid']); } } private static function removeFroxlorKeys(string $authFile, &$cronlog): bool { if (!file_exists($authFile)) { return false; } $lines = file($authFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); $newLines = []; foreach ($lines as $line) { // kill whitespaces $trim = trim($line); // if comment 'froxlor:id=' skip line $matches = []; if (preg_match('/\bfroxlor:id=(.+)/', $trim, $matches)) { $cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_DEBUG, 'Removing ' . $matches[0]); continue; } if (!empty($trim)) { $newLines[] = $line; } } // use LOCK_EX to avoid race if (empty($newLines)) { // empty file, we can safely remove it @unlink($authFile); } else { // keep existing (non-froxlor-managed entries) file_put_contents($authFile, implode("\n", $newLines) . "\n", LOCK_EX); } return true; } } ================================================ FILE: lib/Froxlor/Cron/System/TasksCron.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cron\System; use Exception; use Froxlor\Cron\FroxlorCron; use Froxlor\Cron\Http\ConfigIO; use Froxlor\Cron\Http\HttpConfigBase; use Froxlor\Cron\Http\LetsEncrypt\AcmeSh; use Froxlor\Cron\Mail\Rspamd; use Froxlor\Cron\TaskId; use Froxlor\Database\Database; use Froxlor\Dns\PowerDNS; use Froxlor\Domain\Domain; use Froxlor\FileDir; use Froxlor\FroxlorLogger; use Froxlor\Settings; use PDO; class TasksCron extends FroxlorCron { /** * @throws Exception */ public static function run() { /** * LOOK INTO TASKS TABLE TO SEE IF THERE ARE ANY UNDONE JOBS */ self::$cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "TasksCron: Searching for tasks to do"); // no type 99 (regenerate cron.d-file) and no type 20 (customer data export) // order by type descending to re-create bind and then webserver at the end $result_tasks_stmt = Database::query(" SELECT `id`, `type`, `data` FROM `" . TABLE_PANEL_TASKS . "` WHERE `type` <> '99' AND `type` <> '20' ORDER BY `type` DESC, `id` ASC "); $num_results = Database::num_rows(); $resultIDs = []; while ($row = $result_tasks_stmt->fetch(PDO::FETCH_ASSOC)) { $resultIDs[] = $row['id']; if ($row['data'] != '') { $row['data'] = json_decode($row['data'], true); } if ($row['type'] == TaskId::REBUILD_VHOST) { /** * TYPE=1 MEANS TO REBUILD APACHE VHOSTS.CONF */ self::rebuildWebserverConfigs(); } elseif ($row['type'] == TaskId::CREATE_HOME) { /** * TYPE=2 MEANS TO CREATE A NEW HOME AND CHOWN */ self::createNewHome($row); } elseif ($row['type'] == TaskId::REBUILD_DNS && (int)Settings::Get('system.bind_enable') != 0) { /** * TYPE=4 MEANS THAT SOMETHING IN THE BIND CONFIG HAS CHANGED. * REBUILD froxlor_bind.conf IF BIND IS ENABLED */ self::rebuildDnsConfigs(); } elseif ($row['type'] == TaskId::CREATE_FTP) { /** * TYPE=5 MEANS THAT A NEW FTP-ACCOUNT HAS BEEN CREATED, CREATE THE DIRECTORY */ self::createNewFtpHome($row); } elseif ($row['type'] == TaskId::DELETE_CUSTOMER_FILES) { /** * TYPE=6 MEANS THAT A CUSTOMER HAS BEEN DELETED AND THAT WE HAVE TO REMOVE ITS FILES */ self::deleteCustomerData($row); } elseif ($row['type'] == TaskId::DELETE_EMAIL_DATA) { /** * TYPE=7 Customer deleted an email account and wants the data to be deleted on the filesystem */ self::deleteEmailData($row); } elseif ($row['type'] == TaskId::DELETE_FTP_DATA) { /** * TYPE=8 Customer deleted a ftp account and wants the homedir to be deleted on the filesystem * refs #293 */ self::deleteFtpData($row); } elseif ($row['type'] == TaskId::REBUILD_RSPAMD && (int)Settings::Get('antispam.activated') != 0) { /** * TYPE=9 Rebuild antispam config */ self::rebuildAntiSpamConfigs(); } elseif ($row['type'] == TaskId::CREATE_QUOTA && (int)Settings::Get('system.diskquota_enabled') != 0) { /** * TYPE=10 Set the filesystem - quota */ self::setFilesystemQuota(); } elseif ($row['type'] == TaskId::DELETE_DOMAIN_PDNS && Settings::Get('system.dns_server') == 'PowerDNS') { /** * TYPE=11 domain has been deleted, remove from pdns database if used */ FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, "Removing PowerDNS entries for domain " . $row['data']['domain']); PowerDNS::cleanDomainZone($row['data']['domain']); } elseif ($row['type'] == TaskId::DELETE_DOMAIN_SSL) { /** * TYPE=12 domain has been deleted, remove from acme.sh/let's encrypt directory if used */ FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, "Removing Let's Encrypt entries for domain " . $row['data']['domain']); Domain::doLetsEncryptCleanUp($row['data']['domain']); } elseif ($row['type'] == TaskId::UPDATE_LE_SERVICES) { /** * TYPE=13 set configuration for selected services regarding the use of Let's Encrypt certificate */ FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, "Updating Let's Encrypt configuration for selected services"); AcmeSh::renewHookConfigs(FroxlorLogger::getInstanceOf()); } elseif ($row['type'] == TaskId::REBUILD_NSSUSERS) { /** * TYPE=14 regenerate libnss users/groups */ self::refreshUsers(); } } if ($num_results != 0) { $where = []; $where_data = []; foreach ($resultIDs as $id) { $where[] = "`id` = :id_" . (int)$id; $where_data['id_' . $id] = $id; } $where = implode(' OR ', $where); $del_stmt = Database::prepare("DELETE FROM `" . TABLE_PANEL_TASKS . "` WHERE " . $where); Database::pexecute($del_stmt, $where_data); unset($resultIDs); unset($where); } Database::query("UPDATE `" . TABLE_PANEL_SETTINGS . "` SET `value` = UNIX_TIMESTAMP() WHERE `settinggroup` = 'system' AND `varname` = 'last_tasks_run';"); } private static function rebuildWebserverConfigs() { if (Settings::Get('system.webserver') == "apache2") { $websrv = '\\Froxlor\\Cron\\Http\\Apache'; if (Settings::Get('system.mod_fcgid') == 1 || Settings::Get('phpfpm.enabled') == 1) { $websrv .= 'Fcgi'; } } elseif (Settings::Get('system.webserver') == "nginx") { $websrv = '\\Froxlor\\Cron\\Http\\Nginx'; if (Settings::Get('phpfpm.enabled') == 1) { $websrv .= 'Fcgi'; } } // get configuration-I/O object $configio = new ConfigIO(); // get webserver object $webserver = new $websrv(); if ($webserver instanceof HttpConfigBase) { $webserver->init(); // clean up old configs $configio->cleanUp(); $webserver->createIpPort(); $webserver->createVirtualHosts(); $webserver->createFileDirOptions(); $webserver->writeConfigs(); $webserver->createOwnVhostStarter(); $webserver->reload(); } else { echo "Please check you Webserver settings\n"; } // if we use php-fpm and have a local user for froxlor, we need to // add the webserver-user to the local-group in order to allow the webserver // to access the fpm-socket if (Settings::Get('phpfpm.enabled') == 1 && function_exists("posix_getgrnam")) { // get group info about the local-user's group (e.g. froxlorlocal) $groupinfo = posix_getgrnam(Settings::Get('phpfpm.vhost_httpgroup')); // check group members if (isset($groupinfo['members']) && !in_array(Settings::Get('system.httpuser'), $groupinfo['members'])) { // webserver has no access, add it if (FileDir::isFreeBSD()) { FileDir::safe_exec('pw usermod ' . escapeshellarg(Settings::Get('system.httpuser')) . ' -G ' . escapeshellarg(Settings::Get('phpfpm.vhost_httpgroup'))); } else { FileDir::safe_exec('usermod -a -G ' . escapeshellarg(Settings::Get('phpfpm.vhost_httpgroup')) . ' ' . escapeshellarg(Settings::Get('system.httpuser'))); } } } // Tell the Let's Encrypt cron it's okay to generate the certificate and enable the redirect afterwards $upd_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_DOMAINS . "` SET `ssl_redirect` = '3' WHERE `ssl_redirect` = '2'"); Database::pexecute($upd_stmt); } private static function createNewHome($row = null) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'TasksCron: Task2 started - create new home'); if (is_array($row['data'])) { // define paths $userhomedir = FileDir::makeCorrectDir(Settings::Get('system.documentroot_prefix') . '/' . $row['data']['loginname'] . '/'); $usermaildir = FileDir::makeCorrectDir(Settings::Get('system.vmail_homedir') . '/' . $row['data']['loginname'] . '/'); // stats directory $statsdir = FileDir::makeCorrectDir($userhomedir . '/' . Settings::Get('system.traffictool')); FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Running: mkdir -p ' . escapeshellarg($statsdir)); FileDir::safe_exec('mkdir -p ' . escapeshellarg($statsdir)); foreach (['webalizer', 'awstats', 'goaccess'] as $statstools) { $statsdir = FileDir::makeCorrectDir($userhomedir . '/' . $statstools); // in case we changed from the other stats -> remove old if (Settings::Get('system.traffictool') != $statstools && file_exists($statsdir)) { // (yes i know, the stats are lost - that's why you should not change all the time!) FileDir::safe_exec('rm -rf ' . escapeshellarg($statsdir)); } } // maildir FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Running: mkdir -p ' . escapeshellarg($usermaildir)); FileDir::safe_exec('mkdir -p ' . escapeshellarg($usermaildir)); // check if admin of customer has added template for new customer directories if ((int)$row['data']['store_defaultindex'] == 1) { FileDir::storeDefaultIndex($row['data']['loginname'], $userhomedir, FroxlorLogger::getInstanceOf(), true); } // strip of last slash of paths to have correct chown results $userhomedir = (substr($userhomedir, 0, -1) == '/') ? substr($userhomedir, 0, -1) : $userhomedir; $usermaildir = (substr($usermaildir, 0, -1) == '/') ? substr($usermaildir, 0, -1) : $usermaildir; FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Running: chown -R ' . (int)$row['data']['uid'] . ':' . (int)$row['data']['gid'] . ' ' . escapeshellarg($userhomedir)); FileDir::safe_exec('chown -R ' . (int)$row['data']['uid'] . ':' . (int)$row['data']['gid'] . ' ' . escapeshellarg($userhomedir)); // don't allow others to access the directory (webserver will be the group via libnss-mysql) if (Settings::Get('system.mod_fcgid') == 1 || Settings::Get('phpfpm.enabled') == 1) { // fcgid or fpm FileDir::safe_exec('chmod 0750 ' . escapeshellarg($userhomedir)); } else { // mod_php -> no libnss-mysql -> no webserver-user in group FileDir::safe_exec('chmod 0755 ' . escapeshellarg($userhomedir)); } FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Running: chown -R ' . (int)Settings::Get('system.vmail_uid') . ':' . (int)Settings::Get('system.vmail_gid') . ' ' . escapeshellarg($usermaildir)); FileDir::safe_exec('chown -R ' . (int)Settings::Get('system.vmail_uid') . ':' . (int)Settings::Get('system.vmail_gid') . ' ' . escapeshellarg($usermaildir)); // explicitly create files after user has been created to avoid unknown user issues for apache/php-fpm when task#1 runs after this self::refreshUsers(); } } private static function rebuildDnsConfigs() { $dnssrv = '\\Froxlor\\Cron\\Dns\\' . Settings::Get('system.dns_server'); $nameserver = new $dnssrv(FroxlorLogger::getInstanceOf()); $nameserver->writeConfigs(); } private static function createNewFtpHome($row = null) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'Creating new FTP-home'); $result_directories_stmt = Database::query(" SELECT `f`.`homedir`, `f`.`uid`, `f`.`gid`, `c`.`documentroot` AS `customerroot` FROM `" . TABLE_FTP_USERS . "` `f` LEFT JOIN `" . TABLE_PANEL_CUSTOMERS . "` `c` USING (`customerid`) "); while ($directory = $result_directories_stmt->fetch(PDO::FETCH_ASSOC)) { FileDir::mkDirWithCorrectOwnership($directory['customerroot'], $directory['homedir'], $directory['uid'], $directory['gid']); } } private static function deleteCustomerData($row = null) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'TasksCron: Task6 started - deleting customer data'); if (is_array($row['data'])) { if (isset($row['data']['loginname'])) { // remove homedir $homedir = FileDir::makeCorrectDir(Settings::Get('system.documentroot_prefix') . '/' . $row['data']['loginname']); if (file_exists($homedir) && $homedir != '/' && $homedir != Settings::Get('system.documentroot_prefix') && substr($homedir, 0, strlen(Settings::Get('system.documentroot_prefix'))) == Settings::Get('system.documentroot_prefix')) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Running: rm -rf ' . escapeshellarg($homedir)); FileDir::safe_exec('rm -rf ' . escapeshellarg($homedir)); } // remove maildir $maildir = FileDir::makeCorrectDir(Settings::Get('system.vmail_homedir') . '/' . $row['data']['loginname']); if (file_exists($maildir) && $maildir != '/' && $maildir != Settings::Get('system.vmail_homedir') && substr($maildir, 0, strlen(Settings::Get('system.vmail_homedir'))) == Settings::Get('system.vmail_homedir') && is_dir($maildir) && fileowner($maildir) == Settings::Get('system.vmail_uid') && filegroup($maildir) == Settings::Get('system.vmail_gid')) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Running: rm -rf ' . escapeshellarg($maildir)); // mail-address allows many special characters, see http://en.wikipedia.org/wiki/Email_address#Local_part $return = false; FileDir::safe_exec('rm -rf ' . escapeshellarg($maildir), $return, [ '|', '&', '`', '$', '?' ]); } // remove tmpdir if it exists $tmpdir = FileDir::makeCorrectDir(Settings::Get('system.mod_fcgid_tmpdir') . '/' . $row['data']['loginname'] . '/'); if (file_exists($tmpdir) && is_dir($tmpdir) && $tmpdir != "/" && $tmpdir != Settings::Get('system.mod_fcgid_tmpdir') && substr($tmpdir, 0, strlen(Settings::Get('system.mod_fcgid_tmpdir'))) == Settings::Get('system.mod_fcgid_tmpdir')) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Running: rm -rf ' . escapeshellarg($tmpdir)); FileDir::safe_exec('rm -rf ' . escapeshellarg($tmpdir)); } // webserver logs $logsdir = FileDir::makeCorrectFile(Settings::Get('system.logfiles_directory') . '/' . $row['data']['loginname']); if (file_exists(dirname($logsdir)) && $logsdir != '/' && $logsdir != FileDir::makeCorrectDir(Settings::Get('system.logfiles_directory')) && substr($logsdir, 0, strlen(Settings::Get('system.logfiles_directory'))) == Settings::Get('system.logfiles_directory')) { // build up wildcard for webX-{access,error}.log{*} $logsdir .= '-*.log'; FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Running: rm -rf ' . FileDir::makeCorrectFile($logsdir)); FileDir::safe_exec('rm -f ' . FileDir::makeCorrectFile($logsdir)); } } } } private static function deleteEmailData($row = null) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'TasksCron: Task7 started - deleting customer e-mail data'); if (is_array($row['data'])) { if (isset($row['data']['loginname']) && isset($row['data']['emailpath'])) { // remove specific maildir $email_full = $row['data']['emailpath']; if (empty($email_full)) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, 'FATAL: Task7 asks to delete a email account but emailpath field is empty!'); } $maildir = FileDir::makeCorrectDir($email_full); if ($maildir != '/' && !empty($maildir) && $maildir != Settings::Get('system.vmail_homedir') && substr($maildir, 0, strlen(Settings::Get('system.vmail_homedir'))) == Settings::Get('system.vmail_homedir') && is_dir($maildir) && fileowner($maildir) == Settings::Get('system.vmail_uid') && filegroup($maildir) == Settings::Get('system.vmail_gid')) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Running: rm -rf ' . escapeshellarg($maildir)); // mail-address allows many special characters, see http://en.wikipedia.org/wiki/Email_address#Local_part $return = false; FileDir::safe_exec('rm -rf ' . escapeshellarg($maildir), $return, [ '|', '&', '`', '$', '~', '?' ]); } } } } private static function deleteFtpData($row = null) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'TasksCron: Task8 started - deleting customer ftp homedir'); if (is_array($row['data'])) { if (isset($row['data']['loginname']) && isset($row['data']['homedir'])) { // remove specific homedir $ftphomedir = FileDir::makeCorrectDir($row['data']['homedir']); $customerdocroot = FileDir::makeCorrectDir(Settings::Get('system.documentroot_prefix') . '/' . $row['data']['loginname'] . '/'); if (file_exists($ftphomedir) && $ftphomedir != '/' && $ftphomedir != Settings::Get('system.documentroot_prefix') && $ftphomedir != $customerdocroot) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Running: rm -rf ' . escapeshellarg($ftphomedir)); FileDir::safe_exec('rm -rf ' . escapeshellarg($ftphomedir)); } } } } private static function setFilesystemQuota() { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'TasksCron: Task10 started - setting filesystem quota'); $usedquota = FileDir::getFilesystemQuota(); // Check whether we really have entries to check if (is_array($usedquota) && count($usedquota) > 0) { // Select all customers Froxlor knows about $result_stmt = Database::query("SELECT `guid`, `loginname`, `diskspace` FROM `" . TABLE_PANEL_CUSTOMERS . "`;"); while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { // We do not want to set a quota for root by accident if ($row['guid'] != 0) { $used_quota = isset($usedquota[$row['guid']]) ? $usedquota[$row['guid']]['block']['hard'] : 0; // The user has no quota in Froxlor, but on the filesystem if (($row['diskspace'] == 0 || $row['diskspace'] == -1024) && $used_quota != 0) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, "Disabling quota for " . $row['loginname']); if (FileDir::isFreeBSD()) { FileDir::safe_exec(Settings::Get('system.diskquota_quotatool_path') . " -e " . escapeshellarg(Settings::Get('system.diskquota_customer_partition')) . ":0:0 " . $row['guid']); } else { FileDir::safe_exec(Settings::Get('system.diskquota_quotatool_path') . " -u " . $row['guid'] . " -bl 0 -q 0 " . escapeshellarg(Settings::Get('system.diskquota_customer_partition'))); } } elseif ($row['diskspace'] != $used_quota && $row['diskspace'] != -1024) { // The user quota in Froxlor is different than on the filesystem FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, "Setting quota for " . $row['loginname'] . " from " . $used_quota . " to " . $row['diskspace']); if (FileDir::isFreeBSD()) { FileDir::safe_exec(Settings::Get('system.diskquota_quotatool_path') . " -e " . escapeshellarg(Settings::Get('system.diskquota_customer_partition')) . ":" . $row['diskspace'] . ":" . $row['diskspace'] . " " . $row['guid']); } else { FileDir::safe_exec(Settings::Get('system.diskquota_quotatool_path') . " -u " . $row['guid'] . " -bl " . $row['diskspace'] . " -q " . $row['diskspace'] . " " . escapeshellarg(Settings::Get('system.diskquota_customer_partition'))); } } } } } } /** * @throws Exception */ private static function rebuildAntiSpamConfigs() { $antispam = new Rspamd(FroxlorLogger::getInstanceOf()); $antispam->writeConfigs(); } private static function refreshUsers() { if (Settings::Get('system.nssextrausers') == 1) { $cronLog = FroxlorLogger::getInstanceOf(); Extrausers::generateFiles($cronLog); // reload crond as shell users might use crontab and the user is only known to crond if reloaded FileDir::safe_exec(escapeshellcmd(Settings::Get('system.crondreload'))); return; } // clear NSCD cache if using fcgid or fpm, #1570 - not needed for nss-extrausers if ((Settings::Get('system.mod_fcgid') == 1 || (int)Settings::Get('phpfpm.enabled') == 1) && Settings::Get('system.nssextrausers') == 0) { $false_val = false; FileDir::safe_exec('nscd -i passwd 1> /dev/null', $false_val, [ '>' ]); FileDir::safe_exec('nscd -i group 1> /dev/null', $false_val, [ '>' ]); // reload crond as shell users might use crontab and the user is only known to crond if reloaded FileDir::safe_exec(escapeshellcmd(Settings::Get('system.crondreload'))); } } } ================================================ FILE: lib/Froxlor/Cron/System/index.html ================================================ ================================================ FILE: lib/Froxlor/Cron/TaskId.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cron; use ReflectionClass; final class TaskId { /** * TYPE=1 MEANS TO REBUILD APACHE VHOSTS.CONF */ const REBUILD_VHOST = 1; /** * TYPE=2 MEANS TO CREATE A NEW HOME AND CHOWN */ const CREATE_HOME = 2; /** * TYPE=4 MEANS THAT SOMETHING IN THE DNS CONFIG HAS CHANGED. * REBUILD froxlor_bind.conf IF BIND IS ENABLED, UPDATE DKIM KEYS */ const REBUILD_DNS = 4; /** * TYPE=5 MEANS THAT A NEW FTP-ACCOUNT HAS BEEN CREATED, CREATE THE DIRECTORY */ const CREATE_FTP = 5; /** * TYPE=6 MEANS THAT A CUSTOMER HAS BEEN DELETED AND THAT WE HAVE TO REMOVE ITS FILES */ const DELETE_CUSTOMER_FILES = 6; /** * TYPE=7 Customer deleted an email account and wants the data to be deleted on the filesystem */ const DELETE_EMAIL_DATA = 7; /** * TYPE=8 Customer deleted a ftp account and wants the homedir to be deleted on the filesystem * refs #293 */ const DELETE_FTP_DATA = 8; /** * TYPE=9 MEANS THAT SOMETHING ANTISPAM RELATED HAS CHANGED. * REBUILD froxlor_settings.conf IF ANTISPAM IS ENABLED */ const REBUILD_RSPAMD = 9; /** * TYPE=10 Set the filesystem - quota */ const CREATE_QUOTA = 10; /** * TYPE=11 domain has been deleted, remove from pdns database if used */ const DELETE_DOMAIN_PDNS = 11; /** * TYPE=12 domain has been deleted, remove from acme.sh/let's encrypt directory if used */ const DELETE_DOMAIN_SSL = 12; /** * TYPE=13 set configuration for selected services regarding the use of Let's Encrypt certificate */ const UPDATE_LE_SERVICES = 13; /** * TYPE=14 regenerate libnss users/groups */ const REBUILD_NSSUSERS = 14; /** * TYPE=20 CUSTUMER DATA DUMP */ const CREATE_CUSTOMER_DATADUMP = 20; /** * TYPE=99 REGENERATE CRON */ const REBUILD_CRON = 99; /** * Return if a cron task id is valid * * @param int|string $id cron task id (legacy string support) * @return boolean */ public static function isValid($id) { static $reflContants; if (!is_numeric($id)) { return false; } $numericid = (int)$id; if (!is_array($reflContants)) { $reflClass = new ReflectionClass(get_called_class()); $reflContants = $reflClass->getConstants(); } return in_array($numericid, $reflContants, true); } /** * Get constant name by id * * @param int|string $id cron task id (legacy string support) * @return string|false constant name or false if not found */ public static function convertToConstant($id) { static $reflContants; if (!is_numeric($id)) { return false; } $numericid = (int)$id; if (!is_array($reflContants)) { $reflClass = new ReflectionClass(get_called_class()); $reflContants = $reflClass->getConstants(); } return array_search($numericid, $reflContants, true); } } ================================================ FILE: lib/Froxlor/Cron/Traffic/ReportsCron.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cron\Traffic; /** * @author Florian Lippert (2003-2009) * @author Froxlor team (2010-) */ use Exception; use Froxlor\Cron\FroxlorCron; use Froxlor\Database\Database; use Froxlor\FroxlorLogger; use Froxlor\PhpHelper; use Froxlor\Settings; use Froxlor\System\Mailer; use Froxlor\User; use Froxlor\Language; use PDO; class ReportsCron extends FroxlorCron { public static function run() { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'Web- and Traffic-usage reporting started...'); $yesterday = time() - (60 * 60 * 24); /** * Initialize the mailingsystem */ $mail = new Mailer(true); // set default language before anything else to // ensure that we can display messages Language::setLanguage(Settings::Get('panel.standardlanguage')); if ((int)Settings::Get('system.report_trafficmax') > 0) { // Warn the customers at xx% traffic-usage $result_stmt = Database::prepare(" SELECT `c`.`customerid`, `c`.`loginname`, `c`.`customernumber`, `c`.`adminid`, `c`.`name`, `c`.`firstname`, `c`.`company`, `c`.`traffic`, `c`.`email`, `c`.`def_language`, `a`.`name` AS `adminname`, `a`.`email` AS `adminmail`, (SELECT SUM(`t`.`http` + `t`.`ftp_up` + `t`.`ftp_down` + `t`.`mail`) FROM `" . TABLE_PANEL_TRAFFIC . "` `t` WHERE `t`.`customerid` = `c`.`customerid` AND `t`.`year` = :year AND `t`.`month` = :month ) as `traffic_used` FROM `" . TABLE_PANEL_CUSTOMERS . "` AS `c` LEFT JOIN `" . TABLE_PANEL_ADMINS . "` AS `a` ON `a`.`adminid` = `c`.`adminid` WHERE `c`.`reportsent` & 1 = 0 "); $result_data = [ 'year' => date("Y", $yesterday), 'month' => date("m", $yesterday) ]; Database::pexecute($result_stmt, $result_data); $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `reportsent` = `reportsent` + 1 WHERE `customerid` = :customerid "); while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $row['traffic'] *= 1024; $row['traffic_used'] *= 1024; if (isset($row['traffic']) && $row['traffic'] > 0 && $row['traffic_used'] != null && (($row['traffic_used'] * 100) / $row['traffic']) >= (int)Settings::Get('system.report_trafficmax')) { $rep_userinfo = [ 'name' => $row['name'], 'firstname' => $row['firstname'], 'company' => $row['company'], 'loginname' => $row['loginname'], 'customernumber' => $row['customernumber'] ]; $replace_arr = [ 'SALUTATION' => User::getCorrectUserSalutation($rep_userinfo), 'NAME' => $rep_userinfo['name'], 'FIRSTNAME' => $rep_userinfo['firstname'], 'COMPANY' => $rep_userinfo['company'], 'USERNAME' => $rep_userinfo['loginname'], 'CUSTOMER_NO' => $rep_userinfo['customernumber'], 'TRAFFIC' => PhpHelper::sizeReadable((int)$row['traffic'], null, 'bi'), 'TRAFFICUSED' => PhpHelper::sizeReadable((int)$row['traffic_used'], null, 'bi'), 'USAGE_PERCENT' => round(($row['traffic_used'] * 100) / $row['traffic'], 2), 'MAX_PERCENT' => Settings::Get('system.report_trafficmax') ]; // set target user language Language::setLanguage($row['def_language']); // Get mail templates from database; the ones from 'admin' are fetched for fallback $result2_stmt = Database::prepare(" SELECT `value` FROM `" . TABLE_PANEL_TEMPLATES . "` WHERE `adminid` = :adminid AND `language` = :lang AND `templategroup` = 'mails' AND `varname` = :varname "); $result2_data = [ 'adminid' => $row['adminid'], 'lang' => $row['def_language'], 'varname' => 'trafficmaxpercent_subject' ]; $result2 = Database::pexecute_first($result2_stmt, $result2_data); $mail_subject = html_entity_decode(PhpHelper::replaceVariables((($result2 !== false && $result2['value'] != '') ? $result2['value'] : Language::getTranslation('mails.trafficmaxpercent.subject')), $replace_arr)); $result2_data['varname'] = 'trafficmaxpercent_mailbody'; $result2 = Database::pexecute_first($result2_stmt, $result2_data); $mail_body = html_entity_decode(PhpHelper::replaceVariables((($result2 !== false && $result2['value'] != '') ? $result2['value'] : Language::getTranslation('mails.trafficmaxpercent.mailbody')), $replace_arr)); $_mailerror = false; $mailerr_msg = ""; try { $mail->setFrom(Settings::Get('panel.adminmail'), $row['adminname']); $mail->clearReplyTos(); $mail->addReplyTo($row['adminmail'], $row['adminname']); $mail->Subject = $mail_subject; $mail->AltBody = $mail_body; $mail->MsgHTML(nl2br($mail_body)); $mail->AddAddress($row['email'], $row['firstname'] . ' ' . $row['name']); $mail->Send(); } catch (\PHPMailer\PHPMailer\Exception $e) { $mailerr_msg = $e->errorMessage(); $_mailerror = true; } catch (Exception $e) { $mailerr_msg = $e->getMessage(); $_mailerror = true; } if ($_mailerror) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, 'Error sending mail: ' . $mailerr_msg); echo 'Error sending mail: ' . $mailerr_msg . "\n"; } $mail->ClearAddresses(); Database::pexecute($upd_stmt, [ 'customerid' => $row['customerid'] ]); } } // Warn the admins at xx% traffic-usage $result_stmt = Database::prepare(" SELECT `a`.*, (SELECT SUM(`t`.`http` + `t`.`ftp_up` + `t`.`ftp_down` + `t`.`mail`) FROM `" . TABLE_PANEL_TRAFFIC_ADMINS . "` `t` WHERE `t`.`adminid` = `a`.`adminid` AND `t`.`year` = :year AND `t`.`month` = :month ) as `traffic_used_total` FROM `" . TABLE_PANEL_ADMINS . "` `a` WHERE `a`.`reportsent` & 1 = 0 "); $result_data = [ 'year' => date("Y", $yesterday), 'month' => date("m", $yesterday) ]; Database::pexecute($result_stmt, $result_data); $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_ADMINS . "` SET `reportsent` = `reportsent` + 1 WHERE `adminid` = :adminid "); while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $row['traffic'] *= 1024; $row['traffic_used_total'] *= 1024; if (isset($row['traffic']) && $row['traffic'] > 0 && (($row['traffic_used_total'] * 100) / ($row['traffic'])) >= (int)Settings::Get('system.report_trafficmax')) { $replace_arr = [ 'NAME' => $row['name'], 'TRAFFIC' => PhpHelper::sizeReadable((int)$row['traffic'], null, 'bi'), 'TRAFFICUSED' => PhpHelper::sizeReadable((int)$row['traffic_used_total'], null, 'bi'), 'USAGE_PERCENT' => round(($row['traffic_used_total'] * 100) / $row['traffic'], 2), 'MAX_PERCENT' => Settings::Get('system.report_trafficmax') ]; // set target user language Language::setLanguage($row['def_language']); // Get mail templates from database; the ones from 'admin' are fetched for fallback $result2_stmt = Database::prepare(" SELECT `value` FROM `" . TABLE_PANEL_TEMPLATES . "` WHERE `adminid` = :adminid AND `language` = :lang AND `templategroup` = 'mails' AND `varname` = :varname "); $result2_data = [ 'adminid' => $row['adminid'], 'lang' => $row['def_language'], 'varname' => 'trafficmaxpercent_subject' ]; $result2 = Database::pexecute_first($result2_stmt, $result2_data); $mail_subject = html_entity_decode(PhpHelper::replaceVariables((($result2 !== false && $result2['value'] != '') ? $result2['value'] : Language::getTranslation('mails.trafficmaxpercent.subject')), $replace_arr)); $result2_data['varname'] = 'trafficmaxpercent_mailbody'; $result2 = Database::pexecute_first($result2_stmt, $result2_data); $mail_body = html_entity_decode(PhpHelper::replaceVariables((($result2 !== false && $result2['value'] != '') ? $result2['value'] : Language::getTranslation('mails.trafficmaxpercent.mailbody')), $replace_arr)); $_mailerror = false; $mailerr_msg = ""; try { $mail->SetFrom(Settings::Get('panel.adminmail'), Settings::Get('panel.adminmail_defname')); $mail->Subject = $mail_subject; $mail->AltBody = $mail_body; $mail->MsgHTML(nl2br($mail_body)); $mail->AddAddress($row['email'], $row['name']); $mail->Send(); } catch (\PHPMailer\PHPMailer\Exception $e) { $mailerr_msg = $e->errorMessage(); $_mailerror = true; } catch (Exception $e) { $mailerr_msg = $e->getMessage(); $_mailerror = true; } if ($_mailerror) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, "Error sending mail: " . $mailerr_msg); echo "Error sending mail: " . $mailerr_msg . "\n"; } $mail->ClearAddresses(); Database::pexecute($upd_stmt, [ 'adminid' => $row['adminid'] ]); } // Another month, let's build our report if (date('d') == '01') { $mail_subject = 'Trafficreport ' . date("m/y", $yesterday) . ' for ' . $row['name']; $mail_body = 'Trafficreport ' . date("m/y", $yesterday) . ' for ' . $row['name'] . "\n"; $mail_body .= '---------------------------------------------------------------' . "\n"; $mail_body .= 'Loginname Traffic used (Percent) | Traffic available' . "\n"; $customers_stmt = Database::prepare(" SELECT `c`.*, (SELECT SUM(`t`.`http` + `t`.`ftp_up` + `t`.`ftp_down` + `t`.`mail`) FROM `" . TABLE_PANEL_TRAFFIC . "` `t` WHERE `t`.`customerid` = `c`.`customerid` AND `t`.`year` = :year AND `t`.`month` = :month ) as `traffic_used_total` FROM `" . TABLE_PANEL_CUSTOMERS . "` `c` WHERE `c`.`adminid` = :adminid "); $customers_data = [ 'year' => date("Y", $yesterday), 'month' => date("m", $yesterday), 'adminid' => $row['adminid'] ]; Database::pexecute($customers_stmt, $customers_data); while ($customer = $customers_stmt->fetch(PDO::FETCH_ASSOC)) { $customer['traffic'] *= 1024; $t = (int) $customer['traffic_used_total'] * 1024; if ($customer['traffic'] > 0) { $p = (($t * 100) / $customer['traffic']); $tg = (int) $customer['traffic']; $str = sprintf('%s ( %00.1f %% )', PhpHelper::sizeReadable($t, null, 'bi'), $p); $mail_body .= sprintf('%-15s', $customer['loginname']) . ' ' . sprintf('%-25s', $str) . ' ' . sprintf('%s', PhpHelper::sizeReadable($tg, null, 'bi')) . "\n"; } elseif ($customer['traffic'] == 0) { $str = sprintf('%s ( - )', PhpHelper::sizeReadable($t, null, 'bi')); $mail_body .= sprintf('%-15s', $customer['loginname']) . ' ' . sprintf('%-25s', $str) . ' ' . '0' . "\n"; } else { $str = sprintf('%s ( - )', PhpHelper::sizeReadable($t, null, 'bi')); $mail_body .= sprintf('%-15s', $customer['loginname']) . ' ' . sprintf('%-25s', $str) . ' ' . 'unlimited' . "\n"; } } $mail_body .= '---------------------------------------------------------------' . "\n"; $t = (int) $row['traffic_used_total']; if ($row['traffic'] > 0) { $p = (($t * 100) / $row['traffic']); $tg = (int) $row['traffic']; $str = sprintf('%s ( %00.1f %% )', PhpHelper::sizeReadable($t, null, 'bi'), $p); $mail_body .= sprintf('%-15s', $row['loginname']) . ' ' . sprintf('%-25s', $str) . ' ' . sprintf('%s', PhpHelper::sizeReadable($tg, null, 'bi')) . "\n"; } elseif ($row['traffic'] == 0) { $str = sprintf('%s ( - )', PhpHelper::sizeReadable($t, null, 'bi')); $mail_body .= sprintf('%-15s', $row['loginname']) . ' ' . sprintf('%-25s', $str) . ' ' . '0' . "\n"; } else { $str = sprintf('%s ( - )', PhpHelper::sizeReadable($t, null, 'bi')); $mail_body .= sprintf('%-15s', $row['loginname']) . ' ' . sprintf('%-25s', $str) . ' ' . 'unlimited' . "\n"; } $_mailerror = false; $mailerr_msg = ""; try { $mail->SetFrom(Settings::Get('panel.adminmail'), Settings::Get('panel.adminmail_defname')); $mail->Subject = $mail_subject; $mail->Body = $mail_body; $mail->MsgHTML(nl2br($mail_body)); $mail->AddAddress($row['email'], $row['name']); $mail->Send(); } catch (\PHPMailer\PHPMailer\Exception $e) { $mailerr_msg = $e->errorMessage(); $_mailerror = true; } catch (Exception $e) { $mailerr_msg = $e->getMessage(); $_mailerror = true; } if ($_mailerror) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, 'Error sending mail: ' . $mailerr_msg); echo 'Error sending mail: ' . $mailerr_msg . "\n"; } $mail->ClearAddresses(); } } } // trafficmax > 0 // include diskspace-usage report, #466 self::usageDiskspace(); // Another month, reset the reportstatus if (date('d') == '01') { Database::query("UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `reportsent` = '0';"); Database::query("UPDATE `" . TABLE_PANEL_ADMINS . "` SET `reportsent` = '0';"); } } private static function usageDiskspace() { if ((int)Settings::Get('system.report_webmax') > 0) { /** * report about diskusage for customers */ $result_stmt = Database::query(" SELECT `c`.`customerid`, `c`.`loginname`, `c`.`customernumber`, `c`.`adminid`, `c`.`name`, `c`.`firstname`, `c`.`company`, `c`.`diskspace`, `c`.`diskspace_used`, `c`.`email`, `c`.`def_language`, `a`.`name` AS `adminname`, `a`.`email` AS `adminmail` FROM `" . TABLE_PANEL_CUSTOMERS . "` AS `c` LEFT JOIN `" . TABLE_PANEL_ADMINS . "` AS `a` ON `a`.`adminid` = `c`.`adminid` WHERE `c`.`diskspace` > '0' AND `c`.`reportsent` & 2 = 0 "); $mail = new Mailer(true); $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `reportsent` = `reportsent` + 2 WHERE `customerid` = :customerid "); while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $row['diskspace'] *= 1024; $row['diskspace_used'] *= 1024; if (isset($row['diskspace']) && $row['diskspace_used'] != null && $row['diskspace_used'] > 0 && (($row['diskspace_used'] * 100) / $row['diskspace']) >= (int)Settings::Get('system.report_webmax')) { $rep_userinfo = [ 'name' => $row['name'], 'firstname' => $row['firstname'], 'company' => $row['company'], 'loginname' => $row['loginname'], 'customernumber' => $row['customernumber'] ]; $replace_arr = [ 'SALUTATION' => User::getCorrectUserSalutation($rep_userinfo), 'NAME' => $rep_userinfo['name'], 'FIRSTNAME' => $rep_userinfo['firstname'], 'COMPANY' => $rep_userinfo['company'], 'USERNAME' => $rep_userinfo['loginname'], 'CUSTOMER_NO' => $rep_userinfo['customernumber'], 'DISKAVAILABLE' => PhpHelper::sizeReadable((int)$row['diskspace'], null, 'bi'), 'DISKUSED' => PhpHelper::sizeReadable((int)$row['diskspace_used'], null, 'bi'), 'USAGE_PERCENT' => round(($row['diskspace_used'] * 100) / $row['diskspace'], 2), 'MAX_PERCENT' => Settings::Get('system.report_webmax') ]; // set target user language Language::setLanguage($row['def_language']); // Get mail templates from database; the ones from 'admin' are fetched for fallback $result2_stmt = Database::prepare(" SELECT `value` FROM `" . TABLE_PANEL_TEMPLATES . "` WHERE `adminid` = :adminid AND `language` = :lang AND `templategroup` = 'mails' AND `varname` = :varname "); $result2_data = [ 'adminid' => $row['adminid'], 'lang' => $row['def_language'], 'varname' => 'diskmaxpercent_subject' ]; $result2 = Database::pexecute_first($result2_stmt, $result2_data); $mail_subject = html_entity_decode(PhpHelper::replaceVariables((($result2 !== false && $result2['value'] != '') ? $result2['value'] : Language::getTranslation('mails.diskmaxpercent.subject')), $replace_arr)); $result2_data['varname'] = 'diskmaxpercent_mailbody'; $result2 = Database::pexecute_first($result2_stmt, $result2_data); $mail_body = html_entity_decode(PhpHelper::replaceVariables((($result2 !== false && $result2['value'] != '') ? $result2['value'] : Language::getTranslation('mails.diskmaxpercent.mailbody')), $replace_arr)); $_mailerror = false; $mailerr_msg = ""; try { $mail->setFrom(Settings::Get('panel.adminmail'), $row['adminname']); $mail->clearReplyTos(); $mail->addReplyTo($row['adminmail'], $row['adminname']); $mail->Subject = $mail_subject; $mail->AltBody = $mail_body; $mail->MsgHTML(nl2br($mail_body)); $mail->AddAddress($row['email'], $row['name']); if (Settings::Get('system.report_web_bccadmin')) { $mail->addBCC(Settings::Get('panel.adminmail'), $row['adminname']); } $mail->Send(); } catch (\PHPMailer\PHPMailer\Exception $e) { $mailerr_msg = $e->errorMessage(); $_mailerror = true; } catch (Exception $e) { $mailerr_msg = $e->getMessage(); $_mailerror = true; } if ($_mailerror) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, "Error sending mail: " . $mailerr_msg); echo "Error sending mail: " . $mailerr_msg . "\n"; } $mail->ClearAddresses(); Database::pexecute($upd_stmt, [ 'customerid' => $row['customerid'] ]); } } /** * report about diskusage for admins/reseller */ $result_stmt = Database::query(" SELECT `a`.* FROM `" . TABLE_PANEL_ADMINS . "` `a` WHERE `a`.`reportsent` & 2 = 0 "); $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_ADMINS . "` SET `reportsent` = `reportsent` + 2 WHERE `adminid` = :adminid "); while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $row['diskspace'] *= 1024; $row['diskspace_used'] *= 1024; if (isset($row['diskspace']) && $row['diskspace_used'] != null && $row['diskspace_used'] > 0 && (($row['diskspace_used'] * 100) / $row['diskspace']) >= (int)Settings::Get('system.report_webmax')) { $replace_arr = [ 'NAME' => $row['name'], 'DISKAVAILABLE' => PhpHelper::sizeReadable((int)$row['diskspace'], null, 'bi'), 'DISKUSED' => PhpHelper::sizeReadable((int)$row['diskspace_used'], null, 'bi'), 'USAGE_PERCENT' => ($row['diskspace_used'] * 100) / $row['diskspace'], 'MAX_PERCENT' => Settings::Get('system.report_webmax') ]; // set target user language Language::setLanguage($row['def_language']); // Get mail templates from database; the ones from 'admin' are fetched for fallback $result2_stmt = Database::prepare(" SELECT `value` FROM `" . TABLE_PANEL_TEMPLATES . "` WHERE `adminid` = :adminid AND `language` = :lang AND `templategroup` = 'mails' AND `varname` = :varname "); $result2_data = [ 'adminid' => $row['adminid'], 'lang' => $row['def_language'], 'varname' => 'diskmaxpercent_subject' ]; $result2 = Database::pexecute_first($result2_stmt, $result2_data); $mail_subject = html_entity_decode(PhpHelper::replaceVariables((($result2 !== false && $result2['value'] != '') ? $result2['value'] : Language::getTranslation('mails.diskmaxpercent.subject')), $replace_arr)); $result2_data['varname'] = 'diskmaxpercent_mailbody'; $result2 = Database::pexecute_first($result2_stmt, $result2_data); $mail_body = html_entity_decode(PhpHelper::replaceVariables((($result2 !== false && $result2['value'] != '') ? $result2['value'] : Language::getTranslation('mails.diskmaxpercent.mailbody')), $replace_arr)); $_mailerror = false; $mailerr_msg = ""; try { $mail->SetFrom(Settings::Get('panel.adminmail'), Settings::Get('panel.adminmail_defname')); $mail->Subject = $mail_subject; $mail->AltBody = $mail_body; $mail->MsgHTML(nl2br($mail_body)); $mail->AddAddress($row['email'], $row['name']); $mail->Send(); } catch (\PHPMailer\PHPMailer\Exception $e) { $mailerr_msg = $e->errorMessage(); $_mailerror = true; } catch (Exception $e) { $mailerr_msg = $e->getMessage(); $_mailerror = true; } if ($_mailerror) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, "Error sending mail: " . $mailerr_msg); echo "Error sending mail: " . $mailerr_msg . "\n"; } $mail->ClearAddresses(); Database::pexecute($upd_stmt, [ 'adminid' => $row['adminid'] ]); } } } // webmax > 0 } } ================================================ FILE: lib/Froxlor/Cron/Traffic/TrafficCron.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Cron\Traffic; /** * @author Florian Lippert (2003-2009) * @author Froxlor team (2010-) */ use Froxlor\Cron\Forkable; use Froxlor\Cron\FroxlorCron; use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\Froxlor; use Froxlor\FroxlorLogger; use Froxlor\Http\Statistics; use Froxlor\MailLogParser; use Froxlor\Settings; use PDO; class TrafficCron extends FroxlorCron { use Forkable; public static function run() { self::runFork([self::class, 'handle'], [true]); } public static function handle() { /** * TRAFFIC AND DISKUSAGE MEASURE */ FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'Traffic run started...'); $admin_traffic = []; $domainlist = []; $speciallogfile_domainlist = []; $result_domainlist_stmt = Database::query(" SELECT `id`, `domain`, `customerid`, `parentdomainid`, `speciallogfile` FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `aliasdomain` IS NULL AND `email_only` <> '1'; "); while ($row_domainlist = $result_domainlist_stmt->fetch(PDO::FETCH_ASSOC)) { if (!isset($domainlist[$row_domainlist['customerid']])) { $domainlist[$row_domainlist['customerid']] = []; } $domainlist[$row_domainlist['customerid']][$row_domainlist['id']] = $row_domainlist['domain']; if ($row_domainlist['parentdomainid'] == '0' && $row_domainlist['speciallogfile'] == '1') { if (!isset($speciallogfile_domainlist[$row_domainlist['customerid']])) { $speciallogfile_domainlist[$row_domainlist['customerid']] = []; } $speciallogfile_domainlist[$row_domainlist['customerid']][$row_domainlist['id']] = $row_domainlist['domain']; } } $mysqlusage_all = []; $databases_stmt = Database::query("SELECT * FROM " . TABLE_PANEL_DATABASES . " ORDER BY `dbserver`"); $last_dbserver = 0; $databases_list = []; Database::needRoot(true); $databases_list_result_stmt = Database::query("SHOW DATABASES"); while ($databases_list_row = $databases_list_result_stmt->fetch(PDO::FETCH_ASSOC)) { $databases_list[] = strtolower($databases_list_row['Database']); } while ($row_database = $databases_stmt->fetch(PDO::FETCH_ASSOC)) { if ($last_dbserver != $row_database['dbserver']) { Database::needRoot(true, $row_database['dbserver'], true); $last_dbserver = $row_database['dbserver']; $databases_list = []; $databases_list_result_stmt = Database::query("SHOW DATABASES"); while ($databases_list_row = $databases_list_result_stmt->fetch(PDO::FETCH_ASSOC)) { $databases_list[] = strtolower($databases_list_row['Database']); } } if (in_array(strtolower($row_database['databasename']), $databases_list)) { // sum up data_length and index_length $mysql_usage_result_stmt = Database::prepare(" SELECT SUM(data_length + index_length) AS customerusage FROM information_schema.TABLES WHERE table_schema = :database GROUP BY table_schema; "); // get the result $mysql_usage_row = Database::pexecute_first($mysql_usage_result_stmt, [ 'database' => $row_database['databasename'] ]); // initialize counter for customer if (!isset($mysqlusage_all[$row_database['customerid']])) { $mysqlusage_all[$row_database['customerid']] = 0; } // sum up result if ($mysql_usage_row) { $mysqlusage_all[$row_database['customerid']] += floatval($mysql_usage_row['customerusage']); } else { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, "Cannot get usage for database " . $row_database['databasename'] . "."); } } else { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_WARNING, "Seems like the database " . $row_database['databasename'] . " had been removed manually."); } } Database::needRoot(false); // We are using the file-system quota, this will speed up the diskusage - collection if (Settings::Get('system.diskquota_enabled')) { $usedquota = FileDir::getFilesystemQuota(); } /** * MAIL-Traffic */ if (Settings::Get("system.mailtraffic_enabled")) { $mailTrafficCalc = new MailLogParser(Settings::Get("system.last_traffic_run")); } $result_stmt = Database::query("SELECT * FROM `" . TABLE_PANEL_CUSTOMERS . "` ORDER BY `customerid` ASC"); $currentDate = date("Y-m-d"); $current_stamp = time(); $current_year = date('Y', $current_stamp); $current_month = date('m', $current_stamp); // @todo locale? $current_month_short = date('M', $current_stamp); $current_day = date('d', $current_stamp); while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { /** * HTTP-Traffic */ FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'http traffic for ' . $row['loginname'] . ' started...'); $httptraffic = 0; if (isset($domainlist[$row['customerid']]) && is_array($domainlist[$row['customerid']]) && count($domainlist[$row['customerid']]) != 0) { // Examining which caption to use for default webalizer stats... if ($row['standardsubdomain'] != '0' && isset($domainlist[$row['customerid']][$row['standardsubdomain']])) { // ... of course we'd prefer to use the standardsubdomain ... $caption = $domainlist[$row['customerid']][$row['standardsubdomain']]; } else { // ... but if there is no standardsubdomain, we have to use the loginname ... $caption = $row['loginname']; // ... which results in non-usable links to files in the stats, so let's have a look if we find a domain which is not speciallogfiledomain foreach ($domainlist[$row['customerid']] as $domainid => $domain) { if (!isset($speciallogfile_domainlist[$row['customerid']]) || !isset($speciallogfile_domainlist[$row['customerid']][$domainid])) { $caption = $domain; break; } } } $httptraffic = 0; reset($domainlist[$row['customerid']]); $statsTool = Settings::Get('system.traffictool'); if (isset($speciallogfile_domainlist[$row['customerid']]) && is_array($speciallogfile_domainlist[$row['customerid']]) && count($speciallogfile_domainlist[$row['customerid']]) != 0) { reset($speciallogfile_domainlist[$row['customerid']]); if ($statsTool != 'awstats') { foreach ($speciallogfile_domainlist[$row['customerid']] as $domainid => $domain) { if ($statsTool == 'goaccess') { $httptraffic += floatval(self::callGoaccessGetTraffic($row['customerid'], $row['loginname'] . '-' . $domain, $row['documentroot'] . '/goaccess/' . $domain . '/', $domain, ['month' => $current_month_short, 'year' => $current_year], $current_stamp)); } else { $httptraffic += floatval(self::callWebalizerGetTraffic($row['loginname'] . '-' . $domain, $row['documentroot'] . '/webalizer/' . $domain . '/', $domain, $domainlist[$row['customerid']])); } // kind of a keep-alive-call as this unsets the link which leads to a new connection to the database Database::needRoot(); } } } reset($domainlist[$row['customerid']]); // callAwstatsGetTraffic is called ONLY HERE and // *not* also in the special-logfiles-loop, because the function // will iterate through all customer-domains and the awstats-configs // know the logfile-name, #246 if ($statsTool == 'awstats') { $httptraffic += floatval(self::callAwstatsGetTraffic($row['customerid'], $row['documentroot'] . '/awstats/', $domainlist[$row['customerid']], $current_stamp)); } elseif ($statsTool == 'goaccess') { $httptraffic += floatval(self::callGoaccessGetTraffic($row['customerid'], $row['loginname'], $row['documentroot'] . '/goaccess/', $caption, ['month' => $current_month_short, 'year' => $current_year], $current_stamp)); } else { $httptraffic += floatval(self::callWebalizerGetTraffic($row['loginname'], $row['documentroot'] . '/webalizer/', $caption, $domainlist[$row['customerid']])); } // kind of a keep-alive-call as this unsets the link which leads to a new connection to the database Database::needRoot(); // make the stuff readable for the customer, #258 Statistics::makeChownWithNewStats($row); } /** * FTP-Traffic */ FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'ftp traffic for ' . $row['loginname'] . ' started...'); $ftptraffic_stmt = Database::prepare(" SELECT SUM(`up_bytes`) AS `up_bytes_sum`, SUM(`down_bytes`) AS `down_bytes_sum` FROM `" . TABLE_FTP_USERS . "` WHERE `customerid` = :customerid "); $ftptraffic = Database::pexecute_first($ftptraffic_stmt, [ 'customerid' => $row['customerid'] ]); if (!is_array($ftptraffic)) { $ftptraffic = [ 'up_bytes_sum' => 0, 'down_bytes_sum' => 0 ]; } $upd_stmt = Database::prepare(" UPDATE `" . TABLE_FTP_USERS . "` SET `up_bytes` = '0', `down_bytes` = '0' WHERE `customerid` = :customerid "); Database::pexecute($upd_stmt, [ 'customerid' => $row['customerid'] ]); /** * Mail-Traffic */ $mailtraffic = 0; if (Settings::Get("system.mailtraffic_enabled")) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'mail traffic usage for ' . $row['loginname'] . " started..."); $domains_stmt = Database::prepare("SELECT domain FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `customerid` = :cid"); Database::pexecute($domains_stmt, [ "cid" => $row['customerid'] ]); while ($domainRow = $domains_stmt->fetch(PDO::FETCH_ASSOC)) { $domainMailTraffic = $mailTrafficCalc->getDomainTraffic($domainRow["domain"]); if (!is_array($domainMailTraffic)) { continue; } foreach ($domainMailTraffic as $dateTraffic => $dayTraffic) { $dayTraffic = floatval($dayTraffic / 1024); [$year, $month, $day] = explode("-", $dateTraffic); if ($dateTraffic == $currentDate) { $mailtraffic = $dayTraffic; } else { // Check if an entry for the given day exists $stmt = Database::prepare("SELECT * FROM `" . TABLE_PANEL_TRAFFIC . "` WHERE `customerid` = :cid AND `year` = :year AND `month` = :month AND `day` = :day "); $params = [ "cid" => $row['customerid'], "year" => $year, "month" => $month, "day" => $day ]; Database::pexecute($stmt, $params); if ($stmt->rowCount() > 0) { $updRow = $stmt->fetch(PDO::FETCH_ASSOC); $upd_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_TRAFFIC . "` SET `mail` = :mail WHERE `id` = :id "); Database::pexecute($upd_stmt, [ "mail" => $updRow['mail'] + $dayTraffic, "id" => $updRow['id'] ]); } } } } } /** * Total Traffic */ FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'total traffic for ' . $row['loginname'] . ' started'); $current_traffic = []; $current_traffic['http'] = floatval($httptraffic); $current_traffic['ftp_up'] = floatval(($ftptraffic['up_bytes_sum'] / 1024)); $current_traffic['ftp_down'] = floatval(($ftptraffic['down_bytes_sum'] / 1024)); $current_traffic['mail'] = floatval($mailtraffic); $current_traffic['all'] = $current_traffic['http'] + $current_traffic['ftp_up'] + $current_traffic['ftp_down'] + $current_traffic['mail']; $ins_data = [ 'customerid' => $row['customerid'], 'year' => $current_year, 'month' => $current_month, 'day' => $current_day, 'stamp' => $current_stamp, 'http' => $current_traffic['http'], 'ftp_up' => $current_traffic['ftp_up'], 'ftp_down' => $current_traffic['ftp_down'], 'mail' => $current_traffic['mail'] ]; $ins_stmt = Database::prepare(" INSERT INTO `" . TABLE_PANEL_TRAFFIC . "` SET `customerid` = :customerid, `year` = :year, `month` = :month, `day` = :day, `stamp` = :stamp, `http` = :http, `ftp_up` = :ftp_up, `ftp_down` = :ftp_down, `mail` = :mail "); Database::pexecute($ins_stmt, $ins_data); $sum_month_traffic_stmt = Database::prepare(" SELECT SUM(`http`) AS `http`, SUM(`ftp_up`) AS `ftp_up`, SUM(`ftp_down`) AS `ftp_down`, SUM(`mail`) AS `mail` FROM `" . TABLE_PANEL_TRAFFIC . "` WHERE `year` = :year AND `month` = :month AND `customerid` = :customerid "); $sum_month_traffic = Database::pexecute_first($sum_month_traffic_stmt, [ 'year' => $current_year, 'month' => $current_month, 'customerid' => $row['customerid'] ]); $sum_month_traffic['all'] = $sum_month_traffic['http'] + $sum_month_traffic['ftp_up'] + $sum_month_traffic['ftp_down'] + $sum_month_traffic['mail']; if (!isset($admin_traffic[$row['adminid']]) || !is_array($admin_traffic[$row['adminid']])) { $admin_traffic[$row['adminid']]['http'] = 0; $admin_traffic[$row['adminid']]['ftp_up'] = 0; $admin_traffic[$row['adminid']]['ftp_down'] = 0; $admin_traffic[$row['adminid']]['mail'] = 0; $admin_traffic[$row['adminid']]['all'] = 0; $admin_traffic[$row['adminid']]['sum_month'] = 0; } $admin_traffic[$row['adminid']]['http'] += $current_traffic['http']; $admin_traffic[$row['adminid']]['ftp_up'] += $current_traffic['ftp_up']; $admin_traffic[$row['adminid']]['ftp_down'] += $current_traffic['ftp_down']; $admin_traffic[$row['adminid']]['mail'] += $current_traffic['mail']; $admin_traffic[$row['adminid']]['all'] += $current_traffic['all']; $admin_traffic[$row['adminid']]['sum_month'] += $sum_month_traffic['all']; /** * WebSpace-Usage */ FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'calculating webspace usage for ' . $row['loginname']); $webspaceusage = 0; // Using repquota, it's faster using this tool than using du traversing the complete directory if (Settings::Get('system.diskquota_enabled') && isset($usedquota[$row['guid']]['block']['used']) && $usedquota[$row['guid']]['block']['used'] >= 1) { // We may use the array we created earlier, the used diskspace is stored in [][block][used] $webspaceusage = floatval($usedquota[$row['guid']]['block']['used']); } else { // Use the old fashioned way with "du" if (file_exists($row['documentroot']) && is_dir($row['documentroot'])) { $back = FileDir::safe_exec('du -sk ' . escapeshellarg($row['documentroot'])); foreach ($back as $backrow) { $webspaceusage = explode(' ', $backrow); } $webspaceusage = floatval($webspaceusage['0']); unset($back); } else { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_WARNING, 'documentroot ' . $row['documentroot'] . ' does not exist'); } } /** * MailSpace-Usage */ FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'calculating mailspace usage for ' . $row['loginname']); $emailusage = 0; $maildir = FileDir::makeCorrectDir(Settings::Get('system.vmail_homedir') . $row['loginname']); if (file_exists($maildir) && is_dir($maildir)) { $back = FileDir::safe_exec('du -sk ' . escapeshellarg($maildir)); foreach ($back as $backrow) { $emailusage = explode(' ', $backrow); } $emailusage = floatval($emailusage['0']); unset($back); } else { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_WARNING, 'maildir ' . $maildir . ' does not exist'); } /** * MySQLSpace-Usage */ FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, 'calculating mysqlspace usage for ' . $row['loginname']); $mysqlusage = 0; if (isset($mysqlusage_all[$row['customerid']])) { $mysqlusage = floatval($mysqlusage_all[$row['customerid']] / 1024); } $current_diskspace = []; $current_diskspace['webspace'] = floatval($webspaceusage); $current_diskspace['mail'] = floatval($emailusage); $current_diskspace['mysql'] = floatval($mysqlusage); $current_diskspace['all'] = $current_diskspace['webspace'] + $current_diskspace['mail'] + $current_diskspace['mysql']; $ins_data = [ 'customerid' => $row['customerid'], 'year' => $current_year, 'month' => $current_month, 'day' => $current_day, 'stamp' => $current_stamp, 'webspace' => $current_diskspace['webspace'], 'mail' => $current_diskspace['mail'], 'mysql' => $current_diskspace['mysql'] ]; $ins_stmt = Database::prepare(" INSERT INTO `" . TABLE_PANEL_DISKSPACE . "` SET `customerid` = :customerid, `year` = :year, `month` = :month, `day` = :day, `stamp` = :stamp, `webspace` = :webspace, `mail` = :mail, `mysql` = :mysql "); Database::pexecute($ins_stmt, $ins_data); if (!isset($admin_diskspace[$row['adminid']]) || !is_array($admin_diskspace[$row['adminid']])) { $admin_diskspace[$row['adminid']] = []; $admin_diskspace[$row['adminid']]['webspace'] = 0; $admin_diskspace[$row['adminid']]['mail'] = 0; $admin_diskspace[$row['adminid']]['mysql'] = 0; $admin_diskspace[$row['adminid']]['all'] = 0; } $admin_diskspace[$row['adminid']]['webspace'] += $current_diskspace['webspace']; $admin_diskspace[$row['adminid']]['mail'] += $current_diskspace['mail']; $admin_diskspace[$row['adminid']]['mysql'] += $current_diskspace['mysql']; $admin_diskspace[$row['adminid']]['all'] += $current_diskspace['all']; /** * Total Usage */ $diskusage = floatval($webspaceusage + $emailusage + $mysqlusage); $upd_data = [ 'diskspace' => $current_diskspace['all'], 'traffic' => $sum_month_traffic['all'], 'customerid' => $row['customerid'] ]; $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `diskspace_used` = :diskspace, `traffic_used` = :traffic WHERE `customerid` = :customerid "); Database::pexecute($upd_stmt, $upd_data); /** * Proftpd Quota */ $upd_data = [ 'biu' => ($current_diskspace['all'] * 1024), 'loginname' => $row['loginname'], 'loginnamelike' => $row['loginname'] . Settings::Get('customer.ftpprefix') . "%" ]; $upd_stmt = Database::prepare(" UPDATE `" . TABLE_FTP_QUOTATALLIES . "` SET `bytes_in_used` = :biu WHERE `name` = :loginname OR `name` LIKE :loginnamelike "); Database::pexecute($upd_stmt, $upd_data); /** * Pureftpd Quota */ if (Settings::Get('system.ftpserver') == "pureftpd") { $result_quota_stmt = Database::prepare(" SELECT homedir FROM `" . TABLE_FTP_USERS . "` WHERE customerid = :customerid "); Database::pexecute($result_quota_stmt, [ 'customerid' => $row['customerid'] ]); // get correct user if ((Settings::Get('system.mod_fcgid') == 1 || Settings::Get('phpfpm.enabled') == 1) && $row['deactivated'] == '0') { $user = $row['loginname']; $group = $row['loginname']; } else { $user = $row['guid']; $group = $row['guid']; } while ($row_quota = $result_quota_stmt->fetch(PDO::FETCH_ASSOC)) { $quotafile = "" . $row_quota['homedir'] . ".ftpquota"; $fh = fopen($quotafile, 'w'); $stringdata = "0 " . $current_diskspace['all'] * 1024 . ""; fwrite($fh, $stringdata); fclose($fh); FileDir::safe_exec('chown ' . $user . ':' . $group . ' ' . escapeshellarg($quotafile) . ''); } } } /** * Admin Usage */ $result_stmt = Database::query("SELECT `adminid` FROM `" . TABLE_PANEL_ADMINS . "` ORDER BY `adminid` ASC"); while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { if (isset($admin_traffic[$row['adminid']])) { $ins_data = [ 'adminid' => $row['adminid'], 'year' => $current_year, 'month' => $current_month, 'day' => $current_day, 'stamp' => $current_stamp, 'http' => $admin_traffic[$row['adminid']]['http'], 'ftp_up' => $admin_traffic[$row['adminid']]['ftp_up'], 'ftp_down' => $admin_traffic[$row['adminid']]['ftp_down'], 'mail' => $admin_traffic[$row['adminid']]['mail'] ]; $ins_stmt = Database::prepare(" INSERT INTO `" . TABLE_PANEL_TRAFFIC_ADMINS . "` SET `adminid` = :adminid, `year` = :year, `month` = :month, `day` = :day, `stamp` = :stamp, `http` = :http, `ftp_up` = :ftp_up, `ftp_down` = :ftp_down, `mail` = :mail "); Database::pexecute($ins_stmt, $ins_data); $upd_data = [ 'traffic' => $admin_traffic[$row['adminid']]['sum_month'], 'adminid' => $row['adminid'] ]; $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_ADMINS . "` SET `traffic_used` = :traffic WHERE `adminid` = :adminid "); Database::pexecute($upd_stmt, $upd_data); } if (isset($admin_diskspace[$row['adminid']])) { $upd_data = [ 'diskspace' => $admin_diskspace[$row['adminid']]['all'], 'adminid' => $row['adminid'] ]; $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_ADMINS . "` SET `diskspace_used` = :diskspace WHERE `adminid` = :adminid "); Database::pexecute($upd_stmt, $upd_data); } } Database::query("UPDATE `" . TABLE_PANEL_SETTINGS . "` SET `value` = UNIX_TIMESTAMP() WHERE `settinggroup` = 'system' AND `varname` = 'last_traffic_run'"); } /** * Run goaccess to create statistics and return used traffic since last run * * @param int $customerid * @param string $logfile Name of logfile * @param string $outputdir Place where stats should be build * @param string $caption Caption for webalizer output * @param array $monthyear_arr * @param int $current_stamp * * @return int Used traffic */ private static function callGoaccessGetTraffic($customerid, $logfile, $outputdir, $caption, array $monthyear_arr = [], int $current_stamp = 0) { $returnval = 0; $logfile = FileDir::makeCorrectFile(Settings::Get('system.logfiles_directory') . $logfile . '-access.log'); if (file_exists($logfile)) { $outputdir = FileDir::makeCorrectDir($outputdir); if (!file_exists($outputdir)) { FileDir::safe_exec('mkdir -p ' . escapeshellarg($outputdir)); } if (file_exists($outputdir . '.tmp.json')) { @unlink($outputdir . '.tmp.json'); } // goaccess <1.4 $keep_params = '--keep-db-files --load-from-disk'; $res = FileDir::safe_exec('goaccess --version'); $ver_str = array_shift($res); $cGoVer = substr($ver_str, strrpos($ver_str, " ") + 1, -1); if (version_compare($cGoVer, '1.4', '>=')) { // at least 1.4 $keep_params = '--persist --restore'; } $format = (Settings::Get('system.logfiles_type') == '2' && Settings::Get('system.webserver') == 'apache2') ? 'VCOMBINED' : 'COMBINED'; $monthyear = $monthyear_arr['month'] . '/' . $monthyear_arr['year']; $return_value = false; FileDir::safe_exec("grep '" . $monthyear . "' " . escapeshellarg($logfile) . " | goaccess " . $keep_params . " --db-path=" . escapeshellarg($outputdir) . " -o " . escapeshellarg($outputdir . '.tmp.json') . " -o " . escapeshellarg($outputdir . 'index.html') . " --html-report-title=" . escapeshellarg($caption) . " --log-format=" . $format . " --no-parsing-spinner --no-progress - ", $return_value, ['|']); if (file_exists($outputdir . '.tmp.json')) { // need jq here because of potentially LARGE json files $returnval = FileDir::safe_exec("jq -c '.general.bandwidth' " . escapeshellarg($outputdir . '.tmp.json')); $returnval = array_shift($returnval); // return KB as the others two do $returnval = floatval($returnval / 1024); @unlink($outputdir . '.tmp.json'); } } if ($returnval > 0) { /** * now, because this traffic is being saved daily, we have to * subtract the values from all the month's values to return * a sane value for our panel_traffic and to remain the whole stats * (awstats overwrites the customers .html stats-files) */ if ($customerid !== false) { $result_stmt = Database::prepare(" SELECT SUM(`http`) as `trafficmonth` FROM `" . TABLE_PANEL_TRAFFIC . "` WHERE `customerid` = :customerid AND `year` = :year AND `month` = :month "); $result_data = [ 'customerid' => $customerid, 'year' => date('Y', $current_stamp), 'month' => date('m', $current_stamp) ]; $result = Database::pexecute_first($result_stmt, $result_data); if (is_array($result) && isset($result['trafficmonth'])) { $returnval = ($returnval - floatval($result['trafficmonth'])); } } } return $returnval; } /** * Function which make webalizer statistics and returns used traffic since last run * * @param string $logfile Name of logfile * @param string $outputdir Place where stats should be build * @param string $caption Caption for webalizer output * @param array $usersdomainlist * * @return float Used traffic */ private static function callWebalizerGetTraffic($logfile, $outputdir, $caption, array $usersdomainlist = []) { $returnval = 0; $logfile = FileDir::makeCorrectFile(Settings::Get('system.logfiles_directory') . $logfile . '-access.log'); if (file_exists($logfile)) { $domainargs = ''; foreach ($usersdomainlist as $domain) { // hide referer $domainargs .= ' -r ' . escapeshellarg($domain); } $outputdir = FileDir::makeCorrectDir($outputdir); if (!file_exists($outputdir)) { FileDir::safe_exec('mkdir -p ' . escapeshellarg($outputdir)); } if (file_exists($outputdir . 'webalizer.hist.1')) { @unlink($outputdir . 'webalizer.hist.1'); } if (file_exists($outputdir . 'webalizer.hist') && !file_exists($outputdir . 'webalizer.hist.1')) { FileDir::safe_exec('cp ' . escapeshellarg($outputdir . 'webalizer.hist') . ' ' . escapeshellarg($outputdir . 'webalizer.hist.1')); } $verbosity = ''; if (Settings::Get('system.webalizer_quiet') == '1') { $verbosity = '-q'; } elseif (Settings::Get('system.webalizer_quiet') == '2') { $verbosity = '-Q'; } $we = '/usr/bin/webalizer'; // FreeBSD uses other paths, #140 if (!file_exists($we)) { $we = '/usr/local/bin/webalizer'; } FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Running webalizer for domain '" . $caption . "'"); FileDir::safe_exec($we . ' ' . $verbosity . ' -p -o ' . escapeshellarg($outputdir) . ' -n ' . escapeshellarg($caption) . $domainargs . ' ' . escapeshellarg($logfile)); /** * Format of webalizer.hist-files: * Month: $webalizer_hist_row['0'] * Year: $webalizer_hist_row['1'] * KB: $webalizer_hist_row['5'] */ $httptraffic = []; $webalizer_hist = @file_get_contents($outputdir . 'webalizer.hist'); FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Gathering traffic information from '" . $webalizer_hist . "'"); $webalizer_hist_rows = explode("\n", $webalizer_hist); foreach ($webalizer_hist_rows as $webalizer_hist_row) { if ($webalizer_hist_row != '') { $webalizer_hist_row = explode(' ', $webalizer_hist_row); if (isset($webalizer_hist_row['0']) && isset($webalizer_hist_row['1']) && isset($webalizer_hist_row['5'])) { $month = intval($webalizer_hist_row['0']); $year = intval($webalizer_hist_row['1']); $traffic = floatval($webalizer_hist_row['5']); if (!isset($httptraffic[$year])) { $httptraffic[$year] = []; } $httptraffic[$year][$month] = $traffic; } } } reset($httptraffic); $httptrafficlast = []; $webalizer_lasthist = @file_get_contents($outputdir . 'webalizer.hist.1'); FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Gathering traffic information from '" . $webalizer_lasthist . "'"); $webalizer_lasthist_rows = explode("\n", $webalizer_lasthist); foreach ($webalizer_lasthist_rows as $webalizer_lasthist_row) { if ($webalizer_lasthist_row != '') { $webalizer_lasthist_row = explode(' ', $webalizer_lasthist_row); if (isset($webalizer_lasthist_row['0']) && isset($webalizer_lasthist_row['1']) && isset($webalizer_lasthist_row['5'])) { $month = intval($webalizer_lasthist_row['0']); $year = intval($webalizer_lasthist_row['1']); $traffic = floatval($webalizer_lasthist_row['5']); if (!isset($httptrafficlast[$year])) { $httptrafficlast[$year] = []; } $httptrafficlast[$year][$month] = $traffic; } } } reset($httptrafficlast); foreach ($httptraffic as $year => $months) { foreach ($months as $month => $traffic) { if (!isset($httptrafficlast[$year][$month])) { $returnval += $traffic; } elseif ($httptrafficlast[$year][$month] < $traffic) { $returnval += ($traffic - $httptrafficlast[$year][$month]); } } } } return floatval($returnval); } private static function callAwstatsGetTraffic($customerid, $outputdir, $usersdomainlist, $current_stamp) { $returnval = 0; foreach ($usersdomainlist as $singledomain) { // as we check for the config-model awstats will only parse // 'real' domains and no subdomains which are aliases in the // model-config-file. $returnval += self::awstatsDoSingleDomain($singledomain, $outputdir, $current_stamp); // kind of a keep-alive-call as this unsets the link which leads to a new connection to the database Database::needRoot(); } /** * as of #124, awstats traffic is saved in bytes instead * of kilobytes (like webalizer does) */ $returnval = floatval($returnval / 1024); /** * now, because this traffic is being saved daily, we have to * subtract the values from all the month's values to return * a sane value for our panel_traffic and to remain the whole stats * (awstats overwrites the customers .html stats-files) */ if ($customerid !== false) { $result_stmt = Database::prepare(" SELECT SUM(`http`) as `trafficmonth` FROM `" . TABLE_PANEL_TRAFFIC . "` WHERE `customerid` = :customerid AND `year` = :year AND `month` = :month "); $result_data = [ 'customerid' => $customerid, 'year' => date('Y', $current_stamp), 'month' => date('m', $current_stamp) ]; $result = Database::pexecute_first($result_stmt, $result_data); if (is_array($result) && isset($result['trafficmonth'])) { $returnval = ($returnval - floatval($result['trafficmonth'])); } } return floatval($returnval); } private static function awstatsDoSingleDomain($domain, $outputdir, $current_stamp) { $returnval = 0; $domainconfig = FileDir::makeCorrectFile(Settings::Get('system.awstats_conf') . '/awstats.' . $domain . '.conf'); if (file_exists($domainconfig)) { $outputdir = FileDir::makeCorrectDir($outputdir . '/' . $domain); $staticOutputdir = FileDir::makeCorrectDir($outputdir . '/' . date('Y') . '-' . date('m')); if (!is_dir($staticOutputdir)) { FileDir::safe_exec('mkdir -p ' . escapeshellarg($staticOutputdir)); } // check for correct path of awstats_buildstaticpages.pl $awbsp = FileDir::makeCorrectFile(Settings::Get('system.awstats_path') . '/awstats_buildstaticpages.pl'); $awprog = FileDir::makeCorrectFile(Settings::Get('system.awstats_awstatspath') . '/awstats.pl'); if (!file_exists($awbsp)) { echo "WANRING: Necessary awstats_buildstaticpages.pl script could not be found, no traffic is being calculated and no stats are generated. Please check your AWStats-Path setting"; FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_WARNING, "Necessary awstats_buildstaticpages.pl script could not be found, no traffic is being calculated and no stats are generated. Please check your AWStats-Path setting"); exit(); } FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Running awstats_buildstaticpages.pl for domain '" . $domain . "' (Output: '" . $staticOutputdir . "')"); FileDir::safe_exec($awbsp . ' -awstatsprog=' . escapeshellarg($awprog) . ' -update -month=' . date('m', $current_stamp) . ' -year=' . date('Y', $current_stamp) . ' -config=' . $domain . ' -dir=' . escapeshellarg($staticOutputdir)); // update our awstats index files self::awstatsGenerateIndex($domain, $outputdir); // the default selection is 'current', // so link the latest dir to it $new_current = FileDir::makeCorrectFile($outputdir . '/current'); FileDir::safe_exec('ln -fTs ' . escapeshellarg($staticOutputdir) . ' ' . escapeshellarg($new_current)); // statistics file looks like: 'awstats[month][year].[domain].txt' $file = FileDir::makeCorrectFile($outputdir . '/awstats' . date('mY', time()) . '.' . $domain . '.txt'); FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::CRON_ACTION, LOG_INFO, "Gathering traffic information from '" . $file . "'"); if (file_exists($file)) { $content = @file_get_contents($file); if ($content !== false) { $content_array = explode("\n", $content); $count_bdw = false; foreach ($content_array as $line) { // skip empty lines and comments if (trim($line) == '' || substr(trim($line), 0, 1) == '#') { continue; } $parts = explode(' ', $line); if (isset($parts[0]) && strtoupper($parts[0]) == 'BEGIN_DOMAIN') { $count_bdw = true; } if ($count_bdw) { if (isset($parts[0]) && strtoupper($parts[0]) == 'END_DOMAIN') { $count_bdw = false; break; } elseif (isset($parts[3])) { $returnval += floatval($parts[3]); } } } } } } return $returnval; } private static function awstatsGenerateIndex($domain, $outputdir) { // Generation header $header = "\n"; // Looking for {year}-{month} directories $entries = []; foreach (scandir($outputdir) as $a) { if (is_dir(FileDir::makeCorrectDir($outputdir . '/' . $a)) && preg_match('/^[0-9]{4}-[0-9]{2}$/', $a)) { array_push($entries, ''); } } // These are the variables we will replace $regex = [ '/\{SITE_DOMAIN\}/', '/\{SELECT_ENTRIES\}/' ]; $replace = [ $domain, implode($entries) ]; // File names $index_file = Froxlor::getInstallDir() . '/templates/misc/awstats/index.html'; $index_file = FileDir::makeCorrectFile($index_file); $nav_file = Froxlor::getInstallDir() . '/templates/misc/awstats/nav.html'; $nav_file = FileDir::makeCorrectFile($nav_file); // Write the index file // 'index.html' used to be a symlink (ignore errors in case this is the first run and no index.html exists yet) @unlink(FileDir::makeCorrectFile($outputdir . '/' . 'index.html')); $awstats_index_file = fopen(FileDir::makeCorrectFile($outputdir . '/' . 'index.html'), 'w'); $awstats_index_tpl = fopen($index_file, 'r'); // Write the header fwrite($awstats_index_file, $header); // Write the configuration file while (($line = fgets($awstats_index_tpl, 4096)) !== false) { if (!preg_match('/^#/', $line) && trim($line) != '') { fwrite($awstats_index_file, preg_replace($regex, $replace, $line)); } } fclose($awstats_index_file); fclose($awstats_index_tpl); // Write the nav file $awstats_nav_file = fopen(FileDir::makeCorrectFile($outputdir . '/' . 'nav.html'), 'w'); $awstats_nav_tpl = fopen($nav_file, 'r'); // Write the header fwrite($awstats_nav_file, $header); // Write the configuration file while (($line = fgets($awstats_nav_tpl, 4096)) !== false) { if (!preg_match('/^#/', $line) && trim($line) != '') { fwrite($awstats_nav_file, preg_replace($regex, $replace, $line)); } } fclose($awstats_nav_file); fclose($awstats_nav_tpl); return; } } ================================================ FILE: lib/Froxlor/Cron/Traffic/index.html ================================================ ================================================ FILE: lib/Froxlor/Cron/index.html ================================================ ================================================ FILE: lib/Froxlor/CurrentUser.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor; use Exception; use Froxlor\Api\Commands\Customers; use Froxlor\Api\Commands\SubDomains; use Froxlor\Database\Database; use Froxlor\UI\Collection; use Froxlor\UI\Response; use RobThree\Auth\TwoFactorAuthException; /** * Class to manage the current user / session */ class CurrentUser { /** * returns whether there is an active session * * @return bool */ public static function hasSession(): bool { return !empty($_SESSION) && !empty($_SESSION['userinfo']); } /** * set userinfo field in session * * @param string $index * @param mixed $data * * @return boolean */ public static function setField(string $index, $data): bool { $_SESSION['userinfo'][$index] = $data; return true; } /** * re-read in the user data if a valid session exists * * @return bool * @throws \Exception */ public static function reReadUserData(): bool { $table = self::isAdmin() ? TABLE_PANEL_ADMINS : TABLE_PANEL_CUSTOMERS; $userinfo_stmt = Database::prepare(" SELECT * FROM `" . $table . "` WHERE `loginname`= :loginname AND `deactivated` = '0' "); $userinfo = Database::pexecute_first($userinfo_stmt, [ "loginname" => self::getField('loginname') ]); if ($userinfo) { // don't just set the data, we need to merge with current data // array_merge is a right-reduction - value existing in getData() will be overwritten with $userinfo, // other than the union-operator (+) which would keep the values already existing from getData() $newuserinfo = array_merge(self::getData(), $userinfo); self::setData($newuserinfo); return true; } // unset / logout unset($_SESSION['userinfo']); self::setData([]); return false; } /** * returns whether user has an adminsession * * @return bool */ public static function isAdmin(): bool { return (self::getField('adminsession') == 1 && self::getField('adminid') > 0 && empty(self::getField('customerid'))); } /** * return content of a given field from userinfo-array * * @param string $index * * @return string|array */ public static function getField(string $index) { return $_SESSION['userinfo'][$index] ?? ""; } /** * Return userinfo array * * @return array */ public static function getData(): array { return $_SESSION['userinfo'] ?? []; } /** * set the userinfo data to the session * * @param array $data */ public static function setData(array $data = []): void { $_SESSION['userinfo'] = $data; } /** * @param string $resource * @return bool * @throws \Exception */ public static function canAddResource(string $resource): bool { $addition = true; // special cases if ($resource == 'emails') { $result_stmt = Database::prepare(" SELECT COUNT(`id`) as emaildomains FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `customerid`= :cid AND `isemaildomain` = '1' AND `deactivated` = '0' "); $result = Database::pexecute_first($result_stmt, [ "cid" => $_SESSION['userinfo']['customerid'] ]); $addition = $result['emaildomains'] != 0; } elseif ($resource == 'subdomains') { if (Settings::IsInList('panel.customer_hide_options', 'domains')) { $addition = false; } else { $parentDomainCollection = (new Collection( SubDomains::class, $_SESSION['userinfo'], ['sql_search' => [ 'd.parentdomainid' => 0, 'd.deactivated' => 0, 'd.id' => ['op' => '<>', 'value' => $_SESSION['userinfo']['standardsubdomain']] ] ] )); $addition = $parentDomainCollection->count() != 0; } } elseif ($resource == 'domains') { $customerCollection = (new Collection(Customers::class, $_SESSION['userinfo'])); $addition = $customerCollection->count() != 0; } return ($_SESSION['userinfo'][$resource . '_used'] < $_SESSION['userinfo'][$resource] || $_SESSION['userinfo'][$resource] == '-1') && $addition; } /** * @throws TwoFactorAuthException */ public static function sendOtpEmail() { global $mail; if (self::getField('type_2fa') == 1) { // generate code $tfa = new FroxlorTwoFactorAuth('Froxlor ' . Settings::Get('system.hostname')); $secret = $tfa->createSecret(); $code = $tfa->getCode($secret); // set code for user $table = TABLE_PANEL_CUSTOMERS; $uid = 'customerid'; if (self::isAdmin()) { $table = TABLE_PANEL_ADMINS; $uid = 'adminid'; } $stmt = Database::prepare("UPDATE $table SET `data_2fa` = :d2fa WHERE `$uid` = :uid"); Database::pexecute($stmt, [ "d2fa" => $secret, "uid" => self::getField($uid) ]); // build up & send email $_mailerror = false; $mailerr_msg = ""; $replace_arr = [ 'CODE' => $code ]; $mail_body = html_entity_decode(PhpHelper::replaceVariables(lng('mails.2fa.mailbody'), $replace_arr)); try { $mail->Subject = lng('mails.2fa.subject'); $mail->AltBody = $mail_body; $mail->MsgHTML(str_replace("\n", "
", $mail_body)); $mail->AddAddress(self::getField('email'), User::getCorrectUserSalutation(self::getData())); $mail->Send(); } catch (\PHPMailer\PHPMailer\Exception $e) { $mailerr_msg = $e->errorMessage(); $_mailerror = true; } catch (Exception $e) { $mailerr_msg = $e->getMessage(); $_mailerror = true; } if ($_mailerror) { $rstlog = FroxlorLogger::getInstanceOf([ 'loginname' => '2fa code-sending' ]); $rstlog->logAction(FroxlorLogger::ADM_ACTION, LOG_ERR, "Error sending mail: " . $mailerr_msg); Response::redirectTo('index.php', [ 'showmessage' => '4', 'customermail' => self::getField('email') ]); exit(); } $mail->ClearAddresses(); } } } ================================================ FILE: lib/Froxlor/Customer/Customer.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Customer; use Froxlor\Database\Database; use PDO; class Customer { /** * Get value of a specific field from a given customer * * @param int $customerid * @param string $varname * @return false|mixed * @throws \Exception */ public static function getCustomerDetail(int $customerid, string $varname) { $customer_stmt = Database::prepare(" SELECT `" . $varname . "` FROM `" . TABLE_PANEL_CUSTOMERS . "` WHERE `customerid` = :customerid "); $customer = Database::pexecute_first($customer_stmt, [ 'customerid' => $customerid ]); if (isset($customer[$varname])) { return $customer[$varname]; } return false; } /** * returns the loginname of a customer by given uid * * @param int $uid uid of customer * * @return string customers loginname * @throws \Exception */ public static function getLoginNameByUid(int $uid) { $result_stmt = Database::prepare(" SELECT `loginname` FROM `" . TABLE_PANEL_CUSTOMERS . "` WHERE `guid` = :guid "); $result = Database::pexecute_first($result_stmt, [ 'guid' => $uid ]); if ($result && isset($result['loginname'])) { return $result['loginname']; } return false; } /** * Function customerHasPerlEnabled * * returns true or false whether perl is * enabled for the given customer * * @param int $cid customer-id * * @return bool * @throws \Exception */ public static function customerHasPerlEnabled(int $cid = 0) { if ($cid > 0) { $result_stmt = Database::prepare(" SELECT `perlenabled` FROM `" . TABLE_PANEL_CUSTOMERS . "` WHERE `customerid` = :cid"); $result = Database::pexecute_first($result_stmt, [ 'cid' => $cid ]); if ($result && isset($result['perlenabled'])) { return (bool)$result['perlenabled']; } } return false; } } ================================================ FILE: lib/Froxlor/Customer/index.html ================================================ ================================================ FILE: lib/Froxlor/Database/Database.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Database; use Exception; use Froxlor\FileDir; use Froxlor\Froxlor; use Froxlor\PhpHelper; use Froxlor\Settings; use Froxlor\UI\Panel\UI; use PDO; use PDOException; use PDOStatement; /** * Class Database * * Wrapper-class for PHP-PDO * * @method static PDOStatement prepare($statement, array $driver_options = null) Prepares a statement for execution * and returns a statement object * @method static PDOStatement query ($statement) Executes an SQL statement, returning a result set as a PDOStatement * object * @method static string lastInsertId ($name = null) Returns the ID of the last inserted row or sequence value * @method static string quote ($string, $parameter_type = null) Quotes a string for use in a query. * @method static mixed getAttribute(int $attribute) Retrieve a database connection attribute */ class Database { /** * current database link * * @var object */ private static $link = null; /** * indicator whether to use root-connection or not */ private static bool $needroot = false; /** * indicator which database-server we're on (not really used) */ private static int $dbserver = 0; /** * used database-name */ private static ?string $dbname = null; /** * sql-access data */ private static bool $needsqldata = false; private static $sqldata = null; private static bool $need_dbname = true; /** * Wrapper for PDOStatement::execute, so we can catch the PDOException * and display the error nicely on the panel - also fetches the * result from the statement and returns the resulting array * * @param PDOStatement $stmt * @param array|null $params * (optional) * @param bool $showerror * suppress error display (default true) * @param bool $json_response * * @return mixed * @throws Exception */ public static function pexecute_first(PDOStatement &$stmt, $params = null, bool $showerror = true, bool $json_response = false) { self::pexecute($stmt, $params, $showerror, $json_response); return $stmt->fetch(PDO::FETCH_ASSOC); } /** * Wrapper for PDOStatement::execute so we can catch the PDOException * and display the error nicely on the panel * * @param PDOStatement $stmt * @param array|null $params * (optional) * @param bool $showerror * suppress error display (default true) * @param bool $json_response * * @throws Exception */ public static function pexecute(PDOStatement &$stmt, $params = null, bool $showerror = true, bool $json_response = false) { try { $stmt->execute($params); } catch (PDOException $e) { self::showerror($e, $showerror, $json_response, $stmt); } } /** * display a nice error if it occurs and log everything * * @param PDOException $error * @param bool $showerror * if set to false, the error will be logged, but we go on * @throws Exception */ private static function showerror(Exception $error, bool $showerror = true, bool $json_response = false, ?PDOStatement $stmt = null) { global $userinfo, $theme, $linker; $sql = []; $sql_root = []; // include userdata.inc.php require Froxlor::getInstallDir() . "/lib/userdata.inc.php"; // le format if (isset($sql['root_user']) && isset($sql['root_password']) && empty($sql_root)) { $sql_root = [ 0 => [ 'caption' => 'Default', 'host' => $sql['host'], 'socket' => ($sql['socket'] ?? null), 'user' => $sql['root_user'], 'password' => $sql['root_password'] ] ]; unset($sql['root_user']); unset($sql['root_password']); // write new layout so this won't happen again self::generateNewUserData($sql, $sql_root); // re-read file require Froxlor::getInstallDir() . "/lib/userdata.inc.php"; } $substitutions = [ $sql['password'] => 'DB_UNPRIV_PWD', ]; foreach ($sql_root as $sql_root_data) { $substitutions[$sql_root_data['password']] = 'DB_ROOT_PWD'; } // hide username/password in messages $error_message = $error->getMessage(); $error_trace = $error->getTraceAsString(); // error-message $error_message = self::substitute($error_message, $substitutions); // error-trace $error_trace = self::substitute($error_trace, $substitutions); if ($error->getCode() == 2003) { $error_message = "Unable to connect to database. Either the mysql-server is not running or your user/password is wrong."; $error_trace = ""; } /** * log to a file, so we can actually ask people for the error * (no one seems to find the stuff in the syslog) */ $sl_dir = FileDir::makeCorrectDir(Froxlor::getInstallDir() . "/logs/"); if (!file_exists($sl_dir)) { @mkdir($sl_dir, 0755); } if (!defined('TRAVIS_CI') || TRAVIS_CI == 0) { openlog("froxlor", LOG_PID | LOG_PERROR, LOG_LOCAL0); syslog(LOG_WARNING, str_replace("\n", " ", $error_message)); syslog(LOG_WARNING, str_replace("\n", " ", "--- DEBUG: " . $error_trace)); closelog(); } /** * log error for reporting */ $errid = self::genUniqueToken(); $err_file = FileDir::makeCorrectFile($sl_dir . "/" . $errid . "_sql-error.log"); $errlog = @fopen($err_file, 'w'); @fwrite($errlog, "|CODE " . $error->getCode() . "\n"); @fwrite($errlog, "|MSG " . $error_message . "\n"); @fwrite($errlog, "|FILE " . $error->getFile() . "\n"); @fwrite($errlog, "|LINE " . $error->getLine() . "\n"); @fwrite($errlog, "|TRACE\n" . $error_trace . "\n"); @fclose($errlog); if (empty($sql['debug'])) { $error_trace = ''; } elseif (!is_null($stmt)) { $error_trace .= "\n\n" . $stmt->queryString; } if ($showerror && $json_response) { $exception_message = $error_message; if (isset($sql['debug']) && $sql['debug'] == true) { $exception_message .= "\n\n" . $error_trace; } throw new Exception($exception_message, 500); } if ($showerror) { // clean up sensitive data unset($sql); unset($sql_root); if ((isset($theme) && $theme != '') && !isset($_SERVER['SHELL']) || (isset($_SERVER['SHELL']) && $_SERVER['SHELL'] == '')) { // if we're not on the shell, output a nice error $err_report_link = ''; if (is_array($userinfo) && (($userinfo['adminsession'] == '1' && Settings::Get('system.allow_error_report_admin') == '1') || ($userinfo['adminsession'] == '0' && Settings::Get('system.allow_error_report_customer') == '1'))) { $err_report_link = $linker->getLink([ 'section' => 'index', 'page' => 'send_error_report', 'errorid' => $errid ]); } // show UI::initTwig(true); UI::twig()->addGlobal('install_mode', '1'); UI::view('misc/dberrornice.html.twig', [ 'page_title' => 'Database error', 'message' => $error_message, 'debug' => $error_trace, 'report' => $err_report_link ]); die(); } die("We are sorry, but a MySQL - error occurred. The administrator may find more information in the syslog"); } } /** * Substitutes patterns in content. * * @param string $content * @param array $substitutions * @param int $minLength * @return string */ private static function substitute(string $content, array $substitutions, int $minLength = 6): string { $replacements = []; foreach ($substitutions as $search => $replace) { $replacements += self::createShiftedSubstitutions($search, $replace, $minLength); } return str_replace(array_keys($replacements), array_values($replacements), $content); } /** * Creates substitutions, shifted by length, e.g. * * _createShiftedSubstitutions('abcdefgh', 'value', 4): * array( * 'abcdefgh' => 'value', * 'abcdefg' => 'value', * 'abcdef' => 'value', * 'abcde' => 'value', * 'abcd' => 'value', * ) * * @param string $search * @param string $replace * @param int $minLength * @return array */ private static function createShiftedSubstitutions(string $search, string $replace, int $minLength): array { $substitutions = []; $length = strlen($search); if ($length > $minLength) { for ($shiftedLength = $length; $shiftedLength >= $minLength; $shiftedLength--) { $substitutions[substr($search, 0, $shiftedLength)] = $replace; } } return $substitutions; } /** * generate safe unique token * * @param int $length * @return string * @throws Exception */ private static function genUniqueToken(int $length = 16): string { if (intval($length) <= 8) { $length = 16; } if (function_exists('random_bytes')) { return bin2hex(random_bytes($length)); } if (function_exists('mcrypt_create_iv') && defined('MCRYPT_DEV_URANDOM')) { return bin2hex(mcrypt_create_iv($length, MCRYPT_DEV_URANDOM)); } if (function_exists('openssl_random_pseudo_bytes')) { return bin2hex(openssl_random_pseudo_bytes($length)); } // if everything else fails, use unsafe fallback return substr(md5(uniqid(microtime(), 1)), 0, $length); } /** * returns the number of found rows of the last select query * * @return int */ public static function num_rows(): int { return Database::query("SELECT FOUND_ROWS()")->fetchColumn(); } /** * returns the database-name which is used * * @return string */ public static function getDbName(): ?string { return self::$dbname; } /** * enabled the usage of a root-connection to the database * Note: must be called *before* any prepare/query/etc. * and should be called again with 'false'-parameter to resume * the 'normal' database-connection * * @param bool $needroot * @param int $dbserver optional * @param bool $need_db */ public static function needRoot(bool $needroot = false, int $dbserver = 0, bool $need_db = true) { // force re-connecting to the db with corresponding user // and set the $dbserver (mostly to 0 = default) self::setServer($dbserver); self::$needroot = $needroot; self::$need_dbname = $need_db; } /** * set the database-server (relevant for root-connection) * * @param int $dbserver */ private static function setServer(int $dbserver = 0) { self::$dbserver = $dbserver; self::$link = null; } /** * get the currently used database-server (relevant for root-connection) */ public static function getServer() { return self::$dbserver; } /** * enable the temporary access to sql-access data * note: if you want root-sqldata you need to * call needRoot(true) first. * Also, this will * only give you the data ONCE as it disable itself * after the first access to the data */ public static function needSqlData() { self::$needsqldata = true; self::$sqldata = []; self::$link = null; // we need a connection here because // if getSqlData() is called RIGHT after // this function and no "real" PDO // function was called, getDB() wasn't // involved and no data collected self::getDB(); } /** * function that will be called on every static call * which connects to the database if necessary * * @return object * @throws Exception */ private static function getDB() { if (!extension_loaded('pdo') || !in_array("mysql", PDO::getAvailableDrivers())) { self::showerror(new Exception("The php PDO extension or PDO-MySQL driver is not available")); } // do we have a connection already? if (self::$link) { // return it return self::$link; } // include userdata.inc.php require Froxlor::getInstallDir() . "/lib/userdata.inc.php"; // le format if (isset($sql['root_user']) && isset($sql['root_password']) && (!isset($sql_root) || !is_array($sql_root))) { $sql_root = [ 0 => [ 'caption' => 'Default', 'host' => $sql['host'], 'socket' => ($sql['socket'] ?? null), 'user' => $sql['root_user'], 'password' => $sql['root_password'] ] ]; unset($sql['root_user']); unset($sql['root_password']); // write new layout so this won't happen again self::generateNewUserData($sql, $sql_root); // re-read file require Froxlor::getInstallDir() . "/lib/userdata.inc.php"; } // either root or unprivileged user if (self::$needroot) { $caption = $sql_root[self::$dbserver]['caption']; $user = $sql_root[self::$dbserver]['user']; $password = $sql_root[self::$dbserver]['password']; $host = $sql_root[self::$dbserver]['host']; $socket = $sql_root[self::$dbserver]['socket'] ?? null; $port = $sql_root[self::$dbserver]['port'] ?? '3306'; $sslCAFile = $sql_root[self::$dbserver]['ssl']['caFile'] ?? ""; $sslVerifyServerCertificate = $sql_root[self::$dbserver]['ssl']['verifyServerCertificate'] ?? false; } else { $caption = 'localhost'; $user = $sql["user"]; $password = $sql["password"]; $host = $sql["host"]; $socket = $sql['socket'] ?? null; $port = $sql['port'] ?? '3306'; $sslCAFile = $sql['ssl']['caFile'] ?? ""; $sslVerifyServerCertificate = $sql['ssl']['verifyServerCertificate'] ?? false; } // save sql-access-data if needed if (self::$needsqldata) { self::$sqldata = [ 'user' => $user, 'passwd' => $password, 'host' => $host, 'port' => $port, 'socket' => $socket, 'db' => $sql["db"], 'caption' => $caption, 'ssl_ca_file' => $sslCAFile, 'ssl_verify_server_certificate' => $sslVerifyServerCertificate ]; } // build up connection string $driver = 'mysql'; $dsn = $driver . ":"; $options = [ 'PDO::MYSQL_ATTR_INIT_COMMAND' => 'SET names utf8' ]; $attributes = [ 'ATTR_ERRMODE' => 'ERRMODE_EXCEPTION' ]; $dbconf["dsn"] = ['charset' => 'utf8']; if (self::$need_dbname) { $dbconf["dsn"]['dbname'] = $sql["db"]; } if ($socket != null) { $dbconf["dsn"]['unix_socket'] = FileDir::makeCorrectFile($socket); } else { $dbconf["dsn"]['host'] = $host; $dbconf["dsn"]['port'] = $port; if (!empty(self::$sqldata['ssl_ca_file'])) { $options[PDO::MYSQL_ATTR_SSL_CA] = self::$sqldata['ssl_ca_file']; $options[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = (bool)self::$sqldata['ssl_verify_server_certificate']; } } self::$dbname = $sql["db"]; // add options to dsn-string foreach ($dbconf["dsn"] as $k => $v) { $dsn .= $k . "=" . $v . ";"; } // clean up unset($dbconf); // try to connect try { self::$link = new PDO($dsn, $user, $password, $options); } catch (PDOException $e) { self::showerror($e); } // set attributes foreach ($attributes as $k => $v) { self::$link->setAttribute(constant("PDO::" . $k), constant("PDO::" . $v)); } $version_server = self::$link->getAttribute(PDO::ATTR_SERVER_VERSION); $sql_mode = 'NO_ENGINE_SUBSTITUTION'; if (version_compare($version_server, '8.0.11', '<')) { $sql_mode .= ',NO_AUTO_CREATE_USER'; } self::$link->exec('SET sql_mode = "' . $sql_mode . '"'); // return PDO instance return self::$link; } /** * returns the sql-access data as array using indices * 'user', 'passwd' and 'host'. * Returns false if not enabled * * @return array|bool */ public static function getSqlData() { $return = false; if (self::$sqldata !== null && is_array(self::$sqldata) && isset(self::$sqldata['user'])) { $return = self::$sqldata; // automatically disable sql-data self::$sqldata = null; self::$needsqldata = false; } return $return; } /** * return number of characters that are allowed to use as username * * @return int */ public static function getSqlUsernameLength(): int { // MariaDB supports up to 80 characters but only 64 for databases and as we use the login-name also for // database names, we set the limit to 64 here if (strpos(strtolower(Database::getAttribute(\PDO::ATTR_SERVER_VERSION)), "mariadb") !== false) { $mysql_max = 64; } else { // MySQL user-names can be up to 32 characters long (16 characters before MySQL 5.7.8). $mysql_max = 32; if (version_compare(Database::getAttribute(\PDO::ATTR_SERVER_VERSION), '5.7.8', '<')) { $mysql_max = 16; } } return $mysql_max; } /** * Lets us interact with the PDO-Object by using static * call like "Database::function()" * * @param string $name * @param mixed $args * * @return mixed * @throws Exception */ public static function __callStatic(string $name, $args) { $callback = [ self::getDB(), $name ]; $result = null; try { $result = call_user_func_array($callback, $args); } catch (PDOException $e) { self::showerror($e); } return $result; } /** * write new userdata.inc.php file */ private static function generateNewUserData(array $sql, array $sql_root) { $content = PhpHelper::parseArrayToPhpFile( ['sql' => $sql, 'sql_root' => $sql_root], 'automatically generated userdata.inc.php for froxlor' ); chmod(Froxlor::getInstallDir() . "/lib/userdata.inc.php", 0700); file_put_contents(Froxlor::getInstallDir() . "/lib/userdata.inc.php", $content); chmod(Froxlor::getInstallDir() . "/lib/userdata.inc.php", 0400); clearstatcache(); if (function_exists('opcache_invalidate')) { @opcache_invalidate(Froxlor::getInstallDir() . "/lib/userdata.inc.php", true); } } } ================================================ FILE: lib/Froxlor/Database/DbManager.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Database; use Exception; use Froxlor\Database\Manager\DbManagerMySQL; use Froxlor\Froxlor; use Froxlor\FroxlorLogger; use Froxlor\Settings; use PDO; /** * Class DbManager * * Wrapper-class for database-management like creating * and removing databases, users and permissions */ class DbManager { /** * FroxlorLogger object * * @var object */ private $log = null; /** * Manager object * * @var object */ private $manager = null; /** * main constructor * * @param FroxlorLogger $log */ public function __construct($log = null) { $this->log = $log; $this->setManager(); } /** * set manager-object by type of * dbms: mysql only for now * * sets private $_manager variable */ private function setManager() { // TODO read different dbms from settings later $this->manager = new DbManagerMySQL($this->log); } /** * function called when the mysql-access-host setting changes * * @param array $mysql_access_host_array * * @return void * @throws Exception */ public static function correctMysqlUsers(array $mysql_access_host_array) { // get all databases for all dbservers $databases = []; $databases_result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_DATABASES . "` ORDER BY `dbserver` ASC "); Database::pexecute($databases_result_stmt); while ($databases_row = $databases_result_stmt->fetch(PDO::FETCH_ASSOC)) { if (!isset($databases[$databases_row['dbserver']])) { $databases[$databases_row['dbserver']] = []; } $databases[$databases_row['dbserver']][] = $databases_row['databasename']; } $customers_sel = Database::query(" SELECT DISTINCT c.loginname FROM `" . TABLE_PANEL_CUSTOMERS . "` c LEFT JOIN `" . TABLE_PANEL_DATABASES . "` d ON c.customerid = d.customerid WHERE c.deactivated = '0' AND d.id IS NOT NULL "); $customers = []; while ($customer = $customers_sel->fetch(\PDO::FETCH_ASSOC)) { $customers[] = $customer['loginname']; } $dbservers_stmt = Database::query("SELECT DISTINCT `dbserver` FROM `" . TABLE_PANEL_DATABASES . "`"); while ($dbserver = $dbservers_stmt->fetch(PDO::FETCH_ASSOC)) { // add all customer loginnames to the $databases array for this database-server to correct // a possible existing global mysql-user for that customer foreach ($customers as $customer) { $databases[$dbserver['dbserver']][] = $customer; } // require privileged access for target db-server Database::needRoot(true, $dbserver['dbserver'], false); $dbm = new DbManager(FroxlorLogger::getInstanceOf()); $users = $dbm->getManager()->getAllSqlUsers(false); foreach ($databases[$dbserver['dbserver']] as $username) { if (isset($users[$username]['hosts']) && is_array($users[$username]['hosts'])) { foreach ($mysql_access_host_array as $mysql_access_host) { $mysql_access_host = trim($mysql_access_host); if (!in_array($mysql_access_host, $users[$username]['hosts'])) { // if this is a new host, use credentials from localhost, which should always exist $password = [ 'password' => $users[$username]['hosts']['localhost']['password'], 'plugin' => $users[$username]['hosts']['localhost']['plugin'] ]; $dbm->getManager()->grantPrivilegesTo($username, $password, $mysql_access_host, true); } } foreach ($users[$username]['hosts'] as $mysql_access_host) { if (!in_array($mysql_access_host, $mysql_access_host_array)) { $dbm->getManager()->deleteUser($username, $mysql_access_host); } } } } $dbm->getManager()->flushPrivileges(); Database::needRoot(false); unset($databases[$dbserver['dbserver']]); } } /** * creates a new database and a user with the * same name with all privileges granted on the db. * DB-name and user-name are being generated and * the password for the user will be set * * @param ?string $loginname * @param ?string $password * @param int $dbserver * @param int $last_accnumber * @param ?string $global_user * * @return string|bool $username if successful or false of username is equal to the password * @throws Exception */ public function createDatabase(string $loginname = null, string $password = null, int $dbserver = 0, int $last_accnumber = 0, string $global_user = "") { Database::needRoot(true, $dbserver, false); // check whether we shall create a random username if (strtoupper(Settings::Get('customer.mysqlprefix')) == 'RANDOM') { // get all usernames from db-manager $allsqlusers = $this->getManager()->getAllSqlUsers(); // generate random username $username = $loginname . '-' . substr(Froxlor::genSessionId(), 20, 3); // check whether it exists on the DBMS while (in_array($username, $allsqlusers)) { $username = $loginname . '-' . substr(Froxlor::genSessionId(), 20, 3); } } elseif (strtoupper(Settings::Get('customer.mysqlprefix')) == 'DBNAME') { $username = $loginname; } else { $username = $loginname . Settings::Get('customer.mysqlprefix') . (intval($last_accnumber) + 1); } // don't use a password that is the same as the username if ($username == $password) { return false; } // now create the database itself $this->getManager()->createDatabase($username); // and give permission to the user on every access-host we have foreach (array_map('trim', explode(',', Settings::Get('system.mysql_access_host'))) as $mysql_access_host) { $this->getManager()->grantPrivilegesTo($username, $password, $mysql_access_host); if (!empty($global_user)) { $this->getManager()->grantCreateToDb($global_user, $username, $mysql_access_host); } } $this->getManager()->flushPrivileges(); Database::needRoot(); $this->log->logAction(FroxlorLogger::USR_ACTION, LOG_INFO, "created database '" . $username . "'"); return $username; } /** * returns the manager-object * from where we can control it */ public function getManager() { return $this->manager; } } ================================================ FILE: lib/Froxlor/Database/IntegrityCheck.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Database; use Froxlor\FroxlorLogger; use Froxlor\Settings; use PDO; class IntegrityCheck { // Store all available checks public $available = []; // logger object private $log = null; /** * Constructor * Parses all available checks into $this->available */ public function __construct() { global $userinfo; if (!isset($userinfo) || !is_array($userinfo)) { $userinfo = [ 'loginname' => 'integrity-check' ]; } $this->log = FroxlorLogger::getInstanceOf($userinfo); $this->available = get_class_methods($this); unset($this->available[array_search('__construct', $this->available)]); unset($this->available[array_search('checkAll', $this->available)]); unset($this->available[array_search('fixAll', $this->available)]); sort($this->available); } /** * Check all occurring integrity problems at once */ public function checkAll() { $integrityok = true; foreach ($this->available as $check) { $integrityok = $this->$check() ? $integrityok : false; } return $integrityok; } /** * Fix all occurring integrity problems at once with default settings */ public function fixAll() { $integrityok = true; foreach ($this->available as $check) { $integrityok = $this->$check(true) ? $integrityok : false; } return $integrityok; } /** * check whether the froxlor database and its tables are in utf-8 character-set * * @param bool $fix fix db charset/collation if not utf8 * * @return bool * @throws \Exception */ public function databaseCharset(bool $fix = false): bool { // get character-set $cs_stmt = Database::prepare('SELECT default_character_set_name FROM information_schema.SCHEMATA WHERE schema_name = :dbname'); $resp = Database::pexecute_first($cs_stmt, [ 'dbname' => Database::getDbName() ]); $charset = $resp['default_character_set_name'] ?? null; if (!empty($charset) && substr(strtolower($charset), 0, 4) != 'utf8') { $this->log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "database charset seems to be different from UTF-8, integrity-check can fix that"); if ($fix) { // fix database Database::query('ALTER DATABASE `' . Database::getDbName() . '` CHARACTER SET utf8 COLLATE utf8_general_ci'); // fix all tables $handle = Database::query('SHOW FULL TABLES WHERE Table_type != "VIEW"'); while ($row = $handle->fetch(PDO::FETCH_BOTH)) { $table = $row[0]; Database::query('ALTER TABLE `' . $table . '` CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci;'); } $this->log->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "database charset was different from UTF-8, integrity-check fixed that"); } else { return false; } } if ($fix) { return $this->databaseCharset(); } return true; } /** * Check the integrity of the domain to ip/port - association * * @param bool $fix fix everything found directly * * @return bool * @throws \Exception */ public function domainIpTable(bool $fix = false): bool { $ips = []; $domains = []; $ipstodomains = []; $admips = []; if ($fix) { // Prepare insert / delete statement for the fixes $del_stmt = Database::prepare(" DELETE FROM `" . TABLE_DOMAINTOIP . "` WHERE `id_domain` = :domainid AND `id_ipandports` = :ipandportid "); $ins_stmt = Database::prepare(" INSERT INTO `" . TABLE_DOMAINTOIP . "` SET `id_domain` = :domainid, `id_ipandports` = :ipandportid "); // Cache all IPs the admins have assigned $adm_stmt = Database::prepare("SELECT `adminid`, `ip` FROM `" . TABLE_PANEL_ADMINS . "` ORDER BY `adminid` ASC"); Database::pexecute($adm_stmt); $default_ips = explode(',', Settings::Get('system.defaultip')); $default_ssl_ips = explode(',', Settings::Get('system.defaultsslip')); while ($row = $adm_stmt->fetch(PDO::FETCH_ASSOC)) { if ($row['ip'] < 0 || is_null($row['ip']) || empty($row['ip'])) { // Admin uses default-IP $admips[$row['adminid']] = array_merge($default_ips, $default_ssl_ips); } else { $admips[$row['adminid']] = [ $row['ip'] ]; } } } // Cache all available ip/port - combinations $result_stmt = Database::prepare("SELECT `id`, `ip`, `port` FROM `" . TABLE_PANEL_IPSANDPORTS . "` ORDER BY `id` ASC"); Database::pexecute($result_stmt); while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $ips[$row['id']] = $row['ip'] . ':' . $row['port']; } // Cache all configured domains $result_stmt = Database::prepare("SELECT `id`, `adminid` FROM `" . TABLE_PANEL_DOMAINS . "` ORDER BY `id` ASC"); Database::pexecute($result_stmt); while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $domains[$row['id']] = $row['adminid']; } // Check if every domain to ip/port - association is valid in TABLE_DOMAINTOIP $result_stmt = Database::prepare("SELECT `id_domain`, `id_ipandports` FROM `" . TABLE_DOMAINTOIP . "`"); Database::pexecute($result_stmt); while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { if (!array_key_exists($row['id_ipandports'], $ips)) { if ($fix) { Database::pexecute($del_stmt, [ 'domainid' => $row['id_domain'], 'ipandportid' => $row['id_ipandports'] ]); $this->log->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "found an ip/port-id in domain <> ip table which does not exist, integrity check fixed this"); } else { $this->log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "found an ip/port-id in domain <> ip table which does not exist, integrity check can fix this"); return false; } } if (!array_key_exists($row['id_domain'], $domains)) { if ($fix) { Database::pexecute($del_stmt, [ 'domainid' => $row['id_domain'], 'ipandportid' => $row['id_ipandports'] ]); $this->log->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "found a domain-id in domain <> ip table which does not exist, integrity check fixed this"); } else { $this->log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "found a domain-id in domain <> ip table which does not exist, integrity check can fix this"); return false; } } // Save one IP/Port combination per domain, so we know, if one domain is missing an IP $ipstodomains[$row['id_domain']] = $row['id_ipandports']; } // Check that all domains have at least one IP/Port combination foreach ($domains as $domainid => $adminid) { if (!array_key_exists($domainid, $ipstodomains)) { if ($fix) { foreach ($admips[$adminid] as $defaultip) { Database::pexecute($ins_stmt, [ 'domainid' => $domainid, 'ipandportid' => $defaultip ]); } $this->log->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "found a domain-id with no entry in domain <> ip table, integrity check fixed this"); } else { $this->log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "found a domain-id with no entry in domain <> ip table, integrity check can fix this"); return false; } } } if ($fix) { return $this->domainIpTable(); } return true; } /** * Check if all subdomains have ssl-redirect = 0 if domain has no ssl-port * * @param bool $fix fix everything found directly * * @return bool * @throws \Exception */ public function subdomainSslRedirect(bool $fix = false): bool { $ips = []; $parentdomains = []; $subdomains = []; if ($fix) { // Prepare update statement for the fixes $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_DOMAINS . "` SET `ssl_redirect` = 0 WHERE `parentdomainid` = :domainid"); } // Cache all ssl ip/port - combinations $result_stmt = Database::prepare("SELECT `id`, `ip`, `port` FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE `ssl` = 1 ORDER BY `id` ASC"); Database::pexecute($result_stmt); while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $ips[$row['id']] = $row['ip'] . ':' . $row['port']; } // Cache all configured domains $result_stmt = Database::prepare("SELECT `id`, `parentdomainid`, `ssl_redirect` FROM `" . TABLE_PANEL_DOMAINS . "` ORDER BY `id` ASC"); $ip_stmt = Database::prepare("SELECT `id_domain`, `id_ipandports` FROM `" . TABLE_DOMAINTOIP . "` WHERE `id_domain` = :domainid"); Database::pexecute($result_stmt); while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { if ($row['parentdomainid'] == 0) { // All parentdomains by default have no ssl - ip/port $parentdomains[$row['id']] = false; Database::pexecute($ip_stmt, [ 'domainid' => $row['id'] ]); while ($iprow = $ip_stmt->fetch(PDO::FETCH_ASSOC)) { // If the parentdomain has an ip/port assigned which we know is SSL enabled, set the parentdomain to "true" if (array_key_exists($iprow['id_ipandports'], $ips)) { $parentdomains[$row['id']] = true; } } } elseif ($row['ssl_redirect'] == 1) { // All subdomains with enabled ssl_redirect enabled are stored if (!isset($subdomains[$row['parentdomainid']])) { $subdomains[$row['parentdomainid']] = []; } $subdomains[$row['parentdomainid']][] = $row['id']; } } // Check if every parentdomain with enabled ssl_redirect as SSL enabled foreach ($parentdomains as $id => $sslavailable) { // This parentdomain has no subdomains if (!isset($subdomains[$id])) { continue; } // This parentdomain has SSL enabled, doesn't matter what status the subdomains have if ($sslavailable) { continue; } // At this point only parentdomains reside which have ssl_redirect enabled subdomains if ($fix) { // We make a blanket update to all subdomains of this parentdomain, doesn't matter which one is wrong, all have to be disabled Database::pexecute($upd_stmt, [ 'domainid' => $id ]); $this->log->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "found a subdomain with ssl_redirect=1 but parent-domain has ssl=0, integrity check fixed this"); } else { // It's just the check, let the function fail $this->log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "found a subdomain with ssl_redirect=1 but parent-domain has ssl=0, integrity check can fix this"); return false; } } if ($fix) { return $this->subdomainSslRedirect(); } return true; } /** * Check if all subdomain have letsencrypt = 0 if domain has no ssl-port * * @param bool $fix fix everything found directly * * @return bool * @throws \Exception */ public function subdomainLetsencrypt(bool $fix = false): bool { $ips = []; $parentdomains = []; $subdomains = []; if ($fix) { // Prepare update statement for the fixes $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_DOMAINS . "` SET `letsencrypt` = 0 WHERE `parentdomainid` = :domainid"); } // Cache all ssl ip/port - combinations $result_stmt = Database::prepare("SELECT `id`, `ip`, `port` FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE `ssl` = 1 ORDER BY `id` ASC"); Database::pexecute($result_stmt); while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $ips[$row['id']] = $row['ip'] . ':' . $row['port']; } // Cache all configured domains $result_stmt = Database::prepare("SELECT `id`, `parentdomainid`, `letsencrypt` FROM `" . TABLE_PANEL_DOMAINS . "` ORDER BY `id` ASC"); $ip_stmt = Database::prepare("SELECT `id_domain`, `id_ipandports` FROM `" . TABLE_DOMAINTOIP . "` WHERE `id_domain` = :domainid"); Database::pexecute($result_stmt); while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { if ($row['parentdomainid'] == 0) { // All parentdomains by default have no ssl - ip/port $parentdomains[$row['id']] = false; Database::pexecute($ip_stmt, [ 'domainid' => $row['id'] ]); while ($iprow = $ip_stmt->fetch(PDO::FETCH_ASSOC)) { // If the parentdomain has an ip/port assigned which we know is SSL enabled, set the parentdomain to "true" if (array_key_exists($iprow['id_ipandports'], $ips)) { $parentdomains[$row['id']] = true; } } } elseif ($row['letsencrypt'] == 1) { // All subdomains with enabled letsencrypt enabled are stored if (!isset($subdomains[$row['parentdomainid']])) { $subdomains[$row['parentdomainid']] = []; } $subdomains[$row['parentdomainid']][] = $row['id']; } } // Check if every parentdomain with enabled letsencrypt as SSL enabled foreach ($parentdomains as $id => $sslavailable) { // This parentdomain has no subdomains if (!isset($subdomains[$id])) { continue; } // This parentdomain has SSL enabled, doesn't matter what status the subdomains have if ($sslavailable) { continue; } // At this point only parentdomains reside which have letsencrypt enabled subdomains if ($fix) { // We make a blanket update to all subdomains of this parentdomain, doesn't matter which one is wrong, all have to be disabled Database::pexecute($upd_stmt, [ 'domainid' => $id ]); $this->log->logAction(FroxlorLogger::ADM_ACTION, LOG_WARNING, "found a subdomain with letsencrypt=1 but parent-domain has ssl=0, integrity check fixed this"); } else { // It's just the check, let the function fail $this->log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "found a subdomain with letsencrypt=1 but parent-domain has ssl=0, integrity check can fix this"); return false; } } if ($fix) { return $this->subdomainLetsencrypt(); } return true; } /** * check whether the webserveruser is in * the customers groups when fcgid / php-fpm is used * * @param bool $fix fix member/groups * * @return bool * @throws \Exception */ public function webserverGroupMemberForFcgidPhpFpm(bool $fix = false): bool { if (Settings::Get('system.mod_fcgid') == 0 && Settings::Get('phpfpm.enabled') == 0) { return true; } // get all customers that don't have the webserver-user in their group $cwg_stmt = Database::prepare(" SELECT `id` FROM `" . TABLE_FTP_GROUPS . "` WHERE NOT FIND_IN_SET(:webserveruser, `members`) "); Database::pexecute($cwg_stmt, [ 'webserveruser' => Settings::Get('system.httpuser') ]); if ($cwg_stmt->rowCount() > 0) { $this->log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "Customers are missing the webserver-user as group-member, integrity-check can fix that"); if ($fix) { // prepare update statement $upd_stmt = Database::prepare(" UPDATE `" . TABLE_FTP_GROUPS . "` SET `members` = CONCAT(`members`, :additionaluser) WHERE `id` = :id "); $upd_data = [ 'additionaluser' => "," . Settings::Get('system.httpuser') ]; while ($cwg_row = $cwg_stmt->fetch()) { $upd_data['id'] = $cwg_row['id']; Database::pexecute($upd_stmt, $upd_data); } $this->log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "Customers were missing the webserver-user as group-member, integrity-check fixed that"); } else { return false; } } if ($fix) { return $this->webserverGroupMemberForFcgidPhpFpm(); } return true; } /** * check whether the local froxlor user is in * the customers groups when fcgid / php-fpm and * fcgid/fpm in froxlor vhost is used * * @param bool $fix fix member/groups * * @return bool * @throws \Exception */ public function froxlorLocalGroupMemberForFcgidPhpFpm(bool $fix = false): bool { if (Settings::Get('system.mod_fcgid') == 0 && Settings::Get('phpfpm.enabled') == 0) { return true; } if (Settings::get('system.mod_fcgid') == 1) { if (Settings::get('system.mod_fcgid_ownvhost') == 0) { return true; } else { $localuser = Settings::Get('system.mod_fcgid_httpuser'); } } if (Settings::get('phpfpm.enabled') == 1) { if (Settings::get('phpfpm.enabled_ownvhost') == 0) { return true; } else { $localuser = Settings::Get('phpfpm.vhost_httpuser'); } } // get all customers that don't have the webserver-user in their group $cwg_stmt = Database::prepare(" SELECT `id` FROM `" . TABLE_FTP_GROUPS . "` WHERE NOT FIND_IN_SET(:localuser, `members`) "); Database::pexecute($cwg_stmt, [ 'localuser' => $localuser ]); if ($cwg_stmt->rowCount() > 0) { $this->log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "Customers are missing the local froxlor-user as group-member, integrity-check can fix that"); if ($fix) { // prepare update statement $upd_stmt = Database::prepare(" UPDATE `" . TABLE_FTP_GROUPS . "` SET `members` = CONCAT(`members`, :additionaluser) WHERE `id` = :id "); $upd_data = [ 'additionaluser' => "," . $localuser ]; while ($cwg_row = $cwg_stmt->fetch()) { $upd_data['id'] = $cwg_row['id']; Database::pexecute($upd_stmt, $upd_data); } $this->log->logAction(FroxlorLogger::ADM_ACTION, LOG_NOTICE, "Customers were missing the local froxlor-user as group-member, integrity-check fixed that"); } else { return false; } } if ($fix) { return $this->froxlorLocalGroupMemberForFcgidPhpFpm(); } return true; } } ================================================ FILE: lib/Froxlor/Database/Manager/DbManagerMySQL.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Database\Manager; use Froxlor\Database\Database; use Froxlor\FroxlorLogger; use PDO; /** * Class DbManagerMySQL * * Explicit class for database-management like creating * and removing databases, users and permissions for MySQL */ class DbManagerMySQL { /** * FroxlorLogger object * * @var object */ private $log = null; /** * main constructor * * @param FroxlorLogger|null $log */ public function __construct(&$log = null) { $this->log = $log; } /** * creates a database * * @param string|null $dbname */ public function createDatabase(string $dbname = null) { Database::query("CREATE DATABASE `" . $dbname . "`"); } /** * grants access privileges on a database with the same * username and sets the password for that user the given access_host * * @param string $username * @param string|array $password * @param ?string $access_host * @param bool $p_encrypted * optional, whether the password is encrypted or not, default false * @param bool $update * optional, whether to update the password only (not create user) * @param bool $grant_access_prefix * optional, whether the given user will have access to all databases starting with the username, default false * @throws \Exception */ public function grantPrivilegesTo(string $username, $password, string $access_host = null, bool $p_encrypted = false, bool $update = false, bool $grant_access_prefix = false) { // this is required for mysql8 $pwd_plugin = 'caching_sha2_password'; if (is_array($password) && count($password) == 2) { $pwd_plugin = $password['plugin']; $password = $password['password']; } if (!$update) { // create user if ($p_encrypted) { if (version_compare(Database::getAttribute(\PDO::ATTR_SERVER_VERSION), '5.7.0', '<') || version_compare(Database::getAttribute(\PDO::ATTR_SERVER_VERSION), '10.0.0', '>=')) { $stmt = Database::prepare(" CREATE USER '" . $username . "'@'" . $access_host . "' IDENTIFIED BY PASSWORD :password "); } else { $stmt = Database::prepare(" CREATE USER '" . $username . "'@'" . $access_host . "' IDENTIFIED WITH " . $pwd_plugin . " AS :password "); } } else { $stmt = Database::prepare(" CREATE USER '" . $username . "'@'" . $access_host . "' IDENTIFIED BY :password "); } Database::pexecute($stmt, [ "password" => $password ]); // grant privileges if not global user if (!$grant_access_prefix) { Database::query("GRANT ALL ON `" . str_replace('_', '\_', $username) . "`.* TO `" . $username . "`@`" . $access_host . "`"); } else { // grant explicitly to existing databases $this->grantCreateToCustomerDbs($username, $access_host); } } else { // set password if (version_compare(Database::getAttribute(\PDO::ATTR_SERVER_VERSION), '5.7.6', '<') || version_compare(Database::getAttribute(\PDO::ATTR_SERVER_VERSION), '10.0.0', '>=')) { if ($p_encrypted) { $stmt = Database::prepare("SET PASSWORD FOR :username@:host = :password"); } else { $stmt = Database::prepare("SET PASSWORD FOR :username@:host = PASSWORD(:password)"); } } else { if ($p_encrypted) { $stmt = Database::prepare("ALTER USER :username@:host IDENTIFIED WITH " . $pwd_plugin . " AS :password"); } else { $stmt = Database::prepare("ALTER USER :username@:host IDENTIFIED BY :password"); } } Database::pexecute($stmt, [ "username" => $username, "host" => $access_host, "password" => $password ]); } } /** * removes the given database from the dbms and also * takes away any privileges from a user to that db * * @param string $dbname * @param ?string $global_user * @throws \Exception */ public function deleteDatabase(string $dbname, string $global_user = "") { if (version_compare(Database::getAttribute(PDO::ATTR_SERVER_VERSION), '5.0.2', '<')) { // failsafe if user has been deleted manually (requires MySQL 4.1.2+) $stmt = Database::prepare("REVOKE ALL PRIVILEGES, GRANT OPTION FROM `" . $dbname . "`"); Database::pexecute($stmt, [], false); } $host_res_stmt = Database::prepare(" SELECT `Host` FROM `mysql`.`user` WHERE `User` = :dbname"); Database::pexecute($host_res_stmt, [ 'dbname' => $dbname ]); // as of MySQL 5.0.2 this also revokes privileges. (requires MySQL 4.1.2+) if (version_compare(Database::getAttribute(PDO::ATTR_SERVER_VERSION), '5.7.0', '<')) { $drop_stmt = Database::prepare("DROP USER :dbname@:host"); } else { $drop_stmt = Database::prepare("DROP USER IF EXISTS :dbname@:host"); } $rev_stmt = Database::prepare("REVOKE ALL PRIVILEGES ON `" . $dbname . "`.* FROM :guser@:host;"); while ($host = $host_res_stmt->fetch(PDO::FETCH_ASSOC)) { Database::pexecute($drop_stmt, [ 'dbname' => $dbname, 'host' => $host['Host'] ], false); if (!empty($global_user)) { Database::pexecute($rev_stmt, [ 'guser' => $global_user, 'host' => $host['Host'] ], false); } } $drop_stmt = Database::prepare("DROP DATABASE IF EXISTS `" . $dbname . "`"); Database::pexecute($drop_stmt); } /** * removes a user from the dbms and revokes all privileges * * @param string $username * @param string $host * @throws \Exception */ public function deleteUser(string $username, string $host) { if ($this->userExistsOnHost($username, $host)) { if (version_compare(Database::getAttribute(PDO::ATTR_SERVER_VERSION), '5.0.2', '<')) { // Revoke privileges (only required for MySQL 4.1.2 - 5.0.1) $stmt = Database::prepare("REVOKE ALL PRIVILEGES ON * . * FROM `" . $username . "`@`" . $host . "`"); Database::pexecute($stmt); } // as of MySQL 5.0.2 this also revokes privileges. (requires MySQL 4.1.2+) if (version_compare(Database::getAttribute(PDO::ATTR_SERVER_VERSION), '5.7.0', '<')) { $stmt = Database::prepare("DROP USER :username@:host"); } else { $stmt = Database::prepare("DROP USER IF EXISTS :username@:host"); } Database::pexecute($stmt, [ "username" => $username, "host" => $host ]); } } /** * removes permissions from a user * * @param string $username * @param string $host * @throws \Exception */ public function disableUser(string $username, string $host) { $stmt = Database::prepare('REVOKE ALL PRIVILEGES, GRANT OPTION FROM `' . $username . '`@`' . $host . '`'); Database::pexecute($stmt, [], false); } /** * re-grant permissions to a user * * @param string $username * @param string $host * @param bool $grant_access_prefix * @throws \Exception */ public function enableUser(string $username, string $host, bool $grant_access_prefix = false) { // check whether user exists to avoid errors if ($this->userExistsOnHost($username, $host)) { if (!$grant_access_prefix) { Database::query('GRANT ALL PRIVILEGES ON `' . str_replace('_', '\_', $username) . '`.* TO `' . $username . '`@`' . $host . '`'); } else { $this->grantCreateToCustomerDbs($username, $host); } } } /** * Check whether a given username exists for the given host * * @param string $username * @param string $host * @return bool * @throws \Exception */ public function userExistsOnHost(string $username, string $host): bool { $exist_check_stmt = Database::prepare("SELECT EXISTS(SELECT 1 FROM mysql.user WHERE user = '" . $username . "' AND host = '" . $host . "')"); $exist_check = Database::pexecute_first($exist_check_stmt); return ($exist_check && array_pop($exist_check) == '1'); } /** * flushes the privileges...pretty obvious eh? */ public function flushPrivileges() { Database::query("FLUSH PRIVILEGES"); } /** * return an array of all usernames used in that DBMS * * @param bool $user_only if false, will be selected from mysql.user and slightly different array will be generated * * @return array * @throws \Exception */ public function getAllSqlUsers(bool $user_only = true): array { if (!$user_only) { $result_stmt = Database::prepare('SELECT * FROM mysql.user'); } else { $result_stmt = Database::prepare('SELECT `User` FROM mysql.user'); } Database::pexecute($result_stmt); $allsqlusers = []; while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { if ($user_only == false) { if (!isset($allsqlusers[$row['User']]) || !is_array($allsqlusers[$row['User']])) { $allsqlusers[$row['User']] = [ 'hosts' => [] ]; } $allsqlusers[$row['User']]['hosts'][$row['Host']] = [ 'password' => $row['Password'] ?? $row['authentication_string'], 'plugin' => $row['plugin'] ?? 'caching_sha2_password', ]; } else { $allsqlusers[] = $row['User']; } } return $allsqlusers; } /** * grant "CREATE" for prefix user to all existing databases of that customer * * @param string $username * @param string $access_host * @return void * @throws \Exception */ private function grantCreateToCustomerDbs(string $username, string $access_host) { // remember what (possible remote) db-server we're on $currentDbServer = Database::getServer(); // use "unprivileged" connection Database::needRoot(); $cus_stmt = Database::prepare("SELECT customerid FROM `" . TABLE_PANEL_CUSTOMERS . "` WHERE loginname = :username"); $cust = Database::pexecute_first($cus_stmt, ['username' => $username]); if ($cust) { $sel_stmt = Database::prepare("SELECT databasename FROM `" . TABLE_PANEL_DATABASES . "` WHERE `customerid` = :cid AND `dbserver` = :dbserver"); Database::pexecute($sel_stmt, ['cid' => $cust['customerid'], 'dbserver' => $currentDbServer]); // reset to root-connection for used dbserver Database::needRoot(true, $currentDbServer, false); while ($dbdata = $sel_stmt->fetch(\PDO::FETCH_ASSOC)) { $stmt = Database::prepare(" GRANT ALL ON `" . str_replace('_', '\_', $dbdata['databasename']) . "`.* TO `" . $username . "`@`" . $access_host . "` "); Database::pexecute($stmt); } } } /** * grant "CREATE" for prefix user to all existing databases of that customer * * @param string $username * @param string $database * @param string $access_host * @return void * @throws \Exception */ public function grantCreateToDb(string $username, string $database, string $access_host) { // only grant permission if the user exists if ($this->userExistsOnHost($username, $access_host)) { $stmt = Database::prepare(" GRANT ALL ON `" . str_replace('_', '\_', $database) . "`.* TO `" . $username . "`@`" . $access_host . "` "); Database::pexecute($stmt); } } } ================================================ FILE: lib/Froxlor/Database/Manager/index.html ================================================ ================================================ FILE: lib/Froxlor/Database/index.html ================================================ ================================================ FILE: lib/Froxlor/Dns/Dns.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Dns; use Froxlor\Database\Database; use Froxlor\Idna\IdnaWrapper; use Froxlor\Settings; use Froxlor\UI\Response; use PDO; class Dns { /** * @param int $domain_id * @param string $area * @param array $userinfo * * @return string|void * @throws \Exception */ public static function getAllowedDomainEntry(int $domain_id, string $area = 'customer', array $userinfo = []) { $dom_data = [ 'did' => $domain_id ]; $where_clause = ''; if ($area == 'admin') { if ((int)$userinfo['customers_see_all'] == 0) { $where_clause = '`adminid` = :uid AND '; $dom_data['uid'] = $userinfo['userid']; } } else { $where_clause = '`customerid` = :uid AND '; $dom_data['uid'] = $userinfo['userid']; } $dom_stmt = Database::prepare(" SELECT domain, isbinddomain FROM `" . TABLE_PANEL_DOMAINS . "` WHERE " . $where_clause . " id = :did "); $domain = Database::pexecute_first($dom_stmt, $dom_data); if ($domain) { if ($domain['isbinddomain'] != '1') { Response::standardError('dns_domain_nodns'); } $idna_convert = new IdnaWrapper(); return $idna_convert->decode($domain['domain']); } Response::standardError('dns_notfoundorallowed'); } /** * @param int|array $domain_id id of domain or in case of froxlorhostname, a domain-array with the needed data * @param bool $froxlorhostname * @param bool $isMainButSubTo * * @return DnsZone|void * @throws \Exception */ public static function createDomainZone($domain_id, bool $froxlorhostname = false, bool $isMainButSubTo = false) { if (!$froxlorhostname) { // get domain-name $dom_stmt = Database::prepare("SELECT * FROM `" . TABLE_PANEL_DOMAINS . "` WHERE id = :did"); $domain = Database::pexecute_first($dom_stmt, [ 'did' => $domain_id ]); } else { $domain = $domain_id; } if (!isset($domain['isbinddomain']) || $domain['isbinddomain'] != '1') { return; } $dom_entries = []; if (!$froxlorhostname) { // select all entries $sel_stmt = Database::prepare("SELECT * FROM `" . TABLE_DOMAIN_DNS . "` WHERE domain_id = :did ORDER BY id ASC"); Database::pexecute($sel_stmt, [ 'did' => $domain_id ]); $dom_entries = $sel_stmt->fetchAll(PDO::FETCH_ASSOC); } // check for required records $required_entries = []; if ($domain['email_only'] == '0') { self::addRequiredEntry('@', 'A', $required_entries); self::addRequiredEntry('@', 'AAAA', $required_entries); } if (!$isMainButSubTo) { self::addRequiredEntry('@', 'NS', $required_entries); } if ($domain['isemaildomain'] == '1') { self::addRequiredEntry('@', 'MX', $required_entries); if (Settings::Get('system.dns_createmailentry')) { foreach (['imap', 'pop3', 'mail', 'smtp' ] as $record ) { foreach (['AAAA', 'A' ] as $type ) { self::addRequiredEntry($record, $type, $required_entries); } } } } // additional required records by setting if ($domain['email_only'] == '0') { if ($domain['iswildcarddomain'] == '1') { self::addRequiredEntry('*', 'A', $required_entries); self::addRequiredEntry('*', 'AAAA', $required_entries); } elseif ($domain['wwwserveralias'] == '1') { self::addRequiredEntry('www', 'A', $required_entries); self::addRequiredEntry('www', 'AAAA', $required_entries); } } if (!$froxlorhostname) { // additional required records for subdomains $subdomains_stmt = Database::prepare(" SELECT `domain`, `iswildcarddomain`, `wwwserveralias`, `isemaildomain`, `email_only` FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `parentdomainid` = :domainid "); Database::pexecute($subdomains_stmt, [ 'domainid' => $domain_id ]); while ($subdomain = $subdomains_stmt->fetch(PDO::FETCH_ASSOC)) { $sub_record = str_replace('.' . $domain['domain'], '', $subdomain['domain']); // Listing domains is enough as there currently is no support for choosing // different ips for a subdomain => use same IPs as toplevel if ($subdomain['email_only'] == '0') { self::addRequiredEntry($sub_record, 'A', $required_entries); self::addRequiredEntry($sub_record, 'AAAA', $required_entries); // Check whether to add a www.-prefix if ($subdomain['iswildcarddomain'] == '1') { self::addRequiredEntry('*.' . $sub_record, 'A', $required_entries); self::addRequiredEntry('*.' . $sub_record, 'AAAA', $required_entries); } elseif ($subdomain['wwwserveralias'] == '1') { self::addRequiredEntry('www.' . $sub_record, 'A', $required_entries); self::addRequiredEntry('www.' . $sub_record, 'AAAA', $required_entries); } } // check for email ability if ($subdomain['isemaildomain'] == '1') { if (Settings::Get('spf.use_spf') == '1') { // check for SPF content later self::addRequiredEntry('@SPF@.' . $sub_record, 'TXT', $required_entries); } if (Settings::Get('dmarc.use_dmarc') == '1') { // check for DMARC content later self::addRequiredEntry('@DMARC@.' . $sub_record, 'TXT', $required_entries); } if (Settings::Get('antispam.activated') == '1' && $domain['dkim'] == '1') { // check for DKIM content later self::addRequiredEntry('dkim' . $domain['dkim_id'] . '._domainkey.' . $sub_record, 'TXT', $required_entries); } } } } // additional required records for CAA if activated if (Settings::Get('system.dns_createcaaentry') && Settings::Get('system.use_ssl') == "1") { $result_stmt = Database::prepare(" SELECT i.`ip`, i.`port`, i.`ssl` FROM " . TABLE_PANEL_IPSANDPORTS . " i LEFT JOIN " . TABLE_DOMAINTOIP . " dip ON dip.id_ipandports = i.id WHERE i.ssl = 1 AND dip.id_domain = :domainid "); Database::pexecute($result_stmt, [ 'domainid' => $domain['id'] ]); $ssl_ipandports = []; while ($ssl_ipandport = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $ssl_ipandports[] = $ssl_ipandport; } if (!empty($ssl_ipandports)) { // check for CAA content later self::addRequiredEntry('@CAA@', 'CAA', $required_entries); } } // additional required records for SPF and DKIM if activated if ($domain['isemaildomain'] == '1') { if (Settings::Get('spf.use_spf') == '1') { // check for SPF content later self::addRequiredEntry('@SPF@', 'TXT', $required_entries); } if (Settings::Get('dmarc.use_dmarc') == '1') { // check for DMARC content later self::addRequiredEntry('@DMARC@', 'TXT', $required_entries); } if (Settings::Get('antispam.activated') == '1' && $domain['dkim'] == '1') { // check for DKIM content later self::addRequiredEntry('dkim' . $domain['dkim_id'] . '._domainkey', 'TXT', $required_entries); } } $primary_ns = null; $zonerecords = []; // now generate all records and unset the required entries we have foreach ($dom_entries as $entry) { if (array_key_exists($entry['type'], $required_entries) && array_key_exists( md5($entry['record']), $required_entries[$entry['type']]) ) { unset($required_entries[$entry['type']][md5($entry['record'])]); } if (Settings::Get('system.dns_createcaaentry') == '1' && $entry['type'] == 'CAA' && strtolower(substr($entry['content'], 0, 7 )) == '"v=caa1' ) { // unset special CAA required-entry unset($required_entries[$entry['type']][md5("@CAA@")]); } if (Settings::Get('spf.use_spf') == '1' && $entry['type'] == 'TXT' && (strtolower(substr($entry['content'], 0, 7)) == '"v=spf1' || strtolower(substr($entry['content'], 0, 6)) == 'v=spf1') ) { // unset special spf required-entry if ($entry['record'] == '@') { unset($required_entries[$entry['type']][md5("@SPF@")]); } else { // subdomain unset($required_entries[$entry['type']][md5("@SPF@." . $entry['record'])]); } } if (Settings::Get('dmarc.use_dmarc') == '1' && $entry['type'] == 'TXT' && ($entry['record'] == '_dmarc' || substr($entry['record'], 0, 7) == '_dmarc.') && (strtolower(substr($entry['content'], 0, 9)) == '"v=dmarc1' || strtolower(substr($entry['content'], 0, 8)) == 'v=dmarc1') ) { // unset special dmarc required-entry if ($entry['record'] == '_dmarc') { unset($required_entries[$entry['type']][md5("@DMARC@")]); } else { // subdomain unset($required_entries[$entry['type']][md5("@DMARC@" . substr($entry['record'], 6))]); } } if (empty($primary_ns) && $entry['record'] == '@' && $entry['type'] == 'NS') { // use the first NS entry pertaining to the current domain as primary ns $primary_ns = $entry['content']; } // check for CNAME on @, www- or wildcard-Alias and remove A/AAAA record accordingly foreach (['@', 'www', '*'] 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 (['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'] ?? 0, $entry['ttl']); } // add missing required entries if (!empty($required_entries)) { // A / AAAA records if (array_key_exists("A", $required_entries) || array_key_exists("AAAA", $required_entries)) { if ($froxlorhostname) { // use all available IP's for the froxlor-hostname $result_ip_stmt = Database::prepare(" SELECT `ip` FROM `" . TABLE_PANEL_IPSANDPORTS . "` GROUP BY `ip` "); Database::pexecute($result_ip_stmt); } else { $result_ip_stmt = Database::prepare(" SELECT `p`.`ip` AS `ip` FROM `" . TABLE_PANEL_IPSANDPORTS . "` `p`, `" . TABLE_DOMAINTOIP . "` `di` WHERE `di`.`id_domain` = :domainid AND `p`.`id` = `di`.`id_ipandports` GROUP BY `p`.`ip`; "); Database::pexecute($result_ip_stmt, [ 'domainid' => $domain_id ]); } $all_ips = $result_ip_stmt->fetchAll(PDO::FETCH_ASSOC); foreach ($all_ips as $ip) { foreach ($required_entries as $type => $records) { foreach ($records as $record) { if ($type == 'A' && filter_var($ip['ip'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false) { $zonerecords[] = new DnsEntry($record, 'A', $ip['ip']); } elseif ($type == 'AAAA' && filter_var($ip['ip'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) !== false) { $zonerecords[] = new DnsEntry($record, 'AAAA', $ip['ip']); } } } } unset($required_entries['A']); unset($required_entries['AAAA']); } // NS records if (array_key_exists("NS", $required_entries)) { if (Settings::Get('system.nameservers') != '') { $nameservers = explode(',', Settings::Get('system.nameservers')); foreach ($nameservers as $nameserver) { $nameserver = trim($nameserver); // append dot to hostname if (substr($nameserver, -1, 1) != '.') { $nameserver .= '.'; } foreach ($required_entries as $type => $records) { if ($type == 'NS') { foreach ($records as $record) { if (empty($primary_ns)) { // use the first NS entry as primary ns $primary_ns = $nameserver; } $zonerecords[] = new DnsEntry($record, 'NS', $nameserver); } } } } unset($required_entries['NS']); } } // MX records if (array_key_exists("MX", $required_entries)) { if (Settings::Get('system.mxservers') != '') { $mxservers = explode(',', Settings::Get('system.mxservers')); foreach ($mxservers as $mxserver) { $mxserver = trim($mxserver); if (substr($mxserver, -1, 1) != '.') { $mxserver .= '.'; } // split in prio and server $mx_details = explode(" ", $mxserver); if (count($mx_details) == 1) { $mx_details[1] = $mx_details[0]; $mx_details[0] = 10; } foreach ($required_entries as $type => $records) { if ($type == 'MX') { foreach ($records as $record) { $zonerecords[] = new DnsEntry($record, 'MX', $mx_details[1], $mx_details[0]); } } } } unset($required_entries['MX']); } } // TXT (SPF and DKIM) if (array_key_exists("TXT", $required_entries)) { $dkim_entries = self::generateDkimEntries($domain); foreach ($required_entries as $type => $records) { if ($type == 'TXT') { foreach ($records as $record) { if ($record == '@SPF@') { // spf for main-domain $txt_content = Settings::Get('spf.spf_entry'); $zonerecords[] = new DnsEntry('@', 'TXT', self::encloseTXTContent($txt_content)); } elseif (strlen($record) > 6 && substr($record, 0, 6) == '@SPF@.') { // spf for subdomain $txt_content = Settings::Get('spf.spf_entry'); $sub_record = substr($record, 6); $zonerecords[] = new DnsEntry($sub_record, 'TXT', self::encloseTXTContent($txt_content)); } elseif ($record == '@DMARC@') { // dmarc for main-domain $txt_content = Settings::Get('dmarc.dmarc_entry'); $zonerecords[] = new DnsEntry('_dmarc', 'TXT', self::encloseTXTContent($txt_content)); } elseif (strlen($record) > 8 && substr($record, 0, 8) == '@DMARC@.') { // dmarc for subdomain $txt_content = Settings::Get('dmarc.dmarc_entry'); $sub_record = substr($record, 8); $zonerecords[] = new DnsEntry('_dmarc.' . $sub_record, 'TXT', self::encloseTXTContent($txt_content)); } elseif (!empty($dkim_entries)) { // DKIM entries $dkim_record = 'dkim' . $domain['dkim_id'] . '._domainkey'; if ($record == $dkim_record) { // dkim for main-domain // check for multiline entry $multiline = false; if (substr($dkim_entries[0], 0, 1) == '(') { $multiline = true; } $zonerecords[] = new DnsEntry($record, 'TXT', self::encloseTXTContent($dkim_entries[0], $multiline)); } elseif (strlen($record) > strlen($dkim_record) && substr($record, 0, strlen($dkim_record) + 1) == $dkim_record . '.') { // dkim for subdomain-domain // check for multiline entry $multiline = false; if (substr($dkim_entries[0], 0, 1) == '(') { $multiline = true; } $zonerecords[] = new DnsEntry($record, 'TXT', self::encloseTXTContent($dkim_entries[0], $multiline)); } } } } } } // CAA if (array_key_exists("CAA", $required_entries)) { foreach ($required_entries as $type => $records) { if ($type == 'CAA') { foreach ($records as $record) { if ($record == '@CAA@') { $caa_entries = explode(PHP_EOL, Settings::Get('caa.caa_entry')); $caa_domain = "letsencrypt.org"; if (Settings::Get('system.letsencryptca') == 'buypass' || Settings::Get('system.letsencryptca') == 'buypass_test') { $caa_domain = "buypass.com"; } if ($domain['letsencrypt'] == 1) { if (Settings::Get('system.letsencryptca') == 'zerossl') { $caa_domains = [ "sectigo.com", "trust-provider.com", "usertrust.com", "comodoca.com", "comodo.com" ]; foreach ($caa_domains as $caa_domain) { $le_entry = $domain['iswildcarddomain'] == '1' ? '0 issuewild "' . $caa_domain . '"' : '0 issue "' . $caa_domain . '"'; array_push($caa_entries, $le_entry); } } else { $le_entry = $domain['iswildcarddomain'] == '1' ? '0 issuewild "' . $caa_domain . '"' : '0 issue "' . $caa_domain . '"'; array_push($caa_entries, $le_entry); } } foreach ($caa_entries as $entry) { if (empty($entry)) { continue; } $zonerecords[] = new DnsEntry('@', 'CAA', $entry); // additional required records by subdomain setting if ($domain['wwwserveralias'] == '1') { $zonerecords[] = new DnsEntry('www', 'CAA', $entry); } } } } } } } } if (empty($primary_ns)) { // TODO log error: no NS given, use system-hostname $primary_ns = Settings::Get('system.hostname'); } if (!$isMainButSubTo) { $date = date('Ymd'); $domain['bindserial'] = (preg_match('/^' . $date . '/', $domain['bindserial']) ? $domain['bindserial'] + 1 : $date . '00'); if (!$froxlorhostname) { $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_DOMAINS . "` SET `bindserial` = :serial WHERE `id` = :id "); Database::pexecute($upd_stmt, [ 'serial' => $domain['bindserial'], 'id' => $domain['id'] ]); } // PowerDNS does not like multi-line-format $soa_email = Settings::Get('system.soaemail'); if ($soa_email == "") { $soa_email = Settings::Get('panel.adminmail'); } $soa_content = $primary_ns . " " . self::escapeSoaAdminMail($soa_email) . " "; $soa_content .= $domain['bindserial'] . " "; // TODO for now, dummy time-periods $soa_content .= "3600 900 1209600 1200"; $soa_record = new DnsEntry('@', 'SOA', $soa_content); array_unshift($zonerecords, $soa_record); } $zone = new DnsZone( (int)Settings::Get('system.defaultttl'), $domain['domain'], $domain['bindserial'], $zonerecords ); return $zone; } /** * @param string $record * @param string $type * @param array $required * @return void */ private static function addRequiredEntry(string $record = '@', string $type = 'A', array &$required = []) { if (!isset($required[$type])) { $required[$type] = []; } $required[$type][md5($record)] = $record; } /** * @param array $domain * @return array */ private static function generateDkimEntries(array $domain): array { $zone_dkim = []; if (Settings::Get('antispam.activated') == '1' && $domain['dkim'] == '1' && $domain['dkim_pubkey'] != '') { // start $dkim_txt = 'v=DKIM1;'; // key $dkim_txt .= 'k=rsa;p=' . trim($domain['dkim_pubkey']) . ';'; // dkim-entry $zone_dkim[] = $dkim_txt; } return $zone_dkim; } /** * @param string $txt_content * @param bool $isMultiLine * @return string */ public static function encloseTXTContent(string $txt_content, bool $isMultiLine = false): string { // check that TXT content is enclosed in " " if (!$isMultiLine && Settings::Get('system.dns_server') != 'PowerDNS') { if (substr($txt_content, 0, 1) != '"') { $txt_content = '"' . $txt_content; } if (substr($txt_content, -1) != '"') { $txt_content .= '"'; } } if (Settings::Get('system.dns_server') == 'PowerDNS') { // no quotation for PowerDNS if (substr($txt_content, 0, 1) == '"') { $txt_content = substr($txt_content, 1); } if (substr($txt_content, -1) == '"') { $txt_content = substr($txt_content, 0, -1); } } return $txt_content; } /** * @param string $email * @return string */ private static function escapeSoaAdminMail(string $email): string { $mail_parts = explode("@", $email); return str_replace(".", "\.", $mail_parts[0]) . "." . $mail_parts[1] . "."; } } ================================================ FILE: lib/Froxlor/Dns/DnsEntry.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Dns; use Froxlor\Settings; class DnsEntry { public string $record; public int $ttl; public string $class = 'IN'; public string $type; public int $priority; public ?string $content; /** * @param string $record * @param string $type * @param string|null $content * @param int $prio * @param int $ttl * @param string $class */ public function __construct(string $record = '', string $type = 'A', string $content = null, int $prio = 0, int $ttl = 0, string $class = 'IN') { $this->record = $record; $this->type = $type; $this->content = $content; $this->priority = $prio; $this->ttl = ($ttl <= 0 ? Settings::Get('system.defaultttl') : $ttl); $this->class = $class; } public function __toString() { $_content = $this->content; // check content length for txt records for bind9 (multiline) if (Settings::Get('system.dns_server') != 'pdns' && $this->type == 'TXT' && strlen($_content) >= 255) { // split string $_contentlines = str_split($_content, 254); // first line $_l = array_shift($_contentlines); // check for starting quote if (substr($_l, 0, 1) == '"') { $_l = substr($_l, 1); } $_content = '("' . $_l . '"' . PHP_EOL; $_l = array_pop($_contentlines); // check for ending quote if (substr($_l, -1) == '"') { $_l = substr($_l, 0, -1); } foreach ($_contentlines as $_cl) { // lines in between $_content .= "\t\t\t\t" . '"' . $_cl . '"' . PHP_EOL; } // last line $_content .= "\t\t\t\t" . '"' . $_l . '")'; } return $this->record . "\t" . $this->ttl . "\t" . $this->class . "\t" . $this->type . "\t" . (($this->priority >= 0 && ($this->type == 'MX' || $this->type == 'SRV')) ? $this->priority . "\t" : "") . $_content . PHP_EOL; } } ================================================ FILE: lib/Froxlor/Dns/DnsZone.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Dns; use Froxlor\Settings; class DnsZone { public int $ttl; public string $origin; public string $serial; public ?array $records; /** * @param int $ttl * @param string $origin * @param string $serial * @param array|null $records */ public function __construct(int $ttl = 0, string $origin = '', string $serial = '', array $records = null) { $this->ttl = ($ttl <= 0 ? Settings::Get('system.defaultttl') : $ttl); $this->origin = $origin; $this->serial = $serial; $this->records = $records; } public function __toString() { $zone_file = "\$TTL " . $this->ttl . PHP_EOL; $zone_file .= "\$ORIGIN " . $this->origin . "." . PHP_EOL; if (!empty($this->records)) { foreach ($this->records as $record) { $zone_file .= (string)$record; } } return $zone_file; } } ================================================ FILE: lib/Froxlor/Dns/PowerDNS.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Dns; use Froxlor\FileDir; use Froxlor\Settings; use PDO; use PDOException; class PowerDNS { private static $pdns_db = null; /** * remove all records and entries of a given domain * * @param string|null $domain */ public static function cleanDomainZone(string $domain = null) { if (!empty($domain)) { $pdns_domains_stmt = self::getDB()->prepare("SELECT `id`, `name` FROM `domains` WHERE `name` = :domain"); $del_rec_stmt = self::getDB()->prepare("DELETE FROM `records` WHERE `domain_id` = :did"); $del_meta_stmt = self::getDB()->prepare("DELETE FROM `domainmetadata` WHERE `domain_id` = :did"); $del_dom_stmt = self::getDB()->prepare("DELETE FROM `domains` WHERE `id` = :did"); $pdns_domains_stmt->execute([ 'domain' => $domain ]); $pdns_domain = $pdns_domains_stmt->fetch(PDO::FETCH_ASSOC); $del_rec_stmt->execute([ 'did' => $pdns_domain['id'] ]); $del_meta_stmt->execute([ 'did' => $pdns_domain['id'] ]); $del_dom_stmt->execute([ 'did' => $pdns_domain['id'] ]); } } /** * get pdo database connection to powerdns database * * @return \PDO */ public static function getDB(): \PDO { if (!isset(self::$pdns_db) || !(self::$pdns_db instanceof PDO)) { self::connectToPdnsDb(); } return self::$pdns_db; } /** * @return void */ private static function connectToPdnsDb() { // get froxlor pdns config $cf = Settings::Get('system.bindconf_directory') . '/froxlor/pdns_froxlor.conf'; $config = FileDir::makeCorrectFile($cf); if (!file_exists($config)) { die('PowerDNS configuration file (' . $config . ') not found. Did you go through the configuration templates?' . PHP_EOL); } $lines = file($config); $mysql_data = []; foreach ($lines as $line) { $line = trim($line); if (strtolower(substr($line, 0, 6)) == 'gmysql') { $namevalue = explode("=", $line); $mysql_data[$namevalue[0]] = $namevalue[1]; } } // build up connection string $driver = 'mysql'; $dsn = $driver . ":"; $options = [ 'PDO::MYSQL_ATTR_INIT_COMMAND' => 'SET names utf8' ]; $attributes = [ 'ATTR_ERRMODE' => 'ERRMODE_EXCEPTION' ]; $dbconf = []; $dbconf["dsn"] = [ 'dbname' => $mysql_data["gmysql-dbname"], 'charset' => 'utf8' ]; if (isset($mysql_data['gmysql-socket']) && !empty($mysql_data['gmysql-socket'])) { $dbconf["dsn"]['unix_socket'] = FileDir::makeCorrectFile($mysql_data['gmysql-socket']); } else { $dbconf["dsn"]['host'] = $mysql_data['gmysql-host']; $dbconf["dsn"]['port'] = $mysql_data['gmysql-port']; if (!empty($mysql_data['gmysql-ssl-ca-file'])) { $options[PDO::MYSQL_ATTR_SSL_CA] = $mysql_data['gmysql-ssl-ca-file']; $options[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = (bool)$mysql_data['gmysql-ssl-verify-server-certificate']; } } // add options to dsn-string foreach ($dbconf["dsn"] as $k => $v) { $dsn .= $k . "=" . $v . ";"; } // clean up unset($dbconf); // try to connect try { self::$pdns_db = new PDO($dsn, $mysql_data['gmysql-user'], $mysql_data['gmysql-password'], $options); } catch (PDOException $e) { die($e->getMessage()); } // set attributes foreach ($attributes as $k => $v) { self::$pdns_db->setAttribute(constant("PDO::" . $k), constant("PDO::" . $v)); } $version_server = self::$pdns_db->getAttribute(PDO::ATTR_SERVER_VERSION); $sql_mode = 'NO_ENGINE_SUBSTITUTION'; if (version_compare($version_server, '8.0.11', '<')) { $sql_mode .= ',NO_AUTO_CREATE_USER'; } self::$pdns_db->exec('SET sql_mode = "' . $sql_mode . '"'); } } ================================================ FILE: lib/Froxlor/Dns/index.html ================================================ ================================================ FILE: lib/Froxlor/Domain/Domain.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Domain; use Froxlor\Cron\Http\LetsEncrypt\AcmeSh; use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\FroxlorLogger; use Froxlor\Settings; use PDO; class Domain { /** * return all ip addresses associated with given domain, * returns all ips if domain-id = 0 (froxlor.vhost) * * @param int $domain_id * @return array * @throws \Exception */ public static function getIpsOfDomain(int $domain_id = 0): array { if ($domain_id > 0) { $sel_stmt = Database::prepare(" SELECT i.ip FROM `" . TABLE_PANEL_IPSANDPORTS . "` `i` LEFT JOIN `" . TABLE_DOMAINTOIP . "` `dip` ON dip.id_ipandports = i.id AND dip.id_domain = :domainid GROUP BY i.ip "); $sel_param = [ 'domainid' => $domain_id ]; } else { // assuming froxlor.vhost (id = 0) $sel_stmt = Database::prepare(" SELECT ip FROM `" . TABLE_PANEL_IPSANDPORTS . "` GROUP BY ip "); $sel_param = []; } Database::pexecute($sel_stmt, $sel_param); $result = []; while ($ip = $sel_stmt->fetch(PDO::FETCH_ASSOC)) { $result[] = $ip['ip']; } return $result; } /** * return an array of all enabled redirect-codes * * @return array array of enabled redirect-codes */ public static function getRedirectCodesArray(): array { $sql = "SELECT * FROM `" . TABLE_PANEL_REDIRECTCODES . "` WHERE `enabled` = '1' ORDER BY `id` ASC"; $result_stmt = Database::query($sql); $codes = []; while ($rc = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $codes[] = $rc; } return $codes; } /** * returns the redirect-code for a given * domain-id * * @param int $domainid id of the domain * * @return string redirect-code * @throws \Exception */ public static function getDomainRedirectCode(int $domainid = 0): string { // get system default $default = '301'; if (Settings::Get('customredirect.enabled') == '1') { $all_codes = self::getRedirectCodes(false); $_default = $all_codes[Settings::Get('customredirect.default')]; $default = ($_default == '---') ? $default : $_default; } $code = $default; if ($domainid > 0) { $result_stmt = Database::prepare(" SELECT `r`.`code` as `redirect` FROM `" . TABLE_PANEL_REDIRECTCODES . "` `r`, `" . TABLE_PANEL_DOMAINREDIRECTS . "` `rc` WHERE `r`.`id` = `rc`.`rid` and `rc`.`did` = :domainid "); $result = Database::pexecute_first($result_stmt, [ 'domainid' => $domainid ]); if (is_array($result) && isset($result['redirect'])) { $code = ($result['redirect'] == '---') ? $default : $result['redirect']; } } return $code; } /** * return an array of all enabled redirect-codes * for the settings form * * @param bool $add_desc optional, default true, add the code-description * * @return array array of enabled redirect-codes */ public static function getRedirectCodes(bool $add_desc = true): array { $sql = "SELECT * FROM `" . TABLE_PANEL_REDIRECTCODES . "` WHERE `enabled` = '1' ORDER BY `id` ASC"; $result_stmt = Database::query($sql); $codes = []; while ($rc = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $codes[$rc['id']] = $rc['code']; if ($add_desc) { $codes[$rc['id']] .= ' (' . lng('redirect_desc.' . $rc['desc']) . ')'; } } return $codes; } /** * returns the redirect-id for a given * domain-id * * @param int $domainid id of the domain * * @return int redirect-code-id * @throws \Exception */ public static function getDomainRedirectId(int $domainid = 0): int { $code = 1; if ($domainid > 0) { $result_stmt = Database::prepare(" SELECT `r`.`id` as `redirect` FROM `" . TABLE_PANEL_REDIRECTCODES . "` `r`, `" . TABLE_PANEL_DOMAINREDIRECTS . "` `rc` WHERE `r`.`id` = `rc`.`rid` and `rc`.`did` = :domainid "); $result = Database::pexecute_first($result_stmt, [ 'domainid' => $domainid ]); if ($result && isset($result['redirect'])) { $code = (int)$result['redirect']; } } return $code; } /** * adds a redirect-code for a domain * * @param int $domainid id of the domain to add the code for * @param int $redirect selected redirect-id * * @return null * @throws \Exception */ public static function addRedirectToDomain(int $domainid = 0, int $redirect = 1) { if ($domainid > 0) { $ins_stmt = Database::prepare(" INSERT INTO `" . TABLE_PANEL_DOMAINREDIRECTS . "` SET `rid` = :rid, `did` = :did "); Database::pexecute($ins_stmt, [ 'rid' => $redirect, 'did' => $domainid ]); } } /** * updates the redirect-code of a domain * if redirect-code is false, nothing happens * * @param int $domainid id of the domain to update * @param int $redirect selected redirect-id * * @return null * @throws \Exception */ public static function updateRedirectOfDomain(int $domainid = 0, int $redirect = 0) { if (!$redirect) { return; } if ($domainid > 0) { $del_stmt = Database::prepare(" DELETE FROM `" . TABLE_PANEL_DOMAINREDIRECTS . "` WHERE `did` = :domainid "); Database::pexecute($del_stmt, [ 'domainid' => $domainid ]); $ins_stmt = Database::prepare(" INSERT INTO `" . TABLE_PANEL_DOMAINREDIRECTS . "` SET `rid` = :rid, `did` = :did "); Database::pexecute($ins_stmt, [ 'rid' => $redirect, 'did' => $domainid ]); } } /** * get ids of domains that are main domains but a subdomain of another main domain (for DNS) * * @param int $id main-domain to check * * @return array * @throws \Exception */ public static function getMainSubdomainIds(int $id): array { $result_stmt = Database::prepare(" SELECT id FROM `" . TABLE_PANEL_DOMAINS . "` WHERE isbinddomain = 1 AND domain LIKE CONCAT('%.', ( SELECT d.domain FROM `" . TABLE_PANEL_DOMAINS . "` AS d WHERE d.id = :id )) "); Database::pexecute($result_stmt, [ 'id' => $id ]); $result = []; while ($entry = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $result[] = $entry['id']; } return $result; } /** * Check whether a given domain has an ssl-ip/port assigned * * @param int $domainid * * @return bool * @throws \Exception */ public static function domainHasSslIpPort(int $domainid): bool { $result_stmt = Database::prepare(" SELECT `dt`.* FROM `" . TABLE_DOMAINTOIP . "` `dt`, `" . TABLE_PANEL_IPSANDPORTS . "` `iap` WHERE `dt`.`id_ipandports` = `iap`.`id` AND `iap`.`ssl` = '1' AND `dt`.`id_domain` = :domainid;"); Database::pexecute($result_stmt, [ 'domainid' => $domainid ]); $result = $result_stmt->fetch(PDO::FETCH_ASSOC); if ($result && isset($result['id_ipandports'])) { return true; } return false; } /** * returns true or false whether a given domain id * is the std-subdomain of a customer * * @param int $did domain-id * * @return bool * @throws \Exception */ public static function isCustomerStdSubdomain(int $did): bool { if ($did > 0) { $result_stmt = Database::prepare(" SELECT `customerid` FROM `" . TABLE_PANEL_CUSTOMERS . "` WHERE `standardsubdomain` = :did "); $result = Database::pexecute_first($result_stmt, [ 'did' => $did ]); if ($result && isset($result['customerid'])) { return $result['customerid'] > 0; } } return false; } /** * @param int $aliasDestinationDomainID * @param FroxlorLogger $log * * @return void * @throws \Exception */ public static function triggerLetsEncryptCSRForAliasDestinationDomain( int $aliasDestinationDomainID, FroxlorLogger $log ) { if ($aliasDestinationDomainID > 0) { $log->logAction( FroxlorLogger::ADM_ACTION, LOG_INFO, "LetsEncrypt CSR triggered for domain ID " . $aliasDestinationDomainID ); $upd_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` SET `validtodate` = null WHERE domainid = :domainid "); Database::pexecute($upd_stmt, [ 'domainid' => $aliasDestinationDomainID ]); } } /** * @param string $domainname * @return true */ public static function doLetsEncryptCleanUp(string $domainname): bool { // @ see \Froxlor\Cron\Http\LetsEncrypt\AcmeSh.php $acmesh = AcmeSh::getAcmeSh(); if (file_exists($acmesh)) { $certificate_folder = AcmeSh::getWorkingDirFromEnv($domainname); $certificate_ecc_folder = AcmeSh::getWorkingDirFromEnv($domainname, true); if (file_exists($certificate_folder) || file_exists($certificate_ecc_folder)) { $params = " --remove -d " . $domainname; if (file_exists($certificate_ecc_folder)) { $params .= " --ecc"; } // run remove command FileDir::safe_exec($acmesh . $params); // remove certificates directory if (file_exists($certificate_folder)) { FileDir::safe_exec('rm -rf ' . $certificate_folder); } elseif (file_exists($certificate_ecc_folder)) { FileDir::safe_exec('rm -rf ' . $certificate_ecc_folder); } } } return true; } /** * checks give path for security issues * and returns a string that can be appended * to a line for an open_basedir directive * * @param string $path the path to check and append * @param bool $first if true, no ':' will be prefixed to the path * * @return string * @throws \Exception */ public static function appendOpenBasedirPath(string $path = '', bool $first = false): string { if ($path != '' && $path != '/' && (!preg_match("#^/dev#i", $path) || preg_match("#^/dev/urandom#i", $path)) && !preg_match("#^/proc#i", $path) && !preg_match("#^/etc#i", $path) && !preg_match("#^/sys#i", $path) && !preg_match("#:#", $path)) { if (preg_match("#^/dev/urandom#i", $path)) { $path = FileDir::makeCorrectFile($path); } else { $path = FileDir::makeCorrectDir($path); } // check for php-version that requires the trailing // slash to be removed as it does not allow the usage // of the sub-folders within the given folder, fixes #797 if ((PHP_MINOR_VERSION == 2 && PHP_VERSION_ID >= 50216) || PHP_VERSION_ID >= 50304) { // check trailing slash if (substr($path, -1, 1) == '/') { // remove it $path = substr($path, 0, -1); } } if ($first) { return $path; } return ':' . $path; } return ''; } } ================================================ FILE: lib/Froxlor/Domain/IpAddr.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Domain; use Froxlor\Database\Database; use PDO; class IpAddr { /** * @return array */ public static function getIpAddresses(): array { $result_stmt = Database::query(" SELECT `id`, `ip`, `port` FROM `" . TABLE_PANEL_IPSANDPORTS . "` ORDER BY `ip` ASC, `port` ASC "); $system_ipaddress_array = []; while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { if (!isset($system_ipaddress_array[$row['ip']]) && !in_array($row['ip'], $system_ipaddress_array)) { $system_ipaddress_array[$row['ip']] = $row['ip']; } } return $system_ipaddress_array; } /** * @return array * @throws \Exception */ public static function getSslIpPortCombinations(): array { return [ '' => lng('panel.none_value') ] + self::getIpPortCombinations(true); } /** * @param bool $ssl * @return array * @throws \Exception */ public static function getIpPortCombinations(bool $ssl = false): array { global $userinfo; $additional_conditions_params = []; $additional_conditions_array = []; if (!empty($userinfo) && $userinfo['ip'] != '-1') { $admin_ip_stmt = Database::prepare(" SELECT `id`, `ip`, `port` FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE `id` = IN (:ipid) "); $myips = implode(",", json_decode($userinfo['ip'], true)); Database::pexecute($admin_ip_stmt, [ 'ipid' => $myips ]); $additional_conditions_array[] = "`ip` IN (:adminips)"; $additional_conditions_params['adminips'] = $myips; } if ($ssl !== null) { $additional_conditions_array[] = "`ssl` = :ssl"; $additional_conditions_params['ssl'] = ($ssl === true ? '1' : '0'); } $additional_conditions = ''; if (count($additional_conditions_array) > 0) { $additional_conditions = " WHERE " . implode(" AND ", $additional_conditions_array) . " "; } $result_stmt = Database::prepare(" SELECT `id`, `ip`, `port` FROM `" . TABLE_PANEL_IPSANDPORTS . "` " . $additional_conditions . " ORDER BY `ip` ASC, `port` ASC "); Database::pexecute($result_stmt, $additional_conditions_params); $system_ipaddress_array = []; while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { if (filter_var($row['ip'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $row['ip'] = '[' . $row['ip'] . ']'; } $system_ipaddress_array[$row['id']] = $row['ip'] . ':' . $row['port']; } return $system_ipaddress_array; } } ================================================ FILE: lib/Froxlor/Domain/index.html ================================================ ================================================ FILE: lib/Froxlor/ErrorBag.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor; use Exception; /** * Class to manage the current user / session */ class ErrorBag { /** * returns whether there are errors stored * * @return bool */ public static function hasErrors(): bool { return !empty($_SESSION) && !empty($_SESSION['_errors']); } /** * add error * * @param string $data * * @return void */ public static function addError(string $data): void { if (!isset($_SESSION['_errors']) || !is_array($_SESSION['_errors'])) { $_SESSION['_errors'] = []; } $_SESSION['_errors'][] = $data; } /** * Return errors and clear session * * @return array * @throws Exception */ public static function getErrors(): array { $errors = $_SESSION['_errors'] ?? []; unset($_SESSION['_errors']); if (Settings::Config('display_php_errors')) { return $errors; } return []; } } ================================================ FILE: lib/Froxlor/FileDir.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor; use Exception; use Froxlor\Customer\Customer; use Froxlor\Database\Database; use PDO; use RecursiveCallbackFilterIterator; class FileDir { /** * Creates a directory below a users homedir and sets all directories, * which had to be created below with correct Owner/Group * (Copied from cron_tasks.php:rev1189 as we'll need this more often in future) * * @param string $homeDir The homedir of the user * @param string $dirToCreate The dir which should be created * @param int $uid The uid of the user * @param int $gid The gid of the user * @param bool $placeindex Place standard-index.html into the new folder * @param bool $allow_notwithinhomedir Allow creating a directory out of the customers docroot * * @return bool true if everything went okay, false if something went wrong * @throws Exception */ public static function mkDirWithCorrectOwnership( string $homeDir, string $dirToCreate, int $uid, int $gid, bool $placeindex = false, bool $allow_notwithinhomedir = false ): bool { if ($homeDir != '' && $dirToCreate != '') { $homeDir = self::makeCorrectDir($homeDir); $dirToCreate = self::makeCorrectDir($dirToCreate); if (substr($dirToCreate, 0, strlen($homeDir)) == $homeDir) { $subdir = substr($dirToCreate, strlen($homeDir) - 1); $within_homedir = true; } else { $subdir = $dirToCreate; $within_homedir = false; } $subdir = self::makeCorrectDir($subdir); $subdirs = []; if ($within_homedir || !$allow_notwithinhomedir) { $subdirlen = strlen($subdir); $offset = 0; while ($offset < $subdirlen) { $offset = strpos($subdir, '/', $offset); $subdirelem = substr($subdir, 0, $offset); $offset++; array_push($subdirs, self::makeCorrectDir($homeDir . $subdirelem)); } } else { array_push($subdirs, $dirToCreate); } $subdirs = array_unique($subdirs); sort($subdirs); foreach ($subdirs as $sdir) { if (!is_dir($sdir)) { $sdir = self::makeCorrectDir($sdir); self::safe_exec('mkdir -p ' . escapeshellarg($sdir)); // place index if ($placeindex) { $loginname = Customer::getLoginNameByUid($uid); if ($loginname !== false) { self::storeDefaultIndex($loginname, $sdir, null); } } self::safe_exec('chown -R ' . (int)$uid . ':' . (int)$gid . ' ' . escapeshellarg($sdir)); } } return true; } return false; } /** * Returns a correct/secure dirname, means to add slashes at the beginning and at the end if there weren't * some. If $fixes_homedir is specified, * * * @param string $dir the path to correct * * @return string the corrected path * @throws Exception */ public static function makeCorrectDir(string $dir, string $fixed_homedir = ""): string { if (strlen($dir) > 0) { $dir = trim($dir); if (substr($dir, -1, 1) != '/') { $dir .= '/'; } if (substr($dir, 0, 1) != '/') { $dir = '/' . $dir; } // if given, check that the target path is within the $fixed_homedir // by checking each folder for being a symlink and whether it targets // the customers homedir or points outside of it if (!empty($fixed_homedir)) { $to_check = explode("/", substr($dir, strlen($fixed_homedir) + 1), -1); $check_dir = substr($fixed_homedir, 0, -1); // Symlink check foreach ($to_check as $sub_dir) { $check_dir .= '/' . $sub_dir; if (is_link($check_dir)) { $original_target = $check_dir; $check_dir = readlink($check_dir); $link_dir = dirname($original_target); // check whether the link is relative or absolute if (substr($check_dir, 0, 1) != '/') { // relative directory, prepend link_dir $check_dir = $link_dir . '/' . $check_dir; } if (substr($check_dir, 0, strlen($fixed_homedir)) != $fixed_homedir) { throw new Exception("Found symlink pointing outside of customer home directory: " . substr($original_target, strlen($fixed_homedir))); } } } // check for the path to be within the given homedir if (substr($dir, 0, strlen($fixed_homedir)) != $fixed_homedir) { throw new Exception("Target path not within the required customer home directory"); } } return self::makeSecurePath($dir); } throw new Exception("Cannot validate directory in " . __FUNCTION__ . " which is very dangerous."); } /** * Function which returns a secure path, means to remove all multiple dots and slashes * * @param string $path the path to secure * * @return string the corrected path */ public static function makeSecurePath(string $path): string { // check for bad characters, some are allowed with escaping, // but we generally don't want them in our directory-names, // thx to aaronmueller for this snippet $badchars = [ ':', ';', '|', '&', '>', '<', '`', '$', '~', '?', "\0", "\n", "\r", "\t", "\f" ]; foreach ($badchars as $bc) { $path = str_replace($bc, "", $path); } $search = [ '#/+#', '#\.+#' ]; $replace = [ '/', '.' ]; $path = preg_replace($search, $replace, $path); // don't just replace a space with an escaped space // it might be escaped already $path = str_replace("\ ", " ", $path); $path = str_replace(" ", "\ ", $path); return $path; } /** * Wrapper around the exec command. * * @param string $exec_string command to be executed * @param mixed $return_value referenced variable where the output is stored * @param ?array $allowedChars optional array of allowed characters in path/command * * @return array result of exec() */ public static function safe_exec(string $exec_string, &$return_value = false, $allowedChars = null) { $disallowed = [ ';', '|', '&', '>', '<', '`', '$', '~', '?' ]; $acheck = false; if ($allowedChars != null && is_array($allowedChars) && count($allowedChars) > 0) { $acheck = true; } foreach ($disallowed as $dc) { if ($acheck && in_array($dc, $allowedChars)) { continue; } // check for bad signs in execute command if (stristr($exec_string, $dc)) { die("SECURITY CHECK FAILED!\nThe execute string '" . $exec_string . "' is a possible security risk!\nPlease check your whole server for security problems by hand!\n"); } } // execute the command and return output $return = []; // ------------------------------------------------------------------------------- if ($return_value == false) { exec($exec_string, $return); } else { exec($exec_string, $return, $return_value); } return $return; } /** * Read unconfigured-domain template from database if exists or fallback to default * * @param string $servername * * @return string * @throws Exception */ public static function getUnknownDomainTemplate(string $servername = "") { $result_stmt = Database::prepare(" SELECT * FROM `" . TABLE_PANEL_TEMPLATES . "` WHERE `templategroup` = 'files' AND `varname` = 'unconfigured_html' "); Database::pexecute($result_stmt); if (Database::num_rows() > 0) { $template = $result_stmt->fetch(PDO::FETCH_ASSOC); $replace_arr = [ 'SERVERNAME' => $servername, ]; $tpl_content = PhpHelper::replaceVariables($template['value'], $replace_arr); $tpl_ext = $template['file_extension']; } else { $tpl_ext = 'html'; $unconfiguredPath = FileDir::makeCorrectFile(Froxlor::getInstallDir() . '/templates/misc/unconfigured/index.html'); if (file_exists($unconfiguredPath)) { $tpl_content = file_get_contents($unconfiguredPath); } else { $tpl_content = lng('admin.templates.unconfigured_content_fallback'); } } $redirect_file = FileDir::makeCorrectFile(Froxlor::getInstallDir() . '/notice.' . $tpl_ext); file_put_contents($redirect_file, $tpl_content); return basename($redirect_file); } /** * store the default index-file in a given destination folder * * @param string $loginname customers loginname * @param string $destination path where to create the file * @param object $logger FroxlorLogger object * @param bool $force force creation whatever the settings say (needed for task #2, create new user) * * @return void * @throws Exception */ public static function storeDefaultIndex( string $loginname, string $destination, $logger = null, bool $force = false ) { if ($force || (int)Settings::Get('system.store_index_file_subs') == 1) { $result_stmt = Database::prepare(" SELECT `t`.`value`, `t`.`file_extension`, `c`.`email` AS `customer_email`, `a`.`email` AS `admin_email`, `c`.`loginname` AS `customer_login`, `a`.`loginname` AS `admin_login` FROM `" . TABLE_PANEL_CUSTOMERS . "` AS `c` INNER JOIN `" . TABLE_PANEL_ADMINS . "` AS `a` ON `c`.`adminid` = `a`.`adminid` INNER JOIN `" . TABLE_PANEL_TEMPLATES . "` AS `t` ON `a`.`adminid` = `t`.`adminid` WHERE `varname` = 'index_html' AND `c`.`loginname` = :loginname"); Database::pexecute($result_stmt, [ 'loginname' => $loginname ]); if (Database::num_rows() > 0) { $template = $result_stmt->fetch(PDO::FETCH_ASSOC); $replace_arr = [ 'SERVERNAME' => Settings::Get('system.hostname'), 'CUSTOMER' => $template['customer_login'], 'ADMIN' => $template['admin_login'], 'CUSTOMER_EMAIL' => $template['customer_email'], 'ADMIN_EMAIL' => $template['admin_email'] ]; // replaceVariables $htmlcontent = PhpHelper::replaceVariables($template['value'], $replace_arr); $indexhtmlpath = self::makeCorrectFile($destination . '/index.' . $template['file_extension']); $index_html_handler = fopen($indexhtmlpath, 'w'); fwrite($index_html_handler, $htmlcontent); fclose($index_html_handler); if ($logger !== null) { $logger->logAction( FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Creating \'index.' . $template['file_extension'] . '\' for Customer \'' . $template['customer_login'] . '\' based on template in directory ' . escapeshellarg($indexhtmlpath) ); } } else { $destination = self::makeCorrectDir($destination); if ($logger !== null) { $logger->logAction( FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Running: cp -a ' . Froxlor::getInstallDir() . '/templates/misc/standardcustomer/* ' . escapeshellarg($destination) ); } self::safe_exec('cp -a ' . Froxlor::getInstallDir() . '/templates/misc/standardcustomer/* ' . escapeshellarg($destination)); } } } /** * Function which returns a correct filename, means to add a slash at the beginning if there wasn't one * * @param string $filename the filename * @param string $fixed_homedir whether to check that the given file is within the fixed home-directory * * @return string the corrected filename * @throws Exception */ public static function makeCorrectFile(string $filename, string $fixed_homedir = ""): string { if (trim($filename) == '') { $error = 'Given filename for function ' . __FUNCTION__ . ' is empty.' . "\n"; $error .= 'This is very dangerous and should not happen.' . "\n"; $error .= 'Please inform the Froxlor team about this issue so they can fix it.'; echo $error; // so we can see WHERE this happened debug_print_backtrace(); die(); } if (substr($filename, 0, 1) != '/') { $filename = '/' . $filename; } $filename = FileDir::makeCorrectDir(dirname($filename)) . '/' . basename($filename); // if given, check that the target file is within the $fixed_homedir // by checking each folder and the file for being a symlink and whether it targets // the customers homedir or points outside of it if (!empty($fixed_homedir)) { $to_check = explode("/", substr($filename, strlen(self::makeCorrectDir($fixed_homedir))), -1); $check_dir = substr($fixed_homedir, -1) == '/' ? substr($fixed_homedir, 0, -1) : $fixed_homedir; // Symlink check foreach ($to_check as $sub_dir) { $check_dir .= '/' . $sub_dir; if (is_link($check_dir)) { $original_target = $check_dir; $check_dir = readlink($check_dir); $link_dir = dirname($original_target); // check whether the link is relative or absolute if (substr($check_dir, 0, 1) != '/') { // relative directory, prepend link_dir $check_dir = $link_dir . '/' . $check_dir; } if (substr($check_dir, 0, strlen($fixed_homedir)) != $fixed_homedir) { throw new Exception("Found symlink pointing outside of customer home directory: " . substr($original_target, strlen($fixed_homedir))); } } } // check for the path to be within the given homedir if (substr($filename, 0, strlen($fixed_homedir)) != $fixed_homedir) { throw new Exception("Target path/file not within the required customer home directory"); } // check whether file is symlink itself if (is_link($filename)) { $filename = readlink($filename); $check_dir = FileDir::makeCorrectDir(dirname($filename), $fixed_homedir); if (substr($check_dir, 0, strlen($fixed_homedir)) != $fixed_homedir) { throw new Exception("Found symlink pointing outside of customer home directory: " . substr($filename, strlen($fixed_homedir))); } } } return self::makeSecurePath($filename); } /** * checks a directory against disallowed paths which could * lead to a damaged system if you use them * * @param string|null $path * * @return bool * @throws Exception */ public static function checkDisallowedPaths(string $path): bool { /* * disallow base-directories and / */ $disallowed_values = [ "/", "/bin/", "/boot/", "/dev/", "/etc/", "/home/", "/lib/", "/lib32/", "/lib64/", "/opt/", "/proc/", "/root/", "/run/", "/sbin/", "/sys/", "/tmp/", "/usr/", "/var/" ]; $path = self::makeCorrectDir($path); // check if it's a disallowed path if (in_array($path, $disallowed_values)) { return false; } return true; } /** * Function which returns a correct destination for Postfix Virtual Table * * @param string $destination The destinations * * @return string the corrected destinations */ public static function makeCorrectDestination(string $destination): string { $search = '/ +/'; $replace = ' '; $destination = preg_replace($search, $replace, $destination); if (substr($destination, 0, 1) == ' ') { $destination = substr($destination, 1); } if (substr($destination, -1, 1) == ' ') { $destination = substr($destination, 0, strlen($destination) - 1); } return $destination; } /** * Returns a valid html tag for the chosen $fieldType for paths * * @param string $path The path to start searching in * @param int $uid The uid which must match the found directories * @param int $gid The gid which must match the found directories * @param string $value the value for the input-field * @param bool $dom * * @return array * * @throws Exception * @author Manuel Bernhardt * @author Martin Burchert */ public static function makePathfield(string $path, int $uid, int $gid, string $value = '', bool $dom = false): array { $value = str_replace($path, '', $value); $field = []; // path is given without starting slash // but dirList holds the paths with starting slash, // so we just add one here to get the correct // default path selected, #225 if (substr($value, 0, 1) != '/' && !$dom) { $value = '/' . $value; } $fieldType = strtolower(Settings::Get('panel.pathedit')); if ($fieldType == 'manual') { $field = [ 'type' => 'text', 'value' => htmlspecialchars($value) ]; } elseif ($fieldType == 'dropdown') { $dirList = self::findDirs($path, $uid, $gid); natcasesort($dirList); if (sizeof($dirList) > 0) { $_field = []; foreach ($dirList as $dir) { if (strpos($dir, $path) === 0) { $dir = substr($dir, strlen($path)); // docroot cut off of current directory == empty -> directory is the docroot if (empty($dir)) { $dir = '/'; } $dir = self::makeCorrectDir($dir); } $_field[$dir] = $dir; } $field = [ 'type' => 'select', 'select_var' => $_field, 'selected' => $value, 'value' => $value ]; } else { $field = [ 'type' => 'hidden', 'value' => '/', 'note' => lng('panel.dirsmissing') ]; } } return $field; } /** * Returns an array of found directories * * This function checks every found directory if they match either $uid or $gid, if they do * the found directory is valid. It uses recursive-iterators to find subdirectories. * * @param string $path the path to start searching in * @param int $uid the uid which must match the found directories * @param int $gid the gid which must match the found directories * * @return array Array of found valid paths * @throws Exception */ private static function findDirs(string $path, int $uid, int $gid): array { $_fileList = []; $path = self::makeCorrectDir($path); // valid directory? if (is_dir($path)) { // Will exclude everything under these directories $exclude = [ 'awstats', 'webalizer', 'goaccess' ]; /** * * @param SplFileInfo $file * @param mixed $key * @param RecursiveCallbackFilterIterator $iterator * @return bool True if you need to recurse or if the item is acceptable */ $filter = function ($file, $key, $iterator) use ($exclude) { if (in_array($file->getFilename(), $exclude)) { return false; } elseif (substr($file->getFilename(), 0, 1) == '.') { // also hide hidden folders return false; } return true; }; // create RecursiveIteratorIterator $its = new \RecursiveIteratorIterator( new \RecursiveCallbackFilterIterator( new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS), $filter ), \RecursiveIteratorIterator::SELF_FIRST, \RecursiveIteratorIterator::CATCH_GET_CHILD ); // we can limit the recursion-depth, but will it be helpful or // will people start asking "why do I only see 2 subdirectories, i want to use /a/b/c" // let's keep this in mind and see whether it will be useful $its->setMaxDepth(2); // check every file foreach ($its as $fullFileName => $it) { if ($it->isDir() && (fileowner($fullFileName) == $uid || filegroup($fullFileName) == $gid)) { $_fileList[] = self::makeCorrectDir($fullFileName); } } $_fileList[] = $path; } return array_unique($_fileList); } /** * set the immutable flag for a file * * @param string $filename the file to set the flag for * * @return void */ public static function setImmutable(string $filename) { self::safe_exec(self::getImmutableFunction(false) . escapeshellarg($filename)); } /** * internal function to check whether * to use chattr (Linux) or chflags (FreeBSD) * * @param bool $remove whether to use +i|schg (false) or -i|noschg (true) * * @return string functionname + parameter (not the file) */ private static function getImmutableFunction(bool $remove = false): string { if (self::isFreeBSD()) { // FreeBSD style return 'chflags ' . (($remove === true) ? 'noschg ' : 'schg '); } else { // Linux style return 'chattr ' . (($remove === true) ? '-i ' : '+i '); } } /** * check if the system is FreeBSD (if exact) * or BSD-based (NetBSD, OpenBSD, etc. * if exact = false [default]) * * @param bool $exact whether to check explicitly for FreeBSD or *BSD * * @return bool */ public static function isFreeBSD(bool $exact = false): bool { if (($exact && PHP_OS == 'FreeBSD') || (!$exact && stristr(PHP_OS, 'BSD'))) { return true; } return false; } /** * removes the immutable flag for a file * * @param string $filename the file to set the flag for * * @return void */ public static function removeImmutable(string $filename) { FileDir::safe_exec(self::getImmutableFunction(true) . escapeshellarg($filename)); } /** * * @return array|false */ public static function getFilesystemQuota() { // enabled at all? if (Settings::Get('system.diskquota_enabled')) { // set linux defaults $repquota_params = "-np"; // $quota_line_regex = "/^#([0-9]+)\s*[+-]{2}\s*(\d+)\s*(\d+)\s*(\d+)\s*(\d+)\s*(\d+)\s*(\d+)\s*(\d+)\s*(\d+)/i"; $quota_line_regex = "/^#([0-9]+)\s+[+-]{2}\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/i"; // check for freebsd - which needs other values if (self::isFreeBSD()) { $repquota_params = "-nu"; $quota_line_regex = "/^([0-9]+)\s+[+-]{2}\s+(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\S+)/i"; } // Fetch all quota in the desired partition $repquota = []; exec( Settings::Get('system.diskquota_repquota_path') . " " . $repquota_params . " " . escapeshellarg(Settings::Get('system.diskquota_customer_partition')), $repquota ); $usedquota = []; foreach ($repquota as $tmpquota) { $matches = null; // Let's see if the line matches a quota - line if (preg_match($quota_line_regex, $tmpquota, $matches)) { // It matches - put it into an array with userid as key (for easy lookup later) $usedquota[$matches[1]] = [ 'block' => [ 'used' => $matches[2], 'soft' => $matches[3], 'hard' => $matches[4], 'grace' => (self::isFreeBSD() ? '0' : $matches[5]) ], 'file' => [ 'used' => $matches[6], 'soft' => $matches[7], 'hard' => $matches[8], 'grace' => (self::isFreeBSD() ? '0' : $matches[9]) ] ]; } } return $usedquota; } return false; } } ================================================ FILE: lib/Froxlor/Froxlor.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor; use Froxlor\Database\Database; final class Froxlor { // Main version variable const VERSION = '2.3.7'; // Database version (YYYYMMDDC where C is a daily counter) const DBVERSION = '202603100'; // Distribution branding-tag (used for Debian etc.) const BRANDING = ''; const DOCS_URL = 'https://docs.froxlor.org'; /** * return path to where froxlor is installed, e.g. * /var/www/froxlor/ * * @return string */ public static function getInstallDir(): string { return dirname(__DIR__, 2) . '/'; } public static function getDocsUrl(): string { if (preg_match('/(.+)-(dev|beta|rc)\d+$/', self::VERSION)) { return self::DOCS_URL . '/dev/'; } return self::DOCS_URL . '/v' . self::getShortVersion() . '/'; } /** * return basic version * * @return string */ public static function getVersion(): string { return self::VERSION; } /** * return short basic version * * @return string */ public static function getShortVersion(): string { return explode(".", self::VERSION)[0] . '.' . explode(".", self::VERSION)[1]; } /** * return version + branding and database-version * * @return string */ public static function getVersionString(): string { return self::getFullVersion() . ' (' . self::DBVERSION . ')'; } /** * return version + branding * * @return string */ public static function getFullVersion(): string { return self::VERSION . self::BRANDING; } /** * Function hasUpdates * * checks if a given version is not equal the current one * * @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(string $to_check = ''): bool { if (empty($to_check)) { $to_check = self::VERSION; } if (Settings::Get('panel.version') == null || Settings::Get('panel.version') != $to_check) { return true; } return false; } /** * Function hasDbUpdates * * checks if a given database-version is not equal the current one * * @param string $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(string $to_check = ''): bool { if (empty($to_check)) { $to_check = self::DBVERSION; } if (Settings::Get('panel.db_version') == null || Settings::Get('panel.db_version') != $to_check) { return true; } return false; } /** * Function isDatabaseVersion * * checks if a given database-version is the current one * * @param string $to_check version to check * * @return bool true if version to check matches, else false */ public static function isDatabaseVersion(string $to_check): bool { if (Settings::Get('panel.frontend') == 'froxlor' && Settings::Get('panel.db_version') == $to_check) { return true; } return false; } /** * Function updateToDbVersion * * updates the panel.version field * to the given value (no checks here!) * * @param string $new_version new-version * * @return bool true on success, else false * @throws \Exception */ public static function updateToDbVersion(string $new_version): bool { if ($new_version != '') { $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_SETTINGS . "` SET `value` = :newversion WHERE `settinggroup` = 'panel' AND `varname` = 'db_version'"); Database::pexecute($upd_stmt, [ 'newversion' => $new_version ]); Settings::Set('panel.db_version', $new_version); return true; } return false; } /** * Function updateToVersion * * updates the panel.version field * to the given value (no checks here!) * * @param string $new_version new-version * * @return bool true on success, else false * @throws \Exception */ public static function updateToVersion(string $new_version): bool { if ($new_version != '') { $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_SETTINGS . "` SET `value` = :newversion WHERE `settinggroup` = 'panel' AND `varname` = 'version'"); Database::pexecute($upd_stmt, [ 'newversion' => $new_version ]); Settings::Set('panel.version', $new_version); return true; } return false; } /** * Function isFroxlor * * checks if the panel is froxlor * * @return bool true if panel is froxlor, else false */ public static function isFroxlor(): bool { if (Settings::Get('panel.frontend') !== null && Settings::Get('panel.frontend') == 'froxlor') { return true; } return false; } /** * Function isFroxlorVersion * * checks if a given version is the * current one (and panel is froxlor) * * @param string $to_check version to check * * @return bool true if version to check matches, else false */ public static function isFroxlorVersion(string $to_check): bool { if (Settings::Get('panel.frontend') == 'froxlor' && Settings::Get('panel.version') == $to_check) { return true; } return false; } /** * generate safe unique session id * * @param int $length * @return string * @throws \Exception */ public static function genSessionId(int $length = 16): string { if ($length <= 8) { $length = 16; } if (function_exists('random_bytes')) { return bin2hex(random_bytes($length)); } if (function_exists('mcrypt_create_iv') && defined('MCRYPT_DEV_URANDOM')) { return bin2hex(mcrypt_create_iv($length, MCRYPT_DEV_URANDOM)); } if (function_exists('openssl_random_pseudo_bytes')) { return bin2hex(openssl_random_pseudo_bytes($length)); } // if everything else fails, use unsafe fallback return md5(uniqid(microtime(), 1)); } /** * compare of froxlor versions * * @param string $a * @param string $b * * @return int 0 if equal, 1 if a>b and -1 if b>a */ public static function versionCompare2(string $a, string $b): int { // split version into pieces and remove trailing .0 $a = explode(".", $a); $b = explode(".", $b); self::parseVersionArray($a); self::parseVersionArray($b); while (count($a) != count($b)) { if (count($a) < count($b)) { $a[] = '0'; } elseif (count($b) < count($a)) { $b[] = '0'; } } foreach ($a as $depth => $aVal) { // iterate over each piece of A if (isset($b[$depth])) { // if B matches A to this depth, compare the values if ($aVal > $b[$depth]) { return 1; // A > B } elseif ($aVal < $b[$depth]) { return -1; // B > A } // an equal result is inconclusive at this point } else { // if B does not match A to this depth, then A comes after B in sort order return 1; // so A > B } } // at this point, we know that to the depth that A and B extend to, they are equivalent. // either the loop ended because A is shorter than B, or both are equal. return (count($a) < count($b)) ? -1 : 0; } /** * @param array|null $arr * @return void */ private static function parseVersionArray(?array &$arr) { // -dev or -beta or -rc ? if (stripos($arr[count($arr) - 1], '-') !== false) { $x = explode("-", $arr[count($arr) - 1]); $arr[count($arr) - 1] = $x[0]; if (stripos($x[1], 'rc') !== false) { $arr[] = '-1'; $arr[] = '2'; // dev < beta < rc // number of rc $arr[] = substr($x[1], 2); } else { if (stripos($x[1], 'beta') !== false) { $arr[] = '-1'; $arr[] = '1'; // dev < beta < rc // number of beta $arr[] = substr($x[1], 3); } else { if (stripos($x[1], 'dev') !== false) { $arr[] = '-1'; $arr[] = '0'; // dev < beta < rc // number of dev $arr[] = substr($x[1], 3); } } } } } } ================================================ FILE: lib/Froxlor/FroxlorLogger.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor; use Froxlor\System\MysqlHandler; use Monolog\Handler\StreamHandler; use Monolog\Handler\SyslogHandler; use Monolog\Logger; /** * Class FroxlorLogger */ class FroxlorLogger { const USR_ACTION = '10'; const RES_ACTION = '20'; const ADM_ACTION = '30'; const CRON_ACTION = '40'; const LOGIN_ACTION = '50'; const LOG_ERROR = '99'; /** * current \Monolog\Logger object * * @var ?Logger */ private static ?Logger $ml = null; /** * LogTypes Array * * @var ?array */ private static ?array $logtypes = null; /** * whether to output log-messages to STDOUT (cron) * * @var bool */ private static bool $crondebug_flag = false; /** * user info of logged-in user * * @var array */ private static array $userinfo = []; /** * whether the logger object has already been initialized * * @var bool */ private static bool $is_initialized = false; /** * Class constructor. * * @param array $userinfo * * @throws \Exception */ protected function __construct(array $userinfo = []) { $this->initMonolog(); self::$userinfo = $userinfo; self::$logtypes = []; if ((Settings::Get('logger.logtypes') == null || Settings::Get('logger.logtypes') == '') && (Settings::Get('logger.enabled') !== null && Settings::Get('logger.enabled'))) { self::$logtypes[0] = 'syslog'; self::$logtypes[1] = 'mysql'; } else { if (Settings::Get('logger.logtypes') !== null && Settings::Get('logger.logtypes') != '') { self::$logtypes = explode(',', Settings::Get('logger.logtypes')); } else { self::$logtypes = null; } } if (self::$is_initialized == false) { foreach (self::$logtypes as $logger) { switch ($logger) { case 'syslog': self::$ml->pushHandler(new SyslogHandler('froxlor', LOG_USER, Logger::DEBUG)); break; case 'file': $setings_logfile = Settings::Get('logger.logfile'); if (empty($setings_logfile)) { Settings::Set('logger.logfile', 'froxlor.log'); } $logger_logfile = FileDir::makeCorrectFile(Froxlor::getInstallDir() . '/logs/' . Settings::Get('logger.logfile')); // is_writable needs an existing file to check if it's actually writable if (!@touch($logger_logfile) || !is_writable($logger_logfile)) { // not writable in our own directory? Skip break; } self::$ml->pushHandler(new StreamHandler($logger_logfile, Logger::DEBUG)); break; case 'mysql': self::$ml->pushHandler(new MysqlHandler(Logger::DEBUG)); break; } } self::$is_initialized = true; } } /** * initiate monolog object * * @return Logger */ private function initMonolog() { if (empty(self::$ml)) { // get Theme object self::$ml = new Logger('froxlor'); } return self::$ml; } /** * return FroxlorLogger instance * * @param array $userinfo * * @return FroxlorLogger * @throws \Exception */ public static function getInstanceOf(array $userinfo = []) { if (empty($userinfo)) { $userinfo = [ 'loginname' => 'system' ]; } return new FroxlorLogger($userinfo); } /** * logs a given text to all enabled logger-facilities * * @param int $action * @param int $type * @param ?string $text */ public function logAction($action = FroxlorLogger::USR_ACTION, int $type = LOG_NOTICE, ?string $text = null) { // not logging normal stuff if not set to "paranoid" logging if (!self::$crondebug_flag && Settings::Get('logger.severity') == '1' && $type > LOG_NOTICE) { return; } if (empty(self::$ml)) { $this->initMonolog(); } // clean log-text $text = preg_replace("/[^\w @#\"':.,()\[\]+\-_\/\\\!]/i", "_", $text); if (self::$crondebug_flag || ($action == FroxlorLogger::CRON_ACTION && $type <= LOG_WARNING)) { echo "[" . $this->getLogLevelDesc($type) . "] " . $text . PHP_EOL; } // warnings, errors and critical messages WILL be logged if (Settings::Get('logger.log_cron') == '0' && $action == FroxlorLogger::CRON_ACTION && $type > LOG_WARNING) { return; } $logExtra = [ 'source' => $this->getActionTypeDesc($action), 'action' => $action, 'user' => self::$userinfo['loginname'] ]; switch ($type) { case LOG_DEBUG: self::$ml->addDebug($text, $logExtra); break; case LOG_INFO: self::$ml->addInfo($text, $logExtra); break; case LOG_NOTICE: self::$ml->addNotice($text, $logExtra); break; case LOG_WARNING: self::$ml->addWarning($text, $logExtra); break; case LOG_ERR: self::$ml->addError($text, $logExtra); break; default: self::$ml->addDebug($text, $logExtra); } } /** * @param int $type * @return string */ public function getLogLevelDesc(int $type): string { switch ($type) { case LOG_INFO: $_type = 'information'; break; case LOG_NOTICE: $_type = 'notice'; break; case LOG_WARNING: $_type = 'warning'; break; case LOG_ERR: $_type = 'error'; break; case LOG_CRIT: $_type = 'critical'; break; case LOG_DEBUG: $_type = 'debug'; break; default: $_type = 'unknown'; break; } return $_type; } /** * @param $action * @return string */ private function getActionTypeDesc($action): string { switch ($action) { case FroxlorLogger::USR_ACTION: $_action = 'user'; break; case FroxlorLogger::ADM_ACTION: $_action = 'admin'; break; case FroxlorLogger::RES_ACTION: $_action = 'reseller'; break; case FroxlorLogger::CRON_ACTION: $_action = 'cron'; break; case FroxlorLogger::LOGIN_ACTION: $_action = 'login'; break; default: $_action = 'unknown'; break; } return $_action; } /** * Set whether to log cron-runs * * @param int $cronlog * * @return int */ public function setCronLog(int $cronlog = 0): int { if ($cronlog < 0 || $cronlog > 2) { $cronlog = 0; } Settings::Set('logger.log_cron', $cronlog); return $cronlog; } /** * setter for crondebug-flag * * @param bool $flag * * @return void */ public function setCronDebugFlag(bool $flag = false) { self::$crondebug_flag = $flag; } } ================================================ FILE: lib/Froxlor/FroxlorTwoFactorAuth.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor; use RobThree\Auth\TwoFactorAuth; class FroxlorTwoFactorAuth extends TwoFactorAuth { } ================================================ FILE: lib/Froxlor/Http/Directory.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Http; use Froxlor\Database\Database; use Froxlor\FileDir; /** * Class frxDirectory handles directory actions and gives information * about a given directory in connections with its usage in froxlor */ class Directory { /** * directory string * * @var string */ private $dir = null; /** * class constructor, optionally set directory * * @param string $dir */ public function __construct(?string $dir = null) { $this->dir = $dir; } /** * check whether the directory has options set in panel_htaccess * * @return bool */ public function hasUserOptions(): bool { $uo_stmt = Database::prepare(" SELECT COUNT(`id`) as `usropts` FROM `" . TABLE_PANEL_HTACCESS . "` WHERE `path` = :dir "); $uo_res = Database::pexecute_first($uo_stmt, [ 'dir' => FileDir::makeCorrectDir($this->dir) ]); if ($uo_res && isset($uo_res['usropts'])) { return $uo_res['usropts'] > 0; } return false; } /** * check whether the directory is protected using panel_htpasswd * * @return bool */ public function isUserProtected(): bool { $up_stmt = Database::prepare(" SELECT COUNT(`id`) as `usrprot` FROM `" . TABLE_PANEL_HTPASSWDS . "` WHERE `path` = :dir "); $up_res = Database::pexecute_first($up_stmt, [ 'dir' => FileDir::makeCorrectDir($this->dir) ]); if ($up_res && isset($up_res['usrprot'])) { return $up_res['usrprot'] > 0; } return false; } /** * Checks if a given directory is valid for multiple configurations * or should rather be used as a single file * * @param bool $ifexists also check whether file/dir exists * * @return bool true if usable as dir, false otherwise */ public function isConfigDir(bool $ifexists = false): bool { if (is_null($this->dir)) { trigger_error(__CLASS__ . '::' . __FUNCTION__ . ' has been called with a null value', E_USER_WARNING); return false; } if (file_exists($this->dir)) { if (is_dir($this->dir)) { $returnval = true; } else { $returnval = false; } } else { if (!$ifexists) { if (substr($this->dir, -1) == '/') { $returnval = true; } else { $returnval = false; } } else { $returnval = false; } } return $returnval; } } ================================================ FILE: lib/Froxlor/Http/HttpClient.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Http; use Exception; use Froxlor\Froxlor; class HttpClient { /** * Executes simple GET request * * @param string $url * @param bool $follow_location * @param int $timeout * * @return bool|string * @throws Exception */ public static function urlGet(string $url, bool $follow_location = true, int $timeout = 10) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_USERAGENT, 'Froxlor/' . Froxlor::getVersion()); if ($follow_location) { curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); } curl_setopt($ch, CURLOPT_TIMEOUT, (int)$timeout); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $output = curl_exec($ch); if ($output === false) { $e = curl_error($ch); throw new Exception("Curl error: " . $e); } return $output; } /** * Downloads and stores a file from an url * * @param string $url * @param string $target * * @return bool|string * @throws Exception */ public static function fileGet(string $url, string $target) { $fh = fopen($target, 'w'); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_USERAGENT, 'Froxlor/' . Froxlor::getVersion()); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); curl_setopt($ch, CURLOPT_TIMEOUT, 50); // give curl the file pointer so that it can write to it curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_FILE, $fh); $output = curl_exec($ch); if ($output === false) { $e = curl_error($ch); throw new Exception("Curl error: " . $e); } return $output; } } ================================================ FILE: lib/Froxlor/Http/PhpConfig.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Http; use Froxlor\Database\Database; use PDO; class PhpConfig { /** * returns an array of existing php-configurations * in our database for the settings-array * * @return array */ public static function getPhpConfigs(): array { $configs_array = []; // check if table exists because this is used in a preconfig // where the tables possibly does not exist yet $results = Database::query("SHOW TABLES LIKE '" . TABLE_PANEL_PHPCONFIGS . "'"); if (!$results) { $configs_array[1] = 'Default php.ini'; } else { // get all configs $result_stmt = Database::query("SELECT * FROM `" . TABLE_PANEL_PHPCONFIGS . "`"); while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { if (!isset($configs_array[$row['id']]) && !in_array($row['id'], $configs_array)) { $configs_array[$row['id']] = html_entity_decode($row['description']); } } } return $configs_array; } } ================================================ FILE: lib/Froxlor/Http/RateLimiter.php ================================================ $reset) { $remaining = self::$limit_per_interval; $reset = self::$reset_time; } // If we've hit the limit, return an error if ($remaining <= 0) { header('HTTP/1.1 429 Too Many Requests'); header("Retry-After: $reset"); UI::twig()->addGlobal('install_mode', '1'); echo UI::twig()->render('Froxlor/misc/ratelimithint.html.twig', [ 'retry' => $reset, 'installdir' => Froxlor::getInstallDir() ]); die(); } // Decrement the remaining requests and update the headers $remaining--; $_SESSION['HTTP_X_RATELIMIT_REMAINING'] = $remaining; $_SESSION['HTTP_X_RATELIMIT_RESET'] = $reset; header("X-RateLimit-Limit: " . self::$limit_per_interval); header("X-RateLimit-Remaining: " . $remaining); header("X-RateLimit-Reset: " . $reset); } } ================================================ FILE: lib/Froxlor/Http/Statistics.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Http; use Froxlor\FileDir; use Froxlor\Froxlor; use Froxlor\Settings; class Statistics { /** * Create or modify the AWStats configuration file for the given domain. * Modified by Berend Dekens to allow custom configurations. * * @param string $logFile * @param string $siteDomain * @param string $hostAliases * @param string $customerDocroot * @param array $domain_data * * @return null * @throws \Exception */ public static function createAWStatsConf( string $logFile, string $siteDomain, string $hostAliases, string $customerDocroot, array $domain_data = [] ) { // Generation header $header = "## GENERATED BY FROXLOR\n"; $header2 = "## Do not remove the line above! This tells Froxlor to update this configuration\n## If you wish to manually change this configuration file, remove the first line to make sure Froxlor won't rebuild this file\n## Generated for domain {SITE_DOMAIN} on " . date('l dS \of F Y h:i:s A') . "\n"; $awstats_dir = FileDir::makeCorrectDir($customerDocroot . '/awstats/' . $siteDomain . '/'); if (!is_dir($awstats_dir)) { FileDir::safe_exec('mkdir -p ' . escapeshellarg($awstats_dir)); } // chown created folder, #258 self::makeChownWithNewStats($domain_data); // weird but could happen... if (!is_dir(Settings::Get('system.awstats_conf'))) { FileDir::safe_exec('mkdir -p ' . escapeshellarg(Settings::Get('system.awstats_conf'))); } $logformat = Settings::Get('system.awstats_logformat'); if (!is_numeric($logformat)) { // if LogFormat is NOT numeric (e.g. 1,2,3,4), we quote it. // 1-4 are pre-defined formats by awstats which must not be quoted to work properly. So if // it is not a integer, it is something customized and we simply quote it. // Only escaping double-quote should be fine, as we only put the whole string under double-quote. $logformat = '"' . str_replace('"', '\"', Settings::Get('system.awstats_logformat')) . '"'; } // These are the variables we will replace $regex = [ '/\{LOG_FILE\}/', '/\{SITE_DOMAIN\}/', '/\{HOST_ALIASES\}/', '/\{CUSTOMER_DOCROOT\}/', '/\{AWSTATS_CONF\}/', '/\{AWSTATS_LOGFORMAT\}/' ]; $replace = [ FileDir::makeCorrectFile($logFile), $siteDomain, $hostAliases, $awstats_dir, FileDir::makeCorrectDir(Settings::Get('system.awstats_conf')), $logformat ]; // File names $domain_file = FileDir::makeCorrectFile(Settings::Get('system.awstats_conf') . '/awstats.' . $siteDomain . '.conf'); $model_file = Froxlor::getInstallDir() . '/templates/misc/awstats/awstats.froxlor.model.conf'; $model_file = FileDir::makeCorrectFile($model_file); // Test if the file exists if (file_exists($domain_file)) { // Check for the generated header - if this is a manual modification we won't update $awstats_domain_conf = fopen($domain_file, 'r'); if (fgets($awstats_domain_conf, strlen($header)) != $header) { fclose($awstats_domain_conf); return; } // Close the file fclose($awstats_domain_conf); } $awstats_domain_conf = fopen($domain_file, 'w'); $awstats_model_conf = fopen($model_file, 'r'); // Write the header fwrite($awstats_domain_conf, $header); fwrite($awstats_domain_conf, preg_replace($regex, $replace, $header2)); // Write the configuration file while (($line = fgets($awstats_model_conf, 4096)) !== false) { if (!preg_match('/^#/', $line) && trim($line) != '') { fwrite($awstats_domain_conf, preg_replace($regex, $replace, $line)); } } fclose($awstats_domain_conf); fclose($awstats_model_conf); } /** * chowns stats-tools folder, either with webserver-user or * if fcgid/php-fpm is used, the customers name, #258 * * @param array $row array of panel_customers * * @return void * @throws \Exception */ public static function makeChownWithNewStats(array $row) { // get correct user if ((Settings::Get('system.mod_fcgid') == '1' || Settings::Get('phpfpm.enabled') == '1') && isset($row['deactivated']) && $row['deactivated'] == '0') { $user = $row['loginname']; $group = $row['loginname']; } else { $user = $row['guid']; $group = $row['guid']; } // get correct directory $dir = $row['documentroot'] . '/' . Settings::Get('system.traffictool') . '/'; // only run chown if directory exists if (file_exists($dir)) { // run chown FileDir::safe_exec('chown -R ' . escapeshellarg($user) . ':' . escapeshellarg($group) . ' ' . escapeshellarg(FileDir::makeCorrectDir($dir))); } } } ================================================ FILE: lib/Froxlor/Http/index.html ================================================ ================================================ FILE: lib/Froxlor/Idna/IdnaWrapper.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Idna; use Algo26\IdnaConvert\IdnaConvert; use InvalidArgumentException; /** * Class for wrapping a specific idna conversion class and offering a standard interface * * @author Michael Duergner (2003-2009) */ class IdnaWrapper { /** * idna converter we use * * @var object */ private $idna_converter; /** * Class constructor. * Creates a new idna converter */ public function __construct() { // Instantiate it $this->idna_converter = new IdnaConvert(); } /** * Encode a domain name, an email address or a list of one of both. * * @param string $to_encode May be either a single domain name, e single email address or a list of one * separated either by ',', ';' or ' '. * * @return string Returns either a single domain name, a single email address or a list of one of * both separated by the same string as the input. */ public function encode(string $to_encode): string { $to_encode = $this->isUtf8($to_encode) ? $to_encode : mb_convert_encoding($to_encode, 'UTF-8'); try { return $this->idna_converter->encode($to_encode); } catch (InvalidArgumentException $iae) { if ($iae->getCode() == 100) { return $to_encode; } throw $iae; } } /** * check whether a string is utf-8 encoded or not * * @param string $string * * @return boolean */ private function isUtf8(string $string) { if (function_exists("mb_detect_encoding")) { if (mb_detect_encoding($string, 'UTF-8, ISO-8859-1') === 'UTF-8') { return true; } return false; } $strlen = strlen($string); for ($i = 0; $i < $strlen; $i++) { $ord = ord($string[$i]); if ($ord < 0x80) { continue; // 0bbbbbbb } elseif (($ord & 0xE0) === 0xC0 && $ord > 0xC1) { $n = 1; // 110bbbbb (exkl C0-C1) } elseif (($ord & 0xF0) === 0xE0) { $n = 2; // 1110bbbb } elseif (($ord & 0xF8) === 0xF0 && $ord < 0xF5) { $n = 3; // 11110bbb (exkl F5-FF) } else { // ungültiges UTF-8-Zeichen return false; } // $n Folgebytes? // 10bbbbbb for ($c = 0; $c < $n; $c++) { if (++$i === $strlen || (ord($string[$i]) & 0xC0) !== 0x80) { // ungültiges UTF-8-Zeichen return false; } } } // kein ungültiges UTF-8-Zeichen gefunden return true; } /** * Decode a domain name, an email address or a list of one of both. * * @param string $to_decode May be either a single domain name, e single email address or a list of one * separated either by ',', ';' or ' '. * * @return string Returns either a single domain name, a single email address or a list of one of * both separated by the same string as the input. */ public function decode(string $to_decode): string { return $this->idna_converter->decode($to_decode); } } ================================================ FILE: lib/Froxlor/Idna/index.html ================================================ ================================================ FILE: lib/Froxlor/Install/AutoUpdate.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Install; use Exception; use ZipArchive; use Froxlor\Froxlor; use Froxlor\Settings; use Froxlor\Http\HttpClient; class AutoUpdate { // define update-uri const UPDATE_URI = "https://version.froxlor.org/froxlor/api/v2/"; const RELEASE_URI = "https://autoupdate.froxlor.org/froxlor-{version}.zip"; const CHECKSUM_URI = "https://autoupdate.froxlor.org/froxlor-{version}.zip.sha256"; const ERR_NOZIPEXT = 2; const ERR_COULDNOTSTORE = 4; const ERR_ZIPNOTFOUND = 7; const ERR_COULDNOTEXTRACT = 8; const ERR_CHKSUM_MISMATCH = 9; const ERR_MINPHP = 10; private static $latestversion = ""; private static $lasterror = ""; /** * returns status about whether there is a newer version * * 0 = no new version available * 1 = new version available * -1 = remote error message * >1 = local error message * * @return int */ public static function checkVersion(): int { $result = self::checkPrerequisites(); if ($result == 0) { try { $channel = ''; if (Settings::Get('system.update_channel') == 'testing') { $channel = '/testing'; } elseif (Settings::Get('system.update_channel') == 'nightly') { if (empty(Froxlor::BRANDING) || substr(Froxlor::BRANDING, 0, 1) == '-') { $channel = '/nightly.0000000'; } else { $channel = '/' . substr(Froxlor::BRANDING, 1); } } $latestversion = HttpClient::urlGet(self::UPDATE_URI . Froxlor::VERSION . $channel, true, 3); } catch (Exception $e) { self::$lasterror = "Version-check currently unavailable, please try again later"; return -1; } self::$latestversion = json_decode($latestversion, true); if (self::$latestversion) { if (!empty(self::$latestversion['error']) && self::$latestversion['error']) { $result = -1; self::$lasterror = self::$latestversion['message']; } elseif (isset(self::$latestversion['has_latest']) && self::$latestversion['has_latest'] == false) { $result = 1; } } } return $result; } public static function downloadZip(string $newversion) { // define files to get $toLoad = str_replace('{version}', $newversion, self::RELEASE_URI); $toCheck = str_replace('{version}', $newversion, self::CHECKSUM_URI); // check for local destination folder if (!is_dir(Froxlor::getInstallDir() . '/updates/')) { mkdir(Froxlor::getInstallDir() . '/updates/'); } // name archive $localArchive = Froxlor::getInstallDir() . '/updates/' . basename($toLoad); // remove old archive if (file_exists($localArchive)) { @unlink($localArchive); } // get archive data try { HttpClient::fileGet($toLoad, $localArchive); } catch (Exception $e) { return self::ERR_COULDNOTSTORE; } // validate the integrity of the downloaded file $_shouldsum = HttpClient::urlGet($toCheck); if (!empty($_shouldsum)) { $_t = explode(" ", $_shouldsum); $shouldsum = $_t[0]; } else { $shouldsum = null; } $filesum = hash_file('sha256', $localArchive); if ($filesum != $shouldsum) { return self::ERR_CHKSUM_MISMATCH; } return basename($localArchive); } public static function extractZip(string $localArchive): int { if (!file_exists($localArchive)) { return self::ERR_ZIPNOTFOUND; } // decompress from zip $zip = new ZipArchive(); $res = $zip->open($localArchive); if ($res === true) { $zip->extractTo(Froxlor::getInstallDir()); $zip->close(); // success - remove unused archive @unlink($localArchive); // reset cached version check Settings::Set('system.updatecheck_data', ''); // wait a bit before we redirect to be sure sleep(3); return 0; } return self::ERR_COULDNOTEXTRACT; } private static function checkPrerequisites(): int { if (!extension_loaded('zip')) { return self::ERR_NOZIPEXT; } if (version_compare("7.4.0", PHP_VERSION, ">=")) { return self::ERR_MINPHP; } return 0; } public static function getLastError(): string { return self::$lasterror ?? ""; } public static function getFromResult(string $index) { return self::$latestversion[$index] ?? ""; } } ================================================ FILE: lib/Froxlor/Install/Install/Core.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Install\Install; use Exception; use Froxlor\Config\ConfigParser; use Froxlor\FileDir; use Froxlor\Froxlor; use Froxlor\PhpHelper; use PDO; use PDOException; use PDOStatement; /** * Installation of the froxlor core database and set settings. */ class Core { protected array $validatedData; public function __construct(array $validatedData) { $this->validatedData = $validatedData; } /** * no missing fields or data -> perform actual install * * @return void * @throws Exception */ public function doInstall(bool $create_ud_str = true) { $options = [ 'PDO::MYSQL_ATTR_INIT_COMMAND' => 'SET names utf8' ]; if (!empty($this->validatedData['mysql_ssl_ca_file'])) { $options[PDO::MYSQL_ATTR_SSL_CA] = $this->validatedData['mysql_ssl_ca_file']; $options[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = (bool)$this->validatedData['mysql_ssl_verify_server_certificate']; } $dsn = "mysql:host=" . $this->validatedData['mysql_host'] . ";"; try { $db_root = new PDO($dsn, $this->validatedData['mysql_root_user'], $this->validatedData['mysql_root_pass'], $options); } catch (PDOException $e) { // login failed; try to log in without passwd try { $db_root = new PDO($dsn, $this->validatedData['mysql_root_user'], '', $options); // set the given password $passwd_stmt = $db_root->prepare(" SET PASSWORD = PASSWORD(:passwd) "); $passwd_stmt->execute([ 'passwd' => $this->validatedData['mysql_root_pass'] ]); } catch (PDOException $e) { // login has failed; with and without password throw new Exception(lng('install.errors.privileged_sql_connection_failed'), 0, $e); } } $version_server = $db_root->getAttribute(PDO::ATTR_SERVER_VERSION); $sql_mode = 'NO_ENGINE_SUBSTITUTION'; if (version_compare($version_server, '8.0.11', '<')) { $sql_mode .= ',NO_AUTO_CREATE_USER'; } $db_root->exec('SET sql_mode = "' . $sql_mode . '"'); // ok, if we are here, the database connection is up and running // check for existing pdo and create backup if so $this->backupExistingDatabase($db_root); // create unprivileged user and the database itself $this->createDatabaseAndUser($db_root); // importing data to new database $this->importDatabaseData(); // create DB object for new database $options = [ 'PDO::MYSQL_ATTR_INIT_COMMAND' => 'SET names utf8' ]; if (!empty($this->validatedData['mysql_ssl_ca_file']) && isset($this->validatedData['mysql_ssl_verify_server_certificate'])) { $options[PDO::MYSQL_ATTR_SSL_CA] = $this->validatedData['mysql_ssl_ca_file']; $options[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = (bool)$this->validatedData['mysql_ssl_verify_server_certificate']; } $pdo = $this->getUnprivilegedPdo(); // change settings accordingly $this->doSettings($pdo); // create entries $this->doDataEntries($pdo); // create JSON array for config-services $this->createJsonArray($pdo); if ($create_ud_str) { $this->createUserdataParamStr(); } } /** * @throws Exception */ public function getUnprivilegedPdo(): PDO { $options = [ 'PDO::MYSQL_ATTR_INIT_COMMAND' => 'SET names utf8' ]; if (!empty($this->validatedData['mysql_ssl_ca_file'])) { $options[PDO::MYSQL_ATTR_SSL_CA] = $this->validatedData['mysql_ssl_ca_file']; $options[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = (bool)$this->validatedData['mysql_ssl_verify_server_certificate']; } $dsn = "mysql:host=" . $this->validatedData['mysql_host'] . ";dbname=" . $this->validatedData['mysql_database'] . ";"; try { $pdo = new PDO($dsn, $this->validatedData['mysql_unprivileged_user'], $this->validatedData['mysql_unprivileged_pass'], $options); $version_server = $pdo->getAttribute(PDO::ATTR_SERVER_VERSION); $sql_mode = 'NO_ENGINE_SUBSTITUTION'; if (version_compare($version_server, '8.0.11', '<')) { $sql_mode .= ',NO_AUTO_CREATE_USER'; } $pdo->exec('SET sql_mode = "' . $sql_mode . '"'); return $pdo; } catch (PDOException $e) { throw new Exception(lng('install.errors.unexpected_database_error', [$e->getMessage()]), 0, $e); } } /** * Check if an old database exists and back it up if necessary * * @param object $db_root * @return void * @throws Exception */ private function backupExistingDatabase(object &$db_root) { // check for existing of former database $stmt = $db_root->prepare("SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = :database"); $stmt->execute([ 'database' => $this->validatedData['mysql_database'] ]); $rows = $db_root->query("SELECT FOUND_ROWS()")->fetchColumn(); // backup tables if exist if ($rows > 0) { if (!$this->validatedData['mysql_force_create']) { throw new Exception(lng('install.errors.database_already_exiting')); } // create temporary backup-filename $filename = "/tmp/froxlor_backup_" . date('YmdHi') . ".sql"; // look for mysqldump $section = 'mysqldump'; if (file_exists("/usr/bin/mysqldump")) { $mysql_dump = '/usr/bin/mysqldump'; } elseif (file_exists("/usr/local/bin/mysqldump")) { $mysql_dump = '/usr/local/bin/mysqldump'; } elseif (file_exists("/usr/bin/mariadb-dump")) { $mysql_dump = '/usr/bin/mariadb-dump'; $section = 'mariadb-dump'; } // create temporary .cnf file $cnffilename = "/tmp/froxlor_dump.cnf"; $dumpcnf = "[".$section."]" . PHP_EOL . "password=\"" . $this->validatedData['mysql_root_pass'] . "\"" . PHP_EOL; file_put_contents($cnffilename, $dumpcnf); // make the backup if (isset($mysql_dump)) { $command = $mysql_dump . " --defaults-extra-file=" . $cnffilename . " " . escapeshellarg($this->validatedData['mysql_database']) . " -u " . escapeshellarg($this->validatedData['mysql_root_user']) . " --result-file=" . $filename; $output = []; exec($command, $output); @unlink($cnffilename); if (stristr(implode(" ", $output), "error")) { throw new Exception(lng('install.errors.mysqldump_backup_failed')); } elseif (!file_exists($filename)) { throw new Exception(lng('install.errors.sql_backup_file_missing')); } } else { throw new Exception(lng('install.errors.backup_binary_missing')); } } } /** * Create database and database-user * * @param object $db_root * @return void * @throws Exception */ private function createDatabaseAndUser(object &$db_root) { $this->validatedData['mysql_access_host'] = $this->validatedData['mysql_host']; // so first we have to delete the database and // the user given for the unpriv-user if they exit $del_stmt = $db_root->prepare("DELETE FROM `mysql`.`user` WHERE `User` = :user AND `Host` = :accesshost"); $del_stmt->execute([ 'user' => $this->validatedData['mysql_unprivileged_user'], 'accesshost' => $this->validatedData['mysql_access_host'] ]); $del_stmt = $db_root->prepare("DELETE FROM `mysql`.`db` WHERE `User` = :user AND `Host` = :accesshost"); $del_stmt->execute([ 'user' => $this->validatedData['mysql_unprivileged_user'], 'accesshost' => $this->validatedData['mysql_access_host'] ]); $del_stmt = $db_root->prepare("DELETE FROM `mysql`.`tables_priv` WHERE `User` = :user AND `Host` =:accesshost"); $del_stmt->execute([ 'user' => $this->validatedData['mysql_unprivileged_user'], 'accesshost' => $this->validatedData['mysql_access_host'] ]); $del_stmt = $db_root->prepare("DELETE FROM `mysql`.`columns_priv` WHERE `User` = :user AND `Host` = :accesshost"); $del_stmt->execute([ 'user' => $this->validatedData['mysql_unprivileged_user'], 'accesshost' => $this->validatedData['mysql_access_host'] ]); $del_stmt = $db_root->prepare("DROP DATABASE IF EXISTS `" . str_replace('`', '', $this->validatedData['mysql_database']) . "`;"); $del_stmt->execute(); $db_root->query("FLUSH PRIVILEGES;"); // we have to create a new user and database for the froxlor unprivileged mysql access $ins_stmt = $db_root->prepare("CREATE DATABASE `" . str_replace('`', '', $this->validatedData['mysql_database']) . "` CHARACTER SET=utf8 COLLATE=utf8_general_ci"); $ins_stmt->execute(); $mysql_access_host_array = array_map('trim', explode(',', $this->validatedData['mysql_access_host'])); if (in_array('127.0.0.1', $mysql_access_host_array) && !in_array('localhost', $mysql_access_host_array)) { $mysql_access_host_array[] = 'localhost'; } if (!in_array('127.0.0.1', $mysql_access_host_array) && in_array('localhost', $mysql_access_host_array)) { $mysql_access_host_array[] = '127.0.0.1'; } if (!empty($this->validatedData['serveripv4']) && !in_array($this->validatedData['serveripv4'], $mysql_access_host_array)) { $mysql_access_host_array[] = $this->validatedData['serveripv4']; } if (!empty($this->validatedData['serveripv6']) && !in_array($this->validatedData['serveripv6'], $mysql_access_host_array)) { $mysql_access_host_array[] = $this->validatedData['serveripv6']; } $mysql_access_host_array = array_unique($mysql_access_host_array); foreach ($mysql_access_host_array as $mysql_access_host) { $this->grantDbPrivilegesTo($db_root, $this->validatedData['mysql_database'], $this->validatedData['mysql_unprivileged_user'], $this->validatedData['mysql_unprivileged_pass'], $mysql_access_host); } $db_root->query("FLUSH PRIVILEGES;"); $this->validatedData['mysql_access_host'] = implode(',', $mysql_access_host_array); } /** * Grant privileges to given user. * * @param $db_root * @param $database * @param $username * @param $password * @param $access_host * @return void * @throws Exception */ private function grantDbPrivilegesTo(&$db_root, $database, $username, $password, $access_host) { if ($this->validatedData['mysql_force_create']) { try { // try to drop the user, but ignore exceptions as the mysql-access-hosts might // have changed and we would try to drop a non-existing user $drop_stmt = $db_root->prepare("DROP USER :username@:host"); $drop_stmt->execute([ "username" => $username, "host" => $access_host ]); } catch (PDOException $e) { /* continue */ } } if (version_compare($db_root->getAttribute(PDO::ATTR_SERVER_VERSION), '8.0.11', '>=')) { // mariadb & mysql8 // create user $stmt = $db_root->prepare("CREATE USER '" . $username . "'@'" . $access_host . "' IDENTIFIED BY :password"); $stmt->execute([ "password" => $password ]); // grant privileges $stmt = $db_root->prepare("GRANT ALL ON `" . $database . "`.* TO :username@:host"); $stmt->execute([ "username" => $username, "host" => $access_host ]); } else { // grant privileges $stmt = $db_root->prepare("GRANT ALL PRIVILEGES ON `" . $database . "`.* TO :username@:host IDENTIFIED BY :password"); $stmt->execute([ "username" => $username, "host" => $access_host, "password" => $password ]); } } /** * Import froxlor.sql into database * * @return void * @throws Exception */ private function importDatabaseData() { try { $pdo = $this->getUnprivilegedPdo(); } catch (PDOException $e) { throw new Exception(lng('install.errors.unprivileged_sql_connection_failed')); } // actually import data try { $froxlorSQL = include dirname(__FILE__, 5) . '/install/froxlor.sql.php'; $pdo->query($froxlorSQL); } catch (PDOException $e) { throw new Exception(lng('install.errors.sql_import_failed', [$e->getMessage()]), 0, $e); } } /** * change settings according to users input * * @param object $db_user * @return void * @throws Exception */ private function doSettings(object &$db_user) { $upd_stmt = $db_user->prepare(" UPDATE `" . TABLE_PANEL_SETTINGS . "` SET `value` = :value WHERE `settinggroup` = :group AND `varname` = :varname "); $mainip = !empty($this->validatedData['serveripv6']) ? $this->validatedData['serveripv6'] : $this->validatedData['serveripv4']; if ($this->validatedData['use_admin_email_as_sender'] == '1') { $adminmail_value = $this->validatedData['admin_email']; } elseif ($this->validatedData['use_admin_email_as_sender'] == '0' && !empty($this->validatedData['sender_email'])) { $adminmail_value = $this->validatedData['sender_email']; } else { $adminmail_value = 'admin@' . $this->validatedData['servername']; } $this->updateSetting($upd_stmt, $adminmail_value, 'panel', 'adminmail'); $this->updateSetting($upd_stmt, $mainip, 'system', 'ipaddress'); if ($this->validatedData['use_ssl']) { $this->updateSetting($upd_stmt, 1, 'system', 'use_ssl'); $this->updateSetting($upd_stmt, 1, 'system', 'leenabled'); $this->updateSetting($upd_stmt, 1, 'system', 'le_froxlor_enabled'); } $this->updateSetting($upd_stmt, strtolower($this->validatedData['servername']), 'system', 'hostname'); $this->updateSetting($upd_stmt, 'en', 'panel', 'standardlanguage'); // TODO: set language $this->updateSetting($upd_stmt, $this->validatedData['mysql_access_host'], 'system', 'mysql_access_host'); $this->updateSetting($upd_stmt, $this->validatedData['webserver'], 'system', 'webserver'); $this->updateSetting($upd_stmt, $this->validatedData['httpuser'], 'system', 'httpuser'); $this->updateSetting($upd_stmt, $this->validatedData['httpgroup'], 'system', 'httpgroup'); $this->updateSetting($upd_stmt, $this->validatedData['distribution'], 'system', 'distribution'); // necessary changes for webservers != apache2 if ($this->validatedData['webserver'] == "apache24") { $this->updateSetting($upd_stmt, 'apache2', 'system', 'webserver'); $this->updateSetting($upd_stmt, '1', 'system', 'apache24'); } elseif ($this->validatedData['webserver'] == "nginx") { $this->updateSetting($upd_stmt, '/var/run/', 'phpfpm', 'fastcgi_ipcdir'); $this->updateSetting($upd_stmt, 'error', 'system', 'errorlog_level'); } $distros = glob(FileDir::makeCorrectDir(Froxlor::getInstallDir() . '/lib/configfiles/') . '*.xml'); foreach ($distros as $_distribution) { if ($this->validatedData['distribution'] == str_replace(".xml", "", strtolower(basename($_distribution)))) { $dist = new ConfigParser($_distribution); $defaults = $dist->getDefaults(); if (!empty($defaults)) { foreach ($defaults as $property) { if (!isset($property->attributes()->for) || (isset($property->attributes()->for) && $property->attributes()->for == $this->validatedData['webserver'])) { $this->updateSetting($upd_stmt, $property->attributes()->value, $property->attributes()->settinggroup, $property->attributes()->varname); } } } } } $this->updateSetting($upd_stmt, $this->validatedData['activate_newsfeed'], 'admin', 'show_news_feed'); $this->updateSetting($upd_stmt, dirname(__FILE__, 5), 'system', 'letsencryptchallengepath'); $this->updateSetting($upd_stmt, dirname(__FILE__, 5) . '/templates/misc/deactivated/', 'system', 'deactivateddocroot'); // insert the lastcronrun to be the installation date $this->updateSetting($upd_stmt, time(), 'system', 'lastcronrun'); // set settings according to selected php-backend if ($this->validatedData['webserver_backend'] == 'php-fpm') { $this->updateSetting($upd_stmt, '1', 'phpfpm', 'enabled'); $this->updateSetting($upd_stmt, '1', 'phpfpm', 'enabled_ownvhost'); } elseif ($this->validatedData['webserver_backend'] == 'fcgid') { $this->updateSetting($upd_stmt, '1', 'system', 'mod_fcgid'); $this->updateSetting($upd_stmt, '1', 'system', 'mod_fcgid_ownvhost'); } // check currently used php version and set values of fpm/fcgid accordingly if (defined('PHP_MAJOR_VERSION') && defined('PHP_MINOR_VERSION')) { // php-fpm $reload = "service php" . PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION . "-fpm restart"; $config_dir = "/etc/php/" . PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION . "/fpm/pool.d/"; // fcgid if ($this->validatedData['distribution'] == 'bookworm' || $this->validatedData['distribution'] == 'trixie') { $binary = "/usr/bin/php-cgi" . PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION; } else { $binary = "/usr/bin/php" . PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION . "-cgi"; } $db_user->query("UPDATE `" . TABLE_PANEL_FPMDAEMONS . "` SET `reload_cmd` = '" . $reload . "', `config_dir` = '" . $config_dir . "' WHERE `id` ='1';"); $db_user->query("UPDATE `" . TABLE_PANEL_PHPCONFIGS . "` SET `binary` = '" . $binary . "';"); } if ($this->validatedData['use_ssl']) { // enable let's encrypt cron $db_user->query("UPDATE `" . TABLE_PANEL_CRONRUNS . "` SET `isactive` = '1' WHERE `module` = 'froxlor/letsencrypt';"); } // set specific times for some crons (traffic only at night, etc.) $timestamp = mktime(0, 0, 0, date('m', time()), date('d', time()), date('Y', time())); $db_user->query("UPDATE `" . TABLE_PANEL_CRONRUNS . "` SET `lastrun` = '" . $timestamp . "' WHERE `cronfile` ='cron_traffic';"); // insert task 99 to generate a correct cron.d-file automatically $db_user->query("INSERT INTO `" . TABLE_PANEL_TASKS . "` SET `type` = '99';"); } /** * execute prepared statement to update settings * * @param PDOStatement|null $stmt * @param string|null $group * @param string|null $varname * @param string|null $value */ private function updateSetting(PDOStatement &$stmt = null, string $value = null, string $group = null, string $varname = null) { $stmt->execute([ 'group' => $group, 'varname' => $varname, 'value' => $value ]); } /** * create corresponding entries in froxlor database * * @param $db_user * @return void */ private function doDataEntries(&$db_user) { // lets insert the default ip and port $stmt = $db_user->prepare(" INSERT INTO `" . TABLE_PANEL_IPSANDPORTS . "` SET `ip`= :serverip, `port` = :serverport, `namevirtualhost_statement` = :nvh, `vhostcontainer` = '1', `vhostcontainer_servername_statement` = '1', `ssl` = :ssl "); $nvh = $this->validatedData['webserver'] == 'apache2' ? '1' : '0'; $defaultip = false; if (!empty($this->validatedData['serveripv6'])) { $stmt->execute([ 'nvh' => $nvh, 'serverip' => $this->validatedData['serveripv6'], 'serverport' => 80, 'ssl' => 0 ]); $defaultip = $db_user->lastInsertId(); } if (!empty($this->validatedData['serveripv4'])) { $stmt->execute([ 'nvh' => $nvh, 'serverip' => $this->validatedData['serveripv4'], 'serverport' => 80, 'ssl' => 0 ]); $lastinsert = $db_user->lastInsertId(); $defaultip = $defaultip != false ? $defaultip . ',' . $lastinsert : $lastinsert; } $defaultsslip = false; if ($this->validatedData['use_ssl']) { if (!empty($this->validatedData['serveripv6'])) { $stmt->execute([ 'nvh' => $this->validatedData['webserver'] == 'apache2' ? '1' : '0', 'serverip' => $this->validatedData['serveripv6'], 'serverport' => 443, 'ssl' => 1 ]); $defaultsslip = $db_user->lastInsertId(); } if (!empty($this->validatedData['serveripv4'])) { $stmt->execute([ 'nvh' => $this->validatedData['webserver'] == 'apache2' ? '1' : '0', 'serverip' => $this->validatedData['serveripv4'], 'serverport' => 443, 'ssl' => 1 ]); $lastinsert = $db_user->lastInsertId(); $defaultsslip = $defaultsslip != false ? $defaultsslip . ',' . $lastinsert : $lastinsert; } } // insert the defaultip $upd_stmt = $db_user->prepare(" UPDATE `" . TABLE_PANEL_SETTINGS . "` SET `value` = :defaultip WHERE `settinggroup` = 'system' AND `varname` = :defipfld "); $upd_stmt->execute([ 'defaultip' => $defaultip, 'defipfld' => 'defaultip' ]); if ($defaultsslip) { $upd_stmt->execute([ 'defaultip' => $defaultsslip, 'defipfld' => 'defaultsslip' ]); } // last but not least create the main admin $ins_data = [ 'loginname' => $this->validatedData['admin_user'], 'password' => password_hash($this->validatedData['admin_pass'], PASSWORD_DEFAULT), 'adminname' => $this->validatedData['admin_name'], 'email' => $this->validatedData['admin_email'], 'deflang' => 'en' // TODO: set language ]; $ins_stmt = $db_user->prepare(" INSERT INTO `" . TABLE_PANEL_ADMINS . "` SET `loginname` = :loginname, `password` = :password, `name` = :adminname, `email` = :email, `def_language` = :deflang, `api_allowed` = 1, `customers` = -1, `customers_see_all` = 1, `caneditphpsettings` = 1, `domains` = -1, `change_serversettings` = 1, `diskspace` = -1024, `mysqls` = -1, `emails` = -1, `email_accounts` = -1, `email_forwarders` = -1, `email_quota` = -1, `ftps` = -1, `subdomains` = -1, `traffic` = -1048576 "); $ins_stmt->execute($ins_data); } /** * Create userdata.inc.php file * * @return void * @throws Exception */ public function createUserdataConf() { $userdata = [ 'sql' => [ 'debug' => false, 'host' => $this->validatedData['mysql_host'], 'user' => $this->validatedData['mysql_unprivileged_user'], 'password' => $this->validatedData['mysql_unprivileged_pass'], 'db' => $this->validatedData['mysql_database'], ], 'sql_root' => [ '0' => [ 'caption' => 'Default', 'host' => $this->validatedData['mysql_host'], 'user' => $this->validatedData['mysql_root_user'], 'password' => $this->validatedData['mysql_root_pass'], ] ] ]; // enable sql ssl in userdata for unprivileged and root db user if (!empty($this->validatedData['mysql_ssl_ca_file']) && isset($this->validatedData['mysql_ssl_verify_server_certificate'])) { $userdata['sql']['ssl'] = [ 'caFile' => $this->validatedData['mysql_ssl_ca_file'], 'verifyServerCertificate' => (bool)$this->validatedData['mysql_ssl_verify_server_certificate'], ]; $userdata['sql_root']['0']['ssl'] = [ 'caFile' => $this->validatedData['mysql_ssl_ca_file'], 'verifyServerCertificate' => (bool)$this->validatedData['mysql_ssl_verify_server_certificate'], ]; } // test if we can store the userdata.inc.php in ../lib $umask = @umask(077); $userdata = PhpHelper::parseArrayToPhpFile($userdata); $userdata_file = dirname(__FILE__, 5) . '/lib/userdata.inc.php'; if (@touch($userdata_file) && @is_writable($userdata_file)) { $fp = @fopen($userdata_file, 'w'); @fputs($fp, $userdata, strlen($userdata)); @fclose($fp); } else { @unlink($userdata_file); // try creating it in a temporary file $temp_file = @tempnam(sys_get_temp_dir(), 'fx'); if ($temp_file) { $fp = @fopen($temp_file, 'w'); @fputs($fp, $userdata, strlen($userdata)); @fclose($fp); } else { throw new Exception(lng('install.errors.creating_configfile_failed')); } } @umask($umask); } private function createJsonArray(&$db_user) { // use traffic analyzer and ftpserver from settings as we could define defaults in the lib/configfiles/*.xml templates // which can be useful for third-party package-maintainer (e.g. other distros) to have more control // over the installation defaults (less hardcoded values) $custom_dependency = $db_user->query(" SELECT `varname`, `value` FROM `" . TABLE_PANEL_SETTINGS . "` WHERE `settinggroup` = 'system' AND (`varname` = 'traffictool' OR `varname` = 'ftpserver') "); $cd_result = $custom_dependency->fetchAll(\PDO::FETCH_KEY_PAIR); $system_params = ["cron", "libnssextrausers", "logrotate"]; if (isset($cd_result['traffictool'])) { $system_params[] = $cd_result['traffictool']; } if ($this->validatedData['webserver_backend'] == 'php-fpm') { $system_params[] = 'php-fpm'; } elseif ($this->validatedData['webserver_backend'] == 'fcgid') { $system_params[] = 'fcgid'; } $json_params = [ 'distro' => $this->validatedData['distribution'], 'dns' => 'x', 'http' => $this->validatedData['webserver'], 'smtp' => 'postfix_dovecot', 'mail' => 'dovecot_postfix2', 'antispam' => 'rspamd', 'ftp' => $cd_result['ftpserver'] ?? 'x', 'system' => $system_params ]; $_SESSION['installation']['json_params'] = json_encode($json_params); } private function createUserdataParamStr() { $req_fields = [ 'mysql_host', 'mysql_unprivileged_user', 'mysql_unprivileged_pass', 'mysql_database', 'mysql_root_user', 'mysql_root_pass', 'mysql_ssl_ca_file', 'mysql_ssl_verify_server_certificate' ]; $json_params = []; foreach ($req_fields as $field) { $json_params[$field] = $this->validatedData[$field] ?? ""; } $_SESSION['installation']['ud_str'] = base64_encode(json_encode($json_params)); } } ================================================ FILE: lib/Froxlor/Install/Install.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Install; use Exception; use Froxlor\Config\ConfigParser; use Froxlor\Froxlor; use Froxlor\Install\Install\Core; use Froxlor\System\Cronjob; use Froxlor\System\IPTools; use Froxlor\UI\Panel\UI; use Froxlor\UI\Request; use Froxlor\Validate\Validate; use PDO; class Install { public $currentStep; public $extendedView; public $maxSteps; public $phpVersion; public $formfield; public array $suggestions = []; public array $criticals = []; public array $loadedExtensions; public array $supportedOS = []; public array $webserverBackend = [ 'php-fpm' => 'PHP-FPM', 'fcgid' => 'FCGID (apache2 only)', 'mod_php' => 'mod_php (not recommended)', ]; public function __construct(array $cliData = []) { // set actual php version and extensions $this->phpVersion = phpversion(); $this->loadedExtensions = get_loaded_extensions(); // get all supported OS // show list of available distro's $distros = glob(dirname(__DIR__, 3) . '/lib/configfiles/*.xml'); $distributions_select[''] = '-'; if (in_array('xml', $this->loadedExtensions)) { // read in all the distros foreach ($distros as $distribution) { // get configparser object $dist = new ConfigParser($distribution); // store in tmp array $this->supportedOS[str_replace(".xml", "", strtolower(basename($distribution)))] = $dist->getCompleteDistroName(); } // sort by distribution name asort($this->supportedOS); } // guess distribution and webserver to preselect in formfield $webserverBackend = $this->webserverBackend; $supportedOS = $this->supportedOS; $guessedDistribution = $this->guessDistribution(); $guessedWebserver = $this->guessWebserver(); // set formfield, so we can get the fields and steps etc. $this->formfield = require dirname(__DIR__, 3) . '/lib/formfields/install/formfield.install.php'; // set actual step $this->currentStep = $cliData['step'] ?? Request::any('step', 0); $this->extendedView = $cliData['extended'] ?? Request::any('extended', 0); $this->maxSteps = count($this->formfield['install']['sections']); if (empty($cliData)) { // set global variables UI::twig()->addGlobal('install_mode', true); UI::twig()->addGlobal('basehref', '../'); // unset session if user goes back to step 0 if (isset($_SESSION['installation']) && $this->currentStep == 0) { unset($_SESSION['installation']); } // check for url manipulation or wrong step if ((isset($_SESSION['installation']['stepCompleted']) && $this->currentStep > $_SESSION['installation']['stepCompleted']) || (!isset($_SESSION['installation']['stepCompleted']) && $this->currentStep > 0) ) { $this->currentStep = isset($_SESSION['installation']['stepCompleted']) ? $_SESSION['installation']['stepCompleted'] + 1 : 1; } } } /** * @return void * @throws Exception */ public function handle(): void { // handle form data if (!is_null(Request::any('submit')) && $this->currentStep) { try { $this->handleFormData($this->formfield['install']); } catch (Exception $e) { $error = $e->getMessage(); } } // load template UI::twigBuffer('/install/index.html.twig', [ 'setup' => [ 'step' => $this->currentStep, 'max_steps' => $this->maxSteps, ], 'preflight' => $this->checkRequirements(), 'page' => [ 'title' => 'Database', 'description' => 'Test', ], 'section' => $this->formfield['install']['sections']['step' . $this->currentStep] ?? [], 'error' => $error ?? null, 'extended' => $this->extendedView, 'csrf_token' => Froxlor::genSessionId(20), ]); // output view UI::twigOutputBuffer(); } /** * @throws Exception */ private function handleFormData(array $formfield): void { // handle current step if ($this->currentStep <= $this->maxSteps) { // Validate user data $validatedData = $this->validateRequest($formfield['sections']['step' . $this->currentStep]['fields']); if ($this->currentStep == 1) { // Check database connection $this->checkDatabase($validatedData); } elseif ($this->currentStep == 2) { // Check validity of admin user data $this->checkAdminUser($validatedData); } elseif ($this->currentStep == 3) { // Check validity of system data $this->checkSystem($validatedData); } $validatedData['stepCompleted'] = ($this->currentStep < $this->maxSteps) ? $this->currentStep : ($this->maxSteps - 1); // Store validated data for later use $_SESSION['installation'] = array_merge($_SESSION['installation'] ?? [], $validatedData); } // also handle completion of installation if it's the step before the last step if ($this->currentStep == ($this->maxSteps - 1)) { $core = new Core($_SESSION['installation']); $core->doInstall(); } // redirect user to home if the installation is done if ($this->currentStep == $this->maxSteps) { // check setting for "panel.is_configured" whether user has // run the config-services script (or checked the manual mode) if ($this->checkInstallStateFinished()) { header('Location: ../'); return; } throw new Exception(lng('install.errors.notyetconfigured')); } // redirect to next step header('Location: ?step=' . ($this->currentStep + 1)); } private function checkInstallStateFinished(): bool { $core = new Core($_SESSION['installation']); if (isset($_SESSION['installation']['manual_config']) && (int)$_SESSION['installation']['manual_config'] == 1) { $core->createUserdataConf(); return true; } $pdo = $core->getUnprivilegedPdo(); $stmt = $pdo->prepare("SELECT `value` FROM `panel_settings` WHERE `settinggroup` = 'panel' AND `varname` = 'is_configured'"); $stmt->execute(); $result = $stmt->fetch(PDO::FETCH_ASSOC); if ($result && (int)$result['value'] == 1) { $core->createUserdataConf(); return true; } return false; } /** * @return array */ public function checkRequirements(): array { // check whether we can read the userdata file if (!@touch(dirname(__DIR__, 2) . '/.~writecheck')) { // get possible owner $posixusername = posix_getpwuid(posix_getuid())['name']; $posixgroup = posix_getgrgid(posix_getgid())['name']; $this->criticals['wrong_ownership'] = ['user' => $posixusername, 'group' => $posixgroup]; } else { @unlink(dirname(__DIR__, 2) . '/.~writecheck'); } // check for required extensions foreach (Requirements::REQUIRED_EXTENSIONS as $requiredExtension) { if (in_array($requiredExtension, $this->loadedExtensions)) { continue; } $this->criticals['missing_extensions'][] = $requiredExtension; } // check for suggested extensions foreach (Requirements::SUGGESTED_EXTENSIONS as $suggestedExtension) { if (in_array($suggestedExtension, $this->loadedExtensions)) { continue; } $this->suggestions['missing_extensions'][] = $suggestedExtension; } return [ 'text' => $this->getInformationText(), 'suggestions' => $this->suggestions, 'criticals' => $this->criticals, ]; } /** * @return string */ private function getInformationText(): string { if (version_compare(Requirements::REQUIRED_VERSION, PHP_VERSION, "<")) { $text = lng('install.phpinfosuccess', [$this->phpVersion]); } else { $text = lng('install.phpinfowarn', [Requirements::REQUIRED_VERSION]); $this->criticals[] = lng('install.phpinfoupdate', [$this->phpVersion, Requirements::REQUIRED_VERSION]); } return $text; } /** * @throws Exception */ private function validateRequest(array $fields): array { $attributes = []; foreach ($fields as $name => $field) { $attributes[$name] = $this->validateAttribute(Request::any($name), $field); if (isset($field['next_to'])) { $attributes = array_merge($attributes, $this->validateRequest($field['next_to'])); } } return $attributes; } /** * @return mixed * @throws Exception */ private function validateAttribute($attribute, array $field) { // TODO: do validations if (isset($field['mandatory']) && $field['mandatory'] && empty($attribute)) { throw new Exception(lng('install.errors.mandatory_field_not_set', [$field['label']])); } return $attribute; } /** * @throws Exception */ public function checkSystem(array $validatedData): void { $serveripv4 = $validatedData['serveripv4'] ?? ''; $serveripv6 = $validatedData['serveripv6'] ?? ''; $servername = $validatedData['servername'] ?? ''; $httpuser = $validatedData['httpuser'] ?? 'www-data'; $httpgroup = $validatedData['httpgroup'] ?? 'www-data'; if (empty($serveripv4) && empty($serveripv6)) { throw new Exception(lng('install.errors.nov4andnov6ip')); } elseif (!empty($serveripv4) && (!Validate::validate_ip2($serveripv4, true, '', false, true) || IPTools::is_ipv6($serveripv4))) { throw new Exception(lng('error.invalidip', [$serveripv4])); } elseif (!empty($serveripv6) && (!Validate::validate_ip2($serveripv6, true, '', false, true) || !IPTools::is_ipv6($serveripv6))) { throw new Exception(lng('error.invalidip', [$serveripv6])); } elseif (!Validate::validateDomain($servername)) { throw new Exception(lng('install.errors.servernameneedstobevalid')); } elseif (posix_getpwnam($httpuser) === false) { throw new Exception(lng('install.errors.websrvuserdoesnotexist')); } elseif (posix_getgrnam($httpgroup) === false) { throw new Exception(lng('install.errors.websrvgrpdoesnotexist')); } } /** * @throws Exception */ public function checkAdminUser(array $validatedData): void { $name = $validatedData['admin_name'] ?? 'Administrator'; $loginname = $validatedData['admin_user'] ?? ''; $email = $validatedData['admin_email'] ?? ''; $password = $validatedData['admin_pass'] ?? ''; $password_confirm = $validatedData['admin_pass_confirm'] ?? ''; $useadminmailassender = $validatedData['use_admin_email_as_sender'] ?? '1'; $senderemail = $validatedData['sender_email'] ?? ''; if (!preg_match('/^[^\r\n\t\f\0]*$/D', $name)) { throw new Exception(lng('error.stringformaterror', ['admin_name'])); } elseif (empty(trim($loginname)) || !preg_match('/^[a-z][a-z0-9]+$/Di', $loginname)) { throw new Exception(lng('error.loginnameiswrong', [$loginname])); } elseif (empty(trim($email)) || !Validate::validateEmail($email)) { throw new Exception(lng('error.emailiswrong', [$email])); } elseif ((int)$useadminmailassender == 0 && !empty(trim($senderemail)) && !Validate::validateEmail($senderemail)) { throw new Exception(lng('error.emailiswrong', [$senderemail])); } elseif (empty($password) || $password != $password_confirm) { throw new Exception(lng('error.newpasswordconfirmerror')); } elseif ($password == $loginname) { throw new Exception(lng('error.passwordshouldnotbeusername')); } } /** * @throws Exception */ public function checkDatabase(array $validatedData): void { $dsn = sprintf('mysql:host=%s;charset=utf8', $validatedData['mysql_host']); $pdo = new \PDO($dsn, $validatedData['mysql_root_user'], $validatedData['mysql_root_pass']); // check if the database already exist $stmt = $pdo->prepare('SHOW DATABASES LIKE ?'); $stmt->execute([ $validatedData['mysql_database'] ]); $hasDatabase = $stmt->fetch(); if ($hasDatabase && !$validatedData['mysql_force_create']) { throw new Exception(lng('install.errors.databaseexists')); } // check if we can create a new database $testDatabase = uniqid('froxlor_tmp_'); if ($pdo->exec('CREATE DATABASE IF NOT EXISTS ' . $testDatabase . ';') === false) { throw new Exception(lng('install.errors.unabletocreatedb')); } if ($pdo->exec('DROP DATABASE IF EXISTS ' . $testDatabase . ';') === false) { throw new Exception(lng('install.errors.unabletodropdb')); } // check if the user already exist $stmt = $pdo->prepare("SELECT `User` FROM `mysql`.`user` WHERE `User` = ?"); $stmt->execute([$validatedData['mysql_unprivileged_user']]); if ($stmt->rowCount() && !$validatedData['mysql_force_create']) { throw new Exception(lng('install.errors.mysqlusernameexists')); } // check if we can create a new user $testUser = uniqid('froxlor_tmp_'); $stmt = $pdo->prepare('CREATE USER ?@? IDENTIFIED BY ?'); if ($stmt->execute([$testUser, $validatedData['mysql_host'], uniqid()]) === false) { throw new Exception(lng('install.errors.unabletocreateuser')); } $stmt = $pdo->prepare('DROP USER ?@?'); if ($stmt->execute([$testUser, $validatedData['mysql_host']]) === false) { throw new Exception(lng('install.errors.unabletodropuser')); } if ($pdo->prepare('FLUSH PRIVILEGES')->execute() === false) { throw new Exception(lng('install.errors.unabletoflushprivs')); } } private function guessWebserver(): ?string { if (strtoupper(@php_sapi_name()) == "APACHE2HANDLER" || stristr($_SERVER['SERVER_SOFTWARE'], "apache")) { return 'apache24'; } elseif (substr(strtoupper(@php_sapi_name()), 0, 8) == "NGINX" || stristr($_SERVER['SERVER_SOFTWARE'], "nginx")) { return 'nginx'; } return null; } private function guessDistribution(): ?string { return Cronjob::checkCurrentDistro(true); } } ================================================ FILE: lib/Froxlor/Install/Preconfig.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Install; use Froxlor\Froxlor; use Froxlor\Settings; class Preconfig { private $preconfig_data = []; /** * returns whether there are preconfig items in an update * * @return bool */ public function hasPreConfig(): bool { return count($this->preconfig_data) > 0; } /** * return all collected preconfig data * * @return array */ public function getData(): array { return $this->preconfig_data; } /** * adds an preconfig result-array to the preconfig-data * * @param array $array * * @return void */ public function addToPreConfig(array $array) { if (isset($array['title']) && isset($array['fields']) && count($array['fields']) > 0) { $this->preconfig_data[] = $array; } } /** * read in all preconfig files and build up data-array for admin_updates */ public function __construct() { $preconfigs = glob(Froxlor::getInstallDir() . '/install/updates/preconfig/*.php'); if (!empty($preconfigs)) { $current_version = Settings::Get('panel.version'); $current_db_version = Settings::Get('panel.db_version'); if (empty($current_db_version)) { $current_db_version = "0"; } foreach (array_reverse($preconfigs) as $preconfig_file) { $pconf = include $preconfig_file; $this->addToPreConfig($pconf); } } } /** * Function getPreConfig * * outputs various form-field-arrays before the update process * can be continued (asks for agreement whatever is being asked) * * @param bool $no_check * @return array */ public static function getPreConfig(bool $no_check = false): array { $preconfig = new self(); if ($preconfig->hasPreConfig()) { if (!$no_check) { $agree = [ 'title' => 'Check', 'fields' => [ 'update_changesagreed' => ['mandatory' => true, 'type' => 'checkrequired', 'value' => 1, 'label' => 'I have read the update notifications above and I am aware of the changes made to my system.'], 'update_preconfig' => ['type' => 'hidden', 'value' => 1] ] ]; $preconfig->addToPreConfig($agree); } return $preconfig->getData(); } return []; } } ================================================ FILE: lib/Froxlor/Install/Requirements.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Install; use Froxlor\FileDir; use Froxlor\Froxlor; use Froxlor\FroxlorLogger; use Froxlor\Settings; class Update { private static $update_tasks = []; private static $task_counter = 0; /** * Function showUpdateStep * * stores and logs the current update progress * * @param string $task * @param bool $needs_status (if false, a linebreak will be added) * * @return void */ public static function showUpdateStep(string $task, bool $needs_status = true) { set_time_limit(30); // output self::$update_tasks[self::$task_counter] = ['title' => $task, 'result' => 0]; if (!$needs_status) { self::$task_counter++; } FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::ADM_ACTION, \LOG_WARNING, $task); } /** * Function lastStepStatus * * outputs status of the last update-step * * @param int $status (0 = success, 1 = warning, 2 = failure) * @param string $message * @param string $additional_info * * @return void */ public static function lastStepStatus(int $status = -1, string $message = 'OK', string $additional_info = '') { self::$update_tasks[self::$task_counter]['result_txt'] = $message; self::$update_tasks[self::$task_counter]['result_desc'] = $additional_info; switch ($status) { case 0: break; case 1: self::$update_tasks[self::$task_counter]['result'] = 2; break; case 2: self::$update_tasks[self::$task_counter]['result'] = 1; break; default: self::$update_tasks[self::$task_counter]['result'] = -1; break; } self::$task_counter++; if ($status == -1 || $status == 2) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::ADM_ACTION, \LOG_WARNING, 'Attention - last update task failed!!!'); } elseif ($status == 0 || $status == 1) { FroxlorLogger::getInstanceOf()->logAction(FroxlorLogger::ADM_ACTION, \LOG_WARNING, 'Success'); } } public static function versionInUpdate($current_version, $version_to_check) { if (!Froxlor::isFroxlor()) { return true; } return Froxlor::versionCompare2($current_version, $version_to_check) == -1; } public static function storeUpdateCheckData(array $response) { $data = [ 'ts' => time(), 'channel' => Settings::Get('system.update_channel'), 'data' => $response ]; Settings::Set('system.updatecheck_data', json_encode($data)); } public static function getUpdateCheckData() { $uc_data = Settings::Get('system.updatecheck_data'); if (!empty($uc_data)) { $data = json_decode($uc_data, true); return $data; } return null; } public static function getUpdateTasks(): array { return self::$update_tasks; } public static function getTaskCounter(): int { return self::$task_counter; } public static function cleanOldFiles(array $to_clean) { self::showUpdateStep("Cleaning up old files"); $disabled = explode(',', ini_get('disable_functions')); $exec_allowed = !in_array('exec', $disabled); $del_list = ""; foreach ($to_clean as $filedir) { $complete_filedir = Froxlor::getInstallDir() . $filedir; if (file_exists($complete_filedir)) { if ($exec_allowed) { FileDir::safe_exec("rm -rf " . escapeshellarg($complete_filedir)); } else { $del_list .= "rm -rf " . escapeshellarg($complete_filedir) . PHP_EOL; } } } if ($exec_allowed) { self::lastStepStatus(0); } else { if (empty($del_list)) { // none of the files existed self::lastStepStatus(0); } else { self::lastStepStatus( 1, 'manual commands needed', 'Please run the following commands manually:
' . $del_list . '
' ); } } } } ================================================ FILE: lib/Froxlor/Language.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor; use RecursiveArrayIterator; use RecursiveIteratorIterator; class Language { protected static ?array $lng = null; protected static string $defaultLanguage = 'en'; protected static ?string $requestedLanguage = null; /** * @return array */ public static function getLanguages(): array { $languages = []; $directory = dirname(__DIR__, 2) . '/lng'; foreach (array_diff(scandir($directory), ['..', '.', 'index.html']) as $language) { $iso = explode('.', $language)[0]; $languages[$iso] = self::getTranslation('languages.' . $iso); } return $languages; } public static function getTranslation(string $identifier, array $arguments = []) { // initialize if (is_null(self::$lng)) { // load fallback language self::$lng = self::loadLanguage(self::$defaultLanguage); // load user requested language if (self::$requestedLanguage) { self::$lng = array_merge(self::$lng, self::loadLanguage(self::$requestedLanguage)); } // load fallback from browser if nothing requested $iso = trim(substr(strtok(strtok(($_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? 'en'), ','), ';'), 0, 5)); if (!self::$requestedLanguage && strlen($iso) == 2 && $iso !== self::$defaultLanguage) { self::$lng = array_merge(self::$lng, self::loadLanguage($iso)); } } // shortcut for identifier with => [title, description] if (!isset(self::$lng[$identifier]) && isset(self::$lng[$identifier . '.title'])) { return [ 'title' => vsprintf(self::$lng[$identifier . '.title'] ?? $identifier, $arguments), 'description' => vsprintf(self::$lng[$identifier . '.description'] ?? $identifier, $arguments), ]; } // search by identifier return vsprintf(self::$lng[$identifier] ?? $identifier, $arguments); } /** * @TODO: Possible iso: de, de-DE, de-AT (fallback to de) * * @param $iso * @return array */ private static function loadLanguage($iso): array { // Reject path traversal attempts if ($iso !== basename($iso) || str_contains($iso, '..')) { return []; } $languageFile = dirname(__DIR__, 2) . sprintf('/lng/%s.lng.php', $iso); if (!file_exists($languageFile)) { return []; } // load default language $lng = require $languageFile; // multidimensional array to dot notation keys $reItIt = new RecursiveIteratorIterator(new RecursiveArrayIterator($lng)); $result = []; foreach ($reItIt as $leafValue) { $keys = []; foreach (range(0, $reItIt->getDepth()) as $depth) { $keys[] = $reItIt->getSubIterator($depth)->key(); } $result[join('.', $keys)] = $leafValue; } return $result; } public static function setDefaultLanguage(string $string) { self::$defaultLanguage = $string; } public static function setLanguage(string $string) { self::$requestedLanguage = $string; self::$lng = null; } } ================================================ FILE: lib/Froxlor/MailLogParser.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor; use Exception; use Froxlor\Database\Database; use PDO; class MailLogParser { private $startTime; private $domainTraffic = []; private $myDomains = []; private $mails = []; /** * constructor * * @param * string logFile * @param * int startTime * @param * string logFileExim */ public function __construct($startTime = 0) { $this->startTime = $startTime; // Get all domains from Database $stmt = Database::prepare("SELECT domain FROM `" . TABLE_PANEL_DOMAINS . "`"); Database::pexecute($stmt, []); while ($domain_row = $stmt->fetch(PDO::FETCH_ASSOC)) { $this->myDomains[] = $domain_row["domain"]; } // Parse MTA traffic if (Settings::Get("system.mtaserver") == "postfix") { $this->parsePostfixLog(Settings::Get("system.mtalog")); $this->parsePostfixLog(Settings::Get("system.mtalog") . ".1"); } elseif (Settings::Get("system.mtaserver") == "exim4") { $this->parseExim4Log(Settings::Get("system.mtalog")); } // Parse MDA traffic if (Settings::Get("system.mdaserver") == "dovecot") { $this->parseDovecotLog(Settings::Get("system.mdalog")); $this->parseDovecotLog(Settings::Get("system.mdalog") . ".1"); } elseif (Settings::Get("system.mdaserver") == "courier") { $this->parseCourierLog(Settings::Get("system.mdalog")); $this->parseCourierLog(Settings::Get("system.mdalog") . ".1"); } } /** * parsePostfixLog * parses the traffic from a postfix logfile * * @param string $logFile * logFile */ private function parsePostfixLog($logFile) { // Check if file exists if (!file_exists($logFile)) { return false; } // Open the log file try { $file_handle = fopen($logFile, "r"); if (!$file_handle) { throw new Exception("Could not open the file!"); } } catch (Exception $e) { echo "Error (File: " . $e->getFile() . ", line " . $e->getLine() . "): " . $e->getMessage(); return false; } while (!feof($file_handle)) { unset($matches); $line = fgets($file_handle); if (strpos($line, 'postfix') === false) { continue; } $timestamp = $this->getLogTimestamp($line); if ($this->startTime < $timestamp) { if (preg_match("/postfix\/qmgr.*(?::|\])\s([A-Z\d]+).*from=?, size=(\d+),/", $line, $matches)) { // Postfix from $this->mails[$matches[1]] = [ "domainFrom" => strtolower($matches[2]), "size" => $matches[3] ]; } elseif (preg_match("/postfix\/(?:pipe|smtp|lmtp).*(?::|\])\s([A-Z\d]+).*to=?,/", $line, $matches)) { // Postfix to if (array_key_exists($matches[1], $this->mails)) { $this->mails[$matches[1]]["domainTo"] = strtolower($matches[2]); // Only mails from/to outside the system should be added $mail = $this->mails[$matches[1]]; if (in_array($mail["domainFrom"], $this->myDomains) || in_array($mail["domainTo"], $this->myDomains)) { // Outgoing traffic if (array_key_exists("domainFrom", $mail)) { $this->addDomainTraffic($mail["domainFrom"], $mail["size"], $timestamp); } // Incoming traffic if (array_key_exists("domainTo", $mail) && in_array($mail["domainTo"], $this->myDomains)) { $this->addDomainTraffic($mail["domainTo"], $mail["size"], $timestamp); } } unset($mail); } } } } fclose($file_handle); return true; } /** * getLogTimestamp * * @param * string line * return int */ private function getLogTimestamp($line) { $matches = null; if (preg_match("/((?:[A-Z]{3}\s{1,2}\d{1,2}|\d{4}-\d{2}-\d{2}).\d{2}:\d{2}:\d{2})/i", $line, $matches)) { $timestamp = strtotime($matches[1]); if ($timestamp > ($this->startTime + 60 * 60 * 24)) { return strtotime($matches[1] . " -1 year"); } else { return strtotime($matches[1]); } } else { return 0; } } /** * _addDomainTraffic * adds the traffic to the domain array if we own the domain * * @param * string domain * @param * int traffic */ private function addDomainTraffic($domain, $traffic, $timestamp) { $date = date("Y-m-d", $timestamp); if (in_array($domain, $this->myDomains)) { if (array_key_exists($domain, $this->domainTraffic) && array_key_exists($date, $this->domainTraffic[$domain])) { $this->domainTraffic[$domain][$date] += (int)$traffic; } else { if (!array_key_exists($domain, $this->domainTraffic)) { $this->domainTraffic[$domain] = []; } $this->domainTraffic[$domain][$date] = (int)$traffic; } } } /** * parseExim4Log * parses the smtp traffic from a exim4 logfile * * @param string $logFile * logFile */ private function parseExim4Log($logFile) { // Check if file exists if (!file_exists($logFile)) { return false; } // Open the log file try { $file_handle = fopen($logFile, "r"); if (!$file_handle) { throw new Exception("Could not open the file!"); } } catch (Exception $e) { echo "Error (File: " . $e->getFile() . ", line " . $e->getLine() . "): " . $e->getMessage(); return false; } while (!feof($file_handle)) { unset($matches); $line = fgets($file_handle); $timestamp = $this->getLogTimestamp($line); if ($this->startTime < $timestamp) { if (preg_match("/<= .*@([a-z0-9.\-]+) .*S=(\d+)/i", $line, $matches)) { // Outgoing traffic $this->addDomainTraffic($matches[1], $matches[2], $timestamp); } elseif (preg_match("/=> .*? .*S=(\d+)/i", $line, $matches)) { // Incoming traffic $this->addDomainTraffic($matches[1], $matches[2], $timestamp); } } } fclose($file_handle); return true; } /** * parseDovecotLog * parses the dovecot imap/pop3 traffic from logfile * * @param string $logFile * logFile */ private function parseDovecotLog($logFile) { // Check if file exists if (!file_exists($logFile)) { return false; } // Open the log file try { $file_handle = fopen($logFile, "r"); if (!$file_handle) { throw new Exception("Could not open the file!"); } } catch (Exception $e) { echo "Error (File: " . $e->getFile() . ", line " . $e->getLine() . "): " . $e->getMessage(); return false; } while (!feof($file_handle)) { unset($matches); $line = fgets($file_handle); if (strpos($line, 'dovecot') === false) { continue; } $timestamp = $this->getLogTimestamp($line); if ($this->startTime < $timestamp) { if (preg_match("/dovecot.*(?::|\]) imap\(.*@([a-z0-9\.\-]+)\)(<\d+><[a-z0-9+\/=]+>)?:.*(?:in=(\d+) out=(\d+)|bytes=(\d+)\/(\d+))/i", $line, $matches)) { // Dovecot IMAP $this->addDomainTraffic($matches[1], (int)$matches[3] + (int)$matches[4], $timestamp); } elseif (preg_match("/dovecot.*(?::|\]) pop3\(.*@([a-z0-9\.\-]+)\)(<\d+><[a-z0-9+\/=]+>)?:.*in=(\d+).*out=(\d+)/i", $line, $matches)) { // Dovecot POP3 $this->addDomainTraffic($matches[1], (int)$matches[3] + (int)$matches[4], $timestamp); } } } fclose($file_handle); return true; } /** * parseCourierLog * parses the dovecot imap/pop3 traffic from logfile * * @param string $logFile * logFile */ private function parseCourierLog($logFile) { // Check if file exists if (!file_exists($logFile)) { return false; } // Open the log file try { $file_handle = fopen($logFile, "r"); if (!$file_handle) { throw new Exception("Could not open the file!"); } } catch (Exception $e) { echo "Error (File: " . $e->getFile() . ", line " . $e->getLine() . "): " . $e->getMessage(); return false; } while (!feof($file_handle)) { unset($matches); $line = fgets($file_handle); $timestamp = $this->getLogTimestamp($line); if ($this->startTime < $timestamp) { if (preg_match("/(?:imapd|pop3d)(?:-ssl)?.*(?::|\]).*user=.*@([a-z0-9\.\-]+),.*rcvd=(\d+), sent=(\d+),/i", $line, $matches)) { // Courier IMAP & POP3 $this->addDomainTraffic($matches[1], (int)$matches[2] + (int)$matches[3], $timestamp); } } } fclose($file_handle); return true; } /** * getDomainTraffic * returns the traffic of a given domain or 0 if the domain has no traffic * * @param * string domain * return array */ public function getDomainTraffic($domain) { if (array_key_exists($domain, $this->domainTraffic)) { return $this->domainTraffic[$domain]; } else { return 0; } } } ================================================ FILE: lib/Froxlor/PhpHelper.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor; use Exception; use Froxlor\UI\Panel\UI; use Net_DNS2_Exception; use Net_DNS2_Resolver; use Throwable; use voku\helper\AntiXSS; class PhpHelper { /** * Wrapper around htmlentities to handle arrays, with the advantage that you * can select which fields should be handled by htmlentities * * @param array|string $subject The subject array * @param array|string $fields The fields which should be checked for, separated by spaces * @param int $quote_style See php documentation about this * @param string $charset See php documentation about this * * @return array|string The string or an array with htmlentities converted strings * @author Florian Lippert (2003-2009) */ public static function htmlentitiesArray($subject, $fields = '', $quote_style = ENT_QUOTES, $charset = 'UTF-8') { if (is_array($subject)) { if (!is_array($fields)) { $fields = self::arrayTrim(explode(' ', $fields)); } foreach ($subject as $field => $value) { if ((!is_array($fields) || empty($fields)) || (in_array($field, $fields))) { // Just call ourselve to manage multi-dimensional arrays $subject[$field] = self::htmlentitiesArray($value, $fields, $quote_style, $charset); } } } else { $subject = empty($subject) ? "" : htmlentities($subject, $quote_style, $charset); } return $subject; } /** * Returns array with all empty-values removed * * @param array $source The array to trim * @return array The trim'med array */ public static function arrayTrim(array $source): array { $source = array_map('trim', $source); return array_filter($source, function ($value) { return $value !== ''; }); } /** * froxlor php error handler * * @param int $errno * @param string $errstr * @param string $errfile * @param int $errline * * @return void|boolean */ public static function phpErrHandler($errno, $errstr, $errfile, $errline) { if (!(error_reporting() & $errno)) { // This error code is not included in error_reporting return; } if (!isset($_SERVER['SHELL']) || (isset($_SERVER['SHELL']) && $_SERVER['SHELL'] == '')) { // prevent possible file-path-disclosure $errfile = str_replace(Froxlor::getInstallDir(), "", $errfile); // build alert $type = 'danger'; if ($errno == E_NOTICE || $errno == E_DEPRECATED || $errno == E_STRICT) { $type = 'info'; } elseif ($errno = E_WARNING) { $type = 'warning'; } $err_display = ''; // set errors to session ErrorBag::addError($err_display); // return true to ignore php standard error-handler return true; } // of on shell, use the php standard error-handler return false; } /** * @param Throwable $exception * @return void */ public static function phpExceptionHandler(Throwable $exception) { if (!isset($_SERVER['SHELL']) || $_SERVER['SHELL'] == '') { // show UI::initTwig(true); UI::twig()->addGlobal('install_mode', '1'); UI::view('misc/alert_nosession.html.twig', [ 'page_title' => 'Uncaught exception', 'heading' => 'Uncaught exception', 'type' => 'danger', 'alert_msg' => $exception->getCode() . ' ' . $exception->getMessage(), 'alert_info' => $exception->getTraceAsString() ]); die(); } } /** * @param ...$configdirs * @return array|null */ public static function loadConfigArrayDir(...$configdirs) { if (count($configdirs) <= 0) { return null; } $data = []; $data_files = []; $has_data = false; foreach ($configdirs as $data_dirname) { if (is_dir($data_dirname)) { $data_dirhandle = opendir($data_dirname); while (false !== ($data_filename = readdir($data_dirhandle))) { if ($data_filename != '.' && $data_filename != '..' && $data_filename != '' && substr($data_filename, -4) == '.php' ) { $data_files[] = $data_dirname . $data_filename; } } $has_data = true; } } if ($has_data) { sort($data_files); foreach ($data_files as $data_filename) { $data = array_merge_recursive($data, include $data_filename); } } return $data; } /** * ipv6 aware gethostbynamel function * * @param string $host * @param boolean $try_a default true * @param string|null $nameserver set additional resolver nameserver to use (e.g. 1.1.1.1) * @return bool|array */ public static function gethostbynamel6(string $host, bool $try_a = true, ?string $nameserver = null) { $ips = []; try { // set the default nameservers to use, use the system default if none are provided $resolver = new Net_DNS2_Resolver($nameserver ? ['nameservers' => [$nameserver]] : []); // get all ip addresses from the A record and normalize them if ($try_a) { try { $answer = $resolver->query($host, 'A')->answer; foreach ($answer as $rr) { if ($rr instanceof \Net_DNS2_RR_A) { $ips[] = inet_ntop(inet_pton($rr->address)); } } } catch (Net_DNS2_Exception $e) { // we can't do anything here, just continue } } // get all ip addresses from the AAAA record and normalize them try { $answer = $resolver->query($host, 'AAAA')->answer; foreach ($answer as $rr) { if ($rr instanceof \Net_DNS2_RR_AAAA) { $ips[] = inet_ntop(inet_pton($rr->address)); } } } catch (Net_DNS2_Exception $e) { // we can't do anything here, just continue } } catch (Net_DNS2_Exception $e) { // fallback to php's dns_get_record if Net_DNS2 has no resolver available, but this may cause // problems if the system's dns is not configured correctly; for example, the acme pre-check // will fail because some providers put a local ip in /etc/hosts // get all ip addresses from the A record and normalize them if ($try_a) { $answer = @dns_get_record($host, DNS_A); foreach ($answer as $rr) { $ips[] = inet_ntop(inet_pton($rr['ip'])); } } // get all ip addresses from the AAAA record and normalize them $answer = @dns_get_record($host, DNS_AAAA); foreach ($answer as $rr) { $ips[] = inet_ntop(inet_pton($rr['ipv6'])); } } return count($ips) > 0 ? $ips : false; } /** * Function randomStr * * generate a pseudo-random string of bytes * * @param int $length * @return string * @throws Exception */ public static function randomStr(int $length): string { if (function_exists('openssl_random_pseudo_bytes')) { return openssl_random_pseudo_bytes($length); } return random_bytes($length); } /** * Return human-readable sizes * * @param int $size size in bytes * @param ?string $max maximum unit * @param string $system 'si' for SI, 'bi' for binary prefixes * @param string $retstring string-format * * @return string */ public static function sizeReadable( $size, ?string $max = '', string $system = 'si', string $retstring = '%01.2f %s' ): string { // Pick units $systems = [ 'si' => [ 'prefix' => [ 'B', 'KB', 'MB', 'GB', 'TB', 'PB' ], 'size' => 1000 ], 'bi' => [ 'prefix' => [ 'B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB' ], 'size' => 1024 ] ]; $sys = $systems[$system] ?? $systems['si']; // Max unit to display $depth = count($sys['prefix']) - 1; if ($max && false !== $d = array_search($max, $sys['prefix'])) { $depth = $d; } // Loop $i = 0; while ($size >= $sys['size'] && $i < $depth) { $size /= $sys['size']; $i++; } return sprintf($retstring, $size, $sys['prefix'][$i]); } /** * Replaces all occurrences of variables defined in the second argument * in the first argument with their values. * * @param string $text The string that should be searched for variables * @param array $vars The array containing the variables with their values * * @return string The submitted string with the variables replaced. */ public static function replaceVariables(string $text, array $vars): string { $pattern = "/\{([a-zA-Z0-9\-_]+)\}/"; $matches = []; if (count($vars) > 0 && preg_match_all($pattern, $text, $matches)) { for ($i = 0; $i < count($matches[1]); $i++) { $current = $matches[1][$i]; if (isset($vars[$current])) { $var = $vars[$current]; $text = str_replace("{" . $current . "}", $var, $text); } } } return str_replace('\n', "\n", $text); } /** * @param string $needle * @param array $haystack * @param array $keys * @param string $currentKey * @return true */ public static function recursive_array_search( string $needle, array $haystack, array &$keys = [], string $currentKey = '' ): bool { foreach ($haystack as $key => $value) { if (empty($value)) { continue; } $pathkey = empty($currentKey) ? $key : $currentKey . '.' . $key; if (is_array($value)) { self::recursive_array_search($needle, $value, $keys, $pathkey); } else { if (stripos($value, $needle) !== false) { $keys[] = $pathkey; } } } return true; } /** * function to check a super-global passed by reference, * so it gets automatically updated * * @param array $global * @param AntiXSS $antiXss */ public static function cleanGlobal(array &$global, AntiXSS &$antiXss) { $ignored_fields = [ 'system_default_vhostconf', 'system_default_sslvhostconf', 'system_apache_globaldiropt', 'specialsettings', 'ssl_specialsettings', 'default_vhostconf_domain', 'ssl_default_vhostconf_domain', 'filecontent', 'admin_password', 'password', 'new_customer_password', 'privileged_password', 'email_password', 'directory_password', 'ftp_password', 'mysql_password', 'mysql_root_pass', 'mysql_unprivileged_pass', 'admin_pass', 'admin_pass_confirm', 'panel_password_special_char', 'old_password', 'new_password', 'new_password_confirm', ]; if (!empty($global)) { $tmp = $global; foreach ($tmp as $index => $value) { if (!in_array($index, $ignored_fields)) { $global[$index] = $antiXss->xss_clean($value); } } } } /** * Generate php file from array. * * @param array $array * @param string|null $comment * @param bool $asReturn * @return string */ public static function parseArrayToPhpFile(array $array, ?string $comment = null, bool $asReturn = false): string { $str = sprintf(" $arr) { $str .= sprintf("\$%s = %s;\n", $var, rtrim(self::parseArrayToString($arr), "\n,")); } return $str; } /** * Parse array to array string. * * @param array $array * @param ?string $key * @param int $depth * @return string */ public static function parseArrayToString(array $array, ?string $key = null, int $depth = 1): string { $str = ''; if (!is_null($key)) { $str .= self::tabPrefix(($depth - 1), "'{$key}' => [\n"); } else { $str .= self::tabPrefix(($depth - 1), "[\n"); } foreach ($array as $key => $value) { if (!is_array($value)) { if (is_bool($value)) { $str .= self::tabPrefix($depth, sprintf("'%s' => %s,\n", $key, $value ? 'true' : 'false')); } elseif (is_int($value)) { $str .= self::tabPrefix($depth, "'{$key}' => $value,\n"); } else { if ($key == 'password') { // special case for passwords (nowdoc) $str .= self::tabPrefix($depth, "'{$key}' => <<<'EOT'\n{$value}\nEOT,\n"); } else { // escape backslashes first, then single quotes: $escaped = str_replace(['\\', "'"], ['\\\\', "\\'"], $value); $str .= self::tabPrefix($depth, "'{$key}' => '{$escaped}',\n"); } } } else { $str .= self::parseArrayToString($value, $key, ($depth + 1)); } } $str .= self::tabPrefix(($depth - 1), "],\n"); return $str; } /** * Apply tabs with given depth to string. * * @param int $depth * @param string $str * @return string */ private static function tabPrefix(int $depth, string $str = ''): string { $tab = ''; for ($i = 1; $i <= $depth; $i++) { $tab .= "\t"; } return $tab . $str; } public static function array_merge_recursive_distinct(array &$array1, array &$array2) { $merged = $array1; foreach ($array2 as $key => &$value) { if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) { $merged[$key] = self::array_merge_recursive_distinct($merged[$key], $value); } else { $merged[$key] = $value; } } return $merged; } } ================================================ FILE: lib/Froxlor/SImExporter.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor; use Exception; use Froxlor\Database\Database; use Froxlor\UI\Form; use Froxlor\Validate\Validate; use PDO; /** * Class SImExporter * * Import/Export settings to JSON */ class SImExporter { /** * settings which are not being exported * * @var array */ private static $no_export = [ 'panel.adminmail', 'admin.show_news_feed', 'system.lastaccountnumber', 'system.lastguid', 'system.ipaddress', 'system.last_traffic_run', 'system.hostname', 'system.mysql_access_host', 'system.lastcronrun', 'system.defaultip', 'system.defaultsslip', 'system.last_tasks_run', 'system.last_archive_run', 'system.leprivatekey', 'system.lepublickey', 'system.updatecheck_data', ]; public static function export() { $settings_definitions = []; foreach (PhpHelper::loadConfigArrayDir(Froxlor::getInstallDir() . '/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 "); $_data = []; while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { $index = $row['settinggroup'] . "." . $row['varname']; 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; } public static function import($json_str = null) { // decode data $_data = json_decode($json_str, true); if ($_data) { // get validity check data $_sha = isset($_data['_sha']) ? $_data['_sha'] : false; $_version = isset($_data['panel.version']) ? $_data['panel.version'] : false; $_dbversion = isset($_data['panel.db_version']) ? $_data['panel.db_version'] : false; // check if we have everything we need if (!$_sha || !$_version || !$_dbversion) { throw new Exception("Invalid froxlor settings data. Unable to import."); } // validate import file unset($_data['_sha']); // compare if ($_sha != sha1(var_export($_data, true))) { throw new Exception("SHA check of import data failed. Unable to import."); } // do not import version info - but we need that to possibly update settings // when there were changes in the variable-name or similar unset($_data['panel.version']); unset($_data['panel.db_version']); // validate we got ssl enabled ips when ssl is enabled // otherwise deactivate it if ($_data['system.use_ssl'] == 1) { $result_ssl_ipsandports_stmt = Database::prepare(" SELECT COUNT(*) as count_ssl_ip FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE `ssl`='1' "); $result = Database::pexecute_first($result_ssl_ipsandports_stmt); if ($result['count_ssl_ip'] <= 0) { // no ssl-ip -> deactivate $_data['system.use_ssl'] = 0; // deactivate other ssl-related settings $_data['system.leenabled'] = 0; $_data['system.le_froxlor_enabled'] = 0; $_data['system.le_froxlor_redirect'] = 0; } } $form_data = []; $image_data = []; // read in all current settings $current_settings = Settings::getAll(); foreach ($current_settings as $setting_group => $setting) { foreach ($setting as $varname => $value) { // set all group/varname:values which are not in the import file if (!array_key_exists($setting_group . '.' . $varname, $_data)) { $_data[$setting_group . '.' . $varname] = $value; } } } // re-format the array-key for Form::processForm foreach ($_data as $key => $value) { $index_split = explode('.', $key, 3); if (!isset($current_settings[$index_split[0]][$index_split[1]])) { continue; } if (isset($index_split[2]) && $index_split[2] === 'image_data' && !empty($_data[$index_split[0] . '.' . $index_split[1]])) { $image_data[$key] = $value; } else { $form_data[str_replace(".", "_", $key)] = $value; } } // store new data $settings_data = PhpHelper::loadConfigArrayDir(Froxlor::getInstallDir() . '/actions/admin/settings/'); Settings::loadSettingsInto($settings_data); if (Form::processForm($settings_data, $form_data, [], null, true)) { // save to DB Settings::Flush(); // Process image_data and save it if (count($image_data) > 0) { foreach ($image_data as $index => $value) { $index_split = explode('.', $index, 3); $path = 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"); } } if (Validate::validateBase64Image($value)) { $img_data = base64_decode($value); $img_filename = explode('?', $_data[$index_split[0] . '.' . $index_split[1]], 2)[0]; $spl = explode('.', $img_filename); $file_extension = strtolower(array_pop($spl)); unset($spl); if (!in_array($file_extension, [ 'jpeg', 'jpg', 'png', 'gif' ])) { throw new Exception("Invalid file-extension, use one of: jpeg, jpg, png, gif"); } $img_filename = 'img/' . bin2hex(random_bytes(16)) . '.' . $file_extension; file_put_contents(Froxlor::getInstallDir() . '/' . $img_filename, $img_data); $img_index = $index_split[0].'.'.$index_split[1]; Settings::Set($img_index, $img_filename . '?v=' . time()); } } } // all good return true; } else { throw new Exception("Importing settings failed"); } } throw new Exception("Invalid JSON data: " . json_last_error_msg()); } } ================================================ FILE: lib/Froxlor/Settings/FroxlorVhostSettings.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Settings; use Froxlor\Database\Database; class FroxlorVhostSettings { /** * @param bool $need_ssl * * @return bool * @throws \Exception */ public static function hasVhostContainerEnabled(bool $need_ssl = false): bool { $sel_stmt = Database::prepare("SELECT COUNT(*) as vcentries FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE `vhostcontainer`= '1'" . ($need_ssl ? " AND `ssl` = '1'" : "")); $result = Database::pexecute_first($sel_stmt); if ($result) { return $result['vcentries'] > 0; } return false; } } ================================================ FILE: lib/Froxlor/Settings/Store.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Settings; use Exception; use Froxlor\Cron\TaskId; use Froxlor\Database\Database; use Froxlor\Database\DbManager; use Froxlor\FileDir; use Froxlor\Froxlor; use Froxlor\Idna\IdnaWrapper; use Froxlor\PhpHelper; use Froxlor\Settings; use Froxlor\System\Cronjob; use Froxlor\System\IPTools; use Froxlor\UI\Request; use Froxlor\Validate\Validate; use PDO; class Store { public static function storeSettingClearCertificates($fieldname, $fielddata, $newfieldvalue) { $returnvalue = self::storeSettingField($fieldname, $fielddata, $newfieldvalue); if ($returnvalue !== false && is_array($fielddata) && isset($fielddata['settinggroup']) && $fielddata['settinggroup'] == 'system' && isset($fielddata['varname']) ) { if ($fielddata['varname'] == 'le_froxlor_enabled' && $newfieldvalue == '0') { Database::query(" DELETE FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` WHERE `domainid` = '0' "); } elseif ($fielddata['varname'] == 'froxloraliases' && $newfieldvalue != $fielddata['value']) { Database::query(" UPDATE `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` SET `validtodate`= NULL WHERE `domainid` = '0' "); } } return $returnvalue; } public static function storeSettingField($fieldname, $fielddata, $newfieldvalue) { if (is_array($fielddata) && isset($fielddata['settinggroup']) && $fielddata['settinggroup'] != '' && isset($fielddata['varname']) && $fielddata['varname'] != '') { if (Settings::Set($fielddata['settinggroup'] . '.' . $fielddata['varname'], $newfieldvalue) !== false) { /* * when fielddata[cronmodule] is set, this means enable/disable a cronjob */ if (isset($fielddata['cronmodule']) && $fielddata['cronmodule'] != '') { Cronjob::toggleCronStatus($fielddata['cronmodule'], $newfieldvalue); } /* * satisfy dependencies */ if (isset($fielddata['dependency']) && is_array($fielddata['dependency'])) { if ((int)$fielddata['dependency']['onlyif'] == (int)$newfieldvalue) { self::storeSettingField($fielddata['dependency']['fieldname'], $fielddata['dependency']['fielddata'], $newfieldvalue); } } return [ $fielddata['settinggroup'] . '.' . $fielddata['varname'] => $newfieldvalue ]; } else { return false; } } else { return false; } } public static function storeSettingDefaultIp($fieldname, $fielddata, $newfieldvalue) { $defaultips_old = Settings::Get('system.defaultip'); $returnvalue = self::storeSettingField($fieldname, $fielddata, $newfieldvalue); if ($returnvalue !== false && is_array($fielddata) && isset($fielddata['settinggroup']) && $fielddata['settinggroup'] == 'system' && isset($fielddata['varname']) && $fielddata['varname'] == 'defaultip') { self::updateStdSubdomainDefaultIp($newfieldvalue, $defaultips_old); } return $returnvalue; } private static function updateStdSubdomainDefaultIp($newfieldvalue, $defaultips_old) { // update standard-subdomain of customer if exists $customerstddomains_result_stmt = Database::prepare(" SELECT `standardsubdomain` FROM `" . TABLE_PANEL_CUSTOMERS . "` WHERE `standardsubdomain` <> '0' "); Database::pexecute($customerstddomains_result_stmt); $ids = []; while ($customerstddomains_row = $customerstddomains_result_stmt->fetch(PDO::FETCH_ASSOC)) { $ids[] = (int)$customerstddomains_row['standardsubdomain']; } if (count($ids) > 0) { if (!empty($defaultips_old)) { // Delete the existing mappings linking to default IPs $del_stmt = Database::prepare(" DELETE FROM `" . TABLE_DOMAINTOIP . "` WHERE `id_domain` IN (" . implode(', ', $ids) . ") AND `id_ipandports` IN (" . $defaultips_old . ") "); Database::pexecute($del_stmt); } $defaultips_new = !empty($newfieldvalue) ? explode(",", $newfieldvalue) : []; if (count($defaultips_new) > 0) { // Insert the new mappings $ins_stmt = Database::prepare(" INSERT INTO `" . TABLE_DOMAINTOIP . "` SET `id_domain` = :domainid, `id_ipandports` = :ipandportid "); foreach ($ids as $id) { foreach ($defaultips_new as $defaultip_new) { Database::pexecute($ins_stmt, [ 'domainid' => $id, 'ipandportid' => $defaultip_new ]); } } } } } public static function storeSettingDefaultSslIp($fieldname, $fielddata, $newfieldvalue) { $defaultips_old = Settings::Get('system.defaultsslip'); self::cleanIpSelection($defaultips_old); self::cleanIpSelection($newfieldvalue); $returnvalue = self::storeSettingField($fieldname, $fielddata, $newfieldvalue); if ($returnvalue !== false && is_array($fielddata) && isset($fielddata['settinggroup']) && $fielddata['settinggroup'] == 'system' && isset($fielddata['varname']) && $fielddata['varname'] == 'defaultsslip') { self::updateStdSubdomainDefaultIp($newfieldvalue, $defaultips_old); } return $returnvalue; } private static function cleanIpSelection(&$selection) { $selection_arr = array_filter(explode(',', $selection), function ($value) { return !empty($value); }); $selection = implode(",", $selection_arr); } /** * updates the setting for the default panel-theme * and also the user themes (customers and admins) if * the changing of themes is disallowed for them * * @param string $fieldname * @param array $fielddata * @param mixed $newfieldvalue * * @return boolean|array */ public static function storeSettingDefaultTheme($fieldname, $fielddata, $newfieldvalue) { // first save the setting itself $returnvalue = self::storeSettingField($fieldname, $fielddata, $newfieldvalue); if ($returnvalue !== false && is_array($fielddata) && isset($fielddata['settinggroup']) && $fielddata['settinggroup'] == 'panel' && isset($fielddata['varname']) && $fielddata['varname'] == 'default_theme') { // now, if changing themes is disabled we manually set // the new theme (customers and admin, depending on settings) if (Settings::Get('panel.allow_theme_change_customer') == '0') { $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `theme` = :theme "); Database::pexecute($upd_stmt, [ 'theme' => $newfieldvalue ]); } if (Settings::Get('panel.allow_theme_change_admin') == '0') { $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_ADMINS . "` SET `theme` = :theme "); Database::pexecute($upd_stmt, [ 'theme' => $newfieldvalue ]); } } return $returnvalue; } public static function storeSettingFieldInsertBindTask($fieldname, $fielddata, $newfieldvalue) { // first save the setting itself $returnvalue = self::storeSettingField($fieldname, $fielddata, $newfieldvalue); if ($returnvalue !== false) { Cronjob::inserttask(TaskId::REBUILD_DNS); } return $returnvalue; } public static function storeSettingFieldInsertAntispamTask($fieldname, $fielddata, $newfieldvalue) { // first save the setting itself $returnvalue = self::storeSettingField($fieldname, $fielddata, $newfieldvalue); if ($returnvalue !== false) { Cronjob::inserttask(TaskId::REBUILD_RSPAMD); } return $returnvalue; } public static function storeSettingFieldInsertUpdateServicesTask($fieldname, $fielddata, $newfieldvalue) { // first save the setting itself $returnvalue = self::storeSettingField($fieldname, $fielddata, $newfieldvalue); if ($returnvalue !== false) { Cronjob::inserttask(TaskId::UPDATE_LE_SERVICES); } return $returnvalue; } public static function storeSettingHostname($fieldname, $fielddata, $newfieldvalue) { $returnvalue = self::storeSettingField($fieldname, $fielddata, $newfieldvalue); if ($returnvalue !== false && is_array($fielddata) && isset($fielddata['settinggroup']) && $fielddata['settinggroup'] == 'system' && isset($fielddata['varname']) && ($fielddata['varname'] == 'hostname' || $fielddata['varname'] == 'stdsubdomain')) { $idna_convert = new IdnaWrapper(); $newfieldvalue = $idna_convert->encode($newfieldvalue); if (($fielddata['varname'] == 'hostname' && Settings::Get('system.stdsubdomain') == '') || $fielddata['varname'] == 'stdsubdomain') { if ($fielddata['varname'] == 'stdsubdomain' && $newfieldvalue == '') { // clear field, reset stdsubdomain to system-hostname $oldhost = $idna_convert->encode(Settings::Get('system.stdsubdomain')); $newhost = $idna_convert->encode(Settings::Get('system.hostname')); } elseif ($fielddata['varname'] == 'stdsubdomain' && Settings::Get('system.stdsubdomain') == '') { // former std-subdomain was system-hostname $oldhost = $idna_convert->encode(Settings::Get('system.hostname')); $newhost = $newfieldvalue; } elseif ($fielddata['varname'] == 'stdsubdomain') { // std-subdomain just changed $oldhost = $idna_convert->encode(Settings::Get('system.stdsubdomain')); $newhost = $newfieldvalue; } elseif ($fielddata['varname'] == 'hostname' && Settings::Get('system.stdsubdomain') == '') { // system-hostname has changed and no system-stdsubdomain is not set $oldhost = $idna_convert->encode(Settings::Get('system.hostname')); $newhost = $newfieldvalue; } $customerstddomains_result_stmt = Database::prepare(" SELECT `standardsubdomain` FROM `" . TABLE_PANEL_CUSTOMERS . "` WHERE `standardsubdomain` <> '0' "); Database::pexecute($customerstddomains_result_stmt); $ids = []; while ($customerstddomains_row = $customerstddomains_result_stmt->fetch(PDO::FETCH_ASSOC)) { $ids[] = (int)$customerstddomains_row['standardsubdomain']; } if (count($ids) > 0) { $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_DOMAINS . "` SET `domain` = REPLACE(`domain`, :host, :newval) WHERE `id` IN ('" . implode(', ', $ids) . "') "); Database::pexecute($upd_stmt, [ 'host' => $oldhost, 'newval' => $newhost ]); } } } return $returnvalue; } public static function storeSettingIpAddress($fieldname, $fielddata, $newfieldvalue) { $returnvalue = self::storeSettingField($fieldname, $fielddata, $newfieldvalue); if ($returnvalue !== false && is_array($fielddata) && isset($fielddata['settinggroup']) && $fielddata['settinggroup'] == 'system' && isset($fielddata['varname']) && $fielddata['varname'] == 'ipaddress') { $mysql_access_host_array = array_map('trim', explode(',', Settings::Get('system.mysql_access_host'))); $mysql_access_host_array[] = $newfieldvalue; $mysql_access_host_array = array_unique(PhpHelper::arrayTrim($mysql_access_host_array)); DbManager::correctMysqlUsers($mysql_access_host_array); $mysql_access_host = implode(',', $mysql_access_host_array); Settings::Set('system.mysql_access_host', $mysql_access_host); } return $returnvalue; } public static function storeSettingMysqlAccessHost($fieldname, $fielddata, $newfieldvalue) { $ips = $newfieldvalue; // Convert cidr to netmask for mysql, if needed be if (strpos($ips, ',') !== false) { $ips = explode(',', $ips); } if (is_array($ips) && count($ips) > 0) { $newfieldvalue = []; foreach ($ips as $ip) { $org_ip = $ip; $ip_cidr = explode("/", $ip); if (count($ip_cidr) === 2) { $ip = $ip_cidr[0]; if (strlen($ip_cidr[1]) <= 2) { $ip_cidr[1] = IPTools::cidr2NetmaskAddr($org_ip); } $newfieldvalue[] = $ip . '/' . $ip_cidr[1]; } else { $newfieldvalue[] = $org_ip; } } $newfieldvalue = implode(',', $newfieldvalue); } $returnvalue = self::storeSettingField($fieldname, $fielddata, $newfieldvalue); if ($returnvalue !== false && is_array($fielddata) && isset($fielddata['settinggroup']) && $fielddata['settinggroup'] == 'system' && isset($fielddata['varname']) && $fielddata['varname'] == 'mysql_access_host') { $mysql_access_host_array = array_map('trim', explode(',', $newfieldvalue)); if (in_array('127.0.0.1', $mysql_access_host_array) && !in_array('localhost', $mysql_access_host_array)) { $mysql_access_host_array[] = 'localhost'; } if (!in_array('127.0.0.1', $mysql_access_host_array) && in_array('localhost', $mysql_access_host_array)) { $mysql_access_host_array[] = '127.0.0.1'; } // be aware that ipv6 addresses are enclosed in [ ] when passed here $mysql_access_host_array = array_map([ '\\Froxlor\\Settings\\Store', 'cleanMySQLAccessHost' ], $mysql_access_host_array); $mysql_access_host_array = array_unique(PhpHelper::arrayTrim($mysql_access_host_array)); $newfieldvalue = implode(',', $mysql_access_host_array); DbManager::correctMysqlUsers($mysql_access_host_array); $mysql_access_host = implode(',', $mysql_access_host_array); Settings::Set('system.mysql_access_host', $mysql_access_host); } return $returnvalue; } public static function storeSettingResetCatchall($fieldname, $fielddata, $newfieldvalue) { $returnvalue = self::storeSettingField($fieldname, $fielddata, $newfieldvalue); if ($returnvalue !== false && is_array($fielddata) && isset($fielddata['settinggroup']) && $fielddata['settinggroup'] == 'catchall' && isset($fielddata['varname']) && $fielddata['varname'] == 'catchall_enabled' && $newfieldvalue == '0') { Database::query(" UPDATE `" . TABLE_MAIL_VIRTUAL . "` SET `iscatchall` = '0' WHERE `iscatchall` = '1' "); } return $returnvalue; } /** * Whenever the webserver- / FCGID- or FPM-user gets updated * we need to update ftp_groups accordingly */ public static function storeSettingWebserverFcgidFpmUser($fieldname, $fielddata, $newfieldvalue) { if (is_array($fielddata) && isset($fielddata['settinggroup']) && isset($fielddata['varname'])) { $update_user = null; // webserver if ($fielddata['settinggroup'] == 'system' && $fielddata['varname'] == 'httpuser') { $update_user = Settings::Get('system.httpuser'); } // fcgid if ($fielddata['settinggroup'] == 'system' && $fielddata['varname'] == 'mod_fcgid_httpuser') { $update_user = Settings::Get('system.mod_fcgid_httpuser'); } // webserver if ($fielddata['settinggroup'] == 'phpfpm' && $fielddata['varname'] == 'vhost_httpuser') { $update_user = Settings::Get('phpfpm.vhost_httpuser'); } $returnvalue = self::storeSettingField($fieldname, $fielddata, $newfieldvalue); if ($returnvalue !== false) { /** * only update if anything changed */ if ($update_user != null && $newfieldvalue != $update_user) { $upd_stmt = Database::prepare("UPDATE `" . TABLE_FTP_GROUPS . "` SET `members` = REPLACE(`members`, :olduser, :newuser)"); Database::pexecute($upd_stmt, [ 'olduser' => $update_user, 'newuser' => $newfieldvalue ]); } } } 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::getInstallDir() . '/img/'; $path = FileDir::makeCorrectDir($path); // New file? if (isset($_FILES[$fieldname]) && $_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 $image_content = file_get_contents($_FILES[$fieldname]['tmp_name']); $value = base64_encode($image_content); if (Validate::validateBase64Image($value)) { $img_filename = $_FILES[$fieldname]['name']; $spl = explode('.', $img_filename); $file_extension = strtolower(array_pop($spl)); unset($spl); if (!in_array($file_extension, [ 'jpeg', 'jpg', 'png', 'gif' ])) { throw new Exception("Invalid file-extension, use one of: jpeg, jpg, png, gif"); } $filename = bin2hex(random_bytes(16)) . '.' . $file_extension; // Move file if (!move_uploaded_file($_FILES[$fieldname]['tmp_name'], $path . $filename)) { throw new Exception("Unable to save image to img folder"); } $save_to = 'img/' . $filename . '?v=' . time(); } } // Delete file? if ($fielddata['value'] !== "" && array_key_exists($fieldname . '_delete', $_POST) && Request::post($fieldname . '_delete')) { @unlink(Froxlor::getInstallDir() . '/' . explode('?', $fielddata['value'], 2)[0]); $save_to = ''; } // Nothing changed if ($save_to === null) { return [ $fielddata['settinggroup'] . '.' . $fielddata['varname'] => $fielddata['value'] ]; } if (Settings::Set($fielddata['settinggroup'] . '.' . $fielddata['varname'], $save_to) === false) { return false; } return [ $fielddata['settinggroup'] . '.' . $fielddata['varname'] => $save_to ]; } return false; } private static function cleanMySQLAccessHost($value) { if (substr($value, 0, 1) == '[' && substr($value, -1) == ']') { return substr($value, 1, -1); } return $value; } public static function storeSettingUpdateTrafficTool($fieldname, $fielddata, $newfieldvalue) { $returnvalue = self::storeSettingField($fieldname, $fielddata, $newfieldvalue); if ($returnvalue !== false && is_array($fielddata) && isset($fielddata['settinggroup']) && $fielddata['settinggroup'] == 'system' && isset($fielddata['varname']) && $fielddata['varname'] == 'traffictool' && $newfieldvalue != $fielddata['value']) { $oldpath = '/' . $fielddata['value'] . '/'; $newpath = '/' . $newfieldvalue . '/'; $sel_stmt = Database::prepare("SELECT * FROM `" . TABLE_PANEL_HTPASSWDS . "` WHERE `path` LIKE :oldpath"); $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_HTPASSWDS . "` SET `path` = :newpath WHERE `id` = :id "); Database::pexecute($sel_stmt, [ 'oldpath' => '%' . $oldpath ]); while ($entry = $sel_stmt->fetch(\PDO::FETCH_ASSOC)) { $full_path = str_replace($oldpath, $newpath, $entry['path']); $eid = (int)$entry['id']; Database::pexecute($upd_stmt, [ 'newpath' => $full_path, 'id' => $eid ]); } } return $returnvalue; } } ================================================ FILE: lib/Froxlor/Settings/index.html ================================================ ================================================ FILE: lib/Froxlor/Settings.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor; use Exception; use Froxlor\Database\Database; use PDO; use PDOStatement; /** * Class Settings * * Interaction with settings from the db */ class Settings { /** * settings data * * @var array */ private static $data = null; /** * local config overrides * * @var array */ private static $conf = null; /** * changed and unsaved settings data * * @var array */ private static $updatedata = null; /** * prepared statement for updating the * settings table * * @var PDOStatement */ private static $updstmt = null; /** * tests if a setting-value that i s a comma separated list contains an entry * * @param string $setting * a group and a varname separated by a dot (group.varname) * @param string $entry * the entry that is expected to be in the list * * @return boolean true, if the list contains $entry */ public static function IsInList($setting = null, $entry = null) { self::init(); $svalue = self::Get($setting); if ($svalue == null) { return false; } $slist = explode(",", $svalue); return in_array($entry, $slist); } /** * private constructor, reads in all settings */ private static function init() { if (empty(self::$data)) { self::readSettings(); self::readConfig(); self::$updatedata = []; // prepare statement self::$updstmt = Database::prepare(" UPDATE `" . TABLE_PANEL_SETTINGS . "` SET `value` = :value WHERE `settinggroup` = :group AND `varname` = :varname "); } } /** * Read in all settings from the database * and set the internal $_data array */ private static function readSettings() { $result_stmt = Database::query(" SELECT `settingid`, `settinggroup`, `varname`, `value` FROM `" . TABLE_PANEL_SETTINGS . "` "); self::$data = []; while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { self::$data[$row['settinggroup']][$row['varname']] = $row['value']; } return true; } /** * Read in all config overrides from * config/config.inc.php */ private static function readConfig() { // set defaults self::$conf = [ 'enable_webupdate' => false, 'disable_otp_security_check' => false, 'display_php_errors' => false, ]; $configfile = Froxlor::getInstallDir() . '/lib/config.inc.php'; if (@file_exists($configfile) && is_readable($configfile)) { self::$conf = array_merge(self::$conf, include $configfile); } return true; } /** * return a setting-value by its group and varname * * @param string $setting * a group and a varname separated by a dot (group.varname) * * @return mixed */ public static function Get($setting = null) { self::init(); $sstr = explode(".", $setting); // no separator - do'h if (!isset($sstr[1])) { return null; } $result = null; if (isset(self::$data[$sstr[0]][$sstr[1]])) { $result = self::$data[$sstr[0]][$sstr[1]]; } return $result; } /** * update a setting / set a new value * * @param string $setting * a group and a varname separated by a dot (group.varname) * @param string $value * @param boolean $instant_save * * @return bool */ public static function Set($setting = null, $value = null, $instant_save = true) { self::init(); // check whether the setting exists if (self::Get($setting) !== null) { // set new value in array $sstr = explode(".", $setting); if (!isset($sstr[1])) { return false; } self::$data[$sstr[0]][$sstr[1]] = $value; // should we store to db instantly? if ($instant_save) { self::storeSetting($sstr[0], $sstr[1], $value); } else { // set temporary data for usage if (!isset(self::$data[$sstr[0]]) || !is_array(self::$data[$sstr[0]])) { self::$data[$sstr[0]] = []; } self::$data[$sstr[0]][$sstr[1]] = $value; // set update-data when invoking Flush() if (!isset(self::$updatedata[$sstr[0]]) || !is_array(self::$updatedata[$sstr[0]])) { self::$updatedata[$sstr[0]] = []; } self::$updatedata[$sstr[0]][$sstr[1]] = $value; } return true; } return false; } /** * update a value in the database * * @param string $group * @param string $varname * @param string $value */ private static function storeSetting($group = null, $varname = null, $value = null) { $upd_data = [ 'group' => $group, 'varname' => $varname, 'value' => $value ]; Database::pexecute(self::$updstmt, $upd_data); } /** * add a new setting to the database (mainly used in updater) * * @param string $setting * a group and a varname separated by a dot (group.varname) * @param string $value * * @return boolean */ public static function AddNew($setting = null, $value = null) { self::init(); // first check if it doesn't exist if (self::Get($setting) === null) { // validate parameter $sstr = explode(".", $setting); if (!isset($sstr[1])) { return false; } // prepare statement $ins_stmt = Database::prepare(" INSERT INTO `" . TABLE_PANEL_SETTINGS . "` SET `settinggroup` = :group, `varname` = :varname, `value` = :value "); $ins_data = [ 'group' => $sstr[0], 'varname' => $sstr[1], 'value' => $value ]; Database::pexecute($ins_stmt, $ins_data); // also set new value to internal array and make it available self::$data[$sstr[0]][$sstr[1]] = $value; return true; } return false; } /** * Store all un-saved changes to the database and * re-read in all settings */ public static function Flush() { self::init(); if (is_array(self::$updatedata) && count(self::$updatedata) > 0) { // save all un-saved changes to the settings foreach (self::$updatedata as $group => $vargroup) { foreach ($vargroup as $varname => $value) { self::storeSetting($group, $varname, $value); } } // now empty the array self::$updatedata = []; // re-read in all settings return self::readSettings(); } return false; } /** * forget all un-saved changes to settings */ public static function Stash() { self::init(); // empty update array self::$updatedata = []; // re-read in all settings return self::readSettings(); } public static function loadSettingsInto(&$settings_data) { if (is_array($settings_data) && isset($settings_data['groups']) && is_array($settings_data['groups'])) { // prepare for use in for-loop $row_stmt = Database::prepare(" SELECT `settinggroup`, `varname`, `value` FROM `" . TABLE_PANEL_SETTINGS . "` WHERE `settinggroup` = :group AND `varname` = :varname "); foreach ($settings_data['groups'] as $settings_part => $settings_part_details) { if (is_array($settings_part_details) && isset($settings_part_details['fields']) && is_array($settings_part_details['fields'])) { foreach ($settings_part_details['fields'] as $field_name => $field_details) { if (isset($field_details['settinggroup']) && isset($field_details['varname']) && isset($field_details['default'])) { // execute prepared statement $row = Database::pexecute_first($row_stmt, [ 'group' => $field_details['settinggroup'], 'varname' => $field_details['varname'] ]); if (!empty($row)) { $varvalue = $row['value']; } else { $varvalue = $field_details['default']; } } else { $varvalue = false; } $settings_data['groups'][$settings_part]['fields'][$field_name]['value'] = $varvalue; } } } } } public static function refreshState(): void { self::$data = null; self::init(); } public static function getAll(): array { self::init(); return self::$data; } /** * get value from config by identifier * @throws Exception */ public static function Config(string $config) { self::init(); $result = self::$conf[$config] ?? null; if (is_null($result)) { throw new Exception('Unknown local config name "' . $config . '"'); } return $result; } } ================================================ FILE: lib/Froxlor/System/Cronjob.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\System; use Exception; use Froxlor\Cron\TaskId; use Froxlor\Database\Database; use Froxlor\FileDir; use Froxlor\FroxlorLogger; use Froxlor\Settings; use PDO; class Cronjob { /** * Function checkLastGuid * * Checks if the system's last guid is not higher than the one saved * in froxlor's database. If it's higher, froxlor needs to * set its last guid to this one to avoid conflicts with libnss-users * * @return null */ public static function checkLastGuid() { $mylog = FroxlorLogger::getInstanceOf(); $group_lines = []; $group_guids = []; $update_to_guid = 0; $froxlor_guid = 0; $result_stmt = Database::query("SELECT MAX(`guid`) as `fguid` FROM `" . TABLE_PANEL_CUSTOMERS . "`"); $result = $result_stmt->fetch(PDO::FETCH_ASSOC); $froxlor_guid = $result['fguid']; // possibly no customers yet or f*cked up lastguid settings if ($froxlor_guid < Settings::Get('system.lastguid')) { $froxlor_guid = Settings::Get('system.lastguid'); } $g_file = '/etc/group'; if (file_exists($g_file)) { if (is_readable($g_file)) { if (true == ($groups = file_get_contents($g_file))) { $group_lines = explode("\n", $groups); foreach ($group_lines as $group) { $group_guids[] = explode(":", $group); } foreach ($group_guids as $group) { /** * nogroup | nobody have very high guids * ignore them */ if ($group[0] == 'nogroup' || $group[0] == 'nobody') { continue; } $guid = isset($group[2]) ? (int)$group[2] : 0; if ($guid > $update_to_guid) { $update_to_guid = $guid; } } // if it's lower, then froxlor's highest guid is the last if ($update_to_guid < $froxlor_guid) { $update_to_guid = $froxlor_guid; } elseif ($update_to_guid == $froxlor_guid) { // if it's equal, that means we already have a collision // to ensure it won't happen again, increase the guid by one $update_to_guid = (int)$update_to_guid++; } // now check if it differs from our settings if ($update_to_guid != Settings::Get('system.lastguid')) { $mylog->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Updating froxlor last guid to ' . $update_to_guid); Settings::Set('system.lastguid', $update_to_guid); } } else { $mylog->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'File /etc/group not readable; cannot check for latest guid'); } } else { $mylog->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'File /etc/group not readable; cannot check for latest guid'); } } else { $mylog->logAction(FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'File /etc/group does not exist; cannot check for latest guid'); } } public static function checkCurrentDistro(bool $is_install = false): string { // set default os. if ($is_install) { $distro = "trixie"; } else { $distro = Settings::Get('system.distribution'); } // read os-release if (@file_exists('/etc/os-release') && is_readable('/etc/os-release')) { if (function_exists('parse_ini_file')) { $os_dist = parse_ini_file('/etc/os-release', false); } else { $osrf = explode("\n", file_get_contents('/etc/os-release')); foreach ($osrf as $line) { $osrfline = explode("=", $line); if ($osrfline[0] == 'VERSION_CODENAME') { $os_dist['VERSION_CODENAME'] = $osrfline[1]; } elseif ($osrfline[0] == 'ID') { $os_dist['ID'] = $osrfline[1]; } } } $distro = strtolower($os_dist['VERSION_CODENAME'] ?? ($os_dist['ID'] ?? $distro)); } if (!$is_install && $distro != Settings::Get('system.distribution') && Settings::Get('system.distro_mismatch') != '2') { Settings::Set('system.distro_mismatch', '1'); } return $distro; } /** * @throws Exception */ public static function checkLocalUserGroupMembership(): bool { if ((int)Settings::Get('phpfpm.enabled') == 1) { $username = Settings::Get('phpfpm.vhost_httpuser'); } elseif ((int)Settings::Get('system.mod_fcgid') == 1) { $username = Settings::Get('system.mod_fcgid_httpuser'); } else { $username = Settings::Get('system.httpuser'); } $user = posix_getpwnam($username); $group = posix_getgrnam(Settings::Get('system.httpgroup')); $mylog = FroxlorLogger::getInstanceOf(); if (!$user || !$group) { $mylog->logAction( FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Either local froxlor user or webserver-group could not be found/read. Please check settings.' ); return false; } // primary group? if ($user['gid'] === $group['gid']) { return true; } // supplementary groups? if (in_array($username, $group['members'])) { return true; } // not yet in group, add it $mylog->logAction( FroxlorLogger::CRON_ACTION, LOG_NOTICE, 'Local froxlor user not in webserver-group. Adding user "' . $username . '" to group "' . Settings::Get('system.httpgroup') . '"' ); FileDir::safe_exec('usermod -aG ' . escapeshellarg(Settings::Get('system.httpgroup')) . ' ' . escapeshellarg($username)); return true; } /** * Inserts a task into the PANEL_TASKS-Table * * @param int $type Type of task * @param string $params Parameter (possible to pass multiple times) * * @throws Exception * @author Froxlor team (2010-) */ public static function inserttask(int $type, ...$params) { // prepare the insert-statement $ins_stmt = Database::prepare(" INSERT INTO `" . TABLE_PANEL_TASKS . "` SET `type` = :type, `data` = :data "); if ($type == TaskId::REBUILD_VHOST || $type == TaskId::REBUILD_DNS || $type == TaskId::CREATE_FTP || $type == TaskId::REBUILD_RSPAMD || $type == TaskId::CREATE_QUOTA || $type == TaskId::REBUILD_CRON || $type == TaskId::UPDATE_LE_SERVICES || $type == TaskId::REBUILD_NSSUSERS) { // 4 = bind -> if bind disabled -> no task if ($type == TaskId::REBUILD_DNS && Settings::Get('system.bind_enable') == '0') { return; } // 9 = rspamd -> if antispam disabled -> no task if ($type == TaskId::REBUILD_RSPAMD && Settings::Get('antispam.activated') == '0') { return; } // 10 = quota -> if quota disabled -> no task if ($type == TaskId::CREATE_QUOTA && Settings::Get('system.diskquota_enabled') == '0') { return; } // 13 = let's encrypt for services -> if services empty = no task if ($type == TaskId::UPDATE_LE_SERVICES && (Settings::Get('system.le_froxlor_enabled') == '0' || Settings::Get('system.le_renew_services') == '')) { return; } // delete previously inserted tasks if they are the same as we only need ONE $del_stmt = Database::prepare(" DELETE FROM `" . TABLE_PANEL_TASKS . "` WHERE `type` = :type "); Database::pexecute($del_stmt, [ 'type' => $type ]); // insert the new task Database::pexecute($ins_stmt, [ 'type' => $type, 'data' => '' ]); } elseif ($type == TaskId::CREATE_HOME && count($params) == 4 && $params[0] != '' && $params[1] != '' && $params[2] != '' && ($params[3] == 0 || $params[3] == 1)) { $data = []; $data['loginname'] = $params[0]; $data['uid'] = $params[1]; $data['gid'] = $params[2]; $data['store_defaultindex'] = $params[3]; $data = json_encode($data); Database::pexecute($ins_stmt, [ 'type' => TaskId::CREATE_HOME, 'data' => $data ]); } elseif ($type == TaskId::DELETE_CUSTOMER_FILES && isset($params[0]) && $params[0] != '') { $data = []; $data['loginname'] = $params[0]; $data = json_encode($data); Database::pexecute($ins_stmt, [ 'type' => TaskId::DELETE_CUSTOMER_FILES, 'data' => $data ]); } elseif ($type == TaskId::DELETE_EMAIL_DATA && count($params) == 2 && $params[0] != '' && $params[1] != '') { $data = []; $data['loginname'] = $params[0]; $data['emailpath'] = $params[1]; $data = json_encode($data); Database::pexecute($ins_stmt, [ 'type' => TaskId::DELETE_EMAIL_DATA, 'data' => $data ]); } elseif ($type == TaskId::DELETE_FTP_DATA && count($params) == 2 && $params[0] != '' && $params[1] != '') { $data = []; $data['loginname'] = $params[0]; $data['homedir'] = $params[1]; $data = json_encode($data); Database::pexecute($ins_stmt, [ 'type' => TaskId::DELETE_FTP_DATA, 'data' => $data ]); } elseif ($type == TaskId::DELETE_DOMAIN_PDNS && isset($params[0]) && $params[0] != '' && Settings::Get('system.bind_enable') == '1' && Settings::Get('system.dns_server') == 'PowerDNS') { // -> if bind disabled or dns-server not PowerDNS -> no task $data = []; $data['domain'] = $params[0]; $data = json_encode($data); Database::pexecute($ins_stmt, [ 'type' => TaskId::DELETE_DOMAIN_PDNS, 'data' => $data ]); } elseif ($type == TaskId::DELETE_DOMAIN_SSL && isset($params[0]) && $params[0] != '') { $data = []; $data['domain'] = $params[0]; $data = json_encode($data); Database::pexecute($ins_stmt, [ 'type' => TaskId::DELETE_DOMAIN_SSL, 'data' => $data ]); } elseif ($type == TaskId::CREATE_CUSTOMER_DATADUMP && isset($params[0]) && is_array($params[0])) { $data = json_encode($params[0]); Database::pexecute($ins_stmt, [ 'type' => TaskId::CREATE_CUSTOMER_DATADUMP, 'data' => $data ]); } } /** * returns an array of all cronjobs and when they last were executed * * @return array */ public static function getCronjobsLastRun(): array { $query = "SELECT `lastrun`, `desc_lng_key` FROM `" . TABLE_PANEL_CRONRUNS . "` WHERE `isactive` = '1' ORDER BY `cronfile` ASC"; $result = Database::query($query); $cronjobs_last_run = []; while ($row = $result->fetch(PDO::FETCH_ASSOC)) { $cronjobs_last_run[] = [ 'title' => lng('crondesc.' . $row['desc_lng_key']), 'lastrun' => $row['lastrun'] ]; } return $cronjobs_last_run; } /** * @param string $module * @param int $isactive * @return void * @throws Exception */ public static function toggleCronStatus(string $module, int $isactive = 0) { if ($isactive != 1) { $isactive = 0; } $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_CRONRUNS . "` SET `isactive` = :active WHERE `module` = :module "); Database::pexecute($upd_stmt, [ 'active' => $isactive, 'module' => $module ]); } /** * returns an array of tasks that are queued to be run by the cronjob * * @return array */ public static function getOutstandingTasks(): array { $query = "SELECT * FROM `" . TABLE_PANEL_TASKS . "` ORDER BY `type` ASC"; $result = Database::query($query); $tasks = []; while ($row = $result->fetch(PDO::FETCH_ASSOC)) { if ($row['data'] != '') { $row['data'] = json_decode($row['data'], true); } $task_id = $row['type']; if (TaskId::isValid($task_id)) { $task_constname = TaskId::convertToConstant($task_id); $lngParams = []; if (is_array($row['data'])) { // task includes loginname if (isset($row['data']['loginname'])) { $lngParams = [$row['data']['loginname']]; } // task includes domain data if (isset($row['data']['domain'])) { $lngParams = [$row['data']['domain']]; } } $task = [ 'desc' => lng('tasks.' . $task_constname, $lngParams) ]; } else { // unknown $task = ['desc' => "ERROR: Unknown task type '" . $row['type'] . "'"]; } $tasks[] = $task; } if (empty($tasks)) { $tasks = [['desc' => lng('tasks.noneoutstanding')]]; } return $tasks; } /** * Send notification to system admin via email * * @param string $message * @param string $subject * * @return void */ public static function notifyMailToAdmin(string $message, string $subject = "[froxlor] Important notice") { $mail = new Mailer(true); $mailerror = false; $mailerr_msg = ""; try { $mail->Subject = $subject; $mail->AltBody = $message; $mail->MsgHTML(nl2br($message)); $mail->AddAddress(Settings::Get('panel.adminmail'), Settings::Get('panel.adminmail_defname')); $mail->Send(); } catch (\PHPMailer\PHPMailer\Exception $e) { $mailerr_msg = $e->errorMessage(); $mailerror = true; } catch (Exception $e) { $mailerr_msg = $e->getMessage(); $mailerror = true; } $mail->ClearAddresses(); if ($mailerror) { echo 'Error sending mail: ' . $mailerr_msg . "\n"; } } /** * @param string $cronname * @return void * @throws Exception */ public static function updateLastRunOfCron(string $cronname) { $upd_stmt = Database::prepare(" UPDATE `" . TABLE_PANEL_CRONRUNS . "` SET `lastrun` = UNIX_TIMESTAMP() WHERE `cronfile` = :cron; "); Database::pexecute($upd_stmt, [ 'cron' => $cronname ]); } } ================================================ FILE: lib/Froxlor/System/Crypt.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\System; use Froxlor\Database\Database; use Froxlor\Froxlor; use Froxlor\Settings; use Froxlor\Validate\Validate; class Crypt { /** * Generates a random password * * @param int $length optional, will be read from settings if not given * @param bool $isSalt optional, default false, do not include special characters * * @return string */ public static function generatePassword(int $length = 0, bool $isSalt = false): string { $alpha_lower = 'abcdefghijklmnopqrstuvwxyz'; $alpha_upper = strtoupper($alpha_lower); $numeric = '0123456789'; $special = Settings::Get('panel.password_special_char'); if (empty($length)) { $length = Settings::Get('panel.password_min_length') > 3 ? Settings::Get('panel.password_min_length') : 10; } $pw = self::specialShuffle($alpha_lower); $n = floor(($length) / 4); if (Settings::Get('panel.password_alpha_upper')) { $pw .= mb_substr(self::specialShuffle($alpha_upper), 0, $n); } if (Settings::Get('panel.password_numeric')) { $pw .= mb_substr(self::specialShuffle($numeric), 0, $n); } if (Settings::Get('panel.password_special_char_required') && !$isSalt) { $pw .= mb_substr(self::specialShuffle($special), 0, $n); } $pw = mb_substr($pw, -$length); return self::specialShuffle($pw); } /** * multibyte-character safe shuffle function * * @param string $str * * @return string */ private static function specialShuffle(string $str): string { $len = mb_strlen($str); $sploded = []; while ($len-- > 0) { $sploded[] = mb_substr($str, $len, 1); } shuffle($sploded); return join('', $sploded); } /** * return an array of available hashes * * @return array */ public static function getAvailablePasswordHashes(): array { // get available pwd-hases $available_pwdhashes = [ PASSWORD_DEFAULT => lng('serversettings.systemdefault') ]; if (defined('PASSWORD_BCRYPT')) { $available_pwdhashes[PASSWORD_BCRYPT] = 'Bcrypt/Blowfish' . (PASSWORD_DEFAULT == PASSWORD_BCRYPT ? ' (' . lng('serversettings.systemdefault') . ')' : ''); } if (defined('PASSWORD_ARGON2I')) { $available_pwdhashes[PASSWORD_ARGON2I] = 'Argon2i' . (PASSWORD_DEFAULT == PASSWORD_ARGON2I ? ' (' . lng('serversettings.systemdefault') . ')' : ''); } if (defined('PASSWORD_ARGON2ID')) { $available_pwdhashes[PASSWORD_ARGON2ID] = 'Argon2id' . (PASSWORD_DEFAULT == PASSWORD_ARGON2ID ? ' (' . lng('serversettings.systemdefault') . ')' : ''); } return $available_pwdhashes; } /** * Function validatePassword * * if password-min-length is set in settings * we check against the length, if not matched * an error message will be output and 'exit' is called * * @param string $password the password to validate * @param bool $json_response * * @return string either the password or an errormessage+exit */ public static function validatePassword(string $password, bool $json_response = false): string { if (Settings::Get('panel.password_min_length') > 0) { $password = Validate::validate($password, Settings::Get('panel.password_min_length'), '/^.{' . (int)Settings::Get('panel.password_min_length') . ',}$/D', 'notrequiredpasswordlength', [], $json_response); } if (Settings::Get('panel.password_regex') != '') { $password = Validate::validate($password, Settings::Get('panel.password_regex'), Settings::Get('panel.password_regex'), 'notrequiredpasswordcomplexity', [], $json_response); } else { if (Settings::Get('panel.password_alpha_lower')) { $password = Validate::validate($password, '/.*[a-z]+.*/', '/.*[a-z]+.*/', 'notrequiredpasswordcomplexity', [], $json_response); } if (Settings::Get('panel.password_alpha_upper')) { $password = Validate::validate($password, '/.*[A-Z]+.*/', '/.*[A-Z]+.*/', 'notrequiredpasswordcomplexity', [], $json_response); } if (Settings::Get('panel.password_numeric')) { $password = Validate::validate($password, '/.*[0-9]+.*/', '/.*[0-9]+.*/', 'notrequiredpasswordcomplexity', [], $json_response); } if (Settings::Get('panel.password_special_char_required')) { $password = Validate::validate($password, '/.*[' . preg_quote(Settings::Get('panel.password_special_char'), '/') . ']+.*/', '/.*[' . preg_quote(Settings::Get('panel.password_special_char'), '/') . ']+.*/', 'notrequiredpasswordcomplexity', [], $json_response); } } return $password; } /** * Function validatePasswordLogin * * compare user password-hash with given user-password * and check if they are the same * additionally it updates the hash if the system settings changed * or if the very old md5() sum is used * * @param array $userinfo user-data from table * @param string $password the password to validate * @param string $table either panel_customers or panel_admins * @param string $uid user-id-field in $table * * @return bool * @throws \Exception */ public static function validatePasswordLogin( array $userinfo, string $password, string $table = 'panel_customers', string $uid = 'customerid' ): bool { $algo = Settings::Get('system.passwordcryptfunc') !== null ? Settings::Get('system.passwordcryptfunc') : PASSWORD_DEFAULT; if (is_numeric($algo)) { // old setting format $algo = PASSWORD_DEFAULT; Settings::Set('system.passwordcryptfunc', $algo); } $pwd_hash = $userinfo['password']; $update_hash = false; $pwd_check = ""; // check for good'ole md5 if (strlen($pwd_hash) == 32 && ctype_xdigit($pwd_hash)) { $pwd_check = md5($password); $update_hash = true; } if ($pwd_hash === $pwd_check || password_verify($password, $pwd_hash)) { // check for update of hash (only if our database is ready to handle the bigger string) $is_ready = Froxlor::versionCompare2("0.9.33", Froxlor::getVersion()) <= 0; if ((password_needs_rehash($pwd_hash, $algo) || $update_hash) && $is_ready) { $upd_stmt = Database::prepare(" UPDATE " . $table . " SET `password` = :newpasswd WHERE `" . $uid . "` = :uid "); $params = [ 'newpasswd' => self::makeCryptPassword($password), 'uid' => $userinfo[$uid] ]; Database::pexecute($upd_stmt, $params); } return true; } return false; } /** * Make encrypted password from clear text password * * @param string $password Password to be encrypted * @param bool $htpasswd optional whether to generate a bcrypt password for directory protection * @param bool $ftpd optional generates sha256 password strings for proftpd/pureftpd * * @return string encrypted password */ public static function makeCryptPassword(string $password, bool $htpasswd = false, bool $ftpd = false): string { if ($htpasswd || $ftpd) { if ($ftpd) { // sha256 compatible for proftpd and pure-ftpd return crypt($password, '$5$' . self::generatePassword(16, true) . '$'); } // bcrypt hash for dir-protection return password_hash($password, PASSWORD_BCRYPT); } // crypt using the specified crypt-algorithm or system default $algo = Settings::Get('system.passwordcryptfunc') !== null ? Settings::Get('system.passwordcryptfunc') : PASSWORD_DEFAULT; return password_hash($password, $algo); } /** * creates a self-signed ECC-certificate for the froxlor-vhost * and sets the content to the corresponding files set in the * settings for ssl-certificate-file and ssl-certificate-key * * @return void */ public static function createSelfSignedCertificate() { // validate that we have file names in the settings $certFile = Settings::Get('system.ssl_cert_file'); $keyFile = Settings::Get('system.ssl_key_file'); if (empty($certFile)) { $certFile = '/etc/ssl/froxlor_selfsigned.pem'; Settings::Set('system.ssl_cert_file', $certFile); } if (empty($keyFile)) { $keyFile = '/etc/ssl/froxlor_selfsigned.key'; Settings::Set('system.ssl_key_file', $keyFile); } // certificate info $dn = [ "countryName" => "DE", "stateOrProvinceName" => "Hessen", "localityName" => "Frankfurt am Main", "organizationName" => "froxlor", "organizationalUnitName" => "froxlor Server Management Panel", "commonName" => Settings::Get('system.hostname'), "emailAddress" => Settings::Get('panel.adminmail') ]; // create private key $privkey = openssl_pkey_new([ "private_key_type" => OPENSSL_KEYTYPE_EC, "curve_name" => 'prime256v1', ]); // create signing request $csr = openssl_csr_new($dn, $privkey, array('digest_alg' => 'sha384')); // sign csr $x509 = openssl_csr_sign($csr, null, $privkey, 365, array('digest_alg' => 'sha384')); // export to files openssl_x509_export_to_file($x509, $certFile); openssl_pkey_export_to_file($privkey, $keyFile); } } ================================================ FILE: lib/Froxlor/System/IPTools.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\System; class IPTools { /** * Converts CIDR to a netmask address * * @thx to https://stackoverflow.com/a/5711080/3020926 * @param string $cidr * * @return string */ public static function cidr2NetmaskAddr(string $cidr): string { $ta = substr($cidr, strpos($cidr, '/') + 1) * 1; $netmask = str_split(str_pad(str_pad('', $ta, '1'), 32, '0'), 8); foreach ($netmask as &$element) { $element = bindec($element); } return implode('.', $netmask); } /** * Checks whether the given $ip is in range of given ip/cidr range * * @param array $ip_cidr 0 => ip, 1 => netmask in decimal, e.g. [0 => '123.123.123.123', 1 => 24] * @param string $ip ip-address to check * * @return bool */ public static function ip_in_range(array $ip_cidr, string $ip): bool { $netip = $ip_cidr[0]; if (self::is_ipv6($netip)) { return self::ipv6_in_range($ip_cidr, $ip); } $netmask = $ip_cidr[1]; $range_decimal = ip2long($netip); $ip_decimal = ip2long($ip); $wildcard_decimal = pow(2, (32 - $netmask)) - 1; $netmask_decimal = ~$wildcard_decimal; return (($ip_decimal & $netmask_decimal) == ($range_decimal & $netmask_decimal)); } /** * Checks if an $address (IP) is IPv6 * * @param string $address * * @return string|bool ip address on success, false on failure */ public static function is_ipv6(string $address) { return filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6); } /** * Checks whether the given ipv6 $ip is in range of given ip/cidr range * * @param array $ip_cidr 0 => ip, 1 => netmask in decimal, e.g. [0 => '123:123::1', 1 => 64] * @param string $ip ip-address to check * * @return bool */ private static function ipv6_in_range(array $ip_cidr, string $ip): bool { $in_range = false; $size = 128 - $ip_cidr[1]; if ($size == 0) { return inet_ntop(inet_pton($ip_cidr[0])) == inet_ntop(inet_pton($ip)); } $addr = gmp_init('0x' . str_replace(':', '', self::inet6_expand($ip_cidr[0]))); $mask = gmp_init('0x' . str_replace(':', '', self::inet6_expand(self::inet6_prefix_to_mask($ip_cidr[1])))); $prefix = gmp_and($addr, $mask); $start = gmp_strval(gmp_add($prefix, '0x1'), 16); $end = '0b'; for ($i = 0; $i < $size; $i++) { $end .= '1'; } $end = gmp_strval(gmp_add($prefix, gmp_init($end)), 16); $start_result = ''; for ($i = 0; $i < 8; $i++) { $start_result .= substr($start, $i * 4, 4); if ($i != 7) { $start_result .= ':'; } } $end_result = ''; for ($i = 0; $i < 8; $i++) { $end_result .= substr($end, $i * 4, 4); if ($i != 7) { $end_result .= ':'; } } $first = self::ip2long6($start_result); $last = self::ip2long6($end_result); $ip = self::ip2long6($ip); $in_range = ($ip >= $first && $ip <= $last); return $in_range; } /** * @param string $addr * @return false|string */ private static function inet6_expand(string $addr) { // Check if there are segments missing, insert if necessary if (strpos($addr, '::') !== false) { $part = explode('::', $addr); $part[0] = explode(':', $part[0]); $part[1] = explode(':', $part[1]); $missing = []; for ($i = 0; $i < (8 - (count($part[0]) + count($part[1]))); $i++) { $missing[] = '0000'; } $missing = array_merge($part[0], $missing); $part = array_merge($missing, $part[1]); } else { $part = explode(":", $addr); } // Pad each segment until it has 4 digits foreach ($part as &$p) { while (strlen($p) < 4) { $p = '0' . $p; } } unset($p); // Join segments $result = implode(':', $part); // Quick check to make sure the length is as expected if (strlen($result) == 39) { return $result; } else { return false; } } /** * @param int $prefix * @return false|string */ private static function inet6_prefix_to_mask(int $prefix) { /* Make sure the prefix is a number between 1 and 127 (inclusive) */ if ($prefix < 0 || $prefix > 128) { return false; } $mask = '0b'; $mask .= str_repeat('1', $prefix); for ($i = strlen($mask) - 2; $i < 128; $i++) { $mask .= '0'; } $mask = gmp_strval(gmp_init($mask), 16); $result = ''; for ($i = 0; $i < 8; $i++) { $result .= substr($mask, $i * 4, 4); if ($i != 7) { $result .= ':'; } } // for return inet_ntop(inet_pton($result)); } /** * @param string $ip * @return string */ private static function ip2long6(string $ip): string { $ip_n = inet_pton($ip); $bits = 15; // 16 x 8 bit = 128bit $ipv6long = ''; while ($bits >= 0) { $bin = sprintf("%08b", (ord($ip_n[$bits]))); $ipv6long = $bin . $ipv6long; $bits--; } return gmp_strval(gmp_init($ipv6long, 2), 10); } } ================================================ FILE: lib/Froxlor/System/Mailer.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\System; use Froxlor\Settings; use PHPMailer\PHPMailer\Exception; use PHPMailer\PHPMailer\PHPMailer; class Mailer extends PHPMailer { /** * class constructor * * @param bool $exceptions whether to throw exceptions or not * * @throws Exception */ public function __construct(bool $exceptions = false) { parent::__construct($exceptions); $this->CharSet = "UTF-8"; if (Settings::Get('system.mail_use_smtp')) { $this->isSMTP(); $this->Host = Settings::Get('system.mail_smtp_host'); $this->SMTPAuth = Settings::Get('system.mail_smtp_auth') == '1'; $this->Username = Settings::Get('system.mail_smtp_user'); $this->Password = Settings::Get('system.mail_smtp_passwd'); if (Settings::Get('system.mail_smtp_usetls')) { $this->SMTPSecure = 'tls'; } else { $this->SMTPAutoTLS = false; } $this->Port = Settings::Get('system.mail_smtp_port'); } /** * use froxlor's email-validation */ self::$validator = [ '\Froxlor\\Validate\\Validate', 'validateEmail' ]; if (self::ValidateAddress(Settings::Get('panel.adminmail')) !== false) { // set return-to address and custom sender-name, see #76 $this->setFrom(Settings::Get('panel.adminmail'), Settings::Get('panel.adminmail_defname')); if (Settings::Get('panel.adminmail_return') != '') { $this->addReplyTo(Settings::Get('panel.adminmail_return'), Settings::Get('panel.adminmail_defname')); } } } } ================================================ FILE: lib/Froxlor/System/Markdown.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\System; use League\CommonMark\Exception\CommonMarkException; use League\CommonMark\GithubFlavoredMarkdownConverter; class Markdown { private static $converter = null; public static function converter(): ?GithubFlavoredMarkdownConverter { if (is_null(self::$converter)) { self::$converter = new GithubFlavoredMarkdownConverter([ 'html_input' => 'strip', 'allow_unsafe_links' => false, ]); } return self::$converter; } public static function cleanCustomNotes(string $note = ""): string { if (!empty($note)) { try { $note = self::converter()->convert($note)->getContent(); } catch (CommonMarkException $e) { $note = ""; } } return $note; } } ================================================ FILE: lib/Froxlor/System/MysqlHandler.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\System; use Froxlor\Database\Database; use Monolog\Handler\AbstractProcessingHandler; use Monolog\Logger; class MysqlHandler extends AbstractProcessingHandler { protected static array $froxlorLevels = [ Logger::DEBUG => LOG_DEBUG, Logger::INFO => LOG_INFO, Logger::NOTICE => LOG_NOTICE, Logger::WARNING => LOG_WARNING, Logger::ERROR => LOG_ERR, Logger::CRITICAL => LOG_ERR, Logger::ALERT => LOG_ERR, Logger::EMERGENCY => LOG_ERR ]; protected $pdoStatement = null; /** * Constructor * * @param bool|int $level Debug level which this handler should store * @param bool $bubble */ public function __construct($level = Logger::DEBUG, bool $bubble = true) { parent::__construct($level, $bubble); } /** * Writes the record down to the log * * @param array $record * @return void */ protected function write(array $record) { $this->insert([ ':message' => $record['message'], ':contextUser' => ($record['context']['user'] ?? 'unknown'), ':contextAction' => ($record['context']['action'] ?? '0'), ':level' => self::$froxlorLevels[$record['level']], ':datetime' => $record['datetime']->format('U') ]); } /** * Insert the data to the logger table * * @param array $data * @return bool */ protected function insert(array $data): bool { if ($this->pdoStatement === null) { $sql = "INSERT INTO `panel_syslog` SET `text` = :message, `user` = :contextUser, `action` = :contextAction, `type` = :level, `date` = :datetime"; $this->pdoStatement = Database::prepare($sql); } return $this->pdoStatement->execute($data); } } ================================================ FILE: lib/Froxlor/System/index.html ================================================ ================================================ FILE: lib/Froxlor/Traffic/Traffic.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\Traffic; use Froxlor\Api\Commands\Customers; use Froxlor\Api\Commands\Traffic as TrafficAPI; use Froxlor\Database\Database; use Froxlor\UI\Collection; class Traffic { /** * @param array $userinfo * @param ?string $range * @return array * @throws \Exception */ public static function getCustomerStats(array $userinfo, string $range = null, bool $overview = false): array { $trafficCollectionObj = (new Collection(TrafficAPI::class, $userinfo, self::getParamsByRange($range, ['customer_traffic' => true]))); if (($userinfo['adminsession'] ?? 0) == 1) { $trafficCollectionObj->has('customer', Customers::class, 'customerid', 'customerid'); } $trafficCollection = $trafficCollectionObj->get(); // build stats for each user $users = []; $years = []; $months = []; $days = []; foreach ($trafficCollection['data']['list'] as $item) { $http = $item['http']; $ftp = ($item['ftp_up'] + $item['ftp_down']); $mail = $item['mail']; $total = $http + $ftp + $mail; if (empty($users[$item['customerid']])) { $users[$item['customerid']] = [ 'total' => 0.00, 'http' => 0.00, 'ftp' => 0.00, 'mail' => 0.00, ]; } // per user total if (($userinfo['adminsession'] ?? 0) == 1) { $users[$item['customerid']]['loginname'] = $item['customer']['loginname']; } $users[$item['customerid']]['total'] += $total; $users[$item['customerid']]['http'] += $http; $users[$item['customerid']]['ftp'] += $ftp; $users[$item['customerid']]['mail'] += $mail; if (!$overview) { if (empty($years[$item['year']])) { $years[$item['year']] = [ 'total' => 0.00, 'http' => 0.00, 'ftp' => 0.00, 'mail' => 0.00, ]; } if (empty($months[$item['month'] . '/' . $item['year']])) { $months[$item['month'] . '/' . $item['year']] = [ 'total' => 0.00, 'http' => 0.00, 'ftp' => 0.00, 'mail' => 0.00, ]; } if (empty($days[$item['day'] . '.' . $item['month'] . '.' . $item['year']])) { $days[$item['day'] . '.' . $item['month'] . '.' . $item['year']] = [ 'total' => 0.00, 'http' => 0.00, 'ftp' => 0.00, 'mail' => 0.00, ]; } // per year $years[$item['year']]['total'] += $total; $years[$item['year']]['http'] += $http; $years[$item['year']]['ftp'] += $ftp; $years[$item['year']]['mail'] += $mail; // per month $months[$item['month'] . '/' . $item['year']]['total'] += $total; $months[$item['month'] . '/' . $item['year']]['http'] += $http; $months[$item['month'] . '/' . $item['year']]['ftp'] += $ftp; $months[$item['month'] . '/' . $item['year']]['mail'] += $mail; // per day $days[$item['day'] . '.' . $item['month'] . '.' . $item['year']]['total'] += $total; $days[$item['day'] . '.' . $item['month'] . '.' . $item['year']]['http'] += $http; $days[$item['day'] . '.' . $item['month'] . '.' . $item['year']]['ftp'] += $ftp; $days[$item['day'] . '.' . $item['month'] . '.' . $item['year']]['mail'] += $mail; } } // calculate overview for given range from users $metrics = [ 'total' => 0.00, 'http' => 0.00, 'ftp' => 0.00, 'mail' => 0.00, ]; foreach ($users as $user) { $metrics['total'] += $user['total']; $metrics['http'] += $user['http']; $metrics['ftp'] += $user['ftp']; $metrics['mail'] += $user['mail']; } $years_avail = []; if (!$overview) { // get all possible years for filter $sel_stmt = Database::prepare("SELECT DISTINCT year FROM `" . TABLE_PANEL_TRAFFIC . "` WHERE 1 ORDER BY `year` DESC"); Database::pexecute($sel_stmt); $years_avail = $sel_stmt->fetchAll(\PDO::FETCH_ASSOC); } // sort users by total traffic uasort($users, function ($user_a, $user_b) { if ($user_a['total'] == $user_b['total']) { return 0; } return ($user_a['total'] < $user_b['total']) ? 1 : -1; }); return [ 'metrics' => $metrics, 'users' => $users, 'years' => $years, 'months' => $months, 'days' => $days, 'range' => $range, 'years_avail' => $years_avail ]; } /** * @param ?string $range * @param array $params * @return array * @throws \Exception */ private static function getParamsByRange(string $range = null, array $params = []): array { $dateParams = []; if (preg_match("/year:([0-9]{4})/", $range, $matches)) { $dateParams = ['year' => $matches[1]]; } elseif (preg_match("/months:([1-9]([0-9]+)?)/", $range, $matches)) { $dt = (new \DateTime())->sub(new \DateInterval('P' . $matches[1] . 'M'))->format('U'); $dateParams = ['date_from' => $dt]; } elseif (preg_match("/days:([1-9]([0-9]+)?)/", $range, $matches)) { $dt = (new \DateTime())->sub(new \DateInterval('P' . $matches[1] . 'D'))->format('U'); $dateParams = ['date_from' => $dt]; } elseif (preg_match("/hours:([1-9]([0-9]+)?)/", $range, $matches)) { $dt = (new \DateTime())->sub(new \DateInterval('PT' . $matches[1] . 'H'))->format('U'); $dateParams = ['date_from' => $dt]; } elseif (preg_match("/currentmonth/", $range, $matches)) { $dt = (new \DateTime("first day of this month"))->setTime(0, 0, 0, 1)->format('U'); $dateParams = ['date_from' => $dt]; } elseif (preg_match("/currentyear/", $range, $matches)) { $dt = \DateTime::createFromFormat("d.m.Y", '01.01.' . date('Y'))->setTime(0, 0, 0, 1)->format('U'); $dateParams = ['date_from' => $dt]; } return array_merge($dateParams, $params); } } ================================================ FILE: lib/Froxlor/Traffic/index.html ================================================ ================================================ FILE: lib/Froxlor/UI/Callbacks/Admin.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\UI\Callbacks; use Froxlor\UI\Panel\UI; class Admin { public static function canChangeServerSettings(array $attributes) { return (bool)UI::getCurrentUser()['change_serversettings']; } public static function isNotMe(array $attributes) { return (UI::getCurrentUser()['adminid'] != $attributes['fields']['adminid']); } } ================================================ FILE: lib/Froxlor/UI/Callbacks/Customer.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\UI\Callbacks; use Froxlor\Settings; use Froxlor\System\Markdown; class Customer { public static function isLocked(array $attributes): bool { return $attributes['fields']['loginfail_count'] >= Settings::Get('login.maxloginattempts') && $attributes['fields']['lastlogin_fail'] > (time() - Settings::Get('login.deactivatetime')); } public static function hasNote(array $attributes): bool { $cleanNote = Markdown::cleanCustomNotes($attributes['fields']['custom_notes'] ?? ""); return !empty($cleanNote); } } ================================================ FILE: lib/Froxlor/UI/Callbacks/Dns.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\UI\Callbacks; class Dns { public static function prio(array $attributes): string { return ($attributes['fields']['prio'] <= 0 && $attributes['fields']['type'] != 'MX' && $attributes['fields']['type'] != 'SRV') ? '' : $attributes['data']; } } ================================================ FILE: lib/Froxlor/UI/Callbacks/Domain.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\UI\Callbacks; use Froxlor\CurrentUser; use Froxlor\Database\Database; use Froxlor\Domain\Domain as DDomain; use Froxlor\FileDir; use Froxlor\Settings; use Froxlor\UI\Panel\UI; class Domain { public static function domainEditLink(array $attributes): array { $linker = UI::getLinker(); return [ 'macro' => 'link', 'data' => [ 'text' => $attributes['data'], 'href' => $linker->getLink([ 'section' => 'domains', 'page' => 'domains', 'action' => 'edit', 'id' => $attributes['fields']['id'], ]), 'target' => '_blank' ] ]; } public static function domainWithCustomerLink(array $attributes): string { $linker = UI::getLinker(); $result = '' . $attributes['data'] . ''; if ((int)UI::getCurrentUser()['adminsession'] == 1 && $attributes['fields']['customerid']) { $result .= ' (' . $attributes['fields']['loginname'] . ')'; } return $result; } public static function domainTarget(array $attributes) { if (empty($attributes['fields']['aliasdomain'])) { if ($attributes['fields']['deactivated']) { return lng('admin.deactivated'); } if ($attributes['fields']['email_only']) { return lng('domains.email_only'); } // path or redirect if (preg_match('/^https?\:\/\//', $attributes['fields']['documentroot'])) { return [ 'macro' => 'link', 'data' => [ 'text' => $attributes['fields']['documentroot'], 'href' => $attributes['fields']['documentroot'], 'target' => '_blank' ] ]; } else { // show docroot nicely if (strpos($attributes['fields']['documentroot'], UI::getCurrentUser()['documentroot']) === 0) { $attributes['fields']['documentroot'] = FileDir::makeCorrectDir(str_replace(UI::getCurrentUser()['documentroot'], "/", $attributes['fields']['documentroot'])); } return $attributes['fields']['documentroot']; } } return lng('domains.aliasdomain') . ' ' . $attributes['fields']['aliasdomain']; } public static function domainExternalLinkInfo(array $attributes): string { $result = ''; if ($attributes['fields']['parentdomainid'] != 0) { $result = ''; } $result .= '' . $attributes['data'] . ''; // check for statistics if parentdomainid==0 to show stats-link for customers if ((int)UI::getCurrentUser()['adminsession'] == 0 && $attributes['fields']['parentdomainid'] == 0 && $attributes['fields']['deactivated'] == 0 && !$attributes['fields']['email_only'] && preg_match('/^https?:\/\/(.*)/i', $attributes['fields']['documentroot']) == false ) { $statsapp = Settings::Get('system.traffictool'); $result .= ' '; } if ($attributes['fields']['registration_date'] != '') { $result .= '
' . lng('domains.registration_date') . ': ' . $attributes['fields']['registration_date'] . ''; } if ($attributes['fields']['termination_date'] != '') { $result .= '
' . lng('domains.termination_date_overview') . ': ' . $attributes['fields']['termination_date'] . ''; } return $result; } public static function canEdit(array $attributes): bool { return $attributes['fields']['caneditdomain'] && !$attributes['fields']['deactivated']; } public static function canViewLogs(array $attributes): bool { if ((int)$attributes['fields']['email_only'] == 0 && !$attributes['fields']['deactivated']) { if ((int)UI::getCurrentUser()['adminsession'] == 0 && (bool)UI::getCurrentUser()['logviewenabled']) { return true; } elseif ((int)UI::getCurrentUser()['adminsession'] == 1) { return true; } } return false; } public static function canDelete(array $attributes): bool { return $attributes['fields']['parentdomainid'] != '0' && empty($attributes['fields']['domainaliasid']); } public static function adminCanDelete(array $attributes): bool { return $attributes['fields']['id'] != Settings::Get('system.hostname_id') && empty($attributes['fields']['domainaliasid']) && $attributes['fields']['standardsubdomain'] != $attributes['fields']['id']; } public static function canEditDNS(array $attributes): bool { return $attributes['fields']['isbinddomain'] == '1' && UI::getCurrentUser()['dnsenabled'] == '1' && $attributes['fields']['caneditdomain'] == '1' && Settings::Get('system.bind_enable') == '1' && Settings::Get('system.dnsenabled') == '1' && !$attributes['fields']['deactivated']; } public static function adminCanEditDNS(array $attributes): bool { return $attributes['fields']['isbinddomain'] == '1' && Settings::Get('system.bind_enable') == '1' && Settings::Get('system.dnsenabled') == '1'; } public static function hasLetsEncryptActivated(array $attributes): bool { return ((bool)$attributes['fields']['letsencrypt'] && (int)$attributes['fields']['email_only'] == 0); } /** * @throws \Exception */ public static function canEditSSL(array $attributes): bool { if (Settings::Get('system.use_ssl') == '1' && DDomain::domainHasSslIpPort($attributes['fields']['id']) && (CurrentUser::isAdmin() || (!CurrentUser::isAdmin() && (int)$attributes['fields']['caneditdomain'] == 1)) && (int)$attributes['fields']['letsencrypt'] == 0 && !(int)$attributes['fields']['email_only'] && !$attributes['fields']['deactivated'] ) { return true; } return false; } public static function canEditAlias(array $attributes): bool { return !empty($attributes['fields']['domainaliasid']); } public static function isAssigned(array $attributes): bool { return ($attributes['fields']['parentdomainid'] == 0 && empty($attributes['fields']['domainaliasid'])); } public static function editSSLButtons(array $attributes): array { $result = [ 'icon' => 'fa-solid fa-shield', 'title' => lng('panel.ssleditor'), 'href' => [ 'section' => 'domains', 'page' => 'domainssleditor', 'action' => 'view', 'id' => ':id' ], ]; if ($attributes['fields']['domain_hascert'] == 1) { // specified certificate for domain $result['icon'] .= ' text-success'; } elseif ($attributes['fields']['domain_hascert'] == 2) { // shared certificates (e.g. subdomain of domain where certificate is specified) $result['icon'] .= ' text-warning'; $result['title'] .= "\n" . lng('panel.ssleditor_infoshared'); } elseif ($attributes['fields']['domain_hascert'] == 0) { // no certificate specified, using global fallbacks (IPs and Ports or if empty SSL settings) $result['icon'] .= ' text-danger'; $result['title'] .= "\n" . lng('panel.ssleditor_infoglobal'); } $result['visible'] = [Domain::class, 'canEditSSL']; return $result; } public static function listIPs(array $attributes): string { if (!empty($attributes['fields']['ipsandports'])) { $iplist = ""; foreach ($attributes['fields']['ipsandports'] as $ipport) { $iplist .= $ipport['ip'] . ':' . $ipport['port'] . '
'; } return $iplist; } return lng('panel.listing_empty'); } /** * @throws \Exception */ public static function getPhpConfigName(array $attributes): string { $sel_stmt = Database::prepare("SELECT `description` FROM `" . TABLE_PANEL_PHPCONFIGS . "` WHERE `id` = :id"); $phpconfig = Database::pexecute_first($sel_stmt, ['id' => $attributes['data']]); if ((int)UI::getCurrentUser()['adminsession'] == 1) { $linker = UI::getLinker(); $result = '' . $phpconfig['description'] . ''; } else { $result = $phpconfig['description']; } return $result; } } ================================================ FILE: lib/Froxlor/UI/Callbacks/Email.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\UI\Callbacks; use Froxlor\PhpHelper; use Froxlor\Settings; class Email { public static function account(array $attributes) { return [ 'macro' => 'booleanWithInfo', 'data' => [ 'checked' => $attributes['data'] != 0, 'info' => $attributes['data'] != 0 ? PhpHelper::sizeReadable($attributes['fields']['mboxsize'], 'GiB', 'bi', '%01.' . (int)Settings::Get('panel.decimal_places') . 'f %s') : '' ] ]; } public static function forwarderList(array $attributes) { $forwarders = explode(" ", $attributes['data']); if (($key = array_search($attributes['fields']['email_full'], $forwarders)) !== false) { unset($forwarders[$key]); } if (count($forwarders) > 0) { return implode("
", $forwarders); } return ""; } } ================================================ FILE: lib/Froxlor/UI/Callbacks/Ftp.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\UI\Callbacks; use Froxlor\FileDir; use Froxlor\UI\Panel\UI; class Ftp { public static function pathRelative(array $attributes): string { if (strpos($attributes['data'], UI::getCurrentUser()['documentroot']) === 0) { $attributes['data'] = str_replace(UI::getCurrentUser()['documentroot'], "/", $attributes['data']); } $attributes['data'] = FileDir::makeCorrectDir($attributes['data']); return $attributes['data']; } } ================================================ FILE: lib/Froxlor/UI/Callbacks/Impersonate.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\UI\Callbacks; use Froxlor\UI\Panel\UI; class Impersonate { public static function apiAdminCustomerLink(array $attributes) { // my own key $isMyKey = false; if ($attributes['fields']['adminid'] == UI::getCurrentUser()['adminid'] && ((AREA == 'admin' && $attributes['fields']['customerid'] == 0) || (AREA == 'customer' && $attributes['fields']['customerid'] == UI::getCurrentUser()['customerid']) ) ) { // this is mine $isMyKey = true; } $adminCustomerLink = ""; if (AREA == 'admin') { if ($isMyKey) { $adminCustomerLink = $attributes['fields']['adminname']; } else { if (empty($attributes['fields']['customerid'])) { $adminCustomerLink = self::admin($attributes); } else { $attributes['data'] = $attributes['fields']['loginname']; $adminCustomerLink = self::customer($attributes); } } } else { // customer do not need links $adminCustomerLink = $attributes['fields']['loginname']; } return $adminCustomerLink; } public static function admin(array $attributes) { if (UI::getCurrentUser()['adminid'] != $attributes['fields']['adminid']) { $linker = UI::getLinker(); return [ 'macro' => 'link', 'data' => [ 'text' => $attributes['data'], 'href' => $linker->getLink([ 'section' => 'admins', 'page' => 'admins', 'action' => 'su', 'id' => $attributes['fields']['adminid'], ]), ] ]; } return $attributes['data']; } public static function customer(array $attributes): array { $linker = UI::getLinker(); return [ 'macro' => 'link', 'data' => [ 'text' => $attributes['data'], 'href' => $linker->getLink([ 'section' => 'customers', 'page' => 'customers', 'action' => 'su', 'sort' => $attributes['fields']['loginname'], 'id' => $attributes['fields']['customerid'], ]), ] ]; } } ================================================ FILE: lib/Froxlor/UI/Callbacks/Mysql.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\UI\Callbacks; use Froxlor\Database\Database; class Mysql { public static function dbserver(array $attributes): string { // get sql-root access data Database::needRoot(true, (int)$attributes['data'], false); Database::needSqlData(); $sql_root = Database::getSqlData(); Database::needRoot(false); return $sql_root['caption'] . '
' . $sql_root['host'] . ''; } } ================================================ FILE: lib/Froxlor/UI/Callbacks/PHPConf.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\UI\Callbacks; use Froxlor\Settings; use Froxlor\Idna\IdnaWrapper; use Froxlor\UI\Panel\UI; class PHPConf { public static function domainList(array $attributes): string { $idna = new IdnaWrapper; $domains = ""; $subdomains_count = count($attributes['fields']['subdomains']); foreach ($attributes['fields']['domains'] as $configdomain) { $domains .= $idna->decode($configdomain) . "
"; } if ($subdomains_count == 0 && empty($domains)) { $domains = lng('admin.phpsettings.notused'); } else { if (Settings::Get('panel.phpconfigs_hidesubdomains') == '1') { $domains .= !empty($subdomains_count) ? ((!empty($domains) ? '+ ' : '') . $subdomains_count . ' ' . lng('customer.subdomains')) : ''; } else { foreach ($attributes['fields']['subdomains'] as $configdomain) { $domains .= $idna->decode($configdomain) . "
"; } } } return $domains; } public static function configsList(array $attributes) { $configs = ""; foreach ($attributes['fields']['configs'] as $configused) { $configs .= $configused . "
"; } return $configs; } public static function isNotDefault(array $attributes) { if (UI::getCurrentUser()['change_serversettings']) { return $attributes['fields']['id'] != 1; } return false; } public static function fpmConfLink(array $attributes) { if (UI::getCurrentUser()['change_serversettings']) { $linker = UI::getLinker(); return [ 'macro' => 'link', 'data' => [ 'text' => $attributes['data'], 'href' => $linker->getLink([ 'section' => 'phpsettings', 'page' => 'fpmdaemons', 'searchfield' => 'id', 'searchtext' => $attributes['fields']['fpmsettingid'], ]), ] ]; } return $attributes['data']; } } ================================================ FILE: lib/Froxlor/UI/Callbacks/ProgressBar.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\UI\Callbacks; use Exception; use Froxlor\Traffic\Traffic; use Froxlor\PhpHelper; use Froxlor\Settings; use Froxlor\UI\Response; class ProgressBar { /** * get progressbar data for used diskspace * * @param array $attributes * @return array */ public static function diskspace(array $attributes): array { $infotext = null; if (isset($attributes['fields']['webspace_used']) && isset($attributes['fields']['mailspace_used']) && isset($attributes['fields']['dbspace_used'])) { $infotext = lng('panel.used') . ':' . PHP_EOL; $infotext .= 'web: ' . PhpHelper::sizeReadable($attributes['fields']['webspace_used'] * 1024, null, 'bi') . PHP_EOL; $infotext .= 'mail: ' . PhpHelper::sizeReadable($attributes['fields']['mailspace_used'] * 1024, null, 'bi') . PHP_EOL; $infotext .= 'mysql: ' . PhpHelper::sizeReadable($attributes['fields']['dbspace_used'] * 1024, null, 'bi'); } return self::pbData('diskspace', $attributes['fields'], 1024, (int)Settings::Get('system.report_webmax'), $infotext); } /** * do needed calculations */ private static function pbData(string $field, array $attributes, int $size_factor = 1024, int $report_max = 90, $infotext = null): array { $percent = 0; $style = 'bg-primary'; $text = PhpHelper::sizeReadable($attributes[$field . '_used'] * $size_factor, null, 'bi') . ' / ' . lng('panel.unlimited'); if ((int)$attributes[$field] >= 0) { if (($attributes[$field] / 100) * $report_max < $attributes[$field . '_used']) { $style = 'bg-danger'; } elseif (($attributes[$field] / 100) * ($report_max - 15) < $attributes[$field . '_used']) { $style = 'bg-warning'; } $percent = round(($attributes[$field . '_used'] * 100) / ($attributes[$field] == 0 ? 1 : $attributes[$field]), 0); if ($percent > 100) { $percent = 100; } $text = PhpHelper::sizeReadable($attributes[$field . '_used'] * $size_factor, null, 'bi') . ' / ' . PhpHelper::sizeReadable($attributes[$field] * $size_factor, null, 'bi'); } return [ 'macro' => 'progressbar', 'data' => [ 'percent' => $percent, 'style' => $style, 'text' => $text, 'infotext' => $infotext ] ]; } /** * get progressbar data for traffic * * @param array $attributes ['fields'] * @return array */ public static function traffic(array $attributes): array { $skip_customer_traffic = false; try { $attributes['fields']['deactivated'] = 0; $result = Traffic::getCustomerStats($attributes['fields'], 'currentmonth', true); } catch (Exception $e) { if ($e->getCode() === 405) { $skip_customer_traffic = true; } else { Response::dynamicError($e->getMessage()); } } $infotext = null; if (isset($result['metrics']['http']) && !$skip_customer_traffic) { $infotext = lng('panel.used') . ':' . PHP_EOL; $infotext .= 'http: ' . PhpHelper::sizeReadable($result['metrics']['http'], null, 'bi') . PHP_EOL; $infotext .= 'ftp: ' . PhpHelper::sizeReadable($result['metrics']['ftp'], null, 'bi') . PHP_EOL; $infotext .= 'mail: ' . PhpHelper::sizeReadable($result['metrics']['mail'], null, 'bi'); } return self::pbData('traffic', $attributes['fields'], 1024, (int)Settings::Get('system.report_trafficmax'), $infotext); } /** * get progressbar data for traffic for the admin overview * (key is to set 'adminsession' for the admin-users so the traffic-API selects * the correct customer data for the corresponsing admin/reseller) * * @param array $attributes ['fields'] * @return array */ public static function traffic_admins(array $attributes): array { $attributes['fields']['adminsession'] = 1; return self::traffic($attributes); } } ================================================ FILE: lib/Froxlor/UI/Callbacks/SSLCertificate.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\UI\Callbacks; class SSLCertificate { public static function domainWithSan(array $attributes): array { return [ 'macro' => 'domainWithSan', 'data' => [ 'domain' => $attributes['data'], 'san' => implode(', ', $attributes['fields']['san'] ?? []), ] ]; } public static function canEditSSL(array $attributes): bool { if ((int)$attributes['fields']['domainid'] > 0 && (int)$attributes['fields']['letsencrypt'] == 0 ) { return true; } return false; } public static function isNotLetsEncrypt(array $attributes): bool { return (int)$attributes['fields']['letsencrypt'] == 0; } } ================================================ FILE: lib/Froxlor/UI/Callbacks/Style.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\UI\Callbacks; use Froxlor\CurrentUser; use Froxlor\Settings; class Style { public static function deactivated(array $attributes): string { return $attributes['fields']['deactivated'] ? 'table-danger' : ''; } public static function loginDisabled(array $attributes): string { return $attributes['fields']['login_enabled'] == 'N' ? 'table-danger' : ''; } public static function resultIntegrityBad(array $attributes): string { return $attributes['fields']['result'] ? '' : 'table-warning'; } public static function invalidApiKey(array $attributes): string { // check whether the api key is not valid anymore $isValid = true; if ($attributes['fields']['valid_until'] >= 0) { if ($attributes['fields']['valid_until'] < time()) { $isValid = false; } } return $isValid ? '' : 'table-danger'; } public static function resultDomainTerminatedOrDeactivated(array $attributes): string { $termination_date = str_replace("0000-00-00", "", $attributes['fields']['termination_date'] ?? ''); $termination_css = ''; if (!empty($termination_date)) { $cdate = strtotime($termination_date . " 23:59:59"); $today = time(); $termination_css = 'table-warning'; if ($cdate < $today) { $termination_css = 'table-danger'; } } $deactivated = $attributes['fields']['deactivated'] || (CurrentUser::isAdmin() && $attributes['fields']['customer_deactivated']); return $deactivated ? 'table-info' : $termination_css; } public static function resultCustomerLockedOrDeactivated(array $attributes): string { $row_css = ''; if ((int)$attributes['fields']['deactivated'] == 1) { $row_css = 'table-info'; } elseif ($attributes['fields']['loginfail_count'] >= Settings::Get('login.maxloginattempts') && $attributes['fields']['lastlogin_fail'] > (time() - Settings::Get('login.deactivatetime')) ) { $row_css = 'table-warning'; } return $row_css; } public static function diskspaceWarning(array $attributes): string { return self::getWarningStyle('diskspace', $attributes['fields'], (int)Settings::Get('system.report_webmax')); } private static function getWarningStyle(string $field, array $attributes, int $report_max = 90): string { $style = ''; if ((int)$attributes[$field] >= 0) { if (($attributes[$field] / 100) * $report_max < $attributes[$field . '_used']) { $style = 'table-danger'; } elseif (($attributes[$field] / 100) * ($report_max - 15) < $attributes[$field . '_used']) { $style = 'table-warning'; } } return $style; } public static function trafficWarning(array $attributes): string { return self::getWarningStyle('traffic', $attributes['fields'], (int)Settings::Get('system.report_trafficmax')); } } ================================================ FILE: lib/Froxlor/UI/Callbacks/SysLog.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\UI\Callbacks; use Froxlor\FroxlorLogger; class SysLog { public static function typeDescription(array $attributes): string { return FroxlorLogger::getInstanceOf()->getLogLevelDesc($attributes['data']); } } ================================================ FILE: lib/Froxlor/UI/Callbacks/Text.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\UI\Callbacks; use Froxlor\CurrentUser; use Froxlor\Database\Database; use Froxlor\Froxlor; use Froxlor\PhpHelper; use Froxlor\System\Markdown; use Froxlor\UI\Panel\UI; use Froxlor\User; use PDO; class Text { public static function boolean(array $attributes): array { return [ 'macro' => 'boolean', 'data' => (bool)$attributes['data'] ]; } public static function yesno(array $attributes): array { return [ 'macro' => 'boolean', 'data' => $attributes['data'] == 'Y' ]; } public static function type2fa(array $attributes): array { return [ 'macro' => 'type2fa', 'data' => (int)$attributes['data'] ]; } public static function customerfullname(array $attributes): string { return User::getCorrectFullUserDetails($attributes['fields'], true); } public static function size(array $attributes): string { return PhpHelper::sizeReadable($attributes['data'], null, 'bi'); } public static function timestamp(array $attributes): string { return (int)$attributes['data'] > 0 ? date('d.m.Y H:i', (int)$attributes['data']) : lng('panel.never'); } public static function timestampUntil(array $attributes): string { return (int)$attributes['data'] > 0 ? date('d.m.Y H:i', (int)$attributes['data']) : lng('panel.unlimited'); } public static function crondesc(array $attributes): string { return lng('crondesc.' . $attributes['data']); } public static function shorten(array $attributes): string { return substr($attributes['data'], 0, 20) . '...'; } public static function wordwrap(array $attributes): string { return wordwrap($attributes['data'], 100, '
', true); } public static function customerNoteDetailModal(array $attributes): array { $note = $attributes['fields']['custom_notes'] ?? ''; $key = $attributes['fields']['customerid'] ?? $attributes['fields']['adminid']; return [ 'entry' => $key, 'id' => 'cnModal' . $key, 'title' => lng('usersettings.custom_notes.title') . ': ' . ($attributes['fields']['loginname'] ?? $attributes['fields']['adminname']), 'body' => nl2br(Markdown::cleanCustomNotes($note)) ]; } public static function apikeyDetailModal(array $attributes): array { $linker = UI::getLinker(); $result = $attributes['fields']; $apikey_data = include Froxlor::getInstallDir() . '/lib/formfields/formfield.api_key.php'; $body = UI::twig()->render(UI::validateThemeTemplate('/user/inline-form.html.twig'), [ 'formaction' => $linker->getLink(['section' => 'index', 'page' => 'apikeys']), 'formdata' => $apikey_data['apikey'], 'editid' => $attributes['fields']['id'] ]); return [ 'entry' => $attributes['fields']['id'], 'id' => 'akModal' . $attributes['fields']['id'], 'title' => 'API-key ' . ($attributes['fields']['loginname'] ?? $attributes['fields']['adminname']), 'action' => 'apikeys', 'body' => $body ]; } public static function domainDuplicateModal(array $attributes): array { $linker = UI::getLinker(); $result = $attributes['fields']; $customers = [ 0 => lng('panel.please_choose') ]; $result_customers_stmt = Database::prepare(" SELECT `customerid`, `loginname`, `name`, `firstname`, `company` FROM `" . TABLE_PANEL_CUSTOMERS . "` " . (CurrentUser::getField('customers_see_all') ? '' : " WHERE `adminid` = :adminid ") . " ORDER BY COALESCE(NULLIF(`name`,''), `company`) ASC "); $params = []; if (CurrentUser::getField('customers_see_all') == '0') { $params['adminid'] = CurrentUser::getField('adminid'); } Database::pexecute($result_customers_stmt, $params); while ($row_customer = $result_customers_stmt->fetch(PDO::FETCH_ASSOC)) { $customers[$row_customer['customerid']] = User::getCorrectFullUserDetails($row_customer) . ' (' . $row_customer['loginname'] . ')'; } $domdup_data = include Froxlor::getInstallDir() . '/lib/formfields/admin/domains/formfield.domains_duplicate.php'; $body = UI::twig()->render(UI::validateThemeTemplate('/user/inline-form.html.twig'), [ 'formaction' => $linker->getLink(['section' => 'domains', 'page' => 'domains', 'action' => 'duplicate']), 'formdata' => $domdup_data['domain_duplicate'], 'editid' => $attributes['fields']['id'], 'nosubmit' => 0 ]); return [ 'entry' => $attributes['fields']['id'], 'id' => 'ddModal' . $attributes['fields']['id'], 'title' => lng('admin.domain_duplicate_named', [$attributes['fields']['domain']]), 'action' => 'duplicate', 'body' => $body ]; } } ================================================ FILE: lib/Froxlor/UI/Callbacks/index.html ================================================ ================================================ FILE: lib/Froxlor/UI/Collection.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\UI; use Froxlor\Settings; class Collection { private string $class; private array $has = []; private array $params; private array $userinfo; private ?Pagination $pagination = null; private bool $internal = false; public function __construct(string $class, array $userInfo, array $params = []) { $this->class = $class; $this->params = $params; $this->userinfo = $userInfo; } public function getList(): array { return $this->getData()['list']; } public function getData(): array { return $this->get()['data']; } public function get(): array { $result = $this->getListing($this->class, $this->params); // check if the api result contains any items (not the overall listingCount as we might be in a search-resultset) if (count($result)) { foreach ($this->has as $has) { $attributes = $this->getListing($has['class'], $has['params']); foreach ($result['data']['list'] as $key => $item) { foreach ($attributes['data']['list'] as $list) { if ($item[$has['parentKey']] == $list[$has['childKey']]) { $result['data']['list'][$key][$has['column']] = $list; } } } } } // attach pagination if available if ($this->pagination) { $result = array_merge($result, $this->pagination->getApiResponseParams()); } return $result; } private function getListing($class, $params): array { return json_decode($class::getLocal($this->userinfo, $params, $this->internal)->listing(), true); } public function getJson(): string { return json_encode($this->get()); } public function has(string $column, string $class, string $parentKey = 'id', string $childKey = 'id', array $params = []): Collection { $this->has[] = [ 'column' => $column, 'class' => $class, 'parentKey' => $parentKey, 'childKey' => $childKey, 'params' => $params ]; return $this; } public function addParam(array $keyval): Collection { $this->params = array_merge($this->params, $keyval); return $this; } public function withPagination(array $columns, array $default_sorting = [], array $pagination_additional_params = []): Collection { // Get only searchable columns $sortableColumns = []; foreach ($columns as $key => $column) { if (!isset($column['sortable']) || (isset($column['sortable']) && $column['sortable'])) { $sortableColumns[$key] = $column; } } // Prepare pagination $this->pagination = new Pagination($sortableColumns, $this->count(), (int)Settings::Get('panel.paging'), $default_sorting, $pagination_additional_params); $this->params = array_merge($this->params, $this->pagination->getApiCommandParams()); $this->pagination->setEntries($this->count(true)); return $this; } public function count(bool $with_pagination = false): int { if ($with_pagination) { $this->params = array_merge($this->params, $this->pagination->getApiCommandParams()); } return json_decode($this->class::getLocal($this->userinfo, $this->params, $this->internal)->listingCount(), true)['data']; } public function getPagination(): ?Pagination { return $this->pagination; } public function setInternal(bool $internal): Collection { $this->internal = $internal; return $this; } } ================================================ FILE: lib/Froxlor/UI/Data.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\UI; class Data { public static function getFormFieldDataEmail($fieldname, $fielddata, $input) { return self::getFormFieldDataText($fieldname, $fielddata, $input); } public static function getFormFieldDataText($fieldname, $fielddata, $input) { if (isset($input[$fieldname])) { $newfieldvalue = str_replace("\r\n", "\n", $input[$fieldname]); } else { $newfieldvalue = $fielddata['default']; } return $newfieldvalue; } public static function getFormFieldDataUrl($fieldname, $fielddata, $input) { return self::getFormFieldDataText($fieldname, $fielddata, $input); } public static function getFormFieldDataSelect($fieldname, $fielddata, $input) { if (isset($input[$fieldname])) { $newfieldvalue = $input[$fieldname]; } else { $newfieldvalue = $fielddata['default']; } if (is_array($newfieldvalue)) { $newfieldvalue = implode(',', $newfieldvalue); } return $newfieldvalue; } public static function getFormFieldDataNumber($fieldname, $fielddata, $input) { if (isset($input[$fieldname])) { $newfieldvalue = (int)$input[$fieldname]; } else { $newfieldvalue = (int)$fielddata['default']; } return $newfieldvalue; } public static function getFormFieldDataCheckbox($fieldname, $fielddata, $input) { if (isset($input[$fieldname]) && ($input[$fieldname] === '1' || $input[$fieldname] === 1 || $input[$fieldname] === true || strtolower($input[$fieldname]) === 'yes' || strtolower($input[$fieldname]) === 'ja')) { $newfieldvalue = '1'; } else { $newfieldvalue = '0'; } 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) { $newfieldvalue = strtotime($newfieldvalue); } return $newfieldvalue; } } ================================================ FILE: lib/Froxlor/UI/Form.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\UI; use Froxlor\CurrentUser; use Froxlor\FroxlorTwoFactorAuth; use Froxlor\Settings; use Froxlor\Validate\Check; class Form { public static function buildForm(array $form, string $part = ''): array { $fields = []; if (\Froxlor\Validate\Form::validateFormDefinition($form)) { foreach ($form['groups'] as $groupname => $groupdetails) { // check for advanced mode sections if (isset($groupdetails['advanced_mode']) && $groupdetails['advanced_mode'] && (int)Settings::Get('panel.settings_mode') == 0) { continue; } // show overview if ($part == '' || $part == 'all') { if (isset($groupdetails['title']) && $groupdetails['title'] != '') { $fields[] = self::getFormOverviewGroupOutput($groupname, $groupdetails); } } elseif ($part != '' && $groupname == $part) { // only show one section /** * this part checks for the 'websrv_avail' entry in the settings-array * if found, we check if the current webserver is in the array. * If this * is not the case, we change the setting type to "hidden", #502 */ $do_show = true; if (isset($groupdetails['websrv_avail']) && is_array($groupdetails['websrv_avail'])) { $websrv = Settings::Get('system.webserver'); if (!in_array($websrv, $groupdetails['websrv_avail'])) { $do_show = false; } } // visible = Settings::Get('phpfpm.enabled') for example would result in false if not enabled // and therefore not shown as intended. Only check if do_show is still true as it might // be false due to websrv_avail if (isset($groupdetails['visible']) && $do_show) { $do_show = $groupdetails['visible']; } $fields['_group'] = [ 'title' => $groupdetails['title'] ?? 'unknown group', 'do_show' => $do_show ]; if (\Froxlor\Validate\Form::validateFieldDefinition($groupdetails)) { // Collect form field output foreach ($groupdetails['fields'] as $fieldname => $fielddetails) { // check for advanced mode sections if (isset($fielddetails['advanced_mode']) && $fielddetails['advanced_mode'] && (int)Settings::Get('panel.settings_mode') == 0) { continue; } $fields[$fieldname] = self::getFormFieldOutput($fieldname, $fielddetails); $fields[$fieldname] = array_merge($fields[$fieldname], self::prefetchFormFieldData($fieldname, $fielddetails)); } } } } } return $fields; } public static function getFormOverviewGroupOutput($groupname, $groupdetails) { $activated = true; if (isset($groupdetails['fields'])) { foreach ($groupdetails['fields'] as $fielddetails) { if (isset($fielddetails['overview_option']) && $fielddetails['overview_option'] == true) { if ($fielddetails['type'] != 'checkbox') { // throw exception here as this is most likely an internal issue // if we messed up the arrays Response::standardError('overviewsettingoptionisnotavalidfield', '', true); } $activated = (int)Settings::Get($fielddetails['settinggroup'] . '.' . $fielddetails['varname']); break; } } } $item = [ 'title' => $groupdetails['title'], 'icon' => $groupdetails['icon'] ?? 'fa-solid fa-circle-question', 'part' => $groupname, 'activated' => $activated ]; /** * this part checks for the 'websrv_avail' entry in the settings * if found, we check if the current webserver is in the array. * If this is not the case, we change the setting type to "hidden", #502 */ if (isset($groupdetails['websrv_avail']) && is_array($groupdetails['websrv_avail'])) { $websrv = Settings::Get('system.webserver'); if (!in_array($websrv, $groupdetails['websrv_avail'])) { $item['info'] = lng('serversettings.option_unavailable_websrv', [implode(", ", $groupdetails['websrv_avail'])]); $item['visible'] = false; } } return $item; } public static function getFormFieldOutput($fieldname, $fielddata): array { $returnvalue = []; if (is_array($fielddata) && isset($fielddata['type']) && $fielddata['type'] != '') { if (!isset($fielddata['value'])) { if (isset($fielddata['default'])) { $fielddata['value'] = $fielddata['default']; } else { $fielddata['value'] = null; } } // set value according to type switch ($fielddata['type']) { case 'select': $fielddata['selected'] = $fielddata['value']; unset($fielddata['value']); if (isset($fielddata['select_mode']) && $fielddata['select_mode'] == 'multiple') { $fielddata['selected'] = array_flip(explode(",", $fielddata['selected'])); } break; case 'checkbox': $fielddata['checked'] = (bool)$fielddata['value']; $fielddata['value'] = 1; break; } /** * this part checks for the 'websrv_avail' entry in the settings-array * if found, we check if the current webserver is in the array. * If this * is not the case, we change the setting type to "hidden", #502 */ $do_show = true; if (isset($fielddata['websrv_avail']) && is_array($fielddata['websrv_avail'])) { $websrv = Settings::Get('system.webserver'); if (!in_array($websrv, $fielddata['websrv_avail'])) { $do_show = false; $fielddata['note'] = lng('serversettings.option_unavailable_websrv', [implode(", ", $fielddata['websrv_avail'])]); } } // visible = Settings::Get('phpfpm.enabled') for example would result in false if not enabled // and therefore not shown as intended. Only check if do_show is still true as it might // be false due to websrv_avail if (isset($fielddata['visible']) && $do_show) { $do_show = $fielddata['visible']; if (!$do_show) { $fielddata['note'] = lng('serversettings.option_unavailable'); } } // OTP security validation for sensitive settings if (!Settings::Config('disable_otp_security_check') && isset($fielddata['required_otp']) && $do_show) { $otp_enabled_system = (bool)Settings::Get('2fa.enabled'); $otp_enabled_user = (int)CurrentUser::getField('type_2fa') != 0; $do_show = !$fielddata['required_otp'] || ($otp_enabled_system && $otp_enabled_user); if (!$do_show) { $fielddata['note'] = lng('serversettings.option_requires_otp'); if (!$otp_enabled_system) { $fielddata['disabled'] = true; $fielddata['note'] .= '
' . lng('2fa.2fa_not_activated'); } elseif (!$otp_enabled_user) { $fielddata['disabled'] = true; $fielddata['note'] .= '
' . lng('2fa.2fa_not_activated_for_user'); } // show field in any case $do_show = true; } } if (!$do_show) { $fielddata['visible'] = false; } $returnvalue = $fielddata; } return $returnvalue; } public static function prefetchFormFieldData($fieldname, $fielddata) { $returnvalue = []; if (is_array($fielddata) && isset($fielddata['type']) && $fielddata['type'] == 'select') { if ((empty($fielddata['select_var']) || !is_array($fielddata['select_var'])) && (isset($fielddata['option_options_method'])) ) { $returnvalue['select_var'] = call_user_func($fielddata['option_options_method']); } } return $returnvalue; } public static function processForm(&$form, &$input, $url_params = [], $part = null, bool $settings_all = false, $settings_part = null, bool $only_enabledisable = false) { if (\Froxlor\Validate\Form::validateFormDefinition($form)) { $submitted_fields = []; $changed_fields = []; $saved_fields = []; foreach ($form['groups'] as $groupname => $groupdetails) { if (($settings_part && $part == $groupname) || $settings_all || $only_enabledisable) { if (\Froxlor\Validate\Form::validateFieldDefinition($groupdetails)) { // Prefetch form fields foreach ($groupdetails['fields'] as $fieldname => $fielddetails) { if (!$only_enabledisable || isset($fielddetails['overview_option'])) { $groupdetails['fields'][$fieldname] = array_merge($fielddetails, self::prefetchFormFieldData($fieldname, $fielddetails)); $form['groups'][$groupname]['fields'][$fieldname] = $groupdetails['fields'][$fieldname]; } } } } } foreach ($form['groups'] as $groupname => $groupdetails) { if (($settings_part && $part == $groupname) || $settings_all || $only_enabledisable) { if (\Froxlor\Validate\Form::validateFieldDefinition($groupdetails)) { // Validate fields foreach ($groupdetails['fields'] as $fieldname => $fielddetails) { if (((isset($fielddetails['visible']) && $fielddetails['visible']) || !isset($fielddetails['visible'])) && (!$only_enabledisable || ($only_enabledisable && isset($fielddetails['overview_option'])))) { $newfieldvalue = self::getFormFieldData($fieldname, $fielddetails, $input); if ($newfieldvalue != $fielddetails['value']) { if (($error = \Froxlor\Validate\Form::validateFormField($fieldname, $fielddetails, $newfieldvalue)) !== true) { Response::standardError($error, $fieldname); } else { $changed_fields[$fieldname] = $newfieldvalue; } } $submitted_fields[$fieldname] = $newfieldvalue; } } } } } foreach ($form['groups'] as $groupname => $groupdetails) { if (($settings_part && $part == $groupname) || $settings_all || $only_enabledisable) { if (\Froxlor\Validate\Form::validateFieldDefinition($groupdetails)) { // Check fields for plausibility foreach ($groupdetails['fields'] as $fieldname => $fielddetails) { if (!isset($submitted_fields[$fieldname])) { // skip unset fields due to unavailability for this system/settings-set continue; } if (!$only_enabledisable || ($only_enabledisable && isset($fielddetails['overview_option']))) { if (($plausibility_check = self::checkPlausibilityFormField($fieldname, $fielddetails, $submitted_fields[$fieldname], $submitted_fields)) !== false) { if (is_array($plausibility_check) && isset($plausibility_check[0])) { if ($plausibility_check[0] == Check::FORMFIELDS_PLAUSIBILITY_CHECK_OK) { // Nothing to do here, everything's okay } elseif ($plausibility_check[0] == Check::FORMFIELDS_PLAUSIBILITY_CHECK_ERROR) { unset($plausibility_check[0]); $error = $plausibility_check[1]; unset($plausibility_check[1]); $targetname = implode(' ', $plausibility_check); Response::standardError($error, $targetname); } elseif ($plausibility_check[0] == Check::FORMFIELDS_PLAUSIBILITY_CHECK_QUESTION) { unset($plausibility_check[0]); $question = $plausibility_check[1]; unset($plausibility_check[1]); $targetname = implode(' ', $plausibility_check); if (!isset($input[$question])) { if (is_array($url_params) && isset($url_params['filename'])) { $filename = $url_params['filename']; unset($url_params['filename']); } else { $filename = ''; } HTML::askYesNo($question, $filename, array_merge($url_params, $submitted_fields, [ $question => $question ]), $targetname); } } else { Response::standardError('plausibilitychecknotunderstood'); } } } if (!Settings::Config('disable_otp_security_check') && isset($fielddetails['required_otp']) && isset($changed_fields[$fieldname])) { $otp_enabled_system = (bool)Settings::Get('2fa.enabled'); $otp_enabled_user = (int)CurrentUser::getField('type_2fa') != 0; $do_update = !$fielddetails['required_otp'] || ($otp_enabled_system && $otp_enabled_user); if ($do_update) { // setting that requires OTP verification if (empty($input['otp_verification'])) { // in case email 2fa is enabled, send it now CurrentUser::sendOtpEmail(); // build up form if (is_array($url_params) && isset($url_params['filename'])) { $filename = $url_params['filename']; unset($url_params['filename']); } else { $filename = ''; } HTML::askOTP('please_enter_otp', $filename, array_merge($url_params, $submitted_fields)); } else { // validate given OTP code $code = trim($input['otp_verification']); $tfa = new FroxlorTwoFactorAuth('Froxlor ' . Settings::Get('system.hostname')); $result = $tfa->verifyCode(CurrentUser::getField('data_2fa'), $code, 3); if (!$result) { Response::standardError('otpnotvalidated'); } } } else { // do not update this setting unset($changed_fields[$fieldname]); } } } } } } } foreach ($form['groups'] as $groupname => $groupdetails) { if (($settings_part && $part == $groupname) || $settings_all || $only_enabledisable) { if (\Froxlor\Validate\Form::validateFieldDefinition($groupdetails)) { // Save fields foreach ($groupdetails['fields'] as $fieldname => $fielddetails) { if (!$only_enabledisable || (isset($fielddetails['overview_option']))) { if (isset($changed_fields[$fieldname])) { if (($saved_field = self::saveFormField($fieldname, $fielddetails, self::manipulateFormFieldData($fieldname, $fielddetails, $changed_fields[$fieldname]))) !== false) { $saved_fields = array_merge($saved_fields, $saved_field); } else { Response::standardError('errorwhensaving', $fieldname); } } } } } } } // Save form return self::saveForm($form, $saved_fields); } return false; } public static function getFormFieldData($fieldname, $fielddata, &$input) { if (is_array($fielddata) && isset($fielddata['type']) && $fielddata['type'] != '' && method_exists('\\Froxlor\\UI\\Data', 'getFormFieldData' . ucfirst($fielddata['type']))) { $newfieldvalue = call_user_func([ '\\Froxlor\\UI\\Data', 'getFormFieldData' . ucfirst($fielddata['type']) ], $fieldname, $fielddata, $input); } else { if (isset($input[$fieldname])) { $newfieldvalue = $input[$fieldname]; } elseif (isset($fielddata['default'])) { $newfieldvalue = $fielddata['default']; } else { $newfieldvalue = false; } } return trim($newfieldvalue); } public static function checkPlausibilityFormField($fieldname, $fielddata, $newfieldvalue, $allnewfieldvalues) { $returnvalue = ''; if (is_array($fielddata) && isset($fielddata['plausibility_check_method']) && $fielddata['plausibility_check_method'] != '' && method_exists($fielddata['plausibility_check_method'][0], $fielddata['plausibility_check_method'][1])) { $returnvalue = call_user_func($fielddata['plausibility_check_method'], $fieldname, $fielddata, $newfieldvalue, $allnewfieldvalues); } else { $returnvalue = false; } return $returnvalue; } public static function saveFormField($fieldname, $fielddata, $newfieldvalue) { $returnvalue = ''; if (is_array($fielddata) && isset($fielddata['save_method']) && $fielddata['save_method'] != '') { $returnvalue = call_user_func([ '\\Froxlor\\Settings\\Store', $fielddata['save_method'] ], $fieldname, $fielddata, $newfieldvalue); } elseif (is_array($fielddata) && !isset($fielddata['save_method'])) { $returnvalue = []; } else { $returnvalue = false; } return $returnvalue; } public static function manipulateFormFieldData($fieldname, $fielddata, $newfieldvalue) { if (is_array($fielddata) && isset($fielddata['type']) && $fielddata['type'] != '' && method_exists('\\Froxlor\\UI\\Data', 'manipulateFormFieldData' . ucfirst($fielddata['type']))) { $newfieldvalue = call_user_func([ '\\Froxlor\\UI\\Data', 'manipulateFormFieldData' . ucfirst($fielddata['type']) ], $fieldname, $fielddata, $newfieldvalue); } return $newfieldvalue; } public static function saveForm($fielddata, $newfieldvalue) { $returnvalue = ''; if (is_array($fielddata) && isset($fielddata['save_method']) && $fielddata['save_method'] != '') { $returnvalue = call_user_func([ '\\Froxlor\\Settings\\Store', $fielddata['save_method'] ], $fielddata, $newfieldvalue); } elseif (is_array($fielddata) && !isset($fielddata['save_method'])) { $returnvalue = true; } else { $returnvalue = false; } return $returnvalue; } } ================================================ FILE: lib/Froxlor/UI/HTML.php ================================================ * @license https://files.froxlor.org/misc/COPYING.txt GPLv2 */ namespace Froxlor\UI; use Froxlor\Settings; class HTML { /** * Build Navigation Sidebar * * @param array $navigation data * @param array $userinfo the userinfo of the user * * @return array the content of the navigation bar according to user-permissions */ public static function buildNavigation(array $navigation, array $userinfo) { $returnvalue = []; // sanitize user-given input (url-manipulation) $req_page = Request::get('page'); if (!empty($req_page) && is_array($req_page)) { $req_page = (string)array_shift($req_page); } // need to preserve this $_GET['page'] = $req_page; $req_action = Request::get('action'); if (!empty($req_action) && is_array($req_action)) { $req_action = (string)array_shift($req_action); } // need to preserve this $_GET['action'] = $req_action; foreach ($navigation as $box) { if ((!isset($box['show_element']) || $box['show_element'] === true) && (!isset($box['required_resources']) || $box['required_resources'] == '' || (isset($userinfo[$box['required_resources']]) && ((int)$userinfo[$box['required_resources']] > 0 || $userinfo[$box['required_resources']] == '-1')))) { $navigation_links = []; $box_active = false; if (isset($box['url']) && $box['url'] == basename($_SERVER["SCRIPT_FILENAME"])) { $box_active = true; } foreach ($box['elements'] as $element) { if ((!isset($element['show_element']) || $element['show_element'] === true) && (!isset($element['required_resources']) || $element['required_resources'] == '' || (isset($userinfo[$element['required_resources']]) && ((int)$userinfo[$element['required_resources']] > 0 || $userinfo[$element['required_resources']] == '-1')))) { $target = ''; $active = false; $navurl = '#'; if (isset($element['url']) && trim($element['url']) != '') { if (isset($element['new_window']) && $element['new_window'] == true) { $target = ' target="_blank"'; } if ( ((empty($req_page) && substr_count($element['url'], "page=") == 0) || (!empty($req_page) && substr_count($element['url'], "page=" . $req_page) > 0)) && substr_count($element['url'], basename($_SERVER["SCRIPT_FILENAME"])) > 0 ) { $active = true; $box_active = true; } $navurl = htmlspecialchars($element['url']); $navlabel = $element['label']; $icon = $element['icon'] ?? null; } else { $navlabel = $element['label']; $icon = $element['icon'] ?? null; } $navigation_links[] = [ 'url' => $navurl, 'target' => $target, 'active' => $active, 'label' => $navlabel, 'icon' => $icon, 'add_shortlink' => $element['add_shortlink'] ?? null, 'is_external' => $element['is_external'] ?? false, ]; } } if (!empty($navigation_links)) { $target = ''; if (isset($box['url']) && trim($box['url']) != '') { if (isset($box['new_window']) && $box['new_window'] == true) { $target = ' target="_blank"'; } $navurl = htmlspecialchars($box['url']); $navlabel = $box['label']; $icon = $box['icon'] ?? null; } else { $navurl = "#"; $navlabel = $box['label']; $icon = $box['icon'] ?? null; } $returnvalue[] = [ 'url' => $navurl, 'target' => $target, 'label' => $navlabel, 'icon' => $icon, 'items' => $navigation_links, 'active' => ((int)Settings::Get('panel.menu_collapsed') == 0 ? 1 : $box_active) ]; } } } return $returnvalue; } /** * Return HTML Code for an option within a