Completed
Push — master ( 4b5010...5488f7 )
by
unknown
40:09 queued 01:08
created
apps/provisioning_api/tests/Controller/AppsControllerTest.php 1 patch
Indentation   +80 added lines, -80 removed lines patch added patch discarded remove patch
@@ -27,84 +27,84 @@
 block discarded – undo
27 27
  * @package OCA\Provisioning_API\Tests
28 28
  */
29 29
 class AppsControllerTest extends TestCase {
30
-	private IAppManager $appManager;
31
-	private IAppConfig&MockObject $appConfig;
32
-	private Installer&MockObject $installer;
33
-	private AppsController $api;
34
-	private IUserSession $userSession;
35
-
36
-	protected function setUp(): void {
37
-		parent::setUp();
38
-
39
-		$this->appManager = Server::get(IAppManager::class);
40
-		$this->groupManager = Server::get(IGroupManager::class);
41
-		$this->userSession = Server::get(IUserSession::class);
42
-		$this->appConfig = $this->createMock(IAppConfig::class);
43
-		$this->installer = $this->createMock(Installer::class);
44
-
45
-		$request = $this->createMock(IRequest::class);
46
-
47
-		$this->api = new AppsController(
48
-			'provisioning_api',
49
-			$request,
50
-			$this->appManager,
51
-			$this->installer,
52
-			$this->appConfig,
53
-		);
54
-	}
55
-
56
-	protected function tearDown(): void {
57
-		$this->userSession->setUser(null);
58
-	}
59
-
60
-	public function testGetAppInfo(): void {
61
-		$result = $this->api->getAppInfo('provisioning_api');
62
-		$expected = $this->appManager->getAppInfo('provisioning_api');
63
-		$this->assertEquals($expected, $result->getData());
64
-	}
65
-
66
-
67
-	public function testGetAppInfoOnBadAppID(): void {
68
-		$this->expectException(OCSException::class);
69
-		$this->expectExceptionCode(998);
70
-
71
-		$this->api->getAppInfo('not_provisioning_api');
72
-	}
73
-
74
-	public function testGetApps(): void {
75
-		$user = $this->generateUsers();
76
-		$this->groupManager->get('admin')->addUser($user);
77
-		$this->userSession->setUser($user);
78
-
79
-		$result = $this->api->getApps();
80
-
81
-		$data = $result->getData();
82
-		$this->assertEquals(count((new \OC_App())->listAllApps()), count($data['apps']));
83
-	}
84
-
85
-	public function testGetAppsEnabled(): void {
86
-		$result = $this->api->getApps('enabled');
87
-		$data = $result->getData();
88
-		$this->assertEquals(count(\OC_App::getEnabledApps()), count($data['apps']));
89
-	}
90
-
91
-	public function testGetAppsDisabled(): void {
92
-		$result = $this->api->getApps('disabled');
93
-		$data = $result->getData();
94
-		$apps = (new \OC_App)->listAllApps();
95
-		$list = [];
96
-		foreach ($apps as $app) {
97
-			$list[] = $app['id'];
98
-		}
99
-		$disabled = array_diff($list, \OC_App::getEnabledApps());
100
-		$this->assertEquals(count($disabled), count($data['apps']));
101
-	}
102
-
103
-
104
-	public function testGetAppsInvalidFilter(): void {
105
-		$this->expectException(OCSException::class);
106
-		$this->expectExceptionCode(101);
107
-
108
-		$this->api->getApps('foo');
109
-	}
30
+    private IAppManager $appManager;
31
+    private IAppConfig&MockObject $appConfig;
32
+    private Installer&MockObject $installer;
33
+    private AppsController $api;
34
+    private IUserSession $userSession;
35
+
36
+    protected function setUp(): void {
37
+        parent::setUp();
38
+
39
+        $this->appManager = Server::get(IAppManager::class);
40
+        $this->groupManager = Server::get(IGroupManager::class);
41
+        $this->userSession = Server::get(IUserSession::class);
42
+        $this->appConfig = $this->createMock(IAppConfig::class);
43
+        $this->installer = $this->createMock(Installer::class);
44
+
45
+        $request = $this->createMock(IRequest::class);
46
+
47
+        $this->api = new AppsController(
48
+            'provisioning_api',
49
+            $request,
50
+            $this->appManager,
51
+            $this->installer,
52
+            $this->appConfig,
53
+        );
54
+    }
55
+
56
+    protected function tearDown(): void {
57
+        $this->userSession->setUser(null);
58
+    }
59
+
60
+    public function testGetAppInfo(): void {
61
+        $result = $this->api->getAppInfo('provisioning_api');
62
+        $expected = $this->appManager->getAppInfo('provisioning_api');
63
+        $this->assertEquals($expected, $result->getData());
64
+    }
65
+
66
+
67
+    public function testGetAppInfoOnBadAppID(): void {
68
+        $this->expectException(OCSException::class);
69
+        $this->expectExceptionCode(998);
70
+
71
+        $this->api->getAppInfo('not_provisioning_api');
72
+    }
73
+
74
+    public function testGetApps(): void {
75
+        $user = $this->generateUsers();
76
+        $this->groupManager->get('admin')->addUser($user);
77
+        $this->userSession->setUser($user);
78
+
79
+        $result = $this->api->getApps();
80
+
81
+        $data = $result->getData();
82
+        $this->assertEquals(count((new \OC_App())->listAllApps()), count($data['apps']));
83
+    }
84
+
85
+    public function testGetAppsEnabled(): void {
86
+        $result = $this->api->getApps('enabled');
87
+        $data = $result->getData();
88
+        $this->assertEquals(count(\OC_App::getEnabledApps()), count($data['apps']));
89
+    }
90
+
91
+    public function testGetAppsDisabled(): void {
92
+        $result = $this->api->getApps('disabled');
93
+        $data = $result->getData();
94
+        $apps = (new \OC_App)->listAllApps();
95
+        $list = [];
96
+        foreach ($apps as $app) {
97
+            $list[] = $app['id'];
98
+        }
99
+        $disabled = array_diff($list, \OC_App::getEnabledApps());
100
+        $this->assertEquals(count($disabled), count($data['apps']));
101
+    }
102
+
103
+
104
+    public function testGetAppsInvalidFilter(): void {
105
+        $this->expectException(OCSException::class);
106
+        $this->expectExceptionCode(101);
107
+
108
+        $this->api->getApps('foo');
109
+    }
110 110
 }
Please login to merge, or discard this patch.
apps/provisioning_api/lib/Controller/AppsController.php 1 patch
Indentation   +119 added lines, -119 removed lines patch added patch discarded remove patch
@@ -22,132 +22,132 @@
 block discarded – undo
22 22
 use OCP\IRequest;
23 23
 
24 24
 class AppsController extends OCSController {
25
-	public function __construct(
26
-		string $appName,
27
-		IRequest $request,
28
-		private IAppManager $appManager,
29
-		private Installer $installer,
30
-		private IAppConfig $appConfig,
31
-	) {
32
-		parent::__construct($appName, $request);
33
-	}
25
+    public function __construct(
26
+        string $appName,
27
+        IRequest $request,
28
+        private IAppManager $appManager,
29
+        private Installer $installer,
30
+        private IAppConfig $appConfig,
31
+    ) {
32
+        parent::__construct($appName, $request);
33
+    }
34 34
 
35
-	/**
36
-	 * @throws \InvalidArgumentException
37
-	 */
38
-	protected function verifyAppId(string $app): string {
39
-		$cleanId = $this->appManager->cleanAppId($app);
40
-		if ($cleanId !== $app) {
41
-			throw new \InvalidArgumentException('Invalid app id given');
42
-		}
43
-		return $cleanId;
44
-	}
35
+    /**
36
+     * @throws \InvalidArgumentException
37
+     */
38
+    protected function verifyAppId(string $app): string {
39
+        $cleanId = $this->appManager->cleanAppId($app);
40
+        if ($cleanId !== $app) {
41
+            throw new \InvalidArgumentException('Invalid app id given');
42
+        }
43
+        return $cleanId;
44
+    }
45 45
 
46
-	/**
47
-	 * Get a list of installed apps
48
-	 *
49
-	 * @param ?string $filter Filter for enabled or disabled apps
50
-	 * @return DataResponse<Http::STATUS_OK, array{apps: list<string>}, array{}>
51
-	 * @throws OCSException
52
-	 *
53
-	 * 200: Installed apps returned
54
-	 */
55
-	public function getApps(?string $filter = null): DataResponse {
56
-		$apps = (new OC_App())->listAllApps();
57
-		/** @var list<string> $list */
58
-		$list = [];
59
-		foreach ($apps as $app) {
60
-			$list[] = $app['id'];
61
-		}
62
-		if ($filter) {
63
-			switch ($filter) {
64
-				case 'enabled':
65
-					return new DataResponse(['apps' => \OC_App::getEnabledApps()]);
66
-					break;
67
-				case 'disabled':
68
-					$enabled = OC_App::getEnabledApps();
69
-					return new DataResponse(['apps' => array_values(array_diff($list, $enabled))]);
70
-					break;
71
-				default:
72
-					// Invalid filter variable
73
-					throw new OCSException('', 101);
74
-			}
75
-		} else {
76
-			return new DataResponse(['apps' => $list]);
77
-		}
78
-	}
46
+    /**
47
+     * Get a list of installed apps
48
+     *
49
+     * @param ?string $filter Filter for enabled or disabled apps
50
+     * @return DataResponse<Http::STATUS_OK, array{apps: list<string>}, array{}>
51
+     * @throws OCSException
52
+     *
53
+     * 200: Installed apps returned
54
+     */
55
+    public function getApps(?string $filter = null): DataResponse {
56
+        $apps = (new OC_App())->listAllApps();
57
+        /** @var list<string> $list */
58
+        $list = [];
59
+        foreach ($apps as $app) {
60
+            $list[] = $app['id'];
61
+        }
62
+        if ($filter) {
63
+            switch ($filter) {
64
+                case 'enabled':
65
+                    return new DataResponse(['apps' => \OC_App::getEnabledApps()]);
66
+                    break;
67
+                case 'disabled':
68
+                    $enabled = OC_App::getEnabledApps();
69
+                    return new DataResponse(['apps' => array_values(array_diff($list, $enabled))]);
70
+                    break;
71
+                default:
72
+                    // Invalid filter variable
73
+                    throw new OCSException('', 101);
74
+            }
75
+        } else {
76
+            return new DataResponse(['apps' => $list]);
77
+        }
78
+    }
79 79
 
80
-	/**
81
-	 * Get the app info for an app
82
-	 *
83
-	 * @param string $app ID of the app
84
-	 * @return DataResponse<Http::STATUS_OK, array<string, ?mixed>, array{}>
85
-	 * @throws OCSException
86
-	 *
87
-	 * 200: App info returned
88
-	 */
89
-	public function getAppInfo(string $app): DataResponse {
90
-		try {
91
-			$app = $this->verifyAppId($app);
92
-		} catch (\InvalidArgumentException $e) {
93
-			throw new OCSException($e->getMessage(), OCSController::RESPOND_UNAUTHORISED);
94
-		}
95
-		$info = $this->appManager->getAppInfo($app);
96
-		if (!is_null($info)) {
97
-			return new DataResponse($info);
98
-		}
80
+    /**
81
+     * Get the app info for an app
82
+     *
83
+     * @param string $app ID of the app
84
+     * @return DataResponse<Http::STATUS_OK, array<string, ?mixed>, array{}>
85
+     * @throws OCSException
86
+     *
87
+     * 200: App info returned
88
+     */
89
+    public function getAppInfo(string $app): DataResponse {
90
+        try {
91
+            $app = $this->verifyAppId($app);
92
+        } catch (\InvalidArgumentException $e) {
93
+            throw new OCSException($e->getMessage(), OCSController::RESPOND_UNAUTHORISED);
94
+        }
95
+        $info = $this->appManager->getAppInfo($app);
96
+        if (!is_null($info)) {
97
+            return new DataResponse($info);
98
+        }
99 99
 
100
-		throw new OCSException('The request app was not found', OCSController::RESPOND_NOT_FOUND);
101
-	}
100
+        throw new OCSException('The request app was not found', OCSController::RESPOND_NOT_FOUND);
101
+    }
102 102
 
103
-	/**
104
-	 * Enable an app
105
-	 *
106
-	 * @param string $app ID of the app
107
-	 * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
108
-	 * @throws OCSException
109
-	 *
110
-	 * 200: App enabled successfully
111
-	 */
112
-	#[PasswordConfirmationRequired]
113
-	public function enable(string $app): DataResponse {
114
-		try {
115
-			$app = $this->verifyAppId($app);
103
+    /**
104
+     * Enable an app
105
+     *
106
+     * @param string $app ID of the app
107
+     * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
108
+     * @throws OCSException
109
+     *
110
+     * 200: App enabled successfully
111
+     */
112
+    #[PasswordConfirmationRequired]
113
+    public function enable(string $app): DataResponse {
114
+        try {
115
+            $app = $this->verifyAppId($app);
116 116
 
117
-			if (!$this->installer->isDownloaded($app)) {
118
-				$this->installer->downloadApp($app);
119
-			}
117
+            if (!$this->installer->isDownloaded($app)) {
118
+                $this->installer->downloadApp($app);
119
+            }
120 120
 
121
-			if ($this->appConfig->getValueString($app, 'installed_version', '') === '') {
122
-				$this->installer->installApp($app);
123
-			}
121
+            if ($this->appConfig->getValueString($app, 'installed_version', '') === '') {
122
+                $this->installer->installApp($app);
123
+            }
124 124
 
125
-			$this->appManager->enableApp($app);
126
-		} catch (\InvalidArgumentException $e) {
127
-			throw new OCSException($e->getMessage(), OCSController::RESPOND_UNAUTHORISED);
128
-		} catch (AppPathNotFoundException|AppNotFoundException $e) {
129
-			throw new OCSException('The request app was not found', OCSController::RESPOND_NOT_FOUND);
130
-		}
131
-		return new DataResponse();
132
-	}
125
+            $this->appManager->enableApp($app);
126
+        } catch (\InvalidArgumentException $e) {
127
+            throw new OCSException($e->getMessage(), OCSController::RESPOND_UNAUTHORISED);
128
+        } catch (AppPathNotFoundException|AppNotFoundException $e) {
129
+            throw new OCSException('The request app was not found', OCSController::RESPOND_NOT_FOUND);
130
+        }
131
+        return new DataResponse();
132
+    }
133 133
 
134
-	/**
135
-	 * Disable an app
136
-	 *
137
-	 * @param string $app ID of the app
138
-	 * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
139
-	 * @throws OCSException
140
-	 *
141
-	 * 200: App disabled successfully
142
-	 */
143
-	#[PasswordConfirmationRequired]
144
-	public function disable(string $app): DataResponse {
145
-		try {
146
-			$app = $this->verifyAppId($app);
147
-			$this->appManager->disableApp($app);
148
-		} catch (\InvalidArgumentException $e) {
149
-			throw new OCSException($e->getMessage(), OCSController::RESPOND_UNAUTHORISED);
150
-		}
151
-		return new DataResponse();
152
-	}
134
+    /**
135
+     * Disable an app
136
+     *
137
+     * @param string $app ID of the app
138
+     * @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
139
+     * @throws OCSException
140
+     *
141
+     * 200: App disabled successfully
142
+     */
143
+    #[PasswordConfirmationRequired]
144
+    public function disable(string $app): DataResponse {
145
+        try {
146
+            $app = $this->verifyAppId($app);
147
+            $this->appManager->disableApp($app);
148
+        } catch (\InvalidArgumentException $e) {
149
+            throw new OCSException($e->getMessage(), OCSController::RESPOND_UNAUTHORISED);
150
+        }
151
+        return new DataResponse();
152
+    }
153 153
 }
Please login to merge, or discard this patch.
lib/private/Installer.php 1 patch
Indentation   +596 added lines, -596 removed lines patch added patch discarded remove patch
@@ -34,600 +34,600 @@
 block discarded – undo
34 34
  * This class provides the functionality needed to install, update and remove apps
35 35
  */
36 36
 class Installer {
37
-	private ?bool $isInstanceReadyForUpdates = null;
38
-	private ?array $apps = null;
39
-
40
-	public function __construct(
41
-		private AppFetcher $appFetcher,
42
-		private IClientService $clientService,
43
-		private ITempManager $tempManager,
44
-		private LoggerInterface $logger,
45
-		private IConfig $config,
46
-		private bool $isCLI,
47
-	) {
48
-	}
49
-
50
-	/**
51
-	 * Installs an app that is located in one of the app folders already
52
-	 *
53
-	 * @param string $appId App to install
54
-	 * @param bool $forceEnable
55
-	 * @throws \Exception
56
-	 * @return string app ID
57
-	 */
58
-	public function installApp(string $appId, bool $forceEnable = false): string {
59
-		$app = \OC_App::findAppInDirectories($appId);
60
-		if ($app === false) {
61
-			throw new \Exception('App not found in any app directory');
62
-		}
63
-
64
-		$basedir = $app['path'] . '/' . $appId;
65
-
66
-		if (is_file($basedir . '/appinfo/database.xml')) {
67
-			throw new \Exception('The appinfo/database.xml file is not longer supported. Used in ' . $appId);
68
-		}
69
-
70
-		$l = \OCP\Util::getL10N('core');
71
-		$info = \OCP\Server::get(IAppManager::class)->getAppInfoByPath($basedir . '/appinfo/info.xml', $l->getLanguageCode());
72
-
73
-		if (!is_array($info)) {
74
-			throw new \Exception(
75
-				$l->t('App "%s" cannot be installed because appinfo file cannot be read.',
76
-					[$appId]
77
-				)
78
-			);
79
-		}
80
-
81
-		$ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
82
-		$ignoreMax = $forceEnable || in_array($appId, $ignoreMaxApps, true);
83
-
84
-		$version = implode('.', \OCP\Util::getVersion());
85
-		if (!\OC_App::isAppCompatible($version, $info, $ignoreMax)) {
86
-			throw new \Exception(
87
-				// TODO $l
88
-				$l->t('App "%s" cannot be installed because it is not compatible with this version of the server.',
89
-					[$info['name']]
90
-				)
91
-			);
92
-		}
93
-
94
-		// check for required dependencies
95
-		\OC_App::checkAppDependencies($this->config, $l, $info, $ignoreMax);
96
-		/** @var Coordinator $coordinator */
97
-		$coordinator = \OC::$server->get(Coordinator::class);
98
-		$coordinator->runLazyRegistration($appId);
99
-		\OC_App::registerAutoloading($appId, $basedir);
100
-
101
-		$previousVersion = $this->config->getAppValue($info['id'], 'installed_version', false);
102
-		if ($previousVersion) {
103
-			OC_App::executeRepairSteps($appId, $info['repair-steps']['pre-migration']);
104
-		}
105
-
106
-		//install the database
107
-		$ms = new MigrationService($info['id'], \OCP\Server::get(Connection::class));
108
-		$ms->migrate('latest', !$previousVersion);
109
-
110
-		if ($previousVersion) {
111
-			OC_App::executeRepairSteps($appId, $info['repair-steps']['post-migration']);
112
-		}
113
-
114
-		\OC_App::setupBackgroundJobs($info['background-jobs']);
115
-
116
-		//run appinfo/install.php
117
-		self::includeAppScript($basedir . '/appinfo/install.php');
118
-
119
-		OC_App::executeRepairSteps($appId, $info['repair-steps']['install']);
120
-
121
-		$config = \OCP\Server::get(IConfig::class);
122
-		//set the installed version
123
-		$config->setAppValue($info['id'], 'installed_version', \OCP\Server::get(IAppManager::class)->getAppVersion($info['id'], false));
124
-		$config->setAppValue($info['id'], 'enabled', 'no');
125
-
126
-		//set remote/public handlers
127
-		foreach ($info['remote'] as $name => $path) {
128
-			$config->setAppValue('core', 'remote_' . $name, $info['id'] . '/' . $path);
129
-		}
130
-		foreach ($info['public'] as $name => $path) {
131
-			$config->setAppValue('core', 'public_' . $name, $info['id'] . '/' . $path);
132
-		}
133
-
134
-		OC_App::setAppTypes($info['id']);
135
-
136
-		return $info['id'];
137
-	}
138
-
139
-	/**
140
-	 * Updates the specified app from the appstore
141
-	 *
142
-	 * @param bool $allowUnstable Allow unstable releases
143
-	 */
144
-	public function updateAppstoreApp(string $appId, bool $allowUnstable = false): bool {
145
-		if ($this->isUpdateAvailable($appId, $allowUnstable)) {
146
-			try {
147
-				$this->downloadApp($appId, $allowUnstable);
148
-			} catch (\Exception $e) {
149
-				$this->logger->error($e->getMessage(), [
150
-					'exception' => $e,
151
-				]);
152
-				return false;
153
-			}
154
-			return OC_App::updateApp($appId);
155
-		}
156
-
157
-		return false;
158
-	}
159
-
160
-	/**
161
-	 * Split the certificate file in individual certs
162
-	 *
163
-	 * @param string $cert
164
-	 * @return string[]
165
-	 */
166
-	private function splitCerts(string $cert): array {
167
-		preg_match_all('([\-]{3,}[\S\ ]+?[\-]{3,}[\S\s]+?[\-]{3,}[\S\ ]+?[\-]{3,})', $cert, $matches);
168
-
169
-		return $matches[0];
170
-	}
171
-
172
-	/**
173
-	 * Downloads an app and puts it into the app directory
174
-	 *
175
-	 * @param string $appId
176
-	 * @param bool [$allowUnstable]
177
-	 *
178
-	 * @throws AppNotFoundException If the app is not found on the appstore
179
-	 * @throws \Exception If the installation was not successful
180
-	 */
181
-	public function downloadApp(string $appId, bool $allowUnstable = false): void {
182
-		$appId = strtolower($appId);
183
-
184
-		$apps = $this->appFetcher->get($allowUnstable);
185
-		foreach ($apps as $app) {
186
-			if ($app['id'] === $appId) {
187
-				// Load the certificate
188
-				$certificate = new X509();
189
-				$rootCrt = file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt');
190
-				$rootCrts = $this->splitCerts($rootCrt);
191
-				foreach ($rootCrts as $rootCrt) {
192
-					$certificate->loadCA($rootCrt);
193
-				}
194
-				$loadedCertificate = $certificate->loadX509($app['certificate']);
195
-
196
-				// Verify if the certificate has been revoked
197
-				$crl = new X509();
198
-				foreach ($rootCrts as $rootCrt) {
199
-					$crl->loadCA($rootCrt);
200
-				}
201
-				$crl->loadCRL(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crl'));
202
-				if ($crl->validateSignature() !== true) {
203
-					throw new \Exception('Could not validate CRL signature');
204
-				}
205
-				$csn = $loadedCertificate['tbsCertificate']['serialNumber']->toString();
206
-				$revoked = $crl->getRevoked($csn);
207
-				if ($revoked !== false) {
208
-					throw new \Exception(
209
-						sprintf(
210
-							'Certificate "%s" has been revoked',
211
-							$csn
212
-						)
213
-					);
214
-				}
215
-
216
-				// Verify if the certificate has been issued by the Nextcloud Code Authority CA
217
-				if ($certificate->validateSignature() !== true) {
218
-					throw new \Exception(
219
-						sprintf(
220
-							'App with id %s has a certificate not issued by a trusted Code Signing Authority',
221
-							$appId
222
-						)
223
-					);
224
-				}
225
-
226
-				// Verify if the certificate is issued for the requested app id
227
-				$certInfo = openssl_x509_parse($app['certificate']);
228
-				if (!isset($certInfo['subject']['CN'])) {
229
-					throw new \Exception(
230
-						sprintf(
231
-							'App with id %s has a cert with no CN',
232
-							$appId
233
-						)
234
-					);
235
-				}
236
-				if ($certInfo['subject']['CN'] !== $appId) {
237
-					throw new \Exception(
238
-						sprintf(
239
-							'App with id %s has a cert issued to %s',
240
-							$appId,
241
-							$certInfo['subject']['CN']
242
-						)
243
-					);
244
-				}
245
-
246
-				// Download the release
247
-				$tempFile = $this->tempManager->getTemporaryFile('.tar.gz');
248
-				if ($tempFile === false) {
249
-					throw new \RuntimeException('Could not create temporary file for downloading app archive.');
250
-				}
251
-
252
-				$timeout = $this->isCLI ? 0 : 120;
253
-				$client = $this->clientService->newClient();
254
-				$client->get($app['releases'][0]['download'], ['sink' => $tempFile, 'timeout' => $timeout]);
255
-
256
-				// Check if the signature actually matches the downloaded content
257
-				$certificate = openssl_get_publickey($app['certificate']);
258
-				$verified = openssl_verify(file_get_contents($tempFile), base64_decode($app['releases'][0]['signature']), $certificate, OPENSSL_ALGO_SHA512) === 1;
259
-
260
-				if ($verified === true) {
261
-					// Seems to match, let's proceed
262
-					$extractDir = $this->tempManager->getTemporaryFolder();
263
-					if ($extractDir === false) {
264
-						throw new \RuntimeException('Could not create temporary directory for unpacking app.');
265
-					}
266
-
267
-					$archive = new TAR($tempFile);
268
-					if (!$archive->extract($extractDir)) {
269
-						$errorMessage = 'Could not extract app ' . $appId;
270
-
271
-						$archiveError = $archive->getError();
272
-						if ($archiveError instanceof \PEAR_Error) {
273
-							$errorMessage .= ': ' . $archiveError->getMessage();
274
-						}
275
-
276
-						throw new \Exception($errorMessage);
277
-					}
278
-					$allFiles = scandir($extractDir);
279
-					$folders = array_diff($allFiles, ['.', '..']);
280
-					$folders = array_values($folders);
281
-
282
-					if (count($folders) < 1) {
283
-						throw new \Exception(
284
-							sprintf(
285
-								'Extracted app %s has no folders',
286
-								$appId
287
-							)
288
-						);
289
-					}
290
-
291
-					if (count($folders) > 1) {
292
-						throw new \Exception(
293
-							sprintf(
294
-								'Extracted app %s has more than 1 folder',
295
-								$appId
296
-							)
297
-						);
298
-					}
299
-
300
-					// Check if appinfo/info.xml has the same app ID as well
301
-					$xml = simplexml_load_string(file_get_contents($extractDir . '/' . $folders[0] . '/appinfo/info.xml'));
302
-
303
-					if ($xml === false) {
304
-						throw new \Exception(
305
-							sprintf(
306
-								'Failed to load info.xml for app id %s',
307
-								$appId,
308
-							)
309
-						);
310
-					}
311
-
312
-					if ((string)$xml->id !== $appId) {
313
-						throw new \Exception(
314
-							sprintf(
315
-								'App for id %s has a wrong app ID in info.xml: %s',
316
-								$appId,
317
-								(string)$xml->id
318
-							)
319
-						);
320
-					}
321
-
322
-					// Check if the version is lower than before
323
-					$currentVersion = \OCP\Server::get(IAppManager::class)->getAppVersion($appId, true);
324
-					$newVersion = (string)$xml->version;
325
-					if (version_compare($currentVersion, $newVersion) === 1) {
326
-						throw new \Exception(
327
-							sprintf(
328
-								'App for id %s has version %s and tried to update to lower version %s',
329
-								$appId,
330
-								$currentVersion,
331
-								$newVersion
332
-							)
333
-						);
334
-					}
335
-
336
-					$baseDir = OC_App::getInstallPath() . '/' . $appId;
337
-					// Remove old app with the ID if existent
338
-					Files::rmdirr($baseDir);
339
-					// Move to app folder
340
-					if (@mkdir($baseDir)) {
341
-						$extractDir .= '/' . $folders[0];
342
-					}
343
-					// otherwise we just copy the outer directory
344
-					$this->copyRecursive($extractDir, $baseDir);
345
-					Files::rmdirr($extractDir);
346
-					if (function_exists('opcache_reset')) {
347
-						opcache_reset();
348
-					}
349
-					return;
350
-				}
351
-				// Signature does not match
352
-				throw new \Exception(
353
-					sprintf(
354
-						'App with id %s has invalid signature',
355
-						$appId
356
-					)
357
-				);
358
-			}
359
-		}
360
-
361
-		throw new AppNotFoundException(
362
-			sprintf(
363
-				'Could not download app %s, it was not found on the appstore',
364
-				$appId
365
-			)
366
-		);
367
-	}
368
-
369
-	/**
370
-	 * Check if an update for the app is available
371
-	 *
372
-	 * @param string $appId
373
-	 * @param bool $allowUnstable
374
-	 * @return string|false false or the version number of the update
375
-	 */
376
-	public function isUpdateAvailable($appId, $allowUnstable = false): string|false {
377
-		if ($this->isInstanceReadyForUpdates === null) {
378
-			$installPath = OC_App::getInstallPath();
379
-			if ($installPath === null) {
380
-				$this->isInstanceReadyForUpdates = false;
381
-			} else {
382
-				$this->isInstanceReadyForUpdates = true;
383
-			}
384
-		}
385
-
386
-		if ($this->isInstanceReadyForUpdates === false) {
387
-			return false;
388
-		}
389
-
390
-		if ($this->isInstalledFromGit($appId) === true) {
391
-			return false;
392
-		}
393
-
394
-		if ($this->apps === null) {
395
-			$this->apps = $this->appFetcher->get($allowUnstable);
396
-		}
397
-
398
-		foreach ($this->apps as $app) {
399
-			if ($app['id'] === $appId) {
400
-				$currentVersion = \OCP\Server::get(IAppManager::class)->getAppVersion($appId, true);
401
-
402
-				if (!isset($app['releases'][0]['version'])) {
403
-					return false;
404
-				}
405
-				$newestVersion = $app['releases'][0]['version'];
406
-				if ($currentVersion !== '0' && version_compare($newestVersion, $currentVersion, '>')) {
407
-					return $newestVersion;
408
-				} else {
409
-					return false;
410
-				}
411
-			}
412
-		}
413
-
414
-		return false;
415
-	}
416
-
417
-	/**
418
-	 * Check if app has been installed from git
419
-	 *
420
-	 * The function will check if the path contains a .git folder
421
-	 */
422
-	private function isInstalledFromGit(string $appId): bool {
423
-		$app = \OC_App::findAppInDirectories($appId);
424
-		if ($app === false) {
425
-			return false;
426
-		}
427
-		$basedir = $app['path'] . '/' . $appId;
428
-		return file_exists($basedir . '/.git/');
429
-	}
430
-
431
-	/**
432
-	 * Check if app is already downloaded
433
-	 *
434
-	 * The function will check if the app is already downloaded in the apps repository
435
-	 */
436
-	public function isDownloaded(string $name): bool {
437
-		foreach (\OC::$APPSROOTS as $dir) {
438
-			$dirToTest = $dir['path'];
439
-			$dirToTest .= '/';
440
-			$dirToTest .= $name;
441
-			$dirToTest .= '/';
442
-
443
-			if (is_dir($dirToTest)) {
444
-				return true;
445
-			}
446
-		}
447
-
448
-		return false;
449
-	}
450
-
451
-	/**
452
-	 * Removes an app
453
-	 *
454
-	 * This function works as follows
455
-	 *   -# call uninstall repair steps
456
-	 *   -# removing the files
457
-	 *
458
-	 * The function will not delete preferences, tables and the configuration,
459
-	 * this has to be done by the function oc_app_uninstall().
460
-	 */
461
-	public function removeApp(string $appId): bool {
462
-		if ($this->isDownloaded($appId)) {
463
-			if (\OCP\Server::get(IAppManager::class)->isShipped($appId)) {
464
-				return false;
465
-			}
466
-			$appDir = OC_App::getInstallPath() . '/' . $appId;
467
-			Files::rmdirr($appDir);
468
-			return true;
469
-		} else {
470
-			$this->logger->error('can\'t remove app ' . $appId . '. It is not installed.');
471
-
472
-			return false;
473
-		}
474
-	}
475
-
476
-	/**
477
-	 * Installs the app within the bundle and marks the bundle as installed
478
-	 *
479
-	 * @throws \Exception If app could not get installed
480
-	 */
481
-	public function installAppBundle(Bundle $bundle): void {
482
-		$appIds = $bundle->getAppIdentifiers();
483
-		foreach ($appIds as $appId) {
484
-			if (!$this->isDownloaded($appId)) {
485
-				$this->downloadApp($appId);
486
-			}
487
-			$this->installApp($appId);
488
-			$app = new OC_App();
489
-			$app->enable($appId);
490
-		}
491
-		$bundles = json_decode($this->config->getAppValue('core', 'installed.bundles', json_encode([])), true);
492
-		$bundles[] = $bundle->getIdentifier();
493
-		$this->config->setAppValue('core', 'installed.bundles', json_encode($bundles));
494
-	}
495
-
496
-	/**
497
-	 * Installs shipped apps
498
-	 *
499
-	 * This function installs all apps found in the 'apps' directory that should be enabled by default;
500
-	 * @param bool $softErrors When updating we ignore errors and simply log them, better to have a
501
-	 *                         working ownCloud at the end instead of an aborted update.
502
-	 * @return array Array of error messages (appid => Exception)
503
-	 */
504
-	public static function installShippedApps(bool $softErrors = false, ?IOutput $output = null): array {
505
-		if ($output instanceof IOutput) {
506
-			$output->debug('Installing shipped apps');
507
-		}
508
-		$appManager = \OCP\Server::get(IAppManager::class);
509
-		$config = \OCP\Server::get(IConfig::class);
510
-		$errors = [];
511
-		foreach (\OC::$APPSROOTS as $app_dir) {
512
-			if ($dir = opendir($app_dir['path'])) {
513
-				while (false !== ($filename = readdir($dir))) {
514
-					if ($filename[0] !== '.' and is_dir($app_dir['path'] . "/$filename")) {
515
-						if (file_exists($app_dir['path'] . "/$filename/appinfo/info.xml")) {
516
-							if ($config->getAppValue($filename, 'installed_version', null) === null) {
517
-								$enabled = $appManager->isDefaultEnabled($filename);
518
-								if (($enabled || in_array($filename, $appManager->getAlwaysEnabledApps()))
519
-									  && $config->getAppValue($filename, 'enabled') !== 'no') {
520
-									if ($softErrors) {
521
-										try {
522
-											Installer::installShippedApp($filename, $output);
523
-										} catch (HintException $e) {
524
-											if ($e->getPrevious() instanceof TableExistsException) {
525
-												$errors[$filename] = $e;
526
-												continue;
527
-											}
528
-											throw $e;
529
-										}
530
-									} else {
531
-										Installer::installShippedApp($filename, $output);
532
-									}
533
-									$config->setAppValue($filename, 'enabled', 'yes');
534
-								}
535
-							}
536
-						}
537
-					}
538
-				}
539
-				closedir($dir);
540
-			}
541
-		}
542
-
543
-		return $errors;
544
-	}
545
-
546
-	/**
547
-	 * install an app already placed in the app folder
548
-	 */
549
-	public static function installShippedApp(string $app, ?IOutput $output = null): string|false {
550
-		if ($output instanceof IOutput) {
551
-			$output->debug('Installing ' . $app);
552
-		}
553
-
554
-		$appManager = \OCP\Server::get(IAppManager::class);
555
-		$config = \OCP\Server::get(IConfig::class);
556
-
557
-		$appPath = $appManager->getAppPath($app);
558
-		\OC_App::registerAutoloading($app, $appPath);
559
-
560
-		$ms = new MigrationService($app, \OCP\Server::get(Connection::class));
561
-		if ($output instanceof IOutput) {
562
-			$ms->setOutput($output);
563
-		}
564
-		$previousVersion = $config->getAppValue($app, 'installed_version', false);
565
-		$ms->migrate('latest', !$previousVersion);
566
-
567
-		//run appinfo/install.php
568
-		self::includeAppScript("$appPath/appinfo/install.php");
569
-
570
-		$info = \OCP\Server::get(IAppManager::class)->getAppInfo($app);
571
-		if (is_null($info)) {
572
-			return false;
573
-		}
574
-		if ($output instanceof IOutput) {
575
-			$output->debug('Registering tasks of ' . $app);
576
-		}
577
-		\OC_App::setupBackgroundJobs($info['background-jobs']);
578
-
579
-		OC_App::executeRepairSteps($app, $info['repair-steps']['install']);
580
-
581
-		$config->setAppValue($app, 'installed_version', \OCP\Server::get(IAppManager::class)->getAppVersion($app));
582
-		if (array_key_exists('ocsid', $info)) {
583
-			$config->setAppValue($app, 'ocsid', $info['ocsid']);
584
-		}
585
-
586
-		//set remote/public handlers
587
-		foreach ($info['remote'] as $name => $path) {
588
-			$config->setAppValue('core', 'remote_' . $name, $app . '/' . $path);
589
-		}
590
-		foreach ($info['public'] as $name => $path) {
591
-			$config->setAppValue('core', 'public_' . $name, $app . '/' . $path);
592
-		}
593
-
594
-		OC_App::setAppTypes($info['id']);
595
-
596
-		return $info['id'];
597
-	}
598
-
599
-	private static function includeAppScript(string $script): void {
600
-		if (file_exists($script)) {
601
-			include $script;
602
-		}
603
-	}
604
-
605
-	/**
606
-	 * Recursive copying of local folders.
607
-	 *
608
-	 * @param string $src source folder
609
-	 * @param string $dest target folder
610
-	 */
611
-	private function copyRecursive(string $src, string $dest): void {
612
-		if (!file_exists($src)) {
613
-			return;
614
-		}
615
-
616
-		if (is_dir($src)) {
617
-			if (!is_dir($dest)) {
618
-				mkdir($dest);
619
-			}
620
-			$files = scandir($src);
621
-			foreach ($files as $file) {
622
-				if ($file != '.' && $file != '..') {
623
-					$this->copyRecursive("$src/$file", "$dest/$file");
624
-				}
625
-			}
626
-		} else {
627
-			$validator = Server::get(FilenameValidator::class);
628
-			if (!$validator->isForbidden($src)) {
629
-				copy($src, $dest);
630
-			}
631
-		}
632
-	}
37
+    private ?bool $isInstanceReadyForUpdates = null;
38
+    private ?array $apps = null;
39
+
40
+    public function __construct(
41
+        private AppFetcher $appFetcher,
42
+        private IClientService $clientService,
43
+        private ITempManager $tempManager,
44
+        private LoggerInterface $logger,
45
+        private IConfig $config,
46
+        private bool $isCLI,
47
+    ) {
48
+    }
49
+
50
+    /**
51
+     * Installs an app that is located in one of the app folders already
52
+     *
53
+     * @param string $appId App to install
54
+     * @param bool $forceEnable
55
+     * @throws \Exception
56
+     * @return string app ID
57
+     */
58
+    public function installApp(string $appId, bool $forceEnable = false): string {
59
+        $app = \OC_App::findAppInDirectories($appId);
60
+        if ($app === false) {
61
+            throw new \Exception('App not found in any app directory');
62
+        }
63
+
64
+        $basedir = $app['path'] . '/' . $appId;
65
+
66
+        if (is_file($basedir . '/appinfo/database.xml')) {
67
+            throw new \Exception('The appinfo/database.xml file is not longer supported. Used in ' . $appId);
68
+        }
69
+
70
+        $l = \OCP\Util::getL10N('core');
71
+        $info = \OCP\Server::get(IAppManager::class)->getAppInfoByPath($basedir . '/appinfo/info.xml', $l->getLanguageCode());
72
+
73
+        if (!is_array($info)) {
74
+            throw new \Exception(
75
+                $l->t('App "%s" cannot be installed because appinfo file cannot be read.',
76
+                    [$appId]
77
+                )
78
+            );
79
+        }
80
+
81
+        $ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
82
+        $ignoreMax = $forceEnable || in_array($appId, $ignoreMaxApps, true);
83
+
84
+        $version = implode('.', \OCP\Util::getVersion());
85
+        if (!\OC_App::isAppCompatible($version, $info, $ignoreMax)) {
86
+            throw new \Exception(
87
+                // TODO $l
88
+                $l->t('App "%s" cannot be installed because it is not compatible with this version of the server.',
89
+                    [$info['name']]
90
+                )
91
+            );
92
+        }
93
+
94
+        // check for required dependencies
95
+        \OC_App::checkAppDependencies($this->config, $l, $info, $ignoreMax);
96
+        /** @var Coordinator $coordinator */
97
+        $coordinator = \OC::$server->get(Coordinator::class);
98
+        $coordinator->runLazyRegistration($appId);
99
+        \OC_App::registerAutoloading($appId, $basedir);
100
+
101
+        $previousVersion = $this->config->getAppValue($info['id'], 'installed_version', false);
102
+        if ($previousVersion) {
103
+            OC_App::executeRepairSteps($appId, $info['repair-steps']['pre-migration']);
104
+        }
105
+
106
+        //install the database
107
+        $ms = new MigrationService($info['id'], \OCP\Server::get(Connection::class));
108
+        $ms->migrate('latest', !$previousVersion);
109
+
110
+        if ($previousVersion) {
111
+            OC_App::executeRepairSteps($appId, $info['repair-steps']['post-migration']);
112
+        }
113
+
114
+        \OC_App::setupBackgroundJobs($info['background-jobs']);
115
+
116
+        //run appinfo/install.php
117
+        self::includeAppScript($basedir . '/appinfo/install.php');
118
+
119
+        OC_App::executeRepairSteps($appId, $info['repair-steps']['install']);
120
+
121
+        $config = \OCP\Server::get(IConfig::class);
122
+        //set the installed version
123
+        $config->setAppValue($info['id'], 'installed_version', \OCP\Server::get(IAppManager::class)->getAppVersion($info['id'], false));
124
+        $config->setAppValue($info['id'], 'enabled', 'no');
125
+
126
+        //set remote/public handlers
127
+        foreach ($info['remote'] as $name => $path) {
128
+            $config->setAppValue('core', 'remote_' . $name, $info['id'] . '/' . $path);
129
+        }
130
+        foreach ($info['public'] as $name => $path) {
131
+            $config->setAppValue('core', 'public_' . $name, $info['id'] . '/' . $path);
132
+        }
133
+
134
+        OC_App::setAppTypes($info['id']);
135
+
136
+        return $info['id'];
137
+    }
138
+
139
+    /**
140
+     * Updates the specified app from the appstore
141
+     *
142
+     * @param bool $allowUnstable Allow unstable releases
143
+     */
144
+    public function updateAppstoreApp(string $appId, bool $allowUnstable = false): bool {
145
+        if ($this->isUpdateAvailable($appId, $allowUnstable)) {
146
+            try {
147
+                $this->downloadApp($appId, $allowUnstable);
148
+            } catch (\Exception $e) {
149
+                $this->logger->error($e->getMessage(), [
150
+                    'exception' => $e,
151
+                ]);
152
+                return false;
153
+            }
154
+            return OC_App::updateApp($appId);
155
+        }
156
+
157
+        return false;
158
+    }
159
+
160
+    /**
161
+     * Split the certificate file in individual certs
162
+     *
163
+     * @param string $cert
164
+     * @return string[]
165
+     */
166
+    private function splitCerts(string $cert): array {
167
+        preg_match_all('([\-]{3,}[\S\ ]+?[\-]{3,}[\S\s]+?[\-]{3,}[\S\ ]+?[\-]{3,})', $cert, $matches);
168
+
169
+        return $matches[0];
170
+    }
171
+
172
+    /**
173
+     * Downloads an app and puts it into the app directory
174
+     *
175
+     * @param string $appId
176
+     * @param bool [$allowUnstable]
177
+     *
178
+     * @throws AppNotFoundException If the app is not found on the appstore
179
+     * @throws \Exception If the installation was not successful
180
+     */
181
+    public function downloadApp(string $appId, bool $allowUnstable = false): void {
182
+        $appId = strtolower($appId);
183
+
184
+        $apps = $this->appFetcher->get($allowUnstable);
185
+        foreach ($apps as $app) {
186
+            if ($app['id'] === $appId) {
187
+                // Load the certificate
188
+                $certificate = new X509();
189
+                $rootCrt = file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt');
190
+                $rootCrts = $this->splitCerts($rootCrt);
191
+                foreach ($rootCrts as $rootCrt) {
192
+                    $certificate->loadCA($rootCrt);
193
+                }
194
+                $loadedCertificate = $certificate->loadX509($app['certificate']);
195
+
196
+                // Verify if the certificate has been revoked
197
+                $crl = new X509();
198
+                foreach ($rootCrts as $rootCrt) {
199
+                    $crl->loadCA($rootCrt);
200
+                }
201
+                $crl->loadCRL(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crl'));
202
+                if ($crl->validateSignature() !== true) {
203
+                    throw new \Exception('Could not validate CRL signature');
204
+                }
205
+                $csn = $loadedCertificate['tbsCertificate']['serialNumber']->toString();
206
+                $revoked = $crl->getRevoked($csn);
207
+                if ($revoked !== false) {
208
+                    throw new \Exception(
209
+                        sprintf(
210
+                            'Certificate "%s" has been revoked',
211
+                            $csn
212
+                        )
213
+                    );
214
+                }
215
+
216
+                // Verify if the certificate has been issued by the Nextcloud Code Authority CA
217
+                if ($certificate->validateSignature() !== true) {
218
+                    throw new \Exception(
219
+                        sprintf(
220
+                            'App with id %s has a certificate not issued by a trusted Code Signing Authority',
221
+                            $appId
222
+                        )
223
+                    );
224
+                }
225
+
226
+                // Verify if the certificate is issued for the requested app id
227
+                $certInfo = openssl_x509_parse($app['certificate']);
228
+                if (!isset($certInfo['subject']['CN'])) {
229
+                    throw new \Exception(
230
+                        sprintf(
231
+                            'App with id %s has a cert with no CN',
232
+                            $appId
233
+                        )
234
+                    );
235
+                }
236
+                if ($certInfo['subject']['CN'] !== $appId) {
237
+                    throw new \Exception(
238
+                        sprintf(
239
+                            'App with id %s has a cert issued to %s',
240
+                            $appId,
241
+                            $certInfo['subject']['CN']
242
+                        )
243
+                    );
244
+                }
245
+
246
+                // Download the release
247
+                $tempFile = $this->tempManager->getTemporaryFile('.tar.gz');
248
+                if ($tempFile === false) {
249
+                    throw new \RuntimeException('Could not create temporary file for downloading app archive.');
250
+                }
251
+
252
+                $timeout = $this->isCLI ? 0 : 120;
253
+                $client = $this->clientService->newClient();
254
+                $client->get($app['releases'][0]['download'], ['sink' => $tempFile, 'timeout' => $timeout]);
255
+
256
+                // Check if the signature actually matches the downloaded content
257
+                $certificate = openssl_get_publickey($app['certificate']);
258
+                $verified = openssl_verify(file_get_contents($tempFile), base64_decode($app['releases'][0]['signature']), $certificate, OPENSSL_ALGO_SHA512) === 1;
259
+
260
+                if ($verified === true) {
261
+                    // Seems to match, let's proceed
262
+                    $extractDir = $this->tempManager->getTemporaryFolder();
263
+                    if ($extractDir === false) {
264
+                        throw new \RuntimeException('Could not create temporary directory for unpacking app.');
265
+                    }
266
+
267
+                    $archive = new TAR($tempFile);
268
+                    if (!$archive->extract($extractDir)) {
269
+                        $errorMessage = 'Could not extract app ' . $appId;
270
+
271
+                        $archiveError = $archive->getError();
272
+                        if ($archiveError instanceof \PEAR_Error) {
273
+                            $errorMessage .= ': ' . $archiveError->getMessage();
274
+                        }
275
+
276
+                        throw new \Exception($errorMessage);
277
+                    }
278
+                    $allFiles = scandir($extractDir);
279
+                    $folders = array_diff($allFiles, ['.', '..']);
280
+                    $folders = array_values($folders);
281
+
282
+                    if (count($folders) < 1) {
283
+                        throw new \Exception(
284
+                            sprintf(
285
+                                'Extracted app %s has no folders',
286
+                                $appId
287
+                            )
288
+                        );
289
+                    }
290
+
291
+                    if (count($folders) > 1) {
292
+                        throw new \Exception(
293
+                            sprintf(
294
+                                'Extracted app %s has more than 1 folder',
295
+                                $appId
296
+                            )
297
+                        );
298
+                    }
299
+
300
+                    // Check if appinfo/info.xml has the same app ID as well
301
+                    $xml = simplexml_load_string(file_get_contents($extractDir . '/' . $folders[0] . '/appinfo/info.xml'));
302
+
303
+                    if ($xml === false) {
304
+                        throw new \Exception(
305
+                            sprintf(
306
+                                'Failed to load info.xml for app id %s',
307
+                                $appId,
308
+                            )
309
+                        );
310
+                    }
311
+
312
+                    if ((string)$xml->id !== $appId) {
313
+                        throw new \Exception(
314
+                            sprintf(
315
+                                'App for id %s has a wrong app ID in info.xml: %s',
316
+                                $appId,
317
+                                (string)$xml->id
318
+                            )
319
+                        );
320
+                    }
321
+
322
+                    // Check if the version is lower than before
323
+                    $currentVersion = \OCP\Server::get(IAppManager::class)->getAppVersion($appId, true);
324
+                    $newVersion = (string)$xml->version;
325
+                    if (version_compare($currentVersion, $newVersion) === 1) {
326
+                        throw new \Exception(
327
+                            sprintf(
328
+                                'App for id %s has version %s and tried to update to lower version %s',
329
+                                $appId,
330
+                                $currentVersion,
331
+                                $newVersion
332
+                            )
333
+                        );
334
+                    }
335
+
336
+                    $baseDir = OC_App::getInstallPath() . '/' . $appId;
337
+                    // Remove old app with the ID if existent
338
+                    Files::rmdirr($baseDir);
339
+                    // Move to app folder
340
+                    if (@mkdir($baseDir)) {
341
+                        $extractDir .= '/' . $folders[0];
342
+                    }
343
+                    // otherwise we just copy the outer directory
344
+                    $this->copyRecursive($extractDir, $baseDir);
345
+                    Files::rmdirr($extractDir);
346
+                    if (function_exists('opcache_reset')) {
347
+                        opcache_reset();
348
+                    }
349
+                    return;
350
+                }
351
+                // Signature does not match
352
+                throw new \Exception(
353
+                    sprintf(
354
+                        'App with id %s has invalid signature',
355
+                        $appId
356
+                    )
357
+                );
358
+            }
359
+        }
360
+
361
+        throw new AppNotFoundException(
362
+            sprintf(
363
+                'Could not download app %s, it was not found on the appstore',
364
+                $appId
365
+            )
366
+        );
367
+    }
368
+
369
+    /**
370
+     * Check if an update for the app is available
371
+     *
372
+     * @param string $appId
373
+     * @param bool $allowUnstable
374
+     * @return string|false false or the version number of the update
375
+     */
376
+    public function isUpdateAvailable($appId, $allowUnstable = false): string|false {
377
+        if ($this->isInstanceReadyForUpdates === null) {
378
+            $installPath = OC_App::getInstallPath();
379
+            if ($installPath === null) {
380
+                $this->isInstanceReadyForUpdates = false;
381
+            } else {
382
+                $this->isInstanceReadyForUpdates = true;
383
+            }
384
+        }
385
+
386
+        if ($this->isInstanceReadyForUpdates === false) {
387
+            return false;
388
+        }
389
+
390
+        if ($this->isInstalledFromGit($appId) === true) {
391
+            return false;
392
+        }
393
+
394
+        if ($this->apps === null) {
395
+            $this->apps = $this->appFetcher->get($allowUnstable);
396
+        }
397
+
398
+        foreach ($this->apps as $app) {
399
+            if ($app['id'] === $appId) {
400
+                $currentVersion = \OCP\Server::get(IAppManager::class)->getAppVersion($appId, true);
401
+
402
+                if (!isset($app['releases'][0]['version'])) {
403
+                    return false;
404
+                }
405
+                $newestVersion = $app['releases'][0]['version'];
406
+                if ($currentVersion !== '0' && version_compare($newestVersion, $currentVersion, '>')) {
407
+                    return $newestVersion;
408
+                } else {
409
+                    return false;
410
+                }
411
+            }
412
+        }
413
+
414
+        return false;
415
+    }
416
+
417
+    /**
418
+     * Check if app has been installed from git
419
+     *
420
+     * The function will check if the path contains a .git folder
421
+     */
422
+    private function isInstalledFromGit(string $appId): bool {
423
+        $app = \OC_App::findAppInDirectories($appId);
424
+        if ($app === false) {
425
+            return false;
426
+        }
427
+        $basedir = $app['path'] . '/' . $appId;
428
+        return file_exists($basedir . '/.git/');
429
+    }
430
+
431
+    /**
432
+     * Check if app is already downloaded
433
+     *
434
+     * The function will check if the app is already downloaded in the apps repository
435
+     */
436
+    public function isDownloaded(string $name): bool {
437
+        foreach (\OC::$APPSROOTS as $dir) {
438
+            $dirToTest = $dir['path'];
439
+            $dirToTest .= '/';
440
+            $dirToTest .= $name;
441
+            $dirToTest .= '/';
442
+
443
+            if (is_dir($dirToTest)) {
444
+                return true;
445
+            }
446
+        }
447
+
448
+        return false;
449
+    }
450
+
451
+    /**
452
+     * Removes an app
453
+     *
454
+     * This function works as follows
455
+     *   -# call uninstall repair steps
456
+     *   -# removing the files
457
+     *
458
+     * The function will not delete preferences, tables and the configuration,
459
+     * this has to be done by the function oc_app_uninstall().
460
+     */
461
+    public function removeApp(string $appId): bool {
462
+        if ($this->isDownloaded($appId)) {
463
+            if (\OCP\Server::get(IAppManager::class)->isShipped($appId)) {
464
+                return false;
465
+            }
466
+            $appDir = OC_App::getInstallPath() . '/' . $appId;
467
+            Files::rmdirr($appDir);
468
+            return true;
469
+        } else {
470
+            $this->logger->error('can\'t remove app ' . $appId . '. It is not installed.');
471
+
472
+            return false;
473
+        }
474
+    }
475
+
476
+    /**
477
+     * Installs the app within the bundle and marks the bundle as installed
478
+     *
479
+     * @throws \Exception If app could not get installed
480
+     */
481
+    public function installAppBundle(Bundle $bundle): void {
482
+        $appIds = $bundle->getAppIdentifiers();
483
+        foreach ($appIds as $appId) {
484
+            if (!$this->isDownloaded($appId)) {
485
+                $this->downloadApp($appId);
486
+            }
487
+            $this->installApp($appId);
488
+            $app = new OC_App();
489
+            $app->enable($appId);
490
+        }
491
+        $bundles = json_decode($this->config->getAppValue('core', 'installed.bundles', json_encode([])), true);
492
+        $bundles[] = $bundle->getIdentifier();
493
+        $this->config->setAppValue('core', 'installed.bundles', json_encode($bundles));
494
+    }
495
+
496
+    /**
497
+     * Installs shipped apps
498
+     *
499
+     * This function installs all apps found in the 'apps' directory that should be enabled by default;
500
+     * @param bool $softErrors When updating we ignore errors and simply log them, better to have a
501
+     *                         working ownCloud at the end instead of an aborted update.
502
+     * @return array Array of error messages (appid => Exception)
503
+     */
504
+    public static function installShippedApps(bool $softErrors = false, ?IOutput $output = null): array {
505
+        if ($output instanceof IOutput) {
506
+            $output->debug('Installing shipped apps');
507
+        }
508
+        $appManager = \OCP\Server::get(IAppManager::class);
509
+        $config = \OCP\Server::get(IConfig::class);
510
+        $errors = [];
511
+        foreach (\OC::$APPSROOTS as $app_dir) {
512
+            if ($dir = opendir($app_dir['path'])) {
513
+                while (false !== ($filename = readdir($dir))) {
514
+                    if ($filename[0] !== '.' and is_dir($app_dir['path'] . "/$filename")) {
515
+                        if (file_exists($app_dir['path'] . "/$filename/appinfo/info.xml")) {
516
+                            if ($config->getAppValue($filename, 'installed_version', null) === null) {
517
+                                $enabled = $appManager->isDefaultEnabled($filename);
518
+                                if (($enabled || in_array($filename, $appManager->getAlwaysEnabledApps()))
519
+                                      && $config->getAppValue($filename, 'enabled') !== 'no') {
520
+                                    if ($softErrors) {
521
+                                        try {
522
+                                            Installer::installShippedApp($filename, $output);
523
+                                        } catch (HintException $e) {
524
+                                            if ($e->getPrevious() instanceof TableExistsException) {
525
+                                                $errors[$filename] = $e;
526
+                                                continue;
527
+                                            }
528
+                                            throw $e;
529
+                                        }
530
+                                    } else {
531
+                                        Installer::installShippedApp($filename, $output);
532
+                                    }
533
+                                    $config->setAppValue($filename, 'enabled', 'yes');
534
+                                }
535
+                            }
536
+                        }
537
+                    }
538
+                }
539
+                closedir($dir);
540
+            }
541
+        }
542
+
543
+        return $errors;
544
+    }
545
+
546
+    /**
547
+     * install an app already placed in the app folder
548
+     */
549
+    public static function installShippedApp(string $app, ?IOutput $output = null): string|false {
550
+        if ($output instanceof IOutput) {
551
+            $output->debug('Installing ' . $app);
552
+        }
553
+
554
+        $appManager = \OCP\Server::get(IAppManager::class);
555
+        $config = \OCP\Server::get(IConfig::class);
556
+
557
+        $appPath = $appManager->getAppPath($app);
558
+        \OC_App::registerAutoloading($app, $appPath);
559
+
560
+        $ms = new MigrationService($app, \OCP\Server::get(Connection::class));
561
+        if ($output instanceof IOutput) {
562
+            $ms->setOutput($output);
563
+        }
564
+        $previousVersion = $config->getAppValue($app, 'installed_version', false);
565
+        $ms->migrate('latest', !$previousVersion);
566
+
567
+        //run appinfo/install.php
568
+        self::includeAppScript("$appPath/appinfo/install.php");
569
+
570
+        $info = \OCP\Server::get(IAppManager::class)->getAppInfo($app);
571
+        if (is_null($info)) {
572
+            return false;
573
+        }
574
+        if ($output instanceof IOutput) {
575
+            $output->debug('Registering tasks of ' . $app);
576
+        }
577
+        \OC_App::setupBackgroundJobs($info['background-jobs']);
578
+
579
+        OC_App::executeRepairSteps($app, $info['repair-steps']['install']);
580
+
581
+        $config->setAppValue($app, 'installed_version', \OCP\Server::get(IAppManager::class)->getAppVersion($app));
582
+        if (array_key_exists('ocsid', $info)) {
583
+            $config->setAppValue($app, 'ocsid', $info['ocsid']);
584
+        }
585
+
586
+        //set remote/public handlers
587
+        foreach ($info['remote'] as $name => $path) {
588
+            $config->setAppValue('core', 'remote_' . $name, $app . '/' . $path);
589
+        }
590
+        foreach ($info['public'] as $name => $path) {
591
+            $config->setAppValue('core', 'public_' . $name, $app . '/' . $path);
592
+        }
593
+
594
+        OC_App::setAppTypes($info['id']);
595
+
596
+        return $info['id'];
597
+    }
598
+
599
+    private static function includeAppScript(string $script): void {
600
+        if (file_exists($script)) {
601
+            include $script;
602
+        }
603
+    }
604
+
605
+    /**
606
+     * Recursive copying of local folders.
607
+     *
608
+     * @param string $src source folder
609
+     * @param string $dest target folder
610
+     */
611
+    private function copyRecursive(string $src, string $dest): void {
612
+        if (!file_exists($src)) {
613
+            return;
614
+        }
615
+
616
+        if (is_dir($src)) {
617
+            if (!is_dir($dest)) {
618
+                mkdir($dest);
619
+            }
620
+            $files = scandir($src);
621
+            foreach ($files as $file) {
622
+                if ($file != '.' && $file != '..') {
623
+                    $this->copyRecursive("$src/$file", "$dest/$file");
624
+                }
625
+            }
626
+        } else {
627
+            $validator = Server::get(FilenameValidator::class);
628
+            if (!$validator->isForbidden($src)) {
629
+                copy($src, $dest);
630
+            }
631
+        }
632
+    }
633 633
 }
Please login to merge, or discard this patch.
lib/private/App/AppManager.php 1 patch
Indentation   +908 added lines, -908 removed lines patch added patch discarded remove patch
@@ -31,912 +31,912 @@
 block discarded – undo
31 31
 use Psr\Log\LoggerInterface;
32 32
 
33 33
 class AppManager implements IAppManager {
34
-	/**
35
-	 * Apps with these types can not be enabled for certain groups only
36
-	 * @var string[]
37
-	 */
38
-	protected $protectedAppTypes = [
39
-		'filesystem',
40
-		'prelogin',
41
-		'authentication',
42
-		'logging',
43
-		'prevent_group_restriction',
44
-	];
45
-
46
-	/** @var string[] $appId => $enabled */
47
-	private array $enabledAppsCache = [];
48
-
49
-	/** @var string[]|null */
50
-	private ?array $shippedApps = null;
51
-
52
-	private array $alwaysEnabled = [];
53
-	private array $defaultEnabled = [];
54
-
55
-	/** @var array */
56
-	private array $appInfos = [];
57
-
58
-	/** @var array */
59
-	private array $appVersions = [];
60
-
61
-	/** @var array */
62
-	private array $autoDisabledApps = [];
63
-	private array $appTypes = [];
64
-
65
-	/** @var array<string, true> */
66
-	private array $loadedApps = [];
67
-
68
-	private ?AppConfig $appConfig = null;
69
-	private ?IURLGenerator $urlGenerator = null;
70
-	private ?INavigationManager $navigationManager = null;
71
-
72
-	/**
73
-	 * Be extremely careful when injecting classes here. The AppManager is used by the installer,
74
-	 * so it needs to work before installation. See how AppConfig and IURLGenerator are injected for reference
75
-	 */
76
-	public function __construct(
77
-		private IUserSession $userSession,
78
-		private IConfig $config,
79
-		private IGroupManager $groupManager,
80
-		private ICacheFactory $memCacheFactory,
81
-		private IEventDispatcher $dispatcher,
82
-		private LoggerInterface $logger,
83
-		private ServerVersion $serverVersion,
84
-	) {
85
-	}
86
-
87
-	private function getNavigationManager(): INavigationManager {
88
-		if ($this->navigationManager === null) {
89
-			$this->navigationManager = \OCP\Server::get(INavigationManager::class);
90
-		}
91
-		return $this->navigationManager;
92
-	}
93
-
94
-	public function getAppIcon(string $appId, bool $dark = false): ?string {
95
-		$possibleIcons = $dark ? [$appId . '-dark.svg', 'app-dark.svg'] : [$appId . '.svg', 'app.svg'];
96
-		$icon = null;
97
-		foreach ($possibleIcons as $iconName) {
98
-			try {
99
-				$icon = $this->getUrlGenerator()->imagePath($appId, $iconName);
100
-				break;
101
-			} catch (\RuntimeException $e) {
102
-				// ignore
103
-			}
104
-		}
105
-		return $icon;
106
-	}
107
-
108
-	private function getAppConfig(): AppConfig {
109
-		if ($this->appConfig !== null) {
110
-			return $this->appConfig;
111
-		}
112
-		if (!$this->config->getSystemValueBool('installed', false)) {
113
-			throw new \Exception('Nextcloud is not installed yet, AppConfig is not available');
114
-		}
115
-		$this->appConfig = \OCP\Server::get(AppConfig::class);
116
-		return $this->appConfig;
117
-	}
118
-
119
-	private function getUrlGenerator(): IURLGenerator {
120
-		if ($this->urlGenerator !== null) {
121
-			return $this->urlGenerator;
122
-		}
123
-		if (!$this->config->getSystemValueBool('installed', false)) {
124
-			throw new \Exception('Nextcloud is not installed yet, AppConfig is not available');
125
-		}
126
-		$this->urlGenerator = \OCP\Server::get(IURLGenerator::class);
127
-		return $this->urlGenerator;
128
-	}
129
-
130
-	/**
131
-	 * For all enabled apps, return the value of their 'enabled' config key.
132
-	 *
133
-	 * @return array<string,string> appId => enabled (may be 'yes', or a json encoded list of group ids)
134
-	 */
135
-	private function getEnabledAppsValues(): array {
136
-		if (!$this->enabledAppsCache) {
137
-			$values = $this->getAppConfig()->getValues(false, 'enabled');
138
-
139
-			$alwaysEnabledApps = $this->getAlwaysEnabledApps();
140
-			foreach ($alwaysEnabledApps as $appId) {
141
-				$values[$appId] = 'yes';
142
-			}
143
-
144
-			$this->enabledAppsCache = array_filter($values, function ($value) {
145
-				return $value !== 'no';
146
-			});
147
-			ksort($this->enabledAppsCache);
148
-		}
149
-		return $this->enabledAppsCache;
150
-	}
151
-
152
-	/**
153
-	 * Deprecated alias
154
-	 *
155
-	 * @return string[]
156
-	 */
157
-	public function getInstalledApps() {
158
-		return $this->getEnabledApps();
159
-	}
160
-
161
-	/**
162
-	 * List all enabled apps, either for everyone or for some groups
163
-	 *
164
-	 * @return list<string>
165
-	 */
166
-	public function getEnabledApps(): array {
167
-		return array_keys($this->getEnabledAppsValues());
168
-	}
169
-
170
-	/**
171
-	 * Get a list of all apps in the apps folder
172
-	 *
173
-	 * @return list<string> an array of app names (string IDs)
174
-	 */
175
-	public function getAllAppsInAppsFolders(): array {
176
-		$apps = [];
177
-
178
-		foreach (\OC::$APPSROOTS as $apps_dir) {
179
-			if (!is_readable($apps_dir['path'])) {
180
-				$this->logger->warning('unable to read app folder : ' . $apps_dir['path'], ['app' => 'core']);
181
-				continue;
182
-			}
183
-			$dh = opendir($apps_dir['path']);
184
-
185
-			if (is_resource($dh)) {
186
-				while (($file = readdir($dh)) !== false) {
187
-					if (
188
-						$file[0] != '.' &&
189
-						is_dir($apps_dir['path'] . '/' . $file) &&
190
-						is_file($apps_dir['path'] . '/' . $file . '/appinfo/info.xml')
191
-					) {
192
-						$apps[] = $file;
193
-					}
194
-				}
195
-			}
196
-		}
197
-
198
-		return array_values(array_unique($apps));
199
-	}
200
-
201
-	/**
202
-	 * List all apps enabled for a user
203
-	 *
204
-	 * @param \OCP\IUser $user
205
-	 * @return string[]
206
-	 */
207
-	public function getEnabledAppsForUser(IUser $user) {
208
-		$apps = $this->getEnabledAppsValues();
209
-		$appsForUser = array_filter($apps, function ($enabled) use ($user) {
210
-			return $this->checkAppForUser($enabled, $user);
211
-		});
212
-		return array_keys($appsForUser);
213
-	}
214
-
215
-	public function getEnabledAppsForGroup(IGroup $group): array {
216
-		$apps = $this->getEnabledAppsValues();
217
-		$appsForGroups = array_filter($apps, function ($enabled) use ($group) {
218
-			return $this->checkAppForGroups($enabled, $group);
219
-		});
220
-		return array_keys($appsForGroups);
221
-	}
222
-
223
-	/**
224
-	 * Loads all apps
225
-	 *
226
-	 * @param string[] $types
227
-	 * @return bool
228
-	 *
229
-	 * This function walks through the Nextcloud directory and loads all apps
230
-	 * it can find. A directory contains an app if the file /appinfo/info.xml
231
-	 * exists.
232
-	 *
233
-	 * if $types is set to non-empty array, only apps of those types will be loaded
234
-	 */
235
-	public function loadApps(array $types = []): bool {
236
-		if ($this->config->getSystemValueBool('maintenance', false)) {
237
-			return false;
238
-		}
239
-		// Load the enabled apps here
240
-		$apps = \OC_App::getEnabledApps();
241
-
242
-		// Add each apps' folder as allowed class path
243
-		foreach ($apps as $app) {
244
-			// If the app is already loaded then autoloading it makes no sense
245
-			if (!$this->isAppLoaded($app)) {
246
-				$path = \OC_App::getAppPath($app);
247
-				if ($path !== false) {
248
-					\OC_App::registerAutoloading($app, $path);
249
-				}
250
-			}
251
-		}
252
-
253
-		// prevent app loading from printing output
254
-		ob_start();
255
-		foreach ($apps as $app) {
256
-			if (!$this->isAppLoaded($app) && ($types === [] || $this->isType($app, $types))) {
257
-				try {
258
-					$this->loadApp($app);
259
-				} catch (\Throwable $e) {
260
-					$this->logger->emergency('Error during app loading: ' . $e->getMessage(), [
261
-						'exception' => $e,
262
-						'app' => $app,
263
-					]);
264
-				}
265
-			}
266
-		}
267
-		ob_end_clean();
268
-
269
-		return true;
270
-	}
271
-
272
-	/**
273
-	 * check if an app is of a specific type
274
-	 *
275
-	 * @param string $app
276
-	 * @param array $types
277
-	 * @return bool
278
-	 */
279
-	public function isType(string $app, array $types): bool {
280
-		$appTypes = $this->getAppTypes($app);
281
-		foreach ($types as $type) {
282
-			if (in_array($type, $appTypes, true)) {
283
-				return true;
284
-			}
285
-		}
286
-		return false;
287
-	}
288
-
289
-	/**
290
-	 * get the types of an app
291
-	 *
292
-	 * @param string $app
293
-	 * @return string[]
294
-	 */
295
-	private function getAppTypes(string $app): array {
296
-		//load the cache
297
-		if (count($this->appTypes) === 0) {
298
-			$this->appTypes = $this->getAppConfig()->getValues(false, 'types') ?: [];
299
-		}
300
-
301
-		if (isset($this->appTypes[$app])) {
302
-			return explode(',', $this->appTypes[$app]);
303
-		}
304
-
305
-		return [];
306
-	}
307
-
308
-	/**
309
-	 * @return array
310
-	 */
311
-	public function getAutoDisabledApps(): array {
312
-		return $this->autoDisabledApps;
313
-	}
314
-
315
-	public function getAppRestriction(string $appId): array {
316
-		$values = $this->getEnabledAppsValues();
317
-
318
-		if (!isset($values[$appId])) {
319
-			return [];
320
-		}
321
-
322
-		if ($values[$appId] === 'yes' || $values[$appId] === 'no') {
323
-			return [];
324
-		}
325
-		return json_decode($values[$appId], true);
326
-	}
327
-
328
-	/**
329
-	 * Check if an app is enabled for user
330
-	 *
331
-	 * @param string $appId
332
-	 * @param \OCP\IUser|null $user (optional) if not defined, the currently logged in user will be used
333
-	 * @return bool
334
-	 */
335
-	public function isEnabledForUser($appId, $user = null) {
336
-		if ($this->isAlwaysEnabled($appId)) {
337
-			return true;
338
-		}
339
-		if ($user === null) {
340
-			$user = $this->userSession->getUser();
341
-		}
342
-		$enabledAppsValues = $this->getEnabledAppsValues();
343
-		if (isset($enabledAppsValues[$appId])) {
344
-			return $this->checkAppForUser($enabledAppsValues[$appId], $user);
345
-		} else {
346
-			return false;
347
-		}
348
-	}
349
-
350
-	private function checkAppForUser(string $enabled, ?IUser $user): bool {
351
-		if ($enabled === 'yes') {
352
-			return true;
353
-		} elseif ($user === null) {
354
-			return false;
355
-		} else {
356
-			if (empty($enabled)) {
357
-				return false;
358
-			}
359
-
360
-			$groupIds = json_decode($enabled);
361
-
362
-			if (!is_array($groupIds)) {
363
-				$jsonError = json_last_error();
364
-				$jsonErrorMsg = json_last_error_msg();
365
-				// this really should never happen (if it does, the admin should check the `enabled` key value via `occ config:list` because it's bogus for some reason)
366
-				$this->logger->warning('AppManager::checkAppForUser - can\'t decode group IDs listed in app\'s enabled config key: ' . print_r($enabled, true) . ' - JSON error (' . $jsonError . ') ' . $jsonErrorMsg);
367
-				return false;
368
-			}
369
-
370
-			$userGroups = $this->groupManager->getUserGroupIds($user);
371
-			foreach ($userGroups as $groupId) {
372
-				if (in_array($groupId, $groupIds, true)) {
373
-					return true;
374
-				}
375
-			}
376
-			return false;
377
-		}
378
-	}
379
-
380
-	private function checkAppForGroups(string $enabled, IGroup $group): bool {
381
-		if ($enabled === 'yes') {
382
-			return true;
383
-		} else {
384
-			if (empty($enabled)) {
385
-				return false;
386
-			}
387
-
388
-			$groupIds = json_decode($enabled);
389
-
390
-			if (!is_array($groupIds)) {
391
-				$jsonError = json_last_error();
392
-				$jsonErrorMsg = json_last_error_msg();
393
-				// this really should never happen (if it does, the admin should check the `enabled` key value via `occ config:list` because it's bogus for some reason)
394
-				$this->logger->warning('AppManager::checkAppForGroups - can\'t decode group IDs listed in app\'s enabled config key: ' . print_r($enabled, true) . ' - JSON error (' . $jsonError . ') ' . $jsonErrorMsg);
395
-				return false;
396
-			}
397
-
398
-			return in_array($group->getGID(), $groupIds);
399
-		}
400
-	}
401
-
402
-	/**
403
-	 * Check if an app is enabled in the instance
404
-	 *
405
-	 * Notice: This actually checks if the app is enabled and not only if it is installed.
406
-	 *
407
-	 * @param string $appId
408
-	 */
409
-	public function isInstalled($appId): bool {
410
-		return $this->isEnabledForAnyone($appId);
411
-	}
412
-
413
-	public function isEnabledForAnyone(string $appId): bool {
414
-		$enabledAppsValues = $this->getEnabledAppsValues();
415
-		return isset($enabledAppsValues[$appId]);
416
-	}
417
-
418
-	/**
419
-	 * Overwrite the `max-version` requirement for this app.
420
-	 */
421
-	public function overwriteNextcloudRequirement(string $appId): void {
422
-		$ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
423
-		if (!in_array($appId, $ignoreMaxApps, true)) {
424
-			$ignoreMaxApps[] = $appId;
425
-		}
426
-		$this->config->setSystemValue('app_install_overwrite', $ignoreMaxApps);
427
-	}
428
-
429
-	/**
430
-	 * Remove the `max-version` overwrite for this app.
431
-	 * This means this app now again can not be enabled if the `max-version` is smaller than the current Nextcloud version.
432
-	 */
433
-	public function removeOverwriteNextcloudRequirement(string $appId): void {
434
-		$ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
435
-		$ignoreMaxApps = array_filter($ignoreMaxApps, fn (string $id) => $id !== $appId);
436
-		$this->config->setSystemValue('app_install_overwrite', $ignoreMaxApps);
437
-	}
438
-
439
-	public function loadApp(string $app): void {
440
-		if (isset($this->loadedApps[$app])) {
441
-			return;
442
-		}
443
-		$this->loadedApps[$app] = true;
444
-		$appPath = \OC_App::getAppPath($app);
445
-		if ($appPath === false) {
446
-			return;
447
-		}
448
-		$eventLogger = \OC::$server->get(IEventLogger::class);
449
-		$eventLogger->start("bootstrap:load_app:$app", "Load app: $app");
450
-
451
-		// in case someone calls loadApp() directly
452
-		\OC_App::registerAutoloading($app, $appPath);
453
-
454
-		if (is_file($appPath . '/appinfo/app.php')) {
455
-			$this->logger->error('/appinfo/app.php is not supported anymore, use \OCP\AppFramework\Bootstrap\IBootstrap on the application class instead.', [
456
-				'app' => $app,
457
-			]);
458
-		}
459
-
460
-		$coordinator = \OCP\Server::get(Coordinator::class);
461
-		$coordinator->bootApp($app);
462
-
463
-		$eventLogger->start("bootstrap:load_app:$app:info", "Load info.xml for $app and register any services defined in it");
464
-		$info = $this->getAppInfo($app);
465
-		if (!empty($info['activity'])) {
466
-			$activityManager = \OC::$server->get(IActivityManager::class);
467
-			if (!empty($info['activity']['filters'])) {
468
-				foreach ($info['activity']['filters'] as $filter) {
469
-					$activityManager->registerFilter($filter);
470
-				}
471
-			}
472
-			if (!empty($info['activity']['settings'])) {
473
-				foreach ($info['activity']['settings'] as $setting) {
474
-					$activityManager->registerSetting($setting);
475
-				}
476
-			}
477
-			if (!empty($info['activity']['providers'])) {
478
-				foreach ($info['activity']['providers'] as $provider) {
479
-					$activityManager->registerProvider($provider);
480
-				}
481
-			}
482
-		}
483
-
484
-		if (!empty($info['settings'])) {
485
-			$settingsManager = \OC::$server->get(ISettingsManager::class);
486
-			if (!empty($info['settings']['admin'])) {
487
-				foreach ($info['settings']['admin'] as $setting) {
488
-					$settingsManager->registerSetting('admin', $setting);
489
-				}
490
-			}
491
-			if (!empty($info['settings']['admin-section'])) {
492
-				foreach ($info['settings']['admin-section'] as $section) {
493
-					$settingsManager->registerSection('admin', $section);
494
-				}
495
-			}
496
-			if (!empty($info['settings']['personal'])) {
497
-				foreach ($info['settings']['personal'] as $setting) {
498
-					$settingsManager->registerSetting('personal', $setting);
499
-				}
500
-			}
501
-			if (!empty($info['settings']['personal-section'])) {
502
-				foreach ($info['settings']['personal-section'] as $section) {
503
-					$settingsManager->registerSection('personal', $section);
504
-				}
505
-			}
506
-		}
507
-
508
-		if (!empty($info['collaboration']['plugins'])) {
509
-			// deal with one or many plugin entries
510
-			$plugins = isset($info['collaboration']['plugins']['plugin']['@value']) ?
511
-				[$info['collaboration']['plugins']['plugin']] : $info['collaboration']['plugins']['plugin'];
512
-			$collaboratorSearch = null;
513
-			$autoCompleteManager = null;
514
-			foreach ($plugins as $plugin) {
515
-				if ($plugin['@attributes']['type'] === 'collaborator-search') {
516
-					$pluginInfo = [
517
-						'shareType' => $plugin['@attributes']['share-type'],
518
-						'class' => $plugin['@value'],
519
-					];
520
-					$collaboratorSearch ??= \OC::$server->get(ICollaboratorSearch::class);
521
-					$collaboratorSearch->registerPlugin($pluginInfo);
522
-				} elseif ($plugin['@attributes']['type'] === 'autocomplete-sort') {
523
-					$autoCompleteManager ??= \OC::$server->get(IAutoCompleteManager::class);
524
-					$autoCompleteManager->registerSorter($plugin['@value']);
525
-				}
526
-			}
527
-		}
528
-		$eventLogger->end("bootstrap:load_app:$app:info");
529
-
530
-		$eventLogger->end("bootstrap:load_app:$app");
531
-	}
532
-
533
-	/**
534
-	 * Check if an app is loaded
535
-	 * @param string $app app id
536
-	 * @since 26.0.0
537
-	 */
538
-	public function isAppLoaded(string $app): bool {
539
-		return isset($this->loadedApps[$app]);
540
-	}
541
-
542
-	/**
543
-	 * Enable an app for every user
544
-	 *
545
-	 * @param string $appId
546
-	 * @param bool $forceEnable
547
-	 * @throws AppPathNotFoundException
548
-	 * @throws \InvalidArgumentException if the application is not installed yet
549
-	 */
550
-	public function enableApp(string $appId, bool $forceEnable = false): void {
551
-		// Check if app exists
552
-		$this->getAppPath($appId);
553
-
554
-		if ($this->config->getAppValue($appId, 'installed_version', '') === '') {
555
-			throw new \InvalidArgumentException("$appId is not installed, cannot be enabled.");
556
-		}
557
-
558
-		if ($forceEnable) {
559
-			$this->overwriteNextcloudRequirement($appId);
560
-		}
561
-
562
-		$this->enabledAppsCache[$appId] = 'yes';
563
-		$this->getAppConfig()->setValue($appId, 'enabled', 'yes');
564
-		$this->dispatcher->dispatchTyped(new AppEnableEvent($appId));
565
-		$this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE, new ManagerEvent(
566
-			ManagerEvent::EVENT_APP_ENABLE, $appId
567
-		));
568
-		$this->clearAppsCache();
569
-	}
570
-
571
-	/**
572
-	 * Whether a list of types contains a protected app type
573
-	 *
574
-	 * @param string[] $types
575
-	 * @return bool
576
-	 */
577
-	public function hasProtectedAppType($types) {
578
-		if (empty($types)) {
579
-			return false;
580
-		}
581
-
582
-		$protectedTypes = array_intersect($this->protectedAppTypes, $types);
583
-		return !empty($protectedTypes);
584
-	}
585
-
586
-	/**
587
-	 * Enable an app only for specific groups
588
-	 *
589
-	 * @param string $appId
590
-	 * @param IGroup[] $groups
591
-	 * @param bool $forceEnable
592
-	 * @throws \InvalidArgumentException if app can't be enabled for groups
593
-	 * @throws AppPathNotFoundException
594
-	 */
595
-	public function enableAppForGroups(string $appId, array $groups, bool $forceEnable = false): void {
596
-		// Check if app exists
597
-		$this->getAppPath($appId);
598
-
599
-		$info = $this->getAppInfo($appId);
600
-		if (!empty($info['types']) && $this->hasProtectedAppType($info['types'])) {
601
-			throw new \InvalidArgumentException("$appId can't be enabled for groups.");
602
-		}
603
-
604
-		if ($this->config->getAppValue($appId, 'installed_version', '') === '') {
605
-			throw new \InvalidArgumentException("$appId is not installed, cannot be enabled.");
606
-		}
607
-
608
-		if ($forceEnable) {
609
-			$this->overwriteNextcloudRequirement($appId);
610
-		}
611
-
612
-		/** @var string[] $groupIds */
613
-		$groupIds = array_map(function ($group) {
614
-			/** @var IGroup $group */
615
-			return ($group instanceof IGroup)
616
-				? $group->getGID()
617
-				: $group;
618
-		}, $groups);
619
-
620
-		$this->enabledAppsCache[$appId] = json_encode($groupIds);
621
-		$this->getAppConfig()->setValue($appId, 'enabled', json_encode($groupIds));
622
-		$this->dispatcher->dispatchTyped(new AppEnableEvent($appId, $groupIds));
623
-		$this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, new ManagerEvent(
624
-			ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, $appId, $groups
625
-		));
626
-		$this->clearAppsCache();
627
-	}
628
-
629
-	/**
630
-	 * Disable an app for every user
631
-	 *
632
-	 * @param string $appId
633
-	 * @param bool $automaticDisabled
634
-	 * @throws \Exception if app can't be disabled
635
-	 */
636
-	public function disableApp($appId, $automaticDisabled = false): void {
637
-		if ($this->isAlwaysEnabled($appId)) {
638
-			throw new \Exception("$appId can't be disabled.");
639
-		}
640
-
641
-		if ($automaticDisabled) {
642
-			$previousSetting = $this->getAppConfig()->getValue($appId, 'enabled', 'yes');
643
-			if ($previousSetting !== 'yes' && $previousSetting !== 'no') {
644
-				$previousSetting = json_decode($previousSetting, true);
645
-			}
646
-			$this->autoDisabledApps[$appId] = $previousSetting;
647
-		}
648
-
649
-		unset($this->enabledAppsCache[$appId]);
650
-		$this->getAppConfig()->setValue($appId, 'enabled', 'no');
651
-
652
-		// run uninstall steps
653
-		$appData = $this->getAppInfo($appId);
654
-		if (!is_null($appData)) {
655
-			\OC_App::executeRepairSteps($appId, $appData['repair-steps']['uninstall']);
656
-		}
657
-
658
-		$this->dispatcher->dispatchTyped(new AppDisableEvent($appId));
659
-		$this->dispatcher->dispatch(ManagerEvent::EVENT_APP_DISABLE, new ManagerEvent(
660
-			ManagerEvent::EVENT_APP_DISABLE, $appId
661
-		));
662
-		$this->clearAppsCache();
663
-	}
664
-
665
-	/**
666
-	 * Get the directory for the given app.
667
-	 *
668
-	 * @throws AppPathNotFoundException if app folder can't be found
669
-	 */
670
-	public function getAppPath(string $appId): string {
671
-		$appPath = \OC_App::getAppPath($appId);
672
-		if ($appPath === false) {
673
-			throw new AppPathNotFoundException('Could not find path for ' . $appId);
674
-		}
675
-		return $appPath;
676
-	}
677
-
678
-	/**
679
-	 * Get the web path for the given app.
680
-	 *
681
-	 * @param string $appId
682
-	 * @return string
683
-	 * @throws AppPathNotFoundException if app path can't be found
684
-	 */
685
-	public function getAppWebPath(string $appId): string {
686
-		$appWebPath = \OC_App::getAppWebPath($appId);
687
-		if ($appWebPath === false) {
688
-			throw new AppPathNotFoundException('Could not find web path for ' . $appId);
689
-		}
690
-		return $appWebPath;
691
-	}
692
-
693
-	/**
694
-	 * Clear the cached list of apps when enabling/disabling an app
695
-	 */
696
-	public function clearAppsCache(): void {
697
-		$this->appInfos = [];
698
-	}
699
-
700
-	/**
701
-	 * Returns a list of apps that need upgrade
702
-	 *
703
-	 * @param string $version Nextcloud version as array of version components
704
-	 * @return array list of app info from apps that need an upgrade
705
-	 *
706
-	 * @internal
707
-	 */
708
-	public function getAppsNeedingUpgrade($version) {
709
-		$appsToUpgrade = [];
710
-		$apps = $this->getEnabledApps();
711
-		foreach ($apps as $appId) {
712
-			$appInfo = $this->getAppInfo($appId);
713
-			$appDbVersion = $this->getAppConfig()->getValue($appId, 'installed_version');
714
-			if ($appDbVersion
715
-				&& isset($appInfo['version'])
716
-				&& version_compare($appInfo['version'], $appDbVersion, '>')
717
-				&& \OC_App::isAppCompatible($version, $appInfo)
718
-			) {
719
-				$appsToUpgrade[] = $appInfo;
720
-			}
721
-		}
722
-
723
-		return $appsToUpgrade;
724
-	}
725
-
726
-	/**
727
-	 * Returns the app information from "appinfo/info.xml".
728
-	 *
729
-	 * @param string|null $lang
730
-	 * @return array|null app info
731
-	 */
732
-	public function getAppInfo(string $appId, bool $path = false, $lang = null) {
733
-		if ($path) {
734
-			throw new \InvalidArgumentException('Calling IAppManager::getAppInfo() with a path is no longer supported. Please call IAppManager::getAppInfoByPath() instead and verify that the path is good before calling.');
735
-		}
736
-		if ($lang === null && isset($this->appInfos[$appId])) {
737
-			return $this->appInfos[$appId];
738
-		}
739
-		try {
740
-			$appPath = $this->getAppPath($appId);
741
-		} catch (AppPathNotFoundException) {
742
-			return null;
743
-		}
744
-		$file = $appPath . '/appinfo/info.xml';
745
-
746
-		$data = $this->getAppInfoByPath($file, $lang);
747
-
748
-		if ($lang === null) {
749
-			$this->appInfos[$appId] = $data;
750
-		}
751
-
752
-		return $data;
753
-	}
754
-
755
-	public function getAppInfoByPath(string $path, ?string $lang = null): ?array {
756
-		if (!str_ends_with($path, '/appinfo/info.xml')) {
757
-			return null;
758
-		}
759
-
760
-		$parser = new InfoParser($this->memCacheFactory->createLocal('core.appinfo'));
761
-		$data = $parser->parse($path);
762
-
763
-		if (is_array($data)) {
764
-			$data = \OC_App::parseAppInfo($data, $lang);
765
-		}
766
-
767
-		return $data;
768
-	}
769
-
770
-	public function getAppVersion(string $appId, bool $useCache = true): string {
771
-		if (!$useCache || !isset($this->appVersions[$appId])) {
772
-			if ($appId === 'core') {
773
-				$this->appVersions[$appId] = $this->serverVersion->getVersionString();
774
-			} else {
775
-				$appInfo = $this->getAppInfo($appId);
776
-				$this->appVersions[$appId] = ($appInfo !== null && isset($appInfo['version'])) ? $appInfo['version'] : '0';
777
-			}
778
-		}
779
-		return $this->appVersions[$appId];
780
-	}
781
-
782
-	/**
783
-	 * Returns the installed versions of all apps
784
-	 *
785
-	 * @return array<string, string>
786
-	 */
787
-	public function getAppInstalledVersions(): array {
788
-		return $this->getAppConfig()->getAppInstalledVersions();
789
-	}
790
-
791
-	/**
792
-	 * Returns a list of apps incompatible with the given version
793
-	 *
794
-	 * @param string $version Nextcloud version as array of version components
795
-	 *
796
-	 * @return array list of app info from incompatible apps
797
-	 *
798
-	 * @internal
799
-	 */
800
-	public function getIncompatibleApps(string $version): array {
801
-		$apps = $this->getEnabledApps();
802
-		$incompatibleApps = [];
803
-		foreach ($apps as $appId) {
804
-			$info = $this->getAppInfo($appId);
805
-			if ($info === null) {
806
-				$incompatibleApps[] = ['id' => $appId, 'name' => $appId];
807
-			} elseif (!\OC_App::isAppCompatible($version, $info)) {
808
-				$incompatibleApps[] = $info;
809
-			}
810
-		}
811
-		return $incompatibleApps;
812
-	}
813
-
814
-	/**
815
-	 * @inheritdoc
816
-	 * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::isShipped()
817
-	 */
818
-	public function isShipped($appId) {
819
-		$this->loadShippedJson();
820
-		return in_array($appId, $this->shippedApps, true);
821
-	}
822
-
823
-	private function isAlwaysEnabled(string $appId): bool {
824
-		if ($appId === 'core') {
825
-			return true;
826
-		}
827
-
828
-		$alwaysEnabled = $this->getAlwaysEnabledApps();
829
-		return in_array($appId, $alwaysEnabled, true);
830
-	}
831
-
832
-	/**
833
-	 * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::loadShippedJson()
834
-	 * @throws \Exception
835
-	 */
836
-	private function loadShippedJson(): void {
837
-		if ($this->shippedApps === null) {
838
-			$shippedJson = \OC::$SERVERROOT . '/core/shipped.json';
839
-			if (!file_exists($shippedJson)) {
840
-				throw new \Exception("File not found: $shippedJson");
841
-			}
842
-			$content = json_decode(file_get_contents($shippedJson), true);
843
-			$this->shippedApps = $content['shippedApps'];
844
-			$this->alwaysEnabled = $content['alwaysEnabled'];
845
-			$this->defaultEnabled = $content['defaultEnabled'];
846
-		}
847
-	}
848
-
849
-	/**
850
-	 * @inheritdoc
851
-	 */
852
-	public function getAlwaysEnabledApps() {
853
-		$this->loadShippedJson();
854
-		return $this->alwaysEnabled;
855
-	}
856
-
857
-	/**
858
-	 * @inheritdoc
859
-	 */
860
-	public function isDefaultEnabled(string $appId): bool {
861
-		return (in_array($appId, $this->getDefaultEnabledApps()));
862
-	}
863
-
864
-	/**
865
-	 * @inheritdoc
866
-	 */
867
-	public function getDefaultEnabledApps(): array {
868
-		$this->loadShippedJson();
869
-
870
-		return $this->defaultEnabled;
871
-	}
872
-
873
-	/**
874
-	 * @inheritdoc
875
-	 */
876
-	public function getDefaultAppForUser(?IUser $user = null, bool $withFallbacks = true): string {
877
-		$id = $this->getNavigationManager()->getDefaultEntryIdForUser($user, $withFallbacks);
878
-		$entry = $this->getNavigationManager()->get($id);
879
-		return (string)$entry['app'];
880
-	}
881
-
882
-	/**
883
-	 * @inheritdoc
884
-	 */
885
-	public function getDefaultApps(): array {
886
-		$ids = $this->getNavigationManager()->getDefaultEntryIds();
887
-
888
-		return array_values(array_unique(array_map(function (string $id) {
889
-			$entry = $this->getNavigationManager()->get($id);
890
-			return (string)$entry['app'];
891
-		}, $ids)));
892
-	}
893
-
894
-	/**
895
-	 * @inheritdoc
896
-	 */
897
-	public function setDefaultApps(array $defaultApps): void {
898
-		$entries = $this->getNavigationManager()->getAll();
899
-		$ids = [];
900
-		foreach ($defaultApps as $defaultApp) {
901
-			foreach ($entries as $entry) {
902
-				if ((string)$entry['app'] === $defaultApp) {
903
-					$ids[] = (string)$entry['id'];
904
-					break;
905
-				}
906
-			}
907
-		}
908
-		$this->getNavigationManager()->setDefaultEntryIds($ids);
909
-	}
910
-
911
-	public function isBackendRequired(string $backend): bool {
912
-		foreach ($this->appInfos as $appInfo) {
913
-			foreach ($appInfo['dependencies']['backend'] as $appBackend) {
914
-				if ($backend === $appBackend) {
915
-					return true;
916
-				}
917
-			}
918
-		}
919
-
920
-		return false;
921
-	}
922
-
923
-	/**
924
-	 * Clean the appId from forbidden characters
925
-	 *
926
-	 * @psalm-taint-escape callable
927
-	 * @psalm-taint-escape cookie
928
-	 * @psalm-taint-escape file
929
-	 * @psalm-taint-escape has_quotes
930
-	 * @psalm-taint-escape header
931
-	 * @psalm-taint-escape html
932
-	 * @psalm-taint-escape include
933
-	 * @psalm-taint-escape ldap
934
-	 * @psalm-taint-escape shell
935
-	 * @psalm-taint-escape sql
936
-	 * @psalm-taint-escape unserialize
937
-	 */
938
-	public function cleanAppId(string $app): string {
939
-		/* Only lowercase alphanumeric is allowed */
940
-		return preg_replace('/(^[0-9_]|[^a-z0-9_]+|_$)/', '', $app);
941
-	}
34
+    /**
35
+     * Apps with these types can not be enabled for certain groups only
36
+     * @var string[]
37
+     */
38
+    protected $protectedAppTypes = [
39
+        'filesystem',
40
+        'prelogin',
41
+        'authentication',
42
+        'logging',
43
+        'prevent_group_restriction',
44
+    ];
45
+
46
+    /** @var string[] $appId => $enabled */
47
+    private array $enabledAppsCache = [];
48
+
49
+    /** @var string[]|null */
50
+    private ?array $shippedApps = null;
51
+
52
+    private array $alwaysEnabled = [];
53
+    private array $defaultEnabled = [];
54
+
55
+    /** @var array */
56
+    private array $appInfos = [];
57
+
58
+    /** @var array */
59
+    private array $appVersions = [];
60
+
61
+    /** @var array */
62
+    private array $autoDisabledApps = [];
63
+    private array $appTypes = [];
64
+
65
+    /** @var array<string, true> */
66
+    private array $loadedApps = [];
67
+
68
+    private ?AppConfig $appConfig = null;
69
+    private ?IURLGenerator $urlGenerator = null;
70
+    private ?INavigationManager $navigationManager = null;
71
+
72
+    /**
73
+     * Be extremely careful when injecting classes here. The AppManager is used by the installer,
74
+     * so it needs to work before installation. See how AppConfig and IURLGenerator are injected for reference
75
+     */
76
+    public function __construct(
77
+        private IUserSession $userSession,
78
+        private IConfig $config,
79
+        private IGroupManager $groupManager,
80
+        private ICacheFactory $memCacheFactory,
81
+        private IEventDispatcher $dispatcher,
82
+        private LoggerInterface $logger,
83
+        private ServerVersion $serverVersion,
84
+    ) {
85
+    }
86
+
87
+    private function getNavigationManager(): INavigationManager {
88
+        if ($this->navigationManager === null) {
89
+            $this->navigationManager = \OCP\Server::get(INavigationManager::class);
90
+        }
91
+        return $this->navigationManager;
92
+    }
93
+
94
+    public function getAppIcon(string $appId, bool $dark = false): ?string {
95
+        $possibleIcons = $dark ? [$appId . '-dark.svg', 'app-dark.svg'] : [$appId . '.svg', 'app.svg'];
96
+        $icon = null;
97
+        foreach ($possibleIcons as $iconName) {
98
+            try {
99
+                $icon = $this->getUrlGenerator()->imagePath($appId, $iconName);
100
+                break;
101
+            } catch (\RuntimeException $e) {
102
+                // ignore
103
+            }
104
+        }
105
+        return $icon;
106
+    }
107
+
108
+    private function getAppConfig(): AppConfig {
109
+        if ($this->appConfig !== null) {
110
+            return $this->appConfig;
111
+        }
112
+        if (!$this->config->getSystemValueBool('installed', false)) {
113
+            throw new \Exception('Nextcloud is not installed yet, AppConfig is not available');
114
+        }
115
+        $this->appConfig = \OCP\Server::get(AppConfig::class);
116
+        return $this->appConfig;
117
+    }
118
+
119
+    private function getUrlGenerator(): IURLGenerator {
120
+        if ($this->urlGenerator !== null) {
121
+            return $this->urlGenerator;
122
+        }
123
+        if (!$this->config->getSystemValueBool('installed', false)) {
124
+            throw new \Exception('Nextcloud is not installed yet, AppConfig is not available');
125
+        }
126
+        $this->urlGenerator = \OCP\Server::get(IURLGenerator::class);
127
+        return $this->urlGenerator;
128
+    }
129
+
130
+    /**
131
+     * For all enabled apps, return the value of their 'enabled' config key.
132
+     *
133
+     * @return array<string,string> appId => enabled (may be 'yes', or a json encoded list of group ids)
134
+     */
135
+    private function getEnabledAppsValues(): array {
136
+        if (!$this->enabledAppsCache) {
137
+            $values = $this->getAppConfig()->getValues(false, 'enabled');
138
+
139
+            $alwaysEnabledApps = $this->getAlwaysEnabledApps();
140
+            foreach ($alwaysEnabledApps as $appId) {
141
+                $values[$appId] = 'yes';
142
+            }
143
+
144
+            $this->enabledAppsCache = array_filter($values, function ($value) {
145
+                return $value !== 'no';
146
+            });
147
+            ksort($this->enabledAppsCache);
148
+        }
149
+        return $this->enabledAppsCache;
150
+    }
151
+
152
+    /**
153
+     * Deprecated alias
154
+     *
155
+     * @return string[]
156
+     */
157
+    public function getInstalledApps() {
158
+        return $this->getEnabledApps();
159
+    }
160
+
161
+    /**
162
+     * List all enabled apps, either for everyone or for some groups
163
+     *
164
+     * @return list<string>
165
+     */
166
+    public function getEnabledApps(): array {
167
+        return array_keys($this->getEnabledAppsValues());
168
+    }
169
+
170
+    /**
171
+     * Get a list of all apps in the apps folder
172
+     *
173
+     * @return list<string> an array of app names (string IDs)
174
+     */
175
+    public function getAllAppsInAppsFolders(): array {
176
+        $apps = [];
177
+
178
+        foreach (\OC::$APPSROOTS as $apps_dir) {
179
+            if (!is_readable($apps_dir['path'])) {
180
+                $this->logger->warning('unable to read app folder : ' . $apps_dir['path'], ['app' => 'core']);
181
+                continue;
182
+            }
183
+            $dh = opendir($apps_dir['path']);
184
+
185
+            if (is_resource($dh)) {
186
+                while (($file = readdir($dh)) !== false) {
187
+                    if (
188
+                        $file[0] != '.' &&
189
+                        is_dir($apps_dir['path'] . '/' . $file) &&
190
+                        is_file($apps_dir['path'] . '/' . $file . '/appinfo/info.xml')
191
+                    ) {
192
+                        $apps[] = $file;
193
+                    }
194
+                }
195
+            }
196
+        }
197
+
198
+        return array_values(array_unique($apps));
199
+    }
200
+
201
+    /**
202
+     * List all apps enabled for a user
203
+     *
204
+     * @param \OCP\IUser $user
205
+     * @return string[]
206
+     */
207
+    public function getEnabledAppsForUser(IUser $user) {
208
+        $apps = $this->getEnabledAppsValues();
209
+        $appsForUser = array_filter($apps, function ($enabled) use ($user) {
210
+            return $this->checkAppForUser($enabled, $user);
211
+        });
212
+        return array_keys($appsForUser);
213
+    }
214
+
215
+    public function getEnabledAppsForGroup(IGroup $group): array {
216
+        $apps = $this->getEnabledAppsValues();
217
+        $appsForGroups = array_filter($apps, function ($enabled) use ($group) {
218
+            return $this->checkAppForGroups($enabled, $group);
219
+        });
220
+        return array_keys($appsForGroups);
221
+    }
222
+
223
+    /**
224
+     * Loads all apps
225
+     *
226
+     * @param string[] $types
227
+     * @return bool
228
+     *
229
+     * This function walks through the Nextcloud directory and loads all apps
230
+     * it can find. A directory contains an app if the file /appinfo/info.xml
231
+     * exists.
232
+     *
233
+     * if $types is set to non-empty array, only apps of those types will be loaded
234
+     */
235
+    public function loadApps(array $types = []): bool {
236
+        if ($this->config->getSystemValueBool('maintenance', false)) {
237
+            return false;
238
+        }
239
+        // Load the enabled apps here
240
+        $apps = \OC_App::getEnabledApps();
241
+
242
+        // Add each apps' folder as allowed class path
243
+        foreach ($apps as $app) {
244
+            // If the app is already loaded then autoloading it makes no sense
245
+            if (!$this->isAppLoaded($app)) {
246
+                $path = \OC_App::getAppPath($app);
247
+                if ($path !== false) {
248
+                    \OC_App::registerAutoloading($app, $path);
249
+                }
250
+            }
251
+        }
252
+
253
+        // prevent app loading from printing output
254
+        ob_start();
255
+        foreach ($apps as $app) {
256
+            if (!$this->isAppLoaded($app) && ($types === [] || $this->isType($app, $types))) {
257
+                try {
258
+                    $this->loadApp($app);
259
+                } catch (\Throwable $e) {
260
+                    $this->logger->emergency('Error during app loading: ' . $e->getMessage(), [
261
+                        'exception' => $e,
262
+                        'app' => $app,
263
+                    ]);
264
+                }
265
+            }
266
+        }
267
+        ob_end_clean();
268
+
269
+        return true;
270
+    }
271
+
272
+    /**
273
+     * check if an app is of a specific type
274
+     *
275
+     * @param string $app
276
+     * @param array $types
277
+     * @return bool
278
+     */
279
+    public function isType(string $app, array $types): bool {
280
+        $appTypes = $this->getAppTypes($app);
281
+        foreach ($types as $type) {
282
+            if (in_array($type, $appTypes, true)) {
283
+                return true;
284
+            }
285
+        }
286
+        return false;
287
+    }
288
+
289
+    /**
290
+     * get the types of an app
291
+     *
292
+     * @param string $app
293
+     * @return string[]
294
+     */
295
+    private function getAppTypes(string $app): array {
296
+        //load the cache
297
+        if (count($this->appTypes) === 0) {
298
+            $this->appTypes = $this->getAppConfig()->getValues(false, 'types') ?: [];
299
+        }
300
+
301
+        if (isset($this->appTypes[$app])) {
302
+            return explode(',', $this->appTypes[$app]);
303
+        }
304
+
305
+        return [];
306
+    }
307
+
308
+    /**
309
+     * @return array
310
+     */
311
+    public function getAutoDisabledApps(): array {
312
+        return $this->autoDisabledApps;
313
+    }
314
+
315
+    public function getAppRestriction(string $appId): array {
316
+        $values = $this->getEnabledAppsValues();
317
+
318
+        if (!isset($values[$appId])) {
319
+            return [];
320
+        }
321
+
322
+        if ($values[$appId] === 'yes' || $values[$appId] === 'no') {
323
+            return [];
324
+        }
325
+        return json_decode($values[$appId], true);
326
+    }
327
+
328
+    /**
329
+     * Check if an app is enabled for user
330
+     *
331
+     * @param string $appId
332
+     * @param \OCP\IUser|null $user (optional) if not defined, the currently logged in user will be used
333
+     * @return bool
334
+     */
335
+    public function isEnabledForUser($appId, $user = null) {
336
+        if ($this->isAlwaysEnabled($appId)) {
337
+            return true;
338
+        }
339
+        if ($user === null) {
340
+            $user = $this->userSession->getUser();
341
+        }
342
+        $enabledAppsValues = $this->getEnabledAppsValues();
343
+        if (isset($enabledAppsValues[$appId])) {
344
+            return $this->checkAppForUser($enabledAppsValues[$appId], $user);
345
+        } else {
346
+            return false;
347
+        }
348
+    }
349
+
350
+    private function checkAppForUser(string $enabled, ?IUser $user): bool {
351
+        if ($enabled === 'yes') {
352
+            return true;
353
+        } elseif ($user === null) {
354
+            return false;
355
+        } else {
356
+            if (empty($enabled)) {
357
+                return false;
358
+            }
359
+
360
+            $groupIds = json_decode($enabled);
361
+
362
+            if (!is_array($groupIds)) {
363
+                $jsonError = json_last_error();
364
+                $jsonErrorMsg = json_last_error_msg();
365
+                // this really should never happen (if it does, the admin should check the `enabled` key value via `occ config:list` because it's bogus for some reason)
366
+                $this->logger->warning('AppManager::checkAppForUser - can\'t decode group IDs listed in app\'s enabled config key: ' . print_r($enabled, true) . ' - JSON error (' . $jsonError . ') ' . $jsonErrorMsg);
367
+                return false;
368
+            }
369
+
370
+            $userGroups = $this->groupManager->getUserGroupIds($user);
371
+            foreach ($userGroups as $groupId) {
372
+                if (in_array($groupId, $groupIds, true)) {
373
+                    return true;
374
+                }
375
+            }
376
+            return false;
377
+        }
378
+    }
379
+
380
+    private function checkAppForGroups(string $enabled, IGroup $group): bool {
381
+        if ($enabled === 'yes') {
382
+            return true;
383
+        } else {
384
+            if (empty($enabled)) {
385
+                return false;
386
+            }
387
+
388
+            $groupIds = json_decode($enabled);
389
+
390
+            if (!is_array($groupIds)) {
391
+                $jsonError = json_last_error();
392
+                $jsonErrorMsg = json_last_error_msg();
393
+                // this really should never happen (if it does, the admin should check the `enabled` key value via `occ config:list` because it's bogus for some reason)
394
+                $this->logger->warning('AppManager::checkAppForGroups - can\'t decode group IDs listed in app\'s enabled config key: ' . print_r($enabled, true) . ' - JSON error (' . $jsonError . ') ' . $jsonErrorMsg);
395
+                return false;
396
+            }
397
+
398
+            return in_array($group->getGID(), $groupIds);
399
+        }
400
+    }
401
+
402
+    /**
403
+     * Check if an app is enabled in the instance
404
+     *
405
+     * Notice: This actually checks if the app is enabled and not only if it is installed.
406
+     *
407
+     * @param string $appId
408
+     */
409
+    public function isInstalled($appId): bool {
410
+        return $this->isEnabledForAnyone($appId);
411
+    }
412
+
413
+    public function isEnabledForAnyone(string $appId): bool {
414
+        $enabledAppsValues = $this->getEnabledAppsValues();
415
+        return isset($enabledAppsValues[$appId]);
416
+    }
417
+
418
+    /**
419
+     * Overwrite the `max-version` requirement for this app.
420
+     */
421
+    public function overwriteNextcloudRequirement(string $appId): void {
422
+        $ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
423
+        if (!in_array($appId, $ignoreMaxApps, true)) {
424
+            $ignoreMaxApps[] = $appId;
425
+        }
426
+        $this->config->setSystemValue('app_install_overwrite', $ignoreMaxApps);
427
+    }
428
+
429
+    /**
430
+     * Remove the `max-version` overwrite for this app.
431
+     * This means this app now again can not be enabled if the `max-version` is smaller than the current Nextcloud version.
432
+     */
433
+    public function removeOverwriteNextcloudRequirement(string $appId): void {
434
+        $ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
435
+        $ignoreMaxApps = array_filter($ignoreMaxApps, fn (string $id) => $id !== $appId);
436
+        $this->config->setSystemValue('app_install_overwrite', $ignoreMaxApps);
437
+    }
438
+
439
+    public function loadApp(string $app): void {
440
+        if (isset($this->loadedApps[$app])) {
441
+            return;
442
+        }
443
+        $this->loadedApps[$app] = true;
444
+        $appPath = \OC_App::getAppPath($app);
445
+        if ($appPath === false) {
446
+            return;
447
+        }
448
+        $eventLogger = \OC::$server->get(IEventLogger::class);
449
+        $eventLogger->start("bootstrap:load_app:$app", "Load app: $app");
450
+
451
+        // in case someone calls loadApp() directly
452
+        \OC_App::registerAutoloading($app, $appPath);
453
+
454
+        if (is_file($appPath . '/appinfo/app.php')) {
455
+            $this->logger->error('/appinfo/app.php is not supported anymore, use \OCP\AppFramework\Bootstrap\IBootstrap on the application class instead.', [
456
+                'app' => $app,
457
+            ]);
458
+        }
459
+
460
+        $coordinator = \OCP\Server::get(Coordinator::class);
461
+        $coordinator->bootApp($app);
462
+
463
+        $eventLogger->start("bootstrap:load_app:$app:info", "Load info.xml for $app and register any services defined in it");
464
+        $info = $this->getAppInfo($app);
465
+        if (!empty($info['activity'])) {
466
+            $activityManager = \OC::$server->get(IActivityManager::class);
467
+            if (!empty($info['activity']['filters'])) {
468
+                foreach ($info['activity']['filters'] as $filter) {
469
+                    $activityManager->registerFilter($filter);
470
+                }
471
+            }
472
+            if (!empty($info['activity']['settings'])) {
473
+                foreach ($info['activity']['settings'] as $setting) {
474
+                    $activityManager->registerSetting($setting);
475
+                }
476
+            }
477
+            if (!empty($info['activity']['providers'])) {
478
+                foreach ($info['activity']['providers'] as $provider) {
479
+                    $activityManager->registerProvider($provider);
480
+                }
481
+            }
482
+        }
483
+
484
+        if (!empty($info['settings'])) {
485
+            $settingsManager = \OC::$server->get(ISettingsManager::class);
486
+            if (!empty($info['settings']['admin'])) {
487
+                foreach ($info['settings']['admin'] as $setting) {
488
+                    $settingsManager->registerSetting('admin', $setting);
489
+                }
490
+            }
491
+            if (!empty($info['settings']['admin-section'])) {
492
+                foreach ($info['settings']['admin-section'] as $section) {
493
+                    $settingsManager->registerSection('admin', $section);
494
+                }
495
+            }
496
+            if (!empty($info['settings']['personal'])) {
497
+                foreach ($info['settings']['personal'] as $setting) {
498
+                    $settingsManager->registerSetting('personal', $setting);
499
+                }
500
+            }
501
+            if (!empty($info['settings']['personal-section'])) {
502
+                foreach ($info['settings']['personal-section'] as $section) {
503
+                    $settingsManager->registerSection('personal', $section);
504
+                }
505
+            }
506
+        }
507
+
508
+        if (!empty($info['collaboration']['plugins'])) {
509
+            // deal with one or many plugin entries
510
+            $plugins = isset($info['collaboration']['plugins']['plugin']['@value']) ?
511
+                [$info['collaboration']['plugins']['plugin']] : $info['collaboration']['plugins']['plugin'];
512
+            $collaboratorSearch = null;
513
+            $autoCompleteManager = null;
514
+            foreach ($plugins as $plugin) {
515
+                if ($plugin['@attributes']['type'] === 'collaborator-search') {
516
+                    $pluginInfo = [
517
+                        'shareType' => $plugin['@attributes']['share-type'],
518
+                        'class' => $plugin['@value'],
519
+                    ];
520
+                    $collaboratorSearch ??= \OC::$server->get(ICollaboratorSearch::class);
521
+                    $collaboratorSearch->registerPlugin($pluginInfo);
522
+                } elseif ($plugin['@attributes']['type'] === 'autocomplete-sort') {
523
+                    $autoCompleteManager ??= \OC::$server->get(IAutoCompleteManager::class);
524
+                    $autoCompleteManager->registerSorter($plugin['@value']);
525
+                }
526
+            }
527
+        }
528
+        $eventLogger->end("bootstrap:load_app:$app:info");
529
+
530
+        $eventLogger->end("bootstrap:load_app:$app");
531
+    }
532
+
533
+    /**
534
+     * Check if an app is loaded
535
+     * @param string $app app id
536
+     * @since 26.0.0
537
+     */
538
+    public function isAppLoaded(string $app): bool {
539
+        return isset($this->loadedApps[$app]);
540
+    }
541
+
542
+    /**
543
+     * Enable an app for every user
544
+     *
545
+     * @param string $appId
546
+     * @param bool $forceEnable
547
+     * @throws AppPathNotFoundException
548
+     * @throws \InvalidArgumentException if the application is not installed yet
549
+     */
550
+    public function enableApp(string $appId, bool $forceEnable = false): void {
551
+        // Check if app exists
552
+        $this->getAppPath($appId);
553
+
554
+        if ($this->config->getAppValue($appId, 'installed_version', '') === '') {
555
+            throw new \InvalidArgumentException("$appId is not installed, cannot be enabled.");
556
+        }
557
+
558
+        if ($forceEnable) {
559
+            $this->overwriteNextcloudRequirement($appId);
560
+        }
561
+
562
+        $this->enabledAppsCache[$appId] = 'yes';
563
+        $this->getAppConfig()->setValue($appId, 'enabled', 'yes');
564
+        $this->dispatcher->dispatchTyped(new AppEnableEvent($appId));
565
+        $this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE, new ManagerEvent(
566
+            ManagerEvent::EVENT_APP_ENABLE, $appId
567
+        ));
568
+        $this->clearAppsCache();
569
+    }
570
+
571
+    /**
572
+     * Whether a list of types contains a protected app type
573
+     *
574
+     * @param string[] $types
575
+     * @return bool
576
+     */
577
+    public function hasProtectedAppType($types) {
578
+        if (empty($types)) {
579
+            return false;
580
+        }
581
+
582
+        $protectedTypes = array_intersect($this->protectedAppTypes, $types);
583
+        return !empty($protectedTypes);
584
+    }
585
+
586
+    /**
587
+     * Enable an app only for specific groups
588
+     *
589
+     * @param string $appId
590
+     * @param IGroup[] $groups
591
+     * @param bool $forceEnable
592
+     * @throws \InvalidArgumentException if app can't be enabled for groups
593
+     * @throws AppPathNotFoundException
594
+     */
595
+    public function enableAppForGroups(string $appId, array $groups, bool $forceEnable = false): void {
596
+        // Check if app exists
597
+        $this->getAppPath($appId);
598
+
599
+        $info = $this->getAppInfo($appId);
600
+        if (!empty($info['types']) && $this->hasProtectedAppType($info['types'])) {
601
+            throw new \InvalidArgumentException("$appId can't be enabled for groups.");
602
+        }
603
+
604
+        if ($this->config->getAppValue($appId, 'installed_version', '') === '') {
605
+            throw new \InvalidArgumentException("$appId is not installed, cannot be enabled.");
606
+        }
607
+
608
+        if ($forceEnable) {
609
+            $this->overwriteNextcloudRequirement($appId);
610
+        }
611
+
612
+        /** @var string[] $groupIds */
613
+        $groupIds = array_map(function ($group) {
614
+            /** @var IGroup $group */
615
+            return ($group instanceof IGroup)
616
+                ? $group->getGID()
617
+                : $group;
618
+        }, $groups);
619
+
620
+        $this->enabledAppsCache[$appId] = json_encode($groupIds);
621
+        $this->getAppConfig()->setValue($appId, 'enabled', json_encode($groupIds));
622
+        $this->dispatcher->dispatchTyped(new AppEnableEvent($appId, $groupIds));
623
+        $this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, new ManagerEvent(
624
+            ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, $appId, $groups
625
+        ));
626
+        $this->clearAppsCache();
627
+    }
628
+
629
+    /**
630
+     * Disable an app for every user
631
+     *
632
+     * @param string $appId
633
+     * @param bool $automaticDisabled
634
+     * @throws \Exception if app can't be disabled
635
+     */
636
+    public function disableApp($appId, $automaticDisabled = false): void {
637
+        if ($this->isAlwaysEnabled($appId)) {
638
+            throw new \Exception("$appId can't be disabled.");
639
+        }
640
+
641
+        if ($automaticDisabled) {
642
+            $previousSetting = $this->getAppConfig()->getValue($appId, 'enabled', 'yes');
643
+            if ($previousSetting !== 'yes' && $previousSetting !== 'no') {
644
+                $previousSetting = json_decode($previousSetting, true);
645
+            }
646
+            $this->autoDisabledApps[$appId] = $previousSetting;
647
+        }
648
+
649
+        unset($this->enabledAppsCache[$appId]);
650
+        $this->getAppConfig()->setValue($appId, 'enabled', 'no');
651
+
652
+        // run uninstall steps
653
+        $appData = $this->getAppInfo($appId);
654
+        if (!is_null($appData)) {
655
+            \OC_App::executeRepairSteps($appId, $appData['repair-steps']['uninstall']);
656
+        }
657
+
658
+        $this->dispatcher->dispatchTyped(new AppDisableEvent($appId));
659
+        $this->dispatcher->dispatch(ManagerEvent::EVENT_APP_DISABLE, new ManagerEvent(
660
+            ManagerEvent::EVENT_APP_DISABLE, $appId
661
+        ));
662
+        $this->clearAppsCache();
663
+    }
664
+
665
+    /**
666
+     * Get the directory for the given app.
667
+     *
668
+     * @throws AppPathNotFoundException if app folder can't be found
669
+     */
670
+    public function getAppPath(string $appId): string {
671
+        $appPath = \OC_App::getAppPath($appId);
672
+        if ($appPath === false) {
673
+            throw new AppPathNotFoundException('Could not find path for ' . $appId);
674
+        }
675
+        return $appPath;
676
+    }
677
+
678
+    /**
679
+     * Get the web path for the given app.
680
+     *
681
+     * @param string $appId
682
+     * @return string
683
+     * @throws AppPathNotFoundException if app path can't be found
684
+     */
685
+    public function getAppWebPath(string $appId): string {
686
+        $appWebPath = \OC_App::getAppWebPath($appId);
687
+        if ($appWebPath === false) {
688
+            throw new AppPathNotFoundException('Could not find web path for ' . $appId);
689
+        }
690
+        return $appWebPath;
691
+    }
692
+
693
+    /**
694
+     * Clear the cached list of apps when enabling/disabling an app
695
+     */
696
+    public function clearAppsCache(): void {
697
+        $this->appInfos = [];
698
+    }
699
+
700
+    /**
701
+     * Returns a list of apps that need upgrade
702
+     *
703
+     * @param string $version Nextcloud version as array of version components
704
+     * @return array list of app info from apps that need an upgrade
705
+     *
706
+     * @internal
707
+     */
708
+    public function getAppsNeedingUpgrade($version) {
709
+        $appsToUpgrade = [];
710
+        $apps = $this->getEnabledApps();
711
+        foreach ($apps as $appId) {
712
+            $appInfo = $this->getAppInfo($appId);
713
+            $appDbVersion = $this->getAppConfig()->getValue($appId, 'installed_version');
714
+            if ($appDbVersion
715
+                && isset($appInfo['version'])
716
+                && version_compare($appInfo['version'], $appDbVersion, '>')
717
+                && \OC_App::isAppCompatible($version, $appInfo)
718
+            ) {
719
+                $appsToUpgrade[] = $appInfo;
720
+            }
721
+        }
722
+
723
+        return $appsToUpgrade;
724
+    }
725
+
726
+    /**
727
+     * Returns the app information from "appinfo/info.xml".
728
+     *
729
+     * @param string|null $lang
730
+     * @return array|null app info
731
+     */
732
+    public function getAppInfo(string $appId, bool $path = false, $lang = null) {
733
+        if ($path) {
734
+            throw new \InvalidArgumentException('Calling IAppManager::getAppInfo() with a path is no longer supported. Please call IAppManager::getAppInfoByPath() instead and verify that the path is good before calling.');
735
+        }
736
+        if ($lang === null && isset($this->appInfos[$appId])) {
737
+            return $this->appInfos[$appId];
738
+        }
739
+        try {
740
+            $appPath = $this->getAppPath($appId);
741
+        } catch (AppPathNotFoundException) {
742
+            return null;
743
+        }
744
+        $file = $appPath . '/appinfo/info.xml';
745
+
746
+        $data = $this->getAppInfoByPath($file, $lang);
747
+
748
+        if ($lang === null) {
749
+            $this->appInfos[$appId] = $data;
750
+        }
751
+
752
+        return $data;
753
+    }
754
+
755
+    public function getAppInfoByPath(string $path, ?string $lang = null): ?array {
756
+        if (!str_ends_with($path, '/appinfo/info.xml')) {
757
+            return null;
758
+        }
759
+
760
+        $parser = new InfoParser($this->memCacheFactory->createLocal('core.appinfo'));
761
+        $data = $parser->parse($path);
762
+
763
+        if (is_array($data)) {
764
+            $data = \OC_App::parseAppInfo($data, $lang);
765
+        }
766
+
767
+        return $data;
768
+    }
769
+
770
+    public function getAppVersion(string $appId, bool $useCache = true): string {
771
+        if (!$useCache || !isset($this->appVersions[$appId])) {
772
+            if ($appId === 'core') {
773
+                $this->appVersions[$appId] = $this->serverVersion->getVersionString();
774
+            } else {
775
+                $appInfo = $this->getAppInfo($appId);
776
+                $this->appVersions[$appId] = ($appInfo !== null && isset($appInfo['version'])) ? $appInfo['version'] : '0';
777
+            }
778
+        }
779
+        return $this->appVersions[$appId];
780
+    }
781
+
782
+    /**
783
+     * Returns the installed versions of all apps
784
+     *
785
+     * @return array<string, string>
786
+     */
787
+    public function getAppInstalledVersions(): array {
788
+        return $this->getAppConfig()->getAppInstalledVersions();
789
+    }
790
+
791
+    /**
792
+     * Returns a list of apps incompatible with the given version
793
+     *
794
+     * @param string $version Nextcloud version as array of version components
795
+     *
796
+     * @return array list of app info from incompatible apps
797
+     *
798
+     * @internal
799
+     */
800
+    public function getIncompatibleApps(string $version): array {
801
+        $apps = $this->getEnabledApps();
802
+        $incompatibleApps = [];
803
+        foreach ($apps as $appId) {
804
+            $info = $this->getAppInfo($appId);
805
+            if ($info === null) {
806
+                $incompatibleApps[] = ['id' => $appId, 'name' => $appId];
807
+            } elseif (!\OC_App::isAppCompatible($version, $info)) {
808
+                $incompatibleApps[] = $info;
809
+            }
810
+        }
811
+        return $incompatibleApps;
812
+    }
813
+
814
+    /**
815
+     * @inheritdoc
816
+     * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::isShipped()
817
+     */
818
+    public function isShipped($appId) {
819
+        $this->loadShippedJson();
820
+        return in_array($appId, $this->shippedApps, true);
821
+    }
822
+
823
+    private function isAlwaysEnabled(string $appId): bool {
824
+        if ($appId === 'core') {
825
+            return true;
826
+        }
827
+
828
+        $alwaysEnabled = $this->getAlwaysEnabledApps();
829
+        return in_array($appId, $alwaysEnabled, true);
830
+    }
831
+
832
+    /**
833
+     * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::loadShippedJson()
834
+     * @throws \Exception
835
+     */
836
+    private function loadShippedJson(): void {
837
+        if ($this->shippedApps === null) {
838
+            $shippedJson = \OC::$SERVERROOT . '/core/shipped.json';
839
+            if (!file_exists($shippedJson)) {
840
+                throw new \Exception("File not found: $shippedJson");
841
+            }
842
+            $content = json_decode(file_get_contents($shippedJson), true);
843
+            $this->shippedApps = $content['shippedApps'];
844
+            $this->alwaysEnabled = $content['alwaysEnabled'];
845
+            $this->defaultEnabled = $content['defaultEnabled'];
846
+        }
847
+    }
848
+
849
+    /**
850
+     * @inheritdoc
851
+     */
852
+    public function getAlwaysEnabledApps() {
853
+        $this->loadShippedJson();
854
+        return $this->alwaysEnabled;
855
+    }
856
+
857
+    /**
858
+     * @inheritdoc
859
+     */
860
+    public function isDefaultEnabled(string $appId): bool {
861
+        return (in_array($appId, $this->getDefaultEnabledApps()));
862
+    }
863
+
864
+    /**
865
+     * @inheritdoc
866
+     */
867
+    public function getDefaultEnabledApps(): array {
868
+        $this->loadShippedJson();
869
+
870
+        return $this->defaultEnabled;
871
+    }
872
+
873
+    /**
874
+     * @inheritdoc
875
+     */
876
+    public function getDefaultAppForUser(?IUser $user = null, bool $withFallbacks = true): string {
877
+        $id = $this->getNavigationManager()->getDefaultEntryIdForUser($user, $withFallbacks);
878
+        $entry = $this->getNavigationManager()->get($id);
879
+        return (string)$entry['app'];
880
+    }
881
+
882
+    /**
883
+     * @inheritdoc
884
+     */
885
+    public function getDefaultApps(): array {
886
+        $ids = $this->getNavigationManager()->getDefaultEntryIds();
887
+
888
+        return array_values(array_unique(array_map(function (string $id) {
889
+            $entry = $this->getNavigationManager()->get($id);
890
+            return (string)$entry['app'];
891
+        }, $ids)));
892
+    }
893
+
894
+    /**
895
+     * @inheritdoc
896
+     */
897
+    public function setDefaultApps(array $defaultApps): void {
898
+        $entries = $this->getNavigationManager()->getAll();
899
+        $ids = [];
900
+        foreach ($defaultApps as $defaultApp) {
901
+            foreach ($entries as $entry) {
902
+                if ((string)$entry['app'] === $defaultApp) {
903
+                    $ids[] = (string)$entry['id'];
904
+                    break;
905
+                }
906
+            }
907
+        }
908
+        $this->getNavigationManager()->setDefaultEntryIds($ids);
909
+    }
910
+
911
+    public function isBackendRequired(string $backend): bool {
912
+        foreach ($this->appInfos as $appInfo) {
913
+            foreach ($appInfo['dependencies']['backend'] as $appBackend) {
914
+                if ($backend === $appBackend) {
915
+                    return true;
916
+                }
917
+            }
918
+        }
919
+
920
+        return false;
921
+    }
922
+
923
+    /**
924
+     * Clean the appId from forbidden characters
925
+     *
926
+     * @psalm-taint-escape callable
927
+     * @psalm-taint-escape cookie
928
+     * @psalm-taint-escape file
929
+     * @psalm-taint-escape has_quotes
930
+     * @psalm-taint-escape header
931
+     * @psalm-taint-escape html
932
+     * @psalm-taint-escape include
933
+     * @psalm-taint-escape ldap
934
+     * @psalm-taint-escape shell
935
+     * @psalm-taint-escape sql
936
+     * @psalm-taint-escape unserialize
937
+     */
938
+    public function cleanAppId(string $app): string {
939
+        /* Only lowercase alphanumeric is allowed */
940
+        return preg_replace('/(^[0-9_]|[^a-z0-9_]+|_$)/', '', $app);
941
+    }
942 942
 }
Please login to merge, or discard this patch.