Files
cms-reports/Classes/Report/Status/ConfigurationStatus.php
T

357 lines
17 KiB
PHP

<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Reports\Report\Status;
use Doctrine\DBAL\Platforms\MariaDBPlatform as DoctrineMariaDBPlatform;
use Doctrine\DBAL\Platforms\MySQLPlatform as DoctrineMySQLPlatform;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Cache\Backend\MemcachedBackend;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Registry;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Reports\Status;
use TYPO3\CMS\Reports\Status as ReportStatus;
use TYPO3\CMS\Reports\StatusProviderInterface;
/**
* Performs some checks about the install tool protection status
*/
readonly class ConfigurationStatus implements StatusProviderInterface
{
public function __construct(
private UriBuilder $uriBuilder,
private Registry $registry,
private ConnectionPool $connectionPool,
) {}
/**
* Determines the Install Tool's status, mainly concerning its protection.
*
* @return Status[]
*/
public function getStatus(): array
{
$statuses = [
'emptyReferenceIndex' => $this->getReferenceIndexStatus(),
];
if ($this->isMemcachedUsed()) {
$statuses['memcachedConnection'] = $this->getMemcachedConnectionStatus();
}
if (!Environment::isWindows()) {
$statuses['createdFilesWorldWritable'] = $this->getCreatedFilesWorldWritableStatus();
$statuses['createdDirectoriesWorldWritable'] = $this->getCreatedDirectoriesWorldWritableStatus();
}
if ($this->isMysqlUsed()) {
$statuses['mysqlDatabaseUsesUtf8'] = $this->getMysqlDatabaseUtf8Status();
}
return $statuses;
}
public function getLabel(): string
{
return 'configuration';
}
/**
* Checks if sys_refindex is empty.
*
* @return \TYPO3\CMS\Reports\Status An object representing whether the reference index is empty or not
*/
protected function getReferenceIndexStatus(): ReportStatus
{
$value = $this->getLanguageService()->sL('LLL:EXT:reports/Resources/Private/Language/locallang_reports.xlf:status_ok');
$message = '';
$severity = ContextualFeedbackSeverity::OK;
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_refindex');
$count = $queryBuilder
->count('*')
->from('sys_refindex')
->executeQuery()
->fetchOne();
$lastRefIndexUpdate = $this->registry->get('core', 'sys_refindex_lastUpdate');
if (!$count && $lastRefIndexUpdate) {
$value = $this->getLanguageService()->sL('LLL:EXT:reports/Resources/Private/Language/locallang_reports.xlf:status_empty');
$severity = ContextualFeedbackSeverity::WARNING;
$url = (string)$this->uriBuilder->buildUriFromRoute('system_maintenance');
$message = sprintf($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.backend_reference_index'), '<a href="' . htmlspecialchars($url) . '">', '</a>', BackendUtility::datetime($lastRefIndexUpdate));
}
return new ReportStatus($this->getLanguageService()->sL('LLL:EXT:reports/Resources/Private/Language/locallang_reports.xlf:status_referenceIndex'), $value, $message, $severity);
}
/**
* Checks whether memcached is configured, if that's the case we assume it's also used.
*
* @return bool TRUE if memcached is used, FALSE otherwise.
*/
protected function isMemcachedUsed(): bool
{
$memcachedUsed = false;
$memcachedServers = $this->getConfiguredMemcachedServers();
if (!empty($memcachedServers)) {
$memcachedUsed = true;
}
return $memcachedUsed;
}
/**
* Gets the configured memcached server connections.
*
* @return array An array of configured memcached server connections.
*/
protected function getConfiguredMemcachedServers(): array
{
$configurations = $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations'] ?? [];
$memcachedServers = [];
foreach ($configurations as $table => $conf) {
if (is_array($conf)) {
foreach ($conf as $value) {
if ($value === MemcachedBackend::class && is_array($configurations[$table]['options']['servers'])) {
$memcachedServers = $configurations[$table]['options']['servers'];
break;
}
}
}
}
return $memcachedServers;
}
/**
* Checks whether TYPO3 can connect to the configured memcached servers.
*
* @return \TYPO3\CMS\Reports\Status An object representing whether TYPO3 can connect to the configured memcached servers
*/
protected function getMemcachedConnectionStatus(): ReportStatus
{
$value = $this->getLanguageService()->sL('LLL:EXT:reports/Resources/Private/Language/locallang_reports.xlf:status_ok');
$message = '';
$severity = ContextualFeedbackSeverity::OK;
$failedConnections = [];
$defaultMemcachedPort = ini_get('memcache.default_port');
$defaultMemcachedPort = MathUtility::canBeInterpretedAsInteger($defaultMemcachedPort) ? (int)$defaultMemcachedPort : 11211;
$memcachedServers = $this->getConfiguredMemcachedServers();
if (function_exists('memcache_connect') && $memcachedServers !== []) {
foreach ($memcachedServers as $testServer) {
$configuredServer = $testServer;
if (str_starts_with($testServer, 'unix://')) {
$host = $testServer;
$port = 0;
} else {
if (str_starts_with($testServer, 'tcp://')) {
$testServer = substr($testServer, 6);
}
if (str_contains($testServer, ':')) {
[$host, $port] = explode(':', $testServer, 2);
$port = (int)$port;
} else {
$host = $testServer;
$port = $defaultMemcachedPort;
}
}
$memcachedConnection = @memcache_connect($host, $port);
if ($memcachedConnection != null) {
@memcache_close($memcachedConnection);
} else {
$failedConnections[] = $configuredServer;
}
}
}
if (!empty($failedConnections)) {
$value = $this->getLanguageService()->sL('LLL:EXT:reports/Resources/Private/Language/locallang_reports.xlf:status_connectionFailed');
$severity = ContextualFeedbackSeverity::WARNING;
$message = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.memcache_not_usable') . '<br /><br /><ul><li>' . implode('</li><li>', $failedConnections) . '</li></ul>';
}
return new ReportStatus($this->getLanguageService()->sL('LLL:EXT:reports/Resources/Private/Language/locallang_reports.xlf:status_memcachedConfiguration'), $value, $message, $severity);
}
/**
* Warning, if fileCreateMask has write bit for 'others' set.
*
* @return \TYPO3\CMS\Reports\Status The writable status for 'others'
*/
protected function getCreatedFilesWorldWritableStatus(): ReportStatus
{
$value = $this->getLanguageService()->sL('LLL:EXT:reports/Resources/Private/Language/locallang_reports.xlf:status_ok');
$message = '';
$severity = ContextualFeedbackSeverity::OK;
if ((int)$GLOBALS['TYPO3_CONF_VARS']['SYS']['fileCreateMask'] % 10 & 2) {
$value = $GLOBALS['TYPO3_CONF_VARS']['SYS']['fileCreateMask'];
$severity = ContextualFeedbackSeverity::WARNING;
$message = $this->getLanguageService()->sL('LLL:EXT:reports/Resources/Private/Language/locallang_reports.xlf:status_CreatedFilePermissions.writable');
}
return new ReportStatus($this->getLanguageService()->sL('LLL:EXT:reports/Resources/Private/Language/locallang_reports.xlf:status_CreatedFilePermissions'), $value, $message, $severity);
}
/**
* Warning, if folderCreateMask has write bit for 'others' set.
*
* @return \TYPO3\CMS\Reports\Status The writable status for 'others'
*/
protected function getCreatedDirectoriesWorldWritableStatus(): ReportStatus
{
$value = $this->getLanguageService()->sL('LLL:EXT:reports/Resources/Private/Language/locallang_reports.xlf:status_ok');
$message = '';
$severity = ContextualFeedbackSeverity::OK;
if ((int)$GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask'] % 10 & 2) {
$value = $GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask'];
$severity = ContextualFeedbackSeverity::WARNING;
$message = $this->getLanguageService()->sL('LLL:EXT:reports/Resources/Private/Language/locallang_reports.xlf:status_CreatedDirectoryPermissions.writable');
}
return new ReportStatus($this->getLanguageService()->sL('LLL:EXT:reports/Resources/Private/Language/locallang_reports.xlf:status_CreatedDirectoryPermissions'), $value, $message, $severity);
}
/**
* Checks if the default connection is a MySQL compatible database instance.
*/
protected function isMysqlUsed(): bool
{
$platform = $this->connectionPool
->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME)
->getDatabasePlatform();
return $platform instanceof DoctrineMariaDBPlatform || $platform instanceof DoctrineMySQLPlatform;
}
/**
* Checks the character set of the default database and reports an error if it is not utf-8.
*/
protected function getMysqlDatabaseUtf8Status(): ReportStatus
{
$collationConstraint = null;
$charset = '';
$connection = $this->connectionPool
->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);
$connectionParams = $connection->getParams();
$queryBuilder = $connection->createQueryBuilder();
$defaultDatabaseCharset = (string)$queryBuilder->select('DEFAULT_CHARACTER_SET_NAME')
->from('information_schema.SCHEMATA')
->where(
$queryBuilder->expr()->eq(
'SCHEMA_NAME',
$queryBuilder->createNamedParameter($connection->getDatabase())
)
)
->setMaxResults(1)
->executeQuery()
->fetchOne();
$severity = ContextualFeedbackSeverity::OK;
$statusValue = $this->getLanguageService()->sL('LLL:EXT:reports/Resources/Private/Language/locallang_reports.xlf:status_ok');
// also allow utf8mb3 and utf8mb4
if (!str_starts_with($defaultDatabaseCharset, 'utf8')) {
// If the default character set is e.g. latin1, BUT all tables in the system are UTF-8,
// we assume that TYPO3 has the correct charset for adding tables, and everything is fine
$queryBuilder = $connection->createQueryBuilder();
$nonUtf8TableCollationsFound = $queryBuilder->select('table_collation')
->from('information_schema.tables')
->where(
$queryBuilder->expr()->and(
$queryBuilder->expr()->eq('table_schema', $queryBuilder->quote((string)$connection->getDatabase())),
$queryBuilder->expr()->notLike('table_collation', $queryBuilder->quote('utf8%'))
)
)
->setMaxResults(1)
->executeQuery();
if ($nonUtf8TableCollationsFound->rowCount() > 0) {
$message = sprintf($this->getLanguageService()
->sL('LLL:EXT:reports/Resources/Private/Language/locallang_reports.xlf:status_MysqlDatabaseCharacterSet_Unsupported'), $defaultDatabaseCharset);
$severity = ContextualFeedbackSeverity::ERROR;
$statusValue = $this->getLanguageService()->sL('LLL:EXT:reports/Resources/Private/Language/locallang_reports.xlf:status_wrongValue');
} else {
$message = $this->getLanguageService()->sL('LLL:EXT:reports/Resources/Private/Language/locallang_reports.xlf:status_MysqlDatabaseCharacterSet_Info');
$severity = ContextualFeedbackSeverity::INFO;
$statusValue = $this->getLanguageService()->sL('LLL:EXT:reports/Resources/Private/Language/locallang_reports.xlf:status_info');
}
} elseif (isset($connectionParams['defaultTableOptions'])) {
$message = $this->getLanguageService()->sL('LLL:EXT:reports/Resources/Private/Language/locallang_reports.xlf:status_MysqlDatabaseCharacterSet_Ok');
$tableOptions = $connectionParams['defaultTableOptions'];
if (isset($tableOptions['collation'])) {
$collationConstraint = $queryBuilder->expr()->neq('table_collation', $queryBuilder->quote($tableOptions['collation']));
$charset = $tableOptions['collation'];
} elseif (isset($tableOptions['charset'])) {
$collationConstraint = $queryBuilder->expr()->notLike('table_collation', $queryBuilder->quote($tableOptions['charset'] . '%'));
$charset = $tableOptions['charset'];
}
if (isset($collationConstraint)) {
$queryBuilder = $connection->createQueryBuilder();
$wrongCollationTablesFound = $queryBuilder->select('table_collation')
->from('information_schema.tables')
->where(
$queryBuilder->expr()->and(
$queryBuilder->expr()->eq('table_schema', $queryBuilder->quote($connection->getDatabase())),
$collationConstraint
)
)
->setMaxResults(1)
->executeQuery();
if ($wrongCollationTablesFound->rowCount() > 0) {
$message = sprintf($this->getLanguageService()->sL('LLL:EXT:reports/Resources/Private/Language/locallang_reports.xlf:status_MysqlDatabaseCharacterSet_MixedCollations'), $charset);
$severity = ContextualFeedbackSeverity::ERROR;
$statusValue = $this->getLanguageService()->sL('LLL:EXT:reports/Resources/Private/Language/locallang_reports.xlf:status_checkFailed');
} else {
if (isset($tableOptions['collation'])) {
$collationConstraint = $queryBuilder->expr()->neq('collation_name', $queryBuilder->quote($tableOptions['collation']));
} elseif (isset($tableOptions['charset'])) {
$collationConstraint = $queryBuilder->expr()->notLike('collation_name', $queryBuilder->quote($tableOptions['charset'] . '%'));
}
$queryBuilder = $connection->createQueryBuilder();
$wrongCollationColumnsFound = $queryBuilder->select('collation_name')
->from('information_schema.columns')
->where(
$queryBuilder->expr()->and(
$queryBuilder->expr()->eq('table_schema', $queryBuilder->quote($connection->getDatabase())),
$collationConstraint
)
)
->setMaxResults(1)
->executeQuery();
if ($wrongCollationColumnsFound->rowCount() > 0) {
$message = sprintf($this->getLanguageService()->sL('LLL:EXT:reports/Resources/Private/Language/locallang_reports.xlf:status_MysqlDatabaseCharacterSet_MixedCollations'), $charset);
$severity = ContextualFeedbackSeverity::ERROR;
$statusValue = $this->getLanguageService()->sL('LLL:EXT:reports/Resources/Private/Language/locallang_reports.xlf:status_checkFailed');
}
}
}
} else {
$message = $this->getLanguageService()->sL('LLL:EXT:reports/Resources/Private/Language/locallang_reports.xlf:status_MysqlDatabaseCharacterSet_Ok');
}
return new ReportStatus(
$this->getLanguageService()->sL('LLL:EXT:reports/Resources/Private/Language/locallang_reports.xlf:status_MysqlDatabaseCharacterSet'),
$statusValue,
$message,
$severity
);
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}