1<?php 2/** 3 * @copyright Copyright (c) 2016, ownCloud, Inc. 4 * 5 * @author Arthur Schiwon <blizzz@arthur-schiwon.de> 6 * @author Bjoern Schiessle <bjoern@schiessle.org> 7 * @author Christoph Schaefer "christophł@wolkesicher.de" 8 * @author Christoph Wurst <christoph@winzerhof-wurst.at> 9 * @author Daniel Kesselberg <mail@danielkesselberg.de> 10 * @author Daniel Rudolf <github.com@daniel-rudolf.de> 11 * @author Greta Doci <gretadoci@gmail.com> 12 * @author Joas Schilling <coding@schilljs.com> 13 * @author Julius Haertl <jus@bitgrid.net> 14 * @author Julius Härtl <jus@bitgrid.net> 15 * @author Lukas Reschke <lukas@statuscode.ch> 16 * @author Morris Jobke <hey@morrisjobke.de> 17 * @author Robin Appelman <robin@icewind.nl> 18 * @author Roeland Jago Douma <roeland@famdouma.nl> 19 * @author Thomas Müller <thomas.mueller@tmit.eu> 20 * @author Tobia De Koninck <tobia@ledfan.be> 21 * @author Vincent Petry <vincent@nextcloud.com> 22 * 23 * @license AGPL-3.0 24 * 25 * This code is free software: you can redistribute it and/or modify 26 * it under the terms of the GNU Affero General Public License, version 3, 27 * as published by the Free Software Foundation. 28 * 29 * This program is distributed in the hope that it will be useful, 30 * but WITHOUT ANY WARRANTY; without even the implied warranty of 31 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 32 * GNU Affero General Public License for more details. 33 * 34 * You should have received a copy of the GNU Affero General Public License, version 3, 35 * along with this program. If not, see <http://www.gnu.org/licenses/> 36 * 37 */ 38namespace OC\App; 39 40use OC\AppConfig; 41use OCP\App\AppPathNotFoundException; 42use OCP\App\IAppManager; 43use OCP\App\ManagerEvent; 44use OCP\ICacheFactory; 45use OCP\IConfig; 46use OCP\IGroup; 47use OCP\IGroupManager; 48use OCP\IUser; 49use OCP\IUserSession; 50use Psr\Log\LoggerInterface; 51use Symfony\Component\EventDispatcher\EventDispatcherInterface; 52 53class AppManager implements IAppManager { 54 55 /** 56 * Apps with these types can not be enabled for certain groups only 57 * @var string[] 58 */ 59 protected $protectedAppTypes = [ 60 'filesystem', 61 'prelogin', 62 'authentication', 63 'logging', 64 'prevent_group_restriction', 65 ]; 66 67 /** @var IUserSession */ 68 private $userSession; 69 70 /** @var IConfig */ 71 private $config; 72 73 /** @var AppConfig */ 74 private $appConfig; 75 76 /** @var IGroupManager */ 77 private $groupManager; 78 79 /** @var ICacheFactory */ 80 private $memCacheFactory; 81 82 /** @var EventDispatcherInterface */ 83 private $dispatcher; 84 85 /** @var LoggerInterface */ 86 private $logger; 87 88 /** @var string[] $appId => $enabled */ 89 private $installedAppsCache; 90 91 /** @var string[] */ 92 private $shippedApps; 93 94 /** @var string[] */ 95 private $alwaysEnabled; 96 97 /** @var array */ 98 private $appInfos = []; 99 100 /** @var array */ 101 private $appVersions = []; 102 103 /** @var array */ 104 private $autoDisabledApps = []; 105 106 public function __construct(IUserSession $userSession, 107 IConfig $config, 108 AppConfig $appConfig, 109 IGroupManager $groupManager, 110 ICacheFactory $memCacheFactory, 111 EventDispatcherInterface $dispatcher, 112 LoggerInterface $logger) { 113 $this->userSession = $userSession; 114 $this->config = $config; 115 $this->appConfig = $appConfig; 116 $this->groupManager = $groupManager; 117 $this->memCacheFactory = $memCacheFactory; 118 $this->dispatcher = $dispatcher; 119 $this->logger = $logger; 120 } 121 122 /** 123 * @return string[] $appId => $enabled 124 */ 125 private function getInstalledAppsValues() { 126 if (!$this->installedAppsCache) { 127 $values = $this->appConfig->getValues(false, 'enabled'); 128 129 $alwaysEnabledApps = $this->getAlwaysEnabledApps(); 130 foreach ($alwaysEnabledApps as $appId) { 131 $values[$appId] = 'yes'; 132 } 133 134 $this->installedAppsCache = array_filter($values, function ($value) { 135 return $value !== 'no'; 136 }); 137 ksort($this->installedAppsCache); 138 } 139 return $this->installedAppsCache; 140 } 141 142 /** 143 * List all installed apps 144 * 145 * @return string[] 146 */ 147 public function getInstalledApps() { 148 return array_keys($this->getInstalledAppsValues()); 149 } 150 151 /** 152 * List all apps enabled for a user 153 * 154 * @param \OCP\IUser $user 155 * @return string[] 156 */ 157 public function getEnabledAppsForUser(IUser $user) { 158 $apps = $this->getInstalledAppsValues(); 159 $appsForUser = array_filter($apps, function ($enabled) use ($user) { 160 return $this->checkAppForUser($enabled, $user); 161 }); 162 return array_keys($appsForUser); 163 } 164 165 /** 166 * @param \OCP\IGroup $group 167 * @return array 168 */ 169 public function getEnabledAppsForGroup(IGroup $group): array { 170 $apps = $this->getInstalledAppsValues(); 171 $appsForGroups = array_filter($apps, function ($enabled) use ($group) { 172 return $this->checkAppForGroups($enabled, $group); 173 }); 174 return array_keys($appsForGroups); 175 } 176 177 /** 178 * @return array 179 */ 180 public function getAutoDisabledApps(): array { 181 return $this->autoDisabledApps; 182 } 183 184 /** 185 * @param string $appId 186 * @return array 187 */ 188 public function getAppRestriction(string $appId): array { 189 $values = $this->getInstalledAppsValues(); 190 191 if (!isset($values[$appId])) { 192 return []; 193 } 194 195 if ($values[$appId] === 'yes' || $values[$appId] === 'no') { 196 return []; 197 } 198 return json_decode($values[$appId], true); 199 } 200 201 202 /** 203 * Check if an app is enabled for user 204 * 205 * @param string $appId 206 * @param \OCP\IUser $user (optional) if not defined, the currently logged in user will be used 207 * @return bool 208 */ 209 public function isEnabledForUser($appId, $user = null) { 210 if ($this->isAlwaysEnabled($appId)) { 211 return true; 212 } 213 if ($user === null) { 214 $user = $this->userSession->getUser(); 215 } 216 $installedApps = $this->getInstalledAppsValues(); 217 if (isset($installedApps[$appId])) { 218 return $this->checkAppForUser($installedApps[$appId], $user); 219 } else { 220 return false; 221 } 222 } 223 224 /** 225 * @param string $enabled 226 * @param IUser $user 227 * @return bool 228 */ 229 private function checkAppForUser($enabled, $user) { 230 if ($enabled === 'yes') { 231 return true; 232 } elseif ($user === null) { 233 return false; 234 } else { 235 if (empty($enabled)) { 236 return false; 237 } 238 239 $groupIds = json_decode($enabled); 240 241 if (!is_array($groupIds)) { 242 $jsonError = json_last_error(); 243 $this->logger->warning('AppManger::checkAppForUser - can\'t decode group IDs: ' . print_r($enabled, true) . ' - json error code: ' . $jsonError); 244 return false; 245 } 246 247 $userGroups = $this->groupManager->getUserGroupIds($user); 248 foreach ($userGroups as $groupId) { 249 if (in_array($groupId, $groupIds, true)) { 250 return true; 251 } 252 } 253 return false; 254 } 255 } 256 257 /** 258 * @param string $enabled 259 * @param IGroup $group 260 * @return bool 261 */ 262 private function checkAppForGroups(string $enabled, IGroup $group): bool { 263 if ($enabled === 'yes') { 264 return true; 265 } elseif ($group === null) { 266 return false; 267 } else { 268 if (empty($enabled)) { 269 return false; 270 } 271 272 $groupIds = json_decode($enabled); 273 274 if (!is_array($groupIds)) { 275 $jsonError = json_last_error(); 276 $this->logger->warning('AppManger::checkAppForUser - can\'t decode group IDs: ' . print_r($enabled, true) . ' - json error code: ' . $jsonError); 277 return false; 278 } 279 280 return in_array($group->getGID(), $groupIds); 281 } 282 } 283 284 /** 285 * Check if an app is enabled in the instance 286 * 287 * Notice: This actually checks if the app is enabled and not only if it is installed. 288 * 289 * @param string $appId 290 * @param \OCP\IGroup[]|String[] $groups 291 * @return bool 292 */ 293 public function isInstalled($appId) { 294 $installedApps = $this->getInstalledAppsValues(); 295 return isset($installedApps[$appId]); 296 } 297 298 public function ignoreNextcloudRequirementForApp(string $appId): void { 299 $ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []); 300 if (!in_array($appId, $ignoreMaxApps, true)) { 301 $ignoreMaxApps[] = $appId; 302 $this->config->setSystemValue('app_install_overwrite', $ignoreMaxApps); 303 } 304 } 305 306 /** 307 * Enable an app for every user 308 * 309 * @param string $appId 310 * @param bool $forceEnable 311 * @throws AppPathNotFoundException 312 */ 313 public function enableApp(string $appId, bool $forceEnable = false): void { 314 // Check if app exists 315 $this->getAppPath($appId); 316 317 if ($forceEnable) { 318 $this->ignoreNextcloudRequirementForApp($appId); 319 } 320 321 $this->installedAppsCache[$appId] = 'yes'; 322 $this->appConfig->setValue($appId, 'enabled', 'yes'); 323 $this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE, new ManagerEvent( 324 ManagerEvent::EVENT_APP_ENABLE, $appId 325 )); 326 $this->clearAppsCache(); 327 } 328 329 /** 330 * Whether a list of types contains a protected app type 331 * 332 * @param string[] $types 333 * @return bool 334 */ 335 public function hasProtectedAppType($types) { 336 if (empty($types)) { 337 return false; 338 } 339 340 $protectedTypes = array_intersect($this->protectedAppTypes, $types); 341 return !empty($protectedTypes); 342 } 343 344 /** 345 * Enable an app only for specific groups 346 * 347 * @param string $appId 348 * @param \OCP\IGroup[] $groups 349 * @param bool $forceEnable 350 * @throws \InvalidArgumentException if app can't be enabled for groups 351 * @throws AppPathNotFoundException 352 */ 353 public function enableAppForGroups(string $appId, array $groups, bool $forceEnable = false): void { 354 // Check if app exists 355 $this->getAppPath($appId); 356 357 $info = $this->getAppInfo($appId); 358 if (!empty($info['types']) && $this->hasProtectedAppType($info['types'])) { 359 throw new \InvalidArgumentException("$appId can't be enabled for groups."); 360 } 361 362 if ($forceEnable) { 363 $this->ignoreNextcloudRequirementForApp($appId); 364 } 365 366 $groupIds = array_map(function ($group) { 367 /** @var \OCP\IGroup $group */ 368 return ($group instanceof IGroup) 369 ? $group->getGID() 370 : $group; 371 }, $groups); 372 373 $this->installedAppsCache[$appId] = json_encode($groupIds); 374 $this->appConfig->setValue($appId, 'enabled', json_encode($groupIds)); 375 $this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, new ManagerEvent( 376 ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, $appId, $groups 377 )); 378 $this->clearAppsCache(); 379 } 380 381 /** 382 * Disable an app for every user 383 * 384 * @param string $appId 385 * @param bool $automaticDisabled 386 * @throws \Exception if app can't be disabled 387 */ 388 public function disableApp($appId, $automaticDisabled = false) { 389 if ($this->isAlwaysEnabled($appId)) { 390 throw new \Exception("$appId can't be disabled."); 391 } 392 393 if ($automaticDisabled) { 394 $previousSetting = $this->appConfig->getValue($appId, 'enabled', 'yes'); 395 if ($previousSetting !== 'yes' && $previousSetting !== 'no') { 396 $previousSetting = json_decode($previousSetting, true); 397 } 398 $this->autoDisabledApps[$appId] = $previousSetting; 399 } 400 401 unset($this->installedAppsCache[$appId]); 402 $this->appConfig->setValue($appId, 'enabled', 'no'); 403 404 // run uninstall steps 405 $appData = $this->getAppInfo($appId); 406 if (!is_null($appData)) { 407 \OC_App::executeRepairSteps($appId, $appData['repair-steps']['uninstall']); 408 } 409 410 $this->dispatcher->dispatch(ManagerEvent::EVENT_APP_DISABLE, new ManagerEvent( 411 ManagerEvent::EVENT_APP_DISABLE, $appId 412 )); 413 $this->clearAppsCache(); 414 } 415 416 /** 417 * Get the directory for the given app. 418 * 419 * @param string $appId 420 * @return string 421 * @throws AppPathNotFoundException if app folder can't be found 422 */ 423 public function getAppPath($appId) { 424 $appPath = \OC_App::getAppPath($appId); 425 if ($appPath === false) { 426 throw new AppPathNotFoundException('Could not find path for ' . $appId); 427 } 428 return $appPath; 429 } 430 431 /** 432 * Get the web path for the given app. 433 * 434 * @param string $appId 435 * @return string 436 * @throws AppPathNotFoundException if app path can't be found 437 */ 438 public function getAppWebPath(string $appId): string { 439 $appWebPath = \OC_App::getAppWebPath($appId); 440 if ($appWebPath === false) { 441 throw new AppPathNotFoundException('Could not find web path for ' . $appId); 442 } 443 return $appWebPath; 444 } 445 446 /** 447 * Clear the cached list of apps when enabling/disabling an app 448 */ 449 public function clearAppsCache() { 450 $settingsMemCache = $this->memCacheFactory->createDistributed('settings'); 451 $settingsMemCache->clear('listApps'); 452 $this->appInfos = []; 453 } 454 455 /** 456 * Returns a list of apps that need upgrade 457 * 458 * @param string $version Nextcloud version as array of version components 459 * @return array list of app info from apps that need an upgrade 460 * 461 * @internal 462 */ 463 public function getAppsNeedingUpgrade($version) { 464 $appsToUpgrade = []; 465 $apps = $this->getInstalledApps(); 466 foreach ($apps as $appId) { 467 $appInfo = $this->getAppInfo($appId); 468 $appDbVersion = $this->appConfig->getValue($appId, 'installed_version'); 469 if ($appDbVersion 470 && isset($appInfo['version']) 471 && version_compare($appInfo['version'], $appDbVersion, '>') 472 && \OC_App::isAppCompatible($version, $appInfo) 473 ) { 474 $appsToUpgrade[] = $appInfo; 475 } 476 } 477 478 return $appsToUpgrade; 479 } 480 481 /** 482 * Returns the app information from "appinfo/info.xml". 483 * 484 * @param string $appId app id 485 * 486 * @param bool $path 487 * @param null $lang 488 * @return array|null app info 489 */ 490 public function getAppInfo(string $appId, bool $path = false, $lang = null) { 491 if ($path) { 492 $file = $appId; 493 } else { 494 if ($lang === null && isset($this->appInfos[$appId])) { 495 return $this->appInfos[$appId]; 496 } 497 try { 498 $appPath = $this->getAppPath($appId); 499 } catch (AppPathNotFoundException $e) { 500 return null; 501 } 502 $file = $appPath . '/appinfo/info.xml'; 503 } 504 505 $parser = new InfoParser($this->memCacheFactory->createLocal('core.appinfo')); 506 $data = $parser->parse($file); 507 508 if (is_array($data)) { 509 $data = \OC_App::parseAppInfo($data, $lang); 510 } 511 512 if ($lang === null) { 513 $this->appInfos[$appId] = $data; 514 } 515 516 return $data; 517 } 518 519 public function getAppVersion(string $appId, bool $useCache = true): string { 520 if (!$useCache || !isset($this->appVersions[$appId])) { 521 $appInfo = $this->getAppInfo($appId); 522 $this->appVersions[$appId] = ($appInfo !== null && isset($appInfo['version'])) ? $appInfo['version'] : '0'; 523 } 524 return $this->appVersions[$appId]; 525 } 526 527 /** 528 * Returns a list of apps incompatible with the given version 529 * 530 * @param string $version Nextcloud version as array of version components 531 * 532 * @return array list of app info from incompatible apps 533 * 534 * @internal 535 */ 536 public function getIncompatibleApps(string $version): array { 537 $apps = $this->getInstalledApps(); 538 $incompatibleApps = []; 539 foreach ($apps as $appId) { 540 $info = $this->getAppInfo($appId); 541 if ($info === null) { 542 $incompatibleApps[] = ['id' => $appId, 'name' => $appId]; 543 } elseif (!\OC_App::isAppCompatible($version, $info)) { 544 $incompatibleApps[] = $info; 545 } 546 } 547 return $incompatibleApps; 548 } 549 550 /** 551 * @inheritdoc 552 * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::isShipped() 553 */ 554 public function isShipped($appId) { 555 $this->loadShippedJson(); 556 return in_array($appId, $this->shippedApps, true); 557 } 558 559 private function isAlwaysEnabled($appId) { 560 $alwaysEnabled = $this->getAlwaysEnabledApps(); 561 return in_array($appId, $alwaysEnabled, true); 562 } 563 564 /** 565 * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::loadShippedJson() 566 * @throws \Exception 567 */ 568 private function loadShippedJson() { 569 if ($this->shippedApps === null) { 570 $shippedJson = \OC::$SERVERROOT . '/core/shipped.json'; 571 if (!file_exists($shippedJson)) { 572 throw new \Exception("File not found: $shippedJson"); 573 } 574 $content = json_decode(file_get_contents($shippedJson), true); 575 $this->shippedApps = $content['shippedApps']; 576 $this->alwaysEnabled = $content['alwaysEnabled']; 577 } 578 } 579 580 /** 581 * @inheritdoc 582 */ 583 public function getAlwaysEnabledApps() { 584 $this->loadShippedJson(); 585 return $this->alwaysEnabled; 586 } 587} 588