diff options
Diffstat (limited to 'lib/private/App')
19 files changed, 260 insertions, 192 deletions
diff --git a/lib/private/App/AppManager.php b/lib/private/App/AppManager.php index fe5d3e2faeb..7778393b3b3 100644 --- a/lib/private/App/AppManager.php +++ b/lib/private/App/AppManager.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -8,7 +9,7 @@ namespace OC\App; use OC\AppConfig; use OC\AppFramework\Bootstrap\Coordinator; -use OC\ServerNotAvailableException; +use OC\Config\ConfigManager; use OCP\Activity\IManager as IActivityManager; use OCP\App\AppPathNotFoundException; use OCP\App\Events\AppDisableEvent; @@ -19,6 +20,7 @@ use OCP\Collaboration\AutoComplete\IManager as IAutoCompleteManager; use OCP\Collaboration\Collaborators\ISearch as ICollaboratorSearch; use OCP\Diagnostics\IEventLogger; use OCP\EventDispatcher\IEventDispatcher; +use OCP\IAppConfig; use OCP\ICacheFactory; use OCP\IConfig; use OCP\IGroup; @@ -27,6 +29,8 @@ use OCP\INavigationManager; use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserSession; +use OCP\Server; +use OCP\ServerVersion; use OCP\Settings\IManager as ISettingsManager; use Psr\Log\LoggerInterface; @@ -44,7 +48,7 @@ class AppManager implements IAppManager { ]; /** @var string[] $appId => $enabled */ - private array $installedAppsCache = []; + private array $enabledAppsCache = []; /** @var string[]|null */ private ?array $shippedApps = null; @@ -80,12 +84,14 @@ class AppManager implements IAppManager { private ICacheFactory $memCacheFactory, private IEventDispatcher $dispatcher, private LoggerInterface $logger, + private ServerVersion $serverVersion, + private ConfigManager $configManager, ) { } private function getNavigationManager(): INavigationManager { if ($this->navigationManager === null) { - $this->navigationManager = \OCP\Server::get(INavigationManager::class); + $this->navigationManager = Server::get(INavigationManager::class); } return $this->navigationManager; } @@ -111,7 +117,7 @@ class AppManager implements IAppManager { if (!$this->config->getSystemValueBool('installed', false)) { throw new \Exception('Nextcloud is not installed yet, AppConfig is not available'); } - $this->appConfig = \OCP\Server::get(AppConfig::class); + $this->appConfig = Server::get(AppConfig::class); return $this->appConfig; } @@ -122,37 +128,49 @@ class AppManager implements IAppManager { if (!$this->config->getSystemValueBool('installed', false)) { throw new \Exception('Nextcloud is not installed yet, AppConfig is not available'); } - $this->urlGenerator = \OCP\Server::get(IURLGenerator::class); + $this->urlGenerator = Server::get(IURLGenerator::class); return $this->urlGenerator; } /** - * @return string[] $appId => $enabled + * For all enabled apps, return the value of their 'enabled' config key. + * + * @return array<string,string> appId => enabled (may be 'yes', or a json encoded list of group ids) */ - private function getInstalledAppsValues(): array { - if (!$this->installedAppsCache) { - $values = $this->getAppConfig()->getValues(false, 'enabled'); + private function getEnabledAppsValues(): array { + if (!$this->enabledAppsCache) { + /** @var array<string,string> */ + $values = $this->getAppConfig()->searchValues('enabled', false, IAppConfig::VALUE_STRING); $alwaysEnabledApps = $this->getAlwaysEnabledApps(); foreach ($alwaysEnabledApps as $appId) { $values[$appId] = 'yes'; } - $this->installedAppsCache = array_filter($values, function ($value) { + $this->enabledAppsCache = array_filter($values, function ($value) { return $value !== 'no'; }); - ksort($this->installedAppsCache); + ksort($this->enabledAppsCache); } - return $this->installedAppsCache; + return $this->enabledAppsCache; } /** - * List all installed apps + * Deprecated alias * * @return string[] */ public function getInstalledApps() { - return array_keys($this->getInstalledAppsValues()); + return $this->getEnabledApps(); + } + + /** + * List all enabled apps, either for everyone or for some groups + * + * @return list<string> + */ + public function getEnabledApps(): array { + return array_keys($this->getEnabledAppsValues()); } /** @@ -173,9 +191,9 @@ class AppManager implements IAppManager { if (is_resource($dh)) { while (($file = readdir($dh)) !== false) { if ( - $file[0] != '.' && - is_dir($apps_dir['path'] . '/' . $file) && - is_file($apps_dir['path'] . '/' . $file . '/appinfo/info.xml') + $file[0] != '.' + && is_dir($apps_dir['path'] . '/' . $file) + && is_file($apps_dir['path'] . '/' . $file . '/appinfo/info.xml') ) { $apps[] = $file; } @@ -190,10 +208,10 @@ class AppManager implements IAppManager { * List all apps enabled for a user * * @param \OCP\IUser $user - * @return string[] + * @return list<string> */ public function getEnabledAppsForUser(IUser $user) { - $apps = $this->getInstalledAppsValues(); + $apps = $this->getEnabledAppsValues(); $appsForUser = array_filter($apps, function ($enabled) use ($user) { return $this->checkAppForUser($enabled, $user); }); @@ -201,7 +219,7 @@ class AppManager implements IAppManager { } public function getEnabledAppsForGroup(IGroup $group): array { - $apps = $this->getInstalledAppsValues(); + $apps = $this->getEnabledAppsValues(); $appsForGroups = array_filter($apps, function ($enabled) use ($group) { return $this->checkAppForGroups($enabled, $group); }); @@ -238,7 +256,7 @@ class AppManager implements IAppManager { } } - // prevent app.php from printing output + // prevent app loading from printing output ob_start(); foreach ($apps as $app) { if (!$this->isAppLoaded($app) && ($types === [] || $this->isType($app, $types))) { @@ -301,7 +319,7 @@ class AppManager implements IAppManager { } public function getAppRestriction(string $appId): array { - $values = $this->getInstalledAppsValues(); + $values = $this->getEnabledAppsValues(); if (!isset($values[$appId])) { return []; @@ -327,9 +345,9 @@ class AppManager implements IAppManager { if ($user === null) { $user = $this->userSession->getUser(); } - $installedApps = $this->getInstalledAppsValues(); - if (isset($installedApps[$appId])) { - return $this->checkAppForUser($installedApps[$appId], $user); + $enabledAppsValues = $this->getEnabledAppsValues(); + if (isset($enabledAppsValues[$appId])) { + return $this->checkAppForUser($enabledAppsValues[$appId], $user); } else { return false; } @@ -393,12 +411,14 @@ class AppManager implements IAppManager { * Notice: This actually checks if the app is enabled and not only if it is installed. * * @param string $appId - * @param IGroup[]|String[] $groups - * @return bool */ - public function isInstalled($appId) { - $installedApps = $this->getInstalledAppsValues(); - return isset($installedApps[$appId]); + public function isInstalled($appId): bool { + return $this->isEnabledForAnyone($appId); + } + + public function isEnabledForAnyone(string $appId): bool { + $enabledAppsValues = $this->getEnabledAppsValues(); + return isset($enabledAppsValues[$appId]); } /** @@ -437,43 +457,13 @@ class AppManager implements IAppManager { // in case someone calls loadApp() directly \OC_App::registerAutoloading($app, $appPath); - /** @var Coordinator $coordinator */ - $coordinator = \OC::$server->get(Coordinator::class); - $isBootable = $coordinator->isBootable($app); - - $hasAppPhpFile = is_file($appPath . '/appinfo/app.php'); - - if ($isBootable && $hasAppPhpFile) { - $this->logger->error('/appinfo/app.php is not loaded when \OCP\AppFramework\Bootstrap\IBootstrap on the application class is used. Migrate everything from app.php to the Application class.', [ - 'app' => $app, - ]); - } elseif ($hasAppPhpFile) { - $eventLogger->start("bootstrap:load_app:$app:app.php", "Load legacy app.php app $app"); - $this->logger->debug('/appinfo/app.php is deprecated, use \OCP\AppFramework\Bootstrap\IBootstrap on the application class instead.', [ + if (is_file($appPath . '/appinfo/app.php')) { + $this->logger->error('/appinfo/app.php is not supported anymore, use \OCP\AppFramework\Bootstrap\IBootstrap on the application class instead.', [ 'app' => $app, ]); - try { - self::requireAppFile($appPath); - } catch (\Throwable $ex) { - if ($ex instanceof ServerNotAvailableException) { - throw $ex; - } - if (!$this->isShipped($app) && !$this->isType($app, ['authentication'])) { - $this->logger->error("App $app threw an error during app.php load and will be disabled: " . $ex->getMessage(), [ - 'exception' => $ex, - ]); - - // Only disable apps which are not shipped and that are not authentication apps - $this->disableApp($app, true); - } else { - $this->logger->error("App $app threw an error during app.php load: " . $ex->getMessage(), [ - 'exception' => $ex, - ]); - } - } - $eventLogger->end("bootstrap:load_app:$app:app.php"); } + $coordinator = Server::get(Coordinator::class); $coordinator->bootApp($app); $eventLogger->start("bootstrap:load_app:$app:info", "Load info.xml for $app and register any services defined in it"); @@ -523,8 +513,8 @@ class AppManager implements IAppManager { if (!empty($info['collaboration']['plugins'])) { // deal with one or many plugin entries - $plugins = isset($info['collaboration']['plugins']['plugin']['@value']) ? - [$info['collaboration']['plugins']['plugin']] : $info['collaboration']['plugins']['plugin']; + $plugins = isset($info['collaboration']['plugins']['plugin']['@value']) + ? [$info['collaboration']['plugins']['plugin']] : $info['collaboration']['plugins']['plugin']; $collaboratorSearch = null; $autoCompleteManager = null; foreach ($plugins as $plugin) { @@ -545,6 +535,7 @@ class AppManager implements IAppManager { $eventLogger->end("bootstrap:load_app:$app"); } + /** * Check if an app is loaded * @param string $app app id @@ -555,38 +546,34 @@ class AppManager implements IAppManager { } /** - * Load app.php from the given app - * - * @param string $app app name - * @throws \Error - */ - private static function requireAppFile(string $app): void { - // encapsulated here to avoid variable scope conflicts - require_once $app . '/appinfo/app.php'; - } - - /** * Enable an app for every user * * @param string $appId * @param bool $forceEnable * @throws AppPathNotFoundException + * @throws \InvalidArgumentException if the application is not installed yet */ public function enableApp(string $appId, bool $forceEnable = false): void { // Check if app exists $this->getAppPath($appId); + if ($this->config->getAppValue($appId, 'installed_version', '') === '') { + throw new \InvalidArgumentException("$appId is not installed, cannot be enabled."); + } + if ($forceEnable) { $this->overwriteNextcloudRequirement($appId); } - $this->installedAppsCache[$appId] = 'yes'; + $this->enabledAppsCache[$appId] = 'yes'; $this->getAppConfig()->setValue($appId, 'enabled', 'yes'); $this->dispatcher->dispatchTyped(new AppEnableEvent($appId)); $this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE, new ManagerEvent( ManagerEvent::EVENT_APP_ENABLE, $appId )); $this->clearAppsCache(); + + $this->configManager->migrateConfigLexiconKeys($appId); } /** @@ -622,6 +609,10 @@ class AppManager implements IAppManager { throw new \InvalidArgumentException("$appId can't be enabled for groups."); } + if ($this->config->getAppValue($appId, 'installed_version', '') === '') { + throw new \InvalidArgumentException("$appId is not installed, cannot be enabled."); + } + if ($forceEnable) { $this->overwriteNextcloudRequirement($appId); } @@ -634,13 +625,15 @@ class AppManager implements IAppManager { : $group; }, $groups); - $this->installedAppsCache[$appId] = json_encode($groupIds); + $this->enabledAppsCache[$appId] = json_encode($groupIds); $this->getAppConfig()->setValue($appId, 'enabled', json_encode($groupIds)); $this->dispatcher->dispatchTyped(new AppEnableEvent($appId, $groupIds)); $this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, new ManagerEvent( ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, $appId, $groups )); $this->clearAppsCache(); + + $this->configManager->migrateConfigLexiconKeys($appId); } /** @@ -663,7 +656,7 @@ class AppManager implements IAppManager { $this->autoDisabledApps[$appId] = $previousSetting; } - unset($this->installedAppsCache[$appId]); + unset($this->enabledAppsCache[$appId]); $this->getAppConfig()->setValue($appId, 'enabled', 'no'); // run uninstall steps @@ -724,7 +717,7 @@ class AppManager implements IAppManager { */ public function getAppsNeedingUpgrade($version) { $appsToUpgrade = []; - $apps = $this->getInstalledApps(); + $apps = $this->getEnabledApps(); foreach ($apps as $appId) { $appInfo = $this->getAppInfo($appId); $appDbVersion = $this->getAppConfig()->getValue($appId, 'installed_version'); @@ -778,7 +771,7 @@ class AppManager implements IAppManager { $data = $parser->parse($path); if (is_array($data)) { - $data = \OC_App::parseAppInfo($data, $lang); + $data = $parser->applyL10N($data, $lang); } return $data; @@ -786,13 +779,26 @@ class AppManager implements IAppManager { public function getAppVersion(string $appId, bool $useCache = true): string { if (!$useCache || !isset($this->appVersions[$appId])) { - $appInfo = $this->getAppInfo($appId); - $this->appVersions[$appId] = ($appInfo !== null && isset($appInfo['version'])) ? $appInfo['version'] : '0'; + if ($appId === 'core') { + $this->appVersions[$appId] = $this->serverVersion->getVersionString(); + } else { + $appInfo = $this->getAppInfo($appId); + $this->appVersions[$appId] = ($appInfo !== null && isset($appInfo['version'])) ? $appInfo['version'] : '0'; + } } return $this->appVersions[$appId]; } /** + * Returns the installed versions of all apps + * + * @return array<string, string> + */ + public function getAppInstalledVersions(bool $onlyEnabled = false): array { + return $this->getAppConfig()->getAppInstalledVersions($onlyEnabled); + } + + /** * Returns a list of apps incompatible with the given version * * @param string $version Nextcloud version as array of version components @@ -802,7 +808,7 @@ class AppManager implements IAppManager { * @internal */ public function getIncompatibleApps(string $version): array { - $apps = $this->getInstalledApps(); + $apps = $this->getEnabledApps(); $incompatibleApps = []; foreach ($apps as $appId) { $info = $this->getAppInfo($appId); @@ -825,6 +831,10 @@ class AppManager implements IAppManager { } private function isAlwaysEnabled(string $appId): bool { + if ($appId === 'core') { + return true; + } + $alwaysEnabled = $this->getAlwaysEnabledApps(); return in_array($appId, $alwaysEnabled, true); } @@ -920,8 +930,23 @@ class AppManager implements IAppManager { return false; } + /** + * Clean the appId from forbidden characters + * + * @psalm-taint-escape callable + * @psalm-taint-escape cookie + * @psalm-taint-escape file + * @psalm-taint-escape has_quotes + * @psalm-taint-escape header + * @psalm-taint-escape html + * @psalm-taint-escape include + * @psalm-taint-escape ldap + * @psalm-taint-escape shell + * @psalm-taint-escape sql + * @psalm-taint-escape unserialize + */ public function cleanAppId(string $app): string { - // FIXME should list allowed characters instead - return str_replace(['<', '>', '"', "'", '\0', '/', '\\', '..'], '', $app); + /* Only lowercase alphanumeric is allowed */ + return preg_replace('/(^[0-9_]|[^a-z0-9_]+|_$)/', '', $app); } } diff --git a/lib/private/App/AppStore/AppNotFoundException.php b/lib/private/App/AppStore/AppNotFoundException.php new file mode 100644 index 00000000000..79ceebb4423 --- /dev/null +++ b/lib/private/App/AppStore/AppNotFoundException.php @@ -0,0 +1,13 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\App\AppStore; + +class AppNotFoundException extends \Exception { +} diff --git a/lib/private/App/AppStore/Bundles/Bundle.php b/lib/private/App/AppStore/Bundles/Bundle.php index 62f09b82f79..1443be81e92 100644 --- a/lib/private/App/AppStore/Bundles/Bundle.php +++ b/lib/private/App/AppStore/Bundles/Bundle.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/App/AppStore/Bundles/BundleFetcher.php b/lib/private/App/AppStore/Bundles/BundleFetcher.php index 01325699e2c..4ff53b0c70b 100644 --- a/lib/private/App/AppStore/Bundles/BundleFetcher.php +++ b/lib/private/App/AppStore/Bundles/BundleFetcher.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/App/AppStore/Bundles/EducationBundle.php b/lib/private/App/AppStore/Bundles/EducationBundle.php index 6770d4a7091..23681ec7416 100644 --- a/lib/private/App/AppStore/Bundles/EducationBundle.php +++ b/lib/private/App/AppStore/Bundles/EducationBundle.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/App/AppStore/Bundles/EnterpriseBundle.php b/lib/private/App/AppStore/Bundles/EnterpriseBundle.php index 88a9ec60a00..fc2d43e0388 100644 --- a/lib/private/App/AppStore/Bundles/EnterpriseBundle.php +++ b/lib/private/App/AppStore/Bundles/EnterpriseBundle.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/App/AppStore/Bundles/GroupwareBundle.php b/lib/private/App/AppStore/Bundles/GroupwareBundle.php index 7c7b74ff53d..93fa70268cd 100644 --- a/lib/private/App/AppStore/Bundles/GroupwareBundle.php +++ b/lib/private/App/AppStore/Bundles/GroupwareBundle.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/App/AppStore/Bundles/PublicSectorBundle.php b/lib/private/App/AppStore/Bundles/PublicSectorBundle.php index 158d525bdcc..106a6353029 100644 --- a/lib/private/App/AppStore/Bundles/PublicSectorBundle.php +++ b/lib/private/App/AppStore/Bundles/PublicSectorBundle.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/App/AppStore/Bundles/SocialSharingBundle.php b/lib/private/App/AppStore/Bundles/SocialSharingBundle.php index 7f925688ca3..40f0fb15977 100644 --- a/lib/private/App/AppStore/Bundles/SocialSharingBundle.php +++ b/lib/private/App/AppStore/Bundles/SocialSharingBundle.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/App/AppStore/Fetcher/AppDiscoverFetcher.php b/lib/private/App/AppStore/Fetcher/AppDiscoverFetcher.php index 2537d1dcec9..8389e525750 100644 --- a/lib/private/App/AppStore/Fetcher/AppDiscoverFetcher.php +++ b/lib/private/App/AppStore/Fetcher/AppDiscoverFetcher.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/App/AppStore/Fetcher/AppFetcher.php b/lib/private/App/AppStore/Fetcher/AppFetcher.php index 352646c3f5e..bbf4b00245b 100644 --- a/lib/private/App/AppStore/Fetcher/AppFetcher.php +++ b/lib/private/App/AppStore/Fetcher/AppFetcher.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -78,8 +79,8 @@ class AppFetcher extends Fetcher { $minServerVersion = $serverVersion->getMinimumVersion(); $maxServerVersion = $serverVersion->getMaximumVersion(); $minFulfilled = $this->compareVersion->isCompatible($ncVersion, $minServerVersion, '>='); - $maxFulfilled = $maxServerVersion !== '' && - $this->compareVersion->isCompatible($ncVersion, $maxServerVersion, '<='); + $maxFulfilled = $maxServerVersion !== '' + && $this->compareVersion->isCompatible($ncVersion, $maxServerVersion, '<='); $isPhpCompatible = true; if (($release['rawPhpVersionSpec'] ?? '*') !== '*') { $phpVersion = $versionParser->getVersion($release['rawPhpVersionSpec']); diff --git a/lib/private/App/AppStore/Fetcher/CategoryFetcher.php b/lib/private/App/AppStore/Fetcher/CategoryFetcher.php index d72f8fa7e24..d7857d41bee 100644 --- a/lib/private/App/AppStore/Fetcher/CategoryFetcher.php +++ b/lib/private/App/AppStore/Fetcher/CategoryFetcher.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/App/AppStore/Fetcher/Fetcher.php b/lib/private/App/AppStore/Fetcher/Fetcher.php index f998c9e2023..24876675d60 100644 --- a/lib/private/App/AppStore/Fetcher/Fetcher.php +++ b/lib/private/App/AppStore/Fetcher/Fetcher.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -55,7 +56,7 @@ abstract class Fetcher { * * @return array */ - protected function fetch($ETag, $content) { + protected function fetch($ETag, $content, $allowUnstable = false) { $appstoreenabled = $this->config->getSystemValueBool('appstoreenabled', true); if ((int)$this->config->getAppValue('settings', 'appstore-fetcher-lastFailure', '0') > time() - self::RETRY_AFTER_FAILURE_SECONDS) { return []; diff --git a/lib/private/App/AppStore/Version/Version.php b/lib/private/App/AppStore/Version/Version.php index b7e679d250a..2d169a291f1 100644 --- a/lib/private/App/AppStore/Version/Version.php +++ b/lib/private/App/AppStore/Version/Version.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/App/AppStore/Version/VersionParser.php b/lib/private/App/AppStore/Version/VersionParser.php index 59715c8d385..8976f28837f 100644 --- a/lib/private/App/AppStore/Version/VersionParser.php +++ b/lib/private/App/AppStore/Version/VersionParser.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/App/DependencyAnalyzer.php b/lib/private/App/DependencyAnalyzer.php index 72b38ca12c7..bde8719c41d 100644 --- a/lib/private/App/DependencyAnalyzer.php +++ b/lib/private/App/DependencyAnalyzer.php @@ -1,21 +1,18 @@ <?php + +declare(strict_types=1); + /** - * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016-2025 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. * SPDX-License-Identifier: AGPL-3.0-only */ + namespace OC\App; use OCP\IL10N; class DependencyAnalyzer { - /** @var array */ - private $appInfo; - - /** - * @param Platform $platform - * @param \OCP\IL10N $l - */ public function __construct( private Platform $platform, private IL10N $l, @@ -23,11 +20,9 @@ class DependencyAnalyzer { } /** - * @param array $app - * @returns array of missing dependencies + * @return array of missing dependencies */ - public function analyze(array $app, bool $ignoreMax = false) { - $this->appInfo = $app; + public function analyze(array $app, bool $ignoreMax = false): array { if (isset($app['dependencies'])) { $dependencies = $app['dependencies']; } else { @@ -63,12 +58,10 @@ class DependencyAnalyzer { * Truncates both versions to the lowest common version, e.g. * 5.1.2.3 and 5.1 will be turned into 5.1 and 5.1, * 5.2.6.5 and 5.1 will be turned into 5.2 and 5.1 - * @param string $first - * @param string $second * @return string[] first element is the first version, second element is the * second version */ - private function normalizeVersions($first, $second) { + private function normalizeVersions(string $first, string $second): array { $first = explode('.', $first); $second = explode('.', $second); @@ -83,47 +76,31 @@ class DependencyAnalyzer { /** * Parameters will be normalized and then passed into version_compare * in the same order they are specified in the method header - * @param string $first - * @param string $second - * @param string $operator * @return bool result similar to version_compare */ - private function compare($first, $second, $operator) { - // we can't normalize versions if one of the given parameters is not a - // version string but null. In case one parameter is null normalization - // will therefore be skipped - if ($first !== null && $second !== null) { - [$first, $second] = $this->normalizeVersions($first, $second); - } + private function compare(string $first, string $second, string $operator): bool { + [$first, $second] = $this->normalizeVersions($first, $second); return version_compare($first, $second, $operator); } /** * Checks if a version is bigger than another version - * @param string $first - * @param string $second * @return bool true if the first version is bigger than the second */ - private function compareBigger($first, $second) { + private function compareBigger(string $first, string $second): bool { return $this->compare($first, $second, '>'); } /** * Checks if a version is smaller than another version - * @param string $first - * @param string $second * @return bool true if the first version is smaller than the second */ - private function compareSmaller($first, $second) { + private function compareSmaller(string $first, string $second): bool { return $this->compare($first, $second, '<'); } - /** - * @param array $dependencies - * @return array - */ - private function analyzePhpVersion(array $dependencies) { + private function analyzePhpVersion(array $dependencies): array { $missing = []; if (isset($dependencies['php']['@attributes']['min-version'])) { $minVersion = $dependencies['php']['@attributes']['min-version']; @@ -146,7 +123,7 @@ class DependencyAnalyzer { return $missing; } - private function analyzeArchitecture(array $dependencies) { + private function analyzeArchitecture(array $dependencies): array { $missing = []; if (!isset($dependencies['architecture'])) { return $missing; @@ -169,11 +146,7 @@ class DependencyAnalyzer { return $missing; } - /** - * @param array $dependencies - * @return array - */ - private function analyzeDatabases(array $dependencies) { + private function analyzeDatabases(array $dependencies): array { $missing = []; if (!isset($dependencies['database'])) { return $missing; @@ -186,6 +159,9 @@ class DependencyAnalyzer { if (!is_array($supportedDatabases)) { $supportedDatabases = [$supportedDatabases]; } + if (isset($supportedDatabases['@value'])) { + $supportedDatabases = [$supportedDatabases]; + } $supportedDatabases = array_map(function ($db) { return $this->getValue($db); }, $supportedDatabases); @@ -196,11 +172,7 @@ class DependencyAnalyzer { return $missing; } - /** - * @param array $dependencies - * @return array - */ - private function analyzeCommands(array $dependencies) { + private function analyzeCommands(array $dependencies): array { $missing = []; if (!isset($dependencies['command'])) { return $missing; @@ -226,11 +198,7 @@ class DependencyAnalyzer { return $missing; } - /** - * @param array $dependencies - * @return array - */ - private function analyzeLibraries(array $dependencies) { + private function analyzeLibraries(array $dependencies): array { $missing = []; if (!isset($dependencies['lib'])) { return $missing; @@ -271,11 +239,7 @@ class DependencyAnalyzer { return $missing; } - /** - * @param array $dependencies - * @return array - */ - private function analyzeOS(array $dependencies) { + private function analyzeOS(array $dependencies): array { $missing = []; if (!isset($dependencies['os'])) { return $missing; @@ -299,12 +263,7 @@ class DependencyAnalyzer { return $missing; } - /** - * @param array $dependencies - * @param array $appInfo - * @return array - */ - private function analyzeOC(array $dependencies, array $appInfo, bool $ignoreMax) { + private function analyzeOC(array $dependencies, array $appInfo, bool $ignoreMax): array { $missing = []; $minVersion = null; if (isset($dependencies['nextcloud']['@attributes']['min-version'])) { @@ -320,12 +279,12 @@ class DependencyAnalyzer { if (!is_null($minVersion)) { if ($this->compareSmaller($this->platform->getOcVersion(), $minVersion)) { - $missing[] = $this->l->t('Server version %s or higher is required.', [$this->toVisibleVersion($minVersion)]); + $missing[] = $this->l->t('Server version %s or higher is required.', [$minVersion]); } } if (!$ignoreMax && !is_null($maxVersion)) { if ($this->compareBigger($this->platform->getOcVersion(), $maxVersion)) { - $missing[] = $this->l->t('Server version %s or lower is required.', [$this->toVisibleVersion($maxVersion)]); + $missing[] = $this->l->t('Server version %s or lower is required.', [$maxVersion]); } } return $missing; @@ -346,25 +305,7 @@ class DependencyAnalyzer { } /** - * Map the internal version number to the Nextcloud version - * - * @param string $version - * @return string - */ - protected function toVisibleVersion($version) { - switch ($version) { - case '9.1': - return '10'; - default: - if (str_starts_with($version, '9.1.')) { - $version = '10.0.' . substr($version, 4); - } - return $version; - } - } - - /** - * @param $element + * @param mixed $element * @return mixed */ private function getValue($element) { diff --git a/lib/private/App/InfoParser.php b/lib/private/App/InfoParser.php index 6610121f446..634dc1fbdd5 100644 --- a/lib/private/App/InfoParser.php +++ b/lib/private/App/InfoParser.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. @@ -22,7 +23,7 @@ class InfoParser { * @param string $file the xml file to be loaded * @return null|array where null is an indicator for an error */ - public function parse($file) { + public function parse(string $file): ?array { if (!file_exists($file)) { return null; } @@ -43,7 +44,7 @@ class InfoParser { } $array = $this->xmlToArray($xml); - if (is_null($array)) { + if (is_string($array)) { return null; } @@ -207,11 +208,7 @@ class InfoParser { return $array; } - /** - * @param $data - * @return bool - */ - private function isNavigationItem($data): bool { + private function isNavigationItem(array $data): bool { // Allow settings navigation items with no route entry $type = $data['type'] ?? 'link'; if ($type === 'settings') { @@ -220,17 +217,17 @@ class InfoParser { return isset($data['name'], $data['route']); } - /** - * @param \SimpleXMLElement $xml - * @return array - */ - public function xmlToArray($xml) { - if (!$xml->children()) { + public function xmlToArray(\SimpleXMLElement $xml): array|string { + $children = $xml->children(); + if ($children === null || count($children) === 0) { return (string)$xml; } $array = []; - foreach ($xml->children() as $element => $node) { + foreach ($children as $element => $node) { + if ($element === null) { + throw new \InvalidArgumentException('xml contains a null element'); + } $totalElement = count($xml->{$element}); if (!isset($array[$element])) { @@ -242,15 +239,18 @@ class InfoParser { $data = [ '@attributes' => [], ]; - if (!count($node->children())) { - $value = (string)$node; - if (!empty($value)) { - $data['@value'] = $value; + $converted = $this->xmlToArray($node); + if (is_string($converted)) { + if (!empty($converted)) { + $data['@value'] = $converted; } } else { - $data = array_merge($data, $this->xmlToArray($node)); + $data = array_merge($data, $converted); } foreach ($attributes as $attr => $value) { + if ($attr === null) { + throw new \InvalidArgumentException('xml contains a null element'); + } $data['@attributes'][$attr] = (string)$value; } @@ -271,4 +271,78 @@ class InfoParser { return $array; } + + /** + * Select the appropriate l10n version for fields name, summary and description + */ + public function applyL10N(array $data, ?string $lang = null): array { + if ($lang !== '' && $lang !== null) { + if (isset($data['name']) && is_array($data['name'])) { + $data['name'] = $this->findBestL10NOption($data['name'], $lang); + } + if (isset($data['summary']) && is_array($data['summary'])) { + $data['summary'] = $this->findBestL10NOption($data['summary'], $lang); + } + if (isset($data['description']) && is_array($data['description'])) { + $data['description'] = trim($this->findBestL10NOption($data['description'], $lang)); + } + } elseif (isset($data['description']) && is_string($data['description'])) { + $data['description'] = trim($data['description']); + } else { + $data['description'] = ''; + } + + return $data; + } + + protected function findBestL10NOption(array $options, string $lang): string { + // only a single option + if (isset($options['@value'])) { + return $options['@value']; + } + + $fallback = $similarLangFallback = $englishFallback = false; + + $lang = strtolower($lang); + $similarLang = $lang; + $pos = strpos($similarLang, '_'); + if ($pos !== false && $pos > 0) { + // For "de_DE" we want to find "de" and the other way around + $similarLang = substr($lang, 0, $pos); + } + + foreach ($options as $option) { + if (is_array($option)) { + if ($fallback === false) { + $fallback = $option['@value']; + } + + if (!isset($option['@attributes']['lang'])) { + continue; + } + + $attributeLang = strtolower($option['@attributes']['lang']); + if ($attributeLang === $lang) { + return $option['@value']; + } + + if ($attributeLang === $similarLang) { + $similarLangFallback = $option['@value']; + } elseif (str_starts_with($attributeLang, $similarLang . '_')) { + if ($similarLangFallback === false) { + $similarLangFallback = $option['@value']; + } + } + } else { + $englishFallback = $option; + } + } + + if ($similarLangFallback !== false) { + return $similarLangFallback; + } elseif ($englishFallback !== false) { + return $englishFallback; + } + return (string)$fallback; + } } diff --git a/lib/private/App/Platform.php b/lib/private/App/Platform.php index c2c059220c8..80e4cefed64 100644 --- a/lib/private/App/Platform.php +++ b/lib/private/App/Platform.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. diff --git a/lib/private/App/PlatformRepository.php b/lib/private/App/PlatformRepository.php index 6d9a9cd011a..faed8b07feb 100644 --- a/lib/private/App/PlatformRepository.php +++ b/lib/private/App/PlatformRepository.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors * SPDX-FileCopyrightText: 2016 ownCloud, Inc. |