Completed
Pull Request — stable10 (#1299)
by Morris
15:44 queued 04:00
created

OC_App::proceedNavigation()   C

Complexity

Conditions 10
Paths 3

Size

Total Lines 23
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 16
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 23
rs 5.6534

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Arthur Schiwon <[email protected]>
6
 * @author Bart Visscher <[email protected]>
7
 * @author Bernhard Posselt <[email protected]>
8
 * @author Björn Schießle <[email protected]>
9
 * @author Borjan Tchakaloff <[email protected]>
10
 * @author Brice Maron <[email protected]>
11
 * @author Christopher Schäpers <[email protected]>
12
 * @author Felix Moeller <[email protected]>
13
 * @author Frank Karlitschek <[email protected]>
14
 * @author Georg Ehrke <[email protected]>
15
 * @author Jakob Sack <[email protected]>
16
 * @author Jan-Christoph Borchardt <[email protected]>
17
 * @author Joas Schilling <[email protected]>
18
 * @author Jörn Friedrich Dreyer <[email protected]>
19
 * @author Kamil Domanski <[email protected]>
20
 * @author Klaas Freitag <[email protected]>
21
 * @author Lukas Reschke <[email protected]>
22
 * @author Markus Goetz <[email protected]>
23
 * @author Morris Jobke <[email protected]>
24
 * @author RealRancor <[email protected]>
25
 * @author Robin Appelman <[email protected]>
26
 * @author Robin McCorkell <[email protected]>
27
 * @author Roeland Jago Douma <[email protected]>
28
 * @author Sam Tuke <[email protected]>
29
 * @author Thomas Müller <[email protected]>
30
 * @author Thomas Tanghus <[email protected]>
31
 * @author Tom Needham <[email protected]>
32
 * @author Vincent Petry <[email protected]>
33
 *
34
 * @license AGPL-3.0
35
 *
36
 * This code is free software: you can redistribute it and/or modify
37
 * it under the terms of the GNU Affero General Public License, version 3,
38
 * as published by the Free Software Foundation.
39
 *
40
 * This program is distributed in the hope that it will be useful,
41
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
42
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
43
 * GNU Affero General Public License for more details.
44
 *
45
 * You should have received a copy of the GNU Affero General Public License, version 3,
46
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
47
 *
48
 */
49
use OC\App\DependencyAnalyzer;
50
use OC\App\Platform;
51
use OC\Installer;
52
use OC\OCSClient;
53
use OC\Repair;
54
use OCP\App\ManagerEvent;
55
56
/**
57
 * This class manages the apps. It allows them to register and integrate in the
58
 * ownCloud ecosystem. Furthermore, this class is responsible for installing,
59
 * upgrading and removing apps.
60
 */
61
class OC_App {
62
	static private $appVersion = [];
63
	static private $adminForms = array();
64
	static private $personalForms = array();
65
	static private $appInfo = array();
66
	static private $appTypes = array();
67
	static private $loadedApps = array();
68
	static private $altLogin = array();
69
	static private $alreadyRegistered = [];
70
	const officialApp = 200;
71
72
	/**
73
	 * clean the appId
74
	 *
75
	 * @param string|boolean $app AppId that needs to be cleaned
76
	 * @return string
77
	 */
78
	public static function cleanAppId($app) {
79
		return str_replace(array('\0', '/', '\\', '..'), '', $app);
80
	}
81
82
	/**
83
	 * Check if an app is loaded
84
	 *
85
	 * @param string $app
86
	 * @return bool
87
	 */
88
	public static function isAppLoaded($app) {
89
		return in_array($app, self::$loadedApps, true);
90
	}
91
92
	/**
93
	 * loads all apps
94
	 *
95
	 * @param string[] | string | null $types
96
	 * @return bool
97
	 *
98
	 * This function walks through the ownCloud directory and loads all apps
99
	 * it can find. A directory contains an app if the file /appinfo/info.xml
100
	 * exists.
101
	 *
102
	 * if $types is set, only apps of those types will be loaded
103
	 */
104
	public static function loadApps($types = null) {
105
		if (\OC::$server->getSystemConfig()->getValue('maintenance', false)) {
106
			return false;
107
		}
108
		// Load the enabled apps here
109
		$apps = self::getEnabledApps();
110
111
		// Add each apps' folder as allowed class path
112
		foreach($apps as $app) {
113
			$path = self::getAppPath($app);
114
			if($path !== false) {
115
				self::registerAutoloading($app, $path);
116
			}
117
		}
118
119
		// prevent app.php from printing output
120
		ob_start();
121
		foreach ($apps as $app) {
122
			if ((is_null($types) or self::isType($app, $types)) && !in_array($app, self::$loadedApps)) {
123
				self::loadApp($app);
124
			}
125
		}
126
		ob_end_clean();
127
128
		return true;
129
	}
130
131
	/**
132
	 * load a single app
133
	 *
134
	 * @param string $app
135
	 * @param bool $checkUpgrade whether an upgrade check should be done
136
	 * @throws \OC\NeedsUpdateException
137
	 */
138
	public static function loadApp($app, $checkUpgrade = true) {
139
		self::$loadedApps[] = $app;
140
		$appPath = self::getAppPath($app);
141
		if($appPath === false) {
142
			return;
143
		}
144
145
		// in case someone calls loadApp() directly
146
		self::registerAutoloading($app, $appPath);
147
148
		if (is_file($appPath . '/appinfo/app.php')) {
149
			\OC::$server->getEventLogger()->start('load_app_' . $app, 'Load app: ' . $app);
150
			if ($checkUpgrade and self::shouldUpgrade($app)) {
151
				throw new \OC\NeedsUpdateException();
152
			}
153
			self::requireAppFile($app);
154
			if (self::isType($app, array('authentication'))) {
155
				// since authentication apps affect the "is app enabled for group" check,
156
				// the enabled apps cache needs to be cleared to make sure that the
157
				// next time getEnableApps() is called it will also include apps that were
158
				// enabled for groups
159
				self::$enabledAppsCache = array();
160
			}
161
			\OC::$server->getEventLogger()->end('load_app_' . $app);
162
		}
163
	}
164
165
	/**
166
	 * @internal
167
	 * @param string $app
168
	 * @param string $path
169
	 */
170
	public static function registerAutoloading($app, $path) {
171
		$key = $app . '-' . $path;
172
		if(isset(self::$alreadyRegistered[$key])) {
173
			return;
174
		}
175
		self::$alreadyRegistered[$key] = true;
176
		// Register on PSR-4 composer autoloader
177
		$appNamespace = \OC\AppFramework\App::buildAppNamespace($app);
178
		\OC::$composerAutoloader->addPsr4($appNamespace . '\\', $path . '/lib/', true);
179
		if (defined('PHPUNIT_RUN') || defined('CLI_TEST_RUN')) {
180
			\OC::$composerAutoloader->addPsr4($appNamespace . '\\Tests\\', $path . '/tests/', true);
181
		}
182
183
		// Register on legacy autoloader
184
		\OC::$loader->addValidRoot($path);
185
	}
186
187
	/**
188
	 * Load app.php from the given app
189
	 *
190
	 * @param string $app app name
191
	 */
192
	private static function requireAppFile($app) {
193
		try {
194
			// encapsulated here to avoid variable scope conflicts
195
			require_once $app . '/appinfo/app.php';
196
		} catch (Error $ex) {
0 ignored issues
show
Bug introduced by
The class Error does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
197
			\OC::$server->getLogger()->logException($ex);
198
			$blacklist = \OC::$server->getAppManager()->getAlwaysEnabledApps();
199
			if (!in_array($app, $blacklist)) {
200
				self::disable($app);
201
			}
202
		}
203
	}
204
205
	/**
206
	 * check if an app is of a specific type
207
	 *
208
	 * @param string $app
209
	 * @param string|array $types
210
	 * @return bool
211
	 */
212
	public static function isType($app, $types) {
213
		if (is_string($types)) {
214
			$types = array($types);
215
		}
216
		$appTypes = self::getAppTypes($app);
217
		foreach ($types as $type) {
218
			if (array_search($type, $appTypes) !== false) {
219
				return true;
220
			}
221
		}
222
		return false;
223
	}
224
225
	/**
226
	 * get the types of an app
227
	 *
228
	 * @param string $app
229
	 * @return array
230
	 */
231
	private static function getAppTypes($app) {
232
		//load the cache
233
		if (count(self::$appTypes) == 0) {
234
			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...
235
		}
236
237
		if (isset(self::$appTypes[$app])) {
238
			return explode(',', self::$appTypes[$app]);
239
		} else {
240
			return array();
241
		}
242
	}
243
244
	/**
245
	 * read app types from info.xml and cache them in the database
246
	 */
247
	public static function setAppTypes($app) {
248
		$appData = self::getAppInfo($app);
249
		if(!is_array($appData)) {
250
			return;
251
		}
252
253
		if (isset($appData['types'])) {
254
			$appTypes = implode(',', $appData['types']);
255
		} else {
256
			$appTypes = '';
257
		}
258
259
		\OC::$server->getAppConfig()->setValue($app, 'types', $appTypes);
0 ignored issues
show
Deprecated Code introduced by
The method OCP\IAppConfig::setValue() has been deprecated with message: 8.0.0 use method setAppValue of \OCP\IConfig Sets a value. If the key did not exist before it will be created.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
260
	}
261
262
	/**
263
	 * check if app is shipped
264
	 *
265
	 * @param string $appId the id of the app to check
266
	 * @return bool
267
	 *
268
	 * Check if an app that is installed is a shipped app or installed from the appstore.
269
	 */
270
	public static function isShipped($appId) {
271
		return \OC::$server->getAppManager()->isShipped($appId);
272
	}
273
274
	/**
275
	 * get all enabled apps
276
	 */
277
	protected static $enabledAppsCache = array();
278
279
	/**
280
	 * Returns apps enabled for the current user.
281
	 *
282
	 * @param bool $forceRefresh whether to refresh the cache
283
	 * @param bool $all whether to return apps for all users, not only the
284
	 * currently logged in one
285
	 * @return string[]
286
	 */
287
	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...
288
		if (!\OC::$server->getSystemConfig()->getValue('installed', false)) {
289
			return array();
290
		}
291
		// in incognito mode or when logged out, $user will be false,
292
		// which is also the case during an upgrade
293
		$appManager = \OC::$server->getAppManager();
294
		if ($all) {
295
			$user = null;
296
		} else {
297
			$user = \OC::$server->getUserSession()->getUser();
298
		}
299
300
		if (is_null($user)) {
301
			$apps = $appManager->getInstalledApps();
302
		} else {
303
			$apps = $appManager->getEnabledAppsForUser($user);
304
		}
305
		$apps = array_filter($apps, function ($app) {
306
			return $app !== 'files';//we add this manually
307
		});
308
		sort($apps);
309
		array_unshift($apps, 'files');
310
		return $apps;
311
	}
312
313
	/**
314
	 * checks whether or not an app is enabled
315
	 *
316
	 * @param string $app app
317
	 * @return bool
318
	 *
319
	 * This function checks whether or not an app is enabled.
320
	 */
321
	public static function isEnabled($app) {
322
		return \OC::$server->getAppManager()->isEnabledForUser($app);
323
	}
324
325
	/**
326
	 * enables an app
327
	 *
328
	 * @param mixed $app app
329
	 * @param array $groups (optional) when set, only these groups will have access to the app
330
	 * @throws \Exception
331
	 * @return void
332
	 *
333
	 * This function set an app as enabled in appconfig.
334
	 */
335
	public static function enable($app, $groups = null) {
336
		self::$enabledAppsCache = array(); // flush
337
		if (!Installer::isInstalled($app)) {
338
			$app = self::installApp($app);
339
		}
340
341
		$appManager = \OC::$server->getAppManager();
342
		if (!is_null($groups)) {
343
			$groupManager = \OC::$server->getGroupManager();
344
			$groupsList = [];
345
			foreach ($groups as $group) {
346
				$groupItem = $groupManager->get($group);
347
				if ($groupItem instanceof \OCP\IGroup) {
348
					$groupsList[] = $groupManager->get($group);
349
				}
350
			}
351
			$appManager->enableAppForGroups($app, $groupsList);
352
		} else {
353
			$appManager->enableApp($app);
354
		}
355
356
		$info = self::getAppInfo($app);
357 View Code Duplication
		if(isset($info['settings']) && is_array($info['settings'])) {
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...
358
			$appPath = self::getAppPath($app);
359
			self::registerAutoloading($app, $appPath);
0 ignored issues
show
Security Bug introduced by
It seems like $appPath defined by self::getAppPath($app) on line 358 can also be of type false; however, OC_App::registerAutoloading() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
360
			\OC::$server->getSettingsManager()->setupSettings($info['settings']);
361
		}
362
	}
363
364
	/**
365
	 * @param string $app
366
	 * @return int
367
	 */
368
	private static function downloadApp($app) {
369
		$ocsClient = new OCSClient(
370
			\OC::$server->getHTTPClientService(),
371
			\OC::$server->getConfig(),
372
			\OC::$server->getLogger()
373
		);
374
		$appData = $ocsClient->getApplication($app, \OCP\Util::getVersion());
375
		$download = $ocsClient->getApplicationDownload($app, \OCP\Util::getVersion());
376
		if(isset($download['downloadlink']) and $download['downloadlink']!='') {
377
			// Replace spaces in download link without encoding entire URL
378
			$download['downloadlink'] = str_replace(' ', '%20', $download['downloadlink']);
379
			$info = array('source' => 'http', 'href' => $download['downloadlink'], 'appdata' => $appData);
380
			$app = Installer::installApp($info);
381
		}
382
		return $app;
383
	}
384
385
	/**
386
	 * @param string $app
387
	 * @return bool
388
	 */
389
	public static function removeApp($app) {
390
		if (self::isShipped($app)) {
391
			return false;
392
		}
393
394
		return Installer::removeApp($app);
395
	}
396
397
	/**
398
	 * This function set an app as disabled in appconfig.
399
	 *
400
	 * @param string $app app
401
	 * @throws Exception
402
	 */
403
	public static function disable($app) {
404
		// Convert OCS ID to regular application identifier
405
		if(self::getInternalAppIdByOcs($app) !== false) {
406
			$app = self::getInternalAppIdByOcs($app);
407
		}
408
409
		// flush
410
		self::$enabledAppsCache = array();
411
412
		// run uninstall steps
413
		$appData = OC_App::getAppInfo($app);
414
		if (!is_null($appData)) {
415
			OC_App::executeRepairSteps($app, $appData['repair-steps']['uninstall']);
416
		}
417
418
		// emit disable hook - needed anymore ?
419
		\OC_Hook::emit('OC_App', 'pre_disable', array('app' => $app));
420
421
		// finally disable it
422
		$appManager = \OC::$server->getAppManager();
423
		$appManager->disableApp($app);
424
	}
425
426
	/**
427
	 * Returns the Settings Navigation
428
	 *
429
	 * @return string[]
430
	 *
431
	 * This function returns an array containing all settings pages added. The
432
	 * entries are sorted by the key 'order' ascending.
433
	 */
434
	public static function getSettingsNavigation() {
435
		$l = \OC::$server->getL10N('lib');
436
		$urlGenerator = \OC::$server->getURLGenerator();
437
438
		$settings = array();
439
		// by default, settings only contain the help menu
440
		if (OC_Util::getEditionString() === '' &&
441
			\OC::$server->getSystemConfig()->getValue('knowledgebaseenabled', true) == true
442
		) {
443
			$settings = array(
444
				array(
445
					"id" => "help",
446
					"order" => 1000,
447
					"href" => $urlGenerator->linkToRoute('settings_help'),
448
					"name" => $l->t("Help"),
449
					"icon" => $urlGenerator->imagePath("settings", "help.svg")
450
				)
451
			);
452
		}
453
454
		// if the user is logged-in
455
		if (OC_User::isLoggedIn()) {
0 ignored issues
show
Deprecated Code introduced by
The method OC_User::isLoggedIn() has been deprecated with message: use \OC::$server->getUserSession()->isLoggedIn()

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
456
			// personal menu
457
			$settings[] = array(
458
				"id" => "personal",
459
				"order" => 1,
460
				"href" => $urlGenerator->linkToRoute('settings_personal'),
461
				"name" => $l->t("Personal"),
462
				"icon" => $urlGenerator->imagePath("settings", "personal.svg")
463
			);
464
465
			//SubAdmins are also allowed to access user management
466
			$userObject = \OC::$server->getUserSession()->getUser();
467
			$isSubAdmin = false;
468
			if($userObject !== null) {
469
				$isSubAdmin = \OC::$server->getGroupManager()->getSubAdmin()->isSubAdmin($userObject);
470
			}
471
			if ($isSubAdmin) {
472
				// admin users menu
473
				$settings[] = array(
474
					"id" => "core_users",
475
					"order" => 2,
476
					"href" => $urlGenerator->linkToRoute('settings_users'),
477
					"name" => $l->t("Users"),
478
					"icon" => $urlGenerator->imagePath("settings", "users.svg")
479
				);
480
			}
481
482
			// if the user is an admin
483
			if (OC_User::isAdminUser(OC_User::getUser())) {
484
				// admin settings
485
				$settings[] = array(
486
					"id" => "admin",
487
					"order" => 1000,
488
					"href" => $urlGenerator->linkToRoute('settings.AdminSettings.index'),
489
					"name" => $l->t("Admin"),
490
					"icon" => $urlGenerator->imagePath("settings", "admin.svg")
491
				);
492
			}
493
		}
494
495
		$navigation = self::proceedNavigation($settings);
496
		return $navigation;
497
	}
498
499
	// This is private as well. It simply works, so don't ask for more details
500
	private static function proceedNavigation($list) {
501
		$activeApp = OC::$server->getNavigationManager()->getActiveEntry();
502
		foreach ($list as &$navEntry) {
503
			if ($navEntry['id'] == $activeApp) {
504
				$navEntry['active'] = true;
505
			} else {
506
				$navEntry['active'] = false;
507
			}
508
		}
509
		unset($navEntry);
510
511
		usort($list, function($a, $b) {
512
			if (isset($a['order']) && isset($b['order'])) {
513
				return ($a['order'] < $b['order']) ? -1 : 1;
514
			} else if (isset($a['order']) || isset($b['order'])) {
515
				return isset($a['order']) ? -1 : 1;
516
			} else {
517
				return ($a['name'] < $b['name']) ? -1 : 1;
518
			}
519
		});
520
521
		return $list;
522
	}
523
524
	/**
525
	 * Get the path where to install apps
526
	 *
527
	 * @return string|false
528
	 */
529
	public static function getInstallPath() {
530
		if (\OC::$server->getSystemConfig()->getValue('appstoreenabled', true) == false) {
531
			return false;
532
		}
533
534
		foreach (OC::$APPSROOTS as $dir) {
535
			if (isset($dir['writable']) && $dir['writable'] === true) {
536
				return $dir['path'];
537
			}
538
		}
539
540
		\OCP\Util::writeLog('core', 'No application directories are marked as writable.', \OCP\Util::ERROR);
541
		return null;
542
	}
543
544
545
	/**
546
	 * search for an app in all app-directories
547
	 *
548
	 * @param string $appId
549
	 * @return false|string
550
	 */
551
	protected static function findAppInDirectories($appId) {
552
		$sanitizedAppId = self::cleanAppId($appId);
553
		if($sanitizedAppId !== $appId) {
554
			return false;
555
		}
556
		static $app_dir = array();
557
558
		if (isset($app_dir[$appId])) {
559
			return $app_dir[$appId];
560
		}
561
562
		$possibleApps = array();
563
		foreach (OC::$APPSROOTS as $dir) {
564
			if (file_exists($dir['path'] . '/' . $appId)) {
565
				$possibleApps[] = $dir;
566
			}
567
		}
568
569
		if (empty($possibleApps)) {
570
			return false;
571
		} elseif (count($possibleApps) === 1) {
572
			$dir = array_shift($possibleApps);
573
			$app_dir[$appId] = $dir;
574
			return $dir;
575
		} else {
576
			$versionToLoad = array();
577
			foreach ($possibleApps as $possibleApp) {
578
				$version = self::getAppVersionByPath($possibleApp['path']);
579
				if (empty($versionToLoad) || version_compare($version, $versionToLoad['version'], '>')) {
580
					$versionToLoad = array(
581
						'dir' => $possibleApp,
582
						'version' => $version,
583
					);
584
				}
585
			}
586
			$app_dir[$appId] = $versionToLoad['dir'];
587
			return $versionToLoad['dir'];
588
			//TODO - write test
589
		}
590
	}
591
592
	/**
593
	 * Get the directory for the given app.
594
	 * If the app is defined in multiple directories, the first one is taken. (false if not found)
595
	 *
596
	 * @param string $appId
597
	 * @return string|false
598
	 */
599
	public static function getAppPath($appId) {
600
		if ($appId === null || trim($appId) === '') {
601
			return false;
602
		}
603
604
		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...
605
			return $dir['path'] . '/' . $appId;
606
		}
607
		return false;
608
	}
609
610
611
	/**
612
	 * check if an app's directory is writable
613
	 *
614
	 * @param string $appId
615
	 * @return bool
616
	 */
617
	public static function isAppDirWritable($appId) {
618
		$path = self::getAppPath($appId);
619
		return ($path !== false) ? is_writable($path) : false;
620
	}
621
622
	/**
623
	 * Get the path for the given app on the access
624
	 * If the app is defined in multiple directories, the first one is taken. (false if not found)
625
	 *
626
	 * @param string $appId
627
	 * @return string|false
628
	 */
629
	public static function getAppWebPath($appId) {
630
		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...
631
			return OC::$WEBROOT . $dir['url'] . '/' . $appId;
632
		}
633
		return false;
634
	}
635
636
	/**
637
	 * get the last version of the app from appinfo/info.xml
638
	 *
639
	 * @param string $appId
640
	 * @return string
641
	 */
642
	public static function getAppVersion($appId) {
643
		if (!isset(self::$appVersion[$appId])) {
644
			$file = self::getAppPath($appId);
645
			self::$appVersion[$appId] = ($file !== false) ? self::getAppVersionByPath($file) : '0';
646
		}
647
		return self::$appVersion[$appId];
648
	}
649
650
	/**
651
	 * get app's version based on it's path
652
	 *
653
	 * @param string $path
654
	 * @return string
655
	 */
656
	public static function getAppVersionByPath($path) {
657
		$infoFile = $path . '/appinfo/info.xml';
658
		$appData = self::getAppInfo($infoFile, true);
659
		return isset($appData['version']) ? $appData['version'] : '';
660
	}
661
662
663
	/**
664
	 * Read all app metadata from the info.xml file
665
	 *
666
	 * @param string $appId id of the app or the path of the info.xml file
667
	 * @param boolean $path (optional)
668
	 * @return array|null
669
	 * @note all data is read from info.xml, not just pre-defined fields
670
	 */
671
	public static function getAppInfo($appId, $path = false) {
672
		if ($path) {
673
			$file = $appId;
674
		} else {
675
			if (isset(self::$appInfo[$appId])) {
676
				return self::$appInfo[$appId];
677
			}
678
			$appPath = self::getAppPath($appId);
679
			if($appPath === false) {
680
				return null;
681
			}
682
			$file = $appPath . '/appinfo/info.xml';
683
		}
684
685
		$parser = new \OC\App\InfoParser(\OC::$server->getURLGenerator());
686
		$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 700 which is incompatible with the return type documented by OC_App::getAppInfo of type array|null.
Loading history...
687
688
		if (is_array($data)) {
689
			$data = OC_App::parseAppInfo($data);
690
		}
691
		if(isset($data['ocsid'])) {
692
			$storedId = \OC::$server->getConfig()->getAppValue($appId, 'ocsid');
693
			if($storedId !== '' && $storedId !== $data['ocsid']) {
694
				$data['ocsid'] = $storedId;
695
			}
696
		}
697
698
		self::$appInfo[$appId] = $data;
699
700
		return $data;
701
	}
702
703
	/**
704
	 * Returns the navigation
705
	 *
706
	 * @return array
707
	 *
708
	 * This function returns an array containing all entries added. The
709
	 * entries are sorted by the key 'order' ascending. Additional to the keys
710
	 * given for each app the following keys exist:
711
	 *   - active: boolean, signals if the user is on this navigation entry
712
	 */
713
	public static function getNavigation() {
714
		$entries = OC::$server->getNavigationManager()->getAll();
715
		$navigation = self::proceedNavigation($entries);
716
		return $navigation;
717
	}
718
719
	/**
720
	 * get the id of loaded app
721
	 *
722
	 * @return string
723
	 */
724
	public static function getCurrentApp() {
725
		$request = \OC::$server->getRequest();
726
		$script = substr($request->getScriptName(), strlen(OC::$WEBROOT) + 1);
727
		$topFolder = substr($script, 0, strpos($script, '/'));
728
		if (empty($topFolder)) {
729
			$path_info = $request->getPathInfo();
730
			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...
731
				$topFolder = substr($path_info, 1, strpos($path_info, '/', 1) - 1);
732
			}
733
		}
734
		if ($topFolder == 'apps') {
735
			$length = strlen($topFolder);
736
			return substr($script, $length + 1, strpos($script, '/', $length + 1) - $length - 1);
737
		} else {
738
			return $topFolder;
739
		}
740
	}
741
742
	/**
743
	 * @param string $type
744
	 * @return array
745
	 */
746
	public static function getForms($type) {
747
		$forms = array();
748
		switch ($type) {
749
			case 'admin':
750
				$source = self::$adminForms;
751
				break;
752
			case 'personal':
753
				$source = self::$personalForms;
754
				break;
755
			default:
756
				return array();
757
		}
758
		foreach ($source as $form) {
759
			$forms[] = include $form;
760
		}
761
		return $forms;
762
	}
763
764
	/**
765
	 * register an admin form to be shown
766
	 *
767
	 * @param string $app
768
	 * @param string $page
769
	 */
770
	public static function registerAdmin($app, $page) {
771
		self::$adminForms[] = $app . '/' . $page . '.php';
772
	}
773
774
	/**
775
	 * register a personal form to be shown
776
	 * @param string $app
777
	 * @param string $page
778
	 */
779
	public static function registerPersonal($app, $page) {
780
		self::$personalForms[] = $app . '/' . $page . '.php';
781
	}
782
783
	/**
784
	 * @param array $entry
785
	 */
786
	public static function registerLogIn(array $entry) {
787
		self::$altLogin[] = $entry;
788
	}
789
790
	/**
791
	 * @return array
792
	 */
793
	public static function getAlternativeLogIns() {
794
		return self::$altLogin;
795
	}
796
797
	/**
798
	 * get a list of all apps in the apps folder
799
	 *
800
	 * @return array an array of app names (string IDs)
801
	 * @todo: change the name of this method to getInstalledApps, which is more accurate
802
	 */
803
	public static function getAllApps() {
804
805
		$apps = array();
806
807
		foreach (OC::$APPSROOTS as $apps_dir) {
808
			if (!is_readable($apps_dir['path'])) {
809
				\OCP\Util::writeLog('core', 'unable to read app folder : ' . $apps_dir['path'], \OCP\Util::WARN);
810
				continue;
811
			}
812
			$dh = opendir($apps_dir['path']);
813
814
			if (is_resource($dh)) {
815
				while (($file = readdir($dh)) !== false) {
816
817
					if ($file[0] != '.' and is_dir($apps_dir['path'] . '/' . $file) and is_file($apps_dir['path'] . '/' . $file . '/appinfo/info.xml')) {
818
819
						$apps[] = $file;
820
					}
821
				}
822
			}
823
		}
824
825
		return $apps;
826
	}
827
828
	/**
829
	 * List all apps, this is used in apps.php
830
	 *
831
	 * @param bool $onlyLocal
832
	 * @param bool $includeUpdateInfo Should we check whether there is an update
833
	 *                                in the app store?
834
	 * @param OCSClient $ocsClient
835
	 * @return array
836
	 */
837
	public static function listAllApps($onlyLocal = false,
838
									   $includeUpdateInfo = true,
839
									   OCSClient $ocsClient) {
840
		$installedApps = OC_App::getAllApps();
841
842
		//TODO which apps do we want to blacklist and how do we integrate
843
		// blacklisting with the multi apps folder feature?
844
845
		//we don't want to show configuration for these
846
		$blacklist = \OC::$server->getAppManager()->getAlwaysEnabledApps();
847
		$appList = array();
848
849
		foreach ($installedApps as $app) {
850
			if (array_search($app, $blacklist) === false) {
851
852
				$info = OC_App::getAppInfo($app);
853
				if (!is_array($info)) {
854
					\OCP\Util::writeLog('core', 'Could not read app info file for app "' . $app . '"', \OCP\Util::ERROR);
855
					continue;
856
				}
857
858
				if (!isset($info['name'])) {
859
					\OCP\Util::writeLog('core', 'App id "' . $app . '" has no name in appinfo', \OCP\Util::ERROR);
860
					continue;
861
				}
862
863
				$enabled = \OC::$server->getAppConfig()->getValue($app, 'enabled', 'no');
0 ignored issues
show
Deprecated Code introduced by
The method OCP\IAppConfig::getValue() has been deprecated with message: 8.0.0 use method getAppValue of \OCP\IConfig This function gets a value from the appconfig table. If the key does
not exist the default value will be returned

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
864
				$info['groups'] = null;
865
				if ($enabled === 'yes') {
866
					$active = true;
867
				} else if ($enabled === 'no') {
868
					$active = false;
869
				} else {
870
					$active = true;
871
					$info['groups'] = $enabled;
872
				}
873
874
				$info['active'] = $active;
875
876
				if (self::isShipped($app)) {
877
					$info['internal'] = true;
878
					$info['level'] = self::officialApp;
879
					$info['removable'] = false;
880
				} else {
881
					$info['internal'] = false;
882
					$info['removable'] = true;
883
				}
884
885
				$info['update'] = ($includeUpdateInfo) ? Installer::isUpdateAvailable($app) : null;
886
887
				$appPath = self::getAppPath($app);
888
				if($appPath !== false) {
889
					$appIcon = $appPath . '/img/' . $app . '.svg';
890
					if (file_exists($appIcon)) {
891
						$info['preview'] = \OC::$server->getURLGenerator()->imagePath($app, $app . '.svg');
892
						$info['previewAsIcon'] = true;
893
					} else {
894
						$appIcon = $appPath . '/img/app.svg';
895
						if (file_exists($appIcon)) {
896
							$info['preview'] = \OC::$server->getURLGenerator()->imagePath($app, 'app.svg');
897
							$info['previewAsIcon'] = true;
898
						}
899
					}
900
				}
901
				$info['version'] = OC_App::getAppVersion($app);
902
				$appList[] = $info;
903
			}
904
		}
905
		if ($onlyLocal) {
906
			$remoteApps = [];
907
		} else {
908
			$remoteApps = OC_App::getAppstoreApps('approved', null, $ocsClient);
909
		}
910
		if ($remoteApps) {
911
			// Remove duplicates
912
			foreach ($appList as $app) {
913
				foreach ($remoteApps AS $key => $remote) {
914
					if ($app['name'] === $remote['name'] ||
915
						(isset($app['ocsid']) &&
916
							$app['ocsid'] === $remote['id'])
917
					) {
918
						unset($remoteApps[$key]);
919
					}
920
				}
921
			}
922
			$combinedApps = array_merge($appList, $remoteApps);
923
		} else {
924
			$combinedApps = $appList;
925
		}
926
927
		return $combinedApps;
928
	}
929
930
	/**
931
	 * Returns the internal app ID or false
932
	 * @param string $ocsID
933
	 * @return string|false
934
	 */
935
	public static function getInternalAppIdByOcs($ocsID) {
936
		if(is_numeric($ocsID)) {
937
			$idArray = \OC::$server->getAppConfig()->getValues(false, 'ocsid');
938
			if(array_search($ocsID, $idArray)) {
939
				return array_search($ocsID, $idArray);
940
			}
941
		}
942
		return false;
943
	}
944
945
	/**
946
	 * Get a list of all apps on the appstore
947
	 * @param string $filter
948
	 * @param string|null $category
949
	 * @param OCSClient $ocsClient
950
	 * @return array|bool  multi-dimensional array of apps.
951
	 *                     Keys: id, name, type, typename, personid, license, detailpage, preview, changed, description
952
	 */
953
	public static function getAppstoreApps($filter = 'approved',
954
										   $category = null,
955
										   OCSClient $ocsClient) {
956
		$categories = [$category];
957
958
		if (is_null($category)) {
959
			$categoryNames = $ocsClient->getCategories(\OCP\Util::getVersion());
960
			if (is_array($categoryNames)) {
961
				// Check that categories of apps were retrieved correctly
962
				if (!$categories = array_keys($categoryNames)) {
963
					return false;
964
				}
965
			} else {
966
				return false;
967
			}
968
		}
969
970
		$page = 0;
971
		$remoteApps = $ocsClient->getApplications($categories, $page, $filter, \OCP\Util::getVersion());
972
		$apps = [];
973
		$i = 0;
974
		$l = \OC::$server->getL10N('core');
975
		foreach ($remoteApps as $app) {
976
			$potentialCleanId = self::getInternalAppIdByOcs($app['id']);
977
			// enhance app info (for example the description)
978
			$apps[$i] = OC_App::parseAppInfo($app);
979
			$apps[$i]['author'] = $app['personid'];
980
			$apps[$i]['ocs_id'] = $app['id'];
981
			$apps[$i]['internal'] = 0;
982
			$apps[$i]['active'] = ($potentialCleanId !== false) ? self::isEnabled($potentialCleanId) : false;
983
			$apps[$i]['update'] = false;
984
			$apps[$i]['groups'] = false;
985
			$apps[$i]['score'] = $app['score'];
986
			$apps[$i]['removable'] = false;
987
			if ($app['label'] == 'recommended') {
988
				$apps[$i]['internallabel'] = (string)$l->t('Recommended');
989
				$apps[$i]['internalclass'] = 'recommendedapp';
990
			}
991
992
			// Apps from the appstore are always assumed to be compatible with the
993
			// the current release as the initial filtering is done on the appstore
994
			$apps[$i]['dependencies']['owncloud']['@attributes']['min-version'] = implode('.', \OCP\Util::getVersion());
995
			$apps[$i]['dependencies']['owncloud']['@attributes']['max-version'] = implode('.', \OCP\Util::getVersion());
996
997
			$i++;
998
		}
999
1000
1001
1002
		if (empty($apps)) {
1003
			return false;
1004
		} else {
1005
			return $apps;
1006
		}
1007
	}
1008
1009
	public static function shouldUpgrade($app) {
1010
		$versions = self::getAppVersions();
1011
		$currentVersion = OC_App::getAppVersion($app);
1012
		if ($currentVersion && isset($versions[$app])) {
1013
			$installedVersion = $versions[$app];
1014
			if (!version_compare($currentVersion, $installedVersion, '=')) {
1015
				return true;
1016
			}
1017
		}
1018
		return false;
1019
	}
1020
1021
	/**
1022
	 * Adjust the number of version parts of $version1 to match
1023
	 * the number of version parts of $version2.
1024
	 *
1025
	 * @param string $version1 version to adjust
1026
	 * @param string $version2 version to take the number of parts from
1027
	 * @return string shortened $version1
1028
	 */
1029
	private static function adjustVersionParts($version1, $version2) {
1030
		$version1 = explode('.', $version1);
1031
		$version2 = explode('.', $version2);
1032
		// reduce $version1 to match the number of parts in $version2
1033
		while (count($version1) > count($version2)) {
1034
			array_pop($version1);
1035
		}
1036
		// if $version1 does not have enough parts, add some
1037
		while (count($version1) < count($version2)) {
1038
			$version1[] = '0';
1039
		}
1040
		return implode('.', $version1);
1041
	}
1042
1043
	/**
1044
	 * Check whether the current ownCloud version matches the given
1045
	 * application's version requirements.
1046
	 *
1047
	 * The comparison is made based on the number of parts that the
1048
	 * app info version has. For example for ownCloud 6.0.3 if the
1049
	 * app info version is expecting version 6.0, the comparison is
1050
	 * made on the first two parts of the ownCloud version.
1051
	 * This means that it's possible to specify "requiremin" => 6
1052
	 * and "requiremax" => 6 and it will still match ownCloud 6.0.3.
1053
	 *
1054
	 * @param string $ocVersion ownCloud version to check against
1055
	 * @param array $appInfo app info (from xml)
1056
	 *
1057
	 * @return boolean true if compatible, otherwise false
1058
	 */
1059
	public static function isAppCompatible($ocVersion, $appInfo) {
1060
		$requireMin = '';
1061
		$requireMax = '';
1062
		if (isset($appInfo['dependencies']['owncloud']['@attributes']['min-version'])) {
1063
			$requireMin = $appInfo['dependencies']['owncloud']['@attributes']['min-version'];
1064
		} else if (isset($appInfo['requiremin'])) {
1065
			$requireMin = $appInfo['requiremin'];
1066
		} else if (isset($appInfo['require'])) {
1067
			$requireMin = $appInfo['require'];
1068
		}
1069
1070
		if (isset($appInfo['dependencies']['owncloud']['@attributes']['max-version'])) {
1071
			$requireMax = $appInfo['dependencies']['owncloud']['@attributes']['max-version'];
1072
		} else if (isset($appInfo['requiremax'])) {
1073
			$requireMax = $appInfo['requiremax'];
1074
		}
1075
1076
		if (is_array($ocVersion)) {
1077
			$ocVersion = implode('.', $ocVersion);
1078
		}
1079
1080 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...
1081
			&& version_compare(self::adjustVersionParts($ocVersion, $requireMin), $requireMin, '<')
1082
		) {
1083
1084
			return false;
1085
		}
1086
1087 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...
1088
			&& version_compare(self::adjustVersionParts($ocVersion, $requireMax), $requireMax, '>')
1089
		) {
1090
			return false;
1091
		}
1092
1093
		return true;
1094
	}
1095
1096
	/**
1097
	 * get the installed version of all apps
1098
	 */
1099
	public static function getAppVersions() {
1100
		static $versions;
1101
1102
		if(!$versions) {
1103
			$appConfig = \OC::$server->getAppConfig();
1104
			$versions = $appConfig->getValues(false, 'installed_version');
1105
		}
1106
		return $versions;
1107
	}
1108
1109
	/**
1110
	 * @param string $app
1111
	 * @return bool
1112
	 * @throws Exception if app is not compatible with this version of ownCloud
1113
	 * @throws Exception if no app-name was specified
1114
	 */
1115
	public static function installApp($app) {
1116
		$appName = $app; // $app will be overwritten, preserve name for error logging
1117
		$l = \OC::$server->getL10N('core');
1118
		$config = \OC::$server->getConfig();
1119
		$ocsClient = new OCSClient(
1120
			\OC::$server->getHTTPClientService(),
1121
			$config,
1122
			\OC::$server->getLogger()
1123
		);
1124
		$appData = $ocsClient->getApplication($app, \OCP\Util::getVersion());
1125
1126
		// check if app is a shipped app or not. OCS apps have an integer as id, shipped apps use a string
1127
		if (!is_numeric($app)) {
1128
			$shippedVersion = self::getAppVersion($app);
1129
			if ($appData && version_compare($shippedVersion, $appData['version'], '<')) {
1130
				$app = self::downloadApp($app);
1131
			} else {
1132
				$app = Installer::installShippedApp($app);
1133
			}
1134
		} else {
1135
			// Maybe the app is already installed - compare the version in this
1136
			// case and use the local already installed one.
1137
			// FIXME: This is a horrible hack. I feel sad. The god of code cleanness may forgive me.
1138
			$internalAppId = self::getInternalAppIdByOcs($app);
1139
			if($internalAppId !== false) {
1140
				if($appData && version_compare(\OC_App::getAppVersion($internalAppId), $appData['version'], '<')) {
1141
					$app = self::downloadApp($app);
1142
				} else {
1143
					self::enable($internalAppId);
1144
					$app = $internalAppId;
1145
				}
1146
			} else {
1147
				$app = self::downloadApp($app);
1148
			}
1149
		}
1150
1151
		if ($app !== false) {
1152
			// check if the app is compatible with this version of ownCloud
1153
			$info = self::getAppInfo($app);
1154
			if(!is_array($info)) {
1155
				throw new \Exception(
1156
					$l->t('App "%s" cannot be installed because appinfo file cannot be read.',
1157
						[$info['name']]
1158
					)
1159
				);
1160
			}
1161
1162
			$version = \OCP\Util::getVersion();
1163
			if (!self::isAppCompatible($version, $info)) {
0 ignored issues
show
Documentation introduced by
$version is of type array, but the function expects a string.

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...
1164
				throw new \Exception(
1165
					$l->t('App "%s" cannot be installed because it is not compatible with this version of the server.',
1166
						array($info['name'])
1167
					)
1168
				);
1169
			}
1170
1171
			// check for required dependencies
1172
			$dependencyAnalyzer = new DependencyAnalyzer(new Platform($config), $l);
1173
			$missing = $dependencyAnalyzer->analyze($info);
1174
			if (!empty($missing)) {
1175
				$missingMsg = join(PHP_EOL, $missing);
1176
				throw new \Exception(
1177
					$l->t('App "%s" cannot be installed because the following dependencies are not fulfilled: %s',
1178
						array($info['name'], $missingMsg)
1179
					)
1180
				);
1181
			}
1182
1183
			$config->setAppValue($app, 'enabled', 'yes');
1184
			if (isset($appData['id'])) {
1185
				$config->setAppValue($app, 'ocsid', $appData['id']);
1186
			}
1187
1188 View Code Duplication
			if(isset($info['settings']) && is_array($info['settings'])) {
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...
1189
				$appPath = self::getAppPath($app);
1190
				self::registerAutoloading($app, $appPath);
0 ignored issues
show
Security Bug introduced by
It seems like $appPath defined by self::getAppPath($app) on line 1189 can also be of type false; however, OC_App::registerAutoloading() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
1191
				\OC::$server->getSettingsManager()->setupSettings($info['settings']);
1192
			}
1193
1194
			\OC_Hook::emit('OC_App', 'post_enable', array('app' => $app));
1195
		} else {
1196
			if(empty($appName) ) {
1197
				throw new \Exception($l->t("No app name specified"));
1198
			} else {
1199
				throw new \Exception($l->t("App '%s' could not be installed!", $appName));
0 ignored issues
show
Documentation introduced by
$appName is of type string, but the function expects a array.

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...
1200
			}
1201
		}
1202
1203
		return $app;
1204
	}
1205
1206
	/**
1207
	 * update the database for the app and call the update script
1208
	 *
1209
	 * @param string $appId
1210
	 * @return bool
1211
	 */
1212
	public static function updateApp($appId) {
1213
		$appPath = self::getAppPath($appId);
1214
		if($appPath === false) {
1215
			return false;
1216
		}
1217
		$appData = self::getAppInfo($appId);
1218
		self::executeRepairSteps($appId, $appData['repair-steps']['pre-migration']);
1219
		if (file_exists($appPath . '/appinfo/database.xml')) {
1220
			OC_DB::updateDbFromStructure($appPath . '/appinfo/database.xml');
1221
		}
1222
		self::executeRepairSteps($appId, $appData['repair-steps']['post-migration']);
1223
		self::setupLiveMigrations($appId, $appData['repair-steps']['live-migration']);
1224
		unset(self::$appVersion[$appId]);
1225
		// run upgrade code
1226
		if (file_exists($appPath . '/appinfo/update.php')) {
1227
			self::loadApp($appId, false);
1228
			include $appPath . '/appinfo/update.php';
1229
		}
1230
		self::setupBackgroundJobs($appData['background-jobs']);
1231 View Code Duplication
		if(isset($appData['settings']) && is_array($appData['settings'])) {
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...
1232
			$appPath = self::getAppPath($appId);
1233
			self::registerAutoloading($appId, $appPath);
0 ignored issues
show
Security Bug introduced by
It seems like $appPath defined by self::getAppPath($appId) on line 1232 can also be of type false; however, OC_App::registerAutoloading() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
1234
			\OC::$server->getSettingsManager()->setupSettings($appData['settings']);
1235
		}
1236
1237
		//set remote/public handlers
1238
		if (array_key_exists('ocsid', $appData)) {
1239
			\OC::$server->getConfig()->setAppValue($appId, 'ocsid', $appData['ocsid']);
1240 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...
1241
			\OC::$server->getConfig()->deleteAppValue($appId, 'ocsid');
1242
		}
1243
		foreach ($appData['remote'] as $name => $path) {
1244
			\OC::$server->getConfig()->setAppValue('core', 'remote_' . $name, $appId . '/' . $path);
1245
		}
1246 View Code Duplication
		foreach ($appData['public'] 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...
1247
			\OC::$server->getConfig()->setAppValue('core', 'public_' . $name, $appId . '/' . $path);
1248
		}
1249
1250
		self::setAppTypes($appId);
1251
1252
		$version = \OC_App::getAppVersion($appId);
1253
		\OC::$server->getAppConfig()->setValue($appId, 'installed_version', $version);
0 ignored issues
show
Deprecated Code introduced by
The method OCP\IAppConfig::setValue() has been deprecated with message: 8.0.0 use method setAppValue of \OCP\IConfig Sets a value. If the key did not exist before it will be created.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1254
1255
		\OC::$server->getEventDispatcher()->dispatch(ManagerEvent::EVENT_APP_UPDATE, new ManagerEvent(
1256
			ManagerEvent::EVENT_APP_UPDATE, $appId
1257
		));
1258
1259
		return true;
1260
	}
1261
1262
	/**
1263
	 * @param string $appId
1264
	 * @param string[] $steps
1265
	 * @throws \OC\NeedsUpdateException
1266
	 */
1267
	public static function executeRepairSteps($appId, array $steps) {
1268
		if (empty($steps)) {
1269
			return;
1270
		}
1271
		// load the app
1272
		self::loadApp($appId, false);
1273
1274
		$dispatcher = OC::$server->getEventDispatcher();
1275
1276
		// load the steps
1277
		$r = new Repair([], $dispatcher);
1278
		foreach ($steps as $step) {
1279
			try {
1280
				$r->addStep($step);
1281
			} catch (Exception $ex) {
1282
				$r->emit('\OC\Repair', 'error', [$ex->getMessage()]);
1283
				\OC::$server->getLogger()->logException($ex);
1284
			}
1285
		}
1286
		// run the steps
1287
		$r->run();
1288
	}
1289
1290
	public static function setupBackgroundJobs(array $jobs) {
1291
		$queue = \OC::$server->getJobList();
1292
		foreach ($jobs as $job) {
1293
			$queue->add($job);
1294
		}
1295
	}
1296
1297
	/**
1298
	 * @param string $appId
1299
	 * @param string[] $steps
1300
	 */
1301
	private static function setupLiveMigrations($appId, array $steps) {
1302
		$queue = \OC::$server->getJobList();
1303
		foreach ($steps as $step) {
1304
			$queue->add('OC\Migration\BackgroundRepair', [
1305
				'app' => $appId,
1306
				'step' => $step]);
1307
		}
1308
	}
1309
1310
	/**
1311
	 * @param string $appId
1312
	 * @return \OC\Files\View|false
1313
	 */
1314
	public static function getStorage($appId) {
1315
		if (OC_App::isEnabled($appId)) { //sanity check
1316
			if (OC_User::isLoggedIn()) {
0 ignored issues
show
Deprecated Code introduced by
The method OC_User::isLoggedIn() has been deprecated with message: use \OC::$server->getUserSession()->isLoggedIn()

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1317
				$view = new \OC\Files\View('/' . OC_User::getUser());
1318
				if (!$view->file_exists($appId)) {
1319
					$view->mkdir($appId);
1320
				}
1321
				return new \OC\Files\View('/' . OC_User::getUser() . '/' . $appId);
1322
			} else {
1323
				\OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ', user not logged in', \OCP\Util::ERROR);
1324
				return false;
1325
			}
1326
		} else {
1327
			\OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ' not enabled', \OCP\Util::ERROR);
1328
			return false;
1329
		}
1330
	}
1331
1332
	/**
1333
	 * parses the app data array and enhanced the 'description' value
1334
	 *
1335
	 * @param array $data the app data
1336
	 * @return array improved app data
1337
	 */
1338
	public static function parseAppInfo(array $data) {
1339
1340
		// just modify the description if it is available
1341
		// otherwise this will create a $data element with an empty 'description'
1342
		if (isset($data['description'])) {
1343
			if (is_string($data['description'])) {
1344
				// sometimes the description contains line breaks and they are then also
1345
				// shown in this way in the app management which isn't wanted as HTML
1346
				// manages line breaks itself
1347
1348
				// first of all we split on empty lines
1349
				$paragraphs = preg_split("!\n[[:space:]]*\n!mu", $data['description']);
1350
1351
				$result = [];
1352
				foreach ($paragraphs as $value) {
1353
					// replace multiple whitespace (tabs, space, newlines) inside a paragraph
1354
					// with a single space - also trims whitespace
1355
					$result[] = trim(preg_replace('![[:space:]]+!mu', ' ', $value));
1356
				}
1357
1358
				// join the single paragraphs with a empty line in between
1359
				$data['description'] = implode("\n\n", $result);
1360
1361
			} else {
1362
				$data['description'] = '';
1363
			}
1364
		}
1365
1366
		return $data;
1367
	}
1368
}
1369