Skip to content
Snippets Groups Projects
Commit 69c34dc7 authored by Adam G-H's avatar Adam G-H
Browse files

Issue #3263865 by phenaproxima: Module uninstall form fails because of our shim

parent 9793f71c
No related branches found
No related tags found
1 merge request!210Issue #3263865: Module uninstall form fails because of our shim
Showing
with 67 additions and 445 deletions
<?php
namespace Drupal\automatic_updates_9_3_shim;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase;
use Symfony\Component\DependencyInjection\Reference;
/**
* Modifies container service definitions.
*/
class AutomaticUpdates93ShimServiceProvider extends ServiceProviderBase {
/**
* {@inheritdoc}
*/
public function alter(ContainerBuilder $container) {
parent::alter($container);
$service_id = 'update.update_hook_registry_factory';
if ($container->hasDefinition($service_id)) {
$container->getDefinition($service_id)
->setClass(UpdateHookRegistryFactory::class);
}
else {
$container->register($service_id, UpdateHookRegistryFactory::class)
->addMethodCall('setContainer', [
new Reference('service_container'),
]);
}
$service_id = 'update.update_hook_registry';
if ($container->hasDefinition($service_id)) {
$container->getDefinition($service_id)
->setClass(UpdateHookRegistry::class);
}
else {
$container->register($service_id, UpdateHookRegistry::class)
->setFactory([
new Reference($service_id . '_factory'),
'create',
]);
}
$container->getDefinition('module_installer')
->setClass(ModuleInstaller::class);
}
}
<?php
namespace Drupal\automatic_updates_9_3_shim;
use Drupal\Core\Database\Connection;
use Drupal\Core\DrupalKernelInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ModuleInstaller as CoreModuleInstaller;
/**
* A module installer which accepts our update registry shim in its constructor.
*/
class ModuleInstaller extends CoreModuleInstaller {
/**
* {@inheritdoc}
*/
public function __construct($root, ModuleHandlerInterface $module_handler, DrupalKernelInterface $kernel, Connection $connection = NULL, UpdateHookRegistry $update_registry = NULL) {
$this->root = $root;
$this->moduleHandler = $module_handler;
$this->kernel = $kernel;
if (!$connection) {
@trigger_error('The database connection must be passed to ' . __METHOD__ . '(). Creating ' . __CLASS__ . ' without it is deprecated in drupal:9.2.0 and will be required in drupal:10.0.0. See https://www.drupal.org/node/2970993', E_USER_DEPRECATED);
$connection = \Drupal::service('database');
}
$this->connection = $connection;
if (!$update_registry) {
$update_registry = \Drupal::service('update.update_hook_registry');
}
$this->updateRegistry = $update_registry;
}
}
<?php
// phpcs:ignoreFile
/**
* This file was generated via php core/scripts/generate-proxy-class.php 'Drupal\automatic_updates_9_3_shim\ModuleInstaller' "modules/contrib/automatic_updates/automatic_updates_9_3_shim/src".
*/
namespace Drupal\automatic_updates_9_3_shim\ProxyClass {
/**
* Provides a proxy class for \Drupal\automatic_updates_9_3_shim\ModuleInstaller.
*
* @see \Drupal\Component\ProxyBuilder
*/
class ModuleInstaller implements \Drupal\Core\Extension\ModuleInstallerInterface
{
use \Drupal\Core\DependencyInjection\DependencySerializationTrait;
/**
* The id of the original proxied service.
*
* @var string
*/
protected $drupalProxyOriginalServiceId;
/**
* The real proxied service, after it was lazy loaded.
*
* @var \Drupal\automatic_updates_9_3_shim\ModuleInstaller
*/
protected $service;
/**
* The service container.
*
* @var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
/**
* Constructs a ProxyClass Drupal proxy object.
*
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* The container.
* @param string $drupal_proxy_original_service_id
* The service ID of the original service.
*/
public function __construct(\Symfony\Component\DependencyInjection\ContainerInterface $container, $drupal_proxy_original_service_id)
{
$this->container = $container;
$this->drupalProxyOriginalServiceId = $drupal_proxy_original_service_id;
}
/**
* Lazy loads the real service from the container.
*
* @return object
* Returns the constructed real service.
*/
protected function lazyLoadItself()
{
if (!isset($this->service)) {
$this->service = $this->container->get($this->drupalProxyOriginalServiceId);
}
return $this->service;
}
/**
* {@inheritdoc}
*/
public function addUninstallValidator(\Drupal\Core\Extension\ModuleUninstallValidatorInterface $uninstall_validator)
{
return $this->lazyLoadItself()->addUninstallValidator($uninstall_validator);
}
/**
* {@inheritdoc}
*/
public function install(array $module_list, $enable_dependencies = true)
{
return $this->lazyLoadItself()->install($module_list, $enable_dependencies);
}
/**
* {@inheritdoc}
*/
public function uninstall(array $module_list, $uninstall_dependents = true)
{
return $this->lazyLoadItself()->uninstall($module_list, $uninstall_dependents);
}
/**
* {@inheritdoc}
*/
public function validateUninstall(array $module_list)
{
return $this->lazyLoadItself()->validateUninstall($module_list);
}
}
}
<?php
namespace Drupal\automatic_updates_9_3_shim;
use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
/**
* Provides module updates versions handling.
*/
class UpdateHookRegistry {
/**
* Indicates that a module has not been installed yet.
*/
public const SCHEMA_UNINSTALLED = -1;
/**
* A list of enabled modules.
*
* @var string[]
*/
protected $enabledModules;
/**
* The key value storage.
*
* @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
*/
protected $keyValue;
/**
* A static cache of schema currentVersions per module.
*
* Stores schema versions of the modules based on their defined hook_update_N
* implementations.
* Example:
* ```
* [
* 'example_module' => [
* 8000,
* 8001,
* 8002
* ]
* ]
* ```
*
* @var int[][]
* @see \Drupal\Core\Update\UpdateHookRegistry::getAvailableUpdates()
*/
protected $allAvailableSchemaVersions = [];
/**
* Constructs a new UpdateRegistry.
*
* @param string[] $enabled_modules
* A list of enabled modules.
* @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $key_value
* The key value store.
*/
public function __construct(array $enabled_modules, KeyValueStoreInterface $key_value) {
$this->enabledModules = $enabled_modules;
$this->keyValue = $key_value;
}
/**
* Returns an array of available schema versions for a module.
*
* @param string $module
* A module name.
*
* @return int[]
* An array of available updates sorted by version. Empty array returned if
* no updates available.
*/
public function getAvailableUpdates(string $module): array {
if (!isset($this->allAvailableSchemaVersions[$module])) {
$this->allAvailableSchemaVersions[$module] = [];
foreach ($this->enabledModules as $enabled_module) {
$this->allAvailableSchemaVersions[$enabled_module] = [];
}
// Prepare regular expression to match all possible defined
// hook_update_N().
$regexp = '/^(?<module>.+)_update_(?<version>\d+)$/';
$functions = get_defined_functions();
// Narrow this down to functions ending with an integer, since all
// hook_update_N() functions end this way, and there are other
// possible functions which match '_update_'. We use preg_grep() here
// since looping through all PHP functions can take significant page
// execution time and this function is called on every administrative page
// via system_requirements().
foreach (preg_grep('/_\d+$/', $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)) {
$this->allAvailableSchemaVersions[$matches['module']][] = (int) $matches['version'];
}
}
// Ensure that updates are applied in numerical order.
array_walk(
$this->allAvailableSchemaVersions,
static function (&$module_updates) {
sort($module_updates, SORT_NUMERIC);
}
);
}
return $this->allAvailableSchemaVersions[$module];
}
/**
* Returns the currently installed schema version for a module.
*
* @param string $module
* A module name.
*
* @return int
* The currently installed schema version, or self::SCHEMA_UNINSTALLED if the
* module is not installed.
*/
public function getInstalledVersion(string $module): int {
return $this->keyValue->get($module, self::SCHEMA_UNINSTALLED);
}
/**
* Updates the installed version information for a module.
*
* @param string $module
* A module name.
* @param int $version
* The new schema version.
*
* @return self
* Returns self to support chained method calls.
*/
public function setInstalledVersion(string $module, int $version): self {
$this->keyValue->set($module, $version);
return $this;
}
/**
* Deletes the installed version information for the module.
*
* @param string $module
* The module name to delete.
*/
public function deleteInstalledVersion(string $module): void {
$this->keyValue->delete($module);
}
/**
* Returns the currently installed schema version for all modules.
*
* @return int[]
* Array of modules as the keys and values as the currently installed
* schema version of corresponding module, or self::SCHEMA_UNINSTALLED if the
* module is not installed.
*/
public function getAllInstalledVersions(): array {
return $this->keyValue->getAll();
}
}
<?php
namespace Drupal\automatic_updates_9_3_shim;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
/**
* Service factory for the versioning update registry.
*/
class UpdateHookRegistryFactory implements ContainerAwareInterface {
use ContainerAwareTrait;
/**
* Creates a new UpdateHookRegistry instance.
*
* @return \Drupal\Core\Update\UpdateHookRegistry
* The update registry instance.
*/
public function create() {
return new UpdateHookRegistry(
array_keys($this->container->get('module_handler')->getModuleList()),
$this->container->get('keyvalue')->get('system.schema')
);
}
}
......@@ -50,6 +50,22 @@ class PendingUpdatesValidator implements PreOperationStageValidatorInterface {
* {@inheritdoc}
*/
public function validateStagePreOperation(PreOperationStageEvent $event): void {
if ($this->updatesExist()) {
$message = $this->t('Some modules have database schema updates to install. You should run the <a href=":update">database update script</a> immediately.', [
':update' => Url::fromRoute('system.db_update')->toString(),
]);
$event->addError([$message]);
}
}
/**
* Checks if there are any pending update or post-update hooks.
*
* @return bool
* TRUE if there are any pending update or post-update hooks, FALSE
* otherwise.
*/
public function updatesExist(): bool {
require_once $this->appRoot . '/core/includes/install.inc';
require_once $this->appRoot . '/core/includes/update.inc';
......@@ -57,12 +73,7 @@ class PendingUpdatesValidator implements PreOperationStageValidatorInterface {
$hook_updates = update_get_update_list();
$post_updates = $this->updateRegistry->getPendingUpdateFunctions();
if ($hook_updates || $post_updates) {
$message = $this->t('Some modules have database schema updates to install. You should run the <a href=":update">database update script</a> immediately.', [
':update' => Url::fromRoute('system.db_update')->toString(),
]);
$event->addError([$message]);
}
return $hook_updates || $post_updates;
}
/**
......
......@@ -2,10 +2,9 @@
namespace Drupal\automatic_updates\Controller;
use Drupal\automatic_updates_9_3_shim\UpdateHookRegistry;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Update\UpdateRegistry;
use Drupal\Core\Url;
use Drupal\package_manager\Validator\PendingUpdatesValidator;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
......@@ -18,30 +17,20 @@ use Symfony\Component\HttpFoundation\RedirectResponse;
class UpdateController extends ControllerBase {
/**
* The update hook registry service.
* The pending updates validator.
*
* @var \Drupal\automatic_updates_9_3_shim\UpdateHookRegistry
* @var \Drupal\package_manager\Validator\PendingUpdatesValidator
*/
protected $updateHookRegistry;
/**
* The post-update registry service.
*
* @var \Drupal\Core\Update\UpdateRegistry
*/
protected $postUpdateRegistry;
protected $pendingUpdatesValidator;
/**
* Constructs an UpdateController object.
*
* @param \Drupal\automatic_updates_9_3_shim\UpdateHookRegistry $update_hook_registry
* The update hook registry service.
* @param \Drupal\Core\Update\UpdateRegistry $post_update_registry
* The post-update registry service.
* @param \Drupal\package_manager\Validator\PendingUpdatesValidator $pending_updates_validator
* The pending updates validator.
*/
public function __construct(UpdateHookRegistry $update_hook_registry, UpdateRegistry $post_update_registry) {
$this->updateHookRegistry = $update_hook_registry;
$this->postUpdateRegistry = $post_update_registry;
public function __construct(PendingUpdatesValidator $pending_updates_validator) {
$this->pendingUpdatesValidator = $pending_updates_validator;
}
/**
......@@ -49,8 +38,7 @@ class UpdateController extends ControllerBase {
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('update.update_hook_registry'),
$container->get('update.post_update_registry')
$container->get('package_manager.validator.pending_updates')
);
}
......@@ -65,7 +53,7 @@ class UpdateController extends ControllerBase {
* A redirect to the appropriate destination.
*/
public function onFinish(): RedirectResponse {
if ($this->pendingUpdatesExist()) {
if ($this->pendingUpdatesValidator->updatesExist()) {
$message = $this->t('Please apply database updates to complete the update process.');
$url = Url::fromRoute('system.db_update');
}
......@@ -77,25 +65,4 @@ class UpdateController extends ControllerBase {
return new RedirectResponse($url->setAbsolute()->toString());
}
/**
* Checks if there are any pending database updates.
*
* @return bool
* TRUE if there are any pending update hooks or post-updates, otherwise
* FALSE.
*/
protected function pendingUpdatesExist(): bool {
if ($this->postUpdateRegistry->getPendingUpdateFunctions()) {
return TRUE;
}
$modules = array_keys($this->moduleHandler()->getModuleList());
foreach ($modules as $module) {
if ($this->updateHookRegistry->getAvailableUpdates($module)) {
return TRUE;
}
}
return FALSE;
}
}
<?php
namespace Drupal\automatic_updates_test;
use Drupal\automatic_updates_test\Validator\TestPendingUpdatesValidator;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase;
/**
* Modifies container services for testing purposes.
*/
class AutomaticUpdatesTestServiceProvider extends ServiceProviderBase {
/**
* {@inheritdoc}
*/
public function alter(ContainerBuilder $container) {
parent::alter($container);
$container->getDefinition('package_manager.validator.pending_updates')
->setClass(TestPendingUpdatesValidator::class);
}
}
......@@ -2,7 +2,6 @@
namespace Drupal\automatic_updates_test\Routing;
use Drupal\automatic_updates_test\Controller\TestUpdateController;
use Drupal\automatic_updates_test\Form\TestUpdateReady;
use Drupal\Core\Routing\RouteSubscriberBase;
use Symfony\Component\Routing\RouteCollection;
......@@ -18,8 +17,6 @@ class RouteSubscriber extends RouteSubscriberBase {
protected function alterRoutes(RouteCollection $collection) {
$collection->get('automatic_updates.confirmation_page')
->setDefault('_form', TestUpdateReady::class);
$collection->get('automatic_updates.finish')
->setDefault('_controller', TestUpdateController::class . '::onFinish');
}
}
<?php
namespace Drupal\automatic_updates_test\Controller;
namespace Drupal\automatic_updates_test\Validator;
use Drupal\automatic_updates\Controller\UpdateController;
use Drupal\package_manager\Validator\PendingUpdatesValidator;
/**
* A test-only version of the update controller.
* Defines a test-only implementation of the pending updates validator.
*/
class TestUpdateController extends UpdateController {
class TestPendingUpdatesValidator extends PendingUpdatesValidator {
/**
* {@inheritdoc}
*/
protected function pendingUpdatesExist(): bool {
$pending_updates = $this->state()
public function updatesExist(): bool {
$pending_updates = \Drupal::state()
->get('automatic_updates_test.staged_database_updates', []);
// If the System module should expose a pending update, create one that will
......@@ -24,7 +24,7 @@ class TestUpdateController extends UpdateController {
// @codingStandardsIgnoreLine
eval('function system_update_4294967294() {}');
}
return parent::pendingUpdatesExist();
return parent::updatesExist();
}
}
......@@ -289,14 +289,6 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
$this->setCoreVersion('9.8.0');
$this->checkForUpdates();
// Simulate a staged database update in the System module.
$this->container->get('state')
->set('automatic_updates_test.staged_database_updates', [
'system' => [
'name' => 'System',
],
]);
// Flag a warning, which will not block the update but should be displayed
// on the updater form.
$this->createTestValidationResults();
......@@ -313,6 +305,15 @@ class UpdaterFormTest extends AutomaticUpdatesFunctionalTestBase {
$this->checkForMetaRefresh();
$this->assertUpdateStagedTimes(1);
$this->assertUpdateReady();
// Simulate a staged database update in the System module. We must do this
// after the update has started, because the pending updates validator
// will prevent an update from starting.
$this->container->get('state')
->set('automatic_updates_test.staged_database_updates', [
'system' => [
'name' => 'System',
],
]);
// The warning from the updater form should be not be repeated, but we
// should see a warning about pending database updates, and once the staged
// changes have been applied, we should be redirected to update.php, where
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment