diff options
Diffstat (limited to 'lib/private/App')
21 files changed, 716 insertions, 808 deletions
diff --git a/lib/private/App/AppManager.php b/lib/private/App/AppManager.php index ad5fdc5afed..7778393b3b3 100644 --- a/lib/private/App/AppManager.php +++ b/lib/private/App/AppManager.php @@ -1,47 +1,15 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Christoph Schaefer "christophł@wolkesicher.de" - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Daniel Rudolf <github.com@daniel-rudolf.de> - * @author Greta Doci <gretadoci@gmail.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Haertl <jus@bitgrid.net> - * @author Julius Härtl <jus@bitgrid.net> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Maxence Lange <maxence@artificial-owl.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Tobia De Koninck <tobia@ledfan.be> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @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/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\App; -use InvalidArgumentException; 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; @@ -52,12 +20,17 @@ 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; use OCP\IGroupManager; +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; @@ -74,16 +47,8 @@ class AppManager implements IAppManager { 'prevent_group_restriction', ]; - private IUserSession $userSession; - private IConfig $config; - private AppConfig $appConfig; - private IGroupManager $groupManager; - private ICacheFactory $memCacheFactory; - private IEventDispatcher $dispatcher; - private LoggerInterface $logger; - /** @var string[] $appId => $enabled */ - private array $installedAppsCache = []; + private array $enabledAppsCache = []; /** @var string[]|null */ private ?array $shippedApps = null; @@ -104,71 +69,157 @@ class AppManager implements IAppManager { /** @var array<string, true> */ private array $loadedApps = []; - public function __construct(IUserSession $userSession, - IConfig $config, - AppConfig $appConfig, - IGroupManager $groupManager, - ICacheFactory $memCacheFactory, - IEventDispatcher $dispatcher, - LoggerInterface $logger) { - $this->userSession = $userSession; - $this->config = $config; - $this->appConfig = $appConfig; - $this->groupManager = $groupManager; - $this->memCacheFactory = $memCacheFactory; - $this->dispatcher = $dispatcher; - $this->logger = $logger; + private ?AppConfig $appConfig = null; + private ?IURLGenerator $urlGenerator = null; + private ?INavigationManager $navigationManager = null; + + /** + * Be extremely careful when injecting classes here. The AppManager is used by the installer, + * so it needs to work before installation. See how AppConfig and IURLGenerator are injected for reference + */ + public function __construct( + private IUserSession $userSession, + private IConfig $config, + private IGroupManager $groupManager, + 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 = Server::get(INavigationManager::class); + } + return $this->navigationManager; + } + + public function getAppIcon(string $appId, bool $dark = false): ?string { + $possibleIcons = $dark ? [$appId . '-dark.svg', 'app-dark.svg'] : [$appId . '.svg', 'app.svg']; + $icon = null; + foreach ($possibleIcons as $iconName) { + try { + $icon = $this->getUrlGenerator()->imagePath($appId, $iconName); + break; + } catch (\RuntimeException $e) { + // ignore + } + } + return $icon; + } + + private function getAppConfig(): AppConfig { + if ($this->appConfig !== null) { + return $this->appConfig; + } + if (!$this->config->getSystemValueBool('installed', false)) { + throw new \Exception('Nextcloud is not installed yet, AppConfig is not available'); + } + $this->appConfig = Server::get(AppConfig::class); + return $this->appConfig; + } + + private function getUrlGenerator(): IURLGenerator { + if ($this->urlGenerator !== null) { + return $this->urlGenerator; + } + if (!$this->config->getSystemValueBool('installed', false)) { + throw new \Exception('Nextcloud is not installed yet, AppConfig is not available'); + } + $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->appConfig->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()); + } + + /** + * Get a list of all apps in the apps folder + * + * @return list<string> an array of app names (string IDs) + */ + public function getAllAppsInAppsFolders(): array { + $apps = []; + + foreach (\OC::$APPSROOTS as $apps_dir) { + if (!is_readable($apps_dir['path'])) { + $this->logger->warning('unable to read app folder : ' . $apps_dir['path'], ['app' => 'core']); + continue; + } + $dh = opendir($apps_dir['path']); + + 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') + ) { + $apps[] = $file; + } + } + } + } + + return array_values(array_unique($apps)); } /** * 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); }); return array_keys($appsForUser); } - /** - * @param IGroup $group - * @return array - */ 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); }); @@ -205,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))) { @@ -250,7 +301,7 @@ class AppManager implements IAppManager { private function getAppTypes(string $app): array { //load the cache if (count($this->appTypes) === 0) { - $this->appTypes = $this->appConfig->getValues(false, 'types') ?: []; + $this->appTypes = $this->getAppConfig()->getValues(false, 'types') ?: []; } if (isset($this->appTypes[$app])) { @@ -267,12 +318,8 @@ class AppManager implements IAppManager { return $this->autoDisabledApps; } - /** - * @param string $appId - * @return array - */ public function getAppRestriction(string $appId): array { - $values = $this->getInstalledAppsValues(); + $values = $this->getEnabledAppsValues(); if (!isset($values[$appId])) { return []; @@ -284,12 +331,11 @@ class AppManager implements IAppManager { return json_decode($values[$appId], true); } - /** * Check if an app is enabled for user * * @param string $appId - * @param \OCP\IUser $user (optional) if not defined, the currently logged in user will be used + * @param \OCP\IUser|null $user (optional) if not defined, the currently logged in user will be used * @return bool */ public function isEnabledForUser($appId, $user = null) { @@ -299,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; } @@ -321,7 +367,9 @@ class AppManager implements IAppManager { if (!is_array($groupIds)) { $jsonError = json_last_error(); - $this->logger->warning('AppManger::checkAppForUser - can\'t decode group IDs: ' . print_r($enabled, true) . ' - json error code: ' . $jsonError); + $jsonErrorMsg = json_last_error_msg(); + // this really should never happen (if it does, the admin should check the `enabled` key value via `occ config:list` because it's bogus for some reason) + $this->logger->warning('AppManager::checkAppForUser - can\'t decode group IDs listed in app\'s enabled config key: ' . print_r($enabled, true) . ' - JSON error (' . $jsonError . ') ' . $jsonErrorMsg); return false; } @@ -347,7 +395,9 @@ class AppManager implements IAppManager { if (!is_array($groupIds)) { $jsonError = json_last_error(); - $this->logger->warning('AppManger::checkAppForUser - can\'t decode group IDs: ' . print_r($enabled, true) . ' - json error code: ' . $jsonError); + $jsonErrorMsg = json_last_error_msg(); + // this really should never happen (if it does, the admin should check the `enabled` key value via `occ config:list` because it's bogus for some reason) + $this->logger->warning('AppManager::checkAppForGroups - can\'t decode group IDs listed in app\'s enabled config key: ' . print_r($enabled, true) . ' - JSON error (' . $jsonError . ') ' . $jsonErrorMsg); return false; } @@ -361,20 +411,35 @@ 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]); } - public function ignoreNextcloudRequirementForApp(string $appId): void { + /** + * Overwrite the `max-version` requirement for this app. + */ + public function overwriteNextcloudRequirement(string $appId): void { $ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []); if (!in_array($appId, $ignoreMaxApps, true)) { $ignoreMaxApps[] = $appId; - $this->config->setSystemValue('app_install_overwrite', $ignoreMaxApps); } + $this->config->setSystemValue('app_install_overwrite', $ignoreMaxApps); + } + + /** + * Remove the `max-version` overwrite for this app. + * This means this app now again can not be enabled if the `max-version` is smaller than the current Nextcloud version. + */ + public function removeOverwriteNextcloudRequirement(string $appId): void { + $ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []); + $ignoreMaxApps = array_filter($ignoreMaxApps, fn (string $id) => $id !== $appId); + $this->config->setSystemValue('app_install_overwrite', $ignoreMaxApps); } public function loadApp(string $app): void { @@ -386,51 +451,19 @@ class AppManager implements IAppManager { if ($appPath === false) { return; } - $eventLogger = \OC::$server->get(\OCP\Diagnostics\IEventLogger::class); - $eventLogger->start("bootstrap:load_app:$app", "Load $app"); + $eventLogger = \OC::$server->get(IEventLogger::class); + $eventLogger->start("bootstrap:load_app:$app", "Load app: $app"); // 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'); - - $eventLogger = \OC::$server->get(IEventLogger::class); - $eventLogger->start('bootstrap:load_app_' . $app, 'Load app: ' . $app); - 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.', [ + 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, ]); - } 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.', [ - '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"); @@ -480,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) { @@ -502,6 +535,7 @@ class AppManager implements IAppManager { $eventLogger->end("bootstrap:load_app:$app"); } + /** * Check if an app is loaded * @param string $app app id @@ -512,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->ignoreNextcloudRequirementForApp($appId); + $this->overwriteNextcloudRequirement($appId); } - $this->installedAppsCache[$appId] = 'yes'; - $this->appConfig->setValue($appId, 'enabled', '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); } /** @@ -579,8 +609,12 @@ 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->ignoreNextcloudRequirementForApp($appId); + $this->overwriteNextcloudRequirement($appId); } /** @var string[] $groupIds */ @@ -591,13 +625,15 @@ class AppManager implements IAppManager { : $group; }, $groups); - $this->installedAppsCache[$appId] = json_encode($groupIds); - $this->appConfig->setValue($appId, 'enabled', 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); } /** @@ -607,21 +643,21 @@ class AppManager implements IAppManager { * @param bool $automaticDisabled * @throws \Exception if app can't be disabled */ - public function disableApp($appId, $automaticDisabled = false) { + public function disableApp($appId, $automaticDisabled = false): void { if ($this->isAlwaysEnabled($appId)) { throw new \Exception("$appId can't be disabled."); } if ($automaticDisabled) { - $previousSetting = $this->appConfig->getValue($appId, 'enabled', 'yes'); + $previousSetting = $this->getAppConfig()->getValue($appId, 'enabled', 'yes'); if ($previousSetting !== 'yes' && $previousSetting !== 'no') { $previousSetting = json_decode($previousSetting, true); } $this->autoDisabledApps[$appId] = $previousSetting; } - unset($this->installedAppsCache[$appId]); - $this->appConfig->setValue($appId, 'enabled', 'no'); + unset($this->enabledAppsCache[$appId]); + $this->getAppConfig()->setValue($appId, 'enabled', 'no'); // run uninstall steps $appData = $this->getAppInfo($appId); @@ -639,11 +675,9 @@ class AppManager implements IAppManager { /** * Get the directory for the given app. * - * @param string $appId - * @return string * @throws AppPathNotFoundException if app folder can't be found */ - public function getAppPath($appId) { + public function getAppPath(string $appId): string { $appPath = \OC_App::getAppPath($appId); if ($appPath === false) { throw new AppPathNotFoundException('Could not find path for ' . $appId); @@ -669,7 +703,7 @@ class AppManager implements IAppManager { /** * Clear the cached list of apps when enabling/disabling an app */ - public function clearAppsCache() { + public function clearAppsCache(): void { $this->appInfos = []; } @@ -683,10 +717,10 @@ 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->appConfig->getValue($appId, 'installed_version'); + $appDbVersion = $this->getAppConfig()->getValue($appId, 'installed_version'); if ($appDbVersion && isset($appInfo['version']) && version_compare($appInfo['version'], $appDbVersion, '>') @@ -702,33 +736,24 @@ class AppManager implements IAppManager { /** * Returns the app information from "appinfo/info.xml". * - * @param string $appId app id - * - * @param bool $path - * @param null $lang + * @param string|null $lang * @return array|null app info */ public function getAppInfo(string $appId, bool $path = false, $lang = null) { if ($path) { - $file = $appId; - } else { - if ($lang === null && isset($this->appInfos[$appId])) { - return $this->appInfos[$appId]; - } - try { - $appPath = $this->getAppPath($appId); - } catch (AppPathNotFoundException $e) { - return null; - } - $file = $appPath . '/appinfo/info.xml'; + throw new \InvalidArgumentException('Calling IAppManager::getAppInfo() with a path is no longer supported. Please call IAppManager::getAppInfoByPath() instead and verify that the path is good before calling.'); } - - $parser = new InfoParser($this->memCacheFactory->createLocal('core.appinfo')); - $data = $parser->parse($file); - - if (is_array($data)) { - $data = \OC_App::parseAppInfo($data, $lang); + if ($lang === null && isset($this->appInfos[$appId])) { + return $this->appInfos[$appId]; } + try { + $appPath = $this->getAppPath($appId); + } catch (AppPathNotFoundException) { + return null; + } + $file = $appPath . '/appinfo/info.xml'; + + $data = $this->getAppInfoByPath($file, $lang); if ($lang === null) { $this->appInfos[$appId] = $data; @@ -737,15 +762,43 @@ class AppManager implements IAppManager { return $data; } + public function getAppInfoByPath(string $path, ?string $lang = null): ?array { + if (!str_ends_with($path, '/appinfo/info.xml')) { + return null; + } + + $parser = new InfoParser($this->memCacheFactory->createLocal('core.appinfo')); + $data = $parser->parse($path); + + if (is_array($data)) { + $data = $parser->applyL10N($data, $lang); + } + + return $data; + } + 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 @@ -755,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); @@ -778,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); } @@ -817,65 +874,79 @@ class AppManager implements IAppManager { /** * @inheritdoc */ - public function getDefaultEnabledApps():array { + public function getDefaultEnabledApps(): array { $this->loadShippedJson(); return $this->defaultEnabled; } + /** + * @inheritdoc + */ public function getDefaultAppForUser(?IUser $user = null, bool $withFallbacks = true): string { - // Set fallback to always-enabled files app - $appId = $withFallbacks ? 'files' : ''; - $defaultApps = explode(',', $this->config->getSystemValueString('defaultapp', '')); - $defaultApps = array_filter($defaultApps); - - $user ??= $this->userSession->getUser(); - - if ($user !== null) { - $userDefaultApps = explode(',', $this->config->getUserValue($user->getUID(), 'core', 'defaultapp')); - $defaultApps = array_filter(array_merge($userDefaultApps, $defaultApps)); - if (empty($defaultApps) && $withFallbacks) { - /* Fallback on user defined apporder */ - $customOrders = json_decode($this->config->getUserValue($user->getUID(), 'core', 'apporder', '[]'), true, flags:JSON_THROW_ON_ERROR); - if (!empty($customOrders)) { - // filter only entries with app key (when added using closures or NavigationManager::add the app is not guranteed to be set) - $customOrders = array_filter($customOrders, fn ($entry) => isset($entry['app'])); - // sort apps by order - usort($customOrders, fn ($a, $b) => $a['order'] - $b['order']); - // set default apps to sorted apps - $defaultApps = array_map(fn ($entry) => $entry['app'], $customOrders); - } - } - } + $id = $this->getNavigationManager()->getDefaultEntryIdForUser($user, $withFallbacks); + $entry = $this->getNavigationManager()->get($id); + return (string)$entry['app']; + } - if (empty($defaultApps) && $withFallbacks) { - $defaultApps = ['dashboard','files']; - } + /** + * @inheritdoc + */ + public function getDefaultApps(): array { + $ids = $this->getNavigationManager()->getDefaultEntryIds(); + + return array_values(array_unique(array_map(function (string $id) { + $entry = $this->getNavigationManager()->get($id); + return (string)$entry['app']; + }, $ids))); + } - // Find the first app that is enabled for the current user + /** + * @inheritdoc + */ + public function setDefaultApps(array $defaultApps): void { + $entries = $this->getNavigationManager()->getAll(); + $ids = []; foreach ($defaultApps as $defaultApp) { - $defaultApp = \OC_App::cleanAppId(strip_tags($defaultApp)); - if ($this->isEnabledForUser($defaultApp, $user)) { - $appId = $defaultApp; - break; + foreach ($entries as $entry) { + if ((string)$entry['app'] === $defaultApp) { + $ids[] = (string)$entry['id']; + break; + } } } - - return $appId; - } - - public function getDefaultApps(): array { - return explode(',', $this->config->getSystemValueString('defaultapp', 'dashboard,files')); + $this->getNavigationManager()->setDefaultEntryIds($ids); } - public function setDefaultApps(array $defaultApps): void { - foreach ($defaultApps as $app) { - if (!$this->isInstalled($app)) { - $this->logger->debug('Can not set not installed app as default app', ['missing_app' => $app]); - throw new InvalidArgumentException('App is not installed'); + public function isBackendRequired(string $backend): bool { + foreach ($this->appInfos as $appInfo) { + foreach ($appInfo['dependencies']['backend'] as $appBackend) { + if ($backend === $appBackend) { + return true; + } } } - $this->config->setSystemValue('defaultapp', join(',', $defaultApps)); + 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 { + /* 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 dfc93fdfaa2..1443be81e92 100644 --- a/lib/private/App/AppStore/Bundles/Bundle.php +++ b/lib/private/App/AppStore/Bundles/Bundle.php @@ -1,39 +1,20 @@ <?php + /** - * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Lukas Reschke <lukas@statuscode.ch> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\App\AppStore\Bundles; use OCP\IL10N; abstract class Bundle { - /** @var IL10N */ - protected $l10n; - /** * @param IL10N $l10n */ - public function __construct(IL10N $l10n) { - $this->l10n = $l10n; + public function __construct( + protected IL10N $l10n, + ) { } /** diff --git a/lib/private/App/AppStore/Bundles/BundleFetcher.php b/lib/private/App/AppStore/Bundles/BundleFetcher.php index 0d2bb61495f..4ff53b0c70b 100644 --- a/lib/private/App/AppStore/Bundles/BundleFetcher.php +++ b/lib/private/App/AppStore/Bundles/BundleFetcher.php @@ -1,36 +1,17 @@ <?php + /** - * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch> - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Lukas Reschke <lukas@statuscode.ch> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\App\AppStore\Bundles; use OCP\IL10N; class BundleFetcher { - private IL10N $l10n; - - public function __construct(IL10N $l10n) { - $this->l10n = $l10n; + public function __construct( + private IL10N $l10n, + ) { } /** @@ -43,6 +24,7 @@ class BundleFetcher { new GroupwareBundle($this->l10n), new SocialSharingBundle($this->l10n), new EducationBundle($this->l10n), + new PublicSectorBundle($this->l10n), ]; } diff --git a/lib/private/App/AppStore/Bundles/EducationBundle.php b/lib/private/App/AppStore/Bundles/EducationBundle.php index 58ffd4f83b8..23681ec7416 100644 --- a/lib/private/App/AppStore/Bundles/EducationBundle.php +++ b/lib/private/App/AppStore/Bundles/EducationBundle.php @@ -1,25 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch> - * - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\App\AppStore\Bundles; @@ -28,7 +11,7 @@ class EducationBundle extends Bundle { * {@inheritDoc} */ public function getName() { - return $this->l10n->t('Education Edition'); + return $this->l10n->t('Education bundle'); } /** @@ -42,6 +25,7 @@ class EducationBundle extends Bundle { 'announcementcenter', 'quota_warning', 'user_saml', + 'whiteboard', ]; } } diff --git a/lib/private/App/AppStore/Bundles/EnterpriseBundle.php b/lib/private/App/AppStore/Bundles/EnterpriseBundle.php index bb64ec391ff..fc2d43e0388 100644 --- a/lib/private/App/AppStore/Bundles/EnterpriseBundle.php +++ b/lib/private/App/AppStore/Bundles/EnterpriseBundle.php @@ -1,25 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch> - * - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\App\AppStore\Bundles; diff --git a/lib/private/App/AppStore/Bundles/GroupwareBundle.php b/lib/private/App/AppStore/Bundles/GroupwareBundle.php index 3a46ada52ad..93fa70268cd 100644 --- a/lib/private/App/AppStore/Bundles/GroupwareBundle.php +++ b/lib/private/App/AppStore/Bundles/GroupwareBundle.php @@ -1,26 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch> - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\App\AppStore\Bundles; diff --git a/lib/private/App/AppStore/Bundles/HubBundle.php b/lib/private/App/AppStore/Bundles/HubBundle.php index d5d236a1855..354e01e25c2 100644 --- a/lib/private/App/AppStore/Bundles/HubBundle.php +++ b/lib/private/App/AppStore/Bundles/HubBundle.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020 Arthur Schiwon <blizzz@arthur-schiwon.de> - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Julius Härtl <jus@bitgrid.net> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\App\AppStore\Bundles; diff --git a/lib/private/App/AppStore/Bundles/PublicSectorBundle.php b/lib/private/App/AppStore/Bundles/PublicSectorBundle.php new file mode 100644 index 00000000000..106a6353029 --- /dev/null +++ b/lib/private/App/AppStore/Bundles/PublicSectorBundle.php @@ -0,0 +1,36 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\App\AppStore\Bundles; + +class PublicSectorBundle extends Bundle { + /** + * {@inheritDoc} + */ + public function getName(): string { + return $this->l10n->t('Public sector bundle'); + } + + /** + * {@inheritDoc} + */ + public function getAppIdentifiers(): array { + + return [ + 'files_confidential', + 'forms', + 'collectives', + 'files_antivirus', + 'twofactor_nextcloud_notification', + 'tables', + 'richdocuments', + 'admin_audit', + 'files_retention', + 'whiteboard', + ]; + } + +} diff --git a/lib/private/App/AppStore/Bundles/SocialSharingBundle.php b/lib/private/App/AppStore/Bundles/SocialSharingBundle.php index f823792745b..40f0fb15977 100644 --- a/lib/private/App/AppStore/Bundles/SocialSharingBundle.php +++ b/lib/private/App/AppStore/Bundles/SocialSharingBundle.php @@ -1,25 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch> - * - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\App\AppStore\Bundles; diff --git a/lib/private/App/AppStore/Fetcher/AppDiscoverFetcher.php b/lib/private/App/AppStore/Fetcher/AppDiscoverFetcher.php new file mode 100644 index 00000000000..8389e525750 --- /dev/null +++ b/lib/private/App/AppStore/Fetcher/AppDiscoverFetcher.php @@ -0,0 +1,100 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OC\App\AppStore\Fetcher; + +use DateTimeImmutable; +use OC\App\CompareVersion; +use OC\Files\AppData\Factory; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Http\Client\IClientService; +use OCP\IConfig; +use OCP\Support\Subscription\IRegistry; +use Psr\Log\LoggerInterface; + +class AppDiscoverFetcher extends Fetcher { + + public const INVALIDATE_AFTER_SECONDS = 86400; + + public function __construct( + Factory $appDataFactory, + IClientService $clientService, + ITimeFactory $timeFactory, + IConfig $config, + LoggerInterface $logger, + IRegistry $registry, + private CompareVersion $compareVersion, + ) { + parent::__construct( + $appDataFactory, + $clientService, + $timeFactory, + $config, + $logger, + $registry + ); + + $this->fileName = 'discover.json'; + $this->endpointName = 'discover.json'; + } + + /** + * Get the app discover section entries + * + * @param bool $allowUnstable Include also upcoming entries + */ + public function get($allowUnstable = false) { + $entries = parent::get(false); + $now = new DateTimeImmutable(); + + return array_filter($entries, function (array $entry) use ($now, $allowUnstable) { + // Always remove expired entries + if (isset($entry['expiryDate'])) { + try { + $expiryDate = new DateTimeImmutable($entry['expiryDate']); + if ($expiryDate < $now) { + return false; + } + } catch (\Throwable $e) { + // Invalid expiryDate format + return false; + } + } + + // If not include upcoming entries, check for upcoming dates and remove those entries + if (!$allowUnstable && isset($entry['date'])) { + try { + $date = new DateTimeImmutable($entry['date']); + if ($date > $now) { + return false; + } + } catch (\Throwable $e) { + // Invalid date format + return false; + } + } + // Otherwise the entry is not time limited and should stay + return true; + }); + } + + public function getETag(): ?string { + $rootFolder = $this->appData->getFolder('/'); + + try { + $file = $rootFolder->getFile($this->fileName); + $jsonBlob = json_decode($file->getContent(), true); + + if (is_array($jsonBlob) && isset($jsonBlob['ETag'])) { + return (string)$jsonBlob['ETag']; + } + } catch (\Throwable $e) { + // ignore + } + return null; + } +} diff --git a/lib/private/App/AppStore/Fetcher/AppFetcher.php b/lib/private/App/AppStore/Fetcher/AppFetcher.php index f9fbd05855b..bbf4b00245b 100644 --- a/lib/private/App/AppStore/Fetcher/AppFetcher.php +++ b/lib/private/App/AppStore/Fetcher/AppFetcher.php @@ -1,31 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Jakub Onderka <ahoj@jakubonderka.cz> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\App\AppStore\Fetcher; @@ -39,22 +16,18 @@ use OCP\Support\Subscription\IRegistry; use Psr\Log\LoggerInterface; class AppFetcher extends Fetcher { - /** @var CompareVersion */ - private $compareVersion; - - /** @var IRegistry */ - protected $registry; - /** @var bool */ private $ignoreMaxVersion; - public function __construct(Factory $appDataFactory, + public function __construct( + Factory $appDataFactory, IClientService $clientService, ITimeFactory $timeFactory, IConfig $config, - CompareVersion $compareVersion, + private CompareVersion $compareVersion, LoggerInterface $logger, - IRegistry $registry) { + protected IRegistry $registry, + ) { parent::__construct( $appDataFactory, $clientService, @@ -64,9 +37,6 @@ class AppFetcher extends Fetcher { $registry ); - $this->compareVersion = $compareVersion; - $this->registry = $registry; - $this->fileName = 'apps.json'; $this->endpointName = 'apps.json'; $this->ignoreMaxVersion = true; @@ -85,7 +55,8 @@ class AppFetcher extends Fetcher { /** @var mixed[] $response */ $response = parent::fetch($ETag, $content); - if (empty($response)) { + if (!isset($response['data']) || $response['data'] === null) { + $this->logger->warning('Response from appstore is invalid, apps could not be retrieved. Try again later.', ['app' => 'appstoreFetcher']); return []; } @@ -108,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']); @@ -180,11 +151,13 @@ class AppFetcher extends Fetcher { $this->ignoreMaxVersion = $ignoreMaxVersion; } - - public function get($allowUnstable = false) { + public function get($allowUnstable = false): array { $allowPreReleases = $allowUnstable || $this->getChannel() === 'beta' || $this->getChannel() === 'daily' || $this->getChannel() === 'git'; $apps = parent::get($allowPreReleases); + if (empty($apps)) { + return []; + } $allowList = $this->config->getSystemValue('appsallowlist'); // If the admin specified a allow list, filter apps from the appstore diff --git a/lib/private/App/AppStore/Fetcher/CategoryFetcher.php b/lib/private/App/AppStore/Fetcher/CategoryFetcher.php index d1bbe4f7b04..d7857d41bee 100644 --- a/lib/private/App/AppStore/Fetcher/CategoryFetcher.php +++ b/lib/private/App/AppStore/Fetcher/CategoryFetcher.php @@ -1,28 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch> - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\App\AppStore\Fetcher; @@ -34,12 +14,14 @@ use OCP\Support\Subscription\IRegistry; use Psr\Log\LoggerInterface; class CategoryFetcher extends Fetcher { - public function __construct(Factory $appDataFactory, + public function __construct( + Factory $appDataFactory, IClientService $clientService, ITimeFactory $timeFactory, IConfig $config, LoggerInterface $logger, - IRegistry $registry) { + IRegistry $registry, + ) { parent::__construct( $appDataFactory, $clientService, diff --git a/lib/private/App/AppStore/Fetcher/Fetcher.php b/lib/private/App/AppStore/Fetcher/Fetcher.php index a693804f50f..24876675d60 100644 --- a/lib/private/App/AppStore/Fetcher/Fetcher.php +++ b/lib/private/App/AppStore/Fetcher/Fetcher.php @@ -1,32 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch> - * - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Steffen Lindner <mail@steffen-lindner.de> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\App\AppStore\Fetcher; @@ -38,25 +14,19 @@ use OCP\Files\IAppData; use OCP\Files\NotFoundException; use OCP\Http\Client\IClientService; use OCP\IConfig; +use OCP\Server; +use OCP\ServerVersion; use OCP\Support\Subscription\IRegistry; use Psr\Log\LoggerInterface; abstract class Fetcher { public const INVALIDATE_AFTER_SECONDS = 3600; + public const INVALIDATE_AFTER_SECONDS_UNSTABLE = 900; public const RETRY_AFTER_FAILURE_SECONDS = 300; + public const APP_STORE_URL = 'https://apps.nextcloud.com/api/v1'; /** @var IAppData */ protected $appData; - /** @var IClientService */ - protected $clientService; - /** @var ITimeFactory */ - protected $timeFactory; - /** @var IConfig */ - protected $config; - /** @var LoggerInterface */ - protected $logger; - /** @var IRegistry */ - protected $registry; /** @var string */ protected $fileName; @@ -67,18 +37,15 @@ abstract class Fetcher { /** @var ?string */ protected $channel = null; - public function __construct(Factory $appDataFactory, - IClientService $clientService, - ITimeFactory $timeFactory, - IConfig $config, - LoggerInterface $logger, - IRegistry $registry) { + public function __construct( + Factory $appDataFactory, + protected IClientService $clientService, + protected ITimeFactory $timeFactory, + protected IConfig $config, + protected LoggerInterface $logger, + protected IRegistry $registry, + ) { $this->appData = $appDataFactory->get('appstore'); - $this->clientService = $clientService; - $this->timeFactory = $timeFactory; - $this->config = $config; - $this->logger = $logger; - $this->registry = $registry; } /** @@ -89,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 []; @@ -109,7 +76,7 @@ abstract class Fetcher { ]; } - if ($this->config->getSystemValueString('appstoreurl', 'https://apps.nextcloud.com/api/v1') === 'https://apps.nextcloud.com/api/v1') { + if ($this->config->getSystemValueString('appstoreurl', self::APP_STORE_URL) === self::APP_STORE_URL) { // If we have a valid subscription key, send it to the appstore $subscriptionKey = $this->config->getAppValue('support', 'subscription_key'); if ($this->registry->delegateHasValidSubscription() && $subscriptionKey) { @@ -123,7 +90,8 @@ abstract class Fetcher { $response = $client->get($this->getEndpoint(), $options); } catch (ConnectException $e) { $this->config->setAppValue('settings', 'appstore-fetcher-lastFailure', (string)time()); - throw $e; + $this->logger->error('Failed to connect to the app store', ['exception' => $e]); + return []; } $responseJson = []; @@ -145,7 +113,7 @@ abstract class Fetcher { } /** - * Returns the array with the categories on the appstore server + * Returns the array with the entries on the appstore server * * @param bool [$allowUnstable] Allow unstable releases * @return array @@ -153,8 +121,10 @@ abstract class Fetcher { public function get($allowUnstable = false) { $appstoreenabled = $this->config->getSystemValueBool('appstoreenabled', true); $internetavailable = $this->config->getSystemValueBool('has_internet_connection', true); + $isDefaultAppStore = $this->config->getSystemValueString('appstoreurl', self::APP_STORE_URL) === self::APP_STORE_URL; - if (!$appstoreenabled || !$internetavailable) { + if (!$appstoreenabled || (!$internetavailable && $isDefaultAppStore)) { + $this->logger->info('AppStore is disabled or this instance has no Internet connection to access the default app store', ['app' => 'appstoreFetcher']); return []; } @@ -168,12 +138,17 @@ abstract class Fetcher { $file = $rootFolder->getFile($this->fileName); $jsonBlob = json_decode($file->getContent(), true); - // Always get latests apps info if $allowUnstable - if (!$allowUnstable && is_array($jsonBlob)) { + if (is_array($jsonBlob)) { // No caching when the version has been updated if (isset($jsonBlob['ncversion']) && $jsonBlob['ncversion'] === $this->getVersion()) { // If the timestamp is older than 3600 seconds request the files new - if ((int)$jsonBlob['timestamp'] > ($this->timeFactory->getTime() - self::INVALIDATE_AFTER_SECONDS)) { + $invalidateAfterSeconds = self::INVALIDATE_AFTER_SECONDS; + + if ($allowUnstable) { + $invalidateAfterSeconds = self::INVALIDATE_AFTER_SECONDS_UNSTABLE; + } + + if ((int)$jsonBlob['timestamp'] > ($this->timeFactory->getTime() - $invalidateAfterSeconds)) { return $jsonBlob['data']; } @@ -192,15 +167,10 @@ abstract class Fetcher { try { $responseJson = $this->fetch($ETag, $content, $allowUnstable); - if (empty($responseJson)) { + if (empty($responseJson) || empty($responseJson['data'])) { return []; } - // Don't store the apps request file - if ($allowUnstable) { - return $responseJson['data']; - } - $file->putContent(json_encode($responseJson)); return json_decode($file->getContent(), true)['data']; } catch (ConnectException $e) { @@ -240,7 +210,7 @@ abstract class Fetcher { */ protected function getChannel() { if ($this->channel === null) { - $this->channel = \OC_Util::getChannel(); + $this->channel = Server::get(ServerVersion::class)->getChannel(); } return $this->channel; } diff --git a/lib/private/App/AppStore/Version/Version.php b/lib/private/App/AppStore/Version/Version.php index d41ca1770f0..2d169a291f1 100644 --- a/lib/private/App/AppStore/Version/Version.php +++ b/lib/private/App/AppStore/Version/Version.php @@ -1,40 +1,20 @@ <?php + /** - * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch> - * - * @author Lukas Reschke <lukas@statuscode.ch> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\App\AppStore\Version; class Version { - /** @var string */ - private $minVersion; - /** @var string */ - private $maxVersion; - /** * @param string $minVersion * @param string $maxVersion */ - public function __construct($minVersion, $maxVersion) { - $this->minVersion = $minVersion; - $this->maxVersion = $maxVersion; + public function __construct( + private string $minVersion, + private string $maxVersion, + ) { } /** diff --git a/lib/private/App/AppStore/Version/VersionParser.php b/lib/private/App/AppStore/Version/VersionParser.php index eac9c935517..8976f28837f 100644 --- a/lib/private/App/AppStore/Version/VersionParser.php +++ b/lib/private/App/AppStore/Version/VersionParser.php @@ -1,26 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\App\AppStore\Version; diff --git a/lib/private/App/CompareVersion.php b/lib/private/App/CompareVersion.php index a349c7aa6f2..7edc3e565b1 100644 --- a/lib/private/App/CompareVersion.php +++ b/lib/private/App/CompareVersion.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\App; diff --git a/lib/private/App/DependencyAnalyzer.php b/lib/private/App/DependencyAnalyzer.php index 3bdc551ea5a..bde8719c41d 100644 --- a/lib/private/App/DependencyAnalyzer.php +++ b/lib/private/App/DependencyAnalyzer.php @@ -1,60 +1,28 @@ <?php + +declare(strict_types=1); + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @copyright Copyright (c) 2016, Lukas Reschke <lukas@statuscode.ch> - * - * @author Bernhard Posselt <dev@bernhard-posselt.com> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Stefan Weil <sw@weilnetz.de> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @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/> - * + * 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 Platform */ - private $platform; - /** @var \OCP\IL10N */ - private $l; - /** @var array */ - private $appInfo; - - /** - * @param Platform $platform - * @param \OCP\IL10N $l - */ - public function __construct(Platform $platform, IL10N $l) { - $this->platform = $platform; - $this->l = $l; + public function __construct( + private Platform $platform, + private IL10N $l, + ) { } /** - * @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 { @@ -90,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 + * second version */ - private function normalizeVersions($first, $second) { + private function normalizeVersions(string $first, string $second): array { $first = explode('.', $first); $second = explode('.', $second); @@ -110,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']; @@ -173,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; @@ -196,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; @@ -213,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); @@ -223,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; @@ -253,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; @@ -298,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; @@ -326,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'])) { @@ -347,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; @@ -373,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 79d051fd2a1..634dc1fbdd5 100644 --- a/lib/private/App/InfoParser.php +++ b/lib/private/App/InfoParser.php @@ -1,54 +1,29 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * @copyright Copyright (c) 2016, Lukas Reschke <lukas@statuscode.ch> - * - * @author Andreas Fischer <bantu@owncloud.com> - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Joas Schilling <coding@schilljs.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @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/> - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\App; use OCP\ICache; -use function libxml_disable_entity_loader; use function simplexml_load_string; class InfoParser { - /** @var \OCP\ICache|null */ - private $cache; - /** * @param ICache|null $cache */ - public function __construct(ICache $cache = null) { - $this->cache = $cache; + public function __construct( + private ?ICache $cache = null, + ) { } /** * @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; } @@ -61,13 +36,7 @@ class InfoParser { } libxml_use_internal_errors(true); - if ((PHP_VERSION_ID < 80000)) { - $loadEntities = libxml_disable_entity_loader(false); - $xml = simplexml_load_string(file_get_contents($file)); - libxml_disable_entity_loader($loadEntities); - } else { - $xml = simplexml_load_string(file_get_contents($file)); - } + $xml = simplexml_load_string(file_get_contents($file)); if ($xml === false) { libxml_clear_errors(); @@ -75,7 +44,7 @@ class InfoParser { } $array = $this->xmlToArray($xml); - if (is_null($array)) { + if (is_string($array)) { return null; } @@ -145,6 +114,12 @@ class InfoParser { if (!array_key_exists('personal-section', $array['settings'])) { $array['settings']['personal-section'] = []; } + if (!array_key_exists('dependencies', $array)) { + $array['dependencies'] = []; + } + if (!array_key_exists('backend', $array['dependencies'])) { + $array['dependencies']['backend'] = []; + } if (array_key_exists('types', $array)) { if (is_array($array['types'])) { @@ -209,10 +184,23 @@ class InfoParser { if (isset($array['settings']['personal-section']) && !is_array($array['settings']['personal-section'])) { $array['settings']['personal-section'] = [$array['settings']['personal-section']]; } - if (isset($array['navigations']['navigation']) && $this->isNavigationItem($array['navigations']['navigation'])) { $array['navigations']['navigation'] = [$array['navigations']['navigation']]; } + if (isset($array['dependencies']['backend']) && !is_array($array['dependencies']['backend'])) { + $array['dependencies']['backend'] = [$array['dependencies']['backend']]; + } + + // Ensure some fields are always arrays + if (isset($array['screenshot']) && !is_array($array['screenshot'])) { + $array['screenshot'] = [$array['screenshot']]; + } + if (isset($array['author']) && !is_array($array['author'])) { + $array['author'] = [$array['author']]; + } + if (isset($array['category']) && !is_array($array['category'])) { + $array['category'] = [$array['category']]; + } if ($this->cache !== null) { $this->cache->set($fileCacheKey, json_encode($array)); @@ -220,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') { @@ -233,21 +217,21 @@ 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])) { - $array[$element] = $totalElement > 1 ? [] : ""; + $array[$element] = $totalElement > 1 ? [] : ''; } /** @var \SimpleXMLElement $node */ // Has attributes @@ -255,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; } @@ -272,7 +259,7 @@ class InfoParser { } else { $array[$element] = $data; } - // Just a value + // Just a value } else { if ($totalElement > 1) { $array[$element][] = $this->xmlToArray($node); @@ -284,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 daff247d1bd..80e4cefed64 100644 --- a/lib/private/App/Platform.php +++ b/lib/private/App/Platform.php @@ -1,27 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bernhard Posselt <dev@bernhard-posselt.com> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Julius Härtl <jus@bitgrid.net> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @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/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\App; @@ -36,10 +18,9 @@ use OCP\IConfig; * @package OC\App */ class Platform { - private IConfig $config; - - public function __construct(IConfig $config) { - $this->config = $config; + public function __construct( + private IConfig $config, + ) { } public function getPhpVersion(): string { diff --git a/lib/private/App/PlatformRepository.php b/lib/private/App/PlatformRepository.php index 9b94c0b07bc..faed8b07feb 100644 --- a/lib/private/App/PlatformRepository.php +++ b/lib/private/App/PlatformRepository.php @@ -1,24 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @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/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OC\App; @@ -154,7 +139,7 @@ class PlatformRepository { */ public function normalizeVersion(string $version, ?string $fullVersion = null): string { $version = trim($version); - if (null === $fullVersion) { + if ($fullVersion === null) { $fullVersion = $version; } // ignore aliases and just assume the alias is required instead of the source @@ -165,7 +150,7 @@ class PlatformRepository { if (preg_match('{^(?:dev-)?(?:master|trunk|default)$}i', $version)) { return '9999999-dev'; } - if ('dev-' === strtolower(substr($version, 0, 4))) { + if (strtolower(substr($version, 0, 4)) === 'dev-') { return 'dev-' . substr($version, 4); } // match classical versioning @@ -188,7 +173,7 @@ class PlatformRepository { // add version modifiers if a version was matched if (isset($index)) { if (!empty($matches[$index])) { - if ('stable' === $matches[$index]) { + if ($matches[$index] === 'stable') { return $version; } $version .= '-' . $this->expandStability($matches[$index]) . (!empty($matches[$index + 1]) ? $matches[$index + 1] : ''); |