diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000000000..b18b31297838c --- /dev/null +++ b/Caddyfile @@ -0,0 +1,49 @@ +localhost { + php_server { + worker index.php + } + + log { + level ERROR + output stderr + } + + encode gzip + + redir /.well-known/carddav /remote.php/dav 301 + redir /.well-known/caldav /remote.php/dav 301 + + # Rule: Maps most RFC 8615 compliant well-known URIs to our main frontend controller (/index.php) by default + @wellKnown { + path "/.well-known/" + not { + path /.well-known/acme-challenge + path /.well-known/pki-validation + } + } + rewrite @wellKnown /index.php + + rewrite /ocm-provider/ /index.php + + @forbidden { + path /.htaccess + path /data/* + path /config/* + path /db_structure + path /.xml + path /README + path /3rdparty/* + path /lib/* + path /templates/* + path /occ + path /build + path /tests + path /console.php + path /autotest + path /issue + path /indi + path /db_ + path /console + } + respond @forbidden 404 +} diff --git a/apps/testing/appinfo/info.xml b/apps/testing/appinfo/info.xml index d69f902993373..c2dd9a2f5ee93 100644 --- a/apps/testing/appinfo/info.xml +++ b/apps/testing/appinfo/info.xml @@ -16,6 +16,11 @@ + + + OCA\Testing\Command\StaticHunt + + monitoring https://github.com/nextcloud/server/issues diff --git a/apps/testing/composer/composer/autoload_classmap.php b/apps/testing/composer/composer/autoload_classmap.php index ad50a15224a02..ee7424a0ea809 100644 --- a/apps/testing/composer/composer/autoload_classmap.php +++ b/apps/testing/composer/composer/autoload_classmap.php @@ -9,6 +9,7 @@ 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', 'OCA\\Testing\\AlternativeHomeUserBackend' => $baseDir . '/../lib/AlternativeHomeUserBackend.php', 'OCA\\Testing\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php', + 'OCA\\Testing\\Command\\StaticHunt' => $baseDir . '/../lib/Command/StaticHunt.php', 'OCA\\Testing\\Controller\\ConfigController' => $baseDir . '/../lib/Controller/ConfigController.php', 'OCA\\Testing\\Controller\\LockingController' => $baseDir . '/../lib/Controller/LockingController.php', 'OCA\\Testing\\Controller\\RateLimitTestController' => $baseDir . '/../lib/Controller/RateLimitTestController.php', diff --git a/apps/testing/composer/composer/autoload_static.php b/apps/testing/composer/composer/autoload_static.php index d3e37546416a3..9ea8e6dc0a8f0 100644 --- a/apps/testing/composer/composer/autoload_static.php +++ b/apps/testing/composer/composer/autoload_static.php @@ -24,6 +24,7 @@ class ComposerStaticInitTesting 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 'OCA\\Testing\\AlternativeHomeUserBackend' => __DIR__ . '/..' . '/../lib/AlternativeHomeUserBackend.php', 'OCA\\Testing\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php', + 'OCA\\Testing\\Command\\StaticHunt' => __DIR__ . '/..' . '/../lib/Command/StaticHunt.php', 'OCA\\Testing\\Controller\\ConfigController' => __DIR__ . '/..' . '/../lib/Controller/ConfigController.php', 'OCA\\Testing\\Controller\\LockingController' => __DIR__ . '/..' . '/../lib/Controller/LockingController.php', 'OCA\\Testing\\Controller\\RateLimitTestController' => __DIR__ . '/..' . '/../lib/Controller/RateLimitTestController.php', diff --git a/apps/testing/lib/Command/StaticHunt.php b/apps/testing/lib/Command/StaticHunt.php new file mode 100644 index 0000000000000..2ee6f9b5d9fbe --- /dev/null +++ b/apps/testing/lib/Command/StaticHunt.php @@ -0,0 +1,110 @@ +setName('testing:static-hunt') + ->setDescription('Hunt for static properties in classes'); + } + + + protected function execute(InputInterface $input, OutputInterface $output): int { + $folders = [ + '' => __DIR__ . '/../../../../lib/private/legacy', + '\\OC' => __DIR__ . '/../../../../lib/private', + '\\OC\\Core' => __DIR__ . '/../../../../core', + ]; + $apps = $this->appManager->getAllAppsInAppsFolders(); + foreach ($apps as $app) { + $info = $this->appManager->getAppInfo($app); + if (!isset($info['namespace'])) { + continue; + } + $folders['\\OCA\\' . $info['namespace']] = $this->appManager->getAppPath($app) . '/lib'; + } + $stats = [ + 'classes' => 0, + 'properties' => 0, + ]; + foreach ($folders as $namespace => $folder) { + $this->scanFolder($folder, $namespace, $output, $stats); + } + + $output->writeln('Found ' . $stats['properties'] . ' static properties spread among ' . $stats['classes'] . ' classes'); + + return 0; + } + + private function scanFolder(string $folder, string $namespace, OutputInterface $output, array &$stats): void { + $folder = realpath($folder); + $output->writeln('Folder ' . $folder, OutputInterface::VERBOSITY_VERBOSE); + foreach ($this->recursiveGlob($folder) as $filename) { + try { + $filename = realpath($filename); + if (($namespace === '\\OC') && str_contains($filename, 'lib/private/legacy')) { + // Skip legacy in OC as it’s scanned with an empty namespace separately + continue; + } + foreach (self::SKIP_REGEX as $skipRegex) { + if (preg_match($skipRegex, $filename)) { + continue 2; + } + } + $classname = $namespace . substr(str_replace('/', '\\', substr($filename, strlen($folder))), 0, -4); + $output->writeln('Class ' . $classname, OutputInterface::VERBOSITY_VERBOSE); + if (!class_exists($classname)) { + continue; + } + $rClass = new \ReflectionClass($classname); + $staticProperties = $rClass->getStaticProperties(); + if (empty($staticProperties)) { + continue; + } + $stats['classes']++; + $output->writeln('# ' . str_replace(\OC::$SERVERROOT, '', $filename) . " $classname"); + foreach ($staticProperties as $property => $value) { + $propertyObject = $rClass->getProperty($property); + $stats['properties']++; + $output->write("$propertyObject"); + } + $output->writeln(''); + } catch (\Throwable $t) { + $output->writeln("$t"); + } + } + } + + private function recursiveGlob(string $path, int $depth = 1): \Generator { + $pattern = $path . str_repeat('/*', $depth); + yield from glob($pattern . '.php'); + if (!empty(glob($pattern, GLOB_ONLYDIR))) { + yield from $this->recursiveGlob($path, $depth + 1); + } + } +} diff --git a/apps/twofactor_backupcodes/lib/Service/BackupCodeStorage.php b/apps/twofactor_backupcodes/lib/Service/BackupCodeStorage.php index 7dd6b3949e2d2..24621ba2ee202 100644 --- a/apps/twofactor_backupcodes/lib/Service/BackupCodeStorage.php +++ b/apps/twofactor_backupcodes/lib/Service/BackupCodeStorage.php @@ -17,7 +17,7 @@ use OCP\Security\ISecureRandom; class BackupCodeStorage { - private static $CODE_LENGTH = 16; + private const CODE_LENGTH = 16; public function __construct( private BackupCodeMapper $mapper, @@ -40,7 +40,7 @@ public function createCodes(IUser $user, int $number = 10): array { $uid = $user->getUID(); foreach (range(1, min([$number, 20])) as $i) { - $code = $this->random->generate(self::$CODE_LENGTH, ISecureRandom::CHAR_HUMAN_READABLE); + $code = $this->random->generate(self::CODE_LENGTH, ISecureRandom::CHAR_HUMAN_READABLE); $dbCode = new BackupCode(); $dbCode->setUserId($uid); diff --git a/apps/user_ldap/lib/Configuration.php b/apps/user_ldap/lib/Configuration.php index b4a5b84720421..42eaa464d5f7a 100644 --- a/apps/user_ldap/lib/Configuration.php +++ b/apps/user_ldap/lib/Configuration.php @@ -571,7 +571,7 @@ public function getDefaults(): array { */ public function getConfigTranslationArray(): array { //TODO: merge them into one representation - static $array = [ + return [ 'ldap_host' => 'ldapHost', 'ldap_port' => 'ldapPort', 'ldap_backup_host' => 'ldapBackupHost', @@ -644,7 +644,6 @@ public function getConfigTranslationArray(): array { 'ldap_attr_anniversarydate' => 'ldapAttributeAnniversaryDate', 'ldap_attr_pronouns' => 'ldapAttributePronouns', ]; - return $array; } /** diff --git a/apps/user_ldap/lib/Wizard.php b/apps/user_ldap/lib/Wizard.php index fa77fea8fa2bb..876f001fb2728 100644 --- a/apps/user_ldap/lib/Wizard.php +++ b/apps/user_ldap/lib/Wizard.php @@ -16,7 +16,7 @@ use Psr\Log\LoggerInterface; class Wizard extends LDAPUtility { - protected static ?IL10N $l = null; + private IL10N $l; protected ?\LDAP\Connection $cr = null; protected WizardResult $result; protected LoggerInterface $logger; @@ -40,9 +40,7 @@ public function __construct( protected Access $access, ) { parent::__construct($ldap); - if (is_null(static::$l)) { - static::$l = Server::get(IL10NFactory::class)->get('user_ldap'); - } + $this->l = Server::get(IL10NFactory::class)->get('user_ldap'); $this->result = new WizardResult(); $this->logger = Server::get(LoggerInterface::class); } @@ -94,7 +92,7 @@ public function countGroups() { $filter = $this->configuration->ldapGroupFilter; if (empty($filter)) { - $output = self::$l->n('%n group found', '%n groups found', 0); + $output = $this->l->n('%n group found', '%n groups found', 0); $this->result->addChange('ldap_group_count', $output); return $this->result; } @@ -110,9 +108,9 @@ public function countGroups() { } if ($groupsTotal > 1000) { - $output = self::$l->t('> 1000 groups found'); + $output = $this->l->t('> 1000 groups found'); } else { - $output = self::$l->n( + $output = $this->l->n( '%n group found', '%n groups found', $groupsTotal @@ -130,9 +128,9 @@ public function countUsers(): WizardResult { $usersTotal = $this->countEntries($filter, 'users'); if ($usersTotal > 1000) { - $output = self::$l->t('> 1000 users found'); + $output = $this->l->t('> 1000 users found'); } else { - $output = self::$l->n( + $output = $this->l->n( '%n user found', '%n users found', $usersTotal @@ -216,7 +214,7 @@ public function detectUserDisplayNameAttribute() { } } - throw new \Exception(self::$l->t('Could not detect user display name attribute. Please specify it yourself in advanced LDAP settings.')); + throw new \Exception($this->l->t('Could not detect user display name attribute. Please specify it yourself in advanced LDAP settings.')); } /** @@ -431,7 +429,7 @@ public function fetchGroups(string $dbKey, string $confKey): array { natsort($groupNames); $this->result->addOptions($dbKey, array_values($groupNames)); } else { - throw new \Exception(self::$l->t('Could not find the desired feature')); + throw new \Exception($this->l->t('Could not find the desired feature')); } $setFeatures = $this->configuration->$confKey; @@ -1024,7 +1022,7 @@ private function connectAndBind(int $port, bool $tls): bool { $host = $this->configuration->ldapHost; $hostInfo = parse_url((string)$host); if (!is_string($host) || !$hostInfo) { - throw new \Exception(self::$l->t('Invalid Host')); + throw new \Exception($this->l->t('Invalid Host')); } $this->logger->debug( 'Wiz: Attempting to connect', @@ -1032,7 +1030,7 @@ private function connectAndBind(int $port, bool $tls): bool { ); $cr = $this->ldap->connect($host, (string)$port); if (!$this->ldap->isResource($cr)) { - throw new \Exception(self::$l->t('Invalid Host')); + throw new \Exception($this->l->t('Invalid Host')); } /** @var \LDAP\Connection $cr */ @@ -1219,7 +1217,7 @@ private function determineFeature(array $objectclasses, string $attr, string $db //sorting in the web UI. Therefore: array_values $this->result->addOptions($dbkey, array_values($availableFeatures)); } else { - throw new \Exception(self::$l->t('Could not find the desired feature')); + throw new \Exception($this->l->t('Could not find the desired feature')); } $setFeatures = $this->configuration->$confkey; @@ -1307,7 +1305,7 @@ private function getConnection(): \LDAP\Connection|false { * @return array */ private function getDefaultLdapPortSettings(): array { - static $settings = [ + return [ ['port' => 7636, 'tls' => false], ['port' => 636, 'tls' => false], ['port' => 7389, 'tls' => true], @@ -1315,7 +1313,6 @@ private function getDefaultLdapPortSettings(): array { ['port' => 7389, 'tls' => false], ['port' => 389, 'tls' => false], ]; - return $settings; } /** diff --git a/apps/workflowengine/lib/Command/Index.php b/apps/workflowengine/lib/Command/Index.php index 1fb8cb416b0aa..c38d19a087ede 100644 --- a/apps/workflowengine/lib/Command/Index.php +++ b/apps/workflowengine/lib/Command/Index.php @@ -6,6 +6,7 @@ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ + namespace OCA\WorkflowEngine\Command; use OCA\WorkflowEngine\Helper\ScopeContext; @@ -43,11 +44,11 @@ protected function configure() { } protected function mappedScope(string $scope): int { - static $scopes = [ + return match($scope) { 'admin' => IManager::SCOPE_ADMIN, 'user' => IManager::SCOPE_USER, - ]; - return $scopes[$scope] ?? -1; + default => -1, + }; } protected function execute(InputInterface $input, OutputInterface $output): int { diff --git a/apps/workflowengine/lib/Manager.php b/apps/workflowengine/lib/Manager.php index e426f96ac4aeb..a0f479c437092 100644 --- a/apps/workflowengine/lib/Manager.php +++ b/apps/workflowengine/lib/Manager.php @@ -65,6 +65,9 @@ class Manager implements IManager { /** @var CappedMemoryCache */ protected CappedMemoryCache $operationsByScope; + /** @var array, ScopeContext[]> $scopesByOperation */ + private array $scopesByOperation = []; + public function __construct( protected readonly IDBConnection $connection, protected readonly ContainerInterface $container, @@ -128,10 +131,8 @@ public function getAllConfiguredEvents() { * @return ScopeContext[] */ public function getAllConfiguredScopesForOperation(string $operationClass): array { - /** @var array, ScopeContext[]> $scopesByOperation */ - static $scopesByOperation = []; - if (isset($scopesByOperation[$operationClass])) { - return $scopesByOperation[$operationClass]; + if (isset($this->scopesByOperation[$operationClass])) { + return $this->scopesByOperation[$operationClass]; } try { @@ -152,7 +153,7 @@ public function getAllConfiguredScopesForOperation(string $operationClass): arra $query->setParameters(['operationClass' => $operationClass]); $result = $query->executeQuery(); - $scopesByOperation[$operationClass] = []; + $this->scopesByOperation[$operationClass] = []; while ($row = $result->fetchAssociative()) { $scope = new ScopeContext($row['type'], $row['value']); @@ -160,10 +161,10 @@ public function getAllConfiguredScopesForOperation(string $operationClass): arra continue; } - $scopesByOperation[$operationClass][$scope->getHash()] = $scope; + $this->scopesByOperation[$operationClass][$scope->getHash()] = $scope; } - return $scopesByOperation[$operationClass]; + return $this->scopesByOperation[$operationClass]; } public function getAllOperations(ScopeContext $scopeContext): array { diff --git a/build/psalm/StaticVarsChecker.php b/build/psalm/StaticVarsChecker.php new file mode 100644 index 0000000000000..24d08b45aa8cf --- /dev/null +++ b/build/psalm/StaticVarsChecker.php @@ -0,0 +1,54 @@ +getStmt(); + $statementsSource = $event->getStatementsSource(); + + foreach ($classLike->stmts as $stmt) { + if ($stmt instanceof Property) { + if ($stmt->isStatic()) { + IssueBuffer::maybeAdd( + // ImpureStaticProperty is close enough, all static properties are impure to my eyes + new \Psalm\Issue\ImpureStaticProperty( + 'Static property should not be used as they do not follow requests lifecycle', + new CodeLocation($statementsSource, $stmt), + ) + ); + } + } + } + } + + public static function afterStatementAnalysis(AfterStatementAnalysisEvent $event): ?bool { + $stmt = $event->getStmt(); + if ($stmt instanceof PhpParser\Node\Stmt\Static_) { + IssueBuffer::maybeAdd( + // Same logic + new \Psalm\Issue\ImpureStaticVariable( + 'Static var should not be used as they do not follow requests lifecycle and are hard to reset', + new CodeLocation($event->getStatementsSource(), $stmt), + ) + ); + } + return null; + } +} diff --git a/build/psalm/StaticVarsTest.php b/build/psalm/StaticVarsTest.php new file mode 100644 index 0000000000000..f40ac56ff5144 --- /dev/null +++ b/build/psalm/StaticVarsTest.php @@ -0,0 +1,23 @@ +getMigrationsDirectory(); $this->ensureMigrationDirExists($dir); diff --git a/index.php b/index.php index b368462371d40..3269ec55ff038 100644 --- a/index.php +++ b/index.php @@ -19,90 +19,127 @@ use OCP\Template\ITemplateManager; use Psr\Log\LoggerInterface; -try { - require_once __DIR__ . '/lib/base.php'; +require_once __DIR__ . '/lib/OC.php'; - OC::handleRequest(); -} catch (ServiceUnavailableException $ex) { - Server::get(LoggerInterface::class)->error($ex->getMessage(), [ - 'app' => 'index', - 'exception' => $ex, - ]); +\OC::boot(); - //show the user a detailed error page - Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 503); -} catch (HintException $ex) { +function cleanupStaticCrap() { + // FIXME needed because these use a static var + \OC_Hook::clear(); + \OC_Util::$styles = []; + \OC_Util::$headers = []; + \OC_User::setIncognitoMode(false); + \OC_User::$_setupedBackends = []; + \OC_App::reset(); + \OC_Helper::reset(); +} + +$handler = static function () { try { - Server::get(ITemplateManager::class)->printErrorPage($ex->getMessage(), $ex->getHint(), 503); - } catch (Exception $ex2) { + // In worker mode, script name is empty in FrankenPHP + if ($_SERVER['SCRIPT_NAME'] === '') { + $_SERVER['SCRIPT_NAME'] = $_SERVER['PHP_SELF']; + } + cleanupStaticCrap(); + OC::init(); + OC::handleRequest(); + } catch (ServiceUnavailableException $ex) { + Server::get(LoggerInterface::class)->error($ex->getMessage(), [ + 'app' => 'index', + 'exception' => $ex, + ]); + + //show the user a detailed error page + Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 503); + } catch (HintException $ex) { + try { + Server::get(ITemplateManager::class)->printErrorPage($ex->getMessage(), $ex->getHint(), 503); + } catch (Exception $ex2) { + try { + Server::get(LoggerInterface::class)->error($ex->getMessage(), [ + 'app' => 'index', + 'exception' => $ex, + ]); + Server::get(LoggerInterface::class)->error($ex2->getMessage(), [ + 'app' => 'index', + 'exception' => $ex2, + ]); + } catch (Throwable $e) { + // no way to log it properly - but to avoid a white page of death we try harder and ignore this one here + } + + //show the user a detailed error page + Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 500); + } + } catch (LoginException $ex) { + $request = Server::get(IRequest::class); + /** + * Routes with the @CORS annotation and other API endpoints should + * not return a webpage, so we only print the error page when html is accepted, + * otherwise we reply with a JSON array like the SecurityMiddleware would do. + */ + if (stripos($request->getHeader('Accept'), 'html') === false) { + http_response_code(401); + header('Content-Type: application/json; charset=utf-8'); + echo json_encode(['message' => $ex->getMessage()]); + exit(); + } + Server::get(ITemplateManager::class)->printErrorPage($ex->getMessage(), $ex->getMessage(), 401); + } catch (MaxDelayReached $ex) { + $request = Server::get(IRequest::class); + /** + * Routes with the @CORS annotation and other API endpoints should + * not return a webpage, so we only print the error page when html is accepted, + * otherwise we reply with a JSON array like the BruteForceMiddleware would do. + */ + if (stripos($request->getHeader('Accept'), 'html') === false) { + http_response_code(429); + header('Content-Type: application/json; charset=utf-8'); + echo json_encode(['message' => $ex->getMessage()]); + exit(); + } + http_response_code(429); + Server::get(ITemplateManager::class)->printGuestPage('core', '429'); + } catch (Exception $ex) { + Server::get(LoggerInterface::class)->error($ex->getMessage(), [ + 'app' => 'index', + 'exception' => $ex, + ]); + + //show the user a detailed error page + Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 500); + } catch (Error $ex) { try { Server::get(LoggerInterface::class)->error($ex->getMessage(), [ 'app' => 'index', 'exception' => $ex, ]); - Server::get(LoggerInterface::class)->error($ex2->getMessage(), [ - 'app' => 'index', - 'exception' => $ex2, - ]); - } catch (Throwable $e) { - // no way to log it properly - but to avoid a white page of death we try harder and ignore this one here - } + } catch (Error $e) { + http_response_code(500); + header('Content-Type: text/plain; charset=utf-8'); + print("Internal Server Error\n\n"); + print("The server encountered an internal error and was unable to complete your request.\n"); + print("Please contact the server administrator if this error reappears multiple times, please include the technical details below in your report.\n"); + print("More details can be found in the webserver log.\n"); - //show the user a detailed error page + throw $ex; + } Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 500); } -} catch (LoginException $ex) { - $request = Server::get(IRequest::class); - /** - * Routes with the @CORS annotation and other API endpoints should - * not return a webpage, so we only print the error page when html is accepted, - * otherwise we reply with a JSON array like the SecurityMiddleware would do. - */ - if (stripos($request->getHeader('Accept'), 'html') === false) { - http_response_code(401); - header('Content-Type: application/json; charset=utf-8'); - echo json_encode(['message' => $ex->getMessage()]); - exit(); - } - Server::get(ITemplateManager::class)->printErrorPage($ex->getMessage(), $ex->getMessage(), 401); -} catch (MaxDelayReached $ex) { - $request = Server::get(IRequest::class); - /** - * Routes with the @CORS annotation and other API endpoints should - * not return a webpage, so we only print the error page when html is accepted, - * otherwise we reply with a JSON array like the BruteForceMiddleware would do. - */ - if (stripos($request->getHeader('Accept'), 'html') === false) { - http_response_code(429); - header('Content-Type: application/json; charset=utf-8'); - echo json_encode(['message' => $ex->getMessage()]); - exit(); - } - http_response_code(429); - Server::get(ITemplateManager::class)->printGuestPage('core', '429'); -} catch (Exception $ex) { - Server::get(LoggerInterface::class)->error($ex->getMessage(), [ - 'app' => 'index', - 'exception' => $ex, - ]); +}; - //show the user a detailed error page - Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 500); -} catch (Error $ex) { - try { - Server::get(LoggerInterface::class)->error($ex->getMessage(), [ - 'app' => 'index', - 'exception' => $ex, - ]); - } catch (Error $e) { - http_response_code(500); - header('Content-Type: text/plain; charset=utf-8'); - print("Internal Server Error\n\n"); - print("The server encountered an internal error and was unable to complete your request.\n"); - print("Please contact the server administrator if this error reappears multiple times, please include the technical details below in your report.\n"); - print("More details can be found in the webserver log.\n"); +if (function_exists('frankenphp_handle_request')) { + $maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0); + for ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) { + $keepRunning = \frankenphp_handle_request($handler); - throw $ex; + // Call the garbage collector to reduce the chances of it being triggered in the middle of a page generation + gc_collect_cycles(); + + if (!$keepRunning) { + break; + } } - Server::get(ITemplateManager::class)->printExceptionErrorPage($ex, 500); +} else { + $handler(); } diff --git a/lib/OC.php b/lib/OC.php new file mode 100644 index 0000000000000..488a0cf410548 --- /dev/null +++ b/lib/OC.php @@ -0,0 +1,1299 @@ + [ + 'SCRIPT_NAME' => $_SERVER['SCRIPT_NAME'] ?? null, + 'SCRIPT_FILENAME' => $_SERVER['SCRIPT_FILENAME'] ?? null, + ], + ]; + if (isset($_SERVER['REMOTE_ADDR'])) { + $params['server']['REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR']; + } + $fakeRequest = new \OC\AppFramework\Http\Request( + $params, + new \OC\AppFramework\Http\RequestId($_SERVER['UNIQUE_ID'] ?? '', new \OC\Security\SecureRandom()), + new \OC\AllConfig(new \OC\SystemConfig(self::$config)) + ); + $scriptName = $fakeRequest->getScriptName(); + if (substr($scriptName, -1) == '/') { + $scriptName .= 'index.php'; + //make sure suburi follows the same rules as scriptName + if (substr(OC::$SUBURI, -9) != 'index.php') { + if (substr(OC::$SUBURI, -1) != '/') { + OC::$SUBURI = OC::$SUBURI . '/'; + } + OC::$SUBURI = OC::$SUBURI . 'index.php'; + } + } + + if (OC::$CLI) { + OC::$WEBROOT = self::$config->getValue('overwritewebroot', ''); + } else { + if (substr($scriptName, 0 - strlen(OC::$SUBURI)) === OC::$SUBURI) { + OC::$WEBROOT = substr($scriptName, 0, 0 - strlen(OC::$SUBURI)); + + if (OC::$WEBROOT != '' && OC::$WEBROOT[0] !== '/') { + OC::$WEBROOT = '/' . OC::$WEBROOT; + } + } else { + // The scriptName is not ending with OC::$SUBURI + // This most likely means that we are calling from CLI. + // However some cron jobs still need to generate + // a web URL, so we use overwritewebroot as a fallback. + OC::$WEBROOT = self::$config->getValue('overwritewebroot', ''); + } + + // Resolve /nextcloud to /nextcloud/ to ensure to always have a trailing + // slash which is required by URL generation. + if (isset($_SERVER['REQUEST_URI']) && $_SERVER['REQUEST_URI'] === \OC::$WEBROOT + && substr($_SERVER['REQUEST_URI'], -1) !== '/') { + header('Location: ' . \OC::$WEBROOT . '/'); + exit(); + } + } + + // search the apps folder + $config_paths = self::$config->getValue('apps_paths', []); + if (!empty($config_paths)) { + foreach ($config_paths as $paths) { + if (isset($paths['url']) && isset($paths['path'])) { + $paths['url'] = rtrim($paths['url'], '/'); + $paths['path'] = rtrim($paths['path'], '/'); + OC::$APPSROOTS[] = $paths; + } + } + } elseif (file_exists(OC::$SERVERROOT . '/apps')) { + OC::$APPSROOTS[] = ['path' => OC::$SERVERROOT . '/apps', 'url' => '/apps', 'writable' => true]; + } + + + if (empty(OC::$APPSROOTS)) { + throw new \RuntimeException('apps directory not found! Please put the Nextcloud apps folder in the Nextcloud folder' + . '. You can also configure the location in the config.php file.'); + } + $paths = []; + foreach (OC::$APPSROOTS as $path) { + $paths[] = $path['path']; + if (!is_dir($path['path'])) { + throw new \RuntimeException(sprintf('App directory "%s" not found! Please put the Nextcloud apps folder in the' + . ' Nextcloud folder. You can also configure the location in the config.php file.', $path['path'])); + } + } + + // set the right include path + set_include_path( + implode(PATH_SEPARATOR, $paths) + ); + } + + public static function checkConfig(): void { + // Create config if it does not already exist + $configFilePath = self::$configDir . '/config.php'; + if (!file_exists($configFilePath)) { + @touch($configFilePath); + } + + // Check if config is writable + $configFileWritable = is_writable($configFilePath); + $configReadOnly = Server::get(IConfig::class)->getSystemValueBool('config_is_read_only'); + if (!$configFileWritable && !$configReadOnly + || !$configFileWritable && \OCP\Util::needUpgrade()) { + $urlGenerator = Server::get(IURLGenerator::class); + $l = Server::get(\OCP\L10N\IFactory::class)->get('lib'); + + if (self::$CLI) { + echo $l->t('Cannot write into "config" directory!') . "\n"; + echo $l->t('This can usually be fixed by giving the web server write access to the config directory.') . "\n"; + echo "\n"; + echo $l->t('But, if you prefer to keep config.php file read only, set the option "config_is_read_only" to true in it.') . "\n"; + echo $l->t('See %s', [ $urlGenerator->linkToDocs('admin-config') ]) . "\n"; + exit; + } else { + Server::get(ITemplateManager::class)->printErrorPage( + $l->t('Cannot write into "config" directory!'), + $l->t('This can usually be fixed by giving the web server write access to the config directory.') . ' ' + . $l->t('But, if you prefer to keep config.php file read only, set the option "config_is_read_only" to true in it.') . ' ' + . $l->t('See %s', [ $urlGenerator->linkToDocs('admin-config') ]), + 503 + ); + } + } + } + + public static function checkInstalled(\OC\SystemConfig $systemConfig): void { + if (defined('OC_CONSOLE')) { + return; + } + // Redirect to installer if not installed + if (!$systemConfig->getValue('installed', false) && OC::$SUBURI !== '/index.php' && OC::$SUBURI !== '/status.php') { + if (OC::$CLI) { + throw new Exception('Not installed'); + } else { + $url = OC::$WEBROOT . '/index.php'; + header('Location: ' . $url); + } + exit(); + } + } + + public static function checkMaintenanceMode(\OC\SystemConfig $systemConfig): void { + // Allow ajax update script to execute without being stopped + if (((bool)$systemConfig->getValue('maintenance', false)) && OC::$SUBURI != '/core/ajax/update.php') { + // send http status 503 + http_response_code(503); + header('X-Nextcloud-Maintenance-Mode: 1'); + header('Retry-After: 120'); + + // render error page + $template = Server::get(ITemplateManager::class)->getTemplate('', 'update.user', 'guest'); + \OCP\Util::addScript('core', 'maintenance'); + \OCP\Util::addScript('core', 'common'); + \OCP\Util::addStyle('core', 'guest'); + $template->printPage(); + die(); + } + } + + /** + * Prints the upgrade page + */ + private static function printUpgradePage(\OC\SystemConfig $systemConfig): void { + $cliUpgradeLink = $systemConfig->getValue('upgrade.cli-upgrade-link', ''); + $disableWebUpdater = $systemConfig->getValue('upgrade.disable-web', false); + $tooBig = false; + if (!$disableWebUpdater) { + $apps = Server::get(\OCP\App\IAppManager::class); + if ($apps->isEnabledForAnyone('user_ldap')) { + $qb = Server::get(\OCP\IDBConnection::class)->getQueryBuilder(); + + $result = $qb->select($qb->func()->count('*', 'user_count')) + ->from('ldap_user_mapping') + ->executeQuery(); + $row = $result->fetch(); + $result->closeCursor(); + + $tooBig = ($row['user_count'] > 50); + } + if (!$tooBig && $apps->isEnabledForAnyone('user_saml')) { + $qb = Server::get(\OCP\IDBConnection::class)->getQueryBuilder(); + + $result = $qb->select($qb->func()->count('*', 'user_count')) + ->from('user_saml_users') + ->executeQuery(); + $row = $result->fetch(); + $result->closeCursor(); + + $tooBig = ($row['user_count'] > 50); + } + if (!$tooBig) { + // count users + $totalUsers = Server::get(\OCP\IUserManager::class)->countUsersTotal(51); + $tooBig = ($totalUsers > 50); + } + } + $ignoreTooBigWarning = isset($_GET['IKnowThatThisIsABigInstanceAndTheUpdateRequestCouldRunIntoATimeoutAndHowToRestoreABackup']) + && $_GET['IKnowThatThisIsABigInstanceAndTheUpdateRequestCouldRunIntoATimeoutAndHowToRestoreABackup'] === 'IAmSuperSureToDoThis'; + + Util::addTranslations('core'); + Util::addScript('core', 'common'); + Util::addScript('core', 'main'); + Util::addScript('core', 'update'); + + $initialState = Server::get(IInitialStateService::class); + $serverVersion = Server::get(\OCP\ServerVersion::class); + if ($disableWebUpdater || ($tooBig && !$ignoreTooBigWarning)) { + // send http status 503 + http_response_code(503); + header('Retry-After: 120'); + + $urlGenerator = Server::get(IURLGenerator::class); + $initialState->provideInitialState('core', 'updaterView', 'adminCli'); + $initialState->provideInitialState('core', 'updateInfo', [ + 'cliUpgradeLink' => $cliUpgradeLink ?: $urlGenerator->linkToDocs('admin-cli-upgrade'), + 'productName' => self::getProductName(), + 'version' => $serverVersion->getVersionString(), + 'tooBig' => $tooBig, + ]); + + // render error page + Server::get(ITemplateManager::class) + ->getTemplate('', 'update', 'guest') + ->printPage(); + die(); + } + + // check whether this is a core update or apps update + $installedVersion = $systemConfig->getValue('version', '0.0.0'); + $currentVersion = implode('.', $serverVersion->getVersion()); + + // if not a core upgrade, then it's apps upgrade + $isAppsOnlyUpgrade = version_compare($currentVersion, $installedVersion, '='); + + $oldTheme = $systemConfig->getValue('theme'); + $systemConfig->setValue('theme', ''); + + /** @var \OC\App\AppManager $appManager */ + $appManager = Server::get(\OCP\App\IAppManager::class); + + // get third party apps + $ocVersion = $serverVersion->getVersion(); + $ocVersion = implode('.', $ocVersion); + $incompatibleApps = $appManager->getIncompatibleApps($ocVersion); + $incompatibleOverwrites = $systemConfig->getValue('app_install_overwrite', []); + $incompatibleShippedApps = []; + $incompatibleDisabledApps = []; + foreach ($incompatibleApps as $appInfo) { + if ($appManager->isShipped($appInfo['id'])) { + $incompatibleShippedApps[] = $appInfo['name'] . ' (' . $appInfo['id'] . ')'; + } + if (!in_array($appInfo['id'], $incompatibleOverwrites)) { + $incompatibleDisabledApps[] = $appInfo; + } + } + + if (!empty($incompatibleShippedApps)) { + $l = Server::get(\OCP\L10N\IFactory::class)->get('core'); + $hint = $l->t('Application %1$s is not present or has a non-compatible version with this server. Please check the apps directory.', [implode(', ', $incompatibleShippedApps)]); + throw new \OCP\HintException('Application ' . implode(', ', $incompatibleShippedApps) . ' is not present or has a non-compatible version with this server. Please check the apps directory.', $hint); + } + + $appConfig = Server::get(IAppConfig::class); + $appsToUpgrade = array_map(function ($app) use (&$appConfig) { + return [ + 'id' => $app['id'], + 'name' => $app['name'], + 'version' => $app['version'], + 'oldVersion' => $appConfig->getValueString($app['id'], 'installed_version'), + ]; + }, $appManager->getAppsNeedingUpgrade($ocVersion)); + + $params = [ + 'appsToUpgrade' => $appsToUpgrade, + 'incompatibleAppsList' => $incompatibleDisabledApps, + 'isAppsOnlyUpgrade' => $isAppsOnlyUpgrade, + 'oldTheme' => $oldTheme, + 'productName' => self::getProductName(), + 'version' => $serverVersion->getVersionString(), + ]; + + $initialState->provideInitialState('core', 'updaterView', 'admin'); + $initialState->provideInitialState('core', 'updateInfo', $params); + Server::get(ITemplateManager::class) + ->getTemplate('', 'update', 'guest') + ->printPage(); + } + + private static function getProductName(): string { + $productName = 'Nextcloud'; + try { + $defaults = new \OC_Defaults(); + $productName = $defaults->getName(); + } catch (Throwable $error) { + // ignore + } + return $productName; + } + + public static function initSession(): void { + $request = Server::get(IRequest::class); + + // TODO: Temporary disabled again to solve issues with CalDAV/CardDAV clients like DAVx5 that use cookies + // TODO: See https://github.com/nextcloud/server/issues/37277#issuecomment-1476366147 and the other comments + // TODO: for further information. + // $isDavRequest = strpos($request->getRequestUri(), '/remote.php/dav') === 0 || strpos($request->getRequestUri(), '/remote.php/webdav') === 0; + // if ($request->getHeader('Authorization') !== '' && is_null($request->getCookie('cookie_test')) && $isDavRequest && !isset($_COOKIE['nc_session_id'])) { + // setcookie('cookie_test', 'test', time() + 3600); + // // Do not initialize the session if a request is authenticated directly + // // unless there is a session cookie already sent along + // return; + // } + + if ($request->getServerProtocol() === 'https') { + ini_set('session.cookie_secure', 'true'); + } + + // prevents javascript from accessing php session cookies + ini_set('session.cookie_httponly', 'true'); + + // set the cookie path to the Nextcloud directory + $cookie_path = OC::$WEBROOT ? : '/'; + ini_set('session.cookie_path', $cookie_path); + + // set the cookie domain to the Nextcloud domain + $cookie_domain = self::$config->getValue('cookie_domain', ''); + if ($cookie_domain) { + ini_set('session.cookie_domain', $cookie_domain); + } + + // Do not initialize sessions for 'status.php' requests + // Monitoring endpoints can quickly flood session handlers + // and 'status.php' doesn't require sessions anyway + // We still need to run the ini_set above so that same-site cookies use the correct configuration. + if (str_ends_with($request->getScriptName(), '/status.php')) { + return; + } + + // Let the session name be changed in the initSession Hook + $sessionName = OC_Util::getInstanceId(); + + try { + $logger = null; + if (Server::get(\OC\SystemConfig::class)->getValue('installed', false)) { + $logger = logger('core'); + } + + // set the session name to the instance id - which is unique + $session = new \OC\Session\Internal( + $sessionName, + $logger, + ); + + $cryptoWrapper = Server::get(\OC\Session\CryptoWrapper::class); + $session = $cryptoWrapper->wrapSession($session); + self::$server->setSession($session); + + // if session can't be started break with http 500 error + } catch (Exception $e) { + Server::get(LoggerInterface::class)->error($e->getMessage(), ['app' => 'base','exception' => $e]); + //show the user a detailed error page + Server::get(ITemplateManager::class)->printExceptionErrorPage($e, 500); + die(); + } + + //try to set the session lifetime + $sessionLifeTime = self::getSessionLifeTime(); + + // session timeout + if ($session->exists('LAST_ACTIVITY') && (time() - $session->get('LAST_ACTIVITY') > $sessionLifeTime)) { + if (isset($_COOKIE[session_name()])) { + setcookie(session_name(), '', -1, self::$WEBROOT ? : '/'); + } + Server::get(IUserSession::class)->logout(); + } + + if (!self::hasSessionRelaxedExpiry()) { + $session->set('LAST_ACTIVITY', time()); + } + $session->close(); + } + + private static function getSessionLifeTime(): int { + return Server::get(IConfig::class)->getSystemValueInt('session_lifetime', 60 * 60 * 24); + } + + /** + * @return bool true if the session expiry should only be done by gc instead of an explicit timeout + */ + public static function hasSessionRelaxedExpiry(): bool { + return Server::get(IConfig::class)->getSystemValueBool('session_relaxed_expiry', false); + } + + /** + * Try to set some values to the required Nextcloud default + */ + public static function setRequiredIniValues(): void { + // Don't display errors and log them + @ini_set('display_errors', '0'); + @ini_set('log_errors', '1'); + + // Try to configure php to enable big file uploads. + // This doesn't work always depending on the webserver and php configuration. + // Let's try to overwrite some defaults if they are smaller than 1 hour + + if (intval(@ini_get('max_execution_time') ?: 0) < 3600) { + @ini_set('max_execution_time', strval(3600)); + } + + if (intval(@ini_get('max_input_time') ?: 0) < 3600) { + @ini_set('max_input_time', strval(3600)); + } + + // Try to set the maximum execution time to the largest time limit we have + if (strpos(@ini_get('disable_functions'), 'set_time_limit') === false) { + @set_time_limit(max(intval(@ini_get('max_execution_time')), intval(@ini_get('max_input_time')))); + } + + @ini_set('default_charset', 'UTF-8'); + @ini_set('gd.jpeg_ignore_warning', '1'); + } + + /** + * Send the same site cookies + */ + private static function sendSameSiteCookies(): void { + $cookieParams = session_get_cookie_params(); + $secureCookie = ($cookieParams['secure'] === true) ? 'secure; ' : ''; + $policies = [ + 'lax', + 'strict', + ]; + + // Append __Host to the cookie if it meets the requirements + $cookiePrefix = ''; + if ($cookieParams['secure'] === true && $cookieParams['path'] === '/') { + $cookiePrefix = '__Host-'; + } + + foreach ($policies as $policy) { + header( + sprintf( + 'Set-Cookie: %snc_sameSiteCookie%s=true; path=%s; httponly;' . $secureCookie . 'expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=%s', + $cookiePrefix, + $policy, + $cookieParams['path'], + $policy + ), + false + ); + } + } + + /** + * Same Site cookie to further mitigate CSRF attacks. This cookie has to + * be set in every request if cookies are sent to add a second level of + * defense against CSRF. + * + * If the cookie is not sent this will set the cookie and reload the page. + * We use an additional cookie since we want to protect logout CSRF and + * also we can't directly interfere with PHP's session mechanism. + */ + private static function performSameSiteCookieProtection(IConfig $config): void { + $request = Server::get(IRequest::class); + + // Some user agents are notorious and don't really properly follow HTTP + // specifications. For those, have an automated opt-out. Since the protection + // for remote.php is applied in base.php as starting point we need to opt out + // here. + $incompatibleUserAgents = $config->getSystemValue('csrf.optout'); + + // Fallback, if csrf.optout is unset + if (!is_array($incompatibleUserAgents)) { + $incompatibleUserAgents = [ + // OS X Finder + '/^WebDAVFS/', + // Windows webdav drive + '/^Microsoft-WebDAV-MiniRedir/', + ]; + } + + if ($request->isUserAgent($incompatibleUserAgents)) { + return; + } + + if (count($_COOKIE) > 0) { + $requestUri = $request->getScriptName(); + $processingScript = explode('/', $requestUri); + $processingScript = $processingScript[count($processingScript) - 1]; + + if ($processingScript === 'index.php' // index.php routes are handled in the middleware + || $processingScript === 'cron.php' // and cron.php does not need any authentication at all + || $processingScript === 'public.php' // For public.php, auth for password protected shares is done in the PublicAuth plugin + ) { + return; + } + + // All other endpoints require the lax and the strict cookie + if (!$request->passesStrictCookieCheck()) { + logger('core')->warning('Request does not pass strict cookie check'); + self::sendSameSiteCookies(); + // Debug mode gets access to the resources without strict cookie + // due to the fact that the SabreDAV browser also lives there. + if (!$config->getSystemValueBool('debug', false)) { + http_response_code(\OCP\AppFramework\Http::STATUS_PRECONDITION_FAILED); + header('Content-Type: application/json'); + echo json_encode(['error' => 'Strict Cookie has not been found in request']); + exit(); + } + } + } elseif (!isset($_COOKIE['nc_sameSiteCookielax']) || !isset($_COOKIE['nc_sameSiteCookiestrict'])) { + self::sendSameSiteCookies(); + } + } + + /** + * This function adds some security related headers to all requests served via base.php + * The implementation of this function has to happen here to ensure that all third-party + * components (e.g. SabreDAV) also benefit from this headers. + */ + private static function addSecurityHeaders(): void { + /** + * FIXME: Content Security Policy for legacy components. This + * can be removed once \OCP\AppFramework\Http\Response from the AppFramework + * is used everywhere. + * @see \OCP\AppFramework\Http\Response::getHeaders + */ + $policy = 'default-src \'self\'; ' + . 'script-src \'self\' \'nonce-' . Server::get(ContentSecurityPolicyNonceManager::class)->getNonce() . '\'; ' + . 'style-src \'self\' \'unsafe-inline\'; ' + . 'frame-src *; ' + . 'img-src * data: blob:; ' + . 'font-src \'self\' data:; ' + . 'media-src *; ' + . 'connect-src *; ' + . 'object-src \'none\'; ' + . 'base-uri \'self\'; '; + header('Content-Security-Policy:' . $policy); + + // Send fallback headers for installations that don't have the possibility to send + // custom headers on the webserver side + if (getenv('modHeadersAvailable') !== 'true') { + header('Referrer-Policy: no-referrer'); // https://www.w3.org/TR/referrer-policy/ + header('X-Content-Type-Options: nosniff'); // Disable sniffing the content type for IE + header('X-Frame-Options: SAMEORIGIN'); // Disallow iFraming from other domains + header('X-Permitted-Cross-Domain-Policies: none'); // https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html + header('X-Robots-Tag: noindex, nofollow'); // https://developers.google.com/webmasters/control-crawl-index/docs/robots_meta_tag + } + } + + public static function boot(): void { + // prevent any XML processing from loading external entities + libxml_set_external_entity_loader(static function () { + return null; + }); + + // Set default timezone before the Server object is booted + if (!date_default_timezone_set('UTC')) { + throw new \RuntimeException('Could not set timezone to UTC'); + } + + // calculate the root directories + OC::$SERVERROOT = str_replace('\\', '/', substr(__DIR__, 0, -4)); + + // register autoloader + $loaderStart = microtime(true); + + self::$CLI = (php_sapi_name() == 'cli'); + + // Add default composer PSR-4 autoloader, ensure apcu to be disabled + self::$composerAutoloader = require_once OC::$SERVERROOT . '/lib/composer/autoload.php'; + self::$composerAutoloader->setApcuPrefix(null); + + // setup 3rdparty autoloader + $vendorAutoLoad = OC::$SERVERROOT . '/3rdparty/autoload.php'; + if (!file_exists($vendorAutoLoad)) { + throw new \RuntimeException('Composer autoloader not found, unable to continue. Check the folder "3rdparty". Running "git submodule update --init" will initialize the git submodule that handles the subfolder "3rdparty".'); + } + require_once $vendorAutoLoad; + + $loaderEnd = microtime(true); + + // load configs + if (defined('PHPUNIT_CONFIG_DIR')) { + self::$configDir = OC::$SERVERROOT . '/' . PHPUNIT_CONFIG_DIR . '/'; + } elseif (defined('PHPUNIT_RUN') && PHPUNIT_RUN && is_dir(OC::$SERVERROOT . '/tests/config/')) { + self::$configDir = OC::$SERVERROOT . '/tests/config/'; + } elseif ($dir = getenv('NEXTCLOUD_CONFIG_DIR')) { + self::$configDir = rtrim($dir, '/') . '/'; + } else { + self::$configDir = OC::$SERVERROOT . '/config/'; + } + self::$config = new \OC\Config(self::$configDir); + + // Enable lazy loading if activated + \OC\AppFramework\Utility\SimpleContainer::$useLazyObjects = (bool)self::$config->getValue('enable_lazy_objects', true); + + try { + self::initPaths(); + } catch (\RuntimeException $e) { + if (!self::$CLI) { + http_response_code(503); + } + // we can't use the template error page here, because this needs the + // DI container which isn't available yet + print($e->getMessage()); + exit(); + } + + //$eventLogger = Server::get(\OCP\Diagnostics\IEventLogger::class); + //$eventLogger->log('autoloader', 'Autoloader', $loaderStart, $loaderEnd); + //$eventLogger->start('init', 'Initialize'); + } + + public static function init(): void { + // First handle PHP configuration and copy auth headers to the expected + // $_SERVER variable before doing anything Server object related + self::setRequiredIniValues(); + self::handleAuthHeaders(); + + // set up the basic server + self::$server = new \OC\Server(\OC::$WEBROOT, self::$config); + self::$server->boot(); + + $loaderStart = microtime(true); + + try { + $profiler = new BuiltInProfiler( + Server::get(IConfig::class), + Server::get(IRequest::class), + ); + $profiler->start(); + } catch (\Throwable $e) { + logger('core')->error('Failed to start profiler: ' . $e->getMessage(), ['app' => 'base']); + } + + if (self::$CLI && in_array('--' . \OCP\Console\ReservedOptions::DEBUG_LOG, $_SERVER['argv'])) { + \OC\Core\Listener\BeforeMessageLoggedEventListener::setup(); + } + + $eventLogger = Server::get(\OCP\Diagnostics\IEventLogger::class); + $eventLogger->start('init', 'Initialize'); + + // Override php.ini and log everything if we're troubleshooting + if (self::$config->getValue('loglevel') === ILogger::DEBUG) { + error_reporting(E_ALL); + } + + // initialize intl fallback if necessary + OC_Util::isSetLocaleWorking(); + + $config = Server::get(IConfig::class); + if (!defined('PHPUNIT_RUN')) { + $errorHandler = new OC\Log\ErrorHandler( + Server::get(\Psr\Log\LoggerInterface::class), + ); + $exceptionHandler = [$errorHandler, 'onException']; + if ($config->getSystemValueBool('debug', false)) { + set_error_handler([$errorHandler, 'onAll'], E_ALL); + if (\OC::$CLI) { + $exceptionHandler = [Server::get(ITemplateManager::class), 'printExceptionErrorPage']; + } + } else { + set_error_handler([$errorHandler, 'onError']); + } + register_shutdown_function([$errorHandler, 'onShutdown']); + set_exception_handler($exceptionHandler); + } + + /** @var \OC\AppFramework\Bootstrap\Coordinator $bootstrapCoordinator */ + $bootstrapCoordinator = Server::get(\OC\AppFramework\Bootstrap\Coordinator::class); + $bootstrapCoordinator->runInitialRegistration(); + + $eventLogger->start('init_session', 'Initialize session'); + + // Check for PHP SimpleXML extension earlier since we need it before our other checks and want to provide a useful hint for web users + // see https://github.com/nextcloud/server/pull/2619 + if (!function_exists('simplexml_load_file')) { + throw new \OCP\HintException('The PHP SimpleXML/PHP-XML extension is not installed.', 'Install the extension or make sure it is enabled.'); + } + + $systemConfig = Server::get(\OC\SystemConfig::class); + $appManager = Server::get(\OCP\App\IAppManager::class); + if ($systemConfig->getValue('installed', false)) { + $appManager->loadApps(['session']); + } + if (!self::$CLI) { + self::initSession(); + } + $eventLogger->end('init_session'); + self::checkConfig(); + self::checkInstalled($systemConfig); + + if (!self::$CLI) { + self::addSecurityHeaders(); + self::performSameSiteCookieProtection($config); + } + + if (!defined('OC_CONSOLE')) { + $eventLogger->start('check_server', 'Run a few configuration checks'); + $errors = OC_Util::checkServer($systemConfig); + if (count($errors) > 0) { + if (!self::$CLI) { + http_response_code(503); + Util::addStyle('guest'); + try { + Server::get(ITemplateManager::class)->printGuestPage('', 'error', ['errors' => $errors]); + exit; + } catch (\Exception $e) { + // In case any error happens when showing the error page, we simply fall back to posting the text. + // This might be the case when e.g. the data directory is broken and we can not load/write SCSS to/from it. + } + } + + // Convert l10n string into regular string for usage in database + $staticErrors = []; + foreach ($errors as $error) { + echo $error['error'] . "\n"; + echo $error['hint'] . "\n\n"; + $staticErrors[] = [ + 'error' => (string)$error['error'], + 'hint' => (string)$error['hint'], + ]; + } + + try { + $config->setAppValue('core', 'cronErrors', json_encode($staticErrors)); + } catch (\Exception $e) { + echo('Writing to database failed'); + } + exit(1); + } elseif (self::$CLI && $config->getSystemValueBool('installed', false)) { + $config->deleteAppValue('core', 'cronErrors'); + } + $eventLogger->end('check_server'); + } + + // User and Groups + if (!$systemConfig->getValue('installed', false)) { + Server::get(ISession::class)->set('user_id', ''); + } + + $eventLogger->start('setup_backends', 'Setup group and user backends'); + Server::get(\OCP\IUserManager::class)->registerBackend(new \OC\User\Database()); + Server::get(\OCP\IGroupManager::class)->addBackend(new \OC\Group\Database()); + + // Subscribe to the hook + \OCP\Util::connectHook( + '\OCA\Files_Sharing\API\Server2Server', + 'preLoginNameUsedAsUserName', + '\OC\User\Database', + 'preLoginNameUsedAsUserName' + ); + + //setup extra user backends + if (!\OCP\Util::needUpgrade()) { + OC_User::setupBackends(); + } else { + // Run upgrades in incognito mode + OC_User::setIncognitoMode(true); + } + $eventLogger->end('setup_backends'); + + self::registerCleanupHooks($systemConfig); + self::registerShareHooks($systemConfig); + self::registerEncryptionWrapperAndHooks(); + self::registerAccountHooks(); + self::registerResourceCollectionHooks(); + self::registerFileReferenceEventListener(); + self::registerRenderReferenceEventListener(); + self::registerAppRestrictionsHooks(); + + // Make sure that the application class is not loaded before the database is setup + if ($systemConfig->getValue('installed', false)) { + $appManager->loadApp('settings'); + } + + //make sure temporary files are cleaned up + $tmpManager = Server::get(\OCP\ITempManager::class); + register_shutdown_function([$tmpManager, 'clean']); + $lockProvider = Server::get(\OCP\Lock\ILockingProvider::class); + register_shutdown_function([$lockProvider, 'releaseAll']); + + // Check whether the sample configuration has been copied + if ($systemConfig->getValue('copied_sample_config', false)) { + $l = Server::get(\OCP\L10N\IFactory::class)->get('lib'); + Server::get(ITemplateManager::class)->printErrorPage( + $l->t('Sample configuration detected'), + $l->t('It has been detected that the sample configuration has been copied. This can break your installation and is unsupported. Please read the documentation before performing changes on config.php'), + 503 + ); + return; + } + + $request = Server::get(IRequest::class); + $host = $request->getInsecureServerHost(); + /** + * if the host passed in headers isn't trusted + * FIXME: Should not be in here at all :see_no_evil: + */ + if (!OC::$CLI + && !Server::get(\OC\Security\TrustedDomainHelper::class)->isTrustedDomain($host) + && $config->getSystemValueBool('installed', false) + ) { + // Allow access to CSS resources + $isScssRequest = false; + if (strpos($request->getPathInfo() ?: '', '/css/') === 0) { + $isScssRequest = true; + } + + if (substr($request->getRequestUri(), -11) === '/status.php') { + http_response_code(400); + header('Content-Type: application/json'); + echo '{"error": "Trusted domain error.", "code": 15}'; + exit(); + } + + if (!$isScssRequest) { + http_response_code(400); + Server::get(LoggerInterface::class)->info( + 'Trusted domain error. "{remoteAddress}" tried to access using "{host}" as host.', + [ + 'app' => 'core', + 'remoteAddress' => $request->getRemoteAddress(), + 'host' => $host, + ] + ); + + $tmpl = Server::get(ITemplateManager::class)->getTemplate('core', 'untrustedDomain', 'guest'); + $tmpl->assign('docUrl', Server::get(IURLGenerator::class)->linkToDocs('admin-trusted-domains')); + $tmpl->printPage(); + + exit(); + } + } + $eventLogger->end('boot'); + $eventLogger->log('init', 'OC::init', $loaderStart, microtime(true)); + $eventLogger->start('runtime', 'Runtime'); + $eventLogger->start('request', 'Full request after boot'); + register_shutdown_function(function () use ($eventLogger) { + $eventLogger->end('request'); + }); + + register_shutdown_function(function () use ($config) { + $memoryPeak = memory_get_peak_usage(); + $debugModeEnabled = $config->getSystemValueBool('debug', false); + $memoryLimit = null; + + if (!$debugModeEnabled) { + // Use the memory helper to get the real memory limit in bytes if debug mode is disabled + try { + $memoryInfo = new \OC\MemoryInfo(); + $memoryLimit = $memoryInfo->getMemoryLimit(); + } catch (Throwable $e) { + // Ignore any errors and fall back to hardcoded thresholds + } + } + + // Check if a memory limit is configured and can be retrieved and determine log level if debug mode is disabled + if (!$debugModeEnabled && $memoryLimit !== null && $memoryLimit !== -1) { + $logLevel = match (true) { + $memoryPeak > $memoryLimit * 0.9 => ILogger::FATAL, + $memoryPeak > $memoryLimit * 0.75 => ILogger::ERROR, + $memoryPeak > $memoryLimit * 0.5 => ILogger::WARN, + default => null, + }; + + $memoryLimitIni = @ini_get('memory_limit'); + $message = 'Request used ' . Util::humanFileSize($memoryPeak) . ' of memory. Memory limit: ' . ($memoryLimitIni ?: 'unknown'); + } else { + // Fall back to hardcoded thresholds if memory_limit cannot be determined or if debug mode is enabled + $logLevel = match (true) { + $memoryPeak > 500_000_000 => ILogger::FATAL, + $memoryPeak > 400_000_000 => ILogger::ERROR, + $memoryPeak > 300_000_000 => ILogger::WARN, + default => null, + }; + + $message = 'Request used more than 300 MB of RAM: ' . Util::humanFileSize($memoryPeak); + } + + // Log the message + if ($logLevel !== null) { + $logger = Server::get(LoggerInterface::class); + $logger->log($logLevel, $message, ['app' => 'core']); + } + }); + } + + /** + * register hooks for the cleanup of cache and bruteforce protection + */ + public static function registerCleanupHooks(\OC\SystemConfig $systemConfig): void { + //don't try to do this before we are properly setup + if ($systemConfig->getValue('installed', false) && !\OCP\Util::needUpgrade()) { + // NOTE: This will be replaced to use OCP + $userSession = Server::get(\OC\User\Session::class); + $userSession->listen('\OC\User', 'postLogin', function () use ($userSession) { + if (!defined('PHPUNIT_RUN') && $userSession->isLoggedIn()) { + // reset brute force delay for this IP address and username + $uid = $userSession->getUser()->getUID(); + $request = Server::get(IRequest::class); + $throttler = Server::get(IThrottler::class); + $throttler->resetDelay($request->getRemoteAddress(), 'login', ['user' => $uid]); + } + + try { + $cache = new \OC\Cache\File(); + $cache->gc(); + } catch (\OC\ServerNotAvailableException $e) { + // not a GC exception, pass it on + throw $e; + } catch (\OC\ForbiddenException $e) { + // filesystem blocked for this request, ignore + } catch (\Exception $e) { + // a GC exception should not prevent users from using OC, + // so log the exception + Server::get(LoggerInterface::class)->warning('Exception when running cache gc.', [ + 'app' => 'core', + 'exception' => $e, + ]); + } + }); + } + } + + private static function registerEncryptionWrapperAndHooks(): void { + /** @var \OC\Encryption\Manager */ + $manager = Server::get(\OCP\Encryption\IManager::class); + Server::get(IEventDispatcher::class)->addListener( + BeforeFileSystemSetupEvent::class, + $manager->setupStorage(...), + ); + + $enabled = $manager->isEnabled(); + if ($enabled) { + \OC\Encryption\EncryptionEventListener::register(Server::get(IEventDispatcher::class)); + } + } + + private static function registerAccountHooks(): void { + /** @var IEventDispatcher $dispatcher */ + $dispatcher = Server::get(IEventDispatcher::class); + $dispatcher->addServiceListener(UserChangedEvent::class, \OC\Accounts\Hooks::class); + } + + private static function registerAppRestrictionsHooks(): void { + /** @var \OC\Group\Manager $groupManager */ + $groupManager = Server::get(\OCP\IGroupManager::class); + $groupManager->listen('\OC\Group', 'postDelete', function (\OCP\IGroup $group) { + $appManager = Server::get(\OCP\App\IAppManager::class); + $apps = $appManager->getEnabledAppsForGroup($group); + foreach ($apps as $appId) { + $restrictions = $appManager->getAppRestriction($appId); + if (empty($restrictions)) { + continue; + } + $key = array_search($group->getGID(), $restrictions, true); + unset($restrictions[$key]); + $restrictions = array_values($restrictions); + if (empty($restrictions)) { + $appManager->disableApp($appId); + } else { + $appManager->enableAppForGroups($appId, $restrictions); + } + } + }); + } + + private static function registerResourceCollectionHooks(): void { + \OC\Collaboration\Resources\Listener::register(Server::get(IEventDispatcher::class)); + } + + private static function registerFileReferenceEventListener(): void { + \OC\Collaboration\Reference\File\FileReferenceEventListener::register(Server::get(IEventDispatcher::class)); + } + + private static function registerRenderReferenceEventListener() { + \OC\Collaboration\Reference\RenderReferenceEventListener::register(Server::get(IEventDispatcher::class)); + } + + /** + * register hooks for sharing + */ + public static function registerShareHooks(\OC\SystemConfig $systemConfig): void { + if ($systemConfig->getValue('installed')) { + + $dispatcher = Server::get(IEventDispatcher::class); + $dispatcher->addServiceListener(UserRemovedEvent::class, UserRemovedListener::class); + $dispatcher->addServiceListener(GroupDeletedEvent::class, GroupDeletedListener::class); + $dispatcher->addServiceListener(UserDeletedEvent::class, UserDeletedListener::class); + } + } + + /** + * Handle the request + */ + public static function handleRequest(): void { + Server::get(\OCP\Diagnostics\IEventLogger::class)->start('handle_request', 'Handle request'); + $systemConfig = Server::get(\OC\SystemConfig::class); + + // Check if Nextcloud is installed or in maintenance (update) mode + if (!$systemConfig->getValue('installed', false)) { + Server::get(ISession::class)->clear(); + $controller = Server::get(\OC\Core\Controller\SetupController::class); + $controller->run($_POST); + exit(); + } + + $request = Server::get(IRequest::class); + $request->throwDecodingExceptionIfAny(); + $requestPath = $request->getRawPathInfo(); + if ($requestPath === '/heartbeat') { + return; + } + if (substr($requestPath, -3) !== '.js') { // we need these files during the upgrade + self::checkMaintenanceMode($systemConfig); + + if (\OCP\Util::needUpgrade()) { + if (function_exists('opcache_reset')) { + opcache_reset(); + } + if (!((bool)$systemConfig->getValue('maintenance', false))) { + self::printUpgradePage($systemConfig); + exit(); + } + } + } + + $appManager = Server::get(\OCP\App\IAppManager::class); + + // Always load authentication apps + $appManager->loadApps(['authentication']); + $appManager->loadApps(['extended_authentication']); + + // Load minimum set of apps + if (!\OCP\Util::needUpgrade() + && !((bool)$systemConfig->getValue('maintenance', false))) { + // For logged-in users: Load everything + if (Server::get(IUserSession::class)->isLoggedIn()) { + $appManager->loadApps(); + } else { + // For guests: Load only filesystem and logging + $appManager->loadApps(['filesystem', 'logging']); + + // Don't try to login when a client is trying to get a OAuth token. + // OAuth needs to support basic auth too, so the login is not valid + // inside Nextcloud and the Login exception would ruin it. + if ($request->getRawPathInfo() !== '/apps/oauth2/api/v1/token') { + try { + self::handleLogin($request); + } catch (DisabledUserException $e) { + // Disabled users would not be seen as logged in and + // trying to log them in would fail, so the login + // exception is ignored for the themed stylesheets and + // images. + if ($request->getRawPathInfo() !== '/apps/theming/theme/default.css' + && $request->getRawPathInfo() !== '/apps/theming/theme/light.css' + && $request->getRawPathInfo() !== '/apps/theming/theme/dark.css' + && $request->getRawPathInfo() !== '/apps/theming/theme/light-highcontrast.css' + && $request->getRawPathInfo() !== '/apps/theming/theme/dark-highcontrast.css' + && $request->getRawPathInfo() !== '/apps/theming/theme/opendyslexic.css' + && $request->getRawPathInfo() !== '/apps/theming/image/background' + && $request->getRawPathInfo() !== '/apps/theming/image/logo' + && $request->getRawPathInfo() !== '/apps/theming/image/logoheader' + && !str_starts_with($request->getRawPathInfo(), '/apps/theming/favicon') + && !str_starts_with($request->getRawPathInfo(), '/apps/theming/icon')) { + throw $e; + } + } + } + } + } + + if (!self::$CLI) { + try { + if (!\OCP\Util::needUpgrade()) { + $appManager->loadApps(['filesystem', 'logging']); + $appManager->loadApps(); + } + Server::get(\OC\Route\Router::class)->match($request->getRawPathInfo()); + return; + } catch (Symfony\Component\Routing\Exception\ResourceNotFoundException $e) { + //header('HTTP/1.0 404 Not Found'); + } catch (Symfony\Component\Routing\Exception\MethodNotAllowedException $e) { + http_response_code(405); + return; + } + } + + // Handle WebDAV + if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PROPFIND') { + // not allowed any more to prevent people + // mounting this root directly. + // Users need to mount remote.php/webdav instead. + http_response_code(405); + return; + } + + // Handle requests for JSON or XML + $acceptHeader = $request->getHeader('Accept'); + if (in_array($acceptHeader, ['application/json', 'application/xml'], true)) { + http_response_code(404); + return; + } + + // Handle resources that can't be found + // This prevents browsers from redirecting to the default page and then + // attempting to parse HTML as CSS and similar. + $destinationHeader = $request->getHeader('Sec-Fetch-Dest'); + if (in_array($destinationHeader, ['font', 'script', 'style'])) { + http_response_code(404); + return; + } + + // Redirect to the default app or login only as an entry point + if ($requestPath === '') { + // Someone is logged in + $userSession = Server::get(IUserSession::class); + if ($userSession->isLoggedIn()) { + header('X-User-Id: ' . $userSession->getUser()?->getUID()); + header('Location: ' . Server::get(IURLGenerator::class)->linkToDefaultPageUrl()); + } else { + // Not handled and not logged in + header('Location: ' . Server::get(IURLGenerator::class)->linkToRouteAbsolute('core.login.showLoginForm')); + } + return; + } + + try { + Server::get(\OC\Route\Router::class)->match('/error/404'); + } catch (\Exception $e) { + if (!$e instanceof MethodNotAllowedException) { + logger('core')->emergency($e->getMessage(), ['exception' => $e]); + } + $l = Server::get(\OCP\L10N\IFactory::class)->get('lib'); + Server::get(ITemplateManager::class)->printErrorPage( + '404', + $l->t('The page could not be found on the server.'), + 404 + ); + } + } + + /** + * Check login: apache auth, auth token, basic auth + */ + public static function handleLogin(OCP\IRequest $request): bool { + if ($request->getHeader('X-Nextcloud-Federation')) { + return false; + } + $userSession = Server::get(\OC\User\Session::class); + if (OC_User::handleApacheAuth()) { + return true; + } + if (self::tryAppAPILogin($request)) { + return true; + } + if ($userSession->tryTokenLogin($request)) { + return true; + } + if (isset($_COOKIE['nc_username']) + && isset($_COOKIE['nc_token']) + && isset($_COOKIE['nc_session_id']) + && $userSession->loginWithCookie($_COOKIE['nc_username'], $_COOKIE['nc_token'], $_COOKIE['nc_session_id'])) { + return true; + } + if ($userSession->tryBasicAuthLogin($request, Server::get(IThrottler::class))) { + return true; + } + return false; + } + + protected static function handleAuthHeaders(): void { + //copy http auth headers for apache+php-fcgid work around + if (isset($_SERVER['HTTP_XAUTHORIZATION']) && !isset($_SERVER['HTTP_AUTHORIZATION'])) { + $_SERVER['HTTP_AUTHORIZATION'] = $_SERVER['HTTP_XAUTHORIZATION']; + } + + // Extract PHP_AUTH_USER/PHP_AUTH_PW from other headers if necessary. + $vars = [ + 'HTTP_AUTHORIZATION', // apache+php-cgi work around + 'REDIRECT_HTTP_AUTHORIZATION', // apache+php-cgi alternative + ]; + foreach ($vars as $var) { + if (isset($_SERVER[$var]) && is_string($_SERVER[$var]) && preg_match('/Basic\s+(.*)$/i', $_SERVER[$var], $matches)) { + $credentials = explode(':', base64_decode($matches[1]), 2); + if (count($credentials) === 2) { + $_SERVER['PHP_AUTH_USER'] = $credentials[0]; + $_SERVER['PHP_AUTH_PW'] = $credentials[1]; + break; + } + } + } + } + + protected static function tryAppAPILogin(OCP\IRequest $request): bool { + if (!$request->getHeader('AUTHORIZATION-APP-API')) { + return false; + } + $appManager = Server::get(OCP\App\IAppManager::class); + if (!$appManager->isEnabledForAnyone('app_api')) { + return false; + } + try { + $appAPIService = Server::get(OCA\AppAPI\Service\AppAPIService::class); + return $appAPIService->validateExAppRequestToNC($request); + } catch (\Psr\Container\NotFoundExceptionInterface|\Psr\Container\ContainerExceptionInterface $e) { + return false; + } + } +} diff --git a/lib/base.php b/lib/base.php index a11334c50cfd7..8012db9d76314 100644 --- a/lib/base.php +++ b/lib/base.php @@ -1,1291 +1,14 @@ [ - 'SCRIPT_NAME' => $_SERVER['SCRIPT_NAME'] ?? null, - 'SCRIPT_FILENAME' => $_SERVER['SCRIPT_FILENAME'] ?? null, - ], - ]; - if (isset($_SERVER['REMOTE_ADDR'])) { - $params['server']['REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR']; - } - $fakeRequest = new \OC\AppFramework\Http\Request( - $params, - new \OC\AppFramework\Http\RequestId($_SERVER['UNIQUE_ID'] ?? '', new \OC\Security\SecureRandom()), - new \OC\AllConfig(new \OC\SystemConfig(self::$config)) - ); - $scriptName = $fakeRequest->getScriptName(); - if (substr($scriptName, -1) == '/') { - $scriptName .= 'index.php'; - //make sure suburi follows the same rules as scriptName - if (substr(OC::$SUBURI, -9) != 'index.php') { - if (substr(OC::$SUBURI, -1) != '/') { - OC::$SUBURI = OC::$SUBURI . '/'; - } - OC::$SUBURI = OC::$SUBURI . 'index.php'; - } - } - - if (OC::$CLI) { - OC::$WEBROOT = self::$config->getValue('overwritewebroot', ''); - } else { - if (substr($scriptName, 0 - strlen(OC::$SUBURI)) === OC::$SUBURI) { - OC::$WEBROOT = substr($scriptName, 0, 0 - strlen(OC::$SUBURI)); - - if (OC::$WEBROOT != '' && OC::$WEBROOT[0] !== '/') { - OC::$WEBROOT = '/' . OC::$WEBROOT; - } - } else { - // The scriptName is not ending with OC::$SUBURI - // This most likely means that we are calling from CLI. - // However some cron jobs still need to generate - // a web URL, so we use overwritewebroot as a fallback. - OC::$WEBROOT = self::$config->getValue('overwritewebroot', ''); - } - - // Resolve /nextcloud to /nextcloud/ to ensure to always have a trailing - // slash which is required by URL generation. - if (isset($_SERVER['REQUEST_URI']) && $_SERVER['REQUEST_URI'] === \OC::$WEBROOT - && substr($_SERVER['REQUEST_URI'], -1) !== '/') { - header('Location: ' . \OC::$WEBROOT . '/'); - exit(); - } - } - - // search the apps folder - $config_paths = self::$config->getValue('apps_paths', []); - if (!empty($config_paths)) { - foreach ($config_paths as $paths) { - if (isset($paths['url']) && isset($paths['path'])) { - $paths['url'] = rtrim($paths['url'], '/'); - $paths['path'] = rtrim($paths['path'], '/'); - OC::$APPSROOTS[] = $paths; - } - } - } elseif (file_exists(OC::$SERVERROOT . '/apps')) { - OC::$APPSROOTS[] = ['path' => OC::$SERVERROOT . '/apps', 'url' => '/apps', 'writable' => true]; - } - - if (empty(OC::$APPSROOTS)) { - throw new \RuntimeException('apps directory not found! Please put the Nextcloud apps folder in the Nextcloud folder' - . '. You can also configure the location in the config.php file.'); - } - $paths = []; - foreach (OC::$APPSROOTS as $path) { - $paths[] = $path['path']; - if (!is_dir($path['path'])) { - throw new \RuntimeException(sprintf('App directory "%s" not found! Please put the Nextcloud apps folder in the' - . ' Nextcloud folder. You can also configure the location in the config.php file.', $path['path'])); - } - } - - // set the right include path - set_include_path( - implode(PATH_SEPARATOR, $paths) - ); - } - - public static function checkConfig(): void { - // Create config if it does not already exist - $configFilePath = self::$configDir . '/config.php'; - if (!file_exists($configFilePath)) { - @touch($configFilePath); - } - - // Check if config is writable - $configFileWritable = is_writable($configFilePath); - $configReadOnly = Server::get(IConfig::class)->getSystemValueBool('config_is_read_only'); - if (!$configFileWritable && !$configReadOnly - || !$configFileWritable && \OCP\Util::needUpgrade()) { - $urlGenerator = Server::get(IURLGenerator::class); - $l = Server::get(\OCP\L10N\IFactory::class)->get('lib'); - - if (self::$CLI) { - echo $l->t('Cannot write into "config" directory!') . "\n"; - echo $l->t('This can usually be fixed by giving the web server write access to the config directory.') . "\n"; - echo "\n"; - echo $l->t('But, if you prefer to keep config.php file read only, set the option "config_is_read_only" to true in it.') . "\n"; - echo $l->t('See %s', [ $urlGenerator->linkToDocs('admin-config') ]) . "\n"; - exit; - } else { - Server::get(ITemplateManager::class)->printErrorPage( - $l->t('Cannot write into "config" directory!'), - $l->t('This can usually be fixed by giving the web server write access to the config directory.') . ' ' - . $l->t('But, if you prefer to keep config.php file read only, set the option "config_is_read_only" to true in it.') . ' ' - . $l->t('See %s', [ $urlGenerator->linkToDocs('admin-config') ]), - 503 - ); - } - } - } - - public static function checkInstalled(\OC\SystemConfig $systemConfig): void { - if (defined('OC_CONSOLE')) { - return; - } - // Redirect to installer if not installed - if (!$systemConfig->getValue('installed', false) && OC::$SUBURI !== '/index.php' && OC::$SUBURI !== '/status.php') { - if (OC::$CLI) { - throw new Exception('Not installed'); - } else { - $url = OC::$WEBROOT . '/index.php'; - header('Location: ' . $url); - } - exit(); - } - } - - public static function checkMaintenanceMode(\OC\SystemConfig $systemConfig): void { - // Allow ajax update script to execute without being stopped - if (((bool)$systemConfig->getValue('maintenance', false)) && OC::$SUBURI != '/core/ajax/update.php') { - // send http status 503 - http_response_code(503); - header('X-Nextcloud-Maintenance-Mode: 1'); - header('Retry-After: 120'); - - // render error page - $template = Server::get(ITemplateManager::class)->getTemplate('', 'update.user', 'guest'); - \OCP\Util::addScript('core', 'maintenance'); - \OCP\Util::addScript('core', 'common'); - \OCP\Util::addStyle('core', 'guest'); - $template->printPage(); - die(); - } - } - - /** - * Prints the upgrade page - */ - private static function printUpgradePage(\OC\SystemConfig $systemConfig): void { - $cliUpgradeLink = $systemConfig->getValue('upgrade.cli-upgrade-link', ''); - $disableWebUpdater = $systemConfig->getValue('upgrade.disable-web', false); - $tooBig = false; - if (!$disableWebUpdater) { - $apps = Server::get(\OCP\App\IAppManager::class); - if ($apps->isEnabledForAnyone('user_ldap')) { - $qb = Server::get(\OCP\IDBConnection::class)->getQueryBuilder(); - - $result = $qb->select($qb->func()->count('*', 'user_count')) - ->from('ldap_user_mapping') - ->executeQuery(); - $row = $result->fetch(); - $result->closeCursor(); - - $tooBig = ($row['user_count'] > 50); - } - if (!$tooBig && $apps->isEnabledForAnyone('user_saml')) { - $qb = Server::get(\OCP\IDBConnection::class)->getQueryBuilder(); - - $result = $qb->select($qb->func()->count('*', 'user_count')) - ->from('user_saml_users') - ->executeQuery(); - $row = $result->fetch(); - $result->closeCursor(); - - $tooBig = ($row['user_count'] > 50); - } - if (!$tooBig) { - // count users - $totalUsers = Server::get(\OCP\IUserManager::class)->countUsersTotal(51); - $tooBig = ($totalUsers > 50); - } - } - $ignoreTooBigWarning = isset($_GET['IKnowThatThisIsABigInstanceAndTheUpdateRequestCouldRunIntoATimeoutAndHowToRestoreABackup']) - && $_GET['IKnowThatThisIsABigInstanceAndTheUpdateRequestCouldRunIntoATimeoutAndHowToRestoreABackup'] === 'IAmSuperSureToDoThis'; - - Util::addTranslations('core'); - Util::addScript('core', 'common'); - Util::addScript('core', 'main'); - Util::addScript('core', 'update'); - - $initialState = Server::get(IInitialStateService::class); - $serverVersion = Server::get(\OCP\ServerVersion::class); - if ($disableWebUpdater || ($tooBig && !$ignoreTooBigWarning)) { - // send http status 503 - http_response_code(503); - header('Retry-After: 120'); - - $urlGenerator = Server::get(IURLGenerator::class); - $initialState->provideInitialState('core', 'updaterView', 'adminCli'); - $initialState->provideInitialState('core', 'updateInfo', [ - 'cliUpgradeLink' => $cliUpgradeLink ?: $urlGenerator->linkToDocs('admin-cli-upgrade'), - 'productName' => self::getProductName(), - 'version' => $serverVersion->getVersionString(), - 'tooBig' => $tooBig, - ]); - - // render error page - Server::get(ITemplateManager::class) - ->getTemplate('', 'update', 'guest') - ->printPage(); - die(); - } - - // check whether this is a core update or apps update - $installedVersion = $systemConfig->getValue('version', '0.0.0'); - $currentVersion = implode('.', $serverVersion->getVersion()); - - // if not a core upgrade, then it's apps upgrade - $isAppsOnlyUpgrade = version_compare($currentVersion, $installedVersion, '='); - - $oldTheme = $systemConfig->getValue('theme'); - $systemConfig->setValue('theme', ''); - - /** @var \OC\App\AppManager $appManager */ - $appManager = Server::get(\OCP\App\IAppManager::class); - - // get third party apps - $ocVersion = $serverVersion->getVersion(); - $ocVersion = implode('.', $ocVersion); - $incompatibleApps = $appManager->getIncompatibleApps($ocVersion); - $incompatibleOverwrites = $systemConfig->getValue('app_install_overwrite', []); - $incompatibleShippedApps = []; - $incompatibleDisabledApps = []; - foreach ($incompatibleApps as $appInfo) { - if ($appManager->isShipped($appInfo['id'])) { - $incompatibleShippedApps[] = $appInfo['name'] . ' (' . $appInfo['id'] . ')'; - } - if (!in_array($appInfo['id'], $incompatibleOverwrites)) { - $incompatibleDisabledApps[] = $appInfo; - } - } - - if (!empty($incompatibleShippedApps)) { - $l = Server::get(\OCP\L10N\IFactory::class)->get('core'); - $hint = $l->t('Application %1$s is not present or has a non-compatible version with this server. Please check the apps directory.', [implode(', ', $incompatibleShippedApps)]); - throw new \OCP\HintException('Application ' . implode(', ', $incompatibleShippedApps) . ' is not present or has a non-compatible version with this server. Please check the apps directory.', $hint); - } - - $appConfig = Server::get(IAppConfig::class); - $appsToUpgrade = array_map(function ($app) use (&$appConfig) { - return [ - 'id' => $app['id'], - 'name' => $app['name'], - 'version' => $app['version'], - 'oldVersion' => $appConfig->getValueString($app['id'], 'installed_version'), - ]; - }, $appManager->getAppsNeedingUpgrade($ocVersion)); - - $params = [ - 'appsToUpgrade' => $appsToUpgrade, - 'incompatibleAppsList' => $incompatibleDisabledApps, - 'isAppsOnlyUpgrade' => $isAppsOnlyUpgrade, - 'oldTheme' => $oldTheme, - 'productName' => self::getProductName(), - 'version' => $serverVersion->getVersionString(), - ]; - - $initialState->provideInitialState('core', 'updaterView', 'admin'); - $initialState->provideInitialState('core', 'updateInfo', $params); - Server::get(ITemplateManager::class) - ->getTemplate('', 'update', 'guest') - ->printPage(); - } - - private static function getProductName(): string { - $productName = 'Nextcloud'; - try { - $defaults = new \OC_Defaults(); - $productName = $defaults->getName(); - } catch (Throwable $error) { - // ignore - } - return $productName; - } - - public static function initSession(): void { - $request = Server::get(IRequest::class); - - // TODO: Temporary disabled again to solve issues with CalDAV/CardDAV clients like DAVx5 that use cookies - // TODO: See https://github.com/nextcloud/server/issues/37277#issuecomment-1476366147 and the other comments - // TODO: for further information. - // $isDavRequest = strpos($request->getRequestUri(), '/remote.php/dav') === 0 || strpos($request->getRequestUri(), '/remote.php/webdav') === 0; - // if ($request->getHeader('Authorization') !== '' && is_null($request->getCookie('cookie_test')) && $isDavRequest && !isset($_COOKIE['nc_session_id'])) { - // setcookie('cookie_test', 'test', time() + 3600); - // // Do not initialize the session if a request is authenticated directly - // // unless there is a session cookie already sent along - // return; - // } - - if ($request->getServerProtocol() === 'https') { - ini_set('session.cookie_secure', 'true'); - } - - // prevents javascript from accessing php session cookies - ini_set('session.cookie_httponly', 'true'); - - // set the cookie path to the Nextcloud directory - $cookie_path = OC::$WEBROOT ? : '/'; - ini_set('session.cookie_path', $cookie_path); - - // set the cookie domain to the Nextcloud domain - $cookie_domain = self::$config->getValue('cookie_domain', ''); - if ($cookie_domain) { - ini_set('session.cookie_domain', $cookie_domain); - } - - // Do not initialize sessions for 'status.php' requests - // Monitoring endpoints can quickly flood session handlers - // and 'status.php' doesn't require sessions anyway - // We still need to run the ini_set above so that same-site cookies use the correct configuration. - if (str_ends_with($request->getScriptName(), '/status.php')) { - return; - } - - // Let the session name be changed in the initSession Hook - $sessionName = OC_Util::getInstanceId(); - - try { - $logger = null; - if (Server::get(\OC\SystemConfig::class)->getValue('installed', false)) { - $logger = logger('core'); - } - - // set the session name to the instance id - which is unique - $session = new \OC\Session\Internal( - $sessionName, - $logger, - ); - - $cryptoWrapper = Server::get(\OC\Session\CryptoWrapper::class); - $session = $cryptoWrapper->wrapSession($session); - self::$server->setSession($session); - - // if session can't be started break with http 500 error - } catch (Exception $e) { - Server::get(LoggerInterface::class)->error($e->getMessage(), ['app' => 'base','exception' => $e]); - //show the user a detailed error page - Server::get(ITemplateManager::class)->printExceptionErrorPage($e, 500); - die(); - } - - //try to set the session lifetime - $sessionLifeTime = self::getSessionLifeTime(); - - // session timeout - if ($session->exists('LAST_ACTIVITY') && (time() - $session->get('LAST_ACTIVITY') > $sessionLifeTime)) { - if (isset($_COOKIE[session_name()])) { - setcookie(session_name(), '', -1, self::$WEBROOT ? : '/'); - } - Server::get(IUserSession::class)->logout(); - } - - if (!self::hasSessionRelaxedExpiry()) { - $session->set('LAST_ACTIVITY', time()); - } - $session->close(); - } - - private static function getSessionLifeTime(): int { - return Server::get(IConfig::class)->getSystemValueInt('session_lifetime', 60 * 60 * 24); - } - - /** - * @return bool true if the session expiry should only be done by gc instead of an explicit timeout - */ - public static function hasSessionRelaxedExpiry(): bool { - return Server::get(IConfig::class)->getSystemValueBool('session_relaxed_expiry', false); - } - - /** - * Try to set some values to the required Nextcloud default - */ - public static function setRequiredIniValues(): void { - // Don't display errors and log them - @ini_set('display_errors', '0'); - @ini_set('log_errors', '1'); - - // Try to configure php to enable big file uploads. - // This doesn't work always depending on the webserver and php configuration. - // Let's try to overwrite some defaults if they are smaller than 1 hour - - if (intval(@ini_get('max_execution_time') ?: 0) < 3600) { - @ini_set('max_execution_time', strval(3600)); - } - - if (intval(@ini_get('max_input_time') ?: 0) < 3600) { - @ini_set('max_input_time', strval(3600)); - } - - // Try to set the maximum execution time to the largest time limit we have - if (strpos(@ini_get('disable_functions'), 'set_time_limit') === false) { - @set_time_limit(max(intval(@ini_get('max_execution_time')), intval(@ini_get('max_input_time')))); - } - - @ini_set('default_charset', 'UTF-8'); - @ini_set('gd.jpeg_ignore_warning', '1'); - } - - /** - * Send the same site cookies - */ - private static function sendSameSiteCookies(): void { - $cookieParams = session_get_cookie_params(); - $secureCookie = ($cookieParams['secure'] === true) ? 'secure; ' : ''; - $policies = [ - 'lax', - 'strict', - ]; - - // Append __Host to the cookie if it meets the requirements - $cookiePrefix = ''; - if ($cookieParams['secure'] === true && $cookieParams['path'] === '/') { - $cookiePrefix = '__Host-'; - } - - foreach ($policies as $policy) { - header( - sprintf( - 'Set-Cookie: %snc_sameSiteCookie%s=true; path=%s; httponly;' . $secureCookie . 'expires=Fri, 31-Dec-2100 23:59:59 GMT; SameSite=%s', - $cookiePrefix, - $policy, - $cookieParams['path'], - $policy - ), - false - ); - } - } - - /** - * Same Site cookie to further mitigate CSRF attacks. This cookie has to - * be set in every request if cookies are sent to add a second level of - * defense against CSRF. - * - * If the cookie is not sent this will set the cookie and reload the page. - * We use an additional cookie since we want to protect logout CSRF and - * also we can't directly interfere with PHP's session mechanism. - */ - private static function performSameSiteCookieProtection(IConfig $config): void { - $request = Server::get(IRequest::class); - - // Some user agents are notorious and don't really properly follow HTTP - // specifications. For those, have an automated opt-out. Since the protection - // for remote.php is applied in base.php as starting point we need to opt out - // here. - $incompatibleUserAgents = $config->getSystemValue('csrf.optout'); - - // Fallback, if csrf.optout is unset - if (!is_array($incompatibleUserAgents)) { - $incompatibleUserAgents = [ - // OS X Finder - '/^WebDAVFS/', - // Windows webdav drive - '/^Microsoft-WebDAV-MiniRedir/', - ]; - } - - if ($request->isUserAgent($incompatibleUserAgents)) { - return; - } - - if (count($_COOKIE) > 0) { - $requestUri = $request->getScriptName(); - $processingScript = explode('/', $requestUri); - $processingScript = $processingScript[count($processingScript) - 1]; - - if ($processingScript === 'index.php' // index.php routes are handled in the middleware - || $processingScript === 'cron.php' // and cron.php does not need any authentication at all - || $processingScript === 'public.php' // For public.php, auth for password protected shares is done in the PublicAuth plugin - ) { - return; - } - - // All other endpoints require the lax and the strict cookie - if (!$request->passesStrictCookieCheck()) { - logger('core')->warning('Request does not pass strict cookie check'); - self::sendSameSiteCookies(); - // Debug mode gets access to the resources without strict cookie - // due to the fact that the SabreDAV browser also lives there. - if (!$config->getSystemValueBool('debug', false)) { - http_response_code(\OCP\AppFramework\Http::STATUS_PRECONDITION_FAILED); - header('Content-Type: application/json'); - echo json_encode(['error' => 'Strict Cookie has not been found in request']); - exit(); - } - } - } elseif (!isset($_COOKIE['nc_sameSiteCookielax']) || !isset($_COOKIE['nc_sameSiteCookiestrict'])) { - self::sendSameSiteCookies(); - } - } - - /** - * This function adds some security related headers to all requests served via base.php - * The implementation of this function has to happen here to ensure that all third-party - * components (e.g. SabreDAV) also benefit from this headers. - */ - private static function addSecurityHeaders(): void { - /** - * FIXME: Content Security Policy for legacy components. This - * can be removed once \OCP\AppFramework\Http\Response from the AppFramework - * is used everywhere. - * @see \OCP\AppFramework\Http\Response::getHeaders - */ - $policy = 'default-src \'self\'; ' - . 'script-src \'self\' \'nonce-' . Server::get(ContentSecurityPolicyNonceManager::class)->getNonce() . '\'; ' - . 'style-src \'self\' \'unsafe-inline\'; ' - . 'frame-src *; ' - . 'img-src * data: blob:; ' - . 'font-src \'self\' data:; ' - . 'media-src *; ' - . 'connect-src *; ' - . 'object-src \'none\'; ' - . 'base-uri \'self\'; '; - header('Content-Security-Policy:' . $policy); - - // Send fallback headers for installations that don't have the possibility to send - // custom headers on the webserver side - if (getenv('modHeadersAvailable') !== 'true') { - header('Referrer-Policy: no-referrer'); // https://www.w3.org/TR/referrer-policy/ - header('X-Content-Type-Options: nosniff'); // Disable sniffing the content type for IE - header('X-Frame-Options: SAMEORIGIN'); // Disallow iFraming from other domains - header('X-Permitted-Cross-Domain-Policies: none'); // https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html - header('X-Robots-Tag: noindex, nofollow'); // https://developers.google.com/webmasters/control-crawl-index/docs/robots_meta_tag - } - } - - public static function init(): void { - // First handle PHP configuration and copy auth headers to the expected - // $_SERVER variable before doing anything Server object related - self::setRequiredIniValues(); - self::handleAuthHeaders(); - - // prevent any XML processing from loading external entities - libxml_set_external_entity_loader(static function () { - return null; - }); - - // Set default timezone before the Server object is booted - if (!date_default_timezone_set('UTC')) { - throw new \RuntimeException('Could not set timezone to UTC'); - } - - // calculate the root directories - OC::$SERVERROOT = str_replace('\\', '/', substr(__DIR__, 0, -4)); - - // register autoloader - $loaderStart = microtime(true); - - self::$CLI = (php_sapi_name() == 'cli'); - - // Add default composer PSR-4 autoloader, ensure apcu to be disabled - self::$composerAutoloader = require_once OC::$SERVERROOT . '/lib/composer/autoload.php'; - self::$composerAutoloader->setApcuPrefix(null); - - - try { - self::initPaths(); - // setup 3rdparty autoloader - $vendorAutoLoad = OC::$SERVERROOT . '/3rdparty/autoload.php'; - if (!file_exists($vendorAutoLoad)) { - throw new \RuntimeException('Composer autoloader not found, unable to continue. Check the folder "3rdparty". Running "git submodule update --init" will initialize the git submodule that handles the subfolder "3rdparty".'); - } - require_once $vendorAutoLoad; - } catch (\RuntimeException $e) { - if (!self::$CLI) { - http_response_code(503); - } - // we can't use the template error page here, because this needs the - // DI container which isn't available yet - print($e->getMessage()); - exit(); - } - $loaderEnd = microtime(true); - - // Enable lazy loading if activated - \OC\AppFramework\Utility\SimpleContainer::$useLazyObjects = (bool)self::$config->getValue('enable_lazy_objects', true); - - // setup the basic server - self::$server = new \OC\Server(\OC::$WEBROOT, self::$config); - self::$server->boot(); - - try { - $profiler = new BuiltInProfiler( - Server::get(IConfig::class), - Server::get(IRequest::class), - ); - $profiler->start(); - } catch (\Throwable $e) { - logger('core')->error('Failed to start profiler: ' . $e->getMessage(), ['app' => 'base']); - } - - if (self::$CLI && in_array('--' . \OCP\Console\ReservedOptions::DEBUG_LOG, $_SERVER['argv'])) { - \OC\Core\Listener\BeforeMessageLoggedEventListener::setup(); - } - - $eventLogger = Server::get(\OCP\Diagnostics\IEventLogger::class); - $eventLogger->log('autoloader', 'Autoloader', $loaderStart, $loaderEnd); - $eventLogger->start('boot', 'Initialize'); - - // Override php.ini and log everything if we're troubleshooting - if (self::$config->getValue('loglevel') === ILogger::DEBUG) { - error_reporting(E_ALL); - } - - // initialize intl fallback if necessary - OC_Util::isSetLocaleWorking(); - - $config = Server::get(IConfig::class); - if (!defined('PHPUNIT_RUN')) { - $errorHandler = new OC\Log\ErrorHandler( - Server::get(\Psr\Log\LoggerInterface::class), - ); - $exceptionHandler = [$errorHandler, 'onException']; - if ($config->getSystemValueBool('debug', false)) { - set_error_handler([$errorHandler, 'onAll'], E_ALL); - if (\OC::$CLI) { - $exceptionHandler = [Server::get(ITemplateManager::class), 'printExceptionErrorPage']; - } - } else { - set_error_handler([$errorHandler, 'onError']); - } - register_shutdown_function([$errorHandler, 'onShutdown']); - set_exception_handler($exceptionHandler); - } - - /** @var \OC\AppFramework\Bootstrap\Coordinator $bootstrapCoordinator */ - $bootstrapCoordinator = Server::get(\OC\AppFramework\Bootstrap\Coordinator::class); - $bootstrapCoordinator->runInitialRegistration(); - - $eventLogger->start('init_session', 'Initialize session'); - - // Check for PHP SimpleXML extension earlier since we need it before our other checks and want to provide a useful hint for web users - // see https://github.com/nextcloud/server/pull/2619 - if (!function_exists('simplexml_load_file')) { - throw new \OCP\HintException('The PHP SimpleXML/PHP-XML extension is not installed.', 'Install the extension or make sure it is enabled.'); - } - - $systemConfig = Server::get(\OC\SystemConfig::class); - $appManager = Server::get(\OCP\App\IAppManager::class); - if ($systemConfig->getValue('installed', false)) { - $appManager->loadApps(['session']); - } - if (!self::$CLI) { - self::initSession(); - } - $eventLogger->end('init_session'); - self::checkConfig(); - self::checkInstalled($systemConfig); - - if (!self::$CLI) { - self::addSecurityHeaders(); - self::performSameSiteCookieProtection($config); - } - - if (!defined('OC_CONSOLE')) { - $eventLogger->start('check_server', 'Run a few configuration checks'); - $errors = OC_Util::checkServer($systemConfig); - if (count($errors) > 0) { - if (!self::$CLI) { - http_response_code(503); - Util::addStyle('guest'); - try { - Server::get(ITemplateManager::class)->printGuestPage('', 'error', ['errors' => $errors]); - exit; - } catch (\Exception $e) { - // In case any error happens when showing the error page, we simply fall back to posting the text. - // This might be the case when e.g. the data directory is broken and we can not load/write SCSS to/from it. - } - } - - // Convert l10n string into regular string for usage in database - $staticErrors = []; - foreach ($errors as $error) { - echo $error['error'] . "\n"; - echo $error['hint'] . "\n\n"; - $staticErrors[] = [ - 'error' => (string)$error['error'], - 'hint' => (string)$error['hint'], - ]; - } - - try { - $config->setAppValue('core', 'cronErrors', json_encode($staticErrors)); - } catch (\Exception $e) { - echo('Writing to database failed'); - } - exit(1); - } elseif (self::$CLI && $config->getSystemValueBool('installed', false)) { - $config->deleteAppValue('core', 'cronErrors'); - } - $eventLogger->end('check_server'); - } - - // User and Groups - if (!$systemConfig->getValue('installed', false)) { - Server::get(ISession::class)->set('user_id', ''); - } - - $eventLogger->start('setup_backends', 'Setup group and user backends'); - Server::get(\OCP\IUserManager::class)->registerBackend(new \OC\User\Database()); - Server::get(\OCP\IGroupManager::class)->addBackend(new \OC\Group\Database()); - - // Subscribe to the hook - \OCP\Util::connectHook( - '\OCA\Files_Sharing\API\Server2Server', - 'preLoginNameUsedAsUserName', - '\OC\User\Database', - 'preLoginNameUsedAsUserName' - ); - - //setup extra user backends - if (!\OCP\Util::needUpgrade()) { - OC_User::setupBackends(); - } else { - // Run upgrades in incognito mode - OC_User::setIncognitoMode(true); - } - $eventLogger->end('setup_backends'); - - self::registerCleanupHooks($systemConfig); - self::registerShareHooks($systemConfig); - self::registerEncryptionWrapperAndHooks(); - self::registerAccountHooks(); - self::registerResourceCollectionHooks(); - self::registerFileReferenceEventListener(); - self::registerRenderReferenceEventListener(); - self::registerAppRestrictionsHooks(); - - // Make sure that the application class is not loaded before the database is setup - if ($systemConfig->getValue('installed', false)) { - $appManager->loadApp('settings'); - } - - //make sure temporary files are cleaned up - $tmpManager = Server::get(\OCP\ITempManager::class); - register_shutdown_function([$tmpManager, 'clean']); - $lockProvider = Server::get(\OCP\Lock\ILockingProvider::class); - register_shutdown_function([$lockProvider, 'releaseAll']); - - // Check whether the sample configuration has been copied - if ($systemConfig->getValue('copied_sample_config', false)) { - $l = Server::get(\OCP\L10N\IFactory::class)->get('lib'); - Server::get(ITemplateManager::class)->printErrorPage( - $l->t('Sample configuration detected'), - $l->t('It has been detected that the sample configuration has been copied. This can break your installation and is unsupported. Please read the documentation before performing changes on config.php'), - 503 - ); - return; - } - - $request = Server::get(IRequest::class); - $host = $request->getInsecureServerHost(); - /** - * if the host passed in headers isn't trusted - * FIXME: Should not be in here at all :see_no_evil: - */ - if (!OC::$CLI - && !Server::get(\OC\Security\TrustedDomainHelper::class)->isTrustedDomain($host) - && $config->getSystemValueBool('installed', false) - ) { - // Allow access to CSS resources - $isScssRequest = false; - if (strpos($request->getPathInfo() ?: '', '/css/') === 0) { - $isScssRequest = true; - } - - if (substr($request->getRequestUri(), -11) === '/status.php') { - http_response_code(400); - header('Content-Type: application/json'); - echo '{"error": "Trusted domain error.", "code": 15}'; - exit(); - } - - if (!$isScssRequest) { - http_response_code(400); - Server::get(LoggerInterface::class)->info( - 'Trusted domain error. "{remoteAddress}" tried to access using "{host}" as host.', - [ - 'app' => 'core', - 'remoteAddress' => $request->getRemoteAddress(), - 'host' => $host, - ] - ); - - $tmpl = Server::get(ITemplateManager::class)->getTemplate('core', 'untrustedDomain', 'guest'); - $tmpl->assign('docUrl', Server::get(IURLGenerator::class)->linkToDocs('admin-trusted-domains')); - $tmpl->printPage(); - - exit(); - } - } - $eventLogger->end('boot'); - $eventLogger->log('init', 'OC::init', $loaderStart, microtime(true)); - $eventLogger->start('runtime', 'Runtime'); - $eventLogger->start('request', 'Full request after boot'); - register_shutdown_function(function () use ($eventLogger) { - $eventLogger->end('request'); - }); - - register_shutdown_function(function () use ($config) { - $memoryPeak = memory_get_peak_usage(); - $debugModeEnabled = $config->getSystemValueBool('debug', false); - $memoryLimit = null; - - if (!$debugModeEnabled) { - // Use the memory helper to get the real memory limit in bytes if debug mode is disabled - try { - $memoryInfo = new \OC\MemoryInfo(); - $memoryLimit = $memoryInfo->getMemoryLimit(); - } catch (Throwable $e) { - // Ignore any errors and fall back to hardcoded thresholds - } - } - - // Check if a memory limit is configured and can be retrieved and determine log level if debug mode is disabled - if (!$debugModeEnabled && $memoryLimit !== null && $memoryLimit !== -1) { - $logLevel = match (true) { - $memoryPeak > $memoryLimit * 0.9 => ILogger::FATAL, - $memoryPeak > $memoryLimit * 0.75 => ILogger::ERROR, - $memoryPeak > $memoryLimit * 0.5 => ILogger::WARN, - default => null, - }; - - $memoryLimitIni = @ini_get('memory_limit'); - $message = 'Request used ' . Util::humanFileSize($memoryPeak) . ' of memory. Memory limit: ' . ($memoryLimitIni ?: 'unknown'); - } else { - // Fall back to hardcoded thresholds if memory_limit cannot be determined or if debug mode is enabled - $logLevel = match (true) { - $memoryPeak > 500_000_000 => ILogger::FATAL, - $memoryPeak > 400_000_000 => ILogger::ERROR, - $memoryPeak > 300_000_000 => ILogger::WARN, - default => null, - }; - - $message = 'Request used more than 300 MB of RAM: ' . Util::humanFileSize($memoryPeak); - } - - // Log the message - if ($logLevel !== null) { - $logger = Server::get(LoggerInterface::class); - $logger->log($logLevel, $message, ['app' => 'core']); - } - }); - } - - /** - * register hooks for the cleanup of cache and bruteforce protection - */ - public static function registerCleanupHooks(\OC\SystemConfig $systemConfig): void { - //don't try to do this before we are properly setup - if ($systemConfig->getValue('installed', false) && !\OCP\Util::needUpgrade()) { - // NOTE: This will be replaced to use OCP - $userSession = Server::get(\OC\User\Session::class); - $userSession->listen('\OC\User', 'postLogin', function () use ($userSession) { - if (!defined('PHPUNIT_RUN') && $userSession->isLoggedIn()) { - // reset brute force delay for this IP address and username - $uid = $userSession->getUser()->getUID(); - $request = Server::get(IRequest::class); - $throttler = Server::get(IThrottler::class); - $throttler->resetDelay($request->getRemoteAddress(), 'login', ['user' => $uid]); - } - - try { - $cache = new \OC\Cache\File(); - $cache->gc(); - } catch (\OC\ServerNotAvailableException $e) { - // not a GC exception, pass it on - throw $e; - } catch (\OC\ForbiddenException $e) { - // filesystem blocked for this request, ignore - } catch (\Exception $e) { - // a GC exception should not prevent users from using OC, - // so log the exception - Server::get(LoggerInterface::class)->warning('Exception when running cache gc.', [ - 'app' => 'core', - 'exception' => $e, - ]); - } - }); - } - } - - private static function registerEncryptionWrapperAndHooks(): void { - /** @var \OC\Encryption\Manager */ - $manager = Server::get(\OCP\Encryption\IManager::class); - Server::get(IEventDispatcher::class)->addListener( - BeforeFileSystemSetupEvent::class, - $manager->setupStorage(...), - ); - - $enabled = $manager->isEnabled(); - if ($enabled) { - \OC\Encryption\EncryptionEventListener::register(Server::get(IEventDispatcher::class)); - } - } - - private static function registerAccountHooks(): void { - /** @var IEventDispatcher $dispatcher */ - $dispatcher = Server::get(IEventDispatcher::class); - $dispatcher->addServiceListener(UserChangedEvent::class, \OC\Accounts\Hooks::class); - } - - private static function registerAppRestrictionsHooks(): void { - /** @var \OC\Group\Manager $groupManager */ - $groupManager = Server::get(\OCP\IGroupManager::class); - $groupManager->listen('\OC\Group', 'postDelete', function (\OCP\IGroup $group) { - $appManager = Server::get(\OCP\App\IAppManager::class); - $apps = $appManager->getEnabledAppsForGroup($group); - foreach ($apps as $appId) { - $restrictions = $appManager->getAppRestriction($appId); - if (empty($restrictions)) { - continue; - } - $key = array_search($group->getGID(), $restrictions, true); - unset($restrictions[$key]); - $restrictions = array_values($restrictions); - if (empty($restrictions)) { - $appManager->disableApp($appId); - } else { - $appManager->enableAppForGroups($appId, $restrictions); - } - } - }); - } - - private static function registerResourceCollectionHooks(): void { - \OC\Collaboration\Resources\Listener::register(Server::get(IEventDispatcher::class)); - } - - private static function registerFileReferenceEventListener(): void { - \OC\Collaboration\Reference\File\FileReferenceEventListener::register(Server::get(IEventDispatcher::class)); - } - - private static function registerRenderReferenceEventListener() { - \OC\Collaboration\Reference\RenderReferenceEventListener::register(Server::get(IEventDispatcher::class)); - } - - /** - * register hooks for sharing - */ - public static function registerShareHooks(\OC\SystemConfig $systemConfig): void { - if ($systemConfig->getValue('installed')) { - - $dispatcher = Server::get(IEventDispatcher::class); - $dispatcher->addServiceListener(UserRemovedEvent::class, UserRemovedListener::class); - $dispatcher->addServiceListener(GroupDeletedEvent::class, GroupDeletedListener::class); - $dispatcher->addServiceListener(UserDeletedEvent::class, UserDeletedListener::class); - } - } - - /** - * Handle the request - */ - public static function handleRequest(): void { - Server::get(\OCP\Diagnostics\IEventLogger::class)->start('handle_request', 'Handle request'); - $systemConfig = Server::get(\OC\SystemConfig::class); - - // Check if Nextcloud is installed or in maintenance (update) mode - if (!$systemConfig->getValue('installed', false)) { - Server::get(ISession::class)->clear(); - $controller = Server::get(\OC\Core\Controller\SetupController::class); - $controller->run($_POST); - exit(); - } - - $request = Server::get(IRequest::class); - $request->throwDecodingExceptionIfAny(); - $requestPath = $request->getRawPathInfo(); - if ($requestPath === '/heartbeat') { - return; - } - if (substr($requestPath, -3) !== '.js') { // we need these files during the upgrade - self::checkMaintenanceMode($systemConfig); - - if (\OCP\Util::needUpgrade()) { - if (function_exists('opcache_reset')) { - opcache_reset(); - } - if (!((bool)$systemConfig->getValue('maintenance', false))) { - self::printUpgradePage($systemConfig); - exit(); - } - } - } - - $appManager = Server::get(\OCP\App\IAppManager::class); - - // Always load authentication apps - $appManager->loadApps(['authentication']); - $appManager->loadApps(['extended_authentication']); - - // Load minimum set of apps - if (!\OCP\Util::needUpgrade() - && !((bool)$systemConfig->getValue('maintenance', false))) { - // For logged-in users: Load everything - if (Server::get(IUserSession::class)->isLoggedIn()) { - $appManager->loadApps(); - } else { - // For guests: Load only filesystem and logging - $appManager->loadApps(['filesystem', 'logging']); - - // Don't try to login when a client is trying to get a OAuth token. - // OAuth needs to support basic auth too, so the login is not valid - // inside Nextcloud and the Login exception would ruin it. - if ($request->getRawPathInfo() !== '/apps/oauth2/api/v1/token') { - try { - self::handleLogin($request); - } catch (DisabledUserException $e) { - // Disabled users would not be seen as logged in and - // trying to log them in would fail, so the login - // exception is ignored for the themed stylesheets and - // images. - if ($request->getRawPathInfo() !== '/apps/theming/theme/default.css' - && $request->getRawPathInfo() !== '/apps/theming/theme/light.css' - && $request->getRawPathInfo() !== '/apps/theming/theme/dark.css' - && $request->getRawPathInfo() !== '/apps/theming/theme/light-highcontrast.css' - && $request->getRawPathInfo() !== '/apps/theming/theme/dark-highcontrast.css' - && $request->getRawPathInfo() !== '/apps/theming/theme/opendyslexic.css' - && $request->getRawPathInfo() !== '/apps/theming/image/background' - && $request->getRawPathInfo() !== '/apps/theming/image/logo' - && $request->getRawPathInfo() !== '/apps/theming/image/logoheader' - && !str_starts_with($request->getRawPathInfo(), '/apps/theming/favicon') - && !str_starts_with($request->getRawPathInfo(), '/apps/theming/icon')) { - throw $e; - } - } - } - } - } - - if (!self::$CLI) { - try { - if (!\OCP\Util::needUpgrade()) { - $appManager->loadApps(['filesystem', 'logging']); - $appManager->loadApps(); - } - Server::get(\OC\Route\Router::class)->match($request->getRawPathInfo()); - return; - } catch (Symfony\Component\Routing\Exception\ResourceNotFoundException $e) { - //header('HTTP/1.0 404 Not Found'); - } catch (Symfony\Component\Routing\Exception\MethodNotAllowedException $e) { - http_response_code(405); - return; - } - } - - // Handle WebDAV - if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'PROPFIND') { - // not allowed any more to prevent people - // mounting this root directly. - // Users need to mount remote.php/webdav instead. - http_response_code(405); - return; - } - - // Handle requests for JSON or XML - $acceptHeader = $request->getHeader('Accept'); - if (in_array($acceptHeader, ['application/json', 'application/xml'], true)) { - http_response_code(404); - return; - } - - // Handle resources that can't be found - // This prevents browsers from redirecting to the default page and then - // attempting to parse HTML as CSS and similar. - $destinationHeader = $request->getHeader('Sec-Fetch-Dest'); - if (in_array($destinationHeader, ['font', 'script', 'style'])) { - http_response_code(404); - return; - } - - // Redirect to the default app or login only as an entry point - if ($requestPath === '') { - // Someone is logged in - $userSession = Server::get(IUserSession::class); - if ($userSession->isLoggedIn()) { - header('X-User-Id: ' . $userSession->getUser()?->getUID()); - header('Location: ' . Server::get(IURLGenerator::class)->linkToDefaultPageUrl()); - } else { - // Not handled and not logged in - header('Location: ' . Server::get(IURLGenerator::class)->linkToRouteAbsolute('core.login.showLoginForm')); - } - return; - } - - try { - Server::get(\OC\Route\Router::class)->match('/error/404'); - } catch (\Exception $e) { - if (!$e instanceof MethodNotAllowedException) { - logger('core')->emergency($e->getMessage(), ['exception' => $e]); - } - $l = Server::get(\OCP\L10N\IFactory::class)->get('lib'); - Server::get(ITemplateManager::class)->printErrorPage( - '404', - $l->t('The page could not be found on the server.'), - 404 - ); - } - } - - /** - * Check login: apache auth, auth token, basic auth - */ - public static function handleLogin(OCP\IRequest $request): bool { - if ($request->getHeader('X-Nextcloud-Federation')) { - return false; - } - $userSession = Server::get(\OC\User\Session::class); - if (OC_User::handleApacheAuth()) { - return true; - } - if (self::tryAppAPILogin($request)) { - return true; - } - if ($userSession->tryTokenLogin($request)) { - return true; - } - if (isset($_COOKIE['nc_username']) - && isset($_COOKIE['nc_token']) - && isset($_COOKIE['nc_session_id']) - && $userSession->loginWithCookie($_COOKIE['nc_username'], $_COOKIE['nc_token'], $_COOKIE['nc_session_id'])) { - return true; - } - if ($userSession->tryBasicAuthLogin($request, Server::get(IThrottler::class))) { - return true; - } - return false; - } - - protected static function handleAuthHeaders(): void { - //copy http auth headers for apache+php-fcgid work around - if (isset($_SERVER['HTTP_XAUTHORIZATION']) && !isset($_SERVER['HTTP_AUTHORIZATION'])) { - $_SERVER['HTTP_AUTHORIZATION'] = $_SERVER['HTTP_XAUTHORIZATION']; - } - - // Extract PHP_AUTH_USER/PHP_AUTH_PW from other headers if necessary. - $vars = [ - 'HTTP_AUTHORIZATION', // apache+php-cgi work around - 'REDIRECT_HTTP_AUTHORIZATION', // apache+php-cgi alternative - ]; - foreach ($vars as $var) { - if (isset($_SERVER[$var]) && is_string($_SERVER[$var]) && preg_match('/Basic\s+(.*)$/i', $_SERVER[$var], $matches)) { - $credentials = explode(':', base64_decode($matches[1]), 2); - if (count($credentials) === 2) { - $_SERVER['PHP_AUTH_USER'] = $credentials[0]; - $_SERVER['PHP_AUTH_PW'] = $credentials[1]; - break; - } - } - } - } - - protected static function tryAppAPILogin(OCP\IRequest $request): bool { - if (!$request->getHeader('AUTHORIZATION-APP-API')) { - return false; - } - $appManager = Server::get(OCP\App\IAppManager::class); - if (!$appManager->isEnabledForAnyone('app_api')) { - return false; - } - try { - $appAPIService = Server::get(OCA\AppAPI\Service\AppAPIService::class); - return $appAPIService->validateExAppRequestToNC($request); - } catch (\Psr\Container\NotFoundExceptionInterface|\Psr\Container\ContainerExceptionInterface $e) { - return false; - } - } -} +require_once __DIR__ . '/OC.php'; -OC::init(); +\OC::boot(); +\OC::init(); diff --git a/lib/private/Accounts/AccountManager.php b/lib/private/Accounts/AccountManager.php index 373a697a327a5..102e6a8b9cb34 100644 --- a/lib/private/Accounts/AccountManager.php +++ b/lib/private/Accounts/AccountManager.php @@ -368,7 +368,7 @@ protected function addMissingDefaultValues(array $userData, array $defaultUserDa } protected function updateVerificationStatus(IAccount $updatedAccount, array $oldData): void { - static $propertiesVerifiableByLookupServer = [ + $propertiesVerifiableByLookupServer = [ self::PROPERTY_TWITTER, self::PROPERTY_FEDIVERSE, self::PROPERTY_WEBSITE, diff --git a/lib/private/App/AppManager.php b/lib/private/App/AppManager.php index 6eddb2b2c41e6..9bf5909cce99b 100644 --- a/lib/private/App/AppManager.php +++ b/lib/private/App/AppManager.php @@ -54,6 +54,9 @@ class AppManager implements IAppManager { /** @var string[] $appId => $enabled */ private array $enabledAppsCache = []; + /** @var array $appId => approot information */ + private array $appsDirCache = []; + /** @var string[]|null */ private ?array $shippedApps = null; @@ -743,11 +746,9 @@ public function findAppInDirectories(string $appId, bool $ignoreCache = false) { if ($sanitizedAppId !== $appId) { return false; } - // FIXME replace by a property or a cache - static $app_dir = []; - if (isset($app_dir[$appId]) && !$ignoreCache) { - return $app_dir[$appId]; + if (isset($this->appsDirCache[$appId]) && !$ignoreCache) { + return $this->appsDirCache[$appId]; } $possibleApps = []; @@ -761,7 +762,7 @@ public function findAppInDirectories(string $appId, bool $ignoreCache = false) { return false; } elseif (count($possibleApps) === 1) { $dir = array_shift($possibleApps); - $app_dir[$appId] = $dir; + $this->appsDirCache[$appId] = $dir; return $dir; } else { $versionToLoad = []; @@ -778,7 +779,7 @@ public function findAppInDirectories(string $appId, bool $ignoreCache = false) { if (!isset($versionToLoad['dir'])) { return false; } - $app_dir[$appId] = $versionToLoad['dir']; + $this->appsDirCache[$appId] = $versionToLoad['dir']; return $versionToLoad['dir']; } } diff --git a/lib/private/App/PlatformRepository.php b/lib/private/App/PlatformRepository.php index faed8b07feb50..1a1e27783a1dc 100644 --- a/lib/private/App/PlatformRepository.php +++ b/lib/private/App/PlatformRepository.php @@ -127,7 +127,7 @@ public function findLibrary(string $name): ?string { return null; } - private static string $modifierRegex = '[._-]?(?:(stable|beta|b|RC|alpha|a|patch|pl|p)(?:[.-]?(\d+))?)?([.-]?dev)?'; + private const string MODIFIER_REGEX = '[._-]?(?:(stable|beta|b|RC|alpha|a|patch|pl|p)(?:[.-]?(\d+))?)?([.-]?dev)?'; /** * Normalizes a version string to be able to perform comparisons on it @@ -154,16 +154,16 @@ public function normalizeVersion(string $version, ?string $fullVersion = null): return 'dev-' . substr($version, 4); } // match classical versioning - if (preg_match('{^v?(\d{1,3})(\.\d+)?(\.\d+)?(\.\d+)?' . self::$modifierRegex . '$}i', $version, $matches)) { + if (preg_match('{^v?(\d{1,3})(\.\d+)?(\.\d+)?(\.\d+)?' . self::MODIFIER_REGEX . '$}i', $version, $matches)) { $version = $matches[1] . (!empty($matches[2]) ? $matches[2] : '.0') . (!empty($matches[3]) ? $matches[3] : '.0') . (!empty($matches[4]) ? $matches[4] : '.0'); $index = 5; - } elseif (preg_match('{^v?(\d{4}(?:[.:-]?\d{2}){1,6}(?:[.:-]?\d{1,3})?)' . self::$modifierRegex . '$}i', $version, $matches)) { // match date-based versioning + } elseif (preg_match('{^v?(\d{4}(?:[.:-]?\d{2}){1,6}(?:[.:-]?\d{1,3})?)' . self::MODIFIER_REGEX . '$}i', $version, $matches)) { // match date-based versioning $version = preg_replace('{\D}', '-', $matches[1]); $index = 2; - } elseif (preg_match('{^v?(\d{4,})(\.\d+)?(\.\d+)?(\.\d+)?' . self::$modifierRegex . '$}i', $version, $matches)) { + } elseif (preg_match('{^v?(\d{4,})(\.\d+)?(\.\d+)?(\.\d+)?' . self::MODIFIER_REGEX . '$}i', $version, $matches)) { $version = $matches[1] . (!empty($matches[2]) ? $matches[2] : '.0') . (!empty($matches[3]) ? $matches[3] : '.0') diff --git a/lib/private/Files/Cache/Storage.php b/lib/private/Files/Cache/Storage.php index 49b24ec8b34f9..5417095f44bee 100644 --- a/lib/private/Files/Cache/Storage.php +++ b/lib/private/Files/Cache/Storage.php @@ -28,18 +28,12 @@ * @package OC\Files\Cache */ class Storage { - private static ?StorageGlobal $globalCache = null; - private string $storageId; private int $numericId; public static function getGlobalCache(): StorageGlobal { - if (is_null(self::$globalCache)) { - self::$globalCache = new StorageGlobal(Server::get(IDBConnection::class)); - } - - return self::$globalCache; + return Server::get(StorageGlobal::class); } /** diff --git a/lib/private/Files/SetupManager.php b/lib/private/Files/SetupManager.php index 5cd32fdfe0e08..ce6e479f6522f 100644 --- a/lib/private/Files/SetupManager.php +++ b/lib/private/Files/SetupManager.php @@ -95,6 +95,8 @@ class SetupManager implements ISetupManager { private const SETUP_WITH_CHILDREN = 1; private const SETUP_WITHOUT_CHILDREN = 0; + private bool $updatingProviders = false; + public function __construct( private IEventLogger $eventLogger, private MountProviderCollection $mountProviderCollection, @@ -245,11 +247,10 @@ private function updateNonAuthoritativeProviders(IUser $user): void { } // prevent recursion loop from when getting mounts from providers ends up setting up the filesystem - static $updatingProviders = false; - if ($updatingProviders) { + if ($this->updatingProviders) { return; } - $updatingProviders = true; + $this->updatingProviders = true; $providers = $this->mountProviderCollection->getProviders(); $nonAuthoritativeProviders = array_filter( @@ -265,7 +266,7 @@ private function updateNonAuthoritativeProviders(IUser $user): void { $this->userMountCache->registerMounts($user, $mount, $providerNames); $this->usersMountsUpdated[$user->getUID()] = true; - $updatingProviders = false; + $this->updatingProviders = false; } #[Override] diff --git a/lib/private/Files/Storage/Common.php b/lib/private/Files/Storage/Common.php index 696232da64fa0..bb6848bc4fb11 100644 --- a/lib/private/Files/Storage/Common.php +++ b/lib/private/Files/Storage/Common.php @@ -74,6 +74,8 @@ abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage, private ?LoggerInterface $logger = null; private ?IFilenameValidator $filenameValidator = null; + private ?CacheDependencies $cacheDependencies = null; + public function __construct(array $parameters) { } @@ -304,11 +306,10 @@ public function hasUpdated(string $path, int $time): bool { } protected function getCacheDependencies(): CacheDependencies { - static $dependencies = null; - if (!$dependencies) { - $dependencies = Server::get(CacheDependencies::class); + if ($this->cacheDependencies === null) { + $this->cacheDependencies = Server::get(CacheDependencies::class); } - return $dependencies; + return $this->cacheDependencies; } public function getCache(string $path = '', ?IStorage $storage = null): ICache { diff --git a/lib/private/Memcache/Redis.php b/lib/private/Memcache/Redis.php index 1611d428f3016..bc0a2a47d3aa2 100644 --- a/lib/private/Memcache/Redis.php +++ b/lib/private/Memcache/Redis.php @@ -7,6 +7,7 @@ */ namespace OC\Memcache; +use OC\RedisFactory; use OCP\IMemcacheTTL; use OCP\Server; @@ -37,24 +38,18 @@ class Redis extends Cache implements IMemcacheTTL { private const MAX_TTL = 30 * 24 * 60 * 60; // 1 month - /** - * @var \Redis|\RedisCluster $cache - */ - private static $cache = null; + private \Redis|\RedisCluster $cache; public function __construct($prefix = '', string $logFile = '') { parent::__construct($prefix); + $this->cache = \OCP\Server::get(RedisFactory::class)->getInstance(); } /** - * @return \Redis|\RedisCluster|null * @throws \Exception */ - public function getCache() { - if (is_null(self::$cache)) { - self::$cache = Server::get('RedisFactory')->getInstance(); - } - return self::$cache; + public function getCache(): \Redis|\RedisCluster { + return $this->cache; } public function get($key) { diff --git a/lib/private/NaturalSort.php b/lib/private/NaturalSort.php index 240d4de637a1e..d367cc9d6e960 100644 --- a/lib/private/NaturalSort.php +++ b/lib/private/NaturalSort.php @@ -11,7 +11,6 @@ use Psr\Log\LoggerInterface; class NaturalSort { - private static $instance; private $collator; private $cache = []; @@ -113,10 +112,7 @@ public function compare($a, $b) { * Returns a singleton * @return NaturalSort instance */ - public static function getInstance() { - if (!isset(self::$instance)) { - self::$instance = new NaturalSort(); - } - return self::$instance; + public static function getInstance(): NaturalSort { + return \OCP\Server::get(NaturalSort::class); } } diff --git a/lib/private/RedisFactory.php b/lib/private/RedisFactory.php index 4c8160d81d1f1..fb90e59a5725c 100644 --- a/lib/private/RedisFactory.php +++ b/lib/private/RedisFactory.php @@ -131,15 +131,14 @@ private function getSslContext(array $config): ?array { } public function getInstance(): \Redis|\RedisCluster { - if (!$this->isAvailable()) { - throw new \Exception('Redis support is not available'); - } if ($this->instance === null) { + if (!$this->isAvailable()) { + throw new \Exception('Redis support is not available'); + } $this->create(); - } - - if ($this->instance === null) { - throw new \Exception('Redis support is not available'); + if ($this->instance === null) { + throw new \Exception('Redis support is not available'); + } } return $this->instance; diff --git a/lib/private/Server.php b/lib/private/Server.php index 05af431c00f12..729a60efd04fe 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -60,7 +60,6 @@ use OC\Files\Config\UserMountCacheListener; use OC\Files\Conversion\ConversionManager; use OC\Files\FilenameValidator; -use OC\Files\Filesystem; use OC\Files\Lock\LockManager; use OC\Files\Mount\CacheMountProvider; use OC\Files\Mount\LocalHomeMountProvider; @@ -440,12 +439,11 @@ public function __construct( }); $this->registerAlias(IFileAccess::class, FileAccess::class); $this->registerService('RootFolder', function (ContainerInterface $c) { - $manager = Filesystem::getMountManager(); $view = new View(); /** @var IUserSession $userSession */ $userSession = $c->get(IUserSession::class); $root = new Root( - $manager, + $c->get(\OC\Files\Mount\Manager::class), $view, $userSession->getUser(), $c->get(IUserMountCache::class), @@ -671,10 +669,7 @@ public function __construct( }); $this->registerAlias(ICacheFactory::class, Factory::class); - $this->registerService('RedisFactory', function (Server $c) { - $systemConfig = $c->get(SystemConfig::class); - return new RedisFactory($systemConfig, $c->get(IEventLogger::class)); - }); + $this->registerDeprecatedAlias('RedisFactory', RedisFactory::class); $this->registerService(\OCP\Activity\IManager::class, function (Server $c) { $l10n = $this->get(IFactory::class)->get('lib'); diff --git a/lib/private/Setup.php b/lib/private/Setup.php index efdbd1cfd6c57..7ba40f6578b97 100644 --- a/lib/private/Setup.php +++ b/lib/private/Setup.php @@ -72,7 +72,7 @@ public function __construct( $this->l10n = $l10nFactory->get('lib'); } - protected static array $dbSetupClasses = [ + private const array DB_SETUP_CLASSES = [ 'mysql' => MySQL::class, 'pgsql' => PostgreSQL::class, 'oci' => OCI::class, @@ -334,13 +334,13 @@ public function install(array $options, ?IOutput $output = null): array { $options['directory'] = \OC::$SERVERROOT . '/data'; } - if (!isset(self::$dbSetupClasses[$dbType])) { + if (!isset(self::DB_SETUP_CLASSES[$dbType])) { $dbType = 'sqlite'; } $dataDir = htmlspecialchars_decode($options['directory']); - $class = self::$dbSetupClasses[$dbType]; + $class = self::DB_SETUP_CLASSES[$dbType]; /** @var AbstractDatabase $dbSetup */ $dbSetup = new $class($l, $this->config, $this->logger, $this->random); $error = array_merge($error, $dbSetup->validate($options)); diff --git a/lib/private/TaskProcessing/Db/Task.php b/lib/private/TaskProcessing/Db/Task.php index 3ac4facf97eab..7b30de57d5284 100644 --- a/lib/private/TaskProcessing/Db/Task.php +++ b/lib/private/TaskProcessing/Db/Task.php @@ -76,12 +76,12 @@ class Task extends Entity { /** * @var string[] */ - public static array $columns = ['id', 'last_updated', 'type', 'input', 'output', 'status', 'user_id', 'app_id', 'custom_id', 'completion_expected_at', 'error_message', 'progress', 'webhook_uri', 'webhook_method', 'scheduled_at', 'started_at', 'ended_at', 'allow_cleanup', 'user_facing_error_message', 'include_watermark']; + public const array COLUMNS = ['id', 'last_updated', 'type', 'input', 'output', 'status', 'user_id', 'app_id', 'custom_id', 'completion_expected_at', 'error_message', 'progress', 'webhook_uri', 'webhook_method', 'scheduled_at', 'started_at', 'ended_at', 'allow_cleanup', 'user_facing_error_message', 'include_watermark']; /** * @var string[] */ - public static array $fields = ['id', 'lastUpdated', 'type', 'input', 'output', 'status', 'userId', 'appId', 'customId', 'completionExpectedAt', 'errorMessage', 'progress', 'webhookUri', 'webhookMethod', 'scheduledAt', 'startedAt', 'endedAt', 'allowCleanup', 'userFacingErrorMessage', 'includeWatermark']; + public const array FIELDS = ['id', 'lastUpdated', 'type', 'input', 'output', 'status', 'userId', 'appId', 'customId', 'completionExpectedAt', 'errorMessage', 'progress', 'webhookUri', 'webhookMethod', 'scheduledAt', 'startedAt', 'endedAt', 'allowCleanup', 'userFacingErrorMessage', 'includeWatermark']; public function __construct() { @@ -109,9 +109,9 @@ public function __construct() { } public function toRow(): array { - return array_combine(self::$columns, array_map(function ($field) { + return array_combine(self::COLUMNS, array_map(function ($field) { return $this->{'get' . ucfirst($field)}(); - }, self::$fields)); + }, self::FIELDS)); } public static function fromPublicTask(OCPTask $task): self { diff --git a/lib/private/TaskProcessing/Db/TaskMapper.php b/lib/private/TaskProcessing/Db/TaskMapper.php index f62bb41be3b77..08afdfd923e0a 100644 --- a/lib/private/TaskProcessing/Db/TaskMapper.php +++ b/lib/private/TaskProcessing/Db/TaskMapper.php @@ -38,7 +38,7 @@ public function __construct( */ public function find(int $id): Task { $qb = $this->db->getQueryBuilder(); - $qb->select(Task::$columns) + $qb->select(Task::COLUMNS) ->from($this->tableName) ->where($qb->expr()->eq('id', $qb->createPositionalParameter($id))); return $this->findEntity($qb); @@ -53,7 +53,7 @@ public function find(int $id): Task { */ public function findOldestScheduledByType(array $taskTypes, array $taskIdsToIgnore): Task { $qb = $this->db->getQueryBuilder(); - $qb->select(Task::$columns) + $qb->select(Task::COLUMNS) ->from($this->tableName) ->where($qb->expr()->eq('status', $qb->createPositionalParameter(\OCP\TaskProcessing\Task::STATUS_SCHEDULED, IQueryBuilder::PARAM_INT))) ->setMaxResults(1) @@ -85,7 +85,7 @@ public function findOldestScheduledByType(array $taskTypes, array $taskIdsToIgno */ public function findByIdAndUser(int $id, ?string $userId): Task { $qb = $this->db->getQueryBuilder(); - $qb->select(Task::$columns) + $qb->select(Task::COLUMNS) ->from($this->tableName) ->where($qb->expr()->eq('id', $qb->createPositionalParameter($id))); if ($userId === null) { @@ -105,7 +105,7 @@ public function findByIdAndUser(int $id, ?string $userId): Task { */ public function findByUserAndTaskType(?string $userId, ?string $taskType = null, ?string $customId = null): array { $qb = $this->db->getQueryBuilder(); - $qb->select(Task::$columns) + $qb->select(Task::COLUMNS) ->from($this->tableName) ->where($qb->expr()->eq('user_id', $qb->createPositionalParameter($userId))); if ($taskType !== null) { @@ -126,7 +126,7 @@ public function findByUserAndTaskType(?string $userId, ?string $taskType = null, */ public function findUserTasksByApp(?string $userId, string $appId, ?string $customId = null): array { $qb = $this->db->getQueryBuilder(); - $qb->select(Task::$columns) + $qb->select(Task::COLUMNS) ->from($this->tableName) ->where($qb->expr()->eq('user_id', $qb->createPositionalParameter($userId))) ->andWhere($qb->expr()->eq('app_id', $qb->createPositionalParameter($appId))); @@ -151,7 +151,7 @@ public function findTasks( ?string $userId, ?string $taskType = null, ?string $appId = null, ?string $customId = null, ?int $status = null, ?int $scheduleAfter = null, ?int $endedBefore = null): array { $qb = $this->db->getQueryBuilder(); - $qb->select(Task::$columns) + $qb->select(Task::COLUMNS) ->from($this->tableName); // empty string: no userId filter @@ -205,7 +205,7 @@ public function deleteOlderThan(int $timeout, bool $force = false): int { */ public function getTasksToCleanup(int $timeout, bool $force = false): \Generator { $qb = $this->db->getQueryBuilder(); - $qb->select(Task::$columns) + $qb->select(Task::COLUMNS) ->from($this->tableName) ->where($qb->expr()->lt('last_updated', $qb->createPositionalParameter($this->timeFactory->getDateTime()->getTimestamp() - $timeout))); if (!$force) { @@ -243,7 +243,7 @@ public function lockTask(Entity $entity): int { */ public function findNOldestScheduledByType(array $taskTypes, array $taskIdsToIgnore, int $numberOfTasks) { $qb = $this->db->getQueryBuilder(); - $qb->select(Task::$columns) + $qb->select(Task::COLUMNS) ->from($this->tableName) ->where($qb->expr()->eq('status', $qb->createPositionalParameter(\OCP\TaskProcessing\Task::STATUS_SCHEDULED, IQueryBuilder::PARAM_INT))) ->setMaxResults($numberOfTasks) diff --git a/lib/private/TextProcessing/Db/Task.php b/lib/private/TextProcessing/Db/Task.php index d4ebc19e74a7e..1bb018415ee9c 100644 --- a/lib/private/TextProcessing/Db/Task.php +++ b/lib/private/TextProcessing/Db/Task.php @@ -46,13 +46,12 @@ class Task extends Entity { /** * @var string[] */ - public static array $columns = ['id', 'last_updated', 'type', 'input', 'output', 'status', 'user_id', 'app_id', 'identifier', 'completion_expected_at']; + public const array COLUMNS = ['id', 'last_updated', 'type', 'input', 'output', 'status', 'user_id', 'app_id', 'identifier', 'completion_expected_at']; /** * @var string[] */ - public static array $fields = ['id', 'lastUpdated', 'type', 'input', 'output', 'status', 'userId', 'appId', 'identifier', 'completionExpectedAt']; - + public const array FIELDS = ['id', 'lastUpdated', 'type', 'input', 'output', 'status', 'userId', 'appId', 'identifier', 'completionExpectedAt']; public function __construct() { // add types in constructor @@ -69,9 +68,9 @@ public function __construct() { } public function toRow(): array { - return array_combine(self::$columns, array_map(function ($field) { + return array_combine(self::COLUMNS, array_map(function ($field) { return $this->{'get' . ucfirst($field)}(); - }, self::$fields)); + }, self::FIELDS)); } public static function fromPublicTask(OCPTask $task): Task { diff --git a/lib/private/TextProcessing/Db/TaskMapper.php b/lib/private/TextProcessing/Db/TaskMapper.php index b03e5833958bf..2833a25378d52 100644 --- a/lib/private/TextProcessing/Db/TaskMapper.php +++ b/lib/private/TextProcessing/Db/TaskMapper.php @@ -37,7 +37,7 @@ public function __construct( */ public function find(int $id): Task { $qb = $this->db->getQueryBuilder(); - $qb->select(Task::$columns) + $qb->select(Task::COLUMNS) ->from($this->tableName) ->where($qb->expr()->eq('id', $qb->createPositionalParameter($id))); return $this->findEntity($qb); @@ -53,7 +53,7 @@ public function find(int $id): Task { */ public function findByIdAndUser(int $id, ?string $userId): Task { $qb = $this->db->getQueryBuilder(); - $qb->select(Task::$columns) + $qb->select(Task::COLUMNS) ->from($this->tableName) ->where($qb->expr()->eq('id', $qb->createPositionalParameter($id))); if ($userId === null) { @@ -73,7 +73,7 @@ public function findByIdAndUser(int $id, ?string $userId): Task { */ public function findUserTasksByApp(string $userId, string $appId, ?string $identifier = null): array { $qb = $this->db->getQueryBuilder(); - $qb->select(Task::$columns) + $qb->select(Task::COLUMNS) ->from($this->tableName) ->where($qb->expr()->eq('user_id', $qb->createPositionalParameter($userId))) ->andWhere($qb->expr()->eq('app_id', $qb->createPositionalParameter($appId))); diff --git a/lib/private/TextProcessing/Manager.php b/lib/private/TextProcessing/Manager.php index 3fe45ce55ece2..b6021246c0199 100644 --- a/lib/private/TextProcessing/Manager.php +++ b/lib/private/TextProcessing/Manager.php @@ -44,7 +44,10 @@ class Manager implements IManager { /** @var ?IProvider[] */ private ?array $providers = null; - private static array $taskProcessingCompatibleTaskTypes = [ + /** + * @var array + */ + private const array COMPATIBLE_TASK_TYPES = [ FreePromptTaskType::class => TextToText::ID, HeadlineTaskType::class => TextToTextHeadline::ID, SummaryTaskType::class => TextToTextSummary::ID, @@ -91,7 +94,7 @@ public function getProviders(): array { public function hasProviders(): bool { // check if task processing equivalent types are available $taskTaskTypes = $this->taskProcessingManager->getAvailableTaskTypes(); - foreach (self::$taskProcessingCompatibleTaskTypes as $textTaskTypeClass => $taskTaskTypeId) { + foreach (self::COMPATIBLE_TASK_TYPES as $textTaskTypeClass => $taskTaskTypeId) { if (isset($taskTaskTypes[$taskTaskTypeId])) { return true; } @@ -115,7 +118,7 @@ public function getAvailableTaskTypes(): array { // check if task processing equivalent types are available $taskTaskTypes = $this->taskProcessingManager->getAvailableTaskTypes(); - foreach (self::$taskProcessingCompatibleTaskTypes as $textTaskTypeClass => $taskTaskTypeId) { + foreach (self::COMPATIBLE_TASK_TYPES as $textTaskTypeClass => $taskTaskTypeId) { if (isset($taskTaskTypes[$taskTaskTypeId])) { $tasks[$textTaskTypeClass] = true; } @@ -134,9 +137,9 @@ public function canHandleTask(OCPTask $task): bool { public function runTask(OCPTask $task): string { // try to run a task processing task if possible $taskTypeClass = $task->getType(); - if (isset(self::$taskProcessingCompatibleTaskTypes[$taskTypeClass]) && isset($this->taskProcessingManager->getAvailableTaskTypes()[self::$taskProcessingCompatibleTaskTypes[$taskTypeClass]])) { + if (isset(self::COMPATIBLE_TASK_TYPES[$taskTypeClass]) && isset($this->taskProcessingManager->getAvailableTaskTypes()[self::COMPATIBLE_TASK_TYPES[$taskTypeClass]])) { try { - $taskProcessingTaskTypeId = self::$taskProcessingCompatibleTaskTypes[$taskTypeClass]; + $taskProcessingTaskTypeId = self::COMPATIBLE_TASK_TYPES[$taskTypeClass]; $taskProcessingTask = new \OCP\TaskProcessing\Task( $taskProcessingTaskTypeId, ['input' => $task->getInput()], @@ -222,8 +225,8 @@ public function scheduleTask(OCPTask $task): void { $task->setStatus(OCPTask::STATUS_SCHEDULED); $providers = $this->getPreferredProviders($task); $equivalentTaskProcessingTypeAvailable = ( - isset(self::$taskProcessingCompatibleTaskTypes[$task->getType()]) - && isset($this->taskProcessingManager->getAvailableTaskTypes()[self::$taskProcessingCompatibleTaskTypes[$task->getType()]]) + isset(self::COMPATIBLE_TASK_TYPES[$task->getType()]) + && isset($this->taskProcessingManager->getAvailableTaskTypes()[self::COMPATIBLE_TASK_TYPES[$task->getType()]]) ); if (count($providers) === 0 && !$equivalentTaskProcessingTypeAvailable) { throw new PreConditionNotMetException('No LanguageModel provider is installed that can handle this task'); diff --git a/lib/private/TextToImage/Db/Task.php b/lib/private/TextToImage/Db/Task.php index 48a8bcc6c407d..a3103a8e103fa 100644 --- a/lib/private/TextToImage/Db/Task.php +++ b/lib/private/TextToImage/Db/Task.php @@ -49,12 +49,12 @@ class Task extends Entity { /** * @var string[] */ - public static array $columns = ['id', 'last_updated', 'input', 'status', 'user_id', 'app_id', 'identifier', 'number_of_images', 'completion_expected_at']; + public const array COLUMNS = ['id', 'last_updated', 'input', 'status', 'user_id', 'app_id', 'identifier', 'number_of_images', 'completion_expected_at']; /** * @var string[] */ - public static array $fields = ['id', 'lastUpdated', 'input', 'status', 'userId', 'appId', 'identifier', 'numberOfImages', 'completionExpectedAt']; + public const array FIELDS = ['id', 'lastUpdated', 'input', 'status', 'userId', 'appId', 'identifier', 'numberOfImages', 'completionExpectedAt']; public function __construct() { @@ -71,9 +71,9 @@ public function __construct() { } public function toRow(): array { - return array_combine(self::$columns, array_map(function ($field) { + return array_combine(self::COLUMNS, array_map(function ($field) { return $this->{'get' . ucfirst($field)}(); - }, self::$fields)); + }, self::FIELDS)); } public static function fromPublicTask(OCPTask $task): Task { diff --git a/lib/private/TextToImage/Db/TaskMapper.php b/lib/private/TextToImage/Db/TaskMapper.php index 37f492e14cf3b..31af53c326798 100644 --- a/lib/private/TextToImage/Db/TaskMapper.php +++ b/lib/private/TextToImage/Db/TaskMapper.php @@ -38,7 +38,7 @@ public function __construct( */ public function find(int $id): Task { $qb = $this->db->getQueryBuilder(); - $qb->select(Task::$columns) + $qb->select(Task::COLUMNS) ->from($this->tableName) ->where($qb->expr()->eq('id', $qb->createPositionalParameter($id))); return $this->findEntity($qb); @@ -54,7 +54,7 @@ public function find(int $id): Task { */ public function findByIdAndUser(int $id, ?string $userId): Task { $qb = $this->db->getQueryBuilder(); - $qb->select(Task::$columns) + $qb->select(Task::COLUMNS) ->from($this->tableName) ->where($qb->expr()->eq('id', $qb->createPositionalParameter($id))); if ($userId === null) { @@ -74,7 +74,7 @@ public function findByIdAndUser(int $id, ?string $userId): Task { */ public function findUserTasksByApp(?string $userId, string $appId, ?string $identifier = null): array { $qb = $this->db->getQueryBuilder(); - $qb->select(Task::$columns) + $qb->select(Task::COLUMNS) ->from($this->tableName) ->where($qb->expr()->eq('user_id', $qb->createPositionalParameter($userId))) ->andWhere($qb->expr()->eq('app_id', $qb->createPositionalParameter($appId))); diff --git a/lib/private/legacy/OC_App.php b/lib/private/legacy/OC_App.php index 76e5fda71fb11..17370fa3c1691 100644 --- a/lib/private/legacy/OC_App.php +++ b/lib/private/legacy/OC_App.php @@ -46,6 +46,14 @@ class OC_App { public const supportedApp = 300; public const officialApp = 200; + /** + * @internal + */ + public static function reset(): void { + self::$altLogin = []; + self::$alreadyRegistered = []; + } + /** * clean the appId * diff --git a/lib/private/legacy/OC_Helper.php b/lib/private/legacy/OC_Helper.php index 3dc7470811dbc..09110728b7a87 100644 --- a/lib/private/legacy/OC_Helper.php +++ b/lib/private/legacy/OC_Helper.php @@ -44,6 +44,14 @@ class OC_Helper { private static ?ICacheFactory $cacheFactory = null; private static ?bool $quotaIncludeExternalStorage = null; + /** + * @internal + */ + public static function reset(): void { + self::$cacheFactory = null; + self::$quotaIncludeExternalStorage = null; + } + /** * Recursive copying of folders * @param string $src source folder diff --git a/lib/private/legacy/OC_Hook.php b/lib/private/legacy/OC_Hook.php index 888057a7aca23..0def846b7cfd8 100644 --- a/lib/private/legacy/OC_Hook.php +++ b/lib/private/legacy/OC_Hook.php @@ -114,6 +114,7 @@ public static function clear($signalClass = '', $signalName = '') { } } else { self::$registered = []; + self::$thrownExceptions = []; } } diff --git a/lib/private/legacy/OC_User.php b/lib/private/legacy/OC_User.php index ddd426da942e4..b15e18e8b1650 100644 --- a/lib/private/legacy/OC_User.php +++ b/lib/private/legacy/OC_User.php @@ -51,7 +51,7 @@ * logout() */ class OC_User { - private static $_setupedBackends = []; + public static $_setupedBackends = []; // bool, stores if a user want to access a resource anonymously, e.g if they open a public link private static $incognitoMode = false; diff --git a/lib/public/Util.php b/lib/public/Util.php index 0610460fc2237..0ecfc95075b4f 100644 --- a/lib/public/Util.php +++ b/lib/public/Util.php @@ -412,24 +412,13 @@ public static function emitHook($signalclass, $signalname, $params = []) { return \OC_Hook::emit($signalclass, $signalname, $params); } - /** - * Cached encrypted CSRF token. Some static unit-tests of ownCloud compare - * multiple Template elements which invoke `callRegister`. If the value - * would not be cached these unit-tests would fail. - * @var string - */ - private static $token = ''; - /** * Register an get/post call. This is important to prevent CSRF attacks * @since 4.5.0 * @deprecated 32.0.0 directly use CsrfTokenManager instead */ public static function callRegister() { - if (self::$token === '') { - self::$token = \OCP\Server::get(CsrfTokenManager::class)->getToken()->getEncryptedValue(); - } - return self::$token; + return \OCP\Server::get(CsrfTokenManager::class)->getToken()->getEncryptedValue(); } /** diff --git a/psalm.xml b/psalm.xml index 9b029ce31d497..adfaefffdd9fe 100644 --- a/psalm.xml +++ b/psalm.xml @@ -18,6 +18,7 @@ +