app.php 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
  6. * @author Bart Visscher <bartv@thisnet.nl>
  7. * @author Bernhard Posselt <dev@bernhard-posselt.com>
  8. * @author Björn Schießle <bjoern@schiessle.org>
  9. * @author Borjan Tchakaloff <borjan@tchakaloff.fr>
  10. * @author Brice Maron <brice@bmaron.net>
  11. * @author Christopher Schäpers <kondou@ts.unde.re>
  12. * @author Felix Moeller <mail@felixmoeller.de>
  13. * @author Frank Karlitschek <frank@karlitschek.de>
  14. * @author Georg Ehrke <georg@owncloud.com>
  15. * @author Jakob Sack <mail@jakobsack.de>
  16. * @author Jan-Christoph Borchardt <hey@jancborchardt.net>
  17. * @author Joas Schilling <coding@schilljs.com>
  18. * @author Jörn Friedrich Dreyer <jfd@butonic.de>
  19. * @author Kamil Domanski <kdomanski@kdemail.net>
  20. * @author Lukas Reschke <lukas@statuscode.ch>
  21. * @author Markus Goetz <markus@woboq.com>
  22. * @author Morris Jobke <hey@morrisjobke.de>
  23. * @author RealRancor <Fisch.666@gmx.de>
  24. * @author Robin Appelman <robin@icewind.nl>
  25. * @author Robin McCorkell <robin@mccorkell.me.uk>
  26. * @author Roeland Jago Douma <roeland@famdouma.nl>
  27. * @author Sam Tuke <mail@samtuke.com>
  28. * @author Thomas Müller <thomas.mueller@tmit.eu>
  29. * @author Thomas Tanghus <thomas@tanghus.net>
  30. * @author Tom Needham <tom@owncloud.com>
  31. * @author Vincent Petry <pvince81@owncloud.com>
  32. *
  33. * @license AGPL-3.0
  34. *
  35. * This code is free software: you can redistribute it and/or modify
  36. * it under the terms of the GNU Affero General Public License, version 3,
  37. * as published by the Free Software Foundation.
  38. *
  39. * This program is distributed in the hope that it will be useful,
  40. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  41. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  42. * GNU Affero General Public License for more details.
  43. *
  44. * You should have received a copy of the GNU Affero General Public License, version 3,
  45. * along with this program. If not, see <http://www.gnu.org/licenses/>
  46. *
  47. */
  48. use OC\App\DependencyAnalyzer;
  49. use OC\App\Platform;
  50. use OC\OCSClient;
  51. /**
  52. * This class manages the apps. It allows them to register and integrate in the
  53. * ownCloud ecosystem. Furthermore, this class is responsible for installing,
  54. * upgrading and removing apps.
  55. */
  56. class OC_App {
  57. static private $appVersion = [];
  58. static private $adminForms = array();
  59. static private $personalForms = array();
  60. static private $appInfo = array();
  61. static private $appTypes = array();
  62. static private $loadedApps = array();
  63. static private $altLogin = array();
  64. const officialApp = 200;
  65. /**
  66. * clean the appId
  67. *
  68. * @param string|boolean $app AppId that needs to be cleaned
  69. * @return string
  70. */
  71. public static function cleanAppId($app) {
  72. return str_replace(array('\0', '/', '\\', '..'), '', $app);
  73. }
  74. /**
  75. * Check if an app is loaded
  76. *
  77. * @param string $app
  78. * @return bool
  79. */
  80. public static function isAppLoaded($app) {
  81. return in_array($app, self::$loadedApps, true);
  82. }
  83. /**
  84. * loads all apps
  85. *
  86. * @param string[] | string | null $types
  87. * @return bool
  88. *
  89. * This function walks through the ownCloud directory and loads all apps
  90. * it can find. A directory contains an app if the file /appinfo/info.xml
  91. * exists.
  92. *
  93. * if $types is set, only apps of those types will be loaded
  94. */
  95. public static function loadApps($types = null) {
  96. if (\OC::$server->getSystemConfig()->getValue('maintenance', false)) {
  97. return false;
  98. }
  99. // Load the enabled apps here
  100. $apps = self::getEnabledApps();
  101. // Add each apps' folder as allowed class path
  102. foreach($apps as $app) {
  103. $path = self::getAppPath($app);
  104. if($path !== false) {
  105. \OC::$loader->addValidRoot($path);
  106. }
  107. }
  108. // prevent app.php from printing output
  109. ob_start();
  110. foreach ($apps as $app) {
  111. if ((is_null($types) or self::isType($app, $types)) && !in_array($app, self::$loadedApps)) {
  112. self::loadApp($app);
  113. }
  114. }
  115. ob_end_clean();
  116. return true;
  117. }
  118. /**
  119. * load a single app
  120. *
  121. * @param string $app
  122. * @param bool $checkUpgrade whether an upgrade check should be done
  123. * @throws \OC\NeedsUpdateException
  124. */
  125. public static function loadApp($app, $checkUpgrade = true) {
  126. self::$loadedApps[] = $app;
  127. $appPath = self::getAppPath($app);
  128. if($appPath === false) {
  129. return;
  130. }
  131. \OC::$loader->addValidRoot($appPath); // in case someone calls loadApp() directly
  132. if (is_file($appPath . '/appinfo/app.php')) {
  133. \OC::$server->getEventLogger()->start('load_app_' . $app, 'Load app: ' . $app);
  134. if ($checkUpgrade and self::shouldUpgrade($app)) {
  135. throw new \OC\NeedsUpdateException();
  136. }
  137. self::requireAppFile($app);
  138. if (self::isType($app, array('authentication'))) {
  139. // since authentication apps affect the "is app enabled for group" check,
  140. // the enabled apps cache needs to be cleared to make sure that the
  141. // next time getEnableApps() is called it will also include apps that were
  142. // enabled for groups
  143. self::$enabledAppsCache = array();
  144. }
  145. \OC::$server->getEventLogger()->end('load_app_' . $app);
  146. }
  147. }
  148. /**
  149. * Load app.php from the given app
  150. *
  151. * @param string $app app name
  152. */
  153. private static function requireAppFile($app) {
  154. // encapsulated here to avoid variable scope conflicts
  155. require_once $app . '/appinfo/app.php';
  156. }
  157. /**
  158. * check if an app is of a specific type
  159. *
  160. * @param string $app
  161. * @param string|array $types
  162. * @return bool
  163. */
  164. public static function isType($app, $types) {
  165. if (is_string($types)) {
  166. $types = array($types);
  167. }
  168. $appTypes = self::getAppTypes($app);
  169. foreach ($types as $type) {
  170. if (array_search($type, $appTypes) !== false) {
  171. return true;
  172. }
  173. }
  174. return false;
  175. }
  176. /**
  177. * get the types of an app
  178. *
  179. * @param string $app
  180. * @return array
  181. */
  182. private static function getAppTypes($app) {
  183. //load the cache
  184. if (count(self::$appTypes) == 0) {
  185. self::$appTypes = \OC::$server->getAppConfig()->getValues(false, 'types');
  186. }
  187. if (isset(self::$appTypes[$app])) {
  188. return explode(',', self::$appTypes[$app]);
  189. } else {
  190. return array();
  191. }
  192. }
  193. /**
  194. * read app types from info.xml and cache them in the database
  195. */
  196. public static function setAppTypes($app) {
  197. $appData = self::getAppInfo($app);
  198. if(!is_array($appData)) {
  199. return;
  200. }
  201. if (isset($appData['types'])) {
  202. $appTypes = implode(',', $appData['types']);
  203. } else {
  204. $appTypes = '';
  205. }
  206. \OC::$server->getAppConfig()->setValue($app, 'types', $appTypes);
  207. }
  208. /**
  209. * check if app is shipped
  210. *
  211. * @param string $appId the id of the app to check
  212. * @return bool
  213. *
  214. * Check if an app that is installed is a shipped app or installed from the appstore.
  215. */
  216. public static function isShipped($appId) {
  217. return \OC::$server->getAppManager()->isShipped($appId);
  218. }
  219. /**
  220. * get all enabled apps
  221. */
  222. protected static $enabledAppsCache = array();
  223. /**
  224. * Returns apps enabled for the current user.
  225. *
  226. * @param bool $forceRefresh whether to refresh the cache
  227. * @param bool $all whether to return apps for all users, not only the
  228. * currently logged in one
  229. * @return string[]
  230. */
  231. public static function getEnabledApps($forceRefresh = false, $all = false) {
  232. if (!\OC::$server->getSystemConfig()->getValue('installed', false)) {
  233. return array();
  234. }
  235. // in incognito mode or when logged out, $user will be false,
  236. // which is also the case during an upgrade
  237. $appManager = \OC::$server->getAppManager();
  238. if ($all) {
  239. $user = null;
  240. } else {
  241. $user = \OC::$server->getUserSession()->getUser();
  242. }
  243. if (is_null($user)) {
  244. $apps = $appManager->getInstalledApps();
  245. } else {
  246. $apps = $appManager->getEnabledAppsForUser($user);
  247. }
  248. $apps = array_filter($apps, function ($app) {
  249. return $app !== 'files';//we add this manually
  250. });
  251. sort($apps);
  252. array_unshift($apps, 'files');
  253. return $apps;
  254. }
  255. /**
  256. * checks whether or not an app is enabled
  257. *
  258. * @param string $app app
  259. * @return bool
  260. *
  261. * This function checks whether or not an app is enabled.
  262. */
  263. public static function isEnabled($app) {
  264. return \OC::$server->getAppManager()->isEnabledForUser($app);
  265. }
  266. /**
  267. * enables an app
  268. *
  269. * @param mixed $app app
  270. * @param array $groups (optional) when set, only these groups will have access to the app
  271. * @throws \Exception
  272. * @return void
  273. *
  274. * This function set an app as enabled in appconfig.
  275. */
  276. public static function enable($app, $groups = null) {
  277. self::$enabledAppsCache = array(); // flush
  278. if (!OC_Installer::isInstalled($app)) {
  279. $app = self::installApp($app);
  280. }
  281. $appManager = \OC::$server->getAppManager();
  282. if (!is_null($groups)) {
  283. $groupManager = \OC::$server->getGroupManager();
  284. $groupsList = [];
  285. foreach ($groups as $group) {
  286. $groupItem = $groupManager->get($group);
  287. if ($groupItem instanceof \OCP\IGroup) {
  288. $groupsList[] = $groupManager->get($group);
  289. }
  290. }
  291. $appManager->enableAppForGroups($app, $groupsList);
  292. } else {
  293. $appManager->enableApp($app);
  294. }
  295. }
  296. /**
  297. * @param string $app
  298. * @return int
  299. */
  300. private static function downloadApp($app) {
  301. $ocsClient = new OCSClient(
  302. \OC::$server->getHTTPClientService(),
  303. \OC::$server->getConfig(),
  304. \OC::$server->getLogger()
  305. );
  306. $appData = $ocsClient->getApplication($app, \OCP\Util::getVersion());
  307. $download = $ocsClient->getApplicationDownload($app, \OCP\Util::getVersion());
  308. if(isset($download['downloadlink']) and $download['downloadlink']!='') {
  309. // Replace spaces in download link without encoding entire URL
  310. $download['downloadlink'] = str_replace(' ', '%20', $download['downloadlink']);
  311. $info = array('source' => 'http', 'href' => $download['downloadlink'], 'appdata' => $appData);
  312. $app = OC_Installer::installApp($info);
  313. }
  314. return $app;
  315. }
  316. /**
  317. * @param string $app
  318. * @return bool
  319. */
  320. public static function removeApp($app) {
  321. if (self::isShipped($app)) {
  322. return false;
  323. }
  324. return OC_Installer::removeApp($app);
  325. }
  326. /**
  327. * This function set an app as disabled in appconfig.
  328. *
  329. * @param string $app app
  330. * @throws Exception
  331. */
  332. public static function disable($app) {
  333. // Convert OCS ID to regular application identifier
  334. if(self::getInternalAppIdByOcs($app) !== false) {
  335. $app = self::getInternalAppIdByOcs($app);
  336. }
  337. self::$enabledAppsCache = array(); // flush
  338. // check if app is a shipped app or not. if not delete
  339. \OC_Hook::emit('OC_App', 'pre_disable', array('app' => $app));
  340. $appManager = \OC::$server->getAppManager();
  341. $appManager->disableApp($app);
  342. }
  343. /**
  344. * Returns the Settings Navigation
  345. *
  346. * @return string[]
  347. *
  348. * This function returns an array containing all settings pages added. The
  349. * entries are sorted by the key 'order' ascending.
  350. */
  351. public static function getSettingsNavigation() {
  352. $l = \OC::$server->getL10N('lib');
  353. $urlGenerator = \OC::$server->getURLGenerator();
  354. $settings = array();
  355. // by default, settings only contain the help menu
  356. if (OC_Util::getEditionString() === '' &&
  357. \OC::$server->getSystemConfig()->getValue('knowledgebaseenabled', true) == true
  358. ) {
  359. $settings = array(
  360. array(
  361. "id" => "help",
  362. "order" => 1000,
  363. "href" => $urlGenerator->linkToRoute('settings_help'),
  364. "name" => $l->t("Help"),
  365. "icon" => $urlGenerator->imagePath("settings", "help.svg")
  366. )
  367. );
  368. }
  369. // if the user is logged-in
  370. if (OC_User::isLoggedIn()) {
  371. // personal menu
  372. $settings[] = array(
  373. "id" => "personal",
  374. "order" => 1,
  375. "href" => $urlGenerator->linkToRoute('settings_personal'),
  376. "name" => $l->t("Personal"),
  377. "icon" => $urlGenerator->imagePath("settings", "personal.svg")
  378. );
  379. //SubAdmins are also allowed to access user management
  380. $userObject = \OC::$server->getUserSession()->getUser();
  381. $isSubAdmin = false;
  382. if($userObject !== null) {
  383. $isSubAdmin = \OC::$server->getGroupManager()->getSubAdmin()->isSubAdmin($userObject);
  384. }
  385. if ($isSubAdmin) {
  386. // admin users menu
  387. $settings[] = array(
  388. "id" => "core_users",
  389. "order" => 2,
  390. "href" => $urlGenerator->linkToRoute('settings_users'),
  391. "name" => $l->t("Users"),
  392. "icon" => $urlGenerator->imagePath("settings", "users.svg")
  393. );
  394. }
  395. // if the user is an admin
  396. if (OC_User::isAdminUser(OC_User::getUser())) {
  397. // admin settings
  398. $settings[] = array(
  399. "id" => "admin",
  400. "order" => 1000,
  401. "href" => $urlGenerator->linkToRoute('settings_admin'),
  402. "name" => $l->t("Admin"),
  403. "icon" => $urlGenerator->imagePath("settings", "admin.svg")
  404. );
  405. }
  406. }
  407. $navigation = self::proceedNavigation($settings);
  408. return $navigation;
  409. }
  410. // This is private as well. It simply works, so don't ask for more details
  411. private static function proceedNavigation($list) {
  412. $activeApp = OC::$server->getNavigationManager()->getActiveEntry();
  413. foreach ($list as &$navEntry) {
  414. if ($navEntry['id'] == $activeApp) {
  415. $navEntry['active'] = true;
  416. } else {
  417. $navEntry['active'] = false;
  418. }
  419. }
  420. unset($navEntry);
  421. usort($list, create_function('$a, $b', 'if( $a["order"] == $b["order"] ) {return 0;}elseif( $a["order"] < $b["order"] ) {return -1;}else{return 1;}'));
  422. return $list;
  423. }
  424. /**
  425. * Get the path where to install apps
  426. *
  427. * @return string|false
  428. */
  429. public static function getInstallPath() {
  430. if (\OC::$server->getSystemConfig()->getValue('appstoreenabled', true) == false) {
  431. return false;
  432. }
  433. foreach (OC::$APPSROOTS as $dir) {
  434. if (isset($dir['writable']) && $dir['writable'] === true) {
  435. return $dir['path'];
  436. }
  437. }
  438. \OCP\Util::writeLog('core', 'No application directories are marked as writable.', \OCP\Util::ERROR);
  439. return null;
  440. }
  441. /**
  442. * search for an app in all app-directories
  443. *
  444. * @param string $appId
  445. * @return false|string
  446. */
  447. protected static function findAppInDirectories($appId) {
  448. $sanitizedAppId = self::cleanAppId($appId);
  449. if($sanitizedAppId !== $appId) {
  450. return false;
  451. }
  452. static $app_dir = array();
  453. if (isset($app_dir[$appId])) {
  454. return $app_dir[$appId];
  455. }
  456. $possibleApps = array();
  457. foreach (OC::$APPSROOTS as $dir) {
  458. if (file_exists($dir['path'] . '/' . $appId)) {
  459. $possibleApps[] = $dir;
  460. }
  461. }
  462. if (empty($possibleApps)) {
  463. return false;
  464. } elseif (count($possibleApps) === 1) {
  465. $dir = array_shift($possibleApps);
  466. $app_dir[$appId] = $dir;
  467. return $dir;
  468. } else {
  469. $versionToLoad = array();
  470. foreach ($possibleApps as $possibleApp) {
  471. $version = self::getAppVersionByPath($possibleApp['path']);
  472. if (empty($versionToLoad) || version_compare($version, $versionToLoad['version'], '>')) {
  473. $versionToLoad = array(
  474. 'dir' => $possibleApp,
  475. 'version' => $version,
  476. );
  477. }
  478. }
  479. $app_dir[$appId] = $versionToLoad['dir'];
  480. return $versionToLoad['dir'];
  481. //TODO - write test
  482. }
  483. }
  484. /**
  485. * Get the directory for the given app.
  486. * If the app is defined in multiple directories, the first one is taken. (false if not found)
  487. *
  488. * @param string $appId
  489. * @return string|false
  490. */
  491. public static function getAppPath($appId) {
  492. if ($appId === null || trim($appId) === '') {
  493. return false;
  494. }
  495. if (($dir = self::findAppInDirectories($appId)) != false) {
  496. return $dir['path'] . '/' . $appId;
  497. }
  498. return false;
  499. }
  500. /**
  501. * check if an app's directory is writable
  502. *
  503. * @param string $appId
  504. * @return bool
  505. */
  506. public static function isAppDirWritable($appId) {
  507. $path = self::getAppPath($appId);
  508. return ($path !== false) ? is_writable($path) : false;
  509. }
  510. /**
  511. * Get the path for the given app on the access
  512. * If the app is defined in multiple directories, the first one is taken. (false if not found)
  513. *
  514. * @param string $appId
  515. * @return string|false
  516. */
  517. public static function getAppWebPath($appId) {
  518. if (($dir = self::findAppInDirectories($appId)) != false) {
  519. return OC::$WEBROOT . $dir['url'] . '/' . $appId;
  520. }
  521. return false;
  522. }
  523. /**
  524. * get the last version of the app, either from appinfo/version or from appinfo/info.xml
  525. *
  526. * @param string $appId
  527. * @return string
  528. */
  529. public static function getAppVersion($appId) {
  530. if (!isset(self::$appVersion[$appId])) {
  531. $file = self::getAppPath($appId);
  532. self::$appVersion[$appId] = ($file !== false) ? self::getAppVersionByPath($file) : '0';
  533. }
  534. return self::$appVersion[$appId];
  535. }
  536. /**
  537. * get app's version based on it's path
  538. *
  539. * @param string $path
  540. * @return string
  541. */
  542. public static function getAppVersionByPath($path) {
  543. $versionFile = $path . '/appinfo/version';
  544. $infoFile = $path . '/appinfo/info.xml';
  545. if (is_file($versionFile)) {
  546. return trim(file_get_contents($versionFile));
  547. } else {
  548. $appData = self::getAppInfo($infoFile, true);
  549. return isset($appData['version']) ? $appData['version'] : '';
  550. }
  551. }
  552. /**
  553. * Read all app metadata from the info.xml file
  554. *
  555. * @param string $appId id of the app or the path of the info.xml file
  556. * @param boolean $path
  557. * @param string $lang
  558. * @return array|null
  559. * @note all data is read from info.xml, not just pre-defined fields
  560. */
  561. public static function getAppInfo($appId, $path = false, $lang = null) {
  562. if ($path) {
  563. $file = $appId;
  564. } else {
  565. if ($lang === null && isset(self::$appInfo[$appId])) {
  566. return self::$appInfo[$appId];
  567. }
  568. $appPath = self::getAppPath($appId);
  569. if($appPath === false) {
  570. return null;
  571. }
  572. $file = $appPath . '/appinfo/info.xml';
  573. }
  574. $parser = new \OC\App\InfoParser(\OC::$server->getHTTPHelper(), \OC::$server->getURLGenerator());
  575. $data = $parser->parse($file);
  576. if (is_array($data)) {
  577. $data = OC_App::parseAppInfo($data, $lang);
  578. }
  579. if(isset($data['ocsid'])) {
  580. $storedId = \OC::$server->getConfig()->getAppValue($appId, 'ocsid');
  581. if($storedId !== '' && $storedId !== $data['ocsid']) {
  582. $data['ocsid'] = $storedId;
  583. }
  584. }
  585. if ($lang === null) {
  586. self::$appInfo[$appId] = $data;
  587. }
  588. return $data;
  589. }
  590. /**
  591. * Returns the navigation
  592. *
  593. * @return array
  594. *
  595. * This function returns an array containing all entries added. The
  596. * entries are sorted by the key 'order' ascending. Additional to the keys
  597. * given for each app the following keys exist:
  598. * - active: boolean, signals if the user is on this navigation entry
  599. */
  600. public static function getNavigation() {
  601. $entries = OC::$server->getNavigationManager()->getAll();
  602. $navigation = self::proceedNavigation($entries);
  603. return $navigation;
  604. }
  605. /**
  606. * get the id of loaded app
  607. *
  608. * @return string
  609. */
  610. public static function getCurrentApp() {
  611. $request = \OC::$server->getRequest();
  612. $script = substr($request->getScriptName(), strlen(OC::$WEBROOT) + 1);
  613. $topFolder = substr($script, 0, strpos($script, '/'));
  614. if (empty($topFolder)) {
  615. $path_info = $request->getPathInfo();
  616. if ($path_info) {
  617. $topFolder = substr($path_info, 1, strpos($path_info, '/', 1) - 1);
  618. }
  619. }
  620. if ($topFolder == 'apps') {
  621. $length = strlen($topFolder);
  622. return substr($script, $length + 1, strpos($script, '/', $length + 1) - $length - 1);
  623. } else {
  624. return $topFolder;
  625. }
  626. }
  627. /**
  628. * @param string $type
  629. * @return array
  630. */
  631. public static function getForms($type) {
  632. $forms = array();
  633. switch ($type) {
  634. case 'admin':
  635. $source = self::$adminForms;
  636. break;
  637. case 'personal':
  638. $source = self::$personalForms;
  639. break;
  640. default:
  641. return array();
  642. }
  643. foreach ($source as $form) {
  644. $forms[] = include $form;
  645. }
  646. return $forms;
  647. }
  648. /**
  649. * register an admin form to be shown
  650. *
  651. * @param string $app
  652. * @param string $page
  653. */
  654. public static function registerAdmin($app, $page) {
  655. self::$adminForms[] = $app . '/' . $page . '.php';
  656. }
  657. /**
  658. * register a personal form to be shown
  659. * @param string $app
  660. * @param string $page
  661. */
  662. public static function registerPersonal($app, $page) {
  663. self::$personalForms[] = $app . '/' . $page . '.php';
  664. }
  665. /**
  666. * @param array $entry
  667. */
  668. public static function registerLogIn(array $entry) {
  669. self::$altLogin[] = $entry;
  670. }
  671. /**
  672. * @return array
  673. */
  674. public static function getAlternativeLogIns() {
  675. return self::$altLogin;
  676. }
  677. /**
  678. * get a list of all apps in the apps folder
  679. *
  680. * @return array an array of app names (string IDs)
  681. * @todo: change the name of this method to getInstalledApps, which is more accurate
  682. */
  683. public static function getAllApps() {
  684. $apps = array();
  685. foreach (OC::$APPSROOTS as $apps_dir) {
  686. if (!is_readable($apps_dir['path'])) {
  687. \OCP\Util::writeLog('core', 'unable to read app folder : ' . $apps_dir['path'], \OCP\Util::WARN);
  688. continue;
  689. }
  690. $dh = opendir($apps_dir['path']);
  691. if (is_resource($dh)) {
  692. while (($file = readdir($dh)) !== false) {
  693. if ($file[0] != '.' and is_dir($apps_dir['path'] . '/' . $file) and is_file($apps_dir['path'] . '/' . $file . '/appinfo/info.xml')) {
  694. $apps[] = $file;
  695. }
  696. }
  697. }
  698. }
  699. return $apps;
  700. }
  701. /**
  702. * List all apps, this is used in apps.php
  703. *
  704. * @param bool $onlyLocal
  705. * @param bool $includeUpdateInfo Should we check whether there is an update
  706. * in the app store?
  707. * @param OCSClient $ocsClient
  708. * @return array
  709. */
  710. public static function listAllApps($onlyLocal = false,
  711. $includeUpdateInfo = true,
  712. OCSClient $ocsClient) {
  713. $installedApps = OC_App::getAllApps();
  714. //TODO which apps do we want to blacklist and how do we integrate
  715. // blacklisting with the multi apps folder feature?
  716. //we don't want to show configuration for these
  717. $blacklist = \OC::$server->getAppManager()->getAlwaysEnabledApps();
  718. $appList = array();
  719. $langCode = \OC::$server->getL10N('core')->getLanguageCode();
  720. foreach ($installedApps as $app) {
  721. if (array_search($app, $blacklist) === false) {
  722. $info = OC_App::getAppInfo($app, false, $langCode);
  723. if (!is_array($info)) {
  724. \OCP\Util::writeLog('core', 'Could not read app info file for app "' . $app . '"', \OCP\Util::ERROR);
  725. continue;
  726. }
  727. if (!isset($info['name'])) {
  728. \OCP\Util::writeLog('core', 'App id "' . $app . '" has no name in appinfo', \OCP\Util::ERROR);
  729. continue;
  730. }
  731. $enabled = \OC::$server->getAppConfig()->getValue($app, 'enabled', 'no');
  732. $info['groups'] = null;
  733. if ($enabled === 'yes') {
  734. $active = true;
  735. } else if ($enabled === 'no') {
  736. $active = false;
  737. } else {
  738. $active = true;
  739. $info['groups'] = $enabled;
  740. }
  741. $info['active'] = $active;
  742. if (self::isShipped($app)) {
  743. $info['internal'] = true;
  744. $info['level'] = self::officialApp;
  745. $info['removable'] = false;
  746. } else {
  747. $info['internal'] = false;
  748. $info['removable'] = true;
  749. }
  750. $info['update'] = ($includeUpdateInfo) ? OC_Installer::isUpdateAvailable($app) : null;
  751. $appPath = self::getAppPath($app);
  752. if($appPath !== false) {
  753. $appIcon = $appPath . '/img/' . $app . '.svg';
  754. if (file_exists($appIcon)) {
  755. $info['preview'] = \OC::$server->getURLGenerator()->imagePath($app, $app . '.svg');
  756. $info['previewAsIcon'] = true;
  757. } else {
  758. $appIcon = $appPath . '/img/app.svg';
  759. if (file_exists($appIcon)) {
  760. $info['preview'] = \OC::$server->getURLGenerator()->imagePath($app, 'app.svg');
  761. $info['previewAsIcon'] = true;
  762. }
  763. }
  764. }
  765. $info['version'] = OC_App::getAppVersion($app);
  766. $appList[] = $info;
  767. }
  768. }
  769. if ($onlyLocal) {
  770. $remoteApps = [];
  771. } else {
  772. $remoteApps = OC_App::getAppstoreApps('approved', null, $ocsClient);
  773. }
  774. if ($remoteApps) {
  775. // Remove duplicates
  776. foreach ($appList as $app) {
  777. foreach ($remoteApps AS $key => $remote) {
  778. if ($app['name'] === $remote['name'] ||
  779. (isset($app['ocsid']) &&
  780. $app['ocsid'] === $remote['id'])
  781. ) {
  782. unset($remoteApps[$key]);
  783. }
  784. }
  785. }
  786. $combinedApps = array_merge($appList, $remoteApps);
  787. } else {
  788. $combinedApps = $appList;
  789. }
  790. return $combinedApps;
  791. }
  792. /**
  793. * Returns the internal app ID or false
  794. * @param string $ocsID
  795. * @return string|false
  796. */
  797. public static function getInternalAppIdByOcs($ocsID) {
  798. if(is_numeric($ocsID)) {
  799. $idArray = \OC::$server->getAppConfig()->getValues(false, 'ocsid');
  800. if(array_search($ocsID, $idArray)) {
  801. return array_search($ocsID, $idArray);
  802. }
  803. }
  804. return false;
  805. }
  806. /**
  807. * Get a list of all apps on the appstore
  808. * @param string $filter
  809. * @param string|null $category
  810. * @param OCSClient $ocsClient
  811. * @return array|bool multi-dimensional array of apps.
  812. * Keys: id, name, type, typename, personid, license, detailpage, preview, changed, description
  813. */
  814. public static function getAppstoreApps($filter = 'approved',
  815. $category = null,
  816. OCSClient $ocsClient) {
  817. $categories = [$category];
  818. if (is_null($category)) {
  819. $categoryNames = $ocsClient->getCategories(\OCP\Util::getVersion());
  820. if (is_array($categoryNames)) {
  821. // Check that categories of apps were retrieved correctly
  822. if (!$categories = array_keys($categoryNames)) {
  823. return false;
  824. }
  825. } else {
  826. return false;
  827. }
  828. }
  829. $page = 0;
  830. $remoteApps = $ocsClient->getApplications($categories, $page, $filter, \OCP\Util::getVersion());
  831. $apps = [];
  832. $i = 0;
  833. $l = \OC::$server->getL10N('core');
  834. foreach ($remoteApps as $app) {
  835. $potentialCleanId = self::getInternalAppIdByOcs($app['id']);
  836. // enhance app info (for example the description)
  837. $apps[$i] = OC_App::parseAppInfo($app);
  838. $apps[$i]['author'] = $app['personid'];
  839. $apps[$i]['ocs_id'] = $app['id'];
  840. $apps[$i]['internal'] = 0;
  841. $apps[$i]['active'] = ($potentialCleanId !== false) ? self::isEnabled($potentialCleanId) : false;
  842. $apps[$i]['update'] = false;
  843. $apps[$i]['groups'] = false;
  844. $apps[$i]['score'] = $app['score'];
  845. $apps[$i]['removable'] = false;
  846. if ($app['label'] == 'recommended') {
  847. $apps[$i]['internallabel'] = (string)$l->t('Recommended');
  848. $apps[$i]['internalclass'] = 'recommendedapp';
  849. }
  850. // Apps from the appstore are always assumed to be compatible with the
  851. // the current release as the initial filtering is done on the appstore
  852. $apps[$i]['dependencies']['owncloud']['@attributes']['min-version'] = implode('.', \OCP\Util::getVersion());
  853. $apps[$i]['dependencies']['owncloud']['@attributes']['max-version'] = implode('.', \OCP\Util::getVersion());
  854. $i++;
  855. }
  856. if (empty($apps)) {
  857. return false;
  858. } else {
  859. return $apps;
  860. }
  861. }
  862. public static function shouldUpgrade($app) {
  863. $versions = self::getAppVersions();
  864. $currentVersion = OC_App::getAppVersion($app);
  865. if ($currentVersion && isset($versions[$app])) {
  866. $installedVersion = $versions[$app];
  867. if (version_compare($currentVersion, $installedVersion, '>')) {
  868. return true;
  869. }
  870. }
  871. return false;
  872. }
  873. /**
  874. * Adjust the number of version parts of $version1 to match
  875. * the number of version parts of $version2.
  876. *
  877. * @param string $version1 version to adjust
  878. * @param string $version2 version to take the number of parts from
  879. * @return string shortened $version1
  880. */
  881. private static function adjustVersionParts($version1, $version2) {
  882. $version1 = explode('.', $version1);
  883. $version2 = explode('.', $version2);
  884. // reduce $version1 to match the number of parts in $version2
  885. while (count($version1) > count($version2)) {
  886. array_pop($version1);
  887. }
  888. // if $version1 does not have enough parts, add some
  889. while (count($version1) < count($version2)) {
  890. $version1[] = '0';
  891. }
  892. return implode('.', $version1);
  893. }
  894. /**
  895. * Check whether the current ownCloud version matches the given
  896. * application's version requirements.
  897. *
  898. * The comparison is made based on the number of parts that the
  899. * app info version has. For example for ownCloud 6.0.3 if the
  900. * app info version is expecting version 6.0, the comparison is
  901. * made on the first two parts of the ownCloud version.
  902. * This means that it's possible to specify "requiremin" => 6
  903. * and "requiremax" => 6 and it will still match ownCloud 6.0.3.
  904. *
  905. * @param string $ocVersion ownCloud version to check against
  906. * @param array $appInfo app info (from xml)
  907. *
  908. * @return boolean true if compatible, otherwise false
  909. */
  910. public static function isAppCompatible($ocVersion, $appInfo) {
  911. $requireMin = '';
  912. $requireMax = '';
  913. if (isset($appInfo['dependencies']['owncloud']['@attributes']['min-version'])) {
  914. $requireMin = $appInfo['dependencies']['owncloud']['@attributes']['min-version'];
  915. } else if (isset($appInfo['requiremin'])) {
  916. $requireMin = $appInfo['requiremin'];
  917. } else if (isset($appInfo['require'])) {
  918. $requireMin = $appInfo['require'];
  919. }
  920. if (isset($appInfo['dependencies']['owncloud']['@attributes']['max-version'])) {
  921. $requireMax = $appInfo['dependencies']['owncloud']['@attributes']['max-version'];
  922. } else if (isset($appInfo['requiremax'])) {
  923. $requireMax = $appInfo['requiremax'];
  924. }
  925. if (is_array($ocVersion)) {
  926. $ocVersion = implode('.', $ocVersion);
  927. }
  928. if (!empty($requireMin)
  929. && version_compare(self::adjustVersionParts($ocVersion, $requireMin), $requireMin, '<')
  930. ) {
  931. return false;
  932. }
  933. if (!empty($requireMax)
  934. && version_compare(self::adjustVersionParts($ocVersion, $requireMax), $requireMax, '>')
  935. ) {
  936. return false;
  937. }
  938. return true;
  939. }
  940. /**
  941. * get the installed version of all apps
  942. */
  943. public static function getAppVersions() {
  944. static $versions;
  945. if(!$versions) {
  946. $appConfig = \OC::$server->getAppConfig();
  947. $versions = $appConfig->getValues(false, 'installed_version');
  948. }
  949. return $versions;
  950. }
  951. /**
  952. * @param string $app
  953. * @return bool
  954. * @throws Exception if app is not compatible with this version of ownCloud
  955. * @throws Exception if no app-name was specified
  956. */
  957. public static function installApp($app) {
  958. $l = \OC::$server->getL10N('core');
  959. $config = \OC::$server->getConfig();
  960. $ocsClient = new OCSClient(
  961. \OC::$server->getHTTPClientService(),
  962. $config,
  963. \OC::$server->getLogger()
  964. );
  965. $appData = $ocsClient->getApplication($app, \OCP\Util::getVersion());
  966. // check if app is a shipped app or not. OCS apps have an integer as id, shipped apps use a string
  967. if (!is_numeric($app)) {
  968. $shippedVersion = self::getAppVersion($app);
  969. if ($appData && version_compare($shippedVersion, $appData['version'], '<')) {
  970. $app = self::downloadApp($app);
  971. } else {
  972. $app = OC_Installer::installShippedApp($app);
  973. }
  974. } else {
  975. // Maybe the app is already installed - compare the version in this
  976. // case and use the local already installed one.
  977. // FIXME: This is a horrible hack. I feel sad. The god of code cleanness may forgive me.
  978. $internalAppId = self::getInternalAppIdByOcs($app);
  979. if($internalAppId !== false) {
  980. if($appData && version_compare(\OC_App::getAppVersion($internalAppId), $appData['version'], '<')) {
  981. $app = self::downloadApp($app);
  982. } else {
  983. self::enable($internalAppId);
  984. $app = $internalAppId;
  985. }
  986. } else {
  987. $app = self::downloadApp($app);
  988. }
  989. }
  990. if ($app !== false) {
  991. // check if the app is compatible with this version of ownCloud
  992. $info = self::getAppInfo($app);
  993. if(!is_array($info)) {
  994. throw new \Exception(
  995. $l->t('App "%s" cannot be installed because appinfo file cannot be read.',
  996. [$info['name']]
  997. )
  998. );
  999. }
  1000. $version = \OCP\Util::getVersion();
  1001. if (!self::isAppCompatible($version, $info)) {
  1002. throw new \Exception(
  1003. $l->t('App "%s" cannot be installed because it is not compatible with this version of Nextcloud.',
  1004. array($info['name'])
  1005. )
  1006. );
  1007. }
  1008. // check for required dependencies
  1009. $dependencyAnalyzer = new DependencyAnalyzer(new Platform($config), $l);
  1010. $missing = $dependencyAnalyzer->analyze($info);
  1011. if (!empty($missing)) {
  1012. $missingMsg = join(PHP_EOL, $missing);
  1013. throw new \Exception(
  1014. $l->t('App "%s" cannot be installed because the following dependencies are not fulfilled: %s',
  1015. array($info['name'], $missingMsg)
  1016. )
  1017. );
  1018. }
  1019. $config->setAppValue($app, 'enabled', 'yes');
  1020. if (isset($appData['id'])) {
  1021. $config->setAppValue($app, 'ocsid', $appData['id']);
  1022. }
  1023. \OC_Hook::emit('OC_App', 'post_enable', array('app' => $app));
  1024. } else {
  1025. throw new \Exception($l->t("No app name specified"));
  1026. }
  1027. return $app;
  1028. }
  1029. /**
  1030. * update the database for the app and call the update script
  1031. *
  1032. * @param string $appId
  1033. * @return bool
  1034. */
  1035. public static function updateApp($appId) {
  1036. $appPath = self::getAppPath($appId);
  1037. if($appPath === false) {
  1038. return false;
  1039. }
  1040. if (file_exists($appPath . '/appinfo/database.xml')) {
  1041. OC_DB::updateDbFromStructure($appPath . '/appinfo/database.xml');
  1042. }
  1043. unset(self::$appVersion[$appId]);
  1044. // run upgrade code
  1045. if (file_exists($appPath . '/appinfo/update.php')) {
  1046. self::loadApp($appId, false);
  1047. include $appPath . '/appinfo/update.php';
  1048. }
  1049. //set remote/public handlers
  1050. $appData = self::getAppInfo($appId);
  1051. if (array_key_exists('ocsid', $appData)) {
  1052. \OC::$server->getConfig()->setAppValue($appId, 'ocsid', $appData['ocsid']);
  1053. } elseif(\OC::$server->getConfig()->getAppValue($appId, 'ocsid', null) !== null) {
  1054. \OC::$server->getConfig()->deleteAppValue($appId, 'ocsid');
  1055. }
  1056. foreach ($appData['remote'] as $name => $path) {
  1057. \OC::$server->getConfig()->setAppValue('core', 'remote_' . $name, $appId . '/' . $path);
  1058. }
  1059. foreach ($appData['public'] as $name => $path) {
  1060. \OC::$server->getConfig()->setAppValue('core', 'public_' . $name, $appId . '/' . $path);
  1061. }
  1062. self::setAppTypes($appId);
  1063. $version = \OC_App::getAppVersion($appId);
  1064. \OC::$server->getAppConfig()->setValue($appId, 'installed_version', $version);
  1065. return true;
  1066. }
  1067. /**
  1068. * @param string $appId
  1069. * @return \OC\Files\View|false
  1070. */
  1071. public static function getStorage($appId) {
  1072. if (OC_App::isEnabled($appId)) { //sanity check
  1073. if (OC_User::isLoggedIn()) {
  1074. $view = new \OC\Files\View('/' . OC_User::getUser());
  1075. if (!$view->file_exists($appId)) {
  1076. $view->mkdir($appId);
  1077. }
  1078. return new \OC\Files\View('/' . OC_User::getUser() . '/' . $appId);
  1079. } else {
  1080. \OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ', user not logged in', \OCP\Util::ERROR);
  1081. return false;
  1082. }
  1083. } else {
  1084. \OCP\Util::writeLog('core', 'Can\'t get app storage, app ' . $appId . ' not enabled', \OCP\Util::ERROR);
  1085. return false;
  1086. }
  1087. }
  1088. protected static function findBestL10NOption($options, $lang) {
  1089. $fallback = $similarLangFallback = $englishFallback = false;
  1090. $lang = strtolower($lang);
  1091. $similarLang = $lang;
  1092. if (strpos($similarLang, '_')) {
  1093. // For "de_DE" we want to find "de" and the other way around
  1094. $similarLang = substr($lang, 0, strpos($lang, '_'));
  1095. }
  1096. foreach ($options as $option) {
  1097. if (is_array($option)) {
  1098. if ($fallback === false) {
  1099. $fallback = $option['@value'];
  1100. }
  1101. if (!isset($option['@attributes']['lang'])) {
  1102. continue;
  1103. }
  1104. $attributeLang = strtolower($option['@attributes']['lang']);
  1105. if ($attributeLang === $lang) {
  1106. return $option['@value'];
  1107. }
  1108. if ($attributeLang === $similarLang) {
  1109. $similarLangFallback = $option['@value'];
  1110. } else if (strpos($attributeLang, $similarLang . '_') === 0) {
  1111. if ($similarLangFallback === false) {
  1112. $similarLangFallback = $option['@value'];
  1113. }
  1114. }
  1115. } else {
  1116. $englishFallback = $option;
  1117. }
  1118. }
  1119. if ($similarLangFallback !== false) {
  1120. return $similarLangFallback;
  1121. } else if ($englishFallback !== false) {
  1122. return $englishFallback;
  1123. }
  1124. return (string) $fallback;
  1125. }
  1126. /**
  1127. * parses the app data array and enhanced the 'description' value
  1128. *
  1129. * @param array $data the app data
  1130. * @param string $lang
  1131. * @return array improved app data
  1132. */
  1133. public static function parseAppInfo(array $data, $lang = null) {
  1134. if ($lang && isset($data['name']) && is_array($data['name'])) {
  1135. $data['name'] = self::findBestL10NOption($data['name'], $lang);
  1136. }
  1137. if ($lang && isset($data['summary']) && is_array($data['summary'])) {
  1138. $data['summary'] = self::findBestL10NOption($data['summary'], $lang);
  1139. }
  1140. if ($lang && isset($data['description']) && is_array($data['description'])) {
  1141. $data['description'] = self::findBestL10NOption($data['description'], $lang);
  1142. }
  1143. // just modify the description if it is available
  1144. // otherwise this will create a $data element with an empty 'description'
  1145. if (isset($data['description'])) {
  1146. if (is_string($data['description'])) {
  1147. // sometimes the description contains line breaks and they are then also
  1148. // shown in this way in the app management which isn't wanted as HTML
  1149. // manages line breaks itself
  1150. // first of all we split on empty lines
  1151. $paragraphs = preg_split("!\n[[:space:]]*\n!mu", $data['description']);
  1152. $result = [];
  1153. foreach ($paragraphs as $value) {
  1154. // replace multiple whitespace (tabs, space, newlines) inside a paragraph
  1155. // with a single space - also trims whitespace
  1156. $result[] = trim(preg_replace('![[:space:]]+!mu', ' ', $value));
  1157. }
  1158. // join the single paragraphs with a empty line in between
  1159. $data['description'] = implode("\n\n", $result);
  1160. } else {
  1161. $data['description'] = '';
  1162. }
  1163. }
  1164. return $data;
  1165. }
  1166. }