Completed
Push — master ( d953db...ac63c2 )
by Morris
38s
created

OC_App::findAppInDirectories()   D

Complexity

Conditions 10
Paths 17

Size

Total Lines 40
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 27
nc 17
nop 1
dl 0
loc 40
rs 4.8196
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
declare(strict_types=1);
3
/**
4
 * @copyright Copyright (c) 2016, ownCloud, Inc.
5
 * @copyright Copyright (c) 2016, Lukas Reschke <[email protected]>
6
 *
7
 * @author Arthur Schiwon <[email protected]>
8
 * @author Bart Visscher <[email protected]>
9
 * @author Bernhard Posselt <[email protected]>
10
 * @author Björn Schießle <[email protected]>
11
 * @author Borjan Tchakaloff <[email protected]>
12
 * @author Brice Maron <[email protected]>
13
 * @author Christopher Schäpers <[email protected]>
14
 * @author Felix Moeller <[email protected]>
15
 * @author Frank Karlitschek <[email protected]>
16
 * @author Georg Ehrke <[email protected]>
17
 * @author Jakob Sack <[email protected]>
18
 * @author Joas Schilling <[email protected]>
19
 * @author Julius Haertl <[email protected]>
20
 * @author Julius Härtl <[email protected]>
21
 * @author Jörn Friedrich Dreyer <[email protected]>
22
 * @author Kamil Domanski <[email protected]>
23
 * @author Klaas Freitag <[email protected]>
24
 * @author Lukas Reschke <[email protected]>
25
 * @author Markus Goetz <[email protected]>
26
 * @author Morris Jobke <[email protected]>
27
 * @author RealRancor <[email protected]>
28
 * @author Robin Appelman <[email protected]>
29
 * @author Robin McCorkell <[email protected]>
30
 * @author Roeland Jago Douma <[email protected]>
31
 * @author Sam Tuke <[email protected]>
32
 * @author Sebastian Wessalowski <[email protected]>
33
 * @author Thomas Müller <[email protected]>
34
 * @author Thomas Tanghus <[email protected]>
35
 * @author Tom Needham <[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
use OC\App\DependencyAnalyzer;
54
use OC\App\Platform;
55
use OC\DB\MigrationService;
56
use OC\Installer;
57
use OC\Repair;
58
use OCP\App\ManagerEvent;
59
60
/**
61
 * This class manages the apps. It allows them to register and integrate in the
62
 * ownCloud ecosystem. Furthermore, this class is responsible for installing,
63
 * upgrading and removing apps.
64
 */
65
class OC_App {
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
66
	static private $adminForms = [];
67
	static private $personalForms = [];
68
	static private $appTypes = [];
69
	static private $loadedApps = [];
70
	static private $altLogin = [];
71
	static private $alreadyRegistered = [];
72
	const officialApp = 200;
73
74
	/**
75
	 * clean the appId
76
	 *
77
	 * @param string $app AppId that needs to be cleaned
78
	 * @return string
79
	 */
80
	public static function cleanAppId(string $app): string {
81
		return str_replace(array('\0', '/', '\\', '..'), '', $app);
82
	}
83
84
	/**
85
	 * Check if an app is loaded
86
	 *
87
	 * @param string $app
88
	 * @return bool
89
	 */
90
	public static function isAppLoaded(string $app): bool {
91
		return in_array($app, self::$loadedApps, true);
92
	}
93
94
	/**
95
	 * loads all apps
96
	 *
97
	 * @param string[] $types
98
	 * @return bool
99
	 *
100
	 * This function walks through the ownCloud directory and loads all apps
101
	 * it can find. A directory contains an app if the file /appinfo/info.xml
102
	 * exists.
103
	 *
104
	 * if $types is set to non-empty array, only apps of those types will be loaded
105
	 */
106
	public static function loadApps(array $types = []): bool {
107
		if (\OC::$server->getSystemConfig()->getValue('maintenance', false)) {
108
			return false;
109
		}
110
		// Load the enabled apps here
111
		$apps = self::getEnabledApps();
112
113
		// Add each apps' folder as allowed class path
114
		foreach($apps as $app) {
115
			$path = self::getAppPath($app);
116
			if($path !== false) {
117
				self::registerAutoloading($app, $path);
118
			}
119
		}
120
121
		// prevent app.php from printing output
122
		ob_start();
123
		foreach ($apps as $app) {
124
			if (($types === [] or self::isType($app, $types)) && !in_array($app, self::$loadedApps)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as or instead of || is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
125
				self::loadApp($app);
126
			}
127
		}
128
		ob_end_clean();
129
130
		return true;
131
	}
132
133
	/**
134
	 * load a single app
135
	 *
136
	 * @param string $app
137
	 * @throws Exception
138
	 */
139
	public static function loadApp(string $app) {
140
		self::$loadedApps[] = $app;
141
		$appPath = self::getAppPath($app);
142
		if($appPath === false) {
143
			return;
144
		}
145
146
		// in case someone calls loadApp() directly
147
		self::registerAutoloading($app, $appPath);
148
149
		if (is_file($appPath . '/appinfo/app.php')) {
150
			\OC::$server->getEventLogger()->start('load_app_' . $app, 'Load app: ' . $app);
151
			try {
152
				self::requireAppFile($app);
153
			} catch (Error $ex) {
0 ignored issues
show
Bug introduced by
The class Error does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
154
				\OC::$server->getLogger()->logException($ex);
155
				if (!\OC::$server->getAppManager()->isShipped($app)) {
156
					// Only disable apps which are not shipped
157
					self::disable($app);
158
				}
159
			}
160
			if (self::isType($app, array('authentication'))) {
161
				// since authentication apps affect the "is app enabled for group" check,
162
				// the enabled apps cache needs to be cleared to make sure that the
163
				// next time getEnableApps() is called it will also include apps that were
164
				// enabled for groups
165
				self::$enabledAppsCache = [];
166
			}
167
			\OC::$server->getEventLogger()->end('load_app_' . $app);
168
		}
169
170
		$info = self::getAppInfo($app);
171 View Code Duplication
		if (!empty($info['activity']['filters'])) {
172
			foreach ($info['activity']['filters'] as $filter) {
173
				\OC::$server->getActivityManager()->registerFilter($filter);
174
			}
175
		}
176 View Code Duplication
		if (!empty($info['activity']['settings'])) {
177
			foreach ($info['activity']['settings'] as $setting) {
178
				\OC::$server->getActivityManager()->registerSetting($setting);
179
			}
180
		}
181 View Code Duplication
		if (!empty($info['activity']['providers'])) {
182
			foreach ($info['activity']['providers'] as $provider) {
183
				\OC::$server->getActivityManager()->registerProvider($provider);
184
			}
185
		}
186
187 View Code Duplication
		if (!empty($info['settings']['admin'])) {
188
			foreach ($info['settings']['admin'] as $setting) {
189
				\OC::$server->getSettingsManager()->registerSetting('admin', $setting);
190
			}
191
		}
192
		if (!empty($info['settings']['admin-section'])) {
193
			foreach ($info['settings']['admin-section'] as $section) {
194
				\OC::$server->getSettingsManager()->registerSection('admin', $section);
195
			}
196
		}
197
		if (!empty($info['settings']['personal'])) {
198
			foreach ($info['settings']['personal'] as $setting) {
199
				\OC::$server->getSettingsManager()->registerSetting('personal', $setting);
200
			}
201
		}
202
		if (!empty($info['settings']['personal-section'])) {
203
			foreach ($info['settings']['personal-section'] as $section) {
204
				\OC::$server->getSettingsManager()->registerSection('personal', $section);
205
			}
206
		}
207
208
		if (!empty($info['collaboration']['plugins'])) {
209
			// deal with one or many plugin entries
210
			$plugins = isset($info['collaboration']['plugins']['plugin']['@value']) ?
211
				[$info['collaboration']['plugins']['plugin']] : $info['collaboration']['plugins']['plugin'];
212
			foreach ($plugins as $plugin) {
213
				if($plugin['@attributes']['type'] === 'collaborator-search') {
214
					$pluginInfo = [
215
						'shareType' => $plugin['@attributes']['share-type'],
216
						'class' => $plugin['@value'],
217
					];
218
					\OC::$server->getCollaboratorSearch()->registerPlugin($pluginInfo);
219
				} else if ($plugin['@attributes']['type'] === 'autocomplete-sort') {
220
					\OC::$server->getAutoCompleteManager()->registerSorter($plugin['@value']);
221
				}
222
			}
223
		}
224
	}
225
226
	/**
227
	 * @internal
228
	 * @param string $app
229
	 * @param string $path
230
	 */
231
	public static function registerAutoloading(string $app, string $path) {
232
		$key = $app . '-' . $path;
233
		if(isset(self::$alreadyRegistered[$key])) {
234
			return;
235
		}
236
237
		self::$alreadyRegistered[$key] = true;
238
239
		// Register on PSR-4 composer autoloader
240
		$appNamespace = \OC\AppFramework\App::buildAppNamespace($app);
241
		\OC::$server->registerNamespace($app, $appNamespace);
242
243
		if (file_exists($path . '/composer/autoload.php')) {
244
			require_once $path . '/composer/autoload.php';
245
		} else {
246
			\OC::$composerAutoloader->addPsr4($appNamespace . '\\', $path . '/lib/', true);
247
			// Register on legacy autoloader
248
			\OC::$loader->addValidRoot($path);
249
		}
250
251
		// Register Test namespace only when testing
252
		if (defined('PHPUNIT_RUN') || defined('CLI_TEST_RUN')) {
253
			\OC::$composerAutoloader->addPsr4($appNamespace . '\\Tests\\', $path . '/tests/', true);
254
		}
255
	}
256
257
	/**
258
	 * Load app.php from the given app
259
	 *
260
	 * @param string $app app name
261
	 * @throws Error
262
	 */
263
	private static function requireAppFile(string $app) {
264
		// encapsulated here to avoid variable scope conflicts
265
		require_once $app . '/appinfo/app.php';
266
	}
267
268
	/**
269
	 * check if an app is of a specific type
270
	 *
271
	 * @param string $app
272
	 * @param array $types
273
	 * @return bool
274
	 */
275
	public static function isType(string $app, array $types): bool {
276
		$appTypes = self::getAppTypes($app);
277
		foreach ($types as $type) {
278
			if (array_search($type, $appTypes) !== false) {
279
				return true;
280
			}
281
		}
282
		return false;
283
	}
284
285
	/**
286
	 * get the types of an app
287
	 *
288
	 * @param string $app
289
	 * @return array
290
	 */
291
	private static function getAppTypes(string $app): array {
292
		//load the cache
293
		if (count(self::$appTypes) == 0) {
294
			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...
295
		}
296
297
		if (isset(self::$appTypes[$app])) {
298
			return explode(',', self::$appTypes[$app]);
299
		}
300
301
		return [];
302
	}
303
304
	/**
305
	 * read app types from info.xml and cache them in the database
306
	 */
307
	public static function setAppTypes(string $app) {
308
		$appManager = \OC::$server->getAppManager();
309
		$appData = $appManager->getAppInfo($app);
310
		if(!is_array($appData)) {
311
			return;
312
		}
313
314
		if (isset($appData['types'])) {
315
			$appTypes = implode(',', $appData['types']);
316
		} else {
317
			$appTypes = '';
318
			$appData['types'] = [];
319
		}
320
321
		$config = \OC::$server->getConfig();
322
		$config->setAppValue($app, 'types', $appTypes);
323
324
		if ($appManager->hasProtectedAppType($appData['types'])) {
325
			$enabled = $config->getAppValue($app, 'enabled', 'yes');
326
			if ($enabled !== 'yes' && $enabled !== 'no') {
327
				$config->setAppValue($app, 'enabled', 'yes');
328
			}
329
		}
330
	}
331
332
	/**
333
	 * get all enabled apps
334
	 */
335
	protected static $enabledAppsCache = [];
336
337
	/**
338
	 * Returns apps enabled for the current user.
339
	 *
340
	 * @param bool $forceRefresh whether to refresh the cache
341
	 * @param bool $all whether to return apps for all users, not only the
342
	 * currently logged in one
343
	 * @return string[]
344
	 */
345
	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.

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...
346
		if (!\OC::$server->getSystemConfig()->getValue('installed', false)) {
347
			return [];
348
		}
349
		// in incognito mode or when logged out, $user will be false,
350
		// which is also the case during an upgrade
351
		$appManager = \OC::$server->getAppManager();
352
		if ($all) {
353
			$user = null;
354
		} else {
355
			$user = \OC::$server->getUserSession()->getUser();
356
		}
357
358
		if (is_null($user)) {
359
			$apps = $appManager->getInstalledApps();
360
		} else {
361
			$apps = $appManager->getEnabledAppsForUser($user);
362
		}
363
		$apps = array_filter($apps, function ($app) {
364
			return $app !== 'files';//we add this manually
365
		});
366
		sort($apps);
367
		array_unshift($apps, 'files');
368
		return $apps;
369
	}
370
371
	/**
372
	 * checks whether or not an app is enabled
373
	 *
374
	 * @param string $app app
375
	 * @return bool
376
	 * @deprecated 13.0.0 use \OC::$server->getAppManager()->isEnabledForUser($appId)
377
	 *
378
	 * This function checks whether or not an app is enabled.
379
	 */
380
	public static function isEnabled(string $app): bool {
381
		return \OC::$server->getAppManager()->isEnabledForUser($app);
382
	}
383
384
	/**
385
	 * enables an app
386
	 *
387
	 * @param string $appId
388
	 * @param array $groups (optional) when set, only these groups will have access to the app
389
	 * @throws \Exception
390
	 * @return void
391
	 *
392
	 * This function set an app as enabled in appconfig.
393
	 */
394
	public function enable(string $appId,
395
						   array $groups = []) {
396
		self::$enabledAppsCache = []; // flush
397
398
		// Check if app is already downloaded
399
		$installer = \OC::$server->query(Installer::class);
400
		$isDownloaded = $installer->isDownloaded($appId);
401
402
		if(!$isDownloaded) {
403
			$installer->downloadApp($appId);
404
		}
405
406
		$installer->installApp($appId);
407
408
		$appManager = \OC::$server->getAppManager();
409
		if ($groups !== []) {
410
			$groupManager = \OC::$server->getGroupManager();
411
			$groupsList = [];
412
			foreach ($groups as $group) {
413
				$groupItem = $groupManager->get($group);
414
				if ($groupItem instanceof \OCP\IGroup) {
415
					$groupsList[] = $groupManager->get($group);
416
				}
417
			}
418
			$appManager->enableAppForGroups($appId, $groupsList);
419
		} else {
420
			$appManager->enableApp($appId);
421
		}
422
	}
423
424
	/**
425
	 * This function set an app as disabled in appconfig.
426
	 *
427
	 * @param string $app app
428
	 * @throws Exception
429
	 */
430
	public static function disable(string $app) {
431
		// flush
432
		self::$enabledAppsCache = [];
433
434
		// run uninstall steps
435
		$appData = OC_App::getAppInfo($app);
436
		if (!is_null($appData)) {
437
			OC_App::executeRepairSteps($app, $appData['repair-steps']['uninstall']);
438
		}
439
440
		// emit disable hook - needed anymore ?
441
		\OC_Hook::emit('OC_App', 'pre_disable', array('app' => $app));
442
443
		// finally disable it
444
		$appManager = \OC::$server->getAppManager();
445
		$appManager->disableApp($app);
446
	}
447
448
	/**
449
	 * Get the path where to install apps
450
	 *
451
	 * @return string|false
452
	 */
453
	public static function getInstallPath() {
454
		if (\OC::$server->getSystemConfig()->getValue('appstoreenabled', true) == false) {
455
			return false;
456
		}
457
458
		foreach (OC::$APPSROOTS as $dir) {
459
			if (isset($dir['writable']) && $dir['writable'] === true) {
460
				return $dir['path'];
461
			}
462
		}
463
464
		\OCP\Util::writeLog('core', 'No application directories are marked as writable.', \OCP\Util::ERROR);
465
		return null;
466
	}
467
468
469
	/**
470
	 * search for an app in all app-directories
471
	 *
472
	 * @param string $appId
473
	 * @return false|string
474
	 */
475
	public static function findAppInDirectories(string $appId) {
476
		$sanitizedAppId = self::cleanAppId($appId);
477
		if($sanitizedAppId !== $appId) {
478
			return false;
479
		}
480
		static $app_dir = [];
481
482
		if (isset($app_dir[$appId])) {
483
			return $app_dir[$appId];
484
		}
485
486
		$possibleApps = [];
487
		foreach (OC::$APPSROOTS as $dir) {
488
			if (file_exists($dir['path'] . '/' . $appId)) {
489
				$possibleApps[] = $dir;
490
			}
491
		}
492
493
		if (empty($possibleApps)) {
494
			return false;
495
		} elseif (count($possibleApps) === 1) {
496
			$dir = array_shift($possibleApps);
497
			$app_dir[$appId] = $dir;
498
			return $dir;
499
		} else {
500
			$versionToLoad = [];
501
			foreach ($possibleApps as $possibleApp) {
502
				$version = self::getAppVersionByPath($possibleApp['path']);
503
				if (empty($versionToLoad) || version_compare($version, $versionToLoad['version'], '>')) {
504
					$versionToLoad = array(
505
						'dir' => $possibleApp,
506
						'version' => $version,
507
					);
508
				}
509
			}
510
			$app_dir[$appId] = $versionToLoad['dir'];
511
			return $versionToLoad['dir'];
512
			//TODO - write test
513
		}
514
	}
515
516
	/**
517
	 * Get the directory for the given app.
518
	 * If the app is defined in multiple directories, the first one is taken. (false if not found)
519
	 *
520
	 * @param string $appId
521
	 * @return string|false
522
	 */
523
	public static function getAppPath(string $appId) {
524
		if ($appId === null || trim($appId) === '') {
525
			return false;
526
		}
527
528
		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...
529
			return $dir['path'] . '/' . $appId;
530
		}
531
		return false;
532
	}
533
534
	/**
535
	 * Get the path for the given app on the access
536
	 * If the app is defined in multiple directories, the first one is taken. (false if not found)
537
	 *
538
	 * @param string $appId
539
	 * @return string|false
540
	 */
541
	public static function getAppWebPath(string $appId) {
542
		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...
543
			return OC::$WEBROOT . $dir['url'] . '/' . $appId;
544
		}
545
		return false;
546
	}
547
548
	/**
549
	 * get the last version of the app from appinfo/info.xml
550
	 *
551
	 * @param string $appId
552
	 * @param bool $useCache
553
	 * @return string
554
	 * @deprecated 14.0.0 use \OC::$server->getAppManager()->getAppVersion()
555
	 */
556
	public static function getAppVersion(string $appId, bool $useCache = true): string {
557
		return \OC::$server->getAppManager()->getAppVersion($appId, $useCache);
558
	}
559
560
	/**
561
	 * get app's version based on it's path
562
	 *
563
	 * @param string $path
564
	 * @return string
565
	 */
566
	public static function getAppVersionByPath(string $path): string {
567
		$infoFile = $path . '/appinfo/info.xml';
568
		$appData = \OC::$server->getAppManager()->getAppInfo($infoFile, true);
569
		return isset($appData['version']) ? $appData['version'] : '';
570
	}
571
572
573
	/**
574
	 * Read all app metadata from the info.xml file
575
	 *
576
	 * @param string $appId id of the app or the path of the info.xml file
577
	 * @param bool $path
578
	 * @param string $lang
579
	 * @return array|null
580
	 * @note all data is read from info.xml, not just pre-defined fields
581
	 * @deprecated 14.0.0 use \OC::$server->getAppManager()->getAppInfo()
582
	 */
583
	public static function getAppInfo(string $appId, bool $path = false, string $lang = null) {
584
		return \OC::$server->getAppManager()->getAppInfo($appId, $path, $lang);
585
	}
586
587
	/**
588
	 * Returns the navigation
589
	 *
590
	 * @return array
591
	 * @deprecated 14.0.0 use \OC::$server->getNavigationManager()->getAll()
592
	 *
593
	 * This function returns an array containing all entries added. The
594
	 * entries are sorted by the key 'order' ascending. Additional to the keys
595
	 * given for each app the following keys exist:
596
	 *   - active: boolean, signals if the user is on this navigation entry
597
	 */
598
	public static function getNavigation(): array {
599
		return OC::$server->getNavigationManager()->getAll();
600
	}
601
602
	/**
603
	 * Returns the Settings Navigation
604
	 *
605
	 * @return string[]
606
	 * @deprecated 14.0.0 use \OC::$server->getNavigationManager()->getAll('settings')
607
	 *
608
	 * This function returns an array containing all settings pages added. The
609
	 * entries are sorted by the key 'order' ascending.
610
	 */
611
	public static function getSettingsNavigation(): array {
612
		return OC::$server->getNavigationManager()->getAll('settings');
613
	}
614
615
	/**
616
	 * get the id of loaded app
617
	 *
618
	 * @return string
619
	 */
620
	public static function getCurrentApp(): string {
621
		$request = \OC::$server->getRequest();
622
		$script = substr($request->getScriptName(), strlen(OC::$WEBROOT) + 1);
623
		$topFolder = substr($script, 0, strpos($script, '/') ?: 0);
624
		if (empty($topFolder)) {
625
			$path_info = $request->getPathInfo();
626
			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...
627
				$topFolder = substr($path_info, 1, strpos($path_info, '/', 1) - 1);
628
			}
629
		}
630
		if ($topFolder == 'apps') {
631
			$length = strlen($topFolder);
632
			return substr($script, $length + 1, strpos($script, '/', $length + 1) - $length - 1) ?: '';
633
		} else {
634
			return $topFolder;
635
		}
636
	}
637
638
	/**
639
	 * @param string $type
640
	 * @return array
641
	 */
642
	public static function getForms(string $type): array {
643
		$forms = [];
644
		switch ($type) {
645
			case 'admin':
646
				$source = self::$adminForms;
647
				break;
648
			case 'personal':
649
				$source = self::$personalForms;
650
				break;
651
			default:
652
				return [];
653
		}
654
		foreach ($source as $form) {
655
			$forms[] = include $form;
656
		}
657
		return $forms;
658
	}
659
660
	/**
661
	 * register an admin form to be shown
662
	 *
663
	 * @param string $app
664
	 * @param string $page
665
	 */
666
	public static function registerAdmin(string $app, string $page) {
667
		self::$adminForms[] = $app . '/' . $page . '.php';
668
	}
669
670
	/**
671
	 * register a personal form to be shown
672
	 * @param string $app
673
	 * @param string $page
674
	 */
675
	public static function registerPersonal(string $app, string $page) {
676
		self::$personalForms[] = $app . '/' . $page . '.php';
677
	}
678
679
	/**
680
	 * @param array $entry
681
	 */
682
	public static function registerLogIn(array $entry) {
683
		self::$altLogin[] = $entry;
684
	}
685
686
	/**
687
	 * @return array
688
	 */
689
	public static function getAlternativeLogIns(): array {
690
		return self::$altLogin;
691
	}
692
693
	/**
694
	 * get a list of all apps in the apps folder
695
	 *
696
	 * @return array an array of app names (string IDs)
697
	 * @todo: change the name of this method to getInstalledApps, which is more accurate
698
	 */
699
	public static function getAllApps(): array {
700
701
		$apps = [];
702
703
		foreach (OC::$APPSROOTS as $apps_dir) {
704
			if (!is_readable($apps_dir['path'])) {
705
				\OCP\Util::writeLog('core', 'unable to read app folder : ' . $apps_dir['path'], \OCP\Util::WARN);
706
				continue;
707
			}
708
			$dh = opendir($apps_dir['path']);
709
710
			if (is_resource($dh)) {
711
				while (($file = readdir($dh)) !== false) {
712
713
					if ($file[0] != '.' and is_dir($apps_dir['path'] . '/' . $file) and is_file($apps_dir['path'] . '/' . $file . '/appinfo/info.xml')) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as and instead of && is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
714
715
						$apps[] = $file;
716
					}
717
				}
718
			}
719
		}
720
721
		$apps = array_unique($apps);
722
723
		return $apps;
724
	}
725
726
	/**
727
	 * List all apps, this is used in apps.php
728
	 *
729
	 * @return array
730
	 */
731
	public function listAllApps(): array {
732
		$installedApps = OC_App::getAllApps();
733
734
		$appManager = \OC::$server->getAppManager();
735
		//we don't want to show configuration for these
736
		$blacklist = $appManager->getAlwaysEnabledApps();
737
		$appList = [];
738
		$langCode = \OC::$server->getL10N('core')->getLanguageCode();
739
		$urlGenerator = \OC::$server->getURLGenerator();
740
741
		foreach ($installedApps as $app) {
742
			if (array_search($app, $blacklist) === false) {
743
744
				$info = OC_App::getAppInfo($app, false, $langCode);
745
				if (!is_array($info)) {
746
					\OCP\Util::writeLog('core', 'Could not read app info file for app "' . $app . '"', \OCP\Util::ERROR);
747
					continue;
748
				}
749
750
				if (!isset($info['name'])) {
751
					\OCP\Util::writeLog('core', 'App id "' . $app . '" has no name in appinfo', \OCP\Util::ERROR);
752
					continue;
753
				}
754
755
				$enabled = \OC::$server->getConfig()->getAppValue($app, 'enabled', 'no');
756
				$info['groups'] = null;
757
				if ($enabled === 'yes') {
758
					$active = true;
759
				} else if ($enabled === 'no') {
760
					$active = false;
761
				} else {
762
					$active = true;
763
					$info['groups'] = $enabled;
764
				}
765
766
				$info['active'] = $active;
767
768
				if ($appManager->isShipped($app)) {
769
					$info['internal'] = true;
770
					$info['level'] = self::officialApp;
771
					$info['removable'] = false;
772
				} else {
773
					$info['internal'] = false;
774
					$info['removable'] = true;
775
				}
776
777
				$appPath = self::getAppPath($app);
778
				if($appPath !== false) {
779
					$appIcon = $appPath . '/img/' . $app . '.svg';
780
					if (file_exists($appIcon)) {
781
						$info['preview'] = $urlGenerator->imagePath($app, $app . '.svg');
782
						$info['previewAsIcon'] = true;
783
					} else {
784
						$appIcon = $appPath . '/img/app.svg';
785
						if (file_exists($appIcon)) {
786
							$info['preview'] = $urlGenerator->imagePath($app, 'app.svg');
787
							$info['previewAsIcon'] = true;
788
						}
789
					}
790
				}
791
				// fix documentation
792
				if (isset($info['documentation']) && is_array($info['documentation'])) {
793
					foreach ($info['documentation'] as $key => $url) {
794
						// If it is not an absolute URL we assume it is a key
795
						// i.e. admin-ldap will get converted to go.php?to=admin-ldap
796
						if (stripos($url, 'https://') !== 0 && stripos($url, 'http://') !== 0) {
797
							$url = $urlGenerator->linkToDocs($url);
798
						}
799
800
						$info['documentation'][$key] = $url;
801
					}
802
				}
803
804
				$info['version'] = OC_App::getAppVersion($app);
805
				$appList[] = $info;
806
			}
807
		}
808
809
		return $appList;
810
	}
811
812
	public static function shouldUpgrade(string $app): bool {
813
		$versions = self::getAppVersions();
814
		$currentVersion = OC_App::getAppVersion($app);
815
		if ($currentVersion && isset($versions[$app])) {
816
			$installedVersion = $versions[$app];
817
			if (!version_compare($currentVersion, $installedVersion, '=')) {
818
				return true;
819
			}
820
		}
821
		return false;
822
	}
823
824
	/**
825
	 * Adjust the number of version parts of $version1 to match
826
	 * the number of version parts of $version2.
827
	 *
828
	 * @param string $version1 version to adjust
829
	 * @param string $version2 version to take the number of parts from
830
	 * @return string shortened $version1
831
	 */
832
	private static function adjustVersionParts(string $version1, string $version2): string {
833
		$version1 = explode('.', $version1);
834
		$version2 = explode('.', $version2);
835
		// reduce $version1 to match the number of parts in $version2
836
		while (count($version1) > count($version2)) {
837
			array_pop($version1);
838
		}
839
		// if $version1 does not have enough parts, add some
840
		while (count($version1) < count($version2)) {
841
			$version1[] = '0';
842
		}
843
		return implode('.', $version1);
844
	}
845
846
	/**
847
	 * Check whether the current ownCloud version matches the given
848
	 * application's version requirements.
849
	 *
850
	 * The comparison is made based on the number of parts that the
851
	 * app info version has. For example for ownCloud 6.0.3 if the
852
	 * app info version is expecting version 6.0, the comparison is
853
	 * made on the first two parts of the ownCloud version.
854
	 * This means that it's possible to specify "requiremin" => 6
855
	 * and "requiremax" => 6 and it will still match ownCloud 6.0.3.
856
	 *
857
	 * @param string $ocVersion ownCloud version to check against
858
	 * @param array $appInfo app info (from xml)
859
	 *
860
	 * @return boolean true if compatible, otherwise false
861
	 */
862
	public static function isAppCompatible(string $ocVersion, array $appInfo): bool {
863
		$requireMin = '';
864
		$requireMax = '';
865
		if (isset($appInfo['dependencies']['nextcloud']['@attributes']['min-version'])) {
866
			$requireMin = $appInfo['dependencies']['nextcloud']['@attributes']['min-version'];
867 View Code Duplication
		} elseif (isset($appInfo['dependencies']['owncloud']['@attributes']['min-version'])) {
868
			$requireMin = $appInfo['dependencies']['owncloud']['@attributes']['min-version'];
869
		} else if (isset($appInfo['requiremin'])) {
870
			$requireMin = $appInfo['requiremin'];
871
		} else if (isset($appInfo['require'])) {
872
			$requireMin = $appInfo['require'];
873
		}
874
875
		if (isset($appInfo['dependencies']['nextcloud']['@attributes']['max-version'])) {
876
			$requireMax = $appInfo['dependencies']['nextcloud']['@attributes']['max-version'];
877 View Code Duplication
		} elseif (isset($appInfo['dependencies']['owncloud']['@attributes']['max-version'])) {
878
			$requireMax = $appInfo['dependencies']['owncloud']['@attributes']['max-version'];
879
		} else if (isset($appInfo['requiremax'])) {
880
			$requireMax = $appInfo['requiremax'];
881
		}
882
883 View Code Duplication
		if (!empty($requireMin)
884
			&& version_compare(self::adjustVersionParts($ocVersion, $requireMin), $requireMin, '<')
885
		) {
886
887
			return false;
888
		}
889
890 View Code Duplication
		if (!empty($requireMax)
891
			&& version_compare(self::adjustVersionParts($ocVersion, $requireMax), $requireMax, '>')
892
		) {
893
			return false;
894
		}
895
896
		return true;
897
	}
898
899
	/**
900
	 * get the installed version of all apps
901
	 */
902
	public static function getAppVersions() {
903
		static $versions;
904
905
		if(!$versions) {
906
			$appConfig = \OC::$server->getAppConfig();
907
			$versions = $appConfig->getValues(false, 'installed_version');
908
		}
909
		return $versions;
910
	}
911
912
	/**
913
	 * update the database for the app and call the update script
914
	 *
915
	 * @param string $appId
916
	 * @return bool
917
	 */
918
	public static function updateApp(sstring $appId): bool {
919
		$appPath = self::getAppPath($appId);
920
		if($appPath === false) {
921
			return false;
922
		}
923
		self::registerAutoloading($appId, $appPath);
924
925
		$appData = self::getAppInfo($appId);
926
		self::executeRepairSteps($appId, $appData['repair-steps']['pre-migration']);
927
928
		if (file_exists($appPath . '/appinfo/database.xml')) {
929
			OC_DB::updateDbFromStructure($appPath . '/appinfo/database.xml');
930
		} else {
931
			$ms = new MigrationService($appId, \OC::$server->getDatabaseConnection());
932
			$ms->migrate();
933
		}
934
935
		self::executeRepairSteps($appId, $appData['repair-steps']['post-migration']);
936
		self::setupLiveMigrations($appId, $appData['repair-steps']['live-migration']);
937
		// update appversion in app manager
938
		\OC::$server->getAppManager()->getAppVersion($appId, false);
939
940
		// run upgrade code
941
		if (file_exists($appPath . '/appinfo/update.php')) {
942
			self::loadApp($appId);
943
			include $appPath . '/appinfo/update.php';
944
		}
945
		self::setupBackgroundJobs($appData['background-jobs']);
946
947
		//set remote/public handlers
948
		if (array_key_exists('ocsid', $appData)) {
949
			\OC::$server->getConfig()->setAppValue($appId, 'ocsid', $appData['ocsid']);
950 View Code Duplication
		} elseif(\OC::$server->getConfig()->getAppValue($appId, 'ocsid', null) !== null) {
951
			\OC::$server->getConfig()->deleteAppValue($appId, 'ocsid');
952
		}
953
		foreach ($appData['remote'] as $name => $path) {
954
			\OC::$server->getConfig()->setAppValue('core', 'remote_' . $name, $appId . '/' . $path);
955
		}
956 View Code Duplication
		foreach ($appData['public'] as $name => $path) {
957
			\OC::$server->getConfig()->setAppValue('core', 'public_' . $name, $appId . '/' . $path);
958
		}
959
960
		self::setAppTypes($appId);
961
962
		$version = \OC_App::getAppVersion($appId);
963
		\OC::$server->getConfig()->setAppValue($appId, 'installed_version', $version);
964
965
		\OC::$server->getEventDispatcher()->dispatch(ManagerEvent::EVENT_APP_UPDATE, new ManagerEvent(
966
			ManagerEvent::EVENT_APP_UPDATE, $appId
967
		));
968
969
		return true;
970
	}
971
972
	/**
973
	 * @param string $appId
974
	 * @param string[] $steps
975
	 * @throws \OC\NeedsUpdateException
976
	 */
977
	public static function executeRepairSteps(string $appId, array $steps) {
978
		if (empty($steps)) {
979
			return;
980
		}
981
		// load the app
982
		self::loadApp($appId);
983
984
		$dispatcher = OC::$server->getEventDispatcher();
985
986
		// load the steps
987
		$r = new Repair([], $dispatcher);
988
		foreach ($steps as $step) {
989
			try {
990
				$r->addStep($step);
991
			} catch (Exception $ex) {
992
				$r->emit('\OC\Repair', 'error', [$ex->getMessage()]);
993
				\OC::$server->getLogger()->logException($ex);
994
			}
995
		}
996
		// run the steps
997
		$r->run();
998
	}
999
1000
	public static function setupBackgroundJobs(array $jobs) {
1001
		$queue = \OC::$server->getJobList();
1002
		foreach ($jobs as $job) {
1003
			$queue->add($job);
1004
		}
1005
	}
1006
1007
	/**
1008
	 * @param string $appId
1009
	 * @param string[] $steps
1010
	 */
1011
	private static function setupLiveMigrations(string $appId, array $steps) {
1012
		$queue = \OC::$server->getJobList();
1013
		foreach ($steps as $step) {
1014
			$queue->add('OC\Migration\BackgroundRepair', [
1015
				'app' => $appId,
1016
				'step' => $step]);
1017
		}
1018
	}
1019
1020
	/**
1021
	 * @param string $appId
1022
	 * @return \OC\Files\View|false
1023
	 */
1024
	public static function getStorage(string $appId) {
1025
		if (\OC::$server->getAppManager()->isEnabledForUser($appId)) { //sanity check
1026
			if (\OC::$server->getUserSession()->isLoggedIn()) {
1027
				$view = new \OC\Files\View('/' . OC_User::getUser());
1028
				if (!$view->file_exists($appId)) {
1029
					$view->mkdir($appId);
1030
				}
1031
				return new \OC\Files\View('/' . OC_User::getUser() . '/' . $appId);
1032
			} else {
1033
				\OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ', user not logged in', \OCP\Util::ERROR);
1034
				return false;
1035
			}
1036
		} else {
1037
			\OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ' not enabled', \OCP\Util::ERROR);
1038
			return false;
1039
		}
1040
	}
1041
1042
	protected static function findBestL10NOption(array $options, string $lang): string {
1043
		$fallback = $similarLangFallback = $englishFallback = false;
1044
1045
		$lang = strtolower($lang);
1046
		$similarLang = $lang;
1047
		if (strpos($similarLang, '_')) {
1048
			// For "de_DE" we want to find "de" and the other way around
1049
			$similarLang = substr($lang, 0, strpos($lang, '_'));
1050
		}
1051
1052
		foreach ($options as $option) {
1053
			if (is_array($option)) {
1054
				if ($fallback === false) {
1055
					$fallback = $option['@value'];
1056
				}
1057
1058
				if (!isset($option['@attributes']['lang'])) {
1059
					continue;
1060
				}
1061
1062
				$attributeLang = strtolower($option['@attributes']['lang']);
1063
				if ($attributeLang === $lang) {
1064
					return $option['@value'];
1065
				}
1066
1067
				if ($attributeLang === $similarLang) {
1068
					$similarLangFallback = $option['@value'];
1069
				} else if (strpos($attributeLang, $similarLang . '_') === 0) {
1070
					if ($similarLangFallback === false) {
1071
						$similarLangFallback =  $option['@value'];
1072
					}
1073
				}
1074
			} else {
1075
				$englishFallback = $option;
1076
			}
1077
		}
1078
1079
		if ($similarLangFallback !== false) {
1080
			return $similarLangFallback;
1081
		} else if ($englishFallback !== false) {
1082
			return $englishFallback;
1083
		}
1084
		return (string) $fallback;
1085
	}
1086
1087
	/**
1088
	 * parses the app data array and enhanced the 'description' value
1089
	 *
1090
	 * @param array $data the app data
1091
	 * @param string $lang
1092
	 * @return array improved app data
1093
	 */
1094
	public static function parseAppInfo(array $data, $lang = null): array {
1095
1096 View Code Duplication
		if ($lang && isset($data['name']) && is_array($data['name'])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $lang of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null 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...
1097
			$data['name'] = self::findBestL10NOption($data['name'], $lang);
1098
		}
1099 View Code Duplication
		if ($lang && isset($data['summary']) && is_array($data['summary'])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $lang of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null 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...
1100
			$data['summary'] = self::findBestL10NOption($data['summary'], $lang);
1101
		}
1102
		if ($lang && isset($data['description']) && is_array($data['description'])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $lang of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null 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...
1103
			$data['description'] = trim(self::findBestL10NOption($data['description'], $lang));
1104
		} else if (isset($data['description']) && is_string($data['description'])) {
1105
			$data['description'] = trim($data['description']);
1106
		} else  {
1107
			$data['description'] = '';
1108
		}
1109
1110
		return $data;
1111
	}
1112
1113
	/**
1114
	 * @param \OCP\IConfig $config
1115
	 * @param \OCP\IL10N $l
1116
	 * @param array $info
1117
	 * @throws \Exception
1118
	 */
1119
	public static function checkAppDependencies(\OCP\IConfig $config, \OCP\IL10N $l, array $info) {
1120
		$dependencyAnalyzer = new DependencyAnalyzer(new Platform($config), $l);
1121
		$missing = $dependencyAnalyzer->analyze($info);
1122
		if (!empty($missing)) {
1123
			$missingMsg = implode(PHP_EOL, $missing);
1124
			throw new \Exception(
1125
				$l->t('App "%s" cannot be installed because the following dependencies are not fulfilled: %s',
1126
					[$info['name'], $missingMsg]
1127
				)
1128
			);
1129
		}
1130
	}
1131
}
1132