Completed
Pull Request — master (#32577)
by Victor
09:29
created

Apps::changeSchema()   A

Complexity

Conditions 5
Paths 9

Size

Total Lines 36

Duplication

Lines 20
Ratio 55.56 %

Importance

Changes 0
Metric Value
cc 5
nc 9
nop 2
dl 20
loc 36
rs 9.0328
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Viktar Dubiniuk <[email protected]>
4
 *
5
 * @copyright Copyright (c) 2018, ownCloud GmbH
6
 * @license AGPL-3.0
7
 *
8
 * This code is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU Affero General Public License, version 3,
10
 * as published by the Free Software Foundation.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
 * GNU Affero General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU Affero General Public License, version 3,
18
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
19
 *
20
 */
21
22
namespace OC\Repair;
23
24
use Doctrine\DBAL\Schema\Schema;
25
use Doctrine\DBAL\Types\Type;
26
use OC\RepairException;
27
use OC_App;
28
use OCP\App\AppAlreadyInstalledException;
29
use OCP\App\AppManagerException;
30
use OCP\App\AppNotFoundException;
31
use OCP\App\AppNotInstalledException;
32
use OCP\App\AppUpdateNotFoundException;
33
use OCP\App\IAppManager;
34
use OCP\Migration\IOutput;
35
use OCP\Migration\IRepairStep;
36
use OCP\Util;
37
use Symfony\Component\EventDispatcher\EventDispatcher;
38
use Symfony\Component\EventDispatcher\GenericEvent;
39
use OCP\IConfig;
40
41
class Apps implements IRepairStep {
42
	const KEY_COMPATIBLE = 'compatible';
43
	const KEY_INCOMPATIBLE = 'incompatible';
44
	const KEY_MISSING = 'missing';
45
46
	/** @var  IAppManager */
47
	private $appManager;
48
49
	/** @var  EventDispatcher */
50
	private $eventDispatcher;
51
52
	/** @var IConfig */
53
	private $config;
54
55
	/** @var \OC_Defaults */
56
	private $defaults;
57
58
	/**
59
	 * Apps constructor.
60
	 *
61
	 * @param IAppManager $appManager
62
	 * @param EventDispatcher $eventDispatcher
63
	 * @param IConfig $config
64
	 * @param \OC_Defaults $defaults
65
	 */
66
	public function __construct(IAppManager $appManager, EventDispatcher $eventDispatcher, IConfig $config, \OC_Defaults $defaults) {
67
		$this->appManager = $appManager;
68
		$this->eventDispatcher = $eventDispatcher;
69
		$this->config = $config;
70
		$this->defaults = $defaults;
71
	}
72
73
	/**
74
	 * @return string
75
	 */
76
	public function getName() {
77
		return 'Upgrade app code from the marketplace';
78
	}
79
80
	/**
81
	 * Are we updating from an older version?
82
	 * @return bool
83
	 */
84
	private function isCoreUpdate() {
85
		$installedVersion = $this->config->getSystemValue('version', '0.0.0');
86
		$currentVersion = \implode('.', Util::getVersion());
87
		$versionDiff = \version_compare($currentVersion, $installedVersion);
88
		if ($versionDiff > 0) {
89
			return true;
90
		}
91
		return false;
92
	}
93
94
	/**
95
	 * If we are updating from <= 10.0.0 we need to enable the marketplace before running the update
96
	 * @return bool
97
	 */
98
	private function requiresMarketEnable() {
99
		$installedVersion = $this->config->getSystemValue('version', '0.0.0');
100
		$versionDiff = \version_compare('10.0.0', $installedVersion);
101
		if ($versionDiff < 0) {
102
			return false;
103
		}
104
		return true;
105
	}
106
107
	/**
108
	 * @param IOutput $output
109
	 * @throws RepairException
110
	 */
111
	public function run(IOutput $output) {
112 View Code Duplication
		if ($this->config->getSystemValue('has_internet_connection', true) !== true) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
113
			$link = $this->defaults->buildDocLinkToKey('admin-marketplace-apps');
114
			$output->info('No internet connection available - no app updates will be taken from the marketplace.');
115
			$output->info("How to update apps in such situation please see $link");
116
			$this->appManager->disableApp('market');
117
		}
118
		$appsToUpgrade = $this->getAppsToUpgrade();
119
		$failedCompatibleApps = [];
120
		$failedMissingApps = $appsToUpgrade[self::KEY_MISSING];
121
		$failedIncompatibleApps = $appsToUpgrade[self::KEY_INCOMPATIBLE];
122
		$hasNotUpdatedCompatibleApps = 0;
123
124
		// fix market app state
125
		$shallContactMarketplace = $this->fixMarketAppState($output);
126
127
		// market might be enabled but admin does not want to automatically update apps through it
128
		// (they might want to manually click through the updates in the web UI so keeping the
129
		// market enabled here is a legitimate use case)
130
		if ($this->config->getSystemValue('upgrade.automatic-app-update', true) !== true) {
131
			$shallContactMarketplace = false;
132
		}
133
134
		if ($shallContactMarketplace) {
135
			// Check if we can use the marketplace to update apps as needed?
136
			if ($this->appManager->isEnabledForUser('market')) {
137
				// Use market to fix missing / old apps
138
				$this->loadApp('market');
139
				$output->info('Using market to update existing apps');
140
				try {
141
					// Try to update incompatible apps
142 View Code Duplication
					if (!empty($appsToUpgrade[self::KEY_INCOMPATIBLE])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
143
						$output->info('Attempting to update the following existing but incompatible app from market: ' . \implode(', ', $appsToUpgrade[self::KEY_INCOMPATIBLE]));
144
						$failedIncompatibleApps = $this->getAppsFromMarket(
145
							$output,
146
							$appsToUpgrade[self::KEY_INCOMPATIBLE],
147
							'upgradeAppStoreApp'
148
						);
149
					}
150
151
					// Try to download missing apps
152 View Code Duplication
					if (!empty($appsToUpgrade[self::KEY_MISSING])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
153
						$output->info('Attempting to update the following missing apps from market: ' . \implode(', ', $appsToUpgrade[self::KEY_MISSING]));
154
						$failedMissingApps = $this->getAppsFromMarket(
155
							$output,
156
							$appsToUpgrade[self::KEY_MISSING],
157
							'reinstallAppStoreApp'
158
						);
159
					}
160
161
					// Try to update compatible apps
162 View Code Duplication
					if (!empty($appsToUpgrade[self::KEY_COMPATIBLE])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
163
						$output->info('Attempting to update the following existing compatible apps from market: ' . \implode(', ', $appsToUpgrade[self::KEY_MISSING]));
164
						$failedCompatibleApps = $this->getAppsFromMarket(
165
							$output,
166
							$appsToUpgrade[self::KEY_COMPATIBLE],
167
							'upgradeAppStoreApp'
168
						);
169
					}
170
171
					$hasNotUpdatedCompatibleApps = \count($failedCompatibleApps);
172
				} catch (AppManagerException $e) {
173
					$output->warning($e->getMessage());
174
				}
175
			} else {
176
				// No market available, output error and continue attempt
177
				$link = $this->defaults->buildDocLinkToKey('admin-marketplace-apps');
178
				$output->warning("Market app is unavailable for updating of apps. Please update manually, see $link");
179
			}
180
		}
181
182
		$hasBlockingMissingApps = \count($failedMissingApps);
183
		$hasBlockingIncompatibleApps = \count($failedIncompatibleApps);
184
185
		if ($hasBlockingIncompatibleApps || $hasBlockingMissingApps) {
186
			// fail
187
			$output->warning('You have incompatible or missing apps enabled that could not be found or updated via the marketplace.');
188
			$output->warning(
189
				'Please install or update the following apps manually or disable them with:'
190
				. $this->getOccDisableMessage(\array_merge($failedIncompatibleApps, $failedMissingApps))
191
			);
192
			$link = $this->defaults->buildDocLinkToKey('admin-marketplace-apps');
193
			$output->warning("For manually updating, see $link");
194
195
			throw new RepairException('Upgrade is not possible');
196
		} elseif ($hasNotUpdatedCompatibleApps) {
197
			foreach ($failedCompatibleApps as $app) {
198
				// TODO: Show reason
199
				$output->info("App was not updated: $app");
200
			}
201
		}
202
	}
203
204
	/**
205
	 * Upgrade appList from market
206
	 * Return an array of apps that were not upgraded successfully
207
	 *
208
	 * @param IOutput $output
209
	 * @param string[] $appList
210
	 * @param string $event
211
	 * @return array
212
	 * @throws AppManagerException
213
	 */
214
	protected function getAppsFromMarket(IOutput $output, $appList, $event) {
215
		$failedApps = [];
216
		foreach ($appList as $app) {
217
			$output->info("Fetching app from market: $app");
218
			try {
219
				$this->eventDispatcher->dispatch(
220
					\sprintf('%s::%s', IRepairStep::class, $event),
221
					new GenericEvent($app)
222
				);
223
			} catch (AppAlreadyInstalledException $e) {
224
				$output->info($e->getMessage());
225
				$failedApps[] = $app;
226
			} catch (AppNotInstalledException $e) {
227
				$output->info($e->getMessage());
228
				$failedApps[] = $app;
229
			} catch (AppNotFoundException $e) {
230
				$output->info($e->getMessage());
231
				$failedApps[] = $app;
232
			} catch (AppUpdateNotFoundException $e) {
233
				$output->info($e->getMessage());
234
				$failedApps[] = $app;
235
			} catch (AppManagerException $e) {
236
				// No connection to market. Abort.
237
				throw $e;
238
			} catch (\Exception $e) {
239
				// TODO: check the reason
240
				$failedApps[] = $app;
241
				$output->warning(\get_class($e));
242
243
				$output->warning($e->getMessage());
244
			}
245
		}
246
		return $failedApps;
247
	}
248
249
	/**
250
	 * Get app list separated as compatible/incompatible/missing
251
	 *
252
	 * @return array
253
	 */
254
	protected function getAppsToUpgrade() {
255
		$installedApps = $this->appManager->getInstalledApps();
256
		$appsToUpgrade = [
257
			self::KEY_COMPATIBLE => [],
258
			self::KEY_INCOMPATIBLE => [],
259
			self::KEY_MISSING => []
260
		];
261
262
		foreach ($installedApps as $appId) {
263
			$info = $this->appManager->getAppInfo($appId);
264
			if (!isset($info['id']) || $info['id'] === null) {
265
				$appsToUpgrade[self::KEY_MISSING][] = $appId;
266
				continue;
267
			}
268
			$version = Util::getVersion();
269
			$key = (\OC_App::isAppCompatible($version, $info)) ? self::KEY_COMPATIBLE : self::KEY_INCOMPATIBLE;
270
			$appsToUpgrade[$key][] = $appId;
271
		}
272
		return $appsToUpgrade;
273
	}
274
275
	protected function getOccDisableMessage($appList) {
276
		if (!\count($appList)) {
277
			return '';
278
		}
279
		$appList = \array_map(
280
			function ($appId) {
281
				return "occ app:disable $appId";
282
			},
283
			$appList
284
		);
285
		return "\n" . \implode("\n", $appList);
286
	}
287
288
	/**
289
	 * @codeCoverageIgnore
290
	 * @param string $app
291
	 */
292
	protected function loadApp($app) {
293
		OC_App::loadApp($app, false);
294
	}
295
296
	/**
297
	 * @return bool
298
	 */
299
	private function isAppStoreEnabled() {
300
		// if appstoreenabled was explicitly disabled we shall not use the market app for upgrade
301
		$appStoreEnabled = $this->config->getSystemValue('appstoreenabled', null);
302
		if ($appStoreEnabled === false) {
303
			return false;
304
		}
305
		return true;
306
	}
307
308
	private function fixMarketAppState(IOutput $output) {
309
		// no core update -> nothing to do
310
		if (!$this->isCoreUpdate()) {
311
			return false;
312
		}
313
314
		// no update from a version before 10.0 -> nothing to do, but allow apps to be updated
315
		if (!$this->requiresMarketEnable()) {
316
			return true;
317
		}
318
		// if the appstore was explicitly disabled -> disable market app as well
319 View Code Duplication
		if (!$this->isAppStoreEnabled()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
320
			$this->appManager->disableApp('market');
321
			$link = $this->defaults->buildDocLinkToKey('admin-marketplace-apps');
322
			$output->info('Appstore was disabled in past versions and marketplace interactions are disabled for now as well.');
323
			$output->info('If you would like to get automated app updates on upgrade please enable the market app and remove "appstoreenabled" from your config.');
324
			$output->info("Please note that the market app is not recommended for clustered setups - see $link");
325
			return false;
326
		}
327
328
		// Then we need to enable the market app to support app updates / downloads during upgrade
329
		$output->info('Enabling market app to assist with update');
330
		try {
331
			// Prepare oc_jobs for older ownCloud version fixes https://github.com/owncloud/update-testing/issues/5
332
			$connection = \OC::$server->getDatabaseConnection();
333
			$toSchema = $connection->createSchema();
334
			$this->changeSchema($toSchema, ['tablePrefix' => $connection->getPrefix()]);
335
			$connection->migrateToSchema($toSchema);
336
337
			$this->appManager->enableApp('market');
338
			return true;
339
		} catch (\Exception $ex) {
340
			$output->warning($ex->getMessage());
341
			return false;
342
		}
343
	}
344
345
	/**
346
	 * DB update for oc_jobs table
347
	 * it is intentionally duplicates 20170213215145 and a part of 20170101215145
348
	 * to allow seamless market app installation
349
	 *
350
	 * @param Schema $schema
351
	 * @param array $options
352
	 * @throws \Doctrine\DBAL\Schema\SchemaException
353
	 */
354
	private function changeSchema(Schema $schema, array $options) {
355
		$prefix = $options['tablePrefix'];
356
		if ($schema->hasTable("${prefix}jobs")) {
357
			$jobsTable = $schema->getTable("${prefix}jobs");
358
359 View Code Duplication
			if (!$jobsTable->hasColumn('last_checked')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
360
				$jobsTable->addColumn(
361
					'last_checked',
362
					Type::INTEGER,
363
					[
364
						'default' => 0,
365
						'notnull' => false
366
					]
367
				);
368
			}
369
370 View Code Duplication
			if (!$jobsTable->hasColumn('reserved_at')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
371
				$jobsTable->addColumn(
372
					'reserved_at',
373
					Type::INTEGER,
374
					[
375
						'default' => 0,
376
						'notnull' => false
377
					]
378
				);
379
			}
380
381
			if (!$jobsTable->hasColumn('execution_duration')) {
382
				$jobsTable->addColumn('execution_duration', Type::INTEGER, [
383
					'notnull' => true,
384
					'length' => 5,
385
					'default' => -1,
386
				]);
387
			}
388
		}
389
	}
390
}
391