diff options
Diffstat (limited to 'lib/private/App')
21 files changed, 483 insertions, 740 deletions
diff --git a/lib/private/App/AppManager.php b/lib/private/App/AppManager.php index 3867dcc509e..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,13 +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; @@ -76,7 +48,7 @@ class AppManager implements IAppManager { ]; /** @var string[] $appId => $enabled */ - private array $installedAppsCache = []; + private array $enabledAppsCache = []; /** @var string[]|null */ private ?array $shippedApps = null; @@ -99,6 +71,7 @@ class AppManager implements IAppManager { 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, @@ -111,9 +84,18 @@ 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 = 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; @@ -135,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; } @@ -146,59 +128,98 @@ 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()); + } + + /** + * 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); }); @@ -235,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))) { @@ -297,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 []; @@ -314,7 +331,6 @@ class AppManager implements IAppManager { return json_decode($values[$appId], true); } - /** * Check if an app is enabled for user * @@ -329,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; } @@ -351,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; } @@ -377,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; } @@ -391,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 { @@ -416,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.', [ - '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"); @@ -510,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) { @@ -532,6 +535,7 @@ class AppManager implements IAppManager { $eventLogger->end("bootstrap:load_app:$app"); } + /** * Check if an app is loaded * @param string $app app id @@ -542,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->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); } /** @@ -609,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 */ @@ -621,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); } /** @@ -637,7 +643,7 @@ 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."); } @@ -650,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 @@ -669,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); @@ -699,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 = []; } @@ -713,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'); @@ -737,25 +741,19 @@ class AppManager implements IAppManager { */ 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; @@ -764,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 @@ -782,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); @@ -805,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); } @@ -850,59 +880,73 @@ class AppManager implements IAppManager { 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; + $this->getNavigationManager()->setDefaultEntryIds($ids); } - public function getDefaultApps(): array { - return explode(',', $this->config->getSystemValueString('defaultapp', 'dashboard,files')); - } - - 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 fb86edff559..1443be81e92 100644 --- a/lib/private/App/AppStore/Bundles/Bundle.php +++ b/lib/private/App/AppStore/Bundles/Bundle.php @@ -1,25 +1,8 @@ <?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; diff --git a/lib/private/App/AppStore/Bundles/BundleFetcher.php b/lib/private/App/AppStore/Bundles/BundleFetcher.php index 53e82b39b81..4ff53b0c70b 100644 --- a/lib/private/App/AppStore/Bundles/BundleFetcher.php +++ b/lib/private/App/AppStore/Bundles/BundleFetcher.php @@ -1,26 +1,8 @@ <?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; 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 index 6634e2ff41e..106a6353029 100644 --- a/lib/private/App/AppStore/Bundles/PublicSectorBundle.php +++ b/lib/private/App/AppStore/Bundles/PublicSectorBundle.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2024 Stephan Orbaugh <stephan.orbaugh@nextcloud.com> - * - * @author Stephan Orbaugh <stephan.orbaugh@nextcloud.com> - * - * @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: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\App\AppStore\Bundles; @@ -45,6 +29,7 @@ class PublicSectorBundle extends Bundle { '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 index 46a18eb4c32..8389e525750 100644 --- a/lib/private/App/AppStore/Fetcher/AppDiscoverFetcher.php +++ b/lib/private/App/AppStore/Fetcher/AppDiscoverFetcher.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2024 Ferdinand Thiessen <opensource@fthiessen.de> - * - * @author Ferdinand Thiessen <opensource@fthiessen.de> - * - * @license AGPL-3.0-or-later - * - * 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: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OC\App\AppStore\Fetcher; @@ -98,7 +82,7 @@ class AppDiscoverFetcher extends Fetcher { }); } - public function getETag(): string|null { + public function getETag(): ?string { $rootFolder = $this->appData->getFolder('/'); try { diff --git a/lib/private/App/AppStore/Fetcher/AppFetcher.php b/lib/private/App/AppStore/Fetcher/AppFetcher.php index bb1a0e89484..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; @@ -42,7 +19,8 @@ class AppFetcher extends Fetcher { /** @var bool */ private $ignoreMaxVersion; - public function __construct(Factory $appDataFactory, + public function __construct( + Factory $appDataFactory, IClientService $clientService, ITimeFactory $timeFactory, IConfig $config, @@ -77,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 []; } @@ -100,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']); @@ -172,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 9b84d2a6196..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; diff --git a/lib/private/App/AppStore/Fetcher/Fetcher.php b/lib/private/App/AppStore/Fetcher/Fetcher.php index 75d2069d07a..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,11 +14,14 @@ 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'; @@ -77,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 []; @@ -111,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 = []; @@ -144,6 +124,7 @@ abstract class Fetcher { $isDefaultAppStore = $this->config->getSystemValueString('appstoreurl', self::APP_STORE_URL) === self::APP_STORE_URL; 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 []; } @@ -157,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']; } @@ -185,11 +171,6 @@ abstract class Fetcher { 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) { @@ -229,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 5bd0226528f..2d169a291f1 100644 --- a/lib/private/App/AppStore/Version/Version.php +++ b/lib/private/App/AppStore/Version/Version.php @@ -1,24 +1,8 @@ <?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; 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 2974e31dc5d..bde8719c41d 100644 --- a/lib/private/App/DependencyAnalyzer.php +++ b/lib/private/App/DependencyAnalyzer.php @@ -1,45 +1,18 @@ <?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 array */ - private $appInfo; - - /** - * @param Platform $platform - * @param \OCP\IL10N $l - */ public function __construct( private Platform $platform, private IL10N $l, @@ -47,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 { @@ -87,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); @@ -107,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']; @@ -170,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; @@ -193,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; @@ -210,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); @@ -220,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; @@ -250,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; @@ -295,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; @@ -323,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'])) { @@ -344,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; @@ -370,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 cb1d63ee37c..634dc1fbdd5 100644 --- a/lib/private/App/InfoParser.php +++ b/lib/private/App/InfoParser.php @@ -1,36 +1,13 @@ <?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 { @@ -46,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; } @@ -59,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(); @@ -73,7 +44,7 @@ class InfoParser { } $array = $this->xmlToArray($xml); - if (is_null($array)) { + if (is_string($array)) { return null; } @@ -143,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'])) { @@ -207,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)); @@ -218,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') { @@ -231,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 @@ -253,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; } @@ -282,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 c5565e9e0c2..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; diff --git a/lib/private/App/PlatformRepository.php b/lib/private/App/PlatformRepository.php index 2d6ce7e89be..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; |