Completed
Pull Request — stable9 (#2060)
by Joas
21:38 queued 14:09
created

Updater::upgrade()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 44
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 32
nc 12
nop 0
dl 0
loc 44
rs 8.439
c 0
b 0
f 0
1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 *
5
 * @author Arthur Schiwon <[email protected]>
6
 * @author Bart Visscher <[email protected]>
7
 * @author Björn Schießle <[email protected]>
8
 * @author Frank Karlitschek <[email protected]>
9
 * @author Joas Schilling <[email protected]>
10
 * @author Lukas Reschke <[email protected]>
11
 * @author Morris Jobke <[email protected]>
12
 * @author Robin Appelman <[email protected]>
13
 * @author Robin McCorkell <[email protected]>
14
 * @author Steffen Lindner <[email protected]>
15
 * @author Thomas Müller <[email protected]>
16
 * @author Victor Dubiniuk <[email protected]>
17
 * @author Vincent Petry <[email protected]>
18
 *
19
 * @license AGPL-3.0
20
 *
21
 * This code is free software: you can redistribute it and/or modify
22
 * it under the terms of the GNU Affero General Public License, version 3,
23
 * as published by the Free Software Foundation.
24
 *
25
 * This program is distributed in the hope that it will be useful,
26
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
27
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
28
 * GNU Affero General Public License for more details.
29
 *
30
 * You should have received a copy of the GNU Affero General Public License, version 3,
31
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
32
 *
33
 */
34
35
namespace OC;
36
37
use OC\Hooks\BasicEmitter;
38
use OC\IntegrityCheck\Checker;
39
use OC_App;
40
use OC_Installer;
41
use OC_Util;
42
use OCP\IConfig;
43
use OC\Setup;
44
use OCP\ILogger;
45
46
/**
47
 * Class that handles autoupdating of ownCloud
48
 *
49
 * Hooks provided in scope \OC\Updater
50
 *  - maintenanceStart()
51
 *  - maintenanceEnd()
52
 *  - dbUpgrade()
53
 *  - failure(string $message)
54
 */
55
class Updater extends BasicEmitter {
56
57
	/** @var ILogger $log */
58
	private $log;
59
	
60
	/** @var \OC\HTTPHelper $helper */
61
	private $httpHelper;
62
	
63
	/** @var IConfig */
64
	private $config;
65
66
	/** @var Checker */
67
	private $checker;
68
69
	/** @var bool */
70
	private $simulateStepEnabled;
71
72
	/** @var bool */
73
	private $updateStepEnabled;
74
75
	/** @var bool */
76
	private $skip3rdPartyAppsDisable;
77
78
	private $logLevelNames = [
79
		0 => 'Debug',
80
		1 => 'Info',
81
		2 => 'Warning',
82
		3 => 'Error',
83
		4 => 'Fatal',
84
	];
85
86
	/**
87
	 * @param HTTPHelper $httpHelper
88
	 * @param IConfig $config
89
	 * @param Checker $checker
90
	 * @param ILogger $log
91
	 */
92
	public function __construct(HTTPHelper $httpHelper,
93
								IConfig $config,
94
								Checker $checker,
95
								ILogger $log = null) {
96
		$this->httpHelper = $httpHelper;
97
		$this->log = $log;
98
		$this->config = $config;
99
		$this->checker = $checker;
100
		$this->simulateStepEnabled = true;
101
		$this->updateStepEnabled = true;
102
	}
103
104
	/**
105
	 * Sets whether the database migration simulation must
106
	 * be enabled.
107
	 * This can be set to false to skip this test.
108
	 *
109
	 * @param bool $flag true to enable simulation, false otherwise
110
	 */
111
	public function setSimulateStepEnabled($flag) {
112
		$this->simulateStepEnabled = $flag;
113
	}
114
115
	/**
116
	 * Sets whether the update must be performed.
117
	 * This can be set to false to skip the actual update.
118
	 *
119
	 * @param bool $flag true to enable update, false otherwise
120
	 */
121
	public function setUpdateStepEnabled($flag) {
122
		$this->updateStepEnabled = $flag;
123
	}
124
125
	/**
126
	 * Sets whether the update disables 3rd party apps.
127
	 * This can be set to true to skip the disable.
128
	 *
129
	 * @param bool $flag false to not disable, true otherwise
130
	 */
131
	public function setSkip3rdPartyAppsDisable($flag) {
132
		$this->skip3rdPartyAppsDisable = $flag;
133
	}
134
135
	/**
136
	 * Check if a new version is available
137
	 *
138
	 * @return array|bool
139
	 */
140
	public function check() {
141
142
		// Look up the cache - it is invalidated all 30 minutes
143
		if (((int)$this->config->getAppValue('core', 'lastupdatedat') + 1800) > time()) {
144
			return json_decode($this->config->getAppValue('core', 'lastupdateResult'), true);
145
		}
146
147
		$updaterUrl = $this->config->getSystemValue('updater.server.url', 'https://updates.nextcloud.com/updater_server/');
148
149
		$this->config->setAppValue('core', 'lastupdatedat', time());
150
151
		if ($this->config->getAppValue('core', 'installedat', '') === '') {
152
			$this->config->setAppValue('core', 'installedat', microtime(true));
153
		}
154
155
		$version = \OCP\Util::getVersion();
156
		$version['installed'] = $this->config->getAppValue('core', 'installedat');
157
		$version['updated'] = $this->config->getAppValue('core', 'lastupdatedat');
158
		$version['updatechannel'] = \OC_Util::getChannel();
159
		$version['edition'] = \OC_Util::getEditionString();
160
		$version['build'] = \OC_Util::getBuild();
161
		$versionString = implode('x', $version);
162
163
		//fetch xml data from updater
164
		$url = $updaterUrl . '?version=' . $versionString;
165
166
		$tmp = [];
167
		$xml = $this->httpHelper->getUrlContent($url);
0 ignored issues
show
Deprecated Code introduced by
The method OC\HTTPHelper::getUrlContent() has been deprecated with message: Use \OCP\Http\Client\IClientService

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
168
		if ($xml) {
169
			$loadEntities = libxml_disable_entity_loader(true);
170
			$data = @simplexml_load_string($xml);
171
			libxml_disable_entity_loader($loadEntities);
172
			if ($data !== false) {
173
				$tmp['version'] = (string)$data->version;
174
				$tmp['versionstring'] = (string)$data->versionstring;
175
				$tmp['url'] = (string)$data->url;
176
				$tmp['web'] = (string)$data->web;
177
				$tmp['autoupdater'] = (string)$data->autoupdater;
178
			} else {
179
				libxml_clear_errors();
180
			}
181
		} else {
182
			$data = [];
183
		}
184
185
		// Cache the result
186
		$this->config->setAppValue('core', 'lastupdateResult', json_encode($data));
187
		return $tmp;
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
	 * @return bool true if the operation succeeded, false otherwise
195
	 */
196
	public function upgrade() {
197
		$logLevel = $this->config->getSystemValue('loglevel', \OCP\Util::WARN);
198
		$this->emit('\OC\Updater', 'setDebugLogLevel', [ $logLevel, $this->logLevelNames[$logLevel] ]);
199
		$this->config->setSystemValue('loglevel', \OCP\Util::DEBUG);
200
201
		$wasMaintenanceModeEnabled = $this->config->getSystemValue('maintenance', false);
202
203
		if(!$wasMaintenanceModeEnabled) {
204
			$this->config->setSystemValue('maintenance', true);
205
			$this->emit('\OC\Updater', 'maintenanceEnabled');
206
		}
207
208
		$installedVersion = $this->config->getSystemValue('version', '0.0.0');
209
		$currentVersion = implode('.', \OCP\Util::getVersion());
210
		$this->log->debug('starting upgrade from ' . $installedVersion . ' to ' . $currentVersion, array('app' => 'core'));
211
212
		$success = true;
213
		try {
214
			$this->doUpgrade($currentVersion, $installedVersion);
215
		} catch (HintException $exception) {
216
			$this->log->logException($exception, ['app' => 'core']);
217
			$this->emit('\OC\Updater', 'failure', array($exception->getMessage() . ': ' .$exception->getHint()));
218
			$success = false;
219
		} catch (\Exception $exception) {
220
			$this->log->logException($exception, ['app' => 'core']);
221
			$this->emit('\OC\Updater', 'failure', array(get_class($exception) . ': ' .$exception->getMessage()));
222
			$success = false;
223
		}
224
225
		$this->emit('\OC\Updater', 'updateEnd', array($success));
226
227
		if(!$wasMaintenanceModeEnabled && $success) {
228
			$this->config->setSystemValue('maintenance', false);
229
			$this->emit('\OC\Updater', 'maintenanceDisabled');
230
		} else {
231
			$this->emit('\OC\Updater', 'maintenanceActive');
232
		}
233
234
		$this->emit('\OC\Updater', 'resetLogLevel', [ $logLevel, $this->logLevelNames[$logLevel] ]);
235
		$this->config->setSystemValue('loglevel', $logLevel);
236
		$this->config->setSystemValue('installed', true);
237
238
		return $success;
239
	}
240
241
	/**
242
	 * Return version from which this version is allowed to upgrade from
243
	 *
244
	 * @return string allowed previous version
245
	 */
246
	private function getAllowedPreviousVersion() {
247
		// this should really be a JSON file
248
		require \OC::$SERVERROOT . '/version.php';
249
		/** @var array $OC_VersionCanBeUpgradedFrom */
250
		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...
251
	}
252
253
	/**
254
	 * Whether an upgrade to a specified version is possible
255
	 * @param string $oldVersion
256
	 * @param string $newVersion
257
	 * @param string $allowedPreviousVersion
258
	 * @return bool
259
	 */
260
	public function isUpgradePossible($oldVersion, $newVersion, $allowedPreviousVersion) {
261
		return (version_compare($allowedPreviousVersion, $oldVersion, '<=')
262
			&& (version_compare($oldVersion, $newVersion, '<=') || $this->config->getSystemValue('debug', false)));
263
	}
264
265
	/**
266
	 * Forward messages emitted by the repair routine
267
	 *
268
	 * @param Repair $repair repair routine
269
	 */
270
	private function emitRepairMessages(Repair $repair) {
271
		$repair->listen('\OC\Repair', 'warning', function ($description) {
272
			$this->emit('\OC\Updater', 'repairWarning', array($description));
273
		});
274
		$repair->listen('\OC\Repair', 'error', function ($description) {
275
			$this->emit('\OC\Updater', 'repairError', array($description));
276
		});
277
		$repair->listen('\OC\Repair', 'info', function ($description) {
278
			$this->emit('\OC\Updater', 'repairInfo', array($description));
279
		});
280
		$repair->listen('\OC\Repair', 'step', function ($description) {
281
			$this->emit('\OC\Updater', 'repairStep', array($description));
282
		});
283
	}
284
285
	/**
286
	 * runs the update actions in maintenance mode, does not upgrade the source files
287
	 * except the main .htaccess file
288
	 *
289
	 * @param string $currentVersion current version to upgrade to
290
	 * @param string $installedVersion previous version from which to upgrade from
291
	 *
292
	 * @throws \Exception
293
	 */
294
	private function doUpgrade($currentVersion, $installedVersion) {
295
		// Stop update if the update is over several major versions
296
		$allowedPreviousVersion = $this->getAllowedPreviousVersion();
297
		if (!self::isUpgradePossible($installedVersion, $currentVersion, $allowedPreviousVersion)) {
298
			throw new \Exception('Updates between multiple major versions and downgrades are unsupported.');
299
		}
300
301
		// Update .htaccess files
302
		try {
303
			Setup::updateHtaccess();
304
			Setup::protectDataDirectory();
305
		} catch (\Exception $e) {
306
			throw new \Exception($e->getMessage());
307
		}
308
309
		// create empty file in data dir, so we can later find
310
		// out that this is indeed an ownCloud data directory
311
		// (in case it didn't exist before)
312
		file_put_contents($this->config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . '/.ocdata', '');
313
314
		// pre-upgrade repairs
315
		$repair = new Repair(Repair::getBeforeUpgradeRepairSteps());
316
		$this->emitRepairMessages($repair);
317
		$repair->run();
318
319
		// simulate DB upgrade
320
		if ($this->simulateStepEnabled) {
321
			$this->checkCoreUpgrade();
322
323
			// simulate apps DB upgrade
324
			$this->checkAppUpgrade($currentVersion);
325
326
		}
327
328
		if ($this->updateStepEnabled) {
329
			$this->doCoreUpgrade();
330
331
			// update all shipped apps
332
			$disabledApps = $this->checkAppsRequirements();
333
			$this->doAppUpgrade();
334
335
			// upgrade appstore apps
336
			$this->upgradeAppStoreApps($disabledApps);
337
338
			// install new shipped apps on upgrade
339
			OC_App::loadApps('authentication');
340
			$errors = OC_Installer::installShippedApps(true);
341
			foreach ($errors as $appId => $exception) {
342
				/** @var \Exception $exception */
343
				$this->log->logException($exception, ['app' => $appId]);
344
				$this->emit('\OC\Updater', 'failure', [$appId . ': ' . $exception->getMessage()]);
345
			}
346
347
			// post-upgrade repairs
348
			$repair = new Repair(Repair::getRepairSteps());
349
			$this->emitRepairMessages($repair);
350
			$repair->run();
351
352
			//Invalidate update feed
353
			$this->config->setAppValue('core', 'lastupdatedat', 0);
354
355
			// Check for code integrity if not disabled
356
			if(\OC::$server->getIntegrityCodeChecker()->isCodeCheckEnforced()) {
357
				$this->emit('\OC\Updater', 'startCheckCodeIntegrity');
358
				$this->checker->runInstanceVerification();
359
				$this->emit('\OC\Updater', 'finishedCheckCodeIntegrity');
360
			}
361
362
			// only set the final version if everything went well
363
			$this->config->setSystemValue('version', implode('.', \OCP\Util::getVersion()));
364
		}
365
	}
366
367
	protected function checkCoreUpgrade() {
368
		$this->emit('\OC\Updater', 'dbSimulateUpgradeBefore');
369
370
		// simulate core DB upgrade
371
		\OC_DB::simulateUpdateDbFromStructure(\OC::$SERVERROOT . '/db_structure.xml');
372
373
		$this->emit('\OC\Updater', 'dbSimulateUpgrade');
374
	}
375
376
	protected function doCoreUpgrade() {
377
		$this->emit('\OC\Updater', 'dbUpgradeBefore');
378
379
		// do the real upgrade
380
		\OC_DB::updateDbFromStructure(\OC::$SERVERROOT . '/db_structure.xml');
381
382
		$this->emit('\OC\Updater', 'dbUpgrade');
383
	}
384
385
	/**
386
	 * @param string $version the oc version to check app compatibility with
387
	 */
388
	protected function checkAppUpgrade($version) {
389
		$apps = \OC_App::getEnabledApps();
390
		$this->emit('\OC\Updater', 'appUpgradeCheckBefore');
391
392
		foreach ($apps as $appId) {
393
			$info = \OC_App::getAppInfo($appId);
394
			$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 393 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...
395
			$isShipped = \OC_App::isShipped($appId);
396
397
			if ($compatible && $isShipped && \OC_App::shouldUpgrade($appId)) {
398
				/**
399
				 * FIXME: The preupdate check is performed before the database migration, otherwise database changes
400
				 * are not possible anymore within it. - Consider this when touching the code.
401
				 * @link https://github.com/owncloud/core/issues/10980
402
				 * @see \OC_App::updateApp
403
				 */
404
				if (file_exists(\OC_App::getAppPath($appId) . '/appinfo/preupdate.php')) {
405
					$this->includePreUpdate($appId);
406
				}
407
				if (file_exists(\OC_App::getAppPath($appId) . '/appinfo/database.xml')) {
408
					$this->emit('\OC\Updater', 'appSimulateUpdate', array($appId));
409
					\OC_DB::simulateUpdateDbFromStructure(\OC_App::getAppPath($appId) . '/appinfo/database.xml');
410
				}
411
			}
412
		}
413
414
		$this->emit('\OC\Updater', 'appUpgradeCheck');
415
	}
416
417
	/**
418
	 * Includes the pre-update file. Done here to prevent namespace mixups.
419
	 * @param string $appId
420
	 */
421
	private function includePreUpdate($appId) {
422
		include \OC_App::getAppPath($appId) . '/appinfo/preupdate.php';
423
	}
424
425
	/**
426
	 * upgrades all apps within a major ownCloud upgrade. Also loads "priority"
427
	 * (types authentication, filesystem, logging, in that order) afterwards.
428
	 *
429
	 * @throws NeedsUpdateException
430
	 */
431
	protected function doAppUpgrade() {
432
		$apps = \OC_App::getEnabledApps();
433
		$priorityTypes = array('authentication', 'filesystem', 'logging');
434
		$pseudoOtherType = 'other';
435
		$stacks = array($pseudoOtherType => array());
436
437
		foreach ($apps as $appId) {
438
			$priorityType = false;
439
			foreach ($priorityTypes as $type) {
440
				if(!isset($stacks[$type])) {
441
					$stacks[$type] = array();
442
				}
443
				if (\OC_App::isType($appId, $type)) {
444
					$stacks[$type][] = $appId;
445
					$priorityType = true;
446
					break;
447
				}
448
			}
449
			if (!$priorityType) {
450
				$stacks[$pseudoOtherType][] = $appId;
451
			}
452
		}
453
		foreach ($stacks as $type => $stack) {
454
			foreach ($stack as $appId) {
455
				if (\OC_App::shouldUpgrade($appId)) {
456
					$this->emit('\OC\Updater', 'appUpgradeStarted', array($appId, \OC_App::getAppVersion($appId)));
457
					\OC_App::updateApp($appId);
458
					$this->emit('\OC\Updater', 'appUpgrade', array($appId, \OC_App::getAppVersion($appId)));
459
				}
460
				if($type !== $pseudoOtherType) {
461
					// load authentication, filesystem and logging apps after
462
					// upgrading them. Other apps my need to rely on modifying
463
					// user and/or filesystem aspects.
464
					\OC_App::loadApp($appId, false);
465
				}
466
			}
467
		}
468
	}
469
470
	/**
471
	 * check if the current enabled apps are compatible with the current
472
	 * ownCloud version. disable them if not.
473
	 * This is important if you upgrade ownCloud and have non ported 3rd
474
	 * party apps installed.
475
	 *
476
	 * @return array
477
	 * @throws \Exception
478
	 */
479
	private function checkAppsRequirements() {
480
		$isCoreUpgrade = $this->isCodeUpgrade();
481
		$apps = OC_App::getEnabledApps();
482
		$version = \OCP\Util::getVersion();
483
		$disabledApps = [];
484
		foreach ($apps as $app) {
485
			// check if the app is compatible with this version of ownCloud
486
			$info = OC_App::getAppInfo($app);
487
			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 486 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...
488
				OC_App::disable($app);
489
				$this->emit('\OC\Updater', 'incompatibleAppDisabled', array($app));
490
			}
491
			// no need to disable any app in case this is a non-core upgrade
492
			if (!$isCoreUpgrade) {
493
				continue;
494
			}
495
			// shipped apps will remain enabled
496
			if (OC_App::isShipped($app)) {
497
				continue;
498
			}
499
			// authentication and session apps will remain enabled as well
500
			if (OC_App::isType($app, ['session', 'authentication'])) {
501
				continue;
502
			}
503
504
			// disable any other 3rd party apps if not overriden
505
			if(!$this->skip3rdPartyAppsDisable) {
506
				\OC_App::disable($app);
507
				$disabledApps[]= $app;
508
				$this->emit('\OC\Updater', 'thirdPartyAppDisabled', array($app));
509
			};
510
		}
511
		return $disabledApps;
512
	}
513
514
	/**
515
	 * @return bool
516
	 */
517
	private function isCodeUpgrade() {
518
		$installedVersion = $this->config->getSystemValue('version', '0.0.0');
519
		$currentVersion = implode('.', \OCP\Util::getVersion());
520
		if (version_compare($currentVersion, $installedVersion, '>')) {
521
			return true;
522
		}
523
		return false;
524
	}
525
526
	/**
527
	 * @param array $disabledApps
528
	 * @throws \Exception
529
	 */
530
	private function upgradeAppStoreApps(array $disabledApps) {
531
		foreach($disabledApps as $app) {
532
			try {
533
				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...
534
					$ocsId = \OC::$server->getConfig()->getAppValue($app, 'ocsid', '');
535
536
					$this->emit('\OC\Updater', 'upgradeAppStoreApp', array($app));
537
					OC_Installer::updateAppByOCSId($ocsId);
538
				}
539
			} catch (\Exception $ex) {
540
				$this->log->logException($ex, ['app' => 'core']);
541
			}
542
		}
543
	}
544
}
545
546