Completed
Push — master ( a1f4b5...d9e021 )
by Joas
41:47 queued 14s
created
lib/private/Updater.php 1 patch
Indentation   +468 added lines, -468 removed lines patch added patch discarded remove patch
@@ -45,472 +45,472 @@
 block discarded – undo
45 45
  *  - failure(string $message)
46 46
  */
47 47
 class Updater extends BasicEmitter {
48
-	private array $logLevelNames = [
49
-		0 => 'Debug',
50
-		1 => 'Info',
51
-		2 => 'Warning',
52
-		3 => 'Error',
53
-		4 => 'Fatal',
54
-	];
55
-
56
-	public function __construct(
57
-		private ServerVersion $serverVersion,
58
-		private IConfig $config,
59
-		private IAppConfig $appConfig,
60
-		private Checker $checker,
61
-		private ?LoggerInterface $log,
62
-		private Installer $installer,
63
-	) {
64
-	}
65
-
66
-	/**
67
-	 * runs the update actions in maintenance mode, does not upgrade the source files
68
-	 * except the main .htaccess file
69
-	 *
70
-	 * @return bool true if the operation succeeded, false otherwise
71
-	 */
72
-	public function upgrade(): bool {
73
-		$this->logAllEvents();
74
-
75
-		$logLevel = $this->config->getSystemValue('loglevel', ILogger::WARN);
76
-		$this->emit('\OC\Updater', 'setDebugLogLevel', [ $logLevel, $this->logLevelNames[$logLevel] ]);
77
-		$this->config->setSystemValue('loglevel', ILogger::DEBUG);
78
-
79
-		$wasMaintenanceModeEnabled = $this->config->getSystemValueBool('maintenance');
80
-
81
-		if (!$wasMaintenanceModeEnabled) {
82
-			$this->config->setSystemValue('maintenance', true);
83
-			$this->emit('\OC\Updater', 'maintenanceEnabled');
84
-		}
85
-
86
-		// Clear CAN_INSTALL file if not on git
87
-		if ($this->serverVersion->getChannel() !== 'git' && is_file(\OC::$configDir . '/CAN_INSTALL')) {
88
-			if (!unlink(\OC::$configDir . '/CAN_INSTALL')) {
89
-				$this->log->error('Could not cleanup CAN_INSTALL from your config folder. Please remove this file manually.');
90
-			}
91
-		}
92
-
93
-		$installedVersion = $this->config->getSystemValueString('version', '0.0.0');
94
-		$currentVersion = implode('.', $this->serverVersion->getVersion());
95
-
96
-		$this->log->debug('starting upgrade from ' . $installedVersion . ' to ' . $currentVersion, ['app' => 'core']);
97
-
98
-		$success = true;
99
-		try {
100
-			$this->doUpgrade($currentVersion, $installedVersion);
101
-		} catch (HintException $exception) {
102
-			$this->log->error($exception->getMessage(), [
103
-				'exception' => $exception,
104
-			]);
105
-			$this->emit('\OC\Updater', 'failure', [$exception->getMessage() . ': ' . $exception->getHint()]);
106
-			$success = false;
107
-		} catch (\Exception $exception) {
108
-			$this->log->error($exception->getMessage(), [
109
-				'exception' => $exception,
110
-			]);
111
-			$this->emit('\OC\Updater', 'failure', [get_class($exception) . ': ' . $exception->getMessage()]);
112
-			$success = false;
113
-		}
114
-
115
-		$this->emit('\OC\Updater', 'updateEnd', [$success]);
116
-
117
-		if (!$wasMaintenanceModeEnabled && $success) {
118
-			$this->config->setSystemValue('maintenance', false);
119
-			$this->emit('\OC\Updater', 'maintenanceDisabled');
120
-		} else {
121
-			$this->emit('\OC\Updater', 'maintenanceActive');
122
-		}
123
-
124
-		$this->emit('\OC\Updater', 'resetLogLevel', [ $logLevel, $this->logLevelNames[$logLevel] ]);
125
-		$this->config->setSystemValue('loglevel', $logLevel);
126
-		$this->config->setSystemValue('installed', true);
127
-
128
-		return $success;
129
-	}
130
-
131
-	/**
132
-	 * Return version from which this version is allowed to upgrade from
133
-	 *
134
-	 * @return array allowed previous versions per vendor
135
-	 */
136
-	private function getAllowedPreviousVersions(): array {
137
-		// this should really be a JSON file
138
-		require \OC::$SERVERROOT . '/version.php';
139
-		/** @var array $OC_VersionCanBeUpgradedFrom */
140
-		return $OC_VersionCanBeUpgradedFrom;
141
-	}
142
-
143
-	/**
144
-	 * Return vendor from which this version was published
145
-	 *
146
-	 * @return string Get the vendor
147
-	 */
148
-	private function getVendor(): string {
149
-		// this should really be a JSON file
150
-		require \OC::$SERVERROOT . '/version.php';
151
-		/** @var string $vendor */
152
-		return (string)$vendor;
153
-	}
154
-
155
-	/**
156
-	 * Whether an upgrade to a specified version is possible
157
-	 * @param string $oldVersion
158
-	 * @param string $newVersion
159
-	 * @param array $allowedPreviousVersions
160
-	 * @return bool
161
-	 */
162
-	public function isUpgradePossible(string $oldVersion, string $newVersion, array $allowedPreviousVersions): bool {
163
-		$version = explode('.', $oldVersion);
164
-		$majorMinor = $version[0] . '.' . $version[1];
165
-
166
-		$currentVendor = $this->config->getAppValue('core', 'vendor', '');
167
-
168
-		// Vendor was not set correctly on install, so we have to white-list known versions
169
-		if ($currentVendor === '' && (
170
-			isset($allowedPreviousVersions['owncloud'][$oldVersion])
171
-			|| isset($allowedPreviousVersions['owncloud'][$majorMinor])
172
-		)) {
173
-			$currentVendor = 'owncloud';
174
-			$this->config->setAppValue('core', 'vendor', $currentVendor);
175
-		}
176
-
177
-		if ($currentVendor === 'nextcloud') {
178
-			return isset($allowedPreviousVersions[$currentVendor][$majorMinor])
179
-				&& (version_compare($oldVersion, $newVersion, '<=')
180
-					|| $this->config->getSystemValueBool('debug', false));
181
-		}
182
-
183
-		// Check if the instance can be migrated
184
-		return isset($allowedPreviousVersions[$currentVendor][$majorMinor])
185
-			|| isset($allowedPreviousVersions[$currentVendor][$oldVersion]);
186
-	}
187
-
188
-	/**
189
-	 * runs the update actions in maintenance mode, does not upgrade the source files
190
-	 * except the main .htaccess file
191
-	 *
192
-	 * @param string $currentVersion current version to upgrade to
193
-	 * @param string $installedVersion previous version from which to upgrade from
194
-	 *
195
-	 * @throws \Exception
196
-	 */
197
-	private function doUpgrade(string $currentVersion, string $installedVersion): void {
198
-		// Stop update if the update is over several major versions
199
-		$allowedPreviousVersions = $this->getAllowedPreviousVersions();
200
-		if (!$this->isUpgradePossible($installedVersion, $currentVersion, $allowedPreviousVersions)) {
201
-			throw new \Exception('Updates between multiple major versions and downgrades are unsupported.');
202
-		}
203
-
204
-		// Update .htaccess files
205
-		try {
206
-			Setup::updateHtaccess();
207
-			Setup::protectDataDirectory();
208
-		} catch (\Exception $e) {
209
-			throw new \Exception($e->getMessage());
210
-		}
211
-
212
-		// create empty file in data dir, so we can later find
213
-		// out that this is indeed a Nextcloud data directory
214
-		// (in case it didn't exist before)
215
-		file_put_contents(
216
-			$this->config->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data') . '/.ncdata',
217
-			"# Nextcloud data directory\n# Do not change this file",
218
-		);
219
-
220
-		// pre-upgrade repairs
221
-		$repair = \OCP\Server::get(Repair::class);
222
-		$repair->setRepairSteps(Repair::getBeforeUpgradeRepairSteps());
223
-		$repair->run();
224
-
225
-		$this->doCoreUpgrade();
226
-
227
-		try {
228
-			// TODO: replace with the new repair step mechanism https://github.com/owncloud/core/pull/24378
229
-			Setup::installBackgroundJobs();
230
-		} catch (\Exception $e) {
231
-			throw new \Exception($e->getMessage());
232
-		}
233
-
234
-		// update all shipped apps
235
-		$this->checkAppsRequirements();
236
-		$this->doAppUpgrade();
237
-
238
-		// Update the appfetchers version so it downloads the correct list from the appstore
239
-		\OC::$server->get(AppFetcher::class)->setVersion($currentVersion);
240
-
241
-		/** @var AppManager $appManager */
242
-		$appManager = \OC::$server->getAppManager();
243
-
244
-		// upgrade appstore apps
245
-		$this->upgradeAppStoreApps($appManager->getEnabledApps());
246
-		$autoDisabledApps = $appManager->getAutoDisabledApps();
247
-		if (!empty($autoDisabledApps)) {
248
-			$this->upgradeAppStoreApps(array_keys($autoDisabledApps), $autoDisabledApps);
249
-		}
250
-
251
-		// install new shipped apps on upgrade
252
-		$errors = Installer::installShippedApps(true);
253
-		foreach ($errors as $appId => $exception) {
254
-			/** @var \Exception $exception */
255
-			$this->log->error($exception->getMessage(), [
256
-				'exception' => $exception,
257
-				'app' => $appId,
258
-			]);
259
-			$this->emit('\OC\Updater', 'failure', [$appId . ': ' . $exception->getMessage()]);
260
-		}
261
-
262
-		// post-upgrade repairs
263
-		$repair = \OCP\Server::get(Repair::class);
264
-		$repair->setRepairSteps(Repair::getRepairSteps());
265
-		$repair->run();
266
-
267
-		//Invalidate update feed
268
-		$this->appConfig->setValueInt('core', 'lastupdatedat', 0);
269
-
270
-		// Check for code integrity if not disabled
271
-		if (\OC::$server->getIntegrityCodeChecker()->isCodeCheckEnforced()) {
272
-			$this->emit('\OC\Updater', 'startCheckCodeIntegrity');
273
-			$this->checker->runInstanceVerification();
274
-			$this->emit('\OC\Updater', 'finishedCheckCodeIntegrity');
275
-		}
276
-
277
-		// only set the final version if everything went well
278
-		$this->config->setSystemValue('version', implode('.', Util::getVersion()));
279
-		$this->config->setAppValue('core', 'vendor', $this->getVendor());
280
-	}
281
-
282
-	protected function doCoreUpgrade(): void {
283
-		$this->emit('\OC\Updater', 'dbUpgradeBefore');
284
-
285
-		// execute core migrations
286
-		$ms = new MigrationService('core', \OC::$server->get(Connection::class));
287
-		$ms->migrate();
288
-
289
-		$this->emit('\OC\Updater', 'dbUpgrade');
290
-	}
291
-
292
-	/**
293
-	 * upgrades all apps within a major ownCloud upgrade. Also loads "priority"
294
-	 * (types authentication, filesystem, logging, in that order) afterwards.
295
-	 *
296
-	 * @throws NeedsUpdateException
297
-	 */
298
-	protected function doAppUpgrade(): void {
299
-		$apps = \OC_App::getEnabledApps();
300
-		$priorityTypes = ['authentication', 'extended_authentication', 'filesystem', 'logging'];
301
-		$pseudoOtherType = 'other';
302
-		$stacks = [$pseudoOtherType => []];
303
-
304
-		foreach ($apps as $appId) {
305
-			$priorityType = false;
306
-			foreach ($priorityTypes as $type) {
307
-				if (!isset($stacks[$type])) {
308
-					$stacks[$type] = [];
309
-				}
310
-				if (\OC_App::isType($appId, [$type])) {
311
-					$stacks[$type][] = $appId;
312
-					$priorityType = true;
313
-					break;
314
-				}
315
-			}
316
-			if (!$priorityType) {
317
-				$stacks[$pseudoOtherType][] = $appId;
318
-			}
319
-		}
320
-		foreach (array_merge($priorityTypes, [$pseudoOtherType]) as $type) {
321
-			$stack = $stacks[$type];
322
-			foreach ($stack as $appId) {
323
-				if (\OC_App::shouldUpgrade($appId)) {
324
-					$this->emit('\OC\Updater', 'appUpgradeStarted', [$appId, \OCP\Server::get(IAppManager::class)->getAppVersion($appId)]);
325
-					\OC_App::updateApp($appId);
326
-					$this->emit('\OC\Updater', 'appUpgrade', [$appId, \OCP\Server::get(IAppManager::class)->getAppVersion($appId)]);
327
-				}
328
-				if ($type !== $pseudoOtherType) {
329
-					// load authentication, filesystem and logging apps after
330
-					// upgrading them. Other apps my need to rely on modifying
331
-					// user and/or filesystem aspects.
332
-					\OC_App::loadApp($appId);
333
-				}
334
-			}
335
-		}
336
-	}
337
-
338
-	/**
339
-	 * check if the current enabled apps are compatible with the current
340
-	 * ownCloud version. disable them if not.
341
-	 * This is important if you upgrade ownCloud and have non ported 3rd
342
-	 * party apps installed.
343
-	 *
344
-	 * @throws \Exception
345
-	 */
346
-	private function checkAppsRequirements(): void {
347
-		$isCoreUpgrade = $this->isCodeUpgrade();
348
-		$apps = OC_App::getEnabledApps();
349
-		$version = implode('.', Util::getVersion());
350
-		$appManager = \OC::$server->getAppManager();
351
-		foreach ($apps as $app) {
352
-			// check if the app is compatible with this version of Nextcloud
353
-			$info = $appManager->getAppInfo($app);
354
-			if ($info === null || !OC_App::isAppCompatible($version, $info)) {
355
-				if ($appManager->isShipped($app)) {
356
-					throw new \UnexpectedValueException('The files of the app "' . $app . '" were not correctly replaced before running the update');
357
-				}
358
-				$appManager->disableApp($app, true);
359
-				$this->emit('\OC\Updater', 'incompatibleAppDisabled', [$app]);
360
-			}
361
-		}
362
-	}
363
-
364
-	/**
365
-	 * @return bool
366
-	 */
367
-	private function isCodeUpgrade(): bool {
368
-		$installedVersion = $this->config->getSystemValueString('version', '0.0.0');
369
-		$currentVersion = implode('.', Util::getVersion());
370
-		if (version_compare($currentVersion, $installedVersion, '>')) {
371
-			return true;
372
-		}
373
-		return false;
374
-	}
375
-
376
-	/**
377
-	 * @param array $apps
378
-	 * @param array $previousEnableStates
379
-	 * @throws \Exception
380
-	 */
381
-	private function upgradeAppStoreApps(array $apps, array $previousEnableStates = []): void {
382
-		foreach ($apps as $app) {
383
-			try {
384
-				$this->emit('\OC\Updater', 'checkAppStoreAppBefore', [$app]);
385
-				if ($this->installer->isUpdateAvailable($app)) {
386
-					$this->emit('\OC\Updater', 'upgradeAppStoreApp', [$app]);
387
-					$this->installer->updateAppstoreApp($app);
388
-				} elseif (!empty($previousEnableStates)) {
389
-					/**
390
-					 * When updating a local app we still need to run updateApp
391
-					 * so that repair steps and migrations are correctly executed
392
-					 * Ref: https://github.com/nextcloud/server/issues/53985
393
-					 */
394
-					\OC_App::updateApp($app);
395
-				}
396
-				$this->emit('\OC\Updater', 'checkAppStoreApp', [$app]);
397
-
398
-				if (!empty($previousEnableStates)) {
399
-					$ocApp = new \OC_App();
400
-					if (!empty($previousEnableStates[$app]) && is_array($previousEnableStates[$app])) {
401
-						$ocApp->enable($app, $previousEnableStates[$app]);
402
-					} else {
403
-						$ocApp->enable($app);
404
-					}
405
-				}
406
-			} catch (\Exception $ex) {
407
-				$this->log->error($ex->getMessage(), [
408
-					'exception' => $ex,
409
-				]);
410
-			}
411
-		}
412
-	}
413
-
414
-	private function logAllEvents(): void {
415
-		$log = $this->log;
416
-
417
-		/** @var IEventDispatcher $dispatcher */
418
-		$dispatcher = \OC::$server->get(IEventDispatcher::class);
419
-		$dispatcher->addListener(
420
-			MigratorExecuteSqlEvent::class,
421
-			function (MigratorExecuteSqlEvent $event) use ($log): void {
422
-				$log->info(get_class($event) . ': ' . $event->getSql() . ' (' . $event->getCurrentStep() . ' of ' . $event->getMaxStep() . ')', ['app' => 'updater']);
423
-			}
424
-		);
425
-
426
-		$repairListener = function (Event $event) use ($log): void {
427
-			if ($event instanceof RepairStartEvent) {
428
-				$log->info(get_class($event) . ': Starting ... ' . $event->getMaxStep() . ' (' . $event->getCurrentStepName() . ')', ['app' => 'updater']);
429
-			} elseif ($event instanceof RepairAdvanceEvent) {
430
-				$desc = $event->getDescription();
431
-				if (empty($desc)) {
432
-					$desc = '';
433
-				}
434
-				$log->info(get_class($event) . ': ' . $desc . ' (' . $event->getIncrement() . ')', ['app' => 'updater']);
435
-			} elseif ($event instanceof RepairFinishEvent) {
436
-				$log->info(get_class($event), ['app' => 'updater']);
437
-			} elseif ($event instanceof RepairStepEvent) {
438
-				$log->info(get_class($event) . ': Repair step: ' . $event->getStepName(), ['app' => 'updater']);
439
-			} elseif ($event instanceof RepairInfoEvent) {
440
-				$log->info(get_class($event) . ': Repair info: ' . $event->getMessage(), ['app' => 'updater']);
441
-			} elseif ($event instanceof RepairWarningEvent) {
442
-				$log->warning(get_class($event) . ': Repair warning: ' . $event->getMessage(), ['app' => 'updater']);
443
-			} elseif ($event instanceof RepairErrorEvent) {
444
-				$log->error(get_class($event) . ': Repair error: ' . $event->getMessage(), ['app' => 'updater']);
445
-			}
446
-		};
447
-
448
-		$dispatcher->addListener(RepairStartEvent::class, $repairListener);
449
-		$dispatcher->addListener(RepairAdvanceEvent::class, $repairListener);
450
-		$dispatcher->addListener(RepairFinishEvent::class, $repairListener);
451
-		$dispatcher->addListener(RepairStepEvent::class, $repairListener);
452
-		$dispatcher->addListener(RepairInfoEvent::class, $repairListener);
453
-		$dispatcher->addListener(RepairWarningEvent::class, $repairListener);
454
-		$dispatcher->addListener(RepairErrorEvent::class, $repairListener);
455
-
456
-
457
-		$this->listen('\OC\Updater', 'maintenanceEnabled', function () use ($log) {
458
-			$log->info('\OC\Updater::maintenanceEnabled: Turned on maintenance mode', ['app' => 'updater']);
459
-		});
460
-		$this->listen('\OC\Updater', 'maintenanceDisabled', function () use ($log) {
461
-			$log->info('\OC\Updater::maintenanceDisabled: Turned off maintenance mode', ['app' => 'updater']);
462
-		});
463
-		$this->listen('\OC\Updater', 'maintenanceActive', function () use ($log) {
464
-			$log->info('\OC\Updater::maintenanceActive: Maintenance mode is kept active', ['app' => 'updater']);
465
-		});
466
-		$this->listen('\OC\Updater', 'updateEnd', function ($success) use ($log) {
467
-			if ($success) {
468
-				$log->info('\OC\Updater::updateEnd: Update successful', ['app' => 'updater']);
469
-			} else {
470
-				$log->error('\OC\Updater::updateEnd: Update failed', ['app' => 'updater']);
471
-			}
472
-		});
473
-		$this->listen('\OC\Updater', 'dbUpgradeBefore', function () use ($log) {
474
-			$log->info('\OC\Updater::dbUpgradeBefore: Updating database schema', ['app' => 'updater']);
475
-		});
476
-		$this->listen('\OC\Updater', 'dbUpgrade', function () use ($log) {
477
-			$log->info('\OC\Updater::dbUpgrade: Updated database', ['app' => 'updater']);
478
-		});
479
-		$this->listen('\OC\Updater', 'incompatibleAppDisabled', function ($app) use ($log) {
480
-			$log->info('\OC\Updater::incompatibleAppDisabled: Disabled incompatible app: ' . $app, ['app' => 'updater']);
481
-		});
482
-		$this->listen('\OC\Updater', 'checkAppStoreAppBefore', function ($app) use ($log) {
483
-			$log->debug('\OC\Updater::checkAppStoreAppBefore: Checking for update of app "' . $app . '" in appstore', ['app' => 'updater']);
484
-		});
485
-		$this->listen('\OC\Updater', 'upgradeAppStoreApp', function ($app) use ($log) {
486
-			$log->info('\OC\Updater::upgradeAppStoreApp: Update app "' . $app . '" from appstore', ['app' => 'updater']);
487
-		});
488
-		$this->listen('\OC\Updater', 'checkAppStoreApp', function ($app) use ($log) {
489
-			$log->debug('\OC\Updater::checkAppStoreApp: Checked for update of app "' . $app . '" in appstore', ['app' => 'updater']);
490
-		});
491
-		$this->listen('\OC\Updater', 'appSimulateUpdate', function ($app) use ($log) {
492
-			$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']);
493
-		});
494
-		$this->listen('\OC\Updater', 'appUpgradeStarted', function ($app) use ($log) {
495
-			$log->info('\OC\Updater::appUpgradeStarted: Updating <' . $app . '> ...', ['app' => 'updater']);
496
-		});
497
-		$this->listen('\OC\Updater', 'appUpgrade', function ($app, $version) use ($log) {
498
-			$log->info('\OC\Updater::appUpgrade: Updated <' . $app . '> to ' . $version, ['app' => 'updater']);
499
-		});
500
-		$this->listen('\OC\Updater', 'failure', function ($message) use ($log) {
501
-			$log->error('\OC\Updater::failure: ' . $message, ['app' => 'updater']);
502
-		});
503
-		$this->listen('\OC\Updater', 'setDebugLogLevel', function () use ($log) {
504
-			$log->info('\OC\Updater::setDebugLogLevel: Set log level to debug', ['app' => 'updater']);
505
-		});
506
-		$this->listen('\OC\Updater', 'resetLogLevel', function ($logLevel, $logLevelName) use ($log) {
507
-			$log->info('\OC\Updater::resetLogLevel: Reset log level to ' . $logLevelName . '(' . $logLevel . ')', ['app' => 'updater']);
508
-		});
509
-		$this->listen('\OC\Updater', 'startCheckCodeIntegrity', function () use ($log) {
510
-			$log->info('\OC\Updater::startCheckCodeIntegrity: Starting code integrity check...', ['app' => 'updater']);
511
-		});
512
-		$this->listen('\OC\Updater', 'finishedCheckCodeIntegrity', function () use ($log) {
513
-			$log->info('\OC\Updater::finishedCheckCodeIntegrity: Finished code integrity check', ['app' => 'updater']);
514
-		});
515
-	}
48
+    private array $logLevelNames = [
49
+        0 => 'Debug',
50
+        1 => 'Info',
51
+        2 => 'Warning',
52
+        3 => 'Error',
53
+        4 => 'Fatal',
54
+    ];
55
+
56
+    public function __construct(
57
+        private ServerVersion $serverVersion,
58
+        private IConfig $config,
59
+        private IAppConfig $appConfig,
60
+        private Checker $checker,
61
+        private ?LoggerInterface $log,
62
+        private Installer $installer,
63
+    ) {
64
+    }
65
+
66
+    /**
67
+     * runs the update actions in maintenance mode, does not upgrade the source files
68
+     * except the main .htaccess file
69
+     *
70
+     * @return bool true if the operation succeeded, false otherwise
71
+     */
72
+    public function upgrade(): bool {
73
+        $this->logAllEvents();
74
+
75
+        $logLevel = $this->config->getSystemValue('loglevel', ILogger::WARN);
76
+        $this->emit('\OC\Updater', 'setDebugLogLevel', [ $logLevel, $this->logLevelNames[$logLevel] ]);
77
+        $this->config->setSystemValue('loglevel', ILogger::DEBUG);
78
+
79
+        $wasMaintenanceModeEnabled = $this->config->getSystemValueBool('maintenance');
80
+
81
+        if (!$wasMaintenanceModeEnabled) {
82
+            $this->config->setSystemValue('maintenance', true);
83
+            $this->emit('\OC\Updater', 'maintenanceEnabled');
84
+        }
85
+
86
+        // Clear CAN_INSTALL file if not on git
87
+        if ($this->serverVersion->getChannel() !== 'git' && is_file(\OC::$configDir . '/CAN_INSTALL')) {
88
+            if (!unlink(\OC::$configDir . '/CAN_INSTALL')) {
89
+                $this->log->error('Could not cleanup CAN_INSTALL from your config folder. Please remove this file manually.');
90
+            }
91
+        }
92
+
93
+        $installedVersion = $this->config->getSystemValueString('version', '0.0.0');
94
+        $currentVersion = implode('.', $this->serverVersion->getVersion());
95
+
96
+        $this->log->debug('starting upgrade from ' . $installedVersion . ' to ' . $currentVersion, ['app' => 'core']);
97
+
98
+        $success = true;
99
+        try {
100
+            $this->doUpgrade($currentVersion, $installedVersion);
101
+        } catch (HintException $exception) {
102
+            $this->log->error($exception->getMessage(), [
103
+                'exception' => $exception,
104
+            ]);
105
+            $this->emit('\OC\Updater', 'failure', [$exception->getMessage() . ': ' . $exception->getHint()]);
106
+            $success = false;
107
+        } catch (\Exception $exception) {
108
+            $this->log->error($exception->getMessage(), [
109
+                'exception' => $exception,
110
+            ]);
111
+            $this->emit('\OC\Updater', 'failure', [get_class($exception) . ': ' . $exception->getMessage()]);
112
+            $success = false;
113
+        }
114
+
115
+        $this->emit('\OC\Updater', 'updateEnd', [$success]);
116
+
117
+        if (!$wasMaintenanceModeEnabled && $success) {
118
+            $this->config->setSystemValue('maintenance', false);
119
+            $this->emit('\OC\Updater', 'maintenanceDisabled');
120
+        } else {
121
+            $this->emit('\OC\Updater', 'maintenanceActive');
122
+        }
123
+
124
+        $this->emit('\OC\Updater', 'resetLogLevel', [ $logLevel, $this->logLevelNames[$logLevel] ]);
125
+        $this->config->setSystemValue('loglevel', $logLevel);
126
+        $this->config->setSystemValue('installed', true);
127
+
128
+        return $success;
129
+    }
130
+
131
+    /**
132
+     * Return version from which this version is allowed to upgrade from
133
+     *
134
+     * @return array allowed previous versions per vendor
135
+     */
136
+    private function getAllowedPreviousVersions(): array {
137
+        // this should really be a JSON file
138
+        require \OC::$SERVERROOT . '/version.php';
139
+        /** @var array $OC_VersionCanBeUpgradedFrom */
140
+        return $OC_VersionCanBeUpgradedFrom;
141
+    }
142
+
143
+    /**
144
+     * Return vendor from which this version was published
145
+     *
146
+     * @return string Get the vendor
147
+     */
148
+    private function getVendor(): string {
149
+        // this should really be a JSON file
150
+        require \OC::$SERVERROOT . '/version.php';
151
+        /** @var string $vendor */
152
+        return (string)$vendor;
153
+    }
154
+
155
+    /**
156
+     * Whether an upgrade to a specified version is possible
157
+     * @param string $oldVersion
158
+     * @param string $newVersion
159
+     * @param array $allowedPreviousVersions
160
+     * @return bool
161
+     */
162
+    public function isUpgradePossible(string $oldVersion, string $newVersion, array $allowedPreviousVersions): bool {
163
+        $version = explode('.', $oldVersion);
164
+        $majorMinor = $version[0] . '.' . $version[1];
165
+
166
+        $currentVendor = $this->config->getAppValue('core', 'vendor', '');
167
+
168
+        // Vendor was not set correctly on install, so we have to white-list known versions
169
+        if ($currentVendor === '' && (
170
+            isset($allowedPreviousVersions['owncloud'][$oldVersion])
171
+            || isset($allowedPreviousVersions['owncloud'][$majorMinor])
172
+        )) {
173
+            $currentVendor = 'owncloud';
174
+            $this->config->setAppValue('core', 'vendor', $currentVendor);
175
+        }
176
+
177
+        if ($currentVendor === 'nextcloud') {
178
+            return isset($allowedPreviousVersions[$currentVendor][$majorMinor])
179
+                && (version_compare($oldVersion, $newVersion, '<=')
180
+                    || $this->config->getSystemValueBool('debug', false));
181
+        }
182
+
183
+        // Check if the instance can be migrated
184
+        return isset($allowedPreviousVersions[$currentVendor][$majorMinor])
185
+            || isset($allowedPreviousVersions[$currentVendor][$oldVersion]);
186
+    }
187
+
188
+    /**
189
+     * runs the update actions in maintenance mode, does not upgrade the source files
190
+     * except the main .htaccess file
191
+     *
192
+     * @param string $currentVersion current version to upgrade to
193
+     * @param string $installedVersion previous version from which to upgrade from
194
+     *
195
+     * @throws \Exception
196
+     */
197
+    private function doUpgrade(string $currentVersion, string $installedVersion): void {
198
+        // Stop update if the update is over several major versions
199
+        $allowedPreviousVersions = $this->getAllowedPreviousVersions();
200
+        if (!$this->isUpgradePossible($installedVersion, $currentVersion, $allowedPreviousVersions)) {
201
+            throw new \Exception('Updates between multiple major versions and downgrades are unsupported.');
202
+        }
203
+
204
+        // Update .htaccess files
205
+        try {
206
+            Setup::updateHtaccess();
207
+            Setup::protectDataDirectory();
208
+        } catch (\Exception $e) {
209
+            throw new \Exception($e->getMessage());
210
+        }
211
+
212
+        // create empty file in data dir, so we can later find
213
+        // out that this is indeed a Nextcloud data directory
214
+        // (in case it didn't exist before)
215
+        file_put_contents(
216
+            $this->config->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data') . '/.ncdata',
217
+            "# Nextcloud data directory\n# Do not change this file",
218
+        );
219
+
220
+        // pre-upgrade repairs
221
+        $repair = \OCP\Server::get(Repair::class);
222
+        $repair->setRepairSteps(Repair::getBeforeUpgradeRepairSteps());
223
+        $repair->run();
224
+
225
+        $this->doCoreUpgrade();
226
+
227
+        try {
228
+            // TODO: replace with the new repair step mechanism https://github.com/owncloud/core/pull/24378
229
+            Setup::installBackgroundJobs();
230
+        } catch (\Exception $e) {
231
+            throw new \Exception($e->getMessage());
232
+        }
233
+
234
+        // update all shipped apps
235
+        $this->checkAppsRequirements();
236
+        $this->doAppUpgrade();
237
+
238
+        // Update the appfetchers version so it downloads the correct list from the appstore
239
+        \OC::$server->get(AppFetcher::class)->setVersion($currentVersion);
240
+
241
+        /** @var AppManager $appManager */
242
+        $appManager = \OC::$server->getAppManager();
243
+
244
+        // upgrade appstore apps
245
+        $this->upgradeAppStoreApps($appManager->getEnabledApps());
246
+        $autoDisabledApps = $appManager->getAutoDisabledApps();
247
+        if (!empty($autoDisabledApps)) {
248
+            $this->upgradeAppStoreApps(array_keys($autoDisabledApps), $autoDisabledApps);
249
+        }
250
+
251
+        // install new shipped apps on upgrade
252
+        $errors = Installer::installShippedApps(true);
253
+        foreach ($errors as $appId => $exception) {
254
+            /** @var \Exception $exception */
255
+            $this->log->error($exception->getMessage(), [
256
+                'exception' => $exception,
257
+                'app' => $appId,
258
+            ]);
259
+            $this->emit('\OC\Updater', 'failure', [$appId . ': ' . $exception->getMessage()]);
260
+        }
261
+
262
+        // post-upgrade repairs
263
+        $repair = \OCP\Server::get(Repair::class);
264
+        $repair->setRepairSteps(Repair::getRepairSteps());
265
+        $repair->run();
266
+
267
+        //Invalidate update feed
268
+        $this->appConfig->setValueInt('core', 'lastupdatedat', 0);
269
+
270
+        // Check for code integrity if not disabled
271
+        if (\OC::$server->getIntegrityCodeChecker()->isCodeCheckEnforced()) {
272
+            $this->emit('\OC\Updater', 'startCheckCodeIntegrity');
273
+            $this->checker->runInstanceVerification();
274
+            $this->emit('\OC\Updater', 'finishedCheckCodeIntegrity');
275
+        }
276
+
277
+        // only set the final version if everything went well
278
+        $this->config->setSystemValue('version', implode('.', Util::getVersion()));
279
+        $this->config->setAppValue('core', 'vendor', $this->getVendor());
280
+    }
281
+
282
+    protected function doCoreUpgrade(): void {
283
+        $this->emit('\OC\Updater', 'dbUpgradeBefore');
284
+
285
+        // execute core migrations
286
+        $ms = new MigrationService('core', \OC::$server->get(Connection::class));
287
+        $ms->migrate();
288
+
289
+        $this->emit('\OC\Updater', 'dbUpgrade');
290
+    }
291
+
292
+    /**
293
+     * upgrades all apps within a major ownCloud upgrade. Also loads "priority"
294
+     * (types authentication, filesystem, logging, in that order) afterwards.
295
+     *
296
+     * @throws NeedsUpdateException
297
+     */
298
+    protected function doAppUpgrade(): void {
299
+        $apps = \OC_App::getEnabledApps();
300
+        $priorityTypes = ['authentication', 'extended_authentication', 'filesystem', 'logging'];
301
+        $pseudoOtherType = 'other';
302
+        $stacks = [$pseudoOtherType => []];
303
+
304
+        foreach ($apps as $appId) {
305
+            $priorityType = false;
306
+            foreach ($priorityTypes as $type) {
307
+                if (!isset($stacks[$type])) {
308
+                    $stacks[$type] = [];
309
+                }
310
+                if (\OC_App::isType($appId, [$type])) {
311
+                    $stacks[$type][] = $appId;
312
+                    $priorityType = true;
313
+                    break;
314
+                }
315
+            }
316
+            if (!$priorityType) {
317
+                $stacks[$pseudoOtherType][] = $appId;
318
+            }
319
+        }
320
+        foreach (array_merge($priorityTypes, [$pseudoOtherType]) as $type) {
321
+            $stack = $stacks[$type];
322
+            foreach ($stack as $appId) {
323
+                if (\OC_App::shouldUpgrade($appId)) {
324
+                    $this->emit('\OC\Updater', 'appUpgradeStarted', [$appId, \OCP\Server::get(IAppManager::class)->getAppVersion($appId)]);
325
+                    \OC_App::updateApp($appId);
326
+                    $this->emit('\OC\Updater', 'appUpgrade', [$appId, \OCP\Server::get(IAppManager::class)->getAppVersion($appId)]);
327
+                }
328
+                if ($type !== $pseudoOtherType) {
329
+                    // load authentication, filesystem and logging apps after
330
+                    // upgrading them. Other apps my need to rely on modifying
331
+                    // user and/or filesystem aspects.
332
+                    \OC_App::loadApp($appId);
333
+                }
334
+            }
335
+        }
336
+    }
337
+
338
+    /**
339
+     * check if the current enabled apps are compatible with the current
340
+     * ownCloud version. disable them if not.
341
+     * This is important if you upgrade ownCloud and have non ported 3rd
342
+     * party apps installed.
343
+     *
344
+     * @throws \Exception
345
+     */
346
+    private function checkAppsRequirements(): void {
347
+        $isCoreUpgrade = $this->isCodeUpgrade();
348
+        $apps = OC_App::getEnabledApps();
349
+        $version = implode('.', Util::getVersion());
350
+        $appManager = \OC::$server->getAppManager();
351
+        foreach ($apps as $app) {
352
+            // check if the app is compatible with this version of Nextcloud
353
+            $info = $appManager->getAppInfo($app);
354
+            if ($info === null || !OC_App::isAppCompatible($version, $info)) {
355
+                if ($appManager->isShipped($app)) {
356
+                    throw new \UnexpectedValueException('The files of the app "' . $app . '" were not correctly replaced before running the update');
357
+                }
358
+                $appManager->disableApp($app, true);
359
+                $this->emit('\OC\Updater', 'incompatibleAppDisabled', [$app]);
360
+            }
361
+        }
362
+    }
363
+
364
+    /**
365
+     * @return bool
366
+     */
367
+    private function isCodeUpgrade(): bool {
368
+        $installedVersion = $this->config->getSystemValueString('version', '0.0.0');
369
+        $currentVersion = implode('.', Util::getVersion());
370
+        if (version_compare($currentVersion, $installedVersion, '>')) {
371
+            return true;
372
+        }
373
+        return false;
374
+    }
375
+
376
+    /**
377
+     * @param array $apps
378
+     * @param array $previousEnableStates
379
+     * @throws \Exception
380
+     */
381
+    private function upgradeAppStoreApps(array $apps, array $previousEnableStates = []): void {
382
+        foreach ($apps as $app) {
383
+            try {
384
+                $this->emit('\OC\Updater', 'checkAppStoreAppBefore', [$app]);
385
+                if ($this->installer->isUpdateAvailable($app)) {
386
+                    $this->emit('\OC\Updater', 'upgradeAppStoreApp', [$app]);
387
+                    $this->installer->updateAppstoreApp($app);
388
+                } elseif (!empty($previousEnableStates)) {
389
+                    /**
390
+                     * When updating a local app we still need to run updateApp
391
+                     * so that repair steps and migrations are correctly executed
392
+                     * Ref: https://github.com/nextcloud/server/issues/53985
393
+                     */
394
+                    \OC_App::updateApp($app);
395
+                }
396
+                $this->emit('\OC\Updater', 'checkAppStoreApp', [$app]);
397
+
398
+                if (!empty($previousEnableStates)) {
399
+                    $ocApp = new \OC_App();
400
+                    if (!empty($previousEnableStates[$app]) && is_array($previousEnableStates[$app])) {
401
+                        $ocApp->enable($app, $previousEnableStates[$app]);
402
+                    } else {
403
+                        $ocApp->enable($app);
404
+                    }
405
+                }
406
+            } catch (\Exception $ex) {
407
+                $this->log->error($ex->getMessage(), [
408
+                    'exception' => $ex,
409
+                ]);
410
+            }
411
+        }
412
+    }
413
+
414
+    private function logAllEvents(): void {
415
+        $log = $this->log;
416
+
417
+        /** @var IEventDispatcher $dispatcher */
418
+        $dispatcher = \OC::$server->get(IEventDispatcher::class);
419
+        $dispatcher->addListener(
420
+            MigratorExecuteSqlEvent::class,
421
+            function (MigratorExecuteSqlEvent $event) use ($log): void {
422
+                $log->info(get_class($event) . ': ' . $event->getSql() . ' (' . $event->getCurrentStep() . ' of ' . $event->getMaxStep() . ')', ['app' => 'updater']);
423
+            }
424
+        );
425
+
426
+        $repairListener = function (Event $event) use ($log): void {
427
+            if ($event instanceof RepairStartEvent) {
428
+                $log->info(get_class($event) . ': Starting ... ' . $event->getMaxStep() . ' (' . $event->getCurrentStepName() . ')', ['app' => 'updater']);
429
+            } elseif ($event instanceof RepairAdvanceEvent) {
430
+                $desc = $event->getDescription();
431
+                if (empty($desc)) {
432
+                    $desc = '';
433
+                }
434
+                $log->info(get_class($event) . ': ' . $desc . ' (' . $event->getIncrement() . ')', ['app' => 'updater']);
435
+            } elseif ($event instanceof RepairFinishEvent) {
436
+                $log->info(get_class($event), ['app' => 'updater']);
437
+            } elseif ($event instanceof RepairStepEvent) {
438
+                $log->info(get_class($event) . ': Repair step: ' . $event->getStepName(), ['app' => 'updater']);
439
+            } elseif ($event instanceof RepairInfoEvent) {
440
+                $log->info(get_class($event) . ': Repair info: ' . $event->getMessage(), ['app' => 'updater']);
441
+            } elseif ($event instanceof RepairWarningEvent) {
442
+                $log->warning(get_class($event) . ': Repair warning: ' . $event->getMessage(), ['app' => 'updater']);
443
+            } elseif ($event instanceof RepairErrorEvent) {
444
+                $log->error(get_class($event) . ': Repair error: ' . $event->getMessage(), ['app' => 'updater']);
445
+            }
446
+        };
447
+
448
+        $dispatcher->addListener(RepairStartEvent::class, $repairListener);
449
+        $dispatcher->addListener(RepairAdvanceEvent::class, $repairListener);
450
+        $dispatcher->addListener(RepairFinishEvent::class, $repairListener);
451
+        $dispatcher->addListener(RepairStepEvent::class, $repairListener);
452
+        $dispatcher->addListener(RepairInfoEvent::class, $repairListener);
453
+        $dispatcher->addListener(RepairWarningEvent::class, $repairListener);
454
+        $dispatcher->addListener(RepairErrorEvent::class, $repairListener);
455
+
456
+
457
+        $this->listen('\OC\Updater', 'maintenanceEnabled', function () use ($log) {
458
+            $log->info('\OC\Updater::maintenanceEnabled: Turned on maintenance mode', ['app' => 'updater']);
459
+        });
460
+        $this->listen('\OC\Updater', 'maintenanceDisabled', function () use ($log) {
461
+            $log->info('\OC\Updater::maintenanceDisabled: Turned off maintenance mode', ['app' => 'updater']);
462
+        });
463
+        $this->listen('\OC\Updater', 'maintenanceActive', function () use ($log) {
464
+            $log->info('\OC\Updater::maintenanceActive: Maintenance mode is kept active', ['app' => 'updater']);
465
+        });
466
+        $this->listen('\OC\Updater', 'updateEnd', function ($success) use ($log) {
467
+            if ($success) {
468
+                $log->info('\OC\Updater::updateEnd: Update successful', ['app' => 'updater']);
469
+            } else {
470
+                $log->error('\OC\Updater::updateEnd: Update failed', ['app' => 'updater']);
471
+            }
472
+        });
473
+        $this->listen('\OC\Updater', 'dbUpgradeBefore', function () use ($log) {
474
+            $log->info('\OC\Updater::dbUpgradeBefore: Updating database schema', ['app' => 'updater']);
475
+        });
476
+        $this->listen('\OC\Updater', 'dbUpgrade', function () use ($log) {
477
+            $log->info('\OC\Updater::dbUpgrade: Updated database', ['app' => 'updater']);
478
+        });
479
+        $this->listen('\OC\Updater', 'incompatibleAppDisabled', function ($app) use ($log) {
480
+            $log->info('\OC\Updater::incompatibleAppDisabled: Disabled incompatible app: ' . $app, ['app' => 'updater']);
481
+        });
482
+        $this->listen('\OC\Updater', 'checkAppStoreAppBefore', function ($app) use ($log) {
483
+            $log->debug('\OC\Updater::checkAppStoreAppBefore: Checking for update of app "' . $app . '" in appstore', ['app' => 'updater']);
484
+        });
485
+        $this->listen('\OC\Updater', 'upgradeAppStoreApp', function ($app) use ($log) {
486
+            $log->info('\OC\Updater::upgradeAppStoreApp: Update app "' . $app . '" from appstore', ['app' => 'updater']);
487
+        });
488
+        $this->listen('\OC\Updater', 'checkAppStoreApp', function ($app) use ($log) {
489
+            $log->debug('\OC\Updater::checkAppStoreApp: Checked for update of app "' . $app . '" in appstore', ['app' => 'updater']);
490
+        });
491
+        $this->listen('\OC\Updater', 'appSimulateUpdate', function ($app) use ($log) {
492
+            $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']);
493
+        });
494
+        $this->listen('\OC\Updater', 'appUpgradeStarted', function ($app) use ($log) {
495
+            $log->info('\OC\Updater::appUpgradeStarted: Updating <' . $app . '> ...', ['app' => 'updater']);
496
+        });
497
+        $this->listen('\OC\Updater', 'appUpgrade', function ($app, $version) use ($log) {
498
+            $log->info('\OC\Updater::appUpgrade: Updated <' . $app . '> to ' . $version, ['app' => 'updater']);
499
+        });
500
+        $this->listen('\OC\Updater', 'failure', function ($message) use ($log) {
501
+            $log->error('\OC\Updater::failure: ' . $message, ['app' => 'updater']);
502
+        });
503
+        $this->listen('\OC\Updater', 'setDebugLogLevel', function () use ($log) {
504
+            $log->info('\OC\Updater::setDebugLogLevel: Set log level to debug', ['app' => 'updater']);
505
+        });
506
+        $this->listen('\OC\Updater', 'resetLogLevel', function ($logLevel, $logLevelName) use ($log) {
507
+            $log->info('\OC\Updater::resetLogLevel: Reset log level to ' . $logLevelName . '(' . $logLevel . ')', ['app' => 'updater']);
508
+        });
509
+        $this->listen('\OC\Updater', 'startCheckCodeIntegrity', function () use ($log) {
510
+            $log->info('\OC\Updater::startCheckCodeIntegrity: Starting code integrity check...', ['app' => 'updater']);
511
+        });
512
+        $this->listen('\OC\Updater', 'finishedCheckCodeIntegrity', function () use ($log) {
513
+            $log->info('\OC\Updater::finishedCheckCodeIntegrity: Finished code integrity check', ['app' => 'updater']);
514
+        });
515
+    }
516 516
 }
Please login to merge, or discard this patch.