1<?php 2 3declare(strict_types=1); 4 5/** 6 * @copyright Copyright (c) 2016, ownCloud, Inc. 7 * @copyright Copyright (c) 2016, Lukas Reschke <lukas@statuscode.ch> 8 * 9 * @author Arthur Schiwon <blizzz@arthur-schiwon.de> 10 * @author Bjoern Schiessle <bjoern@schiessle.org> 11 * @author Christoph Wurst <christoph@winzerhof-wurst.at> 12 * @author Frank Karlitschek <frank@karlitschek.de> 13 * @author Georg Ehrke <oc.list@georgehrke.com> 14 * @author J0WI <J0WI@users.noreply.github.com> 15 * @author Joas Schilling <coding@schilljs.com> 16 * @author Julius Härtl <jus@bitgrid.net> 17 * @author Lukas Reschke <lukas@statuscode.ch> 18 * @author Morris Jobke <hey@morrisjobke.de> 19 * @author Robin Appelman <robin@icewind.nl> 20 * @author Roeland Jago Douma <roeland@famdouma.nl> 21 * @author Steffen Lindner <mail@steffen-lindner.de> 22 * @author Thomas Müller <thomas.mueller@tmit.eu> 23 * @author Victor Dubiniuk <dubiniuk@owncloud.com> 24 * @author Vincent Petry <vincent@nextcloud.com> 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 */ 41namespace OC; 42 43use OC\App\AppManager; 44use OC\DB\Connection; 45use OC\DB\MigrationService; 46use OC\Hooks\BasicEmitter; 47use OC\IntegrityCheck\Checker; 48use OC_App; 49use OCP\App\IAppManager; 50use OCP\HintException; 51use OCP\IConfig; 52use OCP\ILogger; 53use OCP\Util; 54use Psr\Log\LoggerInterface; 55use Symfony\Component\EventDispatcher\GenericEvent; 56 57/** 58 * Class that handles autoupdating of ownCloud 59 * 60 * Hooks provided in scope \OC\Updater 61 * - maintenanceStart() 62 * - maintenanceEnd() 63 * - dbUpgrade() 64 * - failure(string $message) 65 */ 66class Updater extends BasicEmitter { 67 68 /** @var LoggerInterface */ 69 private $log; 70 71 /** @var IConfig */ 72 private $config; 73 74 /** @var Checker */ 75 private $checker; 76 77 /** @var Installer */ 78 private $installer; 79 80 private $logLevelNames = [ 81 0 => 'Debug', 82 1 => 'Info', 83 2 => 'Warning', 84 3 => 'Error', 85 4 => 'Fatal', 86 ]; 87 88 public function __construct(IConfig $config, 89 Checker $checker, 90 ?LoggerInterface $log, 91 Installer $installer) { 92 $this->log = $log; 93 $this->config = $config; 94 $this->checker = $checker; 95 $this->installer = $installer; 96 } 97 98 /** 99 * runs the update actions in maintenance mode, does not upgrade the source files 100 * except the main .htaccess file 101 * 102 * @return bool true if the operation succeeded, false otherwise 103 */ 104 public function upgrade(): bool { 105 $this->emitRepairEvents(); 106 $this->logAllEvents(); 107 108 $logLevel = $this->config->getSystemValue('loglevel', ILogger::WARN); 109 $this->emit('\OC\Updater', 'setDebugLogLevel', [ $logLevel, $this->logLevelNames[$logLevel] ]); 110 $this->config->setSystemValue('loglevel', ILogger::DEBUG); 111 112 $wasMaintenanceModeEnabled = $this->config->getSystemValueBool('maintenance'); 113 114 if (!$wasMaintenanceModeEnabled) { 115 $this->config->setSystemValue('maintenance', true); 116 $this->emit('\OC\Updater', 'maintenanceEnabled'); 117 } 118 119 // Clear CAN_INSTALL file if not on git 120 if (\OC_Util::getChannel() !== 'git' && is_file(\OC::$configDir.'/CAN_INSTALL')) { 121 if (!unlink(\OC::$configDir . '/CAN_INSTALL')) { 122 $this->log->error('Could not cleanup CAN_INSTALL from your config folder. Please remove this file manually.'); 123 } 124 } 125 126 $installedVersion = $this->config->getSystemValue('version', '0.0.0'); 127 $currentVersion = implode('.', \OCP\Util::getVersion()); 128 129 $this->log->debug('starting upgrade from ' . $installedVersion . ' to ' . $currentVersion, ['app' => 'core']); 130 131 $success = true; 132 try { 133 $this->doUpgrade($currentVersion, $installedVersion); 134 } catch (HintException $exception) { 135 $this->log->error($exception->getMessage(), [ 136 'exception' => $exception, 137 ]); 138 $this->emit('\OC\Updater', 'failure', [$exception->getMessage() . ': ' .$exception->getHint()]); 139 $success = false; 140 } catch (\Exception $exception) { 141 $this->log->error($exception->getMessage(), [ 142 'exception' => $exception, 143 ]); 144 $this->emit('\OC\Updater', 'failure', [get_class($exception) . ': ' .$exception->getMessage()]); 145 $success = false; 146 } 147 148 $this->emit('\OC\Updater', 'updateEnd', [$success]); 149 150 if (!$wasMaintenanceModeEnabled && $success) { 151 $this->config->setSystemValue('maintenance', false); 152 $this->emit('\OC\Updater', 'maintenanceDisabled'); 153 } else { 154 $this->emit('\OC\Updater', 'maintenanceActive'); 155 } 156 157 $this->emit('\OC\Updater', 'resetLogLevel', [ $logLevel, $this->logLevelNames[$logLevel] ]); 158 $this->config->setSystemValue('loglevel', $logLevel); 159 $this->config->setSystemValue('installed', true); 160 161 return $success; 162 } 163 164 /** 165 * Return version from which this version is allowed to upgrade from 166 * 167 * @return array allowed previous versions per vendor 168 */ 169 private function getAllowedPreviousVersions(): array { 170 // this should really be a JSON file 171 require \OC::$SERVERROOT . '/version.php'; 172 /** @var array $OC_VersionCanBeUpgradedFrom */ 173 return $OC_VersionCanBeUpgradedFrom; 174 } 175 176 /** 177 * Return vendor from which this version was published 178 * 179 * @return string Get the vendor 180 */ 181 private function getVendor(): string { 182 // this should really be a JSON file 183 require \OC::$SERVERROOT . '/version.php'; 184 /** @var string $vendor */ 185 return (string) $vendor; 186 } 187 188 /** 189 * Whether an upgrade to a specified version is possible 190 * @param string $oldVersion 191 * @param string $newVersion 192 * @param array $allowedPreviousVersions 193 * @return bool 194 */ 195 public function isUpgradePossible(string $oldVersion, string $newVersion, array $allowedPreviousVersions): bool { 196 $version = explode('.', $oldVersion); 197 $majorMinor = $version[0] . '.' . $version[1]; 198 199 $currentVendor = $this->config->getAppValue('core', 'vendor', ''); 200 201 // Vendor was not set correctly on install, so we have to white-list known versions 202 if ($currentVendor === '' && ( 203 isset($allowedPreviousVersions['owncloud'][$oldVersion]) || 204 isset($allowedPreviousVersions['owncloud'][$majorMinor]) 205 )) { 206 $currentVendor = 'owncloud'; 207 $this->config->setAppValue('core', 'vendor', $currentVendor); 208 } 209 210 if ($currentVendor === 'nextcloud') { 211 return isset($allowedPreviousVersions[$currentVendor][$majorMinor]) 212 && (version_compare($oldVersion, $newVersion, '<=') || 213 $this->config->getSystemValue('debug', false)); 214 } 215 216 // Check if the instance can be migrated 217 return isset($allowedPreviousVersions[$currentVendor][$majorMinor]) || 218 isset($allowedPreviousVersions[$currentVendor][$oldVersion]); 219 } 220 221 /** 222 * runs the update actions in maintenance mode, does not upgrade the source files 223 * except the main .htaccess file 224 * 225 * @param string $currentVersion current version to upgrade to 226 * @param string $installedVersion previous version from which to upgrade from 227 * 228 * @throws \Exception 229 */ 230 private function doUpgrade(string $currentVersion, string $installedVersion): void { 231 // Stop update if the update is over several major versions 232 $allowedPreviousVersions = $this->getAllowedPreviousVersions(); 233 if (!$this->isUpgradePossible($installedVersion, $currentVersion, $allowedPreviousVersions)) { 234 throw new \Exception('Updates between multiple major versions and downgrades are unsupported.'); 235 } 236 237 // Update .htaccess files 238 try { 239 Setup::updateHtaccess(); 240 Setup::protectDataDirectory(); 241 } catch (\Exception $e) { 242 throw new \Exception($e->getMessage()); 243 } 244 245 // create empty file in data dir, so we can later find 246 // out that this is indeed an ownCloud data directory 247 // (in case it didn't exist before) 248 file_put_contents($this->config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . '/.ocdata', ''); 249 250 // pre-upgrade repairs 251 $repair = new Repair(Repair::getBeforeUpgradeRepairSteps(), \OC::$server->getEventDispatcher(), \OC::$server->get(LoggerInterface::class)); 252 $repair->run(); 253 254 $this->doCoreUpgrade(); 255 256 try { 257 // TODO: replace with the new repair step mechanism https://github.com/owncloud/core/pull/24378 258 Setup::installBackgroundJobs(); 259 } catch (\Exception $e) { 260 throw new \Exception($e->getMessage()); 261 } 262 263 // update all shipped apps 264 $this->checkAppsRequirements(); 265 $this->doAppUpgrade(); 266 267 // Update the appfetchers version so it downloads the correct list from the appstore 268 \OC::$server->getAppFetcher()->setVersion($currentVersion); 269 270 /** @var IAppManager|AppManager $appManager */ 271 $appManager = \OC::$server->getAppManager(); 272 273 // upgrade appstore apps 274 $this->upgradeAppStoreApps($appManager->getInstalledApps()); 275 $autoDisabledApps = $appManager->getAutoDisabledApps(); 276 if (!empty($autoDisabledApps)) { 277 $this->upgradeAppStoreApps(array_keys($autoDisabledApps), $autoDisabledApps); 278 } 279 280 // install new shipped apps on upgrade 281 $errors = Installer::installShippedApps(true); 282 foreach ($errors as $appId => $exception) { 283 /** @var \Exception $exception */ 284 $this->log->error($exception->getMessage(), [ 285 'exception' => $exception, 286 'app' => $appId, 287 ]); 288 $this->emit('\OC\Updater', 'failure', [$appId . ': ' . $exception->getMessage()]); 289 } 290 291 // post-upgrade repairs 292 $repair = new Repair(Repair::getRepairSteps(), \OC::$server->getEventDispatcher(), \OC::$server->get(LoggerInterface::class)); 293 $repair->run(); 294 295 //Invalidate update feed 296 $this->config->setAppValue('core', 'lastupdatedat', 0); 297 298 // Check for code integrity if not disabled 299 if (\OC::$server->getIntegrityCodeChecker()->isCodeCheckEnforced()) { 300 $this->emit('\OC\Updater', 'startCheckCodeIntegrity'); 301 $this->checker->runInstanceVerification(); 302 $this->emit('\OC\Updater', 'finishedCheckCodeIntegrity'); 303 } 304 305 // only set the final version if everything went well 306 $this->config->setSystemValue('version', implode('.', Util::getVersion())); 307 $this->config->setAppValue('core', 'vendor', $this->getVendor()); 308 } 309 310 protected function doCoreUpgrade(): void { 311 $this->emit('\OC\Updater', 'dbUpgradeBefore'); 312 313 // execute core migrations 314 $ms = new MigrationService('core', \OC::$server->get(Connection::class)); 315 $ms->migrate(); 316 317 $this->emit('\OC\Updater', 'dbUpgrade'); 318 } 319 320 /** 321 * upgrades all apps within a major ownCloud upgrade. Also loads "priority" 322 * (types authentication, filesystem, logging, in that order) afterwards. 323 * 324 * @throws NeedsUpdateException 325 */ 326 protected function doAppUpgrade(): void { 327 $apps = \OC_App::getEnabledApps(); 328 $priorityTypes = ['authentication', 'filesystem', 'logging']; 329 $pseudoOtherType = 'other'; 330 $stacks = [$pseudoOtherType => []]; 331 332 foreach ($apps as $appId) { 333 $priorityType = false; 334 foreach ($priorityTypes as $type) { 335 if (!isset($stacks[$type])) { 336 $stacks[$type] = []; 337 } 338 if (\OC_App::isType($appId, [$type])) { 339 $stacks[$type][] = $appId; 340 $priorityType = true; 341 break; 342 } 343 } 344 if (!$priorityType) { 345 $stacks[$pseudoOtherType][] = $appId; 346 } 347 } 348 foreach (array_merge($priorityTypes, [$pseudoOtherType]) as $type) { 349 $stack = $stacks[$type]; 350 foreach ($stack as $appId) { 351 if (\OC_App::shouldUpgrade($appId)) { 352 $this->emit('\OC\Updater', 'appUpgradeStarted', [$appId, \OC_App::getAppVersion($appId)]); 353 \OC_App::updateApp($appId); 354 $this->emit('\OC\Updater', 'appUpgrade', [$appId, \OC_App::getAppVersion($appId)]); 355 } 356 if ($type !== $pseudoOtherType) { 357 // load authentication, filesystem and logging apps after 358 // upgrading them. Other apps my need to rely on modifying 359 // user and/or filesystem aspects. 360 \OC_App::loadApp($appId); 361 } 362 } 363 } 364 } 365 366 /** 367 * check if the current enabled apps are compatible with the current 368 * ownCloud version. disable them if not. 369 * This is important if you upgrade ownCloud and have non ported 3rd 370 * party apps installed. 371 * 372 * @throws \Exception 373 */ 374 private function checkAppsRequirements(): void { 375 $isCoreUpgrade = $this->isCodeUpgrade(); 376 $apps = OC_App::getEnabledApps(); 377 $version = implode('.', Util::getVersion()); 378 $appManager = \OC::$server->getAppManager(); 379 foreach ($apps as $app) { 380 // check if the app is compatible with this version of Nextcloud 381 $info = OC_App::getAppInfo($app); 382 if ($info === null || !OC_App::isAppCompatible($version, $info)) { 383 if ($appManager->isShipped($app)) { 384 throw new \UnexpectedValueException('The files of the app "' . $app . '" were not correctly replaced before running the update'); 385 } 386 $appManager->disableApp($app, true); 387 $this->emit('\OC\Updater', 'incompatibleAppDisabled', [$app]); 388 } 389 } 390 } 391 392 /** 393 * @return bool 394 */ 395 private function isCodeUpgrade(): bool { 396 $installedVersion = $this->config->getSystemValue('version', '0.0.0'); 397 $currentVersion = implode('.', Util::getVersion()); 398 if (version_compare($currentVersion, $installedVersion, '>')) { 399 return true; 400 } 401 return false; 402 } 403 404 /** 405 * @param array $apps 406 * @param array $previousEnableStates 407 * @throws \Exception 408 */ 409 private function upgradeAppStoreApps(array $apps, array $previousEnableStates = []): void { 410 foreach ($apps as $app) { 411 try { 412 $this->emit('\OC\Updater', 'checkAppStoreAppBefore', [$app]); 413 if ($this->installer->isUpdateAvailable($app)) { 414 $this->emit('\OC\Updater', 'upgradeAppStoreApp', [$app]); 415 $this->installer->updateAppstoreApp($app); 416 } 417 $this->emit('\OC\Updater', 'checkAppStoreApp', [$app]); 418 419 if (!empty($previousEnableStates)) { 420 $ocApp = new \OC_App(); 421 if (!empty($previousEnableStates[$app]) && is_array($previousEnableStates[$app])) { 422 $ocApp->enable($app, $previousEnableStates[$app]); 423 } else { 424 $ocApp->enable($app); 425 } 426 } 427 } catch (\Exception $ex) { 428 $this->log->error($ex->getMessage(), [ 429 'exception' => $ex, 430 ]); 431 } 432 } 433 } 434 435 /** 436 * Forward messages emitted by the repair routine 437 */ 438 private function emitRepairEvents(): void { 439 $dispatcher = \OC::$server->getEventDispatcher(); 440 $dispatcher->addListener('\OC\Repair::warning', function ($event) { 441 if ($event instanceof GenericEvent) { 442 $this->emit('\OC\Updater', 'repairWarning', $event->getArguments()); 443 } 444 }); 445 $dispatcher->addListener('\OC\Repair::error', function ($event) { 446 if ($event instanceof GenericEvent) { 447 $this->emit('\OC\Updater', 'repairError', $event->getArguments()); 448 } 449 }); 450 $dispatcher->addListener('\OC\Repair::info', function ($event) { 451 if ($event instanceof GenericEvent) { 452 $this->emit('\OC\Updater', 'repairInfo', $event->getArguments()); 453 } 454 }); 455 $dispatcher->addListener('\OC\Repair::step', function ($event) { 456 if ($event instanceof GenericEvent) { 457 $this->emit('\OC\Updater', 'repairStep', $event->getArguments()); 458 } 459 }); 460 } 461 462 private function logAllEvents(): void { 463 $log = $this->log; 464 465 $dispatcher = \OC::$server->getEventDispatcher(); 466 $dispatcher->addListener('\OC\DB\Migrator::executeSql', function ($event) use ($log) { 467 if (!$event instanceof GenericEvent) { 468 return; 469 } 470 $log->info('\OC\DB\Migrator::executeSql: ' . $event->getSubject() . ' (' . $event->getArgument(0) . ' of ' . $event->getArgument(1) . ')', ['app' => 'updater']); 471 }); 472 $dispatcher->addListener('\OC\DB\Migrator::checkTable', function ($event) use ($log) { 473 if (!$event instanceof GenericEvent) { 474 return; 475 } 476 $log->info('\OC\DB\Migrator::checkTable: ' . $event->getSubject() . ' (' . $event->getArgument(0) . ' of ' . $event->getArgument(1) . ')', ['app' => 'updater']); 477 }); 478 479 $repairListener = function ($event) use ($log) { 480 if (!$event instanceof GenericEvent) { 481 return; 482 } 483 switch ($event->getSubject()) { 484 case '\OC\Repair::startProgress': 485 $log->info('\OC\Repair::startProgress: Starting ... ' . $event->getArgument(1) . ' (' . $event->getArgument(0) . ')', ['app' => 'updater']); 486 break; 487 case '\OC\Repair::advance': 488 $desc = $event->getArgument(1); 489 if (empty($desc)) { 490 $desc = ''; 491 } 492 $log->info('\OC\Repair::advance: ' . $desc . ' (' . $event->getArgument(0) . ')', ['app' => 'updater']); 493 494 break; 495 case '\OC\Repair::finishProgress': 496 $log->info('\OC\Repair::finishProgress', ['app' => 'updater']); 497 break; 498 case '\OC\Repair::step': 499 $log->info('\OC\Repair::step: Repair step: ' . $event->getArgument(0), ['app' => 'updater']); 500 break; 501 case '\OC\Repair::info': 502 $log->info('\OC\Repair::info: Repair info: ' . $event->getArgument(0), ['app' => 'updater']); 503 break; 504 case '\OC\Repair::warning': 505 $log->warning('\OC\Repair::warning: Repair warning: ' . $event->getArgument(0), ['app' => 'updater']); 506 break; 507 case '\OC\Repair::error': 508 $log->error('\OC\Repair::error: Repair error: ' . $event->getArgument(0), ['app' => 'updater']); 509 break; 510 } 511 }; 512 513 $dispatcher->addListener('\OC\Repair::startProgress', $repairListener); 514 $dispatcher->addListener('\OC\Repair::advance', $repairListener); 515 $dispatcher->addListener('\OC\Repair::finishProgress', $repairListener); 516 $dispatcher->addListener('\OC\Repair::step', $repairListener); 517 $dispatcher->addListener('\OC\Repair::info', $repairListener); 518 $dispatcher->addListener('\OC\Repair::warning', $repairListener); 519 $dispatcher->addListener('\OC\Repair::error', $repairListener); 520 521 522 $this->listen('\OC\Updater', 'maintenanceEnabled', function () use ($log) { 523 $log->info('\OC\Updater::maintenanceEnabled: Turned on maintenance mode', ['app' => 'updater']); 524 }); 525 $this->listen('\OC\Updater', 'maintenanceDisabled', function () use ($log) { 526 $log->info('\OC\Updater::maintenanceDisabled: Turned off maintenance mode', ['app' => 'updater']); 527 }); 528 $this->listen('\OC\Updater', 'maintenanceActive', function () use ($log) { 529 $log->info('\OC\Updater::maintenanceActive: Maintenance mode is kept active', ['app' => 'updater']); 530 }); 531 $this->listen('\OC\Updater', 'updateEnd', function ($success) use ($log) { 532 if ($success) { 533 $log->info('\OC\Updater::updateEnd: Update successful', ['app' => 'updater']); 534 } else { 535 $log->error('\OC\Updater::updateEnd: Update failed', ['app' => 'updater']); 536 } 537 }); 538 $this->listen('\OC\Updater', 'dbUpgradeBefore', function () use ($log) { 539 $log->info('\OC\Updater::dbUpgradeBefore: Updating database schema', ['app' => 'updater']); 540 }); 541 $this->listen('\OC\Updater', 'dbUpgrade', function () use ($log) { 542 $log->info('\OC\Updater::dbUpgrade: Updated database', ['app' => 'updater']); 543 }); 544 $this->listen('\OC\Updater', 'incompatibleAppDisabled', function ($app) use ($log) { 545 $log->info('\OC\Updater::incompatibleAppDisabled: Disabled incompatible app: ' . $app, ['app' => 'updater']); 546 }); 547 $this->listen('\OC\Updater', 'checkAppStoreAppBefore', function ($app) use ($log) { 548 $log->debug('\OC\Updater::checkAppStoreAppBefore: Checking for update of app "' . $app . '" in appstore', ['app' => 'updater']); 549 }); 550 $this->listen('\OC\Updater', 'upgradeAppStoreApp', function ($app) use ($log) { 551 $log->info('\OC\Updater::upgradeAppStoreApp: Update app "' . $app . '" from appstore', ['app' => 'updater']); 552 }); 553 $this->listen('\OC\Updater', 'checkAppStoreApp', function ($app) use ($log) { 554 $log->debug('\OC\Updater::checkAppStoreApp: Checked for update of app "' . $app . '" in appstore', ['app' => 'updater']); 555 }); 556 $this->listen('\OC\Updater', 'appSimulateUpdate', function ($app) use ($log) { 557 $log->info('\OC\Updater::appSimulateUpdate: Checking whether the database schema for <' . $app . '> can be updated (this can take a long time depending on the database size)', ['app' => 'updater']); 558 }); 559 $this->listen('\OC\Updater', 'appUpgradeStarted', function ($app) use ($log) { 560 $log->info('\OC\Updater::appUpgradeStarted: Updating <' . $app . '> ...', ['app' => 'updater']); 561 }); 562 $this->listen('\OC\Updater', 'appUpgrade', function ($app, $version) use ($log) { 563 $log->info('\OC\Updater::appUpgrade: Updated <' . $app . '> to ' . $version, ['app' => 'updater']); 564 }); 565 $this->listen('\OC\Updater', 'failure', function ($message) use ($log) { 566 $log->error('\OC\Updater::failure: ' . $message, ['app' => 'updater']); 567 }); 568 $this->listen('\OC\Updater', 'setDebugLogLevel', function () use ($log) { 569 $log->info('\OC\Updater::setDebugLogLevel: Set log level to debug', ['app' => 'updater']); 570 }); 571 $this->listen('\OC\Updater', 'resetLogLevel', function ($logLevel, $logLevelName) use ($log) { 572 $log->info('\OC\Updater::resetLogLevel: Reset log level to ' . $logLevelName . '(' . $logLevel . ')', ['app' => 'updater']); 573 }); 574 $this->listen('\OC\Updater', 'startCheckCodeIntegrity', function () use ($log) { 575 $log->info('\OC\Updater::startCheckCodeIntegrity: Starting code integrity check...', ['app' => 'updater']); 576 }); 577 $this->listen('\OC\Updater', 'finishedCheckCodeIntegrity', function () use ($log) { 578 $log->info('\OC\Updater::finishedCheckCodeIntegrity: Finished code integrity check', ['app' => 'updater']); 579 }); 580 } 581} 582