diff options
Diffstat (limited to 'lib/private/legacy')
-rw-r--r-- | lib/private/legacy/OC_App.php | 781 | ||||
-rw-r--r-- | lib/private/legacy/OC_Defaults.php | 335 | ||||
-rw-r--r-- | lib/private/legacy/OC_Helper.php | 423 | ||||
-rw-r--r-- | lib/private/legacy/OC_Hook.php | 125 | ||||
-rw-r--r-- | lib/private/legacy/OC_JSON.php | 105 | ||||
-rw-r--r-- | lib/private/legacy/OC_Response.php | 83 | ||||
-rw-r--r-- | lib/private/legacy/OC_Template.php | 51 | ||||
-rw-r--r-- | lib/private/legacy/OC_User.php | 405 | ||||
-rw-r--r-- | lib/private/legacy/OC_Util.php | 847 | ||||
-rw-r--r-- | lib/private/legacy/l10n.php | 342 |
10 files changed, 3155 insertions, 342 deletions
diff --git a/lib/private/legacy/OC_App.php b/lib/private/legacy/OC_App.php new file mode 100644 index 00000000000..10b78e2a7ef --- /dev/null +++ b/lib/private/legacy/OC_App.php @@ -0,0 +1,781 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +use OC\App\DependencyAnalyzer; +use OC\App\Platform; +use OC\AppFramework\Bootstrap\Coordinator; +use OC\Config\ConfigManager; +use OC\DB\MigrationService; +use OC\Installer; +use OC\Repair; +use OC\Repair\Events\RepairErrorEvent; +use OCP\App\Events\AppUpdateEvent; +use OCP\App\IAppManager; +use OCP\App\ManagerEvent; +use OCP\Authentication\IAlternativeLogin; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IAppConfig; +use OCP\Server; +use Psr\Container\ContainerExceptionInterface; +use Psr\Log\LoggerInterface; +use function OCP\Log\logger; + +/** + * This class manages the apps. It allows them to register and integrate in the + * Nextcloud ecosystem. Furthermore, this class is responsible for installing, + * upgrading and removing apps. + */ +class OC_App { + private static $altLogin = []; + private static $alreadyRegistered = []; + public const supportedApp = 300; + public const officialApp = 200; + + /** + * clean the appId + * + * @psalm-taint-escape file + * @psalm-taint-escape include + * @psalm-taint-escape html + * @psalm-taint-escape has_quotes + * + * @deprecated 31.0.0 use IAppManager::cleanAppId + */ + public static function cleanAppId(string $app): string { + return str_replace(['<', '>', '"', "'", '\0', '/', '\\', '..'], '', $app); + } + + /** + * Check if an app is loaded + * + * @param string $app + * @return bool + * @deprecated 27.0.0 use IAppManager::isAppLoaded + */ + public static function isAppLoaded(string $app): bool { + return \OC::$server->get(IAppManager::class)->isAppLoaded($app); + } + + /** + * loads all apps + * + * @param string[] $types + * @return bool + * + * This function walks through the Nextcloud directory and loads all apps + * it can find. A directory contains an app if the file /appinfo/info.xml + * exists. + * + * if $types is set to non-empty array, only apps of those types will be loaded + * + * @deprecated 29.0.0 use IAppManager::loadApps instead + */ + public static function loadApps(array $types = []): bool { + if (!\OC::$server->getSystemConfig()->getValue('installed', false)) { + // This should be done before calling this method so that appmanager can be used + return false; + } + return \OC::$server->get(IAppManager::class)->loadApps($types); + } + + /** + * load a single app + * + * @param string $app + * @throws Exception + * @deprecated 27.0.0 use IAppManager::loadApp + */ + public static function loadApp(string $app): void { + \OC::$server->get(IAppManager::class)->loadApp($app); + } + + /** + * @internal + * @param string $app + * @param string $path + * @param bool $force + */ + public static function registerAutoloading(string $app, string $path, bool $force = false) { + $key = $app . '-' . $path; + if (!$force && isset(self::$alreadyRegistered[$key])) { + return; + } + + self::$alreadyRegistered[$key] = true; + + // Register on PSR-4 composer autoloader + $appNamespace = \OC\AppFramework\App::buildAppNamespace($app); + \OC::$server->registerNamespace($app, $appNamespace); + + if (file_exists($path . '/composer/autoload.php')) { + require_once $path . '/composer/autoload.php'; + } else { + \OC::$composerAutoloader->addPsr4($appNamespace . '\\', $path . '/lib/', true); + } + + // Register Test namespace only when testing + if (defined('PHPUNIT_RUN') || defined('CLI_TEST_RUN')) { + \OC::$composerAutoloader->addPsr4($appNamespace . '\\Tests\\', $path . '/tests/', true); + } + } + + /** + * check if an app is of a specific type + * + * @param string $app + * @param array $types + * @return bool + * @deprecated 27.0.0 use IAppManager::isType + */ + public static function isType(string $app, array $types): bool { + return \OC::$server->get(IAppManager::class)->isType($app, $types); + } + + /** + * read app types from info.xml and cache them in the database + */ + public static function setAppTypes(string $app) { + $appManager = \OC::$server->getAppManager(); + $appData = $appManager->getAppInfo($app); + if (!is_array($appData)) { + return; + } + + if (isset($appData['types'])) { + $appTypes = implode(',', $appData['types']); + } else { + $appTypes = ''; + $appData['types'] = []; + } + + $config = \OC::$server->getConfig(); + $config->setAppValue($app, 'types', $appTypes); + + if ($appManager->hasProtectedAppType($appData['types'])) { + $enabled = $config->getAppValue($app, 'enabled', 'yes'); + if ($enabled !== 'yes' && $enabled !== 'no') { + $config->setAppValue($app, 'enabled', 'yes'); + } + } + } + + /** + * Returns apps enabled for the current user. + * + * @param bool $forceRefresh whether to refresh the cache + * @param bool $all whether to return apps for all users, not only the + * currently logged in one + * @return list<string> + */ + public static function getEnabledApps(bool $forceRefresh = false, bool $all = false): array { + if (!\OC::$server->getSystemConfig()->getValue('installed', false)) { + return []; + } + // in incognito mode or when logged out, $user will be false, + // which is also the case during an upgrade + $appManager = \OC::$server->getAppManager(); + if ($all) { + $user = null; + } else { + $user = \OC::$server->getUserSession()->getUser(); + } + + if (is_null($user)) { + $apps = $appManager->getEnabledApps(); + } else { + $apps = $appManager->getEnabledAppsForUser($user); + } + $apps = array_filter($apps, function ($app) { + return $app !== 'files';//we add this manually + }); + sort($apps); + array_unshift($apps, 'files'); + return $apps; + } + + /** + * enables an app + * + * @param string $appId + * @param array $groups (optional) when set, only these groups will have access to the app + * @throws \Exception + * @return void + * + * This function set an app as enabled in appconfig. + */ + public function enable(string $appId, + array $groups = []) { + // Check if app is already downloaded + /** @var Installer $installer */ + $installer = Server::get(Installer::class); + $isDownloaded = $installer->isDownloaded($appId); + + if (!$isDownloaded) { + $installer->downloadApp($appId); + } + + $installer->installApp($appId); + + $appManager = \OC::$server->getAppManager(); + if ($groups !== []) { + $groupManager = \OC::$server->getGroupManager(); + $groupsList = []; + foreach ($groups as $group) { + $groupItem = $groupManager->get($group); + if ($groupItem instanceof \OCP\IGroup) { + $groupsList[] = $groupManager->get($group); + } + } + $appManager->enableAppForGroups($appId, $groupsList); + } else { + $appManager->enableApp($appId); + } + } + + /** + * Find the apps root for an app id. + * + * If multiple copies are found, the apps root the latest version is returned. + * + * @param string $appId + * @param bool $ignoreCache ignore cache and rebuild it + * @return false|array{path: string, url: string} the apps root shape + */ + public static function findAppInDirectories(string $appId, bool $ignoreCache = false) { + $sanitizedAppId = self::cleanAppId($appId); + if ($sanitizedAppId !== $appId) { + return false; + } + static $app_dir = []; + + if (isset($app_dir[$appId]) && !$ignoreCache) { + return $app_dir[$appId]; + } + + $possibleApps = []; + foreach (OC::$APPSROOTS as $dir) { + if (file_exists($dir['path'] . '/' . $appId)) { + $possibleApps[] = $dir; + } + } + + if (empty($possibleApps)) { + return false; + } elseif (count($possibleApps) === 1) { + $dir = array_shift($possibleApps); + $app_dir[$appId] = $dir; + return $dir; + } else { + $versionToLoad = []; + foreach ($possibleApps as $possibleApp) { + $version = self::getAppVersionByPath($possibleApp['path'] . '/' . $appId); + if (empty($versionToLoad) || version_compare($version, $versionToLoad['version'], '>')) { + $versionToLoad = [ + 'dir' => $possibleApp, + 'version' => $version, + ]; + } + } + $app_dir[$appId] = $versionToLoad['dir']; + return $versionToLoad['dir']; + //TODO - write test + } + } + + /** + * Get the directory for the given app. + * If the app is defined in multiple directories, the first one is taken. (false if not found) + * + * @psalm-taint-specialize + * + * @param string $appId + * @param bool $refreshAppPath should be set to true only during install/upgrade + * @return string|false + * @deprecated 11.0.0 use Server::get(IAppManager)->getAppPath() + */ + public static function getAppPath(string $appId, bool $refreshAppPath = false) { + $appId = self::cleanAppId($appId); + if ($appId === '') { + return false; + } elseif ($appId === 'core') { + return __DIR__ . '/../../../core'; + } + + if (($dir = self::findAppInDirectories($appId, $refreshAppPath)) != false) { + return $dir['path'] . '/' . $appId; + } + return false; + } + + /** + * Get the path for the given app on the access + * If the app is defined in multiple directories, the first one is taken. (false if not found) + * + * @param string $appId + * @return string|false + * @deprecated 18.0.0 use \OC::$server->getAppManager()->getAppWebPath() + */ + public static function getAppWebPath(string $appId) { + if (($dir = self::findAppInDirectories($appId)) != false) { + return OC::$WEBROOT . $dir['url'] . '/' . $appId; + } + return false; + } + + /** + * get app's version based on it's path + * + * @param string $path + * @return string + */ + public static function getAppVersionByPath(string $path): string { + $infoFile = $path . '/appinfo/info.xml'; + $appData = Server::get(IAppManager::class)->getAppInfoByPath($infoFile); + return $appData['version'] ?? ''; + } + + /** + * get the id of loaded app + * + * @return string + */ + public static function getCurrentApp(): string { + if (\OC::$CLI) { + return ''; + } + + $request = \OC::$server->getRequest(); + $script = substr($request->getScriptName(), strlen(OC::$WEBROOT) + 1); + $topFolder = substr($script, 0, strpos($script, '/') ?: 0); + if (empty($topFolder)) { + try { + $path_info = $request->getPathInfo(); + } catch (Exception $e) { + // Can happen from unit tests because the script name is `./vendor/bin/phpunit` or something a like then. + \OC::$server->get(LoggerInterface::class)->error('Failed to detect current app from script path', ['exception' => $e]); + return ''; + } + if ($path_info) { + $topFolder = substr($path_info, 1, strpos($path_info, '/', 1) - 1); + } + } + if ($topFolder == 'apps') { + $length = strlen($topFolder); + return substr($script, $length + 1, strpos($script, '/', $length + 1) - $length - 1) ?: ''; + } else { + return $topFolder; + } + } + + /** + * @param array $entry + * @deprecated 20.0.0 Please register your alternative login option using the registerAlternativeLogin() on the RegistrationContext in your Application class implementing the OCP\Authentication\IAlternativeLogin interface + */ + public static function registerLogIn(array $entry) { + Server::get(LoggerInterface::class)->debug('OC_App::registerLogIn() is deprecated, please register your alternative login option using the registerAlternativeLogin() on the RegistrationContext in your Application class implementing the OCP\Authentication\IAlternativeLogin interface'); + self::$altLogin[] = $entry; + } + + /** + * @return array + */ + public static function getAlternativeLogIns(): array { + /** @var Coordinator $bootstrapCoordinator */ + $bootstrapCoordinator = Server::get(Coordinator::class); + + foreach ($bootstrapCoordinator->getRegistrationContext()->getAlternativeLogins() as $registration) { + if (!in_array(IAlternativeLogin::class, class_implements($registration->getService()), true)) { + Server::get(LoggerInterface::class)->error('Alternative login option {option} does not implement {interface} and is therefore ignored.', [ + 'option' => $registration->getService(), + 'interface' => IAlternativeLogin::class, + 'app' => $registration->getAppId(), + ]); + continue; + } + + try { + /** @var IAlternativeLogin $provider */ + $provider = Server::get($registration->getService()); + } catch (ContainerExceptionInterface $e) { + Server::get(LoggerInterface::class)->error('Alternative login option {option} can not be initialized.', + [ + 'exception' => $e, + 'option' => $registration->getService(), + 'app' => $registration->getAppId(), + ]); + } + + try { + $provider->load(); + + self::$altLogin[] = [ + 'name' => $provider->getLabel(), + 'href' => $provider->getLink(), + 'class' => $provider->getClass(), + ]; + } catch (Throwable $e) { + Server::get(LoggerInterface::class)->error('Alternative login option {option} had an error while loading.', + [ + 'exception' => $e, + 'option' => $registration->getService(), + 'app' => $registration->getAppId(), + ]); + } + } + + return self::$altLogin; + } + + /** + * get a list of all apps in the apps folder + * + * @return string[] an array of app names (string IDs) + * @deprecated 31.0.0 Use IAppManager::getAllAppsInAppsFolders instead + */ + public static function getAllApps(): array { + return Server::get(IAppManager::class)->getAllAppsInAppsFolders(); + } + + /** + * List all supported apps + * + * @deprecated 32.0.0 Use \OCP\Support\Subscription\IRegistry::delegateGetSupportedApps instead + */ + public function getSupportedApps(): array { + $subscriptionRegistry = Server::get(\OCP\Support\Subscription\IRegistry::class); + $supportedApps = $subscriptionRegistry->delegateGetSupportedApps(); + return $supportedApps; + } + + /** + * List all apps, this is used in apps.php + * + * @return array + */ + public function listAllApps(): array { + $appManager = \OC::$server->getAppManager(); + + $installedApps = $appManager->getAllAppsInAppsFolders(); + //we don't want to show configuration for these + $blacklist = $appManager->getAlwaysEnabledApps(); + $appList = []; + $langCode = \OC::$server->getL10N('core')->getLanguageCode(); + $urlGenerator = \OC::$server->getURLGenerator(); + $supportedApps = $this->getSupportedApps(); + + foreach ($installedApps as $app) { + if (!in_array($app, $blacklist)) { + $info = $appManager->getAppInfo($app, false, $langCode); + if (!is_array($info)) { + Server::get(LoggerInterface::class)->error('Could not read app info file for app "' . $app . '"', ['app' => 'core']); + continue; + } + + if (!isset($info['name'])) { + Server::get(LoggerInterface::class)->error('App id "' . $app . '" has no name in appinfo', ['app' => 'core']); + continue; + } + + $enabled = \OC::$server->getConfig()->getAppValue($app, 'enabled', 'no'); + $info['groups'] = null; + if ($enabled === 'yes') { + $active = true; + } elseif ($enabled === 'no') { + $active = false; + } else { + $active = true; + $info['groups'] = $enabled; + } + + $info['active'] = $active; + + if ($appManager->isShipped($app)) { + $info['internal'] = true; + $info['level'] = self::officialApp; + $info['removable'] = false; + } else { + $info['internal'] = false; + $info['removable'] = true; + } + + if (in_array($app, $supportedApps)) { + $info['level'] = self::supportedApp; + } + + $appPath = self::getAppPath($app); + if ($appPath !== false) { + $appIcon = $appPath . '/img/' . $app . '.svg'; + if (file_exists($appIcon)) { + $info['preview'] = $urlGenerator->imagePath($app, $app . '.svg'); + $info['previewAsIcon'] = true; + } else { + $appIcon = $appPath . '/img/app.svg'; + if (file_exists($appIcon)) { + $info['preview'] = $urlGenerator->imagePath($app, 'app.svg'); + $info['previewAsIcon'] = true; + } + } + } + // fix documentation + if (isset($info['documentation']) && is_array($info['documentation'])) { + foreach ($info['documentation'] as $key => $url) { + // If it is not an absolute URL we assume it is a key + // i.e. admin-ldap will get converted to go.php?to=admin-ldap + if (stripos($url, 'https://') !== 0 && stripos($url, 'http://') !== 0) { + $url = $urlGenerator->linkToDocs($url); + } + + $info['documentation'][$key] = $url; + } + } + + $info['version'] = $appManager->getAppVersion($app); + $appList[] = $info; + } + } + + return $appList; + } + + public static function shouldUpgrade(string $app): bool { + $versions = self::getAppVersions(); + $currentVersion = Server::get(\OCP\App\IAppManager::class)->getAppVersion($app); + if ($currentVersion && isset($versions[$app])) { + $installedVersion = $versions[$app]; + if (!version_compare($currentVersion, $installedVersion, '=')) { + return true; + } + } + return false; + } + + /** + * Adjust the number of version parts of $version1 to match + * the number of version parts of $version2. + * + * @param string $version1 version to adjust + * @param string $version2 version to take the number of parts from + * @return string shortened $version1 + */ + private static function adjustVersionParts(string $version1, string $version2): string { + $version1 = explode('.', $version1); + $version2 = explode('.', $version2); + // reduce $version1 to match the number of parts in $version2 + while (count($version1) > count($version2)) { + array_pop($version1); + } + // if $version1 does not have enough parts, add some + while (count($version1) < count($version2)) { + $version1[] = '0'; + } + return implode('.', $version1); + } + + /** + * Check whether the current Nextcloud version matches the given + * application's version requirements. + * + * The comparison is made based on the number of parts that the + * app info version has. For example for ownCloud 6.0.3 if the + * app info version is expecting version 6.0, the comparison is + * made on the first two parts of the ownCloud version. + * This means that it's possible to specify "requiremin" => 6 + * and "requiremax" => 6 and it will still match ownCloud 6.0.3. + * + * @param string $ocVersion Nextcloud version to check against + * @param array $appInfo app info (from xml) + * + * @return boolean true if compatible, otherwise false + */ + public static function isAppCompatible(string $ocVersion, array $appInfo, bool $ignoreMax = false): bool { + $requireMin = ''; + $requireMax = ''; + if (isset($appInfo['dependencies']['nextcloud']['@attributes']['min-version'])) { + $requireMin = $appInfo['dependencies']['nextcloud']['@attributes']['min-version']; + } elseif (isset($appInfo['dependencies']['owncloud']['@attributes']['min-version'])) { + $requireMin = $appInfo['dependencies']['owncloud']['@attributes']['min-version']; + } elseif (isset($appInfo['requiremin'])) { + $requireMin = $appInfo['requiremin']; + } elseif (isset($appInfo['require'])) { + $requireMin = $appInfo['require']; + } + + if (isset($appInfo['dependencies']['nextcloud']['@attributes']['max-version'])) { + $requireMax = $appInfo['dependencies']['nextcloud']['@attributes']['max-version']; + } elseif (isset($appInfo['dependencies']['owncloud']['@attributes']['max-version'])) { + $requireMax = $appInfo['dependencies']['owncloud']['@attributes']['max-version']; + } elseif (isset($appInfo['requiremax'])) { + $requireMax = $appInfo['requiremax']; + } + + if (!empty($requireMin) + && version_compare(self::adjustVersionParts($ocVersion, $requireMin), $requireMin, '<') + ) { + return false; + } + + if (!$ignoreMax && !empty($requireMax) + && version_compare(self::adjustVersionParts($ocVersion, $requireMax), $requireMax, '>') + ) { + return false; + } + + return true; + } + + /** + * get the installed version of all apps + * @deprecated 32.0.0 Use IAppManager::getAppInstalledVersions or IAppConfig::getAppInstalledVersions instead + */ + public static function getAppVersions(): array { + return Server::get(IAppConfig::class)->getAppInstalledVersions(); + } + + /** + * update the database for the app and call the update script + * + * @param string $appId + * @return bool + */ + public static function updateApp(string $appId): bool { + // for apps distributed with core, we refresh app path in case the downloaded version + // have been installed in custom apps and not in the default path + $appPath = self::getAppPath($appId, true); + if ($appPath === false) { + return false; + } + + if (is_file($appPath . '/appinfo/database.xml')) { + Server::get(LoggerInterface::class)->error('The appinfo/database.xml file is not longer supported. Used in ' . $appId); + return false; + } + + \OC::$server->getAppManager()->clearAppsCache(); + $l = \OC::$server->getL10N('core'); + $appData = Server::get(\OCP\App\IAppManager::class)->getAppInfo($appId, false, $l->getLanguageCode()); + + $ignoreMaxApps = \OC::$server->getConfig()->getSystemValue('app_install_overwrite', []); + $ignoreMax = in_array($appId, $ignoreMaxApps, true); + \OC_App::checkAppDependencies( + \OC::$server->getConfig(), + $l, + $appData, + $ignoreMax + ); + + self::registerAutoloading($appId, $appPath, true); + self::executeRepairSteps($appId, $appData['repair-steps']['pre-migration']); + + $ms = new MigrationService($appId, \OC::$server->get(\OC\DB\Connection::class)); + $ms->migrate(); + + self::executeRepairSteps($appId, $appData['repair-steps']['post-migration']); + self::setupLiveMigrations($appId, $appData['repair-steps']['live-migration']); + // update appversion in app manager + \OC::$server->getAppManager()->clearAppsCache(); + \OC::$server->getAppManager()->getAppVersion($appId, false); + + self::setupBackgroundJobs($appData['background-jobs']); + + //set remote/public handlers + if (array_key_exists('ocsid', $appData)) { + \OC::$server->getConfig()->setAppValue($appId, 'ocsid', $appData['ocsid']); + } elseif (\OC::$server->getConfig()->getAppValue($appId, 'ocsid', null) !== null) { + \OC::$server->getConfig()->deleteAppValue($appId, 'ocsid'); + } + foreach ($appData['remote'] as $name => $path) { + \OC::$server->getConfig()->setAppValue('core', 'remote_' . $name, $appId . '/' . $path); + } + foreach ($appData['public'] as $name => $path) { + \OC::$server->getConfig()->setAppValue('core', 'public_' . $name, $appId . '/' . $path); + } + + self::setAppTypes($appId); + + $version = Server::get(\OCP\App\IAppManager::class)->getAppVersion($appId); + \OC::$server->getConfig()->setAppValue($appId, 'installed_version', $version); + + // migrate eventual new config keys in the process + /** @psalm-suppress InternalMethod */ + Server::get(ConfigManager::class)->migrateConfigLexiconKeys($appId); + + \OC::$server->get(IEventDispatcher::class)->dispatchTyped(new AppUpdateEvent($appId)); + \OC::$server->get(IEventDispatcher::class)->dispatch(ManagerEvent::EVENT_APP_UPDATE, new ManagerEvent( + ManagerEvent::EVENT_APP_UPDATE, $appId + )); + + return true; + } + + /** + * @param string $appId + * @param string[] $steps + * @throws \OC\NeedsUpdateException + */ + public static function executeRepairSteps(string $appId, array $steps) { + if (empty($steps)) { + return; + } + // load the app + self::loadApp($appId); + + $dispatcher = Server::get(IEventDispatcher::class); + + // load the steps + $r = Server::get(Repair::class); + foreach ($steps as $step) { + try { + $r->addStep($step); + } catch (Exception $ex) { + $dispatcher->dispatchTyped(new RepairErrorEvent($ex->getMessage())); + logger('core')->error('Failed to add app migration step ' . $step, ['exception' => $ex]); + } + } + // run the steps + $r->run(); + } + + public static function setupBackgroundJobs(array $jobs) { + $queue = \OC::$server->getJobList(); + foreach ($jobs as $job) { + $queue->add($job); + } + } + + /** + * @param string $appId + * @param string[] $steps + */ + private static function setupLiveMigrations(string $appId, array $steps) { + $queue = \OC::$server->getJobList(); + foreach ($steps as $step) { + $queue->add('OC\Migration\BackgroundRepair', [ + 'app' => $appId, + 'step' => $step]); + } + } + + /** + * @param \OCP\IConfig $config + * @param \OCP\IL10N $l + * @param array $info + * @throws \Exception + */ + public static function checkAppDependencies(\OCP\IConfig $config, \OCP\IL10N $l, array $info, bool $ignoreMax) { + $dependencyAnalyzer = new DependencyAnalyzer(new Platform($config), $l); + $missing = $dependencyAnalyzer->analyze($info, $ignoreMax); + if (!empty($missing)) { + $missingMsg = implode(PHP_EOL, $missing); + throw new \Exception( + $l->t('App "%1$s" cannot be installed because the following dependencies are not fulfilled: %2$s', + [$info['name'], $missingMsg] + ) + ); + } + } +} diff --git a/lib/private/legacy/OC_Defaults.php b/lib/private/legacy/OC_Defaults.php new file mode 100644 index 00000000000..0d460ff966d --- /dev/null +++ b/lib/private/legacy/OC_Defaults.php @@ -0,0 +1,335 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ + +use OCP\IConfig; +use OCP\Server; +use OCP\ServerVersion; + +class OC_Defaults { + private $theme; + + private $defaultEntity; + private $defaultName; + private $defaultTitle; + private $defaultBaseUrl; + private $defaultSyncClientUrl; + private $defaultiOSClientUrl; + private $defaultiTunesAppId; + private $defaultAndroidClientUrl; + private $defaultFDroidClientUrl; + private $defaultDocBaseUrl; + private $defaultDocVersion; + private $defaultSlogan; + private $defaultColorBackground; + private $defaultColorPrimary; + private $defaultTextColorPrimary; + private $defaultProductName; + + public function __construct() { + $config = Server::get(IConfig::class); + $serverVersion = Server::get(ServerVersion::class); + + $this->defaultEntity = 'Nextcloud'; /* e.g. company name, used for footers and copyright notices */ + $this->defaultName = 'Nextcloud'; /* short name, used when referring to the software */ + $this->defaultTitle = 'Nextcloud'; /* can be a longer name, for titles */ + $this->defaultBaseUrl = 'https://nextcloud.com'; + $this->defaultSyncClientUrl = $config->getSystemValue('customclient_desktop', 'https://nextcloud.com/install/#install-clients'); + $this->defaultiOSClientUrl = $config->getSystemValue('customclient_ios', 'https://geo.itunes.apple.com/us/app/nextcloud/id1125420102?mt=8'); + $this->defaultiTunesAppId = $config->getSystemValue('customclient_ios_appid', '1125420102'); + $this->defaultAndroidClientUrl = $config->getSystemValue('customclient_android', 'https://play.google.com/store/apps/details?id=com.nextcloud.client'); + $this->defaultFDroidClientUrl = $config->getSystemValue('customclient_fdroid', 'https://f-droid.org/packages/com.nextcloud.client/'); + $this->defaultDocBaseUrl = 'https://docs.nextcloud.com'; + $this->defaultDocVersion = $serverVersion->getMajorVersion(); // used to generate doc links + $this->defaultColorBackground = '#00679e'; + $this->defaultColorPrimary = '#00679e'; + $this->defaultTextColorPrimary = '#ffffff'; + $this->defaultProductName = 'Nextcloud'; + + $themePath = OC::$SERVERROOT . '/themes/' . OC_Util::getTheme() . '/defaults.php'; + if (file_exists($themePath)) { + // prevent defaults.php from printing output + ob_start(); + require_once $themePath; + ob_end_clean(); + if (class_exists('OC_Theme')) { + $this->theme = new OC_Theme(); + } + } + } + + /** + * @param string $method + */ + private function themeExist($method) { + if (isset($this->theme) && method_exists($this->theme, $method)) { + return true; + } + return false; + } + + /** + * Returns the base URL + * @return string URL + */ + public function getBaseUrl() { + if ($this->themeExist('getBaseUrl')) { + return $this->theme->getBaseUrl(); + } else { + return $this->defaultBaseUrl; + } + } + + /** + * Returns the URL where the sync clients are listed + * @return string URL + */ + public function getSyncClientUrl() { + if ($this->themeExist('getSyncClientUrl')) { + return $this->theme->getSyncClientUrl(); + } else { + return $this->defaultSyncClientUrl; + } + } + + /** + * Returns the URL to the App Store for the iOS Client + * @return string URL + */ + public function getiOSClientUrl() { + if ($this->themeExist('getiOSClientUrl')) { + return $this->theme->getiOSClientUrl(); + } else { + return $this->defaultiOSClientUrl; + } + } + + /** + * Returns the AppId for the App Store for the iOS Client + * @return string AppId + */ + public function getiTunesAppId() { + if ($this->themeExist('getiTunesAppId')) { + return $this->theme->getiTunesAppId(); + } else { + return $this->defaultiTunesAppId; + } + } + + /** + * Returns the URL to Google Play for the Android Client + * @return string URL + */ + public function getAndroidClientUrl() { + if ($this->themeExist('getAndroidClientUrl')) { + return $this->theme->getAndroidClientUrl(); + } else { + return $this->defaultAndroidClientUrl; + } + } + + /** + * Returns the URL to Google Play for the Android Client + * @return string URL + */ + public function getFDroidClientUrl() { + if ($this->themeExist('getFDroidClientUrl')) { + return $this->theme->getFDroidClientUrl(); + } else { + return $this->defaultFDroidClientUrl; + } + } + + /** + * Returns the documentation URL + * @return string URL + */ + public function getDocBaseUrl() { + if ($this->themeExist('getDocBaseUrl')) { + return $this->theme->getDocBaseUrl(); + } else { + return $this->defaultDocBaseUrl; + } + } + + /** + * Returns the title + * @return string title + */ + public function getTitle() { + if ($this->themeExist('getTitle')) { + return $this->theme->getTitle(); + } else { + return $this->defaultTitle; + } + } + + /** + * Returns the short name of the software + * @return string title + */ + public function getName() { + if ($this->themeExist('getName')) { + return $this->theme->getName(); + } else { + return $this->defaultName; + } + } + + /** + * Returns the short name of the software containing HTML strings + * @return string title + */ + public function getHTMLName() { + if ($this->themeExist('getHTMLName')) { + return $this->theme->getHTMLName(); + } else { + return $this->defaultName; + } + } + + /** + * Returns entity (e.g. company name) - used for footer, copyright + * @return string entity name + */ + public function getEntity() { + if ($this->themeExist('getEntity')) { + return $this->theme->getEntity(); + } else { + return $this->defaultEntity; + } + } + + /** + * Returns slogan + * @return string slogan + */ + public function getSlogan(?string $lang = null) { + if ($this->themeExist('getSlogan')) { + return $this->theme->getSlogan($lang); + } else { + if ($this->defaultSlogan === null) { + $l10n = \OC::$server->getL10N('lib', $lang); + $this->defaultSlogan = $l10n->t('a safe home for all your data'); + } + return $this->defaultSlogan; + } + } + + /** + * Returns short version of the footer + * @return string short footer + */ + public function getShortFooter() { + if ($this->themeExist('getShortFooter')) { + $footer = $this->theme->getShortFooter(); + } else { + $footer = '<a href="' . $this->getBaseUrl() . '" target="_blank"' + . ' rel="noreferrer noopener">' . $this->getEntity() . '</a>' + . ' – ' . $this->getSlogan(); + } + + return $footer; + } + + /** + * Returns long version of the footer + * @return string long footer + */ + public function getLongFooter() { + if ($this->themeExist('getLongFooter')) { + $footer = $this->theme->getLongFooter(); + } else { + $footer = $this->getShortFooter(); + } + + return $footer; + } + + /** + * @param string $key + * @return string URL to doc with key + */ + public function buildDocLinkToKey($key) { + if ($this->themeExist('buildDocLinkToKey')) { + return $this->theme->buildDocLinkToKey($key); + } + return $this->getDocBaseUrl() . '/server/' . $this->defaultDocVersion . '/go.php?to=' . $key; + } + + /** + * Returns primary color + * @return string + */ + public function getColorPrimary() { + if ($this->themeExist('getColorPrimary')) { + return $this->theme->getColorPrimary(); + } + if ($this->themeExist('getMailHeaderColor')) { + return $this->theme->getMailHeaderColor(); + } + return $this->defaultColorPrimary; + } + + /** + * Returns primary color + * @return string + */ + public function getColorBackground() { + if ($this->themeExist('getColorBackground')) { + return $this->theme->getColorBackground(); + } + return $this->defaultColorBackground; + } + + /** + * @return array scss variables to overwrite + */ + public function getScssVariables() { + if ($this->themeExist('getScssVariables')) { + return $this->theme->getScssVariables(); + } + return []; + } + + public function shouldReplaceIcons() { + return false; + } + + /** + * Themed logo url + * + * @param bool $useSvg Whether to point to the SVG image or a fallback + * @return string + */ + public function getLogo($useSvg = true) { + if ($this->themeExist('getLogo')) { + return $this->theme->getLogo($useSvg); + } + + if ($useSvg) { + $logo = \OC::$server->getURLGenerator()->imagePath('core', 'logo/logo.svg'); + } else { + $logo = \OC::$server->getURLGenerator()->imagePath('core', 'logo/logo.png'); + } + return $logo . '?v=' . hash('sha1', implode('.', \OCP\Util::getVersion())); + } + + public function getTextColorPrimary() { + if ($this->themeExist('getTextColorPrimary')) { + return $this->theme->getTextColorPrimary(); + } + return $this->defaultTextColorPrimary; + } + + public function getProductName() { + if ($this->themeExist('getProductName')) { + return $this->theme->getProductName(); + } + return $this->defaultProductName; + } +} diff --git a/lib/private/legacy/OC_Helper.php b/lib/private/legacy/OC_Helper.php new file mode 100644 index 00000000000..4388f775623 --- /dev/null +++ b/lib/private/legacy/OC_Helper.php @@ -0,0 +1,423 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +use bantu\IniGetWrapper\IniGetWrapper; +use OC\Files\FilenameValidator; +use OC\Files\Filesystem; +use OCP\Files\Mount\IMountPoint; +use OCP\IBinaryFinder; +use OCP\ICacheFactory; +use OCP\IUser; +use OCP\Server; +use OCP\Util; +use Psr\Log\LoggerInterface; + +/** + * Collection of useful functions + * + * @psalm-type StorageInfo = array{ + * free: float|int, + * mountPoint: string, + * mountType: string, + * owner: string, + * ownerDisplayName: string, + * quota: float|int, + * relative: float|int, + * total: float|int, + * used: float|int, + * } + */ +class OC_Helper { + private static $templateManager; + private static ?ICacheFactory $cacheFactory = null; + private static ?bool $quotaIncludeExternalStorage = null; + + /** + * Recursive copying of folders + * @param string $src source folder + * @param string $dest target folder + * @return void + * @deprecated 32.0.0 - use \OCP\Files\Folder::copy + */ + public static function copyr($src, $dest) { + if (!file_exists($src)) { + return; + } + + if (is_dir($src)) { + if (!is_dir($dest)) { + mkdir($dest); + } + $files = scandir($src); + foreach ($files as $file) { + if ($file != '.' && $file != '..') { + self::copyr("$src/$file", "$dest/$file"); + } + } + } else { + $validator = \OCP\Server::get(FilenameValidator::class); + if (!$validator->isForbidden($src)) { + copy($src, $dest); + } + } + } + + /** + * @deprecated 18.0.0 + * @return \OC\Files\Type\TemplateManager + */ + public static function getFileTemplateManager() { + if (!self::$templateManager) { + self::$templateManager = new \OC\Files\Type\TemplateManager(); + } + return self::$templateManager; + } + + /** + * detect if a given program is found in the search PATH + * + * @param string $name + * @param bool $path + * @internal param string $program name + * @internal param string $optional search path, defaults to $PATH + * @return bool true if executable program found in path + * @deprecated 32.0.0 use the \OCP\IBinaryFinder + */ + public static function canExecute($name, $path = false) { + // path defaults to PATH from environment if not set + if ($path === false) { + $path = getenv('PATH'); + } + // we look for an executable file of that name + $exts = ['']; + $check_fn = 'is_executable'; + // Default check will be done with $path directories : + $dirs = explode(PATH_SEPARATOR, (string)$path); + // WARNING : We have to check if open_basedir is enabled : + $obd = OC::$server->get(IniGetWrapper::class)->getString('open_basedir'); + if ($obd != 'none') { + $obd_values = explode(PATH_SEPARATOR, $obd); + if (count($obd_values) > 0 and $obd_values[0]) { + // open_basedir is in effect ! + // We need to check if the program is in one of these dirs : + $dirs = $obd_values; + } + } + foreach ($dirs as $dir) { + foreach ($exts as $ext) { + if ($check_fn("$dir/$name" . $ext)) { + return true; + } + } + } + return false; + } + + /** + * copy the contents of one stream to another + * + * @param resource $source + * @param resource $target + * @return array the number of bytes copied and result + * @deprecated 5.0.0 - Use \OCP\Files::streamCopy + */ + public static function streamCopy($source, $target) { + return \OCP\Files::streamCopy($source, $target, true); + } + + /** + * Adds a suffix to the name in case the file exists + * + * @param string $path + * @param string $filename + * @return string + */ + public static function buildNotExistingFileName($path, $filename) { + $view = \OC\Files\Filesystem::getView(); + return self::buildNotExistingFileNameForView($path, $filename, $view); + } + + /** + * Adds a suffix to the name in case the file exists + * + * @param string $path + * @param string $filename + * @return string + */ + public static function buildNotExistingFileNameForView($path, $filename, \OC\Files\View $view) { + if ($path === '/') { + $path = ''; + } + if ($pos = strrpos($filename, '.')) { + $name = substr($filename, 0, $pos); + $ext = substr($filename, $pos); + } else { + $name = $filename; + $ext = ''; + } + + $newpath = $path . '/' . $filename; + if ($view->file_exists($newpath)) { + if (preg_match_all('/\((\d+)\)/', $name, $matches, PREG_OFFSET_CAPTURE)) { + //Replace the last "(number)" with "(number+1)" + $last_match = count($matches[0]) - 1; + $counter = $matches[1][$last_match][0] + 1; + $offset = $matches[0][$last_match][1]; + $match_length = strlen($matches[0][$last_match][0]); + } else { + $counter = 2; + $match_length = 0; + $offset = false; + } + do { + if ($offset) { + //Replace the last "(number)" with "(number+1)" + $newname = substr_replace($name, '(' . $counter . ')', $offset, $match_length); + } else { + $newname = $name . ' (' . $counter . ')'; + } + $newpath = $path . '/' . $newname . $ext; + $counter++; + } while ($view->file_exists($newpath)); + } + + return $newpath; + } + + /** + * Checks if a function is available + * + * @deprecated 25.0.0 use \OCP\Util::isFunctionEnabled instead + */ + public static function is_function_enabled(string $function_name): bool { + return Util::isFunctionEnabled($function_name); + } + + /** + * Try to find a program + * @deprecated 25.0.0 Use \OCP\IBinaryFinder directly + */ + public static function findBinaryPath(string $program): ?string { + $result = Server::get(IBinaryFinder::class)->findBinaryPath($program); + return $result !== false ? $result : null; + } + + /** + * Calculate the disc space for the given path + * + * BEWARE: this requires that Util::setupFS() was called + * already ! + * + * @param string $path + * @param \OCP\Files\FileInfo $rootInfo (optional) + * @param bool $includeMountPoints whether to include mount points in the size calculation + * @param bool $useCache whether to use the cached quota values + * @psalm-suppress LessSpecificReturnStatement Legacy code outputs weird types - manually validated that they are correct + * @return StorageInfo + * @throws \OCP\Files\NotFoundException + */ + public static function getStorageInfo($path, $rootInfo = null, $includeMountPoints = true, $useCache = true) { + if (!self::$cacheFactory) { + self::$cacheFactory = Server::get(ICacheFactory::class); + } + $memcache = self::$cacheFactory->createLocal('storage_info'); + + // return storage info without adding mount points + if (self::$quotaIncludeExternalStorage === null) { + self::$quotaIncludeExternalStorage = \OC::$server->getSystemConfig()->getValue('quota_include_external_storage', false); + } + + $view = Filesystem::getView(); + if (!$view) { + throw new \OCP\Files\NotFoundException(); + } + $fullPath = Filesystem::normalizePath($view->getAbsolutePath($path)); + + $cacheKey = $fullPath . '::' . ($includeMountPoints ? 'include' : 'exclude'); + if ($useCache) { + $cached = $memcache->get($cacheKey); + if ($cached) { + return $cached; + } + } + + if (!$rootInfo) { + $rootInfo = \OC\Files\Filesystem::getFileInfo($path, self::$quotaIncludeExternalStorage ? 'ext' : false); + } + if (!$rootInfo instanceof \OCP\Files\FileInfo) { + throw new \OCP\Files\NotFoundException('The root directory of the user\'s files is missing'); + } + $used = $rootInfo->getSize($includeMountPoints); + if ($used < 0) { + $used = 0.0; + } + /** @var int|float $quota */ + $quota = \OCP\Files\FileInfo::SPACE_UNLIMITED; + $mount = $rootInfo->getMountPoint(); + $storage = $mount->getStorage(); + $sourceStorage = $storage; + if ($storage->instanceOfStorage('\OCA\Files_Sharing\SharedStorage')) { + self::$quotaIncludeExternalStorage = false; + } + if (self::$quotaIncludeExternalStorage) { + if ($storage->instanceOfStorage('\OC\Files\Storage\Home') + || $storage->instanceOfStorage('\OC\Files\ObjectStore\HomeObjectStoreStorage') + ) { + /** @var \OC\Files\Storage\Home $storage */ + $user = $storage->getUser(); + } else { + $user = \OC::$server->getUserSession()->getUser(); + } + $quota = $user?->getQuotaBytes() ?? \OCP\Files\FileInfo::SPACE_UNKNOWN; + if ($quota !== \OCP\Files\FileInfo::SPACE_UNLIMITED) { + // always get free space / total space from root + mount points + return self::getGlobalStorageInfo($quota, $user, $mount); + } + } + + // TODO: need a better way to get total space from storage + if ($sourceStorage->instanceOfStorage('\OC\Files\Storage\Wrapper\Quota')) { + /** @var \OC\Files\Storage\Wrapper\Quota $storage */ + $quota = $sourceStorage->getQuota(); + } + try { + $free = $sourceStorage->free_space($rootInfo->getInternalPath()); + if (is_bool($free)) { + $free = 0.0; + } + } catch (\Exception $e) { + if ($path === '') { + throw $e; + } + /** @var LoggerInterface $logger */ + $logger = \OC::$server->get(LoggerInterface::class); + $logger->warning('Error while getting quota info, using root quota', ['exception' => $e]); + $rootInfo = self::getStorageInfo(''); + $memcache->set($cacheKey, $rootInfo, 5 * 60); + return $rootInfo; + } + if ($free >= 0) { + $total = $free + $used; + } else { + $total = $free; //either unknown or unlimited + } + if ($total > 0) { + if ($quota > 0 && $total > $quota) { + $total = $quota; + } + // prevent division by zero or error codes (negative values) + $relative = round(($used / $total) * 10000) / 100; + } else { + $relative = 0; + } + + /* + * \OCA\Files_Sharing\External\Storage returns the cloud ID as the owner for the storage. + * It is unnecessary to query the user manager for the display name, as it won't have this information. + */ + $isRemoteShare = $storage->instanceOfStorage(\OCA\Files_Sharing\External\Storage::class); + + $ownerId = $storage->getOwner($path); + $ownerDisplayName = ''; + + if ($isRemoteShare === false && $ownerId !== false) { + $ownerDisplayName = \OC::$server->getUserManager()->getDisplayName($ownerId) ?? ''; + } + + if (substr_count($mount->getMountPoint(), '/') < 3) { + $mountPoint = ''; + } else { + [,,,$mountPoint] = explode('/', $mount->getMountPoint(), 4); + } + + $info = [ + 'free' => $free, + 'used' => $used, + 'quota' => $quota, + 'total' => $total, + 'relative' => $relative, + 'owner' => $ownerId, + 'ownerDisplayName' => $ownerDisplayName, + 'mountType' => $mount->getMountType(), + 'mountPoint' => trim($mountPoint, '/'), + ]; + + if ($isRemoteShare === false && $ownerId !== false && $path === '/') { + // If path is root, store this as last known quota usage for this user + \OCP\Server::get(\OCP\IConfig::class)->setUserValue($ownerId, 'files', 'lastSeenQuotaUsage', (string)$relative); + } + + $memcache->set($cacheKey, $info, 5 * 60); + + return $info; + } + + /** + * Get storage info including all mount points and quota + * + * @psalm-suppress LessSpecificReturnStatement Legacy code outputs weird types - manually validated that they are correct + * @return StorageInfo + */ + private static function getGlobalStorageInfo(int|float $quota, IUser $user, IMountPoint $mount): array { + $rootInfo = \OC\Files\Filesystem::getFileInfo('', 'ext'); + /** @var int|float $used */ + $used = $rootInfo['size']; + if ($used < 0) { + $used = 0.0; + } + + $total = $quota; + /** @var int|float $free */ + $free = $quota - $used; + + if ($total > 0) { + if ($quota > 0 && $total > $quota) { + $total = $quota; + } + // prevent division by zero or error codes (negative values) + $relative = round(($used / $total) * 10000) / 100; + } else { + $relative = 0.0; + } + + if (substr_count($mount->getMountPoint(), '/') < 3) { + $mountPoint = ''; + } else { + [,,,$mountPoint] = explode('/', $mount->getMountPoint(), 4); + } + + return [ + 'free' => $free, + 'used' => $used, + 'total' => $total, + 'relative' => $relative, + 'quota' => $quota, + 'owner' => $user->getUID(), + 'ownerDisplayName' => $user->getDisplayName(), + 'mountType' => $mount->getMountType(), + 'mountPoint' => trim($mountPoint, '/'), + ]; + } + + public static function clearStorageInfo(string $absolutePath): void { + /** @var ICacheFactory $cacheFactory */ + $cacheFactory = \OC::$server->get(ICacheFactory::class); + $memcache = $cacheFactory->createLocal('storage_info'); + $cacheKeyPrefix = Filesystem::normalizePath($absolutePath) . '::'; + $memcache->remove($cacheKeyPrefix . 'include'); + $memcache->remove($cacheKeyPrefix . 'exclude'); + } + + /** + * Returns whether the config file is set manually to read-only + * @return bool + * @deprecated 32.0.0 use the `config_is_read_only` system config directly + */ + public static function isReadOnlyConfigEnabled() { + return \OC::$server->getConfig()->getSystemValueBool('config_is_read_only', false); + } +} diff --git a/lib/private/legacy/OC_Hook.php b/lib/private/legacy/OC_Hook.php new file mode 100644 index 00000000000..e472b105498 --- /dev/null +++ b/lib/private/legacy/OC_Hook.php @@ -0,0 +1,125 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ + +use Psr\Log\LoggerInterface; + +class OC_Hook { + public static $thrownExceptions = []; + + private static $registered = []; + + /** + * connects a function to a hook + * + * @param string $signalClass class name of emitter + * @param string $signalName name of signal + * @param string|object $slotClass class name of slot + * @param string $slotName name of slot + * @return bool + * + * This function makes it very easy to connect to use hooks. + * + * TODO: write example + */ + public static function connect($signalClass, $signalName, $slotClass, $slotName) { + // If we're trying to connect to an emitting class that isn't + // yet registered, register it + if (!array_key_exists($signalClass, self::$registered)) { + self::$registered[$signalClass] = []; + } + // If we're trying to connect to an emitting method that isn't + // yet registered, register it with the emitting class + if (!array_key_exists($signalName, self::$registered[$signalClass])) { + self::$registered[$signalClass][$signalName] = []; + } + + // don't connect hooks twice + foreach (self::$registered[$signalClass][$signalName] as $hook) { + if ($hook['class'] === $slotClass and $hook['name'] === $slotName) { + return false; + } + } + // Connect the hook handler to the requested emitter + self::$registered[$signalClass][$signalName][] = [ + 'class' => $slotClass, + 'name' => $slotName + ]; + + // No chance for failure ;-) + return true; + } + + /** + * emits a signal + * + * @param string $signalClass class name of emitter + * @param string $signalName name of signal + * @param mixed $params default: array() array with additional data + * @return bool true if slots exists or false if not + * @throws \OCP\HintException + * @throws \OC\ServerNotAvailableException Emits a signal. To get data from the slot use references! + * + * TODO: write example + */ + public static function emit($signalClass, $signalName, $params = []) { + // Return false if no hook handlers are listening to this + // emitting class + if (!array_key_exists($signalClass, self::$registered)) { + return false; + } + + // Return false if no hook handlers are listening to this + // emitting method + if (!array_key_exists($signalName, self::$registered[$signalClass])) { + return false; + } + + // Call all slots + foreach (self::$registered[$signalClass][$signalName] as $i) { + try { + call_user_func([ $i['class'], $i['name'] ], $params); + } catch (Exception $e) { + self::$thrownExceptions[] = $e; + \OCP\Server::get(LoggerInterface::class)->error($e->getMessage(), ['exception' => $e]); + if ($e instanceof \OCP\HintException) { + throw $e; + } + if ($e instanceof \OC\ServerNotAvailableException) { + throw $e; + } + } + } + + return true; + } + + /** + * clear hooks + * @param string $signalClass + * @param string $signalName + */ + public static function clear($signalClass = '', $signalName = '') { + if ($signalClass) { + if ($signalName) { + self::$registered[$signalClass][$signalName] = []; + } else { + self::$registered[$signalClass] = []; + } + } else { + self::$registered = []; + } + } + + /** + * DO NOT USE! + * For unit tests ONLY! + */ + public static function getHooks() { + return self::$registered; + } +} diff --git a/lib/private/legacy/OC_JSON.php b/lib/private/legacy/OC_JSON.php new file mode 100644 index 00000000000..6daef18dd61 --- /dev/null +++ b/lib/private/legacy/OC_JSON.php @@ -0,0 +1,105 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ + +use OC\Authentication\TwoFactorAuth\Manager as TwoFactorAuthManager; + +class OC_JSON { + /** + * Check if the app is enabled, send json error msg if not + * @param string $app + * @deprecated 12.0.0 Use the AppFramework instead. It will automatically check if the app is enabled. + * @suppress PhanDeprecatedFunction + */ + public static function checkAppEnabled($app) { + if (!\OC::$server->getAppManager()->isEnabledForUser($app)) { + $l = \OC::$server->getL10N('lib'); + self::error([ 'data' => [ 'message' => $l->t('Application is not enabled'), 'error' => 'application_not_enabled' ]]); + exit(); + } + } + + /** + * Check if the user is logged in, send json error msg if not + * @deprecated 12.0.0 Use annotation based ACLs from the AppFramework instead + * @suppress PhanDeprecatedFunction + */ + public static function checkLoggedIn() { + $twoFactorAuthManger = \OC::$server->get(TwoFactorAuthManager::class); + if (!\OC::$server->getUserSession()->isLoggedIn() + || $twoFactorAuthManger->needsSecondFactor(\OC::$server->getUserSession()->getUser())) { + $l = \OC::$server->getL10N('lib'); + http_response_code(\OCP\AppFramework\Http::STATUS_UNAUTHORIZED); + self::error([ 'data' => [ 'message' => $l->t('Authentication error'), 'error' => 'authentication_error' ]]); + exit(); + } + } + + /** + * Check an ajax get/post call if the request token is valid, send json error msg if not. + * @deprecated 12.0.0 Use annotation based CSRF checks from the AppFramework instead + * @suppress PhanDeprecatedFunction + */ + public static function callCheck() { + if (!\OC::$server->getRequest()->passesStrictCookieCheck()) { + header('Location: ' . \OC::$WEBROOT); + exit(); + } + + if (!\OC::$server->getRequest()->passesCSRFCheck()) { + $l = \OC::$server->getL10N('lib'); + self::error([ 'data' => [ 'message' => $l->t('Token expired. Please reload page.'), 'error' => 'token_expired' ]]); + exit(); + } + } + + /** + * Check if the user is a admin, send json error msg if not. + * @deprecated 12.0.0 Use annotation based ACLs from the AppFramework instead + * @suppress PhanDeprecatedFunction + */ + public static function checkAdminUser() { + if (!OC_User::isAdminUser(OC_User::getUser())) { + $l = \OC::$server->getL10N('lib'); + self::error([ 'data' => [ 'message' => $l->t('Authentication error'), 'error' => 'authentication_error' ]]); + exit(); + } + } + + /** + * Send json error msg + * @deprecated 12.0.0 Use a AppFramework JSONResponse instead + * @suppress PhanDeprecatedFunction + */ + public static function error($data = []) { + $data['status'] = 'error'; + header('Content-Type: application/json; charset=utf-8'); + echo self::encode($data); + } + + /** + * Send json success msg + * @deprecated 12.0.0 Use a AppFramework JSONResponse instead + * @suppress PhanDeprecatedFunction + */ + public static function success($data = []) { + $data['status'] = 'success'; + header('Content-Type: application/json; charset=utf-8'); + echo self::encode($data); + } + + /** + * Encode JSON + * @deprecated 12.0.0 Use a AppFramework JSONResponse instead + * + * @psalm-taint-escape has_quotes + * @psalm-taint-escape html + */ + private static function encode($data) { + return json_encode($data, JSON_HEX_TAG); + } +} diff --git a/lib/private/legacy/OC_Response.php b/lib/private/legacy/OC_Response.php new file mode 100644 index 00000000000..c45852b4b1d --- /dev/null +++ b/lib/private/legacy/OC_Response.php @@ -0,0 +1,83 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +class OC_Response { + /** + * Sets the content disposition header (with possible workarounds) + * @param string $filename file name + * @param string $type disposition type, either 'attachment' or 'inline' + */ + public static function setContentDispositionHeader($filename, $type = 'attachment') { + if (\OC::$server->getRequest()->isUserAgent( + [ + \OC\AppFramework\Http\Request::USER_AGENT_IE, + \OC\AppFramework\Http\Request::USER_AGENT_ANDROID_MOBILE_CHROME, + \OC\AppFramework\Http\Request::USER_AGENT_FREEBOX, + ])) { + header('Content-Disposition: ' . rawurlencode($type) . '; filename="' . rawurlencode($filename) . '"'); + } else { + header('Content-Disposition: ' . rawurlencode($type) . '; filename*=UTF-8\'\'' . rawurlencode($filename) + . '; filename="' . rawurlencode($filename) . '"'); + } + } + + /** + * Sets the content length header (with possible workarounds) + * @param string|int|float $length Length to be sent + */ + public static function setContentLengthHeader($length) { + if (PHP_INT_SIZE === 4) { + if ($length > PHP_INT_MAX && stripos(PHP_SAPI, 'apache') === 0) { + // Apache PHP SAPI casts Content-Length headers to PHP integers. + // This enforces a limit of PHP_INT_MAX (2147483647 on 32-bit + // platforms). So, if the length is greater than PHP_INT_MAX, + // we just do not send a Content-Length header to prevent + // bodies from being received incompletely. + return; + } + // Convert signed integer or float to unsigned base-10 string. + $lfh = new \OC\LargeFileHelper; + $length = $lfh->formatUnsignedInteger($length); + } + header('Content-Length: ' . $length); + } + + /** + * 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. + */ + public static function addSecurityHeaders() { + /** + * FIXME: Content Security Policy for legacy ownCloud 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-' . \OC::$server->getContentSecurityPolicyNonceManager()->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 + } + } +} diff --git a/lib/private/legacy/OC_Template.php b/lib/private/legacy/OC_Template.php new file mode 100644 index 00000000000..bccf99af65e --- /dev/null +++ b/lib/private/legacy/OC_Template.php @@ -0,0 +1,51 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +use OCP\Server; +use OCP\Template\ITemplateManager; + +/** + * This class provides the templates for ownCloud. + * @deprecated 32.0.0 Use \OCP\Template\ITemplateManager instead + */ +class OC_Template extends \OC\Template\Template { + /** + * Shortcut to print a simple page for guests + * @param string $application The application we render the template for + * @param string $name Name of the template + * @param array $parameters Parameters for the template + * @return bool + * @deprecated 32.0.0 Use \OCP\Template\ITemplateManager instead + */ + public static function printGuestPage($application, $name, $parameters = []) { + Server::get(ITemplateManager::class)->printGuestPage($application, $name, $parameters); + return true; + } + + /** + * Print a fatal error page and terminates the script + * @param string $error_msg The error message to show + * @param string $hint An optional hint message - needs to be properly escape + * @param int $statusCode + * @return never + * @deprecated 32.0.0 Use \OCP\Template\ITemplateManager instead + */ + public static function printErrorPage($error_msg, $hint = '', $statusCode = 500) { + Server::get(ITemplateManager::class)->printErrorPage($error_msg, $hint, $statusCode); + } + + /** + * print error page using Exception details + * @param Exception|Throwable $exception + * @param int $statusCode + * @return never + * @deprecated 32.0.0 Use \OCP\Template\ITemplateManager instead + */ + public static function printExceptionErrorPage($exception, $statusCode = 503) { + Server::get(ITemplateManager::class)->printExceptionErrorPage($exception, $statusCode); + } +} diff --git a/lib/private/legacy/OC_User.php b/lib/private/legacy/OC_User.php new file mode 100644 index 00000000000..e5343864c45 --- /dev/null +++ b/lib/private/legacy/OC_User.php @@ -0,0 +1,405 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +use OC\Authentication\Token\IProvider; +use OC\User\DisabledUserException; +use OCP\Authentication\Exceptions\InvalidTokenException; +use OCP\Authentication\Exceptions\WipeTokenException; +use OCP\Authentication\Token\IToken; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IGroupManager; +use OCP\ISession; +use OCP\IUser; +use OCP\IUserManager; +use OCP\Server; +use OCP\Session\Exceptions\SessionNotAvailableException; +use OCP\User\Events\BeforeUserLoggedInEvent; +use OCP\User\Events\UserLoggedInEvent; +use Psr\Log\LoggerInterface; + +/** + * This class provides wrapper methods for user management. Multiple backends are + * supported. User management operations are delegated to the configured backend for + * execution. + * + * Note that &run is deprecated and won't work anymore. + * + * Hooks provided: + * pre_createUser(&run, uid, password) + * post_createUser(uid, password) + * pre_deleteUser(&run, uid) + * post_deleteUser(uid) + * pre_setPassword(&run, uid, password, recoveryPassword) + * post_setPassword(uid, password, recoveryPassword) + * pre_login(&run, uid, password) + * post_login(uid) + * logout() + */ +class OC_User { + private 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; + + /** + * Adds the backend to the list of used backends + * + * @param string|\OCP\UserInterface $backend default: database The backend to use for user management + * @return bool + * @deprecated 32.0.0 Use IUserManager::registerBackend instead + * + * Set the User Authentication Module + */ + public static function useBackend($backend = 'database') { + if ($backend instanceof \OCP\UserInterface) { + Server::get(IUserManager::class)->registerBackend($backend); + } else { + // You'll never know what happens + if ($backend === null or !is_string($backend)) { + $backend = 'database'; + } + + // Load backend + switch ($backend) { + case 'database': + case 'mysql': + case 'sqlite': + Server::get(LoggerInterface::class)->debug('Adding user backend ' . $backend . '.', ['app' => 'core']); + Server::get(IUserManager::class)->registerBackend(new \OC\User\Database()); + break; + case 'dummy': + Server::get(IUserManager::class)->registerBackend(new \Test\Util\User\Dummy()); + break; + default: + Server::get(LoggerInterface::class)->debug('Adding default user backend ' . $backend . '.', ['app' => 'core']); + $className = 'OC_USER_' . strtoupper($backend); + Server::get(IUserManager::class)->registerBackend(new $className()); + break; + } + } + return true; + } + + /** + * remove all used backends + * @deprecated 32.0.0 Use IUserManager::clearBackends instead + */ + public static function clearBackends() { + Server::get(IUserManager::class)->clearBackends(); + } + + /** + * setup the configured backends in config.php + * @suppress PhanDeprecatedFunction + */ + public static function setupBackends() { + OC_App::loadApps(['prelogin']); + $backends = \OC::$server->getSystemConfig()->getValue('user_backends', []); + if (isset($backends['default']) && !$backends['default']) { + // clear default backends + self::clearBackends(); + } + foreach ($backends as $i => $config) { + if (!is_array($config)) { + continue; + } + $class = $config['class']; + $arguments = $config['arguments']; + if (class_exists($class)) { + if (!in_array($i, self::$_setupedBackends)) { + // make a reflection object + $reflectionObj = new ReflectionClass($class); + + // use Reflection to create a new instance, using the $args + $backend = $reflectionObj->newInstanceArgs($arguments); + self::useBackend($backend); + self::$_setupedBackends[] = $i; + } else { + Server::get(LoggerInterface::class)->debug('User backend ' . $class . ' already initialized.', ['app' => 'core']); + } + } else { + Server::get(LoggerInterface::class)->error('User backend ' . $class . ' not found.', ['app' => 'core']); + } + } + } + + /** + * Try to login a user, assuming authentication + * has already happened (e.g. via Single Sign On). + * + * Log in a user and regenerate a new session. + * + * @param \OCP\Authentication\IApacheBackend $backend + * @return bool + */ + public static function loginWithApache(\OCP\Authentication\IApacheBackend $backend) { + $uid = $backend->getCurrentUserId(); + $run = true; + OC_Hook::emit('OC_User', 'pre_login', ['run' => &$run, 'uid' => $uid, 'backend' => $backend]); + + if ($uid) { + if (self::getUser() !== $uid) { + self::setUserId($uid); + $userSession = \OC::$server->getUserSession(); + + /** @var IEventDispatcher $dispatcher */ + $dispatcher = \OC::$server->get(IEventDispatcher::class); + + if ($userSession->getUser() && !$userSession->getUser()->isEnabled()) { + $message = \OC::$server->getL10N('lib')->t('Account disabled'); + throw new DisabledUserException($message); + } + $userSession->setLoginName($uid); + $request = OC::$server->getRequest(); + $password = null; + if ($backend instanceof \OCP\Authentication\IProvideUserSecretBackend) { + $password = $backend->getCurrentUserSecret(); + } + + /** @var IEventDispatcher $dispatcher */ + $dispatcher->dispatchTyped(new BeforeUserLoggedInEvent($uid, $password, $backend)); + + $userSession->createSessionToken($request, $uid, $uid, $password); + $userSession->createRememberMeToken($userSession->getUser()); + + if (empty($password)) { + $tokenProvider = \OC::$server->get(IProvider::class); + try { + $token = $tokenProvider->getToken($userSession->getSession()->getId()); + $token->setScope([ + IToken::SCOPE_SKIP_PASSWORD_VALIDATION => true, + IToken::SCOPE_FILESYSTEM => true, + ]); + $tokenProvider->updateToken($token); + } catch (InvalidTokenException|WipeTokenException|SessionNotAvailableException) { + // swallow the exceptions as we do not deal with them here + // simply skip updating the token when is it missing + } + } + + // setup the filesystem + OC_Util::setupFS($uid); + // first call the post_login hooks, the login-process needs to be + // completed before we can safely create the users folder. + // For example encryption needs to initialize the users keys first + // before we can create the user folder with the skeleton files + OC_Hook::emit( + 'OC_User', + 'post_login', + [ + 'uid' => $uid, + 'password' => $password, + 'isTokenLogin' => false, + ] + ); + $dispatcher->dispatchTyped(new UserLoggedInEvent( + \OC::$server->get(IUserManager::class)->get($uid), + $uid, + null, + false) + ); + + //trigger creation of user home and /files folder + \OC::$server->getUserFolder($uid); + } + return true; + } + return false; + } + + /** + * Verify with Apache whether user is authenticated. + * + * @return boolean|null + * true: authenticated + * false: not authenticated + * null: not handled / no backend available + */ + public static function handleApacheAuth() { + $backend = self::findFirstActiveUsedBackend(); + if ($backend) { + OC_App::loadApps(); + + //setup extra user backends + self::setupBackends(); + \OC::$server->getUserSession()->unsetMagicInCookie(); + + return self::loginWithApache($backend); + } + + return null; + } + + + /** + * Sets user id for session and triggers emit + * + * @param string $uid + */ + public static function setUserId($uid) { + $userSession = \OC::$server->getUserSession(); + $userManager = Server::get(IUserManager::class); + if ($user = $userManager->get($uid)) { + $userSession->setUser($user); + } else { + \OC::$server->getSession()->set('user_id', $uid); + } + } + + /** + * Check if the user is logged in, considers also the HTTP basic credentials + * + * @deprecated 12.0.0 use \OC::$server->getUserSession()->isLoggedIn() + * @return bool + */ + public static function isLoggedIn() { + return \OC::$server->getUserSession()->isLoggedIn(); + } + + /** + * set incognito mode, e.g. if a user wants to open a public link + * + * @param bool $status + */ + public static function setIncognitoMode($status) { + self::$incognitoMode = $status; + } + + /** + * get incognito mode status + * + * @return bool + */ + public static function isIncognitoMode() { + return self::$incognitoMode; + } + + /** + * Returns the current logout URL valid for the currently logged-in user + * + * @param \OCP\IURLGenerator $urlGenerator + * @return string + */ + public static function getLogoutUrl(\OCP\IURLGenerator $urlGenerator) { + $backend = self::findFirstActiveUsedBackend(); + if ($backend) { + return $backend->getLogoutUrl(); + } + + $user = \OC::$server->getUserSession()->getUser(); + if ($user instanceof IUser) { + $backend = $user->getBackend(); + if ($backend instanceof \OCP\User\Backend\ICustomLogout) { + return $backend->getLogoutUrl(); + } + } + + $logoutUrl = $urlGenerator->linkToRoute('core.login.logout'); + $logoutUrl .= '?requesttoken=' . urlencode(\OCP\Util::callRegister()); + + return $logoutUrl; + } + + /** + * Check if the user is an admin user + * + * @param string $uid uid of the admin + * @return bool + */ + public static function isAdminUser($uid) { + $user = Server::get(IUserManager::class)->get($uid); + $isAdmin = $user && Server::get(IGroupManager::class)->isAdmin($user->getUID()); + return $isAdmin && self::$incognitoMode === false; + } + + + /** + * get the user id of the user currently logged in. + * + * @return string|false uid or false + */ + public static function getUser() { + $uid = Server::get(ISession::class)?->get('user_id'); + if (!is_null($uid) && self::$incognitoMode === false) { + return $uid; + } else { + return false; + } + } + + /** + * Set password + * + * @param string $uid The username + * @param string $password The new password + * @param string $recoveryPassword for the encryption app to reset encryption keys + * @return bool + * + * Change the password of a user + */ + public static function setPassword($uid, $password, $recoveryPassword = null) { + $user = Server::get(IUserManager::class)->get($uid); + if ($user) { + return $user->setPassword($password, $recoveryPassword); + } else { + return false; + } + } + + /** + * @param string $uid The username + * @return string + * + * returns the path to the users home directory + * @deprecated 12.0.0 Use \OC::$server->getUserManager->getHome() + */ + public static function getHome($uid) { + $user = Server::get(IUserManager::class)->get($uid); + if ($user) { + return $user->getHome(); + } else { + return \OC::$server->getSystemConfig()->getValue('datadirectory', OC::$SERVERROOT . '/data') . '/' . $uid; + } + } + + /** + * Get a list of all users display name + * + * @param string $search + * @param int $limit + * @param int $offset + * @return array associative array with all display names (value) and corresponding uids (key) + * + * Get a list of all display names and user ids. + * @deprecated 12.0.0 Use \OC::$server->getUserManager->searchDisplayName($search, $limit, $offset) instead. + */ + public static function getDisplayNames($search = '', $limit = null, $offset = null) { + $displayNames = []; + $users = Server::get(IUserManager::class)->searchDisplayName($search, $limit, $offset); + foreach ($users as $user) { + $displayNames[$user->getUID()] = $user->getDisplayName(); + } + return $displayNames; + } + + /** + * Returns the first active backend from self::$_usedBackends. + * + * @return OCP\Authentication\IApacheBackend|null if no backend active, otherwise OCP\Authentication\IApacheBackend + */ + private static function findFirstActiveUsedBackend() { + foreach (Server::get(IUserManager::class)->getBackends() as $backend) { + if ($backend instanceof OCP\Authentication\IApacheBackend) { + if ($backend->isSessionActive()) { + return $backend; + } + } + } + + return null; + } +} diff --git a/lib/private/legacy/OC_Util.php b/lib/private/legacy/OC_Util.php new file mode 100644 index 00000000000..ebca1105838 --- /dev/null +++ b/lib/private/legacy/OC_Util.php @@ -0,0 +1,847 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only + */ +use bantu\IniGetWrapper\IniGetWrapper; +use OC\Authentication\TwoFactorAuth\Manager as TwoFactorAuthManager; +use OC\Files\SetupManager; +use OCP\Files\Template\ITemplateManager; +use OCP\IConfig; +use OCP\IGroupManager; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\L10N\IFactory; +use OCP\Security\ISecureRandom; +use OCP\Share\IManager; +use Psr\Log\LoggerInterface; + +/** + * @deprecated 32.0.0 Use \OCP\Util or any appropriate official API instead + */ +class OC_Util { + public static $styles = []; + public static $headers = []; + + /** + * Setup the file system + * + * @param string|null $user + * @return boolean + * @description configure the initial filesystem based on the configuration + * @suppress PhanDeprecatedFunction + * @suppress PhanAccessMethodInternal + */ + public static function setupFS(?string $user = '') { + // If we are not forced to load a specific user we load the one that is logged in + if ($user === '') { + $userObject = \OC::$server->get(\OCP\IUserSession::class)->getUser(); + } else { + $userObject = \OC::$server->get(\OCP\IUserManager::class)->get($user); + } + + /** @var SetupManager $setupManager */ + $setupManager = \OC::$server->get(SetupManager::class); + + if ($userObject) { + $setupManager->setupForUser($userObject); + } else { + $setupManager->setupRoot(); + } + return true; + } + + /** + * Check if a password is required for each public link + * + * @param bool $checkGroupMembership Check group membership exclusion + * @return bool + * @deprecated 32.0.0 use OCP\Share\IManager's shareApiLinkEnforcePassword directly + */ + public static function isPublicLinkPasswordRequired(bool $checkGroupMembership = true) { + /** @var IManager $shareManager */ + $shareManager = \OC::$server->get(IManager::class); + return $shareManager->shareApiLinkEnforcePassword($checkGroupMembership); + } + + /** + * check if sharing is disabled for the current user + * @param IConfig $config + * @param IGroupManager $groupManager + * @param IUser|null $user + * @return bool + * @deprecated 32.0.0 use OCP\Share\IManager's sharingDisabledForUser directly + */ + public static function isSharingDisabledForUser(IConfig $config, IGroupManager $groupManager, $user) { + /** @var IManager $shareManager */ + $shareManager = \OC::$server->get(IManager::class); + $userId = $user ? $user->getUID() : null; + return $shareManager->sharingDisabledForUser($userId); + } + + /** + * check if share API enforces a default expire date + * + * @return bool + * @deprecated 32.0.0 use OCP\Share\IManager's shareApiLinkDefaultExpireDateEnforced directly + */ + public static function isDefaultExpireDateEnforced() { + /** @var IManager $shareManager */ + $shareManager = \OC::$server->get(IManager::class); + return $shareManager->shareApiLinkDefaultExpireDateEnforced(); + } + + /** + * Get the quota of a user + * + * @param IUser|null $user + * @return int|\OCP\Files\FileInfo::SPACE_UNLIMITED|false|float Quota bytes + * @deprecated 9.0.0 - Use \OCP\IUser::getQuota or \OCP\IUser::getQuotaBytes + */ + public static function getUserQuota(?IUser $user) { + if (is_null($user)) { + return \OCP\Files\FileInfo::SPACE_UNLIMITED; + } + $userQuota = $user->getQuota(); + if ($userQuota === 'none') { + return \OCP\Files\FileInfo::SPACE_UNLIMITED; + } + return \OCP\Util::computerFileSize($userQuota); + } + + /** + * copies the skeleton to the users /files + * + * @param string $userId + * @param \OCP\Files\Folder $userDirectory + * @throws \OCP\Files\NotFoundException + * @throws \OCP\Files\NotPermittedException + * @suppress PhanDeprecatedFunction + */ + public static function copySkeleton($userId, \OCP\Files\Folder $userDirectory) { + /** @var LoggerInterface $logger */ + $logger = \OC::$server->get(LoggerInterface::class); + + $plainSkeletonDirectory = \OC::$server->getConfig()->getSystemValueString('skeletondirectory', \OC::$SERVERROOT . '/core/skeleton'); + $userLang = \OC::$server->get(IFactory::class)->findLanguage(); + $skeletonDirectory = str_replace('{lang}', $userLang, $plainSkeletonDirectory); + + if (!file_exists($skeletonDirectory)) { + $dialectStart = strpos($userLang, '_'); + if ($dialectStart !== false) { + $skeletonDirectory = str_replace('{lang}', substr($userLang, 0, $dialectStart), $plainSkeletonDirectory); + } + if ($dialectStart === false || !file_exists($skeletonDirectory)) { + $skeletonDirectory = str_replace('{lang}', 'default', $plainSkeletonDirectory); + } + if (!file_exists($skeletonDirectory)) { + $skeletonDirectory = ''; + } + } + + $instanceId = \OC::$server->getConfig()->getSystemValue('instanceid', ''); + + if ($instanceId === null) { + throw new \RuntimeException('no instance id!'); + } + $appdata = 'appdata_' . $instanceId; + if ($userId === $appdata) { + throw new \RuntimeException('username is reserved name: ' . $appdata); + } + + if (!empty($skeletonDirectory)) { + $logger->debug('copying skeleton for ' . $userId . ' from ' . $skeletonDirectory . ' to ' . $userDirectory->getFullPath('/'), ['app' => 'files_skeleton']); + self::copyr($skeletonDirectory, $userDirectory); + // update the file cache + $userDirectory->getStorage()->getScanner()->scan('', \OC\Files\Cache\Scanner::SCAN_RECURSIVE); + + /** @var ITemplateManager $templateManager */ + $templateManager = \OC::$server->get(ITemplateManager::class); + $templateManager->initializeTemplateDirectory(null, $userId); + } + } + + /** + * copies a directory recursively by using streams + * + * @param string $source + * @param \OCP\Files\Folder $target + * @return void + */ + public static function copyr($source, \OCP\Files\Folder $target) { + $logger = \OCP\Server::get(LoggerInterface::class); + + // Verify if folder exists + $dir = opendir($source); + if ($dir === false) { + $logger->error(sprintf('Could not opendir "%s"', $source), ['app' => 'core']); + return; + } + + // Copy the files + while (false !== ($file = readdir($dir))) { + if (!\OC\Files\Filesystem::isIgnoredDir($file)) { + if (is_dir($source . '/' . $file)) { + $child = $target->newFolder($file); + self::copyr($source . '/' . $file, $child); + } else { + $sourceStream = fopen($source . '/' . $file, 'r'); + if ($sourceStream === false) { + $logger->error(sprintf('Could not fopen "%s"', $source . '/' . $file), ['app' => 'core']); + closedir($dir); + return; + } + $target->newFile($file, $sourceStream); + } + } + } + closedir($dir); + } + + /** + * @deprecated 32.0.0 Call tearDown directly on SetupManager + */ + public static function tearDownFS(): void { + $setupManager = \OCP\Server::get(SetupManager::class); + $setupManager->tearDown(); + } + + /** + * generates a path for JS/CSS files. If no application is provided it will create the path for core. + * + * @param string $application application to get the files from + * @param string $directory directory within this application (css, js, vendor, etc) + * @param ?string $file the file inside of the above folder + */ + private static function generatePath($application, $directory, $file): string { + if (is_null($file)) { + $file = $application; + $application = ''; + } + if (!empty($application)) { + return "$application/$directory/$file"; + } else { + return "$directory/$file"; + } + } + + /** + * add a css file + * + * @param string $application application id + * @param string|null $file filename + * @param bool $prepend prepend the Style to the beginning of the list + * @deprecated 32.0.0 Use \OCP\Util::addStyle + */ + public static function addStyle($application, $file = null, $prepend = false): void { + $path = OC_Util::generatePath($application, 'css', $file); + self::addExternalResource($application, $prepend, $path, 'style'); + } + + /** + * add a css file from the vendor sub folder + * + * @param string $application application id + * @param string|null $file filename + * @param bool $prepend prepend the Style to the beginning of the list + * @deprecated 32.0.0 + */ + public static function addVendorStyle($application, $file = null, $prepend = false): void { + $path = OC_Util::generatePath($application, 'vendor', $file); + self::addExternalResource($application, $prepend, $path, 'style'); + } + + /** + * add an external resource css/js file + * + * @param string $application application id + * @param bool $prepend prepend the file to the beginning of the list + * @param string $path + * @param string $type (script or style) + */ + private static function addExternalResource($application, $prepend, $path, $type = 'script'): void { + if ($type === 'style') { + if (!in_array($path, self::$styles)) { + if ($prepend === true) { + array_unshift(self::$styles, $path); + } else { + self::$styles[] = $path; + } + } + } + } + + /** + * Add a custom element to the header + * If $text is null then the element will be written as empty element. + * So use "" to get a closing tag. + * @param string $tag tag name of the element + * @param array $attributes array of attributes for the element + * @param string $text the text content for the element + * @param bool $prepend prepend the header to the beginning of the list + * @deprecated 32.0.0 Use \OCP\Util::addHeader instead + */ + public static function addHeader($tag, $attributes, $text = null, $prepend = false): void { + $header = [ + 'tag' => $tag, + 'attributes' => $attributes, + 'text' => $text + ]; + if ($prepend === true) { + array_unshift(self::$headers, $header); + } else { + self::$headers[] = $header; + } + } + + /** + * check if the current server configuration is suitable for ownCloud + * + * @return array arrays with error messages and hints + */ + public static function checkServer(\OC\SystemConfig $config) { + $l = \OC::$server->getL10N('lib'); + $errors = []; + $CONFIG_DATADIRECTORY = $config->getValue('datadirectory', OC::$SERVERROOT . '/data'); + + if (!self::needUpgrade($config) && $config->getValue('installed', false)) { + // this check needs to be done every time + $errors = self::checkDataDirectoryValidity($CONFIG_DATADIRECTORY); + } + + // Assume that if checkServer() succeeded before in this session, then all is fine. + if (\OC::$server->getSession()->exists('checkServer_succeeded') && \OC::$server->getSession()->get('checkServer_succeeded')) { + return $errors; + } + + $webServerRestart = false; + $setup = \OCP\Server::get(\OC\Setup::class); + + $urlGenerator = \OC::$server->getURLGenerator(); + + $availableDatabases = $setup->getSupportedDatabases(); + if (empty($availableDatabases)) { + $errors[] = [ + 'error' => $l->t('No database drivers (sqlite, mysql, or postgresql) installed.'), + 'hint' => '' //TODO: sane hint + ]; + $webServerRestart = true; + } + + // Check if config folder is writable. + if (!(bool)$config->getValue('config_is_read_only', false)) { + if (!is_writable(OC::$configDir) or !is_readable(OC::$configDir)) { + $errors[] = [ + 'error' => $l->t('Cannot write into "config" directory.'), + 'hint' => $l->t('This can usually be fixed by giving the web server write access to the config directory. See %s', + [ $urlGenerator->linkToDocs('admin-dir_permissions') ]) . '. ' + . $l->t('Or, if you prefer to keep config.php file read only, set the option "config_is_read_only" to true in it. See %s', + [ $urlGenerator->linkToDocs('admin-config') ]) + ]; + } + } + + // Create root dir. + if ($config->getValue('installed', false)) { + if (!is_dir($CONFIG_DATADIRECTORY)) { + $success = @mkdir($CONFIG_DATADIRECTORY); + if ($success) { + $errors = array_merge($errors, self::checkDataDirectoryPermissions($CONFIG_DATADIRECTORY)); + } else { + $errors[] = [ + 'error' => $l->t('Cannot create "data" directory.'), + 'hint' => $l->t('This can usually be fixed by giving the web server write access to the root directory. See %s', + [$urlGenerator->linkToDocs('admin-dir_permissions')]) + ]; + } + } elseif (!is_writable($CONFIG_DATADIRECTORY) or !is_readable($CONFIG_DATADIRECTORY)) { + // is_writable doesn't work for NFS mounts, so try to write a file and check if it exists. + $testFile = sprintf('%s/%s.tmp', $CONFIG_DATADIRECTORY, uniqid('data_dir_writability_test_')); + $handle = fopen($testFile, 'w'); + if (!$handle || fwrite($handle, 'Test write operation') === false) { + $permissionsHint = $l->t('Permissions can usually be fixed by giving the web server write access to the root directory. See %s.', + [$urlGenerator->linkToDocs('admin-dir_permissions')]); + $errors[] = [ + 'error' => $l->t('Your data directory is not writable.'), + 'hint' => $permissionsHint + ]; + } else { + fclose($handle); + unlink($testFile); + } + } else { + $errors = array_merge($errors, self::checkDataDirectoryPermissions($CONFIG_DATADIRECTORY)); + } + } + + if (!OC_Util::isSetLocaleWorking()) { + $errors[] = [ + 'error' => $l->t('Setting locale to %s failed.', + ['en_US.UTF-8/fr_FR.UTF-8/es_ES.UTF-8/de_DE.UTF-8/ru_RU.UTF-8/' + . 'pt_BR.UTF-8/it_IT.UTF-8/ja_JP.UTF-8/zh_CN.UTF-8']), + 'hint' => $l->t('Please install one of these locales on your system and restart your web server.') + ]; + } + + // Contains the dependencies that should be checked against + // classes = class_exists + // functions = function_exists + // defined = defined + // ini = ini_get + // If the dependency is not found the missing module name is shown to the EndUser + // When adding new checks always verify that they pass on CI as well + $dependencies = [ + 'classes' => [ + 'ZipArchive' => 'zip', + 'DOMDocument' => 'dom', + 'XMLWriter' => 'XMLWriter', + 'XMLReader' => 'XMLReader', + ], + 'functions' => [ + 'xml_parser_create' => 'libxml', + 'mb_strcut' => 'mbstring', + 'ctype_digit' => 'ctype', + 'json_encode' => 'JSON', + 'gd_info' => 'GD', + 'gzencode' => 'zlib', + 'simplexml_load_string' => 'SimpleXML', + 'hash' => 'HASH Message Digest Framework', + 'curl_init' => 'cURL', + 'openssl_verify' => 'OpenSSL', + ], + 'defined' => [ + 'PDO::ATTR_DRIVER_NAME' => 'PDO' + ], + 'ini' => [ + 'default_charset' => 'UTF-8', + ], + ]; + $missingDependencies = []; + $invalidIniSettings = []; + + $iniWrapper = \OC::$server->get(IniGetWrapper::class); + foreach ($dependencies['classes'] as $class => $module) { + if (!class_exists($class)) { + $missingDependencies[] = $module; + } + } + foreach ($dependencies['functions'] as $function => $module) { + if (!function_exists($function)) { + $missingDependencies[] = $module; + } + } + foreach ($dependencies['defined'] as $defined => $module) { + if (!defined($defined)) { + $missingDependencies[] = $module; + } + } + foreach ($dependencies['ini'] as $setting => $expected) { + if (strtolower($iniWrapper->getString($setting)) !== strtolower($expected)) { + $invalidIniSettings[] = [$setting, $expected]; + } + } + + foreach ($missingDependencies as $missingDependency) { + $errors[] = [ + 'error' => $l->t('PHP module %s not installed.', [$missingDependency]), + 'hint' => $l->t('Please ask your server administrator to install the module.'), + ]; + $webServerRestart = true; + } + foreach ($invalidIniSettings as $setting) { + $errors[] = [ + 'error' => $l->t('PHP setting "%s" is not set to "%s".', [$setting[0], var_export($setting[1], true)]), + 'hint' => $l->t('Adjusting this setting in php.ini will make Nextcloud run again') + ]; + $webServerRestart = true; + } + + /** + * The mbstring.func_overload check can only be performed if the mbstring + * module is installed as it will return null if the checking setting is + * not available and thus a check on the boolean value fails. + * + * TODO: Should probably be implemented in the above generic dependency + * check somehow in the long-term. + */ + if ($iniWrapper->getBool('mbstring.func_overload') !== null + && $iniWrapper->getBool('mbstring.func_overload') === true) { + $errors[] = [ + 'error' => $l->t('<code>mbstring.func_overload</code> is set to <code>%s</code> instead of the expected value <code>0</code>.', [$iniWrapper->getString('mbstring.func_overload')]), + 'hint' => $l->t('To fix this issue set <code>mbstring.func_overload</code> to <code>0</code> in your php.ini.') + ]; + } + + if (!self::isAnnotationsWorking()) { + $errors[] = [ + 'error' => $l->t('PHP is apparently set up to strip inline doc blocks. This will make several core apps inaccessible.'), + 'hint' => $l->t('This is probably caused by a cache/accelerator such as Zend OPcache or eAccelerator.') + ]; + } + + if (!\OC::$CLI && $webServerRestart) { + $errors[] = [ + 'error' => $l->t('PHP modules have been installed, but they are still listed as missing?'), + 'hint' => $l->t('Please ask your server administrator to restart the web server.') + ]; + } + + foreach (['secret', 'instanceid', 'passwordsalt'] as $requiredConfig) { + if ($config->getValue($requiredConfig, '') === '' && !\OC::$CLI && $config->getValue('installed', false)) { + $errors[] = [ + 'error' => $l->t('The required %s config variable is not configured in the config.php file.', [$requiredConfig]), + 'hint' => $l->t('Please ask your server administrator to check the Nextcloud configuration.') + ]; + } + } + + // Cache the result of this function + \OC::$server->getSession()->set('checkServer_succeeded', count($errors) == 0); + + return $errors; + } + + /** + * Check for correct file permissions of data directory + * + * @param string $dataDirectory + * @return array arrays with error messages and hints + * @internal + */ + public static function checkDataDirectoryPermissions($dataDirectory) { + if (!\OC::$server->getConfig()->getSystemValueBool('check_data_directory_permissions', true)) { + return []; + } + + $perms = substr(decoct(@fileperms($dataDirectory)), -3); + if (substr($perms, -1) !== '0') { + chmod($dataDirectory, 0770); + clearstatcache(); + $perms = substr(decoct(@fileperms($dataDirectory)), -3); + if ($perms[2] !== '0') { + $l = \OC::$server->getL10N('lib'); + return [[ + 'error' => $l->t('Your data directory is readable by other people.'), + 'hint' => $l->t('Please change the permissions to 0770 so that the directory cannot be listed by other people.'), + ]]; + } + } + return []; + } + + /** + * Check that the data directory exists and is valid by + * checking the existence of the ".ncdata" file. + * + * @param string $dataDirectory data directory path + * @return array errors found + * @internal + */ + public static function checkDataDirectoryValidity($dataDirectory) { + $l = \OC::$server->getL10N('lib'); + $errors = []; + if ($dataDirectory[0] !== '/') { + $errors[] = [ + 'error' => $l->t('Your data directory must be an absolute path.'), + 'hint' => $l->t('Check the value of "datadirectory" in your configuration.') + ]; + } + + if (!file_exists($dataDirectory . '/.ncdata')) { + $errors[] = [ + 'error' => $l->t('Your data directory is invalid.'), + 'hint' => $l->t('Ensure there is a file called "%1$s" in the root of the data directory. It should have the content: "%2$s"', ['.ncdata', '# Nextcloud data directory']), + ]; + } + return $errors; + } + + /** + * Check if the user is logged in, redirects to home if not. With + * redirect URL parameter to the request URI. + * + * @deprecated 32.0.0 + */ + public static function checkLoggedIn(): void { + // Check if we are a user + if (!\OC::$server->getUserSession()->isLoggedIn()) { + header('Location: ' . \OC::$server->getURLGenerator()->linkToRoute( + 'core.login.showLoginForm', + [ + 'redirect_url' => \OC::$server->getRequest()->getRequestUri(), + ] + ) + ); + exit(); + } + // Redirect to 2FA challenge selection if 2FA challenge was not solved yet + if (\OC::$server->get(TwoFactorAuthManager::class)->needsSecondFactor(\OC::$server->getUserSession()->getUser())) { + header('Location: ' . \OC::$server->getURLGenerator()->linkToRoute('core.TwoFactorChallenge.selectChallenge')); + exit(); + } + } + + /** + * Check if the user is a admin, redirects to home if not + * + * @deprecated 32.0.0 + */ + public static function checkAdminUser(): void { + self::checkLoggedIn(); + if (!OC_User::isAdminUser(OC_User::getUser())) { + header('Location: ' . \OCP\Util::linkToAbsolute('', 'index.php')); + exit(); + } + } + + /** + * Returns the URL of the default page + * based on the system configuration and + * the apps visible for the current user + * + * @return string URL + * @deprecated 32.0.0 use IURLGenerator's linkToDefaultPageUrl directly + */ + public static function getDefaultPageUrl() { + /** @var IURLGenerator $urlGenerator */ + $urlGenerator = \OC::$server->get(IURLGenerator::class); + return $urlGenerator->linkToDefaultPageUrl(); + } + + /** + * Redirect to the user default page + * + * @deprecated 32.0.0 + */ + public static function redirectToDefaultPage(): void { + $location = self::getDefaultPageUrl(); + header('Location: ' . $location); + exit(); + } + + /** + * get an id unique for this instance + * + * @return string + */ + public static function getInstanceId(): string { + $id = \OC::$server->getSystemConfig()->getValue('instanceid', null); + if (is_null($id)) { + // We need to guarantee at least one letter in instanceid so it can be used as the session_name + $id = 'oc' . \OC::$server->get(ISecureRandom::class)->generate(10, \OCP\Security\ISecureRandom::CHAR_LOWER . \OCP\Security\ISecureRandom::CHAR_DIGITS); + \OC::$server->getSystemConfig()->setValue('instanceid', $id); + } + return $id; + } + + /** + * Public function to sanitize HTML + * + * This function is used to sanitize HTML and should be applied on any + * string or array of strings before displaying it on a web page. + * + * @param string|string[] $value + * @return ($value is array ? string[] : string) + * @deprecated 32.0.0 use \OCP\Util::sanitizeHTML instead + */ + public static function sanitizeHTML($value) { + if (is_array($value)) { + $value = array_map(function ($value) { + return self::sanitizeHTML($value); + }, $value); + } else { + // Specify encoding for PHP<5.4 + $value = htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'); + } + return $value; + } + + /** + * Public function to encode url parameters + * + * This function is used to encode path to file before output. + * Encoding is done according to RFC 3986 with one exception: + * Character '/' is preserved as is. + * + * @param string $component part of URI to encode + * @return string + * @deprecated 32.0.0 use \OCP\Util::encodePath instead + */ + public static function encodePath($component) { + $encoded = rawurlencode($component); + $encoded = str_replace('%2F', '/', $encoded); + return $encoded; + } + + /** + * Check if current locale is non-UTF8 + * + * @return bool + */ + private static function isNonUTF8Locale() { + if (function_exists('escapeshellcmd')) { + return escapeshellcmd('§') === ''; + } elseif (function_exists('escapeshellarg')) { + return escapeshellarg('§') === '\'\''; + } else { + return preg_match('/utf-?8/i', setlocale(LC_CTYPE, 0)) === 0; + } + } + + /** + * Check if the setlocale call does not work. This can happen if the right + * local packages are not available on the server. + * + * @internal + */ + public static function isSetLocaleWorking(): bool { + if (self::isNonUTF8Locale()) { + // Borrowed from \Patchwork\Utf8\Bootup::initLocale + setlocale(LC_ALL, 'C.UTF-8', 'C'); + setlocale(LC_CTYPE, 'en_US.UTF-8', 'fr_FR.UTF-8', 'es_ES.UTF-8', 'de_DE.UTF-8', 'ru_RU.UTF-8', 'pt_BR.UTF-8', 'it_IT.UTF-8', 'ja_JP.UTF-8', 'zh_CN.UTF-8', '0'); + + // Check again + if (self::isNonUTF8Locale()) { + return false; + } + } + + return true; + } + + /** + * Check if it's possible to get the inline annotations + * + * @internal + */ + public static function isAnnotationsWorking(): bool { + if (PHP_VERSION_ID >= 80300) { + /** @psalm-suppress UndefinedMethod */ + $reflection = \ReflectionMethod::createFromMethodName(__METHOD__); + } else { + $reflection = new \ReflectionMethod(__METHOD__); + } + $docs = $reflection->getDocComment(); + + return (is_string($docs) && strlen($docs) > 50); + } + + /** + * Check if the PHP module fileinfo is loaded. + * + * @internal + */ + public static function fileInfoLoaded(): bool { + return function_exists('finfo_open'); + } + + /** + * clear all levels of output buffering + * + * @return void + */ + public static function obEnd() { + while (ob_get_level()) { + ob_end_clean(); + } + } + + /** + * Checks whether the server is running on Mac OS X + * + * @return bool true if running on Mac OS X, false otherwise + */ + public static function runningOnMac() { + return (strtoupper(substr(PHP_OS, 0, 6)) === 'DARWIN'); + } + + /** + * Handles the case that there may not be a theme, then check if a "default" + * theme exists and take that one + * + * @return string the theme + */ + public static function getTheme() { + $theme = \OC::$server->getSystemConfig()->getValue('theme', ''); + + if ($theme === '') { + if (is_dir(OC::$SERVERROOT . '/themes/default')) { + $theme = 'default'; + } + } + + return $theme; + } + + /** + * Normalize a unicode string + * + * @param string $value a not normalized string + * @return string The normalized string or the input if the normalization failed + */ + public static function normalizeUnicode(string $value): string { + if (Normalizer::isNormalized($value)) { + return $value; + } + + $normalizedValue = Normalizer::normalize($value); + if ($normalizedValue === false) { + \OCP\Server::get(LoggerInterface::class)->warning('normalizing failed for "' . $value . '"', ['app' => 'core']); + return $value; + } + + return $normalizedValue; + } + + /** + * Check whether the instance needs to perform an upgrade, + * either when the core version is higher or any app requires + * an upgrade. + * + * @param \OC\SystemConfig $config + * @return bool whether the core or any app needs an upgrade + * @throws \OCP\HintException When the upgrade from the given version is not allowed + * @deprecated 32.0.0 Use \OCP\Util::needUpgrade instead + */ + public static function needUpgrade(\OC\SystemConfig $config) { + if ($config->getValue('installed', false)) { + $installedVersion = $config->getValue('version', '0.0.0'); + $currentVersion = implode('.', \OCP\Util::getVersion()); + $versionDiff = version_compare($currentVersion, $installedVersion); + if ($versionDiff > 0) { + return true; + } elseif ($config->getValue('debug', false) && $versionDiff < 0) { + // downgrade with debug + $installedMajor = explode('.', $installedVersion); + $installedMajor = $installedMajor[0] . '.' . $installedMajor[1]; + $currentMajor = explode('.', $currentVersion); + $currentMajor = $currentMajor[0] . '.' . $currentMajor[1]; + if ($installedMajor === $currentMajor) { + // Same major, allow downgrade for developers + return true; + } else { + // downgrade attempt, throw exception + throw new \OCP\HintException('Downgrading is not supported and is likely to cause unpredictable issues (from ' . $installedVersion . ' to ' . $currentVersion . ')'); + } + } elseif ($versionDiff < 0) { + // downgrade attempt, throw exception + throw new \OCP\HintException('Downgrading is not supported and is likely to cause unpredictable issues (from ' . $installedVersion . ' to ' . $currentVersion . ')'); + } + + // also check for upgrades for apps (independently from the user) + $apps = \OC_App::getEnabledApps(false, true); + $shouldUpgrade = false; + foreach ($apps as $app) { + if (\OC_App::shouldUpgrade($app)) { + $shouldUpgrade = true; + break; + } + } + return $shouldUpgrade; + } else { + return false; + } + } +} diff --git a/lib/private/legacy/l10n.php b/lib/private/legacy/l10n.php deleted file mode 100644 index 3530bb4aad1..00000000000 --- a/lib/private/legacy/l10n.php +++ /dev/null @@ -1,342 +0,0 @@ -<?php -/** - * @author Bart Visscher <bartv@thisnet.nl> - * @author Bernhard Posselt <dev@bernhard-posselt.com> - * @author Jakob Sack <mail@jakobsack.de> - * @author Jan-Christoph Borchardt <hey@jancborchardt.net> - * @author Joas Schilling <nickvergessen@owncloud.com> - * @author Lukas Reschke <lukas@owncloud.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <icewind@owncloud.com> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Thomas Tanghus <thomas@tanghus.net> - * - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * - */ - -/** - * This class is for i18n and l10n - * @deprecated 9.0.0 Use \OC::$server->getL10NFactory()->get() instead - */ -class OC_L10N implements \OCP\IL10N { - /** - * cache - */ - protected static $cache = array(); - protected static $availableLanguages = array(); - - /** - * The best language - */ - protected static $language = ''; - - /** - * App of this object - */ - protected $app; - - /** - * Language of this object - */ - protected $lang; - - /** - * Translations - */ - private $translations = array(); - - /** - * Plural forms (string) - */ - private $pluralFormString = 'nplurals=2; plural=(n != 1);'; - - /** - * Plural forms (function) - */ - private $pluralFormFunction = null; - - /** - * The constructor - * @param string $app app requesting l10n - * @param string $lang default: null Language - * - * If language is not set, the constructor tries to find the right - * language. - * @deprecated 9.0.0 Use \OC::$server->getL10NFactory()->get() instead - */ - public function __construct($app, $lang = null) { - $app = \OC_App::cleanAppId($app); - $this->app = $app; - - if ($lang !== null) { - $lang = str_replace(array('\0', '/', '\\', '..'), '', $lang); - } - - // Find the right language - if ($app !== 'test' && !\OC::$server->getL10NFactory()->languageExists($app, $lang)) { - $lang = \OC::$server->getL10NFactory()->findLanguage($app); - } - - $this->lang = $lang; - } - - /** - * @param $transFile - * @return bool - */ - public function load($transFile) { - $this->app = true; - - $json = json_decode(file_get_contents($transFile), true); - if (!is_array($json)) { - $jsonError = json_last_error(); - \OC::$server->getLogger()->warning("Failed to load $transFile - json error code: $jsonError", ['app' => 'l10n']); - return false; - } - - $this->pluralFormString = $json['pluralForm']; - $translations = $json['translations']; - - $this->translations = array_merge($this->translations, $translations); - - return true; - } - - protected function init() { - if ($this->app === true) { - return; - } - $app = $this->app; - $lang = $this->lang; - $this->app = true; - - /** @var \OC\L10N\Factory $factory */ - $factory = \OC::$server->getL10NFactory(); - $languageFiles = $factory->getL10nFilesForApp($app, $lang); - - $this->translations = []; - foreach ($languageFiles as $languageFile) { - $this->load($languageFile); - } - } - - /** - * Translating - * @param string $text The text we need a translation for - * @param array $parameters default:array() Parameters for sprintf - * @return \OC_L10N_String Translation or the same text - * - * Returns the translation. If no translation is found, $text will be - * returned. - */ - public function t($text, $parameters = array()) { - return new OC_L10N_String($this, $text, $parameters); - } - - /** - * Translating - * @param string $text_singular the string to translate for exactly one object - * @param string $text_plural the string to translate for n objects - * @param integer $count Number of objects - * @param array $parameters default:array() Parameters for sprintf - * @return \OC_L10N_String Translation or the same text - * - * Returns the translation. If no translation is found, $text will be - * returned. %n will be replaced with the number of objects. - * - * The correct plural is determined by the plural_forms-function - * provided by the po file. - * - */ - public function n($text_singular, $text_plural, $count, $parameters = array()) { - $this->init(); - $identifier = "_${text_singular}_::_${text_plural}_"; - if( array_key_exists($identifier, $this->translations)) { - return new OC_L10N_String( $this, $identifier, $parameters, $count ); - }else{ - if($count === 1) { - return new OC_L10N_String($this, $text_singular, $parameters, $count); - }else{ - return new OC_L10N_String($this, $text_plural, $parameters, $count); - } - } - } - - /** - * getTranslations - * @return array Fetch all translations - * - * Returns an associative array with all translations - */ - public function getTranslations() { - $this->init(); - return $this->translations; - } - - /** - * getPluralFormFunction - * @return string the plural form function - * - * returned function accepts the argument $n - */ - public function getPluralFormFunction() { - $this->init(); - if (is_null($this->pluralFormFunction)) { - $this->pluralFormFunction = \OC::$server->getL10NFactory()->createPluralFunction($this->pluralFormString); - } - return $this->pluralFormFunction; - } - - /** - * Localization - * @param string $type Type of localization - * @param array|int|string $data parameters for this localization - * @param array $options - * @return string|false - * - * Returns the localized data. - * - * Implemented types: - * - date - * - Creates a date - * - params: timestamp (int/string) - * - datetime - * - Creates date and time - * - params: timestamp (int/string) - * - time - * - Creates a time - * - params: timestamp (int/string) - * - firstday: Returns the first day of the week (0 sunday - 6 saturday) - * - jsdate: Returns the short JS date format - */ - public function l($type, $data, $options = array()) { - if ($type === 'firstday') { - return $this->getFirstWeekDay(); - } - if ($type === 'jsdate') { - return $this->getDateFormat(); - } - - $this->init(); - $value = new DateTime(); - if($data instanceof DateTime) { - $value = $data; - } elseif(is_string($data) && !is_numeric($data)) { - $data = strtotime($data); - $value->setTimestamp($data); - } else { - $value->setTimestamp($data); - } - - // Use the language of the instance - $locale = $this->transformToCLDRLocale($this->getLanguageCode()); - - $options = array_merge(array('width' => 'long'), $options); - $width = $options['width']; - switch($type) { - case 'date': - return Punic\Calendar::formatDate($value, $width, $locale); - case 'datetime': - return Punic\Calendar::formatDatetime($value, $width, $locale); - case 'time': - return Punic\Calendar::formatTime($value, $width, $locale); - default: - return false; - } - } - - /** - * The code (en, de, ...) of the language that is used for this OC_L10N object - * - * @return string language - */ - public function getLanguageCode() { - return $this->lang; - } - - /** - * @return string - * @throws \Punic\Exception\ValueNotInList - * @deprecated 9.0.0 Use $this->l('jsdate', null) instead - */ - public function getDateFormat() { - $locale = $this->transformToCLDRLocale($this->getLanguageCode()); - return Punic\Calendar::getDateFormat('short', $locale); - } - - /** - * @return int - * @deprecated 9.0.0 Use $this->l('firstday', null) instead - */ - public function getFirstWeekDay() { - $locale = $this->transformToCLDRLocale($this->getLanguageCode()); - return Punic\Calendar::getFirstWeekday($locale); - } - - /** - * @param string $locale - * @return string - */ - private function transformToCLDRLocale($locale) { - if ($locale === 'sr@latin') { - return 'sr_latn'; - } - - return $locale; - } - - /** - * find the best language - * @param string $app - * @return string language - * - * If nothing works it returns 'en' - * @deprecated 9.0.0 Use \OC::$server->getL10NFactory()->findLanguage() instead - */ - public static function findLanguage($app = null) { - return \OC::$server->getL10NFactory()->findLanguage($app); - } - - /** - * @return string - * @deprecated 9.0.0 Use \OC::$server->getL10NFactory()->setLanguageFromRequest() instead - */ - public static function setLanguageFromRequest() { - return \OC::$server->getL10NFactory()->setLanguageFromRequest(); - } - - /** - * find all available languages for an app - * @param string $app App that needs to be translated - * @return array an array of available languages - * @deprecated 9.0.0 Use \OC::$server->getL10NFactory()->findAvailableLanguages() instead - */ - public static function findAvailableLanguages($app=null) { - return \OC::$server->getL10NFactory()->findAvailableLanguages($app); - } - - /** - * @param string $app - * @param string $lang - * @return bool - * @deprecated 9.0.0 Use \OC::$server->getL10NFactory()->languageExists() instead - */ - public static function languageExists($app, $lang) { - return \OC::$server->getL10NFactory()->languageExists($app, $lang); - } -} |