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