OC_App   F
last analyzed

Complexity

Total Complexity 178

Size/Duplication

Total Lines 1112
Duplicated Lines 2.7 %

Coupling/Cohesion

Components 3
Dependencies 33

Importance

Changes 0
Metric Value
dl 30
loc 1112
rs 1.152
c 0
b 0
f 0
wmc 178
lcom 3
cbo 33

48 Methods

Rating   Name   Duplication   Size   Complexity  
A cleanAppId() 0 3 1
A isAppLoaded() 0 3 1
C loadApps() 0 37 12
B loadApp() 0 28 6
A enableThemeIfApplicable() 0 7 2
A registerAutoloading() 0 11 2
A requireAppFile() 0 18 4
A isType() 0 12 4
A getAppTypes() 0 12 3
A setAppTypes() 0 14 3
A isShipped() 0 3 1
A getEnabledApps() 0 25 4
A isEnabled() 0 3 1
A enable() 0 28 5
A removeApp() 0 7 2
A disable() 0 22 3
A getSettingsNavigation() 0 34 4
A proceedNavigation() 0 25 5
A getInstallPath() 0 10 4
A getAppPath() 0 3 1
A getAppWebPath() 0 3 1
A isAppDirWritable() 0 4 2
A getAppVersion() 0 7 3
A getAppVersionByPath() 0 5 2
B getAppInfo() 0 36 9
A getNavigation() 0 4 1
A getCurrentApp() 0 17 4
A getForms() 0 21 4
A registerAdmin() 0 6 1
A registerPersonal() 0 6 1
A registerLogIn() 0 3 1
A getAlternativeLogIns() 0 3 1
B getAllApps() 0 21 8
D listAllApps() 0 86 17
A getInternalAppIdByOcs() 0 9 3
A shouldUpgrade() 0 8 3
A adjustVersionParts() 0 13 3
B isAppCompatible() 24 35 11
A getAppVersions() 0 9 2
B updateApp() 6 45 10
A executeRepairSteps() 0 22 4
A setupBackgroundJobs() 0 6 2
A setupLiveMigrations() 0 8 2
A getStorage() 0 17 4
A parseAppInfo() 0 29 4
A checkAppDependencies() 0 12 2
A clearAppCache() 0 3 1
A atLeastMinorVersionLevelChanged() 0 19 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like OC_App often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use OC_App, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @author Arthur Schiwon <[email protected]>
4
 * @author Bart Visscher <[email protected]>
5
 * @author Bernhard Posselt <[email protected]>
6
 * @author Borjan Tchakaloff <[email protected]>
7
 * @author Brice Maron <[email protected]>
8
 * @author Christopher Schäpers <[email protected]>
9
 * @author Felix Moeller <[email protected]>
10
 * @author Frank Karlitschek <[email protected]>
11
 * @author Georg Ehrke <[email protected]>
12
 * @author Jakob Sack <[email protected]>
13
 * @author Joas Schilling <[email protected]>
14
 * @author Jörn Friedrich Dreyer <[email protected]>
15
 * @author Kamil Domanski <[email protected]>
16
 * @author Lukas Reschke <[email protected]>
17
 * @author Markus Goetz <[email protected]>
18
 * @author Morris Jobke <[email protected]>
19
 * @author Philipp Schaffrath <[email protected]>
20
 * @author ppaysant <[email protected]>
21
 * @author RealRancor <[email protected]>
22
 * @author Robin Appelman <[email protected]>
23
 * @author Robin McCorkell <[email protected]>
24
 * @author Roeland Jago Douma <[email protected]>
25
 * @author Sam Tuke <[email protected]>
26
 * @author Thomas Müller <[email protected]>
27
 * @author Thomas Tanghus <[email protected]>
28
 * @author Tom Needham <[email protected]>
29
 * @author Vincent Petry <[email protected]>
30
 *
31
 * @copyright Copyright (c) 2018, ownCloud GmbH
32
 * @license AGPL-3.0
33
 *
34
 * This code is free software: you can redistribute it and/or modify
35
 * it under the terms of the GNU Affero General Public License, version 3,
36
 * as published by the Free Software Foundation.
37
 *
38
 * This program is distributed in the hope that it will be useful,
39
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
40
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
41
 * GNU Affero General Public License for more details.
42
 *
43
 * You should have received a copy of the GNU Affero General Public License, version 3,
44
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
45
 *
46
 */
47
use OC\App\DependencyAnalyzer;
48
use OC\App\InfoParser;
49
use OC\App\Platform;
50
use OC\Installer;
51
use OC\Repair;
52
53
/**
54
 * This class manages the apps. It allows them to register and integrate in the
55
 * ownCloud ecosystem. Furthermore, this class is responsible for installing,
56
 * upgrading and removing apps.
57
 */
58
class OC_App {
59
	private static $appVersion = [];
60
	private static $adminForms = [];
61
	private static $personalForms = [];
62
	private static $appInfo = [];
63
	private static $appTypes = [];
64
	private static $loadedApps = [];
65
	private static $loadedTypes = [];
66
	private static $altLogin = [];
67
	const officialApp = 200;
68
	const approvedApp = 100;
69
70
	/**
71
	 * clean the appId
72
	 *
73
	 * @param string|boolean $app AppId that needs to be cleaned
74
	 * @return string
75
	 */
76
	public static function cleanAppId($app) {
77
		return \str_replace(['\0', '/', '\\', '..'], '', $app);
78
	}
79
80
	/**
81
	 * Check if an app is loaded
82
	 *
83
	 * @param string $app
84
	 * @return bool
85
	 */
86
	public static function isAppLoaded($app) {
87
		return \in_array($app, self::$loadedApps, true);
88
	}
89
90
	/**
91
	 * loads all apps
92
	 *
93
	 * @param string[] | string | null $types
94
	 * @return bool
95
	 *
96
	 * This function walks through the ownCloud directory and loads all apps
97
	 * it can find. A directory contains an app if the file /appinfo/info.xml
98
	 * exists.
99
	 *
100
	 * if $types is set, only apps of those types will be loaded
101
	 */
102
	public static function loadApps($types = null) {
103
		if (\is_array($types) && !\array_diff($types, self::$loadedTypes)) {
104
			return true;
105
		}
106
		if (\OC::$server->getSystemConfig()->getValue('maintenance', false)) {
107
			return false;
108
		}
109
		// Load the enabled apps here
110
		$apps = self::getEnabledApps();
111
112
		// Add each apps' folder as allowed class path
113
		foreach ($apps as $app) {
114
			if (self::isAppLoaded($app)) {
115
				continue;
116
			}
117
			$path = self::getAppPath($app);
118
			if ($path !== false) {
119
				self::registerAutoloading($app, $path);
120
			}
121
		}
122
123
		// prevent app.php from printing output
124
		\ob_start();
125
		foreach ($apps as $app) {
126
			if (($types === null or self::isType($app, $types)) && !\in_array($app, self::$loadedApps)) {
127
				self::loadApp($app);
128
			}
129
		}
130
		\ob_end_clean();
131
132
		if (\is_array($types)) {
133
			self::$loadedTypes = \array_merge(self::$loadedTypes, $types);
134
		}
135
136
		\OC_Hook::emit('OC_App', 'loadedApps');
137
		return true;
138
	}
139
140
	/**
141
	 * load a single app
142
	 *
143
	 * @param string $app
144
	 * @param bool $checkUpgrade whether an upgrade check should be done
145
	 * @throws \OC\NeedsUpdateException
146
	 */
147
	public static function loadApp($app, $checkUpgrade = true) {
148
		self::$loadedApps[] = $app;
149
		$appPath = self::getAppPath($app);
150
		if ($appPath === false) {
151
			return;
152
		}
153
154
		// in case someone calls loadApp() directly
155
		self::registerAutoloading($app, $appPath);
156
157
		self::enableThemeIfApplicable($app);
158
159
		if (\is_file($appPath . '/appinfo/app.php')) {
160
			\OC::$server->getEventLogger()->start('load_app_' . $app, 'Load app: ' . $app);
161
			if ($checkUpgrade and self::shouldUpgrade($app)) {
162
				throw new \OC\NeedsUpdateException();
163
			}
164
			self::requireAppFile($app);
165
			if (self::isType($app, ['authentication'])) {
166
				// since authentication apps affect the "is app enabled for group" check,
167
				// the enabled apps cache needs to be cleared to make sure that the
168
				// next time getEnableApps() is called it will also include apps that were
169
				// enabled for groups
170
				self::$enabledAppsCache = [];
171
			}
172
			\OC::$server->getEventLogger()->end('load_app_' . $app);
173
		}
174
	}
175
176
	/**
177
	 * Enables the app as a theme if it has the type "theme"
178
	 * @param string $app
179
	 */
180
	private static function enableThemeIfApplicable($app) {
181
		if (self::isType($app, 'theme')) {
182
			/** @var \OCP\Theme\IThemeService $themeService */
183
			$themeService = \OC::$server->query('ThemeService');
184
			$themeService->setAppTheme($app);
185
		}
186
	}
187
188
	/**
189
	 * @internal
190
	 * @param string $app
191
	 * @param string $path
192
	 */
193
	public static function registerAutoloading($app, $path) {
194
		// Register on PSR-4 composer autoloader
195
		$appNamespace = \OC\AppFramework\App::buildAppNamespace($app);
196
		\OC::$composerAutoloader->addPsr4($appNamespace . '\\', $path . '/lib/', true);
197
		if (\defined('PHPUNIT_RUN')) {
198
			\OC::$composerAutoloader->addPsr4($appNamespace . '\\Tests\\', $path . '/tests/', true);
199
		}
200
201
		// Register on legacy autoloader
202
		\OC::$loader->addValidRoot($path);
203
	}
204
205
	/**
206
	 * Load app.php from the given app
207
	 *
208
	 * @param string $app app name
209
	 */
210
	private static function requireAppFile($app) {
211
		try {
212
			// encapsulated here to avoid variable scope conflicts
213
			require_once $app . '/appinfo/app.php';
214
		} catch (Exception $ex) {
215
			\OC::$server->getLogger()->logException($ex);
216
			$blacklist = \OC::$server->getAppManager()->getAlwaysEnabledApps();
217
			if (!\in_array($app, $blacklist)) {
218
				if (!self::isType($app, ['authentication', 'filesystem'])) {
219
					\OC::$server->getLogger()->warning('Could not load app "' . $app . '", it will be disabled', ['app' => 'core']);
220
					self::disable($app);
221
				} else {
222
					\OC::$server->getLogger()->warning('Could not load app "' . $app . '", see exception above', ['app' => 'core']);
223
				}
224
			}
225
			throw $ex;
226
		}
227
	}
228
229
	/**
230
	 * check if an app is of a specific type
231
	 *
232
	 * @param string $app
233
	 * @param string|array $types
234
	 * @return bool
235
	 */
236
	public static function isType($app, $types) {
237
		if (\is_string($types)) {
238
			$types = [$types];
239
		}
240
		$appTypes = self::getAppTypes($app);
241
		foreach ($types as $type) {
242
			if (\array_search($type, $appTypes) !== false) {
243
				return true;
244
			}
245
		}
246
		return false;
247
	}
248
249
	/**
250
	 * get the types of an app
251
	 *
252
	 * @param string $app
253
	 * @return array
254
	 */
255
	private static function getAppTypes($app) {
256
		//load the cache
257
		if (\count(self::$appTypes) == 0) {
258
			self::$appTypes = \OC::$server->getAppConfig()->getValues(false, 'types');
0 ignored issues
show
Documentation Bug introduced by
It seems like \OC::$server->getAppConf...tValues(false, 'types') can also be of type false. However, the property $appTypes is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
259
		}
260
261
		if (isset(self::$appTypes[$app])) {
262
			return \explode(',', self::$appTypes[$app]);
263
		} else {
264
			return [];
265
		}
266
	}
267
268
	/**
269
	 * read app types from info.xml and cache them in the database
270
	 */
271
	public static function setAppTypes($app) {
272
		$appData = self::getAppInfo($app);
273
		if (!\is_array($appData)) {
274
			return;
275
		}
276
277
		if (isset($appData['types'])) {
278
			$appTypes = \implode(',', $appData['types']);
279
		} else {
280
			$appTypes = '';
281
		}
282
283
		\OC::$server->getAppConfig()->setValue($app, 'types', $appTypes);
284
	}
285
286
	/**
287
	 * check if app is shipped
288
	 *
289
	 * @param string $appId the id of the app to check
290
	 * @return bool
291
	 *
292
	 * Check if an app that is installed is a shipped app or installed from the appstore.
293
	 */
294
	public static function isShipped($appId) {
295
		return \OC::$server->getAppManager()->isShipped($appId);
296
	}
297
298
	/**
299
	 * get all enabled apps
300
	 */
301
	protected static $enabledAppsCache = [];
302
303
	/**
304
	 * Returns apps enabled for the current user.
305
	 *
306
	 * @param bool $forceRefresh whether to refresh the cache
307
	 * @param bool $all whether to return apps for all users, not only the
308
	 * currently logged in one
309
	 * @return string[]
310
	 */
311
	public static function getEnabledApps($forceRefresh = false, $all = false) {
0 ignored issues
show
Unused Code introduced by
The parameter $forceRefresh is not used and could be removed.

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

Loading history...
312
		if (!\OC::$server->getSystemConfig()->getValue('installed', false)) {
313
			return [];
314
		}
315
		// in incognito mode or when logged out, $user will be false,
316
		// which is also the case during an upgrade
317
		$appManager = \OC::$server->getAppManager();
318
		if ($all) {
319
			$user = null;
320
		} else {
321
			$user = \OC::$server->getUserSession()->getUser();
322
		}
323
324
		if ($user === null) {
325
			$apps = $appManager->getInstalledApps();
326
		} else {
327
			$apps = $appManager->getEnabledAppsForUser($user);
328
		}
329
		$apps = \array_filter($apps, function ($app) {
330
			return $app !== 'files';//we add this manually
331
		});
332
		\sort($apps);
333
		\array_unshift($apps, 'files');
334
		return $apps;
335
	}
336
337
	/**
338
	 * checks whether or not an app is enabled
339
	 *
340
	 * @param string $app app
341
	 * @return bool
342
	 *
343
	 * This function checks whether or not an app is enabled.
344
	 */
345
	public static function isEnabled($app) {
346
		return \OC::$server->getAppManager()->isEnabledForUser($app);
347
	}
348
349
	/**
350
	 * enables an app
351
	 *
352
	 * @param mixed $app app
353
	 * @param array $groups (optional) when set, only these groups will have access to the app
354
	 * @throws \Exception
355
	 * @return void
356
	 *
357
	 * This function set an app as enabled in appconfig.
358
	 */
359
	public static function enable($app, $groups = null) {
360
		self::$enabledAppsCache = []; // flush
361
362
		// check for required dependencies
363
		$config = \OC::$server->getConfig();
364
		$l = \OC::$server->getL10N('core');
365
		$info = self::getAppInfo($app);
366
		if ($info === null) {
367
			throw new \Exception("$app can't be enabled since it is not installed.");
368
		}
369
370
		self::checkAppDependencies($config, $l, $info);
371
372
		$appManager = \OC::$server->getAppManager();
373
		if ($groups !== null) {
374
			$groupManager = \OC::$server->getGroupManager();
375
			$groupsList = [];
376
			foreach ($groups as $group) {
377
				$groupItem = $groupManager->get($group);
378
				if ($groupItem instanceof \OCP\IGroup) {
379
					$groupsList[] = $groupManager->get($group);
380
				}
381
			}
382
			$appManager->enableAppForGroups($app, $groupsList);
383
		} else {
384
			$appManager->enableApp($app);
385
		}
386
	}
387
388
	/**
389
	 * @param string $app
390
	 * @return bool
391
	 */
392
	public static function removeApp($app) {
393
		if (self::isShipped($app)) {
394
			return false;
395
		}
396
397
		return Installer::removeApp($app);
398
	}
399
400
	/**
401
	 * This function set an app as disabled in appconfig.
402
	 *
403
	 * @param string $app app
404
	 * @throws Exception
405
	 */
406
	public static function disable($app) {
407
		// Convert OCS ID to regular application identifier
408
		if (self::getInternalAppIdByOcs($app) !== false) {
409
			$app = self::getInternalAppIdByOcs($app);
410
		}
411
412
		// flush
413
		self::$enabledAppsCache = [];
414
415
		// run uninstall steps
416
		$appData = OC_App::getAppInfo($app);
417
		if ($appData !== null) {
418
			OC_App::executeRepairSteps($app, $appData['repair-steps']['uninstall']);
419
		}
420
421
		// emit disable hook - needed anymore ?
422
		\OC_Hook::emit('OC_App', 'pre_disable', ['app' => $app]);
423
424
		// finally disable it
425
		$appManager = \OC::$server->getAppManager();
426
		$appManager->disableApp($app);
427
	}
428
429
	/**
430
	 * Returns the Settings Navigation
431
	 *
432
	 * @return string[]
433
	 *
434
	 * This function returns an array containing all settings pages added. The
435
	 * entries are sorted by the key 'order' ascending.
436
	 */
437
	public static function getSettingsNavigation() {
438
		$l = \OC::$server->getL10N('lib');
439
		$urlGenerator = \OC::$server->getURLGenerator();
440
441
		$settings = [];
442
		// by default, settings only contain the help menu
443
		if (OC_Util::getEditionString() === OC_Util::EDITION_COMMUNITY &&
444
			\OC::$server->getSystemConfig()->getValue('knowledgebaseenabled', true) == true
445
		) {
446
			$settings = [
447
				[
448
					"id" => "help",
449
					"order" => 1000,
450
					"href" => $urlGenerator->linkToRoute('settings_help'),
451
					"name" => $l->t("Help"),
452
					"icon" => $urlGenerator->imagePath("settings", "help.svg")
453
				]
454
			];
455
		}
456
457
		// if the user is logged-in
458
		if (OC_User::isLoggedIn()) {
459
			// personal menu
460
			$settings[] = [
461
				"id" => "settings",
462
				"order" => 1,
463
				"href" => $urlGenerator->linkToRoute('settings.SettingsPage.getPersonal'),
464
				"name" => $l->t("Settings"),
465
				"icon" => $urlGenerator->imagePath("settings", "admin.svg")
466
			];
467
		}
468
469
		return self::proceedNavigation($settings);
470
	}
471
472
	// This is private as well. It simply works, so don't ask for more details
473
	private static function proceedNavigation($list) {
474
		$activeApp = OC::$server->getNavigationManager()->getActiveEntry();
475
		foreach ($list as &$navEntry) {
476
			if ($navEntry['id'] == $activeApp) {
477
				$navEntry['active'] = true;
478
			} else {
479
				$navEntry['active'] = false;
480
			}
481
		}
482
		unset($navEntry);
483
484
		\usort($list, function ($a, $b) {
485
			if ($a["order"] == $b["order"]) {
486
				return 0;
487
			}
488
489
			if ($a["order"] < $b["order"]) {
490
				return -1;
491
			}
492
493
			return 1;
494
		});
495
496
		return $list;
497
	}
498
499
	/**
500
	 * Get the path where to install apps
501
	 *
502
	 * @return string|null
503
	 */
504
	public static function getInstallPath() {
505
		foreach (OC::$APPSROOTS as $dir) {
506
			if (isset($dir['writable']) && $dir['writable'] === true) {
507
				return $dir['path'];
508
			}
509
		}
510
511
		\OCP\Util::writeLog('core', 'No application directories are marked as writable.', \OCP\Util::ERROR);
512
		return null;
513
	}
514
515
	/**
516
	 * Get the directory for the given app.
517
	 * If the app exists in multiple directories, the most recent version is taken.
518
	 * (false if not found)
519
	 *
520
	 * @param string $appId
521
	 * @return string|false
522
	 */
523
	public static function getAppPath($appId) {
524
		return \OC::$server->getAppManager()->getAppPath($appId);
525
	}
526
527
	/**
528
	 * Get the web path for the given app.
529
	 * If the app exists in multiple directories, the most recent version is taken.
530
	 * (false if not found)
531
	 *
532
	 * @param string $appId
533
	 * @return string|false
534
	 */
535
	public static function getAppWebPath($appId) {
536
		return \OC::$server->getAppManager()->getAppWebPath($appId);
537
	}
538
539
	/**
540
	 * check if an app's directory is writable
541
	 *
542
	 * @param string $appId
543
	 * @return bool
544
	 */
545
	public static function isAppDirWritable($appId) {
546
		$path = self::getAppPath($appId);
547
		return ($path !== false) ? \is_writable($path) : false;
548
	}
549
550
	/**
551
	 * get the last version of the app from appinfo/info.xml
552
	 *
553
	 * @param string $appId
554
	 * @return string
555
	 */
556
	public static function getAppVersion($appId) {
557
		if (!isset(self::$appVersion[$appId])) {
558
			$file = self::getAppPath($appId);
559
			self::$appVersion[$appId] = ($file !== false) ? self::getAppVersionByPath($file) : '0';
560
		}
561
		return self::$appVersion[$appId];
562
	}
563
564
	/**
565
	 * get app's version based on it's path
566
	 *
567
	 * @param string $path
568
	 * @return string
569
	 */
570
	public static function getAppVersionByPath($path) {
571
		$infoFile = $path . '/appinfo/info.xml';
572
		$appData = self::getAppInfo($infoFile, true);
573
		return isset($appData['version']) ? $appData['version'] : '';
574
	}
575
576
	/**
577
	 * Read all app metadata from the info.xml file
578
	 *
579
	 * @param string $appId id of the app or the path of the info.xml file
580
	 * @param boolean $path (optional)
581
	 * @return array|null
582
	 * @note all data is read from info.xml, not just pre-defined fields
583
	 */
584
	public static function getAppInfo($appId, $path = false) {
585
		if ($path) {
586
			$file = $appId;
587
		} else {
588
			if (isset(self::$appInfo[$appId])) {
589
				return self::$appInfo[$appId];
590
			}
591
			$appPath = self::getAppPath($appId);
592
			if ($appPath === false) {
593
				return null;
594
			}
595
			$file = $appPath . '/appinfo/info.xml';
596
		}
597
598
		$parser = new InfoParser();
599
		try {
600
			$data = $parser->parse($file);
601
		} catch (\Exception $e) {
602
			\OC::$server->getLogger()->logException($e);
603
			throw $e;
604
		}
605
606
		if (\is_array($data)) {
607
			$data = OC_App::parseAppInfo($data);
608
		}
609
		if (isset($data['ocsid'])) {
610
			$storedId = \OC::$server->getConfig()->getAppValue($appId, 'ocsid');
611
			if ($storedId !== '' && $storedId !== $data['ocsid']) {
612
				$data['ocsid'] = $storedId;
613
			}
614
		}
615
616
		self::$appInfo[$appId] = $data;
617
618
		return $data;
619
	}
620
621
	/**
622
	 * Returns the navigation
623
	 *
624
	 * @return array
625
	 *
626
	 * This function returns an array containing all entries added. The
627
	 * entries are sorted by the key 'order' ascending. Additional to the keys
628
	 * given for each app the following keys exist:
629
	 *   - active: boolean, signals if the user is on this navigation entry
630
	 */
631
	public static function getNavigation() {
632
		$entries = OC::$server->getNavigationManager()->getAll();
633
		return self::proceedNavigation($entries);
634
	}
635
636
	/**
637
	 * get the id of loaded app
638
	 *
639
	 * @return string
640
	 */
641
	public static function getCurrentApp() {
642
		$request = \OC::$server->getRequest();
643
		$script = \substr($request->getScriptName(), \strlen(OC::$WEBROOT) + 1);
644
		$topFolder = \substr($script, 0, \strpos($script, '/'));
645
		if (empty($topFolder)) {
646
			$path_info = $request->getPathInfo();
647
			if ($path_info) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $path_info of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
648
				$topFolder = \substr($path_info, 1, \strpos($path_info, '/', 1) - 1);
649
			}
650
		}
651
		if ($topFolder == 'apps') {
652
			$length = \strlen($topFolder);
653
			return \substr($script, $length + 1, \strpos($script, '/', $length + 1) - $length - 1);
654
		} else {
655
			return $topFolder;
656
		}
657
	}
658
659
	/**
660
	 * @param string $type
661
	 * @return array
662
	 */
663
	public static function getForms($type) {
664
		$forms = [];
665
		switch ($type) {
666
			case 'admin':
667
				$source = self::$adminForms;
668
				break;
669
			case 'personal':
670
				$source = self::$personalForms;
671
				break;
672
			default:
673
				return [];
674
		}
675
		foreach ($source as $form) {
676
			$page = include $form['page'];
677
			$forms[] = [
678
				'appId' => $form['appId'],
679
				'page' => $page,
680
			];
681
		}
682
		return $forms;
683
	}
684
685
	/**
686
	 * register an admin form to be shown
687
	 *
688
	 * @param string $app
689
	 * @param string $page
690
	 * @deprecated Register settings panels in info.xml
691
	 */
692
	public static function registerAdmin($app, $page) {
693
		self::$adminForms[] = [
694
			'appId' => $app,
695
			'page' => $app . '/' . $page . '.php',
696
		];
697
	}
698
699
	/**
700
	 * register a personal form to be shown
701
	 * @param string $app
702
	 * @param string $page
703
	 * @deprecated Register settings panels in info.xml
704
	 */
705
	public static function registerPersonal($app, $page) {
706
		self::$personalForms[] = [
707
			'appId' => $app,
708
			'page' => $app . '/' . $page . '.php',
709
		];
710
	}
711
712
	/**
713
	 * @param array $entry
714
	 */
715
	public static function registerLogIn(array $entry) {
716
		self::$altLogin[] = $entry;
717
	}
718
719
	/**
720
	 * @return array
721
	 */
722
	public static function getAlternativeLogIns() {
723
		return self::$altLogin;
724
	}
725
726
	/**
727
	 * get a list of all apps in the apps folder
728
	 *
729
	 * @return array an array of app names (string IDs)
730
	 * @todo: change the name of this method to getInstalledApps, which is more accurate
731
	 */
732
	public static function getAllApps() {
733
		$apps = [];
734
735
		foreach (OC::$APPSROOTS as $apps_dir) {
736
			if (!\is_readable($apps_dir['path'])) {
737
				\OCP\Util::writeLog('core', 'unable to read app folder : ' . $apps_dir['path'], \OCP\Util::WARN);
738
				continue;
739
			}
740
			$dh = \opendir($apps_dir['path']);
741
742
			if (\is_resource($dh)) {
743
				while (($file = \readdir($dh)) !== false) {
744
					if ($file[0] != '.' and \is_dir($apps_dir['path'] . '/' . $file) and \is_file($apps_dir['path'] . '/' . $file . '/appinfo/info.xml')) {
745
						$apps[] = $file;
746
					}
747
				}
748
			}
749
		}
750
751
		return $apps;
752
	}
753
754
	/**
755
	 * List all apps, this is used in apps.php
756
	 *
757
	 * @param bool $onlyLocal
0 ignored issues
show
Bug introduced by
There is no parameter named $onlyLocal. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
758
	 * @param bool $includeUpdateInfo Should we check whether there is an update
0 ignored issues
show
Bug introduced by
There is no parameter named $includeUpdateInfo. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
759
	 *                                in the app store?
760
	 * @return array
761
	 */
762
	public static function listAllApps() {
763
		$installedApps = OC_App::getAllApps();
764
765
		//TODO which apps do we want to blacklist and how do we integrate
766
		// blacklisting with the multi apps folder feature?
767
768
		//we don't want to show configuration for these
769
		$blacklist = \OC::$server->getAppManager()->getAlwaysEnabledApps();
770
		$appList = [];
771
		$urlGenerator = \OC::$server->getURLGenerator();
772
773
		foreach ($installedApps as $app) {
774
			if (\array_search($app, $blacklist) === false) {
775
				$info = OC_App::getAppInfo($app);
776
				if (!\is_array($info)) {
777
					\OCP\Util::writeLog('core', 'Could not read app info file for app "' . $app . '"', \OCP\Util::ERROR);
778
					continue;
779
				}
780
781
				if (!isset($info['name'])) {
782
					\OCP\Util::writeLog('core', 'App id "' . $app . '" has no name in appinfo', \OCP\Util::ERROR);
783
					continue;
784
				}
785
786
				$enabled = \OC::$server->getAppConfig()->getValue($app, 'enabled', 'no');
787
				$info['groups'] = null;
788
				if ($enabled === 'yes') {
789
					$active = true;
790
				} elseif ($enabled === 'no') {
791
					$active = false;
792
				} else {
793
					$active = true;
794
					$info['groups'] = $enabled;
795
				}
796
797
				$info['active'] = $active;
798
799
				if (self::isShipped($app)) {
800
					$info['internal'] = true;
801
					$info['level'] = self::officialApp;
802
					$info['removable'] = false;
803
				} else {
804
					$result = \OC::$server->getIntegrityCodeChecker()->verifyAppSignature($app, '', true);
805
					if (empty($result)) {
806
						$info['level'] = self::approvedApp;
807
						$info['removable'] = false;
808
					}
809
810
					$info['internal'] = false;
811
					$info['removable'] = true;
812
				}
813
814
				$appPath = self::getAppPath($app);
815
				if ($appPath !== false) {
816
					$appIcon = $appPath . '/img/' . $app . '.svg';
817
					if (\file_exists($appIcon)) {
818
						$info['preview'] = \OC::$server->getURLGenerator()->imagePath($app, $app . '.svg');
819
						$info['previewAsIcon'] = true;
820
					} else {
821
						$appIcon = $appPath . '/img/app.svg';
822
						if (\file_exists($appIcon)) {
823
							$info['preview'] = \OC::$server->getURLGenerator()->imagePath($app, 'app.svg');
824
							$info['previewAsIcon'] = true;
825
						}
826
					}
827
				}
828
				// fix documentation
829
				if (isset($info['documentation']) && \is_array($info['documentation'])) {
830
					foreach ($info['documentation'] as $key => $url) {
831
						// If it is not an absolute URL we assume it is a key
832
						// i.e. admin-ldap will get converted to go.php?to=admin-ldap
833
						if (\stripos($url, 'https://') !== 0 && \stripos($url, 'http://') !== 0) {
834
							$url = $urlGenerator->linkToDocs($url);
835
						}
836
837
						$info['documentation'][$key] = $url;
838
					}
839
				}
840
841
				$info['version'] = OC_App::getAppVersion($app);
842
				$appList[] = $info;
843
			}
844
		}
845
846
		return $appList;
847
	}
848
849
	/**
850
	 * Returns the internal app ID or false
851
	 * @param string $ocsID
852
	 * @return string|false
853
	 */
854
	public static function getInternalAppIdByOcs($ocsID) {
855
		if (\is_numeric($ocsID)) {
856
			$idArray = \OC::$server->getAppConfig()->getValues(false, 'ocsid');
857
			if (\array_search($ocsID, $idArray)) {
858
				return \array_search($ocsID, $idArray);
859
			}
860
		}
861
		return false;
862
	}
863
864
	public static function shouldUpgrade($app) {
865
		$versions = self::getAppVersions();
866
		$currentVersion = self::getAppVersion($app);
867
		if ($currentVersion && isset($versions[$app])) {
868
			return self::atLeastMinorVersionLevelChanged($currentVersion, $versions[$app]);
869
		}
870
		return false;
871
	}
872
873
	/**
874
	 * Adjust the number of version parts of $version1 to match
875
	 * the number of version parts of $version2.
876
	 *
877
	 * @param string $version1 version to adjust
878
	 * @param string $version2 version to take the number of parts from
879
	 * @return string shortened $version1
880
	 */
881
	private static function adjustVersionParts($version1, $version2) {
882
		$version1 = \explode('.', $version1);
883
		$version2 = \explode('.', $version2);
884
		// reduce $version1 to match the number of parts in $version2
885
		while (\count($version1) > \count($version2)) {
886
			\array_pop($version1);
887
		}
888
		// if $version1 does not have enough parts, add some
889
		while (\count($version1) < \count($version2)) {
890
			$version1[] = '0';
891
		}
892
		return \implode('.', $version1);
893
	}
894
895
	/**
896
	 * Check whether the current ownCloud version matches the given
897
	 * application's version requirements.
898
	 *
899
	 * The comparison is made based on the number of parts that the
900
	 * app info version has. For example for ownCloud 6.0.3 if the
901
	 * app info version is expecting version 6.0, the comparison is
902
	 * made on the first two parts of the ownCloud version.
903
	 * This means that it's possible to specify "requiremin" => 6
904
	 * and "requiremax" => 6 and it will still match ownCloud 6.0.3.
905
	 *
906
	 * @param string|array $ocVersion ownCloud version to check against
907
	 * @param array $appInfo app info (from xml)
908
	 *
909
	 * @return boolean true if compatible, otherwise false
910
	 */
911
	public static function isAppCompatible($ocVersion, $appInfo) {
912
		$requireMin = '';
913
		$requireMax = '';
914 View Code Duplication
		if (isset($appInfo['dependencies']['owncloud']['@attributes']['min-version'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
915
			$requireMin = $appInfo['dependencies']['owncloud']['@attributes']['min-version'];
916
		} elseif (isset($appInfo['requiremin'])) {
917
			$requireMin = $appInfo['requiremin'];
918
		} elseif (isset($appInfo['require'])) {
919
			$requireMin = $appInfo['require'];
920
		}
921
922 View Code Duplication
		if (isset($appInfo['dependencies']['owncloud']['@attributes']['max-version'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
923
			$requireMax = $appInfo['dependencies']['owncloud']['@attributes']['max-version'];
924
		} elseif (isset($appInfo['requiremax'])) {
925
			$requireMax = $appInfo['requiremax'];
926
		}
927
928
		if (\is_array($ocVersion)) {
929
			$ocVersion = \implode('.', $ocVersion);
930
		}
931
932 View Code Duplication
		if (!empty($requireMin)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
933
			&& \version_compare(self::adjustVersionParts($ocVersion, $requireMin), $requireMin, '<')
934
		) {
935
			return false;
936
		}
937
938 View Code Duplication
		if (!empty($requireMax)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
939
			&& \version_compare(self::adjustVersionParts($ocVersion, $requireMax), $requireMax, '>')
940
		) {
941
			return false;
942
		}
943
944
		return true;
945
	}
946
947
	/**
948
	 * get the installed version of all apps
949
	 */
950
	public static function getAppVersions() {
951
		static $versions;
952
953
		if (!$versions) {
954
			$appConfig = \OC::$server->getAppConfig();
955
			$versions = $appConfig->getValues(false, 'installed_version');
956
		}
957
		return $versions;
958
	}
959
960
	/**
961
	 * update the database for the app and call the update script
962
	 *
963
	 * @param string $appId
964
	 * @return bool
965
	 */
966
	public static function updateApp($appId) {
967
		$appPath = self::getAppPath($appId);
968
		if ($appPath === false) {
969
			return false;
970
		}
971
		$appData = self::getAppInfo($appId);
972
		self::executeRepairSteps($appId, $appData['repair-steps']['pre-migration']);
973
		if (isset($appData['use-migrations']) && $appData['use-migrations'] === 'true') {
974
			$ms = new \OC\DB\MigrationService($appId, \OC::$server->getDatabaseConnection());
975
			$ms->migrate();
976
		} else {
977
			if (\file_exists($appPath . '/appinfo/database.xml')) {
978
				OC_DB::updateDbFromStructure($appPath . '/appinfo/database.xml');
979
			}
980
		}
981
		self::executeRepairSteps($appId, $appData['repair-steps']['post-migration']);
982
		self::setupLiveMigrations($appId, $appData['repair-steps']['live-migration']);
983
		self::clearAppCache($appId);
984
		// run upgrade code
985
		if (\file_exists($appPath . '/appinfo/update.php')) {
986
			self::loadApp($appId, false);
987
			include $appPath . '/appinfo/update.php';
988
		}
989
		self::setupBackgroundJobs($appData['background-jobs']);
990
991
		//set remote/public handlers
992
		if (\array_key_exists('ocsid', $appData)) {
993
			\OC::$server->getConfig()->setAppValue($appId, 'ocsid', $appData['ocsid']);
994 View Code Duplication
		} elseif (\OC::$server->getConfig()->getAppValue($appId, 'ocsid', null) !== null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
995
			\OC::$server->getConfig()->deleteAppValue($appId, 'ocsid');
996
		}
997 View Code Duplication
		foreach ($appData['remote'] as $name => $path) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
998
			\OC::$server->getConfig()->setAppValue('core', 'remote_' . $name, $appId . '/' . $path);
999
		}
1000
		foreach ($appData['public'] as $name => $path) {
1001
			\OC::$server->getConfig()->setAppValue('core', 'public_' . $name, $appId . '/' . $path);
1002
		}
1003
1004
		self::setAppTypes($appId);
1005
1006
		$version = \OC_App::getAppVersion($appId);
1007
		\OC::$server->getAppConfig()->setValue($appId, 'installed_version', $version);
1008
1009
		return true;
1010
	}
1011
1012
	/**
1013
	 * @param string $appId
1014
	 * @param string[] $steps
1015
	 * @throws \OC\NeedsUpdateException
1016
	 */
1017
	public static function executeRepairSteps($appId, array $steps) {
1018
		if (empty($steps)) {
1019
			return;
1020
		}
1021
		// load the app
1022
		self::loadApp($appId, false);
1023
1024
		$dispatcher = OC::$server->getEventDispatcher();
1025
1026
		// load the steps
1027
		$r = new Repair([], $dispatcher);
0 ignored issues
show
Documentation introduced by
$dispatcher is of type object<Symfony\Component...entDispatcherInterface>, but the function expects a null|object<Symfony\Comp...atcher\EventDispatcher>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1028
		foreach ($steps as $step) {
1029
			try {
1030
				$r->addStep($step);
1031
			} catch (Exception $ex) {
1032
				$r->emit('\OC\Repair', 'error', [$ex->getMessage()]);
1033
				\OC::$server->getLogger()->logException($ex);
1034
			}
1035
		}
1036
		// run the steps
1037
		$r->run();
1038
	}
1039
1040
	public static function setupBackgroundJobs(array $jobs) {
1041
		$queue = \OC::$server->getJobList();
1042
		foreach ($jobs as $job) {
1043
			$queue->add($job);
1044
		}
1045
	}
1046
1047
	/**
1048
	 * @param string $appId
1049
	 * @param string[] $steps
1050
	 */
1051
	private static function setupLiveMigrations($appId, array $steps) {
1052
		$queue = \OC::$server->getJobList();
1053
		foreach ($steps as $step) {
1054
			$queue->add('OC\Migration\BackgroundRepair', [
1055
				'app' => $appId,
1056
				'step' => $step]);
1057
		}
1058
	}
1059
1060
	/**
1061
	 * @param string $appId
1062
	 * @return \OC\Files\View|false
1063
	 */
1064
	public static function getStorage($appId) {
1065
		if (OC_App::isEnabled($appId)) { //sanity check
1066
			if (OC_User::isLoggedIn()) {
1067
				$view = new \OC\Files\View('/' . OC_User::getUser());
1068
				if (!$view->file_exists($appId)) {
1069
					$view->mkdir($appId);
1070
				}
1071
				return new \OC\Files\View('/' . OC_User::getUser() . '/' . $appId);
1072
			} else {
1073
				\OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ', user not logged in', \OCP\Util::ERROR);
1074
				return false;
1075
			}
1076
		} else {
1077
			\OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ' not enabled', \OCP\Util::ERROR);
1078
			return false;
1079
		}
1080
	}
1081
1082
	/**
1083
	 * parses the app data array and enhanced the 'description' value
1084
	 *
1085
	 * @param array $data the app data
1086
	 * @return array improved app data
1087
	 */
1088
	public static function parseAppInfo(array $data) {
1089
1090
		// just modify the description if it is available
1091
		// otherwise this will create a $data element with an empty 'description'
1092
		if (isset($data['description'])) {
1093
			if (\is_string($data['description'])) {
1094
				// sometimes the description contains line breaks and they are then also
1095
				// shown in this way in the app management which isn't wanted as HTML
1096
				// manages line breaks itself
1097
1098
				// first of all we split on empty lines
1099
				$paragraphs = \preg_split("!\n[[:space:]]*\n!mu", $data['description']);
1100
1101
				$result = [];
1102
				foreach ($paragraphs as $value) {
1103
					// replace multiple whitespace (tabs, space, newlines) inside a paragraph
1104
					// with a single space - also trims whitespace
1105
					$result[] = \trim(\preg_replace('![[:space:]]+!mu', ' ', $value));
1106
				}
1107
1108
				// join the single paragraphs with a empty line in between
1109
				$data['description'] = \implode("\n\n", $result);
1110
			} else {
1111
				$data['description'] = '';
1112
			}
1113
		}
1114
1115
		return $data;
1116
	}
1117
1118
	/**
1119
	 * @param OCP\IConfig $config
1120
	 * @param OCP\IL10N $l
1121
	 * @param array $info
1122
	 * @throws Exception
1123
	 */
1124
	protected static function checkAppDependencies($config, $l, $info) {
1125
		$dependencyAnalyzer = new DependencyAnalyzer(new Platform($config), $l);
1126
		$missing = $dependencyAnalyzer->analyze($info);
1127
		if (!empty($missing)) {
1128
			$missingMsg = \join(PHP_EOL, $missing);
1129
			throw new \Exception(
1130
				$l->t('App "%s" cannot be installed because the following dependencies are not fulfilled: %s',
1131
					[$info['name'], $missingMsg]
1132
				)
1133
			);
1134
		}
1135
	}
1136
1137
	/**
1138
	 * @param $appId
1139
	 */
1140
	public static function clearAppCache($appId) {
1141
		unset(self::$appVersion[$appId], self::$appInfo[$appId]);
1142
	}
1143
1144
	/**
1145
	 * @param $app
1146
	 * @param $currentVersion
1147
	 * @param $versions
1148
	 * @return bool
1149
	 */
1150
	public static function atLeastMinorVersionLevelChanged($currentVersion, $installedVersion): bool {
1151
		if ($currentVersion === $installedVersion) {
1152
			return false;
1153
		}
1154
1155
		$p = new \Composer\Semver\VersionParser();
1156
		$currentVersion = $p->normalize($currentVersion);
1157
		$installedVersion = $p->normalize($installedVersion);
1158
1159
		$currentVersion = \explode('.', $currentVersion);
1160
		$installedVersion = \explode('.', $installedVersion);
1161
1162
		if ($currentVersion[0] === $installedVersion[0] &&
1163
			$currentVersion[1] === $installedVersion[1]) {
1164
			return false;
1165
		}
1166
1167
		return true;
1168
	}
1169
}
1170