aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/App
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private/App')
-rw-r--r--lib/private/App/AppManager.php533
-rw-r--r--lib/private/App/AppStore/AppNotFoundException.php13
-rw-r--r--lib/private/App/AppStore/Bundles/Bundle.php31
-rw-r--r--lib/private/App/AppStore/Bundles/BundleFetcher.php32
-rw-r--r--lib/private/App/AppStore/Bundles/EducationBundle.php26
-rw-r--r--lib/private/App/AppStore/Bundles/EnterpriseBundle.php23
-rw-r--r--lib/private/App/AppStore/Bundles/GroupwareBundle.php24
-rw-r--r--lib/private/App/AppStore/Bundles/HubBundle.php22
-rw-r--r--lib/private/App/AppStore/Bundles/PublicSectorBundle.php36
-rw-r--r--lib/private/App/AppStore/Bundles/SocialSharingBundle.php23
-rw-r--r--lib/private/App/AppStore/Fetcher/AppDiscoverFetcher.php100
-rw-r--r--lib/private/App/AppStore/Fetcher/AppFetcher.php59
-rw-r--r--lib/private/App/AppStore/Fetcher/CategoryFetcher.php32
-rw-r--r--lib/private/App/AppStore/Fetcher/Fetcher.php96
-rw-r--r--lib/private/App/AppStore/Version/Version.php34
-rw-r--r--lib/private/App/AppStore/Version/VersionParser.php24
-rw-r--r--lib/private/App/CompareVersion.php21
-rw-r--r--lib/private/App/DependencyAnalyzer.php150
-rw-r--r--lib/private/App/InfoParser.php183
-rw-r--r--lib/private/App/Platform.php33
-rw-r--r--lib/private/App/PlatformRepository.php29
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] : '');