...
 
Commits (12)
......@@ -165,21 +165,6 @@ 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,6 +302,42 @@ 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.
*
......
......@@ -399,14 +399,15 @@ function drupal_add_feed($url = NULL, $title = '') {
if (isset($url)) {
$stored_feed_links[$url] = theme('feed_icon', array('url' => $url, 'title' => $title));
drupal_add_html_head_link(array(
$build['#attached']['drupal_add_html_head_link'][][] = array(
'rel' => 'alternate',
'type' => 'application/rss+xml',
'title' => $title,
// Force the URL to be absolute, for consistency with other <link> tags
// output by Drupal.
'href' => url($url, array('absolute' => TRUE)),
));
);
drupal_render($build);
}
return $stored_feed_links;
}
......@@ -2330,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 \Drupal\Core\Theme\AjaxBasePageNegotiator
// @see ajax_base_page_theme()
$setting['ajaxPageState']['theme'] = $theme_key;
// Checks that the DB is available before filling theme_token.
if (!defined('MAINTENANCE_MODE')) {
......@@ -3147,6 +3148,7 @@ 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', and 'access_arguments', will be filled in based
* on the database values and the objects loaded.
* 'title', 'page_arguments', 'access_arguments', and 'theme_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,6 +501,7 @@ 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;
......@@ -1794,6 +1795,51 @@ 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.
*/
......@@ -3020,6 +3066,13 @@ 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'])) {
......@@ -3056,6 +3109,8 @@ 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',
......@@ -3120,6 +3175,8 @@ function _menu_router_save($menu, $masks) {
'title',
'title_callback',
'title_arguments',
'theme_callback',
'theme_arguments',
'type',
'description',
'description_callback',
......@@ -3150,6 +3207,8 @@ 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,14 +92,16 @@ function drupal_theme_initialize() {
}
drupal_bootstrap(DRUPAL_BOOTSTRAP_DATABASE);
$themes = list_themes();
// @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';
// 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');
// 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;
// Store the identifier for retrieving theme settings with.
$theme_key = $theme;
......@@ -112,6 +114,9 @@ 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');
}
/**
......@@ -1268,8 +1273,14 @@ function template_preprocess_status_messages(&$variables) {
* is used as its CSS class. Each link should be itself an array, with the
* following elements:
* - title: The link text.
* - href: The link URL. If omitted, the 'title' is shown as a plain text
* item in the links list.
* - route_name: (optional) The name of the route to link to. If omitted
* (and if 'href' is omitted as well), the 'title' is shown as
* a plain text item in the links list.
* - route_parameters: (optional) An array of route parameters for the link.
* - href: (optional) The link URL. It is preferred to use 'route_name' and
* 'route parameters' for internal links. Use 'href' for links to external
* URLs. If omitted (and if 'route_name' is omitted as well), the 'title'
* is shown as a plain text item in the links list.
* - html: (optional) Whether or not 'title' is HTML. If set, the title
* will not be passed through
* \Drupal\Component\Utility\String::checkPlain().
......@@ -2215,7 +2226,12 @@ function template_preprocess_html(&$variables) {
if (theme_get_setting('features.favicon')) {
$favicon = theme_get_setting('favicon.url');
$type = theme_get_setting('favicon.mimetype');
drupal_add_html_head_link(array('rel' => 'shortcut icon', 'href' => Url::stripDangerousProtocols($favicon), 'type' => $type));
$build['#attached']['drupal_add_html_head_link'][][] = array(
'rel' => 'shortcut icon',
'href' => Url::stripDangerousProtocols($favicon),
'type' => $type,
);
drupal_render($build);
}
$site_config = \Drupal::config('system.site');
......@@ -2498,7 +2514,12 @@ function template_preprocess_maintenance_page(&$variables) {
if (theme_get_setting('features.favicon')) {
$favicon = theme_get_setting('favicon.url');
$type = theme_get_setting('favicon.mimetype');
drupal_add_html_head_link(array('rel' => 'shortcut icon', 'href' => Url::stripDangerousProtocols($favicon), 'type' => $type));
$build['#attached']['drupal_add_html_head_link'][][] = array(
'rel' => 'shortcut icon',
'href' => Url::stripDangerousProtocols($favicon),
'type' => $type,
);
drupal_render($build);
}
// Get all region content set with drupal_add_region_content().
......
......@@ -9,9 +9,6 @@
/**
* Defines an object which stores multiple plugin instances to lazy load them.
*
* The \ArrayAccess implementation is only for backwards compatibility, it is
* deprecated and should not be used by new code.
*/
abstract class PluginBag implements \Iterator, \Countable {
......@@ -30,7 +27,7 @@ abstract class PluginBag implements \Iterator, \Countable {
protected $instanceIDs = array();
/**
* Initializes a plugin and stores the result in $this->pluginInstances.
* Initializes and stores a plugin.
*
* @param string $instance_id
* The ID of the plugin instance to initialize.
......@@ -85,7 +82,7 @@ public function set($instance_id, $value) {
/**
* Removes an initialized plugin.
*
* The plugin can still be used, it will be reinitialized.
* The plugin can still be used; it will be reinitialized.
*
* @param string $instance_id
* The ID of the plugin instance to remove.
......@@ -95,7 +92,7 @@ public function remove($instance_id) {
}
/**
* Adds an instance ID to the array of available instance IDs.
* Adds an instance ID to the available instance IDs.
*
* @param string $id
* The ID of the plugin instance to add.
......@@ -117,7 +114,7 @@ public function getInstanceIds() {
}
/**
* Sets the instance IDs property.
* Sets all instance IDs.
*
* @param array $instance_ids
* An associative array of instance IDs.
......@@ -138,28 +135,28 @@ public function removeInstanceId($instance_id) {
}
/**
* Implements \Iterator::current().
* {@inheritdoc}
*/
public function current() {
return $this->get($this->key());
}
/**
* Implements \Iterator::next().
* {@inheritdoc}
*/
public function next() {
next($this->instanceIDs);
}
/**
* Implements \Iterator::key().
* {@inheritdoc}
*/
public function key() {
return key($this->instanceIDs);
}
/**
* Implements \Iterator::valid().
* {@inheritdoc}
*/
public function valid() {
$key = key($this->instanceIDs);
......@@ -167,14 +164,14 @@ public function valid() {
}
/**
* Implements \Iterator::rewind().
* {@inheritdoc}
*/
public function rewind() {
reset($this->instanceIDs);
}
/**
* Implements \Countable::count().
* {@inheritdoc}
*/
public function count() {
return count($this->instanceIDs);
......
......@@ -204,14 +204,11 @@ public function listAll($prefix = '') {
throw new StorageException($this->directory . '/ not found.');
}
$extension = '.' . static::getFileExtension();
$files = new \GlobIterator(DRUPAL_ROOT . '/' . $this->directory . '/' . $prefix . '*' . $extension);
$names = array();
foreach ($files as $file) {
$names[] = $file->getBasename($extension);
}
return $names;
$files = glob($this->directory . '/' . $prefix . '*' . $extension);
$clean_name = function ($value) use ($extension) {
return basename($value, $extension);
};
return array_map($clean_name, $files);
}
/**
......
......@@ -134,9 +134,10 @@ public function getComponentNames($type, array $list) {
foreach ($list as $name) {
$directory = $this->getComponentFolder($type, $name);
if (file_exists($directory)) {
$files = new \GlobIterator(DRUPAL_ROOT . '/' . $directory . '/*' . $extension);
foreach ($files as $file) {
$folders[$file->getBasename($extension)] = $directory;
$files = glob($directory . '/*' . $extension);
foreach ($files as $filename) {
$name = basename($filename, $extension);
$folders[$name] = $directory;
}
}
}
......
......@@ -23,7 +23,6 @@
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;
......@@ -72,9 +71,6 @@ 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());
......
......@@ -225,16 +225,11 @@ class EntityType extends Plugin {
public $bundle_keys;
/**
* The base router path for the entity type's field administration page.
*
* If the entity type has a bundle, include {bundle} in the path.
*
* For example, the node entity type specifies
* "admin/structure/types/manage/{bundle}" as its base field admin path.
* The name of the entity type which provides bundles.
*
* @var string (optional)
*/
public $route_base_path;
public $bundle_entity_type = 'bundle';
/**
* Link templates using the URI template syntax.
......
......@@ -301,9 +301,9 @@ public function getAdminPath($entity_type, $bundle) {
$admin_path = '';
$entity_info = $this->getDefinition($entity_type);
// Check for an entity type's admin base path.
if (isset($entity_info['route_base_path'])) {
// Replace any dynamic 'bundle' portion of the path with the actual bundle.
$admin_path = str_replace('{bundle}', $bundle, $entity_info['route_base_path']);
if (isset($entity_info['links']['admin-form'])) {
$route_parameters[$entity_info['bundle_entity_type']] = $bundle;
$admin_path = \Drupal::urlGenerator()->getPathFromRoute($entity_info['links']['admin-form'], $route_parameters);
}
return $admin_path;
......@@ -313,10 +313,11 @@ public function getAdminPath($entity_type, $bundle) {
* {@inheritdoc}
*/
public function getAdminRouteInfo($entity_type, $bundle) {
$entity_info = $this->getDefinition($entity_type);
return array(
'route_name' => "field_ui.overview_$entity_type",
'route_parameters' => array(
'bundle' => $bundle,
$entity_info['bundle_entity_type'] => $bundle,
)
);
}
......
......@@ -164,6 +164,11 @@ public function getViewBuilder($entity_type);
*
* @return string
* The administration path for an entity type bundle, if it exists.
*
* @deprecated since version 8.0
* System paths should not be used - use route names and parameters.
*
* @see self::getAdminRouteInfo()
*/
public function getAdminPath($entity_type, $bundle);
......
......@@ -27,6 +27,9 @@ 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
......@@ -36,16 +39,6 @@ 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.
*
......@@ -54,8 +47,6 @@ public function onKernelRequestLegacyAfterRouting(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 \Drupal\Core\Theme\AjaxBasePageNegotiator
// @see ajax_base_page_theme()
// @see drupal_get_css()
// @see drupal_get_js()
var pageState = drupalSettings.ajaxPageState;
......
......@@ -2,9 +2,7 @@
/**
* @file
* Provides views data and handlers for action.module.
*
* @ingroup views_module_handlers
* Provides views data for action.module.
*/
/**
......
......@@ -3,8 +3,6 @@
/**
* @file
* Provides views data for aggregator.module.
*
* @ingroup views_module_handlers
*/
/**
......
......@@ -109,9 +109,41 @@ 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,8 +9,3 @@ 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 }
......@@ -36,10 +36,10 @@
* admin_permission = "administer blocks",
* base_table = "custom_block",
* revision_table = "custom_block_revision",
* route_base_path = "admin/structure/block/custom-blocks/manage/{bundle}",
* links = {
* "canonical" = "custom_block.edit",
* "edit-form" = "custom_block.edit"
* "edit-form" = "custom_block.edit",
* "admin-form" = "custom_block.type_edit"
* },
* fieldable = TRUE,
* translatable = TRUE,
......@@ -52,7 +52,8 @@
* },
* bundle_keys = {
* "bundle" = "type"
* }
* },
* bundle_entity_type = "custom_block_type"
* )
*/
class CustomBlock extends ContentEntityBase implements CustomBlockInterface {
......
<?php
/**
* @file
* Contains \Drupal\block\Theme\AdminDemoNegotiator.
*/
namespace Drupal\block\Theme;
use Drupal\Core\Theme\ThemeNegotiatorInterface;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Negotiates the theme for the block admin demo page via the URL.
*/
class AdminDemoNegotiator implements ThemeNegotiatorInterface {
/**
* {@inheritdoc}
*/
public function determineActiveTheme(Request $request) {
// We return exactly what was passed in, to guarantee that the page will
// always be displayed using the theme whose blocks are being configured.
if ($request->attributes->get(RouteObjectInterface::ROUTE_NAME) == 'block.admin_demo') {
return $request->attributes->get('theme');
}
}
}
......@@ -696,28 +696,43 @@ function template_preprocess_book_navigation(&$variables) {
if ($book_link['mlid']) {
$variables['tree'] = book_children($book_link);
$build = array();
if ($prev = book_prev($book_link)) {
$prev_href = url($prev['href']);
drupal_add_html_head_link(array('rel' => 'prev', 'href' => $prev_href));
$build['#attached']['drupal_add_html_head_link'][][] = array(
'rel' => 'prev',
'href' => $prev_href,
);
$variables['prev_url'] = $prev_href;
$variables['prev_title'] = check_plain($prev['title']);
}
if ($book_link['plid'] && $parent = book_link_load($book_link['plid'])) {
$parent_href = url($parent['link_path']);
drupal_add_html_head_link(array('rel' => 'up', 'href' => $parent_href));
$build['#attached']['drupal_add_html_head_link'][][] = array(
'rel' => 'up',
'href' => $parent_href,
);
$variables['parent_url'] = $parent_href;
$variables['parent_title'] = check_plain($parent['title']);
}
if ($next = book_next($book_link)) {
$next_href = url($next['href']);
drupal_add_html_head_link(array('rel' => 'next', 'href' => $next_href));
$build['#attached']['drupal_add_html_head_link'][][] = array(
'rel' => 'next',
'href' => $next_href,
);
$variables['next_url'] = $next_href;
$variables['next_title'] = check_plain($next['title']);
}
}
if (!empty($build)) {
drupal_render($build);
}
$variables['has_links'] = FALSE;
// Link variables to filter for values and set state of the flag variable.
$links = array('prev_url', 'prev_title', 'parent_url', 'parent_title', 'next_url', 'next_title');
......
......@@ -320,9 +320,9 @@ public function getLangcodes() {
if (empty($langcodes)) {
$langcodes = array();
// Collect languages included with CKEditor based on file listing.
$ckeditor_languages = new \GlobIterator(DRUPAL_ROOT . '/core/assets/vendor/ckeditor/lang/*.js');
foreach ($ckeditor_languages as $language_file) {
$langcode = $language_file->getBasename('.js');
$ckeditor_languages = glob(DRUPAL_ROOT . '/core/assets/vendor/ckeditor/lang/*.js');
foreach ($ckeditor_languages as $language_filename) {
$langcode = basename($language_filename, '.js');
$langcodes[$langcode] = $langcode;
}
cache('ckeditor.languages')->set('langcodes', $langcodes);
......
......@@ -1731,3 +1731,20 @@ function comment_library_info() {
);
return $libraries;
}
/**
* #post_render_cache callback; replaces the placeholder with the comment form.
*
* @param array $context
* An array with the following keys:
* - entity_type: an entity type
* - entity_id: an entity ID
* - field_name: a comment field name
*
* @return array $element
* The updated $element.
*/
function comment_replace_form_placeholder(array $context) {
$entity = entity_load($context['entity_type'], $context['entity_id']);
return comment_add($entity, $context['field_name']);
}
......@@ -2,9 +2,7 @@
/**
* @file
* Provide views data and handlers for comment.module.
*
* @ingroup views_module_handlers
* Provide views data for comment.module.
*/
/**
......
......@@ -36,7 +36,6 @@
* fieldable = TRUE,
* translatable = TRUE,
* render_cache = FALSE,
* route_base_path = "admin/structure/comments/manage/{bundle}",
* entity_keys = {
* "id" = "cid",
* "bundle" = "field_id",
......@@ -48,7 +47,8 @@
* },
* links = {
* "canonical" = "comment.permalink",
* "edit-form" = "comment.edit_page"
* "edit-form" = "comment.edit_page",
* "admin-form" = "comment.bundle"
* }
* )
*/
......
......@@ -138,7 +138,24 @@ public function viewElements(FieldItemListInterface $items) {
if ($status == COMMENT_OPEN && $comment_settings['form_location'] == COMMENT_FORM_BELOW) {
// Only show the add comment form if the user has permission.
if ($this->currentUser->hasPermission('post comments')) {
$output['comment_form'] = comment_add($entity, $field_name);
// All users in the "anonymous" role can use the same form: it is fine
// for this form to be stored in the render cache.
if ($this->currentUser->isAnonymous()) {
$output['comment_form'] = comment_add($entity, $field_name);
}
// All other users need a user-specific form, which would break the
// render cache: hence use a #post_render_cache callback.
else {
$output['comment_form'] = array(
'#type' => 'render_cache_placeholder',
'#callback' => 'comment_replace_form_placeholder',
'#context' => array(
'entity_type' => $entity->entityType(),
'entity_id' => $entity->id(),
'field_name' => $field_name
),
);
}
}
}
......
......@@ -229,11 +229,11 @@ function testCommentFunctionality() {
));
$this->drupalLogin($limited_user);
// Test that default field exists.
$this->drupalGet('admin/structure/entity-test/manage/entity_test/fields');
$this->drupalGet('entity_test/structure/entity_test/fields');
$this->assertText(t('Comment settings'));
$this->assertLinkByHref('admin/structure/entity-test/manage/entity_test/fields/entity_test.entity_test.comment');
$this->assertLinkByHref('entity_test/structure/entity_test/fields/entity_test.entity_test.comment');
// Test widget hidden option is not visible when there's no comments.
$this->drupalGet('admin/structure/entity-test/manage/entity_test/entity-test/fields/entity_test.entity_test.comment');
$this->drupalGet('entity_test/structure/entity_test/entity-test/fields/entity_test.entity_test.comment');
$this->assertNoField('edit-default-value-input-comment-und-0-status-0');
$this->drupalLogin($this->admin_user);
......@@ -343,20 +343,20 @@ function testCommentFunctionality() {
'administer entity_test content',
));
$this->drupalLogin($limited_user);
$this->drupalGet('admin/structure/entity-test/manage/entity_test/fields/entity_test.entity_test.comment');
$this->drupalGet('entity_test/structure/entity_test/fields/entity_test.entity_test.comment');
$this->assertNoFieldChecked('edit-default-value-input-comment-0-status-0');
$this->assertNoFieldChecked('edit-default-value-input-comment-0-status-1');
$this->assertFieldChecked('edit-default-value-input-comment-0-status-2');
// Test comment option change in field settings.
$edit = array('default_value_input[comment][0][status]' => COMMENT_CLOSED);
$this->drupalPostForm(NULL, $edit, t('Save settings'));
$this->drupalGet('admin/structure/entity-test/manage/entity_test/fields/entity_test.entity_test.comment');
$this->drupalGet('entity_test/structure/entity_test/fields/entity_test.entity_test.comment');
$this->assertNoFieldChecked('edit-default-value-input-comment-0-status-0');
$this->assertFieldChecked('edit-default-value-input-comment-0-status-1');
$this->assertNoFieldChecked('edit-default-value-input-comment-0-status-2');
// Add a new comment field.
$this->drupalGet('admin/structure/entity-test/manage/entity_test/fields');
$this->drupalGet('entity_test/structure/entity_test/fields');
$edit = array(
'fields[_add_new_field][label]' => 'Foobar',
'fields[_add_new_field][field_name]' => 'foobar',
......
......@@ -23,13 +23,10 @@ class ConfigFieldInstanceMapper extends ConfigEntityMapper {
*/
public function getBaseRouteParameters() {
$parameters = parent::getBaseRouteParameters();
// @todo All core content entity path placeholders can be fully filled in
// with an additional {bundle} value in their paths, but a more
// predictable solution would be ideal. See
// https://drupal.org/node/2134871
$base_entity_info = $this->entityManager->getDefinition($this->pluginDefinition['base_entity_type']);
// @todo Field instances have no method to return the bundle the instance is
// attached to. See https://drupal.org/node/2134861
$parameters['bundle'] = $this->entity->bundle;
$parameters[$base_entity_info['bundle_entity_type']] = $this->entity->bundle;
return $parameters;
}
......
contact.category_edit:
title: 'Edit'
route_name: contact.category_edit
tab_root_id: contact.category_edit
......@@ -67,10 +67,6 @@ function contact_menu() {
'title' => 'Edit contact category',
'route_name' => 'contact.category_edit',
);
$items['admin/structure/contact/manage/%contact_category/edit'] = array(
'title' => 'Edit',
'type' => MENU_DEFAULT_LOCAL_TASK,
);
$items['contact'] = array(
'title' => 'Contact',
......
......@@ -3,8 +3,6 @@
/**
* @file
* Provide views data for contact.module.
*
* @ingroup views_module_handlers
*/
/**
......
......@@ -26,10 +26,13 @@
* entity_keys = {
* "bundle" = "category"
* },
* route_base_path = "admin/structure/contact/manage/{bundle}",
* bundle_entity_type = "contact_category",
* fieldable = TRUE,
* bundle_keys = {
* "bundle" = "id"
* },
* links = {
* "admin-form" = "contact.category_edit"
* }
* )
*/
......
......@@ -156,7 +156,7 @@ function content_translation_menu() {
if (content_translation_enabled($entity_type)) {
$path = _content_translation_link_to_router_path($entity_type, $info['links']['canonical']);
$entity_position = count(explode('/', $path)) - 1;
$keys = array_flip(array('load_arguments'));
$keys = array_flip(array('theme_callback', 'theme_arguments', 'load_arguments'));
$menu_info = array_intersect_key($info['translation']['content_translation'], $keys) + array('file' => 'content_translation.pages.inc');
$item = array();
......
......@@ -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'
......@@ -2,9 +2,7 @@
/**
* @file
* Provide views data and handlers for contextual.module.
*
* @ingroup views_module_handlers
* Provide views data for contextual.module.
*/
/**
......
......@@ -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;
......
......@@ -3,8 +3,6 @@
/**
* @file
* Provide views data for dblog.module.
*
* @ingroup views_module_handlers
*/
/**
......
......@@ -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;
}
/**