Completed
Push — 3.x ( 668272...4d1768 )
by Jeroen
156:21 queued 83:40
created

engine/classes/Elgg/UpgradeService.php (1 issue)

1
<?php
2
3
namespace Elgg;
4
5
use Elgg\Cli\Progress;
6
use Elgg\Database\Mutex;
7
use Elgg\i18n\Translator;
8
use Elgg\Upgrade\Locator;
9
use Elgg\Upgrade\Loop;
10
use Elgg\Upgrade\Result;
11
use ElggUpgrade;
12
use function React\Promise\all;
13
use React\Promise\Deferred;
14
use React\Promise\Promise;
15
use RuntimeException;
16
use Throwable;
17
use Elgg\Database\Clauses\EntitySortByClause;
18
19
/**
20
 * Upgrade service for Elgg
21
 *
22
 * @internal
23
 */
24
class UpgradeService {
25
26
	use Loggable;
27
28
	/**
29
	 * @var Locator
30
	 */
31
	protected $locator;
32
33
	/**
34
	 * @var Translator
35
	 */
36
	private $translator;
37
38
	/**
39
	 * @var EventsService
40
	 */
41
	private $events;
42
43
	/**
44
	 * @var Config
45
	 */
46
	private $config;
47
48
	/**
49
	 * @var Mutex
50
	 */
51
	private $mutex;
52
53
	/**
54
	 * @var SystemMessagesService
55
	 */
56
	private $system_messages;
57
58
	/**
59
	 * @var Progress
60
	 */
61
	protected $progress;
62
63
	/**
64
	 * Constructor
65
	 *
66
	 * @param Locator               $locator         Upgrade locator
67
	 * @param Translator            $translator      Translation service
68
	 * @param EventsService         $events          Events service
69
	 * @param Config                $config          Config
70
	 * @param Logger                $logger          Logger
71
	 * @param Mutex                 $mutex           Database mutex service
72
	 * @param SystemMessagesService $system_messages System messages
73
	 * @param Progress              $progress        Progress
74
	 */
75 9
	public function __construct(
76
		Locator $locator,
77
		Translator $translator,
78
		EventsService $events,
79
		Config $config,
80
		Logger $logger,
81
		Mutex $mutex,
82
		SystemMessagesService $system_messages,
83
		Progress $progress
84
	) {
85 9
		$this->locator = $locator;
86 9
		$this->translator = $translator;
87 9
		$this->events = $events;
88 9
		$this->config = $config;
89 9
		$this->logger = $logger;
90 9
		$this->mutex = $mutex;
91 9
		$this->system_messages = $system_messages;
92 9
		$this->progress = $progress;
93 9
	}
94
95
	/**
96
	 * Start an upgrade process
97
	 * @return Promise
98
	 */
99
	protected function up() {
100 3
		return new Promise(function ($resolve, $reject) {
101 3
			Application::migrate();
102
103 3
			if (!$this->events->triggerBefore('upgrade', 'system', null)) {
104
				return $reject(new RuntimeException($this->translator->translate('upgrade:terminated')));
105
			}
106
107
			// prevent someone from running the upgrade script in parallel (see #4643)
108 3
			if (!$this->mutex->lock('upgrade')) {
109
				return $reject(new RuntimeException($this->translator->translate('upgrade:locked')));
110
			}
111
112
			// Clear system caches
113 3
			_elgg_disable_caches();
114 3
			_elgg_clear_caches();
115
116 3
			return $resolve();
117 3
		});
118
	}
119
120
	/**
121
	 * Finish an upgrade process
122
	 * @return Promise
123
	 */
124
	protected function down() {
125 3
		return new Promise(function ($resolve, $reject) {
126 3
			if (!$this->events->trigger('upgrade', 'system', null)) {
127
				return $reject();
128
			}
129
130 3
			elgg_flush_caches();
131
132 3
			$this->mutex->unlock('upgrade');
133
134 3
			$this->events->triggerAfter('upgrade', 'system', null);
135
136 3
			return $resolve();
137 3
		});
138
	}
139
140
	/**
141
	 * Run legacy upgrade scripts
142
	 * @return Promise
143
	 * @deprecated 3.0
144
	 * @codeCoverageIgnore
145
	 */
146
	protected function runLegacyUpgrades() {
147
		return new Promise(function ($resolve, $reject) {
148
			if ($this->getUnprocessedUpgrades()) {
149
				$this->processUpgrades();
150
			}
151
152
			return $resolve();
153
		});
154
	}
155
156
	/**
157
	 * Run system and async upgrades
158
	 *
159
	 * @param ElggUpgrade[] $upgrades Upgrades to run
160
	 *
161
	 * @return Promise
162
	 */
163 3
	protected function runUpgrades($upgrades) {
164 3
		$promises = [];
165
166 3
		foreach ($upgrades as $key => $upgrade) {
167
			if (!$upgrade instanceof ElggUpgrade) {
168
				continue;
169
			}
170
			$promises[] = new Promise(function ($resolve, $reject) use ($upgrade) {
171
				try {
172
					$result = $this->executeUpgrade($upgrade, false);
173
				} catch (Throwable $ex) {
174
					return $reject($ex);
175
				}
176
177
				if ($result->getFailureCount()) {
178
					$msg = elgg_echo('admin:upgrades:failed', [
179
						$upgrade->getDisplayName(),
180
					]);
181
182
					return $reject(new RuntimeException($msg));
183
				} else {
184
					return $resolve($result);
185
				}
186
			});
187
		}
188
189 3
		return all($promises);
190
	}
191
192
	/**
193
	 * Run the upgrade process
194
	 *
195
	 * @param ElggUpgrade[] $upgrades Upgrades to run
196
	 *
197
	 * @return Promise
198
	 * @throws RuntimeException
199
	 */
200 3
	public function run($upgrades = null) {
201
		// turn off time limit
202 3
		set_time_limit(3600);
203
204 3
		$deferred = new Deferred();
205
206 3
		$promise = $deferred->promise();
207
208 3
		$resolve = function ($value) use ($deferred) {
209 3
			$deferred->resolve($value);
210 3
		};
211
212 3
		$reject = function ($error) use ($deferred) {
213
			$deferred->reject($error);
214 3
		};
215
216 3
		if (!isset($upgrades)) {
217 1
			$upgrades = $this->getPendingUpgrades(false);
218
		}
219
220 3
		$this->up()->done(
221 3
			function () use ($resolve, $reject, $upgrades) {
222 3
				all([
223 3
					$this->runLegacyUpgrades(),
224 3
					$this->runUpgrades($upgrades),
225 3
				])->done(
226 3
					function () use ($resolve, $reject) {
227 3
						$this->down()->done(
228 3
							function ($result) use ($resolve) {
229 3
								return $resolve($result);
230 3
							},
231 3
							$reject
232
						);
233 3
					},
234 3
					$reject
235
				);
236 3
			},
237 3
			$reject
238
		);
239
240 3
		return $promise;
241
	}
242
243
	/**
244
	 * Run any php upgrade scripts which are required
245
	 *
246
	 * @param int  $version Version upgrading from.
247
	 * @param bool $quiet   Suppress errors.  Don't use this.
248
	 *
249
	 * @return bool
250
	 * @deprecated 3.0
251
	 * @codeCoverageIgnore
252
	 */
253
	protected function upgradeCode($version, $quiet = false) {
254
		$version = (int) $version;
255
		$upgrade_path = elgg_get_engine_path() . '/lib/upgrades/';
256
		$processed_upgrades = $this->getProcessedUpgrades();
257
258
		$upgrade_files = $this->getUpgradeFiles($upgrade_path);
259
260
		if ($upgrade_files === false) {
261
			return false;
262
		}
263
264
		$upgrades = $this->getUnprocessedUpgrades($upgrade_files, $processed_upgrades);
265
266
		// Sort and execute
267
		sort($upgrades);
268
269
		foreach ($upgrades as $upgrade) {
270
			$upgrade_version = $this->getUpgradeFileVersion($upgrade);
271
			$success = true;
272
273
			if ($upgrade_version <= $version) {
274
				// skip upgrade files from before the installation version of Elgg
275
				// because the upgrade files from before the installation version aren't
276
				// added to the database.
277
				continue;
278
			}
279
280
			// hide all errors.
281
			if ($quiet) {
282
				// hide include errors as well as any exceptions that might happen
283
				try {
284
					if (!@Includer::includeFile("$upgrade_path/$upgrade")) {
285
						$success = false;
286
						$this->logger->error("Could not include $upgrade_path/$upgrade");
287
					}
288
				} catch (\Exception $e) {
289
					$success = false;
290
					$this->logger->error($e);
291
				}
292
			} else {
293
				if (!Includer::includeFile("$upgrade_path/$upgrade")) {
294
					$success = false;
295
					$this->logger->error("Could not include $upgrade_path/$upgrade");
296
				}
297
			}
298
299
			if ($success) {
300
				// don't set the version to a lower number in instances where an upgrade
301
				// has been merged from a lower version of Elgg
302
				if ($upgrade_version > $version) {
303
					$this->config->save('version', $upgrade_version);
304
				}
305
306
				// incrementally set upgrade so we know where to start if something fails.
307
				$this->setProcessedUpgrade($upgrade);
308
			} else {
309
				return false;
310
			}
311
		}
312
313
		return true;
314
	}
315
316
	/**
317
	 * Saves a processed upgrade to a dataset.
318
	 *
319
	 * @param string $upgrade Filename of the processed upgrade
320
	 *                        (not the path, just the file)
321
	 *
322
	 * @return bool
323
	 * @deprecated 3.0
324
	 * @codeCoverageIgnore
325
	 */
326
	protected function setProcessedUpgrade($upgrade) {
327
		$processed_upgrades = $this->getProcessedUpgrades();
328
		$processed_upgrades[] = $upgrade;
329
		$processed_upgrades = array_unique($processed_upgrades);
330
331
		return $this->config->save('processed_upgrades', $processed_upgrades);
332
	}
333
334
	/**
335
	 * Gets a list of processes upgrades
336
	 *
337
	 * @return mixed Array of processed upgrade filenames or false
338
	 * @deprecated 3.0
339
	 * @codeCoverageIgnore
340
	 */
341
	protected function getProcessedUpgrades() {
342
		return $this->config->processed_upgrades;
343
	}
344
345
	/**
346
	 * Returns the version of the upgrade filename.
347
	 *
348
	 * @param string $filename The upgrade filename. No full path.
349
	 *
350
	 * @return int|false
351
	 * @since 1.8.0
352
	 * @deprecated 3.0
353
	 * @codeCoverageIgnore
354
	 */
355
	protected function getUpgradeFileVersion($filename) {
356
		preg_match('/^([0-9]{10})([\.a-z0-9-_]+)?\.(php)$/i', $filename, $matches);
357
358
		if (isset($matches[1])) {
359
			return (int) $matches[1];
360
		}
361
362
		return false;
363
	}
364
365
	/**
366
	 * Returns a list of upgrade files relative to the $upgrade_path dir.
367
	 *
368
	 * @param string $upgrade_path The up
369
	 *
370
	 * @return array|false
371
	 * @deprecated 3.0
372
	 * @codeCoverageIgnore
373
	 */
374
	protected function getUpgradeFiles($upgrade_path = null) {
375
		if (!$upgrade_path) {
376
			$upgrade_path = elgg_get_engine_path() . '/lib/upgrades/';
377
		}
378
		$upgrade_path = \Elgg\Project\Paths::sanitize($upgrade_path);
379
		
380
		if (!is_dir($upgrade_path)) {
381
			return false;
382
		}
383
		
384
		$handle = opendir($upgrade_path);
385
		if (!$handle) {
386
			return false;
387
		}
388
389
		$upgrade_files = [];
390
391
		while (($upgrade_file = readdir($handle)) !== false) {
392
			// make sure this is a wellformed upgrade.
393
			if (!is_file($upgrade_path . $upgrade_file)) {
394
				continue;
395
			}
396
			$upgrade_version = $this->getUpgradeFileVersion($upgrade_file);
397
			if (!$upgrade_version) {
398
				continue;
399
			}
400
			$upgrade_files[] = $upgrade_file;
401
		}
402
403
		sort($upgrade_files);
404
405
		return $upgrade_files;
406
	}
407
408
	/**
409
	 * Checks if any upgrades need to be run.
410
	 *
411
	 * @param null|array $upgrade_files      Optional upgrade files
412
	 * @param null|array $processed_upgrades Optional processed upgrades
413
	 *
414
	 * @return array
415
	 * @deprecated 3.0
416
	 * @codeCoverageIgnore
417
	 */
418
	protected function getUnprocessedUpgrades($upgrade_files = null, $processed_upgrades = null) {
419
		if ($upgrade_files === null) {
420
			$upgrade_files = $this->getUpgradeFiles();
421
		}
422
423
		if (empty($upgrade_files)) {
424
			return [];
425
		}
426
		
427
		if ($processed_upgrades === null) {
428
			$processed_upgrades = $this->config->processed_upgrades;
429
			if (!is_array($processed_upgrades)) {
430
				$processed_upgrades = [];
431
			}
432
		}
433
434
		$unprocessed = array_diff($upgrade_files, $processed_upgrades);
435
436
		return $unprocessed;
437
	}
438
439
	/**
440
	 * Upgrades Elgg Database and code
441
	 *
442
	 * @return bool
443
	 * @deprecated 3.0
444
	 * @codeCoverageIgnore
445
	 */
446
	protected function processUpgrades() {
447
		$dbversion = (int) $this->config->version;
448
449
		if ($this->upgradeCode($dbversion)) {
450
			$this->system_messages->addSuccessMessage($this->translator->translate('upgrade:core'));
451
452
			return true;
453
		}
454
455
		return false;
456
	}
457
458
	/**
459
	 * Get pending async upgrades
460
	 *
461
	 * @param bool $async Include async upgrades
462
	 *
463
	 * @return ElggUpgrade[]
464
	 */
465 6
	public function getPendingUpgrades($async = true) {
466 6
		$pending = [];
467
468 6
		$upgrades = $this->locator->locate();
469
470 6
		foreach ($upgrades as $upgrade) {
471 5
			if ($upgrade->isCompleted()) {
472 3
				continue;
473
			}
474
475 2
			$batch = $upgrade->getBatch();
476 2
			if (!$batch) {
477
				continue;
478
			}
479
480 2
			$pending[] = $upgrade;
481
		}
482
483 6
		if (!$async) {
484 3
			$pending = array_filter($pending, function(ElggUpgrade $upgrade) {
485
				return !$upgrade->isAsynchronous();
486 3
			});
487
		}
488
489 6
		return $pending;
490
	}
491
	
492
	/**
493
	 * Get completed (async) upgrades ordered by recently completed first
494
	 *
495
	 * @param bool $async Include async upgrades
496
	 *
497
	 * @return ElggUpgrade[]
498
	 */
499
	public function getCompletedUpgrades($async = true) {
500
		$completed = [];
501
		
502
		$order_by_completed_time = new EntitySortByClause();
503
		$order_by_completed_time->direction = 'DESC';
504
		$order_by_completed_time->property = 'completed_time';
0 ignored issues
show
Documentation Bug introduced by
It seems like 'completed_time' of type string is incompatible with the declared type string[] of property $property.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
505
		$order_by_completed_time->property_type = 'private_setting';
506
		
507
		$upgrades = elgg_get_entities([
508
			'type' => 'object',
509
			'subtype' => 'elgg_upgrade',
510
			'private_setting_name' => 'class', // filters old upgrades
511
			'private_setting_name_value_pairs' => [
512
				'name' => 'is_completed',
513
				'value' => true,
514
			],
515
			'order_by' => $order_by_completed_time,
516
			'limit' => false,
517
			'batch' => true,
518
		]);
519
		/* @var $upgrade \ElggUpgrade */
520
		foreach ($upgrades as $upgrade) {
521
			$batch = $upgrade->getBatch();
522
			if (!$batch) {
523
				continue;
524
			}
525
526
			$completed[] = $upgrade;
527
		}
528
529
		if (!$async) {
530
			$completed = array_filter($completed, function(ElggUpgrade $upgrade) {
531
				return !$upgrade->isAsynchronous();
532
			});
533
		}
534
535
		return $completed;
536
	}
537
538
	/**
539
	 * Call the upgrade's run() for a specified period of time, or until it completes
540
	 *
541
	 * @param ElggUpgrade $upgrade      Upgrade to run
542
	 * @param int         $max_duration Maximum duration in seconds
543
	 *                                  Set to false to execute an entire upgrade
544
	 *
545
	 * @return Result
546
	 * @throws RuntimeException
547
	 */
548 5
	public function executeUpgrade(ElggUpgrade $upgrade, $max_duration = null) {
549
		// Upgrade also disabled data, so the compatibility is
550
		// preserved in case the data ever gets enabled again
551 5
		return elgg_call(
552 5
			ELGG_IGNORE_ACCESS | ELGG_SHOW_DISABLED_ENTITIES,
553
			function () use ($upgrade, $max_duration) {
554 5
				return $this->events->triggerSequence('upgrade:execute', 'system', $upgrade, function() use ($upgrade, $max_duration) {
555 5
					$result = new Result();
556
					
557 5
					$loop = new Loop(
558 5
						$upgrade,
559 5
						$result,
560 5
						$this->progress,
561 5
						$this->logger
562
					);
563
					
564 5
					$loop->loop($max_duration);
565
					
566 5
					return $result;
567 5
				});
568 5
			}
569
		);
570
	}
571
}
572