Completed
Pull Request — master (#27730)
by Victor
16:06
created

Updater::setSkip3rdPartyAppsDisable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 3
rs 10
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 version from which this version is allowed to upgrade from
133
	 *
134
	 * @return string allowed previous version
135
	 */
136
	private function getAllowedPreviousVersion() {
137
		// this should really be a JSON file
138
		require \OC::$SERVERROOT . '/version.php';
139
		/** @var array $OC_VersionCanBeUpgradedFrom */
140
		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...
141
	}
142
143
	/**
144
	 * Return vendor from which this version was published
145
	 *
146
	 * @return string Get the vendor
147
	 */
148
	private function getVendor() {
149
		// this should really be a JSON file
150
		require \OC::$SERVERROOT . '/version.php';
151
		/** @var string $vendor */
152
		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...
153
	}
154
155
	/**
156
	 * Whether an upgrade to a specified version is possible
157
	 * @param string $oldVersion
158
	 * @param string $newVersion
159
	 * @param string $allowedPreviousVersion
160
	 * @return bool
161
	 */
162
	public function isUpgradePossible($oldVersion, $newVersion, $allowedPreviousVersion) {
163
		$allowedUpgrade =  (version_compare($allowedPreviousVersion, $oldVersion, '<=')
164
			&& (version_compare($oldVersion, $newVersion, '<=') || $this->config->getSystemValue('debug', false)));
165
166
		if ($allowedUpgrade) {
167
			return $allowedUpgrade;
168
		}
169
170
		// Upgrade not allowed, someone switching vendor?
171
		if ($this->getVendor() !== $this->config->getAppValue('core', 'vendor', '')) {
172
			$oldVersion = explode('.', $oldVersion);
173
			$newVersion = explode('.', $newVersion);
174
175
			return $oldVersion[0] === $newVersion[0] && $oldVersion[1] === $newVersion[1];
176
		}
177
178
		return false;
179
	}
180
181
	/**
182
	 * runs the update actions in maintenance mode, does not upgrade the source files
183
	 * except the main .htaccess file
184
	 *
185
	 * @param string $currentVersion current version to upgrade to
186
	 * @param string $installedVersion previous version from which to upgrade from
187
	 *
188
	 * @throws \Exception
189
	 */
190
	private function doUpgrade($currentVersion, $installedVersion) {
191
		// Stop update if the update is over several major versions
192
		$allowedPreviousVersion = $this->getAllowedPreviousVersion();
193
		if (!self::isUpgradePossible($installedVersion, $currentVersion, $allowedPreviousVersion)) {
194
			throw new \Exception('Updates between multiple major versions and downgrades are unsupported.');
195
		}
196
197
		// Update .htaccess files
198
		try {
199
			Setup::updateHtaccess();
200
			Setup::protectDataDirectory();
201
		} catch (\Exception $e) {
202
			throw new \Exception($e->getMessage());
203
		}
204
205
		// create empty file in data dir, so we can later find
206
		// out that this is indeed an ownCloud data directory
207
		// (in case it didn't exist before)
208
		file_put_contents($this->config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . '/.ocdata', '');
209
210
		// pre-upgrade repairs
211
		$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...
212
		$repair->run();
213
214
		$this->doCoreUpgrade();
215
216
		try {
217
			// TODO: replace with the new repair step mechanism https://github.com/owncloud/core/pull/24378
218
			Setup::installBackgroundJobs();
219
		} catch (\Exception $e) {
220
			throw new \Exception($e->getMessage());
221
		}
222
223
		// update all shipped apps
224
		$disabledApps = $this->checkAppsRequirements();
225
		$this->doAppUpgrade();
226
227
		// upgrade appstore apps
228
		$this->upgradeAppStoreApps($disabledApps);
229
230
		// install new shipped apps on upgrade
231
		OC_App::loadApps('authentication');
232
		$errors = Installer::installShippedApps(true);
233
		foreach ($errors as $appId => $exception) {
234
			/** @var \Exception $exception */
235
			$this->log->logException($exception, ['app' => $appId]);
236
			$this->emit('\OC\Updater', 'failure', [$appId . ': ' . $exception->getMessage()]);
237
		}
238
239
		// post-upgrade repairs
240
		$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...
241
		$repair->run();
242
243
		//Invalidate update feed
244
		$this->config->setAppValue('core', 'lastupdatedat', 0);
245
246
		// Check for code integrity if not disabled
247
		if(\OC::$server->getIntegrityCodeChecker()->isCodeCheckEnforced()) {
248
			$this->emit('\OC\Updater', 'startCheckCodeIntegrity');
249
			$this->checker->runInstanceVerification();
250
			$this->emit('\OC\Updater', 'finishedCheckCodeIntegrity');
251
		}
252
253
		// only set the final version if everything went well
254
		$this->config->setSystemValue('version', implode('.', Util::getVersion()));
255
		$this->config->setAppValue('core', 'vendor', $this->getVendor());
256
	}
257
258
	protected function doCoreUpgrade() {
259
		$this->emit('\OC\Updater', 'dbUpgradeBefore');
260
261
		// execute core migrations
262
		if (is_dir(\OC::$SERVERROOT."/core/Migrations")) {
263
			$ms = new \OC\DB\MigrationService('core', \OC::$server->getDatabaseConnection());
264
			$ms->migrate();
265
		}
266
267
		$this->emit('\OC\Updater', 'dbUpgrade');
268
	}
269
270
	/**
271
	 * upgrades all apps within a major ownCloud upgrade. Also loads "priority"
272
	 * (types authentication, filesystem, logging, in that order) afterwards.
273
	 *
274
	 * @throws NeedsUpdateException
275
	 */
276
	protected function doAppUpgrade() {
277
		$apps = \OC_App::getEnabledApps();
278
		$priorityTypes = ['authentication', 'filesystem', 'logging'];
279
		$pseudoOtherType = 'other';
280
		$stacks = [$pseudoOtherType => []];
281
282
		foreach ($apps as $appId) {
283
			$priorityType = false;
284
			foreach ($priorityTypes as $type) {
285
				if(!isset($stacks[$type])) {
286
					$stacks[$type] = [];
287
				}
288
				if (\OC_App::isType($appId, $type)) {
289
					$stacks[$type][] = $appId;
290
					$priorityType = true;
291
					break;
292
				}
293
			}
294
			if (!$priorityType) {
295
				$stacks[$pseudoOtherType][] = $appId;
296
			}
297
		}
298
		foreach ($stacks as $type => $stack) {
299
			foreach ($stack as $appId) {
300
				if (\OC_App::shouldUpgrade($appId)) {
301
					$this->emit('\OC\Updater', 'appUpgradeStarted', [$appId, \OC_App::getAppVersion($appId)]);
302
					\OC_App::updateApp($appId);
303
					$this->emit('\OC\Updater', 'appUpgrade', [$appId, \OC_App::getAppVersion($appId)]);
304
				}
305
				if($type !== $pseudoOtherType) {
306
					// load authentication, filesystem and logging apps after
307
					// upgrading them. Other apps my need to rely on modifying
308
					// user and/or filesystem aspects.
309
					\OC_App::loadApp($appId, false);
310
				}
311
			}
312
		}
313
	}
314
315
	/**
316
	 * check if the current enabled apps are compatible with the current
317
	 * ownCloud version. disable them if not.
318
	 * This is important if you upgrade ownCloud and have non ported 3rd
319
	 * party apps installed.
320
	 *
321
	 * @return array
322
	 * @throws \Exception
323
	 */
324
	private function checkAppsRequirements() {
325
		$isCoreUpgrade = $this->isCodeUpgrade();
326
		$apps = OC_App::getEnabledApps();
327
		$version = Util::getVersion();
328
		$disabledApps = [];
329
		foreach ($apps as $app) {
330
			// check if the app is compatible with this version of ownCloud
331
			$info = OC_App::getAppInfo($app);
332
			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 331 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...
333
				OC_App::disable($app);
334
				$disabledApps[]= $app;
335
				$this->emit('\OC\Updater', 'incompatibleAppDisabled', [$app]);
336
				continue;
337
			}
338
			// no need to disable any app in case this is a non-core upgrade
339
			if (!$isCoreUpgrade) {
340
				continue;
341
			}
342
			// shipped apps will remain enabled
343
			if (OC_App::isShipped($app)) {
344
				continue;
345
			}
346
			// authentication and session apps will remain enabled as well
347
			if (OC_App::isType($app, ['session', 'authentication'])) {
348
				continue;
349
			}
350
		}
351
		return $disabledApps;
352
	}
353
354
	/**
355
	 * @return bool
356
	 */
357
	private function isCodeUpgrade() {
358
		$installedVersion = $this->config->getSystemValue('version', '0.0.0');
359
		$currentVersion = implode('.', Util::getVersion());
360
		if (version_compare($currentVersion, $installedVersion, '>')) {
361
			return true;
362
		}
363
		return false;
364
	}
365
366
	/**
367
	 * @param array $disabledApps
368
	 * @throws \Exception
369
	 */
370
	private function upgradeAppStoreApps(array $disabledApps) {
371
		$dispatcher = \OC::$server->getEventDispatcher();
372
		foreach($disabledApps as $app) {
373
			try {
374
				$this->emit('\OC\Updater', 'upgradeAppStoreApp', [$app]);
375
				$dispatcher->dispatch(
376
					self::class . '::upgradeAppStoreApps',
377
					new GenericEvent($app)
378
				);
379
			} catch (\Exception $ex) {
380
				$this->log->logException($ex, ['app' => 'core']);
381
			}
382
		}
383
	}
384
385
	/**
386
	 * Forward messages emitted by the repair routine
387
	 */
388
	private function emitRepairEvents() {
389
		$dispatcher = \OC::$server->getEventDispatcher();
390
		$dispatcher->addListener('\OC\Repair::warning', function ($event) {
391
			if ($event instanceof GenericEvent) {
392
				$this->emit('\OC\Updater', 'repairWarning', $event->getArguments());
393
			}
394
		});
395
		$dispatcher->addListener('\OC\Repair::error', function ($event) {
396
			if ($event instanceof GenericEvent) {
397
				$this->emit('\OC\Updater', 'repairError', $event->getArguments());
398
			}
399
		});
400
		$dispatcher->addListener('\OC\Repair::info', function ($event) {
401
			if ($event instanceof GenericEvent) {
402
				$this->emit('\OC\Updater', 'repairInfo', $event->getArguments());
403
			}
404
		});
405
		$dispatcher->addListener('\OC\Repair::step', function ($event) {
406
			if ($event instanceof GenericEvent) {
407
				$this->emit('\OC\Updater', 'repairStep', $event->getArguments());
408
			}
409
		});
410
	}
411
412
}
413
414