Newer
Older
<?php
namespace Drupal\automatic_updates\Form;
use Drupal\automatic_updates\BatchProcessor;
use Drupal\automatic_updates\Event\ReadinessCheckEvent;

Ted Bowman
committed
use Drupal\automatic_updates\ProjectInfo;
use Drupal\automatic_updates\ReleaseChooser;
use Drupal\automatic_updates\Updater;
use Drupal\automatic_updates\Validation\ReadinessTrait;
use Drupal\update\ProjectRelease;
use Drupal\Core\Batch\BatchBuilder;

Adam G-H
committed
use Drupal\Core\Extension\ExtensionVersion;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;

Kunal Sachdev
committed
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\State\StateInterface;

Adam G-H
committed
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;

Adam G-H
committed
use Drupal\package_manager\Exception\StageException;

Adam G-H
committed
use Drupal\package_manager\Exception\StageOwnershipException;
use Drupal\package_manager\ValidationResult;
use Drupal\system\SystemManager;
use Drupal\update\UpdateManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Defines a form to update Drupal core.
*
* @internal

Adam G-H
committed
* Form classes are internal and the form structure may change at any time.

Adam G-H
committed
final class UpdaterForm extends FormBase {
use ReadinessTrait {
formatResult as traitFormatResult;
}
/**
* The updater service.
*
* @var \Drupal\automatic_updates\Updater
*/
protected $updater;
/**
* The state service.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* The event dispatcher service.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;

Adam G-H
committed
/**

Ted Bowman
committed
* The release chooser service.

Adam G-H
committed
*

Ted Bowman
committed
* @var \Drupal\automatic_updates\ReleaseChooser

Adam G-H
committed
*/

Ted Bowman
committed
protected $releaseChooser;

Adam G-H
committed
/**
* The renderer service.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
* Constructs a new UpdaterForm object.
*
* @param \Drupal\Core\State\StateInterface $state
* The state service.
* @param \Drupal\automatic_updates\Updater $updater
* The updater service.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher service.

Ted Bowman
committed
* @param \Drupal\automatic_updates\ReleaseChooser $release_chooser
* The release chooser service.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer service.
public function __construct(StateInterface $state, Updater $updater, EventDispatcherInterface $event_dispatcher, ReleaseChooser $release_chooser, RendererInterface $renderer) {
$this->updater = $updater;
$this->state = $state;
$this->eventDispatcher = $event_dispatcher;

Ted Bowman
committed
$this->releaseChooser = $release_chooser;
$this->renderer = $renderer;
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'automatic_updates_updater_form';
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('state'),
$container->get('automatic_updates.updater'),

Adam G-H
committed
$container->get('event_dispatcher'),
$container->get('automatic_updates.release_chooser'),
$container->get('renderer')
);
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$this->messenger()->addWarning($this->t('This is an experimental Automatic Updater using Composer. Use at your own risk.'));

Adam G-H
committed
if ($this->updater->isAvailable()) {
$stage_exists = FALSE;
}
else {
$stage_exists = TRUE;
// If there's a stage ID stored in the session, try to claim the stage
// with it. If we succeed, then an update is already in progress, and the
// current session started it, so redirect them to the confirmation form.

Ted Bowman
committed
$stage_id = $this->getRequest()->getSession()->get(BatchProcessor::STAGE_ID_SESSION_KEY);

Adam G-H
committed
if ($stage_id) {
try {
$this->updater->claim($stage_id);
return $this->redirect('automatic_updates.confirmation_page', [
'stage_id' => $stage_id,
]);
}
catch (StageOwnershipException $e) {
// We already know a stage exists, even if it's not ours, so we don't
// have to do anything else here.
}
}
}
$form['last_check'] = [
'#theme' => 'update_last_check',
'#last' => $this->state->get('update.last_check', 0),
];

Ted Bowman
committed
$project_info = new ProjectInfo('drupal');

Adam G-H
committed
try {

Ted Bowman
committed
// @todo Until https://www.drupal.org/i/3264849 is fixed, we can only show
// one release on the form. First, try to show the latest release in the
// currently installed minor. Failing that, try to show the latest

Kunal Sachdev
committed
// release in the next minor.
$installed_minor_release = $this->releaseChooser->getLatestInInstalledMinor($this->updater);
$next_minor_release = $this->releaseChooser->getLatestInNextMinor($this->updater);

Adam G-H
committed
}
catch (\RuntimeException $e) {
$form['message'] = [

Adam G-H
committed
'#markup' => $e->getMessage(),
];
return $form;
}
// @todo Should we be using the Update module's library here, or our own?
$form['#attached']['library'][] = 'update/drupal.update.admin';

Kunal Sachdev
committed
$project = $project_info->getProjectInfo();

Adam G-H
committed
if ($installed_minor_release === NULL && $next_minor_release === NULL) {

Kunal Sachdev
committed
if ($project['status'] === UpdateManagerInterface::CURRENT) {
$this->messenger()->addMessage($this->t('No update available'));
}
else {
$message = $this->t('Updates were found, but they must be performed manually. See <a href=":url">the list of available updates</a> for more information.', [
':url' => Url::fromRoute('update.status')->toString(),
]);
// If the current release is old, but otherwise secure and supported,
// this should be a regular status message. In any other case, urgent
// action is needed so flag it as an error.
$this->messenger()->addMessage($message, $project['status'] === UpdateManagerInterface::NOT_CURRENT ? MessengerInterface::TYPE_STATUS : MessengerInterface::TYPE_ERROR);
}
return $form;
}
if (empty($project['title']) || empty($project['link'])) {
throw new \UnexpectedValueException('Expected project data to have a title and link.');
}

Adam G-H
committed
$form['title'] = [
'#type' => 'html_tag',
'#tag' => 'h2',
'#value' => $this->t(
'Update <a href=":url">Drupal core</a>',
[':url' => $project['link']],
),
];
$form['current'] = [
'#type' => 'html_tag',
'#tag' => 'p',
'#value' => $this->t(
'Currently installed: @version (@status)',
[
'@version' => $project_info->getInstalledVersion(),
'@status' => $this->getUpdateStatus($project['status']),
]
),
];
switch ($project['status']) {
case UpdateManagerInterface::NOT_SECURE:
case UpdateManagerInterface::REVOKED:

Adam G-H
committed
$release_status = $this->t('Security update');
$type = 'update-security';
break;
default:

Adam G-H
committed
$release_status = $this->t('Available update');
$type = 'update-recommended';

Adam G-H
committed
if ($form_state->getUserInput()) {
$results = [];
}
else {

Adam G-H
committed
$event = new ReadinessCheckEvent($this->updater);
$this->eventDispatcher->dispatch($event);
$results = $event->getResults();
}
$this->displayResults($results, $this->messenger(), $this->renderer);

Adam G-H
committed
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
$create_update_buttons = !$stage_exists && $this->getOverallSeverity($results) !== SystemManager::REQUIREMENT_ERROR;
if ($installed_minor_release) {
$installed_version = ExtensionVersion::createFromVersionString($project_info->getInstalledVersion());
$form['installed_minor'] = $this->createReleaseTable(
$installed_minor_release,
$release_status,
$this->t('Latest version of Drupal @major.@minor (currently installed):', [
'@major' => $installed_version->getMajorVersion(),
'@minor' => $installed_version->getMinorVersion(),
]),
$type,
$create_update_buttons,
// Any update in the current minor should be the primary update.
TRUE,
);
}
if ($next_minor_release) {
// If there is no update in the current minor make the button for the next
// minor primary unless the project status is 'CURRENT' or 'NOT_CURRENT'.
// 'NOT_CURRENT' does not denote that installed version is not a valid
// only that there is newer version available.
$is_primary = !$installed_minor_release && !($project['status'] === UpdateManagerInterface::CURRENT || $project['status'] === UpdateManagerInterface::NOT_CURRENT);
$next_minor_version = ExtensionVersion::createFromVersionString($next_minor_release->getVersion());
// @todo Add documentation to explain what is different about a minor
// update in https://www.drupal.org/i/3291730.
$form['next_minor'] = $this->createReleaseTable(
$next_minor_release,
$installed_minor_release ? $this->t('Minor update') : $release_status,
$this->t('Latest version of Drupal @major.@minor (next minor):', [
'@major' => $next_minor_version->getMajorVersion(),
'@minor' => $next_minor_version->getMinorVersion(),
]),
$installed_minor_release ? 'update-optional' : $type,
$create_update_buttons,
$is_primary
);
}
$form['backup'] = [
'#markup' => $this->t('It\'s a good idea to <a href=":url">back up your database</a> before you begin.', [':url' => 'https://www.drupal.org/node/22281#s-backing-up-the-database']),
];

Adam G-H
committed
if ($stage_exists) {
// If the form has been submitted, do not display this error message

Ted Bowman
committed
// because ::deleteExistingUpdate() may run on submit. The message will
// still be displayed on form build if needed.
if (!$form_state->getUserInput()) {
$this->messenger()->addError($this->t('Cannot begin an update because another Composer operation is currently in progress.'));
}

Adam G-H
committed
$form['actions']['delete'] = [
'#type' => 'submit',
'#value' => $this->t('Delete existing update'),
'#submit' => ['::deleteExistingUpdate'],
];
}

Adam G-H
committed
$form['actions']['#type'] = 'actions';
return $form;
}
/**
* Submit function to delete an existing in-progress update.
*/
public function deleteExistingUpdate(): void {

Adam G-H
committed
try {
$this->updater->destroy(TRUE);
$this->messenger()->addMessage($this->t("Staged update deleted"));
}
catch (StageException $e) {
$this->messenger()->addError($e->getMessage());
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {

Adam G-H
committed
$button = $form_state->getTriggeringElement();
$batch = (new BatchBuilder())
->setTitle($this->t('Downloading updates'))
->setInitMessage($this->t('Preparing to download updates'))

Ted Bowman
committed
->addOperation(
[BatchProcessor::class, 'begin'],

Adam G-H
committed
[['drupal' => $button['#target_version']]]

Ted Bowman
committed
)
->addOperation([BatchProcessor::class, 'stage'])
->setFinishCallback([BatchProcessor::class, 'finishStage'])
->toArray();
batch_set($batch);
}
/**
* {@inheritdoc}
*/
protected function formatResult(ValidationResult $result) {
$messages = $result->getMessages();
if (count($messages) > 1) {
return [
'#theme' => 'item_list__automatic_updates_validation_results',
'#prefix' => $result->getSummary(),
'#items' => $messages,
];
}
return $this->traitFormatResult($result);
}

Adam G-H
committed
/**
* Gets the update table for a specific release.
*
* @param \Drupal\update\ProjectRelease $release

Adam G-H
committed
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
* The project release.
* @param string $release_description
* The release description.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $caption
* The table caption, if any.
* @param string $update_type
* The update type.
* @param bool $create_update_button
* Whether the update button should be created.
* @param bool $is_primary
* Whether update button should be a primary button.
*
* @return array
* The table render array.
*/
private function createReleaseTable(ProjectRelease $release, string $release_description, ?TranslatableMarkup $caption, string $update_type, bool $create_update_button, bool $is_primary): array {
$release_section = ['#type' => 'container'];
$release_section['table'] = [
'#type' => 'table',
'#description' => $this->t('more'),
'#header' => [
'title' => [
'data' => $this->t('Update type'),
'class' => ['update-project-name'],
],
'target_version' => [
'data' => $this->t('Version'),
],
],
];
if ($caption) {
$release_section['table']['#caption'] = $caption;
}
$release_section['table'][$release->getVersion()] = [
'title' => [
'#type' => 'html_tag',
'#tag' => 'p',
'#value' => $release_description,
],
'target_version' => [
'data' => [
// @todo Is an inline template the right tool here? Is there an Update
// module template we should use instead?
'#type' => 'inline_template',
'#template' => '{{ release_version }} (<a href="{{ release_link }}" title="{{ project_title }}">{{ release_notes }}</a>)',
'#context' => [
'release_version' => $release->getVersion(),
'release_link' => $release->getReleaseUrl(),
'project_title' => $this->t(
'Release notes for @project_title @version',
[
'@project_title' => 'Drupal core',
'@version' => $release->getVersion(),
]
),
'release_notes' => $this->t('Release notes'),
],
],
],
'#attributes' => ['class' => ['update-' . $update_type]],
];
if ($create_update_button) {
$release_section['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Update to @version', ['@version' => $release->getVersion()]),
'#target_version' => $release->getVersion(),
];
if ($is_primary) {
$release_section['submit']['#button_type'] = 'primary';
}
}
$release_section['#suffix'] = '<br />';
return $release_section;
}
/**
* Gets the human-readable project status.
*
* @param int $status
* The project status, one of \Drupal\update\UpdateManagerInterface
* constants.
*
* @return \Drupal\Core\StringTranslation\TranslatableMarkup
* The human-readable status.
*/
private function getUpdateStatus(int $status): TranslatableMarkup {
switch ($status) {
case UpdateManagerInterface::NOT_SECURE:
return $this->t('Security update required!');
case UpdateManagerInterface::REVOKED:
return $this->t('Revoked!');
case UpdateManagerInterface::NOT_SUPPORTED:
return $this->t('Not supported!');
case UpdateManagerInterface::NOT_CURRENT:
return $this->t('Update available');
case UpdateManagerInterface::CURRENT:
return $this->t('Up to date');
default:
return $this->t('Unknown status');
}
}