Completed
Push — 3.3 ( 5fced4...d53524 )
by Jerome
22:15 queued 11s
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
	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
		$this->locator = $locator;
86
		$this->translator = $translator;
87
		$this->events = $events;
88
		$this->config = $config;
89
		$this->logger = $logger;
90
		$this->mutex = $mutex;
91
		$this->system_messages = $system_messages;
92
		$this->progress = $progress;
93
	}
94
95
	/**
96
	 * Start an upgrade process
97
	 * @return Promise
98
	 */
99
	protected function up() {
100
		return new Promise(function ($resolve, $reject) {
101
			Application::migrate();
102
103
			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
			if (!$this->mutex->lock('upgrade')) {
109
				return $reject(new RuntimeException($this->translator->translate('upgrade:locked')));
110
			}
111
112
			// Clear system caches
113
			_elgg_disable_caches();
114
			_elgg_clear_caches();
115
116
			return $resolve();
117
		});
118
	}
119
120
	/**
121
	 * Finish an upgrade process
122
	 * @return Promise
123
	 */
124
	protected function down() {
125
		return new Promise(function ($resolve, $reject) {
126
			if (!$this->events->trigger('upgrade', 'system', null)) {
127
				return $reject();
128
			}
129
130
			elgg_flush_caches();
1 ignored issue
show
Deprecated Code introduced by
The function elgg_flush_caches() has been deprecated: 3.3 use elgg_clear_caches() ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

130
			/** @scrutinizer ignore-deprecated */ elgg_flush_caches();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
131
132
			$this->mutex->unlock('upgrade');
133
134
			$this->events->triggerAfter('upgrade', 'system', null);
135
136
			return $resolve();
137
		});
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
	protected function runUpgrades($upgrades) {
164
		$promises = [];
165
166
		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
		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
	public function run($upgrades = null) {
201
		// turn off time limit
202
		set_time_limit(3600);
203
204
		$deferred = new Deferred();
205
206
		$promise = $deferred->promise();
207
208
		$resolve = function ($value) use ($deferred) {
209
			$deferred->resolve($value);
210
		};
211
212
		$reject = function ($error) use ($deferred) {
213
			$deferred->reject($error);
214
		};
215
216
		if (!isset($upgrades)) {
217
			$upgrades = $this->getPendingUpgrades(false);
218
		}
219
220
		$this->up()->done(
221
			function () use ($resolve, $reject, $upgrades) {
222
				all([
223
					$this->runLegacyUpgrades(),
224
					$this->runUpgrades($upgrades),
225
				])->done(
226
					function () use ($resolve, $reject) {
227
						$this->down()->done(
228
							function ($result) use ($resolve) {
229
								return $resolve($result);
230
							},
231
							$reject
232
						);
233
					},
234
					$reject
235
				);
236
			},
237
			$reject
238
		);
239
240
		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
			return true;
451
		}
452
453
		return false;
454
	}
455
456
	/**
457
	 * Get pending async upgrades
458
	 *
459
	 * @param bool $async Include async upgrades
460
	 *
461
	 * @return ElggUpgrade[]
462
	 */
463
	public function getPendingUpgrades($async = true) {
464
		$pending = [];
465
466
		$upgrades = $this->locator->locate();
467
468
		foreach ($upgrades as $upgrade) {
469
			if ($upgrade->isCompleted()) {
470
				continue;
471
			}
472
473
			$batch = $upgrade->getBatch();
474
			if (!$batch) {
475
				continue;
476
			}
477
478
			$pending[] = $upgrade;
479
		}
480
481
		if (!$async) {
482
			$pending = array_filter($pending, function(ElggUpgrade $upgrade) {
483
				return !$upgrade->isAsynchronous();
484
			});
485
		}
486
487
		return $pending;
488
	}
489
	
490
	/**
491
	 * Get completed (async) upgrades ordered by recently completed first
492
	 *
493
	 * @param bool $async Include async upgrades
494
	 *
495
	 * @return ElggUpgrade[]
496
	 */
497
	public function getCompletedUpgrades($async = true) {
498
		// make sure always to return all upgrade entities
499
		return elgg_call(
500
			ELGG_IGNORE_ACCESS | ELGG_SHOW_DISABLED_ENTITIES,
501
			function () use ($async) {
502
				$completed = [];
503
				
504
				$order_by_completed_time = new EntitySortByClause();
505
				$order_by_completed_time->direction = 'DESC';
506
				$order_by_completed_time->property = 'completed_time';
507
				$order_by_completed_time->property_type = 'private_setting';
508
				
509
				$upgrades = elgg_get_entities([
510
					'type' => 'object',
511
					'subtype' => 'elgg_upgrade',
512
					'private_setting_name' => 'class', // filters old upgrades
513
					'private_setting_name_value_pairs' => [
514
						'name' => 'is_completed',
515
						'value' => true,
516
					],
517
					'order_by' => $order_by_completed_time,
518
					'limit' => false,
519
					'batch' => true,
520
				]);
521
				/* @var $upgrade \ElggUpgrade */
522
				foreach ($upgrades as $upgrade) {
523
					$batch = $upgrade->getBatch();
524
					if (!$batch) {
525
						continue;
526
					}
527
		
528
					$completed[] = $upgrade;
529
				}
530
		
531
				if (!$async) {
532
					$completed = array_filter($completed, function(ElggUpgrade $upgrade) {
533
						return !$upgrade->isAsynchronous();
534
					});
535
				}
536
		
537
				return $completed;
538
			}
539
		);
540
	}
541
542
	/**
543
	 * Call the upgrade's run() for a specified period of time, or until it completes
544
	 *
545
	 * @param ElggUpgrade $upgrade      Upgrade to run
546
	 * @param int         $max_duration Maximum duration in seconds
547
	 *                                  Set to false to execute an entire upgrade
548
	 *
549
	 * @return Result
550
	 * @throws RuntimeException
551
	 */
552
	public function executeUpgrade(ElggUpgrade $upgrade, $max_duration = null) {
553
		// Upgrade also disabled data, so the compatibility is
554
		// preserved in case the data ever gets enabled again
555
		return elgg_call(
556
			ELGG_IGNORE_ACCESS | ELGG_SHOW_DISABLED_ENTITIES,
557
			function () use ($upgrade, $max_duration) {
558
				return $this->events->triggerSequence('upgrade:execute', 'system', $upgrade, function() use ($upgrade, $max_duration) {
559
					$result = new Result();
560
					
561
					$loop = new Loop(
562
						$upgrade,
563
						$result,
564
						$this->progress,
565
						$this->logger
566
					);
567
					
568
					$loop->loop($max_duration);
569
					
570
					return $result;
571
				});
572
			}
573
		);
574
	}
575
}
576