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

619 lines
18 KiB

9 years ago
9 years ago
9 years ago
12 years ago
12 years ago
12 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
13 years ago
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. * @copyright Copyright (c) 2016, Lukas Reschke <lukas@statuscode.ch>
  5. *
  6. * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
  7. * @author Brice Maron <brice@bmaron.net>
  8. * @author Christoph Wurst <christoph@owncloud.com>
  9. * @author Frank Karlitschek <frank@karlitschek.de>
  10. * @author Georg Ehrke <oc.list@georgehrke.com>
  11. * @author Joas Schilling <coding@schilljs.com>
  12. * @author Kamil Domanski <kdomanski@kdemail.net>
  13. * @author Lukas Reschke <lukas@statuscode.ch>
  14. * @author Morris Jobke <hey@morrisjobke.de>
  15. * @author Robin Appelman <robin@icewind.nl>
  16. * @author root "root@oc.(none)"
  17. * @author Thomas Müller <thomas.mueller@tmit.eu>
  18. * @author Thomas Tanghus <thomas@tanghus.net>
  19. *
  20. * @license AGPL-3.0
  21. *
  22. * This code is free software: you can redistribute it and/or modify
  23. * it under the terms of the GNU Affero General Public License, version 3,
  24. * as published by the Free Software Foundation.
  25. *
  26. * This program is distributed in the hope that it will be useful,
  27. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  28. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  29. * GNU Affero General Public License for more details.
  30. *
  31. * You should have received a copy of the GNU Affero General Public License, version 3,
  32. * along with this program. If not, see <http://www.gnu.org/licenses/>
  33. *
  34. */
  35. namespace OC;
  36. use Doctrine\DBAL\Exception\TableExistsException;
  37. use OC\App\AppManager;
  38. use OC\App\AppStore\Bundles\Bundle;
  39. use OC\App\AppStore\Fetcher\AppFetcher;
  40. use OC\App\CodeChecker\CodeChecker;
  41. use OC\App\CodeChecker\EmptyCheck;
  42. use OC\App\CodeChecker\PrivateCheck;
  43. use OC\Archive\TAR;
  44. use OC_App;
  45. use OC_DB;
  46. use OC_Helper;
  47. use OCP\App\IAppManager;
  48. use OCP\Http\Client\IClientService;
  49. use OCP\IConfig;
  50. use OCP\ILogger;
  51. use OCP\ITempManager;
  52. use phpseclib\File\X509;
  53. /**
  54. * This class provides the functionality needed to install, update and remove apps
  55. */
  56. class Installer {
  57. /** @var AppFetcher */
  58. private $appFetcher;
  59. /** @var IClientService */
  60. private $clientService;
  61. /** @var ITempManager */
  62. private $tempManager;
  63. /** @var ILogger */
  64. private $logger;
  65. /** @var IConfig */
  66. private $config;
  67. /** @var array - for caching the result of app fetcher */
  68. private $apps = null;
  69. /** @var bool|null - for caching the result of the ready status */
  70. private $isInstanceReadyForUpdates = null;
  71. /**
  72. * @param AppFetcher $appFetcher
  73. * @param IClientService $clientService
  74. * @param ITempManager $tempManager
  75. * @param ILogger $logger
  76. * @param IConfig $config
  77. */
  78. public function __construct(AppFetcher $appFetcher,
  79. IClientService $clientService,
  80. ITempManager $tempManager,
  81. ILogger $logger,
  82. IConfig $config) {
  83. $this->appFetcher = $appFetcher;
  84. $this->clientService = $clientService;
  85. $this->tempManager = $tempManager;
  86. $this->logger = $logger;
  87. $this->config = $config;
  88. }
  89. /**
  90. * Installs an app that is located in one of the app folders already
  91. *
  92. * @param string $appId App to install
  93. * @throws \Exception
  94. * @return string app ID
  95. */
  96. public function installApp($appId) {
  97. $app = \OC_App::findAppInDirectories($appId);
  98. if($app === false) {
  99. throw new \Exception('App not found in any app directory');
  100. }
  101. $basedir = $app['path'].'/'.$appId;
  102. $info = OC_App::getAppInfo($basedir.'/appinfo/info.xml', true);
  103. $l = \OC::$server->getL10N('core');
  104. if(!is_array($info)) {
  105. throw new \Exception(
  106. $l->t('App "%s" cannot be installed because appinfo file cannot be read.',
  107. [$info['name']]
  108. )
  109. );
  110. }
  111. $version = \OCP\Util::getVersion();
  112. if (!\OC_App::isAppCompatible($version, $info)) {
  113. throw new \Exception(
  114. // TODO $l
  115. $l->t('App "%s" cannot be installed because it is not compatible with this version of the server.',
  116. [$info['name']]
  117. )
  118. );
  119. }
  120. // check for required dependencies
  121. \OC_App::checkAppDependencies($this->config, $l, $info);
  122. \OC_App::registerAutoloading($appId, $basedir);
  123. //install the database
  124. if(is_file($basedir.'/appinfo/database.xml')) {
  125. if (\OC::$server->getConfig()->getAppValue($info['id'], 'installed_version') === null) {
  126. OC_DB::createDbFromStructure($basedir.'/appinfo/database.xml');
  127. } else {
  128. OC_DB::updateDbFromStructure($basedir.'/appinfo/database.xml');
  129. }
  130. } else {
  131. $ms = new \OC\DB\MigrationService($info['id'], \OC::$server->getDatabaseConnection());
  132. $ms->migrate();
  133. }
  134. \OC_App::setupBackgroundJobs($info['background-jobs']);
  135. //run appinfo/install.php
  136. if(!isset($data['noinstall']) or $data['noinstall']==false) {
  137. self::includeAppScript($basedir . '/appinfo/install.php');
  138. }
  139. $appData = OC_App::getAppInfo($appId);
  140. OC_App::executeRepairSteps($appId, $appData['repair-steps']['install']);
  141. //set the installed version
  142. \OC::$server->getConfig()->setAppValue($info['id'], 'installed_version', OC_App::getAppVersion($info['id'], false));
  143. \OC::$server->getConfig()->setAppValue($info['id'], 'enabled', 'no');
  144. //set remote/public handlers
  145. foreach($info['remote'] as $name=>$path) {
  146. \OC::$server->getConfig()->setAppValue('core', 'remote_'.$name, $info['id'].'/'.$path);
  147. }
  148. foreach($info['public'] as $name=>$path) {
  149. \OC::$server->getConfig()->setAppValue('core', 'public_'.$name, $info['id'].'/'.$path);
  150. }
  151. OC_App::setAppTypes($info['id']);
  152. return $info['id'];
  153. }
  154. /**
  155. * @brief checks whether or not an app is installed
  156. * @param string $app app
  157. * @returns bool
  158. *
  159. * Checks whether or not an app is installed, i.e. registered in apps table.
  160. */
  161. public static function isInstalled( $app ) {
  162. return (\OC::$server->getConfig()->getAppValue($app, "installed_version", null) !== null);
  163. }
  164. /**
  165. * Updates the specified app from the appstore
  166. *
  167. * @param string $appId
  168. * @return bool
  169. */
  170. public function updateAppstoreApp($appId) {
  171. if($this->isUpdateAvailable($appId)) {
  172. try {
  173. $this->downloadApp($appId);
  174. } catch (\Exception $e) {
  175. $this->logger->logException($e, [
  176. 'level' => \OCP\Util::ERROR,
  177. 'app' => 'core',
  178. ]);
  179. return false;
  180. }
  181. return OC_App::updateApp($appId);
  182. }
  183. return false;
  184. }
  185. /**
  186. * Downloads an app and puts it into the app directory
  187. *
  188. * @param string $appId
  189. *
  190. * @throws \Exception If the installation was not successful
  191. */
  192. public function downloadApp($appId) {
  193. $appId = strtolower($appId);
  194. $apps = $this->appFetcher->get();
  195. foreach($apps as $app) {
  196. if($app['id'] === $appId) {
  197. // Load the certificate
  198. $certificate = new X509();
  199. $certificate->loadCA(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt'));
  200. $loadedCertificate = $certificate->loadX509($app['certificate']);
  201. // Verify if the certificate has been revoked
  202. $crl = new X509();
  203. $crl->loadCA(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crt'));
  204. $crl->loadCRL(file_get_contents(__DIR__ . '/../../resources/codesigning/root.crl'));
  205. if($crl->validateSignature() !== true) {
  206. throw new \Exception('Could not validate CRL signature');
  207. }
  208. $csn = $loadedCertificate['tbsCertificate']['serialNumber']->toString();
  209. $revoked = $crl->getRevoked($csn);
  210. if ($revoked !== false) {
  211. throw new \Exception(
  212. sprintf(
  213. 'Certificate "%s" has been revoked',
  214. $csn
  215. )
  216. );
  217. }
  218. // Verify if the certificate has been issued by the Nextcloud Code Authority CA
  219. if($certificate->validateSignature() !== true) {
  220. throw new \Exception(
  221. sprintf(
  222. 'App with id %s has a certificate not issued by a trusted Code Signing Authority',
  223. $appId
  224. )
  225. );
  226. }
  227. // Verify if the certificate is issued for the requested app id
  228. $certInfo = openssl_x509_parse($app['certificate']);
  229. if(!isset($certInfo['subject']['CN'])) {
  230. throw new \Exception(
  231. sprintf(
  232. 'App with id %s has a cert with no CN',
  233. $appId
  234. )
  235. );
  236. }
  237. if($certInfo['subject']['CN'] !== $appId) {
  238. throw new \Exception(
  239. sprintf(
  240. 'App with id %s has a cert issued to %s',
  241. $appId,
  242. $certInfo['subject']['CN']
  243. )
  244. );
  245. }
  246. // Download the release
  247. $tempFile = $this->tempManager->getTemporaryFile('.tar.gz');
  248. $client = $this->clientService->newClient();
  249. $client->get($app['releases'][0]['download'], ['save_to' => $tempFile]);
  250. // Check if the signature actually matches the downloaded content
  251. $certificate = openssl_get_publickey($app['certificate']);
  252. $verified = (bool)openssl_verify(file_get_contents($tempFile), base64_decode($app['releases'][0]['signature']), $certificate, OPENSSL_ALGO_SHA512);
  253. openssl_free_key($certificate);
  254. if($verified === true) {
  255. // Seems to match, let's proceed
  256. $extractDir = $this->tempManager->getTemporaryFolder();
  257. $archive = new TAR($tempFile);
  258. if($archive) {
  259. if (!$archive->extract($extractDir)) {
  260. throw new \Exception(
  261. sprintf(
  262. 'Could not extract app %s',
  263. $appId
  264. )
  265. );
  266. }
  267. $allFiles = scandir($extractDir);
  268. $folders = array_diff($allFiles, ['.', '..']);
  269. $folders = array_values($folders);
  270. if(count($folders) > 1) {
  271. throw new \Exception(
  272. sprintf(
  273. 'Extracted app %s has more than 1 folder',
  274. $appId
  275. )
  276. );
  277. }
  278. // Check if appinfo/info.xml has the same app ID as well
  279. $loadEntities = libxml_disable_entity_loader(false);
  280. $xml = simplexml_load_file($extractDir . '/' . $folders[0] . '/appinfo/info.xml');
  281. libxml_disable_entity_loader($loadEntities);
  282. if((string)$xml->id !== $appId) {
  283. throw new \Exception(
  284. sprintf(
  285. 'App for id %s has a wrong app ID in info.xml: %s',
  286. $appId,
  287. (string)$xml->id
  288. )
  289. );
  290. }
  291. // Check if the version is lower than before
  292. $currentVersion = OC_App::getAppVersion($appId);
  293. $newVersion = (string)$xml->version;
  294. if(version_compare($currentVersion, $newVersion) === 1) {
  295. throw new \Exception(
  296. sprintf(
  297. 'App for id %s has version %s and tried to update to lower version %s',
  298. $appId,
  299. $currentVersion,
  300. $newVersion
  301. )
  302. );
  303. }
  304. $baseDir = OC_App::getInstallPath() . '/' . $appId;
  305. // Remove old app with the ID if existent
  306. OC_Helper::rmdirr($baseDir);
  307. // Move to app folder
  308. if(@mkdir($baseDir)) {
  309. $extractDir .= '/' . $folders[0];
  310. OC_Helper::copyr($extractDir, $baseDir);
  311. }
  312. OC_Helper::copyr($extractDir, $baseDir);
  313. OC_Helper::rmdirr($extractDir);
  314. return;
  315. } else {
  316. throw new \Exception(
  317. sprintf(
  318. 'Could not extract app with ID %s to %s',
  319. $appId,
  320. $extractDir
  321. )
  322. );
  323. }
  324. } else {
  325. // Signature does not match
  326. throw new \Exception(
  327. sprintf(
  328. 'App with id %s has invalid signature',
  329. $appId
  330. )
  331. );
  332. }
  333. }
  334. }
  335. throw new \Exception(
  336. sprintf(
  337. 'Could not download app %s',
  338. $appId
  339. )
  340. );
  341. }
  342. /**
  343. * Check if an update for the app is available
  344. *
  345. * @param string $appId
  346. * @return string|false false or the version number of the update
  347. */
  348. public function isUpdateAvailable($appId) {
  349. if ($this->isInstanceReadyForUpdates === null) {
  350. $installPath = OC_App::getInstallPath();
  351. if ($installPath === false || $installPath === null) {
  352. $this->isInstanceReadyForUpdates = false;
  353. } else {
  354. $this->isInstanceReadyForUpdates = true;
  355. }
  356. }
  357. if ($this->isInstanceReadyForUpdates === false) {
  358. return false;
  359. }
  360. if ($this->isInstalledFromGit($appId) === true) {
  361. return false;
  362. }
  363. if ($this->apps === null) {
  364. $this->apps = $this->appFetcher->get();
  365. }
  366. foreach($this->apps as $app) {
  367. if($app['id'] === $appId) {
  368. $currentVersion = OC_App::getAppVersion($appId);
  369. $newestVersion = $app['releases'][0]['version'];
  370. if (version_compare($newestVersion, $currentVersion, '>')) {
  371. return $newestVersion;
  372. } else {
  373. return false;
  374. }
  375. }
  376. }
  377. return false;
  378. }
  379. /**
  380. * Check if app has been installed from git
  381. * @param string $name name of the application to remove
  382. * @return boolean
  383. *
  384. * The function will check if the path contains a .git folder
  385. */
  386. private function isInstalledFromGit($appId) {
  387. $app = \OC_App::findAppInDirectories($appId);
  388. if($app === false) {
  389. return false;
  390. }
  391. $basedir = $app['path'].'/'.$appId;
  392. return file_exists($basedir.'/.git/');
  393. }
  394. /**
  395. * Check if app is already downloaded
  396. * @param string $name name of the application to remove
  397. * @return boolean
  398. *
  399. * The function will check if the app is already downloaded in the apps repository
  400. */
  401. public function isDownloaded($name) {
  402. foreach(\OC::$APPSROOTS as $dir) {
  403. $dirToTest = $dir['path'];
  404. $dirToTest .= '/';
  405. $dirToTest .= $name;
  406. $dirToTest .= '/';
  407. if (is_dir($dirToTest)) {
  408. return true;
  409. }
  410. }
  411. return false;
  412. }
  413. /**
  414. * Removes an app
  415. * @param string $appId ID of the application to remove
  416. * @return boolean
  417. *
  418. *
  419. * This function works as follows
  420. * -# call uninstall repair steps
  421. * -# removing the files
  422. *
  423. * The function will not delete preferences, tables and the configuration,
  424. * this has to be done by the function oc_app_uninstall().
  425. */
  426. public function removeApp($appId) {
  427. if($this->isDownloaded( $appId )) {
  428. if (\OC::$server->getAppManager()->isShipped($appId)) {
  429. return false;
  430. }
  431. $appDir = OC_App::getInstallPath() . '/' . $appId;
  432. OC_Helper::rmdirr($appDir);
  433. return true;
  434. }else{
  435. \OCP\Util::writeLog('core', 'can\'t remove app '.$appId.'. It is not installed.', \OCP\Util::ERROR);
  436. return false;
  437. }
  438. }
  439. /**
  440. * Installs the app within the bundle and marks the bundle as installed
  441. *
  442. * @param Bundle $bundle
  443. * @throws \Exception If app could not get installed
  444. */
  445. public function installAppBundle(Bundle $bundle) {
  446. $appIds = $bundle->getAppIdentifiers();
  447. foreach($appIds as $appId) {
  448. if(!$this->isDownloaded($appId)) {
  449. $this->downloadApp($appId);
  450. }
  451. $this->installApp($appId);
  452. $app = new OC_App();
  453. $app->enable($appId);
  454. }
  455. $bundles = json_decode($this->config->getAppValue('core', 'installed.bundles', json_encode([])), true);
  456. $bundles[] = $bundle->getIdentifier();
  457. $this->config->setAppValue('core', 'installed.bundles', json_encode($bundles));
  458. }
  459. /**
  460. * Installs shipped apps
  461. *
  462. * This function installs all apps found in the 'apps' directory that should be enabled by default;
  463. * @param bool $softErrors When updating we ignore errors and simply log them, better to have a
  464. * working ownCloud at the end instead of an aborted update.
  465. * @return array Array of error messages (appid => Exception)
  466. */
  467. public static function installShippedApps($softErrors = false) {
  468. $errors = [];
  469. foreach(\OC::$APPSROOTS as $app_dir) {
  470. if($dir = opendir( $app_dir['path'] )) {
  471. while( false !== ( $filename = readdir( $dir ))) {
  472. if( $filename[0] !== '.' and is_dir($app_dir['path']."/$filename") ) {
  473. if( file_exists( $app_dir['path']."/$filename/appinfo/info.xml" )) {
  474. if(!Installer::isInstalled($filename)) {
  475. $info=OC_App::getAppInfo($filename);
  476. $enabled = isset($info['default_enable']);
  477. if (($enabled || in_array($filename, \OC::$server->getAppManager()->getAlwaysEnabledApps()))
  478. && \OC::$server->getConfig()->getAppValue($filename, 'enabled') !== 'no') {
  479. if ($softErrors) {
  480. try {
  481. Installer::installShippedApp($filename);
  482. } catch (HintException $e) {
  483. if ($e->getPrevious() instanceof TableExistsException) {
  484. $errors[$filename] = $e;
  485. continue;
  486. }
  487. throw $e;
  488. }
  489. } else {
  490. Installer::installShippedApp($filename);
  491. }
  492. \OC::$server->getConfig()->setAppValue($filename, 'enabled', 'yes');
  493. }
  494. }
  495. }
  496. }
  497. }
  498. closedir( $dir );
  499. }
  500. }
  501. return $errors;
  502. }
  503. /**
  504. * install an app already placed in the app folder
  505. * @param string $app id of the app to install
  506. * @return integer
  507. */
  508. public static function installShippedApp($app) {
  509. //install the database
  510. $appPath = OC_App::getAppPath($app);
  511. \OC_App::registerAutoloading($app, $appPath);
  512. if(is_file("$appPath/appinfo/database.xml")) {
  513. try {
  514. OC_DB::createDbFromStructure("$appPath/appinfo/database.xml");
  515. } catch (TableExistsException $e) {
  516. throw new HintException(
  517. 'Failed to enable app ' . $app,
  518. 'Please ask for help via one of our <a href="https://nextcloud.com/support/" target="_blank" rel="noreferrer noopener">support channels</a>.',
  519. 0, $e
  520. );
  521. }
  522. } else {
  523. $ms = new \OC\DB\MigrationService($app, \OC::$server->getDatabaseConnection());
  524. $ms->migrate();
  525. }
  526. //run appinfo/install.php
  527. self::includeAppScript("$appPath/appinfo/install.php");
  528. $info = OC_App::getAppInfo($app);
  529. if (is_null($info)) {
  530. return false;
  531. }
  532. \OC_App::setupBackgroundJobs($info['background-jobs']);
  533. OC_App::executeRepairSteps($app, $info['repair-steps']['install']);
  534. $config = \OC::$server->getConfig();
  535. $config->setAppValue($app, 'installed_version', OC_App::getAppVersion($app));
  536. if (array_key_exists('ocsid', $info)) {
  537. $config->setAppValue($app, 'ocsid', $info['ocsid']);
  538. }
  539. //set remote/public handlers
  540. foreach($info['remote'] as $name=>$path) {
  541. $config->setAppValue('core', 'remote_'.$name, $app.'/'.$path);
  542. }
  543. foreach($info['public'] as $name=>$path) {
  544. $config->setAppValue('core', 'public_'.$name, $app.'/'.$path);
  545. }
  546. OC_App::setAppTypes($info['id']);
  547. return $info['id'];
  548. }
  549. /**
  550. * @param string $script
  551. */
  552. private static function includeAppScript($script) {
  553. if ( file_exists($script) ){
  554. include $script;
  555. }
  556. }
  557. }