1<?php 2 3declare(strict_types=1); 4 5/** 6 * @copyright Copyright (c) 2016, ownCloud, Inc. 7 * @copyright Copyright (c) 2016, Lukas Reschke <lukas@statuscode.ch> 8 * 9 * @author Arthur Schiwon <blizzz@arthur-schiwon.de> 10 * @author Bart Visscher <bartv@thisnet.nl> 11 * @author Bernhard Posselt <dev@bernhard-posselt.com> 12 * @author Borjan Tchakaloff <borjan@tchakaloff.fr> 13 * @author Brice Maron <brice@bmaron.net> 14 * @author Christopher Schäpers <kondou@ts.unde.re> 15 * @author Christoph Wurst <christoph@winzerhof-wurst.at> 16 * @author Daniel Rudolf <github.com@daniel-rudolf.de> 17 * @author Frank Karlitschek <frank@karlitschek.de> 18 * @author Georg Ehrke <oc.list@georgehrke.com> 19 * @author Jakob Sack <mail@jakobsack.de> 20 * @author Joas Schilling <coding@schilljs.com> 21 * @author Jörn Friedrich Dreyer <jfd@butonic.de> 22 * @author Julius Haertl <jus@bitgrid.net> 23 * @author Julius Härtl <jus@bitgrid.net> 24 * @author Kamil Domanski <kdomanski@kdemail.net> 25 * @author Lukas Reschke <lukas@statuscode.ch> 26 * @author Markus Goetz <markus@woboq.com> 27 * @author Morris Jobke <hey@morrisjobke.de> 28 * @author RealRancor <Fisch.666@gmx.de> 29 * @author Robin Appelman <robin@icewind.nl> 30 * @author Robin McCorkell <robin@mccorkell.me.uk> 31 * @author Roeland Jago Douma <roeland@famdouma.nl> 32 * @author Sam Tuke <mail@samtuke.com> 33 * @author Sebastian Wessalowski <sebastian@wessalowski.org> 34 * @author Thomas Müller <thomas.mueller@tmit.eu> 35 * @author Thomas Tanghus <thomas@tanghus.net> 36 * @author Vincent Petry <vincent@nextcloud.com> 37 * 38 * @license AGPL-3.0 39 * 40 * This code is free software: you can redistribute it and/or modify 41 * it under the terms of the GNU Affero General Public License, version 3, 42 * as published by the Free Software Foundation. 43 * 44 * This program is distributed in the hope that it will be useful, 45 * but WITHOUT ANY WARRANTY; without even the implied warranty of 46 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 47 * GNU Affero General Public License for more details. 48 * 49 * You should have received a copy of the GNU Affero General Public License, version 3, 50 * along with this program. If not, see <http://www.gnu.org/licenses/> 51 * 52 */ 53use OC\App\DependencyAnalyzer; 54use OC\App\Platform; 55use OC\AppFramework\Bootstrap\Coordinator; 56use OC\DB\MigrationService; 57use OC\Installer; 58use OC\Repair; 59use OC\ServerNotAvailableException; 60use OCP\App\ManagerEvent; 61use OCP\AppFramework\QueryException; 62use OCP\Authentication\IAlternativeLogin; 63use OCP\ILogger; 64use OCP\Settings\IManager as ISettingsManager; 65use Psr\Log\LoggerInterface; 66 67/** 68 * This class manages the apps. It allows them to register and integrate in the 69 * ownCloud ecosystem. Furthermore, this class is responsible for installing, 70 * upgrading and removing apps. 71 */ 72class OC_App { 73 private static $adminForms = []; 74 private static $personalForms = []; 75 private static $appTypes = []; 76 private static $loadedApps = []; 77 private static $altLogin = []; 78 private static $alreadyRegistered = []; 79 public const supportedApp = 300; 80 public const officialApp = 200; 81 82 /** 83 * clean the appId 84 * 85 * @psalm-taint-escape file 86 * @psalm-taint-escape include 87 * 88 * @param string $app AppId that needs to be cleaned 89 * @return string 90 */ 91 public static function cleanAppId(string $app): string { 92 return str_replace(['\0', '/', '\\', '..'], '', $app); 93 } 94 95 /** 96 * Check if an app is loaded 97 * 98 * @param string $app 99 * @return bool 100 */ 101 public static function isAppLoaded(string $app): bool { 102 return isset(self::$loadedApps[$app]); 103 } 104 105 /** 106 * loads all apps 107 * 108 * @param string[] $types 109 * @return bool 110 * 111 * This function walks through the ownCloud directory and loads all apps 112 * it can find. A directory contains an app if the file /appinfo/info.xml 113 * exists. 114 * 115 * if $types is set to non-empty array, only apps of those types will be loaded 116 */ 117 public static function loadApps(array $types = []): bool { 118 if ((bool) \OC::$server->getSystemConfig()->getValue('maintenance', false)) { 119 return false; 120 } 121 // Load the enabled apps here 122 $apps = self::getEnabledApps(); 123 124 // Add each apps' folder as allowed class path 125 foreach ($apps as $app) { 126 // If the app is already loaded then autoloading it makes no sense 127 if (!isset(self::$loadedApps[$app])) { 128 $path = self::getAppPath($app); 129 if ($path !== false) { 130 self::registerAutoloading($app, $path); 131 } 132 } 133 } 134 135 // prevent app.php from printing output 136 ob_start(); 137 foreach ($apps as $app) { 138 if (!isset(self::$loadedApps[$app]) && ($types === [] || self::isType($app, $types))) { 139 try { 140 self::loadApp($app); 141 } catch (\Throwable $e) { 142 \OC::$server->get(LoggerInterface::class)->emergency('Error during app loading: ' . $e->getMessage(), [ 143 'exception' => $e, 144 'app' => $app, 145 ]); 146 } 147 } 148 } 149 ob_end_clean(); 150 151 return true; 152 } 153 154 /** 155 * load a single app 156 * 157 * @param string $app 158 * @throws Exception 159 */ 160 public static function loadApp(string $app) { 161 self::$loadedApps[$app] = true; 162 $appPath = self::getAppPath($app); 163 if ($appPath === false) { 164 return; 165 } 166 167 // in case someone calls loadApp() directly 168 self::registerAutoloading($app, $appPath); 169 170 /** @var Coordinator $coordinator */ 171 $coordinator = \OC::$server->query(Coordinator::class); 172 $isBootable = $coordinator->isBootable($app); 173 174 $hasAppPhpFile = is_file($appPath . '/appinfo/app.php'); 175 176 if ($isBootable && $hasAppPhpFile) { 177 \OC::$server->getLogger()->error('/appinfo/app.php is not loaded when \OCP\AppFramework\Bootstrap\IBootstrap on the application class is used. Migrate everything from app.php to the Application class.', [ 178 'app' => $app, 179 ]); 180 } elseif ($hasAppPhpFile) { 181 \OC::$server->getLogger()->debug('/appinfo/app.php is deprecated, use \OCP\AppFramework\Bootstrap\IBootstrap on the application class instead.', [ 182 'app' => $app, 183 ]); 184 \OC::$server->getEventLogger()->start('load_app_' . $app, 'Load app: ' . $app); 185 try { 186 self::requireAppFile($app); 187 } catch (Throwable $ex) { 188 if ($ex instanceof ServerNotAvailableException) { 189 throw $ex; 190 } 191 if (!\OC::$server->getAppManager()->isShipped($app) && !self::isType($app, ['authentication'])) { 192 \OC::$server->getLogger()->logException($ex, [ 193 'message' => "App $app threw an error during app.php load and will be disabled: " . $ex->getMessage(), 194 ]); 195 196 // Only disable apps which are not shipped and that are not authentication apps 197 \OC::$server->getAppManager()->disableApp($app, true); 198 } else { 199 \OC::$server->getLogger()->logException($ex, [ 200 'message' => "App $app threw an error during app.php load: " . $ex->getMessage(), 201 ]); 202 } 203 } 204 \OC::$server->getEventLogger()->end('load_app_' . $app); 205 } 206 $coordinator->bootApp($app); 207 208 $info = self::getAppInfo($app); 209 if (!empty($info['activity']['filters'])) { 210 foreach ($info['activity']['filters'] as $filter) { 211 \OC::$server->getActivityManager()->registerFilter($filter); 212 } 213 } 214 if (!empty($info['activity']['settings'])) { 215 foreach ($info['activity']['settings'] as $setting) { 216 \OC::$server->getActivityManager()->registerSetting($setting); 217 } 218 } 219 if (!empty($info['activity']['providers'])) { 220 foreach ($info['activity']['providers'] as $provider) { 221 \OC::$server->getActivityManager()->registerProvider($provider); 222 } 223 } 224 225 if (!empty($info['settings']['admin'])) { 226 foreach ($info['settings']['admin'] as $setting) { 227 \OC::$server->get(ISettingsManager::class)->registerSetting('admin', $setting); 228 } 229 } 230 if (!empty($info['settings']['admin-section'])) { 231 foreach ($info['settings']['admin-section'] as $section) { 232 \OC::$server->get(ISettingsManager::class)->registerSection('admin', $section); 233 } 234 } 235 if (!empty($info['settings']['personal'])) { 236 foreach ($info['settings']['personal'] as $setting) { 237 \OC::$server->get(ISettingsManager::class)->registerSetting('personal', $setting); 238 } 239 } 240 if (!empty($info['settings']['personal-section'])) { 241 foreach ($info['settings']['personal-section'] as $section) { 242 \OC::$server->get(ISettingsManager::class)->registerSection('personal', $section); 243 } 244 } 245 246 if (!empty($info['collaboration']['plugins'])) { 247 // deal with one or many plugin entries 248 $plugins = isset($info['collaboration']['plugins']['plugin']['@value']) ? 249 [$info['collaboration']['plugins']['plugin']] : $info['collaboration']['plugins']['plugin']; 250 foreach ($plugins as $plugin) { 251 if ($plugin['@attributes']['type'] === 'collaborator-search') { 252 $pluginInfo = [ 253 'shareType' => $plugin['@attributes']['share-type'], 254 'class' => $plugin['@value'], 255 ]; 256 \OC::$server->getCollaboratorSearch()->registerPlugin($pluginInfo); 257 } elseif ($plugin['@attributes']['type'] === 'autocomplete-sort') { 258 \OC::$server->getAutoCompleteManager()->registerSorter($plugin['@value']); 259 } 260 } 261 } 262 } 263 264 /** 265 * @internal 266 * @param string $app 267 * @param string $path 268 * @param bool $force 269 */ 270 public static function registerAutoloading(string $app, string $path, bool $force = false) { 271 $key = $app . '-' . $path; 272 if (!$force && isset(self::$alreadyRegistered[$key])) { 273 return; 274 } 275 276 self::$alreadyRegistered[$key] = true; 277 278 // Register on PSR-4 composer autoloader 279 $appNamespace = \OC\AppFramework\App::buildAppNamespace($app); 280 \OC::$server->registerNamespace($app, $appNamespace); 281 282 if (file_exists($path . '/composer/autoload.php')) { 283 require_once $path . '/composer/autoload.php'; 284 } else { 285 \OC::$composerAutoloader->addPsr4($appNamespace . '\\', $path . '/lib/', true); 286 // Register on legacy autoloader 287 \OC::$loader->addValidRoot($path); 288 } 289 290 // Register Test namespace only when testing 291 if (defined('PHPUNIT_RUN') || defined('CLI_TEST_RUN')) { 292 \OC::$composerAutoloader->addPsr4($appNamespace . '\\Tests\\', $path . '/tests/', true); 293 } 294 } 295 296 /** 297 * Load app.php from the given app 298 * 299 * @param string $app app name 300 * @throws Error 301 */ 302 private static function requireAppFile(string $app) { 303 // encapsulated here to avoid variable scope conflicts 304 require_once $app . '/appinfo/app.php'; 305 } 306 307 /** 308 * check if an app is of a specific type 309 * 310 * @param string $app 311 * @param array $types 312 * @return bool 313 */ 314 public static function isType(string $app, array $types): bool { 315 $appTypes = self::getAppTypes($app); 316 foreach ($types as $type) { 317 if (array_search($type, $appTypes) !== false) { 318 return true; 319 } 320 } 321 return false; 322 } 323 324 /** 325 * get the types of an app 326 * 327 * @param string $app 328 * @return array 329 */ 330 private static function getAppTypes(string $app): array { 331 //load the cache 332 if (count(self::$appTypes) == 0) { 333 self::$appTypes = \OC::$server->getAppConfig()->getValues(false, 'types'); 334 } 335 336 if (isset(self::$appTypes[$app])) { 337 return explode(',', self::$appTypes[$app]); 338 } 339 340 return []; 341 } 342 343 /** 344 * read app types from info.xml and cache them in the database 345 */ 346 public static function setAppTypes(string $app) { 347 $appManager = \OC::$server->getAppManager(); 348 $appData = $appManager->getAppInfo($app); 349 if (!is_array($appData)) { 350 return; 351 } 352 353 if (isset($appData['types'])) { 354 $appTypes = implode(',', $appData['types']); 355 } else { 356 $appTypes = ''; 357 $appData['types'] = []; 358 } 359 360 $config = \OC::$server->getConfig(); 361 $config->setAppValue($app, 'types', $appTypes); 362 363 if ($appManager->hasProtectedAppType($appData['types'])) { 364 $enabled = $config->getAppValue($app, 'enabled', 'yes'); 365 if ($enabled !== 'yes' && $enabled !== 'no') { 366 $config->setAppValue($app, 'enabled', 'yes'); 367 } 368 } 369 } 370 371 /** 372 * Returns apps enabled for the current user. 373 * 374 * @param bool $forceRefresh whether to refresh the cache 375 * @param bool $all whether to return apps for all users, not only the 376 * currently logged in one 377 * @return string[] 378 */ 379 public static function getEnabledApps(bool $forceRefresh = false, bool $all = false): array { 380 if (!\OC::$server->getSystemConfig()->getValue('installed', false)) { 381 return []; 382 } 383 // in incognito mode or when logged out, $user will be false, 384 // which is also the case during an upgrade 385 $appManager = \OC::$server->getAppManager(); 386 if ($all) { 387 $user = null; 388 } else { 389 $user = \OC::$server->getUserSession()->getUser(); 390 } 391 392 if (is_null($user)) { 393 $apps = $appManager->getInstalledApps(); 394 } else { 395 $apps = $appManager->getEnabledAppsForUser($user); 396 } 397 $apps = array_filter($apps, function ($app) { 398 return $app !== 'files';//we add this manually 399 }); 400 sort($apps); 401 array_unshift($apps, 'files'); 402 return $apps; 403 } 404 405 /** 406 * checks whether or not an app is enabled 407 * 408 * @param string $app app 409 * @return bool 410 * @deprecated 13.0.0 use \OC::$server->getAppManager()->isEnabledForUser($appId) 411 * 412 * This function checks whether or not an app is enabled. 413 */ 414 public static function isEnabled(string $app): bool { 415 return \OC::$server->getAppManager()->isEnabledForUser($app); 416 } 417 418 /** 419 * enables an app 420 * 421 * @param string $appId 422 * @param array $groups (optional) when set, only these groups will have access to the app 423 * @throws \Exception 424 * @return void 425 * 426 * This function set an app as enabled in appconfig. 427 */ 428 public function enable(string $appId, 429 array $groups = []) { 430 431 // Check if app is already downloaded 432 /** @var Installer $installer */ 433 $installer = \OC::$server->query(Installer::class); 434 $isDownloaded = $installer->isDownloaded($appId); 435 436 if (!$isDownloaded) { 437 $installer->downloadApp($appId); 438 } 439 440 $installer->installApp($appId); 441 442 $appManager = \OC::$server->getAppManager(); 443 if ($groups !== []) { 444 $groupManager = \OC::$server->getGroupManager(); 445 $groupsList = []; 446 foreach ($groups as $group) { 447 $groupItem = $groupManager->get($group); 448 if ($groupItem instanceof \OCP\IGroup) { 449 $groupsList[] = $groupManager->get($group); 450 } 451 } 452 $appManager->enableAppForGroups($appId, $groupsList); 453 } else { 454 $appManager->enableApp($appId); 455 } 456 } 457 458 /** 459 * Get the path where to install apps 460 * 461 * @return string|false 462 */ 463 public static function getInstallPath() { 464 foreach (OC::$APPSROOTS as $dir) { 465 if (isset($dir['writable']) && $dir['writable'] === true) { 466 return $dir['path']; 467 } 468 } 469 470 \OCP\Util::writeLog('core', 'No application directories are marked as writable.', ILogger::ERROR); 471 return null; 472 } 473 474 475 /** 476 * search for an app in all app-directories 477 * 478 * @param string $appId 479 * @return false|string 480 */ 481 public static function findAppInDirectories(string $appId) { 482 $sanitizedAppId = self::cleanAppId($appId); 483 if ($sanitizedAppId !== $appId) { 484 return false; 485 } 486 static $app_dir = []; 487 488 if (isset($app_dir[$appId])) { 489 return $app_dir[$appId]; 490 } 491 492 $possibleApps = []; 493 foreach (OC::$APPSROOTS as $dir) { 494 if (file_exists($dir['path'] . '/' . $appId)) { 495 $possibleApps[] = $dir; 496 } 497 } 498 499 if (empty($possibleApps)) { 500 return false; 501 } elseif (count($possibleApps) === 1) { 502 $dir = array_shift($possibleApps); 503 $app_dir[$appId] = $dir; 504 return $dir; 505 } else { 506 $versionToLoad = []; 507 foreach ($possibleApps as $possibleApp) { 508 $version = self::getAppVersionByPath($possibleApp['path'] . '/' . $appId); 509 if (empty($versionToLoad) || version_compare($version, $versionToLoad['version'], '>')) { 510 $versionToLoad = [ 511 'dir' => $possibleApp, 512 'version' => $version, 513 ]; 514 } 515 } 516 $app_dir[$appId] = $versionToLoad['dir']; 517 return $versionToLoad['dir']; 518 //TODO - write test 519 } 520 } 521 522 /** 523 * Get the directory for the given app. 524 * If the app is defined in multiple directories, the first one is taken. (false if not found) 525 * 526 * @psalm-taint-specialize 527 * 528 * @param string $appId 529 * @return string|false 530 * @deprecated 11.0.0 use \OC::$server->getAppManager()->getAppPath() 531 */ 532 public static function getAppPath(string $appId) { 533 if ($appId === null || trim($appId) === '') { 534 return false; 535 } 536 537 if (($dir = self::findAppInDirectories($appId)) != false) { 538 return $dir['path'] . '/' . $appId; 539 } 540 return false; 541 } 542 543 /** 544 * Get the path for the given app on the access 545 * If the app is defined in multiple directories, the first one is taken. (false if not found) 546 * 547 * @param string $appId 548 * @return string|false 549 * @deprecated 18.0.0 use \OC::$server->getAppManager()->getAppWebPath() 550 */ 551 public static function getAppWebPath(string $appId) { 552 if (($dir = self::findAppInDirectories($appId)) != false) { 553 return OC::$WEBROOT . $dir['url'] . '/' . $appId; 554 } 555 return false; 556 } 557 558 /** 559 * get the last version of the app from appinfo/info.xml 560 * 561 * @param string $appId 562 * @param bool $useCache 563 * @return string 564 * @deprecated 14.0.0 use \OC::$server->getAppManager()->getAppVersion() 565 */ 566 public static function getAppVersion(string $appId, bool $useCache = true): string { 567 return \OC::$server->getAppManager()->getAppVersion($appId, $useCache); 568 } 569 570 /** 571 * get app's version based on it's path 572 * 573 * @param string $path 574 * @return string 575 */ 576 public static function getAppVersionByPath(string $path): string { 577 $infoFile = $path . '/appinfo/info.xml'; 578 $appData = \OC::$server->getAppManager()->getAppInfo($infoFile, true); 579 return isset($appData['version']) ? $appData['version'] : ''; 580 } 581 582 583 /** 584 * Read all app metadata from the info.xml file 585 * 586 * @param string $appId id of the app or the path of the info.xml file 587 * @param bool $path 588 * @param string $lang 589 * @return array|null 590 * @note all data is read from info.xml, not just pre-defined fields 591 * @deprecated 14.0.0 use \OC::$server->getAppManager()->getAppInfo() 592 */ 593 public static function getAppInfo(string $appId, bool $path = false, string $lang = null) { 594 return \OC::$server->getAppManager()->getAppInfo($appId, $path, $lang); 595 } 596 597 /** 598 * Returns the navigation 599 * 600 * @return array 601 * @deprecated 14.0.0 use \OC::$server->getNavigationManager()->getAll() 602 * 603 * This function returns an array containing all entries added. The 604 * entries are sorted by the key 'order' ascending. Additional to the keys 605 * given for each app the following keys exist: 606 * - active: boolean, signals if the user is on this navigation entry 607 */ 608 public static function getNavigation(): array { 609 return OC::$server->getNavigationManager()->getAll(); 610 } 611 612 /** 613 * Returns the Settings Navigation 614 * 615 * @return string[] 616 * @deprecated 14.0.0 use \OC::$server->getNavigationManager()->getAll('settings') 617 * 618 * This function returns an array containing all settings pages added. The 619 * entries are sorted by the key 'order' ascending. 620 */ 621 public static function getSettingsNavigation(): array { 622 return OC::$server->getNavigationManager()->getAll('settings'); 623 } 624 625 /** 626 * get the id of loaded app 627 * 628 * @return string 629 */ 630 public static function getCurrentApp(): string { 631 $request = \OC::$server->getRequest(); 632 $script = substr($request->getScriptName(), strlen(OC::$WEBROOT) + 1); 633 $topFolder = substr($script, 0, strpos($script, '/') ?: 0); 634 if (empty($topFolder)) { 635 $path_info = $request->getPathInfo(); 636 if ($path_info) { 637 $topFolder = substr($path_info, 1, strpos($path_info, '/', 1) - 1); 638 } 639 } 640 if ($topFolder == 'apps') { 641 $length = strlen($topFolder); 642 return substr($script, $length + 1, strpos($script, '/', $length + 1) - $length - 1) ?: ''; 643 } else { 644 return $topFolder; 645 } 646 } 647 648 /** 649 * @param string $type 650 * @return array 651 */ 652 public static function getForms(string $type): array { 653 $forms = []; 654 switch ($type) { 655 case 'admin': 656 $source = self::$adminForms; 657 break; 658 case 'personal': 659 $source = self::$personalForms; 660 break; 661 default: 662 return []; 663 } 664 foreach ($source as $form) { 665 $forms[] = include $form; 666 } 667 return $forms; 668 } 669 670 /** 671 * register an admin form to be shown 672 * 673 * @param string $app 674 * @param string $page 675 */ 676 public static function registerAdmin(string $app, string $page) { 677 self::$adminForms[] = $app . '/' . $page . '.php'; 678 } 679 680 /** 681 * register a personal form to be shown 682 * @param string $app 683 * @param string $page 684 */ 685 public static function registerPersonal(string $app, string $page) { 686 self::$personalForms[] = $app . '/' . $page . '.php'; 687 } 688 689 /** 690 * @param array $entry 691 * @deprecated 20.0.0 Please register your alternative login option using the registerAlternativeLogin() on the RegistrationContext in your Application class implementing the OCP\Authentication\IAlternativeLogin interface 692 */ 693 public static function registerLogIn(array $entry) { 694 \OC::$server->getLogger()->debug('OC_App::registerLogIn() is deprecated, please register your alternative login option using the registerAlternativeLogin() on the RegistrationContext in your Application class implementing the OCP\Authentication\IAlternativeLogin interface'); 695 self::$altLogin[] = $entry; 696 } 697 698 /** 699 * @return array 700 */ 701 public static function getAlternativeLogIns(): array { 702 /** @var Coordinator $bootstrapCoordinator */ 703 $bootstrapCoordinator = \OC::$server->query(Coordinator::class); 704 705 foreach ($bootstrapCoordinator->getRegistrationContext()->getAlternativeLogins() as $registration) { 706 if (!in_array(IAlternativeLogin::class, class_implements($registration->getService()), true)) { 707 \OC::$server->getLogger()->error('Alternative login option {option} does not implement {interface} and is therefore ignored.', [ 708 'option' => $registration->getService(), 709 'interface' => IAlternativeLogin::class, 710 'app' => $registration->getAppId(), 711 ]); 712 continue; 713 } 714 715 try { 716 /** @var IAlternativeLogin $provider */ 717 $provider = \OC::$server->query($registration->getService()); 718 } catch (QueryException $e) { 719 \OC::$server->getLogger()->logException($e, [ 720 'message' => 'Alternative login option {option} can not be initialised.', 721 'option' => $registration->getService(), 722 'app' => $registration->getAppId(), 723 ]); 724 } 725 726 try { 727 $provider->load(); 728 729 self::$altLogin[] = [ 730 'name' => $provider->getLabel(), 731 'href' => $provider->getLink(), 732 'style' => $provider->getClass(), 733 ]; 734 } catch (Throwable $e) { 735 \OC::$server->getLogger()->logException($e, [ 736 'message' => 'Alternative login option {option} had an error while loading.', 737 'option' => $registration->getService(), 738 'app' => $registration->getAppId(), 739 ]); 740 } 741 } 742 743 return self::$altLogin; 744 } 745 746 /** 747 * get a list of all apps in the apps folder 748 * 749 * @return string[] an array of app names (string IDs) 750 * @todo: change the name of this method to getInstalledApps, which is more accurate 751 */ 752 public static function getAllApps(): array { 753 $apps = []; 754 755 foreach (OC::$APPSROOTS as $apps_dir) { 756 if (!is_readable($apps_dir['path'])) { 757 \OCP\Util::writeLog('core', 'unable to read app folder : ' . $apps_dir['path'], ILogger::WARN); 758 continue; 759 } 760 $dh = opendir($apps_dir['path']); 761 762 if (is_resource($dh)) { 763 while (($file = readdir($dh)) !== false) { 764 if ($file[0] != '.' and is_dir($apps_dir['path'] . '/' . $file) and is_file($apps_dir['path'] . '/' . $file . '/appinfo/info.xml')) { 765 $apps[] = $file; 766 } 767 } 768 } 769 } 770 771 $apps = array_unique($apps); 772 773 return $apps; 774 } 775 776 /** 777 * List all supported apps 778 * 779 * @return array 780 */ 781 public function getSupportedApps(): array { 782 /** @var \OCP\Support\Subscription\IRegistry $subscriptionRegistry */ 783 $subscriptionRegistry = \OC::$server->query(\OCP\Support\Subscription\IRegistry::class); 784 $supportedApps = $subscriptionRegistry->delegateGetSupportedApps(); 785 return $supportedApps; 786 } 787 788 /** 789 * List all apps, this is used in apps.php 790 * 791 * @return array 792 */ 793 public function listAllApps(): array { 794 $installedApps = OC_App::getAllApps(); 795 796 $appManager = \OC::$server->getAppManager(); 797 //we don't want to show configuration for these 798 $blacklist = $appManager->getAlwaysEnabledApps(); 799 $appList = []; 800 $langCode = \OC::$server->getL10N('core')->getLanguageCode(); 801 $urlGenerator = \OC::$server->getURLGenerator(); 802 $supportedApps = $this->getSupportedApps(); 803 804 foreach ($installedApps as $app) { 805 if (array_search($app, $blacklist) === false) { 806 $info = OC_App::getAppInfo($app, false, $langCode); 807 if (!is_array($info)) { 808 \OCP\Util::writeLog('core', 'Could not read app info file for app "' . $app . '"', ILogger::ERROR); 809 continue; 810 } 811 812 if (!isset($info['name'])) { 813 \OCP\Util::writeLog('core', 'App id "' . $app . '" has no name in appinfo', ILogger::ERROR); 814 continue; 815 } 816 817 $enabled = \OC::$server->getConfig()->getAppValue($app, 'enabled', 'no'); 818 $info['groups'] = null; 819 if ($enabled === 'yes') { 820 $active = true; 821 } elseif ($enabled === 'no') { 822 $active = false; 823 } else { 824 $active = true; 825 $info['groups'] = $enabled; 826 } 827 828 $info['active'] = $active; 829 830 if ($appManager->isShipped($app)) { 831 $info['internal'] = true; 832 $info['level'] = self::officialApp; 833 $info['removable'] = false; 834 } else { 835 $info['internal'] = false; 836 $info['removable'] = true; 837 } 838 839 if (in_array($app, $supportedApps)) { 840 $info['level'] = self::supportedApp; 841 } 842 843 $appPath = self::getAppPath($app); 844 if ($appPath !== false) { 845 $appIcon = $appPath . '/img/' . $app . '.svg'; 846 if (file_exists($appIcon)) { 847 $info['preview'] = $urlGenerator->imagePath($app, $app . '.svg'); 848 $info['previewAsIcon'] = true; 849 } else { 850 $appIcon = $appPath . '/img/app.svg'; 851 if (file_exists($appIcon)) { 852 $info['preview'] = $urlGenerator->imagePath($app, 'app.svg'); 853 $info['previewAsIcon'] = true; 854 } 855 } 856 } 857 // fix documentation 858 if (isset($info['documentation']) && is_array($info['documentation'])) { 859 foreach ($info['documentation'] as $key => $url) { 860 // If it is not an absolute URL we assume it is a key 861 // i.e. admin-ldap will get converted to go.php?to=admin-ldap 862 if (stripos($url, 'https://') !== 0 && stripos($url, 'http://') !== 0) { 863 $url = $urlGenerator->linkToDocs($url); 864 } 865 866 $info['documentation'][$key] = $url; 867 } 868 } 869 870 $info['version'] = OC_App::getAppVersion($app); 871 $appList[] = $info; 872 } 873 } 874 875 return $appList; 876 } 877 878 public static function shouldUpgrade(string $app): bool { 879 $versions = self::getAppVersions(); 880 $currentVersion = OC_App::getAppVersion($app); 881 if ($currentVersion && isset($versions[$app])) { 882 $installedVersion = $versions[$app]; 883 if (!version_compare($currentVersion, $installedVersion, '=')) { 884 return true; 885 } 886 } 887 return false; 888 } 889 890 /** 891 * Adjust the number of version parts of $version1 to match 892 * the number of version parts of $version2. 893 * 894 * @param string $version1 version to adjust 895 * @param string $version2 version to take the number of parts from 896 * @return string shortened $version1 897 */ 898 private static function adjustVersionParts(string $version1, string $version2): string { 899 $version1 = explode('.', $version1); 900 $version2 = explode('.', $version2); 901 // reduce $version1 to match the number of parts in $version2 902 while (count($version1) > count($version2)) { 903 array_pop($version1); 904 } 905 // if $version1 does not have enough parts, add some 906 while (count($version1) < count($version2)) { 907 $version1[] = '0'; 908 } 909 return implode('.', $version1); 910 } 911 912 /** 913 * Check whether the current ownCloud version matches the given 914 * application's version requirements. 915 * 916 * The comparison is made based on the number of parts that the 917 * app info version has. For example for ownCloud 6.0.3 if the 918 * app info version is expecting version 6.0, the comparison is 919 * made on the first two parts of the ownCloud version. 920 * This means that it's possible to specify "requiremin" => 6 921 * and "requiremax" => 6 and it will still match ownCloud 6.0.3. 922 * 923 * @param string $ocVersion ownCloud version to check against 924 * @param array $appInfo app info (from xml) 925 * 926 * @return boolean true if compatible, otherwise false 927 */ 928 public static function isAppCompatible(string $ocVersion, array $appInfo, bool $ignoreMax = false): bool { 929 $requireMin = ''; 930 $requireMax = ''; 931 if (isset($appInfo['dependencies']['nextcloud']['@attributes']['min-version'])) { 932 $requireMin = $appInfo['dependencies']['nextcloud']['@attributes']['min-version']; 933 } elseif (isset($appInfo['dependencies']['owncloud']['@attributes']['min-version'])) { 934 $requireMin = $appInfo['dependencies']['owncloud']['@attributes']['min-version']; 935 } elseif (isset($appInfo['requiremin'])) { 936 $requireMin = $appInfo['requiremin']; 937 } elseif (isset($appInfo['require'])) { 938 $requireMin = $appInfo['require']; 939 } 940 941 if (isset($appInfo['dependencies']['nextcloud']['@attributes']['max-version'])) { 942 $requireMax = $appInfo['dependencies']['nextcloud']['@attributes']['max-version']; 943 } elseif (isset($appInfo['dependencies']['owncloud']['@attributes']['max-version'])) { 944 $requireMax = $appInfo['dependencies']['owncloud']['@attributes']['max-version']; 945 } elseif (isset($appInfo['requiremax'])) { 946 $requireMax = $appInfo['requiremax']; 947 } 948 949 if (!empty($requireMin) 950 && version_compare(self::adjustVersionParts($ocVersion, $requireMin), $requireMin, '<') 951 ) { 952 return false; 953 } 954 955 if (!$ignoreMax && !empty($requireMax) 956 && version_compare(self::adjustVersionParts($ocVersion, $requireMax), $requireMax, '>') 957 ) { 958 return false; 959 } 960 961 return true; 962 } 963 964 /** 965 * get the installed version of all apps 966 */ 967 public static function getAppVersions() { 968 static $versions; 969 970 if (!$versions) { 971 $appConfig = \OC::$server->getAppConfig(); 972 $versions = $appConfig->getValues(false, 'installed_version'); 973 } 974 return $versions; 975 } 976 977 /** 978 * update the database for the app and call the update script 979 * 980 * @param string $appId 981 * @return bool 982 */ 983 public static function updateApp(string $appId): bool { 984 $appPath = self::getAppPath($appId); 985 if ($appPath === false) { 986 return false; 987 } 988 989 if (is_file($appPath . '/appinfo/database.xml')) { 990 \OC::$server->getLogger()->error('The appinfo/database.xml file is not longer supported. Used in ' . $appId); 991 return false; 992 } 993 994 \OC::$server->getAppManager()->clearAppsCache(); 995 $l = \OC::$server->getL10N('core'); 996 $appData = self::getAppInfo($appId, false, $l->getLanguageCode()); 997 998 $ignoreMaxApps = \OC::$server->getConfig()->getSystemValue('app_install_overwrite', []); 999 $ignoreMax = in_array($appId, $ignoreMaxApps, true); 1000 \OC_App::checkAppDependencies( 1001 \OC::$server->getConfig(), 1002 $l, 1003 $appData, 1004 $ignoreMax 1005 ); 1006 1007 self::registerAutoloading($appId, $appPath, true); 1008 self::executeRepairSteps($appId, $appData['repair-steps']['pre-migration']); 1009 1010 $ms = new MigrationService($appId, \OC::$server->get(\OC\DB\Connection::class)); 1011 $ms->migrate(); 1012 1013 self::executeRepairSteps($appId, $appData['repair-steps']['post-migration']); 1014 self::setupLiveMigrations($appId, $appData['repair-steps']['live-migration']); 1015 // update appversion in app manager 1016 \OC::$server->getAppManager()->clearAppsCache(); 1017 \OC::$server->getAppManager()->getAppVersion($appId, false); 1018 1019 self::setupBackgroundJobs($appData['background-jobs']); 1020 1021 //set remote/public handlers 1022 if (array_key_exists('ocsid', $appData)) { 1023 \OC::$server->getConfig()->setAppValue($appId, 'ocsid', $appData['ocsid']); 1024 } elseif (\OC::$server->getConfig()->getAppValue($appId, 'ocsid', null) !== null) { 1025 \OC::$server->getConfig()->deleteAppValue($appId, 'ocsid'); 1026 } 1027 foreach ($appData['remote'] as $name => $path) { 1028 \OC::$server->getConfig()->setAppValue('core', 'remote_' . $name, $appId . '/' . $path); 1029 } 1030 foreach ($appData['public'] as $name => $path) { 1031 \OC::$server->getConfig()->setAppValue('core', 'public_' . $name, $appId . '/' . $path); 1032 } 1033 1034 self::setAppTypes($appId); 1035 1036 $version = \OC_App::getAppVersion($appId); 1037 \OC::$server->getConfig()->setAppValue($appId, 'installed_version', $version); 1038 1039 \OC::$server->getEventDispatcher()->dispatch(ManagerEvent::EVENT_APP_UPDATE, new ManagerEvent( 1040 ManagerEvent::EVENT_APP_UPDATE, $appId 1041 )); 1042 1043 return true; 1044 } 1045 1046 /** 1047 * @param string $appId 1048 * @param string[] $steps 1049 * @throws \OC\NeedsUpdateException 1050 */ 1051 public static function executeRepairSteps(string $appId, array $steps) { 1052 if (empty($steps)) { 1053 return; 1054 } 1055 // load the app 1056 self::loadApp($appId); 1057 1058 $dispatcher = OC::$server->getEventDispatcher(); 1059 1060 // load the steps 1061 $r = new Repair([], $dispatcher, \OC::$server->get(LoggerInterface::class)); 1062 foreach ($steps as $step) { 1063 try { 1064 $r->addStep($step); 1065 } catch (Exception $ex) { 1066 $r->emit('\OC\Repair', 'error', [$ex->getMessage()]); 1067 \OC::$server->getLogger()->logException($ex); 1068 } 1069 } 1070 // run the steps 1071 $r->run(); 1072 } 1073 1074 public static function setupBackgroundJobs(array $jobs) { 1075 $queue = \OC::$server->getJobList(); 1076 foreach ($jobs as $job) { 1077 $queue->add($job); 1078 } 1079 } 1080 1081 /** 1082 * @param string $appId 1083 * @param string[] $steps 1084 */ 1085 private static function setupLiveMigrations(string $appId, array $steps) { 1086 $queue = \OC::$server->getJobList(); 1087 foreach ($steps as $step) { 1088 $queue->add('OC\Migration\BackgroundRepair', [ 1089 'app' => $appId, 1090 'step' => $step]); 1091 } 1092 } 1093 1094 /** 1095 * @param string $appId 1096 * @return \OC\Files\View|false 1097 */ 1098 public static function getStorage(string $appId) { 1099 if (\OC::$server->getAppManager()->isEnabledForUser($appId)) { //sanity check 1100 if (\OC::$server->getUserSession()->isLoggedIn()) { 1101 $view = new \OC\Files\View('/' . OC_User::getUser()); 1102 if (!$view->file_exists($appId)) { 1103 $view->mkdir($appId); 1104 } 1105 return new \OC\Files\View('/' . OC_User::getUser() . '/' . $appId); 1106 } else { 1107 \OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ', user not logged in', ILogger::ERROR); 1108 return false; 1109 } 1110 } else { 1111 \OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ' not enabled', ILogger::ERROR); 1112 return false; 1113 } 1114 } 1115 1116 protected static function findBestL10NOption(array $options, string $lang): string { 1117 // only a single option 1118 if (isset($options['@value'])) { 1119 return $options['@value']; 1120 } 1121 1122 $fallback = $similarLangFallback = $englishFallback = false; 1123 1124 $lang = strtolower($lang); 1125 $similarLang = $lang; 1126 if (strpos($similarLang, '_')) { 1127 // For "de_DE" we want to find "de" and the other way around 1128 $similarLang = substr($lang, 0, strpos($lang, '_')); 1129 } 1130 1131 foreach ($options as $option) { 1132 if (is_array($option)) { 1133 if ($fallback === false) { 1134 $fallback = $option['@value']; 1135 } 1136 1137 if (!isset($option['@attributes']['lang'])) { 1138 continue; 1139 } 1140 1141 $attributeLang = strtolower($option['@attributes']['lang']); 1142 if ($attributeLang === $lang) { 1143 return $option['@value']; 1144 } 1145 1146 if ($attributeLang === $similarLang) { 1147 $similarLangFallback = $option['@value']; 1148 } elseif (strpos($attributeLang, $similarLang . '_') === 0) { 1149 if ($similarLangFallback === false) { 1150 $similarLangFallback = $option['@value']; 1151 } 1152 } 1153 } else { 1154 $englishFallback = $option; 1155 } 1156 } 1157 1158 if ($similarLangFallback !== false) { 1159 return $similarLangFallback; 1160 } elseif ($englishFallback !== false) { 1161 return $englishFallback; 1162 } 1163 return (string) $fallback; 1164 } 1165 1166 /** 1167 * parses the app data array and enhanced the 'description' value 1168 * 1169 * @param array $data the app data 1170 * @param string $lang 1171 * @return array improved app data 1172 */ 1173 public static function parseAppInfo(array $data, $lang = null): array { 1174 if ($lang && isset($data['name']) && is_array($data['name'])) { 1175 $data['name'] = self::findBestL10NOption($data['name'], $lang); 1176 } 1177 if ($lang && isset($data['summary']) && is_array($data['summary'])) { 1178 $data['summary'] = self::findBestL10NOption($data['summary'], $lang); 1179 } 1180 if ($lang && isset($data['description']) && is_array($data['description'])) { 1181 $data['description'] = trim(self::findBestL10NOption($data['description'], $lang)); 1182 } elseif (isset($data['description']) && is_string($data['description'])) { 1183 $data['description'] = trim($data['description']); 1184 } else { 1185 $data['description'] = ''; 1186 } 1187 1188 return $data; 1189 } 1190 1191 /** 1192 * @param \OCP\IConfig $config 1193 * @param \OCP\IL10N $l 1194 * @param array $info 1195 * @throws \Exception 1196 */ 1197 public static function checkAppDependencies(\OCP\IConfig $config, \OCP\IL10N $l, array $info, bool $ignoreMax) { 1198 $dependencyAnalyzer = new DependencyAnalyzer(new Platform($config), $l); 1199 $missing = $dependencyAnalyzer->analyze($info, $ignoreMax); 1200 if (!empty($missing)) { 1201 $missingMsg = implode(PHP_EOL, $missing); 1202 throw new \Exception( 1203 $l->t('App "%1$s" cannot be installed because the following dependencies are not fulfilled: %2$s', 1204 [$info['name'], $missingMsg] 1205 ) 1206 ); 1207 } 1208 } 1209} 1210