Completed
Push — master ( e8cbe7...f3c40f )
by Victor
21:02 queued 11:32
created

OC_App::getAppVersions()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 0
dl 0
loc 9
rs 9.6666
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
				$request = \OC::$server->getRequest();
137
				$session = \OC::$server->getUserSession();
138
				$davUser = \OC::$server->getUserSession()->getSession()->get(\OCA\DAV\Connector\Sabre\Auth::DAV_AUTHENTICATED);
139
				if (is_null($davUser)) {
140
					$session->validateSession();
141
				} else {
142
					/** @var \OC\Authentication\Token\DefaultTokenProvider $tokenProvider */
143
					$tokenProvider = \OC::$server->query('\OC\Authentication\Token\DefaultTokenProvider');
144
					$token = null;
145
					try {
146
						$token = $tokenProvider->getToken($session->getSession()->getId());
147
					} catch (\Exception $ex) {
148
						$password = null;
149
						if (isset($_SERVER['PHP_AUTH_PW'])) {
150
							$password = $_SERVER['PHP_AUTH_PW'];
151
						}
152
153
						$session->createSessionToken($request, $session->getUser()->getUID(), $session->getLoginName(), $password);
154
					}
155
156
					if ($token) {
157
						$tokenProvider->updateToken($token);
158
					}
159
				}
160
			}
161
		}
162
		if (is_array($types)) {
163
			self::$loadedTypes = array_merge(self::$loadedTypes, $types);
164
		}
165
166
		\OC_Hook::emit('OC_App', 'loadedApps');
167
		return true;
168
	}
169
170
	/**
171
	 * load a single app
172
	 *
173
	 * @param string $app
174
	 * @param bool $checkUpgrade whether an upgrade check should be done
175
	 * @throws \OC\NeedsUpdateException
176
	 */
177
	public static function loadApp($app, $checkUpgrade = true) {
178
		self::$loadedApps[] = $app;
179
		$appPath = self::getAppPath($app);
180
		if($appPath === false) {
181
			return;
182
		}
183
184
		// in case someone calls loadApp() directly
185
		self::registerAutoloading($app, $appPath);
186
187
		self::enableThemeIfApplicable($app);
188
189
		if (is_file($appPath . '/appinfo/app.php')) {
190
			\OC::$server->getEventLogger()->start('load_app_' . $app, 'Load app: ' . $app);
191
			if ($checkUpgrade and self::shouldUpgrade($app)) {
192
				throw new \OC\NeedsUpdateException();
193
			}
194
			self::requireAppFile($app);
195
			if (self::isType($app, ['authentication'])) {
196
				// since authentication apps affect the "is app enabled for group" check,
197
				// the enabled apps cache needs to be cleared to make sure that the
198
				// next time getEnableApps() is called it will also include apps that were
199
				// enabled for groups
200
				self::$enabledAppsCache = [];
201
			}
202
			\OC::$server->getEventLogger()->end('load_app_' . $app);
203
		}
204
	}
205
206
	/**
207
	 * Enables the app as a theme if it has the type "theme"
208
	 * @param string $app
209
	 */
210
	private static function enableThemeIfApplicable($app) {
211
		if (self::isType($app, 'theme')) {
212
			/** @var \OCP\Theme\IThemeService $themeService */
213
			$themeService = \OC::$server->query('ThemeService');
214
			$themeService->setAppTheme($app);
215
		}
216
	}
217
218
	/**
219
	 * @internal
220
	 * @param string $app
221
	 * @param string $path
222
	 */
223
	public static function registerAutoloading($app, $path) {
224
		// Register on PSR-4 composer autoloader
225
		$appNamespace = \OC\AppFramework\App::buildAppNamespace($app);
226
		\OC::$composerAutoloader->addPsr4($appNamespace . '\\', $path . '/lib/', true);
227
		if (defined('PHPUNIT_RUN')) {
228
			\OC::$composerAutoloader->addPsr4($appNamespace . '\\Tests\\', $path . '/tests/', true);
229
		}
230
231
		// Register on legacy autoloader
232
		\OC::$loader->addValidRoot($path);
233
	}
234
235
	/**
236
	 * Load app.php from the given app
237
	 *
238
	 * @param string $app app name
239
	 */
240
	private static function requireAppFile($app) {
241
		try {
242
			// encapsulated here to avoid variable scope conflicts
243
			require_once $app . '/appinfo/app.php';
244
		} catch (Exception $ex) {
245
			\OC::$server->getLogger()->logException($ex);
246
			$blacklist = \OC::$server->getAppManager()->getAlwaysEnabledApps();
247 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...
248
				\OC::$server->getLogger()->warning('Could not load app "' . $app . '", it will be disabled', array('app' => 'core'));
249
				self::disable($app);
250
			}
251
			throw $ex;
252
		}
253
	}
254
255
	/**
256
	 * check if an app is of a specific type
257
	 *
258
	 * @param string $app
259
	 * @param string|array $types
260
	 * @return bool
261
	 */
262
	public static function isType($app, $types) {
263
		if (is_string($types)) {
264
			$types = [$types];
265
		}
266
		$appTypes = self::getAppTypes($app);
267
		foreach ($types as $type) {
268
			if (array_search($type, $appTypes) !== false) {
269
				return true;
270
			}
271
		}
272
		return false;
273
	}
274
275
	/**
276
	 * get the types of an app
277
	 *
278
	 * @param string $app
279
	 * @return array
280
	 */
281
	private static function getAppTypes($app) {
282
		//load the cache
283
		if (count(self::$appTypes) == 0) {
284
			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...
285
		}
286
287
		if (isset(self::$appTypes[$app])) {
288
			return explode(',', self::$appTypes[$app]);
289
		} else {
290
			return [];
291
		}
292
	}
293
294
	/**
295
	 * read app types from info.xml and cache them in the database
296
	 */
297
	public static function setAppTypes($app) {
298
		$appData = self::getAppInfo($app);
299
		if(!is_array($appData)) {
300
			return;
301
		}
302
303
		if (isset($appData['types'])) {
304
			$appTypes = implode(',', $appData['types']);
305
		} else {
306
			$appTypes = '';
307
		}
308
309
		\OC::$server->getAppConfig()->setValue($app, 'types', $appTypes);
310
	}
311
312
	/**
313
	 * check if app is shipped
314
	 *
315
	 * @param string $appId the id of the app to check
316
	 * @return bool
317
	 *
318
	 * Check if an app that is installed is a shipped app or installed from the appstore.
319
	 */
320
	public static function isShipped($appId) {
321
		return \OC::$server->getAppManager()->isShipped($appId);
322
	}
323
324
	/**
325
	 * get all enabled apps
326
	 */
327
	protected static $enabledAppsCache = [];
328
329
	/**
330
	 * Returns apps enabled for the current user.
331
	 *
332
	 * @param bool $forceRefresh whether to refresh the cache
333
	 * @param bool $all whether to return apps for all users, not only the
334
	 * currently logged in one
335
	 * @return string[]
336
	 */
337
	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...
338
		if (!\OC::$server->getSystemConfig()->getValue('installed', false)) {
339
			return [];
340
		}
341
		// in incognito mode or when logged out, $user will be false,
342
		// which is also the case during an upgrade
343
		$appManager = \OC::$server->getAppManager();
344
		if ($all) {
345
			$user = null;
346
		} else {
347
			$user = \OC::$server->getUserSession()->getUser();
348
		}
349
350
		if (is_null($user)) {
351
			$apps = $appManager->getInstalledApps();
352
		} else {
353
			$apps = $appManager->getEnabledAppsForUser($user);
354
		}
355
		$apps = array_filter($apps, function ($app) {
356
			return $app !== 'files';//we add this manually
357
		});
358
		sort($apps);
359
		array_unshift($apps, 'files');
360
		return $apps;
361
	}
362
363
	/**
364
	 * checks whether or not an app is enabled
365
	 *
366
	 * @param string $app app
367
	 * @return bool
368
	 *
369
	 * This function checks whether or not an app is enabled.
370
	 */
371
	public static function isEnabled($app) {
372
		return \OC::$server->getAppManager()->isEnabledForUser($app);
373
	}
374
375
	/**
376
	 * enables an app
377
	 *
378
	 * @param mixed $app app
379
	 * @param array $groups (optional) when set, only these groups will have access to the app
380
	 * @throws \Exception
381
	 * @return void
382
	 *
383
	 * This function set an app as enabled in appconfig.
384
	 */
385
	public static function enable($app, $groups = null) {
386
		self::$enabledAppsCache = []; // flush
387
388
		// check for required dependencies
389
		$config = \OC::$server->getConfig();
390
		$l = \OC::$server->getL10N('core');
391
		$info = self::getAppInfo($app);
392
		if ($info === null) {
393
			throw new \Exception("$app can't be enabled since it is not installed.");
394
		}
395
396
		self::checkAppDependencies($config, $l, $info);
397
398
		if (!Installer::isInstalled($app)) {
399
			Installer::installShippedApp($app);
400
		}
401
402
		$appManager = \OC::$server->getAppManager();
403
		if (!is_null($groups)) {
404
			$groupManager = \OC::$server->getGroupManager();
405
			$groupsList = [];
406
			foreach ($groups as $group) {
407
				$groupItem = $groupManager->get($group);
408
				if ($groupItem instanceof \OCP\IGroup) {
409
					$groupsList[] = $groupManager->get($group);
410
				}
411
			}
412
			$appManager->enableAppForGroups($app, $groupsList);
413
		} else {
414
			$appManager->enableApp($app);
415
		}
416
	}
417
418
	/**
419
	 * @param string $app
420
	 * @return bool
421
	 */
422
	public static function removeApp($app) {
423
		if (self::isShipped($app)) {
424
			return false;
425
		}
426
427
		return Installer::removeApp($app);
428
	}
429
430
	/**
431
	 * This function set an app as disabled in appconfig.
432
	 *
433
	 * @param string $app app
434
	 * @throws Exception
435
	 */
436
	public static function disable($app) {
437
		// Convert OCS ID to regular application identifier
438
		if(self::getInternalAppIdByOcs($app) !== false) {
439
			$app = self::getInternalAppIdByOcs($app);
440
		}
441
442
		// flush
443
		self::$enabledAppsCache = [];
444
445
		// run uninstall steps
446
		$appData = OC_App::getAppInfo($app);
447
		if (!is_null($appData)) {
448
			OC_App::executeRepairSteps($app, $appData['repair-steps']['uninstall']);
449
		}
450
451
		// emit disable hook - needed anymore ?
452
		\OC_Hook::emit('OC_App', 'pre_disable', ['app' => $app]);
453
454
		// finally disable it
455
		$appManager = \OC::$server->getAppManager();
456
		$appManager->disableApp($app);
457
	}
458
459
	/**
460
	 * Returns the Settings Navigation
461
	 *
462
	 * @return string[]
463
	 *
464
	 * This function returns an array containing all settings pages added. The
465
	 * entries are sorted by the key 'order' ascending.
466
	 */
467
	public static function getSettingsNavigation() {
468
		$l = \OC::$server->getL10N('lib');
469
		$urlGenerator = \OC::$server->getURLGenerator();
470
471
		$settings = [];
472
		// by default, settings only contain the help menu
473
		if (OC_Util::getEditionString() === OC_Util::EDITION_COMMUNITY &&
474
			\OC::$server->getSystemConfig()->getValue('knowledgebaseenabled', true) == true
475
		) {
476
			$settings = [
477
				[
478
					"id" => "help",
479
					"order" => 1000,
480
					"href" => $urlGenerator->linkToRoute('settings_help'),
481
					"name" => $l->t("Help"),
482
					"icon" => $urlGenerator->imagePath("settings", "help.svg")
483
				]
484
			];
485
		}
486
487
		// if the user is logged-in
488
		if (OC_User::isLoggedIn()) {
489
			// personal menu
490
			$settings[] = [
491
				"id" => "settings",
492
				"order" => 1,
493
				"href" => $urlGenerator->linkToRoute('settings.SettingsPage.getPersonal'),
494
				"name" => $l->t("Settings"),
495
				"icon" => $urlGenerator->imagePath("settings", "admin.svg")
496
			];
497
498
			$hasUserManagementPrivileges = false;
499
			$userObject = \OC::$server->getUserSession()->getUser();
500 View Code Duplication
			if($userObject !== null) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

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