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.

Updater.php 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  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 Frank Karlitschek <frank@karlitschek.de>
  8. * @author Joas Schilling <coding@schilljs.com>
  9. * @author Lukas Reschke <lukas@statuscode.ch>
  10. * @author Morris Jobke <hey@morrisjobke.de>
  11. * @author Robin Appelman <robin@icewind.nl>
  12. * @author Steffen Lindner <mail@steffen-lindner.de>
  13. * @author Thomas Müller <thomas.mueller@tmit.eu>
  14. * @author Victor Dubiniuk <dubiniuk@owncloud.com>
  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;
  33. use OC\Hooks\BasicEmitter;
  34. use OC\IntegrityCheck\Checker;
  35. use OC_App;
  36. use OCP\IConfig;
  37. use OCP\ILogger;
  38. use OCP\Util;
  39. use Symfony\Component\EventDispatcher\GenericEvent;
  40. /**
  41. * Class that handles autoupdating of ownCloud
  42. *
  43. * Hooks provided in scope \OC\Updater
  44. * - maintenanceStart()
  45. * - maintenanceEnd()
  46. * - dbUpgrade()
  47. * - failure(string $message)
  48. */
  49. class Updater extends BasicEmitter {
  50. /** @var ILogger $log */
  51. private $log;
  52. /** @var IConfig */
  53. private $config;
  54. /** @var Checker */
  55. private $checker;
  56. /** @var bool */
  57. private $skip3rdPartyAppsDisable;
  58. private $logLevelNames = [
  59. 0 => 'Debug',
  60. 1 => 'Info',
  61. 2 => 'Warning',
  62. 3 => 'Error',
  63. 4 => 'Fatal',
  64. ];
  65. /**
  66. * @param IConfig $config
  67. * @param Checker $checker
  68. * @param ILogger $log
  69. */
  70. public function __construct(IConfig $config,
  71. Checker $checker,
  72. ILogger $log = null) {
  73. $this->log = $log;
  74. $this->config = $config;
  75. $this->checker = $checker;
  76. }
  77. /**
  78. * Sets whether the update disables 3rd party apps.
  79. * This can be set to true to skip the disable.
  80. *
  81. * @param bool $flag false to not disable, true otherwise
  82. */
  83. public function setSkip3rdPartyAppsDisable($flag) {
  84. $this->skip3rdPartyAppsDisable = $flag;
  85. }
  86. /**
  87. * runs the update actions in maintenance mode, does not upgrade the source files
  88. * except the main .htaccess file
  89. *
  90. * @return bool true if the operation succeeded, false otherwise
  91. */
  92. public function upgrade() {
  93. $this->emitRepairEvents();
  94. $logLevel = $this->config->getSystemValue('loglevel', Util::WARN);
  95. $this->emit('\OC\Updater', 'setDebugLogLevel', [ $logLevel, $this->logLevelNames[$logLevel] ]);
  96. $this->config->setSystemValue('loglevel', Util::DEBUG);
  97. $wasMaintenanceModeEnabled = $this->config->getSystemValue('maintenance', false);
  98. if(!$wasMaintenanceModeEnabled) {
  99. $this->config->setSystemValue('maintenance', true);
  100. $this->emit('\OC\Updater', 'maintenanceEnabled');
  101. }
  102. $installedVersion = $this->config->getSystemValue('version', '0.0.0');
  103. $currentVersion = implode('.', \OCP\Util::getVersion());
  104. $this->log->debug('starting upgrade from ' . $installedVersion . ' to ' . $currentVersion, array('app' => 'core'));
  105. $success = true;
  106. try {
  107. $this->doUpgrade($currentVersion, $installedVersion);
  108. } catch (HintException $exception) {
  109. $this->log->logException($exception, ['app' => 'core']);
  110. $this->emit('\OC\Updater', 'failure', array($exception->getMessage() . ': ' .$exception->getHint()));
  111. $success = false;
  112. } catch (\Exception $exception) {
  113. $this->log->logException($exception, ['app' => 'core']);
  114. $this->emit('\OC\Updater', 'failure', array(get_class($exception) . ': ' .$exception->getMessage()));
  115. $success = false;
  116. }
  117. $this->emit('\OC\Updater', 'updateEnd', array($success));
  118. if(!$wasMaintenanceModeEnabled && $success) {
  119. $this->config->setSystemValue('maintenance', false);
  120. $this->emit('\OC\Updater', 'maintenanceDisabled');
  121. } else {
  122. $this->emit('\OC\Updater', 'maintenanceActive');
  123. }
  124. $this->emit('\OC\Updater', 'resetLogLevel', [ $logLevel, $this->logLevelNames[$logLevel] ]);
  125. $this->config->setSystemValue('loglevel', $logLevel);
  126. $this->config->setSystemValue('installed', true);
  127. return $success;
  128. }
  129. /**
  130. * Return version from which this version is allowed to upgrade from
  131. *
  132. * @return string allowed previous version
  133. */
  134. private function getAllowedPreviousVersion() {
  135. // this should really be a JSON file
  136. require \OC::$SERVERROOT . '/version.php';
  137. /** @var array $OC_VersionCanBeUpgradedFrom */
  138. return implode('.', $OC_VersionCanBeUpgradedFrom);
  139. }
  140. /**
  141. * Return vendor from which this version was published
  142. *
  143. * @return string Get the vendor
  144. */
  145. private function getVendor() {
  146. // this should really be a JSON file
  147. require \OC::$SERVERROOT . '/version.php';
  148. /** @var string $vendor */
  149. return (string) $vendor;
  150. }
  151. /**
  152. * Whether an upgrade to a specified version is possible
  153. * @param string $oldVersion
  154. * @param string $newVersion
  155. * @param string $allowedPreviousVersion
  156. * @return bool
  157. */
  158. public function isUpgradePossible($oldVersion, $newVersion, $allowedPreviousVersion) {
  159. $allowedUpgrade = (version_compare($allowedPreviousVersion, $oldVersion, '<=')
  160. && (version_compare($oldVersion, $newVersion, '<=') || $this->config->getSystemValue('debug', false)));
  161. if ($allowedUpgrade) {
  162. return $allowedUpgrade;
  163. }
  164. // Upgrade not allowed, someone switching vendor?
  165. if ($this->getVendor() !== $this->config->getAppValue('core', 'vendor', '')) {
  166. $oldVersion = explode('.', $oldVersion);
  167. $newVersion = explode('.', $newVersion);
  168. return $oldVersion[0] === $newVersion[0] && $oldVersion[1] === $newVersion[1];
  169. }
  170. return false;
  171. }
  172. /**
  173. * runs the update actions in maintenance mode, does not upgrade the source files
  174. * except the main .htaccess file
  175. *
  176. * @param string $currentVersion current version to upgrade to
  177. * @param string $installedVersion previous version from which to upgrade from
  178. *
  179. * @throws \Exception
  180. */
  181. private function doUpgrade($currentVersion, $installedVersion) {
  182. // Stop update if the update is over several major versions
  183. $allowedPreviousVersion = $this->getAllowedPreviousVersion();
  184. if (!self::isUpgradePossible($installedVersion, $currentVersion, $allowedPreviousVersion)) {
  185. throw new \Exception('Updates between multiple major versions and downgrades are unsupported.');
  186. }
  187. // Update .htaccess files
  188. try {
  189. Setup::updateHtaccess();
  190. Setup::protectDataDirectory();
  191. } catch (\Exception $e) {
  192. throw new \Exception($e->getMessage());
  193. }
  194. // create empty file in data dir, so we can later find
  195. // out that this is indeed an ownCloud data directory
  196. // (in case it didn't exist before)
  197. file_put_contents($this->config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . '/.ocdata', '');
  198. // pre-upgrade repairs
  199. $repair = new Repair(Repair::getBeforeUpgradeRepairSteps(), \OC::$server->getEventDispatcher());
  200. $repair->run();
  201. $this->doCoreUpgrade();
  202. try {
  203. // TODO: replace with the new repair step mechanism https://github.com/owncloud/core/pull/24378
  204. Setup::installBackgroundJobs();
  205. } catch (\Exception $e) {
  206. throw new \Exception($e->getMessage());
  207. }
  208. // update all shipped apps
  209. $disabledApps = $this->checkAppsRequirements();
  210. $this->doAppUpgrade();
  211. // upgrade appstore apps
  212. $this->upgradeAppStoreApps($disabledApps);
  213. // install new shipped apps on upgrade
  214. OC_App::loadApps('authentication');
  215. $errors = Installer::installShippedApps(true);
  216. foreach ($errors as $appId => $exception) {
  217. /** @var \Exception $exception */
  218. $this->log->logException($exception, ['app' => $appId]);
  219. $this->emit('\OC\Updater', 'failure', [$appId . ': ' . $exception->getMessage()]);
  220. }
  221. // post-upgrade repairs
  222. $repair = new Repair(Repair::getRepairSteps(), \OC::$server->getEventDispatcher());
  223. $repair->run();
  224. //Invalidate update feed
  225. $this->config->setAppValue('core', 'lastupdatedat', 0);
  226. // Check for code integrity if not disabled
  227. if(\OC::$server->getIntegrityCodeChecker()->isCodeCheckEnforced()) {
  228. $this->emit('\OC\Updater', 'startCheckCodeIntegrity');
  229. $this->checker->runInstanceVerification();
  230. $this->emit('\OC\Updater', 'finishedCheckCodeIntegrity');
  231. }
  232. // only set the final version if everything went well
  233. $this->config->setSystemValue('version', implode('.', Util::getVersion()));
  234. $this->config->setAppValue('core', 'vendor', $this->getVendor());
  235. }
  236. protected function doCoreUpgrade() {
  237. $this->emit('\OC\Updater', 'dbUpgradeBefore');
  238. // do the real upgrade
  239. \OC_DB::updateDbFromStructure(\OC::$SERVERROOT . '/db_structure.xml');
  240. $this->emit('\OC\Updater', 'dbUpgrade');
  241. }
  242. /**
  243. * @param string $version the oc version to check app compatibility with
  244. */
  245. protected function checkAppUpgrade($version) {
  246. $apps = \OC_App::getEnabledApps();
  247. $this->emit('\OC\Updater', 'appUpgradeCheckBefore');
  248. foreach ($apps as $appId) {
  249. $info = \OC_App::getAppInfo($appId);
  250. $compatible = \OC_App::isAppCompatible($version, $info);
  251. $isShipped = \OC_App::isShipped($appId);
  252. if ($compatible && $isShipped && \OC_App::shouldUpgrade($appId)) {
  253. /**
  254. * FIXME: The preupdate check is performed before the database migration, otherwise database changes
  255. * are not possible anymore within it. - Consider this when touching the code.
  256. * @link https://github.com/owncloud/core/issues/10980
  257. * @see \OC_App::updateApp
  258. */
  259. if (file_exists(\OC_App::getAppPath($appId) . '/appinfo/preupdate.php')) {
  260. $this->includePreUpdate($appId);
  261. }
  262. if (file_exists(\OC_App::getAppPath($appId) . '/appinfo/database.xml')) {
  263. $this->emit('\OC\Updater', 'appSimulateUpdate', array($appId));
  264. \OC_DB::simulateUpdateDbFromStructure(\OC_App::getAppPath($appId) . '/appinfo/database.xml');
  265. }
  266. }
  267. }
  268. $this->emit('\OC\Updater', 'appUpgradeCheck');
  269. }
  270. /**
  271. * Includes the pre-update file. Done here to prevent namespace mixups.
  272. * @param string $appId
  273. */
  274. private function includePreUpdate($appId) {
  275. include \OC_App::getAppPath($appId) . '/appinfo/preupdate.php';
  276. }
  277. /**
  278. * upgrades all apps within a major ownCloud upgrade. Also loads "priority"
  279. * (types authentication, filesystem, logging, in that order) afterwards.
  280. *
  281. * @throws NeedsUpdateException
  282. */
  283. protected function doAppUpgrade() {
  284. $apps = \OC_App::getEnabledApps();
  285. $priorityTypes = array('authentication', 'filesystem', 'logging');
  286. $pseudoOtherType = 'other';
  287. $stacks = array($pseudoOtherType => array());
  288. foreach ($apps as $appId) {
  289. $priorityType = false;
  290. foreach ($priorityTypes as $type) {
  291. if(!isset($stacks[$type])) {
  292. $stacks[$type] = array();
  293. }
  294. if (\OC_App::isType($appId, $type)) {
  295. $stacks[$type][] = $appId;
  296. $priorityType = true;
  297. break;
  298. }
  299. }
  300. if (!$priorityType) {
  301. $stacks[$pseudoOtherType][] = $appId;
  302. }
  303. }
  304. foreach ($stacks as $type => $stack) {
  305. foreach ($stack as $appId) {
  306. if (\OC_App::shouldUpgrade($appId)) {
  307. $this->emit('\OC\Updater', 'appUpgradeStarted', [$appId, \OC_App::getAppVersion($appId)]);
  308. \OC_App::updateApp($appId);
  309. $this->emit('\OC\Updater', 'appUpgrade', [$appId, \OC_App::getAppVersion($appId)]);
  310. }
  311. if($type !== $pseudoOtherType) {
  312. // load authentication, filesystem and logging apps after
  313. // upgrading them. Other apps my need to rely on modifying
  314. // user and/or filesystem aspects.
  315. \OC_App::loadApp($appId, false);
  316. }
  317. }
  318. }
  319. }
  320. /**
  321. * check if the current enabled apps are compatible with the current
  322. * ownCloud version. disable them if not.
  323. * This is important if you upgrade ownCloud and have non ported 3rd
  324. * party apps installed.
  325. *
  326. * @return array
  327. * @throws \Exception
  328. */
  329. private function checkAppsRequirements() {
  330. $isCoreUpgrade = $this->isCodeUpgrade();
  331. $apps = OC_App::getEnabledApps();
  332. $version = Util::getVersion();
  333. $disabledApps = [];
  334. foreach ($apps as $app) {
  335. // check if the app is compatible with this version of ownCloud
  336. $info = OC_App::getAppInfo($app);
  337. if(!OC_App::isAppCompatible($version, $info)) {
  338. if (OC_App::isShipped($app)) {
  339. throw new \UnexpectedValueException('The files of the app "' . $app . '" were not correctly replaced before running the update');
  340. }
  341. OC_App::disable($app);
  342. $this->emit('\OC\Updater', 'incompatibleAppDisabled', array($app));
  343. }
  344. // no need to disable any app in case this is a non-core upgrade
  345. if (!$isCoreUpgrade) {
  346. continue;
  347. }
  348. // shipped apps will remain enabled
  349. if (OC_App::isShipped($app)) {
  350. continue;
  351. }
  352. // authentication and session apps will remain enabled as well
  353. if (OC_App::isType($app, ['session', 'authentication'])) {
  354. continue;
  355. }
  356. // disable any other 3rd party apps if not overriden
  357. if(!$this->skip3rdPartyAppsDisable) {
  358. \OC_App::disable($app);
  359. $disabledApps[]= $app;
  360. $this->emit('\OC\Updater', 'thirdPartyAppDisabled', array($app));
  361. };
  362. }
  363. return $disabledApps;
  364. }
  365. /**
  366. * @return bool
  367. */
  368. private function isCodeUpgrade() {
  369. $installedVersion = $this->config->getSystemValue('version', '0.0.0');
  370. $currentVersion = implode('.', Util::getVersion());
  371. if (version_compare($currentVersion, $installedVersion, '>')) {
  372. return true;
  373. }
  374. return false;
  375. }
  376. /**
  377. * @param array $disabledApps
  378. * @throws \Exception
  379. */
  380. private function upgradeAppStoreApps(array $disabledApps) {
  381. foreach($disabledApps as $app) {
  382. try {
  383. $installer = new Installer(
  384. \OC::$server->getAppFetcher(),
  385. \OC::$server->getHTTPClientService(),
  386. \OC::$server->getTempManager(),
  387. $this->log
  388. );
  389. if (Installer::isUpdateAvailable($app, \OC::$server->getAppFetcher())) {
  390. $this->emit('\OC\Updater', 'upgradeAppStoreApp', [$app]);
  391. $installer->updateAppstoreApp($app);
  392. }
  393. } catch (\Exception $ex) {
  394. $this->log->logException($ex, ['app' => 'core']);
  395. }
  396. }
  397. }
  398. /**
  399. * Forward messages emitted by the repair routine
  400. */
  401. private function emitRepairEvents() {
  402. $dispatcher = \OC::$server->getEventDispatcher();
  403. $dispatcher->addListener('\OC\Repair::warning', function ($event) {
  404. if ($event instanceof GenericEvent) {
  405. $this->emit('\OC\Updater', 'repairWarning', $event->getArguments());
  406. }
  407. });
  408. $dispatcher->addListener('\OC\Repair::error', function ($event) {
  409. if ($event instanceof GenericEvent) {
  410. $this->emit('\OC\Updater', 'repairError', $event->getArguments());
  411. }
  412. });
  413. $dispatcher->addListener('\OC\Repair::info', function ($event) {
  414. if ($event instanceof GenericEvent) {
  415. $this->emit('\OC\Updater', 'repairInfo', $event->getArguments());
  416. }
  417. });
  418. $dispatcher->addListener('\OC\Repair::step', function ($event) {
  419. if ($event instanceof GenericEvent) {
  420. $this->emit('\OC\Updater', 'repairStep', $event->getArguments());
  421. }
  422. });
  423. }
  424. }