Merge pull request #36591 from nextcloud/enh/refactor-app-loading

Refactor app loading
This commit is contained in:
Côme Chilliet 2023-03-20 15:34:11 +01:00 committed by GitHub
commit a3ca6be4d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 305 additions and 231 deletions

View file

@ -39,18 +39,25 @@
namespace OC\App;
use OC\AppConfig;
use OC\AppFramework\Bootstrap\Coordinator;
use OC\ServerNotAvailableException;
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\EventDispatcher\IEventDispatcher;
use OCP\Collaboration\AutoComplete\IManager as IAutoCompleteManager;
use OCP\Collaboration\Collaborators\ISearch as ICollaboratorSearch;
use OCP\Diagnostics\IEventLogger;
use OCP\ICacheFactory;
use OCP\IConfig;
use OCP\IGroup;
use OCP\IGroupManager;
use OCP\IUser;
use OCP\IUserSession;
use OCP\Settings\IManager as ISettingsManager;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@ -67,46 +74,36 @@ 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 $legacyDispatcher;
private IUserSession $userSession;
private IConfig $config;
private AppConfig $appConfig;
private IGroupManager $groupManager;
private ICacheFactory $memCacheFactory;
private EventDispatcherInterface $legacyDispatcher;
private IEventDispatcher $dispatcher;
/** @var LoggerInterface */
private $logger;
private LoggerInterface $logger;
/** @var string[] $appId => $enabled */
private $installedAppsCache;
private array $installedAppsCache = [];
/** @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 = [];
public function __construct(IUserSession $userSession,
IConfig $config,
@ -129,7 +126,7 @@ class AppManager implements IAppManager {
/**
* @return string[] $appId => $enabled
*/
private function getInstalledAppsValues() {
private function getInstalledAppsValues(): array {
if (!$this->installedAppsCache) {
$values = $this->appConfig->getValues(false, 'enabled');
@ -181,6 +178,91 @@ class AppManager implements IAppManager {
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.php 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->appConfig->getValues(false, 'types') ?: [];
}
if (isset($this->appTypes[$app])) {
return explode(',', $this->appTypes[$app]);
}
return [];
}
/**
* @return array
*/
@ -228,12 +310,7 @@ class AppManager implements IAppManager {
}
}
/**
* @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) {
@ -261,16 +338,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;
@ -310,6 +380,151 @@ class AppManager implements IAppManager {
}
}
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(\OCP\Diagnostics\IEventLogger::class);
$eventLogger->start("bootstrap:load_app:$app", "Load $app");
// in case someone calls loadApp() directly
\OC_App::registerAutoloading($app, $appPath);
/** @var Coordinator $coordinator */
$coordinator = \OC::$server->get(Coordinator::class);
$isBootable = $coordinator->isBootable($app);
$hasAppPhpFile = is_file($appPath . '/appinfo/app.php');
$eventLogger = \OC::$server->get(IEventLogger::class);
$eventLogger->start('bootstrap:load_app_' . $app, 'Load app: ' . $app);
if ($isBootable && $hasAppPhpFile) {
$this->logger->error('/appinfo/app.php is not loaded when \OCP\AppFramework\Bootstrap\IBootstrap on the application class is used. Migrate everything from app.php to the Application class.', [
'app' => $app,
]);
} elseif ($hasAppPhpFile) {
$eventLogger->start("bootstrap:load_app:$app:app.php", "Load legacy app.php app $app");
$this->logger->debug('/appinfo/app.php is deprecated, use \OCP\AppFramework\Bootstrap\IBootstrap on the application class instead.', [
'app' => $app,
]);
try {
self::requireAppFile($appPath);
} catch (\Throwable $ex) {
if ($ex instanceof ServerNotAvailableException) {
throw $ex;
}
if (!$this->isShipped($app) && !$this->isType($app, ['authentication'])) {
$this->logger->error("App $app threw an error during app.php load and will be disabled: " . $ex->getMessage(), [
'exception' => $ex,
]);
// Only disable apps which are not shipped and that are not authentication apps
$this->disableApp($app, true);
} else {
$this->logger->error("App $app threw an error during app.php load: " . $ex->getMessage(), [
'exception' => $ex,
]);
}
}
$eventLogger->end("bootstrap:load_app:$app:app.php");
}
$coordinator->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]);
}
/**
* Load app.php from the given app
*
* @param string $app app name
* @throws \Error
*/
private static function requireAppFile(string $app): void {
// encapsulated here to avoid variable scope conflicts
require_once $app . '/appinfo/app.php';
}
/**
* Enable an app for every user
*
@ -567,7 +782,7 @@ class AppManager implements IAppManager {
return in_array($appId, $this->shippedApps, true);
}
private function isAlwaysEnabled($appId) {
private function isAlwaysEnabled(string $appId): bool {
$alwaysEnabled = $this->getAlwaysEnabledApps();
return in_array($appId, $alwaysEnabled, true);
}
@ -576,7 +791,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)) {

View file

@ -53,11 +53,11 @@ declare(strict_types=1);
use OCP\App\Events\AppUpdateEvent;
use OCP\AppFramework\QueryException;
use OCP\App\IAppManager;
use OCP\App\ManagerEvent;
use OCP\Authentication\IAlternativeLogin;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\ILogger;
use OCP\Settings\IManager as ISettingsManager;
use OC\AppFramework\Bootstrap\Coordinator;
use OC\App\DependencyAnalyzer;
use OC\App\Platform;
@ -65,7 +65,6 @@ use OC\DB\MigrationService;
use OC\Installer;
use OC\Repair;
use OC\Repair\Events\RepairErrorEvent;
use OC\ServerNotAvailableException;
use Psr\Log\LoggerInterface;
/**
@ -76,8 +75,6 @@ use Psr\Log\LoggerInterface;
class OC_App {
private static $adminForms = [];
private static $personalForms = [];
private static $appTypes = [];
private static $loadedApps = [];
private static $altLogin = [];
private static $alreadyRegistered = [];
public const supportedApp = 300;
@ -101,9 +98,10 @@ class OC_App {
*
* @param string $app
* @return bool
* @deprecated 26.0.0 use IAppManager::isAppLoaded
*/
public static function isAppLoaded(string $app): bool {
return isset(self::$loadedApps[$app]);
return \OC::$server->get(IAppManager::class)->isAppLoaded($app);
}
/**
@ -119,40 +117,11 @@ class OC_App {
* if $types is set to non-empty array, only apps of those types will be loaded
*/
public static function loadApps(array $types = []): bool {
if ((bool) \OC::$server->getSystemConfig()->getValue('maintenance', false)) {
if (!\OC::$server->getSystemConfig()->getValue('installed', false)) {
// This should be done before calling this method so that appmanager can be used
return false;
}
// Load the enabled apps here
$apps = self::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 (!isset(self::$loadedApps[$app])) {
$path = self::getAppPath($app);
if ($path !== false) {
self::registerAutoloading($app, $path);
}
}
}
// prevent app.php from printing output
ob_start();
foreach ($apps as $app) {
if (!isset(self::$loadedApps[$app]) && ($types === [] || self::isType($app, $types))) {
try {
self::loadApp($app);
} catch (\Throwable $e) {
\OC::$server->get(LoggerInterface::class)->emergency('Error during app loading: ' . $e->getMessage(), [
'exception' => $e,
'app' => $app,
]);
}
}
}
ob_end_clean();
return true;
return \OC::$server->get(IAppManager::class)->loadApps($types);
}
/**
@ -160,120 +129,10 @@ class OC_App {
*
* @param string $app
* @throws Exception
* @deprecated 26.0.0 use IAppManager::loadApp
*/
public static function loadApp(string $app): void {
if (isset(self::$loadedApps[$app])) {
return;
}
self::$loadedApps[$app] = true;
$appPath = self::getAppPath($app);
if ($appPath === false) {
return;
}
$eventLogger = \OC::$server->get(\OCP\Diagnostics\IEventLogger::class);
$eventLogger->start("bootstrap:load_app:$app", "Load $app");
// in case someone calls loadApp() directly
self::registerAutoloading($app, $appPath);
/** @var Coordinator $coordinator */
$coordinator = \OC::$server->query(Coordinator::class);
$isBootable = $coordinator->isBootable($app);
$hasAppPhpFile = is_file($appPath . '/appinfo/app.php');
if ($isBootable && $hasAppPhpFile) {
\OC::$server->getLogger()->error('/appinfo/app.php is not loaded when \OCP\AppFramework\Bootstrap\IBootstrap on the application class is used. Migrate everything from app.php to the Application class.', [
'app' => $app,
]);
} elseif ($hasAppPhpFile) {
$eventLogger->start("bootstrap:load_app:$app:app.php", "Load legacy app.php app $app");
\OC::$server->getLogger()->debug('/appinfo/app.php is deprecated, use \OCP\AppFramework\Bootstrap\IBootstrap on the application class instead.', [
'app' => $app,
]);
try {
self::requireAppFile($appPath);
} catch (Throwable $ex) {
if ($ex instanceof ServerNotAvailableException) {
throw $ex;
}
if (!\OC::$server->getAppManager()->isShipped($app) && !self::isType($app, ['authentication'])) {
\OC::$server->getLogger()->logException($ex, [
'message' => "App $app threw an error during app.php load and will be disabled: " . $ex->getMessage(),
]);
// Only disable apps which are not shipped and that are not authentication apps
\OC::$server->getAppManager()->disableApp($app, true);
} else {
\OC::$server->getLogger()->logException($ex, [
'message' => "App $app threw an error during app.php load: " . $ex->getMessage(),
]);
}
}
$eventLogger->end("bootstrap:load_app:$app:app.php");
}
$coordinator->bootApp($app);
$eventLogger->start("bootstrap:load_app:$app:info", "Load info.xml for $app and register any services defined in it");
$info = self::getAppInfo($app);
if (!empty($info['activity']['filters'])) {
foreach ($info['activity']['filters'] as $filter) {
\OC::$server->getActivityManager()->registerFilter($filter);
}
}
if (!empty($info['activity']['settings'])) {
foreach ($info['activity']['settings'] as $setting) {
\OC::$server->getActivityManager()->registerSetting($setting);
}
}
if (!empty($info['activity']['providers'])) {
foreach ($info['activity']['providers'] as $provider) {
\OC::$server->getActivityManager()->registerProvider($provider);
}
}
if (!empty($info['settings']['admin'])) {
foreach ($info['settings']['admin'] as $setting) {
\OC::$server->get(ISettingsManager::class)->registerSetting('admin', $setting);
}
}
if (!empty($info['settings']['admin-section'])) {
foreach ($info['settings']['admin-section'] as $section) {
\OC::$server->get(ISettingsManager::class)->registerSection('admin', $section);
}
}
if (!empty($info['settings']['personal'])) {
foreach ($info['settings']['personal'] as $setting) {
\OC::$server->get(ISettingsManager::class)->registerSetting('personal', $setting);
}
}
if (!empty($info['settings']['personal-section'])) {
foreach ($info['settings']['personal-section'] as $section) {
\OC::$server->get(ISettingsManager::class)->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'];
foreach ($plugins as $plugin) {
if ($plugin['@attributes']['type'] === 'collaborator-search') {
$pluginInfo = [
'shareType' => $plugin['@attributes']['share-type'],
'class' => $plugin['@value'],
];
\OC::$server->getCollaboratorSearch()->registerPlugin($pluginInfo);
} elseif ($plugin['@attributes']['type'] === 'autocomplete-sort') {
\OC::$server->getAutoCompleteManager()->registerSorter($plugin['@value']);
}
}
}
$eventLogger->end("bootstrap:load_app:$app:info");
$eventLogger->end("bootstrap:load_app:$app");
\OC::$server->get(IAppManager::class)->loadApp($app);
}
/**
@ -306,51 +165,16 @@ class OC_App {
}
}
/**
* Load app.php from the given app
*
* @param string $app app name
* @throws Error
*/
private static function requireAppFile(string $app) {
// encapsulated here to avoid variable scope conflicts
require_once $app . '/appinfo/app.php';
}
/**
* check if an app is of a specific type
*
* @param string $app
* @param array $types
* @return bool
* @deprecated 26.0.0 use IAppManager::isType
*/
public static function isType(string $app, array $types): bool {
$appTypes = self::getAppTypes($app);
foreach ($types as $type) {
if (array_search($type, $appTypes) !== false) {
return true;
}
}
return false;
}
/**
* get the types of an app
*
* @param string $app
* @return array
*/
private static function getAppTypes(string $app): array {
//load the cache
if (count(self::$appTypes) == 0) {
self::$appTypes = \OC::$server->getAppConfig()->getValues(false, 'types');
}
if (isset(self::$appTypes[$app])) {
return explode(',', self::$appTypes[$app]);
}
return [];
return \OC::$server->get(IAppManager::class)->isType($app, $types);
}
/**

View file

@ -93,6 +93,20 @@ interface IAppManager {
*/
public function isDefaultEnabled(string $appId):bool;
/**
* Load an app, if not already loaded
* @param string $app app id
* @since 26.0.0
*/
public function loadApp(string $app): void;
/**
* Check if an app is loaded
* @param string $app app id
* @since 26.0.0
*/
public function isAppLoaded(string $app): bool;
/**
* Enable an app for every user
*
@ -182,6 +196,27 @@ interface IAppManager {
*/
public function isShipped($appId);
/**
* 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
* @since 26.0.0
*/
public function loadApps(array $types = []): bool;
/**
* Check if an app is of a specific type
* @since 26.0.0
*/
public function isType(string $app, array $types): bool;
/**
* @return string[]
* @since 9.0.0

View file

@ -69,7 +69,7 @@ class BackgroundCleanupJobTest extends \Test\TestCase {
parent::setUp();
$this->userId = $this->getUniqueID();
$this->createUser($this->userId, $this->userId);
$user = $this->createUser($this->userId, $this->userId);
$storage = new \OC\Files\Storage\Temporary([]);
$this->registerMount($this->userId, $storage, '');
@ -79,7 +79,7 @@ class BackgroundCleanupJobTest extends \Test\TestCase {
$this->loginAsUser($this->userId);
$appManager = \OC::$server->getAppManager();
$this->trashEnabled = $appManager->isEnabledForUser('files_trashbin', $this->userId);
$this->trashEnabled = $appManager->isEnabledForUser('files_trashbin', $user);
$appManager->disableApp('files_trashbin');
$this->connection = \OC::$server->getDatabaseConnection();