aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/App/AppManager.php
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private/App/AppManager.php')
-rw-r--r--lib/private/App/AppManager.php952
1 files changed, 952 insertions, 0 deletions
diff --git a/lib/private/App/AppManager.php b/lib/private/App/AppManager.php
new file mode 100644
index 00000000000..7778393b3b3
--- /dev/null
+++ b/lib/private/App/AppManager.php
@@ -0,0 +1,952 @@
+<?php
+
+/**
+ * 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 OC\AppConfig;
+use OC\AppFramework\Bootstrap\Coordinator;
+use OC\Config\ConfigManager;
+use OCP\Activity\IManager as IActivityManager;
+use OCP\App\AppPathNotFoundException;
+use OCP\App\Events\AppDisableEvent;
+use OCP\App\Events\AppEnableEvent;
+use OCP\App\IAppManager;
+use OCP\App\ManagerEvent;
+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;
+
+class AppManager implements IAppManager {
+ /**
+ * Apps with these types can not be enabled for certain groups only
+ * @var string[]
+ */
+ protected $protectedAppTypes = [
+ 'filesystem',
+ 'prelogin',
+ 'authentication',
+ 'logging',
+ 'prevent_group_restriction',
+ ];
+
+ /** @var string[] $appId => $enabled */
+ private array $enabledAppsCache = [];
+
+ /** @var string[]|null */
+ private ?array $shippedApps = null;
+
+ private array $alwaysEnabled = [];
+ private array $defaultEnabled = [];
+
+ /** @var array */
+ private array $appInfos = [];
+
+ /** @var array */
+ private array $appVersions = [];
+
+ /** @var array */
+ private array $autoDisabledApps = [];
+ private array $appTypes = [];
+
+ /** @var array<string, true> */
+ private array $loadedApps = [];
+
+ 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;
+ }
+
+ /**
+ * 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 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->enabledAppsCache = array_filter($values, function ($value) {
+ return $value !== 'no';
+ });
+ ksort($this->enabledAppsCache);
+ }
+ return $this->enabledAppsCache;
+ }
+
+ /**
+ * Deprecated alias
+ *
+ * @return string[]
+ */
+ public function getInstalledApps() {
+ 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 list<string>
+ */
+ public function getEnabledAppsForUser(IUser $user) {
+ $apps = $this->getEnabledAppsValues();
+ $appsForUser = array_filter($apps, function ($enabled) use ($user) {
+ return $this->checkAppForUser($enabled, $user);
+ });
+ return array_keys($appsForUser);
+ }
+
+ public function getEnabledAppsForGroup(IGroup $group): array {
+ $apps = $this->getEnabledAppsValues();
+ $appsForGroups = array_filter($apps, function ($enabled) use ($group) {
+ return $this->checkAppForGroups($enabled, $group);
+ });
+ return array_keys($appsForGroups);
+ }
+
+ /**
+ * Loads all apps
+ *
+ * @param string[] $types
+ * @return bool
+ *
+ * This function walks through the Nextcloud directory and loads all apps
+ * it can find. A directory contains an app if the file /appinfo/info.xml
+ * exists.
+ *
+ * if $types is set to non-empty array, only apps of those types will be loaded
+ */
+ public function loadApps(array $types = []): bool {
+ if ($this->config->getSystemValueBool('maintenance', false)) {
+ return false;
+ }
+ // Load the enabled apps here
+ $apps = \OC_App::getEnabledApps();
+
+ // Add each apps' folder as allowed class path
+ foreach ($apps as $app) {
+ // If the app is already loaded then autoloading it makes no sense
+ if (!$this->isAppLoaded($app)) {
+ $path = \OC_App::getAppPath($app);
+ if ($path !== false) {
+ \OC_App::registerAutoloading($app, $path);
+ }
+ }
+ }
+
+ // prevent app loading from printing output
+ ob_start();
+ foreach ($apps as $app) {
+ if (!$this->isAppLoaded($app) && ($types === [] || $this->isType($app, $types))) {
+ try {
+ $this->loadApp($app);
+ } catch (\Throwable $e) {
+ $this->logger->emergency('Error during app loading: ' . $e->getMessage(), [
+ 'exception' => $e,
+ 'app' => $app,
+ ]);
+ }
+ }
+ }
+ ob_end_clean();
+
+ return true;
+ }
+
+ /**
+ * check if an app is of a specific type
+ *
+ * @param string $app
+ * @param array $types
+ * @return bool
+ */
+ public function isType(string $app, array $types): bool {
+ $appTypes = $this->getAppTypes($app);
+ foreach ($types as $type) {
+ if (in_array($type, $appTypes, true)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * get the types of an app
+ *
+ * @param string $app
+ * @return string[]
+ */
+ private function getAppTypes(string $app): array {
+ //load the cache
+ if (count($this->appTypes) === 0) {
+ $this->appTypes = $this->getAppConfig()->getValues(false, 'types') ?: [];
+ }
+
+ if (isset($this->appTypes[$app])) {
+ return explode(',', $this->appTypes[$app]);
+ }
+
+ return [];
+ }
+
+ /**
+ * @return array
+ */
+ public function getAutoDisabledApps(): array {
+ return $this->autoDisabledApps;
+ }
+
+ public function getAppRestriction(string $appId): array {
+ $values = $this->getEnabledAppsValues();
+
+ if (!isset($values[$appId])) {
+ return [];
+ }
+
+ if ($values[$appId] === 'yes' || $values[$appId] === 'no') {
+ return [];
+ }
+ return json_decode($values[$appId], true);
+ }
+
+ /**
+ * Check if an app is enabled for user
+ *
+ * @param string $appId
+ * @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) {
+ if ($this->isAlwaysEnabled($appId)) {
+ return true;
+ }
+ if ($user === null) {
+ $user = $this->userSession->getUser();
+ }
+ $enabledAppsValues = $this->getEnabledAppsValues();
+ if (isset($enabledAppsValues[$appId])) {
+ return $this->checkAppForUser($enabledAppsValues[$appId], $user);
+ } else {
+ return false;
+ }
+ }
+
+ private function checkAppForUser(string $enabled, ?IUser $user): bool {
+ if ($enabled === 'yes') {
+ return true;
+ } elseif ($user === null) {
+ return false;
+ } else {
+ if (empty($enabled)) {
+ return false;
+ }
+
+ $groupIds = json_decode($enabled);
+
+ if (!is_array($groupIds)) {
+ $jsonError = json_last_error();
+ $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;
+ }
+
+ $userGroups = $this->groupManager->getUserGroupIds($user);
+ foreach ($userGroups as $groupId) {
+ if (in_array($groupId, $groupIds, true)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ private function checkAppForGroups(string $enabled, IGroup $group): bool {
+ if ($enabled === 'yes') {
+ return true;
+ } else {
+ if (empty($enabled)) {
+ return false;
+ }
+
+ $groupIds = json_decode($enabled);
+
+ if (!is_array($groupIds)) {
+ $jsonError = json_last_error();
+ $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;
+ }
+
+ return in_array($group->getGID(), $groupIds);
+ }
+ }
+
+ /**
+ * Check if an app is enabled in the instance
+ *
+ * Notice: This actually checks if the app is enabled and not only if it is installed.
+ *
+ * @param string $appId
+ */
+ public function isInstalled($appId): bool {
+ return $this->isEnabledForAnyone($appId);
+ }
+
+ public function isEnabledForAnyone(string $appId): bool {
+ $enabledAppsValues = $this->getEnabledAppsValues();
+ return isset($enabledAppsValues[$appId]);
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * 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 {
+ if (isset($this->loadedApps[$app])) {
+ return;
+ }
+ $this->loadedApps[$app] = true;
+ $appPath = \OC_App::getAppPath($app);
+ if ($appPath === false) {
+ return;
+ }
+ $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);
+
+ 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,
+ ]);
+ }
+
+ $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");
+ $info = $this->getAppInfo($app);
+ if (!empty($info['activity'])) {
+ $activityManager = \OC::$server->get(IActivityManager::class);
+ if (!empty($info['activity']['filters'])) {
+ foreach ($info['activity']['filters'] as $filter) {
+ $activityManager->registerFilter($filter);
+ }
+ }
+ if (!empty($info['activity']['settings'])) {
+ foreach ($info['activity']['settings'] as $setting) {
+ $activityManager->registerSetting($setting);
+ }
+ }
+ if (!empty($info['activity']['providers'])) {
+ foreach ($info['activity']['providers'] as $provider) {
+ $activityManager->registerProvider($provider);
+ }
+ }
+ }
+
+ if (!empty($info['settings'])) {
+ $settingsManager = \OC::$server->get(ISettingsManager::class);
+ if (!empty($info['settings']['admin'])) {
+ foreach ($info['settings']['admin'] as $setting) {
+ $settingsManager->registerSetting('admin', $setting);
+ }
+ }
+ if (!empty($info['settings']['admin-section'])) {
+ foreach ($info['settings']['admin-section'] as $section) {
+ $settingsManager->registerSection('admin', $section);
+ }
+ }
+ if (!empty($info['settings']['personal'])) {
+ foreach ($info['settings']['personal'] as $setting) {
+ $settingsManager->registerSetting('personal', $setting);
+ }
+ }
+ if (!empty($info['settings']['personal-section'])) {
+ foreach ($info['settings']['personal-section'] as $section) {
+ $settingsManager->registerSection('personal', $section);
+ }
+ }
+ }
+
+ 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'];
+ $collaboratorSearch = null;
+ $autoCompleteManager = null;
+ foreach ($plugins as $plugin) {
+ if ($plugin['@attributes']['type'] === 'collaborator-search') {
+ $pluginInfo = [
+ 'shareType' => $plugin['@attributes']['share-type'],
+ 'class' => $plugin['@value'],
+ ];
+ $collaboratorSearch ??= \OC::$server->get(ICollaboratorSearch::class);
+ $collaboratorSearch->registerPlugin($pluginInfo);
+ } elseif ($plugin['@attributes']['type'] === 'autocomplete-sort') {
+ $autoCompleteManager ??= \OC::$server->get(IAutoCompleteManager::class);
+ $autoCompleteManager->registerSorter($plugin['@value']);
+ }
+ }
+ }
+ $eventLogger->end("bootstrap:load_app:$app:info");
+
+ $eventLogger->end("bootstrap:load_app:$app");
+ }
+
+ /**
+ * Check if an app is loaded
+ * @param string $app app id
+ * @since 26.0.0
+ */
+ public function isAppLoaded(string $app): bool {
+ return isset($this->loadedApps[$app]);
+ }
+
+ /**
+ * Enable an app for every user
+ *
+ * @param string $appId
+ * @param bool $forceEnable
+ * @throws AppPathNotFoundException
+ * @throws \InvalidArgumentException if the application is not installed yet
+ */
+ public function enableApp(string $appId, bool $forceEnable = false): void {
+ // Check if app exists
+ $this->getAppPath($appId);
+
+ if ($this->config->getAppValue($appId, 'installed_version', '') === '') {
+ throw new \InvalidArgumentException("$appId is not installed, cannot be enabled.");
+ }
+
+ if ($forceEnable) {
+ $this->overwriteNextcloudRequirement($appId);
+ }
+
+ $this->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);
+ }
+
+ /**
+ * Whether a list of types contains a protected app type
+ *
+ * @param string[] $types
+ * @return bool
+ */
+ public function hasProtectedAppType($types) {
+ if (empty($types)) {
+ return false;
+ }
+
+ $protectedTypes = array_intersect($this->protectedAppTypes, $types);
+ return !empty($protectedTypes);
+ }
+
+ /**
+ * Enable an app only for specific groups
+ *
+ * @param string $appId
+ * @param IGroup[] $groups
+ * @param bool $forceEnable
+ * @throws \InvalidArgumentException if app can't be enabled for groups
+ * @throws AppPathNotFoundException
+ */
+ public function enableAppForGroups(string $appId, array $groups, bool $forceEnable = false): void {
+ // Check if app exists
+ $this->getAppPath($appId);
+
+ $info = $this->getAppInfo($appId);
+ if (!empty($info['types']) && $this->hasProtectedAppType($info['types'])) {
+ throw new \InvalidArgumentException("$appId can't be enabled for groups.");
+ }
+
+ if ($this->config->getAppValue($appId, 'installed_version', '') === '') {
+ throw new \InvalidArgumentException("$appId is not installed, cannot be enabled.");
+ }
+
+ if ($forceEnable) {
+ $this->overwriteNextcloudRequirement($appId);
+ }
+
+ /** @var string[] $groupIds */
+ $groupIds = array_map(function ($group) {
+ /** @var IGroup $group */
+ return ($group instanceof IGroup)
+ ? $group->getGID()
+ : $group;
+ }, $groups);
+
+ $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);
+ }
+
+ /**
+ * Disable an app for every user
+ *
+ * @param string $appId
+ * @param bool $automaticDisabled
+ * @throws \Exception if app can't be disabled
+ */
+ public function disableApp($appId, $automaticDisabled = false): void {
+ if ($this->isAlwaysEnabled($appId)) {
+ throw new \Exception("$appId can't be disabled.");
+ }
+
+ if ($automaticDisabled) {
+ $previousSetting = $this->getAppConfig()->getValue($appId, 'enabled', 'yes');
+ if ($previousSetting !== 'yes' && $previousSetting !== 'no') {
+ $previousSetting = json_decode($previousSetting, true);
+ }
+ $this->autoDisabledApps[$appId] = $previousSetting;
+ }
+
+ unset($this->enabledAppsCache[$appId]);
+ $this->getAppConfig()->setValue($appId, 'enabled', 'no');
+
+ // run uninstall steps
+ $appData = $this->getAppInfo($appId);
+ if (!is_null($appData)) {
+ \OC_App::executeRepairSteps($appId, $appData['repair-steps']['uninstall']);
+ }
+
+ $this->dispatcher->dispatchTyped(new AppDisableEvent($appId));
+ $this->dispatcher->dispatch(ManagerEvent::EVENT_APP_DISABLE, new ManagerEvent(
+ ManagerEvent::EVENT_APP_DISABLE, $appId
+ ));
+ $this->clearAppsCache();
+ }
+
+ /**
+ * Get the directory for the given app.
+ *
+ * @throws AppPathNotFoundException if app folder can't be found
+ */
+ public function getAppPath(string $appId): string {
+ $appPath = \OC_App::getAppPath($appId);
+ if ($appPath === false) {
+ throw new AppPathNotFoundException('Could not find path for ' . $appId);
+ }
+ return $appPath;
+ }
+
+ /**
+ * Get the web path for the given app.
+ *
+ * @param string $appId
+ * @return string
+ * @throws AppPathNotFoundException if app path can't be found
+ */
+ public function getAppWebPath(string $appId): string {
+ $appWebPath = \OC_App::getAppWebPath($appId);
+ if ($appWebPath === false) {
+ throw new AppPathNotFoundException('Could not find web path for ' . $appId);
+ }
+ return $appWebPath;
+ }
+
+ /**
+ * Clear the cached list of apps when enabling/disabling an app
+ */
+ public function clearAppsCache(): void {
+ $this->appInfos = [];
+ }
+
+ /**
+ * Returns a list of apps that need upgrade
+ *
+ * @param string $version Nextcloud version as array of version components
+ * @return array list of app info from apps that need an upgrade
+ *
+ * @internal
+ */
+ public function getAppsNeedingUpgrade($version) {
+ $appsToUpgrade = [];
+ $apps = $this->getEnabledApps();
+ foreach ($apps as $appId) {
+ $appInfo = $this->getAppInfo($appId);
+ $appDbVersion = $this->getAppConfig()->getValue($appId, 'installed_version');
+ if ($appDbVersion
+ && isset($appInfo['version'])
+ && version_compare($appInfo['version'], $appDbVersion, '>')
+ && \OC_App::isAppCompatible($version, $appInfo)
+ ) {
+ $appsToUpgrade[] = $appInfo;
+ }
+ }
+
+ return $appsToUpgrade;
+ }
+
+ /**
+ * Returns the app information from "appinfo/info.xml".
+ *
+ * @param string|null $lang
+ * @return array|null app info
+ */
+ public function getAppInfo(string $appId, bool $path = false, $lang = null) {
+ if ($path) {
+ 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.');
+ }
+ 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;
+ }
+
+ 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])) {
+ 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
+ *
+ * @return array list of app info from incompatible apps
+ *
+ * @internal
+ */
+ public function getIncompatibleApps(string $version): array {
+ $apps = $this->getEnabledApps();
+ $incompatibleApps = [];
+ foreach ($apps as $appId) {
+ $info = $this->getAppInfo($appId);
+ if ($info === null) {
+ $incompatibleApps[] = ['id' => $appId, 'name' => $appId];
+ } elseif (!\OC_App::isAppCompatible($version, $info)) {
+ $incompatibleApps[] = $info;
+ }
+ }
+ return $incompatibleApps;
+ }
+
+ /**
+ * @inheritdoc
+ * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::isShipped()
+ */
+ public function isShipped($appId) {
+ $this->loadShippedJson();
+ return in_array($appId, $this->shippedApps, true);
+ }
+
+ private function isAlwaysEnabled(string $appId): bool {
+ if ($appId === 'core') {
+ return true;
+ }
+
+ $alwaysEnabled = $this->getAlwaysEnabledApps();
+ return in_array($appId, $alwaysEnabled, true);
+ }
+
+ /**
+ * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::loadShippedJson()
+ * @throws \Exception
+ */
+ private function loadShippedJson(): void {
+ if ($this->shippedApps === null) {
+ $shippedJson = \OC::$SERVERROOT . '/core/shipped.json';
+ if (!file_exists($shippedJson)) {
+ throw new \Exception("File not found: $shippedJson");
+ }
+ $content = json_decode(file_get_contents($shippedJson), true);
+ $this->shippedApps = $content['shippedApps'];
+ $this->alwaysEnabled = $content['alwaysEnabled'];
+ $this->defaultEnabled = $content['defaultEnabled'];
+ }
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getAlwaysEnabledApps() {
+ $this->loadShippedJson();
+ return $this->alwaysEnabled;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function isDefaultEnabled(string $appId): bool {
+ return (in_array($appId, $this->getDefaultEnabledApps()));
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getDefaultEnabledApps(): array {
+ $this->loadShippedJson();
+
+ return $this->defaultEnabled;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getDefaultAppForUser(?IUser $user = null, bool $withFallbacks = true): string {
+ $id = $this->getNavigationManager()->getDefaultEntryIdForUser($user, $withFallbacks);
+ $entry = $this->getNavigationManager()->get($id);
+ return (string)$entry['app'];
+ }
+
+ /**
+ * @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)));
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setDefaultApps(array $defaultApps): void {
+ $entries = $this->getNavigationManager()->getAll();
+ $ids = [];
+ foreach ($defaultApps as $defaultApp) {
+ foreach ($entries as $entry) {
+ if ((string)$entry['app'] === $defaultApp) {
+ $ids[] = (string)$entry['id'];
+ break;
+ }
+ }
+ }
+ $this->getNavigationManager()->setDefaultEntryIds($ids);
+ }
+
+ public function isBackendRequired(string $backend): bool {
+ foreach ($this->appInfos as $appInfo) {
+ foreach ($appInfo['dependencies']['backend'] as $appBackend) {
+ if ($backend === $appBackend) {
+ return true;
+ }
+ }
+ }
+
+ 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);
+ }
+}