Issue #1479454 by Hugo Wetterberg, galooph, dawehner, andypost, marcingy, heyrocker...
[project/drupal.git] / core / modules / block / block.module
1 <?php
2
3 /**
4 * @file
5 * Controls the visual building blocks a page is constructed with.
6 */
7
8 use Drupal\Component\Plugin\Exception\PluginException;
9
10 /**
11 * Denotes that a block is not enabled in any region and should not be shown.
12 */
13 const BLOCK_REGION_NONE = -1;
14
15 /**
16 * Users cannot control whether or not they see this block.
17 */
18 const BLOCK_CUSTOM_FIXED = 0;
19
20 /**
21 * Shows this block by default, but lets individual users hide it.
22 */
23 const BLOCK_CUSTOM_ENABLED = 1;
24
25 /**
26 * Hides this block by default but lets individual users show it.
27 */
28 const BLOCK_CUSTOM_DISABLED = 2;
29
30 /**
31 * Shows this block on every page except the listed pages.
32 */
33 const BLOCK_VISIBILITY_NOTLISTED = 0;
34
35 /**
36 * Shows this block on only the listed pages.
37 */
38 const BLOCK_VISIBILITY_LISTED = 1;
39
40 /**
41 * Shows this block if the associated PHP code returns TRUE.
42 */
43 const BLOCK_VISIBILITY_PHP = 2;
44
45 /**
46 * Implements hook_help().
47 */
48 function block_help($path, $arg) {
49 switch ($path) {
50 case 'admin/help#block':
51 $output = '';
52 $output .= '<h3>' . t('About') . '</h3>';
53 $output .= '<p>' . t('The Block module allows you to create boxes of content, which are rendered into an area, or region, of one or more pages of a website. The core Seven administration theme, for example, implements the regions "Content" and "Help", and a block may appear in either of these regions. The <a href="@blocks">Blocks administration page</a> provides a drag-and-drop interface for assigning a block to a region, and for controlling the order of blocks within regions. For more information, see the online handbook entry for <a href="@block">Block module</a>.', array('@block' => 'http://drupal.org/documentation/modules/block', '@blocks' => url('admin/structure/block'))) . '</p>';
54 $output .= '<h3>' . t('Uses') . '</h3>';
55 $output .= '<dl>';
56 $output .= '<dt>' . t('Positioning content') . '</dt>';
57 $output .= '<dd>' . t('When working with blocks, remember that all themes do <em>not</em> implement the same regions, or display regions in the same way. Blocks are positioned on a per-theme basis. Users with the <em>Administer blocks</em> permission can disable blocks. Disabled blocks are listed on the <a href="@blocks">Blocks administration page</a>, but are not displayed in any region.', array('@block' => 'http://drupal.org/documentation/modules/block', '@blocks' => url('admin/structure/block'))) . '</dd>';
58 $output .= '<dt>' . t('Controlling visibility') . '</dt>';
59 $output .= '<dd>' . t('Blocks can be configured to be visible only on certain pages, only to users of certain roles, or only on pages displaying certain <a href="@content-type">content types</a>. Some dynamic blocks, such as those generated by modules, will be displayed only on certain pages.', array('@content-type' => url('admin/structure/types'), '@user' => url('user'))) . '</dd>';
60 if (module_exists('custom_block')) {
61 $output .= '<dt>' . t('Creating custom blocks') . '</dt>';
62 $output .= '<dd>' . t('Users with the <em>Administer blocks</em> permission can <a href="@block-add">add custom blocks</a>, which are then listed on the <a href="@blocks">Blocks administration page</a>. Once created, custom blocks behave just like default and module-generated blocks.', array('@blocks' => url('admin/structure/block'), '@block-add' => url('admin/structure/block/list/block_plugin_ui:' . variable_get('theme_default', 'stark') . '/add/custom_blocks'))) . '</dd>';
63 }
64 $output .= '</dl>';
65 return $output;
66 }
67 if ($arg[0] == 'admin' && $arg[1] == 'structure' && $arg['2'] == 'block' && (empty($arg[3]) || $arg[3] == 'list') && empty($arg[5])) {
68 if (!empty($arg[4])) {
69 list(, $demo_theme) = explode(':', $arg[4]);
70 }
71 else {
72 $demo_theme = variable_get('theme_default', 'stark');
73 }
74 $themes = list_themes();
75 $output = '<p>' . t('This page provides a drag-and-drop interface for assigning a block to a region, and for controlling the order of blocks within regions. Since not all themes implement the same regions, or display regions in the same way, blocks are positioned on a per-theme basis. Remember that your changes will not be saved until you click the <em>Save blocks</em> button at the bottom of the page. Click the <em>configure</em> link next to each block to configure its specific title and visibility settings.') . '</p>';
76 $output .= '<p>' . l(t('Demonstrate block regions (@theme)', array('@theme' => $themes[$demo_theme]->info['name'])), 'admin/structure/block/demo/' . $demo_theme) . '</p>';
77 return $output;
78 }
79 }
80
81 /**
82 * Implements hook_theme().
83 */
84 function block_theme() {
85 return array(
86 'block' => array(
87 'render element' => 'elements',
88 'template' => 'block',
89 ),
90 'block_admin_display_form' => array(
91 'template' => 'block-admin-display-form',
92 'file' => 'block.admin.inc',
93 'render element' => 'form',
94 ),
95 );
96 }
97
98 /**
99 * Implements hook_permission().
100 */
101 function block_permission() {
102 return array(
103 'administer blocks' => array(
104 'title' => t('Administer blocks'),
105 ),
106 );
107 }
108
109 /**
110 * Implements hook_menu().
111 *
112 * @todo Clarify the documentation for the per-plugin block admin links.
113 */
114 function block_menu() {
115 $default_theme = variable_get('theme_default', 'stark');
116 $items['admin/structure/block'] = array(
117 'title' => 'Blocks',
118 'description' => 'Configure what block content appears in your site\'s sidebars and other regions.',
119 'page callback' => 'block_admin_display',
120 'page arguments' => array($default_theme),
121 'access arguments' => array('administer blocks'),
122 'file' => 'block.admin.inc',
123 );
124 $items['admin/structure/block/add/%/%'] = array(
125 'title' => 'Configure block',
126 'page callback' => 'block_admin_add',
127 'page arguments' => array(4, 5),
128 'access arguments' => array('administer blocks'),
129 'file' => 'block.admin.inc',
130 );
131 $items['admin/structure/block/manage/%block'] = array(
132 'title' => 'Configure block',
133 'page callback' => 'block_admin_edit',
134 'page arguments' => array(4),
135 'access arguments' => array('administer blocks'),
136 'file' => 'block.admin.inc',
137 );
138 $items['admin/structure/block/manage/%block/configure'] = array(
139 'title' => 'Configure block',
140 'type' => MENU_DEFAULT_LOCAL_TASK,
141 'context' => MENU_CONTEXT_INLINE,
142 );
143 $items['admin/structure/block/manage/%block/delete'] = array(
144 'title' => 'Delete block',
145 'page callback' => 'drupal_get_form',
146 'page arguments' => array('block_admin_block_delete', 4),
147 'access arguments' => array('administer blocks'),
148 'type' => MENU_LOCAL_TASK,
149 'context' => MENU_CONTEXT_NONE,
150 'file' => 'block.admin.inc',
151 );
152 // Block administration is actually tied to theme and plugin definition so
153 // that the plugin can appropriately attach to this url structure.
154 $themes = list_themes();
155 foreach (drupal_container()->get('plugin.manager.system.plugin_ui')->getDefinitions() as $plugin_id => $plugin) {
156 list($plugin_base, $key) = explode(':', $plugin_id);
157 if ($plugin_base == 'block_plugin_ui') {
158 $theme = $themes[$key];
159 $items['admin/structure/block/list/' . $plugin_id] = array(
160 'title' => check_plain($theme->info['name']),
161 'page arguments' => array($key),
162 'type' => $key == $default_theme ? MENU_DEFAULT_LOCAL_TASK : MENU_LOCAL_TASK,
163 'weight' => $key == $default_theme ? -10 : 0,
164 'access callback' => '_block_themes_access',
165 'access arguments' => array($key),
166 'file' => 'block.admin.inc',
167 );
168 $items['admin/structure/block/demo/' . $key] = array(
169 'title' => check_plain($theme->info['name']),
170 'page callback' => 'block_admin_demo',
171 'page arguments' => array($key),
172 'type' => MENU_CALLBACK,
173 'access callback' => '_block_themes_access',
174 'access arguments' => array($key),
175 'theme callback' => '_block_custom_theme',
176 'theme arguments' => array($key),
177 'file' => 'block.admin.inc',
178 );
179 }
180 }
181 return $items;
182 }
183
184 /**
185 * Access callback: Only enabled themes can be accessed.
186 *
187 * Path:
188 * - admin/structure/block/list/% (for each theme)
189 * - admin/structure/block/demo/% (for each theme)
190 *
191 * @param $theme
192 * Either the name of a theme or a full theme object.
193 *
194 * @see block_menu()
195 */
196 function _block_themes_access($theme) {
197 return user_access('administer blocks') && drupal_theme_access($theme);
198 }
199
200 /**
201 * Theme callback: Uses the theme specified in the parameter.
202 *
203 * @param $theme
204 * The theme whose blocks are being configured. If not set, the default theme
205 * is assumed.
206 *
207 * @return
208 * The theme that should be used for the block configuration page, or NULL
209 * to indicate that the default theme should be used.
210 *
211 * @see block_menu()
212 */
213 function _block_custom_theme($theme = NULL) {
214 // We return exactly what was passed in, to guarantee that the page will
215 // always be displayed using the theme whose blocks are being configured.
216 return $theme;
217 }
218
219 /**
220 * Implements hook_page_build().
221 *
222 * Renders blocks into their regions.
223 */
224 function block_page_build(&$page) {
225 global $theme;
226
227 // The theme system might not yet be initialized. We need $theme.
228 drupal_theme_initialize();
229
230 // Fetch a list of regions for the current theme.
231 $all_regions = system_region_list($theme);
232
233 $item = menu_get_item();
234 if ($item['path'] != 'admin/structure/block/demo/' . $theme) {
235 // Load all region content assigned via blocks.
236 foreach (array_keys($all_regions) as $region) {
237 // Assign blocks to region.
238 if ($blocks = block_get_blocks_by_region($region)) {
239 $page[$region] = $blocks;
240 }
241 }
242 // Once we've finished attaching all blocks to the page, clear the static
243 // cache to allow modules to alter the block list differently in different
244 // contexts. For example, any code that triggers hook_page_build() more
245 // than once in the same page request may need to alter the block list
246 // differently each time, so that only certain parts of the page are
247 // actually built. We do not clear the cache any earlier than this, though,
248 // because it is used each time block_get_blocks_by_region() gets called
249 // above.
250 drupal_static_reset('block_list');
251 }
252 else {
253 // Append region description if we are rendering the regions demo page.
254 $item = menu_get_item();
255 if ($item['path'] == 'admin/structure/block/demo/' . $theme) {
256 $visible_regions = array_keys(system_region_list($theme, REGIONS_VISIBLE));
257 foreach ($visible_regions as $region) {
258 $description = '<div class="block-region">' . $all_regions[$region] . '</div>';
259 $page[$region]['block_description'] = array(
260 '#markup' => $description,
261 '#weight' => 15,
262 );
263 }
264 $page['page_top']['backlink'] = array(
265 '#type' => 'link',
266 '#title' => t('Exit block region demonstration'),
267 '#href' => 'admin/structure/block' . (variable_get('theme_default', 'stark') == $theme ? '' : '/list/' . $theme),
268 // Add the "overlay-restore" class to indicate this link should restore
269 // the context in which the region demonstration page was opened.
270 '#options' => array('attributes' => array('class' => array('block-demo-backlink', 'overlay-restore'))),
271 '#weight' => -10,
272 );
273 }
274 }
275 }
276
277 /**
278 * Gets a renderable array of a region containing all enabled blocks.
279 *
280 * @param $region
281 * The requested region.
282 *
283 * @return
284 * A renderable array of a region containing all enabled blocks.
285 */
286 function block_get_blocks_by_region($region) {
287 $build = array();
288 if ($list = block_list($region)) {
289 $build = _block_get_renderable_region($list);
290 }
291 return $build;
292 }
293
294 /**
295 * Gets an array of blocks suitable for drupal_render().
296 *
297 * @param $list
298 * A list of blocks such as that returned by block_list().
299 *
300 * @return
301 * A renderable array.
302 */
303 function _block_get_renderable_region($list = array()) {
304 $build = array();
305 // Block caching is not compatible with node_access modules. We also
306 // preserve the submission of forms in blocks, by fetching from cache
307 // only if the request method is 'GET' (or 'HEAD'). User 1 being out of
308 // the regular 'roles define permissions' schema, it brings too many
309 // chances of having unwanted output get in the cache and later be served
310 // to other users. We therefore exclude user 1 from block caching.
311 $not_cacheable = $GLOBALS['user']->uid == 1 ||
312 count(module_implements('node_grants')) ||
313 !in_array($_SERVER['REQUEST_METHOD'], array('GET', 'HEAD'));
314
315 foreach ($list as $key => $block) {
316 $settings = $block->get('settings');
317 if ($not_cacheable || in_array($settings['cache'], array(DRUPAL_NO_CACHE, DRUPAL_CACHE_CUSTOM))) {
318 // Non-cached blocks get built immediately.
319 if ($block->access()) {
320 $build[$key] = entity_view($block, 'block');
321 }
322 }
323 else {
324 $key_components = explode('.', $key);
325 $id = array_pop($key_components);
326 $build[$key] = array(
327 '#block' => $block,
328 '#weight' => $block->get('weight'),
329 '#theme_wrappers' => array('block'),
330 '#pre_render' => array('_block_get_renderable_block'),
331 '#cache' => array(
332 'keys' => array($id, $block->get('module')),
333 'granularity' => $settings['cache'],
334 'bin' => 'block',
335 'tags' => array('content' => TRUE),
336 ),
337 );
338 }
339
340 // Add contextual links for this block; skip the main content block, since
341 // contextual links are basically output as tabs/local tasks already. Also
342 // skip the help block, since we assume that most users do not need or want
343 // to perform contextual actions on the help block, and the links needlessly
344 // draw attention on it.
345 if (!in_array($block->get('plugin'), array('system_help_block', 'system_main_block'))) {
346 $build[$key]['#contextual_links']['block'] = array('admin/structure/block/manage', array($key));
347 }
348 }
349 return $build;
350 }
351
352 /**
353 * Returns an array of block class instances by theme.
354 *
355 * @param $theme
356 * The theme to rehash blocks for. If not provided, defaults to the currently
357 * used theme.
358 *
359 * @return
360 * Blocks currently exported by modules.
361 */
362 function _block_rehash($theme = NULL) {
363 $theme = $theme ? $theme : variable_get('theme_default', 'stark');
364 $regions = system_region_list($theme);
365 $blocks = entity_load_multiple_by_properties('block', array('theme' => $theme));
366 foreach ($blocks as $block_id => $block) {
367 $region = $block->get('region');
368 $status = $block->get('status');
369 // Disable blocks in invalid regions.
370 if (!empty($region) && $region != BLOCK_REGION_NONE && !isset($regions[$region]) && $status == 1) {
371 drupal_set_message(t('The block %info was assigned to the invalid region %region and has been disabled.', array('%info' => $block_id, '%region' => $region)), 'warning');
372 // Disabled modules are moved into the BLOCK_REGION_NONE later so no
373 // need to move the block to another region.
374 $block->set('status', 0);
375 $block->save();
376 }
377 // Set region to none if not enabled and make sure status is set.
378 if (empty($status)) {
379 $block->set('region', BLOCK_REGION_NONE);
380 $block->set('status', 0);
381 $block->save();
382 }
383 }
384 return $blocks;
385 }
386
387 /**
388 * Initializes blocks for enabled themes.
389 *
390 * @param $theme_list
391 * An array of theme names.
392 */
393 function block_themes_enabled($theme_list) {
394 foreach ($theme_list as $theme) {
395 block_theme_initialize($theme);
396 }
397 }
398
399 /**
400 * Assigns an initial, default set of blocks for a theme.
401 *
402 * This function is called the first time a new theme is enabled. The new theme
403 * gets a copy of the default theme's blocks, with the difference that if a
404 * particular region isn't available in the new theme, the block is assigned
405 * to the new theme's default region.
406 *
407 * @param $theme
408 * The name of a theme.
409 */
410 function block_theme_initialize($theme) {
411 // Initialize theme's blocks if none already registered.
412 $has_blocks = entity_load_multiple_by_properties('block', array('theme' => $theme));
413 if (!$has_blocks) {
414 $default_theme = variable_get('theme_default', 'stark');
415 // Apply only to new theme's visible regions.
416 $regions = system_region_list($theme, REGIONS_VISIBLE);
417 $default_theme_blocks = entity_load_multiple_by_properties('block', array('theme' => $default_theme));
418 foreach ($default_theme_blocks as $default_theme_block_id => $default_theme_block) {
419 list(, $machine_name) = explode('.', $default_theme_block_id);
420 $block = $default_theme_block->createDuplicate();
421 $block->set('id', $theme . '.' . $machine_name);
422 // If the region isn't supported by the theme, assign the block to the
423 // theme's default region.
424 if (!isset($regions[$block->get('region')])) {
425 $block->set('region', system_default_region($theme));
426 }
427 $block->save();
428 }
429 }
430 }
431
432 /**
433 * Returns all blocks in the specified region for the current user.
434 *
435 * @param $region
436 * The name of a region.
437 *
438 * @return
439 * An array of block objects, indexed with the configuration object name
440 * that represents the configuration. If you are displaying your blocks in
441 * one or two sidebars, you may check whether this array is empty to see
442 * how many columns are going to be displayed.
443 */
444 function block_list($region) {
445 $blocks = &drupal_static(__FUNCTION__);
446
447 if (!isset($blocks)) {
448 global $theme;
449 $blocks = array();
450 foreach (entity_load_multiple_by_properties('block', array('theme' => $theme)) as $block_id => $block) {
451 $blocks[$block->get('region')][$block_id] = $block;
452 }
453 }
454
455 // Create an empty array if there are no entries.
456 if (!isset($blocks[$region])) {
457 $blocks[$region] = array();
458 }
459
460 return $blocks[$region];
461 }
462
463 /**
464 * Loads a block instance.
465 *
466 * This should only be used when entity_load() cannot be used directly.
467 *
468 * @param string $entity_id
469 * The block ID.
470 *
471 * @return \Drupal\block\Plugin\Core\Entity\Block
472 * The loaded block object.
473 */
474 function block_load($entity_id) {
475 return entity_load('block', $entity_id);
476 }
477
478 /**
479 * Builds the content and subject for a block.
480 *
481 * For cacheable blocks, this is called during #pre_render.
482 *
483 * @param $element
484 * A renderable array.
485 *
486 * @return
487 * A renderable array.
488 */
489 function _block_get_renderable_block($element) {
490 $block = $element['#block'];
491 // Don't bother to build blocks that aren't accessible.
492 if ($element['#access'] = $block->access()) {
493 $element += entity_view($block, 'block');
494 }
495 return $element;
496 }
497
498 /**
499 * Implements hook_cache_flush().
500 */
501 function block_cache_flush() {
502 return array('block');
503 }
504
505 /**
506 * Implements hook_rebuild().
507 */
508 function block_rebuild() {
509 foreach (list_themes() as $name => $data) {
510 if ($data->status) {
511 _block_rehash($name);
512 }
513 }
514 }
515
516 /**
517 * Processes variables for block.tpl.php.
518 *
519 * Prepares the values passed to the theme_block function to be passed
520 * into a pluggable template engine. Uses block properties to generate a
521 * series of template file suggestions. If none are found, the default
522 * block.tpl.php is used.
523 *
524 * Most themes utilize their own copy of block.tpl.php. The default is located
525 * inside "modules/block/block.tpl.php". Look in there for the full list of
526 * variables.
527 *
528 * The $variables array contains the following arguments:
529 * - $block
530 *
531 * @see block.tpl.php
532 */
533 function template_preprocess_block(&$variables) {
534 $block_counter = &drupal_static(__FUNCTION__, array());
535
536 $variables['block'] = (object) $variables['elements']['#block_config'];
537
538 // All blocks get an independent counter for each region.
539 if (!isset($block_counter[$variables['block']->region])) {
540 $block_counter[$variables['block']->region] = 1;
541 }
542 // Same with zebra striping.
543 $variables['block_zebra'] = ($block_counter[$variables['block']->region] % 2) ? 'odd' : 'even';
544 $variables['block_id'] = $block_counter[$variables['block']->region]++;
545
546 // Create the $content variable that templates expect.
547 $variables['content'] = $variables['elements']['#children'];
548
549 $variables['attributes']['class'][] = drupal_html_class('block-' . $variables['block']->module);
550
551 // Add default class for block content.
552 $variables['content_attributes']['class'][] = 'content';
553
554 $variables['theme_hook_suggestions'][] = 'block__' . $variables['block']->region;
555 $variables['theme_hook_suggestions'][] = 'block__' . $variables['block']->module;
556 // Hyphens (-) and underscores (_) play a special role in theme suggestions.
557 // Theme suggestions should only contain underscores, because within
558 // drupal_find_theme_templates(), underscores are converted to hyphens to
559 // match template file names, and then converted back to underscores to match
560 // pre-processing and other function names. So if your theme suggestion
561 // contains a hyphen, it will end up as an underscore after this conversion,
562 // and your function names won't be recognized. So, we need to convert
563 // hyphens to underscores in block deltas for the theme suggestions.
564
565 // We can safely explode on : because we know the Block plugin type manager
566 // enforces that delimiter for all derivatives.
567 $parts = explode(':', $variables['elements']['#block']->get('plugin'));
568 $suggestion = 'block';
569 while ($part = array_shift($parts)) {
570 $variables['theme_hook_suggestions'][] = $suggestion .= '__' . strtr($part, '-', '_');
571 }
572 // Create a valid HTML ID and make sure it is unique.
573 if ($id = $variables['elements']['#block']->id()) {
574 $config_id = explode('.', $id);
575 $machine_name = array_pop($config_id);
576 $variables['block_html_id'] = drupal_html_id('block-' . $machine_name);
577 $variables['theme_hook_suggestions'][] = 'block__' . $machine_name;
578 }
579 }
580
581 /**
582 * Implements hook_user_role_delete().
583 *
584 * Removes deleted role from blocks that use it.
585 */
586 function block_user_role_delete($role) {
587 foreach (entity_load_multiple('block') as $block_id => $block) {
588 $visibility = $block->get('visibility');
589 if (isset($visibility['roles']['roles'][$role->id()])) {
590 unset($visibility['roles']['roles'][$role->id()]);
591 $block->set('visibility', $visibility);
592 $block->save();
593 }
594 }
595 }
596
597 /**
598 * Implements hook_menu_delete().
599 */
600 function block_menu_delete($menu) {
601 foreach (entity_load_multiple('block') as $block_id => $block) {
602 if ($block->get('plugin') == 'menu_menu_block:' . $menu->id()) {
603 $block->delete();
604 }
605 }
606 }
607
608 /**
609 * Implements hook_admin_paths().
610 */
611 function block_admin_paths() {
612 $paths = array(
613 // Exclude the block demonstration page from admin (overlay) treatment.
614 // This allows us to present this page in its true form, full page.
615 'admin/structure/block/demo/*' => FALSE,
616 );
617 return $paths;
618 }
619
620 /**
621 * Implements hook_language_delete().
622 *
623 * Delete the potential block visibility settings of the deleted language.
624 */
625 function block_language_delete($language) {
626 // Remove the block visibility settings for the deleted language.
627 foreach (entity_load_multiple('block') as $block_id => $block) {
628 $visibility = $block->get('visibility');
629 if (isset($visibility['language']['langcodes'][$language->langcode])) {
630 unset($visibility['language']['langcodes'][$language->langcode]);
631 $block->set('visibility', $visibility);
632 $block->save();
633 }
634 }
635 }
636
637 /**
638 * Implements hook_library_info().
639 */
640 function block_library_info() {
641 $libraries['drupal.block'] = array(
642 'title' => 'Block',
643 'version' => VERSION,
644 'js' => array(
645 drupal_get_path('module', 'block') . '/block.js' => array(),
646 ),
647 'dependencies' => array(
648 array('system', 'jquery'),
649 array('system', 'drupal'),
650 ),
651 );
652
653 return $libraries;
654 }