Completed
Push — master ( d0de8e...846b0d )
by Morris
65:39 queued 44:01
created

OC_App::getNavigation()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
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
					\OC::$server->getAppManager()->disableApp($app);
158
				}
159
			}
160
			\OC::$server->getEventLogger()->end('load_app_' . $app);
161
		}
162
163
		$info = self::getAppInfo($app);
164 View Code Duplication
		if (!empty($info['activity']['filters'])) {
165
			foreach ($info['activity']['filters'] as $filter) {
166
				\OC::$server->getActivityManager()->registerFilter($filter);
167
			}
168
		}
169 View Code Duplication
		if (!empty($info['activity']['settings'])) {
170
			foreach ($info['activity']['settings'] as $setting) {
171
				\OC::$server->getActivityManager()->registerSetting($setting);
172
			}
173
		}
174 View Code Duplication
		if (!empty($info['activity']['providers'])) {
175
			foreach ($info['activity']['providers'] as $provider) {
176
				\OC::$server->getActivityManager()->registerProvider($provider);
177
			}
178
		}
179
180 View Code Duplication
		if (!empty($info['settings']['admin'])) {
181
			foreach ($info['settings']['admin'] as $setting) {
182
				\OC::$server->getSettingsManager()->registerSetting('admin', $setting);
183
			}
184
		}
185
		if (!empty($info['settings']['admin-section'])) {
186
			foreach ($info['settings']['admin-section'] as $section) {
187
				\OC::$server->getSettingsManager()->registerSection('admin', $section);
188
			}
189
		}
190
		if (!empty($info['settings']['personal'])) {
191
			foreach ($info['settings']['personal'] as $setting) {
192
				\OC::$server->getSettingsManager()->registerSetting('personal', $setting);
193
			}
194
		}
195
		if (!empty($info['settings']['personal-section'])) {
196
			foreach ($info['settings']['personal-section'] as $section) {
197
				\OC::$server->getSettingsManager()->registerSection('personal', $section);
198
			}
199
		}
200
201
		if (!empty($info['collaboration']['plugins'])) {
202
			// deal with one or many plugin entries
203
			$plugins = isset($info['collaboration']['plugins']['plugin']['@value']) ?
204
				[$info['collaboration']['plugins']['plugin']] : $info['collaboration']['plugins']['plugin'];
205
			foreach ($plugins as $plugin) {
206
				if($plugin['@attributes']['type'] === 'collaborator-search') {
207
					$pluginInfo = [
208
						'shareType' => $plugin['@attributes']['share-type'],
209
						'class' => $plugin['@value'],
210
					];
211
					\OC::$server->getCollaboratorSearch()->registerPlugin($pluginInfo);
212
				} else if ($plugin['@attributes']['type'] === 'autocomplete-sort') {
213
					\OC::$server->getAutoCompleteManager()->registerSorter($plugin['@value']);
214
				}
215
			}
216
		}
217
	}
218
219
	/**
220
	 * @internal
221
	 * @param string $app
222
	 * @param string $path
223
	 */
224
	public static function registerAutoloading(string $app, string $path) {
225
		$key = $app . '-' . $path;
226
		if(isset(self::$alreadyRegistered[$key])) {
227
			return;
228
		}
229
230
		self::$alreadyRegistered[$key] = true;
231
232
		// Register on PSR-4 composer autoloader
233
		$appNamespace = \OC\AppFramework\App::buildAppNamespace($app);
234
		\OC::$server->registerNamespace($app, $appNamespace);
235
236
		if (file_exists($path . '/composer/autoload.php')) {
237
			require_once $path . '/composer/autoload.php';
238
		} else {
239
			\OC::$composerAutoloader->addPsr4($appNamespace . '\\', $path . '/lib/', true);
240
			// Register on legacy autoloader
241
			\OC::$loader->addValidRoot($path);
242
		}
243
244
		// Register Test namespace only when testing
245
		if (defined('PHPUNIT_RUN') || defined('CLI_TEST_RUN')) {
246
			\OC::$composerAutoloader->addPsr4($appNamespace . '\\Tests\\', $path . '/tests/', true);
247
		}
248
	}
249
250
	/**
251
	 * Load app.php from the given app
252
	 *
253
	 * @param string $app app name
254
	 * @throws Error
255
	 */
256
	private static function requireAppFile(string $app) {
257
		// encapsulated here to avoid variable scope conflicts
258
		require_once $app . '/appinfo/app.php';
259
	}
260
261
	/**
262
	 * check if an app is of a specific type
263
	 *
264
	 * @param string $app
265
	 * @param array $types
266
	 * @return bool
267
	 */
268
	public static function isType(string $app, array $types): bool {
269
		$appTypes = self::getAppTypes($app);
270
		foreach ($types as $type) {
271
			if (array_search($type, $appTypes) !== false) {
272
				return true;
273
			}
274
		}
275
		return false;
276
	}
277
278
	/**
279
	 * get the types of an app
280
	 *
281
	 * @param string $app
282
	 * @return array
283
	 */
284
	private static function getAppTypes(string $app): array {
285
		//load the cache
286
		if (count(self::$appTypes) == 0) {
287
			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...
288
		}
289
290
		if (isset(self::$appTypes[$app])) {
291
			return explode(',', self::$appTypes[$app]);
292
		}
293
294
		return [];
295
	}
296
297
	/**
298
	 * read app types from info.xml and cache them in the database
299
	 */
300
	public static function setAppTypes(string $app) {
301
		$appManager = \OC::$server->getAppManager();
302
		$appData = $appManager->getAppInfo($app);
303
		if(!is_array($appData)) {
304
			return;
305
		}
306
307
		if (isset($appData['types'])) {
308
			$appTypes = implode(',', $appData['types']);
309
		} else {
310
			$appTypes = '';
311
			$appData['types'] = [];
312
		}
313
314
		$config = \OC::$server->getConfig();
315
		$config->setAppValue($app, 'types', $appTypes);
316
317
		if ($appManager->hasProtectedAppType($appData['types'])) {
318
			$enabled = $config->getAppValue($app, 'enabled', 'yes');
319
			if ($enabled !== 'yes' && $enabled !== 'no') {
320
				$config->setAppValue($app, 'enabled', 'yes');
321
			}
322
		}
323
	}
324
325
	/**
326
	 * Returns apps enabled for the current user.
327
	 *
328
	 * @param bool $forceRefresh whether to refresh the cache
329
	 * @param bool $all whether to return apps for all users, not only the
330
	 * currently logged in one
331
	 * @return string[]
332
	 */
333
	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...
334
		if (!\OC::$server->getSystemConfig()->getValue('installed', false)) {
335
			return [];
336
		}
337
		// in incognito mode or when logged out, $user will be false,
338
		// which is also the case during an upgrade
339
		$appManager = \OC::$server->getAppManager();
340
		if ($all) {
341
			$user = null;
342
		} else {
343
			$user = \OC::$server->getUserSession()->getUser();
344
		}
345
346
		if (is_null($user)) {
347
			$apps = $appManager->getInstalledApps();
348
		} else {
349
			$apps = $appManager->getEnabledAppsForUser($user);
350
		}
351
		$apps = array_filter($apps, function ($app) {
352
			return $app !== 'files';//we add this manually
353
		});
354
		sort($apps);
355
		array_unshift($apps, 'files');
356
		return $apps;
357
	}
358
359
	/**
360
	 * checks whether or not an app is enabled
361
	 *
362
	 * @param string $app app
363
	 * @return bool
364
	 * @deprecated 13.0.0 use \OC::$server->getAppManager()->isEnabledForUser($appId)
365
	 *
366
	 * This function checks whether or not an app is enabled.
367
	 */
368
	public static function isEnabled(string $app): bool {
369
		return \OC::$server->getAppManager()->isEnabledForUser($app);
370
	}
371
372
	/**
373
	 * enables an app
374
	 *
375
	 * @param string $appId
376
	 * @param array $groups (optional) when set, only these groups will have access to the app
377
	 * @throws \Exception
378
	 * @return void
379
	 *
380
	 * This function set an app as enabled in appconfig.
381
	 */
382
	public function enable(string $appId,
383
						   array $groups = []) {
384
385
		// Check if app is already downloaded
386
		$installer = \OC::$server->query(Installer::class);
387
		$isDownloaded = $installer->isDownloaded($appId);
388
389
		if(!$isDownloaded) {
390
			$installer->downloadApp($appId);
391
		}
392
393
		$installer->installApp($appId);
394
395
		$appManager = \OC::$server->getAppManager();
396
		if ($groups !== []) {
397
			$groupManager = \OC::$server->getGroupManager();
398
			$groupsList = [];
399
			foreach ($groups as $group) {
400
				$groupItem = $groupManager->get($group);
401
				if ($groupItem instanceof \OCP\IGroup) {
402
					$groupsList[] = $groupManager->get($group);
403
				}
404
			}
405
			$appManager->enableAppForGroups($appId, $groupsList);
406
		} else {
407
			$appManager->enableApp($appId);
408
		}
409
	}
410
411
	/**
412
	 * Get the path where to install apps
413
	 *
414
	 * @return string|false
415
	 */
416
	public static function getInstallPath() {
417
		if (\OC::$server->getSystemConfig()->getValue('appstoreenabled', true) == false) {
418
			return false;
419
		}
420
421
		foreach (OC::$APPSROOTS as $dir) {
422
			if (isset($dir['writable']) && $dir['writable'] === true) {
423
				return $dir['path'];
424
			}
425
		}
426
427
		\OCP\Util::writeLog('core', 'No application directories are marked as writable.', \OCP\Util::ERROR);
428
		return null;
429
	}
430
431
432
	/**
433
	 * search for an app in all app-directories
434
	 *
435
	 * @param string $appId
436
	 * @return false|string
437
	 */
438
	public static function findAppInDirectories(string $appId) {
439
		$sanitizedAppId = self::cleanAppId($appId);
440
		if($sanitizedAppId !== $appId) {
441
			return false;
442
		}
443
		static $app_dir = [];
444
445
		if (isset($app_dir[$appId])) {
446
			return $app_dir[$appId];
447
		}
448
449
		$possibleApps = [];
450
		foreach (OC::$APPSROOTS as $dir) {
451
			if (file_exists($dir['path'] . '/' . $appId)) {
452
				$possibleApps[] = $dir;
453
			}
454
		}
455
456
		if (empty($possibleApps)) {
457
			return false;
458
		} elseif (count($possibleApps) === 1) {
459
			$dir = array_shift($possibleApps);
460
			$app_dir[$appId] = $dir;
461
			return $dir;
462
		} else {
463
			$versionToLoad = [];
464
			foreach ($possibleApps as $possibleApp) {
465
				$version = self::getAppVersionByPath($possibleApp['path']);
466
				if (empty($versionToLoad) || version_compare($version, $versionToLoad['version'], '>')) {
467
					$versionToLoad = array(
468
						'dir' => $possibleApp,
469
						'version' => $version,
470
					);
471
				}
472
			}
473
			$app_dir[$appId] = $versionToLoad['dir'];
474
			return $versionToLoad['dir'];
475
			//TODO - write test
476
		}
477
	}
478
479
	/**
480
	 * Get the directory for the given app.
481
	 * If the app is defined in multiple directories, the first one is taken. (false if not found)
482
	 *
483
	 * @param string $appId
484
	 * @return string|false
485
	 */
486
	public static function getAppPath(string $appId) {
487
		if ($appId === null || trim($appId) === '') {
488
			return false;
489
		}
490
491
		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...
492
			return $dir['path'] . '/' . $appId;
493
		}
494
		return false;
495
	}
496
497
	/**
498
	 * Get the path for the given app on the access
499
	 * If the app is defined in multiple directories, the first one is taken. (false if not found)
500
	 *
501
	 * @param string $appId
502
	 * @return string|false
503
	 */
504
	public static function getAppWebPath(string $appId) {
505
		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...
506
			return OC::$WEBROOT . $dir['url'] . '/' . $appId;
507
		}
508
		return false;
509
	}
510
511
	/**
512
	 * get the last version of the app from appinfo/info.xml
513
	 *
514
	 * @param string $appId
515
	 * @param bool $useCache
516
	 * @return string
517
	 * @deprecated 14.0.0 use \OC::$server->getAppManager()->getAppVersion()
518
	 */
519
	public static function getAppVersion(string $appId, bool $useCache = true): string {
520
		return \OC::$server->getAppManager()->getAppVersion($appId, $useCache);
521
	}
522
523
	/**
524
	 * get app's version based on it's path
525
	 *
526
	 * @param string $path
527
	 * @return string
528
	 */
529
	public static function getAppVersionByPath(string $path): string {
530
		$infoFile = $path . '/appinfo/info.xml';
531
		$appData = \OC::$server->getAppManager()->getAppInfo($infoFile, true);
532
		return isset($appData['version']) ? $appData['version'] : '';
533
	}
534
535
536
	/**
537
	 * Read all app metadata from the info.xml file
538
	 *
539
	 * @param string $appId id of the app or the path of the info.xml file
540
	 * @param bool $path
541
	 * @param string $lang
542
	 * @return array|null
543
	 * @note all data is read from info.xml, not just pre-defined fields
544
	 * @deprecated 14.0.0 use \OC::$server->getAppManager()->getAppInfo()
545
	 */
546
	public static function getAppInfo(string $appId, bool $path = false, string $lang = null) {
547
		return \OC::$server->getAppManager()->getAppInfo($appId, $path, $lang);
548
	}
549
550
	/**
551
	 * Returns the navigation
552
	 *
553
	 * @return array
554
	 * @deprecated 14.0.0 use \OC::$server->getNavigationManager()->getAll()
555
	 *
556
	 * This function returns an array containing all entries added. The
557
	 * entries are sorted by the key 'order' ascending. Additional to the keys
558
	 * given for each app the following keys exist:
559
	 *   - active: boolean, signals if the user is on this navigation entry
560
	 */
561
	public static function getNavigation(): array {
562
		return OC::$server->getNavigationManager()->getAll();
563
	}
564
565
	/**
566
	 * Returns the Settings Navigation
567
	 *
568
	 * @return string[]
569
	 * @deprecated 14.0.0 use \OC::$server->getNavigationManager()->getAll('settings')
570
	 *
571
	 * This function returns an array containing all settings pages added. The
572
	 * entries are sorted by the key 'order' ascending.
573
	 */
574
	public static function getSettingsNavigation(): array {
575
		return OC::$server->getNavigationManager()->getAll('settings');
576
	}
577
578
	/**
579
	 * get the id of loaded app
580
	 *
581
	 * @return string
582
	 */
583
	public static function getCurrentApp(): string {
584
		$request = \OC::$server->getRequest();
585
		$script = substr($request->getScriptName(), strlen(OC::$WEBROOT) + 1);
586
		$topFolder = substr($script, 0, strpos($script, '/') ?: 0);
587
		if (empty($topFolder)) {
588
			$path_info = $request->getPathInfo();
589
			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...
590
				$topFolder = substr($path_info, 1, strpos($path_info, '/', 1) - 1);
591
			}
592
		}
593
		if ($topFolder == 'apps') {
594
			$length = strlen($topFolder);
595
			return substr($script, $length + 1, strpos($script, '/', $length + 1) - $length - 1) ?: '';
596
		} else {
597
			return $topFolder;
598
		}
599
	}
600
601
	/**
602
	 * @param string $type
603
	 * @return array
604
	 */
605
	public static function getForms(string $type): array {
606
		$forms = [];
607
		switch ($type) {
608
			case 'admin':
609
				$source = self::$adminForms;
610
				break;
611
			case 'personal':
612
				$source = self::$personalForms;
613
				break;
614
			default:
615
				return [];
616
		}
617
		foreach ($source as $form) {
618
			$forms[] = include $form;
619
		}
620
		return $forms;
621
	}
622
623
	/**
624
	 * register an admin form to be shown
625
	 *
626
	 * @param string $app
627
	 * @param string $page
628
	 */
629
	public static function registerAdmin(string $app, string $page) {
630
		self::$adminForms[] = $app . '/' . $page . '.php';
631
	}
632
633
	/**
634
	 * register a personal form to be shown
635
	 * @param string $app
636
	 * @param string $page
637
	 */
638
	public static function registerPersonal(string $app, string $page) {
639
		self::$personalForms[] = $app . '/' . $page . '.php';
640
	}
641
642
	/**
643
	 * @param array $entry
644
	 */
645
	public static function registerLogIn(array $entry) {
646
		self::$altLogin[] = $entry;
647
	}
648
649
	/**
650
	 * @return array
651
	 */
652
	public static function getAlternativeLogIns(): array {
653
		return self::$altLogin;
654
	}
655
656
	/**
657
	 * get a list of all apps in the apps folder
658
	 *
659
	 * @return array an array of app names (string IDs)
660
	 * @todo: change the name of this method to getInstalledApps, which is more accurate
661
	 */
662
	public static function getAllApps(): array {
663
664
		$apps = [];
665
666
		foreach (OC::$APPSROOTS as $apps_dir) {
667
			if (!is_readable($apps_dir['path'])) {
668
				\OCP\Util::writeLog('core', 'unable to read app folder : ' . $apps_dir['path'], \OCP\Util::WARN);
669
				continue;
670
			}
671
			$dh = opendir($apps_dir['path']);
672
673
			if (is_resource($dh)) {
674
				while (($file = readdir($dh)) !== false) {
675
676
					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...
677
678
						$apps[] = $file;
679
					}
680
				}
681
			}
682
		}
683
684
		$apps = array_unique($apps);
685
686
		return $apps;
687
	}
688
689
	/**
690
	 * List all apps, this is used in apps.php
691
	 *
692
	 * @return array
693
	 */
694
	public function listAllApps(): array {
695
		$installedApps = OC_App::getAllApps();
696
697
		$appManager = \OC::$server->getAppManager();
698
		//we don't want to show configuration for these
699
		$blacklist = $appManager->getAlwaysEnabledApps();
700
		$appList = [];
701
		$langCode = \OC::$server->getL10N('core')->getLanguageCode();
702
		$urlGenerator = \OC::$server->getURLGenerator();
703
704
		foreach ($installedApps as $app) {
705
			if (array_search($app, $blacklist) === false) {
706
707
				$info = OC_App::getAppInfo($app, false, $langCode);
708
				if (!is_array($info)) {
709
					\OCP\Util::writeLog('core', 'Could not read app info file for app "' . $app . '"', \OCP\Util::ERROR);
710
					continue;
711
				}
712
713
				if (!isset($info['name'])) {
714
					\OCP\Util::writeLog('core', 'App id "' . $app . '" has no name in appinfo', \OCP\Util::ERROR);
715
					continue;
716
				}
717
718
				$enabled = \OC::$server->getConfig()->getAppValue($app, 'enabled', 'no');
719
				$info['groups'] = null;
720
				if ($enabled === 'yes') {
721
					$active = true;
722
				} else if ($enabled === 'no') {
723
					$active = false;
724
				} else {
725
					$active = true;
726
					$info['groups'] = $enabled;
727
				}
728
729
				$info['active'] = $active;
730
731
				if ($appManager->isShipped($app)) {
732
					$info['internal'] = true;
733
					$info['level'] = self::officialApp;
734
					$info['removable'] = false;
735
				} else {
736
					$info['internal'] = false;
737
					$info['removable'] = true;
738
				}
739
740
				$appPath = self::getAppPath($app);
741
				if($appPath !== false) {
742
					$appIcon = $appPath . '/img/' . $app . '.svg';
743
					if (file_exists($appIcon)) {
744
						$info['preview'] = $urlGenerator->imagePath($app, $app . '.svg');
745
						$info['previewAsIcon'] = true;
746
					} else {
747
						$appIcon = $appPath . '/img/app.svg';
748
						if (file_exists($appIcon)) {
749
							$info['preview'] = $urlGenerator->imagePath($app, 'app.svg');
750
							$info['previewAsIcon'] = true;
751
						}
752
					}
753
				}
754
				// fix documentation
755
				if (isset($info['documentation']) && is_array($info['documentation'])) {
756
					foreach ($info['documentation'] as $key => $url) {
757
						// If it is not an absolute URL we assume it is a key
758
						// i.e. admin-ldap will get converted to go.php?to=admin-ldap
759
						if (stripos($url, 'https://') !== 0 && stripos($url, 'http://') !== 0) {
760
							$url = $urlGenerator->linkToDocs($url);
761
						}
762
763
						$info['documentation'][$key] = $url;
764
					}
765
				}
766
767
				$info['version'] = OC_App::getAppVersion($app);
768
				$appList[] = $info;
769
			}
770
		}
771
772
		return $appList;
773
	}
774
775
	public static function shouldUpgrade(string $app): bool {
776
		$versions = self::getAppVersions();
777
		$currentVersion = OC_App::getAppVersion($app);
778
		if ($currentVersion && isset($versions[$app])) {
779
			$installedVersion = $versions[$app];
780
			if (!version_compare($currentVersion, $installedVersion, '=')) {
781
				return true;
782
			}
783
		}
784
		return false;
785
	}
786
787
	/**
788
	 * Adjust the number of version parts of $version1 to match
789
	 * the number of version parts of $version2.
790
	 *
791
	 * @param string $version1 version to adjust
792
	 * @param string $version2 version to take the number of parts from
793
	 * @return string shortened $version1
794
	 */
795
	private static function adjustVersionParts(string $version1, string $version2): string {
796
		$version1 = explode('.', $version1);
797
		$version2 = explode('.', $version2);
798
		// reduce $version1 to match the number of parts in $version2
799
		while (count($version1) > count($version2)) {
800
			array_pop($version1);
801
		}
802
		// if $version1 does not have enough parts, add some
803
		while (count($version1) < count($version2)) {
804
			$version1[] = '0';
805
		}
806
		return implode('.', $version1);
807
	}
808
809
	/**
810
	 * Check whether the current ownCloud version matches the given
811
	 * application's version requirements.
812
	 *
813
	 * The comparison is made based on the number of parts that the
814
	 * app info version has. For example for ownCloud 6.0.3 if the
815
	 * app info version is expecting version 6.0, the comparison is
816
	 * made on the first two parts of the ownCloud version.
817
	 * This means that it's possible to specify "requiremin" => 6
818
	 * and "requiremax" => 6 and it will still match ownCloud 6.0.3.
819
	 *
820
	 * @param string $ocVersion ownCloud version to check against
821
	 * @param array $appInfo app info (from xml)
822
	 *
823
	 * @return boolean true if compatible, otherwise false
824
	 */
825
	public static function isAppCompatible(string $ocVersion, array $appInfo): bool {
826
		$requireMin = '';
827
		$requireMax = '';
828
		if (isset($appInfo['dependencies']['nextcloud']['@attributes']['min-version'])) {
829
			$requireMin = $appInfo['dependencies']['nextcloud']['@attributes']['min-version'];
830 View Code Duplication
		} elseif (isset($appInfo['dependencies']['owncloud']['@attributes']['min-version'])) {
831
			$requireMin = $appInfo['dependencies']['owncloud']['@attributes']['min-version'];
832
		} else if (isset($appInfo['requiremin'])) {
833
			$requireMin = $appInfo['requiremin'];
834
		} else if (isset($appInfo['require'])) {
835
			$requireMin = $appInfo['require'];
836
		}
837
838
		if (isset($appInfo['dependencies']['nextcloud']['@attributes']['max-version'])) {
839
			$requireMax = $appInfo['dependencies']['nextcloud']['@attributes']['max-version'];
840 View Code Duplication
		} elseif (isset($appInfo['dependencies']['owncloud']['@attributes']['max-version'])) {
841
			$requireMax = $appInfo['dependencies']['owncloud']['@attributes']['max-version'];
842
		} else if (isset($appInfo['requiremax'])) {
843
			$requireMax = $appInfo['requiremax'];
844
		}
845
846 View Code Duplication
		if (!empty($requireMin)
847
			&& version_compare(self::adjustVersionParts($ocVersion, $requireMin), $requireMin, '<')
848
		) {
849
850
			return false;
851
		}
852
853 View Code Duplication
		if (!empty($requireMax)
854
			&& version_compare(self::adjustVersionParts($ocVersion, $requireMax), $requireMax, '>')
855
		) {
856
			return false;
857
		}
858
859
		return true;
860
	}
861
862
	/**
863
	 * get the installed version of all apps
864
	 */
865
	public static function getAppVersions() {
866
		static $versions;
867
868
		if(!$versions) {
869
			$appConfig = \OC::$server->getAppConfig();
870
			$versions = $appConfig->getValues(false, 'installed_version');
871
		}
872
		return $versions;
873
	}
874
875
	/**
876
	 * update the database for the app and call the update script
877
	 *
878
	 * @param string $appId
879
	 * @return bool
880
	 */
881
	public static function updateApp(string $appId): bool {
882
		$appPath = self::getAppPath($appId);
883
		if($appPath === false) {
884
			return false;
885
		}
886
		self::registerAutoloading($appId, $appPath);
887
888
		$appData = self::getAppInfo($appId);
889
		self::executeRepairSteps($appId, $appData['repair-steps']['pre-migration']);
890
891
		if (file_exists($appPath . '/appinfo/database.xml')) {
892
			OC_DB::updateDbFromStructure($appPath . '/appinfo/database.xml');
893
		} else {
894
			$ms = new MigrationService($appId, \OC::$server->getDatabaseConnection());
895
			$ms->migrate();
896
		}
897
898
		self::executeRepairSteps($appId, $appData['repair-steps']['post-migration']);
899
		self::setupLiveMigrations($appId, $appData['repair-steps']['live-migration']);
900
		// update appversion in app manager
901
		\OC::$server->getAppManager()->getAppVersion($appId, false);
902
903
		// run upgrade code
904
		if (file_exists($appPath . '/appinfo/update.php')) {
905
			self::loadApp($appId);
906
			include $appPath . '/appinfo/update.php';
907
		}
908
		self::setupBackgroundJobs($appData['background-jobs']);
909
910
		//set remote/public handlers
911
		if (array_key_exists('ocsid', $appData)) {
912
			\OC::$server->getConfig()->setAppValue($appId, 'ocsid', $appData['ocsid']);
913 View Code Duplication
		} elseif(\OC::$server->getConfig()->getAppValue($appId, 'ocsid', null) !== null) {
914
			\OC::$server->getConfig()->deleteAppValue($appId, 'ocsid');
915
		}
916
		foreach ($appData['remote'] as $name => $path) {
917
			\OC::$server->getConfig()->setAppValue('core', 'remote_' . $name, $appId . '/' . $path);
918
		}
919 View Code Duplication
		foreach ($appData['public'] as $name => $path) {
920
			\OC::$server->getConfig()->setAppValue('core', 'public_' . $name, $appId . '/' . $path);
921
		}
922
923
		self::setAppTypes($appId);
924
925
		$version = \OC_App::getAppVersion($appId);
926
		\OC::$server->getConfig()->setAppValue($appId, 'installed_version', $version);
927
928
		\OC::$server->getEventDispatcher()->dispatch(ManagerEvent::EVENT_APP_UPDATE, new ManagerEvent(
929
			ManagerEvent::EVENT_APP_UPDATE, $appId
930
		));
931
932
		return true;
933
	}
934
935
	/**
936
	 * @param string $appId
937
	 * @param string[] $steps
938
	 * @throws \OC\NeedsUpdateException
939
	 */
940
	public static function executeRepairSteps(string $appId, array $steps) {
941
		if (empty($steps)) {
942
			return;
943
		}
944
		// load the app
945
		self::loadApp($appId);
946
947
		$dispatcher = OC::$server->getEventDispatcher();
948
949
		// load the steps
950
		$r = new Repair([], $dispatcher);
951
		foreach ($steps as $step) {
952
			try {
953
				$r->addStep($step);
954
			} catch (Exception $ex) {
955
				$r->emit('\OC\Repair', 'error', [$ex->getMessage()]);
956
				\OC::$server->getLogger()->logException($ex);
0 ignored issues
show
Documentation introduced by
$ex is of type object<Exception>, but the function expects a object<Throwable>.

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...
957
			}
958
		}
959
		// run the steps
960
		$r->run();
961
	}
962
963
	public static function setupBackgroundJobs(array $jobs) {
964
		$queue = \OC::$server->getJobList();
965
		foreach ($jobs as $job) {
966
			$queue->add($job);
967
		}
968
	}
969
970
	/**
971
	 * @param string $appId
972
	 * @param string[] $steps
973
	 */
974
	private static function setupLiveMigrations(string $appId, array $steps) {
975
		$queue = \OC::$server->getJobList();
976
		foreach ($steps as $step) {
977
			$queue->add('OC\Migration\BackgroundRepair', [
978
				'app' => $appId,
979
				'step' => $step]);
980
		}
981
	}
982
983
	/**
984
	 * @param string $appId
985
	 * @return \OC\Files\View|false
986
	 */
987
	public static function getStorage(string $appId) {
988
		if (\OC::$server->getAppManager()->isEnabledForUser($appId)) { //sanity check
989
			if (\OC::$server->getUserSession()->isLoggedIn()) {
990
				$view = new \OC\Files\View('/' . OC_User::getUser());
991
				if (!$view->file_exists($appId)) {
992
					$view->mkdir($appId);
993
				}
994
				return new \OC\Files\View('/' . OC_User::getUser() . '/' . $appId);
995
			} else {
996
				\OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ', user not logged in', \OCP\Util::ERROR);
997
				return false;
998
			}
999
		} else {
1000
			\OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ' not enabled', \OCP\Util::ERROR);
1001
			return false;
1002
		}
1003
	}
1004
1005
	protected static function findBestL10NOption(array $options, string $lang): string {
1006
		$fallback = $similarLangFallback = $englishFallback = false;
1007
1008
		$lang = strtolower($lang);
1009
		$similarLang = $lang;
1010
		if (strpos($similarLang, '_')) {
1011
			// For "de_DE" we want to find "de" and the other way around
1012
			$similarLang = substr($lang, 0, strpos($lang, '_'));
1013
		}
1014
1015
		foreach ($options as $option) {
1016
			if (is_array($option)) {
1017
				if ($fallback === false) {
1018
					$fallback = $option['@value'];
1019
				}
1020
1021
				if (!isset($option['@attributes']['lang'])) {
1022
					continue;
1023
				}
1024
1025
				$attributeLang = strtolower($option['@attributes']['lang']);
1026
				if ($attributeLang === $lang) {
1027
					return $option['@value'];
1028
				}
1029
1030
				if ($attributeLang === $similarLang) {
1031
					$similarLangFallback = $option['@value'];
1032
				} else if (strpos($attributeLang, $similarLang . '_') === 0) {
1033
					if ($similarLangFallback === false) {
1034
						$similarLangFallback =  $option['@value'];
1035
					}
1036
				}
1037
			} else {
1038
				$englishFallback = $option;
1039
			}
1040
		}
1041
1042
		if ($similarLangFallback !== false) {
1043
			return $similarLangFallback;
1044
		} else if ($englishFallback !== false) {
1045
			return $englishFallback;
1046
		}
1047
		return (string) $fallback;
1048
	}
1049
1050
	/**
1051
	 * parses the app data array and enhanced the 'description' value
1052
	 *
1053
	 * @param array $data the app data
1054
	 * @param string $lang
1055
	 * @return array improved app data
1056
	 */
1057
	public static function parseAppInfo(array $data, $lang = null): array {
1058
1059 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...
1060
			$data['name'] = self::findBestL10NOption($data['name'], $lang);
1061
		}
1062 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...
1063
			$data['summary'] = self::findBestL10NOption($data['summary'], $lang);
1064
		}
1065
		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...
1066
			$data['description'] = trim(self::findBestL10NOption($data['description'], $lang));
1067
		} else if (isset($data['description']) && is_string($data['description'])) {
1068
			$data['description'] = trim($data['description']);
1069
		} else  {
1070
			$data['description'] = '';
1071
		}
1072
1073
		return $data;
1074
	}
1075
1076
	/**
1077
	 * @param \OCP\IConfig $config
1078
	 * @param \OCP\IL10N $l
1079
	 * @param array $info
1080
	 * @throws \Exception
1081
	 */
1082
	public static function checkAppDependencies(\OCP\IConfig $config, \OCP\IL10N $l, array $info) {
1083
		$dependencyAnalyzer = new DependencyAnalyzer(new Platform($config), $l);
1084
		$missing = $dependencyAnalyzer->analyze($info);
1085
		if (!empty($missing)) {
1086
			$missingMsg = implode(PHP_EOL, $missing);
1087
			throw new \Exception(
1088
				$l->t('App "%s" cannot be installed because the following dependencies are not fulfilled: %s',
1089
					[$info['name'], $missingMsg]
1090
				)
1091
			);
1092
		}
1093
	}
1094
}
1095