diff options
Diffstat (limited to 'apps/settings/lib/Controller/CheckSetupController.php')
-rw-r--r-- | apps/settings/lib/Controller/CheckSetupController.php | 701 |
1 files changed, 701 insertions, 0 deletions
diff --git a/apps/settings/lib/Controller/CheckSetupController.php b/apps/settings/lib/Controller/CheckSetupController.php new file mode 100644 index 00000000000..620920f777b --- /dev/null +++ b/apps/settings/lib/Controller/CheckSetupController.php @@ -0,0 +1,701 @@ +<?php +/** + * @copyright Copyright (c) 2016, ownCloud, Inc. + * + * @author Arthur Schiwon <blizzz@arthur-schiwon.de> + * @author Bjoern Schiessle <bjoern@schiessle.org> + * @author Derek <derek.kelly27@gmail.com> + * @author Joas Schilling <coding@schilljs.com> + * @author Ko- <k.stoffelen@cs.ru.nl> + * @author Lukas Reschke <lukas@statuscode.ch> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin McCorkell <robin@mccorkell.me.uk> + * @author Roeland Jago Douma <roeland@famdouma.nl> + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Settings\Controller; + +use bantu\IniGetWrapper\IniGetWrapper; +use DirectoryIterator; +use Doctrine\DBAL\DBALException; +use Doctrine\DBAL\Platforms\SqlitePlatform; +use Doctrine\DBAL\Types\Type; +use GuzzleHttp\Exception\ClientException; +use OC; +use OC\AppFramework\Http; +use OC\DB\Connection; +use OC\DB\MissingIndexInformation; +use OC\DB\SchemaWrapper; +use OC\IntegrityCheck\Checker; +use OC\Lock\NoopLockingProvider; +use OC\MemoryInfo; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\DataDisplayResponse; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Http\RedirectResponse; +use OCP\Http\Client\IClientService; +use OCP\IConfig; +use OCP\IDateTimeFormatter; +use OCP\IDBConnection; +use OCP\IL10N; +use OCP\ILogger; +use OCP\IRequest; +use OCP\IURLGenerator; +use OCP\Lock\ILockingProvider; +use OCP\Security\ISecureRandom; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\EventDispatcher\GenericEvent; + +class CheckSetupController extends Controller { + /** @var IConfig */ + private $config; + /** @var IClientService */ + private $clientService; + /** @var IURLGenerator */ + private $urlGenerator; + /** @var IL10N */ + private $l10n; + /** @var Checker */ + private $checker; + /** @var ILogger */ + private $logger; + /** @var EventDispatcherInterface */ + private $dispatcher; + /** @var IDBConnection|Connection */ + private $db; + /** @var ILockingProvider */ + private $lockingProvider; + /** @var IDateTimeFormatter */ + private $dateTimeFormatter; + /** @var MemoryInfo */ + private $memoryInfo; + /** @var ISecureRandom */ + private $secureRandom; + + public function __construct($AppName, + IRequest $request, + IConfig $config, + IClientService $clientService, + IURLGenerator $urlGenerator, + IL10N $l10n, + Checker $checker, + ILogger $logger, + EventDispatcherInterface $dispatcher, + IDBConnection $db, + ILockingProvider $lockingProvider, + IDateTimeFormatter $dateTimeFormatter, + MemoryInfo $memoryInfo, + ISecureRandom $secureRandom) { + parent::__construct($AppName, $request); + $this->config = $config; + $this->clientService = $clientService; + $this->urlGenerator = $urlGenerator; + $this->l10n = $l10n; + $this->checker = $checker; + $this->logger = $logger; + $this->dispatcher = $dispatcher; + $this->db = $db; + $this->lockingProvider = $lockingProvider; + $this->dateTimeFormatter = $dateTimeFormatter; + $this->memoryInfo = $memoryInfo; + $this->secureRandom = $secureRandom; + } + + /** + * Checks if the server can connect to the internet using HTTPS and HTTP + * @return bool + */ + private function hasInternetConnectivityProblems(): bool { + if ($this->config->getSystemValue('has_internet_connection', true) === false) { + return false; + } + + $siteArray = $this->config->getSystemValue('connectivity_check_domains', [ + 'www.nextcloud.com', 'www.startpage.com', 'www.eff.org', 'www.edri.org' + ]); + + foreach($siteArray as $site) { + if ($this->isSiteReachable($site)) { + return false; + } + } + return true; + } + + /** + * Checks if the Nextcloud server can connect to a specific URL using both HTTPS and HTTP + * @return bool + */ + private function isSiteReachable($sitename) { + $httpSiteName = 'http://' . $sitename . '/'; + $httpsSiteName = 'https://' . $sitename . '/'; + + try { + $client = $this->clientService->newClient(); + $client->get($httpSiteName); + $client->get($httpsSiteName); + } catch (\Exception $e) { + $this->logger->logException($e, ['app' => 'internet_connection_check']); + return false; + } + return true; + } + + /** + * Checks whether a local memcache is installed or not + * @return bool + */ + private function isMemcacheConfigured() { + return $this->config->getSystemValue('memcache.local', null) !== null; + } + + /** + * Whether PHP can generate "secure" pseudorandom integers + * + * @return bool + */ + private function isRandomnessSecure() { + try { + $this->secureRandom->generate(1); + } catch (\Exception $ex) { + return false; + } + return true; + } + + /** + * Public for the sake of unit-testing + * + * @return array + */ + protected function getCurlVersion() { + return curl_version(); + } + + /** + * Check if the used SSL lib is outdated. Older OpenSSL and NSS versions do + * have multiple bugs which likely lead to problems in combination with + * functionality required by ownCloud such as SNI. + * + * @link https://github.com/owncloud/core/issues/17446#issuecomment-122877546 + * @link https://bugzilla.redhat.com/show_bug.cgi?id=1241172 + * @return string + */ + private function isUsedTlsLibOutdated() { + // Don't run check when: + // 1. Server has `has_internet_connection` set to false + // 2. AppStore AND S2S is disabled + if(!$this->config->getSystemValue('has_internet_connection', true)) { + return ''; + } + if(!$this->config->getSystemValue('appstoreenabled', true) + && $this->config->getAppValue('files_sharing', 'outgoing_server2server_share_enabled', 'yes') === 'no' + && $this->config->getAppValue('files_sharing', 'incoming_server2server_share_enabled', 'yes') === 'no') { + return ''; + } + + $versionString = $this->getCurlVersion(); + if(isset($versionString['ssl_version'])) { + $versionString = $versionString['ssl_version']; + } else { + return ''; + } + + $features = (string)$this->l10n->t('installing and updating apps via the app store or Federated Cloud Sharing'); + if(!$this->config->getSystemValue('appstoreenabled', true)) { + $features = (string)$this->l10n->t('Federated Cloud Sharing'); + } + + // Check if at least OpenSSL after 1.01d or 1.0.2b + if(strpos($versionString, 'OpenSSL/') === 0) { + $majorVersion = substr($versionString, 8, 5); + $patchRelease = substr($versionString, 13, 6); + + if(($majorVersion === '1.0.1' && ord($patchRelease) < ord('d')) || + ($majorVersion === '1.0.2' && ord($patchRelease) < ord('b'))) { + return $this->l10n->t('cURL is using an outdated %1$s version (%2$s). Please update your operating system or features such as %3$s will not work reliably.', ['OpenSSL', $versionString, $features]); + } + } + + // Check if NSS and perform heuristic check + if(strpos($versionString, 'NSS/') === 0) { + try { + $firstClient = $this->clientService->newClient(); + $firstClient->get('https://nextcloud.com/'); + + $secondClient = $this->clientService->newClient(); + $secondClient->get('https://nextcloud.com/'); + } catch (ClientException $e) { + if($e->getResponse()->getStatusCode() === 400) { + return $this->l10n->t('cURL is using an outdated %1$s version (%2$s). Please update your operating system or features such as %3$s will not work reliably.', ['NSS', $versionString, $features]); + } + } + } + + return ''; + } + + /** + * Whether the version is outdated + * + * @return bool + */ + protected function isPhpOutdated() { + if (version_compare(PHP_VERSION, '7.1.0', '<')) { + return true; + } + + return false; + } + + /** + * Whether the php version is still supported (at time of release) + * according to: https://secure.php.net/supported-versions.php + * + * @return array + */ + private function isPhpSupported() { + return ['eol' => $this->isPhpOutdated(), 'version' => PHP_VERSION]; + } + + /** + * Check if the reverse proxy configuration is working as expected + * + * @return bool + */ + private function forwardedForHeadersWorking() { + $trustedProxies = $this->config->getSystemValue('trusted_proxies', []); + $remoteAddress = $this->request->getHeader('REMOTE_ADDR'); + + if (empty($trustedProxies) && $this->request->getHeader('X-Forwarded-Host') !== '') { + return false; + } + + if (\is_array($trustedProxies) && \in_array($remoteAddress, $trustedProxies, true)) { + return $remoteAddress !== $this->request->getRemoteAddress(); + } + + // either not enabled or working correctly + return true; + } + + /** + * Checks if the correct memcache module for PHP is installed. Only + * fails if memcached is configured and the working module is not installed. + * + * @return bool + */ + private function isCorrectMemcachedPHPModuleInstalled() { + if ($this->config->getSystemValue('memcache.distributed', null) !== '\OC\Memcache\Memcached') { + return true; + } + + // there are two different memcached modules for PHP + // we only support memcached and not memcache + // https://code.google.com/p/memcached/wiki/PHPClientComparison + return !(!extension_loaded('memcached') && extension_loaded('memcache')); + } + + /** + * Checks if set_time_limit is not disabled. + * + * @return bool + */ + private function isSettimelimitAvailable() { + if (function_exists('set_time_limit') + && strpos(@ini_get('disable_functions'), 'set_time_limit') === false) { + return true; + } + + return false; + } + + /** + * @return RedirectResponse + */ + public function rescanFailedIntegrityCheck() { + $this->checker->runInstanceVerification(); + return new RedirectResponse( + $this->urlGenerator->linkToRoute('settings.AdminSettings.index', ['section' => 'overview']) + ); + } + + /** + * @NoCSRFRequired + * @return DataResponse + */ + public function getFailedIntegrityCheckFiles() { + if(!$this->checker->isCodeCheckEnforced()) { + return new DataDisplayResponse('Integrity checker has been disabled. Integrity cannot be verified.'); + } + + $completeResults = $this->checker->getResults(); + + if(!empty($completeResults)) { + $formattedTextResponse = 'Technical information +===================== +The following list covers which files have failed the integrity check. Please read +the previous linked documentation to learn more about the errors and how to fix +them. + +Results +======= +'; + foreach($completeResults as $context => $contextResult) { + $formattedTextResponse .= "- $context\n"; + + foreach($contextResult as $category => $result) { + $formattedTextResponse .= "\t- $category\n"; + if($category !== 'EXCEPTION') { + foreach ($result as $key => $results) { + $formattedTextResponse .= "\t\t- $key\n"; + } + } else { + foreach ($result as $key => $results) { + $formattedTextResponse .= "\t\t- $results\n"; + } + } + + } + } + + $formattedTextResponse .= ' +Raw output +========== +'; + $formattedTextResponse .= print_r($completeResults, true); + } else { + $formattedTextResponse = 'No errors have been found.'; + } + + + $response = new DataDisplayResponse( + $formattedTextResponse, + Http::STATUS_OK, + [ + 'Content-Type' => 'text/plain', + ] + ); + + return $response; + } + + /** + * Checks whether a PHP opcache is properly set up + * @return bool + */ + protected function isOpcacheProperlySetup() { + $iniWrapper = new IniGetWrapper(); + + if(!$iniWrapper->getBool('opcache.enable')) { + return false; + } + + if(!$iniWrapper->getBool('opcache.save_comments')) { + return false; + } + + if($iniWrapper->getNumeric('opcache.max_accelerated_files') < 10000) { + return false; + } + + if($iniWrapper->getNumeric('opcache.memory_consumption') < 128) { + return false; + } + + if($iniWrapper->getNumeric('opcache.interned_strings_buffer') < 8) { + return false; + } + + return true; + } + + /** + * Check if the required FreeType functions are present + * @return bool + */ + protected function hasFreeTypeSupport() { + return function_exists('imagettfbbox') && function_exists('imagettftext'); + } + + protected function hasMissingIndexes(): array { + $indexInfo = new MissingIndexInformation(); + // Dispatch event so apps can also hint for pending index updates if needed + $event = new GenericEvent($indexInfo); + $this->dispatcher->dispatch(IDBConnection::CHECK_MISSING_INDEXES_EVENT, $event); + + return $indexInfo->getListOfMissingIndexes(); + } + + protected function isSqliteUsed() { + return strpos($this->config->getSystemValue('dbtype'), 'sqlite') !== false; + } + + protected function isReadOnlyConfig(): bool { + return \OC_Helper::isReadOnlyConfigEnabled(); + } + + protected function hasValidTransactionIsolationLevel(): bool { + try { + if ($this->db->getDatabasePlatform() instanceof SqlitePlatform) { + return true; + } + + return $this->db->getTransactionIsolation() === Connection::TRANSACTION_READ_COMMITTED; + } catch (DBALException $e) { + // ignore + } + + return true; + } + + protected function hasFileinfoInstalled(): bool { + return \OC_Util::fileInfoLoaded(); + } + + protected function hasWorkingFileLocking(): bool { + return !($this->lockingProvider instanceof NoopLockingProvider); + } + + protected function getSuggestedOverwriteCliURL(): string { + $suggestedOverwriteCliUrl = ''; + if ($this->config->getSystemValue('overwrite.cli.url', '') === '') { + $suggestedOverwriteCliUrl = $this->request->getServerProtocol() . '://' . $this->request->getInsecureServerHost() . \OC::$WEBROOT; + if (!$this->config->getSystemValue('config_is_read_only', false)) { + // Set the overwrite URL when it was not set yet. + $this->config->setSystemValue('overwrite.cli.url', $suggestedOverwriteCliUrl); + $suggestedOverwriteCliUrl = ''; + } + } + return $suggestedOverwriteCliUrl; + } + + protected function getLastCronInfo(): array { + $lastCronRun = $this->config->getAppValue('core', 'lastcron', 0); + return [ + 'diffInSeconds' => time() - $lastCronRun, + 'relativeTime' => $this->dateTimeFormatter->formatTimeSpan($lastCronRun), + 'backgroundJobsUrl' => $this->urlGenerator->linkToRoute('settings.AdminSettings.index', ['section' => 'server']) . '#backgroundjobs', + ]; + } + + protected function getCronErrors() { + $errors = json_decode($this->config->getAppValue('core', 'cronErrors', ''), true); + + if (is_array($errors)) { + return $errors; + } + + return []; + } + + protected function isPHPMailerUsed(): bool { + return $this->config->getSystemValue('mail_smtpmode', 'smtp') === 'php'; + } + + protected function hasOpcacheLoaded(): bool { + return function_exists('opcache_get_status'); + } + + /** + * Iterates through the configured app roots and + * tests if the subdirectories are owned by the same user than the current user. + * + * @return array + */ + protected function getAppDirsWithDifferentOwner(): array { + $currentUser = posix_getuid(); + $appDirsWithDifferentOwner = [[]]; + + foreach (OC::$APPSROOTS as $appRoot) { + if ($appRoot['writable'] === true) { + $appDirsWithDifferentOwner[] = $this->getAppDirsWithDifferentOwnerForAppRoot($currentUser, $appRoot); + } + } + + $appDirsWithDifferentOwner = array_merge(...$appDirsWithDifferentOwner); + sort($appDirsWithDifferentOwner); + + return $appDirsWithDifferentOwner; + } + + /** + * Tests if the directories for one apps directory are writable by the current user. + * + * @param int $currentUser The current user + * @param array $appRoot The app root config + * @return string[] The none writable directory paths inside the app root + */ + private function getAppDirsWithDifferentOwnerForAppRoot(int $currentUser, array $appRoot): array { + $appDirsWithDifferentOwner = []; + $appsPath = $appRoot['path']; + $appsDir = new DirectoryIterator($appRoot['path']); + + foreach ($appsDir as $fileInfo) { + if ($fileInfo->isDir() && !$fileInfo->isDot()) { + $absAppPath = $appsPath . DIRECTORY_SEPARATOR . $fileInfo->getFilename(); + $appDirUser = fileowner($absAppPath); + if ($appDirUser !== $currentUser) { + $appDirsWithDifferentOwner[] = $absAppPath; + } + } + } + + return $appDirsWithDifferentOwner; + } + + /** + * Checks for potential PHP modules that would improve the instance + * + * @return string[] A list of PHP modules that is recommended + */ + protected function hasRecommendedPHPModules(): array { + $recommendedPHPModules = []; + + if (!extension_loaded('intl')) { + $recommendedPHPModules[] = 'intl'; + } + + if ($this->config->getAppValue('theming', 'enabled', 'no') === 'yes') { + if (!extension_loaded('imagick')) { + $recommendedPHPModules[] = 'imagick'; + } + } + + return $recommendedPHPModules; + } + + protected function isMysqlUsedWithoutUTF8MB4(): bool { + return ($this->config->getSystemValue('dbtype', 'sqlite') === 'mysql') && ($this->config->getSystemValue('mysql.utf8mb4', false) === false); + } + + protected function hasBigIntConversionPendingColumns(): array { + // copy of ConvertFilecacheBigInt::getColumnsByTable() + $tables = [ + 'activity' => ['activity_id', 'object_id'], + 'activity_mq' => ['mail_id'], + 'filecache' => ['fileid', 'storage', 'parent', 'mimetype', 'mimepart', 'mtime', 'storage_mtime'], + 'mimetypes' => ['id'], + 'storages' => ['numeric_id'], + ]; + + $schema = new SchemaWrapper($this->db); + $isSqlite = $this->db->getDatabasePlatform() instanceof SqlitePlatform; + $pendingColumns = []; + + foreach ($tables as $tableName => $columns) { + if (!$schema->hasTable($tableName)) { + continue; + } + + $table = $schema->getTable($tableName); + foreach ($columns as $columnName) { + $column = $table->getColumn($columnName); + $isAutoIncrement = $column->getAutoincrement(); + $isAutoIncrementOnSqlite = $isSqlite && $isAutoIncrement; + if ($column->getType()->getName() !== Type::BIGINT && !$isAutoIncrementOnSqlite) { + $pendingColumns[] = $tableName . '.' . $columnName; + } + } + } + + return $pendingColumns; + } + + protected function isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed(): bool { + $objectStore = $this->config->getSystemValue('objectstore', null); + $objectStoreMultibucket = $this->config->getSystemValue('objectstore_multibucket', null); + + if (!isset($objectStoreMultibucket) && !isset($objectStore)) { + return true; + } + + if (isset($objectStoreMultibucket['class']) && $objectStoreMultibucket['class'] !== 'OC\\Files\\ObjectStore\\S3') { + return true; + } + + if (isset($objectStore['class']) && $objectStore['class'] !== 'OC\\Files\\ObjectStore\\S3') { + return true; + } + + $tempPath = sys_get_temp_dir(); + if (!is_dir($tempPath)) { + $this->logger->error('Error while checking the temporary PHP path - it was not properly set to a directory. value: ' . $tempPath); + return false; + } + $freeSpaceInTemp = disk_free_space($tempPath); + if ($freeSpaceInTemp === false) { + $this->logger->error('Error while checking the available disk space of temporary PHP path - no free disk space returned. temporary path: ' . $tempPath); + return false; + } + + $freeSpaceInTempInGB = $freeSpaceInTemp / 1024 / 1024 / 1024; + if ($freeSpaceInTempInGB > 50) { + return true; + } + + $this->logger->warning('Checking the available space in the temporary path resulted in ' . round($freeSpaceInTempInGB, 1) . ' GB instead of the recommended 50GB. Path: ' . $tempPath); + return false; + } + + /** + * @return DataResponse + */ + public function check() { + return new DataResponse( + [ + 'isGetenvServerWorking' => !empty(getenv('PATH')), + 'isReadOnlyConfig' => $this->isReadOnlyConfig(), + 'hasValidTransactionIsolationLevel' => $this->hasValidTransactionIsolationLevel(), + 'hasFileinfoInstalled' => $this->hasFileinfoInstalled(), + 'hasWorkingFileLocking' => $this->hasWorkingFileLocking(), + 'suggestedOverwriteCliURL' => $this->getSuggestedOverwriteCliURL(), + 'cronInfo' => $this->getLastCronInfo(), + 'cronErrors' => $this->getCronErrors(), + 'serverHasInternetConnectionProblems' => $this->hasInternetConnectivityProblems(), + 'isMemcacheConfigured' => $this->isMemcacheConfigured(), + 'memcacheDocs' => $this->urlGenerator->linkToDocs('admin-performance'), + 'isRandomnessSecure' => $this->isRandomnessSecure(), + 'securityDocs' => $this->urlGenerator->linkToDocs('admin-security'), + 'isUsedTlsLibOutdated' => $this->isUsedTlsLibOutdated(), + 'phpSupported' => $this->isPhpSupported(), + 'forwardedForHeadersWorking' => $this->forwardedForHeadersWorking(), + 'reverseProxyDocs' => $this->urlGenerator->linkToDocs('admin-reverse-proxy'), + 'isCorrectMemcachedPHPModuleInstalled' => $this->isCorrectMemcachedPHPModuleInstalled(), + 'hasPassedCodeIntegrityCheck' => $this->checker->hasPassedCheck(), + 'codeIntegrityCheckerDocumentation' => $this->urlGenerator->linkToDocs('admin-code-integrity'), + 'isOpcacheProperlySetup' => $this->isOpcacheProperlySetup(), + 'hasOpcacheLoaded' => $this->hasOpcacheLoaded(), + 'phpOpcacheDocumentation' => $this->urlGenerator->linkToDocs('admin-php-opcache'), + 'isSettimelimitAvailable' => $this->isSettimelimitAvailable(), + 'hasFreeTypeSupport' => $this->hasFreeTypeSupport(), + 'missingIndexes' => $this->hasMissingIndexes(), + 'isSqliteUsed' => $this->isSqliteUsed(), + 'databaseConversionDocumentation' => $this->urlGenerator->linkToDocs('admin-db-conversion'), + 'isPHPMailerUsed' => $this->isPHPMailerUsed(), + 'mailSettingsDocumentation' => $this->urlGenerator->getAbsoluteURL('index.php/settings/admin'), + 'isMemoryLimitSufficient' => $this->memoryInfo->isMemoryLimitSufficient(), + 'appDirsWithDifferentOwner' => $this->getAppDirsWithDifferentOwner(), + 'recommendedPHPModules' => $this->hasRecommendedPHPModules(), + 'pendingBigIntConversionColumns' => $this->hasBigIntConversionPendingColumns(), + 'isMysqlUsedWithoutUTF8MB4' => $this->isMysqlUsedWithoutUTF8MB4(), + 'isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed' => $this->isEnoughTempSpaceAvailableIfS3PrimaryStorageIsUsed(), + ] + ); + } +} |