diff --git a/core/lib/Drupal/Core/Extension/ThemeHandler.php b/core/lib/Drupal/Core/Extension/ThemeHandler.php index 1988a14..c3d8684 100644 --- a/core/lib/Drupal/Core/Extension/ThemeHandler.php +++ b/core/lib/Drupal/Core/Extension/ThemeHandler.php @@ -483,4 +483,18 @@ public function getTheme($name) { throw new \InvalidArgumentException(sprintf('The theme %s does not exist.', $name)); } + /** + * {@inheritdoc} + */ + public function hasUi($name) { + $themes = $this->listInfo(); + if (isset($themes[$name])) { + if (!empty($themes[$name]->info['hidden'])) { + $theme_config = $this->configFactory->get('system.theme'); + return $name == $theme_config->get('default') || $name == $theme_config->get('admin'); + } + return TRUE; + } + return FALSE; + } } diff --git a/core/lib/Drupal/Core/Extension/ThemeHandlerInterface.php b/core/lib/Drupal/Core/Extension/ThemeHandlerInterface.php index c46f96a..8b59ae3 100644 --- a/core/lib/Drupal/Core/Extension/ThemeHandlerInterface.php +++ b/core/lib/Drupal/Core/Extension/ThemeHandlerInterface.php @@ -208,4 +208,18 @@ public function themeExists($theme); */ public function getTheme($name); + /** + * Determines if a theme should be shown in the user interface. + * + * To be shown in the UI the theme has to be installed. If the theme is hidden + * it will not be shown unless it is the default or admin theme. + * + * @param string $name + * The name of the theme to check. + * + * @return bool + * TRUE if the theme should be shown in the UI, FALSE if not. + */ + public function hasUi($name); + } diff --git a/core/modules/block/block.module b/core/modules/block/block.module index 29c6e16..f0bbdd3 100644 --- a/core/modules/block/block.module +++ b/core/modules/block/block.module @@ -88,8 +88,13 @@ function block_page_top(array &$page_top) { * An array of theme names. */ function block_themes_installed($theme_list) { + $theme_info = \Drupal::service('theme_handler')->listInfo(); foreach ($theme_list as $theme) { - block_theme_initialize($theme); + // Don't initialize hidden themes as they are not displayed in the block + // management screens. + if (empty($theme_info[$theme]->info['hidden'])) { + block_theme_initialize($theme); + } } } diff --git a/core/modules/block/src/Controller/BlockController.php b/core/modules/block/src/Controller/BlockController.php index 2779a9b..7f75d0b 100644 --- a/core/modules/block/src/Controller/BlockController.php +++ b/core/modules/block/src/Controller/BlockController.php @@ -11,6 +11,7 @@ use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Extension\ThemeHandlerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * Controller routines for admin block routes. @@ -53,6 +54,11 @@ public static function create(ContainerInterface $container) { * A #type 'page' render array containing the block region demo. */ public function demo($theme) { + // Deny access if the theme is not installed or not found. + if (!$this->themeHandler->hasUi($theme)) { + throw new NotFoundHttpException(); + } + $page = [ '#title' => Html::escape($this->themeHandler->getName($theme)), '#type' => 'page', diff --git a/core/modules/block/src/Controller/BlockListController.php b/core/modules/block/src/Controller/BlockListController.php index 72aa445..b0c5e3f 100644 --- a/core/modules/block/src/Controller/BlockListController.php +++ b/core/modules/block/src/Controller/BlockListController.php @@ -8,7 +8,10 @@ namespace Drupal\block\Controller; use Drupal\Core\Entity\Controller\EntityListController; +use Drupal\Core\Extension\ThemeHandlerInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * Defines a controller to list blocks. @@ -16,6 +19,32 @@ class BlockListController extends EntityListController { /** + * The theme handler. + * + * @var \Drupal\Core\Extension\ThemeHandlerInterface + */ + protected $themeHandler; + + /** + * Constructs the BlockListController. + * + * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler + * The theme handler. + */ + public function __construct(ThemeHandlerInterface $theme_handler) { + $this->themeHandler = $theme_handler; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('theme_handler') + ); + } + + /** * Shows the block administration page. * * @param string|null $theme @@ -28,6 +57,11 @@ class BlockListController extends EntityListController { */ public function listing($theme = NULL, Request $request = NULL) { $theme = $theme ?: $this->config('system.theme')->get('default'); + // Deny access if the theme is not installed or not found. + if (!$this->themeHandler->hasUi($theme)) { + throw new NotFoundHttpException(); + } + return $this->entityManager()->getListBuilder('block')->render($theme, $request); } diff --git a/core/modules/block/src/Plugin/Derivative/ThemeLocalTask.php b/core/modules/block/src/Plugin/Derivative/ThemeLocalTask.php index 4e337db..ed9d095 100644 --- a/core/modules/block/src/Plugin/Derivative/ThemeLocalTask.php +++ b/core/modules/block/src/Plugin/Derivative/ThemeLocalTask.php @@ -50,7 +50,7 @@ public function getDerivativeDefinitions($base_plugin_definition) { $default_theme = $this->themeHandler->getDefault(); foreach ($this->themeHandler->listInfo() as $theme_name => $theme) { - if ($theme->status) { + if ($this->themeHandler->hasUi($theme_name)) { $this->derivatives[$theme_name] = $base_plugin_definition; $this->derivatives[$theme_name]['title'] = $theme->info['name']; $this->derivatives[$theme_name]['route_parameters'] = array('theme' => $theme_name); diff --git a/core/modules/block/src/Tests/BlockHiddenRegionTest.php b/core/modules/block/src/Tests/BlockHiddenRegionTest.php index a6af2ce..bb42dce 100644 --- a/core/modules/block/src/Tests/BlockHiddenRegionTest.php +++ b/core/modules/block/src/Tests/BlockHiddenRegionTest.php @@ -56,7 +56,9 @@ public function testBlockNotInHiddenRegion() { // Install "block_test_theme" and set it as the default theme. $theme = 'block_test_theme'; - \Drupal::service('theme_handler')->install(array($theme)); + // We need to install a non-hidden theme so that there is more than one + // local task. + \Drupal::service('theme_handler')->install(array($theme, 'stark')); $this->config('system.theme') ->set('default', $theme) ->save(); diff --git a/core/modules/block/src/Tests/BlockTest.php b/core/modules/block/src/Tests/BlockTest.php index c98b042..261c3ae 100644 --- a/core/modules/block/src/Tests/BlockTest.php +++ b/core/modules/block/src/Tests/BlockTest.php @@ -197,9 +197,9 @@ function testBlock() { */ public function testBlockThemeSelector() { // Install all themes. - \Drupal::service('theme_handler')->install(array('bartik', 'seven')); + \Drupal::service('theme_handler')->install(['bartik', 'seven', 'stark']); $theme_settings = $this->config('system.theme'); - foreach (array('bartik', 'classy', 'seven') as $theme) { + foreach (['bartik', 'seven', 'stark'] as $theme) { $this->drupalGet('admin/structure/block/list/' . $theme); $this->assertTitle(t('Block layout') . ' | Drupal'); // Select the 'Powered by Drupal' block to be placed. diff --git a/core/modules/block/src/Tests/BlockUiTest.php b/core/modules/block/src/Tests/BlockUiTest.php index 25b7942..5828189 100644 --- a/core/modules/block/src/Tests/BlockUiTest.php +++ b/core/modules/block/src/Tests/BlockUiTest.php @@ -90,6 +90,10 @@ public function testBlockDemoUiPage() { \Drupal::service('theme_handler')->install(array('test_theme')); $this->drupalGet('admin/structure/block/demo/test_theme'); $this->assertEscaped('Test theme'); + + \Drupal::service('theme_handler')->install(['stable']); + $this->drupalGet('admin/structure/block/demo/stable'); + $this->assertResponse(404, 'Hidden themes that are not the default theme are not supported by the block demo screen'); } /** @@ -136,6 +140,29 @@ function testBlockAdminUiPage() { $this->drupalGet('admin/structure/block'); $element = $this->xpath('//tr[contains(@class, :class)]', [':class' => 'region-title-header']); $this->assertTrue(!empty($element)); + + // Ensure hidden themes do not appear in the UI. Enable another non base + // theme and place the local tasks block. + $this->assertTrue(\Drupal::service('theme_handler')->themeExists('classy'), 'The classy base theme is enabled'); + $this->drupalPlaceBlock('local_tasks_block', ['region' => 'header']); + \Drupal::service('theme_installer')->install(['stable']); + \Drupal::service('theme_installer')->install(['test_theme']); + $this->drupalGet('admin/structure/block'); + $theme_handler = \Drupal::service('theme_handler'); + $this->assertLink($theme_handler->getName('classy')); + $this->assertLink($theme_handler->getName('test_theme')); + $this->assertNoLink($theme_handler->getName('stable')); + + $this->drupalGet('admin/structure/block/list/stable'); + $this->assertResponse(404, 'Placing blocks through UI is not possible for a hidden base theme.'); + + \Drupal::configFactory()->getEditable('system.theme')->set('admin', 'stable')->save(); + \Drupal::service('router.builder')->rebuildIfNeeded(); + $this->drupalPlaceBlock('local_tasks_block', ['region' => 'header', 'theme' => 'stable']); + $this->drupalGet('admin/structure/block'); + $this->assertLink($theme_handler->getName('stable')); + $this->drupalGet('admin/structure/block/list/stable'); + $this->assertResponse(200, 'Placing blocks through UI is possible for a hidden base theme that is the admin theme.'); } /** diff --git a/core/modules/block/tests/src/Unit/Menu/BlockLocalTasksTest.php b/core/modules/block/tests/src/Unit/Menu/BlockLocalTasksTest.php index f70a57c..6320917 100644 --- a/core/modules/block/tests/src/Unit/Menu/BlockLocalTasksTest.php +++ b/core/modules/block/tests/src/Unit/Menu/BlockLocalTasksTest.php @@ -27,7 +27,11 @@ protected function setUp() { $themes = array(); $themes['test_a'] = (object) array( - 'status' => 0, + 'status' => 1, + 'info' => array( + 'name' => 'test_a', + 'hidden' => TRUE, + ), ); $themes['test_b'] = (object) array( 'status' => 1, @@ -45,6 +49,13 @@ protected function setUp() { $theme_handler->expects($this->any()) ->method('listInfo') ->will($this->returnValue($themes)); + $theme_handler->expects($this->any()) + ->method('hasUi') + ->willReturnMap([ + ['test_a', FALSE], + ['test_b', TRUE], + ['test_c', TRUE], + ]); $container = new ContainerBuilder(); $container->set('config.factory', $config_factory); diff --git a/core/modules/block_content/src/Tests/BlockContentTypeTest.php b/core/modules/block_content/src/Tests/BlockContentTypeTest.php index e669a72..d1405b9 100644 --- a/core/modules/block_content/src/Tests/BlockContentTypeTest.php +++ b/core/modules/block_content/src/Tests/BlockContentTypeTest.php @@ -185,17 +185,15 @@ public function testsBlockContentAddTypes() { ->getStorage('block_content'); // Install all themes. - \Drupal::service('theme_handler')->install(array('bartik', 'seven')); - $themes = array('bartik', 'seven', 'classy'); + \Drupal::service('theme_handler')->install(['bartik', 'seven', 'stark']); $theme_settings = $this->config('system.theme'); - foreach ($themes as $default_theme) { + foreach (['bartik', 'seven', 'stark'] as $default_theme) { // Change the default theme. $theme_settings->set('default', $default_theme)->save(); \Drupal::service('router.builder')->rebuild(); // For each installed theme, go to its block page and test the redirects. - $themes = array('bartik', 'classy', 'seven'); - foreach ($themes as $theme) { + foreach (['bartik', 'seven', 'stark'] as $theme) { // Test that adding a block from the 'place blocks' form sends you to the // block configure form. $path = $theme == $default_theme ? 'admin/structure/block' : "admin/structure/block/list/$theme"; diff --git a/core/modules/system/src/Form/ThemeSettingsForm.php b/core/modules/system/src/Form/ThemeSettingsForm.php index 395c743..a782e15 100644 --- a/core/modules/system/src/Form/ThemeSettingsForm.php +++ b/core/modules/system/src/Form/ThemeSettingsForm.php @@ -108,13 +108,12 @@ public function buildForm(array $form, FormStateInterface $form_state, $theme = $themes = $this->themeHandler->listInfo(); - // Deny access if the theme is not installed or not found. - if (!empty($theme) && (empty($themes[$theme]) || !$themes[$theme]->status)) { - throw new NotFoundHttpException(); - } - // Default settings are defined in theme_get_setting() in includes/theme.inc if ($theme) { + // Deny access if the theme is not installed or not found. + if (!$this->themeHandler->hasUi($theme)) { + throw new NotFoundHttpException(); + } $var = 'theme_' . $theme . '_settings'; $config_key = $theme . '.settings'; $themes = $this->themeHandler->listInfo(); diff --git a/core/modules/system/src/Plugin/Derivative/ThemeLocalTask.php b/core/modules/system/src/Plugin/Derivative/ThemeLocalTask.php index 9c9de19..7193ea5 100644 --- a/core/modules/system/src/Plugin/Derivative/ThemeLocalTask.php +++ b/core/modules/system/src/Plugin/Derivative/ThemeLocalTask.php @@ -48,7 +48,7 @@ public static function create(ContainerInterface $container, $base_plugin_id) { */ public function getDerivativeDefinitions($base_plugin_definition) { foreach ($this->themeHandler->listInfo() as $theme_name => $theme) { - if ($theme->status) { + if ($this->themeHandler->hasUi($theme_name)) { $this->derivatives[$theme_name] = $base_plugin_definition; $this->derivatives[$theme_name]['title'] = $theme->info['name']; $this->derivatives[$theme_name]['route_parameters'] = array('theme' => $theme_name); diff --git a/core/modules/system/src/SystemConfigSubscriber.php b/core/modules/system/src/SystemConfigSubscriber.php index 204d685..76ae573 100644 --- a/core/modules/system/src/SystemConfigSubscriber.php +++ b/core/modules/system/src/SystemConfigSubscriber.php @@ -7,8 +7,10 @@ namespace Drupal\system; +use Drupal\Core\Config\ConfigCrudEvent; use Drupal\Core\Config\ConfigEvents; use Drupal\Core\Config\ConfigImporterEvent; +use Drupal\Core\Routing\RouteBuilderInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -19,6 +21,35 @@ class SystemConfigSubscriber implements EventSubscriberInterface { use StringTranslationTrait; /** + * The router builder. + * + * @var \Drupal\Core\Routing\RouteBuilderInterface + */ + protected $routerBuilder; + + /** + * Constructs the SystemConfigSubscriber. + * + * @param \Drupal\Core\Routing\RouteBuilderInterface $route_builder + * The router builder service. + */ + public function __construct(RouteBuilderInterface $router_builder) { + $this->routerBuilder = $router_builder; + } + + /** + * Rebuilds the router when the default or admin theme is changed. + * + * @param \Drupal\Core\Config\ConfigCrudEvent $event + */ + public function onConfigSave(ConfigCrudEvent $event) { + $saved_config = $event->getConfig(); + if ($saved_config->getName() == 'system.theme' && ($event->isChanged('admin') || $event->isChanged('default'))) { + $this->routerBuilder->setRebuildNeeded(); + } + } + + /** * Checks that the configuration synchronization is valid. * * This event listener prevents deleting all configuration. If there is @@ -55,6 +86,7 @@ public function onConfigImporterValidateSiteUUID(ConfigImporterEvent $event) { * {@inheritdoc} */ public static function getSubscribedEvents() { + $events[ConfigEvents::SAVE][] = array('onConfigSave', 0); // The empty check has a high priority so that is can stop propagation if // there is no configuration to import. $events[ConfigEvents::IMPORT_VALIDATE][] = array('onConfigImporterValidateNotEmpty', 512); diff --git a/core/modules/system/src/Tests/System/ThemeTest.php b/core/modules/system/src/Tests/System/ThemeTest.php index 8b2836b..2f636d9 100644 --- a/core/modules/system/src/Tests/System/ThemeTest.php +++ b/core/modules/system/src/Tests/System/ThemeTest.php @@ -52,6 +52,9 @@ function testThemeSettings() { $this->assertResponse(404, 'The theme settings form URL for a uninstalled theme could not be found.'); $this->drupalGet('admin/appearance/settings/' . $this->randomMachineName()); $this->assertResponse(404, 'The theme settings form URL for a non-existent theme could not be found.'); + $this->assertTrue(\Drupal::service('theme_installer')->install(['stable'])); + $this->drupalGet('admin/appearance/settings/stable'); + $this->assertResponse(404, 'The theme settings form URL for a hidden theme are unavailable.'); // Specify a filesystem path to be used for the logo. $file = current($this->drupalGetTestFiles('image')); @@ -190,6 +193,23 @@ function testThemeSettings() { // The logo field should only be present on the global theme settings form. $this->assertNoFieldByName('logo_path'); $this->drupalPostForm(NULL, [], t('Save configuration')); + + // Ensure only vlaid themes are listed in the local tasks. + $this->drupalPlaceBlock('local_tasks_block', ['region' => 'header']); + $this->drupalGet('admin/appearance/settings'); + $theme_handler = \Drupal::service('theme_handler'); + $this->assertLink($theme_handler->getName('classy')); + $this->assertLink($theme_handler->getName('bartik')); + $this->assertNoLink($theme_handler->getName('stable')); + + // If a hidden theme is an admin theme it should be viewable. + \Drupal::configFactory()->getEditable('system.theme')->set('admin', 'stable')->save(); + \Drupal::service('router.builder')->rebuildIfNeeded(); + $this->drupalPlaceBlock('local_tasks_block', ['region' => 'header', 'theme' => 'stable']); + $this->drupalGet('admin/appearance/settings'); + $this->assertLink($theme_handler->getName('stable')); + $this->drupalGet('admin/appearance/settings/stable'); + $this->assertResponse(200, 'The theme settings form URL for a hidden theme that is the admin theme are avialable.'); } /** @@ -255,8 +275,14 @@ function testAdministrationTheme() { * Test switching the default theme. */ function testSwitchDefaultTheme() { + /** @var \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler */ + $theme_handler = \Drupal::service('theme_handler'); + // First, Install Stark and set it as the default theme programmatically. + $theme_handler->install(array('stark')); + $theme_handler->setDefault('stark'); + // Install Bartik and set it as the default theme. - \Drupal::service('theme_handler')->install(array('bartik')); + $theme_handler->install(array('bartik')); $this->drupalGet('admin/appearance'); $this->clickLink(t('Set as default')); $this->assertEqual($this->config('system.theme')->get('default'), 'bartik'); @@ -266,10 +292,10 @@ function testSwitchDefaultTheme() { $this->assertText('Bartik(' . t('active tab') . ')', 'Default local task on blocks admin page is the default theme.'); // Switch back to Stark and test again to test that the menu cache is cleared. $this->drupalGet('admin/appearance'); - // Classy is the first 'Set as default' link. - $this->clickLink(t('Set as default'), 0); + // Stark is the first 'Set as default' link. + $this->clickLink(t('Set as default')); $this->drupalGet('admin/structure/block'); - $this->assertText('Classy(' . t('active tab') . ')', 'Default local task on blocks admin page has changed.'); + $this->assertText('Stark(' . t('active tab') . ')', 'Default local task on blocks admin page has changed.'); } /** @@ -328,8 +354,8 @@ function testUninstallingThemes() { // base theme of bartik. $this->assertNoRaw('Uninstall Classy theme', 'A link to uninstall the Classy theme does not appear on the theme settings page.'); - // Change the default theme to stark, stark is third in the list. - $this->clickLink(t('Set as default'), 2); + // Change the default theme to stark, stark is second in the list. + $this->clickLink(t('Set as default'), 1); // Check that bartik can be uninstalled now. $this->assertRaw('Uninstall Bartik theme', 'A link to uninstall the Bartik theme does appear on the theme settings page.'); @@ -344,9 +370,9 @@ function testUninstallingThemes() { // Seven is the second in the list. $this->clickLink(t('Uninstall')); $this->assertRaw('The Seven theme has been uninstalled'); - // Now uninstall classy. - $this->clickLink(t('Uninstall')); - $this->assertRaw('The Classy theme has been uninstalled'); + + // Check that the classy theme still can't be uninstalled as it is hidden. + $this->assertNoRaw('Uninstall Classy theme', 'A link to uninstall the Classy theme does not appear on the theme settings page.'); } /** diff --git a/core/modules/system/system.services.yml b/core/modules/system/system.services.yml index 0f816d6..c70889d 100644 --- a/core/modules/system/system.services.yml +++ b/core/modules/system/system.services.yml @@ -35,6 +35,7 @@ services: - { name: theme_negotiator, priority: 100 } system.config_subscriber: class: Drupal\system\SystemConfigSubscriber + arguments: ['@router.builder'] tags: - { name: event_subscriber } system.config_cache_tag: diff --git a/core/modules/system/tests/src/Unit/Menu/SystemLocalTasksTest.php b/core/modules/system/tests/src/Unit/Menu/SystemLocalTasksTest.php index 802f5ef..54e6790 100644 --- a/core/modules/system/tests/src/Unit/Menu/SystemLocalTasksTest.php +++ b/core/modules/system/tests/src/Unit/Menu/SystemLocalTasksTest.php @@ -44,6 +44,10 @@ protected function setUp() { ->will($this->returnValue(array( 'bartik' => $theme, ))); + $this->themeHandler->expects($this->any()) + ->method('hasUi') + ->with('bartik') + ->willReturn(TRUE); $this->container->set('theme_handler', $this->themeHandler); } diff --git a/core/profiles/standard/config/install/block.block.classy_page_title.yml b/core/profiles/standard/config/install/block.block.classy_page_title.yml deleted file mode 100644 index 4236224..0000000 --- a/core/profiles/standard/config/install/block.block.classy_page_title.yml +++ /dev/null @@ -1,17 +0,0 @@ -langcode: en -status: true -dependencies: - theme: - - classy -id: classy_page_title -theme: classy -region: content -weight: -50 -provider: null -plugin: page_title_block -settings: - id: page_title_block - label: 'Page title' - provider: core - label_display: '0' -visibility: { } diff --git a/core/themes/classy/classy.info.yml b/core/themes/classy/classy.info.yml index 2b7705b..5ed9b80 100644 --- a/core/themes/classy/classy.info.yml +++ b/core/themes/classy/classy.info.yml @@ -5,6 +5,7 @@ package: Core version: VERSION core: 8.x base theme: false +hidden: true libraries: - classy/base diff --git a/core/themes/stable/stable.info.yml b/core/themes/stable/stable.info.yml index 687c434..960a683 100644 --- a/core/themes/stable/stable.info.yml +++ b/core/themes/stable/stable.info.yml @@ -5,3 +5,4 @@ package: Core version: VERSION core: 8.x base theme: false +hidden: true