You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1100 lines
31 KiB

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