Commit fc04601c authored by catch's avatar catch

Issue #1954892 by dawehner, tim.plunkett, David_Rothstein, effulgentsia:...

Issue #1954892 by dawehner, tim.plunkett, David_Rothstein, effulgentsia: Replace 'theme callback' and hook_custom_theme() with a clean theme negotiation system.
parent 14336d94
......@@ -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%' ]
......
......@@ -302,42 +302,6 @@ function ajax_render($commands = array()) {
return drupal_json_encode($commands);
}
/**
* 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.
*
......
......@@ -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();
}
}
......
......@@ -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;
......@@ -1795,51 +1794,6 @@ function drupal_help_arg($arg = array()) {
return $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.
*/
......@@ -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'],
......
......@@ -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');
}
/**
......
......@@ -23,6 +23,7 @@
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 @@ public function register(ContainerBuilder $container) {
// 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());
......
......@@ -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
......@@ -39,6 +36,16 @@ public function onKernelRequestLegacy(GetResponseEvent $event) {
}
}
/**
* 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.
*
......@@ -47,6 +54,8 @@ public function onKernelRequestLegacy(GetResponseEvent $event) {
*/
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;
}
......
<?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;
}
}
}
}
<?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');
}
}
<?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');
}
}
}
}
<?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);
}
<?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));
}
}
}
......@@ -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;
......
......@@ -109,41 +109,9 @@ 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().
*
......
......@@ -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 }
<?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') {