AppManager.php 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881
  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 Maxence Lange <maxence@artificial-owl.com>
  17. * @author Morris Jobke <hey@morrisjobke.de>
  18. * @author Robin Appelman <robin@icewind.nl>
  19. * @author Roeland Jago Douma <roeland@famdouma.nl>
  20. * @author Thomas Müller <thomas.mueller@tmit.eu>
  21. * @author Tobia De Koninck <tobia@ledfan.be>
  22. * @author Vincent Petry <vincent@nextcloud.com>
  23. *
  24. * @license AGPL-3.0
  25. *
  26. * This code is free software: you can redistribute it and/or modify
  27. * it under the terms of the GNU Affero General Public License, version 3,
  28. * as published by the Free Software Foundation.
  29. *
  30. * This program is distributed in the hope that it will be useful,
  31. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  32. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  33. * GNU Affero General Public License for more details.
  34. *
  35. * You should have received a copy of the GNU Affero General Public License, version 3,
  36. * along with this program. If not, see <http://www.gnu.org/licenses/>
  37. *
  38. */
  39. namespace OC\App;
  40. use InvalidArgumentException;
  41. use OC\AppConfig;
  42. use OC\AppFramework\Bootstrap\Coordinator;
  43. use OC\ServerNotAvailableException;
  44. use OCP\Activity\IManager as IActivityManager;
  45. use OCP\App\AppPathNotFoundException;
  46. use OCP\App\Events\AppDisableEvent;
  47. use OCP\App\Events\AppEnableEvent;
  48. use OCP\App\IAppManager;
  49. use OCP\App\ManagerEvent;
  50. use OCP\Collaboration\AutoComplete\IManager as IAutoCompleteManager;
  51. use OCP\Collaboration\Collaborators\ISearch as ICollaboratorSearch;
  52. use OCP\Diagnostics\IEventLogger;
  53. use OCP\EventDispatcher\IEventDispatcher;
  54. use OCP\ICacheFactory;
  55. use OCP\IConfig;
  56. use OCP\IGroup;
  57. use OCP\IGroupManager;
  58. use OCP\IURLGenerator;
  59. use OCP\IUser;
  60. use OCP\IUserSession;
  61. use OCP\Settings\IManager as ISettingsManager;
  62. use Psr\Log\LoggerInterface;
  63. class AppManager implements IAppManager {
  64. /**
  65. * Apps with these types can not be enabled for certain groups only
  66. * @var string[]
  67. */
  68. protected $protectedAppTypes = [
  69. 'filesystem',
  70. 'prelogin',
  71. 'authentication',
  72. 'logging',
  73. 'prevent_group_restriction',
  74. ];
  75. /** @var string[] $appId => $enabled */
  76. private array $installedAppsCache = [];
  77. /** @var string[]|null */
  78. private ?array $shippedApps = null;
  79. private array $alwaysEnabled = [];
  80. private array $defaultEnabled = [];
  81. /** @var array */
  82. private array $appInfos = [];
  83. /** @var array */
  84. private array $appVersions = [];
  85. /** @var array */
  86. private array $autoDisabledApps = [];
  87. private array $appTypes = [];
  88. /** @var array<string, true> */
  89. private array $loadedApps = [];
  90. public function __construct(
  91. private IUserSession $userSession,
  92. private IConfig $config,
  93. private AppConfig $appConfig,
  94. private IGroupManager $groupManager,
  95. private ICacheFactory $memCacheFactory,
  96. private IEventDispatcher $dispatcher,
  97. private LoggerInterface $logger,
  98. private IURLGenerator $urlGenerator,
  99. ) {
  100. }
  101. public function getAppIcon(string $appId, bool $dark = false): ?string {
  102. $possibleIcons = $dark ? [$appId . '-dark.svg', 'app-dark.svg'] : [$appId . '.svg', 'app.svg'];
  103. $icon = null;
  104. foreach ($possibleIcons as $iconName) {
  105. try {
  106. $icon = $this->urlGenerator->imagePath($appId, $iconName);
  107. break;
  108. } catch (\RuntimeException $e) {
  109. // ignore
  110. }
  111. }
  112. return $icon;
  113. }
  114. /**
  115. * @return string[] $appId => $enabled
  116. */
  117. private function getInstalledAppsValues(): array {
  118. if (!$this->installedAppsCache) {
  119. $values = $this->appConfig->getValues(false, 'enabled');
  120. $alwaysEnabledApps = $this->getAlwaysEnabledApps();
  121. foreach ($alwaysEnabledApps as $appId) {
  122. $values[$appId] = 'yes';
  123. }
  124. $this->installedAppsCache = array_filter($values, function ($value) {
  125. return $value !== 'no';
  126. });
  127. ksort($this->installedAppsCache);
  128. }
  129. return $this->installedAppsCache;
  130. }
  131. /**
  132. * List all installed apps
  133. *
  134. * @return string[]
  135. */
  136. public function getInstalledApps() {
  137. return array_keys($this->getInstalledAppsValues());
  138. }
  139. /**
  140. * List all apps enabled for a user
  141. *
  142. * @param \OCP\IUser $user
  143. * @return string[]
  144. */
  145. public function getEnabledAppsForUser(IUser $user) {
  146. $apps = $this->getInstalledAppsValues();
  147. $appsForUser = array_filter($apps, function ($enabled) use ($user) {
  148. return $this->checkAppForUser($enabled, $user);
  149. });
  150. return array_keys($appsForUser);
  151. }
  152. /**
  153. * @param IGroup $group
  154. * @return array
  155. */
  156. public function getEnabledAppsForGroup(IGroup $group): array {
  157. $apps = $this->getInstalledAppsValues();
  158. $appsForGroups = array_filter($apps, function ($enabled) use ($group) {
  159. return $this->checkAppForGroups($enabled, $group);
  160. });
  161. return array_keys($appsForGroups);
  162. }
  163. /**
  164. * Loads all apps
  165. *
  166. * @param string[] $types
  167. * @return bool
  168. *
  169. * This function walks through the Nextcloud directory and loads all apps
  170. * it can find. A directory contains an app if the file /appinfo/info.xml
  171. * exists.
  172. *
  173. * if $types is set to non-empty array, only apps of those types will be loaded
  174. */
  175. public function loadApps(array $types = []): bool {
  176. if ($this->config->getSystemValueBool('maintenance', false)) {
  177. return false;
  178. }
  179. // Load the enabled apps here
  180. $apps = \OC_App::getEnabledApps();
  181. // Add each apps' folder as allowed class path
  182. foreach ($apps as $app) {
  183. // If the app is already loaded then autoloading it makes no sense
  184. if (!$this->isAppLoaded($app)) {
  185. $path = \OC_App::getAppPath($app);
  186. if ($path !== false) {
  187. \OC_App::registerAutoloading($app, $path);
  188. }
  189. }
  190. }
  191. // prevent app.php from printing output
  192. ob_start();
  193. foreach ($apps as $app) {
  194. if (!$this->isAppLoaded($app) && ($types === [] || $this->isType($app, $types))) {
  195. try {
  196. $this->loadApp($app);
  197. } catch (\Throwable $e) {
  198. $this->logger->emergency('Error during app loading: ' . $e->getMessage(), [
  199. 'exception' => $e,
  200. 'app' => $app,
  201. ]);
  202. }
  203. }
  204. }
  205. ob_end_clean();
  206. return true;
  207. }
  208. /**
  209. * check if an app is of a specific type
  210. *
  211. * @param string $app
  212. * @param array $types
  213. * @return bool
  214. */
  215. public function isType(string $app, array $types): bool {
  216. $appTypes = $this->getAppTypes($app);
  217. foreach ($types as $type) {
  218. if (in_array($type, $appTypes, true)) {
  219. return true;
  220. }
  221. }
  222. return false;
  223. }
  224. /**
  225. * get the types of an app
  226. *
  227. * @param string $app
  228. * @return string[]
  229. */
  230. private function getAppTypes(string $app): array {
  231. //load the cache
  232. if (count($this->appTypes) === 0) {
  233. $this->appTypes = $this->appConfig->getValues(false, 'types') ?: [];
  234. }
  235. if (isset($this->appTypes[$app])) {
  236. return explode(',', $this->appTypes[$app]);
  237. }
  238. return [];
  239. }
  240. /**
  241. * @return array
  242. */
  243. public function getAutoDisabledApps(): array {
  244. return $this->autoDisabledApps;
  245. }
  246. /**
  247. * @param string $appId
  248. * @return array
  249. */
  250. public function getAppRestriction(string $appId): array {
  251. $values = $this->getInstalledAppsValues();
  252. if (!isset($values[$appId])) {
  253. return [];
  254. }
  255. if ($values[$appId] === 'yes' || $values[$appId] === 'no') {
  256. return [];
  257. }
  258. return json_decode($values[$appId], true);
  259. }
  260. /**
  261. * Check if an app is enabled for user
  262. *
  263. * @param string $appId
  264. * @param \OCP\IUser|null $user (optional) if not defined, the currently logged in user will be used
  265. * @return bool
  266. */
  267. public function isEnabledForUser($appId, $user = null) {
  268. if ($this->isAlwaysEnabled($appId)) {
  269. return true;
  270. }
  271. if ($user === null) {
  272. $user = $this->userSession->getUser();
  273. }
  274. $installedApps = $this->getInstalledAppsValues();
  275. if (isset($installedApps[$appId])) {
  276. return $this->checkAppForUser($installedApps[$appId], $user);
  277. } else {
  278. return false;
  279. }
  280. }
  281. private function checkAppForUser(string $enabled, ?IUser $user): bool {
  282. if ($enabled === 'yes') {
  283. return true;
  284. } elseif ($user === null) {
  285. return false;
  286. } else {
  287. if (empty($enabled)) {
  288. return false;
  289. }
  290. $groupIds = json_decode($enabled);
  291. if (!is_array($groupIds)) {
  292. $jsonError = json_last_error();
  293. $this->logger->warning('AppManger::checkAppForUser - can\'t decode group IDs: ' . print_r($enabled, true) . ' - json error code: ' . $jsonError);
  294. return false;
  295. }
  296. $userGroups = $this->groupManager->getUserGroupIds($user);
  297. foreach ($userGroups as $groupId) {
  298. if (in_array($groupId, $groupIds, true)) {
  299. return true;
  300. }
  301. }
  302. return false;
  303. }
  304. }
  305. private function checkAppForGroups(string $enabled, IGroup $group): bool {
  306. if ($enabled === 'yes') {
  307. return true;
  308. } else {
  309. if (empty($enabled)) {
  310. return false;
  311. }
  312. $groupIds = json_decode($enabled);
  313. if (!is_array($groupIds)) {
  314. $jsonError = json_last_error();
  315. $this->logger->warning('AppManger::checkAppForUser - can\'t decode group IDs: ' . print_r($enabled, true) . ' - json error code: ' . $jsonError);
  316. return false;
  317. }
  318. return in_array($group->getGID(), $groupIds);
  319. }
  320. }
  321. /**
  322. * Check if an app is enabled in the instance
  323. *
  324. * Notice: This actually checks if the app is enabled and not only if it is installed.
  325. *
  326. * @param string $appId
  327. * @param IGroup[]|String[] $groups
  328. * @return bool
  329. */
  330. public function isInstalled($appId) {
  331. $installedApps = $this->getInstalledAppsValues();
  332. return isset($installedApps[$appId]);
  333. }
  334. public function ignoreNextcloudRequirementForApp(string $appId): void {
  335. $ignoreMaxApps = $this->config->getSystemValue('app_install_overwrite', []);
  336. if (!in_array($appId, $ignoreMaxApps, true)) {
  337. $ignoreMaxApps[] = $appId;
  338. $this->config->setSystemValue('app_install_overwrite', $ignoreMaxApps);
  339. }
  340. }
  341. public function loadApp(string $app): void {
  342. if (isset($this->loadedApps[$app])) {
  343. return;
  344. }
  345. $this->loadedApps[$app] = true;
  346. $appPath = \OC_App::getAppPath($app);
  347. if ($appPath === false) {
  348. return;
  349. }
  350. $eventLogger = \OC::$server->get(\OCP\Diagnostics\IEventLogger::class);
  351. $eventLogger->start("bootstrap:load_app:$app", "Load $app");
  352. // in case someone calls loadApp() directly
  353. \OC_App::registerAutoloading($app, $appPath);
  354. /** @var Coordinator $coordinator */
  355. $coordinator = \OC::$server->get(Coordinator::class);
  356. $isBootable = $coordinator->isBootable($app);
  357. $hasAppPhpFile = is_file($appPath . '/appinfo/app.php');
  358. $eventLogger = \OC::$server->get(IEventLogger::class);
  359. $eventLogger->start('bootstrap:load_app_' . $app, 'Load app: ' . $app);
  360. if ($isBootable && $hasAppPhpFile) {
  361. $this->logger->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.', [
  362. 'app' => $app,
  363. ]);
  364. } elseif ($hasAppPhpFile) {
  365. $eventLogger->start("bootstrap:load_app:$app:app.php", "Load legacy app.php app $app");
  366. $this->logger->debug('/appinfo/app.php is deprecated, use \OCP\AppFramework\Bootstrap\IBootstrap on the application class instead.', [
  367. 'app' => $app,
  368. ]);
  369. try {
  370. self::requireAppFile($appPath);
  371. } catch (\Throwable $ex) {
  372. if ($ex instanceof ServerNotAvailableException) {
  373. throw $ex;
  374. }
  375. if (!$this->isShipped($app) && !$this->isType($app, ['authentication'])) {
  376. $this->logger->error("App $app threw an error during app.php load and will be disabled: " . $ex->getMessage(), [
  377. 'exception' => $ex,
  378. ]);
  379. // Only disable apps which are not shipped and that are not authentication apps
  380. $this->disableApp($app, true);
  381. } else {
  382. $this->logger->error("App $app threw an error during app.php load: " . $ex->getMessage(), [
  383. 'exception' => $ex,
  384. ]);
  385. }
  386. }
  387. $eventLogger->end("bootstrap:load_app:$app:app.php");
  388. }
  389. $coordinator->bootApp($app);
  390. $eventLogger->start("bootstrap:load_app:$app:info", "Load info.xml for $app and register any services defined in it");
  391. $info = $this->getAppInfo($app);
  392. if (!empty($info['activity'])) {
  393. $activityManager = \OC::$server->get(IActivityManager::class);
  394. if (!empty($info['activity']['filters'])) {
  395. foreach ($info['activity']['filters'] as $filter) {
  396. $activityManager->registerFilter($filter);
  397. }
  398. }
  399. if (!empty($info['activity']['settings'])) {
  400. foreach ($info['activity']['settings'] as $setting) {
  401. $activityManager->registerSetting($setting);
  402. }
  403. }
  404. if (!empty($info['activity']['providers'])) {
  405. foreach ($info['activity']['providers'] as $provider) {
  406. $activityManager->registerProvider($provider);
  407. }
  408. }
  409. }
  410. if (!empty($info['settings'])) {
  411. $settingsManager = \OC::$server->get(ISettingsManager::class);
  412. if (!empty($info['settings']['admin'])) {
  413. foreach ($info['settings']['admin'] as $setting) {
  414. $settingsManager->registerSetting('admin', $setting);
  415. }
  416. }
  417. if (!empty($info['settings']['admin-section'])) {
  418. foreach ($info['settings']['admin-section'] as $section) {
  419. $settingsManager->registerSection('admin', $section);
  420. }
  421. }
  422. if (!empty($info['settings']['personal'])) {
  423. foreach ($info['settings']['personal'] as $setting) {
  424. $settingsManager->registerSetting('personal', $setting);
  425. }
  426. }
  427. if (!empty($info['settings']['personal-section'])) {
  428. foreach ($info['settings']['personal-section'] as $section) {
  429. $settingsManager->registerSection('personal', $section);
  430. }
  431. }
  432. }
  433. if (!empty($info['collaboration']['plugins'])) {
  434. // deal with one or many plugin entries
  435. $plugins = isset($info['collaboration']['plugins']['plugin']['@value']) ?
  436. [$info['collaboration']['plugins']['plugin']] : $info['collaboration']['plugins']['plugin'];
  437. $collaboratorSearch = null;
  438. $autoCompleteManager = null;
  439. foreach ($plugins as $plugin) {
  440. if ($plugin['@attributes']['type'] === 'collaborator-search') {
  441. $pluginInfo = [
  442. 'shareType' => $plugin['@attributes']['share-type'],
  443. 'class' => $plugin['@value'],
  444. ];
  445. $collaboratorSearch ??= \OC::$server->get(ICollaboratorSearch::class);
  446. $collaboratorSearch->registerPlugin($pluginInfo);
  447. } elseif ($plugin['@attributes']['type'] === 'autocomplete-sort') {
  448. $autoCompleteManager ??= \OC::$server->get(IAutoCompleteManager::class);
  449. $autoCompleteManager->registerSorter($plugin['@value']);
  450. }
  451. }
  452. }
  453. $eventLogger->end("bootstrap:load_app:$app:info");
  454. $eventLogger->end("bootstrap:load_app:$app");
  455. }
  456. /**
  457. * Check if an app is loaded
  458. * @param string $app app id
  459. * @since 26.0.0
  460. */
  461. public function isAppLoaded(string $app): bool {
  462. return isset($this->loadedApps[$app]);
  463. }
  464. /**
  465. * Load app.php from the given app
  466. *
  467. * @param string $app app name
  468. * @throws \Error
  469. */
  470. private static function requireAppFile(string $app): void {
  471. // encapsulated here to avoid variable scope conflicts
  472. require_once $app . '/appinfo/app.php';
  473. }
  474. /**
  475. * Enable an app for every user
  476. *
  477. * @param string $appId
  478. * @param bool $forceEnable
  479. * @throws AppPathNotFoundException
  480. */
  481. public function enableApp(string $appId, bool $forceEnable = false): void {
  482. // Check if app exists
  483. $this->getAppPath($appId);
  484. if ($forceEnable) {
  485. $this->ignoreNextcloudRequirementForApp($appId);
  486. }
  487. $this->installedAppsCache[$appId] = 'yes';
  488. $this->appConfig->setValue($appId, 'enabled', 'yes');
  489. $this->dispatcher->dispatchTyped(new AppEnableEvent($appId));
  490. $this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE, new ManagerEvent(
  491. ManagerEvent::EVENT_APP_ENABLE, $appId
  492. ));
  493. $this->clearAppsCache();
  494. }
  495. /**
  496. * Whether a list of types contains a protected app type
  497. *
  498. * @param string[] $types
  499. * @return bool
  500. */
  501. public function hasProtectedAppType($types) {
  502. if (empty($types)) {
  503. return false;
  504. }
  505. $protectedTypes = array_intersect($this->protectedAppTypes, $types);
  506. return !empty($protectedTypes);
  507. }
  508. /**
  509. * Enable an app only for specific groups
  510. *
  511. * @param string $appId
  512. * @param IGroup[] $groups
  513. * @param bool $forceEnable
  514. * @throws \InvalidArgumentException if app can't be enabled for groups
  515. * @throws AppPathNotFoundException
  516. */
  517. public function enableAppForGroups(string $appId, array $groups, bool $forceEnable = false): void {
  518. // Check if app exists
  519. $this->getAppPath($appId);
  520. $info = $this->getAppInfo($appId);
  521. if (!empty($info['types']) && $this->hasProtectedAppType($info['types'])) {
  522. throw new \InvalidArgumentException("$appId can't be enabled for groups.");
  523. }
  524. if ($forceEnable) {
  525. $this->ignoreNextcloudRequirementForApp($appId);
  526. }
  527. /** @var string[] $groupIds */
  528. $groupIds = array_map(function ($group) {
  529. /** @var IGroup $group */
  530. return ($group instanceof IGroup)
  531. ? $group->getGID()
  532. : $group;
  533. }, $groups);
  534. $this->installedAppsCache[$appId] = json_encode($groupIds);
  535. $this->appConfig->setValue($appId, 'enabled', json_encode($groupIds));
  536. $this->dispatcher->dispatchTyped(new AppEnableEvent($appId, $groupIds));
  537. $this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, new ManagerEvent(
  538. ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, $appId, $groups
  539. ));
  540. $this->clearAppsCache();
  541. }
  542. /**
  543. * Disable an app for every user
  544. *
  545. * @param string $appId
  546. * @param bool $automaticDisabled
  547. * @throws \Exception if app can't be disabled
  548. */
  549. public function disableApp($appId, $automaticDisabled = false) {
  550. if ($this->isAlwaysEnabled($appId)) {
  551. throw new \Exception("$appId can't be disabled.");
  552. }
  553. if ($automaticDisabled) {
  554. $previousSetting = $this->appConfig->getValue($appId, 'enabled', 'yes');
  555. if ($previousSetting !== 'yes' && $previousSetting !== 'no') {
  556. $previousSetting = json_decode($previousSetting, true);
  557. }
  558. $this->autoDisabledApps[$appId] = $previousSetting;
  559. }
  560. unset($this->installedAppsCache[$appId]);
  561. $this->appConfig->setValue($appId, 'enabled', 'no');
  562. // run uninstall steps
  563. $appData = $this->getAppInfo($appId);
  564. if (!is_null($appData)) {
  565. \OC_App::executeRepairSteps($appId, $appData['repair-steps']['uninstall']);
  566. }
  567. $this->dispatcher->dispatchTyped(new AppDisableEvent($appId));
  568. $this->dispatcher->dispatch(ManagerEvent::EVENT_APP_DISABLE, new ManagerEvent(
  569. ManagerEvent::EVENT_APP_DISABLE, $appId
  570. ));
  571. $this->clearAppsCache();
  572. }
  573. /**
  574. * Get the directory for the given app.
  575. *
  576. * @param string $appId
  577. * @return string
  578. * @throws AppPathNotFoundException if app folder can't be found
  579. */
  580. public function getAppPath($appId) {
  581. $appPath = \OC_App::getAppPath($appId);
  582. if ($appPath === false) {
  583. throw new AppPathNotFoundException('Could not find path for ' . $appId);
  584. }
  585. return $appPath;
  586. }
  587. /**
  588. * Get the web path for the given app.
  589. *
  590. * @param string $appId
  591. * @return string
  592. * @throws AppPathNotFoundException if app path can't be found
  593. */
  594. public function getAppWebPath(string $appId): string {
  595. $appWebPath = \OC_App::getAppWebPath($appId);
  596. if ($appWebPath === false) {
  597. throw new AppPathNotFoundException('Could not find web path for ' . $appId);
  598. }
  599. return $appWebPath;
  600. }
  601. /**
  602. * Clear the cached list of apps when enabling/disabling an app
  603. */
  604. public function clearAppsCache() {
  605. $this->appInfos = [];
  606. }
  607. /**
  608. * Returns a list of apps that need upgrade
  609. *
  610. * @param string $version Nextcloud version as array of version components
  611. * @return array list of app info from apps that need an upgrade
  612. *
  613. * @internal
  614. */
  615. public function getAppsNeedingUpgrade($version) {
  616. $appsToUpgrade = [];
  617. $apps = $this->getInstalledApps();
  618. foreach ($apps as $appId) {
  619. $appInfo = $this->getAppInfo($appId);
  620. $appDbVersion = $this->appConfig->getValue($appId, 'installed_version');
  621. if ($appDbVersion
  622. && isset($appInfo['version'])
  623. && version_compare($appInfo['version'], $appDbVersion, '>')
  624. && \OC_App::isAppCompatible($version, $appInfo)
  625. ) {
  626. $appsToUpgrade[] = $appInfo;
  627. }
  628. }
  629. return $appsToUpgrade;
  630. }
  631. /**
  632. * Returns the app information from "appinfo/info.xml".
  633. *
  634. * @param string|null $lang
  635. * @return array|null app info
  636. */
  637. public function getAppInfo(string $appId, bool $path = false, $lang = null) {
  638. if ($path) {
  639. $file = $appId;
  640. } else {
  641. if ($lang === null && isset($this->appInfos[$appId])) {
  642. return $this->appInfos[$appId];
  643. }
  644. try {
  645. $appPath = $this->getAppPath($appId);
  646. } catch (AppPathNotFoundException $e) {
  647. return null;
  648. }
  649. $file = $appPath . '/appinfo/info.xml';
  650. }
  651. $parser = new InfoParser($this->memCacheFactory->createLocal('core.appinfo'));
  652. $data = $parser->parse($file);
  653. if (is_array($data)) {
  654. $data = \OC_App::parseAppInfo($data, $lang);
  655. }
  656. if ($lang === null) {
  657. $this->appInfos[$appId] = $data;
  658. }
  659. return $data;
  660. }
  661. public function getAppVersion(string $appId, bool $useCache = true): string {
  662. if (!$useCache || !isset($this->appVersions[$appId])) {
  663. $appInfo = $this->getAppInfo($appId);
  664. $this->appVersions[$appId] = ($appInfo !== null && isset($appInfo['version'])) ? $appInfo['version'] : '0';
  665. }
  666. return $this->appVersions[$appId];
  667. }
  668. /**
  669. * Returns a list of apps incompatible with the given version
  670. *
  671. * @param string $version Nextcloud version as array of version components
  672. *
  673. * @return array list of app info from incompatible apps
  674. *
  675. * @internal
  676. */
  677. public function getIncompatibleApps(string $version): array {
  678. $apps = $this->getInstalledApps();
  679. $incompatibleApps = [];
  680. foreach ($apps as $appId) {
  681. $info = $this->getAppInfo($appId);
  682. if ($info === null) {
  683. $incompatibleApps[] = ['id' => $appId, 'name' => $appId];
  684. } elseif (!\OC_App::isAppCompatible($version, $info)) {
  685. $incompatibleApps[] = $info;
  686. }
  687. }
  688. return $incompatibleApps;
  689. }
  690. /**
  691. * @inheritdoc
  692. * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::isShipped()
  693. */
  694. public function isShipped($appId) {
  695. $this->loadShippedJson();
  696. return in_array($appId, $this->shippedApps, true);
  697. }
  698. private function isAlwaysEnabled(string $appId): bool {
  699. $alwaysEnabled = $this->getAlwaysEnabledApps();
  700. return in_array($appId, $alwaysEnabled, true);
  701. }
  702. /**
  703. * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::loadShippedJson()
  704. * @throws \Exception
  705. */
  706. private function loadShippedJson(): void {
  707. if ($this->shippedApps === null) {
  708. $shippedJson = \OC::$SERVERROOT . '/core/shipped.json';
  709. if (!file_exists($shippedJson)) {
  710. throw new \Exception("File not found: $shippedJson");
  711. }
  712. $content = json_decode(file_get_contents($shippedJson), true);
  713. $this->shippedApps = $content['shippedApps'];
  714. $this->alwaysEnabled = $content['alwaysEnabled'];
  715. $this->defaultEnabled = $content['defaultEnabled'];
  716. }
  717. }
  718. /**
  719. * @inheritdoc
  720. */
  721. public function getAlwaysEnabledApps() {
  722. $this->loadShippedJson();
  723. return $this->alwaysEnabled;
  724. }
  725. /**
  726. * @inheritdoc
  727. */
  728. public function isDefaultEnabled(string $appId): bool {
  729. return (in_array($appId, $this->getDefaultEnabledApps()));
  730. }
  731. /**
  732. * @inheritdoc
  733. */
  734. public function getDefaultEnabledApps(): array {
  735. $this->loadShippedJson();
  736. return $this->defaultEnabled;
  737. }
  738. public function getDefaultAppForUser(?IUser $user = null, bool $withFallbacks = true): string {
  739. // Set fallback to always-enabled files app
  740. $appId = $withFallbacks ? 'files' : '';
  741. $defaultApps = explode(',', $this->config->getSystemValueString('defaultapp', ''));
  742. $defaultApps = array_filter($defaultApps);
  743. $user ??= $this->userSession->getUser();
  744. if ($user !== null) {
  745. $userDefaultApps = explode(',', $this->config->getUserValue($user->getUID(), 'core', 'defaultapp'));
  746. $defaultApps = array_filter(array_merge($userDefaultApps, $defaultApps));
  747. if (empty($defaultApps) && $withFallbacks) {
  748. /* Fallback on user defined apporder */
  749. $customOrders = json_decode($this->config->getUserValue($user->getUID(), 'core', 'apporder', '[]'), true, flags:JSON_THROW_ON_ERROR);
  750. if (!empty($customOrders)) {
  751. // filter only entries with app key (when added using closures or NavigationManager::add the app is not guranteed to be set)
  752. $customOrders = array_filter($customOrders, fn ($entry) => isset($entry['app']));
  753. // sort apps by order
  754. usort($customOrders, fn ($a, $b) => $a['order'] - $b['order']);
  755. // set default apps to sorted apps
  756. $defaultApps = array_map(fn ($entry) => $entry['app'], $customOrders);
  757. }
  758. }
  759. }
  760. if (empty($defaultApps) && $withFallbacks) {
  761. $defaultApps = ['dashboard','files'];
  762. }
  763. // Find the first app that is enabled for the current user
  764. foreach ($defaultApps as $defaultApp) {
  765. $defaultApp = \OC_App::cleanAppId(strip_tags($defaultApp));
  766. if ($this->isEnabledForUser($defaultApp, $user)) {
  767. $appId = $defaultApp;
  768. break;
  769. }
  770. }
  771. return $appId;
  772. }
  773. public function getDefaultApps(): array {
  774. return explode(',', $this->config->getSystemValueString('defaultapp', 'dashboard,files'));
  775. }
  776. public function setDefaultApps(array $defaultApps): void {
  777. foreach ($defaultApps as $app) {
  778. if (!$this->isInstalled($app)) {
  779. $this->logger->debug('Can not set not installed app as default app', ['missing_app' => $app]);
  780. throw new InvalidArgumentException('App is not installed');
  781. }
  782. }
  783. $this->config->setSystemValue('defaultapp', join(',', $defaultApps));
  784. }
  785. }