summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNathaniel Catchpole2013-11-19 12:22:44 (GMT)
committerNathaniel Catchpole2013-11-19 12:24:14 (GMT)
commitfc04601c357609f6cae7cb3ae6c71610d44647e5 (patch)
treeaa158d635a155a8e0c4de4304f69b8e87d1d461b
parent14336d94b8da95f2c2f26edb7ffb5c12d58a1212 (diff)
Issue #1954892 by dawehner, tim.plunkett, David_Rothstein, effulgentsia: Replace 'theme callback' and hook_custom_theme() with a clean theme negotiation system.
-rw-r--r--core/core.services.yml15
-rw-r--r--core/includes/ajax.inc36
-rw-r--r--core/includes/common.inc3
-rw-r--r--core/includes/menu.inc63
-rw-r--r--core/includes/theme.inc17
-rw-r--r--core/lib/Drupal/Core/CoreServiceProvider.php4
-rw-r--r--core/lib/Drupal/Core/EventSubscriber/LegacyRequestSubscriber.php15
-rw-r--r--core/lib/Drupal/Core/Theme/AjaxBasePageNegotiator.php84
-rw-r--r--core/lib/Drupal/Core/Theme/DefaultNegotiator.php42
-rw-r--r--core/lib/Drupal/Core/Theme/ThemeNegotiator.php133
-rw-r--r--core/lib/Drupal/Core/Theme/ThemeNegotiatorInterface.php41
-rw-r--r--core/lib/Drupal/Core/Theme/ThemeNegotiatorPass.php36
-rw-r--r--core/misc/ajax.js2
-rw-r--r--core/modules/block/block.module32
-rw-r--r--core/modules/block/block.services.yml5
-rw-r--r--core/modules/block/lib/Drupal/block/Theme/AdminDemoNegotiator.php30
-rw-r--r--core/modules/content_translation/content_translation.module2
-rw-r--r--core/modules/contextual/contextual.module15
-rw-r--r--core/modules/contextual/contextual.routing.yml2
-rw-r--r--core/modules/edit/edit.module20
-rw-r--r--core/modules/edit/edit.routing.yml6
-rw-r--r--core/modules/editor/editor.module15
-rw-r--r--core/modules/editor/editor.routing.yml2
-rw-r--r--core/modules/file/file.module15
-rw-r--r--core/modules/file/file.routing.yml2
-rw-r--r--core/modules/search/lib/Drupal/search/Tests/SearchPageOverrideTest.php2
-rw-r--r--core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutLinksTest.php6
-rw-r--r--core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutTestBase.php2
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php73
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/System/ThemeTest.php2
-rw-r--r--core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php12
-rw-r--r--core/modules/system/lib/Drupal/system/Theme/BatchNegotiator.php54
-rw-r--r--core/modules/system/system.api.php30
-rw-r--r--core/modules/system/system.install14
-rw-r--r--core/modules/system/system.module41
-rw-r--r--core/modules/system/system.routing.yml2
-rw-r--r--core/modules/system/system.services.yml5
-rw-r--r--core/modules/system/tests/modules/ajax_test/ajax_test.module13
-rw-r--r--core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml2
-rw-r--r--core/modules/system/tests/modules/menu_test/lib/Drupal/menu_test/EventSubscriber/ActiveTrailSubscriber.php68
-rw-r--r--core/modules/system/tests/modules/menu_test/lib/Drupal/menu_test/Theme/TestThemeNegotiator.php43
-rw-r--r--core/modules/system/tests/modules/menu_test/menu_test.module71
-rw-r--r--core/modules/system/tests/modules/menu_test/menu_test.routing.yml4
-rw-r--r--core/modules/system/tests/modules/menu_test/menu_test.services.yml11
-rw-r--r--core/modules/system/tests/modules/theme_test/lib/Drupal/theme_test/Theme/CustomThemeNegotiator.php29
-rw-r--r--core/modules/system/tests/modules/theme_test/lib/Drupal/theme_test/Theme/HighPriorityThemeNegotiator.php28
-rw-r--r--core/modules/system/tests/modules/theme_test/theme_test.module7
-rw-r--r--core/modules/system/tests/modules/theme_test/theme_test.routing.yml16
-rw-r--r--core/modules/system/tests/modules/theme_test/theme_test.services.yml10
-rw-r--r--core/modules/taxonomy/lib/Drupal/taxonomy/Tests/ThemeTest.php4
-rw-r--r--core/modules/text/lib/Drupal/text/Tests/Formatter/TextPlainUnitTest.php1
-rw-r--r--core/modules/user/lib/Drupal/user/Theme/AdminNegotiator.php71
-rw-r--r--core/modules/user/lib/Drupal/user/Theme/UserNegotiator.php61
-rw-r--r--core/modules/user/user.services.yml10
-rw-r--r--core/modules/views/lib/Drupal/views/Tests/Handler/FieldCounterTest.php8
-rw-r--r--core/modules/views/lib/Drupal/views/Tests/Handler/FieldUnitTest.php8
-rw-r--r--core/modules/views/lib/Drupal/views/Tests/Plugin/DisplayPageTest.php1
-rw-r--r--core/modules/views/lib/Drupal/views/ViewExecutable.php22
-rw-r--r--core/modules/views/views.module14
-rw-r--r--core/modules/views/views.routing.yml2
-rw-r--r--core/tests/Drupal/Tests/Core/Theme/ThemeNegotiatorTest.php141
61 files changed, 1045 insertions, 480 deletions
diff --git a/core/core.services.yml b/core/core.services.yml
index d341606..4e28f55 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -165,6 +165,21 @@ services:
calls:
- [addSubscriber, ['@http_client_simpletest_subscriber']]
- [setUserAgent, ['Drupal (+http://drupal.org/)']]
+ theme.negotiator:
+ class: Drupal\Core\Theme\ThemeNegotiator
+ arguments: ['@access_check.theme']
+ calls:
+ - [setRequest, ['@request']]
+ theme.negotiator.default:
+ class: Drupal\Core\Theme\DefaultNegotiator
+ arguments: ['@config.factory']
+ tags:
+ - { name: theme_negotiator, priority: -100 }
+ theme.negotiator.ajax_base_page:
+ class: Drupal\Core\Theme\AjaxBasePageNegotiator
+ arguments: ['@csrf_token', '@config.factory']
+ tags:
+ - { name: theme_negotiator, priority: 1000 }
container.namespaces:
class: ArrayObject
arguments: [ '%container.namespaces%' ]
diff --git a/core/includes/ajax.inc b/core/includes/ajax.inc
index 3f2389f..3ff6c17 100644
--- a/core/includes/ajax.inc
+++ b/core/includes/ajax.inc
@@ -303,42 +303,6 @@ function ajax_render($commands = array()) {
}
/**
- * Theme callback: Returns the correct theme for an Ajax request.
- *
- * Many different pages can invoke an Ajax request to system/ajax or another
- * generic Ajax path. It is almost always desired for an Ajax response to be
- * rendered using the same theme as the base page, because most themes are built
- * with the assumption that they control the entire page, so if the CSS for two
- * themes are both loaded for a given page, they may conflict with each other.
- * For example, Bartik is Drupal's default theme, and Seven is Drupal's default
- * administration theme. Depending on whether the "Use the administration theme
- * when editing or creating content" checkbox is checked, the node edit form may
- * be displayed in either theme, but the Ajax response to the Field module's
- * "Add another item" button should be rendered using the same theme as the rest
- * of the page. Therefore, system_menu() sets the 'theme callback' for
- * 'system/ajax' to this function, and it is recommended that modules
- * implementing other generic Ajax paths do the same.
- *
- * @see system_menu()
- * @see file_menu()
- */
-function ajax_base_page_theme() {
- if (!empty($_POST['ajax_page_state']['theme']) && !empty($_POST['ajax_page_state']['theme_token'])) {
- $theme = $_POST['ajax_page_state']['theme'];
- $token = $_POST['ajax_page_state']['theme_token'];
-
- // Prevent a request forgery from giving a person access to a theme they
- // shouldn't be otherwise allowed to see. However, since everyone is allowed
- // to see the default theme, token validation isn't required for that, and
- // bypassing it allows most use-cases to work even when accessed from the
- // page cache.
- if ($theme === \Drupal::config('system.theme')->get('default') || drupal_valid_token($token, $theme)) {
- return $theme;
- }
- }
-}
-
-/**
* Converts the return value of a page callback into an Ajax commands array.
*
* @param $page_callback_result
diff --git a/core/includes/common.inc b/core/includes/common.inc
index 90641b6..138c11a 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -2330,7 +2330,7 @@ function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALS
global $theme_key;
// Provide the page with information about the theme that's used, so that
// a later AJAX request can be rendered using the same theme.
- // @see ajax_base_page_theme()
+ // @see \Drupal\Core\Theme\AjaxBasePageNegotiator
$setting['ajaxPageState']['theme'] = $theme_key;
// Checks that the DB is available before filling theme_token.
if (!defined('MAINTENANCE_MODE')) {
@@ -3147,7 +3147,6 @@ function _drupal_bootstrap_full($skip = FALSE) {
// Let all modules take action before the menu system handles the request.
// We do not want this while running update.php.
if (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update') {
- menu_set_custom_theme();
drupal_theme_initialize();
}
}
diff --git a/core/includes/menu.inc b/core/includes/menu.inc
index a8a30b6..2e2c50d 100644
--- a/core/includes/menu.inc
+++ b/core/includes/menu.inc
@@ -463,8 +463,8 @@ function menu_set_item($path, $router_item) {
* menu_router table. The value corresponding to the key 'map' holds the
* loaded objects. The value corresponding to the key 'access' is TRUE if the
* current user can access this page. The values corresponding to the keys
- * 'title', 'page_arguments', 'access_arguments', and 'theme_arguments' will
- * be filled in based on the database values and the objects loaded.
+ * 'title', 'page_arguments', and 'access_arguments', will be filled in based
+ * on the database values and the objects loaded.
*/
function menu_get_item($path = NULL, $router_item = NULL) {
$router_items = &drupal_static(__FUNCTION__);
@@ -501,7 +501,6 @@ function menu_get_item($path = NULL, $router_item = NULL) {
if ($router_item['access']) {
$router_item['map'] = $map;
$router_item['page_arguments'] = array_merge(menu_unserialize($router_item['page_arguments'], $map), array_slice($map, $router_item['number_parts']));
- $router_item['theme_arguments'] = array_merge(menu_unserialize($router_item['theme_arguments'], $map), array_slice($map, $router_item['number_parts']));
}
}
$router_items[$path] = $router_item;
@@ -1796,51 +1795,6 @@ function drupal_help_arg($arg = array()) {
}
/**
- * Gets the custom theme for the current page, if there is one.
- *
- * @param $initialize
- * This parameter should only be used internally; it is set to TRUE in order
- * to force the custom theme to be initialized for the current page request.
- *
- * @return
- * The machine-readable name of the custom theme, if there is one.
- *
- * @see menu_set_custom_theme()
- */
-function menu_get_custom_theme($initialize = FALSE) {
- $custom_theme = &drupal_static(__FUNCTION__);
- // Skip this if the site is offline or being installed or updated, since the
- // menu system may not be correctly initialized then.
- if ($initialize && !_menu_site_is_offline(TRUE) && (!defined('MAINTENANCE_MODE') || (MAINTENANCE_MODE != 'update' && MAINTENANCE_MODE != 'install'))) {
- // First allow modules to dynamically set a custom theme for the current
- // page. Since we can only have one, the last module to return a valid
- // theme takes precedence.
- $custom_themes = array_filter(\Drupal::moduleHandler()->invokeAll('custom_theme'), 'drupal_theme_access');
- if (!empty($custom_themes)) {
- $custom_theme = array_pop($custom_themes);
- }
- // If there is a theme callback function for the current page, execute it.
- // If this returns a valid theme, it will override any theme that was set
- // by a hook_custom_theme() implementation above.
- $router_item = menu_get_item();
- if (!empty($router_item['access']) && !empty($router_item['theme_callback'])) {
- $theme_name = call_user_func_array($router_item['theme_callback'], $router_item['theme_arguments']);
- if (drupal_theme_access($theme_name)) {
- $custom_theme = $theme_name;
- }
- }
- }
- return $custom_theme;
-}
-
-/**
- * Sets a custom theme for the current page, if there is one.
- */
-function menu_set_custom_theme() {
- menu_get_custom_theme(TRUE);
-}
-
-/**
* Returns an array containing the names of system-defined (default) menus.
*/
function menu_list_system_menus() {
@@ -3066,13 +3020,6 @@ function _menu_router_build($callbacks, $save = FALSE) {
}
}
}
- // Same for theme callbacks.
- if (!isset($item['theme callback']) && isset($parent['theme callback'])) {
- $item['theme callback'] = $parent['theme callback'];
- if (!isset($item['theme arguments']) && isset($parent['theme arguments'])) {
- $item['theme arguments'] = $parent['theme arguments'];
- }
- }
// Same for load arguments: if a loader doesn't have any explict
// arguments, try to find arguments in the parent.
if (!isset($item['load arguments'])) {
@@ -3109,8 +3056,6 @@ function _menu_router_build($callbacks, $save = FALSE) {
'page callback' => '',
'title arguments' => array(),
'title callback' => 't',
- 'theme arguments' => array(),
- 'theme callback' => '',
'description' => '',
'description arguments' => array(),
'description callback' => 't',
@@ -3175,8 +3120,6 @@ function _menu_router_save($menu, $masks) {
'title',
'title_callback',
'title_arguments',
- 'theme_callback',
- 'theme_arguments',
'type',
'description',
'description_callback',
@@ -3207,8 +3150,6 @@ function _menu_router_save($menu, $masks) {
'title' => $item['title'],
'title_callback' => $item['title callback'],
'title_arguments' => ($item['title arguments'] ? serialize($item['title arguments']) : ''),
- 'theme_callback' => $item['theme callback'],
- 'theme_arguments' => serialize($item['theme arguments']),
'type' => $item['type'],
'description' => $item['description'],
'description_callback' => $item['description callback'],
diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index 069b497..07fda79 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -92,16 +92,14 @@ function drupal_theme_initialize() {
}
drupal_bootstrap(DRUPAL_BOOTSTRAP_DATABASE);
- $themes = list_themes();
- // Only select the user selected theme if it is available in the
- // list of themes that can be accessed.
- $theme = !empty($user->theme) && drupal_theme_access($user->theme) ? $user->theme : \Drupal::config('system.theme')->get('default');
+ $themes = list_themes();
- // Allow modules to override the theme. Validation has already been performed
- // inside menu_get_custom_theme(), so we do not need to check it again here.
- $custom_theme = menu_get_custom_theme();
- $theme = !empty($custom_theme) ? $custom_theme : $theme;
+ // @todo Let the theme.negotiator listen to the kernel request event.
+ // Determine the active theme for the theme negotiator service. This includes
+ // the default theme as well as really specific ones like the ajax base theme.
+ $request = \Drupal::request();
+ $theme = \Drupal::service('theme.negotiator')->determineActiveTheme($request) ?: 'stark';
// Store the identifier for retrieving theme settings with.
$theme_key = $theme;
@@ -114,9 +112,6 @@ function drupal_theme_initialize() {
$base_theme[] = $themes[$ancestor];
}
_drupal_theme_initialize($themes[$theme], array_reverse($base_theme));
-
- // Themes can have alter functions, so reset the drupal_alter() cache.
- drupal_static_reset('drupal_alter');
}
/**
diff --git a/core/lib/Drupal/Core/CoreServiceProvider.php b/core/lib/Drupal/Core/CoreServiceProvider.php
index 8a525c9..098b165 100644
--- a/core/lib/Drupal/Core/CoreServiceProvider.php
+++ b/core/lib/Drupal/Core/CoreServiceProvider.php
@@ -23,6 +23,7 @@ use Drupal\Core\DependencyInjection\Compiler\RegisterStringTranslatorsPass;
use Drupal\Core\DependencyInjection\Compiler\RegisterBreadcrumbBuilderPass;
use Drupal\Core\DependencyInjection\Compiler\RegisterAuthenticationPass;
use Drupal\Core\DependencyInjection\Compiler\RegisterTwigExtensionsPass;
+use Drupal\Core\Theme\ThemeNegotiatorPass;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Definition;
@@ -71,6 +72,9 @@ class CoreServiceProvider implements ServiceProviderInterface {
// Add the compiler pass that will process the tagged breadcrumb builder
// services.
$container->addCompilerPass(new RegisterBreadcrumbBuilderPass());
+ // Add the compiler pass that will process the tagged theme negotiator
+ // service.
+ $container->addCompilerPass(new ThemeNegotiatorPass());
// Add the compiler pass that lets service providers modify existing
// service definitions.
$container->addCompilerPass(new ModifyServiceDefinitionsPass());
diff --git a/core/lib/Drupal/Core/EventSubscriber/LegacyRequestSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/LegacyRequestSubscriber.php
index 39828ef..2a495f4 100644
--- a/core/lib/Drupal/Core/EventSubscriber/LegacyRequestSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/LegacyRequestSubscriber.php
@@ -27,9 +27,6 @@ class LegacyRequestSubscriber implements EventSubscriberInterface {
*/
public function onKernelRequestLegacy(GetResponseEvent $event) {
if ($event->getRequestType() == HttpKernelInterface::MASTER_REQUEST) {
- menu_set_custom_theme();
- drupal_theme_initialize();
-
// Tell Drupal it is now fully bootstrapped (for the benefit of code that
// calls drupal_get_bootstrap_phase()), but without having
// _drupal_bootstrap_full() do anything, since we've already done the
@@ -40,6 +37,16 @@ class LegacyRequestSubscriber implements EventSubscriberInterface {
}
/**
+ * Initializes the theme system after the routing system.
+ *
+ * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
+ * The Event to process.
+ */
+ public function onKernelRequestLegacyAfterRouting(GetResponseEvent $event) {
+ drupal_theme_initialize();
+ }
+
+ /**
* Registers the methods in this class that should be listeners.
*
* @return array
@@ -47,6 +54,8 @@ class LegacyRequestSubscriber implements EventSubscriberInterface {
*/
static function getSubscribedEvents() {
$events[KernelEvents::REQUEST][] = array('onKernelRequestLegacy', 90);
+ // Initialize the theme system after the routing system.
+ $events[KernelEvents::REQUEST][] = array('onKernelRequestLegacyAfterRouting', 30);
return $events;
}
diff --git a/core/lib/Drupal/Core/Theme/AjaxBasePageNegotiator.php b/core/lib/Drupal/Core/Theme/AjaxBasePageNegotiator.php
new file mode 100644
index 0000000..d2d2004
--- /dev/null
+++ b/core/lib/Drupal/Core/Theme/AjaxBasePageNegotiator.php
@@ -0,0 +1,84 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Theme\AjaxBasePageNegotiator.
+ */
+
+namespace Drupal\Core\Theme;
+
+use Drupal\Core\Access\CsrfTokenGenerator;
+use Drupal\Core\Config\ConfigFactory;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Defines a theme negotiator that deals with the active theme on ajax requests.
+ *
+ * Many different pages can invoke an Ajax request to system/ajax or another
+ * generic Ajax path. It is almost always desired for an Ajax response to be
+ * rendered using the same theme as the base page, because most themes are built
+ * with the assumption that they control the entire page, so if the CSS for two
+ * themes are both loaded for a given page, they may conflict with each other.
+ * For example, Bartik is Drupal's default theme, and Seven is Drupal's default
+ * administration theme. Depending on whether the "Use the administration theme
+ * when editing or creating content" checkbox is checked, the node edit form may
+ * be displayed in either theme, but the Ajax response to the Field module's
+ * "Add another item" button should be rendered using the same theme as the rest
+ * of the page.
+ *
+ * Therefore specify '_theme: ajax_base_page' as part of the router options.
+ */
+class AjaxBasePageNegotiator implements ThemeNegotiatorInterface {
+
+ /**
+ * The CSRF token generator.
+ *
+ * @var \Drupal\Core\Access\CsrfTokenGenerator
+ */
+ protected $csrfGenerator;
+
+ /**
+ * The config factory.
+ *
+ * @var \Drupal\Core\Config\ConfigFactory
+ */
+ protected $configFactory;
+
+ /**
+ * Constructs a new AjaxBasePageNegotiator.
+ *
+ * @param \Drupal\Core\Access\CsrfTokenGenerator $token_generator
+ * The CSRF token generator.
+ * @param \Drupal\Core\Config\ConfigFactory $config_factory
+ * The config factory.
+ */
+ public function __construct(CsrfTokenGenerator $token_generator, ConfigFactory $config_factory) {
+ $this->csrfGenerator = $token_generator;
+ $this->configFactory = $config_factory;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function determineActiveTheme(Request $request) {
+ // Check whether the route was configured to use the base page theme.
+ if (!(($route = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)) && $route->hasOption('_theme') && $route->getOption('_theme') == 'ajax_base_page')) {
+ return NULL;
+ }
+ if (($ajax_page_state = $request->request->get('ajax_page_state')) && !empty($ajax_page_state['theme']) && !empty($ajax_page_state['theme_token'])) {
+ $theme = $ajax_page_state['theme'];
+ $token = $ajax_page_state['theme_token'];
+
+ // Prevent a request forgery from giving a person access to a theme they
+ // shouldn't be otherwise allowed to see. However, since everyone is allowed
+ // to see the default theme, token validation isn't required for that, and
+ // bypassing it allows most use-cases to work even when accessed from the
+ // page cache.
+ if ($theme === $this->configFactory->get('system.theme')->get('default') || $this->csrfGenerator->validate($token, $theme)) {
+ return $theme;
+ }
+ }
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Theme/DefaultNegotiator.php b/core/lib/Drupal/Core/Theme/DefaultNegotiator.php
new file mode 100644
index 0000000..d596678
--- /dev/null
+++ b/core/lib/Drupal/Core/Theme/DefaultNegotiator.php
@@ -0,0 +1,42 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Theme\DefaultNegotiator.
+ */
+
+namespace Drupal\Core\Theme;
+
+use Drupal\Core\Config\ConfigFactory;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Determines the default theme of the site.
+ */
+class DefaultNegotiator implements ThemeNegotiatorInterface {
+
+ /**
+ * The system theme config object.
+ *
+ * @var \Drupal\Core\Config\Config
+ */
+ protected $config;
+
+ /**
+ * Constructs a DefaultNegotiator object.
+ *
+ * @param \Drupal\Core\Config\ConfigFactory $config_factory
+ * The config factory.
+ */
+ public function __construct(ConfigFactory $config_factory) {
+ $this->config = $config_factory->get('system.theme');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function determineActiveTheme(Request $request) {
+ return $this->config->get('default');
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Theme/ThemeNegotiator.php b/core/lib/Drupal/Core/Theme/ThemeNegotiator.php
new file mode 100644
index 0000000..96216ca
--- /dev/null
+++ b/core/lib/Drupal/Core/Theme/ThemeNegotiator.php
@@ -0,0 +1,133 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Theme\ThemeNegotiator.
+ */
+
+namespace Drupal\Core\Theme;
+
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Provides a class which determines the active theme of the page.
+ *
+ * It therefore uses ThemeNegotiatorInterface objects which are passed in
+ * using the 'theme_negotiator' tag.
+ *
+ * @see \Drupal\Core\Theme\ThemeNegotiatorPass
+ * @see \Drupal\Core\Theme\ThemeNegotiatorInterface
+ */
+class ThemeNegotiator implements ThemeNegotiatorInterface {
+
+ /**
+ * Holds arrays of theme negotiators, keyed by priority.
+ *
+ * @var array
+ */
+ protected $negotiators = array();
+
+ /**
+ * Holds the array of theme negotiators sorted by priority.
+ *
+ * Set to NULL if the array needs to be re-calculated.
+ *
+ * @var array|NULL
+ */
+ protected $sortedNegotiators;
+
+ /**
+ * The current request.
+ *
+ * @var \Symfony\Component\HttpFoundation\Request
+ */
+ protected $request;
+
+ /**
+ * The access checker for themes.
+ *
+ * @var \Drupal\Core\Theme\ThemeAccessCheck
+ */
+ protected $themeAccess;
+
+ /**
+ * Constructs a new ThemeNegotiator.
+ *
+ * @param \Drupal\Core\Theme\ThemeAccessCheck $theme_access
+ * The access checker for themes.
+ */
+ public function __construct(ThemeAccessCheck $theme_access) {
+ $this->themeAccess = $theme_access;
+ }
+
+ /**
+ * Sets the request object to use.
+ *
+ * @param \Symfony\Component\HttpFoundation\Request $request
+ * The request object.
+ */
+ public function setRequest(Request $request) {
+ $this->request = $request;
+ }
+
+ /**
+ * Adds a active theme negotiation service.
+ *
+ * @param \Drupal\Core\Theme\ThemeNegotiatorInterface $negotiator
+ * The theme negotiator to add.
+ * @param int $priority
+ * Priority of the breadcrumb builder.
+ */
+ public function addNegotiator(ThemeNegotiatorInterface $negotiator, $priority) {
+ $this->negotiators[$priority][] = $negotiator;
+ // Force the negotiators to be re-sorted.
+ $this->sortedNegotiators = NULL;
+ }
+
+ /**
+ * Returns the sorted array of theme negotiators.
+ *
+ * @return array|\Drupal\Core\Theme\ThemeNegotiatorInterface[]
+ * An array of breadcrumb builder objects.
+ */
+ protected function getSortedNegotiators() {
+ if (!isset($this->sortedNegotiators)) {
+ // Sort the negotiators according to priority.
+ krsort($this->negotiators);
+ // Merge nested negotiators from $this->negotiators into
+ // $this->sortedNegotiators.
+ $this->sortedNegotiators = array();
+ foreach ($this->negotiators as $builders) {
+ $this->sortedNegotiators = array_merge($this->sortedNegotiators, $builders);
+ }
+ }
+ return $this->sortedNegotiators;
+ }
+
+ /**
+ * Get the current active theme.
+ *
+ * @return string
+ * The current active string.
+ */
+ public function getActiveTheme() {
+ if (!$this->request->attributes->has('_theme_active')) {
+ $this->determineActiveTheme($this->request);
+ }
+ return $this->request->attributes->get('_theme_active');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function determineActiveTheme(Request $request) {
+ foreach ($this->getSortedNegotiators() as $negotiator) {
+ $theme = $negotiator->determineActiveTheme($request);
+ if ($theme !== NULL && $this->themeAccess->checkAccess($theme)) {
+ $request->attributes->set('_theme_active', $theme);
+ return $request->attributes->get('_theme_active');
+ }
+ }
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Theme/ThemeNegotiatorInterface.php b/core/lib/Drupal/Core/Theme/ThemeNegotiatorInterface.php
new file mode 100644
index 0000000..55deaeb
--- /dev/null
+++ b/core/lib/Drupal/Core/Theme/ThemeNegotiatorInterface.php
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Theme\ThemeNegotiatorInterface.
+ */
+
+namespace Drupal\Core\Theme;
+
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Defines an interface for classes which determine the active theme.
+ *
+ * To set the active theme, create a new service tagged with 'theme_negotiator'
+ * (see user.services.yml for an example). The only method this service needs
+ * to implement is determineActiveTheme. Return the name of the theme, or NULL
+ * if other negotiators like the configured default one should kick in instead.
+ *
+ * If you are setting a theme which is closely tied to the functionality of a
+ * particular page or set of pages (such that the page might not function
+ * correctly if a different theme is used), make sure to set the priority on
+ * the service to a high number so that it is not accidentally overridden by
+ * other theme negotiators. By convention, a priority of "1000" is used in
+ * these cases; see \Drupal\Core\Theme\AjaxBasePageNegotiator and
+ * core.services.yml for an example.
+ */
+interface ThemeNegotiatorInterface {
+
+ /**
+ * Determine the active theme for the request.
+ *
+ * @param \Symfony\Component\HttpFoundation\Request $request
+ * The active request of the site.
+ *
+ * @return string|null
+ * Returns the active theme name, else return NULL.
+ */
+ public function determineActiveTheme(Request $request);
+
+}
diff --git a/core/lib/Drupal/Core/Theme/ThemeNegotiatorPass.php b/core/lib/Drupal/Core/Theme/ThemeNegotiatorPass.php
new file mode 100644
index 0000000..7f14692
--- /dev/null
+++ b/core/lib/Drupal/Core/Theme/ThemeNegotiatorPass.php
@@ -0,0 +1,36 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Theme\ThemeNegotiatorPass.
+ */
+
+namespace Drupal\Core\Theme;
+
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Reference;
+
+/**
+ * Adds services to the theme negotiator service.
+ *
+ * @see \Drupal\Core\Theme\ThemeNegotiator
+ * @see \Drupal\Core\Theme\ThemeNegotiatorInterfa
+ */
+class ThemeNegotiatorPass implements CompilerPassInterface {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function process(ContainerBuilder $container) {
+ if (!$container->hasDefinition('theme.negotiator')) {
+ return;
+ }
+ $manager = $container->getDefinition('theme.negotiator');
+ foreach ($container->findTaggedServiceIds('theme_negotiator') as $id => $attributes) {
+ $priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0;
+ $manager->addMethodCall('addNegotiator', array(new Reference($id), $priority));
+ }
+ }
+
+}
diff --git a/core/misc/ajax.js b/core/misc/ajax.js
index a586945..e9baf94 100644
--- a/core/misc/ajax.js
+++ b/core/misc/ajax.js
@@ -382,7 +382,7 @@ Drupal.ajax.prototype.beforeSerialize = function (element, options) {
// Allow Drupal to return new JavaScript and CSS files to load without
// returning the ones already loaded.
- // @see ajax_base_page_theme()
+ // @see \Drupal\Core\Theme\AjaxBasePageNegotiator
// @see drupal_get_css()
// @see drupal_get_js()
var pageState = drupalSettings.ajaxPageState;
diff --git a/core/modules/block/block.module b/core/modules/block/block.module
index e1bc631..afe5862 100644
--- a/core/modules/block/block.module
+++ b/core/modules/block/block.module
@@ -109,42 +109,10 @@ function block_menu() {
'type' => MENU_VISIBLE_IN_BREADCRUMB,
'route_name' => 'block.admin_add',
);
- // Block administration is tied to the theme and plugin definition so
- // that the plugin can appropriately attach to this URL structure.
- // @todo D8: Use dynamic % arguments instead of static, hard-coded theme names
- // and plugin IDs to decouple the routes from these dependencies.
- // @see http://drupal.org/node/1067408
- foreach (list_themes() as $key => $theme) {
- $items["admin/structure/block/demo/$key"] = array(
- 'route_name' => 'block.admin_demo',
- 'type' => MENU_CALLBACK,
- 'theme callback' => '_block_custom_theme',
- 'theme arguments' => array($key),
- );
- }
return $items;
}
/**
- * Theme callback: Uses the theme specified in the parameter.
- *
- * @param $theme
- * The theme whose blocks are being configured. If not set, the default theme
- * is assumed.
- *
- * @return
- * The theme that should be used for the block configuration page, or NULL
- * to indicate that the default theme should be used.
- *
- * @see block_menu()
- */
-function _block_custom_theme($theme = NULL) {
- // We return exactly what was passed in, to guarantee that the page will
- // always be displayed using the theme whose blocks are being configured.
- return $theme;
-}
-
-/**
* Implements hook_page_build().
*
* Renders blocks into their regions.
diff --git a/core/modules/block/block.services.yml b/core/modules/block/block.services.yml
index b6bf1f4..29a31c2 100644
--- a/core/modules/block/block.services.yml
+++ b/core/modules/block/block.services.yml
@@ -9,3 +9,8 @@ services:
factory_method: get
factory_service: cache_factory
arguments: [block]
+ theme.negotiator.block.admin_demo:
+ class: Drupal\block\Theme\AdminDemoNegotiator
+ tags:
+ - { name: theme_negotiator, priority: 1000 }
+
diff --git a/core/modules/block/lib/Drupal/block/Theme/AdminDemoNegotiator.php b/core/modules/block/lib/Drupal/block/Theme/AdminDemoNegotiator.php
new file mode 100644
index 0000000..d3dcb33
--- /dev/null
+++ b/core/modules/block/lib/Drupal/block/Theme/AdminDemoNegotiator.php
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\block\Theme\AdminDemoNegotiator.
+ */
+
+namespace Drupal\block\Theme;
+
+use Drupal\Core\Theme\ThemeNegotiatorInterface;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Negotiates the theme for the block admin demo page via the URL.
+ */
+class AdminDemoNegotiator implements ThemeNegotiatorInterface {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function determineActiveTheme(Request $request) {
+ // We return exactly what was passed in, to guarantee that the page will
+ // always be displayed using the theme whose blocks are being configured.
+ if ($request->attributes->get(RouteObjectInterface::ROUTE_NAME) == 'block.admin_demo') {
+ return $request->attributes->get('theme');
+ }
+ }
+
+}
diff --git a/core/modules/content_translation/content_translation.module b/core/modules/content_translation/content_translation.module
index 43127b0..46ed263 100644
--- a/core/modules/content_translation/content_translation.module
+++ b/core/modules/content_translation/content_translation.module
@@ -156,7 +156,7 @@ function content_translation_menu() {
if (content_translation_enabled($entity_type)) {
$path = _content_translation_link_to_router_path($entity_type, $info['links']['canonical']);
$entity_position = count(explode('/', $path)) - 1;
- $keys = array_flip(array('theme_callback', 'theme_arguments', 'load_arguments'));
+ $keys = array_flip(array('load_arguments'));
$menu_info = array_intersect_key($info['translation']['content_translation'], $keys) + array('file' => 'content_translation.pages.inc');
$item = array();
diff --git a/core/modules/contextual/contextual.module b/core/modules/contextual/contextual.module
index e405f20..9f793cb 100644
--- a/core/modules/contextual/contextual.module
+++ b/core/modules/contextual/contextual.module
@@ -7,21 +7,6 @@ use Drupal\Component\Utility\Url;
*/
/**
- * Implements hook_menu().
- */
-function contextual_menu() {
- // @todo Remove this menu item in http://drupal.org/node/1954892 when theme
- // callbacks are replaced with something else.
- $items['contextual/render'] = array(
- 'route_name' => 'contextual.render',
- 'theme callback' => 'ajax_base_page_theme',
- 'type' => MENU_CALLBACK,
- );
-
- return $items;
-}
-
-/**
* Implements hook_toolbar().
*/
function contextual_toolbar() {
diff --git a/core/modules/contextual/contextual.routing.yml b/core/modules/contextual/contextual.routing.yml
index 8ab2f28..b44d619 100644
--- a/core/modules/contextual/contextual.routing.yml
+++ b/core/modules/contextual/contextual.routing.yml
@@ -2,5 +2,7 @@ contextual.render:
path: '/contextual/render'
defaults:
_controller: '\Drupal\contextual\ContextualController::render'
+ options:
+ _theme: ajax_base_page
requirements:
_permission: 'access contextual links'
diff --git a/core/modules/edit/edit.module b/core/modules/edit/edit.module
index 9e4d132..291c4be 100644
--- a/core/modules/edit/edit.module
+++ b/core/modules/edit/edit.module
@@ -18,26 +18,6 @@ use Drupal\entity\Entity\EntityDisplay;
use Drupal\user\TempStoreFactory;
/**
- * Implements hook_menu().
- */
-function edit_menu() {
- // @todo Remove these menu items in http://drupal.org/node/1954892 when theme
- // callbacks are replaced with something else.
- $items['edit/metadata'] = array(
- 'route_name' => 'edit.metadata',
- 'theme callback' => 'ajax_base_page_theme',
- 'type' => MENU_CALLBACK,
- );
- $items['edit/form/%/%/%/%/%'] = array(
- 'route_name' => 'edit.field_form',
- 'theme callback' => 'ajax_base_page_theme',
- 'type' => MENU_CALLBACK,
- );
-
- return $items;
-}
-
-/**
* Implements hook_permission().
*/
function edit_permission() {
diff --git a/core/modules/edit/edit.routing.yml b/core/modules/edit/edit.routing.yml
index 41acaac..80fbbf8 100644
--- a/core/modules/edit/edit.routing.yml
+++ b/core/modules/edit/edit.routing.yml
@@ -2,9 +2,10 @@ edit.metadata:
path: '/edit/metadata'
defaults:
_controller: '\Drupal\edit\EditController::metadata'
+ options:
+ _theme: ajax_base_page
requirements:
_permission: 'access in-place editing'
-
edit.attachments:
path: '/edit/attachments'
defaults:
@@ -16,6 +17,9 @@ edit.field_form:
path: '/edit/form/{entity_type}/{entity}/{field_name}/{langcode}/{view_mode_id}'
defaults:
_controller: '\Drupal\edit\EditController::fieldForm'
+ options:
+ _access_mode: 'ALL'
+ _theme: ajax_base_page
requirements:
_permission: 'access in-place editing'
_access_edit_entity_field: 'TRUE'
diff --git a/core/modules/editor/editor.module b/core/modules/editor/editor.module
index 4a560ee..07fc1b3 100644
--- a/core/modules/editor/editor.module
+++ b/core/modules/editor/editor.module
@@ -140,21 +140,6 @@ function editor_library_info() {
}
/**
- * Implements hook_menu().
- */
-function editor_menu() {
- // @todo Remove this menu item in http://drupal.org/node/1954892 when theme
- // callbacks are replaced with something else.
- $items['editor/%/%/%/%/%'] = array(
- 'route_name' => 'editor.field_untransformed_text',
- 'theme callback' => 'ajax_base_page_theme',
- 'type' => MENU_CALLBACK,
- );
-
- return $items;
-}
-
-/**
* Implements hook_form_FORM_ID_alter().
*/
function editor_form_filter_admin_overview_alter(&$form, $form_state) {
diff --git a/core/modules/editor/editor.routing.yml b/core/modules/editor/editor.routing.yml
index 3308dd0..bf9d360 100644
--- a/core/modules/editor/editor.routing.yml
+++ b/core/modules/editor/editor.routing.yml
@@ -2,6 +2,8 @@ editor.field_untransformed_text:
path: '/editor/{entity_type}/{entity}/{field_name}/{langcode}/{view_mode_id}'
defaults:
_controller: '\Drupal\editor\EditorController::getUntransformedText'
+ options:
+ _theme: ajax_base_page
requirements:
_permission: 'access in-place editing'
_access_edit_entity_field: 'TRUE'
diff --git a/core/modules/file/file.module b/core/modules/file/file.module
index 9bc1d85..e96a4c1 100644
--- a/core/modules/file/file.module
+++ b/core/modules/file/file.module
@@ -38,21 +38,6 @@ function file_help($path, $arg) {
}
/**
- * Implements hook_menu().
- */
-function file_menu() {
- $items = array();
-
- $items['file/ajax'] = array(
- 'route_name' => 'file.ajax_upload',
- 'theme callback' => 'ajax_base_page_theme',
- 'type' => MENU_CALLBACK,
- );
-
- return $items;
-}
-
-/**
* Implements hook_element_info().
*
* The managed file element may be used anywhere in Drupal.
diff --git a/core/modules/file/file.routing.yml b/core/modules/file/file.routing.yml
index 8bf971c..d9d4efa 100644
--- a/core/modules/file/file.routing.yml
+++ b/core/modules/file/file.routing.yml
@@ -2,6 +2,8 @@ file.ajax_upload:
path: '/file/ajax'
defaults:
_controller: '\Drupal\file\Controller\FileWidgetAjaxController::upload'
+ options:
+ _theme: ajax_base_page
requirements:
_permission: 'access content'
diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchPageOverrideTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchPageOverrideTest.php
index ab3f913..101b745 100644
--- a/core/modules/search/lib/Drupal/search/Tests/SearchPageOverrideTest.php
+++ b/core/modules/search/lib/Drupal/search/Tests/SearchPageOverrideTest.php
@@ -38,7 +38,7 @@ class SearchPageOverrideTest extends SearchTestBase {
// Enable the extra type module for searching.
\Drupal::config('search.settings')->set('active_plugins', array('node_search', 'user_search', 'search_extra_type_search'))->save();
- \Drupal::state()->set('menu_rebuild_needed', TRUE);
+ \Drupal::service('router.builder')->rebuild();
}
function testSearchPageHook() {
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutLinksTest.php b/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutLinksTest.php
index bd2d194..b747d79 100644
--- a/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutLinksTest.php
+++ b/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutLinksTest.php
@@ -145,6 +145,7 @@ class ShortcutLinksTest extends ShortcutTestBase {
*/
function testNoShortcutLink() {
// Change to a theme that displays shortcuts.
+ theme_enable(array('seven'));
\Drupal::config('system.theme')
->set('default', 'seven')
->save();
@@ -157,8 +158,9 @@ class ShortcutLinksTest extends ShortcutTestBase {
$this->assertNoRaw('add-shortcut', 'Add to shortcuts link was not shown on a page the user does not have access to.');
// Verify that the testing mechanism works by verifying the shortcut
- // link appears on admin/content/node.
- $this->drupalGet('admin/content/node');
+ // link appears on admin/people.
+ $this->drupalGet('admin/people');
$this->assertRaw('add-shortcut', 'Add to shortcuts link was shown on a page the user does have access to.');
}
+
}
diff --git a/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutTestBase.php b/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutTestBase.php
index 06088ef..e0bdb31 100644
--- a/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutTestBase.php
+++ b/core/modules/shortcut/lib/Drupal/shortcut/Tests/ShortcutTestBase.php
@@ -69,7 +69,7 @@ abstract class ShortcutTestBase extends WebTestBase {
}
// Create users.
- $this->admin_user = $this->drupalCreateUser(array('access toolbar', 'administer shortcuts', 'view the administration theme', 'create article content', 'create page content', 'access content overview'));
+ $this->admin_user = $this->drupalCreateUser(array('access toolbar', 'administer shortcuts', 'view the administration theme', 'create article content', 'create page content', 'access content overview', 'administer users'));
$this->shortcut_user = $this->drupalCreateUser(array('customize shortcut links', 'switch shortcut sets'));
// Create a node.
diff --git a/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php b/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php
index 71cf0c4..ccc327d 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php
@@ -482,18 +482,9 @@ class MenuRouterTest extends WebTestBase {
$this->doTestThemeCallbackFakeTheme();
$this->initializeTestThemeConfiguration();
- $this->doTestHookCustomTheme();
-
- $this->initializeTestThemeConfiguration();
- $this->doTestThemeCallbackHookCustomTheme();
-
- $this->initializeTestThemeConfiguration();
$this->doTestThemeCallbackAdministrative();
$this->initializeTestThemeConfiguration();
- $this->doTestThemeCallbackInheritance();
-
- $this->initializeTestThemeConfiguration();
$this->doTestThemeCallbackNoThemeRequested();
$this->initializeTestThemeConfiguration();
@@ -516,27 +507,17 @@ class MenuRouterTest extends WebTestBase {
}
/**
- * Test the theme callback when it is set to use an administrative theme.
+ * Test the theme negotiation when it is set to use an administrative theme.
*/
protected function doTestThemeCallbackAdministrative() {
theme_enable(array($this->admin_theme));
$this->drupalGet('menu-test/theme-callback/use-admin-theme');
- $this->assertText('Custom theme: seven. Actual theme: seven.', 'The administrative theme can be correctly set in a theme callback.');
- $this->assertRaw('seven/style.css', "The administrative theme's CSS appears on the page.");
- }
-
- /**
- * Test that the theme callback is properly inherited.
- */
- protected function doTestThemeCallbackInheritance() {
- theme_enable(array($this->admin_theme));
- $this->drupalGet('menu-test/theme-callback/use-admin-theme/inheritance');
- $this->assertText('Custom theme: seven. Actual theme: seven. Theme callback inheritance is being tested.', 'Theme callback inheritance correctly uses the administrative theme.');
+ $this->assertText('Active theme: seven. Actual theme: seven.', 'The administrative theme can be correctly set in a theme negotiation.');
$this->assertRaw('seven/style.css', "The administrative theme's CSS appears on the page.");
}
/**
- * Test the theme callback when the site is in maintenance mode.
+ * Test the theme negotiation when the site is in maintenance mode.
*/
protected function doTestThemeCallbackMaintenanceMode() {
$this->container->get('state')->set('system.maintenance_mode', TRUE);
@@ -551,76 +532,44 @@ class MenuRouterTest extends WebTestBase {
$admin_user = $this->drupalCreateUser(array('access site in maintenance mode'));
$this->drupalLogin($admin_user);
$this->drupalGet('menu-test/theme-callback/use-admin-theme');
- $this->assertText('Custom theme: seven. Actual theme: seven.', 'The theme callback system is correctly triggered for an administrator when the site is in maintenance mode.');
+ $this->assertText('Active theme: seven. Actual theme: seven.', 'The theme negotiation system is correctly triggered for an administrator when the site is in maintenance mode.');
$this->assertRaw('seven/style.css', "The administrative theme's CSS appears on the page.");
$this->container->get('state')->set('system.maintenance_mode', FALSE);
}
/**
- * Test the theme callback when it is set to use an optional theme.
+ * Test the theme negotiation when it is set to use an optional theme.
*/
protected function doTestThemeCallbackOptionalTheme() {
// Request a theme that is not enabled.
$this->drupalGet('menu-test/theme-callback/use-stark-theme');
- $this->assertText('Custom theme: NONE. Actual theme: bartik.', 'The theme callback system falls back on the default theme when a theme that is not enabled is requested.');
+ $this->assertText('Active theme: bartik. Actual theme: bartik.', 'The theme negotiation system falls back on the default theme when a theme that is not enabled is requested.');
$this->assertRaw('bartik/css/style.css', "The default theme's CSS appears on the page.");
// Now enable the theme and request it again.
theme_enable(array($this->alternate_theme));
$this->drupalGet('menu-test/theme-callback/use-stark-theme');
- $this->assertText('Custom theme: stark. Actual theme: stark.', 'The theme callback system uses an optional theme once it has been enabled.');
+ $this->assertText('Active theme: stark. Actual theme: stark.', 'The theme negotiation system uses an optional theme once it has been enabled.');
$this->assertRaw('stark/css/layout.css', "The optional theme's CSS appears on the page.");
}
/**
- * Test the theme callback when it is set to use a theme that does not exist.
+ * Test the theme negotiation when it is set to use a theme that does not exist.
*/
protected function doTestThemeCallbackFakeTheme() {
$this->drupalGet('menu-test/theme-callback/use-fake-theme');
- $this->assertText('Custom theme: NONE. Actual theme: bartik.', 'The theme callback system falls back on the default theme when a theme that does not exist is requested.');
+ $this->assertText('Active theme: bartik. Actual theme: bartik.', 'The theme negotiation system falls back on the default theme when a theme that does not exist is requested.');
$this->assertRaw('bartik/css/style.css', "The default theme's CSS appears on the page.");
}
/**
- * Test the theme callback when no theme is requested.
+ * Test the theme negotiation when no theme is requested.
*/
protected function doTestThemeCallbackNoThemeRequested() {
$this->drupalGet('menu-test/theme-callback/no-theme-requested');
- $this->assertText('Custom theme: NONE. Actual theme: bartik.', 'The theme callback system falls back on the default theme when no theme is requested.');
+ $this->assertText('Active theme: bartik. Actual theme: bartik.', 'The theme negotiation system falls back on the default theme when no theme is requested.');
$this->assertRaw('bartik/css/style.css', "The default theme's CSS appears on the page.");
}
- /**
- * Test that hook_custom_theme() can control the theme of a page.
- */
- protected function doTestHookCustomTheme() {
- // Trigger hook_custom_theme() to dynamically request the Stark theme for
- // the requested page.
- \Drupal::state()->set('menu_test.hook_custom_theme_name', $this->alternate_theme);
- theme_enable(array($this->alternate_theme, $this->admin_theme));
-
- // Visit a page that does not implement a theme callback. The above request
- // should be honored.
- $this->drupalGet('menu-test/no-theme-callback');
- $this->assertText('Custom theme: stark. Actual theme: stark.', 'The result of hook_custom_theme() is used as the theme for the current page.');
- $this->assertRaw('stark/css/layout.css', "The Stark theme's CSS appears on the page.");
- }
-
- /**
- * Test that the theme callback wins out over hook_custom_theme().
- */
- protected function doTestThemeCallbackHookCustomTheme() {
- // Trigger hook_custom_theme() to dynamically request the Stark theme for
- // the requested page.
- \Drupal::state()->set('menu_test.hook_custom_theme_name', $this->alternate_theme);
- theme_enable(array($this->alternate_theme, $this->admin_theme));
-
- // The menu "theme callback" should take precedence over a value set in
- // hook_custom_theme().
- $this->drupalGet('menu-test/theme-callback/use-admin-theme');
- $this->assertText('Custom theme: seven. Actual theme: seven.', 'The result of hook_custom_theme() does not override what was set in a theme callback.');
- $this->assertRaw('seven/style.css', "The Seven theme's CSS appears on the page.");
- }
-
}
diff --git a/core/modules/system/lib/Drupal/system/Tests/System/ThemeTest.php b/core/modules/system/lib/Drupal/system/Tests/System/ThemeTest.php
index 9a13acb..554450a 100644
--- a/core/modules/system/lib/Drupal/system/Tests/System/ThemeTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/System/ThemeTest.php
@@ -178,7 +178,7 @@ class ThemeTest extends WebTestBase {
* Test the administration theme functionality.
*/
function testAdministrationTheme() {
- theme_enable(array('seven'));
+ theme_enable(array('bartik', 'seven'));
// Enable an administration theme and show it on the node admin pages.
$edit = array(
diff --git a/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php b/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php
index 069960c..7263a02 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php
@@ -119,6 +119,17 @@ class ThemeTest extends WebTestBase {
}
/**
+ * Tests the priority of some theme negotiators.
+ */
+ public function testNegotiatorPriorities() {
+ $this->drupalGet('theme-test/priority');
+
+ // Ensure that the custom theme negotiator was not able to set the theme.
+
+ $this->assertNoText('Theme hook implementor=test_theme_theme_test__suggestion(). Foo=template_preprocess_theme_test', 'Theme hook suggestion ran with data available from a preprocess function for the base hook.');
+ }
+
+ /**
* Ensure page-front template suggestion is added when on front page.
*/
function testFrontPageThemeSuggestion() {
@@ -268,5 +279,4 @@ class ThemeTest extends WebTestBase {
$this->assertText('theme test page bottom markup', 'Modules are able to set the page bottom region.');
}
-
}
diff --git a/core/modules/system/lib/Drupal/system/Theme/BatchNegotiator.php b/core/modules/system/lib/Drupal/system/Theme/BatchNegotiator.php
new file mode 100644
index 0000000..a10374d
--- /dev/null
+++ b/core/modules/system/lib/Drupal/system/Theme/BatchNegotiator.php
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\system\Theme\BatchNegotiator.
+ */
+
+namespace Drupal\system\Theme;
+
+use Drupal\Core\Batch\BatchStorageInterface;
+use Drupal\Core\Theme\ThemeNegotiatorInterface;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Sets the active theme for the batch page.
+ */
+class BatchNegotiator implements ThemeNegotiatorInterface {
+
+ /**
+ * The batch storage.
+ *
+ * @var \Drupal\Core\Batch\BatchStorageInterface
+ */
+ protected $batchStorage;
+
+ /**
+ * Constructs a BatchNegotiator.
+ *
+ * @param \Drupal\Core\Batch\BatchStorageInterface $batch_storage
+ * The batch storage.
+ */
+ public function __construct(BatchStorageInterface $batch_storage) {
+ $this->batchStorage = $batch_storage;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function determineActiveTheme(Request $request) {
+ if ($request->attributes->get(RouteObjectInterface::ROUTE_NAME) == 'system.batch_page') {
+ // Retrieve the current state of the batch.
+ $batch = &batch_get();
+ if (!$batch && $request->request->has('id')) {
+ $batch = $this->batchStorage->load($request->request->get('id'));
+ }
+ // Use the same theme as the page that started the batch.
+ if (!empty($batch['theme'])) {
+ return $batch['theme'];
+ }
+ }
+ }
+
+}
diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php
index 2bff21f..67c48f3 100644
--- a/core/modules/system/system.api.php
+++ b/core/modules/system/system.api.php
@@ -1426,36 +1426,6 @@ function hook_template_preprocess_default_variables_alter(&$variables) {
}
/**
- * Return the machine-readable name of the theme to use for the current page.
- *
- * This hook can be used to dynamically set the theme for the current page
- * request. It should be used by modules which need to override the theme
- * based on dynamic conditions (for example, a module which allows the theme to
- * be set based on the current user's role). The return value of this hook will
- * be used on all pages except those which have a valid per-page or per-section
- * theme set via a theme callback function in hook_menu(); the themes on those
- * pages can only be overridden using hook_menu_alter().
- *
- * Note that returning different themes for the same path may not work with page
- * caching. This is most likely to be a problem if an anonymous user on a given
- * path could have different themes returned under different conditions.
- *
- * Since only one theme can be used at a time, the last (i.e., highest
- * weighted) module which returns a valid theme name from this hook will
- * prevail.
- *
- * @return
- * The machine-readable name of the theme that should be used for the current
- * page request. The value returned from this function will only have an
- * effect if it corresponds to a currently-active theme on the site. Do not
- * return a value if you do not wish to set a custom theme.
- */
-function hook_custom_theme() {
- // Allow the user to request a particular theme via a query parameter.
- return \Drupal::request()->query->get('theme');
-}
-
-/**
* Log an event message.
*
* This hook allows modules to route log events to custom destinations, such as
diff --git a/core/modules/system/system.install b/core/modules/system/system.install
index 4f848ce..f0cc453 100644
--- a/core/modules/system/system.install
+++ b/core/modules/system/system.install
@@ -824,20 +824,6 @@ function system_schema() {
'not null' => TRUE,
'default' => '',
),
- 'theme_callback' => array(
- 'description' => 'A function which returns the name of the theme that will be used to render this page. If left empty, the default theme will be used.',
- 'type' => 'varchar',
- 'length' => 255,
- 'not null' => TRUE,
- 'default' => '',
- ),
- 'theme_arguments' => array(
- 'description' => 'A serialized array of arguments for the theme callback.',
- 'type' => 'varchar',
- 'length' => 255,
- 'not null' => TRUE,
- 'default' => '',
- ),
'type' => array(
'description' => 'Numeric representation of the type of the menu item, like MENU_LOCAL_TASK.',
'type' => 'int',
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 40c3234..fdf1737 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -613,12 +613,6 @@ function system_element_info() {
* Implements hook_menu().
*/
function system_menu() {
- $items['system/ajax'] = array(
- 'title' => 'AHAH callback',
- 'route_name' => 'system.ajax',
- 'theme callback' => 'ajax_base_page_theme',
- 'type' => MENU_CALLBACK,
- );
$items['admin'] = array(
'title' => 'Administration',
'route_name' => 'system.admin',
@@ -812,13 +806,6 @@ function system_menu() {
'route_name' => 'system.status',
);
- // Default page for batch operations.
- $items['batch'] = array(
- 'route_name' => 'system.batch_page',
- 'theme callback' => '_system_batch_theme',
- 'type' => MENU_CALLBACK,
- );
-
return $items;
}
@@ -871,21 +858,6 @@ function system_theme_suggestions_region(array $variables) {
}
/**
- * Theme callback for the default batch page.
- */
-function _system_batch_theme() {
- // Retrieve the current state of the batch.
- $batch = &batch_get();
- if (!$batch && isset($_REQUEST['id'])) {
- $batch = \Drupal::service('batch.storage')->load($_REQUEST['id']);
- }
- // Use the same theme as the page that started the batch.
- if (!empty($batch['theme'])) {
- return $batch['theme'];
- }
-}
-
-/**
* Implements hook_library_info().
*/
function system_library_info() {
@@ -2132,19 +2104,6 @@ function system_page_build(&$page) {
}
/**
- * Implements hook_custom_theme().
- */
-function system_custom_theme() {
- if (drupal_container()->isScopeActive('request')) {
- $request = \Drupal::request();
- $path = $request->attributes->get('_system_path');
- if (user_access('view the administration theme') && path_is_admin($path)) {
- return \Drupal::config('system.theme')->get('admin');
- }
- }
-}
-
-/**
* Implements hook_form_FORM_ID_alter().
*/
function system_form_user_form_alter(&$form, &$form_state) {
diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml
index 77a7419..a0aadb6 100644
--- a/core/modules/system/system.routing.yml
+++ b/core/modules/system/system.routing.yml
@@ -2,6 +2,8 @@ system.ajax:
path: '/system/ajax'
defaults:
_controller: '\Drupal\system\Controller\FormAjaxController::content'
+ options:
+ _theme: ajax_base_page
requirements:
_access: 'TRUE'
diff --git a/core/modules/system/system.services.yml b/core/modules/system/system.services.yml
index 3371dad..3221048 100644
--- a/core/modules/system/system.services.yml
+++ b/core/modules/system/system.services.yml
@@ -19,3 +19,8 @@ services:
class: Drupal\system\PathProcessor\PathProcessorFiles
tags:
- { name: path_processor_inbound, priority: 200 }
+ theme.negotiator.system.batch:
+ class: Drupal\system\Theme\BatchNegotiator
+ arguments: ['@batch.storage']
+ tags:
+ - { name: theme_negotiator, priority: 1000 }
diff --git a/core/modules/system/tests/modules/ajax_test/ajax_test.module b/core/modules/system/tests/modules/ajax_test/ajax_test.module
index f42844f..5fb8338 100644
--- a/core/modules/system/tests/modules/ajax_test/ajax_test.module
+++ b/core/modules/system/tests/modules/ajax_test/ajax_test.module
@@ -13,19 +13,6 @@ use Drupal\Core\Ajax\CloseDialogCommand;
use Drupal\Core\Ajax\HtmlCommand;
/**
- * Implements hook_menu().
- */
-function ajax_test_menu() {
- $items['ajax-test/order'] = array(
- 'title' => 'AJAX commands order',
- 'route_name' => 'ajax_test.order',
- 'theme callback' => 'ajax_base_page_theme',
- 'type' => MENU_CALLBACK,
- );
- return $items;
-}
-
-/**
* Implements hook_system_theme_info().
*/
function ajax_test_system_theme_info() {
diff --git a/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml b/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml
index 9453bef..270ef51 100644
--- a/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml
+++ b/core/modules/system/tests/modules/ajax_test/ajax_test.routing.yml
@@ -37,6 +37,8 @@ ajax_test.order:
path: '/ajax-test/order'
defaults:
_controller: '\Drupal\ajax_test\Controller\AjaxTestController::order'
+ options:
+ _theme: ajax_base_page
requirements:
_access: 'TRUE'
diff --git a/core/modules/system/tests/modules/menu_test/lib/Drupal/menu_test/EventSubscriber/ActiveTrailSubscriber.php b/core/modules/system/tests/modules/menu_test/lib/Drupal/menu_test/EventSubscriber/ActiveTrailSubscriber.php
new file mode 100644
index 0000000..4bb3595
--- /dev/null
+++ b/core/modules/system/tests/modules/menu_test/lib/Drupal/menu_test/EventSubscriber/ActiveTrailSubscriber.php
@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\menu_test\EventSubscriber\ActiveTrailSubscriber.
+ */
+
+namespace Drupal\menu_test\EventSubscriber;
+
+use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpKernel\Event\GetResponseEvent;
+use Symfony\Component\HttpKernel\KernelEvents;
+
+/**
+ * Tracks the active trail.
+ */
+class ActiveTrailSubscriber implements EventSubscriberInterface {
+
+ /**
+ * The active trail before redirect.
+ *
+ * @var array
+ */
+ protected $trail = array();
+
+ /**
+ * The state service.
+ *
+ * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
+ */
+ protected $state;
+
+ /**
+ * Constructs a new ActiveTrailSubscriber.
+ *
+ * @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $state
+ * The state service.
+ */
+ public function __construct(KeyValueStoreInterface $state) {
+ $this->state = $state;
+ }
+
+ /**
+ * Tracks the active trail.
+ *
+ * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
+ * The event to process.
+ */
+ public function onKernelRequest(GetResponseEvent $event) {
+ // When requested by one of the MenuTrailTestCase tests, record the initial
+ // active trail during Drupal's bootstrap (before the user is redirected to
+ // a custom 403 or 404 page).
+ if (!$this->trail && $this->state->get('menu_test.record_active_trail') ?: FALSE) {
+ $this->trail = menu_get_active_trail();
+ $this->state->set('menu_test.active_trail_initial', $this->trail);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getSubscribedEvents() {
+ $events[KernelEvents::REQUEST][] = array('onKernelRequest');
+ return $events;
+ }
+
+}
diff --git a/core/modules/system/tests/modules/menu_test/lib/Drupal/menu_test/Theme/TestThemeNegotiator.php b/core/modules/system/tests/modules/menu_test/lib/Drupal/menu_test/Theme/TestThemeNegotiator.php
new file mode 100644
index 0000000..b324d8d
--- /dev/null
+++ b/core/modules/system/tests/modules/menu_test/lib/Drupal/menu_test/Theme/TestThemeNegotiator.php
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\menu_test\Theme\TestThemeNegotiator.
+ */
+
+namespace Drupal\menu_test\Theme;
+
+use Drupal\Core\Theme\ThemeNegotiatorInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Tests the theme negotiation functionality.
+ *
+ * Retrieves the theme key of the theme to use for the current request based on
+ * the theme name provided in the URL.
+ */
+class TestThemeNegotiator implements ThemeNegotiatorInterface {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function determineActiveTheme(Request $request) {
+ $argument = $request->attributes->get('inherited');
+ // Test using the variable administrative theme.
+ if ($argument == 'use-admin-theme') {
+ return \Drupal::config('system.theme')->get('admin');
+ }
+ // Test using a theme that exists, but may or may not be enabled.
+ elseif ($argument == 'use-stark-theme') {
+ return 'stark';
+ }
+ // Test using a theme that does not exist.
+ elseif ($argument == 'use-fake-theme') {
+ return 'fake_theme';
+ }
+ // For any other value of the URL argument, do not return anything. This
+ // allows us to test that returning nothing from a theme negotiation
+ // causes the page to correctly fall back on using the main site theme.
+ }
+
+}
diff --git a/core/modules/system/tests/modules/menu_test/menu_test.module b/core/modules/system/tests/modules/menu_test/menu_test.module
index ab2ef30..df5b490 100644
--- a/core/modules/system/tests/modules/menu_test/menu_test.module
+++ b/core/modules/system/tests/modules/menu_test/menu_test.module
@@ -64,18 +64,12 @@ function menu_test_menu() {
'route_name' => 'menu_test.hierarchy_parent_child2',
);
// Theme callback tests.
- $items['menu-test/theme-callback/%'] = array(
- 'title' => 'Page that displays different themes',
- 'route_name' => 'menu_test.theme_callback',
- 'theme callback' => 'menu_test_theme_callback',
- 'theme arguments' => array(2),
- );
$items['menu-test/theme-callback/%/inheritance'] = array(
- 'title' => 'Page that tests theme callback inheritance.',
+ 'title' => 'Page that tests theme negotiation inheritance.',
'route_name' => 'menu_test.theme_callback_inheritance',
);
$items['menu-test/no-theme-callback'] = array(
- 'title' => 'Page that displays different themes without using a theme callback.',
+ 'title' => 'Page that displays different themes without using a theme negotiation.',
'route_name' => 'menu_test.no_theme_callback',
);
// Path containing "exotic" characters.
@@ -439,7 +433,7 @@ function menu_test_custom_403_404_callback() {
}
/**
- * Page callback: Tests the theme callback functionality.
+ * Page callback: Tests the theme negotiation functionality.
*
* @param bool $inherited
* (optional) TRUE when the requested page is intended to inherit
@@ -457,67 +451,16 @@ function menu_test_theme_page_callback($inherited = FALSE) {
global $theme_key;
// Initialize the theme system so that $theme_key will be populated.
drupal_theme_initialize();
- // Now check both the requested custom theme and the actual theme being used.
- $custom_theme = menu_get_custom_theme();
- $requested_theme = empty($custom_theme) ? 'NONE' : $custom_theme;
- $output = "Custom theme: $requested_theme. Actual theme: $theme_key.";
+ // Now we check what the theme negotiator service returns.
+ $active_theme = \Drupal::service('theme.negotiator')->getActiveTheme('getActiveTheme');
+ $output = "Active theme: $active_theme. Actual theme: $theme_key.";
if ($inherited) {
- $output .= ' Theme callback inheritance is being tested.';
+ $output .= ' Theme negotiation inheritance is being tested.';
}
return $output;
}
/**
- * Theme callback: Tests the theme callback functionality.
- *
- * Retrieves the theme key of the theme to use for the current request based on
- * the theme name provided in the URL.
- *
- * @param string $argument
- * The argument passed in from the URL.
- *
- * @return string
- * The name of the custom theme to request for the current page.
- *
- * @see menu_test_menu().
- */
-function menu_test_theme_callback($argument) {
- // Test using the variable administrative theme.
- if ($argument == 'use-admin-theme') {
- return \Drupal::config('system.theme')->get('admin');
- }
- // Test using a theme that exists, but may or may not be enabled.
- elseif ($argument == 'use-stark-theme') {
- return 'stark';
- }
- // Test using a theme that does not exist.
- elseif ($argument == 'use-fake-theme') {
- return 'fake_theme';
- }
- // For any other value of the URL argument, do not return anything. This
- // allows us to test that returning nothing from a theme callback function
- // causes the page to correctly fall back on using the main site theme.
-}
-
-/**
- * Implements hook_custom_theme().
- *
- * If an appropriate variable has been set in the database, request the theme
- * that is stored there. Otherwise, do not attempt to dynamically set the theme.
- */
-function menu_test_custom_theme() {
- // When requested by one of the MenuTrailTestCase tests, record the initial
- // active trail during Drupal's bootstrap (before the user is redirected to a
- // custom 403 or 404 page). See menu_test_custom_403_404_callback().
- if (\Drupal::state()->get('menu_test.record_active_trail') ?: FALSE) {
- \Drupal::state()->set('menu_test.active_trail_initial', menu_get_active_trail());
- }
- if ($theme = \Drupal::state()->get('menu_test.hook_custom_theme_name') ?: FALSE) {
- return $theme;
- }
-}
-
-/**
* Sets a static variable for the testMenuName() test.
*
* Used to change the menu_name parameter of a menu.
diff --git a/core/modules/system/tests/modules/menu_test/menu_test.routing.yml b/core/modules/system/tests/modules/menu_test/menu_test.routing.yml
index 5ab1a94..d37f47b 100644
--- a/core/modules/system/tests/modules/menu_test/menu_test.routing.yml
+++ b/core/modules/system/tests/modules/menu_test/menu_test.routing.yml
@@ -490,7 +490,7 @@ menu_test.theme_callback:
menu_test.no_theme_callback:
path: '/menu-test/no-theme-callback'
defaults:
- _title: 'Page that displays different themes without using a theme callback.'
+ _title: 'Page that displays different themes without using a theme negotiation.'
_content: '\Drupal\menu_test\Controller\MenuTestController::themePage'
inherited: false
requirements:
@@ -511,7 +511,7 @@ menu_test.exotic_path:
menu_test.theme_callback_inheritance:
path: '/menu-test/theme-callback/{inherited}/inheritance'
defaults:
- _title: 'Page that tests theme callback inheritance.'
+ _title: 'Page that tests theme negotiation inheritance.'
_content: '\Drupal\menu_test\Controller\MenuTestController::themePage'
requirements:
_permission: 'access content'
diff --git a/core/modules/system/tests/modules/menu_test/menu_test.services.yml b/core/modules/system/tests/modules/menu_test/menu_test.services.yml
index 097ddf2..3de0169 100644
--- a/core/modules/system/tests/modules/menu_test/menu_test.services.yml
+++ b/core/modules/system/tests/modules/menu_test/menu_test.services.yml
@@ -3,3 +3,14 @@ services:
class: Drupal\menu_test\EventSubscriber\MaintenanceModeSubscriber
tags:
- { name: event_subscriber }
+
+ menu_test.active_trail_subscriber:
+ class: Drupal\menu_test\EventSubscriber\ActiveTrailSubscriber
+ arguments: ['@state']
+ tags:
+ - { name: event_subscriber }
+
+ theme.negotiator.test_theme:
+ class: Drupal\menu_test\Theme\TestThemeNegotiator
+ tags:
+ - { name: theme_negotiator }
diff --git a/core/modules/system/tests/modules/theme_test/lib/Drupal/theme_test/Theme/CustomThemeNegotiator.php b/core/modules/system/tests/modules/theme_test/lib/Drupal/theme_test/Theme/CustomThemeNegotiator.php
new file mode 100644
index 0000000..9c0396e
--- /dev/null
+++ b/core/modules/system/tests/modules/theme_test/lib/Drupal/theme_test/Theme/CustomThemeNegotiator.php
@@ -0,0 +1,29 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\theme_test\Theme\CustomThemeNegotiator.
+ */
+
+namespace Drupal\theme_test\Theme;
+
+use Drupal\Core\Theme\ThemeNegotiatorInterface;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Just forces the 'test_theme' theme.
+ */
+class CustomThemeNegotiator implements ThemeNegotiatorInterface {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function determineActiveTheme(Request $request) {
+ if (($route_object = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)) && $route_object instanceof Route && $route_object->hasOption('_custom_theme')) {
+ return $route_object->getOption('_custom_theme');
+ }
+ }
+
+}
diff --git a/core/modules/system/tests/modules/theme_test/lib/Drupal/theme_test/Theme/HighPriorityThemeNegotiator.php b/core/modules/system/tests/modules/theme_test/lib/Drupal/theme_test/Theme/HighPriorityThemeNegotiator.php
new file mode 100644
index 0000000..2bb5cf6
--- /dev/null
+++ b/core/modules/system/tests/modules/theme_test/lib/Drupal/theme_test/Theme/HighPriorityThemeNegotiator.php
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\theme_test\Theme\HighPriorityThemeNegotiator.
+ */
+
+namespace Drupal\theme_test\Theme;
+
+use Drupal\Core\Theme\ThemeNegotiatorInterface;
+use Symfony\Cmf\Component\Routing\RouteObjectInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Implements a test theme negotiator which was configured with a high priority.
+ */
+class HighPriorityThemeNegotiator implements ThemeNegotiatorInterface {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function determineActiveTheme(Request $request) {
+ if (($route_name = $request->attributes->get(RouteObjectInterface::ROUTE_NAME)) && $route_name == 'theme_test.priority') {
+ return 'stark';
+ }
+ }
+
+}
diff --git a/core/modules/system/tests/modules/theme_test/theme_test.module b/core/modules/system/tests/modules/theme_test/theme_test.module
index 87e26b8..52bff21 100644
--- a/core/modules/system/tests/modules/theme_test/theme_test.module
+++ b/core/modules/system/tests/modules/theme_test/theme_test.module
@@ -81,13 +81,6 @@ function theme_test_menu() {
}
/**
- * Custom theme callback.
- */
-function _theme_custom_theme() {
- return 'test_theme';
-}
-
-/**
* Implements hook_preprocess_HOOK() for HTML document templates.
*/
function theme_test_preprocess_html(&$variables) {
diff --git a/core/modules/system/tests/modules/theme_test/theme_test.routing.yml b/core/modules/system/tests/modules/theme_test/theme_test.routing.yml
index b4a7fd6..9f08d5b 100644
--- a/core/modules/system/tests/modules/theme_test/theme_test.routing.yml
+++ b/core/modules/system/tests/modules/theme_test/theme_test.routing.yml
@@ -1,5 +1,7 @@
theme_test.function_template_override:
path: '/theme-test/function-template-overridden'
+ options:
+ _custom_theme: 'test_theme'
defaults:
_content: '\Drupal\theme_test\ThemeTestController::functionTemplateOverridden'
requirements:
@@ -21,6 +23,18 @@ theme_test.template_test:
theme_test.suggestion:
path: '/theme-test/suggestion'
+ options:
+ _custom_theme: 'test_theme'
+ defaults:
+ _content: '\Drupal\theme_test\ThemeTestController::testSuggestion'
+ _title: 'Suggestion'
+ requirements:
+ _access: 'TRUE'
+
+theme_test.priority:
+ path: '/theme-test/priority'
+ options:
+ _custom_theme: 'test_theme'
defaults:
_content: '\Drupal\theme_test\ThemeTestController::testSuggestion'
_title: 'Suggestion'
@@ -29,6 +43,8 @@ theme_test.suggestion:
theme_test.alter:
path: '/theme-test/alter'
+ options:
+ _custom_theme: 'test_theme'
defaults:
_content: '\Drupal\theme_test\ThemeTestController::testAlter'
_title: 'Suggestion'
diff --git a/core/modules/system/tests/modules/theme_test/theme_test.services.yml b/core/modules/system/tests/modules/theme_test/theme_test.services.yml
index 8e442df..69fd3ca 100644
--- a/core/modules/system/tests/modules/theme_test/theme_test.services.yml
+++ b/core/modules/system/tests/modules/theme_test/theme_test.services.yml
@@ -3,3 +3,13 @@ services:
class: Drupal\theme_test\EventSubscriber\ThemeTestSubscriber
tags:
- { name: event_subscriber }
+
+ theme.negotiator.test_custom_theme:
+ class: Drupal\theme_test\Theme\CustomThemeNegotiator
+ tags:
+ - { name: theme_negotiator }
+
+ theme.negotiator.high_priority:
+ class: Drupal\theme_test\Theme\HighPriorityThemeNegotiator
+ tags:
+ - { name: theme_negotiator, priority: 1000 }
diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/ThemeTest.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/ThemeTest.php
index 452e33f..ee8cc38 100644
--- a/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/ThemeTest.php
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/ThemeTest.php
@@ -25,11 +25,11 @@ class ThemeTest extends TaxonomyTestBase {
// Make sure we are using distinct default and administrative themes for
// the duration of these tests.
+ theme_enable(array('bartik', 'seven'));
\Drupal::config('system.theme')
->set('default', 'bartik')
+ ->set('admin', 'seven')
->save();
- theme_enable(array('seven'));
- \Drupal::config('system.theme')->set('admin', 'seven')->save();
// Create and log in as a user who has permission to add and edit taxonomy
// terms and view the administrative theme.
diff --git a/core/modules/text/lib/Drupal/text/Tests/Formatter/TextPlainUnitTest.php b/core/modules/text/lib/Drupal/text/Tests/Formatter/TextPlainUnitTest.php
index 72c0fe9..3cd5dca 100644
--- a/core/modules/text/lib/Drupal/text/Tests/Formatter/TextPlainUnitTest.php
+++ b/core/modules/text/lib/Drupal/text/Tests/Formatter/TextPlainUnitTest.php
@@ -49,6 +49,7 @@ class TextPlainUnitTest extends DrupalUnitTestBase {
// Configure the theme system.
$this->installConfig(array('system', 'field'));
$this->installSchema('entity_test', 'entity_test');
+ $this->installSchema('user', 'users');
// @todo Add helper methods for all of the following.
diff --git a/core/modules/user/lib/Drupal/user/Theme/AdminNegotiator.php b/core/modules/user/lib/Drupal/user/Theme/AdminNegotiator.php
new file mode 100644
index 0000000..4658713
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/Theme/AdminNegotiator.php
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\user\Theme\AdminNegotiator.
+ */
+
+namespace Drupal\user\Theme;
+
+use Drupal\Core\Config\ConfigFactory;
+use Drupal\Core\Entity\EntityManager;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Theme\ThemeNegotiatorInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Sets the active theme on admin pages.
+ */
+class AdminNegotiator implements ThemeNegotiatorInterface {
+
+ /**
+ * The current user.
+ *
+ * @var \Drupal\Core\Session\AccountInterface
+ */
+ protected $user;
+
+ /**
+ * The config factory.
+ *
+ * @var \Drupal\Core\Config\ConfigFactory
+ */
+ protected $configFactory;
+
+ /**
+ * The entity manager.
+ *
+ * @var \Drupal\Core\Entity\EntityManager
+ */
+ protected $entityManager;
+
+ /**
+ * Creates a new AdminNegotiator instance.
+ *
+ * @param \Drupal\Core\Session\AccountInterface $user
+ * The current user.
+ * @param \Drupal\Core\Config\ConfigFactory $config_factory
+ * The config factory.
+ * @param \Drupal\Core\Entity\EntityManager $entity_manager
+ * The entity manager.
+ */
+ public function __construct(AccountInterface $user, ConfigFactory $config_factory, EntityManager $entity_manager) {
+ $this->user = $user;
+ $this->configFactory = $config_factory;
+ $this->entityManager = $entity_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function determineActiveTheme(Request $request) {
+ $path = $request->attributes->get('_system_path');
+
+ // Don't break if the user_role entity is not available in order to decouple
+ // system and user module.
+ if ($this->entityManager->hasController('user_role', 'storage') && $this->user->hasPermission('view the administration theme') && path_is_admin($path)) {
+ return $this->configFactory->get('system.theme')->get('admin');
+ }
+ }
+
+}
diff --git a/core/modules/user/lib/Drupal/user/Theme/UserNegotiator.php b/core/modules/user/lib/Drupal/user/Theme/UserNegotiator.php
new file mode 100644
index 0000000..17c1e33
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/Theme/UserNegotiator.php
@@ -0,0 +1,61 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\user\Theme\UserNegotiator.
+ */
+
+namespace Drupal\user\Theme;
+
+use Drupal\Core\Entity\EntityManager;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Theme\ActiveTheme;
+use Drupal\Core\Theme\ThemeNegotiatorInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Defines the theme negotiator service for theme configured per user.
+ */
+class UserNegotiator implements ThemeNegotiatorInterface {
+
+ /**
+ * The user storage controller.
+ *
+ * @var \Drupal\user\UserStorageControllerInterface
+ */
+ protected $userStorageController;
+
+ /**
+ * The current user.
+ *
+ * @var \Drupal\Core\Session\AccountInterface
+ */
+ protected $currentUser;
+
+ /**
+ * Constructs a UserNegotiator object.
+ *
+ * @param \Drupal\Core\Entity\EntityManager $entity_manager
+ * The entity manager
+ * @param \Drupal\Core\Session\AccountInterface $current_user
+ * The current user.
+ */
+ public function __construct(EntityManager $entity_manager, AccountInterface $current_user) {
+ $this->userStorageController = $entity_manager->getStorageController('user');
+ $this->currentUser = $current_user;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function determineActiveTheme(Request $request) {
+ if ($user = $this->userStorageController->load($this->currentUser->id())) {;
+ // Only select the user selected theme if it is available in the
+ // list of themes that can be accessed.
+ if (!empty($user->theme) && drupal_theme_access($user->theme)) {
+ return $user->theme;
+ }
+ }
+ }
+
+}
diff --git a/core/modules/user/user.services.yml b/core/modules/user/user.services.yml
index 6fb7d47..bc348fa 100644
--- a/core/modules/user/user.services.yml
+++ b/core/modules/user/user.services.yml
@@ -25,3 +25,13 @@ services:
class: Drupal\user\EventSubscriber\MaintenanceModeSubscriber
tags:
- { name: event_subscriber }
+ theme.negotiator.user:
+ class: Drupal\user\Theme\UserNegotiator
+ arguments: ['@plugin.manager.entity', '@current_user']
+ tags:
+ - { name: theme_negotiator, priority: -50 }
+ theme.negotiator.admin_theme:
+ class: Drupal\user\Theme\AdminNegotiator
+ arguments: ['@current_user', '@config.factory', '@entity.manager']
+ tags:
+ - { name: theme_negotiator, priority: -40 }
diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/FieldCounterTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/FieldCounterTest.php
index 8773248..897bfd9 100644
--- a/core/modules/views/lib/Drupal/views/Tests/Handler/FieldCounterTest.php
+++ b/core/modules/views/lib/Drupal/views/Tests/Handler/FieldCounterTest.php
@@ -19,7 +19,7 @@ class FieldCounterTest extends ViewUnitTestBase {
*
* @var array
*/
- public static $modules = array('user');
+ public static $modules = array('user', 'field');
/**
* Views used by this test.
@@ -36,6 +36,12 @@ class FieldCounterTest extends ViewUnitTestBase {
);
}
+ protected function setUp() {
+ parent::setUp();
+
+ $this->installSchema('user', 'users');
+ }
+
function testSimple() {
$view = views_get_view('test_view');
$view->setDisplay();
diff --git a/core/modules/views/lib/Drupal/views/Tests/Handler/FieldUnitTest.php b/core/modules/views/lib/Drupal/views/Tests/Handler/FieldUnitTest.php
index eff8929..9c50002 100644
--- a/core/modules/views/lib/Drupal/views/Tests/Handler/FieldUnitTest.php
+++ b/core/modules/views/lib/Drupal/views/Tests/Handler/FieldUnitTest.php
@@ -17,7 +17,7 @@ use Drupal\views\Plugin\views\field\FieldPluginBase;
*/
class FieldUnitTest extends ViewUnitTestBase {
- public static $modules = array('user');
+ public static $modules = array('user', 'field');
/**
* Views used by this test.
@@ -38,6 +38,12 @@ class FieldUnitTest extends ViewUnitTestBase {
);
}
+ protected function setUp() {
+ parent::setUp();
+
+ $this->installSchema('user', 'users');
+ }
+
/**
* Overrides Drupal\views\Tests\ViewTestBase::viewsData().
*/
diff --git a/core/modules/views/lib/Drupal/views/Tests/Plugin/DisplayPageTest.php b/core/modules/views/lib/Drupal/views/Tests/Plugin/DisplayPageTest.php
index 3e5b590..dc51709 100644
--- a/core/modules/views/lib/Drupal/views/Tests/Plugin/DisplayPageTest.php
+++ b/core/modules/views/lib/Drupal/views/Tests/Plugin/DisplayPageTest.php
@@ -59,6 +59,7 @@ class DisplayPageTest extends ViewUnitTestBase {
// Setup the needed tables in order to make the drupal router working.
$this->installSchema('system', array('router', 'menu_router', 'url_alias'));
$this->installSchema('menu_link', 'menu_links');
+ $this->installSchema('user', 'users');
}
/**
diff --git a/core/modules/views/lib/Drupal/views/ViewExecutable.php b/core/modules/views/lib/Drupal/views/ViewExecutable.php
index 435da78..413962b 100644
--- a/core/modules/views/lib/Drupal/views/ViewExecutable.php
+++ b/core/modules/views/lib/Drupal/views/ViewExecutable.php
@@ -1302,8 +1302,6 @@ class ViewExecutable {
return;
}
- drupal_theme_initialize();
-
$exposed_form = $this->display_handler->getPlugin('exposed_form');
$exposed_form->preRender($this->result);
@@ -1365,11 +1363,13 @@ class ViewExecutable {
$module_handler->invokeAll('views_pre_render', array($this));
// Let the themes play too, because pre render is a very themey thing.
- foreach ($GLOBALS['base_theme_info'] as $base) {
- $module_handler->invoke($base, 'views_pre_render', array($this));
- }
+ if (isset($GLOBALS['base_theme_info']) && isset($GLOBALS['theme'])) {
+ foreach ($GLOBALS['base_theme_info'] as $base) {
+ $module_handler->invoke($base, 'views_pre_render', array($this));
+ }
- $module_handler->invoke($GLOBALS['theme'], 'views_pre_render', array($this));
+ $module_handler->invoke($GLOBALS['theme'], 'views_pre_render', array($this));
+ }
$this->display_handler->output = $this->display_handler->render();
if ($cache) {
@@ -1387,11 +1387,13 @@ class ViewExecutable {
$module_handler->invokeAll('views_post_render', array($this, &$this->display_handler->output, $cache));
// Let the themes play too, because post render is a very themey thing.
- foreach ($GLOBALS['base_theme_info'] as $base) {
- $module_handler->invoke($base, 'views_post_render', array($this));
- }
+ if (isset($GLOBALS['base_theme_info']) && isset($GLOBALS['theme'])) {
+ foreach ($GLOBALS['base_theme_info'] as $base) {
+ $module_handler->invoke($base, 'views_post_render', array($this));
+ }
- $module_handler->invoke($GLOBALS['theme'], 'views_post_render', array($this));
+ $module_handler->invoke($GLOBALS['theme'], 'views_post_render', array($this));
+ }
return $this->display_handler->output;
}
diff --git a/core/modules/views/views.module b/core/modules/views/views.module
index 5bea350..11dbebb 100644
--- a/core/modules/views/views.module
+++ b/core/modules/views/views.module
@@ -247,20 +247,6 @@ function views_permission() {
}
/**
- * Implement hook_menu().
- */
-function views_menu() {
- $items = array();
- $items['views/ajax'] = array(
- 'title' => 'Views',
- 'theme callback' => 'ajax_base_page_theme',
- 'route_name' => 'views.ajax',
- 'type' => MENU_CALLBACK,
- );
- return $items;
-}
-
-/**
* Implement hook_menu_alter().
*/
function views_menu_alter(&$callbacks) {
diff --git a/core/modules/views/views.routing.yml b/core/modules/views/views.routing.yml
index 9abe5d5..691640d 100644
--- a/core/modules/views/views.routing.yml
+++ b/core/modules/views/views.routing.yml
@@ -2,5 +2,7 @@ views.ajax:
path: '/views/ajax'
defaults:
_controller: '\Drupal\views\Controller\ViewAjaxController::ajaxView'
+ options:
+ _theme: ajax_base_page
requirements:
_access: 'TRUE'
diff --git a/core/tests/Drupal/Tests/Core/Theme/ThemeNegotiatorTest.php b/core/tests/Drupal/Tests/Core/Theme/ThemeNegotiatorTest.php
new file mode 100644
index 0000000..9779e63
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Theme/ThemeNegotiatorTest.php
@@ -0,0 +1,141 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Theme\ThemeNegotiatorTest.
+ */
+
+namespace Drupal\Tests\Core\Theme;
+
+use Drupal\Core\Theme\ThemeNegotiator;
+use Drupal\Tests\UnitTestCase;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Tests the theme negotiator.
+ *
+ * @see \Drupal\Core\Theme\ThemeNegotiator
+ */
+class ThemeNegotiatorTest extends UnitTestCase {
+
+ /**
+ * The mocked theme access checker.
+ *
+ * @var \Drupal\Core\Theme\ThemeAccessCheck|\PHPUnit_Framework_MockObject_MockObject
+ */
+ protected $themeAccessCheck;
+
+ /**
+ * The actual tested theme negotiator.
+ *
+ * @var \Drupal\Core\Theme\ThemeNegotiator
+ */
+ protected $themeNegotiator;
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Theme negotiator',
+ 'description' => 'Tests the theme negotiator.',
+ 'group' => 'Theme',
+ );
+ }
+
+ protected function setUp() {
+ $this->themeAccessCheck = $this->getMockBuilder('\Drupal\Core\Theme\ThemeAccessCheck')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $this->themeNegotiator = new ThemeNegotiator($this->themeAccessCheck);
+ }
+
+ /**
+ * Tests determining the theme.
+ *
+ * @see \Drupal\Core\Theme\ThemeNegotiator::determineActiveTheme()
+ */
+ public function testDetermineActiveTheme() {
+ $negotiator = $this->getMock('Drupal\Core\Theme\ThemeNegotiatorInterface');
+ $negotiator->expects($this->once())
+ ->method('determineActiveTheme')
+ ->will($this->returnValue('example_test'));
+
+ $this->themeNegotiator->addNegotiator($negotiator, 0);
+
+ $this->themeAccessCheck->expects($this->any())
+ ->method('checkAccess')
+ ->will($this->returnValue(TRUE));
+
+ $request = Request::create('/test-route');
+ $theme = $this->themeNegotiator->determineActiveTheme($request);
+
+ $this->assertEquals('example_test', $theme);
+ $this->assertEquals('example_test', $request->attributes->get('_theme_active'));
+ }
+
+ /**
+ * Tests determining with two negotiators checking the priority.
+ *
+ * @see \Drupal\Core\Theme\ThemeNegotiator::determineActiveTheme()
+ */
+ public function testDetermineActiveThemeWithPriority() {
+ $negotiator = $this->getMock('Drupal\Core\Theme\ThemeNegotiatorInterface');
+ $negotiator->expects($this->once())
+ ->method('determineActiveTheme')
+ ->will($this->returnValue('example_test'));
+
+ $this->themeNegotiator->addNegotiator($negotiator, 10);
+
+ $negotiator = $this->getMock('Drupal\Core\Theme\ThemeNegotiatorInterface');
+ $negotiator->expects($this->never())
+ ->method('determineActiveTheme');
+
+ $this->themeNegotiator->addNegotiator($negotiator, 0);
+
+ $this->themeAccessCheck->expects($this->any())
+ ->method('checkAccess')
+ ->will($this->returnValue(TRUE));
+
+ $request = Request::create('/test-route');
+ $theme = $this->themeNegotiator->determineActiveTheme($request);
+
+ $this->assertEquals('example_test', $theme);
+ $this->assertEquals('example_test', $request->attributes->get('_theme_active'));
+ }
+
+ /**
+ * Tests determining with two negotiators of which just one returns access.
+ *
+ * @see \Drupal\Core\Theme\ThemeNegotiator::determineActiveTheme()
+ */
+ public function testDetermineActiveThemeWithAccessCheck() {
+ $negotiator = $this->getMock('Drupal\Core\Theme\ThemeNegotiatorInterface');
+ $negotiator->expects($this->once())
+ ->method('determineActiveTheme')
+ ->will($this->returnValue('example_test'));
+
+ $this->themeNegotiator->addNegotiator($negotiator, 10);
+
+ $negotiator = $this->getMock('Drupal\Core\Theme\ThemeNegotiatorInterface');
+ $negotiator->expects($this->once())
+ ->method('determineActiveTheme')
+ ->will($this->returnValue('example_test2'));
+
+ $this->themeNegotiator->addNegotiator($negotiator, 0);
+
+ $this->themeAccessCheck->expects($this->at(0))
+ ->method('checkAccess')
+ ->with('example_test')
+ ->will($this->returnValue(FALSE));
+
+ $this->themeAccessCheck->expects($this->at(1))
+ ->method('checkAccess')
+ ->with('example_test2')
+ ->will($this->returnValue(TRUE));
+
+ $request = Request::create('/test-route');
+ $theme = $this->themeNegotiator->determineActiveTheme($request);
+
+ $this->assertEquals('example_test2', $theme);
+ $this->assertEquals('example_test2', $request->attributes->get('_theme_active'));
+ }
+
+}