Completed
Push — master ( e40705...fe8d45 )
by Thomas
08:24
created

OC_App::shouldUpgrade()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 8
nc 3
nop 1
dl 0
loc 11
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 Björn Schießle <[email protected]>
7
 * @author Borjan Tchakaloff <[email protected]>
8
 * @author Brice Maron <[email protected]>
9
 * @author Christopher Schäpers <[email protected]>
10
 * @author Felix Moeller <[email protected]>
11
 * @author Frank Karlitschek <[email protected]>
12
 * @author Georg Ehrke <[email protected]>
13
 * @author Jakob Sack <[email protected]>
14
 * @author Jan-Christoph Borchardt <[email protected]>
15
 * @author Joas Schilling <[email protected]>
16
 * @author Jörn Friedrich Dreyer <[email protected]>
17
 * @author Kamil Domanski <[email protected]>
18
 * @author Klaas Freitag <[email protected]>
19
 * @author Lukas Reschke <[email protected]>
20
 * @author Markus Goetz <[email protected]>
21
 * @author Morris Jobke <[email protected]>
22
 * @author RealRancor <[email protected]>
23
 * @author Robin Appelman <[email protected]>
24
 * @author Robin McCorkell <[email protected]>
25
 * @author Roeland Jago Douma <[email protected]>
26
 * @author Sam Tuke <[email protected]>
27
 * @author Thomas Müller <[email protected]>
28
 * @author Thomas Tanghus <[email protected]>
29
 * @author Tom Needham <[email protected]>
30
 * @author Vincent Petry <[email protected]>
31
 *
32
 * @copyright Copyright (c) 2016, ownCloud GmbH.
33
 * @license AGPL-3.0
34
 *
35
 * This code is free software: you can redistribute it and/or modify
36
 * it under the terms of the GNU Affero General Public License, version 3,
37
 * as published by the Free Software Foundation.
38
 *
39
 * This program is distributed in the hope that it will be useful,
40
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
41
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
42
 * GNU Affero General Public License for more details.
43
 *
44
 * You should have received a copy of the GNU Affero General Public License, version 3,
45
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
46
 *
47
 */
48
use OC\App\DependencyAnalyzer;
49
use OC\App\InfoParser;
50
use OC\App\Platform;
51
use OC\Installer;
52
use OC\Repair;
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 $altLogin = [];
67
	const officialApp = 200;
68
69
	/**
70
	 * clean the appId
71
	 *
72
	 * @param string|boolean $app AppId that needs to be cleaned
73
	 * @return string
74
	 */
75
	public static function cleanAppId($app) {
76
		return str_replace(['\0', '/', '\\', '..'], '', $app);
77
	}
78
79
	/**
80
	 * Check if an app is loaded
81
	 *
82
	 * @param string $app
83
	 * @return bool
84
	 */
85
	public static function isAppLoaded($app) {
86
		return in_array($app, self::$loadedApps, true);
87
	}
88
89
	/**
90
	 * loads all apps
91
	 *
92
	 * @param string[] | string | null $types
93
	 * @return bool
94
	 *
95
	 * This function walks through the ownCloud directory and loads all apps
96
	 * it can find. A directory contains an app if the file /appinfo/info.xml
97
	 * exists.
98
	 *
99
	 * if $types is set, only apps of those types will be loaded
100
	 */
101
	public static function loadApps($types = null) {
102
		if (\OC::$server->getSystemConfig()->getValue('maintenance', false)) {
103
			return false;
104
		}
105
		// Load the enabled apps here
106
		$apps = self::getEnabledApps();
107
108
		// Add each apps' folder as allowed class path
109
		foreach($apps as $app) {
110
			$path = self::getAppPath($app);
111
			if($path !== false) {
112
				self::registerAutoloading($app, $path);
113
			}
114
		}
115
116
		// prevent app.php from printing output
117
		ob_start();
118
		foreach ($apps as $app) {
119
			if ((is_null($types) or self::isType($app, $types)) && !in_array($app, self::$loadedApps)) {
120
				self::loadApp($app);
121
			}
122
		}
123
		ob_end_clean();
124
125
		return true;
126
	}
127
128
	/**
129
	 * load a single app
130
	 *
131
	 * @param string $app
132
	 * @param bool $checkUpgrade whether an upgrade check should be done
133
	 * @throws \OC\NeedsUpdateException
134
	 */
135
	public static function loadApp($app, $checkUpgrade = true) {
136
		self::$loadedApps[] = $app;
137
		$appPath = self::getAppPath($app);
138
		if($appPath === false) {
139
			return;
140
		}
141
142
		// in case someone calls loadApp() directly
143
		self::registerAutoloading($app, $appPath);
144
145
		self::enableThemeIfApplicable($app);
146
147
		if (is_file($appPath . '/appinfo/app.php')) {
148
			\OC::$server->getEventLogger()->start('load_app_' . $app, 'Load app: ' . $app);
149
			if ($checkUpgrade and self::shouldUpgrade($app)) {
150
				throw new \OC\NeedsUpdateException();
151
			}
152
			self::requireAppFile($app);
153
			if (self::isType($app, ['authentication'])) {
154
				// since authentication apps affect the "is app enabled for group" check,
155
				// the enabled apps cache needs to be cleared to make sure that the
156
				// next time getEnableApps() is called it will also include apps that were
157
				// enabled for groups
158
				self::$enabledAppsCache = [];
159
			}
160
			\OC::$server->getEventLogger()->end('load_app_' . $app);
161
		}
162
	}
163
164
	/**
165
	 * Enables the app as a theme if it has the type "theme"
166
	 * @param string $app
167
	 */
168
	private static function enableThemeIfApplicable($app) {
169
		if (self::isType($app, 'theme')) {
170
			/** @var \OC\Theme\ThemeService $themeManager */
171
			$themeManager = \OC::$server->query('ThemeService');
172
			$themeManager->setAppTheme($app);
173
		}
174
	}
175
176
	/**
177
	 * @internal
178
	 * @param string $app
179
	 * @param string $path
180
	 */
181
	public static function registerAutoloading($app, $path) {
182
		// Register on PSR-4 composer autoloader
183
		$appNamespace = \OC\AppFramework\App::buildAppNamespace($app);
184
		\OC::$composerAutoloader->addPsr4($appNamespace . '\\', $path . '/lib/', true);
185
		if (defined('PHPUNIT_RUN')) {
186
			\OC::$composerAutoloader->addPsr4($appNamespace . '\\Tests\\', $path . '/tests/', true);
187
		}
188
189
		// Register on legacy autoloader
190
		\OC::$loader->addValidRoot($path);
191
	}
192
193
	/**
194
	 * Load app.php from the given app
195
	 *
196
	 * @param string $app app name
197
	 */
198
	private static function requireAppFile($app) {
199
		try {
200
			// encapsulated here to avoid variable scope conflicts
201
			require_once $app . '/appinfo/app.php';
202
		} catch (Exception $ex) {
203
			\OC::$server->getLogger()->logException($ex);
204
			$blacklist = \OC::$server->getAppManager()->getAlwaysEnabledApps();
205
			if (!in_array($app, $blacklist)) {
206
				self::disable($app);
207
			}
208
		}
209
	}
210
211
	/**
212
	 * check if an app is of a specific type
213
	 *
214
	 * @param string $app
215
	 * @param string|array $types
216
	 * @return bool
217
	 */
218
	public static function isType($app, $types) {
219
		if (is_string($types)) {
220
			$types = [$types];
221
		}
222
		$appTypes = self::getAppTypes($app);
223
		foreach ($types as $type) {
224
			if (array_search($type, $appTypes) !== false) {
225
				return true;
226
			}
227
		}
228
		return false;
229
	}
230
231
	/**
232
	 * get the types of an app
233
	 *
234
	 * @param string $app
235
	 * @return array
236
	 */
237
	private static function getAppTypes($app) {
238
		//load the cache
239
		if (count(self::$appTypes) == 0) {
240
			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...
241
		}
242
243
		if (isset(self::$appTypes[$app])) {
244
			return explode(',', self::$appTypes[$app]);
245
		} else {
246
			return [];
247
		}
248
	}
249
250
	/**
251
	 * read app types from info.xml and cache them in the database
252
	 */
253
	public static function setAppTypes($app) {
254
		$appData = self::getAppInfo($app);
255
		if(!is_array($appData)) {
256
			return;
257
		}
258
259
		if (isset($appData['types'])) {
260
			$appTypes = implode(',', $appData['types']);
261
		} else {
262
			$appTypes = '';
263
		}
264
265
		\OC::$server->getAppConfig()->setValue($app, 'types', $appTypes);
266
	}
267
268
	/**
269
	 * check if app is shipped
270
	 *
271
	 * @param string $appId the id of the app to check
272
	 * @return bool
273
	 *
274
	 * Check if an app that is installed is a shipped app or installed from the appstore.
275
	 */
276
	public static function isShipped($appId) {
277
		return \OC::$server->getAppManager()->isShipped($appId);
278
	}
279
280
	/**
281
	 * get all enabled apps
282
	 */
283
	protected static $enabledAppsCache = [];
284
285
	/**
286
	 * Returns apps enabled for the current user.
287
	 *
288
	 * @param bool $forceRefresh whether to refresh the cache
289
	 * @param bool $all whether to return apps for all users, not only the
290
	 * currently logged in one
291
	 * @return string[]
292
	 */
293
	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...
294
		if (!\OC::$server->getSystemConfig()->getValue('installed', false)) {
295
			return [];
296
		}
297
		// in incognito mode or when logged out, $user will be false,
298
		// which is also the case during an upgrade
299
		$appManager = \OC::$server->getAppManager();
300
		if ($all) {
301
			$user = null;
302
		} else {
303
			$user = \OC::$server->getUserSession()->getUser();
304
		}
305
306
		if (is_null($user)) {
307
			$apps = $appManager->getInstalledApps();
308
		} else {
309
			$apps = $appManager->getEnabledAppsForUser($user);
310
		}
311
		$apps = array_filter($apps, function ($app) {
312
			return $app !== 'files';//we add this manually
313
		});
314
		sort($apps);
315
		array_unshift($apps, 'files');
316
		return $apps;
317
	}
318
319
	/**
320
	 * checks whether or not an app is enabled
321
	 *
322
	 * @param string $app app
323
	 * @return bool
324
	 *
325
	 * This function checks whether or not an app is enabled.
326
	 */
327
	public static function isEnabled($app) {
328
		return \OC::$server->getAppManager()->isEnabledForUser($app);
329
	}
330
331
	/**
332
	 * enables an app
333
	 *
334
	 * @param mixed $app app
335
	 * @param array $groups (optional) when set, only these groups will have access to the app
336
	 * @throws \Exception
337
	 * @return void
338
	 *
339
	 * This function set an app as enabled in appconfig.
340
	 */
341
	public static function enable($app, $groups = null) {
342
		self::$enabledAppsCache = []; // flush
343
344
		// check for required dependencies
345
		$config = \OC::$server->getConfig();
346
		$l = \OC::$server->getL10N('core');
347
		$info = self::getAppInfo($app);
348
349
		self::checkAppDependencies($config, $l, $info);
0 ignored issues
show
Bug introduced by
It seems like $info defined by self::getAppInfo($app) on line 347 can also be of type null; however, OC_App::checkAppDependencies() does only seem to accept array, maybe add an additional type check?

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

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

    return array();
}

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

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

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