Completed
Push — master ( 9bc980...bafdb4 )
by Thomas
53s
created

Apps::isCoreUpdate()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 7

Duplication

Lines 9
Ratio 100 %

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 0
dl 9
loc 9
rs 9.6666
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Victor Dubiniuk <[email protected]>
4
 *
5
 * @copyright Copyright (c) 2017, 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 OC\RepairException;
25
use OC_App;
26
use OCP\App\AppAlreadyInstalledException;
27
use OCP\App\AppManagerException;
28
use OCP\App\AppNotFoundException;
29
use OCP\App\AppNotInstalledException;
30
use OCP\App\AppUpdateNotFoundException;
31
use OCP\App\IAppManager;
32
use OCP\Migration\IOutput;
33
use OCP\Migration\IRepairStep;
34
use OCP\Util;
35
use Symfony\Component\EventDispatcher\EventDispatcher;
36
use Symfony\Component\EventDispatcher\GenericEvent;
37
use OCP\IConfig;
38
39
class Apps implements IRepairStep {
40
41
	const KEY_COMPATIBLE = 'compatible';
42
	const KEY_INCOMPATIBLE = 'incompatible';
43
	const KEY_MISSING = 'missing';
44
45
	/** @var  IAppManager */
46
	private $appManager;
47
48
	/** @var  EventDispatcher */
49
	private $eventDispatcher;
50
51
	/** @var IConfig */
52
	private $config;
53
54
	/** @var \OC_Defaults */
55
	private $defaults;
56
57
	/**
58
	 * Apps constructor.
59
	 *
60
	 * @param IAppManager $appManager
61
	 * @param EventDispatcher $eventDispatcher
62
	 * @param IConfig $config
63
	 * @param \OC_Defaults $defaults
64
	 */
65
	public function __construct(IAppManager $appManager, EventDispatcher $eventDispatcher, IConfig $config, \OC_Defaults $defaults) {
66
		$this->appManager = $appManager;
67
		$this->eventDispatcher = $eventDispatcher;
68
		$this->config = $config;
69
		$this->defaults = $defaults;
70
	}
71
72
	/**
73
	 * @return string
74
	 */
75
	public function getName() {
76
		return 'Upgrade app code from the marketplace';
77
	}
78
79
	/**
80
	 * Are we updating from an older version?
81
	 * @return bool
82
	 */
83 View Code Duplication
	private function isCoreUpdate() {
84
		$installedVersion = $this->config->getSystemValue('version', '0.0.0');
85
		$currentVersion = implode('.', Util::getVersion());
86
		$versionDiff = version_compare($currentVersion, $installedVersion);
87
		if ($versionDiff > 0) {
88
			return true;
89
		}
90
		return false;
91
	}
92
93
	/**
94
	 * If we are updating from <= 10.0.0 we need to enable the marketplace before running the update
95
	 * @return bool
96
	 */
97
	private function requiresMarketEnable() {
98
		$installedVersion = $this->config->getSystemValue('version', '0.0.0');
99
		$versionDiff = version_compare('10.0.0', $installedVersion);
100
		if ($versionDiff < 0) {
101
			return false;
102
		}
103
		return true;
104
105
	}
106
107
	/**
108
	 * @param IOutput $output
109
	 * @throws RepairException
110
	 */
111
	public function run(IOutput $output) {
112
113
		if ($this->config->getSystemValue('has_internet_connection', true) !== true) {
114
			$link = $this->defaults->buildDocLinkToKey('admin-marketplace-apps');
115
			$output->info('No internet connection available - no app updates will be taken from the marketplace.');
116
			$output->info("How to update apps in such situation please see $link");
117
			return;
118
		}
119
		$appsToUpgrade = $this->getAppsToUpgrade();
120
		$failedCompatibleApps = [];
121
		$failedMissingApps = $appsToUpgrade[self::KEY_MISSING];
122
		$failedIncompatibleApps = $appsToUpgrade[self::KEY_INCOMPATIBLE];
123
		$hasNotUpdatedCompatibleApps = 0;
124
125
		// fix market app state
126
		$shallContactMarketplace = $this->fixMarketAppState($output);
127
		if ($shallContactMarketplace) {
128
			// Check if we can use the marketplace to update apps as needed?
129
			if ($this->appManager->isEnabledForUser('market')) {
130
				// Use market to fix missing / old apps
131
				$this->loadApp('market');
132
				$output->info('Using market to update existing apps');
133
				try {
134
					// Try to update incompatible apps
135 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...
136
						$output->info('Attempting to update the following existing but incompatible app from market: ' . implode(', ', $appsToUpgrade[self::KEY_INCOMPATIBLE]));
137
						$failedIncompatibleApps = $this->getAppsFromMarket(
138
							$output,
139
							$appsToUpgrade[self::KEY_INCOMPATIBLE],
140
							'upgradeAppStoreApp'
141
						);
142
					}
143
144
					// Try to download missing apps
145 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...
146
						$output->info('Attempting to update the following missing apps from market: ' . implode(', ', $appsToUpgrade[self::KEY_MISSING]));
147
						$failedMissingApps = $this->getAppsFromMarket(
148
							$output,
149
							$appsToUpgrade[self::KEY_MISSING],
150
							'reinstallAppStoreApp'
151
						);
152
					}
153
154
					// Try to update compatible apps
155 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...
156
						$output->info('Attempting to update the following existing compatible apps from market: ' . implode(', ', $appsToUpgrade[self::KEY_MISSING]));
157
						$failedCompatibleApps = $this->getAppsFromMarket(
158
							$output,
159
							$appsToUpgrade[self::KEY_COMPATIBLE],
160
							'upgradeAppStoreApp'
161
						);
162
					}
163
164
					$hasNotUpdatedCompatibleApps = count($failedCompatibleApps);
165
				} catch (AppManagerException $e) {
166
					$output->warning('No connection to marketplace: ' . $e->getPrevious());
167
				}
168
			} else {
169
				// No market available, output error and continue attempt
170
				$output->warning('Market app is unavailable for updating of apps. Enable with: occ app:enable market');
171
			}
172
		}
173
174
		$hasBlockingMissingApps = count($failedMissingApps);
175
		$hasBlockingIncompatibleApps = count($failedIncompatibleApps);
176
177
		if ($hasBlockingIncompatibleApps || $hasBlockingMissingApps) {
178
			// fail
179
			$output->warning('You have incompatible or missing apps enabled that could not be found or updated via the marketplace.');
180
			$output->warning(
181
				'please install app manually with tarball or disable them with:'
182
				. $this->getOccDisableMessage(array_merge($failedIncompatibleApps, $failedMissingApps))
183
			);
184
185
			throw new RepairException('Upgrade is not possible');
186
		} elseif ($hasNotUpdatedCompatibleApps) {
187
			foreach ($failedCompatibleApps as $app) {
188
				// TODO: Show reason
189
				$output->info("App was not updated: $app");
190
			}
191
		}
192
	}
193
194
	/**
195
	 * Upgrade appList from market
196
	 * Return an array of apps that were not upgraded successfully
197
	 *
198
	 * @param IOutput $output
199
	 * @param string[] $appList
200
	 * @param string $event
201
	 * @return array
202
	 * @throws AppManagerException
203
	 */
204
	protected function getAppsFromMarket(IOutput $output, $appList, $event) {
205
		$failedApps = [];
206
		foreach ($appList as $app) {
207
			$output->info("Fetching app from market: $app");
208
			try {
209
				$this->eventDispatcher->dispatch(
210
					sprintf('%s::%s', IRepairStep::class, $event),
211
					new GenericEvent($app)
212
				);
213
			} catch (AppAlreadyInstalledException $e) {
214
				$output->info($e->getMessage());
215
				$failedApps[] = $app;
216
			} catch (AppNotInstalledException $e) {
217
				$output->info($e->getMessage());
218
				$failedApps[] = $app;
219
			} catch (AppNotFoundException $e) {
220
				$output->info($e->getMessage());
221
				$failedApps[] = $app;
222
			} catch (AppUpdateNotFoundException $e) {
223
				$output->info($e->getMessage());
224
				$failedApps[] = $app;
225
			} catch (AppManagerException $e) {
226
				// No connection to market. Abort.
227
				throw $e;
228
			} catch (\Exception $e) {
229
				// TODO: check the reason
230
				$failedApps[] = $app;
231
				$output->warning(get_class($e));
232
233
				$output->warning($e->getMessage());
234
			}
235
		}
236
		return $failedApps;
237
	}
238
239
	/**
240
	 * Get app list separated as compatible/incompatible/missing
241
	 *
242
	 * @return array
243
	 */
244
	protected function getAppsToUpgrade() {
245
		$installedApps = $this->appManager->getInstalledApps();
246
		$appsToUpgrade = [
247
			self::KEY_COMPATIBLE => [],
248
			self::KEY_INCOMPATIBLE => [],
249
			self::KEY_MISSING => []
250
		];
251
252
		foreach ($installedApps as $appId) {
253
			$info = $this->appManager->getAppInfo($appId);
254
			if (!isset($info['id']) || is_null($info['id'])) {
255
				$appsToUpgrade[self::KEY_MISSING][] = $appId;
256
				continue;
257
			}
258
			$version = Util::getVersion();
259
			$key = (\OC_App::isAppCompatible($version, $info)) ? self::KEY_COMPATIBLE : self::KEY_INCOMPATIBLE;
260
			$appsToUpgrade[$key][] = $appId;
261
		}
262
		return $appsToUpgrade;
263
	}
264
265
	protected function getOccDisableMessage($appList) {
266
		if (!count($appList)) {
267
			return '';
268
		}
269
		$appList = array_map(
270
			function ($appId) {
271
				return "occ app:disable $appId";
272
			},
273
			$appList
274
		);
275
		return "\n" . implode("\n", $appList);
276
	}
277
278
	/**
279
	 * @codeCoverageIgnore
280
	 * @param string $app
281
	 */
282
	protected function loadApp($app) {
283
		OC_App::loadApp($app, false);
284
	}
285
286
	/**
287
	 * @return bool
288
	 */
289
	private function isAppStoreEnabled() {
290
		// if appstoreenabled was explicitly disabled we shall not use the market app for upgrade
291
		$appStoreEnabled = $this->config->getSystemValue('appstoreenabled', null);
292
		if ($appStoreEnabled === false) {
293
			return false;
294
		}
295
		return true;
296
	}
297
298
	private function fixMarketAppState(IOutput $output) {
299
		// no core update -> nothing to do
300
		if (!$this->isCoreUpdate()) {
301
			return false;
302
		}
303
304
		// no update from a version before 10.0 -> nothing to do, but allow apps to be updated
305
		if (!$this->requiresMarketEnable()) {
306
			return true;
307
		}
308
		// if the appstore was explicitly disabled -> disable market app as well
309
		if (!$this->isAppStoreEnabled()) {
310
			$this->appManager->disableApp('market');
311
			$link = $this->defaults->buildDocLinkToKey('admin-marketplace-apps');
312
			$output->info('Appstore was disabled in past versions and marketplace interactions are disabled for now as well.');
313
			$output->info('If you would like to get automated app updates on upgrade please enable the market app and remove "appstoreenabled" from your config.');
314
			$output->info("Please note that the market app is not recommended for clustered setups - see $link");
315
			return false;
316
		}
317
		// Then we need to enable the market app to support app updates / downloads during upgrade
318
		$output->info('Enabling market app to assist with update');
319
		$this->appManager->enableApp('market');
320
		return true;
321
	}
322
}
323