diff --git a/apps/settings/lib/Controller/AppSettingsController.php b/apps/settings/lib/Controller/AppSettingsController.php index df563ac46b7a9..0281ca8b43b4a 100644 --- a/apps/settings/lib/Controller/AppSettingsController.php +++ b/apps/settings/lib/Controller/AppSettingsController.php @@ -347,7 +347,6 @@ public function listApps(): JSONResponse { } } $appData['groups'] = $groups; - $appData['canUnInstall'] = !$appData['active'] && $appData['removable']; // fix licence vs license if (isset($appData['license']) && !isset($appData['licence'])) { @@ -381,6 +380,14 @@ public function listApps(): JSONResponse { * @throws \Exception */ private function getAppsForCategory($requestedCategory = ''): array { + $anyAppsRootWritable = false; + foreach (\OC::$APPSROOTS as $appsRoot) { + if ($appsRoot['writable'] ?? false) { + $anyAppsRootWritable = true; + break; + } + } + $versionParser = new VersionParser(); $formattedApps = []; $apps = $this->appFetcher->get(); @@ -411,11 +418,23 @@ private function getAppsForCategory($requestedCategory = ''): array { } $phpVersion = $versionParser->getVersion($app['releases'][0]['rawPhpVersionSpec']); + $needsDownload = true; + $canUpdate = false; + $canUnInstall = false; + try { - $this->appManager->getAppPath($app['id']); - $existsLocally = true; + $appPath = $this->appManager->getAppPath($app['id']); + $needsDownload = false; + + $appRootPath = dirname($appPath); + foreach (\OC::$APPSROOTS as $appsRoot) { + if ($appsRoot['path'] === $appRootPath) { + $appsRootWritable = $appsRoot['writable'] ?? false; + $canUpdate = $appsRootWritable; + $canUnInstall = $appsRootWritable; + } + } } catch (AppPathNotFoundException) { - $existsLocally = false; } $phpDependencies = []; @@ -482,9 +501,11 @@ private function getAppsForCategory($requestedCategory = ''): array { 'score' => $app['ratingOverall'], 'ratingNumOverall' => $app['ratingNumOverall'], 'ratingNumThresholdReached' => $app['ratingNumOverall'] > 5, - 'removable' => $existsLocally, - 'active' => $this->appManager->isEnabledForUser($app['id']), - 'needsDownload' => !$existsLocally, + 'canDownload' => $anyAppsRootWritable, + 'canUpdate' => $canUpdate, + 'canUnInstall' => $canUnInstall && !$this->appManager->isShipped($app['id']), + 'active' => $this->appManager->isEnabledForAnyone($app['id']), + 'needsDownload' => $needsDownload, 'groups' => $groups, 'fromAppStore' => true, 'appstoreData' => $app, diff --git a/apps/settings/src/app-types.ts b/apps/settings/src/app-types.ts index 49f0d5a17096a..fa18112042080 100644 --- a/apps/settings/src/app-types.ts +++ b/apps/settings/src/app-types.ts @@ -44,9 +44,10 @@ export interface IAppstoreApp { app_api: boolean active: boolean internal: boolean - removable: boolean installed: boolean + canDownload: boolean canInstall: boolean + canUpdate: boolean canUnInstall: boolean isCompatible: boolean needsDownload: boolean diff --git a/apps/settings/src/components/AppList.vue b/apps/settings/src/components/AppList.vue index cfc778fe40965..2016ad1e89a20 100644 --- a/apps/settings/src/components/AppList.vue +++ b/apps/settings/src/components/AppList.vue @@ -17,6 +17,7 @@ {{ n('settings', 'Update', 'Update all', counter) }} @@ -194,6 +195,9 @@ export default { showUpdateAll() { return this.hasPendingUpdate && this.useListView }, + canUpdateAny() { + return this.apps.filter(app => app.update && app.canUpdate).length > 0 + }, apps() { // Exclude ExApps from the list if AppAPI is disabled const exApps = this.$store.getters.isAppApiEnabled ? this.appApiStore.getAllApps : [] @@ -324,7 +328,7 @@ export default { updateAll() { const limit = pLimit(1) this.apps - .filter(app => app.update) + .filter(app => app.update && app.canUpdate) .map((app) => limit(() => { this.update(app.id) })) diff --git a/apps/settings/src/components/AppList/AppItem.vue b/apps/settings/src/components/AppList/AppItem.vue index d0f39f3c74a52..8b0b5a2a2a513 100644 --- a/apps/settings/src/components/AppList/AppItem.vue +++ b/apps/settings/src/components/AppList/AppItem.vue @@ -78,15 +78,15 @@
{{ t('settings', 'Update to {update}', {update:app.update}) }} - {{ t('settings', 'Remove') }} @@ -95,22 +95,24 @@ @click.stop="disable(app.id)"> {{ disableButtonText }} - - {{ enableButtonText }} - - - {{ forceEnableButtonText }} - +
+ + {{ enableButtonText }} + + + {{ forceEnableButtonText }} + +
diff --git a/apps/settings/src/components/AppStoreSidebar/AppDetailsTab.vue b/apps/settings/src/components/AppStoreSidebar/AppDetailsTab.vue index 3aa42f1d15a0d..3b37d401219c3 100644 --- a/apps/settings/src/components/AppStoreSidebar/AppDetailsTab.vue +++ b/apps/settings/src/components/AppStoreSidebar/AppDetailsTab.vue @@ -47,13 +47,13 @@ class="update primary" type="button" :value="t('settings', 'Update to {version}', { version: app.update })" - :disabled="installing || isLoading || isManualInstall" + :disabled="installing || isLoading || isManualInstall || !app.canUpdate" @click="update(app.id)"> - - - +
+ + +
app.id === appId) app.active = false app.groups = [] - if (app.removable) { - app.canUnInstall = true - } if (app.id === 'app_api') { state.appApiEnabled = false } diff --git a/lib/private/Installer.php b/lib/private/Installer.php index 00fdd84c1bc83..a8e38b86f36a6 100644 --- a/lib/private/Installer.php +++ b/lib/private/Installer.php @@ -18,6 +18,7 @@ use OC\DB\MigrationService; use OC_App; use OC_Helper; +use OCP\App\AppPathNotFoundException; use OCP\App\IAppManager; use OCP\HintException; use OCP\Http\Client\IClientService; @@ -176,10 +177,28 @@ private function splitCerts(string $cert): array { */ public function downloadApp(string $appId, bool $allowUnstable = false): void { $appId = strtolower($appId); + $appManager = \OCP\Server::get(IAppManager::class); $apps = $this->appFetcher->get($allowUnstable); foreach ($apps as $app) { if ($app['id'] === $appId) { + try { + $appPath = $appManager->getAppPath($appId); + } catch (AppPathNotFoundException) { + $appPath = OC_App::getInstallPath() . '/' . $appId; + } + + $appsRootWritable = false; + $appRootPath = dirname($appPath); + foreach (\OC::$APPSROOTS as $appsRoot) { + if ($appsRoot['path'] === $appRootPath) { + $appsRootWritable = $appsRoot['writable'] ?? false; + } + } + if (!$appsRootWritable) { + throw new \Exception(sprintf('App %s can not be updated because the app root is not writable.', $appId)); + } + // Load the certificate $certificate = new X509(); $rootCrt = file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt'); @@ -322,15 +341,14 @@ public function downloadApp(string $appId, bool $allowUnstable = false): void { ); } - $baseDir = OC_App::getInstallPath() . '/' . $appId; // Remove old app with the ID if existent - OC_Helper::rmdirr($baseDir); + OC_Helper::rmdirr($appPath); // Move to app folder - if (@mkdir($baseDir)) { + if (@mkdir($appPath)) { $extractDir .= '/' . $folders[0]; - OC_Helper::copyr($extractDir, $baseDir); + OC_Helper::copyr($extractDir, $appPath); } - OC_Helper::copyr($extractDir, $baseDir); + OC_Helper::copyr($extractDir, $appPath); OC_Helper::rmdirr($extractDir); return; } @@ -446,11 +464,26 @@ public function isDownloaded(string $name): bool { */ public function removeApp(string $appId): bool { if ($this->isDownloaded($appId)) { - if (\OCP\Server::get(IAppManager::class)->isShipped($appId)) { + $appManager = \OCP\Server::get(IAppManager::class); + + if ($appManager->isShipped($appId)) { return false; } - $appDir = OC_App::getInstallPath() . '/' . $appId; - OC_Helper::rmdirr($appDir); + + $appPath = $appManager->getAppPath($appId); + + $appsRootWritable = false; + $appRootPath = dirname($appPath); + foreach (\OC::$APPSROOTS as $appsRoot) { + if ($appsRoot['path'] === $appRootPath) { + $appsRootWritable = $appsRoot['writable'] ?? false; + } + } + if (!$appsRootWritable) { + return false; + } + + OC_Helper::rmdirr($appPath); return true; } else { $this->logger->error('can\'t remove app ' . $appId . '. It is not installed.'); diff --git a/lib/private/legacy/OC_App.php b/lib/private/legacy/OC_App.php index 7fee946b77682..e65ab09fa7ee5 100644 --- a/lib/private/legacy/OC_App.php +++ b/lib/private/legacy/OC_App.php @@ -510,10 +510,8 @@ public function listAllApps(): array { if ($appManager->isShipped($app)) { $info['internal'] = true; $info['level'] = self::officialApp; - $info['removable'] = false; } else { $info['internal'] = false; - $info['removable'] = true; } if (in_array($app, $supportedApps)) {