Commit a2988f1f authored by catch's avatar catch

Issue #2574975 by alexpott, Cottser, izus: Allow (base-)themes to be excluded...

Issue #2574975 by alexpott, Cottser, izus: Allow (base-)themes to be excluded from the UI (e.g. blocks, Appearance)
parent b841a563
......@@ -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;
}
}
......@@ -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);
}
......@@ -89,7 +89,10 @@ function block_page_top(array &$page_top) {
*/
function block_themes_installed($theme_list) {
foreach ($theme_list as $theme) {
block_theme_initialize($theme);
// Don't initialize themes that are not displayed in the UI.
if (\Drupal::service('theme_handler')->hasUi($theme)) {
block_theme_initialize($theme);
}
}
}
......
......@@ -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,10 @@ public static function create(ContainerInterface $container) {
* A #type 'page' render array containing the block region demo.
*/
public function demo($theme) {
if (!$this->themeHandler->hasUi($theme)) {
throw new NotFoundHttpException();
}
$page = [
'#title' => Html::escape($this->themeHandler->getName($theme)),
'#type' => 'page',
......
......@@ -8,13 +8,42 @@
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.
*/
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.
*
......@@ -28,6 +57,10 @@ class BlockListController extends EntityListController {
*/
public function listing($theme = NULL, Request $request = NULL) {
$theme = $theme ?: $this->config('system.theme')->get('default');
if (!$this->themeHandler->hasUi($theme)) {
throw new NotFoundHttpException();
}
return $this->entityManager()->getListBuilder('block')->render($theme, $request);
}
......
......@@ -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);
......
......@@ -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();
......
......@@ -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.
......
......@@ -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('<strong>Test theme</strong>');
\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,28 @@ 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', 'stark']);
$this->drupalGet('admin/structure/block');
$theme_handler = \Drupal::service('theme_handler');
$this->assertLink($theme_handler->getName('classy'));
$this->assertLink($theme_handler->getName('stark'));
$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.');
}
/**
......
......@@ -65,6 +65,14 @@ function testNewDefaultThemeBlocks() {
unset($new_blocks[str_replace($default_theme . '_', $new_theme . '_', $default_block_name)]);
}
$this->assertTrue(empty($new_blocks), 'The new theme has exactly the same blocks as the previous default theme.');
// Install a hidden base theme and ensure blocks are not copied.
$base_theme = 'test_basetheme';
\Drupal::service('theme_handler')->install([$base_theme]);
$new_blocks = $this->container->get('entity.query')->get('block')
->condition('theme', $base_theme)
->execute();
$this->assertTrue(empty($new_blocks), 'Installing a hidden base theme does not copy blocks from the default theme.');
}
}
......@@ -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);
......
......@@ -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";
......
......@@ -29,27 +29,17 @@ class ThemeController extends ControllerBase {
*/
protected $themeHandler;
/**
* The route builder service.
*
* @var \Drupal\Core\Routing\RouteBuilderInterface
*/
protected $routeBuilder;
/**
* Constructs a new ThemeController.
*
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler.
* @param \Drupal\Core\Routing\RouteBuilderInterface $route_builder
* The route builder.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
*/
public function __construct(ThemeHandlerInterface $theme_handler, RouteBuilderInterface $route_builder, ConfigFactoryInterface $config_factory) {
public function __construct(ThemeHandlerInterface $theme_handler,ConfigFactoryInterface $config_factory) {
$this->themeHandler = $theme_handler;
$this->configFactory = $config_factory;
$this->routeBuilder = $route_builder;
}
/**
......@@ -58,7 +48,6 @@ public function __construct(ThemeHandlerInterface $theme_handler, RouteBuilderIn
public static function create(ContainerInterface $container) {
return new static(
$container->get('theme_handler'),
$container->get('router.builder'),
$container->get('config.factory')
);
}
......@@ -183,8 +172,6 @@ public function setDefaultTheme(Request $request) {
// Set the default theme.
$config->set('default', $theme)->save();
$this->routeBuilder->setRebuildNeeded();
// The status message depends on whether an admin theme is currently in
// use: a value of 0 means the admin theme is set to be the default
// theme.
......
......@@ -108,13 +108,11 @@ 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) {
if (!$this->themeHandler->hasUi($theme)) {
throw new NotFoundHttpException();
}
$var = 'theme_' . $theme . '_settings';
$config_key = $theme . '.settings';
$themes = $this->themeHandler->listInfo();
......
......@@ -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);
......
......@@ -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;
......@@ -18,6 +20,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.
*
......@@ -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);
......
......@@ -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 is 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 valid 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 is available.');
}
/**
......@@ -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 <em class="placeholder">Seven</em> theme has been uninstalled');
// Now uninstall classy.
$this->clickLink(t('Uninstall'));
$this->assertRaw('The <em class="placeholder">Classy</em> 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.');
}
/**
......
......@@ -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:
......
......@@ -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);
}
......
......@@ -4,6 +4,8 @@ description: 'Test theme which acts as a base theme for other test subthemes.'
version: VERSION
core: 8.x
base theme: false
hidden: true
libraries:
- test_basetheme/global-styling
stylesheets-remove:
......
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: { }
......@@ -5,6 +5,7 @@ package: Core
version: VERSION
core: 8.x
base theme: false
hidden: true
libraries:
- classy/base
......
......@@ -5,3 +5,4 @@ package: Core
version: VERSION
core: 8.x
base theme: false
hidden: true
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment