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
|
|||
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 |
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.