Passed
Push — master ( 496a8d...f0238f )
by Joas
17:19 queued 13s
created

OC_App::isEnabled()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 2
rs 10
c 0
b 0
f 0
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 the last version of the app from appinfo/info.xml
387
	 *
388
	 * @param string $appId
389
	 * @param bool $useCache
390
	 * @return string
391
	 * @deprecated 14.0.0 use \OC::$server->getAppManager()->getAppVersion()
392
	 */
393
	public static function getAppVersion(string $appId, bool $useCache = true): string {
394
		return \OC::$server->getAppManager()->getAppVersion($appId, $useCache);
395
	}
396
397
	/**
398
	 * get app's version based on it's path
399
	 *
400
	 * @param string $path
401
	 * @return string
402
	 */
403
	public static function getAppVersionByPath(string $path): string {
404
		$infoFile = $path . '/appinfo/info.xml';
405
		$appData = \OC::$server->getAppManager()->getAppInfo($infoFile, true);
406
		return isset($appData['version']) ? $appData['version'] : '';
407
	}
408
409
410
	/**
411
	 * Read all app metadata from the info.xml file
412
	 *
413
	 * @param string $appId id of the app or the path of the info.xml file
414
	 * @param bool $path
415
	 * @param string $lang
416
	 * @return array|null
417
	 * @note all data is read from info.xml, not just pre-defined fields
418
	 * @deprecated 14.0.0 use \OC::$server->getAppManager()->getAppInfo()
419
	 */
420
	public static function getAppInfo(string $appId, bool $path = false, string $lang = null) {
421
		return \OC::$server->getAppManager()->getAppInfo($appId, $path, $lang);
422
	}
423
424
	/**
425
	 * Returns the navigation
426
	 *
427
	 * @return array
428
	 * @deprecated 14.0.0 use \OC::$server->getNavigationManager()->getAll()
429
	 *
430
	 * This function returns an array containing all entries added. The
431
	 * entries are sorted by the key 'order' ascending. Additional to the keys
432
	 * given for each app the following keys exist:
433
	 *   - active: boolean, signals if the user is on this navigation entry
434
	 */
435
	public static function getNavigation(): array {
436
		return OC::$server->getNavigationManager()->getAll();
437
	}
438
439
	/**
440
	 * Returns the Settings Navigation
441
	 *
442
	 * @return string[]
443
	 * @deprecated 14.0.0 use \OC::$server->getNavigationManager()->getAll('settings')
444
	 *
445
	 * This function returns an array containing all settings pages added. The
446
	 * entries are sorted by the key 'order' ascending.
447
	 */
448
	public static function getSettingsNavigation(): array {
449
		return OC::$server->getNavigationManager()->getAll('settings');
450
	}
451
452
	/**
453
	 * get the id of loaded app
454
	 *
455
	 * @return string
456
	 */
457
	public static function getCurrentApp(): string {
458
		if (\OC::$CLI) {
459
			return '';
460
		}
461
462
		$request = \OC::$server->getRequest();
463
		$script = substr($request->getScriptName(), strlen(OC::$WEBROOT) + 1);
464
		$topFolder = substr($script, 0, strpos($script, '/') ?: 0);
465
		if (empty($topFolder)) {
466
			try {
467
				$path_info = $request->getPathInfo();
468
			} catch (Exception $e) {
469
				// Can happen from unit tests because the script name is `./vendor/bin/phpunit` or something a like then.
470
				\OC::$server->get(LoggerInterface::class)->error('Failed to detect current app from script path', ['exception' => $e]);
471
				return '';
472
			}
473
			if ($path_info) {
474
				$topFolder = substr($path_info, 1, strpos($path_info, '/', 1) - 1);
475
			}
476
		}
477
		if ($topFolder == 'apps') {
478
			$length = strlen($topFolder);
479
			return substr($script, $length + 1, strpos($script, '/', $length + 1) - $length - 1) ?: '';
480
		} else {
481
			return $topFolder;
482
		}
483
	}
484
485
	/**
486
	 * @param string $type
487
	 * @return array
488
	 */
489
	public static function getForms(string $type): array {
490
		$forms = [];
491
		switch ($type) {
492
			case 'admin':
493
				$source = self::$adminForms;
494
				break;
495
			case 'personal':
496
				$source = self::$personalForms;
497
				break;
498
			default:
499
				return [];
500
		}
501
		foreach ($source as $form) {
502
			$forms[] = include $form;
503
		}
504
		return $forms;
505
	}
506
507
	/**
508
	 * @param array $entry
509
	 * @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
510
	 */
511
	public static function registerLogIn(array $entry) {
512
		\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');
513
		self::$altLogin[] = $entry;
514
	}
515
516
	/**
517
	 * @return array
518
	 */
519
	public static function getAlternativeLogIns(): array {
520
		/** @var Coordinator $bootstrapCoordinator */
521
		$bootstrapCoordinator = \OC::$server->query(Coordinator::class);
522
523
		foreach ($bootstrapCoordinator->getRegistrationContext()->getAlternativeLogins() as $registration) {
524
			if (!in_array(IAlternativeLogin::class, class_implements($registration->getService()), true)) {
525
				\OC::$server->getLogger()->error('Alternative login option {option} does not implement {interface} and is therefore ignored.', [
526
					'option' => $registration->getService(),
527
					'interface' => IAlternativeLogin::class,
528
					'app' => $registration->getAppId(),
529
				]);
530
				continue;
531
			}
532
533
			try {
534
				/** @var IAlternativeLogin $provider */
535
				$provider = \OC::$server->query($registration->getService());
536
			} catch (QueryException $e) {
537
				\OC::$server->getLogger()->logException($e, [
538
					'message' => 'Alternative login option {option} can not be initialised.',
539
					'option' => $registration->getService(),
540
					'app' => $registration->getAppId(),
541
				]);
542
			}
543
544
			try {
545
				$provider->load();
546
547
				self::$altLogin[] = [
548
					'name' => $provider->getLabel(),
549
					'href' => $provider->getLink(),
550
					'class' => $provider->getClass(),
551
				];
552
			} catch (Throwable $e) {
553
				\OC::$server->getLogger()->logException($e, [
554
					'message' => 'Alternative login option {option} had an error while loading.',
555
					'option' => $registration->getService(),
556
					'app' => $registration->getAppId(),
557
				]);
558
			}
559
		}
560
561
		return self::$altLogin;
562
	}
563
564
	/**
565
	 * get a list of all apps in the apps folder
566
	 *
567
	 * @return string[] an array of app names (string IDs)
568
	 * @todo: change the name of this method to getInstalledApps, which is more accurate
569
	 */
570
	public static function getAllApps(): array {
571
		$apps = [];
572
573
		foreach (OC::$APPSROOTS as $apps_dir) {
574
			if (!is_readable($apps_dir['path'])) {
575
				\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

575
				\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...
576
				continue;
577
			}
578
			$dh = opendir($apps_dir['path']);
579
580
			if (is_resource($dh)) {
581
				while (($file = readdir($dh)) !== false) {
582
					if ($file[0] != '.' and is_dir($apps_dir['path'] . '/' . $file) and is_file($apps_dir['path'] . '/' . $file . '/appinfo/info.xml')) {
583
						$apps[] = $file;
584
					}
585
				}
586
			}
587
		}
588
589
		$apps = array_unique($apps);
590
591
		return $apps;
592
	}
593
594
	/**
595
	 * List all supported apps
596
	 *
597
	 * @return array
598
	 */
599
	public function getSupportedApps(): array {
600
		/** @var \OCP\Support\Subscription\IRegistry $subscriptionRegistry */
601
		$subscriptionRegistry = \OC::$server->query(\OCP\Support\Subscription\IRegistry::class);
602
		$supportedApps = $subscriptionRegistry->delegateGetSupportedApps();
603
		return $supportedApps;
604
	}
605
606
	/**
607
	 * List all apps, this is used in apps.php
608
	 *
609
	 * @return array
610
	 */
611
	public function listAllApps(): array {
612
		$installedApps = OC_App::getAllApps();
613
614
		$appManager = \OC::$server->getAppManager();
615
		//we don't want to show configuration for these
616
		$blacklist = $appManager->getAlwaysEnabledApps();
617
		$appList = [];
618
		$langCode = \OC::$server->getL10N('core')->getLanguageCode();
619
		$urlGenerator = \OC::$server->getURLGenerator();
620
		$supportedApps = $this->getSupportedApps();
621
622
		foreach ($installedApps as $app) {
623
			if (array_search($app, $blacklist) === false) {
624
				$info = OC_App::getAppInfo($app, false, $langCode);
625
				if (!is_array($info)) {
626
					\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

626
					\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...
627
					continue;
628
				}
629
630
				if (!isset($info['name'])) {
631
					\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

631
					\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...
632
					continue;
633
				}
634
635
				$enabled = \OC::$server->getConfig()->getAppValue($app, 'enabled', 'no');
636
				$info['groups'] = null;
637
				if ($enabled === 'yes') {
638
					$active = true;
639
				} elseif ($enabled === 'no') {
640
					$active = false;
641
				} else {
642
					$active = true;
643
					$info['groups'] = $enabled;
644
				}
645
646
				$info['active'] = $active;
647
648
				if ($appManager->isShipped($app)) {
649
					$info['internal'] = true;
650
					$info['level'] = self::officialApp;
651
					$info['removable'] = false;
652
				} else {
653
					$info['internal'] = false;
654
					$info['removable'] = true;
655
				}
656
657
				if (in_array($app, $supportedApps)) {
658
					$info['level'] = self::supportedApp;
659
				}
660
661
				$appPath = self::getAppPath($app);
662
				if ($appPath !== false) {
663
					$appIcon = $appPath . '/img/' . $app . '.svg';
664
					if (file_exists($appIcon)) {
665
						$info['preview'] = $urlGenerator->imagePath($app, $app . '.svg');
666
						$info['previewAsIcon'] = true;
667
					} else {
668
						$appIcon = $appPath . '/img/app.svg';
669
						if (file_exists($appIcon)) {
670
							$info['preview'] = $urlGenerator->imagePath($app, 'app.svg');
671
							$info['previewAsIcon'] = true;
672
						}
673
					}
674
				}
675
				// fix documentation
676
				if (isset($info['documentation']) && is_array($info['documentation'])) {
677
					foreach ($info['documentation'] as $key => $url) {
678
						// If it is not an absolute URL we assume it is a key
679
						// i.e. admin-ldap will get converted to go.php?to=admin-ldap
680
						if (stripos($url, 'https://') !== 0 && stripos($url, 'http://') !== 0) {
681
							$url = $urlGenerator->linkToDocs($url);
682
						}
683
684
						$info['documentation'][$key] = $url;
685
					}
686
				}
687
688
				$info['version'] = OC_App::getAppVersion($app);
689
				$appList[] = $info;
690
			}
691
		}
692
693
		return $appList;
694
	}
695
696
	public static function shouldUpgrade(string $app): bool {
697
		$versions = self::getAppVersions();
698
		$currentVersion = OC_App::getAppVersion($app);
699
		if ($currentVersion && isset($versions[$app])) {
700
			$installedVersion = $versions[$app];
701
			if (!version_compare($currentVersion, $installedVersion, '=')) {
702
				return true;
703
			}
704
		}
705
		return false;
706
	}
707
708
	/**
709
	 * Adjust the number of version parts of $version1 to match
710
	 * the number of version parts of $version2.
711
	 *
712
	 * @param string $version1 version to adjust
713
	 * @param string $version2 version to take the number of parts from
714
	 * @return string shortened $version1
715
	 */
716
	private static function adjustVersionParts(string $version1, string $version2): string {
717
		$version1 = explode('.', $version1);
718
		$version2 = explode('.', $version2);
719
		// reduce $version1 to match the number of parts in $version2
720
		while (count($version1) > count($version2)) {
721
			array_pop($version1);
722
		}
723
		// if $version1 does not have enough parts, add some
724
		while (count($version1) < count($version2)) {
725
			$version1[] = '0';
726
		}
727
		return implode('.', $version1);
728
	}
729
730
	/**
731
	 * Check whether the current ownCloud version matches the given
732
	 * application's version requirements.
733
	 *
734
	 * The comparison is made based on the number of parts that the
735
	 * app info version has. For example for ownCloud 6.0.3 if the
736
	 * app info version is expecting version 6.0, the comparison is
737
	 * made on the first two parts of the ownCloud version.
738
	 * This means that it's possible to specify "requiremin" => 6
739
	 * and "requiremax" => 6 and it will still match ownCloud 6.0.3.
740
	 *
741
	 * @param string $ocVersion ownCloud version to check against
742
	 * @param array $appInfo app info (from xml)
743
	 *
744
	 * @return boolean true if compatible, otherwise false
745
	 */
746
	public static function isAppCompatible(string $ocVersion, array $appInfo, bool $ignoreMax = false): bool {
747
		$requireMin = '';
748
		$requireMax = '';
749
		if (isset($appInfo['dependencies']['nextcloud']['@attributes']['min-version'])) {
750
			$requireMin = $appInfo['dependencies']['nextcloud']['@attributes']['min-version'];
751
		} elseif (isset($appInfo['dependencies']['owncloud']['@attributes']['min-version'])) {
752
			$requireMin = $appInfo['dependencies']['owncloud']['@attributes']['min-version'];
753
		} elseif (isset($appInfo['requiremin'])) {
754
			$requireMin = $appInfo['requiremin'];
755
		} elseif (isset($appInfo['require'])) {
756
			$requireMin = $appInfo['require'];
757
		}
758
759
		if (isset($appInfo['dependencies']['nextcloud']['@attributes']['max-version'])) {
760
			$requireMax = $appInfo['dependencies']['nextcloud']['@attributes']['max-version'];
761
		} elseif (isset($appInfo['dependencies']['owncloud']['@attributes']['max-version'])) {
762
			$requireMax = $appInfo['dependencies']['owncloud']['@attributes']['max-version'];
763
		} elseif (isset($appInfo['requiremax'])) {
764
			$requireMax = $appInfo['requiremax'];
765
		}
766
767
		if (!empty($requireMin)
768
			&& version_compare(self::adjustVersionParts($ocVersion, $requireMin), $requireMin, '<')
769
		) {
770
			return false;
771
		}
772
773
		if (!$ignoreMax && !empty($requireMax)
774
			&& version_compare(self::adjustVersionParts($ocVersion, $requireMax), $requireMax, '>')
775
		) {
776
			return false;
777
		}
778
779
		return true;
780
	}
781
782
	/**
783
	 * get the installed version of all apps
784
	 */
785
	public static function getAppVersions() {
786
		static $versions;
787
788
		if (!$versions) {
789
			$appConfig = \OC::$server->getAppConfig();
790
			$versions = $appConfig->getValues(false, 'installed_version');
791
		}
792
		return $versions;
793
	}
794
795
	/**
796
	 * update the database for the app and call the update script
797
	 *
798
	 * @param string $appId
799
	 * @return bool
800
	 */
801
	public static function updateApp(string $appId): bool {
802
		// for apps distributed with core, we refresh app path in case the downloaded version
803
		// have been installed in custom apps and not in the default path
804
		$appPath = self::getAppPath($appId, true);
805
		if ($appPath === false) {
806
			return false;
807
		}
808
809
		if (is_file($appPath . '/appinfo/database.xml')) {
810
			\OC::$server->getLogger()->error('The appinfo/database.xml file is not longer supported. Used in ' . $appId);
811
			return false;
812
		}
813
814
		\OC::$server->getAppManager()->clearAppsCache();
815
		$l = \OC::$server->getL10N('core');
816
		$appData = self::getAppInfo($appId, false, $l->getLanguageCode());
817
818
		$ignoreMaxApps = \OC::$server->getConfig()->getSystemValue('app_install_overwrite', []);
819
		$ignoreMax = in_array($appId, $ignoreMaxApps, true);
820
		\OC_App::checkAppDependencies(
821
			\OC::$server->getConfig(),
822
			$l,
823
			$appData,
824
			$ignoreMax
825
		);
826
827
		self::registerAutoloading($appId, $appPath, true);
828
		self::executeRepairSteps($appId, $appData['repair-steps']['pre-migration']);
829
830
		$ms = new MigrationService($appId, \OC::$server->get(\OC\DB\Connection::class));
831
		$ms->migrate();
832
833
		self::executeRepairSteps($appId, $appData['repair-steps']['post-migration']);
834
		self::setupLiveMigrations($appId, $appData['repair-steps']['live-migration']);
835
		// update appversion in app manager
836
		\OC::$server->getAppManager()->clearAppsCache();
837
		\OC::$server->getAppManager()->getAppVersion($appId, false);
838
839
		self::setupBackgroundJobs($appData['background-jobs']);
840
841
		//set remote/public handlers
842
		if (array_key_exists('ocsid', $appData)) {
843
			\OC::$server->getConfig()->setAppValue($appId, 'ocsid', $appData['ocsid']);
844
		} 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...
845
			\OC::$server->getConfig()->deleteAppValue($appId, 'ocsid');
846
		}
847
		foreach ($appData['remote'] as $name => $path) {
848
			\OC::$server->getConfig()->setAppValue('core', 'remote_' . $name, $appId . '/' . $path);
849
		}
850
		foreach ($appData['public'] as $name => $path) {
851
			\OC::$server->getConfig()->setAppValue('core', 'public_' . $name, $appId . '/' . $path);
852
		}
853
854
		self::setAppTypes($appId);
855
856
		$version = \OC_App::getAppVersion($appId);
857
		\OC::$server->getConfig()->setAppValue($appId, 'installed_version', $version);
858
859
		\OC::$server->get(IEventDispatcher::class)->dispatchTyped(new AppUpdateEvent($appId));
860
		\OC::$server->getEventDispatcher()->dispatch(ManagerEvent::EVENT_APP_UPDATE, new ManagerEvent(
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

860
		\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...
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

860
		\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...
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

860
		\OC::$server->getEventDispatcher()->dispatch(/** @scrutinizer ignore-type */ ManagerEvent::EVENT_APP_UPDATE, new ManagerEvent(
Loading history...
861
			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

861
			/** @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...
862
		));
863
864
		return true;
865
	}
866
867
	/**
868
	 * @param string $appId
869
	 * @param string[] $steps
870
	 * @throws \OC\NeedsUpdateException
871
	 */
872
	public static function executeRepairSteps(string $appId, array $steps) {
873
		if (empty($steps)) {
874
			return;
875
		}
876
		// load the app
877
		self::loadApp($appId);
878
879
		$dispatcher = \OC::$server->get(IEventDispatcher::class);
880
881
		// load the steps
882
		$r = new Repair([], $dispatcher, \OC::$server->get(LoggerInterface::class));
883
		foreach ($steps as $step) {
884
			try {
885
				$r->addStep($step);
886
			} catch (Exception $ex) {
887
				$dispatcher->dispatchTyped(new RepairErrorEvent($ex->getMessage()));
888
				\OC::$server->getLogger()->logException($ex);
889
			}
890
		}
891
		// run the steps
892
		$r->run();
893
	}
894
895
	public static function setupBackgroundJobs(array $jobs) {
896
		$queue = \OC::$server->getJobList();
897
		foreach ($jobs as $job) {
898
			$queue->add($job);
899
		}
900
	}
901
902
	/**
903
	 * @param string $appId
904
	 * @param string[] $steps
905
	 */
906
	private static function setupLiveMigrations(string $appId, array $steps) {
907
		$queue = \OC::$server->getJobList();
908
		foreach ($steps as $step) {
909
			$queue->add('OC\Migration\BackgroundRepair', [
910
				'app' => $appId,
911
				'step' => $step]);
912
		}
913
	}
914
915
	/**
916
	 * @param string $appId
917
	 * @return \OC\Files\View|false
918
	 */
919
	public static function getStorage(string $appId) {
920
		if (\OC::$server->getAppManager()->isEnabledForUser($appId)) { //sanity check
921
			if (\OC::$server->getUserSession()->isLoggedIn()) {
922
				$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

922
				$view = new \OC\Files\View('/' . /** @scrutinizer ignore-type */ OC_User::getUser());
Loading history...
923
				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...
924
					$view->mkdir($appId);
925
				}
926
				return new \OC\Files\View('/' . OC_User::getUser() . '/' . $appId);
927
			} else {
928
				\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

928
				\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...
929
				return false;
930
			}
931
		} else {
932
			\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

932
			\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...
933
			return false;
934
		}
935
	}
936
937
	protected static function findBestL10NOption(array $options, string $lang): string {
938
		// only a single option
939
		if (isset($options['@value'])) {
940
			return $options['@value'];
941
		}
942
943
		$fallback = $similarLangFallback = $englishFallback = false;
944
945
		$lang = strtolower($lang);
946
		$similarLang = $lang;
947
		if (strpos($similarLang, '_')) {
948
			// For "de_DE" we want to find "de" and the other way around
949
			$similarLang = substr($lang, 0, strpos($lang, '_'));
950
		}
951
952
		foreach ($options as $option) {
953
			if (is_array($option)) {
954
				if ($fallback === false) {
955
					$fallback = $option['@value'];
956
				}
957
958
				if (!isset($option['@attributes']['lang'])) {
959
					continue;
960
				}
961
962
				$attributeLang = strtolower($option['@attributes']['lang']);
963
				if ($attributeLang === $lang) {
964
					return $option['@value'];
965
				}
966
967
				if ($attributeLang === $similarLang) {
968
					$similarLangFallback = $option['@value'];
969
				} elseif (strpos($attributeLang, $similarLang . '_') === 0) {
970
					if ($similarLangFallback === false) {
971
						$similarLangFallback = $option['@value'];
972
					}
973
				}
974
			} else {
975
				$englishFallback = $option;
976
			}
977
		}
978
979
		if ($similarLangFallback !== false) {
980
			return $similarLangFallback;
981
		} elseif ($englishFallback !== false) {
982
			return $englishFallback;
983
		}
984
		return (string) $fallback;
985
	}
986
987
	/**
988
	 * parses the app data array and enhanced the 'description' value
989
	 *
990
	 * @param array $data the app data
991
	 * @param string $lang
992
	 * @return array improved app data
993
	 */
994
	public static function parseAppInfo(array $data, $lang = null): array {
995
		if ($lang && isset($data['name']) && is_array($data['name'])) {
996
			$data['name'] = self::findBestL10NOption($data['name'], $lang);
997
		}
998
		if ($lang && isset($data['summary']) && is_array($data['summary'])) {
999
			$data['summary'] = self::findBestL10NOption($data['summary'], $lang);
1000
		}
1001
		if ($lang && isset($data['description']) && is_array($data['description'])) {
1002
			$data['description'] = trim(self::findBestL10NOption($data['description'], $lang));
1003
		} elseif (isset($data['description']) && is_string($data['description'])) {
1004
			$data['description'] = trim($data['description']);
1005
		} else {
1006
			$data['description'] = '';
1007
		}
1008
1009
		return $data;
1010
	}
1011
1012
	/**
1013
	 * @param \OCP\IConfig $config
1014
	 * @param \OCP\IL10N $l
1015
	 * @param array $info
1016
	 * @throws \Exception
1017
	 */
1018
	public static function checkAppDependencies(\OCP\IConfig $config, \OCP\IL10N $l, array $info, bool $ignoreMax) {
1019
		$dependencyAnalyzer = new DependencyAnalyzer(new Platform($config), $l);
1020
		$missing = $dependencyAnalyzer->analyze($info, $ignoreMax);
1021
		if (!empty($missing)) {
1022
			$missingMsg = implode(PHP_EOL, $missing);
1023
			throw new \Exception(
1024
				$l->t('App "%1$s" cannot be installed because the following dependencies are not fulfilled: %2$s',
1025
					[$info['name'], $missingMsg]
1026
				)
1027
			);
1028
		}
1029
	}
1030
}
1031