Issue #2538108 by dawehner, jhedstrom: Add hook_post_update_X() for data value updates to reliably run after data format updates
							parent
							
								
									56993d41d4
								
							
						
					
					
						commit
						7d16f1c6e6
					
				| 
						 | 
				
			
			@ -1518,6 +1518,12 @@ services:
 | 
			
		|||
      - { name: placeholder_strategy, priority: -1000 }
 | 
			
		||||
  email.validator:
 | 
			
		||||
    class: Egulias\EmailValidator\EmailValidator
 | 
			
		||||
  update.post_update_registry:
 | 
			
		||||
    class: Drupal\Core\Update\UpdateRegistry
 | 
			
		||||
    factory: ['@update.post_update_registry_factory', create]
 | 
			
		||||
  update.post_update_registry_factory:
 | 
			
		||||
    class: Drupal\Core\Update\UpdateRegistryFactory
 | 
			
		||||
    parent: container.trait
 | 
			
		||||
 | 
			
		||||
  response_filter.active_link:
 | 
			
		||||
    class: Drupal\Core\EventSubscriber\ActiveLinkResponseFilter
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -182,6 +182,7 @@ function update_do_one($module, $number, $dependency_map, &$context) {
 | 
			
		|||
    // @TODO We may want to do different error handling for different
 | 
			
		||||
    // exception types, but for now we'll just log the exception and
 | 
			
		||||
    // return the message for printing.
 | 
			
		||||
    // @see https://www.drupal.org/node/2564311
 | 
			
		||||
    catch (Exception $e) {
 | 
			
		||||
      watchdog_exception('update', $e);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -214,7 +215,66 @@ function update_do_one($module, $number, $dependency_map, &$context) {
 | 
			
		|||
    drupal_set_installed_schema_version($module, $number);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $context['message'] = 'Updating ' . Html::escape($module) . ' module';
 | 
			
		||||
  $context['message'] = t('Updating @module', ['@module' => $module]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Executes a single hook_post_update_NAME().
 | 
			
		||||
 *
 | 
			
		||||
 * @param string $function
 | 
			
		||||
 *   The function name, that should be executed.
 | 
			
		||||
 * @param array $context
 | 
			
		||||
 *   The batch context array.
 | 
			
		||||
 */
 | 
			
		||||
function update_invoke_post_update($function, &$context) {
 | 
			
		||||
  $ret = [];
 | 
			
		||||
 | 
			
		||||
  // If this update was aborted in a previous step, or has a dependency that was
 | 
			
		||||
  // aborted in a previous step, go no further.
 | 
			
		||||
  if (!empty($context['results']['#abort'])) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  list($module, $name) = explode('_post_update_', $function, 2);
 | 
			
		||||
  module_load_include('php', $module, $module . '.post_update');
 | 
			
		||||
  if (function_exists($function)) {
 | 
			
		||||
    try {
 | 
			
		||||
      $ret['results']['query'] = $function($context['sandbox']);
 | 
			
		||||
      $ret['results']['success'] = TRUE;
 | 
			
		||||
 | 
			
		||||
      \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 return the message
 | 
			
		||||
    // for printing.
 | 
			
		||||
    // @see https://www.drupal.org/node/2564311
 | 
			
		||||
    catch (Exception $e) {
 | 
			
		||||
      watchdog_exception('update', $e);
 | 
			
		||||
 | 
			
		||||
      $variables = Error::decodeException($e);
 | 
			
		||||
      unset($variables['backtrace']);
 | 
			
		||||
      $ret['#abort'] = [
 | 
			
		||||
        'success' => FALSE,
 | 
			
		||||
        'query' => t('%type: @message in %function (line %line of %file).', $variables),
 | 
			
		||||
      ];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (isset($context['sandbox']['#finished'])) {
 | 
			
		||||
    $context['finished'] = $context['sandbox']['#finished'];
 | 
			
		||||
    unset($context['sandbox']['#finished']);
 | 
			
		||||
  }
 | 
			
		||||
  if (!isset($context['results'][$module][$name])) {
 | 
			
		||||
    $context['results'][$module][$name] = array();
 | 
			
		||||
  }
 | 
			
		||||
  $context['results'][$module][$name] = array_merge($context['results'][$module][$name], $ret);
 | 
			
		||||
 | 
			
		||||
  if (!empty($ret['#abort'])) {
 | 
			
		||||
    // Record this function in the list of updates that were aborted.
 | 
			
		||||
    $context['results']['#abort'][] = $function;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $context['message'] = t('Post updating @module', ['@module' => $module]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -90,15 +90,30 @@ class ExtensionDiscovery {
 | 
			
		|||
   */
 | 
			
		||||
  protected $fileCache;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The site path.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $sitePath;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Constructs a new ExtensionDiscovery object.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $root
 | 
			
		||||
   *   The app root.
 | 
			
		||||
   * @param bool $use_file_cache
 | 
			
		||||
   *   Whether file cache should be used.
 | 
			
		||||
   * @param string[] $profile_directories
 | 
			
		||||
   *   The available profile directories
 | 
			
		||||
   * @param string $site_path
 | 
			
		||||
   *   The path to the site.
 | 
			
		||||
   */
 | 
			
		||||
  public function __construct($root) {
 | 
			
		||||
  public function __construct($root, $use_file_cache = TRUE, $profile_directories = NULL, $site_path = NULL) {
 | 
			
		||||
    $this->root = $root;
 | 
			
		||||
    $this->fileCache = FileCacheFactory::get('extension_discovery');
 | 
			
		||||
    $this->fileCache = $use_file_cache ? FileCacheFactory::get('extension_discovery') : NULL;
 | 
			
		||||
    $this->profileDirectories = $profile_directories;
 | 
			
		||||
    $this->sitePath = $site_path;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
| 
						 | 
				
			
			@ -172,7 +187,7 @@ class ExtensionDiscovery {
 | 
			
		|||
      $searchdirs[static::ORIGIN_SITE] = \Drupal::service('site.path');
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      $searchdirs[static::ORIGIN_SITE] = DrupalKernel::findSitePath(Request::createFromGlobals());
 | 
			
		||||
      $searchdirs[static::ORIGIN_SITE] = $this->sitePath ?: DrupalKernel::findSitePath(Request::createFromGlobals());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Unless an explicit value has been passed, manually check whether we are
 | 
			
		||||
| 
						 | 
				
			
			@ -180,7 +195,7 @@ class ExtensionDiscovery {
 | 
			
		|||
    // Test extensions can also be included for debugging purposes by setting a
 | 
			
		||||
    // variable in settings.php.
 | 
			
		||||
    if (!isset($include_tests)) {
 | 
			
		||||
      $include_tests = drupal_valid_test_ua() || Settings::get('extension_discovery_scan_tests');
 | 
			
		||||
      $include_tests = Settings::get('extension_discovery_scan_tests') || drupal_valid_test_ua();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $files = array();
 | 
			
		||||
| 
						 | 
				
			
			@ -427,7 +442,7 @@ class ExtensionDiscovery {
 | 
			
		|||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if ($cached_extension = $this->fileCache->get($fileinfo->getPathName())) {
 | 
			
		||||
      if ($this->fileCache && $cached_extension = $this->fileCache->get($fileinfo->getPathName())) {
 | 
			
		||||
        $files[$cached_extension->getType()][$key] = $cached_extension;
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			@ -467,7 +482,10 @@ class ExtensionDiscovery {
 | 
			
		|||
      $extension->origin = $dir;
 | 
			
		||||
 | 
			
		||||
      $files[$type][$key] = $extension;
 | 
			
		||||
      $this->fileCache->set($fileinfo->getPathName(), $extension);
 | 
			
		||||
 | 
			
		||||
      if ($this->fileCache) {
 | 
			
		||||
        $this->fileCache->set($fileinfo->getPathName(), $extension);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return $files;
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -260,6 +260,11 @@ class ModuleInstaller implements ModuleInstallerInterface {
 | 
			
		|||
        }
 | 
			
		||||
        drupal_set_installed_schema_version($module, $version);
 | 
			
		||||
 | 
			
		||||
        // Ensure that all post_update functions are registered already.
 | 
			
		||||
        /** @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;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -445,6 +450,10 @@ class ModuleInstaller implements ModuleInstallerInterface {
 | 
			
		|||
 | 
			
		||||
      $schema_store = \Drupal::keyValue('system.schema');
 | 
			
		||||
      $schema_store->delete($module);
 | 
			
		||||
 | 
			
		||||
      /** @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);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -659,6 +659,71 @@ function hook_update_N(&$sandbox) {
 | 
			
		|||
  return t('All foo bars were updated with the new suffix');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Executes an update which is intended to update data, like entities.
 | 
			
		||||
 *
 | 
			
		||||
 * These implementations have to be placed in a MODULE.post_update.php file.
 | 
			
		||||
 *
 | 
			
		||||
 * These updates are executed after all hook_update_N() implementations. At this
 | 
			
		||||
 * stage Drupal is already fully repaired so you can use any API as you wish.
 | 
			
		||||
 *
 | 
			
		||||
 * NAME can be arbitrary machine names. In contrast to hook_update_N() the order
 | 
			
		||||
 * of functions in the file is the only thing which ensures the execution order
 | 
			
		||||
 * of those functions.
 | 
			
		||||
 *
 | 
			
		||||
 * Drupal also ensures to not execute the same hook_post_update_NAME() function
 | 
			
		||||
 * twice.
 | 
			
		||||
 *
 | 
			
		||||
 * @param array $sandbox
 | 
			
		||||
 *   Stores information for batch updates. See above for more information.
 | 
			
		||||
 *
 | 
			
		||||
 * @throws \Drupal\Core\Utility\UpdateException|PDOException
 | 
			
		||||
 *   In case of error, update hooks should throw an instance of
 | 
			
		||||
 *   \Drupal\Core\Utility\UpdateException with a meaningful message for the
 | 
			
		||||
 *   user. If a database query fails for whatever reason, it will throw a
 | 
			
		||||
 *   PDOException.
 | 
			
		||||
 *
 | 
			
		||||
 * @return string|null
 | 
			
		||||
 *   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.
 | 
			
		||||
 *
 | 
			
		||||
 * @ingroup update_api
 | 
			
		||||
 *
 | 
			
		||||
 * @see hook_update_N()
 | 
			
		||||
 */
 | 
			
		||||
function hook_post_update_NAME(&$sandbox) {
 | 
			
		||||
  // Example of updating some content.
 | 
			
		||||
  $node = \Drupal\node\Entity\Node::load(123);
 | 
			
		||||
  $node->setTitle('foo');
 | 
			
		||||
  $node->save();
 | 
			
		||||
 | 
			
		||||
  $result = t('Node %nid saved', ['%nid' => $node->id()]);
 | 
			
		||||
 | 
			
		||||
  // Example of disabling blocks with missing condition contexts. Note: The
 | 
			
		||||
  // block itself is in a state which is valid at that point.
 | 
			
		||||
  // @see block_update_8001()
 | 
			
		||||
  // @see block_post_update_disable_blocks_with_missing_contexts()
 | 
			
		||||
  $block_update_8001 = \Drupal::keyValue('update_backup')->get('block_update_8001', []);
 | 
			
		||||
 | 
			
		||||
  $block_ids = array_keys($block_update_8001);
 | 
			
		||||
  $block_storage = \Drupal::entityManager()->getStorage('block');
 | 
			
		||||
  $blocks = $block_storage->loadMultiple($block_ids);
 | 
			
		||||
  /** @var $blocks \Drupal\block\BlockInterface[] */
 | 
			
		||||
  foreach ($blocks as $block) {
 | 
			
		||||
    // This block has had conditions removed due to an inability to resolve
 | 
			
		||||
    // contexts in block_update_8001() so disable it.
 | 
			
		||||
 | 
			
		||||
    // Disable currently enabled blocks.
 | 
			
		||||
    if ($block_update_8001[$block->id()]['status']) {
 | 
			
		||||
      $block->setStatus(FALSE);
 | 
			
		||||
      $block->save();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return $result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Return an array of information about module update dependencies.
 | 
			
		||||
 *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,267 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @file
 | 
			
		||||
 * Contains \Drupal\Core\Update\UpdateRegistry.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Core\Update;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Extension\Extension;
 | 
			
		||||
use Drupal\Core\Extension\ExtensionDiscovery;
 | 
			
		||||
use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Provides all and missing update implementations.
 | 
			
		||||
 *
 | 
			
		||||
 * 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 {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The used update name.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $updateType = 'post_update';
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The app root.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $root;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The filename of the log file.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $logFilename;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @var string[]
 | 
			
		||||
   */
 | 
			
		||||
  protected $enabledModules;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The key value storage.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
 | 
			
		||||
   */
 | 
			
		||||
  protected $keyValue;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Should we respect update functions in tests.
 | 
			
		||||
   *
 | 
			
		||||
   * @var bool|null
 | 
			
		||||
   */
 | 
			
		||||
  protected $includeTests = NULL;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The site path.
 | 
			
		||||
   *
 | 
			
		||||
   * @var string
 | 
			
		||||
   */
 | 
			
		||||
  protected $sitePath;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Constructs a new UpdateRegistry.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $root
 | 
			
		||||
   *   The app root.
 | 
			
		||||
   * @param string $site_path
 | 
			
		||||
   *   The site path.
 | 
			
		||||
   * @param string[] $enabled_modules
 | 
			
		||||
   *   A list of enabled modules.
 | 
			
		||||
   * @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $key_value
 | 
			
		||||
   *   The key value store.
 | 
			
		||||
   * @param bool|NULL $include_tests
 | 
			
		||||
   *   (optional) A flag whether to include tests in the scanning of modules.
 | 
			
		||||
   */
 | 
			
		||||
  public function __construct($root, $site_path, array $enabled_modules, KeyValueStoreInterface $key_value, $include_tests = NULL) {
 | 
			
		||||
    $this->root = $root;
 | 
			
		||||
    $this->sitePath = $site_path;
 | 
			
		||||
    $this->enabledModules = $enabled_modules;
 | 
			
		||||
    $this->keyValue = $key_value;
 | 
			
		||||
    $this->includeTests = $include_tests;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Gets all available update functions.
 | 
			
		||||
   *
 | 
			
		||||
   * @return callable[]
 | 
			
		||||
   *   A list of update functions.
 | 
			
		||||
   */
 | 
			
		||||
  protected function getAvailableUpdateFunctions() {
 | 
			
		||||
    $regexp = '/^(?<module>.+)_' . $this->updateType . '_(?<name>.+)$/';
 | 
			
		||||
    $functions = get_defined_functions();
 | 
			
		||||
 | 
			
		||||
    $updates = [];
 | 
			
		||||
    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)) {
 | 
			
		||||
        if (in_array($matches['module'], $this->enabledModules)) {
 | 
			
		||||
          $updates[] = $matches['module'] . '_' . $this->updateType . '_' . $matches['name'];
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return $updates;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Find all update functions that haven't been executed.
 | 
			
		||||
   *
 | 
			
		||||
   * @return callable[]
 | 
			
		||||
   *   A list of update functions.
 | 
			
		||||
   */
 | 
			
		||||
  public function getPendingUpdateFunctions() {
 | 
			
		||||
    // We need a) the list of active modules (we get that from the config
 | 
			
		||||
    // bootstrap factory) and b) the path to the modules, we use the extension
 | 
			
		||||
    // discovery for that.
 | 
			
		||||
 | 
			
		||||
    $this->scanExtensionsAndLoadUpdateFiles();
 | 
			
		||||
 | 
			
		||||
    // First figure out which hook_{$this->updateType}_NAME got executed
 | 
			
		||||
    // already.
 | 
			
		||||
    $existing_update_functions = $this->keyValue->get('existing_updates', []);
 | 
			
		||||
 | 
			
		||||
    $available_update_functions = $this->getAvailableUpdateFunctions();
 | 
			
		||||
    $not_executed_update_functions = array_diff($available_update_functions, $existing_update_functions);
 | 
			
		||||
 | 
			
		||||
    return $not_executed_update_functions;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Loads all update files for a given list of extension.
 | 
			
		||||
   *
 | 
			
		||||
   * @param \Drupal\Core\Extension\Extension[] $module_extensions
 | 
			
		||||
   *   The extensions used for loading.
 | 
			
		||||
   */
 | 
			
		||||
  protected function loadUpdateFiles(array $module_extensions) {
 | 
			
		||||
    // Load all the {$this->updateType}.php files.
 | 
			
		||||
    foreach ($this->enabledModules as $module) {
 | 
			
		||||
      if (isset($module_extensions[$module])) {
 | 
			
		||||
        $this->loadUpdateFile($module_extensions[$module]);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 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->updateType}.php";
 | 
			
		||||
    if (file_exists($filename)) {
 | 
			
		||||
      include_once $filename;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns a list of all the pending updates.
 | 
			
		||||
   *
 | 
			
		||||
   * @return array[]
 | 
			
		||||
   *   An associative array keyed by module name which contains all information
 | 
			
		||||
   *   about database updates that need to be run, and any updates that are not
 | 
			
		||||
   *   going to proceed due to missing requirements.
 | 
			
		||||
   *
 | 
			
		||||
   *   The subarray for each module can contain the following keys:
 | 
			
		||||
   *   - start: The starting update that is to be processed. If this does not
 | 
			
		||||
   *       exist then do not process any updates for this module as there are
 | 
			
		||||
   *       other requirements that need to be resolved.
 | 
			
		||||
   *   - pending: An array of all the pending updates for the module including
 | 
			
		||||
   *       the description from source code comment for each update function.
 | 
			
		||||
   *       This array is keyed by the update name.
 | 
			
		||||
   */
 | 
			
		||||
  public function getPendingUpdateInformation() {
 | 
			
		||||
    $functions = $this->getPendingUpdateFunctions();
 | 
			
		||||
 | 
			
		||||
    $ret = [];
 | 
			
		||||
    foreach ($functions as $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()), ' ');
 | 
			
		||||
      $ret[$module]['pending'][$update] = $description;
 | 
			
		||||
      if (!isset($ret[$module]['start'])) {
 | 
			
		||||
        $ret[$module]['start'] = $update;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return $ret;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Registers that update fucntions got executed.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string[] $function_names
 | 
			
		||||
   *   The executed update functions.
 | 
			
		||||
   *
 | 
			
		||||
   * @return $this
 | 
			
		||||
   */
 | 
			
		||||
  public function registerInvokedUpdates(array $function_names) {
 | 
			
		||||
    $executed_updates = $this->keyValue->get('existing_updates', []);
 | 
			
		||||
    $executed_updates = array_merge($executed_updates, $function_names);
 | 
			
		||||
    $this->keyValue->set('existing_updates', $executed_updates);
 | 
			
		||||
 | 
			
		||||
    return $this;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns all available updates for a given module.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $module_name
 | 
			
		||||
   *   The module name.
 | 
			
		||||
   *
 | 
			
		||||
   * @return callable[]
 | 
			
		||||
   *   A list of update functions.
 | 
			
		||||
   */
 | 
			
		||||
  public function getModuleUpdateFunctions($module_name) {
 | 
			
		||||
    $this->scanExtensionsAndLoadUpdateFiles();
 | 
			
		||||
    $all_functions = $this->getAvailableUpdateFunctions();
 | 
			
		||||
 | 
			
		||||
    return array_filter($all_functions, function($function_name) use ($module_name) {
 | 
			
		||||
      list($function_module_name, ) = explode("_{$this->updateType}_", $function_name);
 | 
			
		||||
      return $function_module_name === $module_name;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Scans all module + profile extensions and load the update files.
 | 
			
		||||
   */
 | 
			
		||||
  protected function scanExtensionsAndLoadUpdateFiles() {
 | 
			
		||||
    // Scan the module list.
 | 
			
		||||
    $extension_discovery = new ExtensionDiscovery($this->root, FALSE, [], $this->sitePath);
 | 
			
		||||
    $module_extensions = $extension_discovery->scan('module');
 | 
			
		||||
 | 
			
		||||
    $profile_extensions = $extension_discovery->scan('profile');
 | 
			
		||||
    $extensions = array_merge($module_extensions, $profile_extensions);
 | 
			
		||||
 | 
			
		||||
    $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));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,30 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @file
 | 
			
		||||
 * Contains \Drupal\Core\Update\UpdateRegistryFactory.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Core\Update;
 | 
			
		||||
 | 
			
		||||
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
 | 
			
		||||
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Service factory for the update registry.
 | 
			
		||||
 */
 | 
			
		||||
class UpdateRegistryFactory implements ContainerAwareInterface {
 | 
			
		||||
 | 
			
		||||
  use ContainerAwareTrait;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates a new UpdateRegistry instance.
 | 
			
		||||
   *
 | 
			
		||||
   * @return \Drupal\Core\Update\UpdateRegistry
 | 
			
		||||
   *   The update registry instance.
 | 
			
		||||
   */
 | 
			
		||||
  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'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -45,8 +45,9 @@ function block_update_8001() {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  // Contributed modules should leverage hook_update_dependencies() in order to
 | 
			
		||||
  // be executed before block_update_8002(), so they can update their context
 | 
			
		||||
  // mappings, if wanted.
 | 
			
		||||
  // be executed after block_update_8001(). The blocks are then disabled if the
 | 
			
		||||
  // contexts are still missing via
 | 
			
		||||
  // block_post_update_disable_blocks_with_missing_contexts().
 | 
			
		||||
  $config_factory = \Drupal::configFactory();
 | 
			
		||||
  $backup_values = $update_backup = [];
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -95,50 +96,10 @@ function block_update_8001() {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Disable all blocks with missing context IDs in block_update_8001().
 | 
			
		||||
 * Placeholder for the previous 8002 update.
 | 
			
		||||
 */
 | 
			
		||||
function block_update_8002() {
 | 
			
		||||
  $block_update_8001 = \Drupal::keyValue('update_backup')->get('block_update_8001', []);
 | 
			
		||||
 | 
			
		||||
  $block_ids = array_keys($block_update_8001);
 | 
			
		||||
  $config_factory = \Drupal::configFactory();
 | 
			
		||||
  /** @var \Drupal\Core\Config\Config[] $blocks */
 | 
			
		||||
  $blocks = [];
 | 
			
		||||
  foreach ($block_ids as $block_id) {
 | 
			
		||||
    $blocks[$block_id] = $block = $config_factory->getEditable('block.block.' . $block_id);
 | 
			
		||||
    // This block will have an invalid context mapping service and must be
 | 
			
		||||
    // disabled in order to prevent information disclosure.
 | 
			
		||||
 | 
			
		||||
    // Disable currently enabled blocks.
 | 
			
		||||
    if ($block_update_8001[$block_id]['status']) {
 | 
			
		||||
      $block->set('status', FALSE);
 | 
			
		||||
      $block->save(TRUE);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Provides a list of plugin labels, keyed by plugin ID.
 | 
			
		||||
  $condition_plugin_id_label_map = array_column(\Drupal::service('plugin.manager.condition')->getDefinitions(), 'label', 'id');
 | 
			
		||||
 | 
			
		||||
  // Override with the UI labels we are aware of. Sadly they are not machine
 | 
			
		||||
  // accessible, see
 | 
			
		||||
  // \Drupal\node\Plugin\Condition\NodeType::buildConfigurationForm().
 | 
			
		||||
  $condition_plugin_id_label_map['node_type'] = t('Content types');
 | 
			
		||||
  $condition_plugin_id_label_map['request_path'] = t('Pages');
 | 
			
		||||
  $condition_plugin_id_label_map['user_role'] = t('Roles');
 | 
			
		||||
 | 
			
		||||
  if (count($block_ids) > 0) {
 | 
			
		||||
    $message = t('Encountered an unknown context mapping key coming probably from a contributed or custom module: One or more mappings could not be updated. Please manually review your visibility settings for the following blocks, which are disabled now:');
 | 
			
		||||
    $message .= '<ul>';
 | 
			
		||||
    foreach ($blocks as $disabled_block_id => $disabled_block) {
 | 
			
		||||
      $message .= '<li>' . t('@label (Visibility: @plugin_ids)', array(
 | 
			
		||||
            '@label' => $disabled_block->get('settings.label'),
 | 
			
		||||
            '@plugin_ids' => implode(', ', array_intersect_key($condition_plugin_id_label_map, array_flip(array_keys($block_update_8001[$disabled_block_id]['missing_context_ids']))))
 | 
			
		||||
          )) . '</li>';
 | 
			
		||||
    }
 | 
			
		||||
    $message .= '</ul>';
 | 
			
		||||
 | 
			
		||||
    return $message;
 | 
			
		||||
  }
 | 
			
		||||
  \Drupal::state()->set('block_update_8002_placeholder', TRUE);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,79 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @file
 | 
			
		||||
 * Post update functions for Block.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @addtogroup updates-8.0.0-beta
 | 
			
		||||
 * @{
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Disable all blocks with missing context IDs in block_update_8001().
 | 
			
		||||
 */
 | 
			
		||||
function block_post_update_disable_blocks_with_missing_contexts() {
 | 
			
		||||
  // Don't execute the function if block_update_8002() got executed already,
 | 
			
		||||
  // which used to do the same. Note: Its okay to check here, because
 | 
			
		||||
  // update_do_one() does not update the installed schema version until the
 | 
			
		||||
  // batch is finished.
 | 
			
		||||
  $module_schema = drupal_get_installed_schema_version('block');
 | 
			
		||||
 | 
			
		||||
  // 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;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Cleanup the state entry as its no longer needed.
 | 
			
		||||
  \Drupal::state()->delete('block_update_8002');
 | 
			
		||||
 | 
			
		||||
  $block_update_8001 = \Drupal::keyValue('update_backup')->get('block_update_8001', []);
 | 
			
		||||
 | 
			
		||||
  $block_ids = array_keys($block_update_8001);
 | 
			
		||||
  $block_storage = \Drupal::entityManager()->getStorage('block');
 | 
			
		||||
  $blocks = $block_storage->loadMultiple($block_ids);
 | 
			
		||||
  /** @var $blocks \Drupal\block\BlockInterface[] */
 | 
			
		||||
  foreach ($blocks as $block) {
 | 
			
		||||
    // This block has had conditions removed due to an inability to resolve
 | 
			
		||||
    // contexts in block_update_8001() so disable it.
 | 
			
		||||
 | 
			
		||||
    // Disable currently enabled blocks.
 | 
			
		||||
    if ($block_update_8001[$block->id()]['status']) {
 | 
			
		||||
      $block->setStatus(FALSE);
 | 
			
		||||
      $block->save();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Provides a list of plugin labels, keyed by plugin ID.
 | 
			
		||||
  $condition_plugin_id_label_map = array_column(\Drupal::service('plugin.manager.condition')->getDefinitions(), 'label', 'id');
 | 
			
		||||
 | 
			
		||||
  // Override with the UI labels we are aware of. Sadly they are not machine
 | 
			
		||||
  // accessible, see
 | 
			
		||||
  // \Drupal\node\Plugin\Condition\NodeType::buildConfigurationForm().
 | 
			
		||||
  $condition_plugin_id_label_map['node_type'] = t('Content types');
 | 
			
		||||
  $condition_plugin_id_label_map['request_path'] = t('Pages');
 | 
			
		||||
  $condition_plugin_id_label_map['user_role'] = t('Roles');
 | 
			
		||||
 | 
			
		||||
  if (count($block_ids) > 0) {
 | 
			
		||||
    $message = t('Encountered an unknown context mapping key coming probably from a contributed or custom module: One or more mappings could not be updated. Please manually review your visibility settings for the following blocks, which are disabled now:');
 | 
			
		||||
    $message .= '<ul>';
 | 
			
		||||
    foreach ($blocks as $disabled_block_id => $disabled_block) {
 | 
			
		||||
      $message .= '<li>' . t('@label (Visibility: @plugin_ids)', array(
 | 
			
		||||
          '@label' => $disabled_block->get('settings')['label'],
 | 
			
		||||
          '@plugin_ids' => implode(', ', array_intersect_key($condition_plugin_id_label_map, array_flip(array_keys($block_update_8001[$disabled_block_id]['missing_context_ids']))))
 | 
			
		||||
        )) . '</li>';
 | 
			
		||||
    }
 | 
			
		||||
    $message .= '</ul>';
 | 
			
		||||
 | 
			
		||||
    return $message;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @} End of "addtogroup updates-8.0.0-beta".
 | 
			
		||||
 */
 | 
			
		||||
| 
						 | 
				
			
			@ -15,6 +15,7 @@ use Drupal\Core\Render\BareHtmlPageRendererInterface;
 | 
			
		|||
use Drupal\Core\Session\AccountInterface;
 | 
			
		||||
use Drupal\Core\Site\Settings;
 | 
			
		||||
use Drupal\Core\State\StateInterface;
 | 
			
		||||
use Drupal\Core\Update\UpdateRegistry;
 | 
			
		||||
use Drupal\Core\Url;
 | 
			
		||||
use Symfony\Component\DependencyInjection\ContainerInterface;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Response;
 | 
			
		||||
| 
						 | 
				
			
			@ -74,6 +75,13 @@ class DbUpdateController extends ControllerBase {
 | 
			
		|||
   */
 | 
			
		||||
  protected $root;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * The post update registry.
 | 
			
		||||
   *
 | 
			
		||||
   * @var \Drupal\Core\Update\UpdateRegistry
 | 
			
		||||
   */
 | 
			
		||||
  protected $postUpdateRegistry;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Constructs a new UpdateController.
 | 
			
		||||
   *
 | 
			
		||||
| 
						 | 
				
			
			@ -91,8 +99,10 @@ class DbUpdateController extends ControllerBase {
 | 
			
		|||
   *   The current user.
 | 
			
		||||
   * @param \Drupal\Core\Render\BareHtmlPageRendererInterface $bare_html_page_renderer
 | 
			
		||||
   *   The bare HTML page renderer.
 | 
			
		||||
   * @param \Drupal\Core\Update\UpdateRegistry $post_update_registry
 | 
			
		||||
   *   The post update registry.
 | 
			
		||||
   */
 | 
			
		||||
  public function __construct($root, KeyValueExpirableFactoryInterface $key_value_expirable_factory, CacheBackendInterface $cache, StateInterface $state, ModuleHandlerInterface $module_handler, AccountInterface $account, BareHtmlPageRendererInterface $bare_html_page_renderer) {
 | 
			
		||||
  public function __construct($root, KeyValueExpirableFactoryInterface $key_value_expirable_factory, CacheBackendInterface $cache, StateInterface $state, ModuleHandlerInterface $module_handler, AccountInterface $account, BareHtmlPageRendererInterface $bare_html_page_renderer, UpdateRegistry $post_update_registry) {
 | 
			
		||||
    $this->root = $root;
 | 
			
		||||
    $this->keyValueExpirableFactory = $key_value_expirable_factory;
 | 
			
		||||
    $this->cache = $cache;
 | 
			
		||||
| 
						 | 
				
			
			@ -100,6 +110,7 @@ class DbUpdateController extends ControllerBase {
 | 
			
		|||
    $this->moduleHandler = $module_handler;
 | 
			
		||||
    $this->account = $account;
 | 
			
		||||
    $this->bareHtmlPageRenderer = $bare_html_page_renderer;
 | 
			
		||||
    $this->postUpdateRegistry = $post_update_registry;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
| 
						 | 
				
			
			@ -113,7 +124,8 @@ class DbUpdateController extends ControllerBase {
 | 
			
		|||
      $container->get('state'),
 | 
			
		||||
      $container->get('module_handler'),
 | 
			
		||||
      $container->get('current_user'),
 | 
			
		||||
      $container->get('bare_html_page_renderer')
 | 
			
		||||
      $container->get('bare_html_page_renderer'),
 | 
			
		||||
      $container->get('update.post_update_registry')
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -257,35 +269,52 @@ class DbUpdateController extends ControllerBase {
 | 
			
		|||
    // Ensure system.module's updates appear first.
 | 
			
		||||
    $build['start']['system'] = array();
 | 
			
		||||
 | 
			
		||||
    $updates = update_get_update_list();
 | 
			
		||||
    $starting_updates = array();
 | 
			
		||||
    $incompatible_updates_exist = FALSE;
 | 
			
		||||
    foreach ($updates as $module => $update) {
 | 
			
		||||
      if (!isset($update['start'])) {
 | 
			
		||||
        $build['start'][$module] = array(
 | 
			
		||||
          '#type' => 'item',
 | 
			
		||||
          '#title' => $module . ' module',
 | 
			
		||||
          '#markup'  => $update['warning'],
 | 
			
		||||
          '#prefix' => '<div class="messages messages--warning">',
 | 
			
		||||
          '#suffix' => '</div>',
 | 
			
		||||
        );
 | 
			
		||||
        $incompatible_updates_exist = TRUE;
 | 
			
		||||
        continue;
 | 
			
		||||
    $updates_per_module = [];
 | 
			
		||||
    foreach (['update', 'post_update'] as $update_type) {
 | 
			
		||||
      switch ($update_type) {
 | 
			
		||||
        case 'update':
 | 
			
		||||
          $updates = update_get_update_list();
 | 
			
		||||
          break;
 | 
			
		||||
        case 'post_update':
 | 
			
		||||
          $updates = $this->postUpdateRegistry->getPendingUpdateInformation();
 | 
			
		||||
          break;
 | 
			
		||||
      }
 | 
			
		||||
      if (!empty($update['pending'])) {
 | 
			
		||||
        $starting_updates[$module] = $update['start'];
 | 
			
		||||
        $build['start'][$module] = array(
 | 
			
		||||
          '#type' => 'hidden',
 | 
			
		||||
          '#value' => $update['start'],
 | 
			
		||||
        );
 | 
			
		||||
        $build['start'][$module . '_updates'] = array(
 | 
			
		||||
          '#theme' => 'item_list',
 | 
			
		||||
          '#items' => $update['pending'],
 | 
			
		||||
          '#title' => $module . ' module',
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      if (isset($update['pending'])) {
 | 
			
		||||
        $count = $count + count($update['pending']);
 | 
			
		||||
      foreach ($updates as $module => $update) {
 | 
			
		||||
        if (!isset($update['start'])) {
 | 
			
		||||
          $build['start'][$module] = array(
 | 
			
		||||
            '#type' => 'item',
 | 
			
		||||
            '#title' => $module . ' module',
 | 
			
		||||
            '#markup' => $update['warning'],
 | 
			
		||||
            '#prefix' => '<div class="messages messages--warning">',
 | 
			
		||||
            '#suffix' => '</div>',
 | 
			
		||||
          );
 | 
			
		||||
          $incompatible_updates_exist = TRUE;
 | 
			
		||||
          continue;
 | 
			
		||||
        }
 | 
			
		||||
        if (!empty($update['pending'])) {
 | 
			
		||||
          $updates_per_module += [$module => []];
 | 
			
		||||
          $updates_per_module[$module] = array_merge($updates_per_module[$module], $update['pending']);
 | 
			
		||||
          $build['start'][$module] = array(
 | 
			
		||||
            '#type' => 'hidden',
 | 
			
		||||
            '#value' => $update['start'],
 | 
			
		||||
          );
 | 
			
		||||
          // Store the previous items in order to merge normal updates and
 | 
			
		||||
          // post_update functions together.
 | 
			
		||||
          $build['start'][$module] = array(
 | 
			
		||||
            '#theme' => 'item_list',
 | 
			
		||||
            '#items' => $updates_per_module[$module],
 | 
			
		||||
            '#title' => $module . ' module',
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          if ($update_type === 'update') {
 | 
			
		||||
            $starting_updates[$module] = $update['start'];
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        if (isset($update['pending'])) {
 | 
			
		||||
          $count = $count + count($update['pending']);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -415,7 +444,7 @@ class DbUpdateController extends ControllerBase {
 | 
			
		|||
        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.
 | 
			
		||||
| 
						 | 
				
			
			@ -439,10 +468,16 @@ class DbUpdateController extends ControllerBase {
 | 
			
		|||
 | 
			
		||||
            if ($messages) {
 | 
			
		||||
              $module_has_message = TRUE;
 | 
			
		||||
              if (is_numeric($name)) {
 | 
			
		||||
                $title = $this->t('Update #@count', ['@count' => $name]);
 | 
			
		||||
              }
 | 
			
		||||
              else {
 | 
			
		||||
                $title = $this->t('Update @name', ['@name' => trim($name, '_')]);
 | 
			
		||||
              }
 | 
			
		||||
              $info_messages[] = array(
 | 
			
		||||
                '#theme' => 'item_list',
 | 
			
		||||
                '#items' => $messages,
 | 
			
		||||
                '#title' => $this->t('Update #@count', array('@count' => $number)),
 | 
			
		||||
                '#title' => $title,
 | 
			
		||||
              );
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
| 
						 | 
				
			
			@ -575,6 +610,17 @@ class DbUpdateController extends ControllerBase {
 | 
			
		|||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $post_updates = $this->postUpdateRegistry->getPendingUpdateFunctions();
 | 
			
		||||
 | 
			
		||||
    if ($post_updates) {
 | 
			
		||||
      // Now we rebuild all caches and after that execute the hook_post_update()
 | 
			
		||||
      // functions.
 | 
			
		||||
      $operations[] = ['drupal_flush_all_caches', []];
 | 
			
		||||
      foreach ($post_updates as $function) {
 | 
			
		||||
        $operations[] = ['update_invoke_post_update', [$function]];
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $batch['operations'] = $operations;
 | 
			
		||||
    $batch += array(
 | 
			
		||||
      'title' => $this->t('Updating'),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -53,6 +53,21 @@ class InstallTest extends WebTestBase {
 | 
			
		|||
    $this->assertTrue($version > 0, 'System module version is > 0.');
 | 
			
		||||
    $version = drupal_get_installed_schema_version('user', TRUE);
 | 
			
		||||
    $this->assertTrue($version > 0, 'User module version is > 0.');
 | 
			
		||||
 | 
			
		||||
    $post_update_key_value = \Drupal::keyValue('post_update');
 | 
			
		||||
    $existing_updates = $post_update_key_value->get('existing_updates', []);
 | 
			
		||||
    $this->assertTrue(in_array('module_test_post_update_test', $existing_updates));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Ensures that post update functions are removed on uninstall.
 | 
			
		||||
   */
 | 
			
		||||
  public function testUninstallPostUpdateFunctions() {
 | 
			
		||||
    \Drupal::service('module_installer')->uninstall(['module_test']);
 | 
			
		||||
 | 
			
		||||
    $post_update_key_value = \Drupal::keyValue('post_update');
 | 
			
		||||
    $existing_updates = $post_update_key_value->get('existing_updates', []);
 | 
			
		||||
    $this->assertFalse(in_array('module_test_post_update_test', $existing_updates));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,7 +16,10 @@ use Drupal\Core\Logger\RfcLogLevel;
 | 
			
		|||
 */
 | 
			
		||||
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 @@ class InstallUninstallTest extends ModuleTestBase {
 | 
			
		|||
        $this->assertModuleTablesExist($module_to_install);
 | 
			
		||||
        $this->assertModuleConfig($module_to_install);
 | 
			
		||||
        $this->assertLogMessage('system', "%module module installed.", array('%module' => $module_to_install), RfcLogLevel::INFO);
 | 
			
		||||
        $this->assertInstallModuleUpdates($module_to_install);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Uninstall the original module, and check appropriate
 | 
			
		||||
| 
						 | 
				
			
			@ -188,6 +192,59 @@ class InstallUninstallTest extends ModuleTestBase {
 | 
			
		|||
    $this->assertModuleTablesDoNotExist($module);
 | 
			
		||||
    // Check that the module's config files no longer exist.
 | 
			
		||||
    $this->assertNoModuleConfig($module);
 | 
			
		||||
    $this->assertUninstallModuleUpdates($module);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Asserts the module post update functions after install.
 | 
			
		||||
   *
 | 
			
		||||
   * @param string $module
 | 
			
		||||
   *   The module that got installed.
 | 
			
		||||
   */
 | 
			
		||||
  protected function assertInstallModuleUpdates($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();
 | 
			
		||||
    $empty_result = TRUE;
 | 
			
		||||
    foreach ($all_update_functions as $function) {
 | 
			
		||||
      list($function_module, ) = explode('_post_update_', $function);
 | 
			
		||||
      if ($module === $function_module) {
 | 
			
		||||
        $empty_result = FALSE;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    $this->assertTrue($empty_result, 'Ensures that no pending post update functions are available.');
 | 
			
		||||
 | 
			
		||||
    $existing_updates = \Drupal::keyValue('post_update')->get('existing_updates', []);
 | 
			
		||||
    switch ($module) {
 | 
			
		||||
      case 'block':
 | 
			
		||||
        $this->assertFalse(array_diff(['block_post_update_disable_blocks_with_missing_contexts'], $existing_updates));
 | 
			
		||||
        break;
 | 
			
		||||
      case 'update_test_postupdate':
 | 
			
		||||
        $this->assertFalse(array_diff(['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));
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Asserts 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();
 | 
			
		||||
 | 
			
		||||
    switch ($module) {
 | 
			
		||||
      case 'block':
 | 
			
		||||
        $this->assertFalse(array_intersect(['block_post_update_disable_blocks_with_missing_contexts'], $all_update_functions), 'Asserts that no pending post update functions are available.');
 | 
			
		||||
 | 
			
		||||
        $existing_updates = \Drupal::keyValue('post_update')->get('existing_updates', []);
 | 
			
		||||
        $this->assertFalse(array_intersect(['block_post_update_disable_blocks_with_missing_contexts'], $existing_updates), 'Asserts that no post update functions are stored in keyvalue store.');
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,6 +18,11 @@ use Drupal\system\SystemRequirements;
 | 
			
		|||
 */
 | 
			
		||||
class StatusTest extends WebTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  public static $modules = ['update_test_postupdate'];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
| 
						 | 
				
			
			@ -57,6 +62,23 @@ class StatusTest extends WebTestBase {
 | 
			
		|||
      $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 executed.
 | 
			
		||||
    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.');
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -123,6 +123,13 @@ abstract class UpdatePathTestBase extends WebTestBase {
 | 
			
		|||
   */
 | 
			
		||||
  protected $strictConfigSchema = FALSE;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Fail the test if there are failed updates.
 | 
			
		||||
   *
 | 
			
		||||
   * @var bool
 | 
			
		||||
   */
 | 
			
		||||
  protected $checkFailedUpdates = TRUE;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Constructs an UpdatePathTestCase object.
 | 
			
		||||
   *
 | 
			
		||||
| 
						 | 
				
			
			@ -246,7 +253,9 @@ abstract class UpdatePathTestBase extends WebTestBase {
 | 
			
		|||
    $this->clickLink(t('Apply pending updates'));
 | 
			
		||||
 | 
			
		||||
    // Ensure there are no failed updates.
 | 
			
		||||
    $this->assertNoRaw('<strong>' . t('Failed:') . '</strong>');
 | 
			
		||||
    if ($this->checkFailedUpdates) {
 | 
			
		||||
      $this->assertNoRaw('<strong>' . t('Failed:') . '</strong>');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // The config schema can be incorrect while the update functions are being
 | 
			
		||||
    // executed. But once the update has been completed, it needs to be valid
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,53 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @file
 | 
			
		||||
 * Contains \Drupal\system\Tests\Update\UpdatePostUpdateFailingTest.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Drupal\system\Tests\Update;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests hook_post_update() when there are failing update hooks.
 | 
			
		||||
 *
 | 
			
		||||
 * @group Update
 | 
			
		||||
 */
 | 
			
		||||
class UpdatePostUpdateFailingTest extends UpdatePathTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setDatabaseDumpFiles() {
 | 
			
		||||
    $this->databaseDumpFiles = [
 | 
			
		||||
      __DIR__ . '/../../../tests/fixtures/update/drupal-8.bare.standard.php.gz',
 | 
			
		||||
      __DIR__ . '/../../../tests/fixtures/update/drupal-8.update-test-postupdate-failing-enabled.php',
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests hook_post_update_NAME().
 | 
			
		||||
   */
 | 
			
		||||
  public function testPostUpdate() {
 | 
			
		||||
    // There are expected to be failed updates.
 | 
			
		||||
    $this->checkFailedUpdates = FALSE;
 | 
			
		||||
 | 
			
		||||
    $this->runUpdates();
 | 
			
		||||
 | 
			
		||||
    // There should be no post update hooks registered as being run.
 | 
			
		||||
    $this->assertIdentical([], \Drupal::state()->get('post_update_test_execution', []));
 | 
			
		||||
 | 
			
		||||
    $key_value = \Drupal::keyValue('update__post_update');
 | 
			
		||||
    $this->assertEqual([], $key_value->get('existing_updates'));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function doSelectionTest() {
 | 
			
		||||
    // First update, should not be run since this module's update hooks fail.
 | 
			
		||||
    $this->assertRaw('8001 -   This update will fail.');
 | 
			
		||||
    $this->assertRaw('8002 -   A further update.');
 | 
			
		||||
    $this->assertEscaped("First update, should not be run since this module's update hooks fail.");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,69 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @file
 | 
			
		||||
 * Contains \Drupal\system\Tests\Update\UpdatePostUpdateTest.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Drupal\system\Tests\Update;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tests hook_post_update().
 | 
			
		||||
 *
 | 
			
		||||
 * @group Update
 | 
			
		||||
 */
 | 
			
		||||
class UpdatePostUpdateTest extends UpdatePathTestBase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setDatabaseDumpFiles() {
 | 
			
		||||
    $this->databaseDumpFiles = [
 | 
			
		||||
      __DIR__ . '/../../../tests/fixtures/update/drupal-8.bare.standard.php.gz',
 | 
			
		||||
      __DIR__ . '/../../../tests/fixtures/update/drupal-8.update-test-postupdate-enabled.php',
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function doSelectionTest() {
 | 
			
		||||
    parent::doSelectionTest();
 | 
			
		||||
 | 
			
		||||
    // Ensure that normal and post_update updates are merged together on the
 | 
			
		||||
    // selection page.
 | 
			
		||||
    $this->assertRaw('<ul><li>8001 -   Normal update_N() function. </li><li>First update.</li><li>Second update.</li><li>Test1 update.</li><li>Test0 update.</li></ul>');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tests hook_post_update_NAME().
 | 
			
		||||
   */
 | 
			
		||||
  public function testPostUpdate() {
 | 
			
		||||
    $this->runUpdates();
 | 
			
		||||
 | 
			
		||||
    $this->assertRaw('<h3>Update first</h3>');
 | 
			
		||||
    $this->assertRaw('First update');
 | 
			
		||||
    $this->assertRaw('<h3>Update second</h3>');
 | 
			
		||||
    $this->assertRaw('Second update');
 | 
			
		||||
    $this->assertRaw('<h3>Update test1</h3>');
 | 
			
		||||
    $this->assertRaw('Test1 update');
 | 
			
		||||
    $this->assertRaw('<h3>Update test0</h3>');
 | 
			
		||||
    $this->assertRaw('Test0 update');
 | 
			
		||||
 | 
			
		||||
    $updates = [
 | 
			
		||||
      'update_test_postupdate_post_update_first',
 | 
			
		||||
      'update_test_postupdate_post_update_second',
 | 
			
		||||
      'update_test_postupdate_post_update_test1',
 | 
			
		||||
      'update_test_postupdate_post_update_test0',
 | 
			
		||||
    ];
 | 
			
		||||
    $this->assertIdentical($updates, \Drupal::state()->get('post_update_test_execution', []));
 | 
			
		||||
 | 
			
		||||
    $key_value = \Drupal::keyValue('post_update');
 | 
			
		||||
    array_unshift($updates, 'block_post_update_disable_blocks_with_missing_contexts');
 | 
			
		||||
    $this->assertEqual($updates, $key_value->get('existing_updates'));
 | 
			
		||||
 | 
			
		||||
    $this->drupalGet('update.php/selection');
 | 
			
		||||
    $this->assertText('No pending updates.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -598,18 +598,31 @@ function system_requirements($phase) {
 | 
			
		|||
    );
 | 
			
		||||
 | 
			
		||||
    // Check installed modules.
 | 
			
		||||
    $has_pending_updates = FALSE;
 | 
			
		||||
    foreach (\Drupal::moduleHandler()->getModuleList() as $module => $filename) {
 | 
			
		||||
      $updates = drupal_get_schema_versions($module);
 | 
			
		||||
      if ($updates !== FALSE) {
 | 
			
		||||
        $default = drupal_get_installed_schema_version($module);
 | 
			
		||||
        if (max($updates) > $default) {
 | 
			
		||||
          $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 <a href="@update">database update script</a> immediately.', array('@update' => \Drupal::url('system.db_update')));
 | 
			
		||||
          $has_pending_updates = TRUE;
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (!$has_pending_updates) {
 | 
			
		||||
      /** @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)) {
 | 
			
		||||
        $has_pending_updates = TRUE;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if ($has_pending_updates) {
 | 
			
		||||
      $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 <a href="@update">database update script</a> 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()) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										40
									
								
								core/modules/system/tests/fixtures/update/drupal-8.update-test-postupdate-enabled.php
								
								
								
									vendored
								
								
									Normal file
								
							
							
						
						
									
										40
									
								
								core/modules/system/tests/fixtures/update/drupal-8.update-test-postupdate-enabled.php
								
								
								
									vendored
								
								
									Normal file
								
							| 
						 | 
				
			
			@ -0,0 +1,40 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @file
 | 
			
		||||
 * Partial database to mimic the installation of the update_test_post_update
 | 
			
		||||
 * module.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Database\Database;
 | 
			
		||||
 | 
			
		||||
$connection = Database::getConnection();
 | 
			
		||||
 | 
			
		||||
// Set the schema version.
 | 
			
		||||
$connection->merge('key_value')
 | 
			
		||||
  ->condition('collection', 'system.schema')
 | 
			
		||||
  ->condition('name', 'update_test_postupdate')
 | 
			
		||||
  ->fields([
 | 
			
		||||
    'collection' => 'system.schema',
 | 
			
		||||
    'name' => 'update_test_postupdate',
 | 
			
		||||
    'value' => 'i:8000;',
 | 
			
		||||
  ])
 | 
			
		||||
  ->execute();
 | 
			
		||||
 | 
			
		||||
// Update core.extension.
 | 
			
		||||
$extensions = $connection->select('config')
 | 
			
		||||
  ->fields('config', ['data'])
 | 
			
		||||
  ->condition('collection', '')
 | 
			
		||||
  ->condition('name', 'core.extension')
 | 
			
		||||
  ->execute()
 | 
			
		||||
  ->fetchField();
 | 
			
		||||
$extensions = unserialize($extensions);
 | 
			
		||||
$extensions['module']['update_test_postupdate'] = 8000;
 | 
			
		||||
$connection->update('config')
 | 
			
		||||
  ->fields([
 | 
			
		||||
    'data' => serialize($extensions),
 | 
			
		||||
  ])
 | 
			
		||||
  ->condition('collection', '')
 | 
			
		||||
  ->condition('name', 'core.extension')
 | 
			
		||||
  ->execute();
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										39
									
								
								core/modules/system/tests/fixtures/update/drupal-8.update-test-postupdate-failing-enabled.php
								
								
								
									vendored
								
								
									Normal file
								
							
							
						
						
									
										39
									
								
								core/modules/system/tests/fixtures/update/drupal-8.update-test-postupdate-failing-enabled.php
								
								
								
									vendored
								
								
									Normal file
								
							| 
						 | 
				
			
			@ -0,0 +1,39 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @file
 | 
			
		||||
 * Partial database to mimic the installation of the update_test_failing module.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Database\Database;
 | 
			
		||||
 | 
			
		||||
$connection = Database::getConnection();
 | 
			
		||||
 | 
			
		||||
// Set the schema version.
 | 
			
		||||
$connection->merge('key_value')
 | 
			
		||||
  ->condition('collection', 'system.schema')
 | 
			
		||||
  ->condition('name', 'update_test_failing')
 | 
			
		||||
  ->fields([
 | 
			
		||||
    'collection' => 'system.schema',
 | 
			
		||||
    'name' => 'update_test_failing',
 | 
			
		||||
    'value' => 'i:8000;',
 | 
			
		||||
  ])
 | 
			
		||||
  ->execute();
 | 
			
		||||
 | 
			
		||||
// Update core.extension.
 | 
			
		||||
$extensions = $connection->select('config')
 | 
			
		||||
  ->fields('config', ['data'])
 | 
			
		||||
  ->condition('collection', '')
 | 
			
		||||
  ->condition('name', 'core.extension')
 | 
			
		||||
  ->execute()
 | 
			
		||||
  ->fetchField();
 | 
			
		||||
$extensions = unserialize($extensions);
 | 
			
		||||
$extensions['module']['update_test_failing'] = 8000;
 | 
			
		||||
$connection->update('config')
 | 
			
		||||
  ->fields([
 | 
			
		||||
    'data' => serialize($extensions),
 | 
			
		||||
  ])
 | 
			
		||||
  ->condition('collection', '')
 | 
			
		||||
  ->condition('name', 'core.extension')
 | 
			
		||||
  ->execute();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Test post update function.
 | 
			
		||||
 */
 | 
			
		||||
function module_test_post_update_test() {
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
name: 'Update test failing'
 | 
			
		||||
type: module
 | 
			
		||||
description: 'Support module for update testing when an update hook is failing.'
 | 
			
		||||
package: Testing
 | 
			
		||||
version: VERSION
 | 
			
		||||
core: 8.x
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,22 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @file
 | 
			
		||||
 * Contains a failing update hook for testing the update system.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\Utility\UpdateException;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This update will fail.
 | 
			
		||||
 */
 | 
			
		||||
function update_test_failing_update_8001() {
 | 
			
		||||
  throw new UpdateException('This update hook is failing.');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A further update.
 | 
			
		||||
 */
 | 
			
		||||
function update_test_failing_update_8002() {
 | 
			
		||||
  // This hook won't ever run.
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,10 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * First update, should not be run since this module's update hooks fail.
 | 
			
		||||
 */
 | 
			
		||||
function update_test_failing_post_update_first() {
 | 
			
		||||
  $execution = \Drupal::state()->get('post_update_test_execution', []);
 | 
			
		||||
  $execution[] = __FUNCTION__;
 | 
			
		||||
  \Drupal::state()->set('post_update_test_execution', $execution);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
core: 8.x
 | 
			
		||||
name: Update test after
 | 
			
		||||
type: module
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Normal update_N() function.
 | 
			
		||||
 */
 | 
			
		||||
function update_test_postupdate_update_8001() {
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,45 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * First update.
 | 
			
		||||
 */
 | 
			
		||||
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';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Second update.
 | 
			
		||||
 */
 | 
			
		||||
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';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Test1 update.
 | 
			
		||||
 */
 | 
			
		||||
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';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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';
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,301 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @file
 | 
			
		||||
 * Contains \Drupal\Tests\Core\Update\UpdateRegistryTest.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Drupal\Tests\Core\Update;
 | 
			
		||||
 | 
			
		||||
use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
 | 
			
		||||
use Drupal\Core\Site\Settings;
 | 
			
		||||
use Drupal\Core\Update\UpdateRegistry;
 | 
			
		||||
use Drupal\Tests\UnitTestCase;
 | 
			
		||||
use org\bovigo\vfs\vfsStream;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @coversDefaultClass \Drupal\Core\Update\UpdateRegistry
 | 
			
		||||
 * @group Update
 | 
			
		||||
 *
 | 
			
		||||
 * Note we load code, so we should better run in isolation.
 | 
			
		||||
 *
 | 
			
		||||
 * @runTestsInSeparateProcesses
 | 
			
		||||
 */
 | 
			
		||||
class UpdateRegistryTest extends UnitTestCase {
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * {@inheritdoc}
 | 
			
		||||
   */
 | 
			
		||||
  protected function setUp() {
 | 
			
		||||
    parent::setUp();
 | 
			
		||||
 | 
			
		||||
    $settings = [];
 | 
			
		||||
    $settings['extension_discovery_scan_tests'] = TRUE;
 | 
			
		||||
    new Settings($settings);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Sets up some modules with some update functions.
 | 
			
		||||
   */
 | 
			
		||||
  protected function setupBasicModules() {
 | 
			
		||||
    $info_a = <<<'EOS'
 | 
			
		||||
type: module
 | 
			
		||||
name: Module A
 | 
			
		||||
core: 8.x
 | 
			
		||||
EOS;
 | 
			
		||||
 | 
			
		||||
    $info_b = <<<'EOS'
 | 
			
		||||
type: module
 | 
			
		||||
name: Module B
 | 
			
		||||
core: 8.x
 | 
			
		||||
EOS;
 | 
			
		||||
 | 
			
		||||
    $module_a = <<<'EOS'
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Module A update A.
 | 
			
		||||
 */
 | 
			
		||||
function module_a_post_update_a() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Module A update B.
 | 
			
		||||
 */
 | 
			
		||||
function module_a_post_update_b() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
EOS;
 | 
			
		||||
    $module_b = <<<'EOS'
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Module B update A.
 | 
			
		||||
 */
 | 
			
		||||
function module_b_post_update_a() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
EOS;
 | 
			
		||||
    vfsStream::setup('drupal');
 | 
			
		||||
    vfsStream::create([
 | 
			
		||||
      'sites' => [
 | 
			
		||||
        'default' => [
 | 
			
		||||
          'modules' => [
 | 
			
		||||
            'module_a' => [
 | 
			
		||||
              'module_a.post_update.php' => $module_a,
 | 
			
		||||
              'module_a.info.yml' => $info_a
 | 
			
		||||
            ],
 | 
			
		||||
            'module_b' => [
 | 
			
		||||
              'module_b.post_update.php' => $module_b,
 | 
			
		||||
              'module_b.info.yml' => $info_b
 | 
			
		||||
            ],
 | 
			
		||||
          ]
 | 
			
		||||
        ]
 | 
			
		||||
      ],
 | 
			
		||||
    ]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::getPendingUpdateFunctions
 | 
			
		||||
   */
 | 
			
		||||
  public function testGetPendingUpdateFunctionsNoExistingUpdates() {
 | 
			
		||||
    $this->setupBasicModules();
 | 
			
		||||
 | 
			
		||||
    $key_value = $this->prophesize(KeyValueStoreInterface::class);
 | 
			
		||||
    $key_value->get('existing_updates', [])->willReturn([]);
 | 
			
		||||
    $key_value = $key_value->reveal();
 | 
			
		||||
 | 
			
		||||
    $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [
 | 
			
		||||
      'module_a',
 | 
			
		||||
      'module_b'
 | 
			
		||||
    ], $key_value, FALSE);
 | 
			
		||||
 | 
			
		||||
    $this->assertEquals([
 | 
			
		||||
      'module_a_post_update_a',
 | 
			
		||||
      'module_a_post_update_b',
 | 
			
		||||
      'module_b_post_update_a'
 | 
			
		||||
    ], $update_registry->getPendingUpdateFunctions());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::getPendingUpdateFunctions
 | 
			
		||||
   */
 | 
			
		||||
  public function testGetPendingUpdateFunctionsWithLoadedModulesButNotEnabled() {
 | 
			
		||||
    $this->setupBasicModules();
 | 
			
		||||
 | 
			
		||||
    $key_value = $this->prophesize(KeyValueStoreInterface::class);
 | 
			
		||||
    $key_value->get('existing_updates', [])->willReturn([]);
 | 
			
		||||
    $key_value = $key_value->reveal();
 | 
			
		||||
 | 
			
		||||
    // Preload modules to ensure that ::getAvailableUpdateFunctions filters out
 | 
			
		||||
    // not enabled modules.
 | 
			
		||||
    include_once 'vfs://drupal/sites/default/modules/module_a/module_a.post_update.php';
 | 
			
		||||
    include_once 'vfs://drupal/sites/default/modules/module_b/module_b.post_update.php';
 | 
			
		||||
 | 
			
		||||
    $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [
 | 
			
		||||
      'module_a',
 | 
			
		||||
    ], $key_value, FALSE);
 | 
			
		||||
 | 
			
		||||
    $this->assertEquals([
 | 
			
		||||
      'module_a_post_update_a',
 | 
			
		||||
      'module_a_post_update_b',
 | 
			
		||||
    ], $update_registry->getPendingUpdateFunctions());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::getPendingUpdateFunctions
 | 
			
		||||
   */
 | 
			
		||||
  public function testGetPendingUpdateFunctionsExistingUpdates() {
 | 
			
		||||
    $this->setupBasicModules();
 | 
			
		||||
 | 
			
		||||
    $key_value = $this->prophesize(KeyValueStoreInterface::class);
 | 
			
		||||
    $key_value->get('existing_updates', [])->willReturn(['module_a_post_update_a']);
 | 
			
		||||
    $key_value = $key_value->reveal();
 | 
			
		||||
 | 
			
		||||
    $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [
 | 
			
		||||
      'module_a',
 | 
			
		||||
      'module_b'
 | 
			
		||||
    ], $key_value, FALSE);
 | 
			
		||||
 | 
			
		||||
    $this->assertEquals(array_values([
 | 
			
		||||
      'module_a_post_update_b',
 | 
			
		||||
      'module_b_post_update_a'
 | 
			
		||||
    ]), array_values($update_registry->getPendingUpdateFunctions()));
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::getPendingUpdateInformation
 | 
			
		||||
   */
 | 
			
		||||
  public function testGetPendingUpdateInformation() {
 | 
			
		||||
    $this->setupBasicModules();
 | 
			
		||||
 | 
			
		||||
    $key_value = $this->prophesize(KeyValueStoreInterface::class);
 | 
			
		||||
    $key_value->get('existing_updates', [])->willReturn([]);
 | 
			
		||||
    $key_value = $key_value->reveal();
 | 
			
		||||
 | 
			
		||||
    $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [
 | 
			
		||||
      'module_a',
 | 
			
		||||
      'module_b'
 | 
			
		||||
    ], $key_value, FALSE);
 | 
			
		||||
 | 
			
		||||
    $expected = [];
 | 
			
		||||
    $expected['module_a']['pending']['a'] = 'Module A update A.';
 | 
			
		||||
    $expected['module_a']['pending']['b'] = 'Module A update B.';
 | 
			
		||||
    $expected['module_a']['start'] = 'a';
 | 
			
		||||
    $expected['module_b']['pending']['a'] = 'Module B update A.';
 | 
			
		||||
    $expected['module_b']['start'] = 'a';
 | 
			
		||||
 | 
			
		||||
    $this->assertEquals($expected, $update_registry->getPendingUpdateInformation());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::getPendingUpdateInformation
 | 
			
		||||
   */
 | 
			
		||||
  public function testGetPendingUpdateInformationWithExistingUpdates() {
 | 
			
		||||
    $this->setupBasicModules();
 | 
			
		||||
 | 
			
		||||
    $key_value = $this->prophesize(KeyValueStoreInterface::class);
 | 
			
		||||
    $key_value->get('existing_updates', [])->willReturn(['module_a_post_update_a']);
 | 
			
		||||
    $key_value = $key_value->reveal();
 | 
			
		||||
 | 
			
		||||
    $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [
 | 
			
		||||
      'module_a',
 | 
			
		||||
      'module_b'
 | 
			
		||||
    ], $key_value, FALSE);
 | 
			
		||||
 | 
			
		||||
    $expected = [];
 | 
			
		||||
    $expected['module_a']['pending']['b'] = 'Module A update B.';
 | 
			
		||||
    $expected['module_a']['start'] = 'b';
 | 
			
		||||
    $expected['module_b']['pending']['a'] = 'Module B update A.';
 | 
			
		||||
    $expected['module_b']['start'] = 'a';
 | 
			
		||||
 | 
			
		||||
    $this->assertEquals($expected, $update_registry->getPendingUpdateInformation());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::getModuleUpdateFunctions
 | 
			
		||||
   */
 | 
			
		||||
  public function testGetModuleUpdateFunctions() {
 | 
			
		||||
    $this->setupBasicModules();
 | 
			
		||||
    $key_value = $this->prophesize(KeyValueStoreInterface::class)->reveal();
 | 
			
		||||
 | 
			
		||||
    $update_registry = new UpdateRegistry('vfs://drupal', 'sites/default', [
 | 
			
		||||
      'module_a',
 | 
			
		||||
      'module_b'
 | 
			
		||||
    ], $key_value, FALSE);
 | 
			
		||||
 | 
			
		||||
    $this->assertEquals(['module_a_post_update_a', 'module_a_post_update_b'], array_values($update_registry->getModuleUpdateFunctions('module_a')));
 | 
			
		||||
    $this->assertEquals(['module_b_post_update_a'], array_values($update_registry->getModuleUpdateFunctions('module_b')));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @covers ::registerInvokedUpdates
 | 
			
		||||
   */
 | 
			
		||||
  public function testRegisterInvokedUpdatesWithoutExistingUpdates() {
 | 
			
		||||
    $this->setupBasicModules();
 | 
			
		||||
    $key_value = $this->prophesize(KeyValueStoreInterface::class);
 | 
			
		||||
    $key_value->get('existing_updates', [])->willReturn([]);
 | 
			
		||||
    $key_value->set('existing_updates', ['module_a_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->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 ::registerInvokedUpdates
 | 
			
		||||
   */
 | 
			
		||||
  public function testRegisterInvokedUpdatesWithExistingUpdates() {
 | 
			
		||||
    $this->setupBasicModules();
 | 
			
		||||
    $key_value = $this->prophesize(KeyValueStoreInterface::class);
 | 
			
		||||
    $key_value->get('existing_updates', [])->willReturn(['module_a_post_update_b']);
 | 
			
		||||
    $key_value->set('existing_updates', ['module_a_post_update_b', 'module_a_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->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');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue