Completed
Push — master ( 6481a3...106a6a )
by Thomas
19:00
created

OC_App::getCurrentApp()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 17
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 13
nc 6
nop 0
dl 0
loc 17
rs 9.2
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
use OC\HintException;
53
54
/**
55
 * This class manages the apps. It allows them to register and integrate in the
56
 * ownCloud ecosystem. Furthermore, this class is responsible for installing,
57
 * upgrading and removing apps.
58
 */
59
class OC_App {
60
	static private $appVersion = [];
61
	static private $adminForms = [];
62
	static private $personalForms = [];
63
	static private $appInfo = [];
64
	static private $appTypes = [];
65
	static private $loadedApps = [];
66
	static private $loadedTypes = [];
67
	static private $altLogin = [];
68
	const officialApp = 200;
69
	const approvedApp = 100;
70
71
	/**
72
	 * clean the appId
73
	 *
74
	 * @param string|boolean $app AppId that needs to be cleaned
75
	 * @return string
76
	 */
77
	public static function cleanAppId($app) {
78
		return str_replace(['\0', '/', '\\', '..'], '', $app);
79
	}
80
81
	/**
82
	 * Check if an app is loaded
83
	 *
84
	 * @param string $app
85
	 * @return bool
86
	 */
87
	public static function isAppLoaded($app) {
88
		return in_array($app, self::$loadedApps, true);
89
	}
90
91
	/**
92
	 * loads all apps
93
	 *
94
	 * @param string[] | string | null $types
95
	 * @return bool
96
	 *
97
	 * This function walks through the ownCloud directory and loads all apps
98
	 * it can find. A directory contains an app if the file /appinfo/info.xml
99
	 * exists.
100
	 *
101
	 * if $types is set, only apps of those types will be loaded
102
	 */
103
	public static function loadApps($types = null) {
104
		if (is_array($types) && !array_diff($types, self::$loadedTypes)) {
105
			return true;
106
		}
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
			if (self::isAppLoaded($app)) {
116
				continue;
117
			}
118
			$path = self::getAppPath($app);
119
			if($path !== false) {
120
				self::registerAutoloading($app, $path);
121
			}
122
		}
123
124
		// prevent app.php from printing output
125
		ob_start();
126
		foreach ($apps as $app) {
127
			if ((is_null($types) or self::isType($app, $types)) && !in_array($app, self::$loadedApps)) {
128
				self::loadApp($app);
129
			}
130
		}
131
		ob_end_clean();
132
133
		// once all authentication apps are loaded we can validate the session
134
		if (is_null($types) || in_array('authentication', $types)) {
135
			if (\OC::$server->getUserSession()) {
136
				$davUser = \OC::$server->getUserSession()->getSession()->get(\OCA\DAV\Connector\Sabre\Auth::DAV_AUTHENTICATED);
137
				if (is_null($davUser)) {
138
					\OC::$server->getUserSession()->validateSession();
139
				}
140
			}
141
		}
142
		if (is_array($types)) {
143
			self::$loadedTypes = array_merge(self::$loadedTypes, $types);
144
		}
145
146
		\OC_Hook::emit('OC_App', 'loadedApps');
147
		return true;
148
	}
149
150
	/**
151
	 * load a single app
152
	 *
153
	 * @param string $app
154
	 * @param bool $checkUpgrade whether an upgrade check should be done
155
	 * @throws \OC\NeedsUpdateException
156
	 */
157
	public static function loadApp($app, $checkUpgrade = true) {
158
		self::$loadedApps[] = $app;
159
		$appPath = self::getAppPath($app);
160
		if($appPath === false) {
161
			return;
162
		}
163
164
		// in case someone calls loadApp() directly
165
		self::registerAutoloading($app, $appPath);
166
167
		self::enableThemeIfApplicable($app);
168
169
		if (is_file($appPath . '/appinfo/app.php')) {
170
			\OC::$server->getEventLogger()->start('load_app_' . $app, 'Load app: ' . $app);
171
			if ($checkUpgrade and self::shouldUpgrade($app)) {
172
				throw new \OC\NeedsUpdateException();
173
			}
174
			self::requireAppFile($app);
175
			if (self::isType($app, ['authentication'])) {
176
				// since authentication apps affect the "is app enabled for group" check,
177
				// the enabled apps cache needs to be cleared to make sure that the
178
				// next time getEnableApps() is called it will also include apps that were
179
				// enabled for groups
180
				self::$enabledAppsCache = [];
181
			}
182
			\OC::$server->getEventLogger()->end('load_app_' . $app);
183
		}
184
	}
185
186
	/**
187
	 * Enables the app as a theme if it has the type "theme"
188
	 * @param string $app
189
	 */
190
	private static function enableThemeIfApplicable($app) {
191
		if (self::isType($app, 'theme')) {
192
			/** @var \OC\Theme\ThemeService $themeManager */
193
			$themeManager = \OC::$server->query('ThemeService');
194
			$themeManager->setAppTheme($app);
195
		}
196
	}
197
198
	/**
199
	 * @internal
200
	 * @param string $app
201
	 * @param string $path
202
	 */
203
	public static function registerAutoloading($app, $path) {
204
		// Register on PSR-4 composer autoloader
205
		$appNamespace = \OC\AppFramework\App::buildAppNamespace($app);
206
		\OC::$composerAutoloader->addPsr4($appNamespace . '\\', $path . '/lib/', true);
207
		if (defined('PHPUNIT_RUN')) {
208
			\OC::$composerAutoloader->addPsr4($appNamespace . '\\Tests\\', $path . '/tests/', true);
209
		}
210
211
		// Register on legacy autoloader
212
		\OC::$loader->addValidRoot($path);
213
	}
214
215
	/**
216
	 * Load app.php from the given app
217
	 *
218
	 * @param string $app app name
219
	 */
220
	private static function requireAppFile($app) {
221
		try {
222
			// encapsulated here to avoid variable scope conflicts
223
			require_once $app . '/appinfo/app.php';
224
		} catch (Exception $ex) {
225
			\OC::$server->getLogger()->logException($ex);
226
			$blacklist = \OC::$server->getAppManager()->getAlwaysEnabledApps();
227 View Code Duplication
			if (!in_array($app, $blacklist)) {
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...
228
				\OC::$server->getLogger()->warning('Could not load app "' . $app . '", it will be disabled', array('app' => 'core'));
229
				self::disable($app);
230
			}
231
			throw $ex;
232
		}
233
	}
234
235
	/**
236
	 * check if an app is of a specific type
237
	 *
238
	 * @param string $app
239
	 * @param string|array $types
240
	 * @return bool
241
	 */
242
	public static function isType($app, $types) {
243
		if (is_string($types)) {
244
			$types = [$types];
245
		}
246
		$appTypes = self::getAppTypes($app);
247
		foreach ($types as $type) {
248
			if (array_search($type, $appTypes) !== false) {
249
				return true;
250
			}
251
		}
252
		return false;
253
	}
254
255
	/**
256
	 * get the types of an app
257
	 *
258
	 * @param string $app
259
	 * @return array
260
	 */
261
	private static function getAppTypes($app) {
262
		//load the cache
263
		if (count(self::$appTypes) == 0) {
264
			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...
265
		}
266
267
		if (isset(self::$appTypes[$app])) {
268
			return explode(',', self::$appTypes[$app]);
269
		} else {
270
			return [];
271
		}
272
	}
273
274
	/**
275
	 * read app types from info.xml and cache them in the database
276
	 */
277
	public static function setAppTypes($app) {
278
		$appData = self::getAppInfo($app);
279
		if(!is_array($appData)) {
280
			return;
281
		}
282
283
		if (isset($appData['types'])) {
284
			$appTypes = implode(',', $appData['types']);
285
		} else {
286
			$appTypes = '';
287
		}
288
289
		\OC::$server->getAppConfig()->setValue($app, 'types', $appTypes);
290
	}
291
292
	/**
293
	 * check if app is shipped
294
	 *
295
	 * @param string $appId the id of the app to check
296
	 * @return bool
297
	 *
298
	 * Check if an app that is installed is a shipped app or installed from the appstore.
299
	 */
300
	public static function isShipped($appId) {
301
		return \OC::$server->getAppManager()->isShipped($appId);
302
	}
303
304
	/**
305
	 * get all enabled apps
306
	 */
307
	protected static $enabledAppsCache = [];
308
309
	/**
310
	 * Returns apps enabled for the current user.
311
	 *
312
	 * @param bool $forceRefresh whether to refresh the cache
313
	 * @param bool $all whether to return apps for all users, not only the
314
	 * currently logged in one
315
	 * @return string[]
316
	 */
317
	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...
318
		if (!\OC::$server->getSystemConfig()->getValue('installed', false)) {
319
			return [];
320
		}
321
		// in incognito mode or when logged out, $user will be false,
322
		// which is also the case during an upgrade
323
		$appManager = \OC::$server->getAppManager();
324
		if ($all) {
325
			$user = null;
326
		} else {
327
			$user = \OC::$server->getUserSession()->getUser();
328
		}
329
330
		if (is_null($user)) {
331
			$apps = $appManager->getInstalledApps();
332
		} else {
333
			$apps = $appManager->getEnabledAppsForUser($user);
334
		}
335
		$apps = array_filter($apps, function ($app) {
336
			return $app !== 'files';//we add this manually
337
		});
338
		sort($apps);
339
		array_unshift($apps, 'files');
340
		return $apps;
341
	}
342
343
	/**
344
	 * checks whether or not an app is enabled
345
	 *
346
	 * @param string $app app
347
	 * @return bool
348
	 *
349
	 * This function checks whether or not an app is enabled.
350
	 */
351
	public static function isEnabled($app) {
352
		return \OC::$server->getAppManager()->isEnabledForUser($app);
353
	}
354
355
	/**
356
	 * enables an app
357
	 *
358
	 * @param mixed $app app
359
	 * @param array $groups (optional) when set, only these groups will have access to the app
360
	 * @throws \Exception
361
	 * @return void
362
	 *
363
	 * This function set an app as enabled in appconfig.
364
	 */
365
	public static function enable($app, $groups = null) {
366
		self::$enabledAppsCache = []; // flush
367
368
		// check for required dependencies
369
		$config = \OC::$server->getConfig();
370
		$l = \OC::$server->getL10N('core');
371
		$info = self::getAppInfo($app);
372
		if ($info === null) {
373
			throw new \Exception("$app can't be enabled since it is not installed.");
374
		}
375
376
		self::checkAppDependencies($config, $l, $info);
377
378
		if (!Installer::isInstalled($app)) {
379
			Installer::installShippedApp($app);
380
		}
381
382
		$appManager = \OC::$server->getAppManager();
383
		if (!is_null($groups)) {
384
			$groupManager = \OC::$server->getGroupManager();
385
			$groupsList = [];
386
			foreach ($groups as $group) {
387
				$groupItem = $groupManager->get($group);
388
				if ($groupItem instanceof \OCP\IGroup) {
389
					$groupsList[] = $groupManager->get($group);
390
				}
391
			}
392
			$appManager->enableAppForGroups($app, $groupsList);
393
		} else {
394
			$appManager->enableApp($app);
395
		}
396
	}
397
398
	/**
399
	 * @param string $app
400
	 * @return bool
401
	 */
402
	public static function removeApp($app) {
403
		if (self::isShipped($app)) {
404
			return false;
405
		}
406
407
		return Installer::removeApp($app);
408
	}
409
410
	/**
411
	 * This function set an app as disabled in appconfig.
412
	 *
413
	 * @param string $app app
414
	 * @throws Exception
415
	 */
416
	public static function disable($app) {
417
		// Convert OCS ID to regular application identifier
418
		if(self::getInternalAppIdByOcs($app) !== false) {
419
			$app = self::getInternalAppIdByOcs($app);
420
		}
421
422
		// flush
423
		self::$enabledAppsCache = [];
424
425
		// run uninstall steps
426
		$appData = OC_App::getAppInfo($app);
427
		if (!is_null($appData)) {
428
			OC_App::executeRepairSteps($app, $appData['repair-steps']['uninstall']);
429
		}
430
431
		// emit disable hook - needed anymore ?
432
		\OC_Hook::emit('OC_App', 'pre_disable', ['app' => $app]);
433
434
		// finally disable it
435
		$appManager = \OC::$server->getAppManager();
436
		$appManager->disableApp($app);
437
	}
438
439
	/**
440
	 * Returns the Settings Navigation
441
	 *
442
	 * @return string[]
443
	 *
444
	 * This function returns an array containing all settings pages added. The
445
	 * entries are sorted by the key 'order' ascending.
446
	 */
447
	public static function getSettingsNavigation() {
448
		$l = \OC::$server->getL10N('lib');
449
		$urlGenerator = \OC::$server->getURLGenerator();
450
451
		$settings = [];
452
		// by default, settings only contain the help menu
453
		if (OC_Util::getEditionString() === OC_Util::EDITION_COMMUNITY &&
454
			\OC::$server->getSystemConfig()->getValue('knowledgebaseenabled', true) == true
455
		) {
456
			$settings = [
457
				[
458
					"id" => "help",
459
					"order" => 1000,
460
					"href" => $urlGenerator->linkToRoute('settings_help'),
461
					"name" => $l->t("Help"),
462
					"icon" => $urlGenerator->imagePath("settings", "help.svg")
463
				]
464
			];
465
		}
466
467
		// if the user is logged-in
468
		if (OC_User::isLoggedIn()) {
469
			// personal menu
470
			$settings[] = [
471
				"id" => "settings",
472
				"order" => 1,
473
				"href" => $urlGenerator->linkToRoute('settings.SettingsPage.getPersonal'),
474
				"name" => $l->t("Settings"),
475
				"icon" => $urlGenerator->imagePath("settings", "admin.svg")
476
			];
477
478
			//SubAdmins are also allowed to access user management
479
			$userObject = \OC::$server->getUserSession()->getUser();
480
			$isSubAdmin = false;
481
			if($userObject !== null) {
482
				$isSubAdmin = \OC::$server->getGroupManager()->getSubAdmin()->isSubAdmin($userObject);
483
			}
484
			if ($isSubAdmin) {
485
				// admin users menu
486
				$settings[] = [
487
					"id" => "core_users",
488
					"order" => 2,
489
					"href" => $urlGenerator->linkToRoute('settings_users'),
490
					"name" => $l->t("Users"),
491
					"icon" => $urlGenerator->imagePath("settings", "users.svg")
492
				];
493
			}
494
495
		}
496
497
		$navigation = self::proceedNavigation($settings);
498
		return $navigation;
499
	}
500
501
	// This is private as well. It simply works, so don't ask for more details
502
	private static function proceedNavigation($list) {
503
		$activeApp = OC::$server->getNavigationManager()->getActiveEntry();
504
		foreach ($list as &$navEntry) {
505
			if ($navEntry['id'] == $activeApp) {
506
				$navEntry['active'] = true;
507
			} else {
508
				$navEntry['active'] = false;
509
			}
510
		}
511
		unset($navEntry);
512
513
		usort($list, create_function('$a, $b', 'if( $a["order"] == $b["order"] ) {return 0;}elseif( $a["order"] < $b["order"] ) {return -1;}else{return 1;}'));
514
515
		return $list;
516
	}
517
518
	/**
519
	 * Get the path where to install apps
520
	 *
521
	 * @return string|false
522
	 */
523
	public static function getInstallPath() {
524
		foreach (OC::$APPSROOTS as $dir) {
525
			if (isset($dir['writable']) && $dir['writable'] === true) {
526
				return $dir['path'];
527
			}
528
		}
529
530
		\OCP\Util::writeLog('core', 'No application directories are marked as writable.', \OCP\Util::ERROR);
531
		return null;
532
	}
533
534
535
	/**
536
	 * search for an app in all app-directories
537
	 *
538
	 * @param string $appId
539
	 * @return false|string
540
	 */
541
	protected static function findAppInDirectories($appId) {
542
		$sanitizedAppId = self::cleanAppId($appId);
543
		if($sanitizedAppId !== $appId) {
544
			return false;
545
		}
546
		static $app_dir = [];
547
548
		if (isset($app_dir[$appId])) {
549
			return $app_dir[$appId];
550
		}
551
552
		$possibleApps = [];
553
		foreach (OC::$APPSROOTS as $dir) {
554
			if (file_exists($dir['path'] . '/' . $appId)) {
555
				$possibleApps[] = $dir;
556
			}
557
		}
558
559
		if (empty($possibleApps)) {
560
			return false;
561
		} elseif (count($possibleApps) === 1) {
562
			$dir = array_shift($possibleApps);
563
			$app_dir[$appId] = $dir;
564
			return $dir;
565
		} else {
566
			$versionToLoad = [];
567
			foreach ($possibleApps as $possibleApp) {
568
				$version = self::getAppVersionByPath($possibleApp['path'] . '/' . $appId);
569
				if (empty($versionToLoad) || version_compare($version, $versionToLoad['version'], '>')) {
570
					$versionToLoad = [
571
						'dir' => $possibleApp,
572
						'version' => $version,
573
					];
574
				}
575
			}
576
			$app_dir[$appId] = $versionToLoad['dir'];
577
			return $versionToLoad['dir'];
578
			//TODO - write test
579
		}
580
	}
581
582
	/**
583
	 * Get the directory for the given app.
584
	 * If the app is defined in multiple directories, the first one is taken. (false if not found)
585
	 *
586
	 * @param string $appId
587
	 * @return string|false
588
	 */
589
	public static function getAppPath($appId) {
590
		if ($appId === null || trim($appId) === '') {
591
			return false;
592
		}
593
594
		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...
595
			return $dir['path'] . '/' . $appId;
596
		}
597
		return false;
598
	}
599
600
601
	/**
602
	 * check if an app's directory is writable
603
	 *
604
	 * @param string $appId
605
	 * @return bool
606
	 */
607
	public static function isAppDirWritable($appId) {
608
		$path = self::getAppPath($appId);
609
		return ($path !== false) ? is_writable($path) : false;
610
	}
611
612
	/**
613
	 * Get the path for the given app on the access
614
	 * If the app is defined in multiple directories, the first one is taken. (false if not found)
615
	 *
616
	 * @param string $appId
617
	 * @return string|false
618
	 */
619
	public static function getAppWebPath($appId) {
620
		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...
621
			return OC::$WEBROOT . $dir['url'] . '/' . $appId;
622
		}
623
		return false;
624
	}
625
626
	/**
627
	 * get the last version of the app from appinfo/info.xml
628
	 *
629
	 * @param string $appId
630
	 * @return string
631
	 */
632
	public static function getAppVersion($appId) {
633
		if (!isset(self::$appVersion[$appId])) {
634
			$file = self::getAppPath($appId);
635
			self::$appVersion[$appId] = ($file !== false) ? self::getAppVersionByPath($file) : '0';
636
		}
637
		return self::$appVersion[$appId];
638
	}
639
640
	/**
641
	 * get app's version based on it's path
642
	 *
643
	 * @param string $path
644
	 * @return string
645
	 */
646
	public static function getAppVersionByPath($path) {
647
		$infoFile = $path . '/appinfo/info.xml';
648
		$appData = self::getAppInfo($infoFile, true);
649
		return isset($appData['version']) ? $appData['version'] : '';
650
	}
651
652
	/**
653
	 * @return false|string
654
	 */
655
	public static function getDefaultEnabledAppTheme() {
656
		$apps = self::getAllApps();
657
		$parser = new InfoParser();
658
		foreach ($apps as $app) {
659
			$info = $parser->parse(self::getAppPath($app) . '/appinfo/info.xml');
660
			if (is_array($info)) {
661
				$info = OC_App::parseAppInfo($info);
662
			}
663
			if (isset($info['default_enable']) && in_array('theme', $info['types'])) {
664
				return $app;
665
			}
666
		}
667
		return false;
668
	}
669
670
	/**
671
	 * Read all app metadata from the info.xml file
672
	 *
673
	 * @param string $appId id of the app or the path of the info.xml file
674
	 * @param boolean $path (optional)
675
	 * @return array|null
676
	 * @note all data is read from info.xml, not just pre-defined fields
677
	 */
678
	public static function getAppInfo($appId, $path = false) {
679
		if ($path) {
680
			$file = $appId;
681
		} else {
682
			if (isset(self::$appInfo[$appId])) {
683
				return self::$appInfo[$appId];
684
			}
685
			$appPath = self::getAppPath($appId);
686
			if($appPath === false) {
687
				return null;
688
			}
689
			$file = $appPath . '/appinfo/info.xml';
690
		}
691
692
		$parser = new InfoParser();
693
		$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 707 which is incompatible with the return type documented by OC_App::getAppInfo of type array|null.
Loading history...
694
695
		if (is_array($data)) {
696
			$data = OC_App::parseAppInfo($data);
697
		}
698
		if(isset($data['ocsid'])) {
699
			$storedId = \OC::$server->getConfig()->getAppValue($appId, 'ocsid');
700
			if($storedId !== '' && $storedId !== $data['ocsid']) {
701
				$data['ocsid'] = $storedId;
702
			}
703
		}
704
705
		self::$appInfo[$appId] = $data;
706
707
		return $data;
708
	}
709
710
	/**
711
	 * Returns the navigation
712
	 *
713
	 * @return array
714
	 *
715
	 * This function returns an array containing all entries added. The
716
	 * entries are sorted by the key 'order' ascending. Additional to the keys
717
	 * given for each app the following keys exist:
718
	 *   - active: boolean, signals if the user is on this navigation entry
719
	 */
720
	public static function getNavigation() {
721
		$entries = OC::$server->getNavigationManager()->getAll();
722
		$navigation = self::proceedNavigation($entries);
723
		return $navigation;
724
	}
725
726
	/**
727
	 * get the id of loaded app
728
	 *
729
	 * @return string
730
	 */
731
	public static function getCurrentApp() {
732
		$request = \OC::$server->getRequest();
733
		$script = substr($request->getScriptName(), strlen(OC::$WEBROOT) + 1);
734
		$topFolder = substr($script, 0, strpos($script, '/'));
735
		if (empty($topFolder)) {
736
			$path_info = $request->getPathInfo();
737
			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...
738
				$topFolder = substr($path_info, 1, strpos($path_info, '/', 1) - 1);
739
			}
740
		}
741
		if ($topFolder == 'apps') {
742
			$length = strlen($topFolder);
743
			return substr($script, $length + 1, strpos($script, '/', $length + 1) - $length - 1);
744
		} else {
745
			return $topFolder;
746
		}
747
	}
748
749
	/**
750
	 * @param string $type
751
	 * @return array
752
	 */
753
	public static function getForms($type) {
754
		$forms = [];
755
		switch ($type) {
756
			case 'admin':
757
				$source = self::$adminForms;
758
				break;
759
			case 'personal':
760
				$source = self::$personalForms;
761
				break;
762
			default:
763
				return [];
764
		}
765
		foreach ($source as $form) {
766
			$page = include $form['page'];
767
			$forms[] = [
768
				'appId' => $form['appId'],
769
				'page' => $page,
770
			];
771
		}
772
		return $forms;
773
	}
774
775
	/**
776
	 * register an admin form to be shown
777
	 *
778
	 * @param string $app
779
	 * @param string $page
780
	 * @deprecated Register settings panels in info.xml
781
	 */
782
	public static function registerAdmin($app, $page) {
783
		self::$adminForms[] = [
784
			'appId' => $app,
785
			'page' => $app . '/' . $page . '.php',
786
		];
787
	}
788
789
	/**
790
	 * register a personal form to be shown
791
	 * @param string $app
792
	 * @param string $page
793
	 * @deprecated Register settings panels in info.xml
794
	 */
795
	public static function registerPersonal($app, $page) {
796
		self::$personalForms[] = [
797
			'appId' => $app,
798
			'page' => $app . '/' . $page . '.php',
799
		];
800
	}
801
802
	/**
803
	 * @param array $entry
804
	 */
805
	public static function registerLogIn(array $entry) {
806
		self::$altLogin[] = $entry;
807
	}
808
809
	/**
810
	 * @return array
811
	 */
812
	public static function getAlternativeLogIns() {
813
		return self::$altLogin;
814
	}
815
816
	/**
817
	 * get a list of all apps in the apps folder
818
	 *
819
	 * @return array an array of app names (string IDs)
820
	 * @todo: change the name of this method to getInstalledApps, which is more accurate
821
	 */
822
	public static function getAllApps() {
823
824
		$apps = [];
825
826
		foreach (OC::$APPSROOTS as $apps_dir) {
827
			if (!is_readable($apps_dir['path'])) {
828
				\OCP\Util::writeLog('core', 'unable to read app folder : ' . $apps_dir['path'], \OCP\Util::WARN);
829
				continue;
830
			}
831
			$dh = opendir($apps_dir['path']);
832
833
			if (is_resource($dh)) {
834
				while (($file = readdir($dh)) !== false) {
835
836
					if ($file[0] != '.' and is_dir($apps_dir['path'] . '/' . $file) and is_file($apps_dir['path'] . '/' . $file . '/appinfo/info.xml')) {
837
838
						$apps[] = $file;
839
					}
840
				}
841
			}
842
		}
843
844
		return $apps;
845
	}
846
847
	/**
848
	 * List all apps, this is used in apps.php
849
	 *
850
	 * @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...
851
	 * @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...
852
	 *                                in the app store?
853
	 * @return array
854
	 */
855
	public static function listAllApps() {
856
		$installedApps = OC_App::getAllApps();
857
858
		//TODO which apps do we want to blacklist and how do we integrate
859
		// blacklisting with the multi apps folder feature?
860
861
		//we don't want to show configuration for these
862
		$blacklist = \OC::$server->getAppManager()->getAlwaysEnabledApps();
863
		$appList = [];
864
		$urlGenerator = \OC::$server->getURLGenerator();
865
866
		foreach ($installedApps as $app) {
867
			if (array_search($app, $blacklist) === false) {
868
869
				$info = OC_App::getAppInfo($app);
870
				if (!is_array($info)) {
871
					\OCP\Util::writeLog('core', 'Could not read app info file for app "' . $app . '"', \OCP\Util::ERROR);
872
					continue;
873
				}
874
875
				if (!isset($info['name'])) {
876
					\OCP\Util::writeLog('core', 'App id "' . $app . '" has no name in appinfo', \OCP\Util::ERROR);
877
					continue;
878
				}
879
880
				$enabled = \OC::$server->getAppConfig()->getValue($app, 'enabled', 'no');
881
				$info['groups'] = null;
882
				if ($enabled === 'yes') {
883
					$active = true;
884
				} else if ($enabled === 'no') {
885
					$active = false;
886
				} else {
887
					$active = true;
888
					$info['groups'] = $enabled;
889
				}
890
891
				$info['active'] = $active;
892
893
				if (self::isShipped($app)) {
894
					$info['internal'] = true;
895
					$info['level'] = self::officialApp;
896
					$info['removable'] = false;
897
				} else {
898
					$result = \OC::$server->getIntegrityCodeChecker()->verifyAppSignature($app, '', true);
899
					if (empty($result)) {
900
						$info['internal'] = false;
901
						$info['level'] = self::approvedApp;
902
						$info['removable'] = false;
903
					}
904
905
					$info['internal'] = false;
906
					$info['removable'] = true;
907
				}
908
909
				$appPath = self::getAppPath($app);
910
				if($appPath !== false) {
911
					$appIcon = $appPath . '/img/' . $app . '.svg';
912
					if (file_exists($appIcon)) {
913
						$info['preview'] = \OC::$server->getURLGenerator()->imagePath($app, $app . '.svg');
914
						$info['previewAsIcon'] = true;
915
					} else {
916
						$appIcon = $appPath . '/img/app.svg';
917
						if (file_exists($appIcon)) {
918
							$info['preview'] = \OC::$server->getURLGenerator()->imagePath($app, 'app.svg');
919
							$info['previewAsIcon'] = true;
920
						}
921
					}
922
				}
923
				// fix documentation
924
				if (isset($info['documentation']) && is_array($info['documentation'])) {
925
					foreach ($info['documentation'] as $key => $url) {
926
						// If it is not an absolute URL we assume it is a key
927
						// i.e. admin-ldap will get converted to go.php?to=admin-ldap
928
						if (stripos($url, 'https://') !== 0 && stripos($url, 'http://') !== 0) {
929
							$url = $urlGenerator->linkToDocs($url);
930
						}
931
932
						$info['documentation'][$key] = $url;
933
					}
934
				}
935
936
				$info['version'] = OC_App::getAppVersion($app);
937
				$appList[] = $info;
938
			}
939
		}
940
941
		return $appList;
942
	}
943
944
	/**
945
	 * Returns the internal app ID or false
946
	 * @param string $ocsID
947
	 * @return string|false
948
	 */
949
	public static function getInternalAppIdByOcs($ocsID) {
950
		if(is_numeric($ocsID)) {
951
			$idArray = \OC::$server->getAppConfig()->getValues(false, 'ocsid');
952
			if(array_search($ocsID, $idArray)) {
953
				return array_search($ocsID, $idArray);
954
			}
955
		}
956
		return false;
957
	}
958
959
	public static function shouldUpgrade($app) {
960
		$versions = self::getAppVersions();
961
		$currentVersion = OC_App::getAppVersion($app);
962
		if ($currentVersion && isset($versions[$app])) {
963
			$installedVersion = $versions[$app];
964
			if (!version_compare($currentVersion, $installedVersion, '=')) {
965
				return true;
966
			}
967
		}
968
		return false;
969
	}
970
971
	/**
972
	 * Adjust the number of version parts of $version1 to match
973
	 * the number of version parts of $version2.
974
	 *
975
	 * @param string $version1 version to adjust
976
	 * @param string $version2 version to take the number of parts from
977
	 * @return string shortened $version1
978
	 */
979
	private static function adjustVersionParts($version1, $version2) {
980
		$version1 = explode('.', $version1);
981
		$version2 = explode('.', $version2);
982
		// reduce $version1 to match the number of parts in $version2
983
		while (count($version1) > count($version2)) {
984
			array_pop($version1);
985
		}
986
		// if $version1 does not have enough parts, add some
987
		while (count($version1) < count($version2)) {
988
			$version1[] = '0';
989
		}
990
		return implode('.', $version1);
991
	}
992
993
	/**
994
	 * Check whether the current ownCloud version matches the given
995
	 * application's version requirements.
996
	 *
997
	 * The comparison is made based on the number of parts that the
998
	 * app info version has. For example for ownCloud 6.0.3 if the
999
	 * app info version is expecting version 6.0, the comparison is
1000
	 * made on the first two parts of the ownCloud version.
1001
	 * This means that it's possible to specify "requiremin" => 6
1002
	 * and "requiremax" => 6 and it will still match ownCloud 6.0.3.
1003
	 *
1004
	 * @param string $ocVersion ownCloud version to check against
1005
	 * @param array $appInfo app info (from xml)
1006
	 *
1007
	 * @return boolean true if compatible, otherwise false
1008
	 */
1009
	public static function isAppCompatible($ocVersion, $appInfo) {
1010
		$requireMin = '';
1011
		$requireMax = '';
1012
		if (isset($appInfo['dependencies']['owncloud']['@attributes']['min-version'])) {
1013
			$requireMin = $appInfo['dependencies']['owncloud']['@attributes']['min-version'];
1014
		} else if (isset($appInfo['requiremin'])) {
1015
			$requireMin = $appInfo['requiremin'];
1016
		} else if (isset($appInfo['require'])) {
1017
			$requireMin = $appInfo['require'];
1018
		}
1019
1020
		if (isset($appInfo['dependencies']['owncloud']['@attributes']['max-version'])) {
1021
			$requireMax = $appInfo['dependencies']['owncloud']['@attributes']['max-version'];
1022
		} else if (isset($appInfo['requiremax'])) {
1023
			$requireMax = $appInfo['requiremax'];
1024
		}
1025
1026
		if (is_array($ocVersion)) {
1027
			$ocVersion = implode('.', $ocVersion);
1028
		}
1029
1030 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...
1031
			&& version_compare(self::adjustVersionParts($ocVersion, $requireMin), $requireMin, '<')
1032
		) {
1033
1034
			return false;
1035
		}
1036
1037 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...
1038
			&& version_compare(self::adjustVersionParts($ocVersion, $requireMax), $requireMax, '>')
1039
		) {
1040
			return false;
1041
		}
1042
1043
		return true;
1044
	}
1045
1046
	/**
1047
	 * get the installed version of all apps
1048
	 */
1049
	public static function getAppVersions() {
1050
		static $versions;
1051
1052
		if(!$versions) {
1053
			$appConfig = \OC::$server->getAppConfig();
1054
			$versions = $appConfig->getValues(false, 'installed_version');
1055
		}
1056
		return $versions;
1057
	}
1058
1059
	/**
1060
	 * update the database for the app and call the update script
1061
	 *
1062
	 * @param string $appId
1063
	 * @return bool
1064
	 */
1065
	public static function updateApp($appId) {
1066
		$appPath = self::getAppPath($appId);
1067
		if($appPath === false) {
1068
			return false;
1069
		}
1070
		$appData = self::getAppInfo($appId);
1071
		self::executeRepairSteps($appId, $appData['repair-steps']['pre-migration']);
1072 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...
1073
			$ms = new \OC\DB\MigrationService($appId, \OC::$server->getDatabaseConnection());
1074
			$ms->migrate();
1075
		} else {
1076
			if (file_exists($appPath . '/appinfo/database.xml')) {
1077
				OC_DB::updateDbFromStructure($appPath . '/appinfo/database.xml');
1078
			}
1079
		}
1080
		self::executeRepairSteps($appId, $appData['repair-steps']['post-migration']);
1081
		self::setupLiveMigrations($appId, $appData['repair-steps']['live-migration']);
1082
		self::clearAppCache($appId);
1083
		// run upgrade code
1084
		if (file_exists($appPath . '/appinfo/update.php')) {
1085
			self::loadApp($appId, false);
1086
			include $appPath . '/appinfo/update.php';
1087
		}
1088
		self::setupBackgroundJobs($appData['background-jobs']);
1089
1090
		//set remote/public handlers
1091
		if (array_key_exists('ocsid', $appData)) {
1092
			\OC::$server->getConfig()->setAppValue($appId, 'ocsid', $appData['ocsid']);
1093 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...
1094
			\OC::$server->getConfig()->deleteAppValue($appId, 'ocsid');
1095
		}
1096 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...
1097
			\OC::$server->getConfig()->setAppValue('core', 'remote_' . $name, $appId . '/' . $path);
1098
		}
1099
		foreach ($appData['public'] as $name => $path) {
1100
			\OC::$server->getConfig()->setAppValue('core', 'public_' . $name, $appId . '/' . $path);
1101
		}
1102
1103
		self::setAppTypes($appId);
1104
1105
		$version = \OC_App::getAppVersion($appId);
1106
		\OC::$server->getAppConfig()->setValue($appId, 'installed_version', $version);
1107
1108
		return true;
1109
	}
1110
1111
	/**
1112
	 * @param string $appId
1113
	 * @param string[] $steps
1114
	 * @throws \OC\NeedsUpdateException
1115
	 */
1116
	public static function executeRepairSteps($appId, array $steps) {
1117
		if (empty($steps)) {
1118
			return;
1119
		}
1120
		// load the app
1121
		self::loadApp($appId, false);
1122
1123
		$dispatcher = OC::$server->getEventDispatcher();
1124
1125
		// load the steps
1126
		$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...
1127
		foreach ($steps as $step) {
1128
			try {
1129
				$r->addStep($step);
1130
			} catch (Exception $ex) {
1131
				$r->emit('\OC\Repair', 'error', [$ex->getMessage()]);
1132
				\OC::$server->getLogger()->logException($ex);
1133
			}
1134
		}
1135
		// run the steps
1136
		$r->run();
1137
	}
1138
1139
	public static function setupBackgroundJobs(array $jobs) {
1140
		$queue = \OC::$server->getJobList();
1141
		foreach ($jobs as $job) {
1142
			$queue->add($job);
1143
		}
1144
	}
1145
1146
	/**
1147
	 * @param string $appId
1148
	 * @param string[] $steps
1149
	 */
1150
	private static function setupLiveMigrations($appId, array $steps) {
1151
		$queue = \OC::$server->getJobList();
1152
		foreach ($steps as $step) {
1153
			$queue->add('OC\Migration\BackgroundRepair', [
1154
				'app' => $appId,
1155
				'step' => $step]);
1156
		}
1157
	}
1158
1159
	/**
1160
	 * @param string $appId
1161
	 * @return \OC\Files\View|false
1162
	 */
1163
	public static function getStorage($appId) {
1164
		if (OC_App::isEnabled($appId)) { //sanity check
1165
			if (OC_User::isLoggedIn()) {
1166
				$view = new \OC\Files\View('/' . OC_User::getUser());
1167
				if (!$view->file_exists($appId)) {
1168
					$view->mkdir($appId);
1169
				}
1170
				return new \OC\Files\View('/' . OC_User::getUser() . '/' . $appId);
1171
			} else {
1172
				\OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ', user not logged in', \OCP\Util::ERROR);
1173
				return false;
1174
			}
1175
		} else {
1176
			\OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ' not enabled', \OCP\Util::ERROR);
1177
			return false;
1178
		}
1179
	}
1180
1181
	/**
1182
	 * parses the app data array and enhanced the 'description' value
1183
	 *
1184
	 * @param array $data the app data
1185
	 * @return array improved app data
1186
	 */
1187
	public static function parseAppInfo(array $data) {
1188
1189
		// just modify the description if it is available
1190
		// otherwise this will create a $data element with an empty 'description'
1191
		if (isset($data['description'])) {
1192
			if (is_string($data['description'])) {
1193
				// sometimes the description contains line breaks and they are then also
1194
				// shown in this way in the app management which isn't wanted as HTML
1195
				// manages line breaks itself
1196
1197
				// first of all we split on empty lines
1198
				$paragraphs = preg_split("!\n[[:space:]]*\n!mu", $data['description']);
1199
1200
				$result = [];
1201
				foreach ($paragraphs as $value) {
1202
					// replace multiple whitespace (tabs, space, newlines) inside a paragraph
1203
					// with a single space - also trims whitespace
1204
					$result[] = trim(preg_replace('![[:space:]]+!mu', ' ', $value));
1205
				}
1206
1207
				// join the single paragraphs with a empty line in between
1208
				$data['description'] = implode("\n\n", $result);
1209
1210
			} else {
1211
				$data['description'] = '';
1212
			}
1213
		}
1214
1215
		return $data;
1216
	}
1217
1218
	/**
1219
	 * @param OCP\IConfig $config
1220
	 * @param OCP\IL10N $l
1221
	 * @param array $info
1222
	 * @throws Exception
1223
	 */
1224
	protected static function checkAppDependencies($config, $l, $info) {
1225
		$dependencyAnalyzer = new DependencyAnalyzer(new Platform($config), $l);
1226
		$missing = $dependencyAnalyzer->analyze($info);
1227
		if (!empty($missing)) {
1228
			$missingMsg = join(PHP_EOL, $missing);
1229
			throw new \Exception(
1230
				$l->t('App "%s" cannot be installed because the following dependencies are not fulfilled: %s',
1231
					[$info['name'], $missingMsg]
1232
				)
1233
			);
1234
		}
1235
	}
1236
1237
	/**
1238
	 * @param $appId
1239
	 */
1240
	public static function clearAppCache($appId) {
1241
		unset(self::$appVersion[$appId]);
1242
		unset(self::$appInfo[$appId]);
1243
	}
1244
}
1245