Merge pull request #53895 from nextcloud/fix/cleanup-updater-class

feat/provide-server-version-capability
Kate 2025-08-19 17:40:15 +07:00 committed by GitHub
commit 4edfef4dd5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 769 additions and 894 deletions

@ -14,7 +14,6 @@ use OC\App\AppStore\Fetcher\AppFetcher;
use OC\App\AppStore\Fetcher\CategoryFetcher; use OC\App\AppStore\Fetcher\CategoryFetcher;
use OC\App\AppStore\Version\VersionParser; use OC\App\AppStore\Version\VersionParser;
use OC\App\DependencyAnalyzer; use OC\App\DependencyAnalyzer;
use OC\App\Platform;
use OC\Installer; use OC\Installer;
use OCA\AppAPI\Service\ExAppsPageService; use OCA\AppAPI\Service\ExAppsPageService;
use OCP\App\AppPathNotFoundException; use OCP\App\AppPathNotFoundException;
@ -361,7 +360,7 @@ class AppSettingsController extends Controller {
$this->fetchApps(); $this->fetchApps();
$apps = $this->getAllApps(); $apps = $this->getAllApps();
$dependencyAnalyzer = new DependencyAnalyzer(new Platform($this->config), $this->l10n); $dependencyAnalyzer = Server::get(DependencyAnalyzer::class);
$ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []); $ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
if (!is_array($ignoreMaxApps)) { if (!is_array($ignoreMaxApps)) {
@ -568,24 +567,18 @@ class AppSettingsController extends Controller {
$appId = $this->appManager->cleanAppId($appId); $appId = $this->appManager->cleanAppId($appId);
// Check if app is already downloaded // Check if app is already downloaded
/** @var Installer $installer */ if (!$this->installer->isDownloaded($appId)) {
$installer = Server::get(Installer::class); $this->installer->downloadApp($appId);
$isDownloaded = $installer->isDownloaded($appId);
if (!$isDownloaded) {
$installer->downloadApp($appId);
} }
$installer->installApp($appId); $this->installer->installApp($appId);
if (count($groups) > 0) { if (count($groups) > 0) {
$this->appManager->enableAppForGroups($appId, $this->getGroupList($groups)); $this->appManager->enableAppForGroups($appId, $this->getGroupList($groups));
} else { } else {
$this->appManager->enableApp($appId); $this->appManager->enableApp($appId);
} }
if (\OC_App::shouldUpgrade($appId)) { $updateRequired = $updateRequired || $this->appManager->isUpgradeRequired($appId);
$updateRequired = true;
}
} }
return new JSONResponse(['data' => ['update_required' => $updateRequired]]); return new JSONResponse(['data' => ['update_required' => $updateRequired]]);
} catch (\Throwable $e) { } catch (\Throwable $e) {

@ -3317,14 +3317,6 @@
<code><![CDATA[ActivitySettings[]]]></code> <code><![CDATA[ActivitySettings[]]]></code>
</MoreSpecificReturnType> </MoreSpecificReturnType>
</file> </file>
<file src="lib/private/App/DependencyAnalyzer.php">
<InvalidNullableReturnType>
<code><![CDATA[bool]]></code>
</InvalidNullableReturnType>
<NullableReturnStatement>
<code><![CDATA[version_compare($first, $second, $operator)]]></code>
</NullableReturnStatement>
</file>
<file src="lib/private/AppFramework/Bootstrap/FunctionInjector.php"> <file src="lib/private/AppFramework/Bootstrap/FunctionInjector.php">
<UndefinedMethod> <UndefinedMethod>
<code><![CDATA[getName]]></code> <code><![CDATA[getName]]></code>
@ -4011,12 +4003,6 @@
<code><![CDATA[query]]></code> <code><![CDATA[query]]></code>
</UndefinedInterfaceMethod> </UndefinedInterfaceMethod>
</file> </file>
<file src="lib/private/Installer.php">
<InvalidArgument>
<code><![CDATA[false]]></code>
<code><![CDATA[false]]></code>
</InvalidArgument>
</file>
<file src="lib/private/IntegrityCheck/Checker.php"> <file src="lib/private/IntegrityCheck/Checker.php">
<InvalidArrayAccess> <InvalidArrayAccess>
<code><![CDATA[$x509->getDN(X509::DN_OPENSSL)['CN']]]></code> <code><![CDATA[$x509->getDN(X509::DN_OPENSSL)['CN']]]></code>

@ -5,11 +5,11 @@
* SPDX-FileCopyrightText: 2016 ownCloud, Inc. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
namespace OC\Core\Command\Integrity; namespace OC\Core\Command\Integrity;
use OC\Core\Command\Base; use OC\Core\Command\Base;
use OC\IntegrityCheck\Checker; use OC\IntegrityCheck\Checker;
use OC\IntegrityCheck\Helpers\AppLocator;
use OC\IntegrityCheck\Helpers\FileAccessHelper; use OC\IntegrityCheck\Helpers\FileAccessHelper;
use OCP\App\IAppManager; use OCP\App\IAppManager;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
@ -25,7 +25,6 @@ use Symfony\Component\Console\Output\OutputInterface;
class CheckApp extends Base { class CheckApp extends Base {
public function __construct( public function __construct(
private Checker $checker, private Checker $checker,
private AppLocator $appLocator,
private FileAccessHelper $fileAccessHelper, private FileAccessHelper $fileAccessHelper,
private IAppManager $appManager, private IAppManager $appManager,
) { ) {
@ -70,7 +69,7 @@ class CheckApp extends Base {
foreach ($appIds as $appId) { foreach ($appIds as $appId) {
$path = (string)$input->getOption('path'); $path = (string)$input->getOption('path');
if ($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')) { if ($this->appManager->isShipped($appId) || $this->fileAccessHelper->file_exists($path . '/appinfo/signature.json')) {

@ -7,8 +7,6 @@
*/ */
use OC\Core\Listener\FeedBackHandler; use OC\Core\Listener\FeedBackHandler;
use OC\DB\MigratorExecuteSqlEvent; use OC\DB\MigratorExecuteSqlEvent;
use OC\Installer;
use OC\IntegrityCheck\Checker;
use OC\Repair\Events\RepairAdvanceEvent; use OC\Repair\Events\RepairAdvanceEvent;
use OC\Repair\Events\RepairErrorEvent; use OC\Repair\Events\RepairErrorEvent;
use OC\Repair\Events\RepairFinishEvent; use OC\Repair\Events\RepairFinishEvent;
@ -20,13 +18,11 @@ use OC\SystemConfig;
use OC\Updater; use OC\Updater;
use OCP\EventDispatcher\Event; use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventDispatcher; use OCP\EventDispatcher\IEventDispatcher;
use OCP\IAppConfig;
use OCP\IConfig; use OCP\IConfig;
use OCP\IEventSourceFactory; use OCP\IEventSourceFactory;
use OCP\IL10N; use OCP\IL10N;
use OCP\L10N\IFactory; use OCP\L10N\IFactory;
use OCP\Server; use OCP\Server;
use OCP\ServerVersion;
use OCP\Util; use OCP\Util;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@ -58,14 +54,7 @@ if (Util::needUpgrade()) {
\OC_User::setIncognitoMode(true); \OC_User::setIncognitoMode(true);
$config = Server::get(IConfig::class); $config = Server::get(IConfig::class);
$updater = new Updater( $updater = Server::get(Updater::class);
Server::get(ServerVersion::class),
$config,
Server::get(IAppConfig::class),
Server::get(Checker::class),
Server::get(LoggerInterface::class),
Server::get(Installer::class)
);
$incompatibleApps = []; $incompatibleApps = [];
$incompatibleOverwrites = $config->getSystemValue('app_install_overwrite', []); $incompatibleOverwrites = $config->getSystemValue('app_install_overwrite', []);

@ -1790,7 +1790,6 @@ return array(
'OC\\Installer' => $baseDir . '/lib/private/Installer.php', 'OC\\Installer' => $baseDir . '/lib/private/Installer.php',
'OC\\IntegrityCheck\\Checker' => $baseDir . '/lib/private/IntegrityCheck/Checker.php', 'OC\\IntegrityCheck\\Checker' => $baseDir . '/lib/private/IntegrityCheck/Checker.php',
'OC\\IntegrityCheck\\Exceptions\\InvalidSignatureException' => $baseDir . '/lib/private/IntegrityCheck/Exceptions/InvalidSignatureException.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\\EnvironmentHelper' => $baseDir . '/lib/private/IntegrityCheck/Helpers/EnvironmentHelper.php',
'OC\\IntegrityCheck\\Helpers\\FileAccessHelper' => $baseDir . '/lib/private/IntegrityCheck/Helpers/FileAccessHelper.php', 'OC\\IntegrityCheck\\Helpers\\FileAccessHelper' => $baseDir . '/lib/private/IntegrityCheck/Helpers/FileAccessHelper.php',
'OC\\IntegrityCheck\\Iterator\\ExcludeFileByNameFilterIterator' => $baseDir . '/lib/private/IntegrityCheck/Iterator/ExcludeFileByNameFilterIterator.php', 'OC\\IntegrityCheck\\Iterator\\ExcludeFileByNameFilterIterator' => $baseDir . '/lib/private/IntegrityCheck/Iterator/ExcludeFileByNameFilterIterator.php',

@ -1831,7 +1831,6 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Installer' => __DIR__ . '/../../..' . '/lib/private/Installer.php', 'OC\\Installer' => __DIR__ . '/../../..' . '/lib/private/Installer.php',
'OC\\IntegrityCheck\\Checker' => __DIR__ . '/../../..' . '/lib/private/IntegrityCheck/Checker.php', 'OC\\IntegrityCheck\\Checker' => __DIR__ . '/../../..' . '/lib/private/IntegrityCheck/Checker.php',
'OC\\IntegrityCheck\\Exceptions\\InvalidSignatureException' => __DIR__ . '/../../..' . '/lib/private/IntegrityCheck/Exceptions/InvalidSignatureException.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\\EnvironmentHelper' => __DIR__ . '/../../..' . '/lib/private/IntegrityCheck/Helpers/EnvironmentHelper.php',
'OC\\IntegrityCheck\\Helpers\\FileAccessHelper' => __DIR__ . '/../../..' . '/lib/private/IntegrityCheck/Helpers/FileAccessHelper.php', 'OC\\IntegrityCheck\\Helpers\\FileAccessHelper' => __DIR__ . '/../../..' . '/lib/private/IntegrityCheck/Helpers/FileAccessHelper.php',
'OC\\IntegrityCheck\\Iterator\\ExcludeFileByNameFilterIterator' => __DIR__ . '/../../..' . '/lib/private/IntegrityCheck/Iterator/ExcludeFileByNameFilterIterator.php', 'OC\\IntegrityCheck\\Iterator\\ExcludeFileByNameFilterIterator' => __DIR__ . '/../../..' . '/lib/private/IntegrityCheck/Iterator/ExcludeFileByNameFilterIterator.php',

@ -10,12 +10,15 @@ namespace OC\App;
use OC\AppConfig; use OC\AppConfig;
use OC\AppFramework\Bootstrap\Coordinator; use OC\AppFramework\Bootstrap\Coordinator;
use OC\Config\ConfigManager; use OC\Config\ConfigManager;
use OC\DB\MigrationService;
use OCP\Activity\IManager as IActivityManager; use OCP\Activity\IManager as IActivityManager;
use OCP\App\AppPathNotFoundException; use OCP\App\AppPathNotFoundException;
use OCP\App\Events\AppDisableEvent; use OCP\App\Events\AppDisableEvent;
use OCP\App\Events\AppEnableEvent; use OCP\App\Events\AppEnableEvent;
use OCP\App\Events\AppUpdateEvent;
use OCP\App\IAppManager; use OCP\App\IAppManager;
use OCP\App\ManagerEvent; use OCP\App\ManagerEvent;
use OCP\BackgroundJob\IJobList;
use OCP\Collaboration\AutoComplete\IManager as IAutoCompleteManager; use OCP\Collaboration\AutoComplete\IManager as IAutoCompleteManager;
use OCP\Collaboration\Collaborators\ISearch as ICollaboratorSearch; use OCP\Collaboration\Collaborators\ISearch as ICollaboratorSearch;
use OCP\Diagnostics\IEventLogger; use OCP\Diagnostics\IEventLogger;
@ -86,6 +89,7 @@ class AppManager implements IAppManager {
private LoggerInterface $logger, private LoggerInterface $logger,
private ServerVersion $serverVersion, private ServerVersion $serverVersion,
private ConfigManager $configManager, private ConfigManager $configManager,
private DependencyAnalyzer $dependencyAnalyzer,
) { ) {
} }
@ -249,9 +253,14 @@ class AppManager implements IAppManager {
foreach ($apps as $app) { foreach ($apps as $app) {
// If the app is already loaded then autoloading it makes no sense // If the app is already loaded then autoloading it makes no sense
if (!$this->isAppLoaded($app)) { if (!$this->isAppLoaded($app)) {
$path = \OC_App::getAppPath($app); try {
if ($path !== false) { $path = $this->getAppPath($app);
\OC_App::registerAutoloading($app, $path); \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; return;
} }
$this->loadedApps[$app] = true; $this->loadedApps[$app] = true;
$appPath = \OC_App::getAppPath($app); try {
if ($appPath === false) { $appPath = $this->getAppPath($app);
} catch (AppPathNotFoundException $e) {
$this->logger->info('Error during app loading: ' . $e->getMessage(), [
'exception' => $e,
'app' => $app,
]);
return; return;
} }
$eventLogger = \OC::$server->get(IEventLogger::class); $eventLogger = \OC::$server->get(IEventLogger::class);
@ -675,29 +689,87 @@ class AppManager implements IAppManager {
/** /**
* Get the directory for the given app. * Get the directory for the given app.
* *
* @psalm-taint-specialize
*
* @throws AppPathNotFoundException if app folder can't be found * @throws AppPathNotFoundException if app folder can't be found
*/ */
public function getAppPath(string $appId): string { public function getAppPath(string $appId, bool $ignoreCache = false): string {
$appPath = \OC_App::getAppPath($appId); $appId = $this->cleanAppId($appId);
if ($appPath === false) { if ($appId === '') {
throw new AppPathNotFoundException('Could not find path for ' . $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. * Get the web path for the given app.
* *
* @param string $appId
* @return string
* @throws AppPathNotFoundException if app path can't be found * @throws AppPathNotFoundException if app path can't be found
*/ */
public function getAppWebPath(string $appId): string { public function getAppWebPath(string $appId): string {
$appWebPath = \OC_App::getAppWebPath($appId); if (($dir = $this->findAppInDirectories($appId)) != false) {
if ($appWebPath === false) { return \OC::$WEBROOT . $dir['url'] . '/' . $appId;
throw new AppPathNotFoundException('Could not find web path for ' . $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 if ($appDbVersion
&& isset($appInfo['version']) && isset($appInfo['version'])
&& version_compare($appInfo['version'], $appDbVersion, '>') && version_compare($appInfo['version'], $appDbVersion, '>')
&& \OC_App::isAppCompatible($version, $appInfo) && $this->isAppCompatible($version, $appInfo)
) { ) {
$appsToUpgrade[] = $appInfo; $appsToUpgrade[] = $appInfo;
} }
@ -814,7 +886,7 @@ class AppManager implements IAppManager {
$info = $this->getAppInfo($appId); $info = $this->getAppInfo($appId);
if ($info === null) { if ($info === null) {
$incompatibleApps[] = ['id' => $appId, 'name' => $appId]; $incompatibleApps[] = ['id' => $appId, 'name' => $appId];
} elseif (!\OC_App::isAppCompatible($version, $info)) { } elseif (!$this->isAppCompatible($version, $info)) {
$incompatibleApps[] = $info; $incompatibleApps[] = $info;
} }
} }
@ -949,4 +1021,94 @@ class AppManager implements IAppManager {
/* Only lowercase alphanumeric is allowed */ /* Only lowercase alphanumeric is allowed */
return preg_replace('/(^[0-9_]|[^a-z0-9_]+|_$)/', '', $app); 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;
}
} }

@ -11,20 +11,29 @@ declare(strict_types=1);
namespace OC\App; namespace OC\App;
use OCP\IL10N; use OCP\IL10N;
use OCP\L10N\IFactory;
use OCP\Server;
class DependencyAnalyzer { class DependencyAnalyzer {
// Cannot be injected because when this class is built IAppManager is not available yet
private ?IL10N $l = null;
public function __construct( public function __construct(
private Platform $platform, 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 * @return array of missing dependencies
*/ */
public function analyze(array $app, bool $ignoreMax = false): array { public function analyze(array $appInfo, bool $ignoreMax = false): array {
if (isset($app['dependencies'])) { if (isset($appInfo['dependencies'])) {
$dependencies = $app['dependencies']; $dependencies = $appInfo['dependencies'];
} else { } else {
$dependencies = []; $dependencies = [];
} }
@ -36,18 +45,12 @@ class DependencyAnalyzer {
$this->analyzeCommands($dependencies), $this->analyzeCommands($dependencies),
$this->analyzeLibraries($dependencies), $this->analyzeLibraries($dependencies),
$this->analyzeOS($dependencies), $this->analyzeOS($dependencies),
$this->analyzeOC($dependencies, $app, $ignoreMax) $this->analyzeServer($appInfo, $ignoreMax),
); );
} }
public function isMarkedCompatible(array $app): bool { public function isMarkedCompatible(array $appInfo): bool {
if (isset($app['dependencies'])) { $maxVersion = $this->getMaxVersion($appInfo);
$dependencies = $app['dependencies'];
} else {
$dependencies = [];
}
$maxVersion = $this->getMaxVersion($dependencies, $app);
if ($maxVersion === null) { if ($maxVersion === null) {
return true; return true;
} }
@ -76,6 +79,7 @@ class DependencyAnalyzer {
/** /**
* Parameters will be normalized and then passed into version_compare * Parameters will be normalized and then passed into version_compare
* in the same order they are specified in the method header * 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 * @return bool result similar to version_compare
*/ */
private function compare(string $first, string $second, string $operator): bool { private function compare(string $first, string $second, string $operator): bool {
@ -105,19 +109,19 @@ class DependencyAnalyzer {
if (isset($dependencies['php']['@attributes']['min-version'])) { if (isset($dependencies['php']['@attributes']['min-version'])) {
$minVersion = $dependencies['php']['@attributes']['min-version']; $minVersion = $dependencies['php']['@attributes']['min-version'];
if ($this->compareSmaller($this->platform->getPhpVersion(), $minVersion)) { 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'])) { if (isset($dependencies['php']['@attributes']['max-version'])) {
$maxVersion = $dependencies['php']['@attributes']['max-version']; $maxVersion = $dependencies['php']['@attributes']['max-version'];
if ($this->compareBigger($this->platform->getPhpVersion(), $maxVersion)) { 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'])) { if (isset($dependencies['php']['@attributes']['min-int-size'])) {
$intSize = $dependencies['php']['@attributes']['min-int-size']; $intSize = $dependencies['php']['@attributes']['min-int-size'];
if ($intSize > $this->platform->getIntSize() * 8) { 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; return $missing;
@ -141,7 +145,7 @@ class DependencyAnalyzer {
}, $supportedArchitectures); }, $supportedArchitectures);
$currentArchitecture = $this->platform->getArchitecture(); $currentArchitecture = $this->platform->getArchitecture();
if (!in_array($currentArchitecture, $supportedArchitectures, true)) { 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; return $missing;
} }
@ -167,7 +171,7 @@ class DependencyAnalyzer {
}, $supportedDatabases); }, $supportedDatabases);
$currentDatabase = $this->platform->getDatabase(); $currentDatabase = $this->platform->getDatabase();
if (!in_array($currentDatabase, $supportedDatabases)) { 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; return $missing;
} }
@ -192,7 +196,7 @@ class DependencyAnalyzer {
} }
$commandName = $this->getValue($command); $commandName = $this->getValue($command);
if (!$this->platform->isCommandKnown($commandName)) { 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; return $missing;
@ -215,7 +219,7 @@ class DependencyAnalyzer {
$libName = $this->getValue($lib); $libName = $this->getValue($lib);
$libVersion = $this->platform->getLibraryVersion($libName); $libVersion = $this->platform->getLibraryVersion($libName);
if (is_null($libVersion)) { 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; continue;
} }
@ -223,14 +227,14 @@ class DependencyAnalyzer {
if (isset($lib['@attributes']['min-version'])) { if (isset($lib['@attributes']['min-version'])) {
$minVersion = $lib['@attributes']['min-version']; $minVersion = $lib['@attributes']['min-version'];
if ($this->compareSmaller($libVersion, $minVersion)) { 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]); [$libName, $minVersion, $libVersion]);
} }
} }
if (isset($lib['@attributes']['max-version'])) { if (isset($lib['@attributes']['max-version'])) {
$maxVersion = $lib['@attributes']['max-version']; $maxVersion = $lib['@attributes']['max-version'];
if ($this->compareBigger($libVersion, $maxVersion)) { 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]); [$libName, $maxVersion, $libVersion]);
} }
} }
@ -258,44 +262,48 @@ class DependencyAnalyzer {
} }
$currentOS = $this->platform->getOS(); $currentOS = $this->platform->getOS();
if (!in_array($currentOS, $oss)) { 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; 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 = []; $missing = [];
$minVersion = null; $minVersion = null;
if (isset($dependencies['nextcloud']['@attributes']['min-version'])) { if (isset($appInfo['dependencies']['nextcloud']['@attributes']['min-version'])) {
$minVersion = $dependencies['nextcloud']['@attributes']['min-version']; $minVersion = $appInfo['dependencies']['nextcloud']['@attributes']['min-version'];
} elseif (isset($dependencies['owncloud']['@attributes']['min-version'])) { } elseif (isset($appInfo['dependencies']['owncloud']['@attributes']['min-version'])) {
$minVersion = $dependencies['owncloud']['@attributes']['min-version']; $minVersion = $appInfo['dependencies']['owncloud']['@attributes']['min-version'];
} elseif (isset($appInfo['requiremin'])) { } elseif (isset($appInfo['requiremin'])) {
$minVersion = $appInfo['requiremin']; $minVersion = $appInfo['requiremin'];
} elseif (isset($appInfo['require'])) { } elseif (isset($appInfo['require'])) {
$minVersion = $appInfo['require']; $minVersion = $appInfo['require'];
} }
$maxVersion = $this->getMaxVersion($dependencies, $appInfo); $maxVersion = $this->getMaxVersion($appInfo);
if (!is_null($minVersion)) { if (!is_null($minVersion)) {
if ($this->compareSmaller($this->platform->getOcVersion(), $minVersion)) { if ($this->compareSmaller($serverVersion, $minVersion)) {
$missing[] = $this->l->t('Server version %s or higher is required.', [$minVersion]); $missing[] = $this->getL()->t('Server version %s or higher is required.', [$minVersion]);
} }
} }
if (!$ignoreMax && !is_null($maxVersion)) { if (!$ignoreMax && !is_null($maxVersion)) {
if ($this->compareBigger($this->platform->getOcVersion(), $maxVersion)) { if ($this->compareBigger($serverVersion, $maxVersion)) {
$missing[] = $this->l->t('Server version %s or lower is required.', [$maxVersion]); $missing[] = $this->getL()->t('Server version %s or lower is required.', [$maxVersion]);
} }
} }
return $missing; return $missing;
} }
private function getMaxVersion(array $dependencies, array $appInfo): ?string { private function getMaxVersion(array $appInfo): ?string {
if (isset($dependencies['nextcloud']['@attributes']['max-version'])) { if (isset($appInfo['dependencies']['nextcloud']['@attributes']['max-version'])) {
return $dependencies['nextcloud']['@attributes']['max-version']; return $appInfo['dependencies']['nextcloud']['@attributes']['max-version'];
} }
if (isset($dependencies['owncloud']['@attributes']['max-version'])) { if (isset($appInfo['dependencies']['owncloud']['@attributes']['max-version'])) {
return $dependencies['owncloud']['@attributes']['max-version']; return $appInfo['dependencies']['owncloud']['@attributes']['max-version'];
} }
if (isset($appInfo['requiremax'])) { if (isset($appInfo['requiremax'])) {
return $appInfo['requiremax']; return $appInfo['requiremax'];
@ -304,13 +312,9 @@ class DependencyAnalyzer {
return null; return null;
} }
/** private function getValue(mixed $element): string {
* @param mixed $element
* @return mixed
*/
private function getValue($element) {
if (isset($element['@value'])) { if (isset($element['@value'])) {
return $element['@value']; return (string)$element['@value'];
} }
return (string)$element; return (string)$element;
} }

@ -13,8 +13,8 @@ use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\DBAL\Schema\Sequence; use Doctrine\DBAL\Schema\Sequence;
use Doctrine\DBAL\Schema\Table; use Doctrine\DBAL\Schema\Table;
use OC\App\InfoParser; use OC\App\InfoParser;
use OC\IntegrityCheck\Helpers\AppLocator;
use OC\Migration\SimpleOutput; use OC\Migration\SimpleOutput;
use OCP\App\IAppManager;
use OCP\AppFramework\App; use OCP\AppFramework\App;
use OCP\AppFramework\QueryException; use OCP\AppFramework\QueryException;
use OCP\DB\ISchemaWrapper; use OCP\DB\ISchemaWrapper;
@ -39,7 +39,12 @@ class MigrationService {
/** /**
* @throws \Exception * @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->appName = $appName;
$this->connection = $connection; $this->connection = $connection;
if ($logger === null) { if ($logger === null) {
@ -58,10 +63,8 @@ class MigrationService {
$this->migrationsNamespace = 'OC\\Core\\Migrations'; $this->migrationsNamespace = 'OC\\Core\\Migrations';
$this->checkOracle = true; $this->checkOracle = true;
} else { } else {
if ($appLocator === null) { $appManager = Server::get(IAppManager::class);
$appLocator = new AppLocator(); $appPath = $appManager->getAppPath($appName);
}
$appPath = $appLocator->getAppPath($appName);
$namespace = App::buildAppNamespace($appName); $namespace = App::buildAppNamespace($appName);
$this->migrationsPath = "$appPath/lib/Migration"; $this->migrationsPath = "$appPath/lib/Migration";
$this->migrationsNamespace = $namespace . '\\Migration'; $this->migrationsNamespace = $namespace . '\\Migration';
@ -728,7 +731,7 @@ class MigrationService {
} }
} }
private function ensureMigrationsAreLoaded() { private function ensureMigrationsAreLoaded(): void {
if (empty($this->migrations)) { if (empty($this->migrations)) {
$this->migrations = $this->findMigrations(); $this->migrations = $this->findMigrations();
} }

@ -18,13 +18,15 @@ use OC\Archive\TAR;
use OC\DB\Connection; use OC\DB\Connection;
use OC\DB\MigrationService; use OC\DB\MigrationService;
use OC\Files\FilenameValidator; use OC\Files\FilenameValidator;
use OC_App; use OCP\App\AppPathNotFoundException;
use OCP\App\IAppManager; use OCP\App\IAppManager;
use OCP\BackgroundJob\IJobList;
use OCP\Files; use OCP\Files;
use OCP\HintException; use OCP\HintException;
use OCP\Http\Client\IClientService; use OCP\Http\Client\IClientService;
use OCP\IConfig; use OCP\IConfig;
use OCP\ITempManager; use OCP\ITempManager;
use OCP\L10N\IFactory;
use OCP\Migration\IOutput; use OCP\Migration\IOutput;
use OCP\Server; use OCP\Server;
use phpseclib\File\X509; use phpseclib\File\X509;
@ -43,6 +45,8 @@ class Installer {
private ITempManager $tempManager, private ITempManager $tempManager,
private LoggerInterface $logger, private LoggerInterface $logger,
private IConfig $config, private IConfig $config,
private IAppManager $appManager,
private IFactory $l10nFactory,
private bool $isCLI, private bool $isCLI,
) { ) {
} }
@ -56,21 +60,12 @@ class Installer {
* @return string app ID * @return string app ID
*/ */
public function installApp(string $appId, bool $forceEnable = false): string { public function installApp(string $appId, bool $forceEnable = false): string {
$app = \OC_App::findAppInDirectories($appId); $appPath = $this->appManager->getAppPath($appId, true);
if ($app === false) {
throw new \Exception('App not found in any app directory');
}
$basedir = $app['path'] . '/' . $appId;
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'); $l = $this->l10nFactory->get('core');
$info = \OCP\Server::get(IAppManager::class)->getAppInfoByPath($basedir . '/appinfo/info.xml', $l->getLanguageCode()); $info = $this->appManager->getAppInfoByPath($appPath . '/appinfo/info.xml', $l->getLanguageCode());
if (!is_array($info)) { if (!is_array($info) || $info['id'] !== $appId) {
throw new \Exception( throw new \Exception(
$l->t('App "%s" cannot be installed because appinfo file cannot be read.', $l->t('App "%s" cannot be installed because appinfo file cannot be read.',
[$appId] [$appId]
@ -82,9 +77,8 @@ class Installer {
$ignoreMax = $forceEnable || in_array($appId, $ignoreMaxApps, true); $ignoreMax = $forceEnable || in_array($appId, $ignoreMaxApps, true);
$version = implode('.', \OCP\Util::getVersion()); $version = implode('.', \OCP\Util::getVersion());
if (!\OC_App::isAppCompatible($version, $info, $ignoreMax)) { if (!$this->appManager->isAppCompatible($version, $info, $ignoreMax)) {
throw new \Exception( throw new \Exception(
// TODO $l
$l->t('App "%s" cannot be installed because it is not compatible with this version of the server.', $l->t('App "%s" cannot be installed because it is not compatible with this version of the server.',
[$info['name']] [$info['name']]
) )
@ -93,47 +87,10 @@ class Installer {
// check for required dependencies // check for required dependencies
\OC_App::checkAppDependencies($this->config, $l, $info, $ignoreMax); \OC_App::checkAppDependencies($this->config, $l, $info, $ignoreMax);
/** @var Coordinator $coordinator */ $coordinator = Server::get(Coordinator::class);
$coordinator = \OC::$server->get(Coordinator::class);
$coordinator->runLazyRegistration($appId); $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); return $this->installAppLastSteps($appPath, $info, null, 'no');
//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'];
} }
/** /**
@ -142,7 +99,7 @@ class Installer {
* @param bool $allowUnstable Allow unstable releases * @param bool $allowUnstable Allow unstable releases
*/ */
public function updateAppstoreApp(string $appId, bool $allowUnstable = false): bool { public function updateAppstoreApp(string $appId, bool $allowUnstable = false): bool {
if ($this->isUpdateAvailable($appId, $allowUnstable)) { if ($this->isUpdateAvailable($appId, $allowUnstable) !== false) {
try { try {
$this->downloadApp($appId, $allowUnstable); $this->downloadApp($appId, $allowUnstable);
} catch (\Exception $e) { } catch (\Exception $e) {
@ -151,7 +108,7 @@ class Installer {
]); ]);
return false; return false;
} }
return OC_App::updateApp($appId); return $this->appManager->upgradeApp($appId);
} }
return false; return false;
@ -347,7 +304,7 @@ class Installer {
} }
// Check if the version is lower than before // 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; $newVersion = (string)$xml->version;
if (version_compare($currentVersion, $newVersion) === 1) { if (version_compare($currentVersion, $newVersion) === 1) {
throw new \Exception( throw new \Exception(
@ -424,7 +381,7 @@ class Installer {
foreach ($this->apps as $app) { foreach ($this->apps as $app) {
if ($app['id'] === $appId) { 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'])) { if (!isset($app['releases'][0]['version'])) {
return false; return false;
@ -447,12 +404,12 @@ class Installer {
* The function will check if the path contains a .git folder * The function will check if the path contains a .git folder
*/ */
private function isInstalledFromGit(string $appId): bool { private function isInstalledFromGit(string $appId): bool {
$app = \OC_App::findAppInDirectories($appId); try {
if ($app === false) { $appPath = $this->appManager->getAppPath($appId);
return file_exists($appPath . '/.git/');
} catch (AppPathNotFoundException) {
return false; return false;
} }
$basedir = $app['path'] . '/' . $appId;
return file_exists($basedir . '/.git/');
} }
/** /**
@ -487,7 +444,7 @@ class Installer {
*/ */
public function removeApp(string $appId): bool { public function removeApp(string $appId): bool {
if ($this->isDownloaded($appId)) { if ($this->isDownloaded($appId)) {
if (\OCP\Server::get(IAppManager::class)->isShipped($appId)) { if ($this->appManager->isShipped($appId)) {
return false; return false;
} }
@ -518,8 +475,7 @@ class Installer {
$this->downloadApp($appId); $this->downloadApp($appId);
} }
$this->installApp($appId); $this->installApp($appId);
$app = new OC_App(); $this->appManager->enableApp($appId);
$app->enable($appId);
} }
$bundles = json_decode($this->config->getAppValue('core', 'installed.bundles', json_encode([])), true); $bundles = json_decode($this->config->getAppValue('core', 'installed.bundles', json_encode([])), true);
$bundles[] = $bundle->getIdentifier(); $bundles[] = $bundle->getIdentifier();
@ -534,25 +490,23 @@ class Installer {
* working ownCloud at the end instead of an aborted update. * working ownCloud at the end instead of an aborted update.
* @return array Array of error messages (appid => Exception) * @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) { if ($output instanceof IOutput) {
$output->debug('Installing shipped apps'); $output->debug('Installing shipped apps');
} }
$appManager = \OCP\Server::get(IAppManager::class);
$config = \OCP\Server::get(IConfig::class);
$errors = []; $errors = [];
foreach (\OC::$APPSROOTS as $app_dir) { foreach (\OC::$APPSROOTS as $app_dir) {
if ($dir = opendir($app_dir['path'])) { if ($dir = opendir($app_dir['path'])) {
while (false !== ($filename = readdir($dir))) { while (false !== ($filename = readdir($dir))) {
if ($filename[0] !== '.' and is_dir($app_dir['path'] . "/$filename")) { if ($filename[0] !== '.' and is_dir($app_dir['path'] . "/$filename")) {
if (file_exists($app_dir['path'] . "/$filename/appinfo/info.xml")) { if (file_exists($app_dir['path'] . "/$filename/appinfo/info.xml")) {
if ($config->getAppValue($filename, 'installed_version') === '') { if ($this->config->getAppValue($filename, 'installed_version') === '') {
$enabled = $appManager->isDefaultEnabled($filename); $enabled = $this->appManager->isDefaultEnabled($filename);
if (($enabled || in_array($filename, $appManager->getAlwaysEnabledApps())) if (($enabled || in_array($filename, $this->appManager->getAlwaysEnabledApps()))
&& $config->getAppValue($filename, 'enabled') !== 'no') { && $this->config->getAppValue($filename, 'enabled') !== 'no') {
if ($softErrors) { if ($softErrors) {
try { try {
Installer::installShippedApp($filename, $output); $this->installShippedApp($filename, $output);
} catch (HintException $e) { } catch (HintException $e) {
if ($e->getPrevious() instanceof TableExistsException) { if ($e->getPrevious() instanceof TableExistsException) {
$errors[$filename] = $e; $errors[$filename] = $e;
@ -561,9 +515,9 @@ class Installer {
throw $e; throw $e;
} }
} else { } 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; return $errors;
} }
/** private function installAppLastSteps(string $appPath, array $info, ?IOutput $output = null, string $enabled = 'no'): string {
* install an app already placed in the app folder \OC_App::registerAutoloading($info['id'], $appPath);
*/
public static function installShippedApp(string $app, ?IOutput $output = null): string|false {
if ($output instanceof IOutput) {
$output->debug('Installing ' . $app);
}
$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) { if ($output instanceof IOutput) {
$ms->setOutput($output); $ms->setOutput($output);
} }
$previousVersion = $config->getAppValue($app, 'installed_version', false); if ($previousVersion !== '') {
$ms->migrate('latest', !$previousVersion); \OC_App::executeRepairSteps($info['id'], $info['repair-steps']['pre-migration']);
}
//run appinfo/install.php $ms->migrate('latest', $previousVersion === '');
self::includeAppScript("$appPath/appinfo/install.php");
$info = \OCP\Server::get(IAppManager::class)->getAppInfo($app); if ($previousVersion !== '') {
if (is_null($info)) { \OC_App::executeRepairSteps($info['id'], $info['repair-steps']['post-migration']);
return false;
} }
if ($output instanceof IOutput) { if ($output instanceof IOutput) {
$output->debug('Registering tasks of ' . $app); $output->debug('Registering tasks of ' . $info['id']);
} }
\OC_App::setupBackgroundJobs($info['background-jobs']);
OC_App::executeRepairSteps($app, $info['repair-steps']['install']); // Setup background jobs
$queue = Server::get(IJobList::class);
foreach ($info['background-jobs'] as $job) {
$queue->add($job);
}
$config->setAppValue($app, 'installed_version', \OCP\Server::get(IAppManager::class)->getAppVersion($app)); // Run deprecated appinfo/install.php if any
if (array_key_exists('ocsid', $info)) { $appInstallScriptPath = $appPath . '/appinfo/install.php';
$config->setAppValue($app, 'ocsid', $info['ocsid']); 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);
} }
//set remote/public handlers \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) { 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) { 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']; 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 { private static function includeAppScript(string $script): void {
if (file_exists($script)) { if (file_exists($script)) {
include $script; include $script;

@ -10,7 +10,6 @@ namespace OC\IntegrityCheck;
use OC\Core\Command\Maintenance\Mimetype\GenerateMimetypeFileBuilder; use OC\Core\Command\Maintenance\Mimetype\GenerateMimetypeFileBuilder;
use OC\IntegrityCheck\Exceptions\InvalidSignatureException; use OC\IntegrityCheck\Exceptions\InvalidSignatureException;
use OC\IntegrityCheck\Helpers\AppLocator;
use OC\IntegrityCheck\Helpers\EnvironmentHelper; use OC\IntegrityCheck\Helpers\EnvironmentHelper;
use OC\IntegrityCheck\Helpers\FileAccessHelper; use OC\IntegrityCheck\Helpers\FileAccessHelper;
use OC\IntegrityCheck\Iterator\ExcludeFileByNameFilterIterator; use OC\IntegrityCheck\Iterator\ExcludeFileByNameFilterIterator;
@ -44,7 +43,6 @@ class Checker {
private ServerVersion $serverVersion, private ServerVersion $serverVersion,
private EnvironmentHelper $environmentHelper, private EnvironmentHelper $environmentHelper,
private FileAccessHelper $fileAccessHelper, private FileAccessHelper $fileAccessHelper,
private AppLocator $appLocator,
private ?IConfig $config, private ?IConfig $config,
private ?IAppConfig $appConfig, private ?IAppConfig $appConfig,
ICacheFactory $cacheFactory, ICacheFactory $cacheFactory,
@ -460,7 +458,7 @@ class Checker {
public function verifyAppSignature(string $appId, string $path = '', bool $forceVerify = false): array { public function verifyAppSignature(string $appId, string $path = '', bool $forceVerify = false): array {
try { try {
if ($path === '') { if ($path === '') {
$path = $this->appLocator->getAppPath($appId); $path = $this->appManager->getAppPath($appId);
} }
$result = $this->verify( $result = $this->verify(
$path . '/appinfo/signature.json', $path . '/appinfo/signature.json',
@ -545,7 +543,7 @@ class Checker {
$appNeedsToBeChecked = false; $appNeedsToBeChecked = false;
if ($isShipped) { if ($isShipped) {
$appNeedsToBeChecked = true; $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 // Otherwise only if the application explicitly ships a signature.json file
$appNeedsToBeChecked = true; $appNeedsToBeChecked = true;
} }

@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OC\IntegrityCheck\Helpers;
/**
* Class AppLocator provides a non-static helper for OC_App::getPath($appId)
* it is not possible to use IAppManager at this point as IAppManager has a
* dependency on a running Nextcloud.
*
* @package OC\IntegrityCheck\Helpers
*/
class AppLocator {
/**
* Provides \OC_App::getAppPath($appId)
*
* @param string $appId
* @return string
* @throws \Exception If the app cannot be found
*/
public function getAppPath(string $appId): string {
$path = \OC_App::getAppPath($appId);
if ($path === false) {
throw new \Exception('App not found');
}
return $path;
}
}

@ -12,7 +12,6 @@ use NCU\Security\Signature\ISignatureManager;
use OC\Accounts\AccountManager; use OC\Accounts\AccountManager;
use OC\App\AppManager; use OC\App\AppManager;
use OC\App\AppStore\Bundles\BundleFetcher; use OC\App\AppStore\Bundles\BundleFetcher;
use OC\App\AppStore\Fetcher\AppFetcher;
use OC\AppFramework\Bootstrap\Coordinator; use OC\AppFramework\Bootstrap\Coordinator;
use OC\AppFramework\Http\Request; use OC\AppFramework\Http\Request;
use OC\AppFramework\Http\RequestId; use OC\AppFramework\Http\RequestId;
@ -66,7 +65,6 @@ use OC\FullTextSearch\FullTextSearchManager;
use OC\Http\Client\ClientService; use OC\Http\Client\ClientService;
use OC\Http\Client\NegativeDnsCache; use OC\Http\Client\NegativeDnsCache;
use OC\IntegrityCheck\Checker; use OC\IntegrityCheck\Checker;
use OC\IntegrityCheck\Helpers\AppLocator;
use OC\IntegrityCheck\Helpers\EnvironmentHelper; use OC\IntegrityCheck\Helpers\EnvironmentHelper;
use OC\IntegrityCheck\Helpers\FileAccessHelper; use OC\IntegrityCheck\Helpers\FileAccessHelper;
use OC\KnownUser\KnownUserService; use OC\KnownUser\KnownUserService;
@ -842,7 +840,6 @@ class Server extends ServerContainer implements IServerContainer {
$c->get(ServerVersion::class), $c->get(ServerVersion::class),
$c->get(EnvironmentHelper::class), $c->get(EnvironmentHelper::class),
new FileAccessHelper(), new FileAccessHelper(),
new AppLocator(),
$config, $config,
$appConfig, $appConfig,
$c->get(ICacheFactory::class), $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) { $this->registerService(IApiFactory::class, function (ContainerInterface $c) {
return new ApiFactory($c->get(IClientService::class)); return new ApiFactory($c->get(IClientService::class));
}); });

@ -443,7 +443,8 @@ class Setup {
// Install shipped apps and specified app bundles // Install shipped apps and specified app bundles
$this->outputDebug($output, 'Install default apps'); $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 // create empty file in data dir, so we can later find
// out that this is indeed a Nextcloud data directory // out that this is indeed a Nextcloud data directory

@ -23,7 +23,6 @@ use OC\Repair\Events\RepairInfoEvent;
use OC\Repair\Events\RepairStartEvent; use OC\Repair\Events\RepairStartEvent;
use OC\Repair\Events\RepairStepEvent; use OC\Repair\Events\RepairStepEvent;
use OC\Repair\Events\RepairWarningEvent; use OC\Repair\Events\RepairWarningEvent;
use OC_App;
use OCP\App\IAppManager; use OCP\App\IAppManager;
use OCP\EventDispatcher\Event; use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventDispatcher; use OCP\EventDispatcher\IEventDispatcher;
@ -60,6 +59,7 @@ class Updater extends BasicEmitter {
private Checker $checker, private Checker $checker,
private ?LoggerInterface $log, private ?LoggerInterface $log,
private Installer $installer, 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 // Update the appfetchers version so it downloads the correct list from the appstore
\OC::$server->get(AppFetcher::class)->setVersion($currentVersion); \OC::$server->get(AppFetcher::class)->setVersion($currentVersion);
/** @var AppManager $appManager */
$appManager = \OC::$server->getAppManager();
// upgrade appstore apps // upgrade appstore apps
$this->upgradeAppStoreApps($appManager->getEnabledApps()); $this->upgradeAppStoreApps($this->appManager->getEnabledApps());
$autoDisabledApps = $appManager->getAutoDisabledApps(); /** @var AppManager $this->appManager */
$autoDisabledApps = $this->appManager->getAutoDisabledApps();
if (!empty($autoDisabledApps)) { if (!empty($autoDisabledApps)) {
$this->upgradeAppStoreApps(array_keys($autoDisabledApps), $autoDisabledApps); $this->upgradeAppStoreApps(array_keys($autoDisabledApps), $autoDisabledApps);
} }
// install new shipped apps on upgrade // install new shipped apps on upgrade
$errors = Installer::installShippedApps(true); $errors = $this->installer->installShippedApps(true);
foreach ($errors as $appId => $exception) { foreach ($errors as $appId => $exception) {
/** @var \Exception $exception */ /** @var \Exception $exception */
$this->log->error($exception->getMessage(), [ $this->log->error($exception->getMessage(), [
@ -296,7 +294,7 @@ class Updater extends BasicEmitter {
* @throws NeedsUpdateException * @throws NeedsUpdateException
*/ */
protected function doAppUpgrade(): void { protected function doAppUpgrade(): void {
$apps = \OC_App::getEnabledApps(); $apps = $this->appManager->getEnabledApps();
$priorityTypes = ['authentication', 'extended_authentication', 'filesystem', 'logging']; $priorityTypes = ['authentication', 'extended_authentication', 'filesystem', 'logging'];
$pseudoOtherType = 'other'; $pseudoOtherType = 'other';
$stacks = [$pseudoOtherType => []]; $stacks = [$pseudoOtherType => []];
@ -307,7 +305,7 @@ class Updater extends BasicEmitter {
if (!isset($stacks[$type])) { if (!isset($stacks[$type])) {
$stacks[$type] = []; $stacks[$type] = [];
} }
if (\OC_App::isType($appId, [$type])) { if ($this->appManager->isType($appId, [$type])) {
$stacks[$type][] = $appId; $stacks[$type][] = $appId;
$priorityType = true; $priorityType = true;
break; break;
@ -320,16 +318,16 @@ class Updater extends BasicEmitter {
foreach (array_merge($priorityTypes, [$pseudoOtherType]) as $type) { foreach (array_merge($priorityTypes, [$pseudoOtherType]) as $type) {
$stack = $stacks[$type]; $stack = $stacks[$type];
foreach ($stack as $appId) { foreach ($stack as $appId) {
if (\OC_App::shouldUpgrade($appId)) { if ($this->appManager->isUpgradeRequired($appId)) {
$this->emit('\OC\Updater', 'appUpgradeStarted', [$appId, \OCP\Server::get(IAppManager::class)->getAppVersion($appId)]); $this->emit('\OC\Updater', 'appUpgradeStarted', [$appId, $this->appManager->getAppVersion($appId)]);
\OC_App::updateApp($appId); $this->appManager->upgradeApp($appId);
$this->emit('\OC\Updater', 'appUpgrade', [$appId, \OCP\Server::get(IAppManager::class)->getAppVersion($appId)]); $this->emit('\OC\Updater', 'appUpgrade', [$appId, $this->appManager->getAppVersion($appId)]);
} }
if ($type !== $pseudoOtherType) { if ($type !== $pseudoOtherType) {
// load authentication, filesystem and logging apps after // load authentication, filesystem and logging apps after
// upgrading them. Other apps my need to rely on modifying // upgrading them. Other apps my need to rely on modifying
// user and/or filesystem aspects. // 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 { private function checkAppsRequirements(): void {
$isCoreUpgrade = $this->isCodeUpgrade(); $isCoreUpgrade = $this->isCodeUpgrade();
$apps = OC_App::getEnabledApps(); $apps = $this->appManager->getEnabledApps();
$version = implode('.', Util::getVersion()); $version = implode('.', Util::getVersion());
$appManager = \OC::$server->getAppManager();
foreach ($apps as $app) { foreach ($apps as $app) {
// check if the app is compatible with this version of Nextcloud // check if the app is compatible with this version of Nextcloud
$info = $appManager->getAppInfo($app); $info = $this->appManager->getAppInfo($app);
if ($info === null || !OC_App::isAppCompatible($version, $info)) { if ($info === null || !$this->appManager->isAppCompatible($version, $info)) {
if ($appManager->isShipped($app)) { if ($this->appManager->isShipped($app)) {
throw new \UnexpectedValueException('The files of the app "' . $app . '" were not correctly replaced before running the update'); 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]); $this->emit('\OC\Updater', 'incompatibleAppDisabled', [$app]);
} }
} }
} }
/**
* @return bool
*/
private function isCodeUpgrade(): bool { private function isCodeUpgrade(): bool {
$installedVersion = $this->config->getSystemValueString('version', '0.0.0'); $installedVersion = $this->config->getSystemValueString('version', '0.0.0');
$currentVersion = implode('.', Util::getVersion()); $currentVersion = implode('.', Util::getVersion());
@ -395,12 +389,11 @@ class Updater extends BasicEmitter {
} }
$this->emit('\OC\Updater', 'checkAppStoreApp', [$app]); $this->emit('\OC\Updater', 'checkAppStoreApp', [$app]);
if (!empty($previousEnableStates)) { if (isset($previousEnableStates[$app])) {
$ocApp = new \OC_App();
if (!empty($previousEnableStates[$app]) && is_array($previousEnableStates[$app])) { if (!empty($previousEnableStates[$app]) && is_array($previousEnableStates[$app])) {
$ocApp->enable($app, $previousEnableStates[$app]); $this->appManager->enableAppForGroups($app, $previousEnableStates[$app]);
} else { } elseif ($previousEnableStates[$app] === 'yes') {
$ocApp->enable($app); $this->appManager->enableApp($app);
} }
} }
} catch (\Exception $ex) { } catch (\Exception $ex) {

@ -6,17 +6,14 @@ declare(strict_types=1);
* SPDX-FileCopyrightText: 2016 ownCloud, Inc. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
use OC\App\AppManager;
use OC\App\DependencyAnalyzer; use OC\App\DependencyAnalyzer;
use OC\App\Platform;
use OC\AppFramework\Bootstrap\Coordinator; use OC\AppFramework\Bootstrap\Coordinator;
use OC\Config\ConfigManager;
use OC\DB\MigrationService;
use OC\Installer; use OC\Installer;
use OC\Repair; use OC\Repair;
use OC\Repair\Events\RepairErrorEvent; use OC\Repair\Events\RepairErrorEvent;
use OCP\App\Events\AppUpdateEvent; use OCP\App\AppPathNotFoundException;
use OCP\App\IAppManager; use OCP\App\IAppManager;
use OCP\App\ManagerEvent;
use OCP\Authentication\IAlternativeLogin; use OCP\Authentication\IAlternativeLogin;
use OCP\EventDispatcher\IEventDispatcher; use OCP\EventDispatcher\IEventDispatcher;
use OCP\IAppConfig; 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 * @param array $groups (optional) when set, only these groups will have access to the app
* @throws \Exception * @throws \Exception
* @return void * @return void
* @deprecated 32.0.0 Use the installer and the app manager instead
* *
* This function set an app as enabled in appconfig. * 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. * If multiple copies are found, the apps root the latest version is returned.
* *
* @param string $appId
* @param bool $ignoreCache ignore cache and rebuild it * @param bool $ignoreCache ignore cache and rebuild it
* @return false|array{path: string, url: string} the apps root shape * @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) { public static function findAppInDirectories(string $appId, bool $ignoreCache = false) {
$sanitizedAppId = self::cleanAppId($appId); return Server::get(AppManager::class)->findAppInDirectories($appId, $ignoreCache);
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
}
} }
/** /**
@ -299,17 +260,11 @@ class OC_App {
* @deprecated 11.0.0 use Server::get(IAppManager)->getAppPath() * @deprecated 11.0.0 use Server::get(IAppManager)->getAppPath()
*/ */
public static function getAppPath(string $appId, bool $refreshAppPath = false) { public static function getAppPath(string $appId, bool $refreshAppPath = false) {
$appId = self::cleanAppId($appId); try {
if ($appId === '') { return Server::get(IAppManager::class)->getAppPath($appId, $refreshAppPath);
} catch (AppPathNotFoundException) {
return false; 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 * @param string $appId
* @return string|false * @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) { public static function getAppWebPath(string $appId) {
if (($dir = self::findAppInDirectories($appId)) != false) { try {
return OC::$WEBROOT . $dir['url'] . '/' . $appId; return Server::get(IAppManager::class)->getAppWebPath($appId);
} catch (AppPathNotFoundException) {
return false;
} }
return false;
} }
/** /**
* get app's version based on it's path * get app's version based on it's path
* *
* @param string $path * @deprecated 32.0.0 use Server::get(IAppManager)->getAppInfoByPath() with the path to info.xml directly
* @return string
*/ */
public static function getAppVersionByPath(string $path): string { public static function getAppVersionByPath(string $path): string {
$infoFile = $path . '/appinfo/info.xml'; $infoFile = $path . '/appinfo/info.xml';
@ -542,38 +497,11 @@ class OC_App {
return $appList; 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 * @deprecated 32.0.0 Use IAppManager::isUpgradeRequired instead
* 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
*/ */
private static function adjustVersionParts(string $version1, string $version2): string { public static function shouldUpgrade(string $app): bool {
$version1 = explode('.', $version1); return Server::get(\OCP\App\IAppManager::class)->isUpgradeRequired($app);
$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);
} }
/** /**
@ -590,42 +518,11 @@ class OC_App {
* @param string $ocVersion Nextcloud version to check against * @param string $ocVersion Nextcloud version to check against
* @param array $appInfo app info (from xml) * @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 { public static function isAppCompatible(string $ocVersion, array $appInfo, bool $ignoreMax = false): bool {
$requireMin = ''; return Server::get(\OCP\App\IAppManager::class)->isAppCompatible($ocVersion, $appInfo, $ignoreMax);
$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;
} }
/** /**
@ -639,77 +536,14 @@ class OC_App {
/** /**
* update the database for the app and call the update script * update the database for the app and call the update script
* *
* @param string $appId * @deprecated 32.0.0 Use IAppManager::upgradeApp instead
* @return bool
*/ */
public static function updateApp(string $appId): bool { public static function updateApp(string $appId): bool {
// for apps distributed with core, we refresh app path in case the downloaded version try {
// have been installed in custom apps and not in the default path return Server::get(\OC\App\AppManager::class)->upgradeApp($appId);
$appPath = self::getAppPath($appId, true); } catch (\OCP\App\AppPathNotFoundException $e) {
if ($appPath === false) {
return false; 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(); $r->run();
} }
/**
* @deprecated 32.0.0 Use the IJobList directly instead
*/
public static function setupBackgroundJobs(array $jobs) { public static function setupBackgroundJobs(array $jobs) {
$queue = \OC::$server->getJobList(); $queue = \OC::$server->getJobList();
foreach ($jobs as $job) { 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\IConfig $config
* @param \OCP\IL10N $l * @param \OCP\IL10N $l
@ -767,7 +591,7 @@ class OC_App {
* @throws \Exception * @throws \Exception
*/ */
public static function checkAppDependencies(\OCP\IConfig $config, \OCP\IL10N $l, array $info, bool $ignoreMax) { 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); $missing = $dependencyAnalyzer->analyze($info, $ignoreMax);
if (!empty($missing)) { if (!empty($missing)) {
$missingMsg = implode(PHP_EOL, $missing); $missingMsg = implode(PHP_EOL, $missing);

@ -166,9 +166,10 @@ interface IAppManager {
* Get the directory for the given app. * Get the directory for the given app.
* *
* @since 11.0.0 * @since 11.0.0
* @since 32.0.0 Added param $ignoreCache to ignore cache
* @throws AppPathNotFoundException * @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. * Get the web path for the given app.
@ -340,4 +341,36 @@ interface IAppManager {
* @since 31.0.0 * @since 31.0.0
*/ */
public function getAllAppsInAppsFolders(): array; 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;
} }

@ -16,7 +16,6 @@
"admin": "admin-encryption" "admin": "admin-encryption"
}, },
"types": ["filesystem"], "types": ["filesystem"],
"ocsid": "166047",
"dependencies": { "dependencies": {
"php": { "php": {
"@attributes" : { "@attributes" : {

@ -22,5 +22,4 @@
<types> <types>
<filesystem/> <filesystem/>
</types> </types>
<ocsid>166047</ocsid>
</info> </info>

@ -22,7 +22,6 @@
<types> <types>
<filesystem/> <filesystem/>
</types> </types>
<ocsid>166047</ocsid>
<dependencies> <dependencies>
<php min-version="5.4" max-version="5.5"/> <php min-version="5.4" max-version="5.5"/>
<database min-version="3.0">sqlite</database> <database min-version="3.0">sqlite</database>

@ -6,10 +6,18 @@
* SPDX-License-Identifier: AGPL-3.0-or-later * SPDX-License-Identifier: AGPL-3.0-or-later
*/ */
use OC\Installer;
use OCP\App\IAppManager;
use OCP\Server;
require_once __DIR__ . '/../lib/base.php'; require_once __DIR__ . '/../lib/base.php';
function enableApp($app) { 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"; echo "Enabled application {$app}\n";
} }

@ -11,12 +11,13 @@ declare(strict_types=1);
namespace Test\App; namespace Test\App;
use OC\App\AppManager; use OC\App\AppManager;
use OC\App\DependencyAnalyzer;
use OC\App\Platform;
use OC\AppConfig; use OC\AppConfig;
use OC\Config\ConfigManager; use OC\Config\ConfigManager;
use OCP\App\AppPathNotFoundException; use OCP\App\AppPathNotFoundException;
use OCP\App\Events\AppDisableEvent; use OCP\App\Events\AppDisableEvent;
use OCP\App\Events\AppEnableEvent; use OCP\App\Events\AppEnableEvent;
use OCP\App\IAppManager;
use OCP\EventDispatcher\IEventDispatcher; use OCP\EventDispatcher\IEventDispatcher;
use OCP\ICache; use OCP\ICache;
use OCP\ICacheFactory; use OCP\ICacheFactory;
@ -96,8 +97,9 @@ class AppManagerTest extends TestCase {
protected ServerVersion&MockObject $serverVersion; protected ServerVersion&MockObject $serverVersion;
protected ConfigManager&MockObject $configManager; protected ConfigManager&MockObject $configManager;
/** @var IAppManager */ protected DependencyAnalyzer $dependencyAnalyzer;
protected $manager;
protected AppManager $manager;
protected function setUp(): void { protected function setUp(): void {
parent::setUp(); parent::setUp();
@ -113,6 +115,7 @@ class AppManagerTest extends TestCase {
$this->urlGenerator = $this->createMock(IURLGenerator::class); $this->urlGenerator = $this->createMock(IURLGenerator::class);
$this->serverVersion = $this->createMock(ServerVersion::class); $this->serverVersion = $this->createMock(ServerVersion::class);
$this->configManager = $this->createMock(ConfigManager::class); $this->configManager = $this->createMock(ConfigManager::class);
$this->dependencyAnalyzer = new DependencyAnalyzer($this->createMock(Platform::class));
$this->overwriteService(AppConfig::class, $this->appConfig); $this->overwriteService(AppConfig::class, $this->appConfig);
$this->overwriteService(IURLGenerator::class, $this->urlGenerator); $this->overwriteService(IURLGenerator::class, $this->urlGenerator);
@ -136,6 +139,7 @@ class AppManagerTest extends TestCase {
$this->logger, $this->logger,
$this->serverVersion, $this->serverVersion,
$this->configManager, $this->configManager,
$this->dependencyAnalyzer,
); );
} }
@ -275,6 +279,7 @@ class AppManagerTest extends TestCase {
$this->logger, $this->logger,
$this->serverVersion, $this->serverVersion,
$this->configManager, $this->configManager,
$this->dependencyAnalyzer,
]) ])
->onlyMethods([ ->onlyMethods([
'getAppPath', 'getAppPath',
@ -331,6 +336,7 @@ class AppManagerTest extends TestCase {
$this->logger, $this->logger,
$this->serverVersion, $this->serverVersion,
$this->configManager, $this->configManager,
$this->dependencyAnalyzer,
]) ])
->onlyMethods([ ->onlyMethods([
'getAppPath', 'getAppPath',
@ -394,6 +400,7 @@ class AppManagerTest extends TestCase {
$this->logger, $this->logger,
$this->serverVersion, $this->serverVersion,
$this->configManager, $this->configManager,
$this->dependencyAnalyzer,
]) ])
->onlyMethods([ ->onlyMethods([
'getAppPath', 'getAppPath',
@ -474,16 +481,16 @@ class AppManagerTest extends TestCase {
'writable' => false, 'writable' => false,
]; ];
$fakeTestAppPath = $fakeAppPath . '/' . 'test-test-app'; $fakeTestAppPath = $fakeAppPath . '/' . 'test_test_app';
mkdir($fakeTestAppPath); mkdir($fakeTestAppPath);
$generatedAppPath = $this->manager->getAppPath('test-test-app'); $generatedAppPath = $this->manager->getAppPath('test_test_app');
rmdir($fakeTestAppPath); rmdir($fakeTestAppPath);
unlink($fakeAppLink); unlink($fakeAppLink);
rmdir($fakeAppPath); rmdir($fakeAppPath);
$this->assertEquals($fakeAppLink . '/test-test-app', $generatedAppPath); $this->assertEquals($fakeAppLink . '/test_test_app', $generatedAppPath);
} }
public function testGetAppPathFail(): void { public function testGetAppPathFail(): void {
@ -589,7 +596,7 @@ class AppManagerTest extends TestCase {
} }
public function testGetAppsNeedingUpgrade(): void { public function testGetAppsNeedingUpgrade(): void {
/** @var AppManager|MockObject $manager */ /** @var AppManager&MockObject $manager */
$manager = $this->getMockBuilder(AppManager::class) $manager = $this->getMockBuilder(AppManager::class)
->setConstructorArgs([ ->setConstructorArgs([
$this->userSession, $this->userSession,
@ -600,6 +607,7 @@ class AppManagerTest extends TestCase {
$this->logger, $this->logger,
$this->serverVersion, $this->serverVersion,
$this->configManager, $this->configManager,
$this->dependencyAnalyzer,
]) ])
->onlyMethods(['getAppInfo']) ->onlyMethods(['getAppInfo'])
->getMock(); ->getMock();
@ -661,6 +669,7 @@ class AppManagerTest extends TestCase {
$this->logger, $this->logger,
$this->serverVersion, $this->serverVersion,
$this->configManager, $this->configManager,
$this->dependencyAnalyzer,
]) ])
->onlyMethods(['getAppInfo']) ->onlyMethods(['getAppInfo'])
->getMock(); ->getMock();
@ -801,6 +810,7 @@ class AppManagerTest extends TestCase {
$this->logger, $this->logger,
$this->serverVersion, $this->serverVersion,
$this->configManager, $this->configManager,
$this->dependencyAnalyzer,
]) ])
->onlyMethods([ ->onlyMethods([
'getAppInfo', 'getAppInfo',
@ -833,6 +843,7 @@ class AppManagerTest extends TestCase {
$this->logger, $this->logger,
$this->serverVersion, $this->serverVersion,
$this->configManager, $this->configManager,
$this->dependencyAnalyzer,
]) ])
->onlyMethods([ ->onlyMethods([
'getAppInfo', 'getAppInfo',
@ -864,6 +875,7 @@ class AppManagerTest extends TestCase {
$this->logger, $this->logger,
$this->serverVersion, $this->serverVersion,
$this->configManager, $this->configManager,
$this->dependencyAnalyzer,
]) ])
->onlyMethods([ ->onlyMethods([
'getAppInfo', 'getAppInfo',
@ -884,5 +896,4 @@ class AppManagerTest extends TestCase {
$manager->getAppVersion('unknown'), $manager->getAppVersion('unknown'),
); );
} }
} }

@ -9,18 +9,13 @@ namespace Test\App;
use OC\App\DependencyAnalyzer; use OC\App\DependencyAnalyzer;
use OC\App\Platform; use OC\App\Platform;
use OCP\IL10N;
use Test\TestCase; use Test\TestCase;
class DependencyAnalyzerTest extends TestCase { class DependencyAnalyzerTest extends TestCase {
/** @var Platform|\PHPUnit\Framework\MockObject\MockObject */ /** @var Platform|\PHPUnit\Framework\MockObject\MockObject */
private $platformMock; private $platformMock;
/** @var IL10N */ private DependencyAnalyzer $analyser;
private $l10nMock;
/** @var DependencyAnalyzer */
private $analyser;
protected function setUp(): void { protected function setUp(): void {
$this->platformMock = $this->getMockBuilder(Platform::class) $this->platformMock = $this->getMockBuilder(Platform::class)
@ -55,16 +50,7 @@ class DependencyAnalyzerTest extends TestCase {
->method('getOcVersion') ->method('getOcVersion')
->willReturn('8.0.2'); ->willReturn('8.0.2');
$this->l10nMock = $this->getMockBuilder(IL10N::class) $this->analyser = new DependencyAnalyzer($this->platformMock);
->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);
} }
/** /**
@ -485,4 +471,286 @@ class DependencyAnalyzerTest extends TestCase {
[[], '5.4', '5.4', null], [[], '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);
}
} }

@ -9,6 +9,7 @@
namespace Test; namespace Test;
use OC\App\AppManager; use OC\App\AppManager;
use OC\App\DependencyAnalyzer;
use OC\AppConfig; use OC\AppConfig;
use OC\Config\ConfigManager; use OC\Config\ConfigManager;
use OCP\EventDispatcher\IEventDispatcher; use OCP\EventDispatcher\IEventDispatcher;
@ -36,288 +37,6 @@ class AppTest extends \Test\TestCase {
public const TEST_GROUP1 = 'group1'; public const TEST_GROUP1 = 'group1';
public const TEST_GROUP2 = 'group2'; 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 * Tests that the app order is correct
*/ */
@ -572,6 +291,7 @@ class AppTest extends \Test\TestCase {
Server::get(LoggerInterface::class), Server::get(LoggerInterface::class),
Server::get(ServerVersion::class), Server::get(ServerVersion::class),
Server::get(ConfigManager::class), Server::get(ConfigManager::class),
Server::get(DependencyAnalyzer::class),
)); ));
} }

@ -20,6 +20,7 @@ use OC\DB\Connection;
use OC\DB\MigrationService; use OC\DB\MigrationService;
use OC\DB\SchemaWrapper; use OC\DB\SchemaWrapper;
use OC\Migration\MetadataManager; use OC\Migration\MetadataManager;
use OCP\App\AppPathNotFoundException;
use OCP\App\IAppManager; use OCP\App\IAppManager;
use OCP\IDBConnection; use OCP\IDBConnection;
use OCP\Migration\Attributes\AddColumn; use OCP\Migration\Attributes\AddColumn;
@ -81,10 +82,10 @@ class MigrationsTest extends \Test\TestCase {
public function testUnknownApp(): void { public function testUnknownApp(): void {
$this->expectException(\Exception::class); $this->expectException(AppPathNotFoundException::class);
$this->expectExceptionMessage('App not found'); $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);
} }

@ -16,7 +16,9 @@ use OCP\Http\Client\IClient;
use OCP\Http\Client\IClientService; use OCP\Http\Client\IClientService;
use OCP\IConfig; use OCP\IConfig;
use OCP\ITempManager; use OCP\ITempManager;
use OCP\L10N\IFactory;
use OCP\Server; use OCP\Server;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
/** /**
@ -38,6 +40,8 @@ class InstallerTest extends TestCase {
private $logger; private $logger;
/** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */ /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */
private $config; private $config;
private IAppManager&MockObject $appManager;
private IFactory&MockObject $l10nFactory;
protected function setUp(): void { protected function setUp(): void {
parent::setUp(); parent::setUp();
@ -47,18 +51,13 @@ class InstallerTest extends TestCase {
$this->tempManager = $this->createMock(ITempManager::class); $this->tempManager = $this->createMock(ITempManager::class);
$this->logger = $this->createMock(LoggerInterface::class); $this->logger = $this->createMock(LoggerInterface::class);
$this->config = $this->createMock(IConfig::class); $this->config = $this->createMock(IConfig::class);
$this->appManager = $this->createMock(IAppManager::class);
$this->l10nFactory = $this->createMock(IFactory::class);
$config = Server::get(IConfig::class); $config = Server::get(IConfig::class);
$this->appstore = $config->setSystemValue('appstoreenabled', true); $this->appstore = $config->setSystemValue('appstoreenabled', true);
$config->setSystemValue('appstoreenabled', true); $config->setSystemValue('appstoreenabled', true);
$installer = new Installer( $installer = Server::get(Installer::class);
Server::get(AppFetcher::class),
Server::get(IClientService::class),
Server::get(ITempManager::class),
Server::get(LoggerInterface::class),
$config,
false
);
$installer->removeApp(self::$appid); $installer->removeApp(self::$appid);
} }
@ -69,19 +68,14 @@ class InstallerTest extends TestCase {
$this->tempManager, $this->tempManager,
$this->logger, $this->logger,
$this->config, $this->config,
$this->appManager,
$this->l10nFactory,
false false
); );
} }
protected function tearDown(): void { protected function tearDown(): void {
$installer = new Installer( $installer = Server::get(Installer::class);
Server::get(AppFetcher::class),
Server::get(IClientService::class),
Server::get(ITempManager::class),
Server::get(LoggerInterface::class),
Server::get(IConfig::class),
false
);
$installer->removeApp(self::$appid); $installer->removeApp(self::$appid);
Server::get(IConfig::class)->setSystemValue('appstoreenabled', $this->appstore); Server::get(IConfig::class)->setSystemValue('appstoreenabled', $this->appstore);
@ -93,14 +87,7 @@ class InstallerTest extends TestCase {
Server::get(IAppManager::class)->getAppVersion('testapp', true); Server::get(IAppManager::class)->getAppVersion('testapp', true);
// Build installer // Build installer
$installer = new Installer( $installer = Server::get(Installer::class);
Server::get(AppFetcher::class),
Server::get(IClientService::class),
Server::get(ITempManager::class),
Server::get(LoggerInterface::class),
Server::get(IConfig::class),
false
);
// Extract app // Extract app
$pathOfTestApp = __DIR__ . '/../data/testapp.zip'; $pathOfTestApp = __DIR__ . '/../data/testapp.zip';
@ -158,6 +145,10 @@ class InstallerTest extends TestCase {
->expects($this->once()) ->expects($this->once())
->method('get') ->method('get')
->willReturn($appArray); ->willReturn($appArray);
$this->appManager
->expects($this->exactly(2))
->method('getAppVersion')
->willReturn('1.0');
$installer = $this->getInstaller(); $installer = $this->getInstaller();
$this->assertSame($updateAvailable, $installer->isUpdateAvailable('files')); $this->assertSame($updateAvailable, $installer->isUpdateAvailable('files'));
@ -700,6 +691,11 @@ JXhrdaWDZ8fzpUjugrtC3qslsqL0dzgU37anS3HwrT8=',
$this->assertTrue(file_exists(__DIR__ . '/../../apps/testapp/appinfo/info.xml')); $this->assertTrue(file_exists(__DIR__ . '/../../apps/testapp/appinfo/info.xml'));
$this->assertEquals('0.9', \OC_App::getAppVersionByPath(__DIR__ . '/../../apps/testapp/')); $this->assertEquals('0.9', \OC_App::getAppVersionByPath(__DIR__ . '/../../apps/testapp/'));
$this->appManager
->expects($this->once())
->method('getAppVersion')
->willReturn('0.9');
$installer = $this->getInstaller(); $installer = $this->getInstaller();
$installer->downloadApp('testapp'); $installer->downloadApp('testapp');
$this->assertTrue(file_exists(__DIR__ . '/../../apps/testapp/appinfo/info.xml')); $this->assertTrue(file_exists(__DIR__ . '/../../apps/testapp/appinfo/info.xml'));

@ -11,7 +11,6 @@ namespace Test\IntegrityCheck;
use OC\Core\Command\Maintenance\Mimetype\GenerateMimetypeFileBuilder; use OC\Core\Command\Maintenance\Mimetype\GenerateMimetypeFileBuilder;
use OC\Files\Type\Detection; use OC\Files\Type\Detection;
use OC\IntegrityCheck\Checker; use OC\IntegrityCheck\Checker;
use OC\IntegrityCheck\Helpers\AppLocator;
use OC\IntegrityCheck\Helpers\EnvironmentHelper; use OC\IntegrityCheck\Helpers\EnvironmentHelper;
use OC\IntegrityCheck\Helpers\FileAccessHelper; use OC\IntegrityCheck\Helpers\FileAccessHelper;
use OC\Memcache\NullCache; use OC\Memcache\NullCache;
@ -29,10 +28,6 @@ class CheckerTest extends TestCase {
private $serverVersion; private $serverVersion;
/** @var EnvironmentHelper|\PHPUnit\Framework\MockObject\MockObject */ /** @var EnvironmentHelper|\PHPUnit\Framework\MockObject\MockObject */
private $environmentHelper; private $environmentHelper;
/** @var AppLocator|\PHPUnit\Framework\MockObject\MockObject */
private $appLocator;
/** @var Checker */
private $checker;
/** @var FileAccessHelper|\PHPUnit\Framework\MockObject\MockObject */ /** @var FileAccessHelper|\PHPUnit\Framework\MockObject\MockObject */
private $fileAccessHelper; private $fileAccessHelper;
/** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */ /** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */
@ -46,12 +41,13 @@ class CheckerTest extends TestCase {
/** @var \OC\Files\Type\Detection|\PHPUnit\Framework\MockObject\MockObject */ /** @var \OC\Files\Type\Detection|\PHPUnit\Framework\MockObject\MockObject */
private $mimeTypeDetector; private $mimeTypeDetector;
private Checker $checker;
protected function setUp(): void { protected function setUp(): void {
parent::setUp(); parent::setUp();
$this->serverVersion = $this->createMock(ServerVersion::class); $this->serverVersion = $this->createMock(ServerVersion::class);
$this->environmentHelper = $this->createMock(EnvironmentHelper::class); $this->environmentHelper = $this->createMock(EnvironmentHelper::class);
$this->fileAccessHelper = $this->createMock(FileAccessHelper::class); $this->fileAccessHelper = $this->createMock(FileAccessHelper::class);
$this->appLocator = $this->createMock(AppLocator::class);
$this->config = $this->createMock(IConfig::class); $this->config = $this->createMock(IConfig::class);
$this->appConfig = $this->createMock(IAppConfig::class); $this->appConfig = $this->createMock(IAppConfig::class);
$this->cacheFactory = $this->createMock(ICacheFactory::class); $this->cacheFactory = $this->createMock(ICacheFactory::class);
@ -71,7 +67,6 @@ class CheckerTest extends TestCase {
$this->serverVersion, $this->serverVersion,
$this->environmentHelper, $this->environmentHelper,
$this->fileAccessHelper, $this->fileAccessHelper,
$this->appLocator,
$this->config, $this->config,
$this->appConfig, $this->appConfig,
$this->cacheFactory, $this->cacheFactory,
@ -186,7 +181,7 @@ class CheckerTest extends TestCase {
->with('integrity.check.disabled', false) ->with('integrity.check.disabled', false)
->willReturn(false); ->willReturn(false);
$this->appLocator $this->appManager
->expects($this->once()) ->expects($this->once())
->method('getAppPath') ->method('getAppPath')
->with('SomeApp') ->with('SomeApp')
@ -221,7 +216,7 @@ class CheckerTest extends TestCase {
->with('integrity.check.disabled', false) ->with('integrity.check.disabled', false)
->willReturn(false); ->willReturn(false);
$this->appLocator $this->appManager
->expects($this->once()) ->expects($this->once())
->method('getAppPath') ->method('getAppPath')
->with('SomeApp') ->with('SomeApp')
@ -262,7 +257,7 @@ class CheckerTest extends TestCase {
->with('integrity.check.disabled', false) ->with('integrity.check.disabled', false)
->willReturn(false); ->willReturn(false);
$this->appLocator $this->appManager
->expects($this->once()) ->expects($this->once())
->method('getAppPath') ->method('getAppPath')
->with('SomeApp') ->with('SomeApp')
@ -319,7 +314,7 @@ class CheckerTest extends TestCase {
->with('integrity.check.disabled', false) ->with('integrity.check.disabled', false)
->willReturn(false); ->willReturn(false);
$this->appLocator $this->appManager
->expects($this->never()) ->expects($this->never())
->method('getAppPath') ->method('getAppPath')
->with('SomeApp'); ->with('SomeApp');
@ -374,7 +369,7 @@ class CheckerTest extends TestCase {
->with('integrity.check.disabled', false) ->with('integrity.check.disabled', false)
->willReturn(false); ->willReturn(false);
$this->appLocator $this->appManager
->expects($this->once()) ->expects($this->once())
->method('getAppPath') ->method('getAppPath')
->with('SomeApp') ->with('SomeApp')
@ -415,7 +410,7 @@ class CheckerTest extends TestCase {
->with('integrity.check.disabled', false) ->with('integrity.check.disabled', false)
->willReturn(false); ->willReturn(false);
$this->appLocator $this->appManager
->expects($this->once()) ->expects($this->once())
->method('getAppPath') ->method('getAppPath')
->with('SomeApp') ->with('SomeApp')
@ -984,7 +979,6 @@ class CheckerTest extends TestCase {
$this->serverVersion, $this->serverVersion,
$this->environmentHelper, $this->environmentHelper,
$this->fileAccessHelper, $this->fileAccessHelper,
$this->appLocator,
$this->config, $this->config,
$this->appConfig, $this->appConfig,
$this->cacheFactory, $this->cacheFactory,
@ -1032,7 +1026,7 @@ class CheckerTest extends TestCase {
$this->assertSame($expected, $app); $this->assertSame($expected, $app);
return []; return [];
}); });
$this->appLocator $this->appManager
->expects($this->exactly(2)) ->expects($this->exactly(2))
->method('getAppPath') ->method('getAppPath')
->willReturnMap([ ->willReturnMap([

@ -1,34 +0,0 @@
<?php
/**
* SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only
*/
namespace Test\IntegrityCheck\Helpers;
use OC\IntegrityCheck\Helpers\AppLocator;
use Test\TestCase;
class AppLocatorTest extends TestCase {
/** @var AppLocator */
private $locator;
protected function setUp(): void {
parent::setUp();
$this->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');
}
}

@ -87,7 +87,7 @@ class CSSResourceLocatorTest extends \Test\TestCase {
symlink($apps_dirname, $new_apps_path_symlink); symlink($apps_dirname, $new_apps_path_symlink);
// Create an app within that path // 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 // Use the symlink as the app path
\OC::$APPSROOTS[] = [ \OC::$APPSROOTS[] = [
@ -97,7 +97,7 @@ class CSSResourceLocatorTest extends \Test\TestCase {
]; ];
$locator = $this->cssResourceLocator(); $locator = $this->cssResourceLocator();
$locator->find(['test-css-app/test-file']); $locator->find(['test_css_app/test-file']);
$resources = $locator->getResources(); $resources = $locator->getResources();
$this->assertCount(1, $resources); $this->assertCount(1, $resources);
@ -107,8 +107,8 @@ class CSSResourceLocatorTest extends \Test\TestCase {
$webRoot = $resource[1]; $webRoot = $resource[1];
$file = $resource[2]; $file = $resource[2];
$expectedRoot = $new_apps_path . '/test-css-app'; $expectedRoot = $new_apps_path . '/test_css_app';
$expectedWebRoot = \OC::$WEBROOT . '/css-apps-test/test-css-app'; $expectedWebRoot = \OC::$WEBROOT . '/css-apps-test/test_css_app';
$expectedFile = 'test-file.css'; $expectedFile = 'test-file.css';
$this->assertEquals($expectedRoot, $root, $this->assertEquals($expectedRoot, $root,

@ -11,6 +11,7 @@ namespace Test;
use OC\Installer; use OC\Installer;
use OC\IntegrityCheck\Checker; use OC\IntegrityCheck\Checker;
use OC\Updater; use OC\Updater;
use OCP\App\IAppManager;
use OCP\IAppConfig; use OCP\IAppConfig;
use OCP\IConfig; use OCP\IConfig;
use OCP\ServerVersion; use OCP\ServerVersion;
@ -32,6 +33,7 @@ class UpdaterTest extends TestCase {
private $checker; private $checker;
/** @var Installer|MockObject */ /** @var Installer|MockObject */
private $installer; private $installer;
private IAppManager&MockObject $appManager;
protected function setUp(): void { protected function setUp(): void {
parent::setUp(); parent::setUp();
@ -41,6 +43,7 @@ class UpdaterTest extends TestCase {
$this->logger = $this->createMock(LoggerInterface::class); $this->logger = $this->createMock(LoggerInterface::class);
$this->checker = $this->createMock(Checker::class); $this->checker = $this->createMock(Checker::class);
$this->installer = $this->createMock(Installer::class); $this->installer = $this->createMock(Installer::class);
$this->appManager = $this->createMock(IAppManager::class);
$this->updater = new Updater( $this->updater = new Updater(
$this->serverVersion, $this->serverVersion,
@ -48,7 +51,8 @@ class UpdaterTest extends TestCase {
$this->appConfig, $this->appConfig,
$this->checker, $this->checker,
$this->logger, $this->logger,
$this->installer $this->installer,
$this->appManager,
); );
} }