Completed
Pull Request — master (#30710)
by Thomas
10:21
created

OC_App::proceedNavigation()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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

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

Loading history...
341
		if (!\OC::$server->getSystemConfig()->getValue('installed', false)) {
342
			return [];
343
		}
344
		// in incognito mode or when logged out, $user will be false,
345
		// which is also the case during an upgrade
346
		$appManager = \OC::$server->getAppManager();
347
		if ($all) {
348
			$user = null;
349
		} else {
350
			$user = \OC::$server->getUserSession()->getUser();
351
		}
352
353
		if (is_null($user)) {
354
			$apps = $appManager->getInstalledApps();
355
		} else {
356
			$apps = $appManager->getEnabledAppsForUser($user);
357
		}
358
		$apps = array_filter($apps, function ($app) {
359
			return $app !== 'files';//we add this manually
360
		});
361
		sort($apps);
362
		array_unshift($apps, 'files');
363
		return $apps;
364
	}
365
366
	/**
367
	 * checks whether or not an app is enabled
368
	 *
369
	 * @param string $app app
370
	 * @return bool
371
	 *
372
	 * This function checks whether or not an app is enabled.
373
	 */
374
	public static function isEnabled($app) {
375
		return \OC::$server->getAppManager()->isEnabledForUser($app);
376
	}
377
378
	/**
379
	 * enables an app
380
	 *
381
	 * @param mixed $app app
382
	 * @param array $groups (optional) when set, only these groups will have access to the app
383
	 * @throws \Exception
384
	 * @return void
385
	 *
386
	 * This function set an app as enabled in appconfig.
387
	 */
388
	public static function enable($app, $groups = null) {
389
		self::$enabledAppsCache = []; // flush
390
391
		// check for required dependencies
392
		$config = \OC::$server->getConfig();
393
		$l = \OC::$server->getL10N('core');
394
		$info = self::getAppInfo($app);
395
		if ($info === null) {
396
			throw new \Exception("$app can't be enabled since it is not installed.");
397
		}
398
399
		self::checkAppDependencies($config, $l, $info);
400
401
		if (!Installer::isInstalled($app)) {
402
			Installer::installShippedApp($app);
403
		}
404
405
		$appManager = \OC::$server->getAppManager();
406
		if (!is_null($groups)) {
407
			$groupManager = \OC::$server->getGroupManager();
408
			$groupsList = [];
409
			foreach ($groups as $group) {
410
				$groupItem = $groupManager->get($group);
411
				if ($groupItem instanceof \OCP\IGroup) {
412
					$groupsList[] = $groupManager->get($group);
413
				}
414
			}
415
			$appManager->enableAppForGroups($app, $groupsList);
416
		} else {
417
			$appManager->enableApp($app);
418
		}
419
	}
420
421
	/**
422
	 * @param string $app
423
	 * @return bool
424
	 */
425
	public static function removeApp($app) {
426
		if (self::isShipped($app)) {
427
			return false;
428
		}
429
430
		return Installer::removeApp($app);
431
	}
432
433
	/**
434
	 * This function set an app as disabled in appconfig.
435
	 *
436
	 * @param string $app app
437
	 * @throws Exception
438
	 */
439
	public static function disable($app) {
440
		// Convert OCS ID to regular application identifier
441
		if(self::getInternalAppIdByOcs($app) !== false) {
442
			$app = self::getInternalAppIdByOcs($app);
443
		}
444
445
		// flush
446
		self::$enabledAppsCache = [];
447
448
		// run uninstall steps
449
		$appData = OC_App::getAppInfo($app);
450
		if (!is_null($appData)) {
451
			OC_App::executeRepairSteps($app, $appData['repair-steps']['uninstall']);
452
		}
453
454
		// emit disable hook - needed anymore ?
455
		\OC_Hook::emit('OC_App', 'pre_disable', ['app' => $app]);
456
457
		// finally disable it
458
		$appManager = \OC::$server->getAppManager();
459
		$appManager->disableApp($app);
460
	}
461
462
	/**
463
	 * Returns the Settings Navigation
464
	 *
465
	 * @return string[]
466
	 *
467
	 * This function returns an array containing all settings pages added. The
468
	 * entries are sorted by the key 'order' ascending.
469
	 */
470
	public static function getSettingsNavigation() {
471
		$l = \OC::$server->getL10N('lib');
472
		$urlGenerator = \OC::$server->getURLGenerator();
473
474
		$settings = [];
475
		// by default, settings only contain the help menu
476
		if (OC_Util::getEditionString() === OC_Util::EDITION_COMMUNITY &&
477
			\OC::$server->getSystemConfig()->getValue('knowledgebaseenabled', true) == true
478
		) {
479
			$settings = [
480
				[
481
					"id" => "help",
482
					"order" => 1000,
483
					"href" => $urlGenerator->linkToRoute('settings_help'),
484
					"name" => $l->t("Help"),
485
					"icon" => $urlGenerator->imagePath("settings", "help.svg")
486
				]
487
			];
488
		}
489
490
		// if the user is logged-in
491
		if (OC_User::isLoggedIn()) {
492
			// personal menu
493
			$settings[] = [
494
				"id" => "settings",
495
				"order" => 1,
496
				"href" => $urlGenerator->linkToRoute('settings.SettingsPage.getPersonal'),
497
				"name" => $l->t("Settings"),
498
				"icon" => $urlGenerator->imagePath("settings", "admin.svg")
499
			];
500
501
			$hasUserManagementPrivileges = false;
502
			$userObject = \OC::$server->getUserSession()->getUser();
503 View Code Duplication
			if($userObject !== null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
504
				//Admin and SubAdmins are allowed to access user management
505
				$hasUserManagementPrivileges = \OC::$server->getGroupManager()->isAdmin($userObject->getUID())
506
					|| \OC::$server->getGroupManager()->getSubAdmin()->isSubAdmin($userObject);
507
			}
508
			if ($hasUserManagementPrivileges) {
509
				// admin users menu
510
				$settings[] = [
511
					"id" => "core_users",
512
					"order" => 2,
513
					"href" => $urlGenerator->linkToRoute('settings_users'),
514
					"name" => $l->t("Users"),
515
					"icon" => $urlGenerator->imagePath("settings", "users.svg")
516
				];
517
			}
518
519
		}
520
521
		return self::proceedNavigation($settings);
522
	}
523
524
	// This is private as well. It simply works, so don't ask for more details
525
	private static function proceedNavigation($list) {
526
		$activeApp = OC::$server->getNavigationManager()->getActiveEntry();
527
		foreach ($list as &$navEntry) {
528
			if ($navEntry['id'] == $activeApp) {
529
				$navEntry['active'] = true;
530
			} else {
531
				$navEntry['active'] = false;
532
			}
533
		}
534
		unset($navEntry);
535
536
		usort($list, function($a, $b) {
537
			if ($a["order"] == $b["order"]) {
538
				return 0;
539
			}
540
541
			if($a["order"] < $b["order"]) {
542
				return -1;
543
			}
544
545
			return 1;
546
		});
547
548
		return $list;
549
	}
550
551
	/**
552
	 * Get the path where to install apps
553
	 *
554
	 * @return string|false
555
	 */
556
	public static function getInstallPath() {
557
		foreach (OC::$APPSROOTS as $dir) {
558
			if (isset($dir['writable']) && $dir['writable'] === true) {
559
				return $dir['path'];
560
			}
561
		}
562
563
		\OCP\Util::writeLog('core', 'No application directories are marked as writable.', \OCP\Util::ERROR);
564
		return null;
565
	}
566
567
	/**
568
	 * Get the directory for the given app.
569
	 * If the app exists in multiple directories, the most recent version is taken.
570
	 * (false if not found)
571
	 *
572
	 * @param string $appId
573
	 * @return string|false
574
	 */
575
	public static function getAppPath($appId) {
576
		return \OC::$server->getAppManager()->getAppPath($appId);
577
	}
578
579
	/**
580
	 * Get the web path for the given app.
581
	 * If the app exists in multiple directories, the most recent version is taken.
582
	 * (false if not found)
583
	 *
584
	 * @param string $appId
585
	 * @return string|false
586
	 */
587
	public static function getAppWebPath($appId) {
588
		return \OC::$server->getAppManager()->getAppWebPath($appId);
589
	}
590
591
	/**
592
	 * check if an app's directory is writable
593
	 *
594
	 * @param string $appId
595
	 * @return bool
596
	 */
597
	public static function isAppDirWritable($appId) {
598
		$path = self::getAppPath($appId);
599
		return ($path !== false) ? is_writable($path) : false;
600
	}
601
602
	/**
603
	 * get the last version of the app from appinfo/info.xml
604
	 *
605
	 * @param string $appId
606
	 * @return string
607
	 */
608
	public static function getAppVersion($appId) {
609
		if (!isset(self::$appVersion[$appId])) {
610
			$file = self::getAppPath($appId);
611
			self::$appVersion[$appId] = ($file !== false) ? self::getAppVersionByPath($file) : '0';
612
		}
613
		return self::$appVersion[$appId];
614
	}
615
616
	/**
617
	 * get app's version based on it's path
618
	 *
619
	 * @param string $path
620
	 * @return string
621
	 */
622
	public static function getAppVersionByPath($path) {
623
		$infoFile = $path . '/appinfo/info.xml';
624
		$appData = self::getAppInfo($infoFile, true);
625
		return isset($appData['version']) ? $appData['version'] : '';
626
	}
627
628
	/**
629
	 * Read all app metadata from the info.xml file
630
	 *
631
	 * @param string $appId id of the app or the path of the info.xml file
632
	 * @param boolean $path (optional)
633
	 * @return array|null
634
	 * @note all data is read from info.xml, not just pre-defined fields
635
	 */
636
	public static function getAppInfo($appId, $path = false) {
637
		if ($path) {
638
			$file = $appId;
639
		} else {
640
			if (isset(self::$appInfo[$appId])) {
641
				return self::$appInfo[$appId];
642
			}
643
			$appPath = self::getAppPath($appId);
644
			if($appPath === false) {
645
				return null;
646
			}
647
			$file = $appPath . '/appinfo/info.xml';
648
		}
649
650
		$parser = new InfoParser();
651
		try {
652
			$data = $parser->parse($file);
653
		} catch (\Exception $e) {
654
			\OC::$server->getLogger()->logException($e);
655
			throw $e;
656
		}
657
658
		if (is_array($data)) {
659
			$data = OC_App::parseAppInfo($data);
660
		}
661
		if(isset($data['ocsid'])) {
662
			$storedId = \OC::$server->getConfig()->getAppValue($appId, 'ocsid');
663
			if($storedId !== '' && $storedId !== $data['ocsid']) {
664
				$data['ocsid'] = $storedId;
665
			}
666
		}
667
668
		self::$appInfo[$appId] = $data;
669
670
		return $data;
671
	}
672
673
	/**
674
	 * Returns the navigation
675
	 *
676
	 * @return array
677
	 *
678
	 * This function returns an array containing all entries added. The
679
	 * entries are sorted by the key 'order' ascending. Additional to the keys
680
	 * given for each app the following keys exist:
681
	 *   - active: boolean, signals if the user is on this navigation entry
682
	 */
683
	public static function getNavigation() {
684
		$entries = OC::$server->getNavigationManager()->getAll();
685
		return self::proceedNavigation($entries);
686
	}
687
688
	/**
689
	 * get the id of loaded app
690
	 *
691
	 * @return string
692
	 */
693
	public static function getCurrentApp() {
694
		$request = \OC::$server->getRequest();
695
		$script = substr($request->getScriptName(), strlen(OC::$WEBROOT) + 1);
696
		$topFolder = substr($script, 0, strpos($script, '/'));
697
		if (empty($topFolder)) {
698
			$path_info = $request->getPathInfo();
699
			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...
700
				$topFolder = substr($path_info, 1, strpos($path_info, '/', 1) - 1);
701
			}
702
		}
703
		if ($topFolder == 'apps') {
704
			$length = strlen($topFolder);
705
			return substr($script, $length + 1, strpos($script, '/', $length + 1) - $length - 1);
706
		} else {
707
			return $topFolder;
708
		}
709
	}
710
711
	/**
712
	 * @param string $type
713
	 * @return array
714
	 */
715
	public static function getForms($type) {
716
		$forms = [];
717
		switch ($type) {
718
			case 'admin':
719
				$source = self::$adminForms;
720
				break;
721
			case 'personal':
722
				$source = self::$personalForms;
723
				break;
724
			default:
725
				return [];
726
		}
727
		foreach ($source as $form) {
728
			$page = include $form['page'];
729
			$forms[] = [
730
				'appId' => $form['appId'],
731
				'page' => $page,
732
			];
733
		}
734
		return $forms;
735
	}
736
737
	/**
738
	 * register an admin form to be shown
739
	 *
740
	 * @param string $app
741
	 * @param string $page
742
	 * @deprecated Register settings panels in info.xml
743
	 */
744
	public static function registerAdmin($app, $page) {
745
		self::$adminForms[] = [
746
			'appId' => $app,
747
			'page' => $app . '/' . $page . '.php',
748
		];
749
	}
750
751
	/**
752
	 * register a personal form to be shown
753
	 * @param string $app
754
	 * @param string $page
755
	 * @deprecated Register settings panels in info.xml
756
	 */
757
	public static function registerPersonal($app, $page) {
758
		self::$personalForms[] = [
759
			'appId' => $app,
760
			'page' => $app . '/' . $page . '.php',
761
		];
762
	}
763
764
	/**
765
	 * @param array $entry
766
	 */
767
	public static function registerLogIn(array $entry) {
768
		self::$altLogin[] = $entry;
769
	}
770
771
	/**
772
	 * @return array
773
	 */
774
	public static function getAlternativeLogIns() {
775
		return self::$altLogin;
776
	}
777
778
	/**
779
	 * get a list of all apps in the apps folder
780
	 *
781
	 * @return array an array of app names (string IDs)
782
	 * @todo: change the name of this method to getInstalledApps, which is more accurate
783
	 */
784
	public static function getAllApps() {
785
786
		$apps = [];
787
788
		foreach (OC::$APPSROOTS as $apps_dir) {
789
			if (!is_readable($apps_dir['path'])) {
790
				\OCP\Util::writeLog('core', 'unable to read app folder : ' . $apps_dir['path'], \OCP\Util::WARN);
791
				continue;
792
			}
793
			$dh = opendir($apps_dir['path']);
794
795
			if (is_resource($dh)) {
796
				while (($file = readdir($dh)) !== false) {
797
798
					if ($file[0] != '.' and is_dir($apps_dir['path'] . '/' . $file) and is_file($apps_dir['path'] . '/' . $file . '/appinfo/info.xml')) {
799
800
						$apps[] = $file;
801
					}
802
				}
803
			}
804
		}
805
806
		return $apps;
807
	}
808
809
	/**
810
	 * List all apps, this is used in apps.php
811
	 *
812
	 * @param bool $onlyLocal
0 ignored issues
show
Bug introduced by
There is no parameter named $onlyLocal. Was it maybe removed?

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

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

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

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

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

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

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

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

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

Loading history...
814
	 *                                in the app store?
815
	 * @return array
816
	 */
817
	public static function listAllApps() {
818
		$installedApps = OC_App::getAllApps();
819
820
		//TODO which apps do we want to blacklist and how do we integrate
821
		// blacklisting with the multi apps folder feature?
822
823
		//we don't want to show configuration for these
824
		$blacklist = \OC::$server->getAppManager()->getAlwaysEnabledApps();
825
		$appList = [];
826
		$urlGenerator = \OC::$server->getURLGenerator();
827
828
		foreach ($installedApps as $app) {
829
			if (array_search($app, $blacklist) === false) {
830
831
				$info = OC_App::getAppInfo($app);
832
				if (!is_array($info)) {
833
					\OCP\Util::writeLog('core', 'Could not read app info file for app "' . $app . '"', \OCP\Util::ERROR);
834
					continue;
835
				}
836
837
				if (!isset($info['name'])) {
838
					\OCP\Util::writeLog('core', 'App id "' . $app . '" has no name in appinfo', \OCP\Util::ERROR);
839
					continue;
840
				}
841
842
				$enabled = \OC::$server->getAppConfig()->getValue($app, 'enabled', 'no');
843
				$info['groups'] = null;
844
				if ($enabled === 'yes') {
845
					$active = true;
846
				} else if ($enabled === 'no') {
847
					$active = false;
848
				} else {
849
					$active = true;
850
					$info['groups'] = $enabled;
851
				}
852
853
				$info['active'] = $active;
854
855
				if (self::isShipped($app)) {
856
					$info['internal'] = true;
857
					$info['level'] = self::officialApp;
858
					$info['removable'] = false;
859
				} else {
860
					$result = \OC::$server->getIntegrityCodeChecker()->verifyAppSignature($app, '', true);
861
					if (empty($result)) {
862
						$info['internal'] = false;
863
						$info['level'] = self::approvedApp;
864
						$info['removable'] = false;
865
					}
866
867
					$info['internal'] = false;
868
					$info['removable'] = true;
869
				}
870
871
				$appPath = self::getAppPath($app);
872
				if($appPath !== false) {
873
					$appIcon = $appPath . '/img/' . $app . '.svg';
874
					if (file_exists($appIcon)) {
875
						$info['preview'] = \OC::$server->getURLGenerator()->imagePath($app, $app . '.svg');
876
						$info['previewAsIcon'] = true;
877
					} else {
878
						$appIcon = $appPath . '/img/app.svg';
879
						if (file_exists($appIcon)) {
880
							$info['preview'] = \OC::$server->getURLGenerator()->imagePath($app, 'app.svg');
881
							$info['previewAsIcon'] = true;
882
						}
883
					}
884
				}
885
				// fix documentation
886
				if (isset($info['documentation']) && is_array($info['documentation'])) {
887
					foreach ($info['documentation'] as $key => $url) {
888
						// If it is not an absolute URL we assume it is a key
889
						// i.e. admin-ldap will get converted to go.php?to=admin-ldap
890
						if (stripos($url, 'https://') !== 0 && stripos($url, 'http://') !== 0) {
891
							$url = $urlGenerator->linkToDocs($url);
892
						}
893
894
						$info['documentation'][$key] = $url;
895
					}
896
				}
897
898
				$info['version'] = OC_App::getAppVersion($app);
899
				$appList[] = $info;
900
			}
901
		}
902
903
		return $appList;
904
	}
905
906
	/**
907
	 * Returns the internal app ID or false
908
	 * @param string $ocsID
909
	 * @return string|false
910
	 */
911
	public static function getInternalAppIdByOcs($ocsID) {
912
		if(is_numeric($ocsID)) {
913
			$idArray = \OC::$server->getAppConfig()->getValues(false, 'ocsid');
914
			if(array_search($ocsID, $idArray)) {
915
				return array_search($ocsID, $idArray);
916
			}
917
		}
918
		return false;
919
	}
920
921
	public static function shouldUpgrade($app) {
922
		$versions = self::getAppVersions();
923
		$currentVersion = OC_App::getAppVersion($app);
924
		if ($currentVersion && isset($versions[$app])) {
925
			$installedVersion = $versions[$app];
926
			if (!version_compare($currentVersion, $installedVersion, '=')) {
927
				return true;
928
			}
929
		}
930
		return false;
931
	}
932
933
	/**
934
	 * Adjust the number of version parts of $version1 to match
935
	 * the number of version parts of $version2.
936
	 *
937
	 * @param string $version1 version to adjust
938
	 * @param string $version2 version to take the number of parts from
939
	 * @return string shortened $version1
940
	 */
941
	private static function adjustVersionParts($version1, $version2) {
942
		$version1 = explode('.', $version1);
943
		$version2 = explode('.', $version2);
944
		// reduce $version1 to match the number of parts in $version2
945
		while (count($version1) > count($version2)) {
946
			array_pop($version1);
947
		}
948
		// if $version1 does not have enough parts, add some
949
		while (count($version1) < count($version2)) {
950
			$version1[] = '0';
951
		}
952
		return implode('.', $version1);
953
	}
954
955
	/**
956
	 * Check whether the current ownCloud version matches the given
957
	 * application's version requirements.
958
	 *
959
	 * The comparison is made based on the number of parts that the
960
	 * app info version has. For example for ownCloud 6.0.3 if the
961
	 * app info version is expecting version 6.0, the comparison is
962
	 * made on the first two parts of the ownCloud version.
963
	 * This means that it's possible to specify "requiremin" => 6
964
	 * and "requiremax" => 6 and it will still match ownCloud 6.0.3.
965
	 *
966
	 * @param string|array $ocVersion ownCloud version to check against
967
	 * @param array $appInfo app info (from xml)
968
	 *
969
	 * @return boolean true if compatible, otherwise false
970
	 */
971
	public static function isAppCompatible($ocVersion, $appInfo) {
972
		$requireMin = '';
973
		$requireMax = '';
974
		if (isset($appInfo['dependencies']['owncloud']['@attributes']['min-version'])) {
975
			$requireMin = $appInfo['dependencies']['owncloud']['@attributes']['min-version'];
976
		} else if (isset($appInfo['requiremin'])) {
977
			$requireMin = $appInfo['requiremin'];
978
		} else if (isset($appInfo['require'])) {
979
			$requireMin = $appInfo['require'];
980
		}
981
982
		if (isset($appInfo['dependencies']['owncloud']['@attributes']['max-version'])) {
983
			$requireMax = $appInfo['dependencies']['owncloud']['@attributes']['max-version'];
984
		} else if (isset($appInfo['requiremax'])) {
985
			$requireMax = $appInfo['requiremax'];
986
		}
987
988
		if (is_array($ocVersion)) {
989
			$ocVersion = implode('.', $ocVersion);
990
		}
991
992 View Code Duplication
		if (!empty($requireMin)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
993
			&& version_compare(self::adjustVersionParts($ocVersion, $requireMin), $requireMin, '<')
994
		) {
995
996
			return false;
997
		}
998
999 View Code Duplication
		if (!empty($requireMax)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1000
			&& version_compare(self::adjustVersionParts($ocVersion, $requireMax), $requireMax, '>')
1001
		) {
1002
			return false;
1003
		}
1004
1005
		return true;
1006
	}
1007
1008
	/**
1009
	 * get the installed version of all apps
1010
	 */
1011
	public static function getAppVersions() {
1012
		static $versions;
1013
1014
		if(!$versions) {
1015
			$appConfig = \OC::$server->getAppConfig();
1016
			$versions = $appConfig->getValues(false, 'installed_version');
1017
		}
1018
		return $versions;
1019
	}
1020
1021
	/**
1022
	 * update the database for the app and call the update script
1023
	 *
1024
	 * @param string $appId
1025
	 * @return bool
1026
	 */
1027
	public static function updateApp($appId) {
1028
		$appPath = self::getAppPath($appId);
1029
		if($appPath === false) {
1030
			return false;
1031
		}
1032
		$appData = self::getAppInfo($appId);
1033
		self::executeRepairSteps($appId, $appData['repair-steps']['pre-migration']);
1034
		if (isset($appData['use-migrations']) && $appData['use-migrations'] === 'true') {
1035
			$ms = new \OC\DB\MigrationService($appId, \OC::$server->getDatabaseConnection());
1036
			$ms->migrate();
1037
		} else {
1038
			if (file_exists($appPath . '/appinfo/database.xml')) {
1039
				OC_DB::updateDbFromStructure($appPath . '/appinfo/database.xml');
1040
			}
1041
		}
1042
		self::executeRepairSteps($appId, $appData['repair-steps']['post-migration']);
1043
		self::setupLiveMigrations($appId, $appData['repair-steps']['live-migration']);
1044
		self::clearAppCache($appId);
1045
		// run upgrade code
1046
		if (file_exists($appPath . '/appinfo/update.php')) {
1047
			self::loadApp($appId, false);
1048
			include $appPath . '/appinfo/update.php';
1049
		}
1050
		self::setupBackgroundJobs($appData['background-jobs']);
1051
1052
		//set remote/public handlers
1053
		if (array_key_exists('ocsid', $appData)) {
1054
			\OC::$server->getConfig()->setAppValue($appId, 'ocsid', $appData['ocsid']);
1055 View Code Duplication
		} elseif(\OC::$server->getConfig()->getAppValue($appId, 'ocsid', null) !== null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

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

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

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

Loading history...
1059
			\OC::$server->getConfig()->setAppValue('core', 'remote_' . $name, $appId . '/' . $path);
1060
		}
1061
		foreach ($appData['public'] as $name => $path) {
1062
			\OC::$server->getConfig()->setAppValue('core', 'public_' . $name, $appId . '/' . $path);
1063
		}
1064
1065
		self::setAppTypes($appId);
1066
1067
		$version = \OC_App::getAppVersion($appId);
1068
		\OC::$server->getAppConfig()->setValue($appId, 'installed_version', $version);
1069
1070
		return true;
1071
	}
1072
1073
	/**
1074
	 * @param string $appId
1075
	 * @param string[] $steps
1076
	 * @throws \OC\NeedsUpdateException
1077
	 */
1078
	public static function executeRepairSteps($appId, array $steps) {
1079
		if (empty($steps)) {
1080
			return;
1081
		}
1082
		// load the app
1083
		self::loadApp($appId, false);
1084
1085
		$dispatcher = OC::$server->getEventDispatcher();
1086
1087
		// load the steps
1088
		$r = new Repair([], $dispatcher);
0 ignored issues
show
Documentation introduced by
$dispatcher is of type object<Symfony\Component...entDispatcherInterface>, but the function expects a null|object<Symfony\Comp...atcher\EventDispatcher>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1089
		foreach ($steps as $step) {
1090
			try {
1091
				$r->addStep($step);
1092
			} catch (Exception $ex) {
1093
				$r->emit('\OC\Repair', 'error', [$ex->getMessage()]);
1094
				\OC::$server->getLogger()->logException($ex);
1095
			}
1096
		}
1097
		// run the steps
1098
		$r->run();
1099
	}
1100
1101
	public static function setupBackgroundJobs(array $jobs) {
1102
		$queue = \OC::$server->getJobList();
1103
		foreach ($jobs as $job) {
1104
			$queue->add($job);
1105
		}
1106
	}
1107
1108
	/**
1109
	 * @param string $appId
1110
	 * @param string[] $steps
1111
	 */
1112
	private static function setupLiveMigrations($appId, array $steps) {
1113
		$queue = \OC::$server->getJobList();
1114
		foreach ($steps as $step) {
1115
			$queue->add('OC\Migration\BackgroundRepair', [
1116
				'app' => $appId,
1117
				'step' => $step]);
1118
		}
1119
	}
1120
1121
	/**
1122
	 * @param string $appId
1123
	 * @return \OC\Files\View|false
1124
	 */
1125
	public static function getStorage($appId) {
1126
		if (OC_App::isEnabled($appId)) { //sanity check
1127
			if (OC_User::isLoggedIn()) {
1128
				$view = new \OC\Files\View('/' . OC_User::getUser());
1129
				if (!$view->file_exists($appId)) {
1130
					$view->mkdir($appId);
1131
				}
1132
				return new \OC\Files\View('/' . OC_User::getUser() . '/' . $appId);
1133
			} else {
1134
				\OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ', user not logged in', \OCP\Util::ERROR);
1135
				return false;
1136
			}
1137
		} else {
1138
			\OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ' not enabled', \OCP\Util::ERROR);
1139
			return false;
1140
		}
1141
	}
1142
1143
	/**
1144
	 * parses the app data array and enhanced the 'description' value
1145
	 *
1146
	 * @param array $data the app data
1147
	 * @return array improved app data
1148
	 */
1149
	public static function parseAppInfo(array $data) {
1150
1151
		// just modify the description if it is available
1152
		// otherwise this will create a $data element with an empty 'description'
1153
		if (isset($data['description'])) {
1154
			if (is_string($data['description'])) {
1155
				// sometimes the description contains line breaks and they are then also
1156
				// shown in this way in the app management which isn't wanted as HTML
1157
				// manages line breaks itself
1158
1159
				// first of all we split on empty lines
1160
				$paragraphs = preg_split("!\n[[:space:]]*\n!mu", $data['description']);
1161
1162
				$result = [];
1163
				foreach ($paragraphs as $value) {
1164
					// replace multiple whitespace (tabs, space, newlines) inside a paragraph
1165
					// with a single space - also trims whitespace
1166
					$result[] = trim(preg_replace('![[:space:]]+!mu', ' ', $value));
1167
				}
1168
1169
				// join the single paragraphs with a empty line in between
1170
				$data['description'] = implode("\n\n", $result);
1171
1172
			} else {
1173
				$data['description'] = '';
1174
			}
1175
		}
1176
1177
		return $data;
1178
	}
1179
1180
	/**
1181
	 * @param OCP\IConfig $config
1182
	 * @param OCP\IL10N $l
1183
	 * @param array $info
1184
	 * @throws Exception
1185
	 */
1186
	protected static function checkAppDependencies($config, $l, $info) {
1187
		$dependencyAnalyzer = new DependencyAnalyzer(new Platform($config), $l);
1188
		$missing = $dependencyAnalyzer->analyze($info);
1189
		if (!empty($missing)) {
1190
			$missingMsg = join(PHP_EOL, $missing);
1191
			throw new \Exception(
1192
				$l->t('App "%s" cannot be installed because the following dependencies are not fulfilled: %s',
1193
					[$info['name'], $missingMsg]
1194
				)
1195
			);
1196
		}
1197
	}
1198
1199
	/**
1200
	 * @param $appId
1201
	 */
1202
	public static function clearAppCache($appId) {
1203
		unset(self::$appVersion[$appId]);
1204
		unset(self::$appInfo[$appId]);
1205
	}
1206
}
1207