Browse Source

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

feat/provide-server-version-capability
Kate 2 months ago
committed by GitHub
parent
commit
4edfef4dd5
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 17
      apps/settings/lib/Controller/AppSettingsController.php
  2. 14
      build/psalm-baseline.xml
  3. 5
      core/Command/Integrity/CheckApp.php
  4. 13
      core/ajax/update.php
  5. 1
      lib/composer/composer/autoload_classmap.php
  6. 1
      lib/composer/composer/autoload_static.php
  7. 196
      lib/private/App/AppManager.php
  8. 92
      lib/private/App/DependencyAnalyzer.php
  9. 17
      lib/private/DB/MigrationService.php
  10. 188
      lib/private/Installer.php
  11. 6
      lib/private/IntegrityCheck/Checker.php
  12. 33
      lib/private/IntegrityCheck/Helpers/AppLocator.php
  13. 14
      lib/private/Server.php
  14. 3
      lib/private/Setup.php
  15. 49
      lib/private/Updater.php
  16. 232
      lib/private/legacy/OC_App.php
  17. 35
      lib/public/App/IAppManager.php
  18. 1
      tests/data/app/expected-info.json
  19. 1
      tests/data/app/invalid-info.xml
  20. 1
      tests/data/app/valid-info.xml
  21. 10
      tests/enable_all.php
  22. 27
      tests/lib/App/AppManagerTest.php
  23. 300
      tests/lib/App/DependencyAnalyzerTest.php
  24. 284
      tests/lib/AppTest.php
  25. 7
      tests/lib/DB/MigrationsTest.php
  26. 44
      tests/lib/InstallerTest.php
  27. 24
      tests/lib/IntegrityCheck/CheckerTest.php
  28. 34
      tests/lib/IntegrityCheck/Helpers/AppLocatorTest.php
  29. 8
      tests/lib/Template/CSSResourceLocatorTest.php
  30. 6
      tests/lib/UpdaterTest.php

17
apps/settings/lib/Controller/AppSettingsController.php

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

14
build/psalm-baseline.xml

@ -3317,14 +3317,6 @@
<code><![CDATA[ActivitySettings[]]]></code>
</MoreSpecificReturnType>
</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">
<UndefinedMethod>
<code><![CDATA[getName]]></code>
@ -4011,12 +4003,6 @@
<code><![CDATA[query]]></code>
</UndefinedInterfaceMethod>
</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">
<InvalidArrayAccess>
<code><![CDATA[$x509->getDN(X509::DN_OPENSSL)['CN']]]></code>

5
core/Command/Integrity/CheckApp.php

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

13
core/ajax/update.php

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

1
lib/composer/composer/autoload_classmap.php

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

1
lib/composer/composer/autoload_static.php

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

196
lib/private/App/AppManager.php

@ -10,12 +10,15 @@ namespace OC\App;
use OC\AppConfig;
use OC\AppFramework\Bootstrap\Coordinator;
use OC\Config\ConfigManager;
use OC\DB\MigrationService;
use OCP\Activity\IManager as IActivityManager;
use OCP\App\AppPathNotFoundException;
use OCP\App\Events\AppDisableEvent;
use OCP\App\Events\AppEnableEvent;
use OCP\App\Events\AppUpdateEvent;
use OCP\App\IAppManager;
use OCP\App\ManagerEvent;
use OCP\BackgroundJob\IJobList;
use OCP\Collaboration\AutoComplete\IManager as IAutoCompleteManager;
use OCP\Collaboration\Collaborators\ISearch as ICollaboratorSearch;
use OCP\Diagnostics\IEventLogger;
@ -86,6 +89,7 @@ class AppManager implements IAppManager {
private LoggerInterface $logger,
private ServerVersion $serverVersion,
private ConfigManager $configManager,
private DependencyAnalyzer $dependencyAnalyzer,
) {
}
@ -249,9 +253,14 @@ class AppManager implements IAppManager {
foreach ($apps as $app) {
// If the app is already loaded then autoloading it makes no sense
if (!$this->isAppLoaded($app)) {
$path = \OC_App::getAppPath($app);
if ($path !== false) {
try {
$path = $this->getAppPath($app);
\OC_App::registerAutoloading($app, $path);
} catch (AppPathNotFoundException $e) {
$this->logger->info('Error during app loading: ' . $e->getMessage(), [
'exception' => $e,
'app' => $app,
]);
}
}
}
@ -447,8 +456,13 @@ class AppManager implements IAppManager {
return;
}
$this->loadedApps[$app] = true;
$appPath = \OC_App::getAppPath($app);
if ($appPath === false) {
try {
$appPath = $this->getAppPath($app);
} catch (AppPathNotFoundException $e) {
$this->logger->info('Error during app loading: ' . $e->getMessage(), [
'exception' => $e,
'app' => $app,
]);
return;
}
$eventLogger = \OC::$server->get(IEventLogger::class);
@ -675,29 +689,87 @@ class AppManager implements IAppManager {
/**
* Get the directory for the given app.
*
* @psalm-taint-specialize
*
* @throws AppPathNotFoundException if app folder can't be found
*/
public function getAppPath(string $appId): string {
$appPath = \OC_App::getAppPath($appId);
if ($appPath === false) {
throw new AppPathNotFoundException('Could not find path for ' . $appId);
public function getAppPath(string $appId, bool $ignoreCache = false): string {
$appId = $this->cleanAppId($appId);
if ($appId === '') {
throw new AppPathNotFoundException('App id is empty');
} elseif ($appId === 'core') {
return __DIR__ . '/../../../core';
}
return $appPath;
if (($dir = $this->findAppInDirectories($appId, $ignoreCache)) != false) {
return $dir['path'] . '/' . $appId;
}
throw new AppPathNotFoundException('Could not find path for ' . $appId);
}
/**
* Get the web path for the given app.
*
* @param string $appId
* @return string
* @throws AppPathNotFoundException if app path can't be found
*/
public function getAppWebPath(string $appId): string {
$appWebPath = \OC_App::getAppWebPath($appId);
if ($appWebPath === false) {
throw new AppPathNotFoundException('Could not find web path for ' . $appId);
if (($dir = $this->findAppInDirectories($appId)) != false) {
return \OC::$WEBROOT . $dir['url'] . '/' . $appId;
}
throw new AppPathNotFoundException('Could not find web path for ' . $appId);
}
/**
* Find the apps root for an app id.
*
* If multiple copies are found, the apps root the latest version is returned.
*
* @param bool $ignoreCache ignore cache and rebuild it
* @return false|array{path: string, url: string} the apps root shape
*/
public function findAppInDirectories(string $appId, bool $ignoreCache = false) {
$sanitizedAppId = $this->cleanAppId($appId);
if ($sanitizedAppId !== $appId) {
return false;
}
// FIXME replace by a property or a cache
static $app_dir = [];
if (isset($app_dir[$appId]) && !$ignoreCache) {
return $app_dir[$appId];
}
$possibleApps = [];
foreach (\OC::$APPSROOTS as $dir) {
if (file_exists($dir['path'] . '/' . $appId)) {
$possibleApps[] = $dir;
}
}
if (empty($possibleApps)) {
return false;
} elseif (count($possibleApps) === 1) {
$dir = array_shift($possibleApps);
$app_dir[$appId] = $dir;
return $dir;
} else {
$versionToLoad = [];
foreach ($possibleApps as $possibleApp) {
$appData = $this->getAppInfoByPath($possibleApp['path'] . '/' . $appId . '/appinfo/info.xml');
$version = $appData['version'] ?? '';
if (empty($versionToLoad) || version_compare($version, $versionToLoad['version'], '>')) {
$versionToLoad = [
'dir' => $possibleApp,
'version' => $version,
];
}
}
if (!isset($versionToLoad['dir'])) {
return false;
}
$app_dir[$appId] = $versionToLoad['dir'];
return $versionToLoad['dir'];
}
return $appWebPath;
}
/**
@ -724,7 +796,7 @@ class AppManager implements IAppManager {
if ($appDbVersion
&& isset($appInfo['version'])
&& version_compare($appInfo['version'], $appDbVersion, '>')
&& \OC_App::isAppCompatible($version, $appInfo)
&& $this->isAppCompatible($version, $appInfo)
) {
$appsToUpgrade[] = $appInfo;
}
@ -814,7 +886,7 @@ class AppManager implements IAppManager {
$info = $this->getAppInfo($appId);
if ($info === null) {
$incompatibleApps[] = ['id' => $appId, 'name' => $appId];
} elseif (!\OC_App::isAppCompatible($version, $info)) {
} elseif (!$this->isAppCompatible($version, $info)) {
$incompatibleApps[] = $info;
}
}
@ -949,4 +1021,94 @@ class AppManager implements IAppManager {
/* Only lowercase alphanumeric is allowed */
return preg_replace('/(^[0-9_]|[^a-z0-9_]+|_$)/', '', $app);
}
/**
* Run upgrade tasks for an app after the code has already been updated
*
* @throws AppPathNotFoundException if app folder can't be found
*/
public function upgradeApp(string $appId): bool {
// for apps distributed with core, we refresh app path in case the downloaded version
// have been installed in custom apps and not in the default path
$appPath = $this->getAppPath($appId, true);
$this->clearAppsCache();
$l = \OC::$server->getL10N('core');
$appData = $this->getAppInfo($appId, false, $l->getLanguageCode());
if ($appData === null) {
throw new AppPathNotFoundException('Could not find ' . $appId);
}
$ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
$ignoreMax = in_array($appId, $ignoreMaxApps, true);
\OC_App::checkAppDependencies(
$this->config,
$l,
$appData,
$ignoreMax
);
\OC_App::registerAutoloading($appId, $appPath, true);
\OC_App::executeRepairSteps($appId, $appData['repair-steps']['pre-migration']);
$ms = new MigrationService($appId, Server::get(\OC\DB\Connection::class));
$ms->migrate();
\OC_App::executeRepairSteps($appId, $appData['repair-steps']['post-migration']);
$queue = Server::get(IJobList::class);
foreach ($appData['repair-steps']['live-migration'] as $step) {
$queue->add(\OC\Migration\BackgroundRepair::class, [
'app' => $appId,
'step' => $step]);
}
// update appversion in app manager
$this->clearAppsCache();
$this->getAppVersion($appId, false);
// Setup background jobs
foreach ($appData['background-jobs'] as $job) {
$queue->add($job);
}
//set remote/public handlers
foreach ($appData['remote'] as $name => $path) {
$this->config->setAppValue('core', 'remote_' . $name, $appId . '/' . $path);
}
foreach ($appData['public'] as $name => $path) {
$this->config->setAppValue('core', 'public_' . $name, $appId . '/' . $path);
}
\OC_App::setAppTypes($appId);
$version = $this->getAppVersion($appId);
$this->config->setAppValue($appId, 'installed_version', $version);
// migrate eventual new config keys in the process
/** @psalm-suppress InternalMethod */
$this->configManager->migrateConfigLexiconKeys($appId);
$this->dispatcher->dispatchTyped(new AppUpdateEvent($appId));
$this->dispatcher->dispatch(ManagerEvent::EVENT_APP_UPDATE, new ManagerEvent(
ManagerEvent::EVENT_APP_UPDATE, $appId
));
return true;
}
public function isUpgradeRequired(string $appId): bool {
$versions = $this->getAppInstalledVersions();
$currentVersion = $this->getAppVersion($appId);
if ($currentVersion && isset($versions[$appId])) {
$installedVersion = $versions[$appId];
if (!version_compare($currentVersion, $installedVersion, '=')) {
return true;
}
}
return false;
}
public function isAppCompatible(string $serverVersion, array $appInfo, bool $ignoreMax = false): bool {
return count($this->dependencyAnalyzer->analyzeServerVersion($serverVersion, $appInfo, $ignoreMax)) === 0;
}
}

92
lib/private/App/DependencyAnalyzer.php

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

17
lib/private/DB/MigrationService.php

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

188
lib/private/Installer.php

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

6
lib/private/IntegrityCheck/Checker.php

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

33
lib/private/IntegrityCheck/Helpers/AppLocator.php

@ -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;
}
}

14
lib/private/Server.php

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

3
lib/private/Setup.php

@ -443,7 +443,8 @@ class Setup {
// Install shipped apps and specified app bundles
$this->outputDebug($output, 'Install default apps');
Installer::installShippedApps(false, $output);
$installer = Server::get(Installer::class);
$installer->installShippedApps(false, $output);
// create empty file in data dir, so we can later find
// out that this is indeed a Nextcloud data directory

49
lib/private/Updater.php

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

232
lib/private/legacy/OC_App.php

@ -6,17 +6,14 @@ declare(strict_types=1);
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only
*/
use OC\App\AppManager;
use OC\App\DependencyAnalyzer;
use OC\App\Platform;
use OC\AppFramework\Bootstrap\Coordinator;
use OC\Config\ConfigManager;
use OC\DB\MigrationService;
use OC\Installer;
use OC\Repair;
use OC\Repair\Events\RepairErrorEvent;
use OCP\App\Events\AppUpdateEvent;
use OCP\App\AppPathNotFoundException;
use OCP\App\IAppManager;
use OCP\App\ManagerEvent;
use OCP\Authentication\IAlternativeLogin;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IAppConfig;
@ -205,6 +202,7 @@ class OC_App {
* @param array $groups (optional) when set, only these groups will have access to the app
* @throws \Exception
* @return void
* @deprecated 32.0.0 Use the installer and the app manager instead
*
* This function set an app as enabled in appconfig.
*/
@ -242,49 +240,12 @@ class OC_App {
*
* If multiple copies are found, the apps root the latest version is returned.
*
* @param string $appId
* @param bool $ignoreCache ignore cache and rebuild it
* @return false|array{path: string, url: string} the apps root shape
* @deprecated 32.0.0 internal, use getAppPath or getAppWebPath
*/
public static function findAppInDirectories(string $appId, bool $ignoreCache = false) {
$sanitizedAppId = self::cleanAppId($appId);
if ($sanitizedAppId !== $appId) {
return false;
}
static $app_dir = [];
if (isset($app_dir[$appId]) && !$ignoreCache) {
return $app_dir[$appId];
}
$possibleApps = [];
foreach (OC::$APPSROOTS as $dir) {
if (file_exists($dir['path'] . '/' . $appId)) {
$possibleApps[] = $dir;
}
}
if (empty($possibleApps)) {
return false;
} elseif (count($possibleApps) === 1) {
$dir = array_shift($possibleApps);
$app_dir[$appId] = $dir;
return $dir;
} else {
$versionToLoad = [];
foreach ($possibleApps as $possibleApp) {
$version = self::getAppVersionByPath($possibleApp['path'] . '/' . $appId);
if (empty($versionToLoad) || version_compare($version, $versionToLoad['version'], '>')) {
$versionToLoad = [
'dir' => $possibleApp,
'version' => $version,
];
}
}
$app_dir[$appId] = $versionToLoad['dir'];
return $versionToLoad['dir'];
//TODO - write test
}
return Server::get(AppManager::class)->findAppInDirectories($appId, $ignoreCache);
}
/**
@ -299,17 +260,11 @@ class OC_App {
* @deprecated 11.0.0 use Server::get(IAppManager)->getAppPath()
*/
public static function getAppPath(string $appId, bool $refreshAppPath = false) {
$appId = self::cleanAppId($appId);
if ($appId === '') {
try {
return Server::get(IAppManager::class)->getAppPath($appId, $refreshAppPath);
} catch (AppPathNotFoundException) {
return false;
} elseif ($appId === 'core') {
return __DIR__ . '/../../../core';
}
if (($dir = self::findAppInDirectories($appId, $refreshAppPath)) != false) {
return $dir['path'] . '/' . $appId;
}
return false;
}
/**
@ -318,20 +273,20 @@ class OC_App {
*
* @param string $appId
* @return string|false
* @deprecated 18.0.0 use \OC::$server->getAppManager()->getAppWebPath()
* @deprecated 18.0.0 use Server::get(IAppManager)->getAppWebPath()
*/
public static function getAppWebPath(string $appId) {
if (($dir = self::findAppInDirectories($appId)) != false) {
return OC::$WEBROOT . $dir['url'] . '/' . $appId;
try {
return Server::get(IAppManager::class)->getAppWebPath($appId);
} catch (AppPathNotFoundException) {
return false;
}
return false;
}
/**
* get app's version based on it's path
*
* @param string $path
* @return string
* @deprecated 32.0.0 use Server::get(IAppManager)->getAppInfoByPath() with the path to info.xml directly
*/
public static function getAppVersionByPath(string $path): string {
$infoFile = $path . '/appinfo/info.xml';
@ -542,38 +497,11 @@ class OC_App {
return $appList;
}
public static function shouldUpgrade(string $app): bool {
$versions = self::getAppVersions();
$currentVersion = Server::get(\OCP\App\IAppManager::class)->getAppVersion($app);
if ($currentVersion && isset($versions[$app])) {
$installedVersion = $versions[$app];
if (!version_compare($currentVersion, $installedVersion, '=')) {
return true;
}
}
return false;
}
/**
* Adjust the number of version parts of $version1 to match
* the number of version parts of $version2.
*
* @param string $version1 version to adjust
* @param string $version2 version to take the number of parts from
* @return string shortened $version1
* @deprecated 32.0.0 Use IAppManager::isUpgradeRequired instead
*/
private static function adjustVersionParts(string $version1, string $version2): string {
$version1 = explode('.', $version1);
$version2 = explode('.', $version2);
// reduce $version1 to match the number of parts in $version2
while (count($version1) > count($version2)) {
array_pop($version1);
}
// if $version1 does not have enough parts, add some
while (count($version1) < count($version2)) {
$version1[] = '0';
}
return implode('.', $version1);
public static function shouldUpgrade(string $app): bool {
return Server::get(\OCP\App\IAppManager::class)->isUpgradeRequired($app);
}
/**
@ -590,42 +518,11 @@ class OC_App {
* @param string $ocVersion Nextcloud version to check against
* @param array $appInfo app info (from xml)
*
* @return boolean true if compatible, otherwise false
* @return bool true if compatible, otherwise false
* @deprecated 32.0.0 Use IAppManager::isAppCompatible instead
*/
public static function isAppCompatible(string $ocVersion, array $appInfo, bool $ignoreMax = false): bool {
$requireMin = '';
$requireMax = '';
if (isset($appInfo['dependencies']['nextcloud']['@attributes']['min-version'])) {
$requireMin = $appInfo['dependencies']['nextcloud']['@attributes']['min-version'];
} elseif (isset($appInfo['dependencies']['owncloud']['@attributes']['min-version'])) {
$requireMin = $appInfo['dependencies']['owncloud']['@attributes']['min-version'];
} elseif (isset($appInfo['requiremin'])) {
$requireMin = $appInfo['requiremin'];
} elseif (isset($appInfo['require'])) {
$requireMin = $appInfo['require'];
}
if (isset($appInfo['dependencies']['nextcloud']['@attributes']['max-version'])) {
$requireMax = $appInfo['dependencies']['nextcloud']['@attributes']['max-version'];
} elseif (isset($appInfo['dependencies']['owncloud']['@attributes']['max-version'])) {
$requireMax = $appInfo['dependencies']['owncloud']['@attributes']['max-version'];
} elseif (isset($appInfo['requiremax'])) {
$requireMax = $appInfo['requiremax'];
}
if (!empty($requireMin)
&& version_compare(self::adjustVersionParts($ocVersion, $requireMin), $requireMin, '<')
) {
return false;
}
if (!$ignoreMax && !empty($requireMax)
&& version_compare(self::adjustVersionParts($ocVersion, $requireMax), $requireMax, '>')
) {
return false;
}
return true;
return Server::get(\OCP\App\IAppManager::class)->isAppCompatible($ocVersion, $appInfo, $ignoreMax);
}
/**
@ -639,77 +536,14 @@ class OC_App {
/**
* update the database for the app and call the update script
*
* @param string $appId
* @return bool
* @deprecated 32.0.0 Use IAppManager::upgradeApp instead
*/
public static function updateApp(string $appId): bool {
// for apps distributed with core, we refresh app path in case the downloaded version
// have been installed in custom apps and not in the default path
$appPath = self::getAppPath($appId, true);
if ($appPath === false) {
try {
return Server::get(\OC\App\AppManager::class)->upgradeApp($appId);
} catch (\OCP\App\AppPathNotFoundException $e) {
return false;
}
if (is_file($appPath . '/appinfo/database.xml')) {
Server::get(LoggerInterface::class)->error('The appinfo/database.xml file is not longer supported. Used in ' . $appId);
return false;
}
\OC::$server->getAppManager()->clearAppsCache();
$l = \OC::$server->getL10N('core');
$appData = Server::get(\OCP\App\IAppManager::class)->getAppInfo($appId, false, $l->getLanguageCode());
$ignoreMaxApps = \OC::$server->getConfig()->getSystemValue('app_install_overwrite', []);
$ignoreMax = in_array($appId, $ignoreMaxApps, true);
\OC_App::checkAppDependencies(
\OC::$server->getConfig(),
$l,
$appData,
$ignoreMax
);
self::registerAutoloading($appId, $appPath, true);
self::executeRepairSteps($appId, $appData['repair-steps']['pre-migration']);
$ms = new MigrationService($appId, \OC::$server->get(\OC\DB\Connection::class));
$ms->migrate();
self::executeRepairSteps($appId, $appData['repair-steps']['post-migration']);
self::setupLiveMigrations($appId, $appData['repair-steps']['live-migration']);
// update appversion in app manager
\OC::$server->getAppManager()->clearAppsCache();
\OC::$server->getAppManager()->getAppVersion($appId, false);
self::setupBackgroundJobs($appData['background-jobs']);
//set remote/public handlers
if (array_key_exists('ocsid', $appData)) {
\OC::$server->getConfig()->setAppValue($appId, 'ocsid', $appData['ocsid']);
} elseif (\OC::$server->getConfig()->getAppValue($appId, 'ocsid') !== '') {
\OC::$server->getConfig()->deleteAppValue($appId, 'ocsid');
}
foreach ($appData['remote'] as $name => $path) {
\OC::$server->getConfig()->setAppValue('core', 'remote_' . $name, $appId . '/' . $path);
}
foreach ($appData['public'] as $name => $path) {
\OC::$server->getConfig()->setAppValue('core', 'public_' . $name, $appId . '/' . $path);
}
self::setAppTypes($appId);
$version = Server::get(\OCP\App\IAppManager::class)->getAppVersion($appId);
\OC::$server->getConfig()->setAppValue($appId, 'installed_version', $version);
// migrate eventual new config keys in the process
/** @psalm-suppress InternalMethod */
Server::get(ConfigManager::class)->migrateConfigLexiconKeys($appId);
\OC::$server->get(IEventDispatcher::class)->dispatchTyped(new AppUpdateEvent($appId));
\OC::$server->get(IEventDispatcher::class)->dispatch(ManagerEvent::EVENT_APP_UPDATE, new ManagerEvent(
ManagerEvent::EVENT_APP_UPDATE, $appId
));
return true;
}
/**
@ -740,6 +574,9 @@ class OC_App {
$r->run();
}
/**
* @deprecated 32.0.0 Use the IJobList directly instead
*/
public static function setupBackgroundJobs(array $jobs) {
$queue = \OC::$server->getJobList();
foreach ($jobs as $job) {
@ -747,19 +584,6 @@ class OC_App {
}
}
/**
* @param string $appId
* @param string[] $steps
*/
private static function setupLiveMigrations(string $appId, array $steps) {
$queue = \OC::$server->getJobList();
foreach ($steps as $step) {
$queue->add('OC\Migration\BackgroundRepair', [
'app' => $appId,
'step' => $step]);
}
}
/**
* @param \OCP\IConfig $config
* @param \OCP\IL10N $l
@ -767,7 +591,7 @@ class OC_App {
* @throws \Exception
*/
public static function checkAppDependencies(\OCP\IConfig $config, \OCP\IL10N $l, array $info, bool $ignoreMax) {
$dependencyAnalyzer = new DependencyAnalyzer(new Platform($config), $l);
$dependencyAnalyzer = Server::get(DependencyAnalyzer::class);
$missing = $dependencyAnalyzer->analyze($info, $ignoreMax);
if (!empty($missing)) {
$missingMsg = implode(PHP_EOL, $missing);

35
lib/public/App/IAppManager.php

@ -166,9 +166,10 @@ interface IAppManager {
* Get the directory for the given app.
*
* @since 11.0.0
* @since 32.0.0 Added param $ignoreCache to ignore cache
* @throws AppPathNotFoundException
*/
public function getAppPath(string $appId): string;
public function getAppPath(string $appId, bool $ignoreCache = false): string;
/**
* Get the web path for the given app.
@ -340,4 +341,36 @@ interface IAppManager {
* @since 31.0.0
*/
public function getAllAppsInAppsFolders(): array;
/**
* Run upgrade tasks for an app after the code has already been updated
*
* @throws AppPathNotFoundException if app folder can't be found
* @since 32.0.0
*/
public function upgradeApp(string $appId): bool;
/**
* Check whether the installed version is the same as the version from info.xml
*
* @since 32.0.0
*/
public function isUpgradeRequired(string $appId): bool;
/**
* Check whether the current Nextcloud version matches the given
* application's version requirements.
*
* The comparison is made based on the number of parts that the
* app info version has. For example for Nextcloud 26.0.3 if the
* app info version is expecting version 26.0, the comparison is
* made on the first two parts of the Nextcloud version.
* This means that it's possible to specify "requiremin" => 26
* and "requiremax" => 26 and it will still match Nextcloud 26.0.3.
*
* @param string $serverVersion Nextcloud version to check against
* @param array $appInfo app info (from xml)
* @since 32.0.0
*/
public function isAppCompatible(string $serverVersion, array $appInfo, bool $ignoreMax = false): bool;
}

1
tests/data/app/expected-info.json

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

1
tests/data/app/invalid-info.xml

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

1
tests/data/app/valid-info.xml

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

10
tests/enable_all.php

@ -6,10 +6,18 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
use OC\Installer;
use OCP\App\IAppManager;
use OCP\Server;
require_once __DIR__ . '/../lib/base.php';
function enableApp($app) {
(new \OC_App())->enable($app);
$installer = Server::get(Installer::class);
$appManager = Server::get(IAppManager::class);
$installer->installApp($app);
$appManager->enableApp($app);
echo "Enabled application {$app}\n";
}

27
tests/lib/App/AppManagerTest.php

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

300
tests/lib/App/DependencyAnalyzerTest.php

@ -9,18 +9,13 @@ namespace Test\App;
use OC\App\DependencyAnalyzer;
use OC\App\Platform;
use OCP\IL10N;
use Test\TestCase;
class DependencyAnalyzerTest extends TestCase {
/** @var Platform|\PHPUnit\Framework\MockObject\MockObject */
private $platformMock;
/** @var IL10N */
private $l10nMock;
/** @var DependencyAnalyzer */
private $analyser;
private DependencyAnalyzer $analyser;
protected function setUp(): void {
$this->platformMock = $this->getMockBuilder(Platform::class)
@ -55,16 +50,7 @@ class DependencyAnalyzerTest extends TestCase {
->method('getOcVersion')
->willReturn('8.0.2');
$this->l10nMock = $this->getMockBuilder(IL10N::class)
->disableOriginalConstructor()
->getMock();
$this->l10nMock->expects($this->any())
->method('t')
->willReturnCallback(function ($text, $parameters = []) {
return vsprintf($text, $parameters);
});
$this->analyser = new DependencyAnalyzer($this->platformMock, $this->l10nMock);
$this->analyser = new DependencyAnalyzer($this->platformMock);
}
/**
@ -485,4 +471,286 @@ class DependencyAnalyzerTest extends TestCase {
[[], '5.4', '5.4', null],
];
}
public static function appVersionsProvider(): array {
return [
// exact match
[
'6.0.0.0',
[
'requiremin' => '6.0',
'requiremax' => '6.0',
],
true
],
// in-between match
[
'6.0.0.0',
[
'requiremin' => '5.0',
'requiremax' => '7.0',
],
true
],
// app too old
[
'6.0.0.0',
[
'requiremin' => '5.0',
'requiremax' => '5.0',
],
false
],
// app too new
[
'5.0.0.0',
[
'requiremin' => '6.0',
'requiremax' => '6.0',
],
false
],
// only min specified
[
'6.0.0.0',
[
'requiremin' => '6.0',
],
true
],
// only min specified fail
[
'5.0.0.0',
[
'requiremin' => '6.0',
],
false
],
// only min specified legacy
[
'6.0.0.0',
[
'require' => '6.0',
],
true
],
// only min specified legacy fail
[
'4.0.0.0',
[
'require' => '6.0',
],
false
],
// only max specified
[
'5.0.0.0',
[
'requiremax' => '6.0',
],
true
],
// only max specified fail
[
'7.0.0.0',
[
'requiremax' => '6.0',
],
false
],
// variations of versions
// single OC number
[
'4',
[
'require' => '4.0',
],
true
],
// multiple OC number
[
'4.3.1',
[
'require' => '4.3',
],
true
],
// single app number
[
'4',
[
'require' => '4',
],
true
],
// single app number fail
[
'4.3',
[
'require' => '5',
],
false
],
// complex
[
'5.0.0',
[
'require' => '4.5.1',
],
true
],
// complex fail
[
'4.3.1',
[
'require' => '4.3.2',
],
false
],
// two numbers
[
'4.3.1',
[
'require' => '4.4',
],
false
],
// one number fail
[
'4.3.1',
[
'require' => '5',
],
false
],
// pre-alpha app
[
'5.0.3',
[
'require' => '4.93',
],
true
],
// pre-alpha OC
[
'6.90.0.2',
[
'require' => '6.90',
],
true
],
// pre-alpha OC max
[
'6.90.0.2',
[
'requiremax' => '7',
],
true
],
// expect same major number match
[
'5.0.3',
[
'require' => '5',
],
true
],
// expect same major number match
[
'5.0.3',
[
'requiremax' => '5',
],
true
],
// dependencies versions before require*
[
'6.0.0.0',
[
'requiremin' => '5.0',
'requiremax' => '7.0',
'dependencies' => [
'owncloud' => [
'@attributes' => [
'min-version' => '7.0',
'max-version' => '7.0',
],
],
],
],
false
],
[
'6.0.0.0',
[
'requiremin' => '5.0',
'requiremax' => '7.0',
'dependencies' => [
'owncloud' => [
'@attributes' => [
'min-version' => '5.0',
'max-version' => '5.0',
],
],
],
],
false
],
[
'6.0.0.0',
[
'requiremin' => '5.0',
'requiremax' => '5.0',
'dependencies' => [
'owncloud' => [
'@attributes' => [
'min-version' => '5.0',
'max-version' => '7.0',
],
],
],
],
true
],
[
'9.2.0.0',
[
'dependencies' => [
'owncloud' => [
'@attributes' => [
'min-version' => '9.0',
'max-version' => '9.1',
],
],
'nextcloud' => [
'@attributes' => [
'min-version' => '9.1',
'max-version' => '9.2',
],
],
],
],
true
],
[
'9.2.0.0',
[
'dependencies' => [
'nextcloud' => [
'@attributes' => [
'min-version' => '9.1',
'max-version' => '9.2',
],
],
],
],
true
],
];
}
#[\PHPUnit\Framework\Attributes\DataProvider('appVersionsProvider')]
public function testServerVersion($ncVersion, $appInfo, $expectedResult): void {
$this->assertEquals($expectedResult, count($this->analyser->analyzeServerVersion($ncVersion, $appInfo, false)) === 0);
}
}

284
tests/lib/AppTest.php

@ -9,6 +9,7 @@
namespace Test;
use OC\App\AppManager;
use OC\App\DependencyAnalyzer;
use OC\AppConfig;
use OC\Config\ConfigManager;
use OCP\EventDispatcher\IEventDispatcher;
@ -36,288 +37,6 @@ class AppTest extends \Test\TestCase {
public const TEST_GROUP1 = 'group1';
public const TEST_GROUP2 = 'group2';
public static function appVersionsProvider(): array {
return [
// exact match
[
'6.0.0.0',
[
'requiremin' => '6.0',
'requiremax' => '6.0',
],
true
],
// in-between match
[
'6.0.0.0',
[
'requiremin' => '5.0',
'requiremax' => '7.0',
],
true
],
// app too old
[
'6.0.0.0',
[
'requiremin' => '5.0',
'requiremax' => '5.0',
],
false
],
// app too new
[
'5.0.0.0',
[
'requiremin' => '6.0',
'requiremax' => '6.0',
],
false
],
// only min specified
[
'6.0.0.0',
[
'requiremin' => '6.0',
],
true
],
// only min specified fail
[
'5.0.0.0',
[
'requiremin' => '6.0',
],
false
],
// only min specified legacy
[
'6.0.0.0',
[
'require' => '6.0',
],
true
],
// only min specified legacy fail
[
'4.0.0.0',
[
'require' => '6.0',
],
false
],
// only max specified
[
'5.0.0.0',
[
'requiremax' => '6.0',
],
true
],
// only max specified fail
[
'7.0.0.0',
[
'requiremax' => '6.0',
],
false
],
// variations of versions
// single OC number
[
'4',
[
'require' => '4.0',
],
true
],
// multiple OC number
[
'4.3.1',
[
'require' => '4.3',
],
true
],
// single app number
[
'4',
[
'require' => '4',
],
true
],
// single app number fail
[
'4.3',
[
'require' => '5',
],
false
],
// complex
[
'5.0.0',
[
'require' => '4.5.1',
],
true
],
// complex fail
[
'4.3.1',
[
'require' => '4.3.2',
],
false
],
// two numbers
[
'4.3.1',
[
'require' => '4.4',
],
false
],
// one number fail
[
'4.3.1',
[
'require' => '5',
],
false
],
// pre-alpha app
[
'5.0.3',
[
'require' => '4.93',
],
true
],
// pre-alpha OC
[
'6.90.0.2',
[
'require' => '6.90',
],
true
],
// pre-alpha OC max
[
'6.90.0.2',
[
'requiremax' => '7',
],
true
],
// expect same major number match
[
'5.0.3',
[
'require' => '5',
],
true
],
// expect same major number match
[
'5.0.3',
[
'requiremax' => '5',
],
true
],
// dependencies versions before require*
[
'6.0.0.0',
[
'requiremin' => '5.0',
'requiremax' => '7.0',
'dependencies' => [
'owncloud' => [
'@attributes' => [
'min-version' => '7.0',
'max-version' => '7.0',
],
],
],
],
false
],
[
'6.0.0.0',
[
'requiremin' => '5.0',
'requiremax' => '7.0',
'dependencies' => [
'owncloud' => [
'@attributes' => [
'min-version' => '5.0',
'max-version' => '5.0',
],
],
],
],
false
],
[
'6.0.0.0',
[
'requiremin' => '5.0',
'requiremax' => '5.0',
'dependencies' => [
'owncloud' => [
'@attributes' => [
'min-version' => '5.0',
'max-version' => '7.0',
],
],
],
],
true
],
[
'9.2.0.0',
[
'dependencies' => [
'owncloud' => [
'@attributes' => [
'min-version' => '9.0',
'max-version' => '9.1',
],
],
'nextcloud' => [
'@attributes' => [
'min-version' => '9.1',
'max-version' => '9.2',
],
],
],
],
true
],
[
'9.2.0.0',
[
'dependencies' => [
'nextcloud' => [
'@attributes' => [
'min-version' => '9.1',
'max-version' => '9.2',
],
],
],
],
true
],
];
}
#[\PHPUnit\Framework\Attributes\DataProvider('appVersionsProvider')]
public function testIsAppCompatible($ocVersion, $appInfo, $expectedResult): void {
$this->assertEquals($expectedResult, \OC_App::isAppCompatible($ocVersion, $appInfo));
}
/**
* Tests that the app order is correct
*/
@ -572,6 +291,7 @@ class AppTest extends \Test\TestCase {
Server::get(LoggerInterface::class),
Server::get(ServerVersion::class),
Server::get(ConfigManager::class),
Server::get(DependencyAnalyzer::class),
));
}

7
tests/lib/DB/MigrationsTest.php

@ -20,6 +20,7 @@ use OC\DB\Connection;
use OC\DB\MigrationService;
use OC\DB\SchemaWrapper;
use OC\Migration\MetadataManager;
use OCP\App\AppPathNotFoundException;
use OCP\App\IAppManager;
use OCP\IDBConnection;
use OCP\Migration\Attributes\AddColumn;
@ -81,10 +82,10 @@ class MigrationsTest extends \Test\TestCase {
public function testUnknownApp(): void {
$this->expectException(\Exception::class);
$this->expectExceptionMessage('App not found');
$this->expectException(AppPathNotFoundException::class);
$this->expectExceptionMessage('Could not find path for unknown_bloody_app');
$migrationService = new MigrationService('unknown-bloody-app', $this->db);
$migrationService = new MigrationService('unknown_bloody_app', $this->db);
}

44
tests/lib/InstallerTest.php

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

24
tests/lib/IntegrityCheck/CheckerTest.php

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

34
tests/lib/IntegrityCheck/Helpers/AppLocatorTest.php

@ -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');
}
}

8
tests/lib/Template/CSSResourceLocatorTest.php

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

6
tests/lib/UpdaterTest.php

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

Loading…
Cancel
Save