Completed
Push — stable10 ( c7e40d...1bdc4d )
by Roeland
115:21 queued 102:20
created

OC_App   F

Complexity

Total Complexity 242

Size/Duplication

Total Lines 1368
Duplicated Lines 3 %

Coupling/Cohesion

Components 3
Dependencies 33

Importance

Changes 0
Metric Value
wmc 242
lcom 3
cbo 33
dl 41
loc 1368
rs 0.5217
c 0
b 0
f 0

49 Methods

Rating   Name   Duplication   Size   Complexity  
A cleanAppId() 0 3 1
A isAppLoaded() 0 3 1
C loadApps() 0 26 8
B loadApp() 0 26 6
A requireAppFile() 0 12 3
A isType() 0 12 4
A getAppTypes() 0 12 3
A setAppTypes() 0 14 3
A isShipped() 0 3 1
B getEnabledApps() 0 25 4
A isEnabled() 0 3 1
C enable() 5 28 7
A downloadApp() 0 16 3
A removeApp() 0 7 2
A disable() 0 22 3
B getSettingsNavigation() 0 64 7
A registerAutoloading() 0 16 4
B getInstallPath() 0 14 5
D findAppInDirectories() 0 40 10
A getAppPath() 0 10 4
A isAppDirWritable() 0 4 2
A getAppWebPath() 0 6 2
A getAppVersion() 0 7 3
A getAppVersionByPath() 0 5 2
C proceedNavigation() 0 23 10
D getAppInfo() 0 33 10
A getNavigation() 0 5 1
A getCurrentApp() 0 17 4
A getForms() 0 17 4
A registerAdmin() 0 3 1
A registerPersonal() 0 3 1
A registerLogIn() 0 3 1
A getAlternativeLogIns() 0 3 1
C getAllApps() 0 24 8
D listAllApps() 0 93 19
A getInternalAppIdByOcs() 0 9 3
B getAppstoreApps() 0 55 8
A shouldUpgrade() 0 11 4
A adjustVersionParts() 0 13 3
C isAppCompatible() 11 36 11
A getAppVersions() 0 9 2
D installApp() 5 90 15
C updateApp() 11 49 10
B executeRepairSteps() 0 22 4
A setupBackgroundJobs() 0 6 2
A setupLiveMigrations() 0 8 2
A getStorage() 0 17 4
C findBestL10NOption() 0 44 12
C parseAppInfo() 9 40 13

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like OC_App often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use OC_App, and based on these observations, apply Extract Interface, too.

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 bool $path
668
	 * @param string $lang
669
	 * @return array|null
670
	 * @note all data is read from info.xml, not just pre-defined fields
671
	 */
672
	public static function getAppInfo($appId, $path = false, $lang = null) {
673
		if ($path) {
674
			$file = $appId;
675
		} else {
676
			if ($lang === null && isset(self::$appInfo[$appId])) {
677
				return self::$appInfo[$appId];
678
			}
679
			$appPath = self::getAppPath($appId);
680
			if($appPath === false) {
681
				return null;
682
			}
683
			$file = $appPath . '/appinfo/info.xml';
684
		}
685
686
		$parser = new \OC\App\InfoParser(\OC::$server->getURLGenerator());
687
		$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 703 which is incompatible with the return type documented by OC_App::getAppInfo of type array|null.
Loading history...
688
689
		if (is_array($data)) {
690
			$data = OC_App::parseAppInfo($data, $lang);
691
		}
692
		if(isset($data['ocsid'])) {
693
			$storedId = \OC::$server->getConfig()->getAppValue($appId, 'ocsid');
694
			if($storedId !== '' && $storedId !== $data['ocsid']) {
695
				$data['ocsid'] = $storedId;
696
			}
697
		}
698
699
		if ($lang === null) {
700
			self::$appInfo[$appId] = $data;
701
		}
702
703
		return $data;
704
	}
705
706
	/**
707
	 * Returns the navigation
708
	 *
709
	 * @return array
710
	 *
711
	 * This function returns an array containing all entries added. The
712
	 * entries are sorted by the key 'order' ascending. Additional to the keys
713
	 * given for each app the following keys exist:
714
	 *   - active: boolean, signals if the user is on this navigation entry
715
	 */
716
	public static function getNavigation() {
717
		$entries = OC::$server->getNavigationManager()->getAll();
718
		$navigation = self::proceedNavigation($entries);
719
		return $navigation;
720
	}
721
722
	/**
723
	 * get the id of loaded app
724
	 *
725
	 * @return string
726
	 */
727
	public static function getCurrentApp() {
728
		$request = \OC::$server->getRequest();
729
		$script = substr($request->getScriptName(), strlen(OC::$WEBROOT) + 1);
730
		$topFolder = substr($script, 0, strpos($script, '/'));
731
		if (empty($topFolder)) {
732
			$path_info = $request->getPathInfo();
733
			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...
734
				$topFolder = substr($path_info, 1, strpos($path_info, '/', 1) - 1);
735
			}
736
		}
737
		if ($topFolder == 'apps') {
738
			$length = strlen($topFolder);
739
			return substr($script, $length + 1, strpos($script, '/', $length + 1) - $length - 1);
740
		} else {
741
			return $topFolder;
742
		}
743
	}
744
745
	/**
746
	 * @param string $type
747
	 * @return array
748
	 */
749
	public static function getForms($type) {
750
		$forms = array();
751
		switch ($type) {
752
			case 'admin':
753
				$source = self::$adminForms;
754
				break;
755
			case 'personal':
756
				$source = self::$personalForms;
757
				break;
758
			default:
759
				return array();
760
		}
761
		foreach ($source as $form) {
762
			$forms[] = include $form;
763
		}
764
		return $forms;
765
	}
766
767
	/**
768
	 * register an admin form to be shown
769
	 *
770
	 * @param string $app
771
	 * @param string $page
772
	 */
773
	public static function registerAdmin($app, $page) {
774
		self::$adminForms[] = $app . '/' . $page . '.php';
775
	}
776
777
	/**
778
	 * register a personal form to be shown
779
	 * @param string $app
780
	 * @param string $page
781
	 */
782
	public static function registerPersonal($app, $page) {
783
		self::$personalForms[] = $app . '/' . $page . '.php';
784
	}
785
786
	/**
787
	 * @param array $entry
788
	 */
789
	public static function registerLogIn(array $entry) {
790
		self::$altLogin[] = $entry;
791
	}
792
793
	/**
794
	 * @return array
795
	 */
796
	public static function getAlternativeLogIns() {
797
		return self::$altLogin;
798
	}
799
800
	/**
801
	 * get a list of all apps in the apps folder
802
	 *
803
	 * @return array an array of app names (string IDs)
804
	 * @todo: change the name of this method to getInstalledApps, which is more accurate
805
	 */
806
	public static function getAllApps() {
807
808
		$apps = array();
809
810
		foreach (OC::$APPSROOTS as $apps_dir) {
811
			if (!is_readable($apps_dir['path'])) {
812
				\OCP\Util::writeLog('core', 'unable to read app folder : ' . $apps_dir['path'], \OCP\Util::WARN);
813
				continue;
814
			}
815
			$dh = opendir($apps_dir['path']);
816
817
			if (is_resource($dh)) {
818
				while (($file = readdir($dh)) !== false) {
819
820
					if ($file[0] != '.' and is_dir($apps_dir['path'] . '/' . $file) and is_file($apps_dir['path'] . '/' . $file . '/appinfo/info.xml')) {
821
822
						$apps[] = $file;
823
					}
824
				}
825
			}
826
		}
827
828
		return $apps;
829
	}
830
831
	/**
832
	 * List all apps, this is used in apps.php
833
	 *
834
	 * @param bool $onlyLocal
835
	 * @param bool $includeUpdateInfo Should we check whether there is an update
836
	 *                                in the app store?
837
	 * @param OCSClient $ocsClient
838
	 * @return array
839
	 */
840
	public static function listAllApps($onlyLocal = false,
841
									   $includeUpdateInfo = true,
842
									   OCSClient $ocsClient) {
843
		$installedApps = OC_App::getAllApps();
844
845
		//TODO which apps do we want to blacklist and how do we integrate
846
		// blacklisting with the multi apps folder feature?
847
848
		//we don't want to show configuration for these
849
		$blacklist = \OC::$server->getAppManager()->getAlwaysEnabledApps();
850
		$appList = array();
851
		$langCode = \OC::$server->getL10N('core')->getLanguageCode();
852
853
		foreach ($installedApps as $app) {
854
			if (array_search($app, $blacklist) === false) {
855
856
				$info = OC_App::getAppInfo($app, false, $langCode);
857
				if (!is_array($info)) {
858
					\OCP\Util::writeLog('core', 'Could not read app info file for app "' . $app . '"', \OCP\Util::ERROR);
859
					continue;
860
				}
861
862
				if (!isset($info['name'])) {
863
					\OCP\Util::writeLog('core', 'App id "' . $app . '" has no name in appinfo', \OCP\Util::ERROR);
864
					continue;
865
				}
866
867
				$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...
868
				$info['groups'] = null;
869
				if ($enabled === 'yes') {
870
					$active = true;
871
				} else if ($enabled === 'no') {
872
					$active = false;
873
				} else {
874
					$active = true;
875
					$info['groups'] = $enabled;
876
				}
877
878
				$info['active'] = $active;
879
880
				if (self::isShipped($app)) {
881
					$info['internal'] = true;
882
					$info['level'] = self::officialApp;
883
					$info['removable'] = false;
884
				} else {
885
					$info['internal'] = false;
886
					$info['removable'] = true;
887
				}
888
889
				$info['update'] = ($includeUpdateInfo) ? Installer::isUpdateAvailable($app) : null;
890
891
				$appPath = self::getAppPath($app);
892
				if($appPath !== false) {
893
					$appIcon = $appPath . '/img/' . $app . '.svg';
894
					if (file_exists($appIcon)) {
895
						$info['preview'] = \OC::$server->getURLGenerator()->imagePath($app, $app . '.svg');
896
						$info['previewAsIcon'] = true;
897
					} else {
898
						$appIcon = $appPath . '/img/app.svg';
899
						if (file_exists($appIcon)) {
900
							$info['preview'] = \OC::$server->getURLGenerator()->imagePath($app, 'app.svg');
901
							$info['previewAsIcon'] = true;
902
						}
903
					}
904
				}
905
				$info['version'] = OC_App::getAppVersion($app);
906
				$appList[] = $info;
907
			}
908
		}
909
		if ($onlyLocal) {
910
			$remoteApps = [];
911
		} else {
912
			$remoteApps = OC_App::getAppstoreApps('approved', null, $ocsClient);
913
		}
914
		if ($remoteApps) {
915
			// Remove duplicates
916
			foreach ($appList as $app) {
917
				foreach ($remoteApps AS $key => $remote) {
918
					if ($app['name'] === $remote['name'] ||
919
						(isset($app['ocsid']) &&
920
							$app['ocsid'] === $remote['id'])
921
					) {
922
						unset($remoteApps[$key]);
923
					}
924
				}
925
			}
926
			$combinedApps = array_merge($appList, $remoteApps);
927
		} else {
928
			$combinedApps = $appList;
929
		}
930
931
		return $combinedApps;
932
	}
933
934
	/**
935
	 * Returns the internal app ID or false
936
	 * @param string $ocsID
937
	 * @return string|false
938
	 */
939
	public static function getInternalAppIdByOcs($ocsID) {
940
		if(is_numeric($ocsID)) {
941
			$idArray = \OC::$server->getAppConfig()->getValues(false, 'ocsid');
942
			if(array_search($ocsID, $idArray)) {
943
				return array_search($ocsID, $idArray);
944
			}
945
		}
946
		return false;
947
	}
948
949
	/**
950
	 * Get a list of all apps on the appstore
951
	 * @param string $filter
952
	 * @param string|null $category
953
	 * @param OCSClient $ocsClient
954
	 * @return array|bool  multi-dimensional array of apps.
955
	 *                     Keys: id, name, type, typename, personid, license, detailpage, preview, changed, description
956
	 */
957
	public static function getAppstoreApps($filter = 'approved',
958
										   $category = null,
959
										   OCSClient $ocsClient) {
960
		$categories = [$category];
961
962
		if (is_null($category)) {
963
			$categoryNames = $ocsClient->getCategories(\OCP\Util::getVersion());
964
			if (is_array($categoryNames)) {
965
				// Check that categories of apps were retrieved correctly
966
				if (!$categories = array_keys($categoryNames)) {
967
					return false;
968
				}
969
			} else {
970
				return false;
971
			}
972
		}
973
974
		$page = 0;
975
		$remoteApps = $ocsClient->getApplications($categories, $page, $filter, \OCP\Util::getVersion());
976
		$apps = [];
977
		$i = 0;
978
		$l = \OC::$server->getL10N('core');
979
		foreach ($remoteApps as $app) {
980
			$potentialCleanId = self::getInternalAppIdByOcs($app['id']);
981
			// enhance app info (for example the description)
982
			$apps[$i] = OC_App::parseAppInfo($app);
983
			$apps[$i]['author'] = $app['personid'];
984
			$apps[$i]['ocs_id'] = $app['id'];
985
			$apps[$i]['internal'] = 0;
986
			$apps[$i]['active'] = ($potentialCleanId !== false) ? self::isEnabled($potentialCleanId) : false;
987
			$apps[$i]['update'] = false;
988
			$apps[$i]['groups'] = false;
989
			$apps[$i]['score'] = $app['score'];
990
			$apps[$i]['removable'] = false;
991
			if ($app['label'] == 'recommended') {
992
				$apps[$i]['internallabel'] = (string)$l->t('Recommended');
993
				$apps[$i]['internalclass'] = 'recommendedapp';
994
			}
995
996
			// Apps from the appstore are always assumed to be compatible with the
997
			// the current release as the initial filtering is done on the appstore
998
			$apps[$i]['dependencies']['owncloud']['@attributes']['min-version'] = implode('.', \OCP\Util::getVersion());
999
			$apps[$i]['dependencies']['owncloud']['@attributes']['max-version'] = implode('.', \OCP\Util::getVersion());
1000
1001
			$i++;
1002
		}
1003
1004
1005
1006
		if (empty($apps)) {
1007
			return false;
1008
		} else {
1009
			return $apps;
1010
		}
1011
	}
1012
1013
	public static function shouldUpgrade($app) {
1014
		$versions = self::getAppVersions();
1015
		$currentVersion = OC_App::getAppVersion($app);
1016
		if ($currentVersion && isset($versions[$app])) {
1017
			$installedVersion = $versions[$app];
1018
			if (!version_compare($currentVersion, $installedVersion, '=')) {
1019
				return true;
1020
			}
1021
		}
1022
		return false;
1023
	}
1024
1025
	/**
1026
	 * Adjust the number of version parts of $version1 to match
1027
	 * the number of version parts of $version2.
1028
	 *
1029
	 * @param string $version1 version to adjust
1030
	 * @param string $version2 version to take the number of parts from
1031
	 * @return string shortened $version1
1032
	 */
1033
	private static function adjustVersionParts($version1, $version2) {
1034
		$version1 = explode('.', $version1);
1035
		$version2 = explode('.', $version2);
1036
		// reduce $version1 to match the number of parts in $version2
1037
		while (count($version1) > count($version2)) {
1038
			array_pop($version1);
1039
		}
1040
		// if $version1 does not have enough parts, add some
1041
		while (count($version1) < count($version2)) {
1042
			$version1[] = '0';
1043
		}
1044
		return implode('.', $version1);
1045
	}
1046
1047
	/**
1048
	 * Check whether the current ownCloud version matches the given
1049
	 * application's version requirements.
1050
	 *
1051
	 * The comparison is made based on the number of parts that the
1052
	 * app info version has. For example for ownCloud 6.0.3 if the
1053
	 * app info version is expecting version 6.0, the comparison is
1054
	 * made on the first two parts of the ownCloud version.
1055
	 * This means that it's possible to specify "requiremin" => 6
1056
	 * and "requiremax" => 6 and it will still match ownCloud 6.0.3.
1057
	 *
1058
	 * @param string $ocVersion ownCloud version to check against
1059
	 * @param array $appInfo app info (from xml)
1060
	 *
1061
	 * @return boolean true if compatible, otherwise false
1062
	 */
1063
	public static function isAppCompatible($ocVersion, $appInfo) {
1064
		$requireMin = '';
1065
		$requireMax = '';
1066
		if (isset($appInfo['dependencies']['owncloud']['@attributes']['min-version'])) {
1067
			$requireMin = $appInfo['dependencies']['owncloud']['@attributes']['min-version'];
1068
		} else if (isset($appInfo['requiremin'])) {
1069
			$requireMin = $appInfo['requiremin'];
1070
		} else if (isset($appInfo['require'])) {
1071
			$requireMin = $appInfo['require'];
1072
		}
1073
1074
		if (isset($appInfo['dependencies']['owncloud']['@attributes']['max-version'])) {
1075
			$requireMax = $appInfo['dependencies']['owncloud']['@attributes']['max-version'];
1076
		} else if (isset($appInfo['requiremax'])) {
1077
			$requireMax = $appInfo['requiremax'];
1078
		}
1079
1080
		if (is_array($ocVersion)) {
1081
			$ocVersion = implode('.', $ocVersion);
1082
		}
1083
1084 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...
1085
			&& version_compare(self::adjustVersionParts($ocVersion, $requireMin), $requireMin, '<')
1086
		) {
1087
1088
			return false;
1089
		}
1090
1091 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...
1092
			&& version_compare(self::adjustVersionParts($ocVersion, $requireMax), $requireMax, '>')
1093
		) {
1094
			return false;
1095
		}
1096
1097
		return true;
1098
	}
1099
1100
	/**
1101
	 * get the installed version of all apps
1102
	 */
1103
	public static function getAppVersions() {
1104
		static $versions;
1105
1106
		if(!$versions) {
1107
			$appConfig = \OC::$server->getAppConfig();
1108
			$versions = $appConfig->getValues(false, 'installed_version');
1109
		}
1110
		return $versions;
1111
	}
1112
1113
	/**
1114
	 * @param string $app
1115
	 * @return bool
1116
	 * @throws Exception if app is not compatible with this version of ownCloud
1117
	 * @throws Exception if no app-name was specified
1118
	 */
1119
	public static function installApp($app) {
1120
		$appName = $app; // $app will be overwritten, preserve name for error logging
1121
		$l = \OC::$server->getL10N('core');
1122
		$config = \OC::$server->getConfig();
1123
		$ocsClient = new OCSClient(
1124
			\OC::$server->getHTTPClientService(),
1125
			$config,
1126
			\OC::$server->getLogger()
1127
		);
1128
		$appData = $ocsClient->getApplication($app, \OCP\Util::getVersion());
1129
1130
		// check if app is a shipped app or not. OCS apps have an integer as id, shipped apps use a string
1131
		if (!is_numeric($app)) {
1132
			$shippedVersion = self::getAppVersion($app);
1133
			if ($appData && version_compare($shippedVersion, $appData['version'], '<')) {
1134
				$app = self::downloadApp($app);
1135
			} else {
1136
				$app = Installer::installShippedApp($app);
1137
			}
1138
		} else {
1139
			// Maybe the app is already installed - compare the version in this
1140
			// case and use the local already installed one.
1141
			// FIXME: This is a horrible hack. I feel sad. The god of code cleanness may forgive me.
1142
			$internalAppId = self::getInternalAppIdByOcs($app);
1143
			if($internalAppId !== false) {
1144
				if($appData && version_compare(\OC_App::getAppVersion($internalAppId), $appData['version'], '<')) {
1145
					$app = self::downloadApp($app);
1146
				} else {
1147
					self::enable($internalAppId);
1148
					$app = $internalAppId;
1149
				}
1150
			} else {
1151
				$app = self::downloadApp($app);
1152
			}
1153
		}
1154
1155
		if ($app !== false) {
1156
			// check if the app is compatible with this version of ownCloud
1157
			$info = self::getAppInfo($app);
1158
			if(!is_array($info)) {
1159
				throw new \Exception(
1160
					$l->t('App "%s" cannot be installed because appinfo file cannot be read.',
1161
						[$info['name']]
1162
					)
1163
				);
1164
			}
1165
1166
			$version = \OCP\Util::getVersion();
1167
			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...
1168
				throw new \Exception(
1169
					$l->t('App "%s" cannot be installed because it is not compatible with this version of the server.',
1170
						array($info['name'])
1171
					)
1172
				);
1173
			}
1174
1175
			// check for required dependencies
1176
			$dependencyAnalyzer = new DependencyAnalyzer(new Platform($config), $l);
1177
			$missing = $dependencyAnalyzer->analyze($info);
1178
			if (!empty($missing)) {
1179
				$missingMsg = join(PHP_EOL, $missing);
1180
				throw new \Exception(
1181
					$l->t('App "%s" cannot be installed because the following dependencies are not fulfilled: %s',
1182
						array($info['name'], $missingMsg)
1183
					)
1184
				);
1185
			}
1186
1187
			$config->setAppValue($app, 'enabled', 'yes');
1188
			if (isset($appData['id'])) {
1189
				$config->setAppValue($app, 'ocsid', $appData['id']);
1190
			}
1191
1192 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...
1193
				$appPath = self::getAppPath($app);
1194
				self::registerAutoloading($app, $appPath);
0 ignored issues
show
Security Bug introduced by
It seems like $appPath defined by self::getAppPath($app) on line 1193 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...
1195
				\OC::$server->getSettingsManager()->setupSettings($info['settings']);
1196
			}
1197
1198
			\OC_Hook::emit('OC_App', 'post_enable', array('app' => $app));
1199
		} else {
1200
			if(empty($appName) ) {
1201
				throw new \Exception($l->t("No app name specified"));
1202
			} else {
1203
				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...
1204
			}
1205
		}
1206
1207
		return $app;
1208
	}
1209
1210
	/**
1211
	 * update the database for the app and call the update script
1212
	 *
1213
	 * @param string $appId
1214
	 * @return bool
1215
	 */
1216
	public static function updateApp($appId) {
1217
		$appPath = self::getAppPath($appId);
1218
		if($appPath === false) {
1219
			return false;
1220
		}
1221
		$appData = self::getAppInfo($appId);
1222
		self::executeRepairSteps($appId, $appData['repair-steps']['pre-migration']);
1223
		if (file_exists($appPath . '/appinfo/database.xml')) {
1224
			OC_DB::updateDbFromStructure($appPath . '/appinfo/database.xml');
1225
		}
1226
		self::executeRepairSteps($appId, $appData['repair-steps']['post-migration']);
1227
		self::setupLiveMigrations($appId, $appData['repair-steps']['live-migration']);
1228
		unset(self::$appVersion[$appId]);
1229
		// run upgrade code
1230
		if (file_exists($appPath . '/appinfo/update.php')) {
1231
			self::loadApp($appId, false);
1232
			include $appPath . '/appinfo/update.php';
1233
		}
1234
		self::setupBackgroundJobs($appData['background-jobs']);
1235 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...
1236
			$appPath = self::getAppPath($appId);
1237
			self::registerAutoloading($appId, $appPath);
0 ignored issues
show
Security Bug introduced by
It seems like $appPath defined by self::getAppPath($appId) on line 1236 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...
1238
			\OC::$server->getSettingsManager()->setupSettings($appData['settings']);
1239
		}
1240
1241
		//set remote/public handlers
1242
		if (array_key_exists('ocsid', $appData)) {
1243
			\OC::$server->getConfig()->setAppValue($appId, 'ocsid', $appData['ocsid']);
1244 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...
1245
			\OC::$server->getConfig()->deleteAppValue($appId, 'ocsid');
1246
		}
1247
		foreach ($appData['remote'] as $name => $path) {
1248
			\OC::$server->getConfig()->setAppValue('core', 'remote_' . $name, $appId . '/' . $path);
1249
		}
1250 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...
1251
			\OC::$server->getConfig()->setAppValue('core', 'public_' . $name, $appId . '/' . $path);
1252
		}
1253
1254
		self::setAppTypes($appId);
1255
1256
		$version = \OC_App::getAppVersion($appId);
1257
		\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...
1258
1259
		\OC::$server->getEventDispatcher()->dispatch(ManagerEvent::EVENT_APP_UPDATE, new ManagerEvent(
1260
			ManagerEvent::EVENT_APP_UPDATE, $appId
1261
		));
1262
1263
		return true;
1264
	}
1265
1266
	/**
1267
	 * @param string $appId
1268
	 * @param string[] $steps
1269
	 * @throws \OC\NeedsUpdateException
1270
	 */
1271
	public static function executeRepairSteps($appId, array $steps) {
1272
		if (empty($steps)) {
1273
			return;
1274
		}
1275
		// load the app
1276
		self::loadApp($appId, false);
1277
1278
		$dispatcher = OC::$server->getEventDispatcher();
1279
1280
		// load the steps
1281
		$r = new Repair([], $dispatcher);
1282
		foreach ($steps as $step) {
1283
			try {
1284
				$r->addStep($step);
1285
			} catch (Exception $ex) {
1286
				$r->emit('\OC\Repair', 'error', [$ex->getMessage()]);
1287
				\OC::$server->getLogger()->logException($ex);
1288
			}
1289
		}
1290
		// run the steps
1291
		$r->run();
1292
	}
1293
1294
	public static function setupBackgroundJobs(array $jobs) {
1295
		$queue = \OC::$server->getJobList();
1296
		foreach ($jobs as $job) {
1297
			$queue->add($job);
1298
		}
1299
	}
1300
1301
	/**
1302
	 * @param string $appId
1303
	 * @param string[] $steps
1304
	 */
1305
	private static function setupLiveMigrations($appId, array $steps) {
1306
		$queue = \OC::$server->getJobList();
1307
		foreach ($steps as $step) {
1308
			$queue->add('OC\Migration\BackgroundRepair', [
1309
				'app' => $appId,
1310
				'step' => $step]);
1311
		}
1312
	}
1313
1314
	/**
1315
	 * @param string $appId
1316
	 * @return \OC\Files\View|false
1317
	 */
1318
	public static function getStorage($appId) {
1319
		if (OC_App::isEnabled($appId)) { //sanity check
1320
			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...
1321
				$view = new \OC\Files\View('/' . OC_User::getUser());
1322
				if (!$view->file_exists($appId)) {
1323
					$view->mkdir($appId);
1324
				}
1325
				return new \OC\Files\View('/' . OC_User::getUser() . '/' . $appId);
1326
			} else {
1327
				\OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ', user not logged in', \OCP\Util::ERROR);
1328
				return false;
1329
			}
1330
		} else {
1331
			\OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ' not enabled', \OCP\Util::ERROR);
1332
			return false;
1333
		}
1334
	}
1335
1336
	protected static function findBestL10NOption($options, $lang) {
1337
		$fallback = $similarLangFallback = $englishFallback = false;
1338
1339
		$lang = strtolower($lang);
1340
		$similarLang = $lang;
1341
		if (strpos($similarLang, '_')) {
1342
			// For "de_DE" we want to find "de" and the other way around
1343
			$similarLang = substr($lang, 0, strpos($lang, '_'));
1344
		}
1345
1346
		foreach ($options as $option) {
1347
			if (is_array($option)) {
1348
				if ($fallback === false) {
1349
					$fallback = $option['@value'];
1350
				}
1351
1352
				if (!isset($option['@attributes']['lang'])) {
1353
					continue;
1354
				}
1355
1356
				$attributeLang = strtolower($option['@attributes']['lang']);
1357
				if ($attributeLang === $lang) {
1358
					return $option['@value'];
1359
				}
1360
1361
				if ($attributeLang === $similarLang) {
1362
					$similarLangFallback = $option['@value'];
1363
				} else if (strpos($attributeLang, $similarLang . '_') === 0) {
1364
					if ($similarLangFallback === false) {
1365
						$similarLangFallback =  $option['@value'];
1366
					}
1367
				}
1368
			} else {
1369
				$englishFallback = $option;
1370
			}
1371
		}
1372
1373
		if ($similarLangFallback !== false) {
1374
			return $similarLangFallback;
1375
		} else if ($englishFallback !== false) {
1376
			return $englishFallback;
1377
		}
1378
		return (string) $fallback;
1379
	}
1380
1381
	/**
1382
	 * parses the app data array and enhanced the 'description' value
1383
	 *
1384
	 * @param array $data the app data
1385
	 * @param string $lang
1386
	 * @return array improved app data
1387
	 */
1388
	public static function parseAppInfo(array $data, $lang = null) {
1389
1390 View Code Duplication
		if ($lang && isset($data['name']) && is_array($data['name'])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $lang of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null 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...
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...
1391
			$data['name'] = self::findBestL10NOption($data['name'], $lang);
1392
		}
1393 View Code Duplication
		if ($lang && isset($data['summary']) && is_array($data['summary'])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $lang of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null 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...
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...
1394
			$data['summary'] = self::findBestL10NOption($data['summary'], $lang);
1395
		}
1396 View Code Duplication
		if ($lang && isset($data['description']) && is_array($data['description'])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $lang of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null 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...
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...
1397
			$data['description'] = self::findBestL10NOption($data['description'], $lang);
1398
		}
1399
1400
		// just modify the description if it is available
1401
		// otherwise this will create a $data element with an empty 'description'
1402
		if (isset($data['description'])) {
1403
			if (is_string($data['description'])) {
1404
				// sometimes the description contains line breaks and they are then also
1405
				// shown in this way in the app management which isn't wanted as HTML
1406
				// manages line breaks itself
1407
1408
				// first of all we split on empty lines
1409
				$paragraphs = preg_split("!\n[[:space:]]*\n!mu", $data['description']);
1410
1411
				$result = [];
1412
				foreach ($paragraphs as $value) {
1413
					// replace multiple whitespace (tabs, space, newlines) inside a paragraph
1414
					// with a single space - also trims whitespace
1415
					$result[] = trim(preg_replace('![[:space:]]+!mu', ' ', $value));
1416
				}
1417
1418
				// join the single paragraphs with a empty line in between
1419
				$data['description'] = implode("\n\n", $result);
1420
1421
			} else {
1422
				$data['description'] = '';
1423
			}
1424
		}
1425
1426
		return $data;
1427
	}
1428
}
1429