Completed
Push — master ( 343a3e...9c30ac )
by Thomas
25:28
created

OC_App::getDefaultEnabledAppTheme()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 10
nc 5
nop 0
dl 0
loc 14
rs 8.8571
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) 2017, 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 $altLogin = [];
66
	const officialApp = 200;
67
	const approvedApp = 100;
68
69
	/**
70
	 * clean the appId
71
	 *
72
	 * @param string|boolean $app AppId that needs to be cleaned
73
	 * @return string
74
	 */
75
	public static function cleanAppId($app) {
76
		return str_replace(['\0', '/', '\\', '..'], '', $app);
77
	}
78
79
	/**
80
	 * Check if an app is loaded
81
	 *
82
	 * @param string $app
83
	 * @return bool
84
	 */
85
	public static function isAppLoaded($app) {
86
		return in_array($app, self::$loadedApps, true);
87
	}
88
89
	/**
90
	 * loads all apps
91
	 *
92
	 * @param string[] | string | null $types
93
	 * @return bool
94
	 *
95
	 * This function walks through the ownCloud directory and loads all apps
96
	 * it can find. A directory contains an app if the file /appinfo/info.xml
97
	 * exists.
98
	 *
99
	 * if $types is set, only apps of those types will be loaded
100
	 */
101
	public static function loadApps($types = null) {
102
		if (\OC::$server->getSystemConfig()->getValue('maintenance', false)) {
103
			return false;
104
		}
105
		// Load the enabled apps here
106
		$apps = self::getEnabledApps();
107
108
		// Add each apps' folder as allowed class path
109
		foreach($apps as $app) {
110
			$path = self::getAppPath($app);
111
			if($path !== false) {
112
				self::registerAutoloading($app, $path);
113
			}
114
		}
115
116
		// prevent app.php from printing output
117
		ob_start();
118
		foreach ($apps as $app) {
119
			if ((is_null($types) or self::isType($app, $types)) && !in_array($app, self::$loadedApps)) {
120
				self::loadApp($app);
121
			}
122
		}
123
		ob_end_clean();
124
125
		// once all authentication apps are loaded we can validate the session
126
		if (is_null($types) || in_array('authentication', $types)) {
127
			if (\OC::$server->getUserSession()) {
128
				$davUser = \OC::$server->getUserSession()->getSession()->get(\OCA\DAV\Connector\Sabre\Auth::DAV_AUTHENTICATED);
129
				if (is_null($davUser)) {
130
					\OC::$server->getUserSession()->validateSession();
131
				}
132
			}
133
		}
134
135
		\OC_Hook::emit('OC_App', 'loadedApps');
136
		return true;
137
	}
138
139
	/**
140
	 * load a single app
141
	 *
142
	 * @param string $app
143
	 * @param bool $checkUpgrade whether an upgrade check should be done
144
	 * @throws \OC\NeedsUpdateException
145
	 */
146
	public static function loadApp($app, $checkUpgrade = true) {
147
		self::$loadedApps[] = $app;
148
		$appPath = self::getAppPath($app);
149
		if($appPath === false) {
150
			return;
151
		}
152
153
		// in case someone calls loadApp() directly
154
		self::registerAutoloading($app, $appPath);
155
156
		self::enableThemeIfApplicable($app);
157
158
		if (is_file($appPath . '/appinfo/app.php')) {
159
			\OC::$server->getEventLogger()->start('load_app_' . $app, 'Load app: ' . $app);
160
			if ($checkUpgrade and self::shouldUpgrade($app)) {
161
				throw new \OC\NeedsUpdateException();
162
			}
163
			self::requireAppFile($app);
164
			if (self::isType($app, ['authentication'])) {
165
				// since authentication apps affect the "is app enabled for group" check,
166
				// the enabled apps cache needs to be cleared to make sure that the
167
				// next time getEnableApps() is called it will also include apps that were
168
				// enabled for groups
169
				self::$enabledAppsCache = [];
170
			}
171
			\OC::$server->getEventLogger()->end('load_app_' . $app);
172
		}
173
	}
174
175
	/**
176
	 * Enables the app as a theme if it has the type "theme"
177
	 * @param string $app
178
	 */
179
	private static function enableThemeIfApplicable($app) {
180
		if (self::isType($app, 'theme')) {
181
			/** @var \OC\Theme\ThemeService $themeManager */
182
			$themeManager = \OC::$server->query('ThemeService');
183
			$themeManager->setAppTheme($app);
184
		}
185
	}
186
187
	/**
188
	 * @internal
189
	 * @param string $app
190
	 * @param string $path
191
	 */
192
	public static function registerAutoloading($app, $path) {
193
		// Register on PSR-4 composer autoloader
194
		$appNamespace = \OC\AppFramework\App::buildAppNamespace($app);
195
		\OC::$composerAutoloader->addPsr4($appNamespace . '\\', $path . '/lib/', true);
196
		if (defined('PHPUNIT_RUN')) {
197
			\OC::$composerAutoloader->addPsr4($appNamespace . '\\Tests\\', $path . '/tests/', true);
198
		}
199
200
		// Register on legacy autoloader
201
		\OC::$loader->addValidRoot($path);
202
	}
203
204
	/**
205
	 * Load app.php from the given app
206
	 *
207
	 * @param string $app app name
208
	 */
209
	private static function requireAppFile($app) {
210
		try {
211
			// encapsulated here to avoid variable scope conflicts
212
			require_once $app . '/appinfo/app.php';
213
		} catch (Exception $ex) {
214
			\OC::$server->getLogger()->logException($ex);
215
			$blacklist = \OC::$server->getAppManager()->getAlwaysEnabledApps();
216
			if (!in_array($app, $blacklist)) {
217
				self::disable($app);
218
			}
219
		}
220
	}
221
222
	/**
223
	 * check if an app is of a specific type
224
	 *
225
	 * @param string $app
226
	 * @param string|array $types
227
	 * @return bool
228
	 */
229
	public static function isType($app, $types) {
230
		if (is_string($types)) {
231
			$types = [$types];
232
		}
233
		$appTypes = self::getAppTypes($app);
234
		foreach ($types as $type) {
235
			if (array_search($type, $appTypes) !== false) {
236
				return true;
237
			}
238
		}
239
		return false;
240
	}
241
242
	/**
243
	 * get the types of an app
244
	 *
245
	 * @param string $app
246
	 * @return array
247
	 */
248
	private static function getAppTypes($app) {
249
		//load the cache
250
		if (count(self::$appTypes) == 0) {
251
			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...
252
		}
253
254
		if (isset(self::$appTypes[$app])) {
255
			return explode(',', self::$appTypes[$app]);
256
		} else {
257
			return [];
258
		}
259
	}
260
261
	/**
262
	 * read app types from info.xml and cache them in the database
263
	 */
264
	public static function setAppTypes($app) {
265
		$appData = self::getAppInfo($app);
266
		if(!is_array($appData)) {
267
			return;
268
		}
269
270
		if (isset($appData['types'])) {
271
			$appTypes = implode(',', $appData['types']);
272
		} else {
273
			$appTypes = '';
274
		}
275
276
		\OC::$server->getAppConfig()->setValue($app, 'types', $appTypes);
277
	}
278
279
	/**
280
	 * check if app is shipped
281
	 *
282
	 * @param string $appId the id of the app to check
283
	 * @return bool
284
	 *
285
	 * Check if an app that is installed is a shipped app or installed from the appstore.
286
	 */
287
	public static function isShipped($appId) {
288
		return \OC::$server->getAppManager()->isShipped($appId);
289
	}
290
291
	/**
292
	 * get all enabled apps
293
	 */
294
	protected static $enabledAppsCache = [];
295
296
	/**
297
	 * Returns apps enabled for the current user.
298
	 *
299
	 * @param bool $forceRefresh whether to refresh the cache
300
	 * @param bool $all whether to return apps for all users, not only the
301
	 * currently logged in one
302
	 * @return string[]
303
	 */
304
	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...
305
		if (!\OC::$server->getSystemConfig()->getValue('installed', false)) {
306
			return [];
307
		}
308
		// in incognito mode or when logged out, $user will be false,
309
		// which is also the case during an upgrade
310
		$appManager = \OC::$server->getAppManager();
311
		if ($all) {
312
			$user = null;
313
		} else {
314
			$user = \OC::$server->getUserSession()->getUser();
315
		}
316
317
		if (is_null($user)) {
318
			$apps = $appManager->getInstalledApps();
319
		} else {
320
			$apps = $appManager->getEnabledAppsForUser($user);
321
		}
322
		$apps = array_filter($apps, function ($app) {
323
			return $app !== 'files';//we add this manually
324
		});
325
		sort($apps);
326
		array_unshift($apps, 'files');
327
		return $apps;
328
	}
329
330
	/**
331
	 * checks whether or not an app is enabled
332
	 *
333
	 * @param string $app app
334
	 * @return bool
335
	 *
336
	 * This function checks whether or not an app is enabled.
337
	 */
338
	public static function isEnabled($app) {
339
		return \OC::$server->getAppManager()->isEnabledForUser($app);
340
	}
341
342
	/**
343
	 * enables an app
344
	 *
345
	 * @param mixed $app app
346
	 * @param array $groups (optional) when set, only these groups will have access to the app
347
	 * @throws \Exception
348
	 * @return void
349
	 *
350
	 * This function set an app as enabled in appconfig.
351
	 */
352
	public static function enable($app, $groups = null) {
353
		self::$enabledAppsCache = []; // flush
354
355
		// check for required dependencies
356
		$config = \OC::$server->getConfig();
357
		$l = \OC::$server->getL10N('core');
358
		$info = self::getAppInfo($app);
359
360
		self::checkAppDependencies($config, $l, $info);
0 ignored issues
show
Bug introduced by
It seems like $info defined by self::getAppInfo($app) on line 358 can also be of type null; however, OC_App::checkAppDependencies() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
361
362
		if (!Installer::isInstalled($app)) {
363
			Installer::installShippedApp($app);
364
		}
365
366
		$appManager = \OC::$server->getAppManager();
367
		if (!is_null($groups)) {
368
			$groupManager = \OC::$server->getGroupManager();
369
			$groupsList = [];
370
			foreach ($groups as $group) {
371
				$groupItem = $groupManager->get($group);
372
				if ($groupItem instanceof \OCP\IGroup) {
373
					$groupsList[] = $groupManager->get($group);
374
				}
375
			}
376
			$appManager->enableAppForGroups($app, $groupsList);
377
		} else {
378
			$appManager->enableApp($app);
379
		}
380
	}
381
382
	/**
383
	 * @param string $app
384
	 * @return bool
385
	 */
386
	public static function removeApp($app) {
387
		if (self::isShipped($app)) {
388
			return false;
389
		}
390
391
		return Installer::removeApp($app);
392
	}
393
394
	/**
395
	 * This function set an app as disabled in appconfig.
396
	 *
397
	 * @param string $app app
398
	 * @throws Exception
399
	 */
400
	public static function disable($app) {
401
		// Convert OCS ID to regular application identifier
402
		if(self::getInternalAppIdByOcs($app) !== false) {
403
			$app = self::getInternalAppIdByOcs($app);
404
		}
405
406
		// flush
407
		self::$enabledAppsCache = [];
408
409
		// run uninstall steps
410
		$appData = OC_App::getAppInfo($app);
411
		if (!is_null($appData)) {
412
			OC_App::executeRepairSteps($app, $appData['repair-steps']['uninstall']);
413
		}
414
415
		// emit disable hook - needed anymore ?
416
		\OC_Hook::emit('OC_App', 'pre_disable', ['app' => $app]);
417
418
		// finally disable it
419
		$appManager = \OC::$server->getAppManager();
420
		$appManager->disableApp($app);
421
	}
422
423
	/**
424
	 * Returns the Settings Navigation
425
	 *
426
	 * @return string[]
427
	 *
428
	 * This function returns an array containing all settings pages added. The
429
	 * entries are sorted by the key 'order' ascending.
430
	 */
431
	public static function getSettingsNavigation() {
432
		$l = \OC::$server->getL10N('lib');
433
		$urlGenerator = \OC::$server->getURLGenerator();
434
435
		$settings = [];
436
		// by default, settings only contain the help menu
437
		if (OC_Util::getEditionString() === OC_Util::EDITION_COMMUNITY &&
438
			\OC::$server->getSystemConfig()->getValue('knowledgebaseenabled', true) == true
439
		) {
440
			$settings = [
441
				[
442
					"id" => "help",
443
					"order" => 1000,
444
					"href" => $urlGenerator->linkToRoute('settings_help'),
445
					"name" => $l->t("Help"),
446
					"icon" => $urlGenerator->imagePath("settings", "help.svg")
447
				]
448
			];
449
		}
450
451
		// if the user is logged-in
452
		if (OC_User::isLoggedIn()) {
453
			// personal menu
454
			$settings[] = [
455
				"id" => "settings",
456
				"order" => 1,
457
				"href" => $urlGenerator->linkToRoute('settings.SettingsPage.getPersonal'),
458
				"name" => $l->t("Settings"),
459
				"icon" => $urlGenerator->imagePath("settings", "admin.svg")
460
			];
461
462
			//SubAdmins are also allowed to access user management
463
			$userObject = \OC::$server->getUserSession()->getUser();
464
			$isSubAdmin = false;
465
			if($userObject !== null) {
466
				$isSubAdmin = \OC::$server->getGroupManager()->getSubAdmin()->isSubAdmin($userObject);
467
			}
468
			if ($isSubAdmin) {
469
				// admin users menu
470
				$settings[] = [
471
					"id" => "core_users",
472
					"order" => 2,
473
					"href" => $urlGenerator->linkToRoute('settings_users'),
474
					"name" => $l->t("Users"),
475
					"icon" => $urlGenerator->imagePath("settings", "users.svg")
476
				];
477
			}
478
479
		}
480
481
		$navigation = self::proceedNavigation($settings);
482
		return $navigation;
483
	}
484
485
	// This is private as well. It simply works, so don't ask for more details
486
	private static function proceedNavigation($list) {
487
		$activeApp = OC::$server->getNavigationManager()->getActiveEntry();
488
		foreach ($list as &$navEntry) {
489
			if ($navEntry['id'] == $activeApp) {
490
				$navEntry['active'] = true;
491
			} else {
492
				$navEntry['active'] = false;
493
			}
494
		}
495
		unset($navEntry);
496
497
		usort($list, create_function('$a, $b', 'if( $a["order"] == $b["order"] ) {return 0;}elseif( $a["order"] < $b["order"] ) {return -1;}else{return 1;}'));
498
499
		return $list;
500
	}
501
502
	/**
503
	 * Get the path where to install apps
504
	 *
505
	 * @return string|false
506
	 */
507
	public static function getInstallPath() {
508
		if (\OC::$server->getSystemConfig()->getValue('appstoreenabled', true) == false) {
509
			return false;
510
		}
511
512
		foreach (OC::$APPSROOTS as $dir) {
513
			if (isset($dir['writable']) && $dir['writable'] === true) {
514
				return $dir['path'];
515
			}
516
		}
517
518
		\OCP\Util::writeLog('core', 'No application directories are marked as writable.', \OCP\Util::ERROR);
519
		return null;
520
	}
521
522
523
	/**
524
	 * search for an app in all app-directories
525
	 *
526
	 * @param string $appId
527
	 * @return false|string
528
	 */
529
	protected static function findAppInDirectories($appId) {
530
		$sanitizedAppId = self::cleanAppId($appId);
531
		if($sanitizedAppId !== $appId) {
532
			return false;
533
		}
534
		static $app_dir = [];
535
536
		if (isset($app_dir[$appId])) {
537
			return $app_dir[$appId];
538
		}
539
540
		$possibleApps = [];
541
		foreach (OC::$APPSROOTS as $dir) {
542
			if (file_exists($dir['path'] . '/' . $appId)) {
543
				$possibleApps[] = $dir;
544
			}
545
		}
546
547
		if (empty($possibleApps)) {
548
			return false;
549
		} elseif (count($possibleApps) === 1) {
550
			$dir = array_shift($possibleApps);
551
			$app_dir[$appId] = $dir;
552
			return $dir;
553
		} else {
554
			$versionToLoad = [];
555
			foreach ($possibleApps as $possibleApp) {
556
				$version = self::getAppVersionByPath($possibleApp['path']);
557
				if (empty($versionToLoad) || version_compare($version, $versionToLoad['version'], '>')) {
558
					$versionToLoad = [
559
						'dir' => $possibleApp,
560
						'version' => $version,
561
					];
562
				}
563
			}
564
			$app_dir[$appId] = $versionToLoad['dir'];
565
			return $versionToLoad['dir'];
566
			//TODO - write test
567
		}
568
	}
569
570
	/**
571
	 * Get the directory for the given app.
572
	 * If the app is defined in multiple directories, the first one is taken. (false if not found)
573
	 *
574
	 * @param string $appId
575
	 * @return string|false
576
	 */
577
	public static function getAppPath($appId) {
578
		if ($appId === null || trim($appId) === '') {
579
			return false;
580
		}
581
582
		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...
583
			return $dir['path'] . '/' . $appId;
584
		}
585
		return false;
586
	}
587
588
589
	/**
590
	 * check if an app's directory is writable
591
	 *
592
	 * @param string $appId
593
	 * @return bool
594
	 */
595
	public static function isAppDirWritable($appId) {
596
		$path = self::getAppPath($appId);
597
		return ($path !== false) ? is_writable($path) : false;
598
	}
599
600
	/**
601
	 * Get the path for the given app on the access
602
	 * If the app is defined in multiple directories, the first one is taken. (false if not found)
603
	 *
604
	 * @param string $appId
605
	 * @return string|false
606
	 */
607
	public static function getAppWebPath($appId) {
608
		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...
609
			return OC::$WEBROOT . $dir['url'] . '/' . $appId;
610
		}
611
		return false;
612
	}
613
614
	/**
615
	 * get the last version of the app from appinfo/info.xml
616
	 *
617
	 * @param string $appId
618
	 * @return string
619
	 */
620
	public static function getAppVersion($appId) {
621
		if (!isset(self::$appVersion[$appId])) {
622
			$file = self::getAppPath($appId);
623
			self::$appVersion[$appId] = ($file !== false) ? self::getAppVersionByPath($file) : '0';
624
		}
625
		return self::$appVersion[$appId];
626
	}
627
628
	/**
629
	 * get app's version based on it's path
630
	 *
631
	 * @param string $path
632
	 * @return string
633
	 */
634
	public static function getAppVersionByPath($path) {
635
		$infoFile = $path . '/appinfo/info.xml';
636
		$appData = self::getAppInfo($infoFile, true);
637
		return isset($appData['version']) ? $appData['version'] : '';
638
	}
639
640
	/**
641
	 * @return false|string
642
	 */
643
	public static function getDefaultEnabledAppTheme() {
644
		$apps = self::getAllApps();
645
		$parser = new InfoParser();
646
		foreach ($apps as $app) {
647
			$info = $parser->parse(self::getAppPath($app) . '/appinfo/info.xml');
648
			if (is_array($info)) {
649
				$info = OC_App::parseAppInfo($info);
650
			}
651
			if (isset($info['default_enable']) && in_array('theme', $info['types'])) {
652
				return $app;
653
			}
654
		}
655
		return false;
656
	}
657
658
	/**
659
	 * Read all app metadata from the info.xml file
660
	 *
661
	 * @param string $appId id of the app or the path of the info.xml file
662
	 * @param boolean $path (optional)
663
	 * @return array|null
664
	 * @note all data is read from info.xml, not just pre-defined fields
665
	 */
666
	public static function getAppInfo($appId, $path = false) {
667
		if ($path) {
668
			$file = $appId;
669
		} else {
670
			if (isset(self::$appInfo[$appId])) {
671
				return self::$appInfo[$appId];
672
			}
673
			$appPath = self::getAppPath($appId);
674
			if($appPath === false) {
675
				return null;
676
			}
677
			$file = $appPath . '/appinfo/info.xml';
678
		}
679
680
		$parser = new InfoParser();
681
		$data = $parser->parse($file);
0 ignored issues
show
Bug Compatibility introduced by
The expression $parser->parse($file); of type null|string|array adds the type string to the return on line 695 which is incompatible with the return type documented by OC_App::getAppInfo of type array|null.
Loading history...
682
683
		if (is_array($data)) {
684
			$data = OC_App::parseAppInfo($data);
685
		}
686
		if(isset($data['ocsid'])) {
687
			$storedId = \OC::$server->getConfig()->getAppValue($appId, 'ocsid');
688
			if($storedId !== '' && $storedId !== $data['ocsid']) {
689
				$data['ocsid'] = $storedId;
690
			}
691
		}
692
693
		self::$appInfo[$appId] = $data;
694
695
		return $data;
696
	}
697
698
	/**
699
	 * Returns the navigation
700
	 *
701
	 * @return array
702
	 *
703
	 * This function returns an array containing all entries added. The
704
	 * entries are sorted by the key 'order' ascending. Additional to the keys
705
	 * given for each app the following keys exist:
706
	 *   - active: boolean, signals if the user is on this navigation entry
707
	 */
708
	public static function getNavigation() {
709
		$entries = OC::$server->getNavigationManager()->getAll();
710
		$navigation = self::proceedNavigation($entries);
711
		return $navigation;
712
	}
713
714
	/**
715
	 * get the id of loaded app
716
	 *
717
	 * @return string
718
	 */
719
	public static function getCurrentApp() {
720
		$request = \OC::$server->getRequest();
721
		$script = substr($request->getScriptName(), strlen(OC::$WEBROOT) + 1);
722
		$topFolder = substr($script, 0, strpos($script, '/'));
723
		if (empty($topFolder)) {
724
			$path_info = $request->getPathInfo();
725
			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...
726
				$topFolder = substr($path_info, 1, strpos($path_info, '/', 1) - 1);
727
			}
728
		}
729
		if ($topFolder == 'apps') {
730
			$length = strlen($topFolder);
731
			return substr($script, $length + 1, strpos($script, '/', $length + 1) - $length - 1);
732
		} else {
733
			return $topFolder;
734
		}
735
	}
736
737
	/**
738
	 * @param string $type
739
	 * @return array
740
	 */
741
	public static function getForms($type) {
742
		$forms = [];
743
		switch ($type) {
744
			case 'admin':
745
				$source = self::$adminForms;
746
				break;
747
			case 'personal':
748
				$source = self::$personalForms;
749
				break;
750
			default:
751
				return [];
752
		}
753
		foreach ($source as $form) {
754
			$page = include $form['page'];
755
			$forms[] = [
756
				'appId' => $form['appId'],
757
				'page' => $page,
758
			];
759
		}
760
		return $forms;
761
	}
762
763
	/**
764
	 * register an admin form to be shown
765
	 *
766
	 * @param string $app
767
	 * @param string $page
768
	 * @deprecated Register settings panels in info.xml
769
	 */
770
	public static function registerAdmin($app, $page) {
771
		self::$adminForms[] = [
772
			'appId' => $app,
773
			'page' => $app . '/' . $page . '.php',
774
		];
775
	}
776
777
	/**
778
	 * register a personal form to be shown
779
	 * @param string $app
780
	 * @param string $page
781
	 * @deprecated Register settings panels in info.xml
782
	 */
783
	public static function registerPersonal($app, $page) {
784
		self::$personalForms[] = [
785
			'appId' => $app,
786
			'page' => $app . '/' . $page . '.php',
787
		];
788
	}
789
790
	/**
791
	 * @param array $entry
792
	 */
793
	public static function registerLogIn(array $entry) {
794
		self::$altLogin[] = $entry;
795
	}
796
797
	/**
798
	 * @return array
799
	 */
800
	public static function getAlternativeLogIns() {
801
		return self::$altLogin;
802
	}
803
804
	/**
805
	 * get a list of all apps in the apps folder
806
	 *
807
	 * @return array an array of app names (string IDs)
808
	 * @todo: change the name of this method to getInstalledApps, which is more accurate
809
	 */
810
	public static function getAllApps() {
811
812
		$apps = [];
813
814
		foreach (OC::$APPSROOTS as $apps_dir) {
815
			if (!is_readable($apps_dir['path'])) {
816
				\OCP\Util::writeLog('core', 'unable to read app folder : ' . $apps_dir['path'], \OCP\Util::WARN);
817
				continue;
818
			}
819
			$dh = opendir($apps_dir['path']);
820
821
			if (is_resource($dh)) {
822
				while (($file = readdir($dh)) !== false) {
823
824
					if ($file[0] != '.' and is_dir($apps_dir['path'] . '/' . $file) and is_file($apps_dir['path'] . '/' . $file . '/appinfo/info.xml')) {
825
826
						$apps[] = $file;
827
					}
828
				}
829
			}
830
		}
831
832
		return $apps;
833
	}
834
835
	/**
836
	 * List all apps, this is used in apps.php
837
	 *
838
	 * @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...
839
	 * @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...
840
	 *                                in the app store?
841
	 * @return array
842
	 */
843
	public static function listAllApps() {
844
		$installedApps = OC_App::getAllApps();
845
846
		//TODO which apps do we want to blacklist and how do we integrate
847
		// blacklisting with the multi apps folder feature?
848
849
		//we don't want to show configuration for these
850
		$blacklist = \OC::$server->getAppManager()->getAlwaysEnabledApps();
851
		$appList = [];
852
		$urlGenerator = \OC::$server->getURLGenerator();
853
854
		foreach ($installedApps as $app) {
855
			if (array_search($app, $blacklist) === false) {
856
857
				$info = OC_App::getAppInfo($app);
858
				if (!is_array($info)) {
859
					\OCP\Util::writeLog('core', 'Could not read app info file for app "' . $app . '"', \OCP\Util::ERROR);
860
					continue;
861
				}
862
863
				if (!isset($info['name'])) {
864
					\OCP\Util::writeLog('core', 'App id "' . $app . '" has no name in appinfo', \OCP\Util::ERROR);
865
					continue;
866
				}
867
868
				$enabled = \OC::$server->getAppConfig()->getValue($app, 'enabled', 'no');
869
				$info['groups'] = null;
870
				if ($enabled === 'yes') {
871
					$active = true;
872
				} else if ($enabled === 'no') {
873
					$active = false;
874
				} else {
875
					$active = true;
876
					$info['groups'] = $enabled;
877
				}
878
879
				$info['active'] = $active;
880
881
				if (self::isShipped($app)) {
882
					$info['internal'] = true;
883
					$info['level'] = self::officialApp;
884
					$info['removable'] = false;
885
				} else {
886
					$result = \OC::$server->getIntegrityCodeChecker()->verifyAppSignature($app, '', true);
887
					if (empty($result)) {
888
						$info['internal'] = false;
889
						$info['level'] = self::approvedApp;
890
						$info['removable'] = false;
891
					}
892
893
					$info['internal'] = false;
894
					$info['removable'] = true;
895
				}
896
897
				$appPath = self::getAppPath($app);
898
				if($appPath !== false) {
899
					$appIcon = $appPath . '/img/' . $app . '.svg';
900
					if (file_exists($appIcon)) {
901
						$info['preview'] = \OC::$server->getURLGenerator()->imagePath($app, $app . '.svg');
902
						$info['previewAsIcon'] = true;
903
					} else {
904
						$appIcon = $appPath . '/img/app.svg';
905
						if (file_exists($appIcon)) {
906
							$info['preview'] = \OC::$server->getURLGenerator()->imagePath($app, 'app.svg');
907
							$info['previewAsIcon'] = true;
908
						}
909
					}
910
				}
911
				// fix documentation
912
				if (isset($info['documentation']) && is_array($info['documentation'])) {
913
					foreach ($info['documentation'] as $key => $url) {
914
						// If it is not an absolute URL we assume it is a key
915
						// i.e. admin-ldap will get converted to go.php?to=admin-ldap
916
						if (stripos($url, 'https://') !== 0 && stripos($url, 'http://') !== 0) {
917
							$url = $urlGenerator->linkToDocs($url);
918
						}
919
920
						$info['documentation'][$key] = $url;
921
					}
922
				}
923
924
				$info['version'] = OC_App::getAppVersion($app);
925
				$appList[] = $info;
926
			}
927
		}
928
929
		return $appList;
930
	}
931
932
	/**
933
	 * Returns the internal app ID or false
934
	 * @param string $ocsID
935
	 * @return string|false
936
	 */
937
	public static function getInternalAppIdByOcs($ocsID) {
938
		if(is_numeric($ocsID)) {
939
			$idArray = \OC::$server->getAppConfig()->getValues(false, 'ocsid');
940
			if(array_search($ocsID, $idArray)) {
941
				return array_search($ocsID, $idArray);
942
			}
943
		}
944
		return false;
945
	}
946
947
	public static function shouldUpgrade($app) {
948
		$versions = self::getAppVersions();
949
		$currentVersion = OC_App::getAppVersion($app);
950
		if ($currentVersion && isset($versions[$app])) {
951
			$installedVersion = $versions[$app];
952
			if (!version_compare($currentVersion, $installedVersion, '=')) {
953
				return true;
954
			}
955
		}
956
		return false;
957
	}
958
959
	/**
960
	 * Adjust the number of version parts of $version1 to match
961
	 * the number of version parts of $version2.
962
	 *
963
	 * @param string $version1 version to adjust
964
	 * @param string $version2 version to take the number of parts from
965
	 * @return string shortened $version1
966
	 */
967
	private static function adjustVersionParts($version1, $version2) {
968
		$version1 = explode('.', $version1);
969
		$version2 = explode('.', $version2);
970
		// reduce $version1 to match the number of parts in $version2
971
		while (count($version1) > count($version2)) {
972
			array_pop($version1);
973
		}
974
		// if $version1 does not have enough parts, add some
975
		while (count($version1) < count($version2)) {
976
			$version1[] = '0';
977
		}
978
		return implode('.', $version1);
979
	}
980
981
	/**
982
	 * Check whether the current ownCloud version matches the given
983
	 * application's version requirements.
984
	 *
985
	 * The comparison is made based on the number of parts that the
986
	 * app info version has. For example for ownCloud 6.0.3 if the
987
	 * app info version is expecting version 6.0, the comparison is
988
	 * made on the first two parts of the ownCloud version.
989
	 * This means that it's possible to specify "requiremin" => 6
990
	 * and "requiremax" => 6 and it will still match ownCloud 6.0.3.
991
	 *
992
	 * @param string $ocVersion ownCloud version to check against
993
	 * @param array $appInfo app info (from xml)
994
	 *
995
	 * @return boolean true if compatible, otherwise false
996
	 */
997
	public static function isAppCompatible($ocVersion, $appInfo) {
998
		$requireMin = '';
999
		$requireMax = '';
1000
		if (isset($appInfo['dependencies']['owncloud']['@attributes']['min-version'])) {
1001
			$requireMin = $appInfo['dependencies']['owncloud']['@attributes']['min-version'];
1002
		} else if (isset($appInfo['requiremin'])) {
1003
			$requireMin = $appInfo['requiremin'];
1004
		} else if (isset($appInfo['require'])) {
1005
			$requireMin = $appInfo['require'];
1006
		}
1007
1008
		if (isset($appInfo['dependencies']['owncloud']['@attributes']['max-version'])) {
1009
			$requireMax = $appInfo['dependencies']['owncloud']['@attributes']['max-version'];
1010
		} else if (isset($appInfo['requiremax'])) {
1011
			$requireMax = $appInfo['requiremax'];
1012
		}
1013
1014
		if (is_array($ocVersion)) {
1015
			$ocVersion = implode('.', $ocVersion);
1016
		}
1017
1018 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...
1019
			&& version_compare(self::adjustVersionParts($ocVersion, $requireMin), $requireMin, '<')
1020
		) {
1021
1022
			return false;
1023
		}
1024
1025 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...
1026
			&& version_compare(self::adjustVersionParts($ocVersion, $requireMax), $requireMax, '>')
1027
		) {
1028
			return false;
1029
		}
1030
1031
		return true;
1032
	}
1033
1034
	/**
1035
	 * get the installed version of all apps
1036
	 */
1037
	public static function getAppVersions() {
1038
		static $versions;
1039
1040
		if(!$versions) {
1041
			$appConfig = \OC::$server->getAppConfig();
1042
			$versions = $appConfig->getValues(false, 'installed_version');
1043
		}
1044
		return $versions;
1045
	}
1046
1047
	/**
1048
	 * update the database for the app and call the update script
1049
	 *
1050
	 * @param string $appId
1051
	 * @return bool
1052
	 */
1053
	public static function updateApp($appId) {
1054
		$appPath = self::getAppPath($appId);
1055
		if($appPath === false) {
1056
			return false;
1057
		}
1058
		$appData = self::getAppInfo($appId);
1059
		self::executeRepairSteps($appId, $appData['repair-steps']['pre-migration']);
1060 View Code Duplication
		if (isset($appData['use-migrations']) && $appData['use-migrations'] === 'true') {
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...
1061
			$ms = new \OC\DB\MigrationService($appId, \OC::$server->getDatabaseConnection());
1062
			$ms->migrate();
1063
		} else {
1064
			if (file_exists($appPath . '/appinfo/database.xml')) {
1065
				OC_DB::updateDbFromStructure($appPath . '/appinfo/database.xml');
1066
			}
1067
		}
1068
		self::executeRepairSteps($appId, $appData['repair-steps']['post-migration']);
1069
		self::setupLiveMigrations($appId, $appData['repair-steps']['live-migration']);
1070
		unset(self::$appVersion[$appId]);
1071
		unset(self::$appInfo[$appId]);
1072
		// run upgrade code
1073
		if (file_exists($appPath . '/appinfo/update.php')) {
1074
			self::loadApp($appId, false);
1075
			include $appPath . '/appinfo/update.php';
1076
		}
1077
		self::setupBackgroundJobs($appData['background-jobs']);
1078
1079
		//set remote/public handlers
1080
		if (array_key_exists('ocsid', $appData)) {
1081
			\OC::$server->getConfig()->setAppValue($appId, 'ocsid', $appData['ocsid']);
1082 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...
1083
			\OC::$server->getConfig()->deleteAppValue($appId, 'ocsid');
1084
		}
1085 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...
1086
			\OC::$server->getConfig()->setAppValue('core', 'remote_' . $name, $appId . '/' . $path);
1087
		}
1088
		foreach ($appData['public'] as $name => $path) {
1089
			\OC::$server->getConfig()->setAppValue('core', 'public_' . $name, $appId . '/' . $path);
1090
		}
1091
1092
		self::setAppTypes($appId);
1093
1094
		$version = \OC_App::getAppVersion($appId);
1095
		\OC::$server->getAppConfig()->setValue($appId, 'installed_version', $version);
1096
1097
		return true;
1098
	}
1099
1100
	/**
1101
	 * @param string $appId
1102
	 * @param string[] $steps
1103
	 * @throws \OC\NeedsUpdateException
1104
	 */
1105
	public static function executeRepairSteps($appId, array $steps) {
1106
		if (empty($steps)) {
1107
			return;
1108
		}
1109
		// load the app
1110
		self::loadApp($appId, false);
1111
1112
		$dispatcher = OC::$server->getEventDispatcher();
1113
1114
		// load the steps
1115
		$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...
1116
		foreach ($steps as $step) {
1117
			try {
1118
				$r->addStep($step);
1119
			} catch (Exception $ex) {
1120
				$r->emit('\OC\Repair', 'error', [$ex->getMessage()]);
1121
				\OC::$server->getLogger()->logException($ex);
1122
			}
1123
		}
1124
		// run the steps
1125
		$r->run();
1126
	}
1127
1128
	public static function setupBackgroundJobs(array $jobs) {
1129
		$queue = \OC::$server->getJobList();
1130
		foreach ($jobs as $job) {
1131
			$queue->add($job);
1132
		}
1133
	}
1134
1135
	/**
1136
	 * @param string $appId
1137
	 * @param string[] $steps
1138
	 */
1139
	private static function setupLiveMigrations($appId, array $steps) {
1140
		$queue = \OC::$server->getJobList();
1141
		foreach ($steps as $step) {
1142
			$queue->add('OC\Migration\BackgroundRepair', [
1143
				'app' => $appId,
1144
				'step' => $step]);
1145
		}
1146
	}
1147
1148
	/**
1149
	 * @param string $appId
1150
	 * @return \OC\Files\View|false
1151
	 */
1152
	public static function getStorage($appId) {
1153
		if (OC_App::isEnabled($appId)) { //sanity check
1154
			if (OC_User::isLoggedIn()) {
1155
				$view = new \OC\Files\View('/' . OC_User::getUser());
1156
				if (!$view->file_exists($appId)) {
1157
					$view->mkdir($appId);
1158
				}
1159
				return new \OC\Files\View('/' . OC_User::getUser() . '/' . $appId);
1160
			} else {
1161
				\OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ', user not logged in', \OCP\Util::ERROR);
1162
				return false;
1163
			}
1164
		} else {
1165
			\OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ' not enabled', \OCP\Util::ERROR);
1166
			return false;
1167
		}
1168
	}
1169
1170
	/**
1171
	 * parses the app data array and enhanced the 'description' value
1172
	 *
1173
	 * @param array $data the app data
1174
	 * @return array improved app data
1175
	 */
1176
	public static function parseAppInfo(array $data) {
1177
1178
		// just modify the description if it is available
1179
		// otherwise this will create a $data element with an empty 'description'
1180
		if (isset($data['description'])) {
1181
			if (is_string($data['description'])) {
1182
				// sometimes the description contains line breaks and they are then also
1183
				// shown in this way in the app management which isn't wanted as HTML
1184
				// manages line breaks itself
1185
1186
				// first of all we split on empty lines
1187
				$paragraphs = preg_split("!\n[[:space:]]*\n!mu", $data['description']);
1188
1189
				$result = [];
1190
				foreach ($paragraphs as $value) {
1191
					// replace multiple whitespace (tabs, space, newlines) inside a paragraph
1192
					// with a single space - also trims whitespace
1193
					$result[] = trim(preg_replace('![[:space:]]+!mu', ' ', $value));
1194
				}
1195
1196
				// join the single paragraphs with a empty line in between
1197
				$data['description'] = implode("\n\n", $result);
1198
1199
			} else {
1200
				$data['description'] = '';
1201
			}
1202
		}
1203
1204
		return $data;
1205
	}
1206
1207
	/**
1208
	 * @param OCP\IConfig $config
1209
	 * @param OCP\IL10N $l
1210
	 * @param array $info
1211
	 * @throws Exception
1212
	 */
1213
	protected static function checkAppDependencies($config, $l, $info) {
1214
		$dependencyAnalyzer = new DependencyAnalyzer(new Platform($config), $l);
1215
		$missing = $dependencyAnalyzer->analyze($info);
1216
		if (!empty($missing)) {
1217
			$missingMsg = join(PHP_EOL, $missing);
1218
			throw new \Exception(
1219
				$l->t('App "%s" cannot be installed because the following dependencies are not fulfilled: %s',
1220
					[$info['name'], $missingMsg]
1221
				)
1222
			);
1223
		}
1224
	}
1225
}
1226