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) { |
|
|
|
|
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])) { |
|
|
|
|
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])) { |
|
|
|
|
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])) { |
|
|
|
|
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()) { |
|
|
|
|
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')) { |
|
|
|
|
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')) { |
|
|
|
|
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
|
|
|
|
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.