Passed
Push — master ( 63e7ac...1a3bb2 )
by Joas
17:16 queued 14s
created

OC_App::registerAutoloading()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 12
c 0
b 0
f 0
nc 5
nop 3
dl 0
loc 21
rs 9.2222
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * @copyright Copyright (c) 2016, ownCloud, Inc.
7
 * @copyright Copyright (c) 2016, Lukas Reschke <[email protected]>
8
 *
9
 * @author Arthur Schiwon <[email protected]>
10
 * @author Bart Visscher <[email protected]>
11
 * @author Bernhard Posselt <[email protected]>
12
 * @author Borjan Tchakaloff <[email protected]>
13
 * @author Brice Maron <[email protected]>
14
 * @author Christopher Schäpers <[email protected]>
15
 * @author Christoph Wurst <[email protected]>
16
 * @author Daniel Rudolf <[email protected]>
17
 * @author Frank Karlitschek <[email protected]>
18
 * @author Georg Ehrke <[email protected]>
19
 * @author Jakob Sack <[email protected]>
20
 * @author Joas Schilling <[email protected]>
21
 * @author Jörn Friedrich Dreyer <[email protected]>
22
 * @author Julius Haertl <[email protected]>
23
 * @author Julius Härtl <[email protected]>
24
 * @author Kamil Domanski <[email protected]>
25
 * @author Lukas Reschke <[email protected]>
26
 * @author Markus Goetz <[email protected]>
27
 * @author Morris Jobke <[email protected]>
28
 * @author RealRancor <[email protected]>
29
 * @author Robin Appelman <[email protected]>
30
 * @author Robin McCorkell <[email protected]>
31
 * @author Roeland Jago Douma <[email protected]>
32
 * @author Sam Tuke <[email protected]>
33
 * @author Sebastian Wessalowski <[email protected]>
34
 * @author Thomas Müller <[email protected]>
35
 * @author Thomas Tanghus <[email protected]>
36
 * @author Vincent Petry <[email protected]>
37
 *
38
 * @license AGPL-3.0
39
 *
40
 * This code is free software: you can redistribute it and/or modify
41
 * it under the terms of the GNU Affero General Public License, version 3,
42
 * as published by the Free Software Foundation.
43
 *
44
 * This program is distributed in the hope that it will be useful,
45
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
46
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
47
 * GNU Affero General Public License for more details.
48
 *
49
 * You should have received a copy of the GNU Affero General Public License, version 3,
50
 * along with this program. If not, see <http://www.gnu.org/licenses/>
51
 *
52
 */
53
54
use OCP\App\Events\AppUpdateEvent;
55
use OCP\AppFramework\QueryException;
56
use OCP\App\IAppManager;
57
use OCP\App\ManagerEvent;
58
use OCP\Authentication\IAlternativeLogin;
59
use OCP\EventDispatcher\IEventDispatcher;
60
use OCP\ILogger;
61
use OC\AppFramework\Bootstrap\Coordinator;
62
use OC\App\DependencyAnalyzer;
63
use OC\App\Platform;
64
use OC\DB\MigrationService;
65
use OC\Installer;
66
use OC\Repair;
67
use OC\Repair\Events\RepairErrorEvent;
68
use Psr\Log\LoggerInterface;
69
70
/**
71
 * This class manages the apps. It allows them to register and integrate in the
72
 * ownCloud ecosystem. Furthermore, this class is responsible for installing,
73
 * upgrading and removing apps.
74
 */
75
class OC_App {
76
	private static $adminForms = [];
77
	private static $personalForms = [];
78
	private static $altLogin = [];
79
	private static $alreadyRegistered = [];
80
	public const supportedApp = 300;
81
	public const officialApp = 200;
82
83
	/**
84
	 * clean the appId
85
	 *
86
	 * @psalm-taint-escape file
87
	 * @psalm-taint-escape include
88
	 * @psalm-taint-escape html
89
	 * @psalm-taint-escape has_quotes
90
	 *
91
	 * @param string $app AppId that needs to be cleaned
92
	 * @return string
93
	 */
94
	public static function cleanAppId(string $app): string {
95
		return str_replace(['<', '>', '"', "'", '\0', '/', '\\', '..'], '', $app);
96
	}
97
98
	/**
99
	 * Check if an app is loaded
100
	 *
101
	 * @param string $app
102
	 * @return bool
103
	 * @deprecated 27.0.0 use IAppManager::isAppLoaded
104
	 */
105
	public static function isAppLoaded(string $app): bool {
106
		return \OC::$server->get(IAppManager::class)->isAppLoaded($app);
107
	}
108
109
	/**
110
	 * loads all apps
111
	 *
112
	 * @param string[] $types
113
	 * @return bool
114
	 *
115
	 * This function walks through the ownCloud directory and loads all apps
116
	 * it can find. A directory contains an app if the file /appinfo/info.xml
117
	 * exists.
118
	 *
119
	 * if $types is set to non-empty array, only apps of those types will be loaded
120
	 */
121
	public static function loadApps(array $types = []): bool {
122
		if (!\OC::$server->getSystemConfig()->getValue('installed', false)) {
123
			// This should be done before calling this method so that appmanager can be used
124
			return false;
125
		}
126
		return \OC::$server->get(IAppManager::class)->loadApps($types);
127
	}
128
129
	/**
130
	 * load a single app
131
	 *
132
	 * @param string $app
133
	 * @throws Exception
134
	 * @deprecated 27.0.0 use IAppManager::loadApp
135
	 */
136
	public static function loadApp(string $app): void {
137
		\OC::$server->get(IAppManager::class)->loadApp($app);
138
	}
139
140
	/**
141
	 * @internal
142
	 * @param string $app
143
	 * @param string $path
144
	 * @param bool $force
145
	 */
146
	public static function registerAutoloading(string $app, string $path, bool $force = false) {
147
		$key = $app . '-' . $path;
148
		if (!$force && isset(self::$alreadyRegistered[$key])) {
149
			return;
150
		}
151
152
		self::$alreadyRegistered[$key] = true;
153
154
		// Register on PSR-4 composer autoloader
155
		$appNamespace = \OC\AppFramework\App::buildAppNamespace($app);
156
		\OC::$server->registerNamespace($app, $appNamespace);
157
158
		if (file_exists($path . '/composer/autoload.php')) {
159
			require_once $path . '/composer/autoload.php';
160
		} else {
161
			\OC::$composerAutoloader->addPsr4($appNamespace . '\\', $path . '/lib/', true);
162
		}
163
164
		// Register Test namespace only when testing
165
		if (defined('PHPUNIT_RUN') || defined('CLI_TEST_RUN')) {
166
			\OC::$composerAutoloader->addPsr4($appNamespace . '\\Tests\\', $path . '/tests/', true);
167
		}
168
	}
169
170
	/**
171
	 * check if an app is of a specific type
172
	 *
173
	 * @param string $app
174
	 * @param array $types
175
	 * @return bool
176
	 * @deprecated 27.0.0 use IAppManager::isType
177
	 */
178
	public static function isType(string $app, array $types): bool {
179
		return \OC::$server->get(IAppManager::class)->isType($app, $types);
180
	}
181
182
	/**
183
	 * read app types from info.xml and cache them in the database
184
	 */
185
	public static function setAppTypes(string $app) {
186
		$appManager = \OC::$server->getAppManager();
187
		$appData = $appManager->getAppInfo($app);
188
		if (!is_array($appData)) {
189
			return;
190
		}
191
192
		if (isset($appData['types'])) {
193
			$appTypes = implode(',', $appData['types']);
194
		} else {
195
			$appTypes = '';
196
			$appData['types'] = [];
197
		}
198
199
		$config = \OC::$server->getConfig();
200
		$config->setAppValue($app, 'types', $appTypes);
201
202
		if ($appManager->hasProtectedAppType($appData['types'])) {
203
			$enabled = $config->getAppValue($app, 'enabled', 'yes');
204
			if ($enabled !== 'yes' && $enabled !== 'no') {
205
				$config->setAppValue($app, 'enabled', 'yes');
206
			}
207
		}
208
	}
209
210
	/**
211
	 * Returns apps enabled for the current user.
212
	 *
213
	 * @param bool $forceRefresh whether to refresh the cache
214
	 * @param bool $all whether to return apps for all users, not only the
215
	 * currently logged in one
216
	 * @return string[]
217
	 */
218
	public static function getEnabledApps(bool $forceRefresh = false, bool $all = false): array {
0 ignored issues
show
Unused Code introduced by
The parameter $forceRefresh is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

218
	public static function getEnabledApps(/** @scrutinizer ignore-unused */ bool $forceRefresh = false, bool $all = false): array {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
219
		if (!\OC::$server->getSystemConfig()->getValue('installed', false)) {
220
			return [];
221
		}
222
		// in incognito mode or when logged out, $user will be false,
223
		// which is also the case during an upgrade
224
		$appManager = \OC::$server->getAppManager();
225
		if ($all) {
226
			$user = null;
227
		} else {
228
			$user = \OC::$server->getUserSession()->getUser();
229
		}
230
231
		if (is_null($user)) {
232
			$apps = $appManager->getInstalledApps();
233
		} else {
234
			$apps = $appManager->getEnabledAppsForUser($user);
235
		}
236
		$apps = array_filter($apps, function ($app) {
237
			return $app !== 'files';//we add this manually
238
		});
239
		sort($apps);
240
		array_unshift($apps, 'files');
241
		return $apps;
242
	}
243
244
	/**
245
	 * enables an app
246
	 *
247
	 * @param string $appId
248
	 * @param array $groups (optional) when set, only these groups will have access to the app
249
	 * @throws \Exception
250
	 * @return void
251
	 *
252
	 * This function set an app as enabled in appconfig.
253
	 */
254
	public function enable(string $appId,
255
						   array $groups = []) {
256
		// Check if app is already downloaded
257
		/** @var Installer $installer */
258
		$installer = \OC::$server->query(Installer::class);
259
		$isDownloaded = $installer->isDownloaded($appId);
260
261
		if (!$isDownloaded) {
262
			$installer->downloadApp($appId);
263
		}
264
265
		$installer->installApp($appId);
266
267
		$appManager = \OC::$server->getAppManager();
268
		if ($groups !== []) {
269
			$groupManager = \OC::$server->getGroupManager();
270
			$groupsList = [];
271
			foreach ($groups as $group) {
272
				$groupItem = $groupManager->get($group);
273
				if ($groupItem instanceof \OCP\IGroup) {
274
					$groupsList[] = $groupManager->get($group);
275
				}
276
			}
277
			$appManager->enableAppForGroups($appId, $groupsList);
278
		} else {
279
			$appManager->enableApp($appId);
280
		}
281
	}
282
283
	/**
284
	 * Get the path where to install apps
285
	 *
286
	 * @return string|false
287
	 */
288
	public static function getInstallPath() {
289
		foreach (OC::$APPSROOTS as $dir) {
290
			if (isset($dir['writable']) && $dir['writable'] === true) {
291
				return $dir['path'];
292
			}
293
		}
294
295
		\OCP\Util::writeLog('core', 'No application directories are marked as writable.', ILogger::ERROR);
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\ILogger::ERROR has been deprecated: 20.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

295
		\OCP\Util::writeLog('core', 'No application directories are marked as writable.', /** @scrutinizer ignore-deprecated */ ILogger::ERROR);

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
296
		return null;
297
	}
298
299
300
	/**
301
	 * search for an app in all app-directories
302
	 *
303
	 * @param string $appId
304
	 * @param bool $ignoreCache ignore cache and rebuild it
305
	 * @return false|string
306
	 */
307
	public static function findAppInDirectories(string $appId, bool $ignoreCache = false) {
308
		$sanitizedAppId = self::cleanAppId($appId);
309
		if ($sanitizedAppId !== $appId) {
310
			return false;
311
		}
312
		static $app_dir = [];
313
314
		if (isset($app_dir[$appId]) && !$ignoreCache) {
315
			return $app_dir[$appId];
316
		}
317
318
		$possibleApps = [];
319
		foreach (OC::$APPSROOTS as $dir) {
320
			if (file_exists($dir['path'] . '/' . $appId)) {
321
				$possibleApps[] = $dir;
322
			}
323
		}
324
325
		if (empty($possibleApps)) {
326
			return false;
327
		} elseif (count($possibleApps) === 1) {
328
			$dir = array_shift($possibleApps);
329
			$app_dir[$appId] = $dir;
330
			return $dir;
331
		} else {
332
			$versionToLoad = [];
333
			foreach ($possibleApps as $possibleApp) {
334
				$version = self::getAppVersionByPath($possibleApp['path'] . '/' . $appId);
335
				if (empty($versionToLoad) || version_compare($version, $versionToLoad['version'], '>')) {
336
					$versionToLoad = [
337
						'dir' => $possibleApp,
338
						'version' => $version,
339
					];
340
				}
341
			}
342
			$app_dir[$appId] = $versionToLoad['dir'];
343
			return $versionToLoad['dir'];
344
			//TODO - write test
345
		}
346
	}
347
348
	/**
349
	 * Get the directory for the given app.
350
	 * If the app is defined in multiple directories, the first one is taken. (false if not found)
351
	 *
352
	 * @psalm-taint-specialize
353
	 *
354
	 * @param string $appId
355
	 * @param bool $refreshAppPath should be set to true only during install/upgrade
356
	 * @return string|false
357
	 * @deprecated 11.0.0 use \OC::$server->getAppManager()->getAppPath()
358
	 */
359
	public static function getAppPath(string $appId, bool $refreshAppPath = false) {
360
		if ($appId === null || trim($appId) === '') {
361
			return false;
362
		}
363
364
		if (($dir = self::findAppInDirectories($appId, $refreshAppPath)) != false) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $dir = self::findAppInDi...appId, $refreshAppPath) of type false|string against false; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
365
			return $dir['path'] . '/' . $appId;
366
		}
367
		return false;
368
	}
369
370
	/**
371
	 * Get the path for the given app on the access
372
	 * If the app is defined in multiple directories, the first one is taken. (false if not found)
373
	 *
374
	 * @param string $appId
375
	 * @return string|false
376
	 * @deprecated 18.0.0 use \OC::$server->getAppManager()->getAppWebPath()
377
	 */
378
	public static function getAppWebPath(string $appId) {
379
		if (($dir = self::findAppInDirectories($appId)) != false) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $dir = self::findAppInDirectories($appId) of type false|string against false; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
380
			return OC::$WEBROOT . $dir['url'] . '/' . $appId;
381
		}
382
		return false;
383
	}
384
385
	/**
386
	 * get app's version based on it's path
387
	 *
388
	 * @param string $path
389
	 * @return string
390
	 */
391
	public static function getAppVersionByPath(string $path): string {
392
		$infoFile = $path . '/appinfo/info.xml';
393
		$appData = \OC::$server->getAppManager()->getAppInfo($infoFile, true);
394
		return isset($appData['version']) ? $appData['version'] : '';
395
	}
396
397
	/**
398
	 * get the id of loaded app
399
	 *
400
	 * @return string
401
	 */
402
	public static function getCurrentApp(): string {
403
		if (\OC::$CLI) {
404
			return '';
405
		}
406
407
		$request = \OC::$server->getRequest();
408
		$script = substr($request->getScriptName(), strlen(OC::$WEBROOT) + 1);
409
		$topFolder = substr($script, 0, strpos($script, '/') ?: 0);
410
		if (empty($topFolder)) {
411
			try {
412
				$path_info = $request->getPathInfo();
413
			} catch (Exception $e) {
414
				// Can happen from unit tests because the script name is `./vendor/bin/phpunit` or something a like then.
415
				\OC::$server->get(LoggerInterface::class)->error('Failed to detect current app from script path', ['exception' => $e]);
416
				return '';
417
			}
418
			if ($path_info) {
419
				$topFolder = substr($path_info, 1, strpos($path_info, '/', 1) - 1);
420
			}
421
		}
422
		if ($topFolder == 'apps') {
423
			$length = strlen($topFolder);
424
			return substr($script, $length + 1, strpos($script, '/', $length + 1) - $length - 1) ?: '';
425
		} else {
426
			return $topFolder;
427
		}
428
	}
429
430
	/**
431
	 * @param string $type
432
	 * @return array
433
	 */
434
	public static function getForms(string $type): array {
435
		$forms = [];
436
		switch ($type) {
437
			case 'admin':
438
				$source = self::$adminForms;
439
				break;
440
			case 'personal':
441
				$source = self::$personalForms;
442
				break;
443
			default:
444
				return [];
445
		}
446
		foreach ($source as $form) {
447
			$forms[] = include $form;
448
		}
449
		return $forms;
450
	}
451
452
	/**
453
	 * @param array $entry
454
	 * @deprecated 20.0.0 Please register your alternative login option using the registerAlternativeLogin() on the RegistrationContext in your Application class implementing the OCP\Authentication\IAlternativeLogin interface
455
	 */
456
	public static function registerLogIn(array $entry) {
457
		\OC::$server->getLogger()->debug('OC_App::registerLogIn() is deprecated, please register your alternative login option using the registerAlternativeLogin() on the RegistrationContext in your Application class implementing the OCP\Authentication\IAlternativeLogin interface');
458
		self::$altLogin[] = $entry;
459
	}
460
461
	/**
462
	 * @return array
463
	 */
464
	public static function getAlternativeLogIns(): array {
465
		/** @var Coordinator $bootstrapCoordinator */
466
		$bootstrapCoordinator = \OC::$server->query(Coordinator::class);
467
468
		foreach ($bootstrapCoordinator->getRegistrationContext()->getAlternativeLogins() as $registration) {
469
			if (!in_array(IAlternativeLogin::class, class_implements($registration->getService()), true)) {
470
				\OC::$server->getLogger()->error('Alternative login option {option} does not implement {interface} and is therefore ignored.', [
471
					'option' => $registration->getService(),
472
					'interface' => IAlternativeLogin::class,
473
					'app' => $registration->getAppId(),
474
				]);
475
				continue;
476
			}
477
478
			try {
479
				/** @var IAlternativeLogin $provider */
480
				$provider = \OC::$server->query($registration->getService());
481
			} catch (QueryException $e) {
482
				\OC::$server->getLogger()->logException($e, [
483
					'message' => 'Alternative login option {option} can not be initialised.',
484
					'option' => $registration->getService(),
485
					'app' => $registration->getAppId(),
486
				]);
487
			}
488
489
			try {
490
				$provider->load();
491
492
				self::$altLogin[] = [
493
					'name' => $provider->getLabel(),
494
					'href' => $provider->getLink(),
495
					'class' => $provider->getClass(),
496
				];
497
			} catch (Throwable $e) {
498
				\OC::$server->getLogger()->logException($e, [
499
					'message' => 'Alternative login option {option} had an error while loading.',
500
					'option' => $registration->getService(),
501
					'app' => $registration->getAppId(),
502
				]);
503
			}
504
		}
505
506
		return self::$altLogin;
507
	}
508
509
	/**
510
	 * get a list of all apps in the apps folder
511
	 *
512
	 * @return string[] an array of app names (string IDs)
513
	 * @todo: change the name of this method to getInstalledApps, which is more accurate
514
	 */
515
	public static function getAllApps(): array {
516
		$apps = [];
517
518
		foreach (OC::$APPSROOTS as $apps_dir) {
519
			if (!is_readable($apps_dir['path'])) {
520
				\OCP\Util::writeLog('core', 'unable to read app folder : ' . $apps_dir['path'], ILogger::WARN);
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\ILogger::WARN has been deprecated: 20.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

520
				\OCP\Util::writeLog('core', 'unable to read app folder : ' . $apps_dir['path'], /** @scrutinizer ignore-deprecated */ ILogger::WARN);

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
521
				continue;
522
			}
523
			$dh = opendir($apps_dir['path']);
524
525
			if (is_resource($dh)) {
526
				while (($file = readdir($dh)) !== false) {
527
					if ($file[0] != '.' and is_dir($apps_dir['path'] . '/' . $file) and is_file($apps_dir['path'] . '/' . $file . '/appinfo/info.xml')) {
528
						$apps[] = $file;
529
					}
530
				}
531
			}
532
		}
533
534
		$apps = array_unique($apps);
535
536
		return $apps;
537
	}
538
539
	/**
540
	 * List all supported apps
541
	 *
542
	 * @return array
543
	 */
544
	public function getSupportedApps(): array {
545
		/** @var \OCP\Support\Subscription\IRegistry $subscriptionRegistry */
546
		$subscriptionRegistry = \OC::$server->query(\OCP\Support\Subscription\IRegistry::class);
547
		$supportedApps = $subscriptionRegistry->delegateGetSupportedApps();
548
		return $supportedApps;
549
	}
550
551
	/**
552
	 * List all apps, this is used in apps.php
553
	 *
554
	 * @return array
555
	 */
556
	public function listAllApps(): array {
557
		$installedApps = OC_App::getAllApps();
558
559
		$appManager = \OC::$server->getAppManager();
560
		//we don't want to show configuration for these
561
		$blacklist = $appManager->getAlwaysEnabledApps();
562
		$appList = [];
563
		$langCode = \OC::$server->getL10N('core')->getLanguageCode();
564
		$urlGenerator = \OC::$server->getURLGenerator();
565
		$supportedApps = $this->getSupportedApps();
566
567
		foreach ($installedApps as $app) {
568
			if (array_search($app, $blacklist) === false) {
569
				$info = $appManager->getAppInfo($app, false, $langCode);
570
				if (!is_array($info)) {
571
					\OCP\Util::writeLog('core', 'Could not read app info file for app "' . $app . '"', ILogger::ERROR);
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\ILogger::ERROR has been deprecated: 20.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

571
					\OCP\Util::writeLog('core', 'Could not read app info file for app "' . $app . '"', /** @scrutinizer ignore-deprecated */ ILogger::ERROR);

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
572
					continue;
573
				}
574
575
				if (!isset($info['name'])) {
576
					\OCP\Util::writeLog('core', 'App id "' . $app . '" has no name in appinfo', ILogger::ERROR);
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\ILogger::ERROR has been deprecated: 20.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

576
					\OCP\Util::writeLog('core', 'App id "' . $app . '" has no name in appinfo', /** @scrutinizer ignore-deprecated */ ILogger::ERROR);

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
577
					continue;
578
				}
579
580
				$enabled = \OC::$server->getConfig()->getAppValue($app, 'enabled', 'no');
581
				$info['groups'] = null;
582
				if ($enabled === 'yes') {
583
					$active = true;
584
				} elseif ($enabled === 'no') {
585
					$active = false;
586
				} else {
587
					$active = true;
588
					$info['groups'] = $enabled;
589
				}
590
591
				$info['active'] = $active;
592
593
				if ($appManager->isShipped($app)) {
594
					$info['internal'] = true;
595
					$info['level'] = self::officialApp;
596
					$info['removable'] = false;
597
				} else {
598
					$info['internal'] = false;
599
					$info['removable'] = true;
600
				}
601
602
				if (in_array($app, $supportedApps)) {
603
					$info['level'] = self::supportedApp;
604
				}
605
606
				$appPath = self::getAppPath($app);
607
				if ($appPath !== false) {
608
					$appIcon = $appPath . '/img/' . $app . '.svg';
609
					if (file_exists($appIcon)) {
610
						$info['preview'] = $urlGenerator->imagePath($app, $app . '.svg');
611
						$info['previewAsIcon'] = true;
612
					} else {
613
						$appIcon = $appPath . '/img/app.svg';
614
						if (file_exists($appIcon)) {
615
							$info['preview'] = $urlGenerator->imagePath($app, 'app.svg');
616
							$info['previewAsIcon'] = true;
617
						}
618
					}
619
				}
620
				// fix documentation
621
				if (isset($info['documentation']) && is_array($info['documentation'])) {
622
					foreach ($info['documentation'] as $key => $url) {
623
						// If it is not an absolute URL we assume it is a key
624
						// i.e. admin-ldap will get converted to go.php?to=admin-ldap
625
						if (stripos($url, 'https://') !== 0 && stripos($url, 'http://') !== 0) {
626
							$url = $urlGenerator->linkToDocs($url);
627
						}
628
629
						$info['documentation'][$key] = $url;
630
					}
631
				}
632
633
				$info['version'] = $appManager->getAppVersion($app);
634
				$appList[] = $info;
635
			}
636
		}
637
638
		return $appList;
639
	}
640
641
	public static function shouldUpgrade(string $app): bool {
642
		$versions = self::getAppVersions();
643
		$currentVersion = \OCP\Server::get(\OCP\App\IAppManager::class)->getAppVersion($app);
644
		if ($currentVersion && isset($versions[$app])) {
645
			$installedVersion = $versions[$app];
646
			if (!version_compare($currentVersion, $installedVersion, '=')) {
647
				return true;
648
			}
649
		}
650
		return false;
651
	}
652
653
	/**
654
	 * Adjust the number of version parts of $version1 to match
655
	 * the number of version parts of $version2.
656
	 *
657
	 * @param string $version1 version to adjust
658
	 * @param string $version2 version to take the number of parts from
659
	 * @return string shortened $version1
660
	 */
661
	private static function adjustVersionParts(string $version1, string $version2): string {
662
		$version1 = explode('.', $version1);
663
		$version2 = explode('.', $version2);
664
		// reduce $version1 to match the number of parts in $version2
665
		while (count($version1) > count($version2)) {
666
			array_pop($version1);
667
		}
668
		// if $version1 does not have enough parts, add some
669
		while (count($version1) < count($version2)) {
670
			$version1[] = '0';
671
		}
672
		return implode('.', $version1);
673
	}
674
675
	/**
676
	 * Check whether the current ownCloud version matches the given
677
	 * application's version requirements.
678
	 *
679
	 * The comparison is made based on the number of parts that the
680
	 * app info version has. For example for ownCloud 6.0.3 if the
681
	 * app info version is expecting version 6.0, the comparison is
682
	 * made on the first two parts of the ownCloud version.
683
	 * This means that it's possible to specify "requiremin" => 6
684
	 * and "requiremax" => 6 and it will still match ownCloud 6.0.3.
685
	 *
686
	 * @param string $ocVersion ownCloud version to check against
687
	 * @param array $appInfo app info (from xml)
688
	 *
689
	 * @return boolean true if compatible, otherwise false
690
	 */
691
	public static function isAppCompatible(string $ocVersion, array $appInfo, bool $ignoreMax = false): bool {
692
		$requireMin = '';
693
		$requireMax = '';
694
		if (isset($appInfo['dependencies']['nextcloud']['@attributes']['min-version'])) {
695
			$requireMin = $appInfo['dependencies']['nextcloud']['@attributes']['min-version'];
696
		} elseif (isset($appInfo['dependencies']['owncloud']['@attributes']['min-version'])) {
697
			$requireMin = $appInfo['dependencies']['owncloud']['@attributes']['min-version'];
698
		} elseif (isset($appInfo['requiremin'])) {
699
			$requireMin = $appInfo['requiremin'];
700
		} elseif (isset($appInfo['require'])) {
701
			$requireMin = $appInfo['require'];
702
		}
703
704
		if (isset($appInfo['dependencies']['nextcloud']['@attributes']['max-version'])) {
705
			$requireMax = $appInfo['dependencies']['nextcloud']['@attributes']['max-version'];
706
		} elseif (isset($appInfo['dependencies']['owncloud']['@attributes']['max-version'])) {
707
			$requireMax = $appInfo['dependencies']['owncloud']['@attributes']['max-version'];
708
		} elseif (isset($appInfo['requiremax'])) {
709
			$requireMax = $appInfo['requiremax'];
710
		}
711
712
		if (!empty($requireMin)
713
			&& version_compare(self::adjustVersionParts($ocVersion, $requireMin), $requireMin, '<')
714
		) {
715
			return false;
716
		}
717
718
		if (!$ignoreMax && !empty($requireMax)
719
			&& version_compare(self::adjustVersionParts($ocVersion, $requireMax), $requireMax, '>')
720
		) {
721
			return false;
722
		}
723
724
		return true;
725
	}
726
727
	/**
728
	 * get the installed version of all apps
729
	 */
730
	public static function getAppVersions() {
731
		static $versions;
732
733
		if (!$versions) {
734
			$appConfig = \OC::$server->getAppConfig();
735
			$versions = $appConfig->getValues(false, 'installed_version');
736
		}
737
		return $versions;
738
	}
739
740
	/**
741
	 * update the database for the app and call the update script
742
	 *
743
	 * @param string $appId
744
	 * @return bool
745
	 */
746
	public static function updateApp(string $appId): bool {
747
		// for apps distributed with core, we refresh app path in case the downloaded version
748
		// have been installed in custom apps and not in the default path
749
		$appPath = self::getAppPath($appId, true);
750
		if ($appPath === false) {
751
			return false;
752
		}
753
754
		if (is_file($appPath . '/appinfo/database.xml')) {
755
			\OC::$server->getLogger()->error('The appinfo/database.xml file is not longer supported. Used in ' . $appId);
756
			return false;
757
		}
758
759
		\OC::$server->getAppManager()->clearAppsCache();
760
		$l = \OC::$server->getL10N('core');
761
		$appData = \OCP\Server::get(\OCP\App\IAppManager::class)->getAppInfo($appId, false, $l->getLanguageCode());
762
763
		$ignoreMaxApps = \OC::$server->getConfig()->getSystemValue('app_install_overwrite', []);
764
		$ignoreMax = in_array($appId, $ignoreMaxApps, true);
765
		\OC_App::checkAppDependencies(
766
			\OC::$server->getConfig(),
767
			$l,
768
			$appData,
769
			$ignoreMax
770
		);
771
772
		self::registerAutoloading($appId, $appPath, true);
773
		self::executeRepairSteps($appId, $appData['repair-steps']['pre-migration']);
774
775
		$ms = new MigrationService($appId, \OC::$server->get(\OC\DB\Connection::class));
776
		$ms->migrate();
777
778
		self::executeRepairSteps($appId, $appData['repair-steps']['post-migration']);
779
		self::setupLiveMigrations($appId, $appData['repair-steps']['live-migration']);
780
		// update appversion in app manager
781
		\OC::$server->getAppManager()->clearAppsCache();
782
		\OC::$server->getAppManager()->getAppVersion($appId, false);
783
784
		self::setupBackgroundJobs($appData['background-jobs']);
785
786
		//set remote/public handlers
787
		if (array_key_exists('ocsid', $appData)) {
788
			\OC::$server->getConfig()->setAppValue($appId, 'ocsid', $appData['ocsid']);
789
		} elseif (\OC::$server->getConfig()->getAppValue($appId, 'ocsid', null) !== null) {
0 ignored issues
show
introduced by
The condition OC::server->getConfig()-...'ocsid', null) !== null is always true.
Loading history...
790
			\OC::$server->getConfig()->deleteAppValue($appId, 'ocsid');
791
		}
792
		foreach ($appData['remote'] as $name => $path) {
793
			\OC::$server->getConfig()->setAppValue('core', 'remote_' . $name, $appId . '/' . $path);
794
		}
795
		foreach ($appData['public'] as $name => $path) {
796
			\OC::$server->getConfig()->setAppValue('core', 'public_' . $name, $appId . '/' . $path);
797
		}
798
799
		self::setAppTypes($appId);
800
801
		$version = \OCP\Server::get(\OCP\App\IAppManager::class)->getAppVersion($appId);
802
		\OC::$server->getConfig()->setAppValue($appId, 'installed_version', $version);
803
804
		\OC::$server->get(IEventDispatcher::class)->dispatchTyped(new AppUpdateEvent($appId));
805
		\OC::$server->getEventDispatcher()->dispatch(ManagerEvent::EVENT_APP_UPDATE, new ManagerEvent(
0 ignored issues
show
Bug introduced by
OCP\App\ManagerEvent::EVENT_APP_UPDATE of type string is incompatible with the type object expected by parameter $event of Symfony\Contracts\EventD...erInterface::dispatch(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

805
		\OC::$server->getEventDispatcher()->dispatch(/** @scrutinizer ignore-type */ ManagerEvent::EVENT_APP_UPDATE, new ManagerEvent(
Loading history...
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with new OCP\App\ManagerEvent...ENT_APP_UPDATE, $appId). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

805
		\OC::$server->getEventDispatcher()->/** @scrutinizer ignore-call */ dispatch(ManagerEvent::EVENT_APP_UPDATE, new ManagerEvent(

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
Deprecated Code introduced by
The constant OCP\App\ManagerEvent::EVENT_APP_UPDATE has been deprecated: 22.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

805
		\OC::$server->getEventDispatcher()->dispatch(/** @scrutinizer ignore-deprecated */ ManagerEvent::EVENT_APP_UPDATE, new ManagerEvent(

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
806
			ManagerEvent::EVENT_APP_UPDATE, $appId
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\App\ManagerEvent::EVENT_APP_UPDATE has been deprecated: 22.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

806
			/** @scrutinizer ignore-deprecated */ ManagerEvent::EVENT_APP_UPDATE, $appId

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
807
		));
808
809
		return true;
810
	}
811
812
	/**
813
	 * @param string $appId
814
	 * @param string[] $steps
815
	 * @throws \OC\NeedsUpdateException
816
	 */
817
	public static function executeRepairSteps(string $appId, array $steps) {
818
		if (empty($steps)) {
819
			return;
820
		}
821
		// load the app
822
		self::loadApp($appId);
823
824
		$dispatcher = \OC::$server->get(IEventDispatcher::class);
825
826
		// load the steps
827
		$r = new Repair([], $dispatcher, \OC::$server->get(LoggerInterface::class));
828
		foreach ($steps as $step) {
829
			try {
830
				$r->addStep($step);
831
			} catch (Exception $ex) {
832
				$dispatcher->dispatchTyped(new RepairErrorEvent($ex->getMessage()));
833
				\OC::$server->getLogger()->logException($ex);
834
			}
835
		}
836
		// run the steps
837
		$r->run();
838
	}
839
840
	public static function setupBackgroundJobs(array $jobs) {
841
		$queue = \OC::$server->getJobList();
842
		foreach ($jobs as $job) {
843
			$queue->add($job);
844
		}
845
	}
846
847
	/**
848
	 * @param string $appId
849
	 * @param string[] $steps
850
	 */
851
	private static function setupLiveMigrations(string $appId, array $steps) {
852
		$queue = \OC::$server->getJobList();
853
		foreach ($steps as $step) {
854
			$queue->add('OC\Migration\BackgroundRepair', [
855
				'app' => $appId,
856
				'step' => $step]);
857
		}
858
	}
859
860
	/**
861
	 * @param string $appId
862
	 * @return \OC\Files\View|false
863
	 */
864
	public static function getStorage(string $appId) {
865
		if (\OC::$server->getAppManager()->isEnabledForUser($appId)) { //sanity check
866
			if (\OC::$server->getUserSession()->isLoggedIn()) {
867
				$view = new \OC\Files\View('/' . OC_User::getUser());
0 ignored issues
show
Bug introduced by
Are you sure OC_User::getUser() of type false|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

867
				$view = new \OC\Files\View('/' . /** @scrutinizer ignore-type */ OC_User::getUser());
Loading history...
868
				if (!$view->file_exists($appId)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $view->file_exists($appId) of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
869
					$view->mkdir($appId);
870
				}
871
				return new \OC\Files\View('/' . OC_User::getUser() . '/' . $appId);
872
			} else {
873
				\OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ', user not logged in', ILogger::ERROR);
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\ILogger::ERROR has been deprecated: 20.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

873
				\OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ', user not logged in', /** @scrutinizer ignore-deprecated */ ILogger::ERROR);

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
874
				return false;
875
			}
876
		} else {
877
			\OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ' not enabled', ILogger::ERROR);
0 ignored issues
show
Deprecated Code introduced by
The constant OCP\ILogger::ERROR has been deprecated: 20.0.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

877
			\OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ' not enabled', /** @scrutinizer ignore-deprecated */ ILogger::ERROR);

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
878
			return false;
879
		}
880
	}
881
882
	protected static function findBestL10NOption(array $options, string $lang): string {
883
		// only a single option
884
		if (isset($options['@value'])) {
885
			return $options['@value'];
886
		}
887
888
		$fallback = $similarLangFallback = $englishFallback = false;
889
890
		$lang = strtolower($lang);
891
		$similarLang = $lang;
892
		if (strpos($similarLang, '_')) {
893
			// For "de_DE" we want to find "de" and the other way around
894
			$similarLang = substr($lang, 0, strpos($lang, '_'));
895
		}
896
897
		foreach ($options as $option) {
898
			if (is_array($option)) {
899
				if ($fallback === false) {
900
					$fallback = $option['@value'];
901
				}
902
903
				if (!isset($option['@attributes']['lang'])) {
904
					continue;
905
				}
906
907
				$attributeLang = strtolower($option['@attributes']['lang']);
908
				if ($attributeLang === $lang) {
909
					return $option['@value'];
910
				}
911
912
				if ($attributeLang === $similarLang) {
913
					$similarLangFallback = $option['@value'];
914
				} elseif (strpos($attributeLang, $similarLang . '_') === 0) {
915
					if ($similarLangFallback === false) {
916
						$similarLangFallback = $option['@value'];
917
					}
918
				}
919
			} else {
920
				$englishFallback = $option;
921
			}
922
		}
923
924
		if ($similarLangFallback !== false) {
925
			return $similarLangFallback;
926
		} elseif ($englishFallback !== false) {
927
			return $englishFallback;
928
		}
929
		return (string) $fallback;
930
	}
931
932
	/**
933
	 * parses the app data array and enhanced the 'description' value
934
	 *
935
	 * @param array $data the app data
936
	 * @param string $lang
937
	 * @return array improved app data
938
	 */
939
	public static function parseAppInfo(array $data, $lang = null): array {
940
		if ($lang && isset($data['name']) && is_array($data['name'])) {
941
			$data['name'] = self::findBestL10NOption($data['name'], $lang);
942
		}
943
		if ($lang && isset($data['summary']) && is_array($data['summary'])) {
944
			$data['summary'] = self::findBestL10NOption($data['summary'], $lang);
945
		}
946
		if ($lang && isset($data['description']) && is_array($data['description'])) {
947
			$data['description'] = trim(self::findBestL10NOption($data['description'], $lang));
948
		} elseif (isset($data['description']) && is_string($data['description'])) {
949
			$data['description'] = trim($data['description']);
950
		} else {
951
			$data['description'] = '';
952
		}
953
954
		return $data;
955
	}
956
957
	/**
958
	 * @param \OCP\IConfig $config
959
	 * @param \OCP\IL10N $l
960
	 * @param array $info
961
	 * @throws \Exception
962
	 */
963
	public static function checkAppDependencies(\OCP\IConfig $config, \OCP\IL10N $l, array $info, bool $ignoreMax) {
964
		$dependencyAnalyzer = new DependencyAnalyzer(new Platform($config), $l);
965
		$missing = $dependencyAnalyzer->analyze($info, $ignoreMax);
966
		if (!empty($missing)) {
967
			$missingMsg = implode(PHP_EOL, $missing);
968
			throw new \Exception(
969
				$l->t('App "%1$s" cannot be installed because the following dependencies are not fulfilled: %2$s',
970
					[$info['name'], $missingMsg]
971
				)
972
			);
973
		}
974
	}
975
}
976