Completed
Push — master ( 430fd0...c13468 )
by Thomas
11:39 queued 01:10
created

Updater::upgradeAppStoreApps()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 10
nc 4
nop 1
dl 0
loc 14
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Arthur Schiwon <[email protected]>
4
 * @author Frank Karlitschek <[email protected]>
5
 * @author Joas Schilling <[email protected]>
6
 * @author Lukas Reschke <[email protected]>
7
 * @author Morris Jobke <[email protected]>
8
 * @author Robin Appelman <[email protected]>
9
 * @author Steffen Lindner <[email protected]>
10
 * @author Thomas Müller <[email protected]>
11
 * @author Victor Dubiniuk <[email protected]>
12
 * @author Vincent Petry <[email protected]>
13
 *
14
 * @copyright Copyright (c) 2017, ownCloud GmbH
15
 * @license AGPL-3.0
16
 *
17
 * This code is free software: you can redistribute it and/or modify
18
 * it under the terms of the GNU Affero General Public License, version 3,
19
 * as published by the Free Software Foundation.
20
 *
21
 * This program is distributed in the hope that it will be useful,
22
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24
 * GNU Affero General Public License for more details.
25
 *
26
 * You should have received a copy of the GNU Affero General Public License, version 3,
27
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
28
 *
29
 */
30
31
namespace OC;
32
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
/**
42
 * Class that handles autoupdating of ownCloud
43
 *
44
 * Hooks provided in scope \OC\Updater
45
 *  - maintenanceStart()
46
 *  - maintenanceEnd()
47
 *  - dbUpgrade()
48
 *  - failure(string $message)
49
 */
50
class Updater extends BasicEmitter {
51
52
	/** @var ILogger $log */
53
	private $log;
54
	
55
	/** @var IConfig */
56
	private $config;
57
58
	/** @var Checker */
59
	private $checker;
60
61
	private $logLevelNames = [
62
		0 => 'Debug',
63
		1 => 'Info',
64
		2 => 'Warning',
65
		3 => 'Error',
66
		4 => 'Fatal',
67
	];
68
69
	/**
70
	 * @param IConfig $config
71
	 * @param Checker $checker
72
	 * @param ILogger $log
73
	 */
74
	public function __construct(IConfig $config,
75
								Checker $checker,
76
								ILogger $log = null) {
77
		$this->log = $log;
78
		$this->config = $config;
79
		$this->checker = $checker;
80
	}
81
82
	/**
83
	 * runs the update actions in maintenance mode, does not upgrade the source files
84
	 * except the main .htaccess file
85
	 *
86
	 * @return bool true if the operation succeeded, false otherwise
87
	 */
88
	public function upgrade() {
89
		$this->emitRepairEvents();
90
91
		$logLevel = $this->config->getSystemValue('loglevel', Util::WARN);
92
		$this->emit('\OC\Updater', 'setDebugLogLevel', [ $logLevel, $this->logLevelNames[$logLevel] ]);
93
		$this->config->setSystemValue('loglevel', Util::DEBUG);
94
95
		$wasMaintenanceModeEnabled = $this->config->getSystemValue('maintenance', false);
96
97
		if(!$wasMaintenanceModeEnabled) {
98
			$this->config->setSystemValue('maintenance', true);
99
			$this->emit('\OC\Updater', 'maintenanceEnabled');
100
		}
101
102
		$installedVersion = $this->config->getSystemValue('version', '0.0.0');
103
		$currentVersion = implode('.', Util::getVersion());
104
		$this->log->debug('starting upgrade from ' . $installedVersion . ' to ' . $currentVersion, ['app' => 'core']);
105
106
		$success = true;
107
		try {
108
			$this->doUpgrade($currentVersion, $installedVersion);
109
		} catch (\Exception $exception) {
110
			$this->log->logException($exception, ['app' => 'core']);
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 versions from which this version is allowed to upgrade from
133
	 *
134
	 * @return string[] allowed previous versions
135
	 */
136
	private function getAllowedPreviousVersions() {
137
		// this should really be a JSON file
138
		require \OC::$SERVERROOT . '/version.php';
139
140
		$allowedPreviousVersions = [];
141
142
		/** @var array $OC_VersionCanBeUpgradedFrom */
143
		foreach ($OC_VersionCanBeUpgradedFrom as $version) {
0 ignored issues
show
Bug introduced by
The variable $OC_VersionCanBeUpgradedFrom does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
144
			$allowedPreviousVersions[] = implode('.', $version);
145
		}
146
147
		return $allowedPreviousVersions;
148
	}
149
150
	/**
151
	 * Return vendor from which this version was published
152
	 *
153
	 * @return string Get the vendor
154
	 */
155
	private function getVendor() {
156
		// this should really be a JSON file
157
		require \OC::$SERVERROOT . '/version.php';
158
		/** @var string $vendor */
159
		return (string) $vendor;
0 ignored issues
show
Bug introduced by
The variable $vendor does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
160
	}
161
162
	/**
163
	 * Whether an upgrade to a specified version is possible
164
	 * @param string $oldVersion
165
	 * @param string $newVersion
166
	 * @param string[] $allowedPreviousVersions
167
	 * @return bool
168
	 */
169
	public function isUpgradePossible($oldVersion, $newVersion, $allowedPreviousVersions) {
170
		// TODO: write tests for this, since i just wrapped it to get started with migrations and this might fail in some cases
171
		foreach ($allowedPreviousVersions as $allowedPreviousVersion) {
172
			$allowedUpgrade =  (version_compare($allowedPreviousVersion, $oldVersion, '<=')
173
				&& (version_compare($oldVersion, $newVersion, '<=') || $this->config->getSystemValue('debug', false)));
174
			if ($allowedUpgrade) {
175
				return $allowedUpgrade;
176
			}
177
		}
178
179
		// Upgrade not allowed, someone switching vendor?
180
		if ($this->getVendor() !== $this->config->getAppValue('core', 'vendor', '')) {
181
			$oldVersion = explode('.', $oldVersion);
182
			$newVersion = explode('.', $newVersion);
183
184
			return $oldVersion[0] === $newVersion[0] && $oldVersion[1] === $newVersion[1];
185
		}
186
187
		return false;
188
	}
189
190
	/**
191
	 * runs the update actions in maintenance mode, does not upgrade the source files
192
	 * except the main .htaccess file
193
	 *
194
	 * @param string $currentVersion current version to upgrade to
195
	 * @param string $installedVersion previous version from which to upgrade from
196
	 *
197
	 * @throws \Exception
198
	 */
199
	private function doUpgrade($currentVersion, $installedVersion) {
200
		// Stop update if the update is over several major versions
201
		$allowedPreviousVersions = $this->getAllowedPreviousVersions();
202
		if (!self::isUpgradePossible($installedVersion, $currentVersion, $allowedPreviousVersions)) {
203
			throw new \Exception('Updates between multiple major versions and downgrades are unsupported.');
204
		}
205
206
		// Update .htaccess files
207
		try {
208
			Setup::updateHtaccess();
209
			Setup::protectDataDirectory();
210
		} catch (\Exception $e) {
211
			throw new \Exception($e->getMessage());
212
		}
213
214
		// create empty file in data dir, so we can later find
215
		// out that this is indeed an ownCloud data directory
216
		// (in case it didn't exist before)
217
		file_put_contents($this->config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . '/.ocdata', '');
218
219
		// pre-upgrade repairs
220
		$repair = new Repair(Repair::getBeforeUpgradeRepairSteps(), \OC::$server->getEventDispatcher());
0 ignored issues
show
Documentation introduced by
\OC::$server->getEventDispatcher() is of type object<Symfony\Component...entDispatcherInterface>, but the function expects a null|object<Symfony\Comp...atcher\EventDispatcher>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
221
		$repair->run();
222
223
		$this->doCoreUpgrade();
224
225
		try {
226
			// TODO: replace with the new repair step mechanism https://github.com/owncloud/core/pull/24378
227
			Setup::installBackgroundJobs();
228
		} catch (\Exception $e) {
229
			throw new \Exception($e->getMessage());
230
		}
231
232
		// update all apps
233
		$this->doAppUpgrade();
234
235
		// install new shipped apps on upgrade
236
		OC_App::loadApps('authentication');
237
		$errors = Installer::installShippedApps(true);
238
		foreach ($errors as $appId => $exception) {
239
			/** @var \Exception $exception */
240
			$this->log->logException($exception, ['app' => $appId]);
241
			$this->emit('\OC\Updater', 'failure', [$appId . ': ' . $exception->getMessage()]);
242
		}
243
244
		// post-upgrade repairs
245
		$repair = new Repair(Repair::getRepairSteps(), \OC::$server->getEventDispatcher());
0 ignored issues
show
Documentation introduced by
\OC::$server->getEventDispatcher() is of type object<Symfony\Component...entDispatcherInterface>, but the function expects a null|object<Symfony\Comp...atcher\EventDispatcher>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
246
		$repair->run();
247
248
		//Invalidate update feed
249
		$this->config->setAppValue('core', 'lastupdatedat', 0);
250
251
		// Check for code integrity if not disabled
252
		if(\OC::$server->getIntegrityCodeChecker()->isCodeCheckEnforced()) {
253
			$this->emit('\OC\Updater', 'startCheckCodeIntegrity');
254
			$this->checker->runInstanceVerification();
255
			$this->emit('\OC\Updater', 'finishedCheckCodeIntegrity');
256
		}
257
258
		// only set the final version if everything went well
259
		$this->config->setSystemValue('version', implode('.', Util::getVersion()));
260
		$this->config->setAppValue('core', 'vendor', $this->getVendor());
261
	}
262
263
	protected function doCoreUpgrade() {
264
		$this->emit('\OC\Updater', 'dbUpgradeBefore');
265
266
		// execute core migrations
267
		if (is_dir(\OC::$SERVERROOT."/core/Migrations")) {
268
			$ms = new \OC\DB\MigrationService('core', \OC::$server->getDatabaseConnection());
269
			$ms->migrate();
270
		}
271
272
		$this->emit('\OC\Updater', 'dbUpgrade');
273
	}
274
275
	/**
276
	 * upgrades all apps within a major ownCloud upgrade. Also loads "priority"
277
	 * (types authentication, filesystem, logging, in that order) afterwards.
278
	 *
279
	 * @throws NeedsUpdateException
280
	 */
281
	protected function doAppUpgrade() {
282
		$apps = \OC_App::getEnabledApps();
283
		$priorityTypes = ['authentication', 'filesystem', 'logging'];
284
		$pseudoOtherType = 'other';
285
		$stacks = [$pseudoOtherType => []];
286
287
		foreach ($apps as $appId) {
288
			$priorityType = false;
289
			foreach ($priorityTypes as $type) {
290
				if(!isset($stacks[$type])) {
291
					$stacks[$type] = [];
292
				}
293
				if (\OC_App::isType($appId, $type)) {
294
					$stacks[$type][] = $appId;
295
					$priorityType = true;
296
					break;
297
				}
298
			}
299
			if (!$priorityType) {
300
				$stacks[$pseudoOtherType][] = $appId;
301
			}
302
		}
303
		foreach ($stacks as $type => $stack) {
304
			foreach ($stack as $appId) {
305
				if (\OC_App::shouldUpgrade($appId)) {
306
					$this->emit('\OC\Updater', 'appUpgradeStarted', [$appId, \OC_App::getAppVersion($appId)]);
307
					\OC_App::updateApp($appId);
308
					$this->emit('\OC\Updater', 'appUpgrade', [$appId, \OC_App::getAppVersion($appId)]);
309
				}
310
				if($type !== $pseudoOtherType) {
311
					// load authentication, filesystem and logging apps after
312
					// upgrading them. Other apps my need to rely on modifying
313
					// user and/or filesystem aspects.
314
					\OC_App::loadApp($appId, false);
315
				}
316
			}
317
		}
318
	}
319
320
	/**
321
	 * Forward messages emitted by the repair routine
322
	 */
323
	private function emitRepairEvents() {
324
		$dispatcher = \OC::$server->getEventDispatcher();
325
		$dispatcher->addListener('\OC\Repair::warning', function ($event) {
326
			if ($event instanceof GenericEvent) {
327
				$this->emit('\OC\Updater', 'repairWarning', $event->getArguments());
328
			}
329
		});
330
		$dispatcher->addListener('\OC\Repair::error', function ($event) {
331
			if ($event instanceof GenericEvent) {
332
				$this->emit('\OC\Updater', 'repairError', $event->getArguments());
333
			}
334
		});
335
		$dispatcher->addListener('\OC\Repair::info', function ($event) {
336
			if ($event instanceof GenericEvent) {
337
				$this->emit('\OC\Updater', 'repairInfo', $event->getArguments());
338
			}
339
		});
340
		$dispatcher->addListener('\OC\Repair::step', function ($event) {
341
			if ($event instanceof GenericEvent) {
342
				$this->emit('\OC\Updater', 'repairStep', $event->getArguments());
343
			}
344
		});
345
	}
346
347
}
348
349