AppManager.php 26 KB

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