ConfigurationStatus.php 15.5 KB
Newer Older
1
<?php
2

3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
7
8
 * 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.
9
 *
10
11
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
14
 * The TYPO3 project - inspiring people to share!
 */
15

16
17
namespace TYPO3\CMS\Reports\Report\Status;

18
use TYPO3\CMS\Backend\Routing\UriBuilder;
19
use TYPO3\CMS\Backend\Utility\BackendUtility;
20
use TYPO3\CMS\Core\Cache\Backend\MemcachedBackend;
21
use TYPO3\CMS\Core\Core\Environment;
22
use TYPO3\CMS\Core\Database\ConnectionPool;
23
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
24
use TYPO3\CMS\Core\Localization\LanguageService;
25
use TYPO3\CMS\Core\Registry;
26
use TYPO3\CMS\Core\Utility\GeneralUtility;
27
use TYPO3\CMS\Core\Utility\MathUtility;
28
use TYPO3\CMS\Reports\Status as ReportStatus;
29
use TYPO3\CMS\Reports\StatusProviderInterface;
30

31
32
33
/**
 * Performs some checks about the install tool protection status
 */
34
class ConfigurationStatus implements StatusProviderInterface
35
36
37
38
39
40
41
42
{
    /**
     * Determines the Install Tool's status, mainly concerning its protection.
     *
     * @return array List of statuses
     */
    public function getStatus()
    {
43
        $statuses = [
44
            'emptyReferenceIndex' => $this->getReferenceIndexStatus(),
45
        ];
46
47
48
        if ($this->isMemcachedUsed()) {
            $statuses['memcachedConnection'] = $this->getMemcachedConnectionStatus();
        }
49
        if (!Environment::isWindows()) {
50
51
52
            $statuses['createdFilesWorldWritable'] = $this->getCreatedFilesWorldWritableStatus();
            $statuses['createdDirectoriesWorldWritable'] = $this->getCreatedDirectoriesWorldWritableStatus();
        }
53
54
55
        if ($this->isMysqlUsed()) {
            $statuses['mysqlDatabaseUsesUtf8'] = $this->getMysqlDatabaseUtf8Status();
        }
56
57
        return $statuses;
    }
58

59
60
61
62
63
64
65
    /**
     * 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()
    {
66
        $value = $this->getLanguageService()->getLL('status_ok');
67
        $message = '';
68
        $severity = ReportStatus::OK;
69
70
71
72
73
74

        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_refindex');
        $count = $queryBuilder
            ->count('*')
            ->from('sys_refindex')
            ->execute()
75
            ->fetchOne();
76

77
        $registry = GeneralUtility::makeInstance(Registry::class);
78
        $lastRefIndexUpdate = $registry->get('core', 'sys_refindex_lastUpdate');
79
        /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
80
        $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
81
        if (!$count && $lastRefIndexUpdate) {
82
            $value = $this->getLanguageService()->getLL('status_empty');
83
            $severity = ReportStatus::WARNING;
84
            $url = (string)$uriBuilder->buildUriFromRoute('system_dbint', ['id' => 0, 'SET' => ['function' => 'refindex']]);
85
            $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));
86
        }
87
        return GeneralUtility::makeInstance(ReportStatus::class, $this->getLanguageService()->getLL('status_referenceIndex'), $value, $message, $severity);
88
    }
89

90
91
92
93
94
95
96
97
98
99
100
101
102
103
    /**
     * 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()
    {
        $memcachedUsed = false;
        $memcachedServers = $this->getConfiguredMemcachedServers();
        if (!empty($memcachedServers)) {
            $memcachedUsed = true;
        }
        return $memcachedUsed;
    }
104

105
106
107
108
109
110
111
    /**
     * Gets the configured memcached server connections.
     *
     * @return array An array of configured memcached server connections.
     */
    protected function getConfiguredMemcachedServers()
    {
112
        $configurations = $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations'] ?? [];
113
        $memcachedServers = [];
114
115
116
        foreach ($configurations as $table => $conf) {
            if (is_array($conf)) {
                foreach ($conf as $key => $value) {
117
                    if ($value === MemcachedBackend::class) {
118
119
                        $memcachedServers = $configurations[$table]['options']['servers'];
                        break;
120
121
122
123
124
125
                    }
                }
            }
        }
        return $memcachedServers;
    }
126

127
128
129
130
131
132
133
    /**
     * 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()
    {
134
        $value = $this->getLanguageService()->getLL('status_ok');
135
        $message = '';
136
        $severity = ReportStatus::OK;
137
        $failedConnections = [];
138
        $defaultMemcachedPort = ini_get('memcache.default_port');
139
        $defaultMemcachedPort = MathUtility::canBeInterpretedAsInteger($defaultMemcachedPort) ? (int)$defaultMemcachedPort : 11211;
140
141
142
143
        $memcachedServers = $this->getConfiguredMemcachedServers();
        if (function_exists('memcache_connect') && is_array($memcachedServers)) {
            foreach ($memcachedServers as $testServer) {
                $configuredServer = $testServer;
144
                if (strpos($testServer, 'unix://') === 0) {
145
146
147
                    $host = $testServer;
                    $port = 0;
                } else {
148
                    if (strpos($testServer, 'tcp://') === 0) {
149
150
                        $testServer = substr($testServer, 6);
                    }
151
                    if (strpos($testServer, ':') !== false) {
152
                        [$host, $port] = explode(':', $testServer, 2);
153
                        $port = (int)$port;
154
155
156
157
158
159
160
                    } else {
                        $host = $testServer;
                        $port = $defaultMemcachedPort;
                    }
                }
                $memcachedConnection = @memcache_connect($host, $port);
                if ($memcachedConnection != null) {
161
                    memcache_close();
162
163
164
165
166
167
                } else {
                    $failedConnections[] = $configuredServer;
                }
            }
        }
        if (!empty($failedConnections)) {
168
            $value = $this->getLanguageService()->getLL('status_connectionFailed');
169
            $severity = ReportStatus::WARNING;
170
            $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>';
171
        }
172
        return GeneralUtility::makeInstance(ReportStatus::class, $this->getLanguageService()->getLL('status_memcachedConfiguration'), $value, $message, $severity);
173
    }
174

175
176
177
178
179
180
181
    /**
     * Warning, if fileCreateMask has write bit for 'others' set.
     *
     * @return \TYPO3\CMS\Reports\Status The writable status for 'others'
     */
    protected function getCreatedFilesWorldWritableStatus()
    {
182
        $value = $this->getLanguageService()->getLL('status_ok');
183
        $message = '';
184
        $severity = ReportStatus::OK;
185
186
        if ((int)$GLOBALS['TYPO3_CONF_VARS']['SYS']['fileCreateMask'] % 10 & 2) {
            $value = $GLOBALS['TYPO3_CONF_VARS']['SYS']['fileCreateMask'];
187
            $severity = ReportStatus::WARNING;
188
            $message = $this->getLanguageService()->getLL('status_CreatedFilePermissions.writable');
189
        }
190
        return GeneralUtility::makeInstance(ReportStatus::class, $this->getLanguageService()->getLL('status_CreatedFilePermissions'), $value, $message, $severity);
191
    }
192

193
194
195
196
197
198
199
    /**
     * Warning, if folderCreateMask has write bit for 'others' set.
     *
     * @return \TYPO3\CMS\Reports\Status The writable status for 'others'
     */
    protected function getCreatedDirectoriesWorldWritableStatus()
    {
200
        $value = $this->getLanguageService()->getLL('status_ok');
201
        $message = '';
202
        $severity = ReportStatus::OK;
203
204
        if ((int)$GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask'] % 10 & 2) {
            $value = $GLOBALS['TYPO3_CONF_VARS']['SYS']['folderCreateMask'];
205
            $severity = ReportStatus::WARNING;
206
            $message = $this->getLanguageService()->getLL('status_CreatedDirectoryPermissions.writable');
207
        }
208
        return GeneralUtility::makeInstance(ReportStatus::class, $this->getLanguageService()->getLL('status_CreatedDirectoryPermissions'), $value, $message, $severity);
209
    }
210

211
    /**
212
     * Checks if the default connection is a MySQL compatible database instance.
213
214
215
216
217
     *
     * @return bool
     */
    protected function isMysqlUsed()
    {
218
219
220
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)
            ->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);

221
        return strpos($connection->getServerVersion(), 'MySQL') === 0;
222
223
224
    }

    /**
225
     * Checks the character set of the default database and reports an error if it is not utf-8.
226
227
228
229
230
     *
     * @return ReportStatus
     */
    protected function getMysqlDatabaseUtf8Status()
    {
231
232
        $collationConstraint = null;
        $charset = '';
233
234
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)
            ->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);
235
        /** @var QueryBuilder $queryBuilder */
236
237
238
239
        $queryBuilder = $connection->createQueryBuilder();
        $defaultDatabaseCharset = (string)$queryBuilder->select('DEFAULT_CHARACTER_SET_NAME')
            ->from('information_schema.SCHEMATA')
            ->where(
240
241
242
243
                $queryBuilder->expr()->eq(
                    'SCHEMA_NAME',
                    $queryBuilder->createNamedParameter($connection->getDatabase(), \PDO::PARAM_STR)
                )
244
245
246
            )
            ->setMaxResults(1)
            ->execute()
247
            ->fetchOne();
248
249
250
251

        $severity = ReportStatus::OK;
        $statusValue = $this->getLanguageService()->getLL('status_ok');
        // also allow utf8mb4
252
        if (strpos($defaultDatabaseCharset, 'utf8') !== 0) {
253
254
            // 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
255
            $queryBuilder = $connection->createQueryBuilder();
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
            $nonUtf8TableCollationsFound = $queryBuilder->select('table_collation')
                ->from('information_schema.tables')
                ->where(
                    $queryBuilder->expr()->andX(
                        $queryBuilder->expr()->eq('table_schema', $queryBuilder->quote($connection->getDatabase())),
                        $queryBuilder->expr()->notLike('table_collation', $queryBuilder->quote('utf8%'))
                    )
                )
                ->setMaxResults(1)
                ->execute();

            if ($nonUtf8TableCollationsFound->rowCount() > 0) {
                $message = sprintf($this->getLanguageService()
                    ->getLL('status_MysqlDatabaseCharacterSet_Unsupported'), $defaultDatabaseCharset);
                $severity = ReportStatus::ERROR;
                $statusValue = $this->getLanguageService()->getLL('status_wrongValue');
            } else {
                $message = $this->getLanguageService()->getLL('status_MysqlDatabaseCharacterSet_Info');
                $severity = ReportStatus::INFO;
                $statusValue = $this->getLanguageService()->getLL('status_info');
            }
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
        } elseif (isset($GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][ConnectionPool::DEFAULT_CONNECTION_NAME]['tableoptions'])) {
            $message = $this->getLanguageService()->getLL('status_MysqlDatabaseCharacterSet_Ok');

            $tableOptions = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections'][ConnectionPool::DEFAULT_CONNECTION_NAME]['tableoptions'];
            if (isset($tableOptions['collate'])) {
                $collationConstraint = $queryBuilder->expr()->neq('table_collation', $queryBuilder->quote($tableOptions['collate']));
                $charset = $tableOptions['collate'];
            } 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()->andX(
                            $queryBuilder->expr()->eq('table_schema', $queryBuilder->quote($connection->getDatabase())),
                            $collationConstraint
                        )
                    )
                    ->setMaxResults(1)
                    ->execute();

                if ($wrongCollationTablesFound->rowCount() > 0) {
                    $message = sprintf($this->getLanguageService()->getLL('status_MysqlDatabaseCharacterSet_MixedCollations'), $charset);
                    $severity = ReportStatus::ERROR;
                    $statusValue = $this->getLanguageService()->getLL('status_checkFailed');
                } else {
                    if (isset($tableOptions['collate'])) {
                        $collationConstraint = $queryBuilder->expr()->neq('collation_name', $queryBuilder->quote($tableOptions['collate']));
                    } 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()->andX(
                                $queryBuilder->expr()->eq('table_schema', $queryBuilder->quote($connection->getDatabase())),
                                $collationConstraint
                            )
                        )
                        ->setMaxResults(1)
                        ->execute();

                    if ($wrongCollationColumnsFound->rowCount() > 0) {
                        $message = sprintf($this->getLanguageService()->getLL('status_MysqlDatabaseCharacterSet_MixedCollations'), $charset);
                        $severity = ReportStatus::ERROR;
                        $statusValue = $this->getLanguageService()->getLL('status_checkFailed');
                    }
                }
            }
332
333
334
335
        } else {
            $message = $this->getLanguageService()->getLL('status_MysqlDatabaseCharacterSet_Ok');
        }

336
337
        return GeneralUtility::makeInstance(
            ReportStatus::class,
338
            $this->getLanguageService()->getLL('status_MysqlDatabaseCharacterSet'),
339
340
341
            $statusValue,
            $message,
            $severity
342
343
344
        );
    }

345
346
347
348
349
350
351
    /**
     * @return LanguageService
     */
    protected function getLanguageService()
    {
        return $GLOBALS['LANG'];
    }
352
}