Elgg  Version 3.0
UpgradeService.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg;
4 
11 use ElggUpgrade;
12 use function React\Promise\all;
16 use Throwable;
17 
24 
25  use Loggable;
26 
30  protected $locator;
31 
35  private $translator;
36 
40  private $events;
41 
45  private $config;
46 
50  private $mutex;
51 
55  private $system_messages;
56 
60  protected $progress;
61 
74  public function __construct(
75  Locator $locator,
76  Translator $translator,
77  EventsService $events,
79  Logger $logger,
80  Mutex $mutex,
81  SystemMessagesService $system_messages,
82  Progress $progress
83  ) {
84  $this->locator = $locator;
85  $this->translator = $translator;
86  $this->events = $events;
87  $this->config = $config;
88  $this->logger = $logger;
89  $this->mutex = $mutex;
90  $this->system_messages = $system_messages;
91  $this->progress = $progress;
92  }
93 
98  protected function up() {
99  return new Promise(function ($resolve, $reject) {
100  Application::migrate();
101 
102  if (!$this->events->triggerBefore('upgrade', 'system', null)) {
103  return $reject(new RuntimeException($this->translator->translate('upgrade:terminated')));
104  }
105 
106  // prevent someone from running the upgrade script in parallel (see #4643)
107  if (!$this->mutex->lock('upgrade')) {
108  return $reject(new RuntimeException($this->translator->translate('upgrade:locked')));
109  }
110 
111  // Clear system caches
114 
115  return $resolve();
116  });
117  }
118 
123  protected function down() {
124  return new Promise(function ($resolve, $reject) {
125  if (!$this->events->trigger('upgrade', 'system', null)) {
126  return $reject();
127  }
128 
130 
131  $this->mutex->unlock('upgrade');
132 
133  $this->events->triggerAfter('upgrade', 'system', null);
134 
135  return $resolve();
136  });
137  }
138 
145  protected function runLegacyUpgrades() {
146  return new Promise(function ($resolve, $reject) {
147  if ($this->getUnprocessedUpgrades()) {
148  $this->processUpgrades();
149  }
150 
151  return $resolve();
152  });
153  }
154 
162  protected function runUpgrades($upgrades) {
163  $promises = [];
164 
165  foreach ($upgrades as $key => $upgrade) {
166  if (!$upgrade instanceof ElggUpgrade) {
167  continue;
168  }
169  $promises[] = new Promise(function ($resolve, $reject) use ($upgrade) {
170  try {
171  $result = $this->executeUpgrade($upgrade, false);
172  } catch (Throwable $ex) {
173  return $reject($ex);
174  }
175 
176  if ($result->getFailureCount()) {
177  $msg = elgg_echo('admin:upgrades:failed', [
178  $upgrade->getDisplayName(),
179  ]);
180 
181  return $reject(new RuntimeException($msg));
182  } else {
183  return $resolve($result);
184  }
185  });
186  }
187 
188  return all($promises);
189  }
190 
199  public function run($upgrades = null) {
200  // turn off time limit
201  set_time_limit(3600);
202 
203  $deferred = new Deferred();
204 
205  $promise = $deferred->promise();
206 
207  $resolve = function ($value) use ($deferred) {
208  $deferred->resolve($value);
209  };
210 
211  $reject = function ($error) use ($deferred) {
212  $deferred->reject($error);
213  };
214 
215  if (!isset($upgrades)) {
216  $upgrades = $this->getPendingUpgrades(false);
217  }
218 
219  $this->up()->done(
220  function () use ($resolve, $reject, $upgrades) {
221  all([
222  $this->runLegacyUpgrades(),
223  $this->runUpgrades($upgrades),
224  ])->done(
225  function () use ($resolve, $reject) {
226  $this->down()->done(
227  function ($result) use ($resolve) {
228  return $resolve($result);
229  },
230  $reject
231  );
232  },
233  $reject
234  );
235  },
236  $reject
237  );
238 
239  return $promise;
240  }
241 
252  protected function upgradeCode($version, $quiet = false) {
253  $version = (int) $version;
254  $upgrade_path = elgg_get_engine_path() . '/lib/upgrades/';
255  $processed_upgrades = $this->getProcessedUpgrades();
256 
257  $upgrade_files = $this->getUpgradeFiles($upgrade_path);
258 
259  if ($upgrade_files === false) {
260  return false;
261  }
262 
263  $upgrades = $this->getUnprocessedUpgrades($upgrade_files, $processed_upgrades);
264 
265  // Sort and execute
266  sort($upgrades);
267 
268  foreach ($upgrades as $upgrade) {
269  $upgrade_version = $this->getUpgradeFileVersion($upgrade);
270  $success = true;
271 
272  if ($upgrade_version <= $version) {
273  // skip upgrade files from before the installation version of Elgg
274  // because the upgrade files from before the installation version aren't
275  // added to the database.
276  continue;
277  }
278 
279  // hide all errors.
280  if ($quiet) {
281  // hide include errors as well as any exceptions that might happen
282  try {
283  if (!@Includer::includeFile("$upgrade_path/$upgrade")) {
284  $success = false;
285  $this->logger->error("Could not include $upgrade_path/$upgrade");
286  }
287  } catch (\Exception $e) {
288  $success = false;
289  $this->logger->error($e);
290  }
291  } else {
292  if (!Includer::includeFile("$upgrade_path/$upgrade")) {
293  $success = false;
294  $this->logger->error("Could not include $upgrade_path/$upgrade");
295  }
296  }
297 
298  if ($success) {
299  // don't set the version to a lower number in instances where an upgrade
300  // has been merged from a lower version of Elgg
301  if ($upgrade_version > $version) {
302  $this->config->save('version', $upgrade_version);
303  }
304 
305  // incrementally set upgrade so we know where to start if something fails.
306  $this->setProcessedUpgrade($upgrade);
307  } else {
308  return false;
309  }
310  }
311 
312  return true;
313  }
314 
325  protected function setProcessedUpgrade($upgrade) {
326  $processed_upgrades = $this->getProcessedUpgrades();
327  $processed_upgrades[] = $upgrade;
328  $processed_upgrades = array_unique($processed_upgrades);
329 
330  return $this->config->save('processed_upgrades', $processed_upgrades);
331  }
332 
340  protected function getProcessedUpgrades() {
341  return $this->config->processed_upgrades;
342  }
343 
354  protected function getUpgradeFileVersion($filename) {
355  preg_match('/^([0-9]{10})([\.a-z0-9-_]+)?\.(php)$/i', $filename, $matches);
356 
357  if (isset($matches[1])) {
358  return (int) $matches[1];
359  }
360 
361  return false;
362  }
363 
373  protected function getUpgradeFiles($upgrade_path = null) {
374  if (!$upgrade_path) {
375  $upgrade_path = elgg_get_engine_path() . '/lib/upgrades/';
376  }
377  $upgrade_path = \Elgg\Project\Paths::sanitize($upgrade_path);
378 
379  if (!is_dir($upgrade_path)) {
380  return false;
381  }
382 
383  $handle = opendir($upgrade_path);
384  if (!$handle) {
385  return false;
386  }
387 
388  $upgrade_files = [];
389 
390  while (($upgrade_file = readdir($handle)) !== false) {
391  // make sure this is a wellformed upgrade.
392  if (!is_file($upgrade_path . $upgrade_file)) {
393  continue;
394  }
395  $upgrade_version = $this->getUpgradeFileVersion($upgrade_file);
396  if (!$upgrade_version) {
397  continue;
398  }
399  $upgrade_files[] = $upgrade_file;
400  }
401 
402  sort($upgrade_files);
403 
404  return $upgrade_files;
405  }
406 
417  protected function getUnprocessedUpgrades($upgrade_files = null, $processed_upgrades = null) {
418  if ($upgrade_files === null) {
419  $upgrade_files = $this->getUpgradeFiles();
420  }
421 
422  if (empty($upgrade_files)) {
423  return [];
424  }
425 
426  if ($processed_upgrades === null) {
427  $processed_upgrades = $this->config->processed_upgrades;
428  if (!is_array($processed_upgrades)) {
429  $processed_upgrades = [];
430  }
431  }
432 
433  $unprocessed = array_diff($upgrade_files, $processed_upgrades);
434 
435  return $unprocessed;
436  }
437 
445  protected function processUpgrades() {
446  $dbversion = (int) $this->config->version;
447 
448  if ($this->upgradeCode($dbversion)) {
449  $this->system_messages->addSuccessMessage($this->translator->translate('upgrade:core'));
450 
451  return true;
452  }
453 
454  return false;
455  }
456 
464  public function getPendingUpgrades($async = true) {
465  $pending = [];
466 
467  $upgrades = $this->locator->locate();
468 
469  foreach ($upgrades as $upgrade) {
470  if ($upgrade->isCompleted()) {
471  continue;
472  }
473 
474  $batch = $upgrade->getBatch();
475  if (!$batch) {
476  continue;
477  }
478 
479  $pending[] = $upgrade;
480  }
481 
482  if (!$async) {
483  $pending = array_filter($pending, function(ElggUpgrade $upgrade) {
484  return !$upgrade->isAsynchronous();
485  });
486  }
487 
488  return $pending;
489  }
490 
498  public function getCompletedUpgrades($async = true) {
499  $completed = [];
500 
502  'type' => 'object',
503  'subtype' => 'elgg_upgrade',
504  'private_setting_name' => 'class', // filters old upgrades
505  'private_setting_name_value_pairs' => [
506  'name' => 'is_completed',
507  'value' => true,
508  ],
509  'limit' => false,
510  'batch' => true,
511  ]);
512  /* @var $upgrade \ElggUpgrade */
513  foreach ($upgrades as $upgrade) {
514  $batch = $upgrade->getBatch();
515  if (!$batch) {
516  continue;
517  }
518 
519  $completed[] = $upgrade;
520  }
521 
522  if (!$async) {
523  $completed = array_filter($completed, function(ElggUpgrade $upgrade) {
524  return !$upgrade->isAsynchronous();
525  });
526  }
527 
528  return $completed;
529  }
530 
541  public function executeUpgrade(ElggUpgrade $upgrade, $max_duration = null) {
542  // Upgrade also disabled data, so the compatibility is
543  // preserved in case the data ever gets enabled again
544  return elgg_call(
546  function () use ($upgrade, $max_duration) {
547  return $this->events->triggerSequence('upgrade:execute', 'system', $upgrade, function() use ($upgrade, $max_duration) {
548  $result = new Result();
549 
550  $loop = new Loop(
551  $upgrade,
552  $result,
553  $this->progress,
554  $this->logger
555  );
556 
557  $loop->loop($max_duration);
558 
559  return $result;
560  });
561  }
562  );
563  }
564 }
elgg_call(int $flags, Closure $closure)
Calls a callable autowiring the arguments using public DI services and applying logic based on flags...
Definition: elgglib.php:1176
runLegacyUpgrades()
Run legacy upgrade scripts.
getUpgradeFiles($upgrade_path=null)
Returns a list of upgrade files relative to the $upgrade_path dir.
Locates and registers both core and plugin upgrades.
Definition: Locator.php:20
getCompletedUpgrades($async=true)
Get completed (async) upgrades.
$mutex
Unlocks the upgrade script.
if(!$enabled) if(PHP_SAPI!== 'cli')
Interates through each element of an array and calls callback a function.
setProcessedUpgrade($upgrade)
Saves a processed upgrade to a dataset.
trait Loggable
Enables adding a logger.
Definition: Loggable.php:12
Events service.
getUpgradeFileVersion($filename)
Returns the version of the upgrade filename.
WARNING: API IN FLUX.
Definition: Mutex.php:18
_elgg_disable_caches()
Disable all caches.
Definition: cache.php:317
isAsynchronous()
Check if the upgrade should be run asynchronously.
Upgrade service for Elgg.
elgg_echo($message_key, array $args=[], $language="")
Given a message key, returns an appropriately translated full-text string.
Definition: languages.php:21
__construct(Locator $locator, Translator $translator, EventsService $events, Config $config, Logger $logger, Mutex $mutex, SystemMessagesService $system_messages, Progress $progress)
Constructor.
$config
Advanced site settings, debugging section.
Definition: debugging.php:6
$upgrades
Lists pending upgrades.
Definition: upgrades.php:11
const ELGG_IGNORE_ACCESS
elgg_call() flags
Definition: constants.php:156
$upgrade
Definition: upgrade.php:8
$error
Bad request error.
Definition: 400.php:6
Logger.
Definition: Logger.php:25
Configuration exception.
getPendingUpgrades($async=true)
Get pending async upgrades.
getUnprocessedUpgrades($upgrade_files=null, $processed_upgrades=null)
Checks if any upgrades need to be run.
const ELGG_SHOW_DISABLED_ENTITIES
Definition: constants.php:158
elgg_get_entities(array $options=[])
Fetches/counts entities or performs a calculation on their properties.
Definition: entities.php:545
runUpgrades($upgrades)
Run system and async upgrades.
CLI Progress reporter.
Definition: Progress.php:11
_elgg_clear_caches()
Clear all caches.
Definition: cache.php:333
Result of a single BatchUpgrade run.
Definition: Result.php:8
down()
Finish an upgrade process.
up()
Start an upgrade process.
if($container instanceof ElggGroup &&$container->guid!=elgg_get_page_owner_guid()) $key
Definition: summary.php:55
$value
Definition: debugging.php:7
executeUpgrade(ElggUpgrade $upgrade, $max_duration=null)
Call the upgrade&#39;s run() for a specified period of time, or until it completes.
System messages service.
processUpgrades()
Upgrades Elgg Database and code.
$filename
static sanitize($path, $append_slash=true)
Sanitise file paths ensuring that they begin and end with slashes etc.
Definition: Paths.php:76
elgg_get_engine_path()
/path/to/elgg/engine with no trailing slash.
run($upgrades=null)
Run the upgrade process.
$version
Definition: version.php:14
elgg_flush_caches()
Flush all the registered caches.
Definition: cache.php:234
getProcessedUpgrades()
Gets a list of processes upgrades.
Upgrade loop Executes upgrade batches for a given duration of time.
Definition: Loop.php:16
upgradeCode($version, $quiet=false)
Run any php upgrade scripts which are required.