diff --git a/automatic_updates_extensions/automatic_updates_extensions.info.yml b/automatic_updates_extensions/automatic_updates_extensions.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..921c13feeb4acc770e3a6bd81f413e6bb6b6f692 --- /dev/null +++ b/automatic_updates_extensions/automatic_updates_extensions.info.yml @@ -0,0 +1,7 @@ +name: 'Automatic Updates Extensions' +type: module +description: 'Allows updates to themes and modules' +core_version_requirement: ^9.2 +dependencies: + - drupal:automatic_updates +hidden: true diff --git a/automatic_updates_extensions/automatic_updates_extensions.routing.yml b/automatic_updates_extensions/automatic_updates_extensions.routing.yml new file mode 100644 index 0000000000000000000000000000000000000000..db8214748bcb4dced2b54c280668818bb4685230 --- /dev/null +++ b/automatic_updates_extensions/automatic_updates_extensions.routing.yml @@ -0,0 +1,9 @@ +automatic_updates_extensions.update: + path: '/admin/automatic-update-extensions' + defaults: + _form: 'Drupal\automatic_updates_extensions\Form\UpdaterForm' + _title: 'Automatic Updates Form' + requirements: + _permission: 'administer software updates' + options: + _admin_route: TRUE diff --git a/automatic_updates_extensions/src/Form/UpdaterForm.php b/automatic_updates_extensions/src/Form/UpdaterForm.php new file mode 100644 index 0000000000000000000000000000000000000000..f7a3a31f858bff0f0758e354d5f8d2ca0a735b0e --- /dev/null +++ b/automatic_updates_extensions/src/Form/UpdaterForm.php @@ -0,0 +1,175 @@ +<?php + +namespace Drupal\automatic_updates_extensions\Form; + +use Drupal\automatic_updates\Updater; +use Drupal\Core\Form\FormBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\update\UpdateManagerInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * A form for selecting extension updates. + */ +class UpdaterForm extends FormBase { + + /** + * The updater service. + * + * @var \Drupal\automatic_updates\Updater + */ + private $updater; + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static($container->get('automatic_updates.updater')); + } + + /** + * Constructs a new UpdaterForm object. + * + * @param \Drupal\automatic_updates\Updater $updater + * The extension updater service. + */ + public function __construct(Updater $updater) { + $this->updater = $updater; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'automatic_updates_extensions_updater_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $update_projects = $this->getRecommendedModuleUpdates(); + $options = []; + $recommended_versions = []; + foreach ($update_projects as $project_name => $update_project) { + switch ($update_project['status']) { + case UpdateManagerInterface::NOT_SECURE: + case UpdateManagerInterface::REVOKED: + $status_message = $this->t('(Security update)'); + break; + + case UpdateManagerInterface::NOT_SUPPORTED: + $status_message = $this->t('(Unsupported)'); + break; + + default: + $status_message = ''; + } + $options[$project_name] = [ + $update_project['title'] . $status_message, + $update_project['existing_version'], + $update_project['recommended'], + ]; + $recommended_versions[$project_name] = $update_project['recommended']; + } + $form['recommended_versions'] = [ + '#type' => 'value', + '#value' => $recommended_versions, + ]; + $form['projects'] = [ + '#type' => 'tableselect', + '#header' => [ + $this->t('Project:'), + $this->t('Current Version:'), + $this->t('Update Version:'), + ], + '#options' => $options, + '#empty' => $this->t('There are no available updates.'), + '#attributes' => ['class' => ['update-recommended']], + ]; + if ($update_projects) { + $form['actions'] = $this->actions($form_state); + } + return $form; + } + + /** + * Builds the form actions. + * + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @return mixed[][] + * The form's actions elements. + */ + protected function actions(FormStateInterface $form_state): array { + $actions = ['#type' => 'actions']; + if (!$this->updater->isAvailable()) { + // If the form has been submitted do not display this error message + // 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.')); + } + $actions['delete'] = [ + '#type' => 'submit', + '#value' => $this->t('Delete existing update'), + '#submit' => ['::deleteExistingUpdate'], + ]; + } + else { + $actions['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Update'), + ]; + } + return $actions; + } + + /** + * Submit function to delete an existing in-progress update. + */ + public function deleteExistingUpdate(): void { + $this->updater->destroy(TRUE); + $this->messenger()->addMessage($this->t("Staged update deleted")); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $projects = $form_state->getValue('projects'); + $selected_projects = array_filter($projects); + $recommended_versions = $form_state->getValue('recommended_versions'); + $selected_versions = array_intersect_key($recommended_versions, $selected_projects); + $this->messenger()->addMessage(print_r($selected_versions, TRUE)); + } + + /** + * Gets the modules that require updates. + * + * @return array + * Modules that require updates. + */ + private function getRecommendedModuleUpdates(): array { + $available_updates = update_get_available(TRUE); + if (empty($available_updates)) { + $this->messenger()->addError('There was a problem getting update information. Try again later.'); + return []; + } + + $project_data = update_calculate_project_data($available_updates); + $outdated_modules = []; + foreach ($project_data as $project_name => $project_info) { + if ($project_info['project_type'] === 'module' || $project_info['project_type'] === 'module-disabled') { + if ($project_info['status'] !== UpdateManagerInterface::CURRENT) { + if (!empty($project_info['recommended'])) { + $outdated_modules[$project_name] = $project_info; + } + } + } + } + return $outdated_modules; + } + +} diff --git a/automatic_updates_extensions/tests/src/Functional/UpdaterFormTest.php b/automatic_updates_extensions/tests/src/Functional/UpdaterFormTest.php new file mode 100644 index 0000000000000000000000000000000000000000..b4a1ff50558e882dc10b3c3d5b46102426212605 --- /dev/null +++ b/automatic_updates_extensions/tests/src/Functional/UpdaterFormTest.php @@ -0,0 +1,108 @@ +<?php + +namespace Drupal\Tests\automatic_updates_extensions\Functional; + +use Drupal\Tests\automatic_updates\Functional\AutomaticUpdatesFunctionalTestBase; + +/** + * Tests updating using the form. + * + * @group automatic_updates_extensions + */ +class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase { + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'automatic_updates_test', + 'automatic_updates_extensions', + 'semver_test', + ]; + + /** + * Project to test updates. + * + * @var string + */ + protected $updateProject = 'semver_test'; + + /** + * {@inheritdoc} + */ + protected function setUp():void { + parent::setUp(); + $this->setReleaseMetadata(__DIR__ . '/../../../../tests/fixtures/release-history/semver_test.1.1.xml'); + } + + /** + * Sets installed project version. + * + * @todo This is copied from core. We need to file a core issue so we do not + * have to copy this. + */ + protected function setProjectInstalledVersion($version) { + $this->config('update.settings') + ->set('fetch.url', $this->baseUrl . '/automatic-update-test') + ->save(); + $system_info = [ + $this->updateProject => [ + 'project' => $this->updateProject, + 'version' => $version, + 'hidden' => FALSE, + ], + // Ensure Drupal core on the same version for all test runs. + 'drupal' => [ + 'project' => 'drupal', + 'version' => '8.0.0', + 'hidden' => FALSE, + ], + ]; + $this->config('update_test.settings')->set('system_info', $system_info)->save(); + } + + /** + * Tests the form when a module requires an update. + */ + public function testHasUpdate():void { + $assert = $this->assertSession(); + $user = $this->createUser(['administer site configuration']); + $this->drupalLogin($user); + $this->setProjectInstalledVersion('8.1.0'); + $this->checkForUpdates(); + $this->drupalGet('/admin/automatic-update-extensions'); + $assert->pageTextContains('Access Denied'); + $assert->pageTextNotContains('Automatic Updates Form'); + $user = $this->createUser(['administer software updates']); + $this->drupalLogin($user); + $this->drupalGet('/admin/automatic-update-extensions'); + $assert->pageTextContains('Automatic Updates Form'); + $assert->elementTextContains('css', '.update-recommended td:nth-of-type(2)', 'Semver Test'); + $assert->elementTextContains('css', '.update-recommended td:nth-of-type(3)', '8.1.0'); + $assert->elementTextContains('css', '.update-recommended td:nth-of-type(4)', '8.1.1'); + $assert->elementsCount('css', '.update-recommended tbody tr', 1); + $assert->buttonExists('Update'); + } + + /** + * Tests the form when there are no available updates. + */ + public function testNoUpdate():void { + $assert = $this->assertSession(); + $user = $this->createUser(['administer site configuration', + 'administer software updates', + ]); + $this->drupalLogin($user); + $this->setProjectInstalledVersion('8.1.1'); + $this->checkForUpdates(); + $this->drupalGet('/admin/automatic-update-extensions'); + $assert->pageTextContains('There are no available updates.'); + $assert->buttonNotExists('Update'); + } + +} diff --git a/tests/fixtures/release-history/semver_test.1.1.xml b/tests/fixtures/release-history/semver_test.1.1.xml new file mode 100644 index 0000000000000000000000000000000000000000..cdb353fd4208f3beb975b79d709444b7f65c2c95 --- /dev/null +++ b/tests/fixtures/release-history/semver_test.1.1.xml @@ -0,0 +1,184 @@ +<?xml version="1.0" encoding="utf-8"?> +<project xmlns:dc="http://purl.org/dc/elements/1.1/"> +<title>Semver Test</title> +<short_name>semver_test</short_name> +<dc:creator>Drupal</dc:creator> +<supported_branches>8.0.,8.1.</supported_branches> +<project_status>published</project_status> +<link>http://example.com/project/semver_test</link> + <terms> + <term><name>Projects</name><value>Semver Test project</value></term> + </terms> +<releases> + <release> + <!-- This release is not in a supported branch; therefore it should not be recommended. --> + <name>Semver Test 8.2.0</name> + <version>8.2.0</version> + <tag>8.2.0</tag> + <status>published</status> + <release_link>http://example.com/semver_test-8-2-0-release</release_link> + <download_link>http://example.com/semver_test-8-2-0.tar.gz</download_link> + <date>1584195300</date> + <terms> + <term><name>Release type</name><value>New features</value></term> + <term><name>Release type</name><value>Bug fixes</value></term> + </terms> + </release> + <release> + <name>Semver Test 8.1.1</name> + <version>8.1.1</version> + <tag>8.1.1</tag> + <status>published</status> + <release_link>http://example.com/semver_test-8-1-1-release</release_link> + <download_link>http://example.com/semver_test-8-1-1.tar.gz</download_link> + <date>1581603300</date> + <terms> + <term><name>Release type</name><value>New features</value></term> + <term><name>Release type</name><value>Bug fixes</value></term> + </terms> + </release> + <release> + <name>Semver Test 8.1.1-beta1</name> + <version>8.1.1-beta1</version> + <tag>8.1.1-beta1</tag> + <status>published</status> + <release_link>http://example.com/semver_test-8-1-1-beta1-release</release_link> + <download_link>http://example.com/semver_test-8-1-1-beta1.tar.gz</download_link> + <date>1579011300</date> + <terms> + <term><name>Release type</name><value>New features</value></term> + <term><name>Release type</name><value>Bug fixes</value></term> + </terms> + </release> + <release> + <name>Semver Test 8.1.1-alpha1</name> + <version>8.1.1-alpha1</version> + <tag>8.1.1-alpha1</tag> + <status>published</status> + <release_link>http://example.com/semver_test-8-1-1-alpha1-release</release_link> + <download_link>http://example.com/semver_test-8-1-1-alpha1.tar.gz</download_link> + <date>1576419300</date> + <terms> + <term><name>Release type</name><value>New features</value></term> + <term><name>Release type</name><value>Bug fixes</value></term> + </terms> + </release> + <release> + <name>Semver Test 8.1.0</name> + <version>8.1.0</version> + <tag>8.1.0</tag> + <status>published</status> + <release_link>http://example.com/semver_test-8-1-0-release</release_link> + <download_link>http://example.com/semver_test-8-1-0.tar.gz</download_link> + <date>1573827300</date> + <terms> + <term><name>Release type</name><value>New features</value></term> + <term><name>Release type</name><value>Bug fixes</value></term> + </terms> + </release> + <release> + <name>Semver Test 8.1.0-beta1</name> + <version>8.1.0-beta1</version> + <tag>8.1.0-beta1</tag> + <status>published</status> + <release_link>http://example.com/semver_test-8-1-0-beta1-release</release_link> + <download_link>http://example.com/semver_test-8-1-0-beta1.tar.gz</download_link> + <date>1571235300</date> + <terms> + <term><name>Release type</name><value>New features</value></term> + <term><name>Release type</name><value>Bug fixes</value></term> + </terms> + </release> + <release> + <name>Semver Test 8.1.0-alpha1</name> + <version>8.1.0-alpha1</version> + <tag>8.1.0-alpha1</tag> + <status>published</status> + <release_link>http://example.com/semver_test-8-1-0-alpha1-release</release_link> + <download_link>http://example.com/semver_test-8-1-0-alpha1.tar.gz</download_link> + <date>1568643300</date> + <terms> + <term><name>Release type</name><value>New features</value></term> + <term><name>Release type</name><value>Bug fixes</value></term> + </terms> + </release> + <release> + <name>Semver Test 8.0.1</name> + <version>8.0.1</version> + <tag>8.0.1</tag> + <status>published</status> + <release_link>http://example.com/semver_test-8-0-1-release</release_link> + <download_link>http://example.com/semver_test-8-0-1.tar.gz</download_link> + <date>1566051300</date> + <terms> + <term><name>Release type</name><value>New features</value></term> + <term><name>Release type</name><value>Bug fixes</value></term> + </terms> + </release> + <release> + <name>Semver Test 8.0.1-beta1</name> + <version>8.0.1-beta1</version> + <tag>8.0.1-beta1</tag> + <status>published</status> + <release_link>http://example.com/semver_test-8-0-1-beta1-release</release_link> + <download_link>http://example.com/semver_test-8-0-1-beta1.tar.gz</download_link> + <date>1563459300</date> + <terms> + <term><name>Release type</name><value>New features</value></term> + <term><name>Release type</name><value>Bug fixes</value></term> + </terms> + </release> + <release> + <name>Semver Test 8.0.1-alpha1</name> + <version>8.0.1-alpha1</version> + <tag>8.0.1-alpha1</tag> + <status>published</status> + <release_link>http://example.com/semver_test-8-0-1-alpha1-release</release_link> + <download_link>http://example.com/semver_test-8-0-1-alpha1.tar.gz</download_link> + <date>1560867300</date> + <terms> + <term><name>Release type</name><value>New features</value></term> + <term><name>Release type</name><value>Bug fixes</value></term> + </terms> + </release> + <release> + <name>Semver Test 8.0.0</name> + <version>8.0.0</version> + <tag>8.0.0</tag> + <status>published</status> + <release_link>http://example.com/semver_test-8-0-0-release</release_link> + <download_link>http://example.com/semver_test-8-0-0.tar.gz</download_link> + <date>1558275300</date> + <terms> + <term><name>Release type</name><value>New features</value></term> + <term><name>Release type</name><value>Bug fixes</value></term> + </terms> + </release> + <release> + <name>Semver Test 8.0.0-beta1</name> + <version>8.0.0-beta1</version> + <tag>8.0.0-beta1</tag> + <status>published</status> + <release_link>http://example.com/semver_test-8-0-0-beta1-release</release_link> + <download_link>http://example.com/semver_test-8-0-0-beta1.tar.gz</download_link> + <date>1555683300</date> + <terms> + <term><name>Release type</name><value>New features</value></term> + <term><name>Release type</name><value>Bug fixes</value></term> + </terms> + </release> + <release> + <name>Semver Test 8.0.0-alpha1</name> + <version>8.0.0-alpha1</version> + <tag>8.0.0-alpha1</tag> + <status>published</status> + <release_link>http://example.com/semver_test-8-0-0-alpha1-release</release_link> + <download_link>http://example.com/semver_test-8-0-0-alpha1.tar.gz</download_link> + <date>1553091300</date> + <terms> + <term><name>Release type</name><value>New features</value></term> + <term><name>Release type</name><value>Bug fixes</value></term> + </terms> + </release> +</releases> +</project> diff --git a/tests/modules/automatic_updates_test/src/TestController.php b/tests/modules/automatic_updates_test/src/TestController.php index abb448bcd14b8224775b28f9cd682b8a106d5227..7609cbb38328bf48c902faec5103f1815eda401f 100644 --- a/tests/modules/automatic_updates_test/src/TestController.php +++ b/tests/modules/automatic_updates_test/src/TestController.php @@ -63,9 +63,6 @@ class TestController extends ControllerBase { * directory of mock XML files. */ public function metadata($project_name = 'drupal', $version = NULL): Response { - if ($project_name !== 'drupal') { - return new Response(); - } $xml_map = $this->config('update_test.settings')->get('xml_map'); if (isset($xml_map[$project_name])) { $availability_scenario = $xml_map[$project_name];