diff --git a/core/modules/update/src/Form/UpdateManagerUpdate.php b/core/modules/update/src/Form/UpdateManagerUpdate.php index fb5385c0260..7b2b680ed6b 100644 --- a/core/modules/update/src/Form/UpdateManagerUpdate.php +++ b/core/modules/update/src/Form/UpdateManagerUpdate.php @@ -199,42 +199,49 @@ class UpdateManagerUpdate extends FormBase { // Drupal core needs to be upgraded manually. $needs_manual = $project['project_type'] == 'core'; + // If the recommended release for a contributed project is not compatible + // with the currently installed version of core, list that project in a + // separate table. To determine if the release is compatible, we inspect + // the 'core_compatible' key from the release info array. If it's not + // defined, it means we can't determine compatibility requirements (or + // we're looking at core), so we assume it is compatible. + $compatible = $recommended_release['core_compatible'] ?? TRUE; + if ($needs_manual) { - // There are no checkboxes in the 'Manual updates' table so it will be - // rendered by '#theme' => 'table', not '#theme' => 'tableselect'. Since - // the data formats are incompatible, we convert now to the format - // expected by '#theme' => 'table'. - unset($entry['#weight']); - $attributes = $entry['#attributes']; - unset($entry['#attributes']); - $entry = [ - 'data' => $entry, - ] + $attributes; + $this->removeCheckboxFromRow($entry); + $projects['manual'][$name] = $entry; + } + elseif (!$compatible) { + $this->removeCheckboxFromRow($entry); + // If the release has a core_compatibility_message, inject it. + if (!empty($recommended_release['core_compatibility_message'])) { + // @todo In https://www.drupal.org/project/drupal/issues/3121769 + // refactor this into something theme-friendly so we don't have a + // classless
here. + $entry['data']['recommended_version']['data']['#template'] .= '
{{ core_compatibility_message }}
'; + $entry['data']['recommended_version']['data']['#context']['core_compatibility_message'] = $recommended_release['core_compatibility_message']; + } + $projects['not-compatible'][$name] = $entry; } else { $form['project_downloads'][$name] = [ '#type' => 'value', '#value' => $recommended_release['download_link'], ]; - } - // Based on what kind of project this is, save the entry into the - // appropriate subarray. - switch ($project['project_type']) { - case 'core': - // Core needs manual updates at this time. - $projects['manual'][$name] = $entry; - break; + // Based on what kind of project this is, save the entry into the + // appropriate subarray. + switch ($project['project_type']) { + case 'module': + case 'theme': + $projects['enabled'][$name] = $entry; + break; - case 'module': - case 'theme': - $projects['enabled'][$name] = $entry; - break; - - case 'module-disabled': - case 'theme-disabled': - $projects['disabled'][$name] = $entry; - break; + case 'module-disabled': + case 'theme-disabled': + $projects['disabled'][$name] = $entry; + break; + } } } @@ -297,9 +304,45 @@ class UpdateManagerUpdate extends FormBase { ]; } + if (!empty($projects['not-compatible'])) { + $form['not_compatible'] = [ + '#type' => 'table', + '#header' => $headers, + '#rows' => $projects['not-compatible'], + '#prefix' => '

' . $this->t('Not compatible') . '

', + '#weight' => 150, + ]; + } + return $form; } + /** + * Prepares a row entry for use in a regular table, not a 'tableselect'. + * + * There are no checkboxes in the 'Manual updates' or 'Not compatible' tables, + * so they will be rendered by '#theme' => 'table', not 'tableselect'. Since + * the data formats are incompatible, this method converts to the format + * expected by '#theme' => 'table'. Generally, rows end up in the main tables + * that have a checkbox to allow the site admin to select which missing + * updates to install. This method is only used for the special case tables + * that have no such checkbox. + * + * @todo In https://www.drupal.org/project/drupal/issues/3121775 refactor + * self::buildForm() so that we don't need this method at all. + * + * @param array[] $row + * The render array for a table row. + */ + protected function removeCheckboxFromRow(array &$row) { + unset($row['#weight']); + $attributes = $row['#attributes']; + unset($row['#attributes']); + $row = [ + 'data' => $row, + ] + $attributes; + } + /** * {@inheritdoc} */ diff --git a/core/modules/update/tests/modules/update_test/bbb_update_test.1_1.xml b/core/modules/update/tests/modules/update_test/bbb_update_test.1_1.xml new file mode 100644 index 00000000000..9bac7ba1f61 --- /dev/null +++ b/core/modules/update/tests/modules/update_test/bbb_update_test.1_1.xml @@ -0,0 +1,48 @@ + + + +BBB Update test +bbb_update_test +Drupal +8.x-1. +published +http://example.com/project/bbb_update_test + + ProjectsModules + + + + bbb_update_test 8.x-1.1 + 8.x-1.1 + 8.x-1.1 + published + http://example.com/bbb_update_test-8-x-1-1-release + http://example.com/bbb_update_test-8.x-1.1.tar.gz + 1250444521 + + Release typeBug fixes + + + + bbb_update_test 8.x-1.0 + 8.x-1.0 + 8.x-1.0 + published + http://example.com/bbb_update_test-8-x-1-0-release + http://example.com/bbb_update_test-8.x-1.0.tar.gz + 1250424521 + + Release typeNew features + Release typeBug fixes + + + + diff --git a/core/modules/update/tests/modules/update_test/bbb_update_test.1_2.xml b/core/modules/update/tests/modules/update_test/bbb_update_test.1_2.xml new file mode 100644 index 00000000000..f15467e689c --- /dev/null +++ b/core/modules/update/tests/modules/update_test/bbb_update_test.1_2.xml @@ -0,0 +1,62 @@ + + + +BBB Update test +bbb_update_test +Drupal +8.x-1. +published +http://example.com/project/bbb_update_test + + ProjectsModules + + + + bbb_update_test 8.x-1.2 + 8.x-1.2 + 8.x-1.2 + ^8.1.0 + published + http://example.com/bbb_update_test-8-x-1-2-release + http://example.com/bbb_update_test-8.x-1.2.tar.gz + 1250445521 + + Release typeBug fixes + + + + bbb_update_test 8.x-1.1 + 8.x-1.1 + 8.x-1.1 + published + http://example.com/bbb_update_test-8-x-1-1-release + http://example.com/bbb_update_test-8.x-1.1.tar.gz + 1250444521 + + Release typeBug fixes + + + + bbb_update_test 8.x-1.0 + 8.x-1.0 + 8.x-1.0 + published + http://example.com/bbb_update_test-8-x-1-0-release + http://example.com/bbb_update_test-8.x-1.0.tar.gz + 1250424521 + + Release typeNew features + Release typeBug fixes + + + + diff --git a/core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php b/core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php new file mode 100644 index 00000000000..3e87e9a651c --- /dev/null +++ b/core/modules/update/tests/src/Functional/UpdateManagerUpdateTest.php @@ -0,0 +1,236 @@ +drupalCreateUser([ + 'administer software updates', + 'administer site configuration', + ]); + $this->drupalLogin($admin_user); + + // The installed state of the system is the same for all test cases. What + // varies for each test scenario is which release history fixture we fetch, + // which in turn changes the expected state of the UpdateManagerUpdateForm. + $system_info = [ + '#all' => [ + 'version' => '8.0.0', + ], + 'aaa_update_test' => [ + 'project' => 'aaa_update_test', + 'version' => '8.x-1.0', + 'hidden' => FALSE, + ], + 'bbb_update_test' => [ + 'project' => 'bbb_update_test', + 'version' => '8.x-1.0', + 'hidden' => FALSE, + ], + ]; + $this->config('update_test.settings')->set('system_info', $system_info)->save(); + } + + /** + * Provides data for test scenarios involving incompatible updates. + * + * These test cases rely on the following fixtures containing the following + * releases: + * - aaa_update_test.8.x-1.2.xml + * - 8.x-1.2 Compatible with 8.0.0 core. + * - aaa_update_test.core_compatibility.8.x-1.2_8.x-2.2.xml + * - 8.x-1.2 Requires 8.1.0 and above. + * - bbb_update_test.1_0.xml + * - 8.x-1.0 is the only available release. + * - bbb_update_test.1_1.xml + * - 8.x-1.1 is available and compatible with everything (does not define + * at all). + * - bbb_update_test.1_2.xml + * - 8.x-1.1 is available and compatible with everything (does not define + * at all). + * - 8.x-1.2 is available and requires Drupal 8.1.0 and above. + * + * @todo In https://www.drupal.org/project/drupal/issues/3112962: + * Change the 'core_fixture' values here to use: + * - '1.1' instead of '1.1-core_compatibility'. + * - '1.1-alpha1' instead of '1.1-alpha1-core_compatibility'. + * Delete the files: + * - core/modules/update/tests/modules/update_test/drupal.1.1-alpha1-core_compatibility.xml + * - core/modules/update/tests/modules/update_test/drupal.1.1-core_compatibility.xml + * + * @return array[] + * Test data. + */ + public function incompatibleUpdatesTableProvider() { + return [ + 'only one compatible' => [ + 'core_fixture' => '1.1-core_compatibility', + // aaa_update_test.8.x-1.2.xml has core compatibility set and will test + // the case where $recommended_release['core_compatible'] === TRUE in + // \Drupal\update\Form\UpdateManagerUpdate. + 'a_fixture' => '8.x-1.2', + // Use a fixture with only a 8.x-1.0 release so BBB is up to date. + 'b_fixture' => '1_0', + 'compatible' => [ + 'AAA' => '8.x-1.2', + ], + 'incompatible' => [], + ], + 'only one incompatible' => [ + 'core_fixture' => '1.1-core_compatibility', + 'a_fixture' => 'core_compatibility.8.x-1.2_8.x-2.2', + // Use a fixture with only a 8.x-1.0 release so BBB is up to date. + 'b_fixture' => '1_0', + 'compatible' => [], + 'incompatible' => [ + 'AAA' => [ + 'recommended' => '8.x-1.2', + 'range' => '8.1.0 to 8.1.1', + ], + ], + ], + 'two compatible, no incompatible' => [ + 'core_fixture' => '1.1-core_compatibility', + 'a_fixture' => '8.x-1.2', + // bbb_update_test.1_1.xml does not have core compatibility set and will + // test the case where $recommended_release['core_compatible'] === NULL + // in \Drupal\update\Form\UpdateManagerUpdate. + 'b_fixture' => '1_1', + 'compatible' => [ + 'AAA' => '8.x-1.2', + 'BBB' => '8.x-1.1', + ], + 'incompatible' => [], + ], + 'two incompatible, no compatible' => [ + 'core_fixture' => '1.1-core_compatibility', + 'a_fixture' => 'core_compatibility.8.x-1.2_8.x-2.2', + // bbb_update_test.1_2.xml has core compatibility set and will test the + // case where $recommended_release['core_compatible'] === FALSE in + // \Drupal\update\Form\UpdateManagerUpdate. + 'b_fixture' => '1_2', + 'compatible' => [], + 'incompatible' => [ + 'AAA' => [ + 'recommended' => '8.x-1.2', + 'range' => '8.1.0 to 8.1.1', + ], + 'BBB' => [ + 'recommended' => '8.x-1.2', + 'range' => '8.1.0 to 8.1.1', + ], + ], + ], + 'one compatible, one incompatible' => [ + 'core_fixture' => '1.1-core_compatibility', + 'a_fixture' => 'core_compatibility.8.x-1.2_8.x-2.2', + 'b_fixture' => '1_1', + 'compatible' => [ + 'BBB' => '8.x-1.1', + ], + 'incompatible' => [ + 'AAA' => [ + 'recommended' => '8.x-1.2', + 'range' => '8.1.0 to 8.1.1', + ], + ], + ], + ]; + } + + /** + * Tests the Update form for a single test scenario of incompatible updates. + * + * @dataProvider incompatibleUpdatesTableProvider + * + * @param string $core_fixture + * The fixture file to use for Drupal core. + * @param string $a_fixture + * The fixture file to use for the aaa_update_test module. + * @param string $b_fixture + * The fixture file to use for the bbb_update_test module. + * @param string[] $compatible + * Compatible recommended updates (if any). Keys are module identifier + * ('AAA' or 'BBB') and values are the expected recommended release. + * @param string[][] $incompatible + * Incompatible recommended updates (if any). Keys are module identifier + * ('AAA' or 'BBB') and values are subarrays with the following keys: + * - 'recommended': The recommended version. + * - 'range': The versions of Drupal core required for that version. + */ + public function testIncompatibleUpdatesTable($core_fixture, $a_fixture, $b_fixture, array $compatible, array $incompatible) { + + $assert_session = $this->assertSession(); + $compatible_table_locator = '[data-drupal-selector="edit-projects"]'; + $incompatible_table_locator = '[data-drupal-selector="edit-not-compatible"]'; + + $this->refreshUpdateStatus(['drupal' => $core_fixture, 'aaa_update_test' => $a_fixture, 'bbb_update_test' => $b_fixture]); + $this->drupalGet('admin/reports/updates/update'); + + if ($compatible) { + // Verify the number of rows in the table. + $assert_session->elementsCount('css', "$compatible_table_locator tbody tr", count($compatible)); + // We never want to see a compatibility range in the compatible table. + $assert_session->elementTextNotContains('css', $compatible_table_locator, 'Requires Drupal core'); + foreach ($compatible as $module => $version) { + $compatible_row = "$compatible_table_locator tbody tr:contains('$module Update test')"; + // First is the checkbox, so start with td #2. + $assert_session->elementTextContains('css', "$compatible_row td:nth-of-type(2)", "$module Update test"); + // Both contrib modules use 8.x-1.0 as the currently installed version. + $assert_session->elementTextContains('css', "$compatible_row td:nth-of-type(3)", '8.x-1.0'); + $assert_session->elementTextContains('css', "$compatible_row td:nth-of-type(4)", $version); + } + } + else { + // Verify there is no compatible updates table. + $assert_session->elementNotExists('css', $compatible_table_locator); + } + + if ($incompatible) { + // Verify the number of rows in the table. + $assert_session->elementsCount('css', "$incompatible_table_locator tbody tr", count($incompatible)); + foreach ($incompatible as $module => $data) { + $incompatible_row = "$incompatible_table_locator tbody tr:contains('$module Update test')"; + $assert_session->elementTextContains('css', "$incompatible_row td:nth-of-type(1)", "$module Update test"); + // Both contrib modules use 8.x-1.0 as the currently installed version. + $assert_session->elementTextContains('css', "$incompatible_row td:nth-of-type(2)", '8.x-1.0'); + $assert_session->elementTextContains('css', "$incompatible_row td:nth-of-type(3)", $data['recommended']); + $assert_session->elementTextContains('css', "$incompatible_row td:nth-of-type(3)", 'Requires Drupal core: ' . $data['range']); + } + } + else { + // Verify there is no incompatible updates table. + $assert_session->elementNotExists('css', $incompatible_table_locator); + } + } + +}