diff options
Diffstat (limited to 'lib/private/Updater.php')
-rw-r--r-- | lib/private/Updater.php | 486 |
1 files changed, 486 insertions, 0 deletions
diff --git a/lib/private/Updater.php b/lib/private/Updater.php new file mode 100644 index 00000000000..093ebebbbe4 --- /dev/null +++ b/lib/private/Updater.php @@ -0,0 +1,486 @@ +<?php +/** + * @author Arthur Schiwon <blizzz@owncloud.com> + * @author Bart Visscher <bartv@thisnet.nl> + * @author Björn Schießle <schiessle@owncloud.com> + * @author Frank Karlitschek <frank@owncloud.org> + * @author Joas Schilling <nickvergessen@owncloud.com> + * @author Lukas Reschke <lukas@owncloud.com> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Robin Appelman <icewind@owncloud.com> + * @author Robin McCorkell <robin@mccorkell.me.uk> + * @author Steffen Lindner <mail@steffen-lindner.de> + * @author Thomas Müller <thomas.mueller@tmit.eu> + * @author Victor Dubiniuk <dubiniuk@owncloud.com> + * @author Vincent Petry <pvince81@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OC; + +use OC\Hooks\BasicEmitter; +use OC\IntegrityCheck\Checker; +use OC_App; +use OC_Installer; +use OCP\IConfig; +use OC\Setup; +use OCP\ILogger; +use Symfony\Component\EventDispatcher\GenericEvent; + +/** + * Class that handles autoupdating of ownCloud + * + * Hooks provided in scope \OC\Updater + * - maintenanceStart() + * - maintenanceEnd() + * - dbUpgrade() + * - failure(string $message) + */ +class Updater extends BasicEmitter { + + /** @var ILogger $log */ + private $log; + + /** @var IConfig */ + private $config; + + /** @var Checker */ + private $checker; + + /** @var bool */ + private $simulateStepEnabled; + + /** @var bool */ + private $updateStepEnabled; + + /** @var bool */ + private $skip3rdPartyAppsDisable; + + private $logLevelNames = [ + 0 => 'Debug', + 1 => 'Info', + 2 => 'Warning', + 3 => 'Error', + 4 => 'Fatal', + ]; + + /** + * @param IConfig $config + * @param Checker $checker + * @param ILogger $log + */ + public function __construct(IConfig $config, + Checker $checker, + ILogger $log = null) { + $this->log = $log; + $this->config = $config; + $this->checker = $checker; + $this->simulateStepEnabled = true; + $this->updateStepEnabled = true; + } + + /** + * Sets whether the database migration simulation must + * be enabled. + * This can be set to false to skip this test. + * + * @param bool $flag true to enable simulation, false otherwise + */ + public function setSimulateStepEnabled($flag) { + $this->simulateStepEnabled = $flag; + } + + /** + * Sets whether the update must be performed. + * This can be set to false to skip the actual update. + * + * @param bool $flag true to enable update, false otherwise + */ + public function setUpdateStepEnabled($flag) { + $this->updateStepEnabled = $flag; + } + + /** + * Sets whether the update disables 3rd party apps. + * This can be set to true to skip the disable. + * + * @param bool $flag false to not disable, true otherwise + */ + public function setSkip3rdPartyAppsDisable($flag) { + $this->skip3rdPartyAppsDisable = $flag; + } + + /** + * runs the update actions in maintenance mode, does not upgrade the source files + * except the main .htaccess file + * + * @return bool true if the operation succeeded, false otherwise + */ + public function upgrade() { + $this->emitRepairEvents(); + + $logLevel = $this->config->getSystemValue('loglevel', \OCP\Util::WARN); + $this->emit('\OC\Updater', 'setDebugLogLevel', [ $logLevel, $this->logLevelNames[$logLevel] ]); + $this->config->setSystemValue('loglevel', \OCP\Util::DEBUG); + + $wasMaintenanceModeEnabled = $this->config->getSystemValue('maintenance', false); + + if(!$wasMaintenanceModeEnabled) { + $this->config->setSystemValue('maintenance', true); + $this->emit('\OC\Updater', 'maintenanceEnabled'); + } + + $installedVersion = $this->config->getSystemValue('version', '0.0.0'); + $currentVersion = implode('.', \OCP\Util::getVersion()); + $this->log->debug('starting upgrade from ' . $installedVersion . ' to ' . $currentVersion, array('app' => 'core')); + + $success = true; + try { + $this->doUpgrade($currentVersion, $installedVersion); + } catch (\Exception $exception) { + $this->log->logException($exception, ['app' => 'core']); + $this->emit('\OC\Updater', 'failure', array(get_class($exception) . ': ' .$exception->getMessage())); + $success = false; + } + + $this->emit('\OC\Updater', 'updateEnd', array($success)); + + if(!$wasMaintenanceModeEnabled && $success) { + $this->config->setSystemValue('maintenance', false); + $this->emit('\OC\Updater', 'maintenanceDisabled'); + } else { + $this->emit('\OC\Updater', 'maintenanceActive'); + } + + $this->emit('\OC\Updater', 'resetLogLevel', [ $logLevel, $this->logLevelNames[$logLevel] ]); + $this->config->setSystemValue('loglevel', $logLevel); + + return $success; + } + + /** + * Return version from which this version is allowed to upgrade from + * + * @return string allowed previous version + */ + private function getAllowedPreviousVersion() { + // this should really be a JSON file + require \OC::$SERVERROOT . '/version.php'; + /** @var array $OC_VersionCanBeUpgradedFrom */ + return implode('.', $OC_VersionCanBeUpgradedFrom); + } + + /** + * Whether an upgrade to a specified version is possible + * @param string $oldVersion + * @param string $newVersion + * @param string $allowedPreviousVersion + * @return bool + */ + public function isUpgradePossible($oldVersion, $newVersion, $allowedPreviousVersion) { + return (version_compare($allowedPreviousVersion, $oldVersion, '<=') + && (version_compare($oldVersion, $newVersion, '<=') || $this->config->getSystemValue('debug', false))); + } + + /** + * runs the update actions in maintenance mode, does not upgrade the source files + * except the main .htaccess file + * + * @param string $currentVersion current version to upgrade to + * @param string $installedVersion previous version from which to upgrade from + * + * @throws \Exception + */ + private function doUpgrade($currentVersion, $installedVersion) { + // Stop update if the update is over several major versions + $allowedPreviousVersion = $this->getAllowedPreviousVersion(); + if (!self::isUpgradePossible($installedVersion, $currentVersion, $allowedPreviousVersion)) { + throw new \Exception('Updates between multiple major versions and downgrades are unsupported.'); + } + + // Update .htaccess files + try { + Setup::updateHtaccess(); + Setup::protectDataDirectory(); + } catch (\Exception $e) { + throw new \Exception($e->getMessage()); + } + + // create empty file in data dir, so we can later find + // out that this is indeed an ownCloud data directory + // (in case it didn't exist before) + file_put_contents($this->config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . '/.ocdata', ''); + + // pre-upgrade repairs + $repair = new Repair(Repair::getBeforeUpgradeRepairSteps(), \OC::$server->getEventDispatcher()); + $repair->run(); + + // simulate DB upgrade + if ($this->simulateStepEnabled) { + $this->checkCoreUpgrade(); + + // simulate apps DB upgrade + $this->checkAppUpgrade($currentVersion); + + } + + if ($this->updateStepEnabled) { + $this->doCoreUpgrade(); + + // update all shipped apps + $disabledApps = $this->checkAppsRequirements(); + $this->doAppUpgrade(); + + // upgrade appstore apps + $this->upgradeAppStoreApps($disabledApps); + + // install new shipped apps on upgrade + OC_App::loadApps('authentication'); + $errors = OC_Installer::installShippedApps(true); + foreach ($errors as $appId => $exception) { + /** @var \Exception $exception */ + $this->log->logException($exception, ['app' => $appId]); + $this->emit('\OC\Updater', 'failure', [$appId . ': ' . $exception->getMessage()]); + } + + // post-upgrade repairs + $repair = new Repair(Repair::getRepairSteps(), \OC::$server->getEventDispatcher()); + $repair->run(); + + //Invalidate update feed + $this->config->setAppValue('core', 'lastupdatedat', 0); + + // Check for code integrity if not disabled + if(\OC::$server->getIntegrityCodeChecker()->isCodeCheckEnforced()) { + $this->emit('\OC\Updater', 'startCheckCodeIntegrity'); + $this->checker->runInstanceVerification(); + $this->emit('\OC\Updater', 'finishedCheckCodeIntegrity'); + } + + // only set the final version if everything went well + $this->config->setSystemValue('version', implode('.', \OCP\Util::getVersion())); + } + } + + protected function checkCoreUpgrade() { + $this->emit('\OC\Updater', 'dbSimulateUpgradeBefore'); + + // simulate core DB upgrade + \OC_DB::simulateUpdateDbFromStructure(\OC::$SERVERROOT . '/db_structure.xml'); + + $this->emit('\OC\Updater', 'dbSimulateUpgrade'); + } + + protected function doCoreUpgrade() { + $this->emit('\OC\Updater', 'dbUpgradeBefore'); + + // do the real upgrade + \OC_DB::updateDbFromStructure(\OC::$SERVERROOT . '/db_structure.xml'); + + $this->emit('\OC\Updater', 'dbUpgrade'); + } + + /** + * @param string $version the oc version to check app compatibility with + */ + protected function checkAppUpgrade($version) { + $apps = \OC_App::getEnabledApps(); + $this->emit('\OC\Updater', 'appUpgradeCheckBefore'); + + foreach ($apps as $appId) { + $info = \OC_App::getAppInfo($appId); + $compatible = \OC_App::isAppCompatible($version, $info); + $isShipped = \OC_App::isShipped($appId); + + if ($compatible && $isShipped && \OC_App::shouldUpgrade($appId)) { + /** + * FIXME: The preupdate check is performed before the database migration, otherwise database changes + * are not possible anymore within it. - Consider this when touching the code. + * @link https://github.com/owncloud/core/issues/10980 + * @see \OC_App::updateApp + */ + if (file_exists(\OC_App::getAppPath($appId) . '/appinfo/preupdate.php')) { + $this->includePreUpdate($appId); + } + if (file_exists(\OC_App::getAppPath($appId) . '/appinfo/database.xml')) { + $this->emit('\OC\Updater', 'appSimulateUpdate', array($appId)); + \OC_DB::simulateUpdateDbFromStructure(\OC_App::getAppPath($appId) . '/appinfo/database.xml'); + } + } + } + + $this->emit('\OC\Updater', 'appUpgradeCheck'); + } + + /** + * Includes the pre-update file. Done here to prevent namespace mixups. + * @param string $appId + */ + private function includePreUpdate($appId) { + include \OC_App::getAppPath($appId) . '/appinfo/preupdate.php'; + } + + /** + * upgrades all apps within a major ownCloud upgrade. Also loads "priority" + * (types authentication, filesystem, logging, in that order) afterwards. + * + * @throws NeedsUpdateException + */ + protected function doAppUpgrade() { + $apps = \OC_App::getEnabledApps(); + $priorityTypes = array('authentication', 'filesystem', 'logging'); + $pseudoOtherType = 'other'; + $stacks = array($pseudoOtherType => array()); + + foreach ($apps as $appId) { + $priorityType = false; + foreach ($priorityTypes as $type) { + if(!isset($stacks[$type])) { + $stacks[$type] = array(); + } + if (\OC_App::isType($appId, $type)) { + $stacks[$type][] = $appId; + $priorityType = true; + break; + } + } + if (!$priorityType) { + $stacks[$pseudoOtherType][] = $appId; + } + } + foreach ($stacks as $type => $stack) { + foreach ($stack as $appId) { + if (\OC_App::shouldUpgrade($appId)) { + $this->emit('\OC\Updater', 'appUpgradeStarted', [$appId, \OC_App::getAppVersion($appId)]); + \OC_App::updateApp($appId); + $this->emit('\OC\Updater', 'appUpgrade', [$appId, \OC_App::getAppVersion($appId)]); + } + if($type !== $pseudoOtherType) { + // load authentication, filesystem and logging apps after + // upgrading them. Other apps my need to rely on modifying + // user and/or filesystem aspects. + \OC_App::loadApp($appId, false); + } + } + } + } + + /** + * check if the current enabled apps are compatible with the current + * ownCloud version. disable them if not. + * This is important if you upgrade ownCloud and have non ported 3rd + * party apps installed. + * + * @return array + * @throws \Exception + */ + private function checkAppsRequirements() { + $isCoreUpgrade = $this->isCodeUpgrade(); + $apps = OC_App::getEnabledApps(); + $version = \OCP\Util::getVersion(); + $disabledApps = []; + foreach ($apps as $app) { + // check if the app is compatible with this version of ownCloud + $info = OC_App::getAppInfo($app); + if(!OC_App::isAppCompatible($version, $info)) { + OC_App::disable($app); + $this->emit('\OC\Updater', 'incompatibleAppDisabled', array($app)); + } + // no need to disable any app in case this is a non-core upgrade + if (!$isCoreUpgrade) { + continue; + } + // shipped apps will remain enabled + if (OC_App::isShipped($app)) { + continue; + } + // authentication and session apps will remain enabled as well + if (OC_App::isType($app, ['session', 'authentication'])) { + continue; + } + + // disable any other 3rd party apps if not overriden + if(!$this->skip3rdPartyAppsDisable) { + \OC_App::disable($app); + $disabledApps[]= $app; + $this->emit('\OC\Updater', 'thirdPartyAppDisabled', array($app)); + }; + } + return $disabledApps; + } + + /** + * @return bool + */ + private function isCodeUpgrade() { + $installedVersion = $this->config->getSystemValue('version', '0.0.0'); + $currentVersion = implode('.', \OCP\Util::getVersion()); + if (version_compare($currentVersion, $installedVersion, '>')) { + return true; + } + return false; + } + + /** + * @param array $disabledApps + * @throws \Exception + */ + private function upgradeAppStoreApps(array $disabledApps) { + foreach($disabledApps as $app) { + try { + if (OC_Installer::isUpdateAvailable($app)) { + $ocsId = \OC::$server->getConfig()->getAppValue($app, 'ocsid', ''); + + $this->emit('\OC\Updater', 'upgradeAppStoreApp', array($app)); + OC_Installer::updateAppByOCSId($ocsId); + } + } catch (\Exception $ex) { + $this->log->logException($ex, ['app' => 'core']); + } + } + } + + /** + * Forward messages emitted by the repair routine + */ + private function emitRepairEvents() { + $dispatcher = \OC::$server->getEventDispatcher(); + $dispatcher->addListener('\OC\Repair::warning', function ($event) { + if ($event instanceof GenericEvent) { + $this->emit('\OC\Updater', 'repairWarning', $event->getArguments()); + } + }); + $dispatcher->addListener('\OC\Repair::error', function ($event) { + if ($event instanceof GenericEvent) { + $this->emit('\OC\Updater', 'repairError', $event->getArguments()); + } + }); + $dispatcher->addListener('\OC\Repair::info', function ($event) { + if ($event instanceof GenericEvent) { + $this->emit('\OC\Updater', 'repairInfo', $event->getArguments()); + } + }); + $dispatcher->addListener('\OC\Repair::step', function ($event) { + if ($event instanceof GenericEvent) { + $this->emit('\OC\Updater', 'repairStep', $event->getArguments()); + } + }); + } + +} + |