Completed
Push — master ( 4c59ee...b4c5a5 )
by Christoph
08:38
created

Updater::doUpgrade()   C

Complexity

Conditions 7
Paths 13

Size

Total Lines 72
Code Lines 35

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 72
rs 6.7427
cc 7
eloc 35
nc 13
nop 2

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @author Arthur Schiwon <[email protected]>
4
 * @author Bart Visscher <[email protected]>
5
 * @author Björn Schießle <[email protected]>
6
 * @author Frank Karlitschek <[email protected]>
7
 * @author Joas Schilling <[email protected]>
8
 * @author Lukas Reschke <[email protected]>
9
 * @author Morris Jobke <[email protected]>
10
 * @author Robin Appelman <[email protected]>
11
 * @author Robin McCorkell <[email protected]>
12
 * @author Steffen Lindner <[email protected]>
13
 * @author Thomas Müller <[email protected]>
14
 * @author Victor Dubiniuk <[email protected]>
15
 * @author Vincent Petry <[email protected]>
16
 *
17
 * @copyright Copyright (c) 2016, ownCloud, Inc.
18
 * @license AGPL-3.0
19
 *
20
 * This code is free software: you can redistribute it and/or modify
21
 * it under the terms of the GNU Affero General Public License, version 3,
22
 * as published by the Free Software Foundation.
23
 *
24
 * This program is distributed in the hope that it will be useful,
25
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
26
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
27
 * GNU Affero General Public License for more details.
28
 *
29
 * You should have received a copy of the GNU Affero General Public License, version 3,
30
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
31
 *
32
 */
33
34
namespace OC;
35
36
use OC\Hooks\BasicEmitter;
37
use OC\IntegrityCheck\Checker;
38
use OC_App;
39
use OC_Installer;
40
use OCP\IConfig;
41
use OC\Setup;
42
use OCP\ILogger;
43
44
/**
45
 * Class that handles autoupdating of ownCloud
46
 *
47
 * Hooks provided in scope \OC\Updater
48
 *  - maintenanceStart()
49
 *  - maintenanceEnd()
50
 *  - dbUpgrade()
51
 *  - failure(string $message)
52
 */
53
class Updater extends BasicEmitter {
54
55
	/** @var ILogger $log */
56
	private $log;
57
	
58
	/** @var IConfig */
59
	private $config;
60
61
	/** @var Checker */
62
	private $checker;
63
64
	/** @var bool */
65
	private $simulateStepEnabled;
66
67
	/** @var bool */
68
	private $updateStepEnabled;
69
70
	/** @var bool */
71
	private $skip3rdPartyAppsDisable;
72
73
	private $logLevelNames = [
74
		0 => 'Debug',
75
		1 => 'Info',
76
		2 => 'Warning',
77
		3 => 'Error',
78
		4 => 'Fatal',
79
	];
80
81
	/**
82
	 * @param IConfig $config
83
	 * @param Checker $checker
84
	 * @param ILogger $log
85
	 */
86
	public function __construct(IConfig $config,
87
								Checker $checker,
88
								ILogger $log = null) {
89
		$this->log = $log;
90
		$this->config = $config;
91
		$this->checker = $checker;
92
		$this->simulateStepEnabled = true;
93
		$this->updateStepEnabled = true;
94
	}
95
96
	/**
97
	 * Sets whether the database migration simulation must
98
	 * be enabled.
99
	 * This can be set to false to skip this test.
100
	 *
101
	 * @param bool $flag true to enable simulation, false otherwise
102
	 */
103
	public function setSimulateStepEnabled($flag) {
104
		$this->simulateStepEnabled = $flag;
105
	}
106
107
	/**
108
	 * Sets whether the update must be performed.
109
	 * This can be set to false to skip the actual update.
110
	 *
111
	 * @param bool $flag true to enable update, false otherwise
112
	 */
113
	public function setUpdateStepEnabled($flag) {
114
		$this->updateStepEnabled = $flag;
115
	}
116
117
	/**
118
	 * Sets whether the update disables 3rd party apps.
119
	 * This can be set to true to skip the disable.
120
	 *
121
	 * @param bool $flag false to not disable, true otherwise
122
	 */
123
	public function setSkip3rdPartyAppsDisable($flag) {
124
		$this->skip3rdPartyAppsDisable = $flag;
125
	}
126
127
	/**
128
	 * runs the update actions in maintenance mode, does not upgrade the source files
129
	 * except the main .htaccess file
130
	 *
131
	 * @return bool true if the operation succeeded, false otherwise
132
	 */
133
	public function upgrade() {
134
		$logLevel = $this->config->getSystemValue('loglevel', \OCP\Util::WARN);
135
		$this->emit('\OC\Updater', 'setDebugLogLevel', [ $logLevel, $this->logLevelNames[$logLevel] ]);
136
		$this->config->setSystemValue('loglevel', \OCP\Util::DEBUG);
137
138
		$wasMaintenanceModeEnabled = $this->config->getSystemValue('maintenance', false);
139
140
		if(!$wasMaintenanceModeEnabled) {
141
			$this->config->setSystemValue('maintenance', true);
142
			$this->emit('\OC\Updater', 'maintenanceEnabled');
143
		}
144
145
		$installedVersion = $this->config->getSystemValue('version', '0.0.0');
146
		$currentVersion = implode('.', \OCP\Util::getVersion());
147
		$this->log->debug('starting upgrade from ' . $installedVersion . ' to ' . $currentVersion, array('app' => 'core'));
148
149
		$success = true;
150
		try {
151
			$this->doUpgrade($currentVersion, $installedVersion);
152
		} catch (\Exception $exception) {
153
			$this->log->logException($exception, ['app' => 'core']);
154
			$this->emit('\OC\Updater', 'failure', array(get_class($exception) . ': ' .$exception->getMessage()));
155
			$success = false;
156
		}
157
158
		$this->emit('\OC\Updater', 'updateEnd', array($success));
159
160
		if(!$wasMaintenanceModeEnabled && $success) {
161
			$this->config->setSystemValue('maintenance', false);
162
			$this->emit('\OC\Updater', 'maintenanceDisabled');
163
		} else {
164
			$this->emit('\OC\Updater', 'maintenanceActive');
165
		}
166
167
		$this->emit('\OC\Updater', 'resetLogLevel', [ $logLevel, $this->logLevelNames[$logLevel] ]);
168
		$this->config->setSystemValue('loglevel', $logLevel);
169
170
		return $success;
171
	}
172
173
	/**
174
	 * Return version from which this version is allowed to upgrade from
175
	 *
176
	 * @return string allowed previous version
177
	 */
178
	private function getAllowedPreviousVersion() {
179
		// this should really be a JSON file
180
		require \OC::$SERVERROOT . '/version.php';
181
		/** @var array $OC_VersionCanBeUpgradedFrom */
182
		return implode('.', $OC_VersionCanBeUpgradedFrom);
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...
183
	}
184
185
	/**
186
	 * Whether an upgrade to a specified version is possible
187
	 * @param string $oldVersion
188
	 * @param string $newVersion
189
	 * @param string $allowedPreviousVersion
190
	 * @return bool
191
	 */
192
	public function isUpgradePossible($oldVersion, $newVersion, $allowedPreviousVersion) {
193
		return (version_compare($allowedPreviousVersion, $oldVersion, '<=')
194
			&& (version_compare($oldVersion, $newVersion, '<=') || $this->config->getSystemValue('debug', false)));
195
	}
196
197
	/**
198
	 * Forward messages emitted by the repair routine
199
	 *
200
	 * @param Repair $repair repair routine
201
	 */
202
	private function emitRepairMessages(Repair $repair) {
203
		$repair->listen('\OC\Repair', 'warning', function ($description) {
204
			$this->emit('\OC\Updater', 'repairWarning', array($description));
205
		});
206
		$repair->listen('\OC\Repair', 'error', function ($description) {
207
			$this->emit('\OC\Updater', 'repairError', array($description));
208
		});
209
		$repair->listen('\OC\Repair', 'info', function ($description) {
210
			$this->emit('\OC\Updater', 'repairInfo', array($description));
211
		});
212
		$repair->listen('\OC\Repair', 'step', function ($description) {
213
			$this->emit('\OC\Updater', 'repairStep', array($description));
214
		});
215
	}
216
217
	/**
218
	 * runs the update actions in maintenance mode, does not upgrade the source files
219
	 * except the main .htaccess file
220
	 *
221
	 * @param string $currentVersion current version to upgrade to
222
	 * @param string $installedVersion previous version from which to upgrade from
223
	 *
224
	 * @throws \Exception
225
	 */
226
	private function doUpgrade($currentVersion, $installedVersion) {
227
		// Stop update if the update is over several major versions
228
		$allowedPreviousVersion = $this->getAllowedPreviousVersion();
229
		if (!self::isUpgradePossible($installedVersion, $currentVersion, $allowedPreviousVersion)) {
230
			throw new \Exception('Updates between multiple major versions and downgrades are unsupported.');
231
		}
232
233
		// Update .htaccess files
234
		try {
235
			Setup::updateHtaccess();
236
			Setup::protectDataDirectory();
237
		} catch (\Exception $e) {
238
			throw new \Exception($e->getMessage());
239
		}
240
241
		// create empty file in data dir, so we can later find
242
		// out that this is indeed an ownCloud data directory
243
		// (in case it didn't exist before)
244
		file_put_contents($this->config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . '/.ocdata', '');
245
246
		// pre-upgrade repairs
247
		$repair = new Repair(Repair::getBeforeUpgradeRepairSteps());
248
		$this->emitRepairMessages($repair);
249
		$repair->run();
250
251
		// simulate DB upgrade
252
		if ($this->simulateStepEnabled) {
253
			$this->checkCoreUpgrade();
254
255
			// simulate apps DB upgrade
256
			$this->checkAppUpgrade($currentVersion);
257
258
		}
259
260
		if ($this->updateStepEnabled) {
261
			$this->doCoreUpgrade();
262
263
			// update all shipped apps
264
			$disabledApps = $this->checkAppsRequirements();
265
			$this->doAppUpgrade();
266
267
			// upgrade appstore apps
268
			$this->upgradeAppStoreApps($disabledApps);
269
270
			// install new shipped apps on upgrade
271
			OC_App::loadApps('authentication');
272
			$errors = OC_Installer::installShippedApps(true);
273
			foreach ($errors as $appId => $exception) {
274
				/** @var \Exception $exception */
275
				$this->log->logException($exception, ['app' => $appId]);
276
				$this->emit('\OC\Updater', 'failure', [$appId . ': ' . $exception->getMessage()]);
277
			}
278
279
			// post-upgrade repairs
280
			$repair = new Repair(Repair::getRepairSteps());
281
			$this->emitRepairMessages($repair);
282
			$repair->run();
283
284
			//Invalidate update feed
285
			$this->config->setAppValue('core', 'lastupdatedat', 0);
286
287
			// Check for code integrity if not disabled
288
			if(\OC::$server->getIntegrityCodeChecker()->isCodeCheckEnforced()) {
289
				$this->emit('\OC\Updater', 'startCheckCodeIntegrity');
290
				$this->checker->runInstanceVerification();
291
				$this->emit('\OC\Updater', 'finishedCheckCodeIntegrity');
292
			}
293
294
			// only set the final version if everything went well
295
			$this->config->setSystemValue('version', implode('.', \OCP\Util::getVersion()));
296
		}
297
	}
298
299
	protected function checkCoreUpgrade() {
300
		$this->emit('\OC\Updater', 'dbSimulateUpgradeBefore');
301
302
		// simulate core DB upgrade
303
		\OC_DB::simulateUpdateDbFromStructure(\OC::$SERVERROOT . '/db_structure.xml');
304
305
		$this->emit('\OC\Updater', 'dbSimulateUpgrade');
306
	}
307
308
	protected function doCoreUpgrade() {
309
		$this->emit('\OC\Updater', 'dbUpgradeBefore');
310
311
		// do the real upgrade
312
		\OC_DB::updateDbFromStructure(\OC::$SERVERROOT . '/db_structure.xml');
313
314
		$this->emit('\OC\Updater', 'dbUpgrade');
315
	}
316
317
	/**
318
	 * @param string $version the oc version to check app compatibility with
319
	 */
320
	protected function checkAppUpgrade($version) {
321
		$apps = \OC_App::getEnabledApps();
322
		$this->emit('\OC\Updater', 'appUpgradeCheckBefore');
323
324
		foreach ($apps as $appId) {
325
			$info = \OC_App::getAppInfo($appId);
326
			$compatible = \OC_App::isAppCompatible($version, $info);
0 ignored issues
show
Bug introduced by
It seems like $info defined by \OC_App::getAppInfo($appId) on line 325 can also be of type null; however, OC_App::isAppCompatible() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
327
			$isShipped = \OC_App::isShipped($appId);
328
329
			if ($compatible && $isShipped && \OC_App::shouldUpgrade($appId)) {
330
				/**
331
				 * FIXME: The preupdate check is performed before the database migration, otherwise database changes
332
				 * are not possible anymore within it. - Consider this when touching the code.
333
				 * @link https://github.com/owncloud/core/issues/10980
334
				 * @see \OC_App::updateApp
335
				 */
336
				if (file_exists(\OC_App::getAppPath($appId) . '/appinfo/preupdate.php')) {
337
					$this->includePreUpdate($appId);
338
				}
339
				if (file_exists(\OC_App::getAppPath($appId) . '/appinfo/database.xml')) {
340
					$this->emit('\OC\Updater', 'appSimulateUpdate', array($appId));
341
					\OC_DB::simulateUpdateDbFromStructure(\OC_App::getAppPath($appId) . '/appinfo/database.xml');
342
				}
343
			}
344
		}
345
346
		$this->emit('\OC\Updater', 'appUpgradeCheck');
347
	}
348
349
	/**
350
	 * Includes the pre-update file. Done here to prevent namespace mixups.
351
	 * @param string $appId
352
	 */
353
	private function includePreUpdate($appId) {
354
		include \OC_App::getAppPath($appId) . '/appinfo/preupdate.php';
355
	}
356
357
	/**
358
	 * upgrades all apps within a major ownCloud upgrade. Also loads "priority"
359
	 * (types authentication, filesystem, logging, in that order) afterwards.
360
	 *
361
	 * @throws NeedsUpdateException
362
	 */
363
	protected function doAppUpgrade() {
364
		$apps = \OC_App::getEnabledApps();
365
		$priorityTypes = array('authentication', 'filesystem', 'logging');
366
		$pseudoOtherType = 'other';
367
		$stacks = array($pseudoOtherType => array());
368
369
		foreach ($apps as $appId) {
370
			$priorityType = false;
371
			foreach ($priorityTypes as $type) {
372
				if(!isset($stacks[$type])) {
373
					$stacks[$type] = array();
374
				}
375
				if (\OC_App::isType($appId, $type)) {
376
					$stacks[$type][] = $appId;
377
					$priorityType = true;
378
					break;
379
				}
380
			}
381
			if (!$priorityType) {
382
				$stacks[$pseudoOtherType][] = $appId;
383
			}
384
		}
385
		foreach ($stacks as $type => $stack) {
386
			foreach ($stack as $appId) {
387
				if (\OC_App::shouldUpgrade($appId)) {
388
					$this->emit('\OC\Updater', 'appUpgradeStarted', array($appId, \OC_App::getAppVersion($appId)));
389
					\OC_App::updateApp($appId);
390
					$this->emit('\OC\Updater', 'appUpgrade', array($appId, \OC_App::getAppVersion($appId)));
391
				}
392
				if($type !== $pseudoOtherType) {
393
					// load authentication, filesystem and logging apps after
394
					// upgrading them. Other apps my need to rely on modifying
395
					// user and/or filesystem aspects.
396
					\OC_App::loadApp($appId, false);
397
				}
398
			}
399
		}
400
	}
401
402
	/**
403
	 * check if the current enabled apps are compatible with the current
404
	 * ownCloud version. disable them if not.
405
	 * This is important if you upgrade ownCloud and have non ported 3rd
406
	 * party apps installed.
407
	 *
408
	 * @return array
409
	 * @throws \Exception
410
	 */
411
	private function checkAppsRequirements() {
412
		$isCoreUpgrade = $this->isCodeUpgrade();
413
		$apps = OC_App::getEnabledApps();
414
		$version = \OCP\Util::getVersion();
415
		$disabledApps = [];
416
		foreach ($apps as $app) {
417
			// check if the app is compatible with this version of ownCloud
418
			$info = OC_App::getAppInfo($app);
419
			if(!OC_App::isAppCompatible($version, $info)) {
0 ignored issues
show
Documentation introduced by
$version is of type array, but the function expects a string.

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...
Bug introduced by
It seems like $info defined by \OC_App::getAppInfo($app) on line 418 can also be of type null; however, OC_App::isAppCompatible() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
420
				OC_App::disable($app);
421
				$this->emit('\OC\Updater', 'incompatibleAppDisabled', array($app));
422
			}
423
			// no need to disable any app in case this is a non-core upgrade
424
			if (!$isCoreUpgrade) {
425
				continue;
426
			}
427
			// shipped apps will remain enabled
428
			if (OC_App::isShipped($app)) {
429
				continue;
430
			}
431
			// authentication and session apps will remain enabled as well
432
			if (OC_App::isType($app, ['session', 'authentication'])) {
433
				continue;
434
			}
435
436
			// disable any other 3rd party apps if not overriden
437
			if(!$this->skip3rdPartyAppsDisable) {
438
				\OC_App::disable($app);
439
				$disabledApps[]= $app;
440
				$this->emit('\OC\Updater', 'thirdPartyAppDisabled', array($app));
441
			};
442
		}
443
		return $disabledApps;
444
	}
445
446
	/**
447
	 * @return bool
448
	 */
449
	private function isCodeUpgrade() {
450
		$installedVersion = $this->config->getSystemValue('version', '0.0.0');
451
		$currentVersion = implode('.', \OCP\Util::getVersion());
452
		if (version_compare($currentVersion, $installedVersion, '>')) {
453
			return true;
454
		}
455
		return false;
456
	}
457
458
	/**
459
	 * @param array $disabledApps
460
	 * @throws \Exception
461
	 */
462
	private function upgradeAppStoreApps(array $disabledApps) {
463
		foreach($disabledApps as $app) {
464
			try {
465
				if (OC_Installer::isUpdateAvailable($app)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression \OC_Installer::isUpdateAvailable($app) of type false|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
466
					$ocsId = \OC::$server->getConfig()->getAppValue($app, 'ocsid', '');
467
468
					$this->emit('\OC\Updater', 'upgradeAppStoreApp', array($app));
469
					OC_Installer::updateAppByOCSId($ocsId);
470
				}
471
			} catch (\Exception $ex) {
472
				$this->log->logException($ex, ['app' => 'core']);
473
			}
474
		}
475
	}
476
}
477
478