From f2a2136875e830ed904f4a78c165f56f5da4b5a8 Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 11 May 2026 13:29:08 -0400 Subject: [PATCH 1/5] refactor(AppManager): clean up code for clarity and drop redundant docblocks Signed-off-by: Josh --- lib/private/App/AppManager.php | 344 +++++++++++++-------------------- 1 file changed, 133 insertions(+), 211 deletions(-) diff --git a/lib/private/App/AppManager.php b/lib/private/App/AppManager.php index 0c3519b99d30a..e6c7b71279c04 100644 --- a/lib/private/App/AppManager.php +++ b/lib/private/App/AppManager.php @@ -82,7 +82,8 @@ class AppManager implements IAppManager { /** * 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 + * so it needs to work before installation. See how AppConfig and IURLGenerator are injected + * for reference. */ public function __construct( private IUserSession $userSession, @@ -165,30 +166,18 @@ private function getEnabledAppsValues(): array { } /** - * Deprecated alias - * - * @return string[] + * @deprecated 32.0.0 Use either {@see self::getEnabledApps} or {@see self::getEnabledAppsForUser} */ #[\Override] public function getInstalledApps() { return $this->getEnabledApps(); } - /** - * List all enabled apps, either for everyone or for some groups - * - * @return list - */ #[\Override] public function getEnabledApps(): array { return array_keys($this->getEnabledAppsValues()); } - /** - * Get a list of all apps in the apps folder - * - * @return list an array of app names (string IDs) - */ #[\Override] public function getAllAppsInAppsFolders(): array { $apps = []; @@ -216,12 +205,6 @@ public function getAllAppsInAppsFolders(): array { return array_values(array_unique($apps)); } - /** - * List all apps enabled for a user - * - * @param IUser $user - * @return list - */ #[\Override] public function getEnabledAppsForUser(IUser $user) { $apps = $this->getEnabledAppsValues(); @@ -240,18 +223,6 @@ public function getEnabledAppsForGroup(IGroup $group): array { 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 - */ #[\Override] public function loadApps(array $types = []): bool { if ($this->config->getSystemValueBool('maintenance', false)) { @@ -295,13 +266,6 @@ public function loadApps(array $types = []): bool { return true; } - /** - * check if an app is of a specific type - * - * @param string $app - * @param array $types - * @return bool - */ #[\Override] public function isType(string $app, array $types): bool { $appTypes = $this->getAppTypes($app); @@ -332,9 +296,6 @@ private function getAppTypes(string $app): array { return []; } - /** - * @return array - */ public function getAutoDisabledApps(): array { return $this->autoDisabledApps; } @@ -353,15 +314,8 @@ public function getAppRestriction(string $appId): array { return json_decode($values[$appId], true); } - /** - * Check if an app is enabled for user - * - * @param string $appId - * @param IUser|null $user (optional) if not defined, the currently logged in user will be used - * @return bool - */ #[\Override] - public function isEnabledForUser($appId, $user = null) { + public function isEnabledForUser(string $appId, IUser ?$user = null): bool { if ($this->isAlwaysEnabled($appId)) { return true; } @@ -428,15 +382,8 @@ private function checkAppForGroups(string $enabled, IGroup $group): bool { } } - /** - * 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 - */ #[\Override] - public function isInstalled($appId): bool { + public function isInstalled(string $appId): bool { return $this->isEnabledForAnyone($appId); } @@ -447,7 +394,11 @@ public function isEnabledForAnyone(string $appId): bool { } /** - * Overwrite the `max-version` requirement for this app. + * Disables Nextcloud version compatibility checks for a specific app. + * + * Adds the app to the 'app_install_overwrite' list, allowing it to be + * enabled even if the current Nextcloud version exceeds the app's + * defined 'max-version'. */ public function overwriteNextcloudRequirement(string $appId): void { $ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []); @@ -458,8 +409,11 @@ public function overwriteNextcloudRequirement(string $appId): void { } /** - * 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. + * Restores Nextcloud version compatibility checks for a specific app. + * + * This removes the app from the 'app_install_overwrite' list, meaning it can + * no longer be enabled if its maximum supported version is lower than the + * current Nextcloud version. */ public function removeOverwriteNextcloudRequirement(string $appId): void { $ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []); @@ -499,6 +453,7 @@ public function loadApp(string $app): void { $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 = Server::get(IActivityManager::class); if (!empty($info['activity']['filters'])) { @@ -577,22 +532,12 @@ public function loadApp(string $app): void { $eventLogger->end("bootstrap:load_app:$app"); } - /** - * Check if an app is loaded - * @param string $app app id - * @since 26.0.0 - */ #[\Override] 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 */ #[\Override] @@ -619,14 +564,8 @@ public function enableApp(string $appId, bool $forceEnable = false): void { $this->configManager->migrateConfigLexiconKeys($appId); } - /** - * Whether a list of types contains a protected app type - * - * @param string[] $types - * @return bool - */ #[\Override] - public function hasProtectedAppType($types) { + public function hasProtectedAppType(array $types): bool { if (empty($types)) { return false; } @@ -636,11 +575,7 @@ public function hasProtectedAppType($types) { } /** - * Enable an app only for specific groups - * - * @param string $appId - * @param IGroup[] $groups - * @param bool $forceEnable + * @param IGroup[]|string[] $groups * @throws \InvalidArgumentException if app can't be enabled for groups * @throws AppPathNotFoundException */ @@ -664,7 +599,7 @@ public function enableAppForGroups(string $appId, array $groups, bool $forceEnab /** @var string[] $groupIds */ $groupIds = array_map(function ($group) { - /** @var IGroup $group */ + /** @var IGroup|string $group */ return ($group instanceof IGroup) ? $group->getGID() : $group; @@ -682,10 +617,6 @@ public function enableAppForGroups(string $appId, array $groups, bool $forceEnab } /** - * Disable an app for every user - * - * @param string $appId - * @param bool $automaticDisabled * @throws \Exception if app can't be disabled */ #[\Override] @@ -719,11 +650,7 @@ public function disableApp($appId, $automaticDisabled = false): void { } /** - * Get the directory for the given app. - * * @psalm-taint-specialize - * - * @throws AppPathNotFoundException if app folder can't be found */ #[\Override] public function getAppPath(string $appId, bool $ignoreCache = false): string { @@ -740,11 +667,6 @@ public function getAppPath(string $appId, bool $ignoreCache = false): string { throw new AppPathNotFoundException('Could not find path for ' . $appId); } - /** - * Get the web path for the given app. - * - * @throws AppPathNotFoundException if app path can't be found - */ #[\Override] public function getAppWebPath(string $appId): string { if (($dir = $this->findAppInDirectories($appId)) !== false) { @@ -760,8 +682,12 @@ public function getAppWebPath(string $appId): string { * * @param bool $ignoreCache ignore cache and rebuild it * @return false|array{path: string, url: string} the apps root shape + * + * @internal + * + * TODO: Make private when OC_App::findAppInDirectories() is dropped. */ - public function findAppInDirectories(string $appId, bool $ignoreCache = false) { + public function findAppInDirectories(string $appId, bool $ignoreCache = false): array|false { $sanitizedAppId = $this->cleanAppId($appId); if ($sanitizedAppId !== $appId) { return false; @@ -804,23 +730,20 @@ public function findAppInDirectories(string $appId, bool $ignoreCache = false) { } } - /** - * Clear the cached list of apps when enabling/disabling an app - */ #[\Override] public function clearAppsCache(): void { $this->appInfos = []; } /** - * Returns a list of apps that need upgrade + * Returns a list of apps that need an upgrade for a specific Nextcloud version. * - * @param string $version Nextcloud version as array of version components - * @return array list of app info from apps that need an upgrade + * @param string $version The Nextcloud version to check compatibility against (e.g., '28.0.1') + * @return array[] A list of app info arrays for apps that require an upgrade * * @internal */ - public function getAppsNeedingUpgrade($version) { + public function getAppsNeedingUpgrade(string $version): array { $appsToUpgrade = []; $apps = $this->getEnabledApps(); foreach ($apps as $appId) { @@ -838,104 +761,106 @@ public function getAppsNeedingUpgrade($version) { return $appsToUpgrade; } - /** - * Returns the app information from "appinfo/info.xml". - * - * @param string|null $lang - * @return array|null app info - */ #[\Override] - public function getAppInfo(string $appId, bool $path = false, $lang = null) { + public function getAppInfo(string $appId, bool $path = false, string|null $lang = null): array|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.'); + throw new \InvalidArgumentException( + 'IAppManager::getAppInfo() no longer accepts paths. Use getAppInfoByPath() ' . + 'and validate the path 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); + $infoPath = $appPath . '/appinfo/info.xml'; + $appInfo = $this->getAppInfoByPath($infoPath, $lang); if ($lang === null) { - $this->appInfos[$appId] = $data; + $this->appInfos[$appId] = $appInfo; } - return $data; + return $appInfo; } #[\Override] - public function getAppInfoByPath(string $path, ?string $lang = null): ?array { + public function getAppInfoByPath(string $path, string|null $lang = null): array|null { if (!str_ends_with($path, '/appinfo/info.xml')) { return null; } $parser = new InfoParser($this->memCacheFactory->createLocal('core.appinfo')); - $data = $parser->parse($path); + $appInfo = $parser->parse($path); - if (is_array($data)) { - $data = $parser->applyL10N($data, $lang); + if ($appInfo === null) { + return null; // info file parsing error of some sort } - return $data; + $appInfo = $parser->applyL10N($appInfo, $lang); + return $appInfo; } #[\Override] 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'; - } + if ($useCache && isset($this->appVersions[$appId])) { + return $this->appVersions[$appId]; + } + + if ($appId === 'core') { + return $this->appVersions[$appId] = $this->serverVersion->getVersionString(); } - return $this->appVersions[$appId]; + + $appInfo = $this->getAppInfo($appId); + return $this->appVersions[$appId] = $appInfo['version'] ?? '0'; } - /** - * Returns the installed versions of all apps - * - * @return array - */ #[\Override] public function getAppInstalledVersions(bool $onlyEnabled = false): array { return $this->getAppConfig()->getAppInstalledVersions($onlyEnabled); } /** - * Returns a list of apps incompatible with the given version + * Returns a list of enabled apps incompatible with the given Nextcloud version. * - * @param string $version Nextcloud version as array of version components - * - * @return array list of app info from incompatible apps + * @param string $version The Nextcloud version to check compatibility against (e.g., '28.0.1') + * @return array[] A list of app info arrays for apps that are incompatible * * @internal */ public function getIncompatibleApps(string $version): array { - $apps = $this->getEnabledApps(); + $enabledAppIds = $this->getEnabledApps(); $incompatibleApps = []; - foreach ($apps as $appId) { - $info = $this->getAppInfo($appId); - if ($info === null) { - $incompatibleApps[] = ['id' => $appId, 'name' => $appId]; - } elseif (!$this->isAppCompatible($version, $info)) { - $incompatibleApps[] = $info; + + foreach ($enabledAppIds as $appId) { + $appInfo = $this->getAppInfo($appId); + + if ($appInfo === null) { + // assume incompatible if unable to load app info + // FIXME: This seems fragile; consider throwing instead? + $incompatibleApps[] = [ + 'id' => $appId, + 'name' => $appId, + ]; + } elseif (!$this->isAppCompatible($version, $appInfo)) { + $incompatibleApps[] = $appInfo; } } + return $incompatibleApps; } /** - * @inheritdoc - * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::isShipped() + * @throws \Exception if shipped apps inventory file cannot be loaded. */ #[\Override] - public function isShipped($appId) { + public function isShipped(string $appId): bool { $this->loadShippedJson(); return in_array($appId, $this->shippedApps, true); } @@ -950,88 +875,88 @@ private function isAlwaysEnabled(string $appId): bool { } /** - * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::loadShippedJson() - * @throws \Exception + * @throws \Exception if shipped apps inventory file cannot be loaded. */ 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"); + $filePath = \OC::$SERVERROOT . '/core/shipped.json'; + + if (!file_exists($filePath)) { + throw new \Exception("File not found: $filePath"); } - $content = json_decode(file_get_contents($shippedJson), true); - $this->shippedApps = $content['shippedApps']; - $this->alwaysEnabled = $content['alwaysEnabled']; - $this->defaultEnabled = $content['defaultEnabled']; + + $data = json_decode(file_get_contents($filePath), true); + + $this->shippedApps = $data['shippedApps']; + $this->alwaysEnabled = $data['alwaysEnabled']; + $this->defaultEnabled = $data['defaultEnabled']; } } - /** - * @inheritdoc - */ #[\Override] - public function getAlwaysEnabledApps() { + public function getAlwaysEnabledApps(): array { $this->loadShippedJson(); return $this->alwaysEnabled; } - /** - * @inheritdoc - */ #[\Override] public function isDefaultEnabled(string $appId): bool { return (in_array($appId, $this->getDefaultEnabledApps())); } - /** - * @inheritdoc - */ #[\Override] public function getDefaultEnabledApps(): array { $this->loadShippedJson(); - return $this->defaultEnabled; } - /** - * @inheritdoc - */ #[\Override] public function getDefaultAppForUser(?IUser $user = null, bool $withFallbacks = true): string { - $id = $this->getNavigationManager()->getDefaultEntryIdForUser($user, $withFallbacks); - $entry = $this->getNavigationManager()->get($id); + $navigationManager = $this->getNavigationManager(); + + $entryId = $navigationManager->getDefaultEntryIdForUser($user, $withFallbacks); + $entry = $navigationManager->get($entryId); + return (string)$entry['app']; } - /** - * @inheritdoc - */ #[\Override] public function getDefaultApps(): array { - $ids = $this->getNavigationManager()->getDefaultEntryIds(); + $navigationManager = $this->getNavigationManager(); - return array_values(array_unique(array_map(function (string $id) { - $entry = $this->getNavigationManager()->get($id); - return (string)$entry['app']; - }, $ids))); + $entryIds = $navigationManager->getDefaultEntryIds(); + + $apps = array_map( + fn(string $entryId) => (string)($navigationManager->get($entryId)['app']), + $entryIds + ); + + return array_values(array_unique(array_filter($apps))); } - /** - * @inheritdoc - */ #[\Override] 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; - } + $navigationManager = $this->getNavigationManager(); + + $entries = $navigationManager->getAll(); // technically this gets only 'link' entries not 'all' + + // Create a lookup map: ['appName' => 'entryId'] + // TODO: switch to array_column(); only concern is in theory I think we permit all numeric app ids/names though rare + $appToEntryMap = []; + foreach ($entries as $entry) { + $appName = (string)($entry['app']); + $appToEntryMap[$appName] = (string)($entry['id']); + } + + // Map the requested app names to their corresponding entry IDs + $entryIds = []; + foreach ($defaultApps as $appName) { + if (isset($appToEntryMap[$appName])) { + $entryids[] = $appToEntryMap[$appName]; } } - $this->getNavigationManager()->setDefaultEntryIds($ids); + + $navigationManager->setDefaultEntryIds($entryIds); } #[\Override] @@ -1050,8 +975,6 @@ public function isBackendRequired(string $backend): bool { } /** - * Clean the appId from forbidden characters - * * @psalm-taint-escape callable * @psalm-taint-escape cookie * @psalm-taint-escape file @@ -1073,35 +996,34 @@ public function cleanAppId(string $app): string { 'app' => $cleanAppId, // safer to log $cleanAppId even if it makes more challenging to troubleshooting (part of why character count is at least logged) ]); } + return $cleanAppId; } /** - * Read app types from info.xml and cache them in the database + * Store the app's types in config and, if a protected app, ensure it always has a valid enabled state. + * + * Protected apps are not permitted to have group restrictions, so any non-yes/non-no enabled state is + * normalized to 'yes'. + * + * @param string $app The app ID. + * @param array{types?: string[]} $appData App metadata containing the types list. + * + * @internal */ public function setAppTypes(string $app, array $appData): void { - if (isset($appData['types'])) { - $appTypes = implode(',', $appData['types']); - } else { - $appTypes = ''; - $appData['types'] = []; - } - - $this->config->setAppValue($app, 'types', $appTypes); + $types = $appData['types'] ?? []; + $this->config->setAppValue($app, 'types', implode(',', $types)); - if ($this->hasProtectedAppType($appData['types'])) { + if ($this->hasProtectedAppType($types)) { $enabled = $this->config->getAppValue($app, 'enabled', 'yes'); + // If enabled is a group list (not 'yes' or 'no'), force it to 'yes' if ($enabled !== 'yes' && $enabled !== 'no') { $this->config->setAppValue($app, 'enabled', 'yes'); } } } - /** - * Run upgrade tasks for an app after the code has already been updated - * - * @throws AppPathNotFoundException if app folder can't be found - */ #[\Override] public function upgradeApp(string $appId): bool { // for apps distributed with core, we refresh app path in case the downloaded version From 68cb47bd458527b8bcb2cedd83dafd5efb753c09 Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 11 May 2026 13:32:16 -0400 Subject: [PATCH 2/5] chore: fixup typo in AppManager.php Signed-off-by: Josh --- lib/private/App/AppManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/private/App/AppManager.php b/lib/private/App/AppManager.php index e6c7b71279c04..7fe1fcf187f9e 100644 --- a/lib/private/App/AppManager.php +++ b/lib/private/App/AppManager.php @@ -315,7 +315,7 @@ public function getAppRestriction(string $appId): array { } #[\Override] - public function isEnabledForUser(string $appId, IUser ?$user = null): bool { + public function isEnabledForUser(string $appId, ?IUser $user = null): bool { if ($this->isAlwaysEnabled($appId)) { return true; } From e0f574f0630baa0e9cba4f1e88b21ed8c8c53277 Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 1 Jun 2026 22:30:18 -0400 Subject: [PATCH 3/5] docs(IAppManager): fully document all OCP methods Signed-off-by: Josh --- lib/public/App/IAppManager.php | 293 ++++++++++++++++++++------------- 1 file changed, 175 insertions(+), 118 deletions(-) diff --git a/lib/public/App/IAppManager.php b/lib/public/App/IAppManager.php index 97acd9af7b1e3..b2164f8b96030 100644 --- a/lib/public/App/IAppManager.php +++ b/lib/public/App/IAppManager.php @@ -5,16 +5,20 @@ * SPDX-FileCopyrightText: 2016 ownCloud, Inc. * SPDX-License-Identifier: AGPL-3.0-only */ + namespace OCP\App; use OCP\IGroup; use OCP\IUser; /** - * Interface IAppManager + * Central interface for managing Nextcloud apps. + * + * Manages app discovery, enablement, loading. metadata, compatibility checks, + * and lifecycle operations. * - * @warning This interface shouldn't be included with dependency injection in - * classes used for installing Nextcloud. + * @warning This interface should NOT be included via DI in classes used for + * installing Nextcloud. * * @since 8.0.0 */ @@ -25,299 +29,351 @@ interface IAppManager { public const BACKEND_CALDAV = 'caldav'; /** - * Returns the app information from "appinfo/info.xml" for an app + * Returns parsed app metadata for the app identified by $appId. + * + * Reads the app's `appinfo/info.xml` and returns the parsed app info, + * optionally localized for the given language. * - * @param string|null $lang - * @return array|null + * @param string $appId App ID + * @param bool $path Deprecated. Must remain false; passing true throws an \InvalidArgumentException. + * @param string|null $lang Language code for localized metadata, or null for the default language + * @return AppInfoDefinition|AppInfoXmlDefinition|null + * @psalm-return ($lang is null ? (AppInfoXmlDefinition|null) : (AppInfoDefinition|null)) * @since 14.0.0 - * @since 31.0.0 Usage of $path is discontinued and throws an \InvalidArgumentException, use {@see self::getAppInfoByPath} instead. + * @since 31.0.0 The $path parameter is no longer supported; use {@see self::getAppInfoByPath()} instead. */ public function getAppInfo(string $appId, bool $path = false, $lang = null); /** - * Returns the app information from a given path ending with "/appinfo/info.xml" + * Returns parsed app metadata from a specific `appinfo/info.xml` file. * + * The path must point to an app's `.../appinfo/info.xml` file. + * + * @param string $path Absolute path to `appinfo/info.xml` + * @param string|null $lang Language code for localized metadata, or null for the default language + * @return AppInfoDefinition|AppInfoXmlDefinition|null + * @psalm-return ($lang is null ? (AppInfoXmlDefinition|null) : (AppInfoDefinition|null)) * @since 31.0.0 */ public function getAppInfoByPath(string $path, ?string $lang = null): ?array; /** - * Returns the app information from "appinfo/info.xml". + * Returns the version declared by the app's `appinfo/info.xml`. * - * @param string $appId - * @param bool $useCache - * @return string + * @param string $appId App ID + * @param bool $useCache Whether to reuse a cached version value + * @return string App version, or "0" if no version is declared * @since 14.0.0 */ public function getAppVersion(string $appId, bool $useCache = true): string; /** - * Returns the installed version of all apps + * Returns the installed version of apps known to the app configuration. * - * @return array + * @param bool $onlyEnabled Limit the result to enabled apps only + * @return array Map of app ID to installed version * @since 32.0.0 */ public function getAppInstalledVersions(bool $onlyEnabled = false): array; /** - * Returns the app icon or null if none is found + * Returns a URL to the app icon, or null if none is available. + * + * Tries app-specific icons first, then falls back to the generic app icon. * - * @param string $appId - * @param bool $dark Enable to request a dark icon variant, default is a white icon - * @return string|null + * @param string $appId App ID + * @param bool $dark Whether to prefer the dark icon variant + * @return string|null Public URL to the icon * @since 29.0.0 */ public function getAppIcon(string $appId, bool $dark = false): ?string; /** - * Check if an app is enabled for user + * Checks whether an app is enabled for the given user. * - * @param string $appId - * @param \OCP\IUser|null $user (optional) if not defined, the currently loggedin user will be used + * Apps enabled globally or for one of the user's groups count as enabled. + * + * @param string $appId App ID + * @param IUser|null $user User to check, or null to use the current user * @return bool * @since 8.0.0 */ public function isEnabledForUser($appId, $user = null); /** - * Check if an app is enabled in the instance + * Checks whether an app is enabled in the instance. * - * Notice: This actually checks if the app is enabled and not only if it is installed. + * This is a legacy alias that returns whether the app is enabled globally + * or for at least one group; it does not merely check whether the app is installed. * - * @param string $appId + * @param string $appId App ID * @return bool * @since 8.0.0 - * @deprecated 32.0.0 Use either {@see self::isEnabledForUser} or {@see self::isEnabledForAnyone} + * @deprecated 32.0.0 Use either {@see self::isEnabledForUser()} or {@see self::isEnabledForAnyone()} */ public function isInstalled($appId); /** - * Check if an app is enabled in the instance, either for everyone or for specific groups + * Checks whether an app is enabled for anyone in the instance. + * + * This returns true for apps enabled globally or restricted to specific groups. * + * @param string $appId App ID + * @return bool * @since 32.0.0 */ public function isEnabledForAnyone(string $appId): bool; /** - * Check if an app should be enabled by default + * Checks whether an app is part of the default-enabled app set. * - * Notice: This actually checks if the app should be enabled by default - * and not if currently installed/enabled + * This indicates whether the app should be enabled by default on a fresh install, + * not whether it is currently installed or enabled. * - * @param string $appId ID of the app + * @param string $appId App ID + * @return bool * @since 25.0.0 */ public function isDefaultEnabled(string $appId):bool; /** - * Load an app, if not already loaded - * @param string $app app id + * Loads an app's bootstrap and registers its services, if not already loaded. + * + * @param string $app App ID * @since 27.0.0 */ public function loadApp(string $app): void; /** - * Check if an app is loaded - * @param string $app app id + * Checks whether an app has already been loaded in the current process. + * + * @param string $app App ID + * @return bool * @since 27.0.0 */ public function isAppLoaded(string $app): bool; /** - * Enable an app for every user + * Enables an app globally for all users. * - * @param string $appId - * @param bool $forceEnable - * @throws AppPathNotFoundException + * @param string $appId App ID + * @param bool $forceEnable Whether to bypass Nextcloud version requirement checks + * @throws AppPathNotFoundException If the app cannot be found * @since 8.0.0 */ public function enableApp(string $appId, bool $forceEnable = false): void; /** - * Whether a list of types contains a protected app type + * Checks whether the given app types contain a protected type. + * + * Protected apps cannot be enabled for specific groups only. * - * @param string[] $types + * @param string[] $types App types to check * @return bool * @since 12.0.0 */ public function hasProtectedAppType($types); /** - * Enable an app only for specific groups + * Enables an app only for the specified groups. * - * @param string $appId - * @param \OCP\IGroup[] $groups - * @param bool $forceEnable - * @throws \Exception + * @param string $appId App ID + * @param IGroup[]|string[] $groups Group objects or group IDs + * @param bool $forceEnable Whether to bypass Nextcloud version requirement checks + * @throws \InvalidArgumentException If the app cannot be enabled for groups + * @throws AppPathNotFoundException If the app cannot be found * @since 8.0.0 */ public function enableAppForGroups(string $appId, array $groups, bool $forceEnable = false): void; /** - * Disable an app for every user + * Disables an app globally for all users. * - * @param string $appId - * @param bool $automaticDisabled + * If $automaticDisabled is true, the previous enabled state is remembered so it can be restored. + * + * @param string $appId App ID + * @param bool $automaticDisabled Whether the app was disabled automatically + * @throws \Exception If the app cannot be disabled * @since 8.0.0 */ public function disableApp($appId, $automaticDisabled = false): void; /** - * Get the directory for the given app. + * Returns the filesystem path to an app directory. * + * @param string $appId App ID + * @param bool $ignoreCache Whether to bypass the cached app directory lookup + * @return string Absolute filesystem path to the app directory + * @throws AppPathNotFoundException If the app cannot be found * @since 11.0.0 - * @since 32.0.0 Added param $ignoreCache to ignore cache - * @throws AppPathNotFoundException + * @since 32.0.0 Added $ignoreCache */ public function getAppPath(string $appId, bool $ignoreCache = false): string; /** - * Get the web path for the given app. + * Returns the web-accessible path for the given app. * - * @param string $appId - * @return string + * @param string $appId App ID + * @return string Web path to the app directory + * @throws AppPathNotFoundException If the app cannot be found * @since 18.0.0 - * @throws AppPathNotFoundException */ public function getAppWebPath(string $appId): string; /** - * List all apps enabled for a user + * Returns all apps enabled for the given user. + * + * Includes apps enabled globally and apps enabled for one of the user's groups. * - * @param \OCP\IUser $user - * @return list + * @param IUser $user User to inspect + * @return list Enabled app IDs * @since 8.1.0 */ public function getEnabledAppsForUser(IUser $user); /** - * List all installed apps + * Returns all enabled apps. + * + * This is a legacy alias for {@see self::getEnabledApps()}. * * @return string[] * @since 8.1.0 - * @deprecated 32.0.0 Use either {@see self::getEnabledApps} or {@see self::getEnabledAppsForUser} + * @deprecated 32.0.0 Use either {@see self::getEnabledApps()} or {@see self::getEnabledAppsForUser()} */ public function getInstalledApps(); /** - * List all apps enabled, either for everyone or for specific groups only + * Returns all apps that are enabled for anyone. + * + * This includes apps enabled globally and apps enabled for specific groups. * - * @return list + * @return list Enabled app IDs * @since 32.0.0 */ public function getEnabledApps(): array; /** - * Clear the cached list of apps when enabling/disabling an app + * Clears cached app metadata so it will be reloaded on the next access. + * * @since 8.1.0 */ public function clearAppsCache(): void; /** - * @param string $appId - * @return boolean + * Checks whether an app is shipped with Nextcloud. + * + * @param string $appId App ID + * @return bool * @since 9.0.0 */ public function isShipped($appId); /** - * Loads all apps - * - * @param string[] $types - * @return bool + * Loads all enabled apps, optionally filtered by app type. * - * 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. + * When $types is non-empty, only enabled apps matching at least one of the given + * types are loaded. * - * if $types is set to non-empty array, only apps of those types will be loaded + * @param string[] $types App types to filter by + * @return bool True if loading was attempted, false if blocked by maintenance mode * @since 27.0.0 */ public function loadApps(array $types = []): bool; /** - * Check if an app is of a specific type + * Checks whether an app has at least one of the specified types. + * + * @param string $app App ID + * @param string[] $types Types to match against + * @return bool * @since 27.0.0 */ public function isType(string $app, array $types): bool; /** - * @return string[] + * Returns apps that are always enabled and cannot be disabled. + * + * @return string[] App IDs * @since 9.0.0 */ public function getAlwaysEnabledApps(); /** - * @return string[] app IDs + * Returns apps that are enabled by default on a fresh installation. + * + * @return string[] App IDs * @since 25.0.0 */ public function getDefaultEnabledApps(): array; /** - * @param \OCP\IGroup $group - * @return String[] + * Returns all apps enabled for the given group. + * + * @param IGroup $group Group to inspect + * @return string[] Enabled app IDs * @since 17.0.0 */ public function getEnabledAppsForGroup(IGroup $group): array; /** - * @param String $appId - * @return string[] + * Returns the group restriction for an app, if one is configured. + * + * @param string $appId App ID + * @return string[] Group IDs, or an empty array if the app is not group-restricted * @since 17.0.0 */ public function getAppRestriction(string $appId): array; /** - * Returns the id of the user's default app + * Returns the app ID of the user's default app. * - * If `user` is not passed, the currently logged in user will be used - * - * @param ?IUser $user User to query default app for - * @param bool $withFallbacks Include fallback values if no default app was configured manually - * Before falling back to predefined default apps, - * the user defined app order is considered and the first app would be used as the fallback. + * If $user is null, the currently logged-in user is used. * + * @param IUser|null $user User to query, or null for the current user + * @param bool $withFallbacks Whether to use fallback defaults when no explicit default is configured + * @return string Default app ID * @since 25.0.6 * @since 28.0.0 Added optional $withFallbacks parameter - * @deprecated 31.0.0 - * Use @see \OCP\INavigationManager::getDefaultEntryIdForUser() instead + * @deprecated 31.0.0 Use {@see \OCP\INavigationManager::getDefaultEntryIdForUser()} instead */ public function getDefaultAppForUser(?IUser $user = null, bool $withFallbacks = true): string; /** - * Get the global default apps with fallbacks + * Returns the globally configured default apps. * - * @return string[] The default applications + * @return string[] Default app IDs * @since 28.0.0 - * @deprecated 31.0.0 - * Use @see \OCP\INavigationManager::getDefaultEntryIds() instead + * @deprecated 31.0.0 Use {@see \OCP\INavigationManager::getDefaultEntryIds()} instead */ public function getDefaultApps(): array; /** - * Set the global default apps with fallbacks + * Sets the globally configured default apps. * - * @param string[] $defaultApps - * @throws \InvalidArgumentException If any of the apps is not installed + * @param string[] $defaultApps App IDs that should become the default apps + * @throws \InvalidArgumentException If any requested app is not available in the navigation entries * @since 28.0.0 - * @deprecated 31.0.0 - * Use @see \OCP\INavigationManager::setDefaultEntryIds() instead + * @deprecated 31.0.0 Use {@see \OCP\INavigationManager::setDefaultEntryIds()} instead */ public function setDefaultApps(array $defaultApps): void; /** - * Check whether the given backend is required by at least one app. + * Checks whether at least one loaded app requires the given backend. * - * @param self::BACKEND_* $backend Name of the backend, one of `self::BACKEND_*` + * @param string $backend Backend identifier, such as self::BACKEND_CALDAV * @return bool True if at least one app requires the backend - * * @since 30.0.0 */ public function isBackendRequired(string $backend): bool; /** - * Clean the appId from forbidden characters + * Sanitizes an app ID by removing forbidden characters. * + * The returned ID contains only lowercase alphanumeric characters, underscores, + * and hyphens, with invalid leading/trailing characters removed. + * + * @param string $app Raw app ID + * @return string Sanitized app ID * @psalm-taint-escape callable * @psalm-taint-escape cookie * @psalm-taint-escape file @@ -329,47 +385,48 @@ public function isBackendRequired(string $backend): bool; * @psalm-taint-escape shell * @psalm-taint-escape sql * @psalm-taint-escape unserialize - * * @since 31.0.0 */ public function cleanAppId(string $app): string; /** - * Get a list of all apps in the apps folder + * Returns all app IDs found in the configured apps folders. * - * @return list an array of app names (string IDs) + * @return list App IDs * @since 31.0.0 */ public function getAllAppsInAppsFolders(): array; /** - * Run upgrade tasks for an app after the code has already been updated + * Runs the upgrade steps for an app after its code has been updated. * - * @throws AppPathNotFoundException if app folder can't be found + * @param string $appId App ID + * @return bool True if the upgrade completed successfully + * @throws AppPathNotFoundException If the app folder cannot be found * @since 32.0.0 */ public function upgradeApp(string $appId): bool; /** - * Check whether the installed version is the same as the version from info.xml + * Checks whether the app's installed version differs from the version in `info.xml`. * + * @param string $appId App ID + * @return bool True if an upgrade is required * @since 32.0.0 */ public function isUpgradeRequired(string $appId): bool; /** - * Check whether the current Nextcloud version matches the given - * application's version requirements. + * Checks whether the given Nextcloud version is compatible with an app's requirements. * - * The comparison is made based on the number of parts that the - * app info version has. For example for Nextcloud 26.0.3 if the - * app info version is expecting version 26.0, the comparison is - * made on the first two parts of the Nextcloud version. - * This means that it's possible to specify "requiremin" => 26 - * and "requiremax" => 26 and it will still match Nextcloud 26.0.3. + * Compatibility is determined from the app's declared minimum and maximum supported + * Nextcloud versions. Partial version constraints are supported, so comparing against + * `26` or `26.0` will match `26.0.3` when appropriate. * - * @param string $serverVersion Nextcloud version to check against - * @param array $appInfo app info (from xml) + * @param string $serverVersion Nextcloud version to check + * @param array $appInfo Parsed app info array + * @param bool $ignoreMax Whether to ignore the app's max-version constraint + * @return bool True if the app is compatible * @since 32.0.0 */ public function isAppCompatible(string $serverVersion, array $appInfo, bool $ignoreMax = false): bool; From 5685ad4213d8df71d71464c2393f6876bc922ecd Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 1 Jun 2026 22:52:46 -0400 Subject: [PATCH 4/5] refactor(AppManager): drop dup docblocks + tidy up exception msgs Signed-off-by: Josh --- lib/private/App/AppManager.php | 35 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/lib/private/App/AppManager.php b/lib/private/App/AppManager.php index 7fe1fcf187f9e..d85ff770fa673 100644 --- a/lib/private/App/AppManager.php +++ b/lib/private/App/AppManager.php @@ -5,6 +5,7 @@ * SPDX-FileCopyrightText: 2016 ownCloud, Inc. * SPDX-License-Identifier: AGPL-3.0-only */ + namespace OC\App; use OC\AppConfig; @@ -40,7 +41,7 @@ class AppManager implements IAppManager { /** - * Apps with these types can not be enabled for certain groups only + * Protected apps cannot be enabled for specific groups only. * @var string[] */ protected $protectedAppTypes = [ @@ -125,7 +126,7 @@ private function getAppConfig(): AppConfig { return $this->appConfig; } if (!$this->config->getSystemValueBool('installed', false)) { - throw new \Exception('Nextcloud is not installed yet, AppConfig is not available'); + throw new \Exception('Nextcloud is not installed, AppConfig is not available yet'); } $this->appConfig = Server::get(AppConfig::class); return $this->appConfig; @@ -136,7 +137,7 @@ private function getUrlGenerator(): IURLGenerator { return $this->urlGenerator; } if (!$this->config->getSystemValueBool('installed', false)) { - throw new \Exception('Nextcloud is not installed yet, AppConfig is not available'); + throw new \Exception('Nextcloud is not installed, URLGenerator is not available yet'); } $this->urlGenerator = Server::get(IURLGenerator::class); return $this->urlGenerator; @@ -278,10 +279,10 @@ public function isType(string $app, array $types): bool { } /** - * get the types of an app + * Returns the type(s) of a given app. * - * @param string $app - * @return string[] + * @param string $app App ID + * @return string[] Type(s) applicable to app */ private function getAppTypes(string $app): array { //load the cache @@ -399,6 +400,8 @@ public function isEnabledForAnyone(string $appId): bool { * Adds the app to the 'app_install_overwrite' list, allowing it to be * enabled even if the current Nextcloud version exceeds the app's * defined 'max-version'. + * + * @internal */ public function overwriteNextcloudRequirement(string $appId): void { $ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []); @@ -414,6 +417,8 @@ public function overwriteNextcloudRequirement(string $appId): void { * This removes the app from the 'app_install_overwrite' list, meaning it can * no longer be enabled if its maximum supported version is lower than the * current Nextcloud version. + * + * @internal */ public function removeOverwriteNextcloudRequirement(string $appId): void { $ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []); @@ -537,9 +542,6 @@ public function isAppLoaded(string $app): bool { return isset($this->loadedApps[$app]); } - /** - * @throws \InvalidArgumentException if the application is not installed yet - */ #[\Override] public function enableApp(string $appId, bool $forceEnable = false): void { // Check if app exists @@ -574,11 +576,6 @@ public function hasProtectedAppType(array $types): bool { return !empty($protectedTypes); } - /** - * @param IGroup[]|string[] $groups - * @throws \InvalidArgumentException if app can't be enabled for groups - * @throws AppPathNotFoundException - */ #[\Override] public function enableAppForGroups(string $appId, array $groups, bool $forceEnable = false): void { // Check if app exists @@ -616,13 +613,10 @@ public function enableAppForGroups(string $appId, array $groups, bool $forceEnab $this->configManager->migrateConfigLexiconKeys($appId); } - /** - * @throws \Exception if app can't be disabled - */ #[\Override] public function disableApp($appId, $automaticDisabled = false): void { if ($this->isAlwaysEnabled($appId)) { - throw new \Exception("$appId can't be disabled."); + throw new \Exception("$appId cannot be disabled; it must always remain enabled"); } if ($automaticDisabled) { @@ -856,9 +850,6 @@ public function getIncompatibleApps(string $version): array { return $incompatibleApps; } - /** - * @throws \Exception if shipped apps inventory file cannot be loaded. - */ #[\Override] public function isShipped(string $appId): bool { $this->loadShippedJson(); @@ -876,6 +867,8 @@ private function isAlwaysEnabled(string $appId): bool { /** * @throws \Exception if shipped apps inventory file cannot be loaded. + * + * @internal */ private function loadShippedJson(): void { if ($this->shippedApps === null) { From dea06f68a7cd851426ad16551a33588d43239d26 Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 1 Jun 2026 22:53:15 -0400 Subject: [PATCH 5/5] docs(IAppManager): additional exceptions Signed-off-by: Josh --- lib/public/App/IAppManager.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/public/App/IAppManager.php b/lib/public/App/IAppManager.php index b2164f8b96030..8c697fc94f7a2 100644 --- a/lib/public/App/IAppManager.php +++ b/lib/public/App/IAppManager.php @@ -158,7 +158,8 @@ public function isAppLoaded(string $app): bool; * * @param string $appId App ID * @param bool $forceEnable Whether to bypass Nextcloud version requirement checks - * @throws AppPathNotFoundException If the app cannot be found + * @throws \InvalidArgumentException If the app is not installed + * @throws AppPathNotFoundException If the app path cannot be found * @since 8.0.0 */ public function enableApp(string $appId, bool $forceEnable = false): void; @@ -180,7 +181,7 @@ public function hasProtectedAppType($types); * @param string $appId App ID * @param IGroup[]|string[] $groups Group objects or group IDs * @param bool $forceEnable Whether to bypass Nextcloud version requirement checks - * @throws \InvalidArgumentException If the app cannot be enabled for groups + * @throws \InvalidArgumentException If the app is not installed or cannot be enabled for groups * @throws AppPathNotFoundException If the app cannot be found * @since 8.0.0 */ @@ -264,6 +265,7 @@ public function clearAppsCache(): void; * * @param string $appId App ID * @return bool + * @throws \Exception if shipped apps inventory file cannot be loaded. * @since 9.0.0 */ public function isShipped($appId);