diff --git a/core/core.services.yml b/core/core.services.yml index 72bf10e..d1f055c 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -278,7 +278,7 @@ services: arguments: ['%container.modules%', '@cache.bootstrap'] theme_handler: class: Drupal\Core\Extension\ThemeHandler - arguments: ['@config.factory', '@module_handler', '@state', '@info_parser', '@logger.channel.default', '@asset.css.collection_optimizer', '@config.installer', '@router.builder'] + arguments: ['@config.factory', '@module_handler', '@state', '@info_parser', '@logger.channel.default', '@asset.css.collection_optimizer', '@config.installer', '@config.manager', '@router.builder'] entity.manager: class: Drupal\Core\Entity\EntityManager arguments: ['@container.namespaces', '@module_handler', '@cache.discovery', '@language_manager', '@string_translation', '@class_resolver', '@typed_data_manager'] diff --git a/core/lib/Drupal/Core/Extension/ThemeHandler.php b/core/lib/Drupal/Core/Extension/ThemeHandler.php index d9ef77e..df83a02 100644 --- a/core/lib/Drupal/Core/Extension/ThemeHandler.php +++ b/core/lib/Drupal/Core/Extension/ThemeHandler.php @@ -12,6 +12,7 @@ use Drupal\Core\Cache\Cache; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Config\ConfigInstallerInterface; +use Drupal\Core\Config\ConfigManagerInterface; use Drupal\Core\State\StateInterface; use Drupal\Core\Routing\RouteBuilder; use Psr\Log\LoggerInterface; @@ -109,6 +110,13 @@ class ThemeHandler implements ThemeHandlerInterface { protected $cssCollectionOptimizer; /** + * The config manager used to uninstall a theme. + * + * @var \Drupal\Core\Config\ConfigManagerInterface + */ + protected $configManager; + + /** * Constructs a new ThemeHandler. * * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory @@ -119,20 +127,22 @@ class ThemeHandler implements ThemeHandlerInterface { * The state store. * @param \Drupal\Core\Extension\InfoParserInterface $info_parser * The info parser to parse the theme.info.yml files. - * @param \Drupal\Core\Asset\AssetCollectionOptimizerInterface $css_collection_optimizer - * The CSS asset collection optimizer service. * @param \Psr\Log\LoggerInterface $logger * A logger instance. + * @param \Drupal\Core\Asset\AssetCollectionOptimizerInterface $css_collection_optimizer + * The CSS asset collection optimizer service. * @param \Drupal\Core\Config\ConfigInstallerInterface $config_installer * (optional) The config installer to install configuration. This optional * to allow the theme handler to work before Drupal is installed and has a * database. + * @param \Drupal\Core\Config\ConfigManagerInterface $config_manager + * The config manager used to uninstall a theme. * @param \Drupal\Core\Routing\RouteBuilder $route_builder * (optional) The route builder to rebuild the routes if a theme is enabled. * @param \Drupal\Core\Extension\ExtensionDiscovery $extension_discovery * (optional) A extension discovery instance (for unit tests). */ - public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, StateInterface $state, InfoParserInterface $info_parser,LoggerInterface $logger, AssetCollectionOptimizerInterface $css_collection_optimizer = NULL, ConfigInstallerInterface $config_installer = NULL, RouteBuilder $route_builder = NULL, ExtensionDiscovery $extension_discovery = NULL) { + public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, StateInterface $state, InfoParserInterface $info_parser,LoggerInterface $logger, AssetCollectionOptimizerInterface $css_collection_optimizer = NULL, ConfigInstallerInterface $config_installer = NULL, ConfigManagerInterface $config_manager = NULL, RouteBuilder $route_builder = NULL, ExtensionDiscovery $extension_discovery = NULL) { $this->configFactory = $config_factory; $this->moduleHandler = $module_handler; $this->state = $state; @@ -140,6 +150,7 @@ public function __construct(ConfigFactoryInterface $config_factory, ModuleHandle $this->logger = $logger; $this->cssCollectionOptimizer = $css_collection_optimizer; $this->configInstaller = $config_installer; + $this->configManager = $config_manager; $this->routeBuilder = $route_builder; $this->extensionDiscovery = $extension_discovery; } @@ -354,6 +365,32 @@ public function disable(array $theme_list) { /** * {@inheritdoc} */ + public function uninstall(array $theme_list) { + $extension_config = $this->configFactory->get('core.extension'); + + foreach ($theme_list as $key) { + if ($extension_config->get("theme.$key") !== NULL) { + throw new \InvalidArgumentException("Theme $key is still enabled."); + } + if ($extension_config->get("disabled.theme.$key") === NULL) { + throw new \InvalidArgumentException("Theme $key is not installed."); + } + } + + foreach ($theme_list as $key) { + // Remove all configuration belonging to the theme. + $this->configManager->uninstall('theme', $key); + + $extension_config->clear("disabled.theme.$key"); + } + $extension_config->save(); + + $this->moduleHandler->invokeAll('themes_uninstalled', [$theme_list]); + } + + /** + * {@inheritdoc} + */ public function listInfo() { if (!isset($this->list)) { $this->list = array(); diff --git a/core/lib/Drupal/Core/Extension/ThemeHandlerInterface.php b/core/lib/Drupal/Core/Extension/ThemeHandlerInterface.php index 74f9ad2..353ab81 100644 --- a/core/lib/Drupal/Core/Extension/ThemeHandlerInterface.php +++ b/core/lib/Drupal/Core/Extension/ThemeHandlerInterface.php @@ -42,6 +42,22 @@ public function enable(array $theme_list, $enable_dependencies = TRUE); public function disable(array $theme_list); /** + * Uninstalls a given list of disabled themes. + * + * Uninstalling a theme removes all related configuration (like blocks) and + * invokes the 'themes_uninstalled' hook. + * + * @param array $theme_list + * The themes to uninstall. + * + * @throws \InvalidArgumentException + * Thrown when you uninstall an not installed or not disabled theme. + * + * @see hook_themes_uninstalled() + */ + public function uninstall(array $theme_list); + + /** * Returns a list of currently enabled themes. * * @return \Drupal\Core\Extension\Extension[] @@ -140,6 +156,16 @@ public function getName($theme); public function getDefault(); /** + * Sets a new default theme. + * + * @param string $theme + * The new default theme. + * + * @return $this + */ + public function setDefault($theme); + + /** * Returns an array of directories for all enabled themes. * * Useful for tasks such as finding a file that exists in all theme diff --git a/core/modules/block/src/Tests/BlockTest.php b/core/modules/block/src/Tests/BlockTest.php index ebd4a7c..abfd375 100644 --- a/core/modules/block/src/Tests/BlockTest.php +++ b/core/modules/block/src/Tests/BlockTest.php @@ -7,6 +7,7 @@ namespace Drupal\block\Tests; +use Drupal\block\Entity\Block; use Drupal\Core\Cache\Cache; use Drupal\simpletest\WebTestBase; use Drupal\Component\Utility\String; @@ -376,4 +377,25 @@ public function testBlockCacheTags() { $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS'); } + /** + * Tests that uninstalling a theme removes its block configuration. + */ + public function testUninstallTheme() { + /** @var \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler */ + $theme_handler = \Drupal::service('theme_handler'); + + $theme_handler->enable(['seven']); + $theme_handler->setDefault('seven'); + $block = $this->drupalPlaceBlock('system_powered_by_block', ['theme' => 'seven', 'region' => 'help']); + $this->drupalGet(''); + $this->assertText('Powered by Drupal'); + + $theme_handler->setDefault('stark'); + $theme_handler->disable(['seven']); + $theme_handler->uninstall(['seven']); + + // Ensure that the block configuration does not exist anymore. + $this->assertIdentical(NULL, Block::load($block->id())); + } + } diff --git a/core/modules/system/src/Tests/Extension/ThemeHandlerTest.php b/core/modules/system/src/Tests/Extension/ThemeHandlerTest.php index f652bfd..1dbabe0 100644 --- a/core/modules/system/src/Tests/Extension/ThemeHandlerTest.php +++ b/core/modules/system/src/Tests/Extension/ThemeHandlerTest.php @@ -331,6 +331,60 @@ function testDisableNonExisting() { } /** + * Tests uninstalling a theme. + */ + function testUninstall() { + $name = 'test_basetheme'; + + $this->themeHandler()->enable(array($name)); + $this->themeHandler()->disable(array($name)); + $this->assertTrue($this->config("$name.settings")->get()); + + $this->themeHandler()->uninstall(array($name)); + + $this->assertFalse(array_keys($this->themeHandler()->listInfo())); + $this->assertFalse(array_keys(system_list('theme'))); + + $this->assertFalse($this->config("$name.settings")->get()); + } + + /** + * Tests uninstalling a theme that is not installed. + */ + function testUninstallNotInstalled() { + $name = 'test_basetheme'; + + try { + $message = 'ThemeHandler::uninstall() throws InvalidArgumentException upon uninstalling a theme that is not installed.'; + $this->themeHandler()->uninstall(array($name)); + $this->fail($message); + } + catch (\InvalidArgumentException $e) { + $this->pass(get_class($e) . ': ' . $e->getMessage()); + } + } + + /** + * Tests uninstalling a theme that is not disabled. + */ + function testUninstallEnabled() { + $name = 'test_basetheme'; + + $this->themeHandler()->enable(array($name)); + + try { + $message = 'ThemeHandler::uninstall() throws InvalidArgumentException upon uninstalling a theme that is not installed.'; + $this->themeHandler()->uninstall(array($name)); + $this->fail($message); + } + catch (\InvalidArgumentException $e) { + $this->pass(get_class($e) . ': ' . $e->getMessage()); + } + + $this->assertTrue($this->config("$name.settings")->get()); + } + + /** * Tests that theme info can be altered by a module. * * @see module_test_system_info_alter() diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php index 59922b7..75987fa 100644 --- a/core/modules/system/system.api.php +++ b/core/modules/system/system.api.php @@ -1397,6 +1397,42 @@ function hook_module_preuninstall($module) { } /** + * Perform necessary actions when themes are enabled. + * + * @param array $themes + * An array of theme names which are enabled. + */ +function hook_themes_enabled(array $themes) { + // Add some state entries depending on the theme. + foreach ($themes as $theme) { + \Drupal::state()->set('example.' . $theme, 'some-value'); + } +} + +/** + * Perform necessary actions when themes are disabled. + * + * @param array $themes + * An array of theme names which are enabled. + */ +function hook_themes_disabled(array $themes) { + // Update some state entries depending on the theme. + foreach ($themes as $theme) { + \Drupal::state()->set('example.' . $theme, 0); + } +} + +/** + * Perform necessary actions when themes are uninstalled. + */ +function hook_themes_uninstalled(array $themes) { + // Remove some state entries depending on the theme. + foreach ($themes as $theme) { + \Drupal::state()->delete('example.' . $theme); + } +} + +/** * Perform necessary actions after modules are uninstalled. * * This function differs from hook_uninstall() in that it gives all other diff --git a/core/tests/Drupal/Tests/Core/Extension/ThemeHandlerTest.php b/core/tests/Drupal/Tests/Core/Extension/ThemeHandlerTest.php index eb6cb0e..5ec5e21 100644 --- a/core/tests/Drupal/Tests/Core/Extension/ThemeHandlerTest.php +++ b/core/tests/Drupal/Tests/Core/Extension/ThemeHandlerTest.php @@ -64,6 +64,13 @@ class ThemeHandlerTest extends UnitTestCase { protected $configInstaller; /** + * The mocked config manager. + * + * @var \Drupal\Core\Config\ConfigManagerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $configManager; + + /** * The extension discovery. * * @var \Drupal\Core\Extension\ExtensionDiscovery|\PHPUnit_Framework_MockObject_MockObject @@ -101,6 +108,7 @@ protected function setUp() { $this->state = new State(new KeyValueMemoryFactory()); $this->infoParser = $this->getMock('Drupal\Core\Extension\InfoParserInterface'); $this->configInstaller = $this->getMock('Drupal\Core\Config\ConfigInstallerInterface'); + $this->configManager = $this->getMock('Drupal\Core\Config\ConfigManagerInterface'); $this->routeBuilder = $this->getMockBuilder('Drupal\Core\Routing\RouteBuilder') ->disableOriginalConstructor() ->getMock(); @@ -111,7 +119,7 @@ protected function setUp() { ->disableOriginalConstructor() ->getMock(); $logger = $this->getMock('Psr\Log\LoggerInterface'); - $this->themeHandler = new TestThemeHandler($this->configFactory, $this->moduleHandler, $this->state, $this->infoParser, $logger, $this->cssCollectionOptimizer, $this->configInstaller, $this->routeBuilder, $this->extensionDiscovery); + $this->themeHandler = new TestThemeHandler($this->configFactory, $this->moduleHandler, $this->state, $this->infoParser, $logger, $this->cssCollectionOptimizer, $this->configInstaller, $this->configManager, $this->routeBuilder, $this->extensionDiscovery); $cache_backend = $this->getMock('Drupal\Core\Cache\CacheBackendInterface'); $this->getContainerWithCacheBins($cache_backend);