Newer
Older
Dries Buytaert
committed
<?php
/**
* @file
* Administration toolbar for quick access to top level administration items.
*/
use Drupal\Core\Cache\Cache;
use Drupal\Core\Menu\MenuTreeParameters;
Angie Byron
committed
use Drupal\Core\Render\Element;
Angie Byron
committed
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Template\Attribute;
Alex Pott
committed
use Drupal\Component\Datetime\DateTimePlus;
Angie Byron
committed
use Drupal\Component\Utility\Crypt;
Alex Pott
committed
use Drupal\Component\Utility\String;
use Drupal\user\RoleInterface;
use Drupal\user\UserInterface;
/**
* Implements hook_help().
*/
Angie Byron
committed
function toolbar_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.toolbar':
$output = '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The Toolbar module displays links to top-level administration menu items and links from other modules at the top of the screen. For more information, see the online handbook entry for <a href="@toolbar">Toolbar module</a>.', array('@toolbar' => 'http://drupal.org/documentation/modules/toolbar')) . '</p>';
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<dl>';
$output .= '<dt>' . t('Displaying administrative links') . '</dt>';
Angie Byron
committed
$output .= '<dd>' . t('The Toolbar module displays a bar containing top-level administrative components across the top of the screen. Below that, the Toolbar module has a <em>drawer</em> section where it displays links provided by other modules, such as the core <a href="@shortcuts-help">Shortcut module</a>. The drawer can be hidden/shown by clicking on its corresponding tab.', array('@shortcuts-help' => url('admin/help/shortcut'))) . '</dd>';
$output .= '</dl>';
return $output;
}
}
Dries Buytaert
committed
/**
Dries Buytaert
committed
* Implements hook_permission().
Dries Buytaert
committed
*/
function toolbar_permission() {
Dries Buytaert
committed
return array(
'access toolbar' => array(
'title' => t('Use the administration toolbar'),
Dries Buytaert
committed
),
);
}
/**
Dries Buytaert
committed
* Implements hook_theme().
Dries Buytaert
committed
*/
function toolbar_theme($existing, $type, $theme, $path) {
$items['toolbar'] = array(
Angie Byron
committed
'render element' => 'element',
Angie Byron
committed
'template' => 'toolbar',
Angie Byron
committed
);
$items['toolbar_item'] = array(
'render element' => 'element',
);
Angie Byron
committed
Angie Byron
committed
return $items;
}
Angie Byron
committed
/**
* Implements hook_element_info().
*/
function toolbar_element_info() {
$elements = array();
$elements['toolbar'] = array(
'#pre_render' => array('toolbar_pre_render'),
'#theme' => 'toolbar',
'#attached' => array(
'library' => array(
'toolbar/toolbar',
Angie Byron
committed
),
),
// Metadata for the toolbar wrapping element.
'#attributes' => array(
// The id cannot be simply "toolbar" or it will clash with the simpletest
// tests listing which produces a checkbox with attribute id="toolbar"
'id' => 'toolbar-administration',
Angie Byron
committed
'class' => array('toolbar'),
'role' => 'group',
'aria-label' => t('Site administration toolbar'),
Angie Byron
committed
),
// Metadata for the administration bar.
'#bar' => array(
'#heading' => t('Toolbar items'),
'#attributes' => array(
'id' => 'toolbar-bar',
'class' => array('toolbar-bar', 'clearfix',),
'role' => 'navigation',
'aria-label' => t('Toolbar items'),
Angie Byron
committed
),
),
);
// A toolbar item is wrapped in markup for common styling. The 'tray'
Angie Byron
committed
// property contains a renderable array.
Angie Byron
committed
$elements['toolbar_item'] = array(
'#pre_render' => array('toolbar_pre_render_item'),
'#theme' => 'toolbar_item',
'tab' => array(
'#type' => 'link',
'#title' => NULL,
'#href' => '',
),
);
return $elements;
}
Angie Byron
committed
/**
* Use Drupal's page cache for toolbar/subtrees/*, even for authenticated users.
Dries Buytaert
committed
*
* This gets invoked after full bootstrap, so must duplicate some of what's
* done by \Drupal\Core\DrupalKernel::handlePageCache().
*
* @todo Replace this hack with something better integrated with DrupalKernel
* once Drupal's page caching itself is properly integrated.
Angie Byron
committed
*/
function _toolbar_initialize_page_cache() {
$GLOBALS['conf']['system.performance']['cache']['page']['enabled'] = TRUE;
drupal_page_is_cacheable(TRUE);
// If we have a cache, serve it.
// @see \Drupal\Core\DrupalKernel::handlePageCache()
$request = \Drupal::request();
$response = drupal_page_get_cache($request);
if ($response) {
$response->headers->set('X-Drupal-Cache', 'HIT');
drupal_serve_page_from_cache($response, $request);
$response->prepare($request);
$response->send();
// We are done.
exit;
}
// The Expires HTTP header is the heart of the client-side HTTP caching. The
// additional server-side page cache only takes effect when the client
// accesses the callback URL again (e.g., after clearing the browser cache or
// when force-reloading a Drupal page).
$max_age = 3600 * 24 * 365;
Alex Pott
committed
drupal_add_http_header('Expires', gmdate(DateTimePlus::RFC7231, REQUEST_TIME + $max_age));
drupal_add_http_header('Cache-Control', 'private, max-age=' . $max_age);
Angie Byron
committed
}
Dries Buytaert
committed
/**
Dries Buytaert
committed
* Implements hook_page_build().
Angie Byron
committed
*
Dries Buytaert
committed
* Add admin toolbar to the page_top region automatically.
*/
Angie Byron
committed
function toolbar_page_build(&$page) {
$page['page_top']['toolbar'] = array(
Angie Byron
committed
'#type' => 'toolbar',
'#access' => \Drupal::currentUser()->hasPermission('access toolbar'),
);
}
/**
Angie Byron
committed
* Builds the Toolbar as a structured array ready for drupal_render().
*
* Since building the toolbar takes some time, it is done just prior to
* rendering to ensure that it is built only if it will be displayed.
Angie Byron
committed
* @param array $element
* A renderable array.
*
* @return
* A renderable array.
*
* @see toolbar_page_build().
*/
Angie Byron
committed
function toolbar_pre_render($element) {
// Get the configured breakpoints to switch from vertical to horizontal
// toolbar presentation.
Angie Byron
committed
$breakpoints = \Drupal::service('breakpoint.manager')->getBreakpointsByGroup('toolbar');
Angie Byron
committed
if (!empty($breakpoints)) {
Angie Byron
committed
$media_queries = array();
foreach ($breakpoints as $id => $breakpoint) {
$media_queries[$id] = $breakpoint->getMediaQuery();
}
Angie Byron
committed
$element['#attached']['js'][] = array(
Angie Byron
committed
'data' => array(
'toolbar' => array(
'breakpoints' => $media_queries,
)
),
Angie Byron
committed
'type' => 'setting',
);
}
// Get toolbar items from all modules that implement hook_toolbar().
$items = \Drupal::moduleHandler()->invokeAll('toolbar');
Angie Byron
committed
// Allow for altering of hook_toolbar().
\Drupal::moduleHandler()->alter('toolbar', $items);
Angie Byron
committed
// Sort the children.
uasort($items, array('\Drupal\Component\Utility\SortArray', 'sortByWeightProperty'));
Angie Byron
committed
// Merge in the original toolbar values.
$element = array_merge($element, $items);
// Render the children.
$element['#children'] = drupal_render_children($element);
return $element;
}
/**
Angie Byron
committed
* Prepares variables for administration toolbar templates.
*
* Default template: toolbar.html.twig.
Angie Byron
committed
*
* @param array $variables
* An associative array containing:
* - element: An associative array containing the properties and children of
* the tray. Properties used: #children, #attributes and #bar.
*/
Angie Byron
committed
function template_preprocess_toolbar(&$variables) {
$element = $variables['element'];
// Prepare the toolbar attributes.
$variables['attributes'] = $element['#attributes'];
$variables['toolbar_attributes'] = new Attribute($element['#bar']['#attributes']);
$variables['toolbar_heading'] = $element['#bar']['#heading'];
// Prepare the trays and tabs for each toolbar item as well as the remainder
// variable that will hold any non-tray, non-tab elements.
$variables['trays'] = array();
$variables['tabs'] = array();
$variables['remainder'] = array();
Angie Byron
committed
foreach (Element::children($element) as $key) {
Angie Byron
committed
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
// Add the tray.
if (isset($element[$key]['tray'])) {
$variables['trays'][$key] = array(
'links' => $element[$key]['tray'],
'attributes' => new Attribute($element[$key]['tray']['#wrapper_attributes']),
);
if (array_key_exists('#heading', $element[$key]['tray'])) {
$variables['trays'][$key]['label'] = $element[$key]['tray']['#heading'];
}
}
// Pass the wrapper attributes along.
if (array_key_exists('#wrapper_attributes', $element[$key])) {
$element[$key]['#wrapper_attributes']['class'][] = 'toolbar-tab';
$attributes = $element[$key]['#wrapper_attributes'];
}
else {
$attributes = array('class' => array('toolbar-tab'));
}
// Add the tab.
$variables['tabs'][$key] = array(
'link' => $element[$key]['tab'],
'attributes' => new Attribute($attributes),
);
// Add other non-tray, non-tab child elements to the remainder variable for
// later rendering.
Angie Byron
committed
foreach (Element::children($element[$key]) as $child_key) {
Angie Byron
committed
if (!in_array($child_key, array('tray', 'tab'))) {
$variables['remainder'][$key][$child_key] = $element[$key][$child_key];
}
Angie Byron
committed
}
}
Dries Buytaert
committed
}
/**
Angie Byron
committed
* Provides markup for associating a tray trigger with a tray element.
*
* A tray is a responsive container that wraps renderable content. Trays present
* content well on small and large screens alike.
*
* @param array $element
* A renderable array.
*
* @return
* A renderable array.
Dries Buytaert
committed
*/
Angie Byron
committed
function toolbar_pre_render_item($element) {
// Assign each item a unique ID.
$id = drupal_html_id('toolbar-item');
// Provide attributes for a toolbar item.
$attributes = array(
'id' => $id,
);
Angie Byron
committed
// If tray content is present, markup the tray and its associated trigger.
if (!empty($element['tray'])) {
// Provide attributes necessary for trays.
$attributes += array(
'data-toolbar-tray' => $id . '-tray',
Angie Byron
committed
'aria-owns' => $id,
'role' => 'button',
'aria-pressed' => 'false',
);
Angie Byron
committed
// Merge in module-provided attributes.
$element['tab'] += array('#attributes' => array());
$element['tab']['#attributes'] += $attributes;
$element['tab']['#attributes']['class'][] = 'trigger';
// Provide attributes for the tray theme wrapper.
$attributes = array(
'id' => $id . '-tray',
'data-toolbar-tray' => $id . '-tray',
'aria-owned-by' => $id,
Angie Byron
committed
);
// Merge in module-provided attributes.
if (!isset($element['tray']['#wrapper_attributes'])) {
$element['tray']['#wrapper_attributes'] = array();
}
$element['tray']['#wrapper_attributes'] += $attributes;
$element['tray']['#wrapper_attributes']['class'][] = 'toolbar-tray';
Angie Byron
committed
}
Angie Byron
committed
$element['tab']['#attributes']['class'][] = 'toolbar-item';
Angie Byron
committed
return $element;
}
Dries Buytaert
committed
/**
* Implements hook_toolbar().
Dries Buytaert
committed
*/
function toolbar_toolbar() {
// The 'Home' tab is a simple link, with no corresponding tray.
$items['home'] = array(
Angie Byron
committed
'#type' => 'toolbar_item',
'tab' => array(
Angie Byron
committed
'#type' => 'link',
'#title' => t('Back to site'),
Angie Byron
committed
'#href' => '<front>',
Angie Byron
committed
'#attributes' => array(
'title' => t('Return to site content'),
'class' => array('toolbar-icon', 'toolbar-icon-escape-admin'),
'data-toolbar-escape-admin' => TRUE,
Dries Buytaert
committed
),
Dries Buytaert
committed
),
'#wrapper_attributes' => array(
'class' => array('hidden'),
),
'#attached' => array(
'library' => array(
'toolbar/toolbar.escapeAdmin',
),
),
Angie Byron
committed
'#weight' => -20,
Dries Buytaert
committed
);
// To conserve bandwidth, we only include the top-level links in the HTML.
// The subtrees are fetched through a JSONP script that is generated at the
// toolbar_subtrees route. We provide the JavaScript requesting that JSONP
// script here with the hash parameter that is needed for that route.
// @see toolbar_subtrees_jsonp()
Alex Pott
committed
$langcode = \Drupal::languageManager()->getCurrentLanguage()->id;
$subtrees_attached['js'][] = array(
'type' => 'setting',
'data' => array('toolbar' => array(
Alex Pott
committed
'subtreesHash' => _toolbar_get_subtrees_hash($langcode),
'langcode' => $langcode,
)),
);
Angie Byron
committed
// The administration element has a link that is themed to correspond to
// a toolbar tray. The tray contains the full administrative menu of the site.
$items['administration'] = array(
Angie Byron
committed
'#type' => 'toolbar_item',
'tab' => array(
Angie Byron
committed
'#type' => 'link',
'#title' => t('Manage'),
Angie Byron
committed
'#href' => 'admin',
Angie Byron
committed
'#attributes' => array(
'title' => t('Admin menu'),
'class' => array('toolbar-icon', 'toolbar-icon-menu'),
// A data attribute that indicates to the client to defer loading of
// the admin menu subtrees until this tab is activated. Admin menu
// subtrees will not render to the DOM if this attribute is removed.
// The value of the attribute is intentionally left blank. Only the
// presence of the attribute is necessary.
'data-drupal-subtrees' => '',
),
),
'tray' => array(
'#heading' => t('Administration menu'),
'#attached' => $subtrees_attached,
'toolbar_administration' => array(
'#pre_render' => array(
'toolbar_prerender_toolbar_administration_tray',
),
'#type' => 'container',
'#attributes' => array(
'class' => array('toolbar-menu-administration'),
),
),
),
Angie Byron
committed
'#weight' => -15,
);
return $items;
}
Dries Buytaert
committed
/**
* Renders the toolbar's administration tray.
* @param array $element
* A renderable array.
*
* @return array
* The updated renderable array.
*
* @see drupal_render()
Dries Buytaert
committed
*/
function toolbar_prerender_toolbar_administration_tray(array $element) {
$menu_tree = \Drupal::menuTree();
// Render the top-level administration menu links.
$parameters = new MenuTreeParameters();
Alex Pott
committed
$parameters->setRoot('system.admin')->excludeRoot()->setTopLevelOnly()->onlyEnabledLinks();
$tree = $menu_tree->load(NULL, $parameters);
$manipulators = array(
array('callable' => 'menu.default_tree_manipulators:checkAccess'),
array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'),
array('callable' => 'toolbar_menu_navigation_links'),
);
$tree = $menu_tree->transform($tree, $manipulators);
$element['administration_menu'] = $menu_tree->build($tree);
return $element;
Dries Buytaert
committed
}
/**
* Adds toolbar-specific attributes to the menu link tree.
Dries Buytaert
committed
*
* @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
* The menu link tree to manipulate.
* @return \Drupal\Core\Menu\MenuLinkTreeElement[]
* The manipulated menu link tree.
Dries Buytaert
committed
*/
function toolbar_menu_navigation_links(array $tree) {
foreach ($tree as $element) {
if ($element->subtree) {
toolbar_menu_navigation_links($element->subtree);
}
// Make sure we have a path specific ID in place, so we can attach icons
// and behaviors to the menu links.
$link = $element->link;
$url = $link->getUrlObject();
if ($url->isExternal()) {
// This is an unusual case, so just get a distinct, safe string.
$id = substr(Crypt::hashBase64($url->getPath()), 0, 16);
}
else {
$id = str_replace(array('.', '<', '>'), array('-', '', ''), $url->getRouteName());
}
// Get the non-localized title to make the icon class.
$definition = $link->getPluginDefinition();
$element->options['attributes']['id'] = 'toolbar-link-' . $id;
$element->options['attributes']['class'][] = 'toolbar-icon';
Alex Pott
committed
$element->options['attributes']['class'][] = 'toolbar-icon-' . strtolower(str_replace(array('.', ' ', '_'), array('-', '-', '-'), $definition['id']));
$element->options['attributes']['title'] = String::checkPlain($link->getDescription());
}
return $tree;
}
Dries Buytaert
committed
/**
* Returns the rendered subtree of each top-level toolbar link.
*/
function toolbar_get_rendered_subtrees() {
$menu_tree = \Drupal::menuTree();
$parameters = new MenuTreeParameters();
Alex Pott
committed
$parameters->setRoot('system.admin')->excludeRoot()->setMaxDepth(3)->onlyEnabledLinks();
$tree = $menu_tree->load(NULL, $parameters);
$manipulators = array(
array('callable' => 'menu.default_tree_manipulators:checkAccess'),
array('callable' => 'menu.default_tree_manipulators:generateIndexAndSort'),
array('callable' => 'toolbar_menu_navigation_links'),
);
$tree = $menu_tree->transform($tree, $manipulators);
$subtrees = array();
foreach ($tree as $element) {
/** @var \Drupal\Core\Menu\MenuLinkInterface $link */
$link = $element->link;
if ($element->subtree) {
$subtree = $menu_tree->build($element->subtree);
$output = drupal_render($subtree);
else {
$output = '';
}
// Many routes have dots as route name, while some special ones like <front>
// have <> characters in them.
$id = str_replace(array('.', '<', '>'), array('-', '', '' ), $link->getUrlObject()->getRouteName());
$subtrees[$id] = $output;
Dries Buytaert
committed
}
return $subtrees;
Dries Buytaert
committed
}
/**
* Returns the hash of the per-user rendered toolbar subtrees.
*
Alex Pott
committed
* @param string $langcode
* The langcode of the current request.
*
* @return string
* The hash of the admin_menu subtrees.
*/
Alex Pott
committed
function _toolbar_get_subtrees_hash($langcode) {
$uid = \Drupal::currentUser()->id();
Alex Pott
committed
$cid = _toolbar_get_user_cid($uid, $langcode);
if ($cache = \Drupal::cache('toolbar')->get($cid)) {
$hash = $cache->data;
}
else {
$subtrees = toolbar_get_rendered_subtrees();
Angie Byron
committed
$hash = Crypt::hashBase64(serialize($subtrees));
// Cache using a tag 'user' so that we can invalidate all user-specific
// caches later, based on the user's ID regardless of language.
// Clear the cache when the 'locale' tag is deleted. This ensures a fresh
// subtrees rendering when string translations are made.
\Drupal::cache('toolbar')->set($cid, $hash, Cache::PERMANENT, array('user' => array($uid), 'locale' => TRUE, 'menu' => 'admin', 'user_roles' => TRUE));
}
return $hash;
}
/**
* Returns a cache ID from the user and language IDs.
*
* @param int $uid
* A user ID.
Alex Pott
committed
* @param string $langcode
* The langcode of the current request.
*
* @return string
* A unique cache ID for the user.
*/
Alex Pott
committed
function _toolbar_get_user_cid($uid, $langcode) {
return 'toolbar_' . $uid . ':' . $langcode;