diff --git a/core/core.services.yml b/core/core.services.yml index 465d145..b39fa87 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -153,6 +153,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'] + tags: + - { name: theme_negotiator } 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 54d4212..40a4971 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -2331,7 +2331,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')) { @@ -3128,7 +3128,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 6abab20..362e6b9 100644 --- a/core/includes/menu.inc +++ b/core/includes/menu.inc @@ -460,8 +460,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__); @@ -498,7 +498,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; @@ -1793,51 +1792,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() { @@ -3191,13 +3145,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'])) { @@ -3234,8 +3181,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', @@ -3300,8 +3245,6 @@ function _menu_router_save($menu, $masks) { 'title', 'title_callback', 'title_arguments', - 'theme_callback', - 'theme_arguments', 'type', 'description', 'description_callback', @@ -3332,8 +3275,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 e49bb76..805457c 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -93,16 +93,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. + $request = \Drupal::request(); + $theme = \Drupal::service('theme.negotiator')->determineActiveTheme($request) ?: 'stark'; // Store the identifier for retrieving theme settings with. $theme_key = $theme; @@ -117,7 +115,7 @@ function drupal_theme_initialize() { _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'); + \Drupal::moduleHandler()->resetImplementations(); } /** diff --git a/core/lib/Drupal/Core/CoreServiceProvider.php b/core/lib/Drupal/Core/CoreServiceProvider.php index 9cd8f5a..4bd88ee 100644 --- a/core/lib/Drupal/Core/CoreServiceProvider.php +++ b/core/lib/Drupal/Core/CoreServiceProvider.php @@ -22,6 +22,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; @@ -69,6 +70,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()); diff --git a/core/lib/Drupal/Core/EventSubscriber/LegacyRequestSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/LegacyRequestSubscriber.php index 39828ef..e1686b4 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 @@ 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. * * @return array @@ -47,6 +54,7 @@ public function onKernelRequestLegacy(GetResponseEvent $event) { */ static function getSubscribedEvents() { $events[KernelEvents::REQUEST][] = array('onKernelRequestLegacy', 90); + $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..8c412fe --- /dev/null +++ b/core/lib/Drupal/Core/Theme/AjaxBasePageNegotiator.php @@ -0,0 +1,69 @@ +csrfGenerator = $token_generator; + } + + /** + * {@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('_ajax_base_page_theme'))) { + 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']; + + // Ensure that the user only access a theme they are allowed to see. + if ($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 @@ +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..8d5b7ee --- /dev/null +++ b/core/lib/Drupal/Core/Theme/ThemeNegotiator.php @@ -0,0 +1,134 @@ +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) { + if (($active_theme = $negotiator->determineActiveTheme($request))) { + if ($this->themeAccess->checkAccess($active_theme)) { + $request->attributes->set('_theme_active', $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..9bb80b1 --- /dev/null +++ b/core/lib/Drupal/Core/Theme/ThemeNegotiatorInterface.php @@ -0,0 +1,28 @@ +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 ce1ecb7..e5fbc16 100644 --- a/core/modules/block/block.module +++ b/core/modules/block/block.module @@ -135,12 +135,6 @@ function block_menu() { 'type' => $key == $default_theme ? MENU_DEFAULT_LOCAL_TASK : MENU_LOCAL_TASK, 'route_name' => "block.admin_display_$key", ); - $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; } @@ -161,25 +155,6 @@ function _block_themes_access($theme) { } /** - * 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 c6704bc..39b414a 100644 --- a/core/modules/block/block.services.yml +++ b/core/modules/block/block.services.yml @@ -13,3 +13,7 @@ services: class: Drupal\block\Routing\RouteSubscriber tags: - { name: event_subscriber} + theme.negotiator.block.admin_demo: + class: Drupal\block\Theme\AdminDemoNegotiator + tags: + - { name: theme_negotiator } 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 @@ +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 661aac9..00672ad 100644 --- a/core/modules/content_translation/content_translation.module +++ b/core/modules/content_translation/content_translation.module @@ -165,7 +165,7 @@ function content_translation_menu() { if (content_translation_enabled($entity_type)) { $path = $info['menu_base_path']; $entity_position = count(explode('/', $path)) - 1; - $keys = array_flip(array('theme_callback', 'theme_arguments', 'access_callback', 'access_arguments', 'load_arguments')); + $keys = array_flip(array('access_callback', 'access_arguments', '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 529fa5c..508b9ae 100644 --- a/core/modules/contextual/contextual.module +++ b/core/modules/contextual/contextual.module @@ -6,21 +6,6 @@ */ /** - * 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..6947f6a 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: + _ajax_base_page_theme: 'TRUE' requirements: _permission: 'access contextual links' diff --git a/core/modules/edit/edit.module b/core/modules/edit/edit.module index 339e255..3a0b167 100644 --- a/core/modules/edit/edit.module +++ b/core/modules/edit/edit.module @@ -18,26 +18,6 @@ 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..4429299 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: + _ajax_base_page_theme: 'TRUE' 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' + _ajax_base_page_theme: 'TRUE' 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 6364102..cdf0a79 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..3d8776a 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: + _ajax_base_page_theme: 'TRUE' 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 bb0dd76..5a82f09 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..2b84612 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: + _ajax_base_page_theme: 'TRUE' 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 @@ function setUp() { // 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 @@ function testShortcutLinkDelete() { */ 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 @@ function testNoShortcutLink() { $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 @@ function setUp() { } // 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/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php index 85419d3..075f752 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/DrupalUnitTestBase.php @@ -122,6 +122,10 @@ protected function setUp() { $this->enableModules($modules, FALSE); // In order to use theme functions default theme config needs to exist. \Drupal::config('system.theme')->set('default', 'stark'); + + if (in_array('user', $modules)) { + $this->installSchema('user', 'users'); + } } protected function tearDown() { 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 f341681..1975154 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Menu/MenuRouterTest.php @@ -498,12 +498,6 @@ public function testThemeIntegration() { $this->doTestThemeCallbackFakeTheme(); $this->initializeTestThemeConfiguration(); - $this->doTestHookCustomTheme(); - - $this->initializeTestThemeConfiguration(); - $this->doTestThemeCallbackHookCustomTheme(); - - $this->initializeTestThemeConfiguration(); $this->doTestThemeCallbackAdministrative(); $this->initializeTestThemeConfiguration(); @@ -532,27 +526,27 @@ protected function initializeTestThemeConfiguration() { } /** - * 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->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 that the theme callback is properly inherited. + * Test that the theme negotiation 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. Theme negotiation inheritance is being tested.', 'Theme negotiation inheritance correctly uses the administrative theme.'); $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); @@ -567,76 +561,44 @@ protected function doTestThemeCallbackMaintenanceMode() { $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 @@ function testThemeSettings() { * 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/Theme/AdminNegotiator.php b/core/modules/system/lib/Drupal/system/Theme/AdminNegotiator.php new file mode 100644 index 0000000..72359ea --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Theme/AdminNegotiator.php @@ -0,0 +1,71 @@ +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/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 @@ +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 abda14e..3b427da 100644 --- a/core/modules/system/system.api.php +++ b/core/modules/system/system.api.php @@ -523,14 +523,14 @@ function hook_menu_get_item_alter(&$router_item, $path, $original_map) { * $items['admin/config/system/foo/tab1'] = array( * 'title' => 'Tab 1', * 'type' => MENU_DEFAULT_LOCAL_TASK, - * // Access callback, page callback, and theme callback will be inherited + * // Access callback, page callback will be inherited * // from 'admin/config/system/foo', if not specified here to override. * ); * // Make an additional tab called "Tab 2" on "Foo settings" * $items['admin/config/system/foo/tab2'] = array( * 'title' => 'Tab 2', * 'type' => MENU_LOCAL_TASK, - * // Page callback and theme callback will be inherited from + * // Page callback will be inherited from * // 'admin/config/system/foo', if not specified here to override. * // Need to add access callback or access arguments. * ); @@ -567,26 +567,6 @@ function hook_menu_get_item_alter(&$router_item, $path, $original_map) { * function, with path component substitution as described above. If the * access callback is inherited (see above), the access arguments will be * inherited with it, unless overridden in the child menu item. - * - "theme callback": (optional) A function returning the machine-readable - * name of the theme that will be used to render the page. If not provided, - * the value will be inherited from a parent menu item. If there is no - * theme callback, or if the function does not return the name of a current - * active theme on the site, the theme for this page will be determined by - * either hook_custom_theme() or the default theme instead. As a general - * rule, the use of theme callback functions should be limited to pages - * whose functionality is very closely tied to a particular theme, since - * they can only be overridden by modules which specifically target those - * pages in hook_menu_alter(). Modules implementing more generic theme - * switching functionality (for example, a module which allows the theme to - * be set dynamically based on the current user's role) should use - * hook_custom_theme() instead. - * - "theme arguments": An array of arguments to pass to the theme callback - * function, with path component substitution as described above. - * - "file": A file that will be included before the page callback is called; - * this allows page callback functions to be in separate files. The file - * should be relative to the implementing module's directory unless - * otherwise specified by the "file path" option. Does not apply to other - * callbacks (only page callback). * - "file path": The path to the directory containing the file specified in * "file". This defaults to the path to the module implementing the hook. * - "load arguments": An array of arguments to be passed to each of the @@ -1523,36 +1503,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 0d2dbeb..559ece1 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 80b02f8..6a1d5b0 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -608,12 +608,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', @@ -878,13 +872,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, - ); - // Localize date formats. if (\Drupal::moduleHandler()->moduleExists('language')) { $items['admin/config/regional/date-time/locale'] = array( @@ -958,21 +945,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() { diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml index 4d1c90ac..d400842 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: + _ajax_base_page_theme: 'TRUE' requirements: _access: 'TRUE' diff --git a/core/modules/system/system.services.yml b/core/modules/system/system.services.yml index 6a68e29..66faf76 100644 --- a/core/modules/system/system.services.yml +++ b/core/modules/system/system.services.yml @@ -23,3 +23,14 @@ services: class: Drupal\system\Routing\RouteSubscriber tags: - { name: event_subscriber } + theme.negotiator.system.batch: + class: Drupal\system\Theme\BatchNegotiator + arguments: ['@batch.storage'] + tags: + - { name: theme_negotiator } + + theme.negotiator.admin_theme: + class: Drupal\system\Theme\AdminNegotiator + arguments: ['@current_user', '@config.factory', '@entity.manager'] + tags: + - { name: theme_negotiator, priority: 40 } 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 802b34e..0d0eb28 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\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..36c03cb 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: + _ajax_base_page_theme: 'TRUE' 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 @@ +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 @@ +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 566a620..54854c5 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. @@ -482,7 +476,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 @@ -500,67 +494,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 @@ +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/theme_test.module b/core/modules/system/tests/modules/theme_test/theme_test.module index badea81..468fc9c 100644 --- a/core/modules/system/tests/modules/theme_test/theme_test.module +++ b/core/modules/system/tests/modules/theme_test/theme_test.module @@ -57,26 +57,6 @@ function theme_test_system_theme_info() { } /** - * Implements hook_menu(). - */ -function theme_test_menu() { - $items['theme-test/suggestion'] = array( - 'route_name' => 'theme_test.suggestion', - 'theme callback' => '_theme_custom_theme', - 'type' => MENU_CALLBACK, - ); - $items['theme-test/alter'] = array( - 'theme callback' => '_theme_custom_theme', - 'route_name' => 'theme_test.alter', - 'type' => MENU_CALLBACK, - ); - $items['theme-test/function-template-overridden'] = array( - 'theme callback' => '_theme_custom_theme', - 'route_name' => 'theme_test.function_template_override', - ); - return $items; -} -/** * Fake registry loading callback. */ function _theme_test_load_registry() { @@ -85,13 +65,6 @@ function _theme_test_load_registry() { } /** - * 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..8a750f3 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,8 @@ 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' @@ -29,6 +33,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..29c419d 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,8 @@ 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 } 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 @@ function setUp() { // 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/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 @@ +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..c70cad1 100644 --- a/core/modules/user/user.services.yml +++ b/core/modules/user/user.services.yml @@ -25,3 +25,9 @@ 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 } + 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..3894f75 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. 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..38b86c3 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 @@ */ class FieldUnitTest extends ViewUnitTestBase { - public static $modules = array('user'); + public static $modules = array('user', 'field'); /** * Views used by this test. 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 f420943..0e29538 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 @@ protected function setUp() { // 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/Tests/ViewPageControllerTest.php b/core/modules/views/lib/Drupal/views/Tests/ViewPageControllerTest.php index d4806f0..7e7afe1 100644 --- a/core/modules/views/lib/Drupal/views/Tests/ViewPageControllerTest.php +++ b/core/modules/views/lib/Drupal/views/Tests/ViewPageControllerTest.php @@ -25,7 +25,7 @@ class ViewPageControllerTest extends ViewUnitTestBase { * * @var array */ - public static $modules = array('user'); + public static $modules = array('user', 'field'); /** * Views used by this test. diff --git a/core/modules/views/lib/Drupal/views/ViewExecutable.php b/core/modules/views/lib/Drupal/views/ViewExecutable.php index fe11e11..bc6e190 100644 --- a/core/modules/views/lib/Drupal/views/ViewExecutable.php +++ b/core/modules/views/lib/Drupal/views/ViewExecutable.php @@ -1302,8 +1302,6 @@ public function render($display_id = NULL) { return; } - drupal_theme_initialize(); - $exposed_form = $this->display_handler->getPlugin('exposed_form'); $exposed_form->preRender($this->result); @@ -1365,11 +1363,13 @@ public function render($display_id = NULL) { $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 @@ public function render($display_id = NULL) { $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 f8f9ff5..7019e64 100644 --- a/core/modules/views/views.module +++ b/core/modules/views/views.module @@ -329,20 +329,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..83f2aad 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: + _ajax_base_page_theme: 'TRUE' 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..ac42e66 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Theme/ThemeNegotiatorTest.php @@ -0,0 +1,142 @@ + '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, 0); + + $negotiator = $this->getMock('Drupal\Core\Theme\ThemeNegotiatorInterface'); + $negotiator->expects($this->once()) + ->method('determineActiveTheme') + ->will($this->returnValue('example_test2')); + + $this->themeNegotiator->addNegotiator($negotiator, 10); + + $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, 0); + + $negotiator = $this->getMock('Drupal\Core\Theme\ThemeNegotiatorInterface'); + $negotiator->expects($this->once()) + ->method('determineActiveTheme') + ->will($this->returnValue('example_test2')); + + $this->themeNegotiator->addNegotiator($negotiator, 10); + + $this->themeAccessCheck->expects($this->at(0)) + ->method('checkAccess') + ->with('example_test2') + ->will($this->returnValue(TRUE)); + + $this->themeAccessCheck->expects($this->at(1)) + ->method('checkAccess') + ->with('example_test') + ->will($this->returnValue(FALSE)); + + $request = Request::create('/test-route'); + $theme = $this->themeNegotiator->determineActiveTheme($request); + + $this->assertEquals('example_test2', $theme); + $this->assertEquals('example_test2', $request->attributes->get('_theme_active')); + } + +}