theme.inc 91.6 KB
Newer Older
Dries's avatar
   
Dries committed
1
<?php
2

3
/**
Dries's avatar
   
Dries committed
4
 * @file
5
 * The theme system, which controls the output of Drupal.
Dries's avatar
   
Dries committed
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
}

Dries's avatar
   
Dries committed
87
/**
88
 * Initializes the theme system by loading the theme.
Dries's avatar
   
Dries committed
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;
  }
Dries's avatar
   
Dries committed
97

98
  $themes = list_themes();
Dries's avatar
   
Dries committed
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';
Dries's avatar
   
Dries committed
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
      }
Dries's avatar
   
Dries committed
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;
Dries's avatar
   
Dries committed
269
270
    }
  }
271
272

  // Always include Twig as the default theme engine.
273
  include_once DRUPAL_ROOT . '/core/themes/engines/twig/twig.engine';
Dries's avatar
   
Dries committed
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
}

Dries's avatar
   
Dries committed
313
/**
314
 * Returns a list of all currently available themes.
Dries's avatar
   
Dries committed
315
 *
316
317
 * Retrieved from the database, if available and the site is not in maintenance
 * mode; otherwise compiled freshly from the filesystem.
318
 *
Dries's avatar
   
Dries committed
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');
Dries's avatar
   
Dries committed
333
334

  if ($refresh) {
335
    $theme_handler->reset();
336
    system_list_reset();
Dries's avatar
   
Dries committed
337
338
  }

339
  return $theme_handler->listInfo();
Dries's avatar
   
Dries committed
340
341
}

Dries's avatar
   
Dries committed
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()
Dries's avatar
   
Dries committed
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'];
Dries's avatar
   
Dries committed
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);
Dries's avatar
   
Dries committed
563
    }
Dries's avatar
   
Dries committed
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);
Dries's avatar
   
Dries committed
629
  }
630

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

Dries's avatar
   
Dries committed
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.
Dries's avatar
   
Dries committed
645
 */
Dries's avatar
   
Dries committed
646
function path_to_theme() {
647
  global $theme_path;
Dries's avatar
   
Dries committed
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
}

Dries's avatar
   
Dries committed
818
/**
819
 * Retrieves a setting for the current theme or for a given theme.
Dries's avatar
   
Dries committed
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.
Dries's avatar
   
Dries committed
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.
 *
Dries's avatar
   
Dries committed
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());
Dries's avatar
   
Dries committed
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
  }
Dries's avatar
   
Dries committed
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;
Dries's avatar
   
Dries committed
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
        }
Dries's avatar
   
Dries committed
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
    }
Dries's avatar
   
Dries committed
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;
Dries's avatar
   
Dries committed
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.
985
 *
986