AppManager.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  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@owncloud.com>
  9. * @author Joas Schilling <coding@schilljs.com>
  10. * @author Julius Haertl <jus@bitgrid.net>
  11. * @author Lukas Reschke <lukas@statuscode.ch>
  12. * @author Morris Jobke <hey@morrisjobke.de>
  13. * @author Robin Appelman <robin@icewind.nl>
  14. * @author Thomas Müller <thomas.mueller@tmit.eu>
  15. * @author Vincent Petry <pvince81@owncloud.com>
  16. *
  17. * @license AGPL-3.0
  18. *
  19. * This code is free software: you can redistribute it and/or modify
  20. * it under the terms of the GNU Affero General Public License, version 3,
  21. * as published by the Free Software Foundation.
  22. *
  23. * This program is distributed in the hope that it will be useful,
  24. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  25. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  26. * GNU Affero General Public License for more details.
  27. *
  28. * You should have received a copy of the GNU Affero General Public License, version 3,
  29. * along with this program. If not, see <http://www.gnu.org/licenses/>
  30. *
  31. */
  32. namespace OC\App;
  33. use OC\AppConfig;
  34. use OCP\App\AppPathNotFoundException;
  35. use OCP\App\IAppManager;
  36. use OCP\App\ManagerEvent;
  37. use OCP\ICacheFactory;
  38. use OCP\IGroupManager;
  39. use OCP\IUser;
  40. use OCP\IUserSession;
  41. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  42. class AppManager implements IAppManager {
  43. /**
  44. * Apps with these types can not be enabled for certain groups only
  45. * @var string[]
  46. */
  47. protected $protectedAppTypes = [
  48. 'filesystem',
  49. 'prelogin',
  50. 'authentication',
  51. 'logging',
  52. 'prevent_group_restriction',
  53. ];
  54. /** @var IUserSession */
  55. private $userSession;
  56. /** @var AppConfig */
  57. private $appConfig;
  58. /** @var IGroupManager */
  59. private $groupManager;
  60. /** @var ICacheFactory */
  61. private $memCacheFactory;
  62. /** @var EventDispatcherInterface */
  63. private $dispatcher;
  64. /** @var string[] $appId => $enabled */
  65. private $installedAppsCache;
  66. /** @var string[] */
  67. private $shippedApps;
  68. /** @var string[] */
  69. private $alwaysEnabled;
  70. /** @var array */
  71. private $appInfos = [];
  72. /** @var array */
  73. private $appVersions = [];
  74. /** @var array */
  75. private $autoDisabledApps = [];
  76. /**
  77. * @param IUserSession $userSession
  78. * @param AppConfig $appConfig
  79. * @param IGroupManager $groupManager
  80. * @param ICacheFactory $memCacheFactory
  81. * @param EventDispatcherInterface $dispatcher
  82. */
  83. public function __construct(IUserSession $userSession,
  84. AppConfig $appConfig,
  85. IGroupManager $groupManager,
  86. ICacheFactory $memCacheFactory,
  87. EventDispatcherInterface $dispatcher) {
  88. $this->userSession = $userSession;
  89. $this->appConfig = $appConfig;
  90. $this->groupManager = $groupManager;
  91. $this->memCacheFactory = $memCacheFactory;
  92. $this->dispatcher = $dispatcher;
  93. }
  94. /**
  95. * @return string[] $appId => $enabled
  96. */
  97. private function getInstalledAppsValues() {
  98. if (!$this->installedAppsCache) {
  99. $values = $this->appConfig->getValues(false, 'enabled');
  100. $alwaysEnabledApps = $this->getAlwaysEnabledApps();
  101. foreach($alwaysEnabledApps as $appId) {
  102. $values[$appId] = 'yes';
  103. }
  104. $this->installedAppsCache = array_filter($values, function ($value) {
  105. return $value !== 'no';
  106. });
  107. ksort($this->installedAppsCache);
  108. }
  109. return $this->installedAppsCache;
  110. }
  111. /**
  112. * List all installed apps
  113. *
  114. * @return string[]
  115. */
  116. public function getInstalledApps() {
  117. return array_keys($this->getInstalledAppsValues());
  118. }
  119. /**
  120. * List all apps enabled for a user
  121. *
  122. * @param \OCP\IUser $user
  123. * @return string[]
  124. */
  125. public function getEnabledAppsForUser(IUser $user) {
  126. $apps = $this->getInstalledAppsValues();
  127. $appsForUser = array_filter($apps, function ($enabled) use ($user) {
  128. return $this->checkAppForUser($enabled, $user);
  129. });
  130. return array_keys($appsForUser);
  131. }
  132. /**
  133. * @return array
  134. */
  135. public function getAutoDisabledApps(): array {
  136. return $this->autoDisabledApps;
  137. }
  138. /**
  139. * Check if an app is enabled for user
  140. *
  141. * @param string $appId
  142. * @param \OCP\IUser $user (optional) if not defined, the currently logged in user will be used
  143. * @return bool
  144. */
  145. public function isEnabledForUser($appId, $user = null) {
  146. if ($this->isAlwaysEnabled($appId)) {
  147. return true;
  148. }
  149. if ($user === null) {
  150. $user = $this->userSession->getUser();
  151. }
  152. $installedApps = $this->getInstalledAppsValues();
  153. if (isset($installedApps[$appId])) {
  154. return $this->checkAppForUser($installedApps[$appId], $user);
  155. } else {
  156. return false;
  157. }
  158. }
  159. /**
  160. * @param string $enabled
  161. * @param IUser $user
  162. * @return bool
  163. */
  164. private function checkAppForUser($enabled, $user) {
  165. if ($enabled === 'yes') {
  166. return true;
  167. } elseif ($user === null) {
  168. return false;
  169. } else {
  170. if(empty($enabled)){
  171. return false;
  172. }
  173. $groupIds = json_decode($enabled);
  174. if (!is_array($groupIds)) {
  175. $jsonError = json_last_error();
  176. \OC::$server->getLogger()->warning('AppManger::checkAppForUser - can\'t decode group IDs: ' . print_r($enabled, true) . ' - json error code: ' . $jsonError, ['app' => 'lib']);
  177. return false;
  178. }
  179. $userGroups = $this->groupManager->getUserGroupIds($user);
  180. foreach ($userGroups as $groupId) {
  181. if (in_array($groupId, $groupIds, true)) {
  182. return true;
  183. }
  184. }
  185. return false;
  186. }
  187. }
  188. /**
  189. * Check if an app is enabled in the instance
  190. *
  191. * Notice: This actually checks if the app is enabled and not only if it is installed.
  192. *
  193. * @param string $appId
  194. * @return bool
  195. */
  196. public function isInstalled($appId) {
  197. $installedApps = $this->getInstalledAppsValues();
  198. return isset($installedApps[$appId]);
  199. }
  200. /**
  201. * Enable an app for every user
  202. *
  203. * @param string $appId
  204. * @throws AppPathNotFoundException
  205. */
  206. public function enableApp($appId) {
  207. // Check if app exists
  208. $this->getAppPath($appId);
  209. $this->installedAppsCache[$appId] = 'yes';
  210. $this->appConfig->setValue($appId, 'enabled', 'yes');
  211. $this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE, new ManagerEvent(
  212. ManagerEvent::EVENT_APP_ENABLE, $appId
  213. ));
  214. $this->clearAppsCache();
  215. }
  216. /**
  217. * Whether a list of types contains a protected app type
  218. *
  219. * @param string[] $types
  220. * @return bool
  221. */
  222. public function hasProtectedAppType($types) {
  223. if (empty($types)) {
  224. return false;
  225. }
  226. $protectedTypes = array_intersect($this->protectedAppTypes, $types);
  227. return !empty($protectedTypes);
  228. }
  229. /**
  230. * Enable an app only for specific groups
  231. *
  232. * @param string $appId
  233. * @param \OCP\IGroup[] $groups
  234. * @throws \InvalidArgumentException if app can't be enabled for groups
  235. * @throws AppPathNotFoundException
  236. */
  237. public function enableAppForGroups($appId, $groups) {
  238. // Check if app exists
  239. $this->getAppPath($appId);
  240. $info = $this->getAppInfo($appId);
  241. if (!empty($info['types']) && $this->hasProtectedAppType($info['types'])) {
  242. throw new \InvalidArgumentException("$appId can't be enabled for groups.");
  243. }
  244. $groupIds = array_map(function ($group) {
  245. /** @var \OCP\IGroup $group */
  246. return $group->getGID();
  247. }, $groups);
  248. $this->installedAppsCache[$appId] = json_encode($groupIds);
  249. $this->appConfig->setValue($appId, 'enabled', json_encode($groupIds));
  250. $this->dispatcher->dispatch(ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, new ManagerEvent(
  251. ManagerEvent::EVENT_APP_ENABLE_FOR_GROUPS, $appId, $groups
  252. ));
  253. $this->clearAppsCache();
  254. }
  255. /**
  256. * Disable an app for every user
  257. *
  258. * @param string $appId
  259. * @param bool $automaticDisabled
  260. * @throws \Exception if app can't be disabled
  261. */
  262. public function disableApp($appId, $automaticDisabled = false) {
  263. if ($this->isAlwaysEnabled($appId)) {
  264. throw new \Exception("$appId can't be disabled.");
  265. }
  266. if ($automaticDisabled) {
  267. $this->autoDisabledApps[] = $appId;
  268. }
  269. unset($this->installedAppsCache[$appId]);
  270. $this->appConfig->setValue($appId, 'enabled', 'no');
  271. // run uninstall steps
  272. $appData = $this->getAppInfo($appId);
  273. if (!is_null($appData)) {
  274. \OC_App::executeRepairSteps($appId, $appData['repair-steps']['uninstall']);
  275. }
  276. $this->dispatcher->dispatch(ManagerEvent::EVENT_APP_DISABLE, new ManagerEvent(
  277. ManagerEvent::EVENT_APP_DISABLE, $appId
  278. ));
  279. $this->clearAppsCache();
  280. }
  281. /**
  282. * Get the directory for the given app.
  283. *
  284. * @param string $appId
  285. * @return string
  286. * @throws AppPathNotFoundException if app folder can't be found
  287. */
  288. public function getAppPath($appId) {
  289. $appPath = \OC_App::getAppPath($appId);
  290. if($appPath === false) {
  291. throw new AppPathNotFoundException('Could not find path for ' . $appId);
  292. }
  293. return $appPath;
  294. }
  295. /**
  296. * Clear the cached list of apps when enabling/disabling an app
  297. */
  298. public function clearAppsCache() {
  299. $settingsMemCache = $this->memCacheFactory->createDistributed('settings');
  300. $settingsMemCache->clear('listApps');
  301. $this->appInfos = [];
  302. }
  303. /**
  304. * Returns a list of apps that need upgrade
  305. *
  306. * @param string $version Nextcloud version as array of version components
  307. * @return array list of app info from apps that need an upgrade
  308. *
  309. * @internal
  310. */
  311. public function getAppsNeedingUpgrade($version) {
  312. $appsToUpgrade = [];
  313. $apps = $this->getInstalledApps();
  314. foreach ($apps as $appId) {
  315. $appInfo = $this->getAppInfo($appId);
  316. $appDbVersion = $this->appConfig->getValue($appId, 'installed_version');
  317. if ($appDbVersion
  318. && isset($appInfo['version'])
  319. && version_compare($appInfo['version'], $appDbVersion, '>')
  320. && \OC_App::isAppCompatible($version, $appInfo)
  321. ) {
  322. $appsToUpgrade[] = $appInfo;
  323. }
  324. }
  325. return $appsToUpgrade;
  326. }
  327. /**
  328. * Returns the app information from "appinfo/info.xml".
  329. *
  330. * @param string $appId app id
  331. *
  332. * @param bool $path
  333. * @param null $lang
  334. * @return array|null app info
  335. */
  336. public function getAppInfo(string $appId, bool $path = false, $lang = null) {
  337. if ($path) {
  338. $file = $appId;
  339. } else {
  340. if ($lang === null && isset($this->appInfos[$appId])) {
  341. return $this->appInfos[$appId];
  342. }
  343. try {
  344. $appPath = $this->getAppPath($appId);
  345. } catch (AppPathNotFoundException $e) {
  346. return null;
  347. }
  348. $file = $appPath . '/appinfo/info.xml';
  349. }
  350. $parser = new InfoParser($this->memCacheFactory->createLocal('core.appinfo'));
  351. $data = $parser->parse($file);
  352. if (is_array($data)) {
  353. $data = \OC_App::parseAppInfo($data, $lang);
  354. }
  355. if ($lang === null) {
  356. $this->appInfos[$appId] = $data;
  357. }
  358. return $data;
  359. }
  360. public function getAppVersion(string $appId, bool $useCache = true): string {
  361. if(!$useCache || !isset($this->appVersions[$appId])) {
  362. $appInfo = \OC::$server->getAppManager()->getAppInfo($appId);
  363. $this->appVersions[$appId] = ($appInfo !== null && isset($appInfo['version'])) ? $appInfo['version'] : '0';
  364. }
  365. return $this->appVersions[$appId];
  366. }
  367. /**
  368. * Returns a list of apps incompatible with the given version
  369. *
  370. * @param string $version Nextcloud version as array of version components
  371. *
  372. * @return array list of app info from incompatible apps
  373. *
  374. * @internal
  375. */
  376. public function getIncompatibleApps(string $version): array {
  377. $apps = $this->getInstalledApps();
  378. $incompatibleApps = array();
  379. foreach ($apps as $appId) {
  380. $info = $this->getAppInfo($appId);
  381. if ($info === null) {
  382. $incompatibleApps[] = ['id' => $appId];
  383. } else if (!\OC_App::isAppCompatible($version, $info)) {
  384. $incompatibleApps[] = $info;
  385. }
  386. }
  387. return $incompatibleApps;
  388. }
  389. /**
  390. * @inheritdoc
  391. * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::isShipped()
  392. */
  393. public function isShipped($appId) {
  394. $this->loadShippedJson();
  395. return in_array($appId, $this->shippedApps, true);
  396. }
  397. private function isAlwaysEnabled($appId) {
  398. $alwaysEnabled = $this->getAlwaysEnabledApps();
  399. return in_array($appId, $alwaysEnabled, true);
  400. }
  401. /**
  402. * In case you change this method, also change \OC\App\CodeChecker\InfoChecker::loadShippedJson()
  403. * @throws \Exception
  404. */
  405. private function loadShippedJson() {
  406. if ($this->shippedApps === null) {
  407. $shippedJson = \OC::$SERVERROOT . '/core/shipped.json';
  408. if (!file_exists($shippedJson)) {
  409. throw new \Exception("File not found: $shippedJson");
  410. }
  411. $content = json_decode(file_get_contents($shippedJson), true);
  412. $this->shippedApps = $content['shippedApps'];
  413. $this->alwaysEnabled = $content['alwaysEnabled'];
  414. }
  415. }
  416. /**
  417. * @inheritdoc
  418. */
  419. public function getAlwaysEnabledApps() {
  420. $this->loadShippedJson();
  421. return $this->alwaysEnabled;
  422. }
  423. }