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.

Installer.php 16KB

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