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.php692
1 files changed, 520 insertions, 172 deletions
diff --git a/lib/private/App/AppManager.php b/lib/private/App/AppManager.php
index d14f0a2644e..7778393b3b3 100644
--- a/lib/private/App/AppManager.php
+++ b/lib/private/App/AppManager.php
@@ -1,55 +1,38 @@
<?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 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;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class AppManager implements IAppManager {
/**
@@ -64,110 +47,179 @@ class AppManager implements IAppManager {
'prevent_group_restriction',
];
- /** @var IUserSession */
- private $userSession;
-
- /** @var IConfig */
- private $config;
-
- /** @var AppConfig */
- private $appConfig;
-
- /** @var IGroupManager */
- private $groupManager;
-
- /** @var ICacheFactory */
- private $memCacheFactory;
-
- /** @var EventDispatcherInterface */
- private $dispatcher;
-
- /** @var LoggerInterface */
- private $logger;
-
/** @var string[] $appId => $enabled */
- private $installedAppsCache;
+ private array $enabledAppsCache = [];
- /** @var string[] */
- private $shippedApps;
+ /** @var string[]|null */
+ private ?array $shippedApps = null;
private array $alwaysEnabled = [];
private array $defaultEnabled = [];
/** @var array */
- private $appInfos = [];
+ private array $appInfos = [];
/** @var array */
- private $appVersions = [];
+ private array $appVersions = [];
/** @var array */
- private $autoDisabledApps = [];
+ 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,
+ ) {
+ }
- public function __construct(IUserSession $userSession,
- IConfig $config,
- AppConfig $appConfig,
- IGroupManager $groupManager,
- ICacheFactory $memCacheFactory,
- EventDispatcherInterface $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 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() {
- 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 \OCP\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);
});
@@ -175,18 +227,99 @@ class AppManager implements IAppManager {
}
/**
- * @return array
+ * 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 getAutoDisabledApps(): array {
- return $this->autoDisabledApps;
+ 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 [];
}
/**
- * @param string $appId
* @return array
*/
+ public function getAutoDisabledApps(): array {
+ return $this->autoDisabledApps;
+ }
+
public function getAppRestriction(string $appId): array {
- $values = $this->getInstalledAppsValues();
+ $values = $this->getEnabledAppsValues();
if (!isset($values[$appId])) {
return [];
@@ -198,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) {
@@ -213,20 +345,15 @@ 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;
}
}
- /**
- * @param string $enabled
- * @param IUser $user
- * @return bool
- */
- private function checkAppForUser($enabled, $user) {
+ private function checkAppForUser(string $enabled, ?IUser $user): bool {
if ($enabled === 'yes') {
return true;
} elseif ($user === null) {
@@ -240,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;
}
@@ -254,16 +383,9 @@ class AppManager implements IAppManager {
}
}
- /**
- * @param string $enabled
- * @param IGroup $group
- * @return bool
- */
private function checkAppForGroups(string $enabled, IGroup $group): bool {
if ($enabled === 'yes') {
return true;
- } elseif ($group === null) {
- return false;
} else {
if (empty($enabled)) {
return false;
@@ -273,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;
}
@@ -287,20 +411,138 @@ class AppManager implements IAppManager {
* Notice: This actually checks if the app is enabled and not only if it is installed.
*
* @param string $appId
- * @param \OCP\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 {
+ 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]);
}
/**
@@ -309,21 +551,29 @@ class AppManager implements IAppManager {
* @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);
}
/**
@@ -345,7 +595,7 @@ class AppManager implements IAppManager {
* Enable an app only for specific groups
*
* @param string $appId
- * @param \OCP\IGroup[] $groups
+ * @param IGroup[] $groups
* @param bool $forceEnable
* @throws \InvalidArgumentException if app can't be enabled for groups
* @throws AppPathNotFoundException
@@ -359,23 +609,31 @@ 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 */
$groupIds = array_map(function ($group) {
- /** @var \OCP\IGroup $group */
+ /** @var IGroup $group */
return ($group instanceof IGroup)
? $group->getGID()
: $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);
}
/**
@@ -385,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);
@@ -407,6 +665,7 @@ class AppManager implements IAppManager {
\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
));
@@ -416,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);
@@ -446,9 +703,7 @@ class AppManager implements IAppManager {
/**
* Clear the cached list of apps when enabling/disabling an app
*/
- public function clearAppsCache() {
- $settingsMemCache = $this->memCacheFactory->createDistributed('settings');
- $settingsMemCache->clear('listApps');
+ public function clearAppsCache(): void {
$this->appInfos = [];
}
@@ -462,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, '>')
@@ -481,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;
@@ -516,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
@@ -534,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);
@@ -556,7 +830,11 @@ class AppManager implements IAppManager {
return in_array($appId, $this->shippedApps, true);
}
- private function isAlwaysEnabled($appId) {
+ private function isAlwaysEnabled(string $appId): bool {
+ if ($appId === 'core') {
+ return true;
+ }
+
$alwaysEnabled = $this->getAlwaysEnabledApps();
return in_array($appId, $alwaysEnabled, true);
}
@@ -565,7 +843,7 @@ class AppManager implements IAppManager {
* In case you change this method, also change \OC\App\CodeChecker\InfoChecker::loadShippedJson()
* @throws \Exception
*/
- private function loadShippedJson() {
+ private function loadShippedJson(): void {
if ($this->shippedApps === null) {
$shippedJson = \OC::$SERVERROOT . '/core/shipped.json';
if (!file_exists($shippedJson)) {
@@ -596,9 +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 {
+ $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);
+ }
}