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)) {