diff --git a/core/core.services.yml b/core/core.services.yml index 92309f2..def1403 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1521,7 +1521,7 @@ services: class: Egulias\EmailValidator\EmailValidator update.post_update_registry: class: Drupal\Core\Update\UpdateRegistry - factory: ['@update.post_update_registry_factory', configure] + factory: ['@update.post_update_registry_factory', create] update.post_update_registry_factory: class: Drupal\Core\Update\UpdateRegistryFactory parent: container.trait diff --git a/core/includes/update.inc b/core/includes/update.inc index 44389e7..85246c3 100644 --- a/core/includes/update.inc +++ b/core/includes/update.inc @@ -241,7 +241,7 @@ function update_invoke_post_update($function, &$context) { $ret['results']['query'] = $function($context['sandbox']); $ret['results']['success'] = TRUE; - \Drupal::service('update.post_update_registry')->registerInvokedUpdate($function); + \Drupal::service('update.post_update_registry')->registerInvokedUpdates([$function]); } // @TODO We may want to do different error handling for different // exception types, but for now we'll just log the exception and diff --git a/core/lib/Drupal/Core/Extension/ModuleInstaller.php b/core/lib/Drupal/Core/Extension/ModuleInstaller.php index c97de42..683450c 100644 --- a/core/lib/Drupal/Core/Extension/ModuleInstaller.php +++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php @@ -262,10 +262,9 @@ public function install(array $module_list, $enable_dependencies = TRUE) { drupal_set_installed_schema_version($module, $version); // Ensure that all post_update functions are registered already. - $post_update_key_value = \Drupal::keyValue('post_update'); - $post_update_registry = new UpdateRegistry($this->root, \Drupal::service('site.path'), array_keys($this->moduleHandler->getModuleList()), $post_update_key_value); - $module_post_update_functions = $post_update_registry->getModuleUpdateFunctions($module); - $post_update_key_value->set('existing_updates', array_merge($post_update_key_value->get('existing_updates', []), $module_post_update_functions)); + /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */ + $post_update_registry = \Drupal::service('update.post_update_registry'); + $post_update_registry->registerInvokedUpdates($post_update_registry->getModuleUpdateFunctions($module)); // Record the fact that it was installed. $modules_installed[] = $module; @@ -457,6 +456,10 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) { $post_update_registry = new UpdateRegistry($this->root, \Drupal::service('site.path'), array_keys($this->moduleHandler->getModuleList()), $post_update_key_value); $module_post_update_functions = $post_update_registry->getModuleUpdateFunctions($module); $post_update_key_value->set('existing_updates', array_diff($post_update_key_value->get('existing_updates', []), $module_post_update_functions)); + + /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */ + $post_update_registry = \Drupal::service('update.post_update_registry'); + $post_update_registry->filterOutInvokedUpdatesByModule($module); } \Drupal::service('router.builder')->setRebuildNeeded(); drupal_get_installed_schema_version(NULL, TRUE); diff --git a/core/lib/Drupal/Core/Extension/module.api.php b/core/lib/Drupal/Core/Extension/module.api.php index 5b3de7d..41393de 100644 --- a/core/lib/Drupal/Core/Extension/module.api.php +++ b/core/lib/Drupal/Core/Extension/module.api.php @@ -684,7 +684,7 @@ function hook_update_N(&$sandbox) { * PDOException. * * @return string|null - * Optionally, post_update_NAME() hooks may return a translated string that will + * Optionally, hook_post_update_NAME() hooks may return a translated string that will * be displayed to the user after the update has completed. If no message is * returned, no message will be presented to the user. * diff --git a/core/lib/Drupal/Core/Update/UpdateRegistry.php b/core/lib/Drupal/Core/Update/UpdateRegistry.php index 1c4ade4..93cc694 100644 --- a/core/lib/Drupal/Core/Update/UpdateRegistry.php +++ b/core/lib/Drupal/Core/Update/UpdateRegistry.php @@ -12,14 +12,22 @@ use Drupal\Core\KeyValueStore\KeyValueStoreInterface; /** - * Provides all and missing hook_$updateName_NAME implementations. + * Provides all and missing update implementations. * - * It therefore scans for functions in MODULE.$updateName.php named like - * MODULE_$updateName_NAME with NAME being a machine name. + * Note: This registry is specific to a type of updates, like 'post_update' as + * example. + * + * It therefore scans for functions named like the type of updates, so it looks + * like MODULE_UPDATETYPE_NAME() with NAME being a machine name. */ class UpdateRegistry { - protected $updateName = 'post_update'; + /** + * The used update name. + * + * @var string + */ + protected $updateType = 'post_update'; /** * The app root. @@ -90,15 +98,15 @@ public function __construct($root, $site_path, array $enabled_modules, KeyValueS * A list of update functions. */ protected function getAvailableUpdateFunctions() { - $regexp = '/^(?.+)_' . $this->updateName . '_(?.+)$/'; + $regexp = '/^(?.+)_' . $this->updateType . '_(?.+)$/'; $functions = get_defined_functions(); $updates = []; - foreach (preg_grep('/_' . $this->updateName . '_/', $functions['user']) as $function) { + foreach (preg_grep('/_' . $this->updateType . '_/', $functions['user']) as $function) { // If this function is a module update function, add it to the list of // module updates. if (preg_match($regexp, $function, $matches)) { - $updates[] = $matches['module'] . '_' . $this->updateName . '_' . $matches['name']; + $updates[] = $matches['module'] . '_' . $this->updateType . '_' . $matches['name']; } } @@ -118,7 +126,7 @@ public function getPendingUpdateFunctions() { $this->scanExtensionsAndLoadUpdateFiles(); - // First figure out which hook_{$this->updateName}_NAME got executed + // First figure out which hook_{$this->updateType}_NAME got executed // already. $existing_update_functions = $this->keyValue->get('existing_updates', []); @@ -135,7 +143,7 @@ public function getPendingUpdateFunctions() { * The extensions used for loading. */ protected function loadUpdateFiles(array $module_extensions) { - // Load all the {$this->updateName}.php files. + // Load all the {$this->updateType}.php files. foreach ($this->enabledModules as $module) { if (isset($module_extensions[$module])) { $this->loadUpdateFile($module_extensions[$module]); @@ -144,13 +152,13 @@ protected function loadUpdateFiles(array $module_extensions) { } /** - * Loads the {$this->updateName}.php file for a given extension. + * Loads the {$this->updateType}.php file for a given extension. * * @param \Drupal\Core\Extension\Extension $module * The extension of the module to load its file. */ protected function loadUpdateFile(Extension $module) { - $filename = $this->root . '/' . $module->getPath() . '/' . $module->getName() . ".{$this->updateName}.php"; + $filename = $this->root . '/' . $module->getPath() . '/' . $module->getName() . ".{$this->updateType}.php"; if (file_exists($filename)) { include_once $filename; } @@ -177,7 +185,7 @@ public function getPendingUpdateInformation() { $ret = []; foreach ($functions as $function) { - list($module, $update) = explode("_{$this->updateName}_", $function); + list($module, $update) = explode("_{$this->updateType}_", $function); // The description for an update comes from its Doxygen. $func = new \ReflectionFunction($function); $description = trim(str_replace(array("\n", '*', '/'), '', $func->getDocComment()), ' '); @@ -190,16 +198,16 @@ public function getPendingUpdateInformation() { } /** - * Registers that a specific update got executed. + * Registers that update fucntions got executed. * - * @param string $function_name - * The executed update function. + * @param string[] $function_names + * The executed update functions. * * @return $this */ - public function registerInvokedUpdate($function_name) { + public function registerInvokedUpdates(array $function_names) { $executed_updates = $this->keyValue->get('existing_updates', []); - $executed_updates[] = $function_name; + $executed_updates = array_merge($executed_updates, $function_names); $this->keyValue->set('existing_updates', $executed_updates); return $this; @@ -219,7 +227,7 @@ public function getModuleUpdateFunctions($module_name) { $all_functions = $this->getAvailableUpdateFunctions(); return array_filter($all_functions, function($function_name) use ($module_name) { - list($function_module_name, ) = explode("_{$this->updateName}_", $function_name); + list($function_module_name, ) = explode("_{$this->updateType}_", $function_name); return $function_module_name === $module_name; }); } @@ -238,4 +246,20 @@ protected function scanExtensionsAndLoadUpdateFiles() { $this->loadUpdateFiles($extensions); } + /** + * Filters out already executed update functions by module. + * + * @param string $module + * The module name. + */ + public function filterOutInvokedUpdatesByModule($module) { + $existing_update_functions = $this->keyValue->get('existing_updates', []); + + $remaining_update_functions = array_filter($existing_update_functions, function($function_name) use ($module) { + return strpos($function_name, "{$module}_{$this->updateType}_") !== 0; + }); + + $this->keyValue->set('existing_updates', array_values($remaining_update_functions)); + } + } diff --git a/core/lib/Drupal/Core/Update/UpdateRegistryFactory.php b/core/lib/Drupal/Core/Update/UpdateRegistryFactory.php index 0c076cb..ef41154 100644 --- a/core/lib/Drupal/Core/Update/UpdateRegistryFactory.php +++ b/core/lib/Drupal/Core/Update/UpdateRegistryFactory.php @@ -23,7 +23,7 @@ class UpdateRegistryFactory implements ContainerAwareInterface { * @return \Drupal\Core\Update\UpdateRegistry * The update registry instance. */ - public function configure() { + public function create() { return new UpdateRegistry($this->container->get('app.root'), $this->container->get('site.path'), array_keys($this->container->get('module_handler')->getModuleList()), $this->container->get('keyvalue')->get('post_update')); } diff --git a/core/modules/block/block.install b/core/modules/block/block.install index 07a0356..9abe82e 100644 --- a/core/modules/block/block.install +++ b/core/modules/block/block.install @@ -99,7 +99,7 @@ function block_update_8001() { * Placeholder for the previous 8002 update. */ function block_update_8002() { - \Drupal::state()->set('block_update_8002', TRUE); + \Drupal::state()->set('block_update_8002_placeholder', TRUE); } /** diff --git a/core/modules/block/block.post_update.php b/core/modules/block/block.post_update.php index f0325ce..17e0497 100644 --- a/core/modules/block/block.post_update.php +++ b/core/modules/block/block.post_update.php @@ -20,12 +20,12 @@ function block_post_update_disable_blocks_with_missing_contexts() { // batch is finished. $module_schema = drupal_get_installed_schema_version('block'); - // The state entry 'block_update_8002' is used in order to indicate that - // block_update_8002() would have been executed, so this function needs to be - // executed as well. If the non placeholder block_update_8002() got executed - // already the state won't be set, and we skip this update. - // We also need to check whether - if ($module_schema >= 8002 && !\Drupal::state()->get('block_update_8002', FALSE)) { + // The state entry 'block_update_8002_placeholder' is used in order to + // indicate that the placeholder block_update_8002() function has been + // executed, so this function needs to be executed as well. If the non + // placeholder version of block_update_8002() got executed already, the state + // won't be set and we skip this update. + if ($module_schema >= 8002 && !\Drupal::state()->get('block_update_8002_placeholder', FALSE)) { return; } diff --git a/core/modules/system/src/Controller/DbUpdateController.php b/core/modules/system/src/Controller/DbUpdateController.php index db0492b..8baa784 100644 --- a/core/modules/system/src/Controller/DbUpdateController.php +++ b/core/modules/system/src/Controller/DbUpdateController.php @@ -444,7 +444,7 @@ protected function results(Request $request) { if ($module != '#abort') { $module_has_message = FALSE; $info_messages = array(); - foreach ($updates as $number => $queries) { + foreach ($updates as $name => $queries) { $messages = array(); foreach ($queries as $query) { // If there is no message for this update, don't show anything. @@ -468,10 +468,16 @@ protected function results(Request $request) { if ($messages) { $module_has_message = TRUE; + if (is_numeric($name)) { + $title = $this->t('Update #@count', ['@count' => $name]); + } + else { + $title = $this->t('Update @name', ['@name' => $name]); + } $info_messages[] = array( '#theme' => 'item_list', '#items' => $messages, - '#title' => $this->t('Update #@count', array('@count' => $number)), + '#title' => $title, ); } } diff --git a/core/modules/system/src/Tests/Module/InstallUninstallTest.php b/core/modules/system/src/Tests/Module/InstallUninstallTest.php index 2295b75..261b306 100644 --- a/core/modules/system/src/Tests/Module/InstallUninstallTest.php +++ b/core/modules/system/src/Tests/Module/InstallUninstallTest.php @@ -16,7 +16,10 @@ */ class InstallUninstallTest extends ModuleTestBase { - public static $modules = array('system_test', 'dblog', 'taxonomy'); + /** + * {@inheritdoc} + */ + public static $modules = array('system_test', 'dblog', 'taxonomy', 'update_test_postupdate'); /** * Tests that a fixed set of modules can be installed and uninstalled. @@ -105,6 +108,7 @@ public function testInstallUninstall() { $this->assertModuleTablesExist($module_to_install); $this->assertModuleConfig($module_to_install); $this->assertLogMessage('system', "%module module installed.", array('%module' => $module_to_install), RfcLogLevel::INFO); + $this->assertModuleUpdates($module_to_install); } // Uninstall the original module, and check appropriate @@ -188,6 +192,46 @@ protected function assertSuccessfulUninstall($module, $package = 'Core') { $this->assertModuleTablesDoNotExist($module); // Check that the module's config files no longer exist. $this->assertNoModuleConfig($module); + $this->assertUninstallModuleUpdates($module); + } + + /** + * Ensures the module post update functions after install. + * + * @param string $module + * The module that got installed. + */ + protected function assertModuleUpdates($module) { + /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */ + $post_update_registry = \Drupal::service('update.post_update_registry'); + $all_update_functions = $post_update_registry->getPendingUpdateFunctions(); + $this->assertTrue(empty($all_update_functions[$module]), 'Ensures that no pending post update functions are available.'); + + $existing_updates = \Drupal::keyValue('post_update')->get('existing_updates', []); + switch ($module) { + case 'block': + $this->assertEqual(['block_post_update_disable_blocks_with_missing_contexts'], $existing_updates[$module]); + break; + case 'update_test_postupdate': + $this->assertEqual(['update_test_postupdate_post_update_first', 'update_test_postupdate_post_update_second', 'update_test_postupdate_post_update_test1', 'update_test_postupdate_post_update_test0'], $existing_updates[$module]); + break; + } + } + + /** + * Ensures the module post update functions after uninstall. + * + * @param string $module + * The module that got installed. + */ + protected function assertUninstallModuleUpdates($module) { + /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */ + $post_update_registry = \Drupal::service('update.post_update_registry'); + $all_update_functions = $post_update_registry->getPendingUpdateFunctions(); + $this->assertTrue(empty($all_update_functions[$module]), 'Ensures that no pending post update functions are available.'); + + $existing_updates = \Drupal::keyValue('post_update')->get('existing_updates', []); + $this->assertTrue(empty($existing_updates[$module]), 'Ensures that no pending post update functions are available.'); } } diff --git a/core/modules/system/src/Tests/System/StatusTest.php b/core/modules/system/src/Tests/System/StatusTest.php index 57ef4b0..3f4f83f 100644 --- a/core/modules/system/src/Tests/System/StatusTest.php +++ b/core/modules/system/src/Tests/System/StatusTest.php @@ -21,6 +21,11 @@ class StatusTest extends WebTestBase { /** * {@inheritdoc} */ + public static $modules = ['update_test_postupdate']; + + /** + * {@inheritdoc} + */ protected function setUp() { parent::setUp(); @@ -57,6 +62,23 @@ public function testStatusPage() { $this->assertNoLinkByHref(Url::fromRoute('system.php')->toString()); } + // If a module is fully installed no pending updates exists. + $this->assertNoText(t('Out of date')); + + // Set the schema version of update_test_postupdate to a lower version, so + // update_test_postupdate_update_8001() needs to be exeucted. + drupal_set_installed_schema_version('update_test_postupdate', 8000); + $this->drupalGet('admin/reports/status'); + $this->assertText(t('Out of date')); + + // Now cleanup the executed post update functions. + drupal_set_installed_schema_version('update_test_postupdate', 8001); + /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */ + $post_update_registry = \Drupal::service('update.post_update_registry'); + $post_update_registry->filterOutInvokedUpdatesByModule('update_test_postupdate'); + $this->drupalGet('admin/reports/status'); + $this->assertText(t('Out of date')); + $this->drupalGet('admin/reports/status/php'); $this->assertResponse(200, 'The phpinfo page is reachable.'); } diff --git a/core/modules/system/src/Tests/Update/UpdatePostUpdateTest.php b/core/modules/system/src/Tests/Update/UpdatePostUpdateTest.php index 7b84670..3431515 100644 --- a/core/modules/system/src/Tests/Update/UpdatePostUpdateTest.php +++ b/core/modules/system/src/Tests/Update/UpdatePostUpdateTest.php @@ -32,7 +32,7 @@ protected function doSelectionTest() { // Ensure that normal and post_update updates are merged together on the // selection page. - $this->assertRaw('
  • 8001 - Normal update_N() function.
  • First update.
  • Second update.
  • Test1 update.
  • Test2 update.
'); + $this->assertRaw('
  • 8001 - Normal update_N() function.
  • First update.
  • Second update.
  • Test1 update.
  • Test0 update.
'); } /** @@ -41,6 +41,15 @@ protected function doSelectionTest() { public function testPostUpdate() { $this->runUpdates(); + $this->assertRaw('

Update first

'); + $this->assertRaw('First update'); + $this->assertRaw('

Update second

'); + $this->assertRaw('Second update'); + $this->assertRaw('

Update test1

'); + $this->assertRaw('Test1 update'); + $this->assertRaw('

Update test0

'); + $this->assertRaw('Test0 update'); + $updates = [ 'update_test_postupdate_post_update_first', 'update_test_postupdate_post_update_second', diff --git a/core/modules/system/system.install b/core/modules/system/system.install index 86365a9..9182301 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -611,6 +611,15 @@ function system_requirements($phase) { } } + /** @var \Drupal\Core\Update\UpdateRegistry $post_update_registry */ + $post_update_registry = \Drupal::service('update.post_update_registry'); + $missing_post_update_functions = $post_update_registry->getPendingUpdateFunctions(); + if (!empty($missing_post_update_functions)) { + $requirements['update']['severity'] = REQUIREMENT_ERROR; + $requirements['update']['value'] = t('Out of date'); + $requirements['update']['description'] = t('Some modules have database schema updates to install. You should run the database update script immediately.', array('@update' => \Drupal::url('system.db_update'))); + } + // Verify that no entity updates are pending after running every DB update. if (!isset($requirements['update']['severity']) && \Drupal::entityDefinitionUpdateManager()->needsUpdates()) { $requirements['entity_update'] = array( diff --git a/core/modules/system/tests/modules/update_test_postupdate/update_test_postupdate.post_update.php b/core/modules/system/tests/modules/update_test_postupdate/update_test_postupdate.post_update.php index d67ea14..e914cac 100644 --- a/core/modules/system/tests/modules/update_test_postupdate/update_test_postupdate.post_update.php +++ b/core/modules/system/tests/modules/update_test_postupdate/update_test_postupdate.post_update.php @@ -7,6 +7,8 @@ function update_test_postupdate_post_update_first() { $execution = \Drupal::state()->get('post_update_test_execution', []); $execution[] = __FUNCTION__; \Drupal::state()->set('post_update_test_execution', $execution); + + return 'First update'; } /** @@ -16,6 +18,8 @@ function update_test_postupdate_post_update_second() { $execution = \Drupal::state()->get('post_update_test_execution', []); $execution[] = __FUNCTION__; \Drupal::state()->set('post_update_test_execution', $execution); + + return 'Second update'; } /** @@ -25,13 +29,17 @@ function update_test_postupdate_post_update_test1() { $execution = \Drupal::state()->get('post_update_test_execution', []); $execution[] = __FUNCTION__; \Drupal::state()->set('post_update_test_execution', $execution); + + return 'Test1 update'; } /** - * Test2 update. + * Test0 update. */ function update_test_postupdate_post_update_test0() { $execution = \Drupal::state()->get('post_update_test_execution', []); $execution[] = __FUNCTION__; \Drupal::state()->set('post_update_test_execution', $execution); + + return 'Test0 update'; } diff --git a/core/tests/Drupal/Tests/Core/Update/UpdateRegistryTest.php b/core/tests/Drupal/Tests/Core/Update/UpdateRegistryTest.php index d740580..c8b3f91 100644 --- a/core/tests/Drupal/Tests/Core/Update/UpdateRegistryTest.php +++ b/core/tests/Drupal/Tests/Core/Update/UpdateRegistryTest.php @@ -17,6 +17,8 @@ * @coversDefaultClass \Drupal\Core\Update\UpdateRegistry * @group Update * + * Note we load code, so we shoudl better run in isolation. + * * @runTestsInSeparateProcesses */ class UpdateRegistryTest extends UnitTestCase { @@ -203,9 +205,9 @@ public function testGetModuleUpdateFunctions() { } /** - * @covers ::registerInvokedUpdate + * @covers ::registerInvokedUpdates */ - public function testRegisterInvokedUpdateWithoutExistingUpdates() { + public function testRegisterInvokedUpdatesWithoutExistingUpdates() { $this->setupBasicModules(); $key_value = $this->prophesize(KeyValueStoreInterface::class); $key_value->get('existing_updates', [])->willReturn([]); @@ -216,13 +218,30 @@ public function testRegisterInvokedUpdateWithoutExistingUpdates() { 'module_a', 'module_b' ], $key_value, FALSE); - $update_registry->registerInvokedUpdate('module_a_post_update_a'); + $update_registry->registerInvokedUpdates(['module_a_post_update_a']); + } + + /** + * @covers ::registerInvokedUpdates + */ + public function testRegisterInvokedUpdatesWithMultiple() { + $this->setupBasicModules(); + $key_value = $this->prophesize(KeyValueStoreInterface::class); + $key_value->get('existing_updates', [])->willReturn([]); + $key_value->set('existing_updates', ['module_a_post_update_a', 'module_a_post_update_b'])->willReturn(NULL); + $key_value = $key_value->reveal(); + + $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [ + 'module_a', + 'module_b' + ], $key_value, FALSE); + $update_registry->registerInvokedUpdates(['module_a_post_update_a', 'module_a_post_update_b']); } /** - * @covers ::registerInvokedUpdate + * @covers ::registerInvokedUpdates */ - public function testRegisterInvokedUpdateWithExistingUpdates() { + public function testRegisterInvokedUpdatesWithExistingUpdates() { $this->setupBasicModules(); $key_value = $this->prophesize(KeyValueStoreInterface::class); $key_value->get('existing_updates', [])->willReturn(['module_a_post_update_b']); @@ -233,7 +252,25 @@ public function testRegisterInvokedUpdateWithExistingUpdates() { 'module_a', 'module_b' ], $key_value, FALSE); - $update_registry->registerInvokedUpdate('module_a_post_update_a'); + $update_registry->registerInvokedUpdates(['module_a_post_update_a']); + } + + /** + * @covers ::filterOutInvokedUpdatesByModule + */ + public function testFilterOutInvokedUpdatesByModule() { + $this->setupBasicModules(); + $key_value = $this->prophesize(KeyValueStoreInterface::class); + $key_value->get('existing_updates', [])->willReturn(['module_a_post_update_b', 'module_a_post_update_a', 'module_b_post_update_a']); + $key_value->set('existing_updates', ['module_b_post_update_a'])->willReturn(NULL); + $key_value = $key_value->reveal(); + + $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [ + 'module_a', + 'module_b' + ], $key_value, FALSE); + + $update_registry->filterOutInvokedUpdatesByModule('module_a'); } }