Skip to content
......@@ -6,6 +6,21 @@
* Adds contextual links to perform actions related to elements on a page.
*/
/**
* 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().
*/
......
......@@ -2,7 +2,5 @@ contextual.render:
path: '/contextual/render'
defaults:
_controller: '\Drupal\contextual\ContextualController::render'
options:
_theme: ajax_base_page
requirements:
_permission: 'access contextual links'
......@@ -91,9 +91,7 @@
}
.contextual-region .contextual .contextual-links a {
background-color: #fff;
/* This is an unfortunately necessary use of !important to prevent white
* links on a white background or some similar illegible combination. */
color: #333 !important;
color: #333;
display: block;
font-family: sans-serif;
font-size: small;
......
......@@ -17,6 +17,26 @@
use Drupal\entity\Entity\EntityDisplay;
use Drupal\user\TempStoreFactory;
/**
* Implements hook_menu().
*/
function edit_menu() {
// @todo Remove these menu items in http://drupal.org/node/1954892 when theme
// callbacks are replaced with something else.
$items['edit/metadata'] = array(
'route_name' => 'edit.metadata',
'theme callback' => 'ajax_base_page_theme',
'type' => MENU_CALLBACK,
);
$items['edit/form/%/%/%/%/%'] = array(
'route_name' => 'edit.field_form',
'theme callback' => 'ajax_base_page_theme',
'type' => MENU_CALLBACK,
);
return $items;
}
/**
* Implements hook_permission().
*/
......
......@@ -2,10 +2,9 @@ edit.metadata:
path: '/edit/metadata'
defaults:
_controller: '\Drupal\edit\EditController::metadata'
options:
_theme: ajax_base_page
requirements:
_permission: 'access in-place editing'
edit.attachments:
path: '/edit/attachments'
defaults:
......@@ -17,9 +16,6 @@ edit.field_form:
path: '/edit/form/{entity_type}/{entity}/{field_name}/{langcode}/{view_mode_id}'
defaults:
_controller: '\Drupal\edit\EditController::fieldForm'
options:
_access_mode: 'ALL'
_theme: ajax_base_page
requirements:
_permission: 'access in-place editing'
_access_edit_entity_field: 'TRUE'
......
......@@ -139,6 +139,21 @@ function editor_library_info() {
return $libraries;
}
/**
* 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().
*/
......
......@@ -2,8 +2,6 @@ editor.field_untransformed_text:
path: '/editor/{entity_type}/{entity}/{field_name}/{langcode}/{view_mode_id}'
defaults:
_controller: '\Drupal\editor\EditorController::getUntransformedText'
options:
_theme: ajax_base_page
requirements:
_permission: 'access in-place editing'
_access_edit_entity_field: 'TRUE'
......
......@@ -37,6 +37,21 @@ 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().
*
......
......@@ -2,8 +2,6 @@ file.ajax_upload:
path: '/file/ajax'
defaults:
_controller: '\Drupal\file\Controller\FileWidgetAjaxController::upload'
options:
_theme: ajax_base_page
requirements:
_permission: 'access content'
......
......@@ -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::service('router.builder')->rebuild();
\Drupal::state()->set('menu_rebuild_needed', TRUE);
}
function testSearchPageHook() {
......
......@@ -145,7 +145,6 @@ function testShortcutLinkDelete() {
*/
function testNoShortcutLink() {
// Change to a theme that displays shortcuts.
theme_enable(array('seven'));
\Drupal::config('system.theme')
->set('default', 'seven')
->save();
......@@ -158,9 +157,8 @@ 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/people.
$this->drupalGet('admin/people');
// link appears on admin/content/node.
$this->drupalGet('admin/content/node');
$this->assertRaw('add-shortcut', 'Add to shortcuts link was shown on a page the user does have access to.');
}
}
......@@ -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', 'administer 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->shortcut_user = $this->drupalCreateUser(array('customize shortcut links', 'switch shortcut sets'));
// Create a node.
......
......@@ -481,9 +481,18 @@ public function testThemeIntegration() {
$this->initializeTestThemeConfiguration();
$this->doTestThemeCallbackFakeTheme();
$this->initializeTestThemeConfiguration();
$this->doTestHookCustomTheme();
$this->initializeTestThemeConfiguration();
$this->doTestThemeCallbackHookCustomTheme();
$this->initializeTestThemeConfiguration();
$this->doTestThemeCallbackAdministrative();
$this->initializeTestThemeConfiguration();
$this->doTestThemeCallbackInheritance();
$this->initializeTestThemeConfiguration();
$this->doTestThemeCallbackNoThemeRequested();
......@@ -507,17 +516,27 @@ protected function initializeTestThemeConfiguration() {
}
/**
* Test the theme negotiation when it is set to use an administrative theme.
* Test the theme callback 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('Active theme: seven. Actual theme: seven.', 'The administrative theme can be correctly set in a theme negotiation.');
$this->assertText('Custom theme: seven. Actual theme: seven.', 'The administrative theme can be correctly set in a theme callback.');
$this->assertRaw('seven/style.css', "The administrative theme's CSS appears on the page.");
}
/**
* Test that the theme callback is properly inherited.
*/
protected function doTestThemeCallbackInheritance() {
theme_enable(array($this->admin_theme));
$this->drupalGet('menu-test/theme-callback/use-admin-theme/inheritance');
$this->assertText('Custom theme: seven. Actual theme: seven. Theme callback inheritance is being tested.', 'Theme callback inheritance correctly uses the administrative theme.');
$this->assertRaw('seven/style.css', "The administrative theme's CSS appears on the page.");
}
/**
* Test the theme negotiation when the site is in maintenance mode.
* Test the theme callback when the site is in maintenance mode.
*/
protected function doTestThemeCallbackMaintenanceMode() {
$this->container->get('state')->set('system.maintenance_mode', TRUE);
......@@ -532,44 +551,76 @@ 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('Active theme: seven. Actual theme: seven.', 'The theme negotiation system is correctly triggered for an administrator when the site is in maintenance mode.');
$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->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 negotiation when it is set to use an optional theme.
* Test the theme callback 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('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->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->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('Active theme: stark. Actual theme: stark.', 'The theme negotiation system uses an optional theme once it has been enabled.');
$this->assertText('Custom theme: stark. Actual theme: stark.', 'The theme callback 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 negotiation when it is set to use a theme that does not exist.
* Test the theme callback 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('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->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->assertRaw('bartik/css/style.css', "The default theme's CSS appears on the page.");
}
/**
* Test the theme negotiation when no theme is requested.
* Test the theme callback when no theme is requested.
*/
protected function doTestThemeCallbackNoThemeRequested() {
$this->drupalGet('menu-test/theme-callback/no-theme-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->assertText('Custom theme: NONE. Actual theme: bartik.', 'The theme callback 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.");
}
}
......@@ -178,7 +178,7 @@ function testThemeSettings() {
* Test the administration theme functionality.
*/
function testAdministrationTheme() {
theme_enable(array('bartik', 'seven'));
theme_enable(array('seven'));
// Enable an administration theme and show it on the node admin pages.
$edit = array(
......
......@@ -118,17 +118,6 @@ function testPreprocessForSuggestions() {
}
}
/**
* Tests the priority of some theme negotiators.
*/
public function testNegotiatorPriorities() {
$this->drupalGet('theme-test/priority');
// Ensure that the custom theme negotiator was not able to set the theme.
$this->assertNoText('Theme hook implementor=test_theme_theme_test__suggestion(). Foo=template_preprocess_theme_test', 'Theme hook suggestion ran with data available from a preprocess function for the base hook.');
}
/**
* Ensure page-front template suggestion is added when on front page.
*/
......@@ -279,4 +268,5 @@ function testPreprocessHtml() {
$this->assertText('theme test page bottom markup', 'Modules are able to set the page bottom region.');
}
}
<?php
/**
* @file
* Contains \Drupal\system\Theme\BatchNegotiator.
*/
namespace Drupal\system\Theme;
use Drupal\Core\Batch\BatchStorageInterface;
use Drupal\Core\Theme\ThemeNegotiatorInterface;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Sets the active theme for the batch page.
*/
class BatchNegotiator implements ThemeNegotiatorInterface {
/**
* The batch storage.
*
* @var \Drupal\Core\Batch\BatchStorageInterface
*/
protected $batchStorage;
/**
* Constructs a BatchNegotiator.
*
* @param \Drupal\Core\Batch\BatchStorageInterface $batch_storage
* The batch storage.
*/
public function __construct(BatchStorageInterface $batch_storage) {
$this->batchStorage = $batch_storage;
}
/**
* {@inheritdoc}
*/
public function determineActiveTheme(Request $request) {
if ($request->attributes->get(RouteObjectInterface::ROUTE_NAME) == 'system.batch_page') {
// Retrieve the current state of the batch.
$batch = &batch_get();
if (!$batch && $request->request->has('id')) {
$batch = $this->batchStorage->load($request->request->get('id'));
}
// Use the same theme as the page that started the batch.
if (!empty($batch['theme'])) {
return $batch['theme'];
}
}
}
}
......@@ -1425,6 +1425,36 @@ function hook_template_preprocess_default_variables_alter(&$variables) {
$variables['is_admin'] = user_access('access administration pages');
}
/**
* 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.
*
......
......@@ -824,6 +824,20 @@ 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',
......
......@@ -613,6 +613,12 @@ 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',
......@@ -806,6 +812,13 @@ function system_menu() {
'route_name' => 'system.status',
);
// Default page for batch operations.
$items['batch'] = array(
'route_name' => 'system.batch_page',
'theme callback' => '_system_batch_theme',
'type' => MENU_CALLBACK,
);
return $items;
}
......@@ -857,6 +870,21 @@ function system_theme_suggestions_region(array $variables) {
return $suggestions;
}
/**
* 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().
*/
......@@ -2103,6 +2131,19 @@ function system_page_build(&$page) {
}
}
/**
* Implements hook_custom_theme().
*/
function system_custom_theme() {
if (drupal_container()->isScopeActive('request')) {
$request = \Drupal::request();
$path = $request->attributes->get('_system_path');
if (user_access('view the administration theme') && path_is_admin($path)) {
return \Drupal::config('system.theme')->get('admin');
}
}
}
/**
* Implements hook_form_FORM_ID_alter().
*/
......
......@@ -2,8 +2,6 @@ system.ajax:
path: '/system/ajax'
defaults:
_controller: '\Drupal\system\Controller\FormAjaxController::content'
options:
_theme: ajax_base_page
requirements:
_access: 'TRUE'
......