diff --git a/apps/settings/lib/Controller/AppSettingsController.php b/apps/settings/lib/Controller/AppSettingsController.php index a85ee8cc20a..bd713cab201 100644 --- a/apps/settings/lib/Controller/AppSettingsController.php +++ b/apps/settings/lib/Controller/AppSettingsController.php @@ -14,7 +14,6 @@ use OC\App\AppStore\Fetcher\AppFetcher; use OC\App\AppStore\Fetcher\CategoryFetcher; use OC\App\AppStore\Version\VersionParser; use OC\App\DependencyAnalyzer; -use OC\App\Platform; use OC\Installer; use OCA\AppAPI\Service\ExAppsPageService; use OCP\App\AppPathNotFoundException; @@ -361,7 +360,7 @@ class AppSettingsController extends Controller { $this->fetchApps(); $apps = $this->getAllApps(); - $dependencyAnalyzer = new DependencyAnalyzer(new Platform($this->config), $this->l10n); + $dependencyAnalyzer = Server::get(DependencyAnalyzer::class); $ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []); if (!is_array($ignoreMaxApps)) { @@ -568,24 +567,18 @@ class AppSettingsController extends Controller { $appId = $this->appManager->cleanAppId($appId); // Check if app is already downloaded - /** @var Installer $installer */ - $installer = Server::get(Installer::class); - $isDownloaded = $installer->isDownloaded($appId); - - if (!$isDownloaded) { - $installer->downloadApp($appId); + if (!$this->installer->isDownloaded($appId)) { + $this->installer->downloadApp($appId); } - $installer->installApp($appId); + $this->installer->installApp($appId); if (count($groups) > 0) { $this->appManager->enableAppForGroups($appId, $this->getGroupList($groups)); } else { $this->appManager->enableApp($appId); } - if (\OC_App::shouldUpgrade($appId)) { - $updateRequired = true; - } + $updateRequired = $updateRequired || $this->appManager->isUpgradeRequired($appId); } return new JSONResponse(['data' => ['update_required' => $updateRequired]]); } catch (\Throwable $e) { diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml index 61d68578781..fd9c1b31a31 100644 --- a/build/psalm-baseline.xml +++ b/build/psalm-baseline.xml @@ -3317,14 +3317,6 @@ - - - - - - - - @@ -4011,12 +4003,6 @@ - - - - - - getDN(X509::DN_OPENSSL)['CN']]]> diff --git a/core/Command/Integrity/CheckApp.php b/core/Command/Integrity/CheckApp.php index 0145a3f8070..df9e9973aa7 100644 --- a/core/Command/Integrity/CheckApp.php +++ b/core/Command/Integrity/CheckApp.php @@ -5,11 +5,11 @@ * SPDX-FileCopyrightText: 2016 ownCloud, Inc. * SPDX-License-Identifier: AGPL-3.0-only */ + namespace OC\Core\Command\Integrity; use OC\Core\Command\Base; use OC\IntegrityCheck\Checker; -use OC\IntegrityCheck\Helpers\AppLocator; use OC\IntegrityCheck\Helpers\FileAccessHelper; use OCP\App\IAppManager; use Symfony\Component\Console\Input\InputArgument; @@ -25,7 +25,6 @@ use Symfony\Component\Console\Output\OutputInterface; class CheckApp extends Base { public function __construct( private Checker $checker, - private AppLocator $appLocator, private FileAccessHelper $fileAccessHelper, private IAppManager $appManager, ) { @@ -70,7 +69,7 @@ class CheckApp extends Base { foreach ($appIds as $appId) { $path = (string)$input->getOption('path'); if ($path === '') { - $path = $this->appLocator->getAppPath($appId); + $path = $this->appManager->getAppPath($appId); } if ($this->appManager->isShipped($appId) || $this->fileAccessHelper->file_exists($path . '/appinfo/signature.json')) { diff --git a/core/ajax/update.php b/core/ajax/update.php index 69665cf62df..22bbdcff3e0 100644 --- a/core/ajax/update.php +++ b/core/ajax/update.php @@ -7,8 +7,6 @@ */ use OC\Core\Listener\FeedBackHandler; use OC\DB\MigratorExecuteSqlEvent; -use OC\Installer; -use OC\IntegrityCheck\Checker; use OC\Repair\Events\RepairAdvanceEvent; use OC\Repair\Events\RepairErrorEvent; use OC\Repair\Events\RepairFinishEvent; @@ -20,13 +18,11 @@ use OC\SystemConfig; use OC\Updater; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventDispatcher; -use OCP\IAppConfig; use OCP\IConfig; use OCP\IEventSourceFactory; use OCP\IL10N; use OCP\L10N\IFactory; use OCP\Server; -use OCP\ServerVersion; use OCP\Util; use Psr\Log\LoggerInterface; @@ -58,14 +54,7 @@ if (Util::needUpgrade()) { \OC_User::setIncognitoMode(true); $config = Server::get(IConfig::class); - $updater = new Updater( - Server::get(ServerVersion::class), - $config, - Server::get(IAppConfig::class), - Server::get(Checker::class), - Server::get(LoggerInterface::class), - Server::get(Installer::class) - ); + $updater = Server::get(Updater::class); $incompatibleApps = []; $incompatibleOverwrites = $config->getSystemValue('app_install_overwrite', []); diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index e975adac6ef..84f75545b25 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -1790,7 +1790,6 @@ return array( 'OC\\Installer' => $baseDir . '/lib/private/Installer.php', 'OC\\IntegrityCheck\\Checker' => $baseDir . '/lib/private/IntegrityCheck/Checker.php', 'OC\\IntegrityCheck\\Exceptions\\InvalidSignatureException' => $baseDir . '/lib/private/IntegrityCheck/Exceptions/InvalidSignatureException.php', - 'OC\\IntegrityCheck\\Helpers\\AppLocator' => $baseDir . '/lib/private/IntegrityCheck/Helpers/AppLocator.php', 'OC\\IntegrityCheck\\Helpers\\EnvironmentHelper' => $baseDir . '/lib/private/IntegrityCheck/Helpers/EnvironmentHelper.php', 'OC\\IntegrityCheck\\Helpers\\FileAccessHelper' => $baseDir . '/lib/private/IntegrityCheck/Helpers/FileAccessHelper.php', 'OC\\IntegrityCheck\\Iterator\\ExcludeFileByNameFilterIterator' => $baseDir . '/lib/private/IntegrityCheck/Iterator/ExcludeFileByNameFilterIterator.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index b6bd20b84fa..d533a3bcf1c 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -1831,7 +1831,6 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Installer' => __DIR__ . '/../../..' . '/lib/private/Installer.php', 'OC\\IntegrityCheck\\Checker' => __DIR__ . '/../../..' . '/lib/private/IntegrityCheck/Checker.php', 'OC\\IntegrityCheck\\Exceptions\\InvalidSignatureException' => __DIR__ . '/../../..' . '/lib/private/IntegrityCheck/Exceptions/InvalidSignatureException.php', - 'OC\\IntegrityCheck\\Helpers\\AppLocator' => __DIR__ . '/../../..' . '/lib/private/IntegrityCheck/Helpers/AppLocator.php', 'OC\\IntegrityCheck\\Helpers\\EnvironmentHelper' => __DIR__ . '/../../..' . '/lib/private/IntegrityCheck/Helpers/EnvironmentHelper.php', 'OC\\IntegrityCheck\\Helpers\\FileAccessHelper' => __DIR__ . '/../../..' . '/lib/private/IntegrityCheck/Helpers/FileAccessHelper.php', 'OC\\IntegrityCheck\\Iterator\\ExcludeFileByNameFilterIterator' => __DIR__ . '/../../..' . '/lib/private/IntegrityCheck/Iterator/ExcludeFileByNameFilterIterator.php', diff --git a/lib/private/App/AppManager.php b/lib/private/App/AppManager.php index 7778393b3b3..b9883d01dbd 100644 --- a/lib/private/App/AppManager.php +++ b/lib/private/App/AppManager.php @@ -10,12 +10,15 @@ namespace OC\App; use OC\AppConfig; use OC\AppFramework\Bootstrap\Coordinator; use OC\Config\ConfigManager; +use OC\DB\MigrationService; use OCP\Activity\IManager as IActivityManager; use OCP\App\AppPathNotFoundException; use OCP\App\Events\AppDisableEvent; use OCP\App\Events\AppEnableEvent; +use OCP\App\Events\AppUpdateEvent; use OCP\App\IAppManager; use OCP\App\ManagerEvent; +use OCP\BackgroundJob\IJobList; use OCP\Collaboration\AutoComplete\IManager as IAutoCompleteManager; use OCP\Collaboration\Collaborators\ISearch as ICollaboratorSearch; use OCP\Diagnostics\IEventLogger; @@ -86,6 +89,7 @@ class AppManager implements IAppManager { private LoggerInterface $logger, private ServerVersion $serverVersion, private ConfigManager $configManager, + private DependencyAnalyzer $dependencyAnalyzer, ) { } @@ -249,9 +253,14 @@ class AppManager implements IAppManager { 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) { + try { + $path = $this->getAppPath($app); \OC_App::registerAutoloading($app, $path); + } catch (AppPathNotFoundException $e) { + $this->logger->info('Error during app loading: ' . $e->getMessage(), [ + 'exception' => $e, + 'app' => $app, + ]); } } } @@ -447,8 +456,13 @@ class AppManager implements IAppManager { return; } $this->loadedApps[$app] = true; - $appPath = \OC_App::getAppPath($app); - if ($appPath === false) { + try { + $appPath = $this->getAppPath($app); + } catch (AppPathNotFoundException $e) { + $this->logger->info('Error during app loading: ' . $e->getMessage(), [ + 'exception' => $e, + 'app' => $app, + ]); return; } $eventLogger = \OC::$server->get(IEventLogger::class); @@ -675,29 +689,87 @@ class AppManager implements IAppManager { /** * Get the directory for the given app. * + * @psalm-taint-specialize + * * @throws AppPathNotFoundException if app folder can't be found */ - public function getAppPath(string $appId): string { - $appPath = \OC_App::getAppPath($appId); - if ($appPath === false) { - throw new AppPathNotFoundException('Could not find path for ' . $appId); + public function getAppPath(string $appId, bool $ignoreCache = false): string { + $appId = $this->cleanAppId($appId); + if ($appId === '') { + throw new AppPathNotFoundException('App id is empty'); + } elseif ($appId === 'core') { + return __DIR__ . '/../../../core'; } - return $appPath; + + if (($dir = $this->findAppInDirectories($appId, $ignoreCache)) != false) { + return $dir['path'] . '/' . $appId; + } + throw new AppPathNotFoundException('Could not find path for ' . $appId); } /** * Get the web path for the given app. * - * @param string $appId - * @return string * @throws AppPathNotFoundException if app path can't be found */ public function getAppWebPath(string $appId): string { - $appWebPath = \OC_App::getAppWebPath($appId); - if ($appWebPath === false) { - throw new AppPathNotFoundException('Could not find web path for ' . $appId); + if (($dir = $this->findAppInDirectories($appId)) != false) { + return \OC::$WEBROOT . $dir['url'] . '/' . $appId; + } + throw new AppPathNotFoundException('Could not find web path for ' . $appId); + } + + /** + * Find the apps root for an app id. + * + * If multiple copies are found, the apps root the latest version is returned. + * + * @param bool $ignoreCache ignore cache and rebuild it + * @return false|array{path: string, url: string} the apps root shape + */ + public function findAppInDirectories(string $appId, bool $ignoreCache = false) { + $sanitizedAppId = $this->cleanAppId($appId); + if ($sanitizedAppId !== $appId) { + return false; + } + // FIXME replace by a property or a cache + static $app_dir = []; + + if (isset($app_dir[$appId]) && !$ignoreCache) { + return $app_dir[$appId]; + } + + $possibleApps = []; + foreach (\OC::$APPSROOTS as $dir) { + if (file_exists($dir['path'] . '/' . $appId)) { + $possibleApps[] = $dir; + } + } + + if (empty($possibleApps)) { + return false; + } elseif (count($possibleApps) === 1) { + $dir = array_shift($possibleApps); + $app_dir[$appId] = $dir; + return $dir; + } else { + $versionToLoad = []; + foreach ($possibleApps as $possibleApp) { + $appData = $this->getAppInfoByPath($possibleApp['path'] . '/' . $appId . '/appinfo/info.xml'); + $version = $appData['version'] ?? ''; + if (empty($versionToLoad) || version_compare($version, $versionToLoad['version'], '>')) { + $versionToLoad = [ + 'dir' => $possibleApp, + 'version' => $version, + ]; + } + } + if (!isset($versionToLoad['dir'])) { + return false; + } + $app_dir[$appId] = $versionToLoad['dir']; + return $versionToLoad['dir']; } - return $appWebPath; } /** @@ -724,7 +796,7 @@ class AppManager implements IAppManager { if ($appDbVersion && isset($appInfo['version']) && version_compare($appInfo['version'], $appDbVersion, '>') - && \OC_App::isAppCompatible($version, $appInfo) + && $this->isAppCompatible($version, $appInfo) ) { $appsToUpgrade[] = $appInfo; } @@ -814,7 +886,7 @@ class AppManager implements IAppManager { $info = $this->getAppInfo($appId); if ($info === null) { $incompatibleApps[] = ['id' => $appId, 'name' => $appId]; - } elseif (!\OC_App::isAppCompatible($version, $info)) { + } elseif (!$this->isAppCompatible($version, $info)) { $incompatibleApps[] = $info; } } @@ -949,4 +1021,94 @@ class AppManager implements IAppManager { /* Only lowercase alphanumeric is allowed */ return preg_replace('/(^[0-9_]|[^a-z0-9_]+|_$)/', '', $app); } + + /** + * Run upgrade tasks for an app after the code has already been updated + * + * @throws AppPathNotFoundException if app folder can't be found + */ + public function upgradeApp(string $appId): bool { + // for apps distributed with core, we refresh app path in case the downloaded version + // have been installed in custom apps and not in the default path + $appPath = $this->getAppPath($appId, true); + + $this->clearAppsCache(); + $l = \OC::$server->getL10N('core'); + $appData = $this->getAppInfo($appId, false, $l->getLanguageCode()); + if ($appData === null) { + throw new AppPathNotFoundException('Could not find ' . $appId); + } + + $ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []); + $ignoreMax = in_array($appId, $ignoreMaxApps, true); + \OC_App::checkAppDependencies( + $this->config, + $l, + $appData, + $ignoreMax + ); + + \OC_App::registerAutoloading($appId, $appPath, true); + \OC_App::executeRepairSteps($appId, $appData['repair-steps']['pre-migration']); + + $ms = new MigrationService($appId, Server::get(\OC\DB\Connection::class)); + $ms->migrate(); + + \OC_App::executeRepairSteps($appId, $appData['repair-steps']['post-migration']); + $queue = Server::get(IJobList::class); + foreach ($appData['repair-steps']['live-migration'] as $step) { + $queue->add(\OC\Migration\BackgroundRepair::class, [ + 'app' => $appId, + 'step' => $step]); + } + + // update appversion in app manager + $this->clearAppsCache(); + $this->getAppVersion($appId, false); + + // Setup background jobs + foreach ($appData['background-jobs'] as $job) { + $queue->add($job); + } + + //set remote/public handlers + foreach ($appData['remote'] as $name => $path) { + $this->config->setAppValue('core', 'remote_' . $name, $appId . '/' . $path); + } + foreach ($appData['public'] as $name => $path) { + $this->config->setAppValue('core', 'public_' . $name, $appId . '/' . $path); + } + + \OC_App::setAppTypes($appId); + + $version = $this->getAppVersion($appId); + $this->config->setAppValue($appId, 'installed_version', $version); + + // migrate eventual new config keys in the process + /** @psalm-suppress InternalMethod */ + $this->configManager->migrateConfigLexiconKeys($appId); + + $this->dispatcher->dispatchTyped(new AppUpdateEvent($appId)); + $this->dispatcher->dispatch(ManagerEvent::EVENT_APP_UPDATE, new ManagerEvent( + ManagerEvent::EVENT_APP_UPDATE, $appId + )); + + return true; + } + + public function isUpgradeRequired(string $appId): bool { + $versions = $this->getAppInstalledVersions(); + $currentVersion = $this->getAppVersion($appId); + if ($currentVersion && isset($versions[$appId])) { + $installedVersion = $versions[$appId]; + if (!version_compare($currentVersion, $installedVersion, '=')) { + return true; + } + } + return false; + } + + public function isAppCompatible(string $serverVersion, array $appInfo, bool $ignoreMax = false): bool { + return count($this->dependencyAnalyzer->analyzeServerVersion($serverVersion, $appInfo, $ignoreMax)) === 0; + } } diff --git a/lib/private/App/DependencyAnalyzer.php b/lib/private/App/DependencyAnalyzer.php index bde8719c41d..374fe4cd272 100644 --- a/lib/private/App/DependencyAnalyzer.php +++ b/lib/private/App/DependencyAnalyzer.php @@ -11,20 +11,29 @@ declare(strict_types=1); namespace OC\App; use OCP\IL10N; +use OCP\L10N\IFactory; +use OCP\Server; class DependencyAnalyzer { + // Cannot be injected because when this class is built IAppManager is not available yet + private ?IL10N $l = null; + public function __construct( private Platform $platform, - private IL10N $l, ) { } + private function getL(): IL10N { + $this->l ??= Server::get(IFactory::class)->get('lib'); + return $this->l; + } + /** * @return array of missing dependencies */ - public function analyze(array $app, bool $ignoreMax = false): array { - if (isset($app['dependencies'])) { - $dependencies = $app['dependencies']; + public function analyze(array $appInfo, bool $ignoreMax = false): array { + if (isset($appInfo['dependencies'])) { + $dependencies = $appInfo['dependencies']; } else { $dependencies = []; } @@ -36,18 +45,12 @@ class DependencyAnalyzer { $this->analyzeCommands($dependencies), $this->analyzeLibraries($dependencies), $this->analyzeOS($dependencies), - $this->analyzeOC($dependencies, $app, $ignoreMax) + $this->analyzeServer($appInfo, $ignoreMax), ); } - public function isMarkedCompatible(array $app): bool { - if (isset($app['dependencies'])) { - $dependencies = $app['dependencies']; - } else { - $dependencies = []; - } - - $maxVersion = $this->getMaxVersion($dependencies, $app); + public function isMarkedCompatible(array $appInfo): bool { + $maxVersion = $this->getMaxVersion($appInfo); if ($maxVersion === null) { return true; } @@ -76,6 +79,7 @@ class DependencyAnalyzer { /** * Parameters will be normalized and then passed into version_compare * in the same order they are specified in the method header + * @param '<'|'lt'|'<='|'le'|'>'|'gt'|'>='|'ge'|'=='|'='|'eq'|'!='|'<>'|'ne' $operator * @return bool result similar to version_compare */ private function compare(string $first, string $second, string $operator): bool { @@ -105,19 +109,19 @@ class DependencyAnalyzer { if (isset($dependencies['php']['@attributes']['min-version'])) { $minVersion = $dependencies['php']['@attributes']['min-version']; if ($this->compareSmaller($this->platform->getPhpVersion(), $minVersion)) { - $missing[] = $this->l->t('PHP %s or higher is required.', [$minVersion]); + $missing[] = $this->getL()->t('PHP %s or higher is required.', [$minVersion]); } } if (isset($dependencies['php']['@attributes']['max-version'])) { $maxVersion = $dependencies['php']['@attributes']['max-version']; if ($this->compareBigger($this->platform->getPhpVersion(), $maxVersion)) { - $missing[] = $this->l->t('PHP with a version lower than %s is required.', [$maxVersion]); + $missing[] = $this->getL()->t('PHP with a version lower than %s is required.', [$maxVersion]); } } if (isset($dependencies['php']['@attributes']['min-int-size'])) { $intSize = $dependencies['php']['@attributes']['min-int-size']; if ($intSize > $this->platform->getIntSize() * 8) { - $missing[] = $this->l->t('%sbit or higher PHP required.', [$intSize]); + $missing[] = $this->getL()->t('%sbit or higher PHP required.', [$intSize]); } } return $missing; @@ -141,7 +145,7 @@ class DependencyAnalyzer { }, $supportedArchitectures); $currentArchitecture = $this->platform->getArchitecture(); if (!in_array($currentArchitecture, $supportedArchitectures, true)) { - $missing[] = $this->l->t('The following architectures are supported: %s', [implode(', ', $supportedArchitectures)]); + $missing[] = $this->getL()->t('The following architectures are supported: %s', [implode(', ', $supportedArchitectures)]); } return $missing; } @@ -167,7 +171,7 @@ class DependencyAnalyzer { }, $supportedDatabases); $currentDatabase = $this->platform->getDatabase(); if (!in_array($currentDatabase, $supportedDatabases)) { - $missing[] = $this->l->t('The following databases are supported: %s', [implode(', ', $supportedDatabases)]); + $missing[] = $this->getL()->t('The following databases are supported: %s', [implode(', ', $supportedDatabases)]); } return $missing; } @@ -192,7 +196,7 @@ class DependencyAnalyzer { } $commandName = $this->getValue($command); if (!$this->platform->isCommandKnown($commandName)) { - $missing[] = $this->l->t('The command line tool %s could not be found', [$commandName]); + $missing[] = $this->getL()->t('The command line tool %s could not be found', [$commandName]); } } return $missing; @@ -215,7 +219,7 @@ class DependencyAnalyzer { $libName = $this->getValue($lib); $libVersion = $this->platform->getLibraryVersion($libName); if (is_null($libVersion)) { - $missing[] = $this->l->t('The library %s is not available.', [$libName]); + $missing[] = $this->getL()->t('The library %s is not available.', [$libName]); continue; } @@ -223,14 +227,14 @@ class DependencyAnalyzer { if (isset($lib['@attributes']['min-version'])) { $minVersion = $lib['@attributes']['min-version']; if ($this->compareSmaller($libVersion, $minVersion)) { - $missing[] = $this->l->t('Library %1$s with a version higher than %2$s is required - available version %3$s.', + $missing[] = $this->getL()->t('Library %1$s with a version higher than %2$s is required - available version %3$s.', [$libName, $minVersion, $libVersion]); } } if (isset($lib['@attributes']['max-version'])) { $maxVersion = $lib['@attributes']['max-version']; if ($this->compareBigger($libVersion, $maxVersion)) { - $missing[] = $this->l->t('Library %1$s with a version lower than %2$s is required - available version %3$s.', + $missing[] = $this->getL()->t('Library %1$s with a version lower than %2$s is required - available version %3$s.', [$libName, $maxVersion, $libVersion]); } } @@ -258,44 +262,48 @@ class DependencyAnalyzer { } $currentOS = $this->platform->getOS(); if (!in_array($currentOS, $oss)) { - $missing[] = $this->l->t('The following platforms are supported: %s', [implode(', ', $oss)]); + $missing[] = $this->getL()->t('The following platforms are supported: %s', [implode(', ', $oss)]); } return $missing; } - private function analyzeOC(array $dependencies, array $appInfo, bool $ignoreMax): array { + private function analyzeServer(array $appInfo, bool $ignoreMax): array { + return $this->analyzeServerVersion($this->platform->getOcVersion(), $appInfo, $ignoreMax); + } + + public function analyzeServerVersion(string $serverVersion, array $appInfo, bool $ignoreMax): array { $missing = []; $minVersion = null; - if (isset($dependencies['nextcloud']['@attributes']['min-version'])) { - $minVersion = $dependencies['nextcloud']['@attributes']['min-version']; - } elseif (isset($dependencies['owncloud']['@attributes']['min-version'])) { - $minVersion = $dependencies['owncloud']['@attributes']['min-version']; + if (isset($appInfo['dependencies']['nextcloud']['@attributes']['min-version'])) { + $minVersion = $appInfo['dependencies']['nextcloud']['@attributes']['min-version']; + } elseif (isset($appInfo['dependencies']['owncloud']['@attributes']['min-version'])) { + $minVersion = $appInfo['dependencies']['owncloud']['@attributes']['min-version']; } elseif (isset($appInfo['requiremin'])) { $minVersion = $appInfo['requiremin']; } elseif (isset($appInfo['require'])) { $minVersion = $appInfo['require']; } - $maxVersion = $this->getMaxVersion($dependencies, $appInfo); + $maxVersion = $this->getMaxVersion($appInfo); if (!is_null($minVersion)) { - if ($this->compareSmaller($this->platform->getOcVersion(), $minVersion)) { - $missing[] = $this->l->t('Server version %s or higher is required.', [$minVersion]); + if ($this->compareSmaller($serverVersion, $minVersion)) { + $missing[] = $this->getL()->t('Server version %s or higher is required.', [$minVersion]); } } if (!$ignoreMax && !is_null($maxVersion)) { - if ($this->compareBigger($this->platform->getOcVersion(), $maxVersion)) { - $missing[] = $this->l->t('Server version %s or lower is required.', [$maxVersion]); + if ($this->compareBigger($serverVersion, $maxVersion)) { + $missing[] = $this->getL()->t('Server version %s or lower is required.', [$maxVersion]); } } return $missing; } - private function getMaxVersion(array $dependencies, array $appInfo): ?string { - if (isset($dependencies['nextcloud']['@attributes']['max-version'])) { - return $dependencies['nextcloud']['@attributes']['max-version']; + private function getMaxVersion(array $appInfo): ?string { + if (isset($appInfo['dependencies']['nextcloud']['@attributes']['max-version'])) { + return $appInfo['dependencies']['nextcloud']['@attributes']['max-version']; } - if (isset($dependencies['owncloud']['@attributes']['max-version'])) { - return $dependencies['owncloud']['@attributes']['max-version']; + if (isset($appInfo['dependencies']['owncloud']['@attributes']['max-version'])) { + return $appInfo['dependencies']['owncloud']['@attributes']['max-version']; } if (isset($appInfo['requiremax'])) { return $appInfo['requiremax']; @@ -304,13 +312,9 @@ class DependencyAnalyzer { return null; } - /** - * @param mixed $element - * @return mixed - */ - private function getValue($element) { + private function getValue(mixed $element): string { if (isset($element['@value'])) { - return $element['@value']; + return (string)$element['@value']; } return (string)$element; } diff --git a/lib/private/DB/MigrationService.php b/lib/private/DB/MigrationService.php index 40579c7a898..e1723ff09a9 100644 --- a/lib/private/DB/MigrationService.php +++ b/lib/private/DB/MigrationService.php @@ -13,8 +13,8 @@ use Doctrine\DBAL\Schema\SchemaException; use Doctrine\DBAL\Schema\Sequence; use Doctrine\DBAL\Schema\Table; use OC\App\InfoParser; -use OC\IntegrityCheck\Helpers\AppLocator; use OC\Migration\SimpleOutput; +use OCP\App\IAppManager; use OCP\AppFramework\App; use OCP\AppFramework\QueryException; use OCP\DB\ISchemaWrapper; @@ -39,7 +39,12 @@ class MigrationService { /** * @throws \Exception */ - public function __construct(string $appName, Connection $connection, ?IOutput $output = null, ?AppLocator $appLocator = null, ?LoggerInterface $logger = null) { + public function __construct( + string $appName, + Connection $connection, + ?IOutput $output = null, + ?LoggerInterface $logger = null, + ) { $this->appName = $appName; $this->connection = $connection; if ($logger === null) { @@ -58,10 +63,8 @@ class MigrationService { $this->migrationsNamespace = 'OC\\Core\\Migrations'; $this->checkOracle = true; } else { - if ($appLocator === null) { - $appLocator = new AppLocator(); - } - $appPath = $appLocator->getAppPath($appName); + $appManager = Server::get(IAppManager::class); + $appPath = $appManager->getAppPath($appName); $namespace = App::buildAppNamespace($appName); $this->migrationsPath = "$appPath/lib/Migration"; $this->migrationsNamespace = $namespace . '\\Migration'; @@ -728,7 +731,7 @@ class MigrationService { } } - private function ensureMigrationsAreLoaded() { + private function ensureMigrationsAreLoaded(): void { if (empty($this->migrations)) { $this->migrations = $this->findMigrations(); } diff --git a/lib/private/Installer.php b/lib/private/Installer.php index fd737d286ad..e40e77c8b50 100644 --- a/lib/private/Installer.php +++ b/lib/private/Installer.php @@ -18,13 +18,15 @@ use OC\Archive\TAR; use OC\DB\Connection; use OC\DB\MigrationService; use OC\Files\FilenameValidator; -use OC_App; +use OCP\App\AppPathNotFoundException; use OCP\App\IAppManager; +use OCP\BackgroundJob\IJobList; use OCP\Files; use OCP\HintException; use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\ITempManager; +use OCP\L10N\IFactory; use OCP\Migration\IOutput; use OCP\Server; use phpseclib\File\X509; @@ -43,6 +45,8 @@ class Installer { private ITempManager $tempManager, private LoggerInterface $logger, private IConfig $config, + private IAppManager $appManager, + private IFactory $l10nFactory, private bool $isCLI, ) { } @@ -56,21 +60,12 @@ class Installer { * @return string app ID */ public function installApp(string $appId, bool $forceEnable = false): string { - $app = \OC_App::findAppInDirectories($appId); - if ($app === false) { - throw new \Exception('App not found in any app directory'); - } + $appPath = $this->appManager->getAppPath($appId, true); - $basedir = $app['path'] . '/' . $appId; + $l = $this->l10nFactory->get('core'); + $info = $this->appManager->getAppInfoByPath($appPath . '/appinfo/info.xml', $l->getLanguageCode()); - if (is_file($basedir . '/appinfo/database.xml')) { - throw new \Exception('The appinfo/database.xml file is not longer supported. Used in ' . $appId); - } - - $l = \OCP\Util::getL10N('core'); - $info = \OCP\Server::get(IAppManager::class)->getAppInfoByPath($basedir . '/appinfo/info.xml', $l->getLanguageCode()); - - if (!is_array($info)) { + if (!is_array($info) || $info['id'] !== $appId) { throw new \Exception( $l->t('App "%s" cannot be installed because appinfo file cannot be read.', [$appId] @@ -82,9 +77,8 @@ class Installer { $ignoreMax = $forceEnable || in_array($appId, $ignoreMaxApps, true); $version = implode('.', \OCP\Util::getVersion()); - if (!\OC_App::isAppCompatible($version, $info, $ignoreMax)) { + if (!$this->appManager->isAppCompatible($version, $info, $ignoreMax)) { throw new \Exception( - // TODO $l $l->t('App "%s" cannot be installed because it is not compatible with this version of the server.', [$info['name']] ) @@ -93,47 +87,10 @@ class Installer { // check for required dependencies \OC_App::checkAppDependencies($this->config, $l, $info, $ignoreMax); - /** @var Coordinator $coordinator */ - $coordinator = \OC::$server->get(Coordinator::class); + $coordinator = Server::get(Coordinator::class); $coordinator->runLazyRegistration($appId); - \OC_App::registerAutoloading($appId, $basedir); - $previousVersion = $this->config->getAppValue($info['id'], 'installed_version', false); - if ($previousVersion) { - OC_App::executeRepairSteps($appId, $info['repair-steps']['pre-migration']); - } - - //install the database - $ms = new MigrationService($info['id'], \OCP\Server::get(Connection::class)); - $ms->migrate('latest', !$previousVersion); - - if ($previousVersion) { - OC_App::executeRepairSteps($appId, $info['repair-steps']['post-migration']); - } - - \OC_App::setupBackgroundJobs($info['background-jobs']); - - //run appinfo/install.php - self::includeAppScript($basedir . '/appinfo/install.php'); - - OC_App::executeRepairSteps($appId, $info['repair-steps']['install']); - - $config = \OCP\Server::get(IConfig::class); - //set the installed version - $config->setAppValue($info['id'], 'installed_version', \OCP\Server::get(IAppManager::class)->getAppVersion($info['id'], false)); - $config->setAppValue($info['id'], 'enabled', 'no'); - - //set remote/public handlers - foreach ($info['remote'] as $name => $path) { - $config->setAppValue('core', 'remote_' . $name, $info['id'] . '/' . $path); - } - foreach ($info['public'] as $name => $path) { - $config->setAppValue('core', 'public_' . $name, $info['id'] . '/' . $path); - } - - OC_App::setAppTypes($info['id']); - - return $info['id']; + return $this->installAppLastSteps($appPath, $info, null, 'no'); } /** @@ -142,7 +99,7 @@ class Installer { * @param bool $allowUnstable Allow unstable releases */ public function updateAppstoreApp(string $appId, bool $allowUnstable = false): bool { - if ($this->isUpdateAvailable($appId, $allowUnstable)) { + if ($this->isUpdateAvailable($appId, $allowUnstable) !== false) { try { $this->downloadApp($appId, $allowUnstable); } catch (\Exception $e) { @@ -151,7 +108,7 @@ class Installer { ]); return false; } - return OC_App::updateApp($appId); + return $this->appManager->upgradeApp($appId); } return false; @@ -347,7 +304,7 @@ class Installer { } // Check if the version is lower than before - $currentVersion = \OCP\Server::get(IAppManager::class)->getAppVersion($appId, true); + $currentVersion = $this->appManager->getAppVersion($appId, true); $newVersion = (string)$xml->version; if (version_compare($currentVersion, $newVersion) === 1) { throw new \Exception( @@ -424,7 +381,7 @@ class Installer { foreach ($this->apps as $app) { if ($app['id'] === $appId) { - $currentVersion = \OCP\Server::get(IAppManager::class)->getAppVersion($appId, true); + $currentVersion = $this->appManager->getAppVersion($appId, true); if (!isset($app['releases'][0]['version'])) { return false; @@ -447,12 +404,12 @@ class Installer { * The function will check if the path contains a .git folder */ private function isInstalledFromGit(string $appId): bool { - $app = \OC_App::findAppInDirectories($appId); - if ($app === false) { + try { + $appPath = $this->appManager->getAppPath($appId); + return file_exists($appPath . '/.git/'); + } catch (AppPathNotFoundException) { return false; } - $basedir = $app['path'] . '/' . $appId; - return file_exists($basedir . '/.git/'); } /** @@ -487,7 +444,7 @@ class Installer { */ public function removeApp(string $appId): bool { if ($this->isDownloaded($appId)) { - if (\OCP\Server::get(IAppManager::class)->isShipped($appId)) { + if ($this->appManager->isShipped($appId)) { return false; } @@ -518,8 +475,7 @@ class Installer { $this->downloadApp($appId); } $this->installApp($appId); - $app = new OC_App(); - $app->enable($appId); + $this->appManager->enableApp($appId); } $bundles = json_decode($this->config->getAppValue('core', 'installed.bundles', json_encode([])), true); $bundles[] = $bundle->getIdentifier(); @@ -534,25 +490,23 @@ class Installer { * working ownCloud at the end instead of an aborted update. * @return array Array of error messages (appid => Exception) */ - public static function installShippedApps(bool $softErrors = false, ?IOutput $output = null): array { + public function installShippedApps(bool $softErrors = false, ?IOutput $output = null): array { if ($output instanceof IOutput) { $output->debug('Installing shipped apps'); } - $appManager = \OCP\Server::get(IAppManager::class); - $config = \OCP\Server::get(IConfig::class); $errors = []; foreach (\OC::$APPSROOTS as $app_dir) { if ($dir = opendir($app_dir['path'])) { while (false !== ($filename = readdir($dir))) { if ($filename[0] !== '.' and is_dir($app_dir['path'] . "/$filename")) { if (file_exists($app_dir['path'] . "/$filename/appinfo/info.xml")) { - if ($config->getAppValue($filename, 'installed_version') === '') { - $enabled = $appManager->isDefaultEnabled($filename); - if (($enabled || in_array($filename, $appManager->getAlwaysEnabledApps())) - && $config->getAppValue($filename, 'enabled') !== 'no') { + if ($this->config->getAppValue($filename, 'installed_version') === '') { + $enabled = $this->appManager->isDefaultEnabled($filename); + if (($enabled || in_array($filename, $this->appManager->getAlwaysEnabledApps())) + && $this->config->getAppValue($filename, 'enabled') !== 'no') { if ($softErrors) { try { - Installer::installShippedApp($filename, $output); + $this->installShippedApp($filename, $output); } catch (HintException $e) { if ($e->getPrevious() instanceof TableExistsException) { $errors[$filename] = $e; @@ -561,9 +515,9 @@ class Installer { throw $e; } } else { - Installer::installShippedApp($filename, $output); + $this->installShippedApp($filename, $output); } - $config->setAppValue($filename, 'enabled', 'yes'); + $this->config->setAppValue($filename, 'enabled', 'yes'); } } } @@ -576,59 +530,79 @@ class Installer { return $errors; } - /** - * install an app already placed in the app folder - */ - public static function installShippedApp(string $app, ?IOutput $output = null): string|false { - if ($output instanceof IOutput) { - $output->debug('Installing ' . $app); - } + private function installAppLastSteps(string $appPath, array $info, ?IOutput $output = null, string $enabled = 'no'): string { + \OC_App::registerAutoloading($info['id'], $appPath); - $appManager = \OCP\Server::get(IAppManager::class); - $config = \OCP\Server::get(IConfig::class); - - $appPath = $appManager->getAppPath($app); - \OC_App::registerAutoloading($app, $appPath); - - $ms = new MigrationService($app, \OCP\Server::get(Connection::class)); + $previousVersion = $this->config->getAppValue($info['id'], 'installed_version', ''); + $ms = new MigrationService($info['id'], Server::get(Connection::class)); if ($output instanceof IOutput) { $ms->setOutput($output); } - $previousVersion = $config->getAppValue($app, 'installed_version', false); - $ms->migrate('latest', !$previousVersion); - - //run appinfo/install.php - self::includeAppScript("$appPath/appinfo/install.php"); - - $info = \OCP\Server::get(IAppManager::class)->getAppInfo($app); - if (is_null($info)) { - return false; + if ($previousVersion !== '') { + \OC_App::executeRepairSteps($info['id'], $info['repair-steps']['pre-migration']); } + + $ms->migrate('latest', $previousVersion === ''); + + if ($previousVersion !== '') { + \OC_App::executeRepairSteps($info['id'], $info['repair-steps']['post-migration']); + } + if ($output instanceof IOutput) { - $output->debug('Registering tasks of ' . $app); - } - \OC_App::setupBackgroundJobs($info['background-jobs']); - - OC_App::executeRepairSteps($app, $info['repair-steps']['install']); - - $config->setAppValue($app, 'installed_version', \OCP\Server::get(IAppManager::class)->getAppVersion($app)); - if (array_key_exists('ocsid', $info)) { - $config->setAppValue($app, 'ocsid', $info['ocsid']); + $output->debug('Registering tasks of ' . $info['id']); } - //set remote/public handlers + // Setup background jobs + $queue = Server::get(IJobList::class); + foreach ($info['background-jobs'] as $job) { + $queue->add($job); + } + + // Run deprecated appinfo/install.php if any + $appInstallScriptPath = $appPath . '/appinfo/install.php'; + if (file_exists($appInstallScriptPath)) { + $this->logger->warning('Using an appinfo/install.php file is deprecated. Application "{app}" still uses one.', [ + 'app' => $info['id'], + ]); + self::includeAppScript($appInstallScriptPath); + } + + \OC_App::executeRepairSteps($info['id'], $info['repair-steps']['install']); + + // Set the installed version + $this->config->setAppValue($info['id'], 'installed_version', $this->appManager->getAppVersion($info['id'], false)); + $this->config->setAppValue($info['id'], 'enabled', $enabled); + + // Set remote/public handlers foreach ($info['remote'] as $name => $path) { - $config->setAppValue('core', 'remote_' . $name, $app . '/' . $path); + $this->config->setAppValue('core', 'remote_' . $name, $info['id'] . '/' . $path); } foreach ($info['public'] as $name => $path) { - $config->setAppValue('core', 'public_' . $name, $app . '/' . $path); + $this->config->setAppValue('core', 'public_' . $name, $info['id'] . '/' . $path); } - OC_App::setAppTypes($info['id']); + \OC_App::setAppTypes($info['id']); return $info['id']; } + /** + * install an app already placed in the app folder + */ + public function installShippedApp(string $app, ?IOutput $output = null): string|false { + if ($output instanceof IOutput) { + $output->debug('Installing ' . $app); + } + $info = $this->appManager->getAppInfo($app); + if (is_null($info) || $info['id'] !== $app) { + return false; + } + + $appPath = $this->appManager->getAppPath($app); + + return $this->installAppLastSteps($appPath, $info, $output, 'yes'); + } + private static function includeAppScript(string $script): void { if (file_exists($script)) { include $script; diff --git a/lib/private/IntegrityCheck/Checker.php b/lib/private/IntegrityCheck/Checker.php index 2bd6e426b79..e98f77acaa0 100644 --- a/lib/private/IntegrityCheck/Checker.php +++ b/lib/private/IntegrityCheck/Checker.php @@ -10,7 +10,6 @@ namespace OC\IntegrityCheck; use OC\Core\Command\Maintenance\Mimetype\GenerateMimetypeFileBuilder; use OC\IntegrityCheck\Exceptions\InvalidSignatureException; -use OC\IntegrityCheck\Helpers\AppLocator; use OC\IntegrityCheck\Helpers\EnvironmentHelper; use OC\IntegrityCheck\Helpers\FileAccessHelper; use OC\IntegrityCheck\Iterator\ExcludeFileByNameFilterIterator; @@ -44,7 +43,6 @@ class Checker { private ServerVersion $serverVersion, private EnvironmentHelper $environmentHelper, private FileAccessHelper $fileAccessHelper, - private AppLocator $appLocator, private ?IConfig $config, private ?IAppConfig $appConfig, ICacheFactory $cacheFactory, @@ -460,7 +458,7 @@ class Checker { public function verifyAppSignature(string $appId, string $path = '', bool $forceVerify = false): array { try { if ($path === '') { - $path = $this->appLocator->getAppPath($appId); + $path = $this->appManager->getAppPath($appId); } $result = $this->verify( $path . '/appinfo/signature.json', @@ -545,7 +543,7 @@ class Checker { $appNeedsToBeChecked = false; if ($isShipped) { $appNeedsToBeChecked = true; - } elseif ($this->fileAccessHelper->file_exists($this->appLocator->getAppPath($appId) . '/appinfo/signature.json')) { + } elseif ($this->fileAccessHelper->file_exists($this->appManager->getAppPath($appId) . '/appinfo/signature.json')) { // Otherwise only if the application explicitly ships a signature.json file $appNeedsToBeChecked = true; } diff --git a/lib/private/IntegrityCheck/Helpers/AppLocator.php b/lib/private/IntegrityCheck/Helpers/AppLocator.php deleted file mode 100644 index 148a3aeda76..00000000000 --- a/lib/private/IntegrityCheck/Helpers/AppLocator.php +++ /dev/null @@ -1,33 +0,0 @@ -get(ServerVersion::class), $c->get(EnvironmentHelper::class), new FileAccessHelper(), - new AppLocator(), $config, $appConfig, $c->get(ICacheFactory::class), @@ -1177,17 +1174,6 @@ class Server extends ServerContainer implements IServerContainer { ); }); - $this->registerService(Installer::class, function (ContainerInterface $c) { - return new Installer( - $c->get(AppFetcher::class), - $c->get(IClientService::class), - $c->get(ITempManager::class), - $c->get(LoggerInterface::class), - $c->get(\OCP\IConfig::class), - \OC::$CLI - ); - }); - $this->registerService(IApiFactory::class, function (ContainerInterface $c) { return new ApiFactory($c->get(IClientService::class)); }); diff --git a/lib/private/Setup.php b/lib/private/Setup.php index 4082cd5df50..15f0a4eb617 100644 --- a/lib/private/Setup.php +++ b/lib/private/Setup.php @@ -443,7 +443,8 @@ class Setup { // Install shipped apps and specified app bundles $this->outputDebug($output, 'Install default apps'); - Installer::installShippedApps(false, $output); + $installer = Server::get(Installer::class); + $installer->installShippedApps(false, $output); // create empty file in data dir, so we can later find // out that this is indeed a Nextcloud data directory diff --git a/lib/private/Updater.php b/lib/private/Updater.php index 9cd33863612..da7b52e5493 100644 --- a/lib/private/Updater.php +++ b/lib/private/Updater.php @@ -23,7 +23,6 @@ use OC\Repair\Events\RepairInfoEvent; use OC\Repair\Events\RepairStartEvent; use OC\Repair\Events\RepairStepEvent; use OC\Repair\Events\RepairWarningEvent; -use OC_App; use OCP\App\IAppManager; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventDispatcher; @@ -60,6 +59,7 @@ class Updater extends BasicEmitter { private Checker $checker, private ?LoggerInterface $log, private Installer $installer, + private IAppManager $appManager, ) { } @@ -238,18 +238,16 @@ class Updater extends BasicEmitter { // Update the appfetchers version so it downloads the correct list from the appstore \OC::$server->get(AppFetcher::class)->setVersion($currentVersion); - /** @var AppManager $appManager */ - $appManager = \OC::$server->getAppManager(); - // upgrade appstore apps - $this->upgradeAppStoreApps($appManager->getEnabledApps()); - $autoDisabledApps = $appManager->getAutoDisabledApps(); + $this->upgradeAppStoreApps($this->appManager->getEnabledApps()); + /** @var AppManager $this->appManager */ + $autoDisabledApps = $this->appManager->getAutoDisabledApps(); if (!empty($autoDisabledApps)) { $this->upgradeAppStoreApps(array_keys($autoDisabledApps), $autoDisabledApps); } // install new shipped apps on upgrade - $errors = Installer::installShippedApps(true); + $errors = $this->installer->installShippedApps(true); foreach ($errors as $appId => $exception) { /** @var \Exception $exception */ $this->log->error($exception->getMessage(), [ @@ -296,7 +294,7 @@ class Updater extends BasicEmitter { * @throws NeedsUpdateException */ protected function doAppUpgrade(): void { - $apps = \OC_App::getEnabledApps(); + $apps = $this->appManager->getEnabledApps(); $priorityTypes = ['authentication', 'extended_authentication', 'filesystem', 'logging']; $pseudoOtherType = 'other'; $stacks = [$pseudoOtherType => []]; @@ -307,7 +305,7 @@ class Updater extends BasicEmitter { if (!isset($stacks[$type])) { $stacks[$type] = []; } - if (\OC_App::isType($appId, [$type])) { + if ($this->appManager->isType($appId, [$type])) { $stacks[$type][] = $appId; $priorityType = true; break; @@ -320,16 +318,16 @@ class Updater extends BasicEmitter { foreach (array_merge($priorityTypes, [$pseudoOtherType]) as $type) { $stack = $stacks[$type]; foreach ($stack as $appId) { - if (\OC_App::shouldUpgrade($appId)) { - $this->emit('\OC\Updater', 'appUpgradeStarted', [$appId, \OCP\Server::get(IAppManager::class)->getAppVersion($appId)]); - \OC_App::updateApp($appId); - $this->emit('\OC\Updater', 'appUpgrade', [$appId, \OCP\Server::get(IAppManager::class)->getAppVersion($appId)]); + if ($this->appManager->isUpgradeRequired($appId)) { + $this->emit('\OC\Updater', 'appUpgradeStarted', [$appId, $this->appManager->getAppVersion($appId)]); + $this->appManager->upgradeApp($appId); + $this->emit('\OC\Updater', 'appUpgrade', [$appId, $this->appManager->getAppVersion($appId)]); } if ($type !== $pseudoOtherType) { // load authentication, filesystem and logging apps after // upgrading them. Other apps my need to rely on modifying // user and/or filesystem aspects. - \OC_App::loadApp($appId); + $this->appManager->loadApp($appId); } } } @@ -345,25 +343,21 @@ class Updater extends BasicEmitter { */ private function checkAppsRequirements(): void { $isCoreUpgrade = $this->isCodeUpgrade(); - $apps = OC_App::getEnabledApps(); + $apps = $this->appManager->getEnabledApps(); $version = implode('.', Util::getVersion()); - $appManager = \OC::$server->getAppManager(); foreach ($apps as $app) { // check if the app is compatible with this version of Nextcloud - $info = $appManager->getAppInfo($app); - if ($info === null || !OC_App::isAppCompatible($version, $info)) { - if ($appManager->isShipped($app)) { + $info = $this->appManager->getAppInfo($app); + if ($info === null || !$this->appManager->isAppCompatible($version, $info)) { + if ($this->appManager->isShipped($app)) { throw new \UnexpectedValueException('The files of the app "' . $app . '" were not correctly replaced before running the update'); } - $appManager->disableApp($app, true); + $this->appManager->disableApp($app, true); $this->emit('\OC\Updater', 'incompatibleAppDisabled', [$app]); } } } - /** - * @return bool - */ private function isCodeUpgrade(): bool { $installedVersion = $this->config->getSystemValueString('version', '0.0.0'); $currentVersion = implode('.', Util::getVersion()); @@ -395,12 +389,11 @@ class Updater extends BasicEmitter { } $this->emit('\OC\Updater', 'checkAppStoreApp', [$app]); - if (!empty($previousEnableStates)) { - $ocApp = new \OC_App(); + if (isset($previousEnableStates[$app])) { if (!empty($previousEnableStates[$app]) && is_array($previousEnableStates[$app])) { - $ocApp->enable($app, $previousEnableStates[$app]); - } else { - $ocApp->enable($app); + $this->appManager->enableAppForGroups($app, $previousEnableStates[$app]); + } elseif ($previousEnableStates[$app] === 'yes') { + $this->appManager->enableApp($app); } } } catch (\Exception $ex) { diff --git a/lib/private/legacy/OC_App.php b/lib/private/legacy/OC_App.php index 6dbef42594b..c97c4f3d81e 100644 --- a/lib/private/legacy/OC_App.php +++ b/lib/private/legacy/OC_App.php @@ -6,17 +6,14 @@ declare(strict_types=1); * SPDX-FileCopyrightText: 2016 ownCloud, Inc. * SPDX-License-Identifier: AGPL-3.0-only */ +use OC\App\AppManager; use OC\App\DependencyAnalyzer; -use OC\App\Platform; use OC\AppFramework\Bootstrap\Coordinator; -use OC\Config\ConfigManager; -use OC\DB\MigrationService; use OC\Installer; use OC\Repair; use OC\Repair\Events\RepairErrorEvent; -use OCP\App\Events\AppUpdateEvent; +use OCP\App\AppPathNotFoundException; use OCP\App\IAppManager; -use OCP\App\ManagerEvent; use OCP\Authentication\IAlternativeLogin; use OCP\EventDispatcher\IEventDispatcher; use OCP\IAppConfig; @@ -205,6 +202,7 @@ class OC_App { * @param array $groups (optional) when set, only these groups will have access to the app * @throws \Exception * @return void + * @deprecated 32.0.0 Use the installer and the app manager instead * * This function set an app as enabled in appconfig. */ @@ -242,49 +240,12 @@ class OC_App { * * If multiple copies are found, the apps root the latest version is returned. * - * @param string $appId * @param bool $ignoreCache ignore cache and rebuild it * @return false|array{path: string, url: string} the apps root shape + * @deprecated 32.0.0 internal, use getAppPath or getAppWebPath */ public static function findAppInDirectories(string $appId, bool $ignoreCache = false) { - $sanitizedAppId = self::cleanAppId($appId); - if ($sanitizedAppId !== $appId) { - return false; - } - static $app_dir = []; - - if (isset($app_dir[$appId]) && !$ignoreCache) { - return $app_dir[$appId]; - } - - $possibleApps = []; - foreach (OC::$APPSROOTS as $dir) { - if (file_exists($dir['path'] . '/' . $appId)) { - $possibleApps[] = $dir; - } - } - - if (empty($possibleApps)) { - return false; - } elseif (count($possibleApps) === 1) { - $dir = array_shift($possibleApps); - $app_dir[$appId] = $dir; - return $dir; - } else { - $versionToLoad = []; - foreach ($possibleApps as $possibleApp) { - $version = self::getAppVersionByPath($possibleApp['path'] . '/' . $appId); - if (empty($versionToLoad) || version_compare($version, $versionToLoad['version'], '>')) { - $versionToLoad = [ - 'dir' => $possibleApp, - 'version' => $version, - ]; - } - } - $app_dir[$appId] = $versionToLoad['dir']; - return $versionToLoad['dir']; - //TODO - write test - } + return Server::get(AppManager::class)->findAppInDirectories($appId, $ignoreCache); } /** @@ -299,17 +260,11 @@ class OC_App { * @deprecated 11.0.0 use Server::get(IAppManager)->getAppPath() */ public static function getAppPath(string $appId, bool $refreshAppPath = false) { - $appId = self::cleanAppId($appId); - if ($appId === '') { + try { + return Server::get(IAppManager::class)->getAppPath($appId, $refreshAppPath); + } catch (AppPathNotFoundException) { return false; - } elseif ($appId === 'core') { - return __DIR__ . '/../../../core'; } - - if (($dir = self::findAppInDirectories($appId, $refreshAppPath)) != false) { - return $dir['path'] . '/' . $appId; - } - return false; } /** @@ -318,20 +273,20 @@ class OC_App { * * @param string $appId * @return string|false - * @deprecated 18.0.0 use \OC::$server->getAppManager()->getAppWebPath() + * @deprecated 18.0.0 use Server::get(IAppManager)->getAppWebPath() */ public static function getAppWebPath(string $appId) { - if (($dir = self::findAppInDirectories($appId)) != false) { - return OC::$WEBROOT . $dir['url'] . '/' . $appId; + try { + return Server::get(IAppManager::class)->getAppWebPath($appId); + } catch (AppPathNotFoundException) { + return false; } - return false; } /** * get app's version based on it's path * - * @param string $path - * @return string + * @deprecated 32.0.0 use Server::get(IAppManager)->getAppInfoByPath() with the path to info.xml directly */ public static function getAppVersionByPath(string $path): string { $infoFile = $path . '/appinfo/info.xml'; @@ -542,38 +497,11 @@ class OC_App { return $appList; } - public static function shouldUpgrade(string $app): bool { - $versions = self::getAppVersions(); - $currentVersion = Server::get(\OCP\App\IAppManager::class)->getAppVersion($app); - if ($currentVersion && isset($versions[$app])) { - $installedVersion = $versions[$app]; - if (!version_compare($currentVersion, $installedVersion, '=')) { - return true; - } - } - return false; - } - /** - * Adjust the number of version parts of $version1 to match - * the number of version parts of $version2. - * - * @param string $version1 version to adjust - * @param string $version2 version to take the number of parts from - * @return string shortened $version1 + * @deprecated 32.0.0 Use IAppManager::isUpgradeRequired instead */ - private static function adjustVersionParts(string $version1, string $version2): string { - $version1 = explode('.', $version1); - $version2 = explode('.', $version2); - // reduce $version1 to match the number of parts in $version2 - while (count($version1) > count($version2)) { - array_pop($version1); - } - // if $version1 does not have enough parts, add some - while (count($version1) < count($version2)) { - $version1[] = '0'; - } - return implode('.', $version1); + public static function shouldUpgrade(string $app): bool { + return Server::get(\OCP\App\IAppManager::class)->isUpgradeRequired($app); } /** @@ -590,42 +518,11 @@ class OC_App { * @param string $ocVersion Nextcloud version to check against * @param array $appInfo app info (from xml) * - * @return boolean true if compatible, otherwise false + * @return bool true if compatible, otherwise false + * @deprecated 32.0.0 Use IAppManager::isAppCompatible instead */ public static function isAppCompatible(string $ocVersion, array $appInfo, bool $ignoreMax = false): bool { - $requireMin = ''; - $requireMax = ''; - if (isset($appInfo['dependencies']['nextcloud']['@attributes']['min-version'])) { - $requireMin = $appInfo['dependencies']['nextcloud']['@attributes']['min-version']; - } elseif (isset($appInfo['dependencies']['owncloud']['@attributes']['min-version'])) { - $requireMin = $appInfo['dependencies']['owncloud']['@attributes']['min-version']; - } elseif (isset($appInfo['requiremin'])) { - $requireMin = $appInfo['requiremin']; - } elseif (isset($appInfo['require'])) { - $requireMin = $appInfo['require']; - } - - if (isset($appInfo['dependencies']['nextcloud']['@attributes']['max-version'])) { - $requireMax = $appInfo['dependencies']['nextcloud']['@attributes']['max-version']; - } elseif (isset($appInfo['dependencies']['owncloud']['@attributes']['max-version'])) { - $requireMax = $appInfo['dependencies']['owncloud']['@attributes']['max-version']; - } elseif (isset($appInfo['requiremax'])) { - $requireMax = $appInfo['requiremax']; - } - - if (!empty($requireMin) - && version_compare(self::adjustVersionParts($ocVersion, $requireMin), $requireMin, '<') - ) { - return false; - } - - if (!$ignoreMax && !empty($requireMax) - && version_compare(self::adjustVersionParts($ocVersion, $requireMax), $requireMax, '>') - ) { - return false; - } - - return true; + return Server::get(\OCP\App\IAppManager::class)->isAppCompatible($ocVersion, $appInfo, $ignoreMax); } /** @@ -639,77 +536,14 @@ class OC_App { /** * update the database for the app and call the update script * - * @param string $appId - * @return bool + * @deprecated 32.0.0 Use IAppManager::upgradeApp instead */ public static function updateApp(string $appId): bool { - // for apps distributed with core, we refresh app path in case the downloaded version - // have been installed in custom apps and not in the default path - $appPath = self::getAppPath($appId, true); - if ($appPath === false) { + try { + return Server::get(\OC\App\AppManager::class)->upgradeApp($appId); + } catch (\OCP\App\AppPathNotFoundException $e) { return false; } - - if (is_file($appPath . '/appinfo/database.xml')) { - Server::get(LoggerInterface::class)->error('The appinfo/database.xml file is not longer supported. Used in ' . $appId); - return false; - } - - \OC::$server->getAppManager()->clearAppsCache(); - $l = \OC::$server->getL10N('core'); - $appData = Server::get(\OCP\App\IAppManager::class)->getAppInfo($appId, false, $l->getLanguageCode()); - - $ignoreMaxApps = \OC::$server->getConfig()->getSystemValue('app_install_overwrite', []); - $ignoreMax = in_array($appId, $ignoreMaxApps, true); - \OC_App::checkAppDependencies( - \OC::$server->getConfig(), - $l, - $appData, - $ignoreMax - ); - - self::registerAutoloading($appId, $appPath, true); - self::executeRepairSteps($appId, $appData['repair-steps']['pre-migration']); - - $ms = new MigrationService($appId, \OC::$server->get(\OC\DB\Connection::class)); - $ms->migrate(); - - self::executeRepairSteps($appId, $appData['repair-steps']['post-migration']); - self::setupLiveMigrations($appId, $appData['repair-steps']['live-migration']); - // update appversion in app manager - \OC::$server->getAppManager()->clearAppsCache(); - \OC::$server->getAppManager()->getAppVersion($appId, false); - - self::setupBackgroundJobs($appData['background-jobs']); - - //set remote/public handlers - if (array_key_exists('ocsid', $appData)) { - \OC::$server->getConfig()->setAppValue($appId, 'ocsid', $appData['ocsid']); - } elseif (\OC::$server->getConfig()->getAppValue($appId, 'ocsid') !== '') { - \OC::$server->getConfig()->deleteAppValue($appId, 'ocsid'); - } - foreach ($appData['remote'] as $name => $path) { - \OC::$server->getConfig()->setAppValue('core', 'remote_' . $name, $appId . '/' . $path); - } - foreach ($appData['public'] as $name => $path) { - \OC::$server->getConfig()->setAppValue('core', 'public_' . $name, $appId . '/' . $path); - } - - self::setAppTypes($appId); - - $version = Server::get(\OCP\App\IAppManager::class)->getAppVersion($appId); - \OC::$server->getConfig()->setAppValue($appId, 'installed_version', $version); - - // migrate eventual new config keys in the process - /** @psalm-suppress InternalMethod */ - Server::get(ConfigManager::class)->migrateConfigLexiconKeys($appId); - - \OC::$server->get(IEventDispatcher::class)->dispatchTyped(new AppUpdateEvent($appId)); - \OC::$server->get(IEventDispatcher::class)->dispatch(ManagerEvent::EVENT_APP_UPDATE, new ManagerEvent( - ManagerEvent::EVENT_APP_UPDATE, $appId - )); - - return true; } /** @@ -740,6 +574,9 @@ class OC_App { $r->run(); } + /** + * @deprecated 32.0.0 Use the IJobList directly instead + */ public static function setupBackgroundJobs(array $jobs) { $queue = \OC::$server->getJobList(); foreach ($jobs as $job) { @@ -747,19 +584,6 @@ class OC_App { } } - /** - * @param string $appId - * @param string[] $steps - */ - private static function setupLiveMigrations(string $appId, array $steps) { - $queue = \OC::$server->getJobList(); - foreach ($steps as $step) { - $queue->add('OC\Migration\BackgroundRepair', [ - 'app' => $appId, - 'step' => $step]); - } - } - /** * @param \OCP\IConfig $config * @param \OCP\IL10N $l @@ -767,7 +591,7 @@ class OC_App { * @throws \Exception */ public static function checkAppDependencies(\OCP\IConfig $config, \OCP\IL10N $l, array $info, bool $ignoreMax) { - $dependencyAnalyzer = new DependencyAnalyzer(new Platform($config), $l); + $dependencyAnalyzer = Server::get(DependencyAnalyzer::class); $missing = $dependencyAnalyzer->analyze($info, $ignoreMax); if (!empty($missing)) { $missingMsg = implode(PHP_EOL, $missing); diff --git a/lib/public/App/IAppManager.php b/lib/public/App/IAppManager.php index 20019ce1ffd..97acd9af7b1 100644 --- a/lib/public/App/IAppManager.php +++ b/lib/public/App/IAppManager.php @@ -166,9 +166,10 @@ interface IAppManager { * Get the directory for the given app. * * @since 11.0.0 + * @since 32.0.0 Added param $ignoreCache to ignore cache * @throws AppPathNotFoundException */ - public function getAppPath(string $appId): string; + public function getAppPath(string $appId, bool $ignoreCache = false): string; /** * Get the web path for the given app. @@ -340,4 +341,36 @@ interface IAppManager { * @since 31.0.0 */ public function getAllAppsInAppsFolders(): array; + + /** + * Run upgrade tasks for an app after the code has already been updated + * + * @throws AppPathNotFoundException if app folder can't 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 + * + * @since 32.0.0 + */ + public function isUpgradeRequired(string $appId): bool; + + /** + * Check whether the current Nextcloud version matches the given + * application's version 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. + * + * @param string $serverVersion Nextcloud version to check against + * @param array $appInfo app info (from xml) + * @since 32.0.0 + */ + public function isAppCompatible(string $serverVersion, array $appInfo, bool $ignoreMax = false): bool; } diff --git a/tests/data/app/expected-info.json b/tests/data/app/expected-info.json index 40ba6104104..9c5ce230cad 100644 --- a/tests/data/app/expected-info.json +++ b/tests/data/app/expected-info.json @@ -16,7 +16,6 @@ "admin": "admin-encryption" }, "types": ["filesystem"], - "ocsid": "166047", "dependencies": { "php": { "@attributes" : { diff --git a/tests/data/app/invalid-info.xml b/tests/data/app/invalid-info.xml index 1c39ae2d625..8a95669beb0 100644 --- a/tests/data/app/invalid-info.xml +++ b/tests/data/app/invalid-info.xml @@ -22,5 +22,4 @@ - 166047 diff --git a/tests/data/app/valid-info.xml b/tests/data/app/valid-info.xml index d2569788399..811fa634264 100644 --- a/tests/data/app/valid-info.xml +++ b/tests/data/app/valid-info.xml @@ -22,7 +22,6 @@ - 166047 sqlite diff --git a/tests/enable_all.php b/tests/enable_all.php index 85e5f3b9247..e9aaf3299a5 100644 --- a/tests/enable_all.php +++ b/tests/enable_all.php @@ -6,10 +6,18 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ +use OC\Installer; +use OCP\App\IAppManager; +use OCP\Server; + require_once __DIR__ . '/../lib/base.php'; function enableApp($app) { - (new \OC_App())->enable($app); + $installer = Server::get(Installer::class); + $appManager = Server::get(IAppManager::class); + + $installer->installApp($app); + $appManager->enableApp($app); echo "Enabled application {$app}\n"; } diff --git a/tests/lib/App/AppManagerTest.php b/tests/lib/App/AppManagerTest.php index 9a4716aa8f1..dcaeef90ff5 100644 --- a/tests/lib/App/AppManagerTest.php +++ b/tests/lib/App/AppManagerTest.php @@ -11,12 +11,13 @@ declare(strict_types=1); namespace Test\App; use OC\App\AppManager; +use OC\App\DependencyAnalyzer; +use OC\App\Platform; use OC\AppConfig; use OC\Config\ConfigManager; use OCP\App\AppPathNotFoundException; use OCP\App\Events\AppDisableEvent; use OCP\App\Events\AppEnableEvent; -use OCP\App\IAppManager; use OCP\EventDispatcher\IEventDispatcher; use OCP\ICache; use OCP\ICacheFactory; @@ -96,8 +97,9 @@ class AppManagerTest extends TestCase { protected ServerVersion&MockObject $serverVersion; protected ConfigManager&MockObject $configManager; - /** @var IAppManager */ - protected $manager; + protected DependencyAnalyzer $dependencyAnalyzer; + + protected AppManager $manager; protected function setUp(): void { parent::setUp(); @@ -113,6 +115,7 @@ class AppManagerTest extends TestCase { $this->urlGenerator = $this->createMock(IURLGenerator::class); $this->serverVersion = $this->createMock(ServerVersion::class); $this->configManager = $this->createMock(ConfigManager::class); + $this->dependencyAnalyzer = new DependencyAnalyzer($this->createMock(Platform::class)); $this->overwriteService(AppConfig::class, $this->appConfig); $this->overwriteService(IURLGenerator::class, $this->urlGenerator); @@ -136,6 +139,7 @@ class AppManagerTest extends TestCase { $this->logger, $this->serverVersion, $this->configManager, + $this->dependencyAnalyzer, ); } @@ -275,6 +279,7 @@ class AppManagerTest extends TestCase { $this->logger, $this->serverVersion, $this->configManager, + $this->dependencyAnalyzer, ]) ->onlyMethods([ 'getAppPath', @@ -331,6 +336,7 @@ class AppManagerTest extends TestCase { $this->logger, $this->serverVersion, $this->configManager, + $this->dependencyAnalyzer, ]) ->onlyMethods([ 'getAppPath', @@ -394,6 +400,7 @@ class AppManagerTest extends TestCase { $this->logger, $this->serverVersion, $this->configManager, + $this->dependencyAnalyzer, ]) ->onlyMethods([ 'getAppPath', @@ -474,16 +481,16 @@ class AppManagerTest extends TestCase { 'writable' => false, ]; - $fakeTestAppPath = $fakeAppPath . '/' . 'test-test-app'; + $fakeTestAppPath = $fakeAppPath . '/' . 'test_test_app'; mkdir($fakeTestAppPath); - $generatedAppPath = $this->manager->getAppPath('test-test-app'); + $generatedAppPath = $this->manager->getAppPath('test_test_app'); rmdir($fakeTestAppPath); unlink($fakeAppLink); rmdir($fakeAppPath); - $this->assertEquals($fakeAppLink . '/test-test-app', $generatedAppPath); + $this->assertEquals($fakeAppLink . '/test_test_app', $generatedAppPath); } public function testGetAppPathFail(): void { @@ -589,7 +596,7 @@ class AppManagerTest extends TestCase { } public function testGetAppsNeedingUpgrade(): void { - /** @var AppManager|MockObject $manager */ + /** @var AppManager&MockObject $manager */ $manager = $this->getMockBuilder(AppManager::class) ->setConstructorArgs([ $this->userSession, @@ -600,6 +607,7 @@ class AppManagerTest extends TestCase { $this->logger, $this->serverVersion, $this->configManager, + $this->dependencyAnalyzer, ]) ->onlyMethods(['getAppInfo']) ->getMock(); @@ -661,6 +669,7 @@ class AppManagerTest extends TestCase { $this->logger, $this->serverVersion, $this->configManager, + $this->dependencyAnalyzer, ]) ->onlyMethods(['getAppInfo']) ->getMock(); @@ -801,6 +810,7 @@ class AppManagerTest extends TestCase { $this->logger, $this->serverVersion, $this->configManager, + $this->dependencyAnalyzer, ]) ->onlyMethods([ 'getAppInfo', @@ -833,6 +843,7 @@ class AppManagerTest extends TestCase { $this->logger, $this->serverVersion, $this->configManager, + $this->dependencyAnalyzer, ]) ->onlyMethods([ 'getAppInfo', @@ -864,6 +875,7 @@ class AppManagerTest extends TestCase { $this->logger, $this->serverVersion, $this->configManager, + $this->dependencyAnalyzer, ]) ->onlyMethods([ 'getAppInfo', @@ -884,5 +896,4 @@ class AppManagerTest extends TestCase { $manager->getAppVersion('unknown'), ); } - } diff --git a/tests/lib/App/DependencyAnalyzerTest.php b/tests/lib/App/DependencyAnalyzerTest.php index 88cb6009cc0..9cdcb24b95f 100644 --- a/tests/lib/App/DependencyAnalyzerTest.php +++ b/tests/lib/App/DependencyAnalyzerTest.php @@ -9,18 +9,13 @@ namespace Test\App; use OC\App\DependencyAnalyzer; use OC\App\Platform; -use OCP\IL10N; use Test\TestCase; class DependencyAnalyzerTest extends TestCase { /** @var Platform|\PHPUnit\Framework\MockObject\MockObject */ private $platformMock; - /** @var IL10N */ - private $l10nMock; - - /** @var DependencyAnalyzer */ - private $analyser; + private DependencyAnalyzer $analyser; protected function setUp(): void { $this->platformMock = $this->getMockBuilder(Platform::class) @@ -55,16 +50,7 @@ class DependencyAnalyzerTest extends TestCase { ->method('getOcVersion') ->willReturn('8.0.2'); - $this->l10nMock = $this->getMockBuilder(IL10N::class) - ->disableOriginalConstructor() - ->getMock(); - $this->l10nMock->expects($this->any()) - ->method('t') - ->willReturnCallback(function ($text, $parameters = []) { - return vsprintf($text, $parameters); - }); - - $this->analyser = new DependencyAnalyzer($this->platformMock, $this->l10nMock); + $this->analyser = new DependencyAnalyzer($this->platformMock); } /** @@ -485,4 +471,286 @@ class DependencyAnalyzerTest extends TestCase { [[], '5.4', '5.4', null], ]; } + + public static function appVersionsProvider(): array { + return [ + // exact match + [ + '6.0.0.0', + [ + 'requiremin' => '6.0', + 'requiremax' => '6.0', + ], + true + ], + // in-between match + [ + '6.0.0.0', + [ + 'requiremin' => '5.0', + 'requiremax' => '7.0', + ], + true + ], + // app too old + [ + '6.0.0.0', + [ + 'requiremin' => '5.0', + 'requiremax' => '5.0', + ], + false + ], + // app too new + [ + '5.0.0.0', + [ + 'requiremin' => '6.0', + 'requiremax' => '6.0', + ], + false + ], + // only min specified + [ + '6.0.0.0', + [ + 'requiremin' => '6.0', + ], + true + ], + // only min specified fail + [ + '5.0.0.0', + [ + 'requiremin' => '6.0', + ], + false + ], + // only min specified legacy + [ + '6.0.0.0', + [ + 'require' => '6.0', + ], + true + ], + // only min specified legacy fail + [ + '4.0.0.0', + [ + 'require' => '6.0', + ], + false + ], + // only max specified + [ + '5.0.0.0', + [ + 'requiremax' => '6.0', + ], + true + ], + // only max specified fail + [ + '7.0.0.0', + [ + 'requiremax' => '6.0', + ], + false + ], + // variations of versions + // single OC number + [ + '4', + [ + 'require' => '4.0', + ], + true + ], + // multiple OC number + [ + '4.3.1', + [ + 'require' => '4.3', + ], + true + ], + // single app number + [ + '4', + [ + 'require' => '4', + ], + true + ], + // single app number fail + [ + '4.3', + [ + 'require' => '5', + ], + false + ], + // complex + [ + '5.0.0', + [ + 'require' => '4.5.1', + ], + true + ], + // complex fail + [ + '4.3.1', + [ + 'require' => '4.3.2', + ], + false + ], + // two numbers + [ + '4.3.1', + [ + 'require' => '4.4', + ], + false + ], + // one number fail + [ + '4.3.1', + [ + 'require' => '5', + ], + false + ], + // pre-alpha app + [ + '5.0.3', + [ + 'require' => '4.93', + ], + true + ], + // pre-alpha OC + [ + '6.90.0.2', + [ + 'require' => '6.90', + ], + true + ], + // pre-alpha OC max + [ + '6.90.0.2', + [ + 'requiremax' => '7', + ], + true + ], + // expect same major number match + [ + '5.0.3', + [ + 'require' => '5', + ], + true + ], + // expect same major number match + [ + '5.0.3', + [ + 'requiremax' => '5', + ], + true + ], + // dependencies versions before require* + [ + '6.0.0.0', + [ + 'requiremin' => '5.0', + 'requiremax' => '7.0', + 'dependencies' => [ + 'owncloud' => [ + '@attributes' => [ + 'min-version' => '7.0', + 'max-version' => '7.0', + ], + ], + ], + ], + false + ], + [ + '6.0.0.0', + [ + 'requiremin' => '5.0', + 'requiremax' => '7.0', + 'dependencies' => [ + 'owncloud' => [ + '@attributes' => [ + 'min-version' => '5.0', + 'max-version' => '5.0', + ], + ], + ], + ], + false + ], + [ + '6.0.0.0', + [ + 'requiremin' => '5.0', + 'requiremax' => '5.0', + 'dependencies' => [ + 'owncloud' => [ + '@attributes' => [ + 'min-version' => '5.0', + 'max-version' => '7.0', + ], + ], + ], + ], + true + ], + [ + '9.2.0.0', + [ + 'dependencies' => [ + 'owncloud' => [ + '@attributes' => [ + 'min-version' => '9.0', + 'max-version' => '9.1', + ], + ], + 'nextcloud' => [ + '@attributes' => [ + 'min-version' => '9.1', + 'max-version' => '9.2', + ], + ], + ], + ], + true + ], + [ + '9.2.0.0', + [ + 'dependencies' => [ + 'nextcloud' => [ + '@attributes' => [ + 'min-version' => '9.1', + 'max-version' => '9.2', + ], + ], + ], + ], + true + ], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider('appVersionsProvider')] + public function testServerVersion($ncVersion, $appInfo, $expectedResult): void { + $this->assertEquals($expectedResult, count($this->analyser->analyzeServerVersion($ncVersion, $appInfo, false)) === 0); + } } diff --git a/tests/lib/AppTest.php b/tests/lib/AppTest.php index c56e31aadf8..71186f5ffb0 100644 --- a/tests/lib/AppTest.php +++ b/tests/lib/AppTest.php @@ -9,6 +9,7 @@ namespace Test; use OC\App\AppManager; +use OC\App\DependencyAnalyzer; use OC\AppConfig; use OC\Config\ConfigManager; use OCP\EventDispatcher\IEventDispatcher; @@ -36,288 +37,6 @@ class AppTest extends \Test\TestCase { public const TEST_GROUP1 = 'group1'; public const TEST_GROUP2 = 'group2'; - public static function appVersionsProvider(): array { - return [ - // exact match - [ - '6.0.0.0', - [ - 'requiremin' => '6.0', - 'requiremax' => '6.0', - ], - true - ], - // in-between match - [ - '6.0.0.0', - [ - 'requiremin' => '5.0', - 'requiremax' => '7.0', - ], - true - ], - // app too old - [ - '6.0.0.0', - [ - 'requiremin' => '5.0', - 'requiremax' => '5.0', - ], - false - ], - // app too new - [ - '5.0.0.0', - [ - 'requiremin' => '6.0', - 'requiremax' => '6.0', - ], - false - ], - // only min specified - [ - '6.0.0.0', - [ - 'requiremin' => '6.0', - ], - true - ], - // only min specified fail - [ - '5.0.0.0', - [ - 'requiremin' => '6.0', - ], - false - ], - // only min specified legacy - [ - '6.0.0.0', - [ - 'require' => '6.0', - ], - true - ], - // only min specified legacy fail - [ - '4.0.0.0', - [ - 'require' => '6.0', - ], - false - ], - // only max specified - [ - '5.0.0.0', - [ - 'requiremax' => '6.0', - ], - true - ], - // only max specified fail - [ - '7.0.0.0', - [ - 'requiremax' => '6.0', - ], - false - ], - // variations of versions - // single OC number - [ - '4', - [ - 'require' => '4.0', - ], - true - ], - // multiple OC number - [ - '4.3.1', - [ - 'require' => '4.3', - ], - true - ], - // single app number - [ - '4', - [ - 'require' => '4', - ], - true - ], - // single app number fail - [ - '4.3', - [ - 'require' => '5', - ], - false - ], - // complex - [ - '5.0.0', - [ - 'require' => '4.5.1', - ], - true - ], - // complex fail - [ - '4.3.1', - [ - 'require' => '4.3.2', - ], - false - ], - // two numbers - [ - '4.3.1', - [ - 'require' => '4.4', - ], - false - ], - // one number fail - [ - '4.3.1', - [ - 'require' => '5', - ], - false - ], - // pre-alpha app - [ - '5.0.3', - [ - 'require' => '4.93', - ], - true - ], - // pre-alpha OC - [ - '6.90.0.2', - [ - 'require' => '6.90', - ], - true - ], - // pre-alpha OC max - [ - '6.90.0.2', - [ - 'requiremax' => '7', - ], - true - ], - // expect same major number match - [ - '5.0.3', - [ - 'require' => '5', - ], - true - ], - // expect same major number match - [ - '5.0.3', - [ - 'requiremax' => '5', - ], - true - ], - // dependencies versions before require* - [ - '6.0.0.0', - [ - 'requiremin' => '5.0', - 'requiremax' => '7.0', - 'dependencies' => [ - 'owncloud' => [ - '@attributes' => [ - 'min-version' => '7.0', - 'max-version' => '7.0', - ], - ], - ], - ], - false - ], - [ - '6.0.0.0', - [ - 'requiremin' => '5.0', - 'requiremax' => '7.0', - 'dependencies' => [ - 'owncloud' => [ - '@attributes' => [ - 'min-version' => '5.0', - 'max-version' => '5.0', - ], - ], - ], - ], - false - ], - [ - '6.0.0.0', - [ - 'requiremin' => '5.0', - 'requiremax' => '5.0', - 'dependencies' => [ - 'owncloud' => [ - '@attributes' => [ - 'min-version' => '5.0', - 'max-version' => '7.0', - ], - ], - ], - ], - true - ], - [ - '9.2.0.0', - [ - 'dependencies' => [ - 'owncloud' => [ - '@attributes' => [ - 'min-version' => '9.0', - 'max-version' => '9.1', - ], - ], - 'nextcloud' => [ - '@attributes' => [ - 'min-version' => '9.1', - 'max-version' => '9.2', - ], - ], - ], - ], - true - ], - [ - '9.2.0.0', - [ - 'dependencies' => [ - 'nextcloud' => [ - '@attributes' => [ - 'min-version' => '9.1', - 'max-version' => '9.2', - ], - ], - ], - ], - true - ], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('appVersionsProvider')] - public function testIsAppCompatible($ocVersion, $appInfo, $expectedResult): void { - $this->assertEquals($expectedResult, \OC_App::isAppCompatible($ocVersion, $appInfo)); - } - /** * Tests that the app order is correct */ @@ -572,6 +291,7 @@ class AppTest extends \Test\TestCase { Server::get(LoggerInterface::class), Server::get(ServerVersion::class), Server::get(ConfigManager::class), + Server::get(DependencyAnalyzer::class), )); } diff --git a/tests/lib/DB/MigrationsTest.php b/tests/lib/DB/MigrationsTest.php index 2b39b26d852..2116678baa1 100644 --- a/tests/lib/DB/MigrationsTest.php +++ b/tests/lib/DB/MigrationsTest.php @@ -20,6 +20,7 @@ use OC\DB\Connection; use OC\DB\MigrationService; use OC\DB\SchemaWrapper; use OC\Migration\MetadataManager; +use OCP\App\AppPathNotFoundException; use OCP\App\IAppManager; use OCP\IDBConnection; use OCP\Migration\Attributes\AddColumn; @@ -81,10 +82,10 @@ class MigrationsTest extends \Test\TestCase { public function testUnknownApp(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('App not found'); + $this->expectException(AppPathNotFoundException::class); + $this->expectExceptionMessage('Could not find path for unknown_bloody_app'); - $migrationService = new MigrationService('unknown-bloody-app', $this->db); + $migrationService = new MigrationService('unknown_bloody_app', $this->db); } diff --git a/tests/lib/InstallerTest.php b/tests/lib/InstallerTest.php index 06163cdaedb..e763807be29 100644 --- a/tests/lib/InstallerTest.php +++ b/tests/lib/InstallerTest.php @@ -16,7 +16,9 @@ use OCP\Http\Client\IClient; use OCP\Http\Client\IClientService; use OCP\IConfig; use OCP\ITempManager; +use OCP\L10N\IFactory; use OCP\Server; +use PHPUnit\Framework\MockObject\MockObject; use Psr\Log\LoggerInterface; /** @@ -38,6 +40,8 @@ class InstallerTest extends TestCase { private $logger; /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */ private $config; + private IAppManager&MockObject $appManager; + private IFactory&MockObject $l10nFactory; protected function setUp(): void { parent::setUp(); @@ -47,18 +51,13 @@ class InstallerTest extends TestCase { $this->tempManager = $this->createMock(ITempManager::class); $this->logger = $this->createMock(LoggerInterface::class); $this->config = $this->createMock(IConfig::class); + $this->appManager = $this->createMock(IAppManager::class); + $this->l10nFactory = $this->createMock(IFactory::class); $config = Server::get(IConfig::class); $this->appstore = $config->setSystemValue('appstoreenabled', true); $config->setSystemValue('appstoreenabled', true); - $installer = new Installer( - Server::get(AppFetcher::class), - Server::get(IClientService::class), - Server::get(ITempManager::class), - Server::get(LoggerInterface::class), - $config, - false - ); + $installer = Server::get(Installer::class); $installer->removeApp(self::$appid); } @@ -69,19 +68,14 @@ class InstallerTest extends TestCase { $this->tempManager, $this->logger, $this->config, + $this->appManager, + $this->l10nFactory, false ); } protected function tearDown(): void { - $installer = new Installer( - Server::get(AppFetcher::class), - Server::get(IClientService::class), - Server::get(ITempManager::class), - Server::get(LoggerInterface::class), - Server::get(IConfig::class), - false - ); + $installer = Server::get(Installer::class); $installer->removeApp(self::$appid); Server::get(IConfig::class)->setSystemValue('appstoreenabled', $this->appstore); @@ -93,14 +87,7 @@ class InstallerTest extends TestCase { Server::get(IAppManager::class)->getAppVersion('testapp', true); // Build installer - $installer = new Installer( - Server::get(AppFetcher::class), - Server::get(IClientService::class), - Server::get(ITempManager::class), - Server::get(LoggerInterface::class), - Server::get(IConfig::class), - false - ); + $installer = Server::get(Installer::class); // Extract app $pathOfTestApp = __DIR__ . '/../data/testapp.zip'; @@ -158,6 +145,10 @@ class InstallerTest extends TestCase { ->expects($this->once()) ->method('get') ->willReturn($appArray); + $this->appManager + ->expects($this->exactly(2)) + ->method('getAppVersion') + ->willReturn('1.0'); $installer = $this->getInstaller(); $this->assertSame($updateAvailable, $installer->isUpdateAvailable('files')); @@ -700,6 +691,11 @@ JXhrdaWDZ8fzpUjugrtC3qslsqL0dzgU37anS3HwrT8=', $this->assertTrue(file_exists(__DIR__ . '/../../apps/testapp/appinfo/info.xml')); $this->assertEquals('0.9', \OC_App::getAppVersionByPath(__DIR__ . '/../../apps/testapp/')); + $this->appManager + ->expects($this->once()) + ->method('getAppVersion') + ->willReturn('0.9'); + $installer = $this->getInstaller(); $installer->downloadApp('testapp'); $this->assertTrue(file_exists(__DIR__ . '/../../apps/testapp/appinfo/info.xml')); diff --git a/tests/lib/IntegrityCheck/CheckerTest.php b/tests/lib/IntegrityCheck/CheckerTest.php index a8a2596a3d8..823258cab10 100644 --- a/tests/lib/IntegrityCheck/CheckerTest.php +++ b/tests/lib/IntegrityCheck/CheckerTest.php @@ -11,7 +11,6 @@ namespace Test\IntegrityCheck; use OC\Core\Command\Maintenance\Mimetype\GenerateMimetypeFileBuilder; use OC\Files\Type\Detection; use OC\IntegrityCheck\Checker; -use OC\IntegrityCheck\Helpers\AppLocator; use OC\IntegrityCheck\Helpers\EnvironmentHelper; use OC\IntegrityCheck\Helpers\FileAccessHelper; use OC\Memcache\NullCache; @@ -29,10 +28,6 @@ class CheckerTest extends TestCase { private $serverVersion; /** @var EnvironmentHelper|\PHPUnit\Framework\MockObject\MockObject */ private $environmentHelper; - /** @var AppLocator|\PHPUnit\Framework\MockObject\MockObject */ - private $appLocator; - /** @var Checker */ - private $checker; /** @var FileAccessHelper|\PHPUnit\Framework\MockObject\MockObject */ private $fileAccessHelper; /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */ @@ -46,12 +41,13 @@ class CheckerTest extends TestCase { /** @var \OC\Files\Type\Detection|\PHPUnit\Framework\MockObject\MockObject */ private $mimeTypeDetector; + private Checker $checker; + protected function setUp(): void { parent::setUp(); $this->serverVersion = $this->createMock(ServerVersion::class); $this->environmentHelper = $this->createMock(EnvironmentHelper::class); $this->fileAccessHelper = $this->createMock(FileAccessHelper::class); - $this->appLocator = $this->createMock(AppLocator::class); $this->config = $this->createMock(IConfig::class); $this->appConfig = $this->createMock(IAppConfig::class); $this->cacheFactory = $this->createMock(ICacheFactory::class); @@ -71,7 +67,6 @@ class CheckerTest extends TestCase { $this->serverVersion, $this->environmentHelper, $this->fileAccessHelper, - $this->appLocator, $this->config, $this->appConfig, $this->cacheFactory, @@ -186,7 +181,7 @@ class CheckerTest extends TestCase { ->with('integrity.check.disabled', false) ->willReturn(false); - $this->appLocator + $this->appManager ->expects($this->once()) ->method('getAppPath') ->with('SomeApp') @@ -221,7 +216,7 @@ class CheckerTest extends TestCase { ->with('integrity.check.disabled', false) ->willReturn(false); - $this->appLocator + $this->appManager ->expects($this->once()) ->method('getAppPath') ->with('SomeApp') @@ -262,7 +257,7 @@ class CheckerTest extends TestCase { ->with('integrity.check.disabled', false) ->willReturn(false); - $this->appLocator + $this->appManager ->expects($this->once()) ->method('getAppPath') ->with('SomeApp') @@ -319,7 +314,7 @@ class CheckerTest extends TestCase { ->with('integrity.check.disabled', false) ->willReturn(false); - $this->appLocator + $this->appManager ->expects($this->never()) ->method('getAppPath') ->with('SomeApp'); @@ -374,7 +369,7 @@ class CheckerTest extends TestCase { ->with('integrity.check.disabled', false) ->willReturn(false); - $this->appLocator + $this->appManager ->expects($this->once()) ->method('getAppPath') ->with('SomeApp') @@ -415,7 +410,7 @@ class CheckerTest extends TestCase { ->with('integrity.check.disabled', false) ->willReturn(false); - $this->appLocator + $this->appManager ->expects($this->once()) ->method('getAppPath') ->with('SomeApp') @@ -984,7 +979,6 @@ class CheckerTest extends TestCase { $this->serverVersion, $this->environmentHelper, $this->fileAccessHelper, - $this->appLocator, $this->config, $this->appConfig, $this->cacheFactory, @@ -1032,7 +1026,7 @@ class CheckerTest extends TestCase { $this->assertSame($expected, $app); return []; }); - $this->appLocator + $this->appManager ->expects($this->exactly(2)) ->method('getAppPath') ->willReturnMap([ diff --git a/tests/lib/IntegrityCheck/Helpers/AppLocatorTest.php b/tests/lib/IntegrityCheck/Helpers/AppLocatorTest.php deleted file mode 100644 index 837b397ac1f..00000000000 --- a/tests/lib/IntegrityCheck/Helpers/AppLocatorTest.php +++ /dev/null @@ -1,34 +0,0 @@ -locator = new AppLocator(); - } - - public function testGetAppPath(): void { - $this->assertSame(\OC_App::getAppPath('files'), $this->locator->getAppPath('files')); - } - - - public function testGetAppPathNotExistentApp(): void { - $this->expectException(\Exception::class); - $this->expectExceptionMessage('App not found'); - - $this->locator->getAppPath('aTotallyNotExistingApp'); - } -} diff --git a/tests/lib/Template/CSSResourceLocatorTest.php b/tests/lib/Template/CSSResourceLocatorTest.php index 2ae37999b32..fc82c9c3e57 100644 --- a/tests/lib/Template/CSSResourceLocatorTest.php +++ b/tests/lib/Template/CSSResourceLocatorTest.php @@ -87,7 +87,7 @@ class CSSResourceLocatorTest extends \Test\TestCase { symlink($apps_dirname, $new_apps_path_symlink); // Create an app within that path - mkdir($new_apps_path . '/' . 'test-css-app'); + mkdir($new_apps_path . '/' . 'test_css_app'); // Use the symlink as the app path \OC::$APPSROOTS[] = [ @@ -97,7 +97,7 @@ class CSSResourceLocatorTest extends \Test\TestCase { ]; $locator = $this->cssResourceLocator(); - $locator->find(['test-css-app/test-file']); + $locator->find(['test_css_app/test-file']); $resources = $locator->getResources(); $this->assertCount(1, $resources); @@ -107,8 +107,8 @@ class CSSResourceLocatorTest extends \Test\TestCase { $webRoot = $resource[1]; $file = $resource[2]; - $expectedRoot = $new_apps_path . '/test-css-app'; - $expectedWebRoot = \OC::$WEBROOT . '/css-apps-test/test-css-app'; + $expectedRoot = $new_apps_path . '/test_css_app'; + $expectedWebRoot = \OC::$WEBROOT . '/css-apps-test/test_css_app'; $expectedFile = 'test-file.css'; $this->assertEquals($expectedRoot, $root, diff --git a/tests/lib/UpdaterTest.php b/tests/lib/UpdaterTest.php index 37a4a105628..23a4d5329c8 100644 --- a/tests/lib/UpdaterTest.php +++ b/tests/lib/UpdaterTest.php @@ -11,6 +11,7 @@ namespace Test; use OC\Installer; use OC\IntegrityCheck\Checker; use OC\Updater; +use OCP\App\IAppManager; use OCP\IAppConfig; use OCP\IConfig; use OCP\ServerVersion; @@ -32,6 +33,7 @@ class UpdaterTest extends TestCase { private $checker; /** @var Installer|MockObject */ private $installer; + private IAppManager&MockObject $appManager; protected function setUp(): void { parent::setUp(); @@ -41,6 +43,7 @@ class UpdaterTest extends TestCase { $this->logger = $this->createMock(LoggerInterface::class); $this->checker = $this->createMock(Checker::class); $this->installer = $this->createMock(Installer::class); + $this->appManager = $this->createMock(IAppManager::class); $this->updater = new Updater( $this->serverVersion, @@ -48,7 +51,8 @@ class UpdaterTest extends TestCase { $this->appConfig, $this->checker, $this->logger, - $this->installer + $this->installer, + $this->appManager, ); }