theme.inc 91.6 KB
Newer Older
1
<?php
2

3
/**
4
 * @file
5
 * The theme system, which controls the output of Drupal.
6 7 8 9
 *
 * The theme system allows for nearly all output of the Drupal system to be
 * customized by user themes.
 */
Dries's avatar
Dries committed
10

11
use Drupal\Component\Utility\String;
12
use Drupal\Component\Utility\UrlHelper;
13
use Drupal\Core\Config\Config;
14
use Drupal\Core\Language\Language;
15
use Drupal\Core\Extension\Extension;
16
use Drupal\Core\Extension\ExtensionNameLengthException;
17
use Drupal\Core\Template\Attribute;
18
use Drupal\Core\Template\RenderWrapper;
19
use Drupal\Core\Theme\ThemeSettings;
20
use Drupal\Component\Utility\NestedArray;
21
use Drupal\Core\Render\Element;
22

23
/**
24
 * @defgroup content_flags Content markers
25 26 27 28
 * @{
 * Markers used by theme_mark() and node_mark() to designate content.
 * @see theme_mark(), node_mark()
 */
29 30 31 32

/**
 * Mark content as read.
 */
33
const MARK_READ = 0;
34 35 36 37

/**
 * Mark content as being new.
 */
38
const MARK_NEW = 1;
39 40 41 42

/**
 * Mark content as being updated.
 */
43
const MARK_UPDATED = 2;
44

45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
/**
 * A responsive table class; hide table cell on narrow devices.
 *
 * Indicates that a column has medium priority and thus can be hidden on narrow
 * width devices and shown on medium+ width devices (i.e. tablets and desktops).
 */
const RESPONSIVE_PRIORITY_MEDIUM = 'priority-medium';

/**
 * A responsive table class; only show table cell on wide devices.
 *
 * Indicates that a column has low priority and thus can be hidden on narrow
 * and medium viewports and shown on wide devices (i.e. desktops).
 */
const RESPONSIVE_PRIORITY_LOW = 'priority-low';

61
/**
62
 * @} End of "defgroup content_flags".
63 64
 */

65 66 67
/**
 * Determines if a theme is available to use.
 *
68
 * @param string|\Drupal\Core\Extension\Extension $theme
69 70
 *   Either the name of a theme or a full theme object.
 *
71
 * @return bool
72 73
 *   Boolean TRUE if the theme is enabled or is the site administration theme;
 *   FALSE otherwise.
74
 *
75 76 77 78
 * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
 *   Use \Drupal::service('access_check.theme')->checkAccess().
 *
 * @see \Drupal\Core\Theme\ThemeAccessCheck::checkAccess().
79 80
 */
function drupal_theme_access($theme) {
81
  if ($theme instanceof Extension) {
82
    $theme = $theme->getName();
83
  }
84
  return \Drupal::service('access_check.theme')->checkAccess($theme);
85 86
}

87
/**
88
 * Initializes the theme system by loading the theme.
89
 */
90
function drupal_theme_initialize() {
91
  global $theme, $theme_key;
92 93 94 95 96

  // If $theme is already set, assume the others are set, too, and do nothing
  if (isset($theme)) {
    return;
  }
97

98
  $themes = list_themes();
99

100 101 102 103 104
  // @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';
105 106 107 108

  // Store the identifier for retrieving theme settings with.
  $theme_key = $theme;

109 110 111 112
  // Find all our ancestor themes and put them in an array.
  $base_theme = array();
  $ancestor = $theme;
  while ($ancestor && isset($themes[$ancestor]->base_theme)) {
113
    $ancestor = $themes[$ancestor]->base_theme;
114
    $base_theme[] = $themes[$ancestor];
115
  }
116
  _drupal_theme_initialize($themes[$theme], array_reverse($base_theme));
117 118 119
}

/**
120 121 122
 * Initializes the theme system given already loaded information.
 *
 * This function is useful to initialize a theme when no database is present.
123
 *
124 125 126
 * @param \Drupal\Core\Extension\Extension $theme
 *   The theme extension object.
 * @param \Drupal\Core\Extension\Extension[] $base_theme
127 128 129 130 131 132
 *    An optional array of objects that represent the 'base theme' if the
 *    theme is meant to be derivative of another theme. It requires
 *    the same information as the $theme object. It should be in
 *    'oldest first' order, meaning the top level of the chain will
 *    be first.
 */
133
function _drupal_theme_initialize($theme, $base_theme = array()) {
134 135 136 137
  global $theme_info, $base_theme_info, $theme_engine, $theme_path;
  $theme_info = $theme;
  $base_theme_info = $base_theme;

138
  $theme_path = $theme->getPath();
139

140 141 142 143
  // Prepare stylesheets from this theme as well as all ancestor themes.
  // We work it this way so that we can have child themes override parent
  // theme stylesheets easily.
  $final_stylesheets = array();
144 145 146 147
  // CSS file basenames to override, pointing to the final, overridden filepath.
  $theme->stylesheets_override = array();
  // CSS file basenames to remove.
  $theme->stylesheets_remove = array();
148 149 150 151 152 153 154 155 156 157

  // Grab stylesheets from base theme
  foreach ($base_theme as $base) {
    if (!empty($base->stylesheets)) {
      foreach ($base->stylesheets as $media => $stylesheets) {
        foreach ($stylesheets as $name => $stylesheet) {
          $final_stylesheets[$media][$name] = $stylesheet;
        }
      }
    }
158
    $base_theme_path = $base->getPath();
159 160 161 162 163 164 165 166 167 168 169
    if (!empty($base->info['stylesheets-remove'])) {
      foreach ($base->info['stylesheets-remove'] as $basename) {
        $theme->stylesheets_remove[$basename] = $base_theme_path . '/' . $basename;
      }
    }
    if (!empty($base->info['stylesheets-override'])) {
      foreach ($base->info['stylesheets-override'] as $name) {
        $basename = drupal_basename($name);
        $theme->stylesheets_override[$basename] = $base_theme_path . '/' . $name;
      }
    }
170 171
  }

172 173 174
  // Add stylesheets used by this theme.
  if (!empty($theme->stylesheets)) {
    foreach ($theme->stylesheets as $media => $stylesheets) {
175 176 177 178 179
      foreach ($stylesheets as $name => $stylesheet) {
        $final_stylesheets[$media][$name] = $stylesheet;
      }
    }
  }
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
  if (!empty($theme->info['stylesheets-remove'])) {
    foreach ($theme->info['stylesheets-remove'] as $basename) {
      $theme->stylesheets_remove[$basename] = $theme_path . '/' . $basename;

      if (isset($theme->stylesheets_override[$basename])) {
        unset($theme->stylesheets_override[$basename]);
      }
    }
  }
  if (!empty($theme->info['stylesheets-override'])) {
    foreach ($theme->info['stylesheets-override'] as $name) {
      $basename = drupal_basename($name);
      $theme->stylesheets_override[$basename] = $theme_path . '/' . $name;

      if (isset($theme->stylesheets_remove[$basename])) {
        unset($theme->stylesheets_remove[$basename]);
      }
    }
  }
199

200 201
  // And now add the stylesheets properly.
  $css = array();
202 203
  foreach ($final_stylesheets as $media => $stylesheets) {
    foreach ($stylesheets as $stylesheet) {
204 205 206 207 208
      $css['#attached']['css'][$stylesheet] = array(
        'group' => CSS_AGGREGATE_THEME,
        'every_page' => TRUE,
        'media' => $media
      );
209 210
    }
  }
211
  drupal_render($css);
212 213 214 215 216 217 218 219 220

  // Do basically the same as the above for scripts
  $final_scripts = array();

  // Grab scripts from base theme
  foreach ($base_theme as $base) {
    if (!empty($base->scripts)) {
      foreach ($base->scripts as $name => $script) {
        $final_scripts[$name] = $script;
221
      }
222 223
    }
  }
224

225 226
  // Add scripts used by this theme.
  if (!empty($theme->scripts)) {
227 228
    foreach ($theme->scripts as $name => $script) {
      $final_scripts[$name] = $script;
229 230 231
    }
  }

232
  // Add scripts used by this theme.
233
  $js = array();
234
  foreach ($final_scripts as $script) {
235 236 237 238 239
    $js['#attached']['js'][] = array(
      'data' => $script,
      'group' => JS_THEME,
      'every_page' => TRUE,
    );
240
  }
241
  drupal_render($js);
242

243 244 245 246 247
  $theme_engine = NULL;

  // Initialize the theme.
  if (isset($theme->engine)) {
    // Include the engine.
248
    include_once DRUPAL_ROOT . '/' . $theme->owner;
249 250

    $theme_engine = $theme->engine;
251
    if (function_exists($theme_engine . '_init')) {
252
      foreach ($base_theme as $base) {
253
        call_user_func($theme_engine . '_init', $base);
254
      }
255
      call_user_func($theme_engine . '_init', $theme);
256 257 258 259 260 261 262
    }
  }
  else {
    // include non-engine theme files
    foreach ($base_theme as $base) {
      // Include the theme file or the engine.
      if (!empty($base->owner)) {
263
        include_once DRUPAL_ROOT . '/' . $base->owner;
264 265 266 267
      }
    }
    // and our theme gets one too.
    if (!empty($theme->owner)) {
268
      include_once DRUPAL_ROOT . '/' . $theme->owner;
269 270
    }
  }
271 272

  // Always include Twig as the default theme engine.
273
  include_once DRUPAL_ROOT . '/core/themes/engines/twig/twig.engine';
274 275
}

276
/**
277
 * Gets the theme registry.
278
 *
279
 * @param bool $complete
280
 *   Optional boolean to indicate whether to return the complete theme registry
281 282 283 284 285 286 287
 *   array or an instance of the Drupal\Core\Utility\ThemeRegistry class.
 *   If TRUE, the complete theme registry array will be returned. This is useful
 *   if you want to foreach over the whole registry, use array_* functions or
 *   inspect it in a debugger. If FALSE, an instance of the
 *   Drupal\Core\Utility\ThemeRegistry class will be returned, this provides an
 *   ArrayObject which allows it to be accessed with array syntax and isset(),
 *   and should be more lightweight than the full registry. Defaults to TRUE.
288
 *
289
 * @return
290 291
 *   The complete theme registry array, or an instance of the
 *   Drupal\Core\Utility\ThemeRegistry class.
292
 */
293
function theme_get_registry($complete = TRUE) {
294
  $theme_registry = \Drupal::service('theme.registry');
295
  if ($complete) {
296
    return $theme_registry->get();
297 298
  }
  else {
299
    return $theme_registry->getRuntime();
300 301 302 303
  }
}

/**
304 305 306 307
 * Forces the system to rebuild the theme registry.
 *
 * This function should be called when modules are added to the system, or when
 * a dynamic system needs to add more theme hooks.
308
 */
309
function drupal_theme_rebuild() {
310
  \Drupal::service('theme.registry')->reset();
311 312
}

313
/**
314
 * Returns a list of all currently available themes.
315
 *
316 317
 * Retrieved from the database, if available and the site is not in maintenance
 * mode; otherwise compiled freshly from the filesystem.
318
 *
319
 * @param $refresh
320
 *   Whether to reload the list of themes from the database. Defaults to FALSE.
321
 *
322 323
 * @return array
 *   An associative array of the currently available themes.
324
 *
325 326 327 328
 * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
 *   Use \Drupal::service('theme_handler')->listInfo().
 *
 * @see \Drupal\Core\Extension\ThemeHandler::listInfo().
329
 */
330
function list_themes($refresh = FALSE) {
331 332
  /** @var \Drupal\Core\Extension\ThemeHandler $theme_handler */
  $theme_handler = \Drupal::service('theme_handler');
333 334

  if ($refresh) {
335
    $theme_handler->reset();
336
    system_list_reset();
337 338
  }

339
  return $theme_handler->listInfo();
340 341
}

342
/**
343
 * Generates themed output (internal use only).
344
 *
345 346 347 348 349 350 351 352
 * _theme() is an internal function. Do not call this function directly as it
 * will prevent the following items from working correctly:
 * - Render caching.
 * - JavaScript and CSS asset attachment.
 * - Pre / post render hooks.
 * - Defaults provided by hook_element_info(), including attached assets.
 * Instead, build a render array with a #theme key, and either return the
 * array (where possible) or call drupal_render() to convert it to HTML.
353
 *
354 355 356 357 358 359
 * All requests for themed output must go through this function, which is
 * invoked as part of the @link theme_render drupal_render() process @endlink.
 * The appropriate theme function is indicated by the #theme property
 * of a renderable array. _theme() examines the request and routes it to the
 * appropriate @link themeable theme function or template @endlink, by checking
 * the theme registry.
360
 *
361
 * @param $hook
362 363
 *   The name of the theme hook to call. If the name contains a
 *   double-underscore ('__') and there isn't an implementation for the full
364
 *   name, the part before the '__' is checked. This allows a fallback to a
365
 *   more generic implementation. For example, if _theme('links__node', ...) is
366 367
 *   called, but there is no implementation of that theme hook, then the
 *   'links' implementation is used. This process is iterative, so if
368
 *   _theme('links__contextual__node', ...) is called, _theme() checks for the
369 370 371 372 373 374
 *   following implementations, and uses the first one that exists:
 *   - links__contextual__node
 *   - links__contextual
 *   - links
 *   This allows themes to create specific theme implementations for named
 *   objects and contexts of otherwise generic theme hooks. The $hook parameter
375
 *   may also be an array, in which case the first theme hook that has an
376
 *   implementation is used. This allows for the code that calls _theme() to
377
 *   explicitly specify the fallback order in a situation where using the '__'
378
 *   convention is not desired or is insufficient.
379 380
 * @param $variables
 *   An associative array of variables to merge with defaults from the theme
381 382 383 384
 *   registry, pass to preprocess functions for modification, and finally, pass
 *   to the function or template implementing the theme hook. Alternatively,
 *   this can be a renderable array, in which case, its properties are mapped to
 *   variables expected by the theme hook implementations.
385
 *
386 387 388
 * @return string|false
 *   An HTML string representing the themed output or FALSE if the passed $hook
 *   is not implemented.
389
 *
390
 * @see drupal_render()
391
 * @see themeable
392 393
 * @see hook_theme()
 * @see template_preprocess()
394
 */
395
function _theme($hook, $variables = array()) {
396
  static $default_attributes;
397 398
  // If called before all modules are loaded, we do not necessarily have a full
  // theme registry to work with, and therefore cannot process the theme
399
  // request properly. See also \Drupal\Core\Theme\Registry::get().
400
  if (!\Drupal::moduleHandler()->isLoaded() && !defined('MAINTENANCE_MODE')) {
401
    throw new Exception(t('_theme() may not be called until all modules are loaded.'));
402 403
  }

404 405
  /** @var \Drupal\Core\Utility\ThemeRegistry $theme_registry */
  $theme_registry = \Drupal::service('theme.registry')->getRuntime();
406

407 408
  // If an array of hook candidates were passed, use the first one that has an
  // implementation.
409 410
  if (is_array($hook)) {
    foreach ($hook as $candidate) {
411
      if ($theme_registry->has($candidate)) {
412 413 414 415 416
        break;
      }
    }
    $hook = $candidate;
  }
417 418 419
  // Save the original theme hook, so it can be supplied to theme variable
  // preprocess callbacks.
  $original_hook = $hook;
420

421 422
  // If there's no implementation, check for more generic fallbacks. If there's
  // still no implementation, log an error and return an empty string.
423
  if (!$theme_registry->has($hook)) {
424 425 426 427
    // Iteratively strip everything after the last '__' delimiter, until an
    // implementation is found.
    while ($pos = strrpos($hook, '__')) {
      $hook = substr($hook, 0, $pos);
428
      if ($theme_registry->has($hook)) {
429 430 431
        break;
      }
    }
432
    if (!$theme_registry->has($hook)) {
433 434 435
      // Only log a message when not trying theme suggestions ($hook being an
      // array).
      if (!isset($candidate)) {
436
        watchdog('theme', 'Theme hook %hook not found.', array('%hook' => $hook), WATCHDOG_WARNING);
437
      }
438
      // There is no theme implementation for the hook passed. Return FALSE so
439
      // the function calling _theme() can differentiate between a hook that
440 441
      // exists and renders an empty string and a hook that is not implemented.
      return FALSE;
442
    }
443 444
  }

445
  $info = $theme_registry->get($hook);
446 447 448
  global $theme_path;
  $temp = $theme_path;
  // point path_to_theme() to the currently used theme path:
449
  $theme_path = $info['theme path'];
450

451 452

  // If a renderable array is passed as $variables, then set $variables to
453
  // the arguments expected by the theme function.
454 455 456
  if (isset($variables['#theme']) || isset($variables['#theme_wrappers'])) {
    $element = $variables;
    $variables = array();
457 458
    if (isset($info['variables'])) {
      foreach (array_keys($info['variables']) as $name) {
459
        if (isset($element["#$name"]) || array_key_exists("#$name", $element)) {
460 461
          $variables[$name] = $element["#$name"];
        }
462 463
      }
    }
464 465
    else {
      $variables[$info['render element']] = $element;
466 467
      // Give a hint to render engines to prevent infinite recursion.
      $variables[$info['render element']]['#render_children'] = TRUE;
468
    }
469
  }
470

471
  // Merge in argument defaults.
472 473 474 475 476
  if (!empty($info['variables'])) {
    $variables += $info['variables'];
  }
  elseif (!empty($info['render element'])) {
    $variables += array($info['render element'] => array());
477
  }
478 479 480 481
  // Supply original caller info.
  $variables += array(
    'theme_hook_original' => $original_hook,
  );
482

483 484 485 486 487 488 489 490 491 492 493 494 495
  // Set base hook for later use. For example if '#theme' => 'node__article'
  // is called, we run hook_theme_suggestions_node_alter() rather than
  // hook_theme_suggestions_node__article_alter(), and also pass in the base
  // hook as the last parameter to the suggestions alter hooks.
  if (isset($info['base hook'])) {
    $base_theme_hook = $info['base hook'];
  }
  else {
    $base_theme_hook = $hook;
  }

  // Invoke hook_theme_suggestions_HOOK().
  $suggestions = Drupal::moduleHandler()->invokeAll('theme_suggestions_' . $base_theme_hook, array($variables));
496
  // If _theme() was invoked with a direct theme suggestion like
497 498 499 500 501
  // '#theme' => 'node__article', add it to the suggestions array before
  // invoking suggestion alter hooks.
  if (isset($info['base hook'])) {
    $suggestions[] = $hook;
  }
502 503 504 505 506 507 508 509

  // Invoke hook_theme_suggestions_alter() and
  // hook_theme_suggestions_HOOK_alter().
  $hooks = array(
    'theme_suggestions',
    'theme_suggestions_' . $base_theme_hook,
  );
  \Drupal::moduleHandler()->alter($hooks, $suggestions, $variables, $base_theme_hook);
510 511

  // Check if each suggestion exists in the theme registry, and if so,
512 513
  // use it instead of the hook that _theme() was called with. For example, a
  // function may call _theme('node', ...), but a module can add
514 515 516
  // 'node__article' as a suggestion via hook_theme_suggestions_HOOK_alter(),
  // enabling a theme to have an alternate template file for article nodes.
  foreach (array_reverse($suggestions) as $suggestion) {
517 518
    if ($theme_registry->has($suggestion)) {
      $info = $theme_registry->get($suggestion);
519 520 521 522
      break;
    }
  }

523 524 525 526 527 528 529 530
  // Include a file if the theme function or variable preprocessor is held
  // elsewhere.
  if (!empty($info['includes'])) {
    foreach ($info['includes'] as $include_file) {
      include_once DRUPAL_ROOT . '/' . $include_file;
    }
  }

531
  // Invoke the variable preprocessors, if any.
532 533
  if (isset($info['base hook'])) {
    $base_hook = $info['base hook'];
534
    $base_hook_info = $theme_registry->get($base_hook);
535
    // Include files required by the base hook, since its variable preprocessors
536 537 538 539 540 541
    // might reside there.
    if (!empty($base_hook_info['includes'])) {
      foreach ($base_hook_info['includes'] as $include_file) {
        include_once DRUPAL_ROOT . '/' . $include_file;
      }
    }
542
    // Replace the preprocess functions with those from the base hook.
543
    if (isset($base_hook_info['preprocess functions'])) {
544 545 546 547
      // Set a variable for the 'theme_hook_suggestion'. This is used to
      // maintain backwards compatibility with template engines.
      $theme_hook_suggestion = $hook;
      $info['preprocess functions'] = $base_hook_info['preprocess functions'];
548 549
    }
  }
550 551 552 553
  if (isset($info['preprocess functions'])) {
    foreach ($info['preprocess functions'] as $preprocessor_function) {
      if (function_exists($preprocessor_function)) {
        $preprocessor_function($variables, $hook, $info);
554
      }
555 556
    }
  }
557

558
  // Generate the output using either a function or a template.
559
  $output = '';
560
  if (isset($info['function'])) {
561
    if (function_exists($info['function'])) {
562
      $output = $info['function']($variables);
563
    }
564
  }
565
  else {
566 567
    $render_function = 'twig_render_template';
    $extension = '.html.twig';
568

569
    // The theme engine may use a different extension and a different renderer.
570 571
    global $theme_engine;
    if (isset($theme_engine)) {
572
      if ($info['type'] != 'module') {
573 574
        if (function_exists($theme_engine . '_render_template')) {
          $render_function = $theme_engine . '_render_template';
575
        }
576
        $extension_function = $theme_engine . '_extension';
577 578 579 580 581 582
        if (function_exists($extension_function)) {
          $extension = $extension_function();
        }
      }
    }

583 584 585 586 587
    // In some cases, a template implementation may not have had
    // template_preprocess() run (for example, if the default implementation is
    // a function, but a template overrides that default implementation). In
    // these cases, a template should still be able to expect to have access to
    // the variables provided by template_preprocess(), so we add them here if
588 589 590 591 592
    // they don't already exist. We don't want the overhead of running
    // template_preprocess() twice, so we use the 'directory' variable to
    // determine if it has already run, which while not completely intuitive,
    // is reasonably safe, and allows us to save on the overhead of adding some
    // new variable to track that.
593 594
    if (!isset($variables['directory'])) {
      $default_template_variables = array();
595
      template_preprocess($default_template_variables, $hook, $info);
596 597
      $variables += $default_template_variables;
    }
598 599 600 601 602 603 604 605 606 607 608 609 610 611
    if (!isset($default_attributes)) {
      $default_attributes = new Attribute();
    }
    foreach (array('attributes', 'title_attributes', 'content_attributes') as $key) {
      if (isset($variables[$key]) && !($variables[$key] instanceof Attribute)) {
        if ($variables[$key]) {
          $variables[$key] = new Attribute($variables[$key]);
        }
        else {
          // Create empty attributes.
          $variables[$key] = clone $default_attributes;
        }
      }
    }
612

613 614 615 616
    // Render the output using the template file.
    $template_file = $info['template'] . $extension;
    if (isset($info['path'])) {
      $template_file = $info['path'] . '/' . $template_file;
617
    }
618 619 620 621 622 623 624 625 626 627
    // Add the theme suggestions to the variables array just before rendering
    // the template for backwards compatibility with template engines.
    $variables['theme_hook_suggestions'] = $suggestions;
    // For backwards compatibility, pass 'theme_hook_suggestion' on to the
    // template engine. This is only set when calling a direct suggestion like
    // '#theme' => 'menu_tree__shortcut_default' when the template exists in the
    // current theme.
    if (isset($theme_hook_suggestion)) {
      $variables['theme_hook_suggestion'] = $theme_hook_suggestion;
    }
628
    $output = $render_function($template_file, $variables);
629
  }
630

631 632
  // restore path_to_theme()
  $theme_path = $temp;
633
  return (string) $output;
634 635
}

636
/**
637 638 639 640 641 642 643 644
 * Returns the path to the current themed element.
 *
 * It can point to the active theme or the module handling a themed
 * implementation. For example, when invoked within the scope of a theming call
 * it will depend on where the theming function is handled. If implemented from
 * a module, it will point to the module. If implemented from the active theme,
 * it will point to the active theme. When called outside the scope of a
 * theming call, it will always point to the active theme.
645
 */
646
function path_to_theme() {
647
  global $theme_path;
648

649
  if (!isset($theme_path)) {
650
    drupal_theme_initialize();
651 652
  }

653
  return $theme_path;
654 655
}

656
/**
657
 * Allows themes and/or theme engines to discover overridden theme functions.
658 659 660 661 662 663
 *
 * @param $cache
 *   The existing cache of theme hooks to test against.
 * @param $prefixes
 *   An array of prefixes to test, in reverse order of importance.
 *
664
 * @return $implementations
665 666 667
 *   The functions found, suitable for returning from hook_theme;
 */
function drupal_find_theme_functions($cache, $prefixes) {
668
  $implementations = array();
669 670 671 672
  $functions = get_defined_functions();

  foreach ($cache as $hook => $info) {
    foreach ($prefixes as $prefix) {
673 674 675 676 677 678
      // Find theme functions that implement possible "suggestion" variants of
      // registered theme hooks and add those as new registered theme hooks.
      // The 'pattern' key defines a common prefix that all suggestions must
      // start with. The default is the name of the hook followed by '__'. An
      // 'base hook' key is added to each entry made for a found suggestion,
      // so that common functionality can be implemented for all suggestions of
679
      // the same base hook. To keep things simple, deep hierarchy of
680 681 682 683
      // suggestions is not supported: each suggestion's 'base hook' key
      // refers to a base hook, not to another suggestion, and all suggestions
      // are found using the base hook's pattern, not a pattern from an
      // intermediary suggestion.
684
      $pattern = isset($info['pattern']) ? $info['pattern'] : ($hook . '__');
685
      if (!isset($info['base hook']) && !empty($pattern)) {
686
        $matches = preg_grep('/^' . $prefix . '_' . $pattern . '/', $functions['user']);
687 688
        if ($matches) {
          foreach ($matches as $match) {
689
            $new_hook = substr($match, strlen($prefix) + 1);
690
            $arg_name = isset($info['variables']) ? 'variables' : 'render element';
691
            $implementations[$new_hook] = array(
692
              'function' => $match,
693
              $arg_name => $info[$arg_name],
694
              'base hook' => $hook,
695 696 697 698
            );
          }
        }
      }
699 700 701
      // Find theme functions that implement registered theme hooks and include
      // that in what is returned so that the registry knows that the theme has
      // this implementation.
702
      if (function_exists($prefix . '_' . $hook)) {
703
        $implementations[$hook] = array(
704
          'function' => $prefix . '_' . $hook,
705 706 707 708 709
        );
      }
    }
  }

710
  return $implementations;
711 712 713
}

/**
714
 * Allows themes and/or theme engines to easily discover overridden templates.
715 716 717 718 719 720 721 722 723
 *
 * @param $cache
 *   The existing cache of theme hooks to test against.
 * @param $extension
 *   The extension that these templates will have.
 * @param $path
 *   The path to search.
 */
function drupal_find_theme_templates($cache, $extension, $path) {
724
  $implementations = array();
725

726 727 728 729
  // Collect paths to all sub-themes grouped by base themes. These will be
  // used for filtering. This allows base themes to have sub-themes in its
  // folder hierarchy without affecting the base themes template discovery.
  $theme_paths = array();
730
  foreach (list_themes() as $theme_info) {
731
    if (!empty($theme_info->base_theme)) {
732
      $theme_paths[$theme_info->base_theme][$theme_info->getName()] = $theme_info->getPath();
733 734 735 736 737 738 739
    }
  }
  foreach ($theme_paths as $basetheme => $subthemes) {
    foreach ($subthemes as $subtheme => $subtheme_path) {
      if (isset($theme_paths[$subtheme])) {
        $theme_paths[$basetheme] = array_merge($theme_paths[$basetheme], $theme_paths[$subtheme]);
      }
740 741
    }
  }
742 743
  global $theme;
  $subtheme_paths = isset($theme_paths[$theme]) ? $theme_paths[$theme] : array();
744

745
  // Escape the periods in the extension.
746
  $regex = '/' . str_replace('.', '\.', $extension) . '$/';
747
  // Get a listing of all template files in the path to search.
748
  $files = file_scan_directory($path, $regex, array('key' => 'filename'));
749 750 751 752

  // Find templates that implement registered theme hooks and include that in
  // what is returned so that the registry knows that the theme has this
  // implementation.
753
  foreach ($files as $template => $file) {
754
    // Ignore sub-theme templates for the current theme.
755
    if (strpos($file->uri, str_replace($subtheme_paths, '', $file->uri)) !== 0) {
756 757
      continue;
    }
758 759
    // Remove the extension from the filename.
    $template = str_replace($extension, '', $template);
760 761 762 763
    // Transform - in filenames to _ to match function naming scheme
    // for the purposes of searching.
    $hook = strtr($template, '-', '_');
    if (isset($cache[$hook])) {
764
      $implementations[$hook] = array(
765
        'template' => $template,
766
        'path' => dirname($file->uri),
767 768
      );
    }
769 770 771 772

    // Match templates based on the 'template' filename.
    foreach ($cache as $hook => $info) {
      if (isset($info['template'])) {
773
        $template_candidates = array($info['template'], str_replace($info['theme path'] . '/templates/', '', $info['template']));
774 775 776 777 778 779 780 781
        if (in_array($template, $template_candidates)) {
          $implementations[$hook] = array(
            'template' => $template,
            'path' => dirname($file->uri),
          );
        }
      }
    }
782 783
  }

784
  // Find templates that implement possible "suggestion" variants of registered
785
  // theme hooks and add those as new registered theme hooks. See
786 787
  // drupal_find_theme_functions() for more information about suggestions and
  // the use of 'pattern' and 'base hook'.
788 789
  $patterns = array_keys($files);
  foreach ($cache as $hook => $info) {
790
    $pattern = isset($info['pattern']) ? $info['pattern'] : ($hook . '__');
791
    if (!isset($info['base hook']) && !empty($pattern)) {
792 793
      // Transform _ in pattern to - to match file naming scheme
      // for the purposes of searching.
794
      $pattern = strtr($pattern, '_', '-');
795

796
      $matches = preg_grep('/^' . $pattern . '/', $patterns);
797 798
      if ($matches) {
        foreach ($matches as $match) {
799
          $file = $match;
800 801
          // Remove the extension from the filename.
          $file = str_replace($extension, '', $file);
802 803
          // Put the underscores back in for the hook name and register this
          // pattern.
804
          $arg_name = isset($info['variables']) ? 'variables' : 'render element';
805
          $implementations[strtr($file, '-', '_')] = array(
806
            'template' => $file,
807
            'path' => dirname($files[$match]->uri),
808
            $arg_name => $info[$arg_name],
809
            'base hook' => $hook,
810 811 812 813 814
          );
        }
      }
    }
  }
815
  return $implementations;
816 817
}

818
/**
819
 * Retrieves a setting for the current theme or for a given theme.
820
 *
821 822
 * The final setting is obtained from the last value found in the following
 * sources:
823 824 825
 * - the default theme-specific settings defined in any base theme's .info.yml
 *   file
 * - the default theme-specific settings defined in the theme's .info.yml file
826 827 828 829
 * - the saved values from the global theme settings form
 * - the saved values from the theme's settings form
 * To only retrieve the default global theme setting, an empty string should be
 * given for $theme.
830 831
 *
 * @param $setting_name
832
 *   The name of the setting to be retrieved.
833
 * @param $theme
834 835
 *   The name of a given theme; defaults to the current theme.
 *
836 837 838
 * @return
 *   The value of the requested setting, NULL if the setting does not exist.
 */
839 840
function theme_get_setting($setting_name, $theme = NULL) {
  $cache = &drupal_static(__FUNCTION__, array());
841

842
  // If no key is given, use the current theme if we can determine it.
843
  if (!isset($theme)) {
844
    $theme = !empty($GLOBALS['theme_key']) ? $GLOBALS['theme_key'] : '';
845
  }
846

847
  if (empty($cache[$theme])) {
848 849
    // Create a theme settings object.
    $cache[$theme] = new ThemeSettings($theme);
850

851 852
    // Get the values for the theme-specific settings from the .info.yml files
    // of the theme and all its base themes.
853 854 855 856 857 858 859 860
    if ($theme) {
      $themes = list_themes();
      $theme_object = $themes[$theme];

      // Create a list which includes the current theme and all its base themes.
      if (isset($theme_object->base_themes)) {
        $theme_keys = array_keys($theme_object->base_themes);
        $theme_keys[] = $theme;
861
      }
862 863 864 865 866
      else {
        $theme_keys = array($theme);
      }
      foreach ($theme_keys as $theme_key) {
        if (!empty($themes[$theme_key]->info['settings'])) {
867
          $cache[$theme]->merge($themes[$theme_key]->info['settings']);
868
        }
869 870 871
      }
    }

872
    // Get the global settings from configuration.
873
    $cache[$theme]->merge(\Drupal::config('system.theme.global')->get());
874 875

    if ($theme) {
876
      // Get the saved theme-specific settings from the configuration system.
877
      $cache[$theme]->merge(\Drupal::config($theme . '.settings')->get());
878

879 880
      // If the theme does not support a particular feature, override the global
      // setting and set the value to NULL.
881
      //$supports = $cache[$theme]->get('supports');
882
      if (!empty($theme_object->info['features'])) {
883
        foreach (_system_default_theme_features() as $feature) {
884
          if (!in_array($feature, $theme_object->info['features'])) {
885
            $cache[$theme]->set('features.' . $feature, NULL);
886 887 888 889
          }
        }
      }

890
      // Generate the path to the logo image.
891 892 893
      if ($cache[$theme]->get('features.logo')) {
        $logo_path = $cache[$theme]->get('logo.path');
        if ($cache[$theme]->get('logo.use_default')) {
894
          $cache[$theme]->set('logo.url', file_create_url($theme_object->getPath() . '/logo.png'));
895
        }
896 897
        elseif ($logo_path) {
          $cache[$theme]->set('logo.url', file_create_url($logo_path));
898 899
        }
      }
900 901

      // Generate the path to the favicon.
902 903 904
      if ($cache[$theme]->get('features.favicon')) {
        $favicon_path = $cache[$theme]->get('favicon.path');
        if ($cache[$theme]->get('favicon.use_default')) {
905
          if (file_exists($favicon = $theme_object->getPath() . '/favicon.ico')) {
906
            $cache[$theme]->set('favicon.url', file_create_url($favicon));
907 908
          }
          else {
909
            $cache[$theme]->set('favicon.url', file_create_url('core/misc/favicon.ico'));
910 911
          }
        }
912 913
        elseif ($favicon_path) {
          $cache[$theme]->set('favicon.url', file_create_url($favicon_path));
914 915
        }
        else {
916
          $cache[$theme]->set('features.favicon', FALSE);
917
        }
918
      }
919
    }
920 921
  }

922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962
  return $cache[$theme]->get($setting_name);
}

/**
 * Converts theme settings to configuration.
 *
 * @see system_theme_settings_submit()
 *
 * @param array $theme_settings
 *   An array of theme settings from system setting form or a Drupal 7 variable.
 * @param Config $config
 *   The configuration object to update.
 *
 * @return
 *   The Config object with updated data.
 */
function theme_settings_convert_to_config(array $theme_settings, Config $config) {
  foreach ($theme_settings as $key => $value) {
    if ($key == 'default_logo') {
      $config->set('logo.use_default', $value);
    }
    else if ($key == 'logo_path') {
      $config->set('logo.path', $value);
    }
    else if ($key == 'default_favicon') {
      $config->set('favicon.use_default', $value);
    }
    else if ($key == 'favicon_path') {
      $config->set('favicon.path', $value);
    }
    else if ($key == 'favicon_mimetype') {
      $config->set('favicon.mimetype', $value);
    }
    else if (substr($key, 0, 7) == 'toggle_') {
      $config->set('features.' . drupal_substr($key, 7), $value);
    }
    else if (!in_array($key, array('theme', 'logo_upload'))) {
      $config->set($key, $value);
    }
  }
  return $config;
963 964
}

965
/**
966
 * Enables a given list of themes.
967 968 969
 *
 * @param $theme_list
 *   An array of theme names.
970
 *
971 972 973 974
 * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0.
 *   Use \Drupal::service('theme_handler')->enable().
 *
 * @see \Drupal\Core\Extension\ThemeHandler::enable().
975 976
 */
function theme_enable($theme_list) {
977
  \Drupal::service('theme_handler')->enable($theme_list);
978 979 980
}

/**
981
 * Disables a given list of themes.
982 983 984
 *
 * @param $theme_list
 *   An array of theme names.