Completed
Pull Request — master (#31670)
by Phil
76:35 queued 37:37
created

OC_App::getAppVersions()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 0
dl 0
loc 9
rs 9.9666
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Arthur Schiwon <[email protected]>
4
 * @author Bart Visscher <[email protected]>
5
 * @author Bernhard Posselt <[email protected]>
6
 * @author Borjan Tchakaloff <[email protected]>
7
 * @author Brice Maron <[email protected]>
8
 * @author Christopher Schäpers <[email protected]>
9
 * @author Felix Moeller <[email protected]>
10
 * @author Frank Karlitschek <[email protected]>
11
 * @author Georg Ehrke <[email protected]>
12
 * @author Jakob Sack <[email protected]>
13
 * @author Joas Schilling <[email protected]>
14
 * @author Jörn Friedrich Dreyer <[email protected]>
15
 * @author Kamil Domanski <[email protected]>
16
 * @author Lukas Reschke <[email protected]>
17
 * @author Markus Goetz <[email protected]>
18
 * @author Morris Jobke <[email protected]>
19
 * @author Philipp Schaffrath <[email protected]>
20
 * @author ppaysant <[email protected]>
21
 * @author RealRancor <[email protected]>
22
 * @author Robin Appelman <[email protected]>
23
 * @author Robin McCorkell <[email protected]>
24
 * @author Roeland Jago Douma <[email protected]>
25
 * @author Sam Tuke <[email protected]>
26
 * @author Thomas Müller <[email protected]>
27
 * @author Thomas Tanghus <[email protected]>
28
 * @author Tom Needham <[email protected]>
29
 * @author Vincent Petry <[email protected]>
30
 *
31
 * @copyright Copyright (c) 2018, ownCloud GmbH
32
 * @license AGPL-3.0
33
 *
34
 * This code is free software: you can redistribute it and/or modify
35
 * it under the terms of the GNU Affero General Public License, version 3,
36
 * as published by the Free Software Foundation.
37
 *
38
 * This program is distributed in the hope that it will be useful,
39
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
40
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
41
 * GNU Affero General Public License for more details.
42
 *
43
 * You should have received a copy of the GNU Affero General Public License, version 3,
44
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
45
 *
46
 */
47
use OC\App\DependencyAnalyzer;
48
use OC\App\InfoParser;
49
use OC\App\Platform;
50
use OC\Installer;
51
use OC\Repair;
52
53
/**
54
 * This class manages the apps. It allows them to register and integrate in the
55
 * ownCloud ecosystem. Furthermore, this class is responsible for installing,
56
 * upgrading and removing apps.
57
 */
58
class OC_App {
59
	private static $appVersion = [];
60
	private static $adminForms = [];
61
	private static $personalForms = [];
62
	private static $appInfo = [];
63
	private static $appTypes = [];
64
	private static $loadedApps = [];
65
	private static $loadedTypes = [];
66
	private static $altLogin = [];
67
	const officialApp = 200;
68
	const approvedApp = 100;
69
70
	/**
71
	 * clean the appId
72
	 *
73
	 * @param string|boolean $app AppId that needs to be cleaned
74
	 * @return string
75
	 */
76
	public static function cleanAppId($app) {
77
		return \str_replace(['\0', '/', '\\', '..'], '', $app);
78
	}
79
80
	/**
81
	 * Check if an app is loaded
82
	 *
83
	 * @param string $app
84
	 * @return bool
85
	 */
86
	public static function isAppLoaded($app) {
87
		return \in_array($app, self::$loadedApps, true);
88
	}
89
90
	/**
91
	 * loads all apps
92
	 *
93
	 * @param string[] | string | null $types
94
	 * @return bool
95
	 *
96
	 * This function walks through the ownCloud directory and loads all apps
97
	 * it can find. A directory contains an app if the file /appinfo/info.xml
98
	 * exists.
99
	 *
100
	 * if $types is set, only apps of those types will be loaded
101
	 */
102
	public static function loadApps($types = null) {
103
		if (\is_array($types) && !\array_diff($types, self::$loadedTypes)) {
104
			return true;
105
		}
106
		if (\OC::$server->getSystemConfig()->getValue('maintenance', false)) {
107
			return false;
108
		}
109
		// Load the enabled apps here
110
		$apps = self::getEnabledApps();
111
112
		// Add each apps' folder as allowed class path
113
		foreach ($apps as $app) {
114
			if (self::isAppLoaded($app)) {
115
				continue;
116
			}
117
			$path = self::getAppPath($app);
118
			if ($path !== false) {
119
				self::registerAutoloading($app, $path);
120
			}
121
		}
122
123
		// prevent app.php from printing output
124
		\ob_start();
125
		foreach ($apps as $app) {
126
			if (($types === null or self::isType($app, $types)) && !\in_array($app, self::$loadedApps)) {
127
				self::loadApp($app);
128
			}
129
		}
130
		\ob_end_clean();
131
132
		if (\is_array($types)) {
133
			self::$loadedTypes = \array_merge(self::$loadedTypes, $types);
134
		}
135
136
		\OC_Hook::emit('OC_App', 'loadedApps');
137
		return true;
138
	}
139
140
	/**
141
	 * load a single app
142
	 *
143
	 * @param string $app
144
	 * @param bool $checkUpgrade whether an upgrade check should be done
145
	 * @throws \OC\NeedsUpdateException
146
	 */
147
	public static function loadApp($app, $checkUpgrade = true) {
148
		self::$loadedApps[] = $app;
149
		$appPath = self::getAppPath($app);
150
		if ($appPath === false) {
151
			return;
152
		}
153
154
		// in case someone calls loadApp() directly
155
		self::registerAutoloading($app, $appPath);
156
157
		self::enableThemeIfApplicable($app);
158
159
		if (\is_file($appPath . '/appinfo/app.php')) {
160
			\OC::$server->getEventLogger()->start('load_app_' . $app, 'Load app: ' . $app);
161
			if ($checkUpgrade and self::shouldUpgrade($app)) {
162
				throw new \OC\NeedsUpdateException();
163
			}
164
			self::requireAppFile($app);
165
			if (self::isType($app, ['authentication'])) {
166
				// since authentication apps affect the "is app enabled for group" check,
167
				// the enabled apps cache needs to be cleared to make sure that the
168
				// next time getEnableApps() is called it will also include apps that were
169
				// enabled for groups
170
				self::$enabledAppsCache = [];
171
			}
172
			\OC::$server->getEventLogger()->end('load_app_' . $app);
173
		}
174
	}
175
176
	/**
177
	 * Enables the app as a theme if it has the type "theme"
178
	 * @param string $app
179
	 */
180
	private static function enableThemeIfApplicable($app) {
181
		if (self::isType($app, 'theme')) {
182
			/** @var \OCP\Theme\IThemeService $themeService */
183
			$themeService = \OC::$server->query('ThemeService');
184
			$themeService->setAppTheme($app);
185
		}
186
	}
187
188
	/**
189
	 * @internal
190
	 * @param string $app
191
	 * @param string $path
192
	 */
193
	public static function registerAutoloading($app, $path) {
194
		// Register on PSR-4 composer autoloader
195
		$appNamespace = \OC\AppFramework\App::buildAppNamespace($app);
196
		\OC::$composerAutoloader->addPsr4($appNamespace . '\\', $path . '/lib/', true);
197
		if (\defined('PHPUNIT_RUN')) {
198
			\OC::$composerAutoloader->addPsr4($appNamespace . '\\Tests\\', $path . '/tests/', true);
199
		}
200
201
		// Register on legacy autoloader
202
		\OC::$loader->addValidRoot($path);
203
	}
204
205
	/**
206
	 * Load app.php from the given app
207
	 *
208
	 * @param string $app app name
209
	 */
210
	private static function requireAppFile($app) {
211
		try {
212
			// encapsulated here to avoid variable scope conflicts
213
			require_once $app . '/appinfo/app.php';
214
		} catch (Exception $ex) {
215
			\OC::$server->getLogger()->logException($ex);
216
			$blacklist = \OC::$server->getAppManager()->getAlwaysEnabledApps();
217
			if (!\in_array($app, $blacklist)) {
218
				if (!self::isType($app, ['authentication', 'filesystem'])) {
219
					\OC::$server->getLogger()->warning('Could not load app "' . $app . '", it will be disabled', ['app' => 'core']);
220
					self::disable($app);
221
				} else {
222
					\OC::$server->getLogger()->warning('Could not load app "' . $app . '", see exception above', ['app' => 'core']);
223
				}
224
			}
225
			throw $ex;
226
		}
227
	}
228
229
	/**
230
	 * check if an app is of a specific type
231
	 *
232
	 * @param string $app
233
	 * @param string|array $types
234
	 * @return bool
235
	 */
236
	public static function isType($app, $types) {
237
		if (\is_string($types)) {
238
			$types = [$types];
239
		}
240
		$appTypes = self::getAppTypes($app);
241
		foreach ($types as $type) {
242
			if (\array_search($type, $appTypes) !== false) {
243
				return true;
244
			}
245
		}
246
		return false;
247
	}
248
249
	/**
250
	 * get the types of an app
251
	 *
252
	 * @param string $app
253
	 * @return array
254
	 */
255
	private static function getAppTypes($app) {
256
		//load the cache
257
		if (\count(self::$appTypes) == 0) {
258
			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...
259
		}
260
261
		if (isset(self::$appTypes[$app])) {
262
			return \explode(',', self::$appTypes[$app]);
263
		} else {
264
			return [];
265
		}
266
	}
267
268
	/**
269
	 * read app types from info.xml and cache them in the database
270
	 */
271
	public static function setAppTypes($app) {
272
		$appData = self::getAppInfo($app);
273
		if (!\is_array($appData)) {
274
			return;
275
		}
276
277
		if (isset($appData['types'])) {
278
			$appTypes = \implode(',', $appData['types']);
279
		} else {
280
			$appTypes = '';
281
		}
282
283
		\OC::$server->getAppConfig()->setValue($app, 'types', $appTypes);
284
	}
285
286
	/**
287
	 * check if app is shipped
288
	 *
289
	 * @param string $appId the id of the app to check
290
	 * @return bool
291
	 *
292
	 * Check if an app that is installed is a shipped app or installed from the appstore.
293
	 */
294
	public static function isShipped($appId) {
295
		return \OC::$server->getAppManager()->isShipped($appId);
296
	}
297
298
	/**
299
	 * get all enabled apps
300
	 */
301
	protected static $enabledAppsCache = [];
302
303
	/**
304
	 * Returns apps enabled for the current user.
305
	 *
306
	 * @param bool $forceRefresh whether to refresh the cache
307
	 * @param bool $all whether to return apps for all users, not only the
308
	 * currently logged in one
309
	 * @return string[]
310
	 */
311
	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...
312
		if (!\OC::$server->getSystemConfig()->getValue('installed', false)) {
313
			return [];
314
		}
315
		// in incognito mode or when logged out, $user will be false,
316
		// which is also the case during an upgrade
317
		$appManager = \OC::$server->getAppManager();
318
		if ($all) {
319
			$user = null;
320
		} else {
321
			$user = \OC::$server->getUserSession()->getUser();
322
		}
323
324
		if ($user === null) {
325
			$apps = $appManager->getInstalledApps();
326
		} else {
327
			$apps = $appManager->getEnabledAppsForUser($user);
328
		}
329
		$apps = \array_filter($apps, function ($app) {
330
			return $app !== 'files';//we add this manually
331
		});
332
		\sort($apps);
333
		\array_unshift($apps, 'files');
334
		return $apps;
335
	}
336
337
	/**
338
	 * checks whether or not an app is enabled
339
	 *
340
	 * @param string $app app
341
	 * @return bool
342
	 *
343
	 * This function checks whether or not an app is enabled.
344
	 */
345
	public static function isEnabled($app) {
346
		return \OC::$server->getAppManager()->isEnabledForUser($app);
347
	}
348
349
	/**
350
	 * enables an app
351
	 *
352
	 * @param mixed $app app
353
	 * @param array $groups (optional) when set, only these groups will have access to the app
354
	 * @throws \Exception
355
	 * @return void
356
	 *
357
	 * This function set an app as enabled in appconfig.
358
	 */
359
	public static function enable($app, $groups = null) {
360
		self::$enabledAppsCache = []; // flush
361
362
		// check for required dependencies
363
		$config = \OC::$server->getConfig();
364
		$l = \OC::$server->getL10N('core');
365
		$info = self::getAppInfo($app);
366
		if ($info === null) {
367
			throw new \Exception("$app can't be enabled since it is not installed.");
368
		}
369
370
		self::checkAppDependencies($config, $l, $info);
371
372
		$appManager = \OC::$server->getAppManager();
373
		if ($groups !== null) {
374
			$groupManager = \OC::$server->getGroupManager();
375
			$groupsList = [];
376
			foreach ($groups as $group) {
377
				$groupItem = $groupManager->get($group);
378
				if ($groupItem instanceof \OCP\IGroup) {
379
					$groupsList[] = $groupManager->get($group);
380
				}
381
			}
382
			$appManager->enableAppForGroups($app, $groupsList);
383
		} else {
384
			$appManager->enableApp($app);
385
		}
386
	}
387
388
	/**
389
	 * @param string $app
390
	 * @return bool
391
	 */
392
	public static function removeApp($app) {
393
		if (self::isShipped($app)) {
394
			return false;
395
		}
396
397
		return Installer::removeApp($app);
398
	}
399
400
	/**
401
	 * This function set an app as disabled in appconfig.
402
	 *
403
	 * @param string $app app
404
	 * @throws Exception
405
	 */
406
	public static function disable($app) {
407
		// Convert OCS ID to regular application identifier
408
		if (self::getInternalAppIdByOcs($app) !== false) {
409
			$app = self::getInternalAppIdByOcs($app);
410
		}
411
412
		// flush
413
		self::$enabledAppsCache = [];
414
415
		// run uninstall steps
416
		$appData = OC_App::getAppInfo($app);
417
		if ($appData !== null) {
418
			OC_App::executeRepairSteps($app, $appData['repair-steps']['uninstall']);
419
		}
420
421
		// emit disable hook - needed anymore ?
422
		\OC_Hook::emit('OC_App', 'pre_disable', ['app' => $app]);
423
424
		// finally disable it
425
		$appManager = \OC::$server->getAppManager();
426
		$appManager->disableApp($app);
427
	}
428
429
	/**
430
	 * Returns the Settings Navigation
431
	 *
432
	 * @return string[]
433
	 *
434
	 * This function returns an array containing all settings pages added. The
435
	 * entries are sorted by the key 'order' ascending.
436
	 */
437
	public static function getSettingsNavigation() {
438
		$l = \OC::$server->getL10N('lib');
439
		$urlGenerator = \OC::$server->getURLGenerator();
440
441
		$settings = [];
442
		// by default, settings only contain the help menu
443
		if (OC_Util::getEditionString() === OC_Util::EDITION_COMMUNITY &&
444
			\OC::$server->getSystemConfig()->getValue('knowledgebaseenabled', true) == true
445
		) {
446
			$settings = [
447
				[
448
					"id" => "help",
449
					"order" => 1000,
450
					"href" => $urlGenerator->linkToRoute('settings_help'),
451
					"name" => $l->t("Help"),
452
					"icon" => $urlGenerator->imagePath("settings", "help.svg")
453
				]
454
			];
455
		}
456
457
		// if the user is logged-in
458
		if (OC_User::isLoggedIn()) {
459
			// personal menu
460
			$settings[] = [
461
				"id" => "settings",
462
				"order" => 1,
463
				"href" => $urlGenerator->linkToRoute('settings.SettingsPage.getPersonal'),
464
				"name" => $l->t("Settings"),
465
				"icon" => $urlGenerator->imagePath("settings", "admin.svg")
466
			];
467
		}
468
469
		return self::proceedNavigation($settings);
470
	}
471
472
	// This is private as well. It simply works, so don't ask for more details
473
	private static function proceedNavigation($list) {
474
		$activeApp = OC::$server->getNavigationManager()->getActiveEntry();
475
		foreach ($list as &$navEntry) {
476
			if ($navEntry['id'] == $activeApp) {
477
				$navEntry['active'] = true;
478
			} else {
479
				$navEntry['active'] = false;
480
			}
481
		}
482
		unset($navEntry);
483
484
		\usort($list, function ($a, $b) {
485
			if ($a["order"] == $b["order"]) {
486
				return 0;
487
			}
488
489
			if ($a["order"] < $b["order"]) {
490
				return -1;
491
			}
492
493
			return 1;
494
		});
495
496
		return $list;
497
	}
498
499
	/**
500
	 * Get the path where to install apps
501
	 *
502
	 * @return string|null
503
	 */
504
	public static function getInstallPath() {
505
		foreach (OC::$APPSROOTS as $dir) {
506
			if (isset($dir['writable']) && $dir['writable'] === true) {
507
				return $dir['path'];
508
			}
509
		}
510
511
		\OCP\Util::writeLog('core', 'No application directories are marked as writable.', \OCP\Util::ERROR);
512
		return null;
513
	}
514
515
	/**
516
	 * Get the directory for the given app.
517
	 * If the app exists in multiple directories, the most recent version is taken.
518
	 * (false if not found)
519
	 *
520
	 * @param string $appId
521
	 * @return string|false
522
	 */
523
	public static function getAppPath($appId) {
524
		return \OC::$server->getAppManager()->getAppPath($appId);
525
	}
526
527
	/**
528
	 * Get the web path for the given app.
529
	 * If the app exists in multiple directories, the most recent version is taken.
530
	 * (false if not found)
531
	 *
532
	 * @param string $appId
533
	 * @return string|false
534
	 */
535
	public static function getAppWebPath($appId) {
536
		return \OC::$server->getAppManager()->getAppWebPath($appId);
537
	}
538
539
	/**
540
	 * check if an app's directory is writable
541
	 *
542
	 * @param string $appId
543
	 * @return bool
544
	 */
545
	public static function isAppDirWritable($appId) {
546
		$path = self::getAppPath($appId);
547
		return ($path !== false) ? \is_writable($path) : false;
548
	}
549
550
	/**
551
	 * get the last version of the app from appinfo/info.xml
552
	 *
553
	 * @param string $appId
554
	 * @return string
555
	 */
556
	public static function getAppVersion($appId) {
557
		if (!isset(self::$appVersion[$appId])) {
558
			$file = self::getAppPath($appId);
559
			self::$appVersion[$appId] = ($file !== false) ? self::getAppVersionByPath($file) : '0';
560
		}
561
		return self::$appVersion[$appId];
562
	}
563
564
	/**
565
	 * get app's version based on it's path
566
	 *
567
	 * @param string $path
568
	 * @return string
569
	 */
570
	public static function getAppVersionByPath($path) {
571
		$infoFile = $path . '/appinfo/info.xml';
572
		$appData = self::getAppInfo($infoFile, true);
573
		return isset($appData['version']) ? $appData['version'] : '';
574
	}
575
576
	/**
577
	 * Read all app metadata from the info.xml file
578
	 *
579
	 * @param string $appId id of the app or the path of the info.xml file
580
	 * @param boolean $path (optional)
581
	 * @return array|null
582
	 * @note all data is read from info.xml, not just pre-defined fields
583
	 */
584
	public static function getAppInfo($appId, $path = false) {
585
		if ($path) {
586
			$file = $appId;
587
		} else {
588
			if (isset(self::$appInfo[$appId])) {
589
				return self::$appInfo[$appId];
590
			}
591
			$appPath = self::getAppPath($appId);
592
			if ($appPath === false) {
593
				return null;
594
			}
595
			$file = $appPath . '/appinfo/info.xml';
596
		}
597
598
		$parser = new InfoParser();
599
		try {
600
			$data = $parser->parse($file);
601
		} catch (\Exception $e) {
602
			\OC::$server->getLogger()->logException($e);
603
			throw $e;
604
		}
605
606
		if (\is_array($data)) {
607
			$data = OC_App::parseAppInfo($data);
608
		}
609
		if (isset($data['ocsid'])) {
610
			$storedId = \OC::$server->getConfig()->getAppValue($appId, 'ocsid');
611
			if ($storedId !== '' && $storedId !== $data['ocsid']) {
612
				$data['ocsid'] = $storedId;
613
			}
614
		}
615
616
		self::$appInfo[$appId] = $data;
617
618
		return $data;
619
	}
620
621
	/**
622
	 * Returns the navigation
623
	 *
624
	 * @return array
625
	 *
626
	 * This function returns an array containing all entries added. The
627
	 * entries are sorted by the key 'order' ascending. Additional to the keys
628
	 * given for each app the following keys exist:
629
	 *   - active: boolean, signals if the user is on this navigation entry
630
	 */
631
	public static function getNavigation() {
632
		$entries = OC::$server->getNavigationManager()->getAll();
633
		return self::proceedNavigation($entries);
634
	}
635
636
	/**
637
	 * get the id of loaded app
638
	 *
639
	 * @return string
640
	 */
641
	public static function getCurrentApp() {
642
		$request = \OC::$server->getRequest();
643
		$script = \substr($request->getScriptName(), \strlen(OC::$WEBROOT) + 1);
644
		$topFolder = \substr($script, 0, \strpos($script, '/'));
645
		if (empty($topFolder)) {
646
			$path_info = $request->getPathInfo();
647
			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...
648
				$topFolder = \substr($path_info, 1, \strpos($path_info, '/', 1) - 1);
649
			}
650
		}
651
		if ($topFolder == 'apps') {
652
			$length = \strlen($topFolder);
653
			return \substr($script, $length + 1, \strpos($script, '/', $length + 1) - $length - 1);
654
		} else {
655
			return $topFolder;
656
		}
657
	}
658
659
	/**
660
	 * @param string $type
661
	 * @return array
662
	 */
663
	public static function getForms($type) {
664
		$forms = [];
665
		switch ($type) {
666
			case 'admin':
667
				$source = self::$adminForms;
668
				break;
669
			case 'personal':
670
				$source = self::$personalForms;
671
				break;
672
			default:
673
				return [];
674
		}
675
		foreach ($source as $form) {
676
			$page = include $form['page'];
677
			$forms[] = [
678
				'appId' => $form['appId'],
679
				'page' => $page,
680
			];
681
		}
682
		return $forms;
683
	}
684
685
	/**
686
	 * register an admin form to be shown
687
	 *
688
	 * @param string $app
689
	 * @param string $page
690
	 * @deprecated Register settings panels in info.xml
691
	 */
692
	public static function registerAdmin($app, $page) {
693
		self::$adminForms[] = [
694
			'appId' => $app,
695
			'page' => $app . '/' . $page . '.php',
696
		];
697
	}
698
699
	/**
700
	 * register a personal form to be shown
701
	 * @param string $app
702
	 * @param string $page
703
	 * @deprecated Register settings panels in info.xml
704
	 */
705
	public static function registerPersonal($app, $page) {
706
		self::$personalForms[] = [
707
			'appId' => $app,
708
			'page' => $app . '/' . $page . '.php',
709
		];
710
	}
711
712
	/**
713
	 * @param array $entry
714
	 */
715
	public static function registerLogIn(array $entry) {
716
		self::$altLogin[] = $entry;
717
	}
718
719
	/**
720
	 * @return array
721
	 */
722
	public static function getAlternativeLogIns() {
723
		return self::$altLogin;
724
	}
725
726
	/**
727
	 * get a list of all apps in the apps folder
728
	 *
729
	 * @return array an array of app names (string IDs)
730
	 * @todo: change the name of this method to getInstalledApps, which is more accurate
731
	 */
732
	public static function getAllApps() {
733
		$apps = [];
734
735
		foreach (OC::$APPSROOTS as $apps_dir) {
736
			if (!\is_readable($apps_dir['path'])) {
737
				\OCP\Util::writeLog('core', 'unable to read app folder : ' . $apps_dir['path'], \OCP\Util::WARN);
738
				continue;
739
			}
740
			$dh = \opendir($apps_dir['path']);
741
742
			if (\is_resource($dh)) {
743
				while (($file = \readdir($dh)) !== false) {
744
					if ($file[0] != '.' and \is_dir($apps_dir['path'] . '/' . $file) and \is_file($apps_dir['path'] . '/' . $file . '/appinfo/info.xml')) {
745
						$apps[] = $file;
746
					}
747
				}
748
			}
749
		}
750
751
		return $apps;
752
	}
753
754
	/**
755
	 * List all apps, this is used in apps.php
756
	 *
757
	 * @param bool $onlyLocal
0 ignored issues
show
Bug introduced by
There is no parameter named $onlyLocal. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
758
	 * @param bool $includeUpdateInfo Should we check whether there is an update
0 ignored issues
show
Bug introduced by
There is no parameter named $includeUpdateInfo. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
759
	 *                                in the app store?
760
	 * @return array
761
	 */
762
	public static function listAllApps() {
763
		$installedApps = OC_App::getAllApps();
764
765
		//TODO which apps do we want to blacklist and how do we integrate
766
		// blacklisting with the multi apps folder feature?
767
768
		//we don't want to show configuration for these
769
		$blacklist = \OC::$server->getAppManager()->getAlwaysEnabledApps();
770
		$appList = [];
771
		$urlGenerator = \OC::$server->getURLGenerator();
772
773
		foreach ($installedApps as $app) {
774
			if (\array_search($app, $blacklist) === false) {
775
				$info = OC_App::getAppInfo($app);
776
				if (!\is_array($info)) {
777
					\OCP\Util::writeLog('core', 'Could not read app info file for app "' . $app . '"', \OCP\Util::ERROR);
778
					continue;
779
				}
780
781
				if (!isset($info['name'])) {
782
					\OCP\Util::writeLog('core', 'App id "' . $app . '" has no name in appinfo', \OCP\Util::ERROR);
783
					continue;
784
				}
785
786
				$enabled = \OC::$server->getAppConfig()->getValue($app, 'enabled', 'no');
787
				$info['groups'] = null;
788
				if ($enabled === 'yes') {
789
					$active = true;
790
				} elseif ($enabled === 'no') {
791
					$active = false;
792
				} else {
793
					$active = true;
794
					$info['groups'] = $enabled;
795
				}
796
797
				$info['active'] = $active;
798
799
				if (self::isShipped($app)) {
800
					$info['internal'] = true;
801
					$info['level'] = self::officialApp;
802
					$info['removable'] = false;
803
				} else {
804
					$result = \OC::$server->getIntegrityCodeChecker()->verifyAppSignature($app, '', true);
805
					if (empty($result)) {
806
						$info['internal'] = false;
807
						$info['level'] = self::approvedApp;
808
						$info['removable'] = false;
809
					}
810
811
					$info['internal'] = false;
812
					$info['removable'] = true;
813
				}
814
815
				$appPath = self::getAppPath($app);
816
				if ($appPath !== false) {
817
					$appIcon = $appPath . '/img/' . $app . '.svg';
818
					if (\file_exists($appIcon)) {
819
						$info['preview'] = \OC::$server->getURLGenerator()->imagePath($app, $app . '.svg');
820
						$info['previewAsIcon'] = true;
821
					} else {
822
						$appIcon = $appPath . '/img/app.svg';
823
						if (\file_exists($appIcon)) {
824
							$info['preview'] = \OC::$server->getURLGenerator()->imagePath($app, 'app.svg');
825
							$info['previewAsIcon'] = true;
826
						}
827
					}
828
				}
829
				// fix documentation
830
				if (isset($info['documentation']) && \is_array($info['documentation'])) {
831
					foreach ($info['documentation'] as $key => $url) {
832
						// If it is not an absolute URL we assume it is a key
833
						// i.e. admin-ldap will get converted to go.php?to=admin-ldap
834
						if (\stripos($url, 'https://') !== 0 && \stripos($url, 'http://') !== 0) {
835
							$url = $urlGenerator->linkToDocs($url);
836
						}
837
838
						$info['documentation'][$key] = $url;
839
					}
840
				}
841
842
				$info['version'] = OC_App::getAppVersion($app);
843
				$appList[] = $info;
844
			}
845
		}
846
847
		return $appList;
848
	}
849
850
	/**
851
	 * Returns the internal app ID or false
852
	 * @param string $ocsID
853
	 * @return string|false
854
	 */
855
	public static function getInternalAppIdByOcs($ocsID) {
856
		if (\is_numeric($ocsID)) {
857
			$idArray = \OC::$server->getAppConfig()->getValues(false, 'ocsid');
858
			if (\array_search($ocsID, $idArray)) {
859
				return \array_search($ocsID, $idArray);
860
			}
861
		}
862
		return false;
863
	}
864
865
	public static function shouldUpgrade($app) {
866
		$versions = self::getAppVersions();
867
		$currentVersion = OC_App::getAppVersion($app);
868
		if ($currentVersion && isset($versions[$app])) {
869
			$installedVersion = $versions[$app];
870
			if (!\version_compare($currentVersion, $installedVersion, '=')) {
871
				return true;
872
			}
873
		}
874
		return false;
875
	}
876
877
	/**
878
	 * Adjust the number of version parts of $version1 to match
879
	 * the number of version parts of $version2.
880
	 *
881
	 * @param string $version1 version to adjust
882
	 * @param string $version2 version to take the number of parts from
883
	 * @return string shortened $version1
884
	 */
885
	private static function adjustVersionParts($version1, $version2) {
886
		$version1 = \explode('.', $version1);
887
		$version2 = \explode('.', $version2);
888
		// reduce $version1 to match the number of parts in $version2
889
		while (\count($version1) > \count($version2)) {
890
			\array_pop($version1);
891
		}
892
		// if $version1 does not have enough parts, add some
893
		while (\count($version1) < \count($version2)) {
894
			$version1[] = '0';
895
		}
896
		return \implode('.', $version1);
897
	}
898
899
	/**
900
	 * Check whether the current ownCloud version matches the given
901
	 * application's version requirements.
902
	 *
903
	 * The comparison is made based on the number of parts that the
904
	 * app info version has. For example for ownCloud 6.0.3 if the
905
	 * app info version is expecting version 6.0, the comparison is
906
	 * made on the first two parts of the ownCloud version.
907
	 * This means that it's possible to specify "requiremin" => 6
908
	 * and "requiremax" => 6 and it will still match ownCloud 6.0.3.
909
	 *
910
	 * @param string|array $ocVersion ownCloud version to check against
911
	 * @param array $appInfo app info (from xml)
912
	 *
913
	 * @return boolean true if compatible, otherwise false
914
	 */
915
	public static function isAppCompatible($ocVersion, $appInfo) {
916
		$requireMin = '';
917
		$requireMax = '';
918 View Code Duplication
		if (isset($appInfo['dependencies']['owncloud']['@attributes']['min-version'])) {
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...
919
			$requireMin = $appInfo['dependencies']['owncloud']['@attributes']['min-version'];
920
		} elseif (isset($appInfo['requiremin'])) {
921
			$requireMin = $appInfo['requiremin'];
922
		} elseif (isset($appInfo['require'])) {
923
			$requireMin = $appInfo['require'];
924
		}
925
926 View Code Duplication
		if (isset($appInfo['dependencies']['owncloud']['@attributes']['max-version'])) {
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...
927
			$requireMax = $appInfo['dependencies']['owncloud']['@attributes']['max-version'];
928
		} elseif (isset($appInfo['requiremax'])) {
929
			$requireMax = $appInfo['requiremax'];
930
		}
931
932
		if (\is_array($ocVersion)) {
933
			$ocVersion = \implode('.', $ocVersion);
934
		}
935
936 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...
937
			&& \version_compare(self::adjustVersionParts($ocVersion, $requireMin), $requireMin, '<')
938
		) {
939
			return false;
940
		}
941
942 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...
943
			&& \version_compare(self::adjustVersionParts($ocVersion, $requireMax), $requireMax, '>')
944
		) {
945
			return false;
946
		}
947
948
		return true;
949
	}
950
951
	/**
952
	 * get the installed version of all apps
953
	 */
954
	public static function getAppVersions() {
955
		static $versions;
956
957
		if (!$versions) {
958
			$appConfig = \OC::$server->getAppConfig();
959
			$versions = $appConfig->getValues(false, 'installed_version');
960
		}
961
		return $versions;
962
	}
963
964
	/**
965
	 * update the database for the app and call the update script
966
	 *
967
	 * @param string $appId
968
	 * @return bool
969
	 */
970
	public static function updateApp($appId) {
971
		$appPath = self::getAppPath($appId);
972
		if ($appPath === false) {
973
			return false;
974
		}
975
		$appData = self::getAppInfo($appId);
976
		self::executeRepairSteps($appId, $appData['repair-steps']['pre-migration']);
977
		if (isset($appData['use-migrations']) && $appData['use-migrations'] === 'true') {
978
			$ms = new \OC\DB\MigrationService($appId, \OC::$server->getDatabaseConnection());
979
			$ms->migrate();
980
		} else {
981
			if (\file_exists($appPath . '/appinfo/database.xml')) {
982
				OC_DB::updateDbFromStructure($appPath . '/appinfo/database.xml');
983
			}
984
		}
985
		self::executeRepairSteps($appId, $appData['repair-steps']['post-migration']);
986
		self::setupLiveMigrations($appId, $appData['repair-steps']['live-migration']);
987
		self::clearAppCache($appId);
988
		// run upgrade code
989
		if (\file_exists($appPath . '/appinfo/update.php')) {
990
			self::loadApp($appId, false);
991
			include $appPath . '/appinfo/update.php';
992
		}
993
		self::setupBackgroundJobs($appData['background-jobs']);
994
995
		//set remote/public handlers
996
		if (\array_key_exists('ocsid', $appData)) {
997
			\OC::$server->getConfig()->setAppValue($appId, 'ocsid', $appData['ocsid']);
998 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...
999
			\OC::$server->getConfig()->deleteAppValue($appId, 'ocsid');
1000
		}
1001 View Code Duplication
		foreach ($appData['remote'] as $name => $path) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1002
			\OC::$server->getConfig()->setAppValue('core', 'remote_' . $name, $appId . '/' . $path);
1003
		}
1004
		foreach ($appData['public'] as $name => $path) {
1005
			\OC::$server->getConfig()->setAppValue('core', 'public_' . $name, $appId . '/' . $path);
1006
		}
1007
1008
		self::setAppTypes($appId);
1009
1010
		$version = \OC_App::getAppVersion($appId);
1011
		\OC::$server->getAppConfig()->setValue($appId, 'installed_version', $version);
1012
1013
		return true;
1014
	}
1015
1016
	/**
1017
	 * @param string $appId
1018
	 * @param string[] $steps
1019
	 * @throws \OC\NeedsUpdateException
1020
	 */
1021
	public static function executeRepairSteps($appId, array $steps) {
1022
		if (empty($steps)) {
1023
			return;
1024
		}
1025
		// load the app
1026
		self::loadApp($appId, false);
1027
1028
		$dispatcher = OC::$server->getEventDispatcher();
1029
1030
		// load the steps
1031
		$r = new Repair([], $dispatcher);
0 ignored issues
show
Documentation introduced by
$dispatcher is of type object<Symfony\Component...entDispatcherInterface>, but the function expects a null|object<Symfony\Comp...atcher\EventDispatcher>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1032
		foreach ($steps as $step) {
1033
			try {
1034
				$r->addStep($step);
1035
			} catch (Exception $ex) {
1036
				$r->emit('\OC\Repair', 'error', [$ex->getMessage()]);
1037
				\OC::$server->getLogger()->logException($ex);
1038
			}
1039
		}
1040
		// run the steps
1041
		$r->run();
1042
	}
1043
1044
	public static function setupBackgroundJobs(array $jobs) {
1045
		$queue = \OC::$server->getJobList();
1046
		foreach ($jobs as $job) {
1047
			$queue->add($job);
1048
		}
1049
	}
1050
1051
	/**
1052
	 * @param string $appId
1053
	 * @param string[] $steps
1054
	 */
1055
	private static function setupLiveMigrations($appId, array $steps) {
1056
		$queue = \OC::$server->getJobList();
1057
		foreach ($steps as $step) {
1058
			$queue->add('OC\Migration\BackgroundRepair', [
1059
				'app' => $appId,
1060
				'step' => $step]);
1061
		}
1062
	}
1063
1064
	/**
1065
	 * @param string $appId
1066
	 * @return \OC\Files\View|false
1067
	 */
1068
	public static function getStorage($appId) {
1069
		if (OC_App::isEnabled($appId)) { //sanity check
1070
			if (OC_User::isLoggedIn()) {
1071
				$view = new \OC\Files\View('/' . OC_User::getUser());
1072
				if (!$view->file_exists($appId)) {
1073
					$view->mkdir($appId);
1074
				}
1075
				return new \OC\Files\View('/' . OC_User::getUser() . '/' . $appId);
1076
			} else {
1077
				\OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ', user not logged in', \OCP\Util::ERROR);
1078
				return false;
1079
			}
1080
		} else {
1081
			\OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ' not enabled', \OCP\Util::ERROR);
1082
			return false;
1083
		}
1084
	}
1085
1086
	/**
1087
	 * parses the app data array and enhanced the 'description' value
1088
	 *
1089
	 * @param array $data the app data
1090
	 * @return array improved app data
1091
	 */
1092
	public static function parseAppInfo(array $data) {
1093
1094
		// just modify the description if it is available
1095
		// otherwise this will create a $data element with an empty 'description'
1096
		if (isset($data['description'])) {
1097
			if (\is_string($data['description'])) {
1098
				// sometimes the description contains line breaks and they are then also
1099
				// shown in this way in the app management which isn't wanted as HTML
1100
				// manages line breaks itself
1101
1102
				// first of all we split on empty lines
1103
				$paragraphs = \preg_split("!\n[[:space:]]*\n!mu", $data['description']);
1104
1105
				$result = [];
1106
				foreach ($paragraphs as $value) {
1107
					// replace multiple whitespace (tabs, space, newlines) inside a paragraph
1108
					// with a single space - also trims whitespace
1109
					$result[] = \trim(\preg_replace('![[:space:]]+!mu', ' ', $value));
1110
				}
1111
1112
				// join the single paragraphs with a empty line in between
1113
				$data['description'] = \implode("\n\n", $result);
1114
			} else {
1115
				$data['description'] = '';
1116
			}
1117
		}
1118
1119
		return $data;
1120
	}
1121
1122
	/**
1123
	 * @param OCP\IConfig $config
1124
	 * @param OCP\IL10N $l
1125
	 * @param array $info
1126
	 * @throws Exception
1127
	 */
1128
	protected static function checkAppDependencies($config, $l, $info) {
1129
		$dependencyAnalyzer = new DependencyAnalyzer(new Platform($config), $l);
1130
		$missing = $dependencyAnalyzer->analyze($info);
1131
		if (!empty($missing)) {
1132
			$missingMsg = \join(PHP_EOL, $missing);
1133
			throw new \Exception(
1134
				$l->t('App "%s" cannot be installed because the following dependencies are not fulfilled: %s',
1135
					[$info['name'], $missingMsg]
1136
				)
1137
			);
1138
		}
1139
	}
1140
1141
	/**
1142
	 * @param $appId
1143
	 */
1144
	public static function clearAppCache($appId) {
1145
		unset(self::$appVersion[$appId], self::$appInfo[$appId]);
1146
	}
1147
}
1148