theme.inc 62.9 KB
Newer Older
1
<?php
2
// $Id$
3

4
/**
5
 * @file
6
 * The theme system, which controls the output of Drupal.
7 8 9 10
 *
 * The theme system allows for nearly all output of the Drupal system to be
 * customized by user themes.
 *
Dries's avatar
Dries committed
11
 * @see <a href="http://drupal.org/node/253">Theme system</a>
12 13
 * @see themeable
 */
Dries's avatar
Dries committed
14

15 16 17 18 19 20 21 22 23 24 25 26 27
 /**
 * @name Content markers
 * @{
 * Markers used by theme_mark() and node_mark() to designate content.
 * @see theme_mark(), node_mark()
 */
define('MARK_READ',    0);
define('MARK_NEW',     1);
define('MARK_UPDATED', 2);
/**
 * @} End of "Content markers".
 */

28
/**
29
 * Initialize the theme system by loading the theme.
30
 */
31
function init_theme() {
32 33 34 35 36 37
  global $theme, $user, $custom_theme, $theme_engine, $theme_key;

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

39
  drupal_bootstrap(DRUPAL_BOOTSTRAP_DATABASE);
40 41
  $themes = list_themes();

42 43
  // Only select the user selected theme if it is available in the
  // list of enabled themes.
44
  $theme = !empty($user->theme) && !empty($themes[$user->theme]->status) ? $user->theme : variable_get('theme_default', 'garland');
45 46

  // Allow modules to override the present theme... only select custom theme
47
  // if it is available in the list of installed themes.
48
  $theme = $custom_theme && $themes[$custom_theme] ? $custom_theme : $theme;
49 50 51 52

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

53 54 55 56
  // Find all our ancestor themes and put them in an array.
  $base_theme = array();
  $ancestor = $theme;
  while ($ancestor && isset($themes[$ancestor]->base_theme)) {
57 58
    $base_theme[] = $new_base_theme = $themes[$themes[$ancestor]->base_theme];
    $ancestor = $themes[$ancestor]->base_theme;
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
  }
  _init_theme($themes[$theme], array_reverse($base_theme));
}

/**
 * Initialize the theme system given already loaded information. This
 * function is useful to initialize a theme when no database is present.
 *
 * @param $theme
 *   An object with the following information:
 *     filename
 *       The .info file for this theme. The 'path' to
 *       the theme will be in this file's directory. (Required)
 *     owner
 *       The path to the .theme file or the .engine file to load for
 *       the theme. (Required)
 *     stylesheet
 *       The primary stylesheet for the theme. (Optional)
 *     engine
 *       The name of theme engine to use. (Optional)
 * @param $base_theme
 *    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.
 */
function _init_theme($theme, $base_theme = array()) {
  global $theme_info, $base_theme_info, $theme_engine, $theme_path;
  $theme_info = $theme;
  $base_theme_info = $base_theme;

  $theme_path = dirname($theme->filename);

93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
  // 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();

  // 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;
        }
      }
    }
  }

109 110 111
  // Add stylesheets used by this theme.
  if (!empty($theme->stylesheets)) {
    foreach ($theme->stylesheets as $media => $stylesheets) {
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132
      foreach ($stylesheets as $name => $stylesheet) {
        $final_stylesheets[$media][$name] = $stylesheet;
      }
    }
  }

  // And now add the stylesheets properly
  foreach ($final_stylesheets as $media => $stylesheets) {
    foreach ($stylesheets as $stylesheet) {
      drupal_add_css($stylesheet, 'theme', $media);
    }
  }

  // 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;
133
      }
134 135
    }
  }
136

137 138
  // Add scripts used by this theme.
  if (!empty($theme->scripts)) {
139 140
    foreach ($theme->scripts as $name => $script) {
      $final_scripts[$name] = $script;
141 142 143
    }
  }

144 145 146 147 148
  // Add scripts used by this theme.
  foreach ($final_scripts as $script) {
    drupal_add_js($script, 'theme');
  }

149 150 151 152 153 154 155 156
  $theme_engine = NULL;

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

    $theme_engine = $theme->engine;
157
    if (function_exists($theme_engine .'_init')) {
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
      foreach ($base_theme as $base) {
        call_user_func($theme_engine .'_init', $base);
      }
      call_user_func($theme_engine .'_init', $theme);
    }
  }
  else {
    // include non-engine theme files
    foreach ($base_theme as $base) {
      // Include the theme file or the engine.
      if (!empty($base->owner)) {
        include_once './'. $base->owner;
      }
    }
    // and our theme gets one too.
    if (!empty($theme->owner)) {
      include_once './'. $theme->owner;
175 176
    }
  }
177
  _theme_load_registry($theme, $base_theme, $theme_engine);
178 179
}

180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
/**
 * Retrieve the stored theme registry. If the theme registry is already
 * in memory it will be returned; otherwise it will attempt to load the
 * registry from cache. If this fails, it will construct the registry and
 * cache it.
 */
function theme_get_registry($registry = NULL) {
  static $theme_registry = NULL;
  if (isset($registry)) {
    $theme_registry = $registry;
  }

  return $theme_registry;
}

/**
 * Store the theme registry in memory.
 */
function _theme_set_registry($registry) {
  // Pass through for setting of static variable.
  return theme_get_registry($registry);
}

/**
 * Get the theme_registry cache from the database; if it doesn't exist, build
 * it.
206 207 208 209 210 211 212 213
 *
 * @param $theme
 *   The loaded $theme object.
 * @param $base_theme
 *   An array of loaded $theme objects representing the ancestor themes in
 *   oldest first order.
 * @param theme_engine
 *   The name of the theme engine.
214
 */
215 216 217
function _theme_load_registry($theme, $base_theme = NULL, $theme_engine = NULL) {
  // Check the theme registry cache; if it exists, use it.
  $cache = cache_get("theme_registry:$theme->name", 'cache');
218
  if (isset($cache->data)) {
219
    $registry = $cache->data;
220 221
  }
  else {
222 223
    // If not, build one and cache it.
    $registry = _theme_build_registry($theme, $base_theme, $theme_engine);
224 225 226 227 228 229 230 231 232
    _theme_save_registry($theme, $registry);
  }
  _theme_set_registry($registry);
}

/**
 * Write the theme_registry cache into the database.
 */
function _theme_save_registry($theme, $registry) {
233
  cache_set("theme_registry:$theme->name", $registry);
234 235 236 237 238 239 240 241 242 243 244 245
}

/**
 * Force the system to rebuild the theme registry; this should be called
 * when modules are added to the system, or when a dynamic system needs
 * to add more theme hooks.
 */
function drupal_rebuild_theme_registry() {
  cache_clear_all('theme_registry', 'cache', TRUE);
}

/**
246 247 248 249 250 251 252 253
 * Process a single invocation of the theme hook. $type will be one
 * of 'module', 'theme_engine' or 'theme' and it tells us some
 * important information.
 *
 * Because $cache is a reference, the cache will be continually
 * expanded upon; new entries will replace old entries in the
 * array_merge, but we are careful to ensure some data is carried
 * forward, such as the arguments a theme hook needs.
254 255 256 257 258
 *
 * An override flag can be set for preprocess functions. When detected the
 * cached preprocessors for the hook will not be merged with the newly set.
 * This can be useful to themes and theme engines by giving them more control
 * over how and when the preprocess functions are run.
259
 */
260
function _theme_process_registry(&$cache, $name, $type, $theme, $path) {
261 262
  $function = $name .'_theme';
  if (function_exists($function)) {
263
    $result = $function($cache, $type, $theme, $path);
264 265 266

    foreach ($result as $hook => $info) {
      $result[$hook]['type'] = $type;
267
      $result[$hook]['theme path'] = $path;
268 269
      // if function and file are left out, default to standard naming
      // conventions.
270
      if (!isset($info['template']) && !isset($info['function'])) {
271 272
        $result[$hook]['function'] = ($type == 'module' ? 'theme_' : $name .'_') . $hook;
      }
273 274 275
      // If a path is set in the info, use what was set. Otherwise use the
      // default path. This is mostly so system.module can declare theme
      // functions on behalf of core .include files.
276 277
      // All files are included to be safe. Conditionally included
      // files can prevent them from getting registered.
278 279
      if (isset($info['file']) && !isset($info['path'])) {
        $result[$hook]['file'] = $path .'/'. $info['file'];
280 281 282 283 284 285 286 287
        include_once($result[$hook]['file']);
      }
      elseif (isset($info['file']) && isset($info['path'])) {
        include_once($info['path'] .'/'. $info['file']);
      }

      if (isset($info['template']) && !isset($info['path'])) {
        $result[$hook]['template'] = $path .'/'. $info['template'];
288 289 290 291 292 293 294
      }
      // If 'arguments' have been defined previously, carry them forward.
      // This should happen if a theme overrides a Drupal defined theme
      // function, for example.
      if (!isset($info['arguments']) && isset($cache[$hook])) {
        $result[$hook]['arguments'] = $cache[$hook]['arguments'];
      }
295 296 297 298 299 300 301 302
      // Likewise with theme paths. These are used for template naming suggestions.
      // Theme implementations can occur in multiple paths. Suggestions should follow.
      if (!isset($info['theme paths']) && isset($cache[$hook])) {
        $result[$hook]['theme paths'] = $cache[$hook]['theme paths'];
      }
      // Check for sub-directories.
      $result[$hook]['theme paths'][] = isset($info['path']) ? $info['path'] : $path;

303 304 305 306
      // Check for default _preprocess_ functions. Ensure arrayness.
      if (!isset($info['preprocess functions']) || !is_array($info['preprocess functions'])) {
        $info['preprocess functions'] = array();
        $prefix = ($type == 'module' ? 'template' : $name);
307
        // theme engines get an extra set that come before the normally named preprocess.
308 309 310 311 312 313 314 315
        if ($type == 'theme_engine') {
          if (function_exists($prefix .'_engine_preprocess')) {
            $info['preprocess functions'][] = $prefix .'_engine_preprocess';
          }
          if (function_exists($prefix .'_engine_preprocess_'. $hook)) {
            $info['preprocess functions'][] = $prefix .'_engine_preprocess_'. $hook;
          }
        }
316

317
        // Let the theme engine register theme specific variable functions.
318
        $prefixes = array($prefix);
319 320
        if ($type == 'theme_engine') {
          $prefixes[] = $theme;
321
        }
322 323 324 325 326 327 328
        foreach ($prefixes as $prefix) {
          if (function_exists($prefix .'_preprocess')) {
            $info['preprocess functions'][] = $prefix .'_preprocess';
          }
          if (function_exists($prefix .'_preprocess_'. $hook)) {
            $info['preprocess functions'][] = $prefix .'_preprocess_'. $hook;
          }
329
        }
330
      }
331 332 333 334 335 336 337
      // Check for the override flag and prevent the cached preprocess functions from being used.
      // This allows themes or theme engines to remove preprocessors set earlier in the registry build.
      if (!empty($info['override preprocess functions'])) {
        // Flag not needed inside the registry.
        unset($result[$hook]['override preprocess functions']);
      }
      elseif (isset($cache[$hook]['preprocess functions']) && is_array($cache[$hook]['preprocess functions'])) {
338 339 340
        $info['preprocess functions'] = array_merge($cache[$hook]['preprocess functions'], $info['preprocess functions']);
      }
      $result[$hook]['preprocess functions'] = $info['preprocess functions'];
341 342
    }

343
    // Merge the newly created theme hooks into the existing cache.
344 345 346 347 348 349
    $cache = array_merge($cache, $result);
  }
}

/**
 * Rebuild the hook theme_registry cache.
350 351 352 353 354 355 356 357
 *
 * @param $theme
 *   The loaded $theme object.
 * @param $base_theme
 *   An array of loaded $theme objects representing the ancestor themes in
 *   oldest first order.
 * @param theme_engine
 *   The name of the theme engine.
358
 */
359
function _theme_build_registry($theme, $base_theme, $theme_engine) {
360
  $cache = array();
361 362
  // First, process the theme hooks advertised by modules. This will
  // serve as the basic registry.
363
  foreach (module_implements('theme') as $module) {
364 365 366 367 368 369 370 371 372 373 374
    _theme_process_registry($cache, $module, 'module', $module, drupal_get_path('module', $module));
  }

  // Process each base theme.
  foreach ($base_theme as $base) {
    // If the theme uses a theme engine, process its hooks.
    $base_path = dirname($base->filename);
    if ($theme_engine) {
      _theme_process_registry($cache, $theme_engine, 'base_theme_engine', $base->name, $base_path);
    }
    _theme_process_registry($cache, $base->name, 'base_theme', $base->name, $base_path);
375 376
  }

377
  // And then the same thing, but for the theme.
378
  if ($theme_engine) {
379
    _theme_process_registry($cache, $theme_engine, 'theme_engine', $theme->name, dirname($theme->filename));
380 381
  }

382 383
  // Finally, hooks provided by the theme itself.
  _theme_process_registry($cache, $theme->name, 'theme', $theme->name, dirname($theme->filename));
384

385 386
  // Let modules alter the registry
  drupal_alter('theme_registry', $cache);
387 388 389
  return $cache;
}

390
/**
391 392 393
 * Provides a list of currently available themes.
 *
 * @param $refresh
394 395 396
 *   Whether to reload the list of themes from the database.
 * @return
 *   An array of the currently available themes.
397
 */
398
function list_themes($refresh = FALSE) {
399
  static $list = array();
400 401

  if ($refresh) {
402
    $list = array();
403 404
  }

405
  if (empty($list)) {
406
    $list = array();
407
    $result = db_query("SELECT * FROM {system} WHERE type = '%s'", 'theme');
408 409
    while ($theme = db_fetch_object($result)) {
      if (file_exists($theme->filename)) {
410
        $theme->info = unserialize($theme->info);
411 412 413 414 415 416
        foreach ($theme->info['stylesheets'] as $media => $stylesheets) {
          foreach ($stylesheets as $stylesheet => $path) {
            if (file_exists($path)) {
              $theme->stylesheets[$media][$stylesheet] = $path;
            }
          }
417
        }
418 419 420 421
        foreach ($theme->info['scripts'] as $script => $path) {
          if (file_exists($path)) {
            $theme->scripts[$script] = $path;
          }
422
        }
423 424 425 426 427 428
        if (isset($theme->info['engine'])) {
          $theme->engine = $theme->info['engine'];
        }
        if (isset($theme->info['base theme'])) {
          $theme->base_theme = $theme->info['base theme'];
        }
429 430 431 432 433 434 435 436
        $list[$theme->name] = $theme;
      }
    }
  }

  return $list;
}

437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453
/**
 * Provides a list of currently available theme engines
 *
 * @param $refresh
 *   Whether to reload the list of themes from the database.
 * @return
 *   An array of the currently available theme engines.
 */
function list_theme_engines($refresh = FALSE) {
  static $list;

  if ($refresh) {
    unset($list);
  }

  if (!$list) {
    $list = array();
454
    $result = db_query("SELECT * FROM {system} WHERE type = '%s' AND status = %d ORDER BY name", 'theme_engine', '1');
455 456
    while ($engine = db_fetch_object($result)) {
      if (file_exists($engine->filename)) {
457
        $engine->info = unserialize($engine->info);
458 459 460 461 462 463 464 465
        $list[$engine->name] = $engine;
      }
    }
  }

  return $list;
}

466
/**
467 468 469 470 471 472 473 474 475
 * Generate the themed output.
 *
 * All requests for theme hooks must go through this function. It examines
 * the request and routes it to the appropriate theme function. The theme
 * registry is checked to determine which implementation to use, which may
 * be a function or a template.
 *
 * If the implementation is a function, it is executed and its return value
 * passed along.
476
 *
477 478 479 480
 * If the implementation is a template, the arguments are converted to a
 * $variables array. This array is then modified by the theme engine (if
 * applicable) and the theme. The following functions may be used to modify
 * the $variables array:
481
 *
482
 * ENGINE_engine_preprocess(&$variables)
483
 *   This function should only be implemented by theme engines and exists
484 485
 *   so that the theme engine can set necessary variables. It is commonly
 *   used to set global variables such as $directory and $is_front_page.
486
 *
487
 * ENGINE_engine_preprocess_HOOK(&$variables)
488
 *   This is the same as the previous function, but is called only per hook.
489
 *
490
 * ENGINE_preprocess_HOOK(&$variables)
491
 *
492
 * ENGINE_preprocess(&$variables)
493 494 495 496
 *   This is meant to be used by themes that utilize a theme engine; as it is
 *   good practice for these themes to use the theme engine's name for
 *   their functions so that they may share code. In PHPTemplate, these
 *   functions will appear in template.php
497
 *
498
 * THEME_preprocess_HOOK(&$variables)
499
 *
500
 * THEME_preprocess(&$variables)
501 502 503 504
 *   These functions are based upon the raw theme; they should primarily be
 *   used by themes that do not use an engine or by themes that need small
 *   changes to what has already been established in the theme engine version
 *   of the function.
505
 *
506 507 508 509
 * template_preprocess(&$variables)
 *   This function will only be called for theme functions registered by
 *   the named module. In general it is preferred to use the following
 *   function if possible, but it may not always be the case.
510
 *
511 512
 * template_preprocess_HOOK(&$variables)
 *   This is the same as the previous function, but is called only per hook.
513
 *
514 515 516 517 518 519 520 521
 * There are two special variables that these hooks can set:
 *   'template_file' and 'template_files'. These will be merged together
 *   to form a list of 'suggested' alternate template files to use, in
 *   reverse order of priority. template_file will always be a higher
 *   priority than items in template_files. theme() will then look for these
 *   files, one at a time, and use the first one
 *   that exists.
 * @param $hook
522 523 524 525 526 527
 *   The name of the theme function to call. May be an array, in which
 *   case the first hook that actually has an implementation registered
 *   will be used. This can be used to choose 'fallback' theme implementations,
 *   so that if the specific theme hook isn't implemented anywhere, a more
 *   generic one will be used. This can allow themes to create specific theme
 *   implementations for named objects.
528 529 530 531
 * @param ...
 *   Additional arguments to pass along to the theme function.
 * @return
 *   An HTML string that generates the themed output.
532
 */
533
function theme() {
534
  $args = func_get_args();
535
  $hook = array_shift($args);
536

537 538 539 540
  static $hooks = NULL;
  if (!isset($hooks)) {
    init_theme();
    $hooks = theme_get_registry();
541
  }
542

543 544 545 546 547 548 549 550 551
  if (is_array($hook)) {
    foreach ($hook as $candidate) {
      if (isset($hooks[$candidate])) {
        break;
      }
    }
    $hook = $candidate;
  }

552 553
  if (!isset($hooks[$hook])) {
    return;
554 555
  }

556
  $info = $hooks[$hook];
557 558 559 560
  global $theme_path;
  $temp = $theme_path;
  // point path_to_theme() to the currently used theme path:
  $theme_path = $hooks[$hook]['theme path'];
561

562 563 564 565 566 567 568 569
  // Include a file if the theme function or preprocess function is held elsewhere.
  if (!empty($info['file'])) {
    $include_file = $info['file'];
    if (isset($info['path'])) {
      $include_file = $info['path'] .'/'. $include_file;
    }
    include_once($include_file);
  }
570 571
  if (isset($info['function'])) {
    // The theme call is a function.
572
    $output = call_user_func_array($info['function'], $args);
573
  }
574 575 576 577 578 579 580 581 582 583 584 585
  else {
    // The theme call is a template.
    $variables = array(
      'template_files' => array()
    );
    if (!empty($info['arguments'])) {
      $count = 0;
      foreach ($info['arguments'] as $name => $default) {
        $variables[$name] = isset($args[$count]) ? $args[$count] : $default;
        $count++;
      }
    }
586

587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606
    // default render function and extension.
    $render_function = 'theme_render_template';
    $extension = '.tpl.php';

    // Run through the theme engine variables, if necessary
    global $theme_engine;
    if (isset($theme_engine)) {
      // If theme or theme engine is implementing this, it may have
      // a different extension and a different renderer.
      if ($hooks[$hook]['type'] != 'module') {
        if (function_exists($theme_engine .'_render_template')) {
          $render_function = $theme_engine .'_render_template';
        }
        $extension_function = $theme_engine .'_extension';
        if (function_exists($extension_function)) {
          $extension = $extension_function();
        }
      }
    }

607 608 609 610 611 612 613 614
    if (isset($info['preprocess functions']) && is_array($info['preprocess functions'])) {
      // This construct ensures that we can keep a reference through
      // call_user_func_array.
      $args = array(&$variables, $hook);
      foreach ($info['preprocess functions'] as $preprocess_function) {
        if (function_exists($preprocess_function)) {
          call_user_func_array($preprocess_function, $args);
        }
615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631
      }
    }

    // Get suggestions for alternate templates out of the variables
    // that were set. This lets us dynamically choose a template
    // from a list. The order is FILO, so this array is ordered from
    // least appropriate first to most appropriate last.
    $suggestions = array();

    if (isset($variables['template_files'])) {
      $suggestions = $variables['template_files'];
    }
    if (isset($variables['template_file'])) {
      $suggestions[] = $variables['template_file'];
    }

    if ($suggestions) {
632
      $template_file = drupal_discover_template($info['theme paths'], $suggestions, $extension);
633 634 635
    }

    if (empty($template_file)) {
636
      $template_file = $hooks[$hook]['template'] . $extension;
637 638 639 640
      if (isset($hooks[$hook]['path'])) {
        $template_file = $hooks[$hook]['path'] .'/'. $template_file;
      }
    }
641
    $output = $render_function($template_file, $variables);
642
  }
643 644 645
  // restore path_to_theme()
  $theme_path = $temp;
  return $output;
646 647 648
}

/**
649 650 651
 * Choose which template file to actually render. These are all suggested
 * templates from themes and modules. Theming implementations can occur on
 * multiple levels. All paths are checked to account for this.
652
 */
653
function drupal_discover_template($paths, $suggestions, $extension = '.tpl.php') {
654 655
  global $theme_engine;

656
  // Loop through all paths and suggestions in FIFO order.
657
  $suggestions = array_reverse($suggestions);
658
  $paths = array_reverse($paths);
659
  foreach ($suggestions as $suggestion) {
660 661 662 663 664 665
    if (!empty($suggestion)) {
      foreach ($paths as $path) {
        if (file_exists($file = $path .'/'. $suggestion . $extension)) {
          return $file;
        }
      }
666
    }
667 668 669 670
  }
}

/**
671
 * Return the path to the currently selected theme.
672
 */
673
function path_to_theme() {
674
  global $theme_path;
675

676
  if (!isset($theme_path)) {
677 678 679
    init_theme();
  }

680
  return $theme_path;
681 682
}

683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704
/**
 * Find overridden theme functions. Called by themes and/or theme engines to
 * easily discover theme functions.
 *
 * @param $cache
 *   The existing cache of theme hooks to test against.
 * @param $prefixes
 *   An array of prefixes to test, in reverse order of importance.
 *
 * @return $templates
 *   The functions found, suitable for returning from hook_theme;
 */
function drupal_find_theme_functions($cache, $prefixes) {
  $templates = array();
  $functions = get_defined_functions();

  foreach ($cache as $hook => $info) {
    foreach ($prefixes as $prefix) {
      if (!empty($info['pattern'])) {
        $matches = preg_grep('/^'. $prefix .'_'. $info['pattern'] .'/', $functions['user']);
        if ($matches) {
          foreach ($matches as $match) {
705
            $new_hook = str_replace($prefix .'_', '', $match);
706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738
            $templates[$new_hook] = array(
              'function' => $match,
              'arguments' => $info['arguments'],
            );
          }
        }
      }
      if (function_exists($prefix .'_'. $hook)) {
        $templates[$hook] = array(
          'function' => $prefix .'_'. $hook,
        );
      }
    }
  }

  return $templates;
}

/**
 * Find overridden theme templates. Called by themes and/or theme engines to
 * easily discover templates.
 *
 * @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) {
  $templates = array();

  // Escape the dots in the extension.
739
  $regex = str_replace('.', '\.', $extension) .'$';
740 741 742 743 744 745 746 747 748

  // Because drupal_system_listing works the way it does, we check for real
  // templates separately from checking for patterns.
  $files = drupal_system_listing($regex, $path, 'name', 0);
  foreach ($files as $template => $file) {
    // Chop off the extension. We do it this way because $template will
    // have one extension chopped off, but there might be more than one,
    // such as with .tpl.php
    $template = substr($template, 0, strpos($template, '.'));
749 750 751 752 753
    // Transform - in filenames to _ to match function naming scheme
    // for the purposes of searching.
    $hook = strtr($template, '-', '_');
    if (isset($cache[$hook])) {
      $templates[$hook] = array(
754
        'template' => $template,
755 756 757 758 759 760 761 762 763
        'path' => dirname($file->filename),
      );
    }
  }

  $patterns = array_keys($files);

  foreach ($cache as $hook => $info) {
    if (!empty($info['pattern'])) {
764 765 766 767 768
      // Transform _ in pattern to - to match file naming scheme
      // for the purposes of searching.
      $pattern = strtr($info['pattern'], '_', '-');

      $matches = preg_grep('/^'. $pattern .'/', $patterns);
769 770 771
      if ($matches) {
        foreach ($matches as $match) {
          $file = substr($match, 0, strpos($match, '.'));
772 773
          // Put the underscores back in for the hook name and register this pattern.
          $templates[strtr($file, '-', '_')] = array(
774
            'template' => $file,
775 776 777 778 779 780 781 782 783 784
            'path' => dirname($files[$match]->filename),
            'arguments' => $info['arguments'],
          );
        }
      }
    }
  }
  return $templates;
}

785 786 787
/**
 * Retrieve an associative array containing the settings for a theme.
 *
788
 * The final settings are arrived at by merging the default settings,
789 790 791 792 793 794 795 796 797 798 799 800 801
 * the site-wide settings, and the settings defined for the specific theme.
 * If no $key was specified, only the site-wide theme defaults are retrieved.
 *
 * The default values for each of settings are also defined in this function.
 * To add new settings, add their default values here, and then add form elements
 * to system_theme_settings() in system.module.
 *
 * @param $key
 *  The template/style value for a given theme.
 *
 * @return
 *   An associative array containing theme settings.
 */
802
function theme_get_settings($key = NULL) {
803 804 805 806
  $defaults = array(
    'mission'                       =>  '',
    'default_logo'                  =>  1,
    'logo_path'                     =>  '',
807 808
    'default_favicon'               =>  1,
    'favicon_path'                  =>  '',
809 810
    'primary_links'                 =>  1,
    'secondary_links'               =>  1,
811
    'toggle_logo'                   =>  1,
812
    'toggle_favicon'                =>  1,
813 814 815 816 817 818
    'toggle_name'                   =>  1,
    'toggle_search'                 =>  1,
    'toggle_slogan'                 =>  0,
    'toggle_mission'                =>  1,
    'toggle_node_user_picture'      =>  0,
    'toggle_comment_user_picture'   =>  0,
819 820
    'toggle_primary_links'          =>  1,
    'toggle_secondary_links'        =>  1,
821 822
  );

823
  if (module_exists('node')) {
824
    foreach (node_get_types() as $type => $name) {
825
      $defaults['toggle_node_info_'. $type] = 1;
826
    }
827 828 829 830 831 832 833
  }
  $settings = array_merge($defaults, variable_get('theme_settings', array()));

  if ($key) {
    $settings = array_merge($settings, variable_get(str_replace('/', '_', 'theme_'. $key .'_settings'), array()));
  }

834
  // Only offer search box if search.module is enabled.
835
  if (!module_exists('search') || !user_access('search content')) {
836 837 838
    $settings['toggle_search'] = 0;
  }

839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857
  return $settings;
}

/**
 * Retrieve a setting for the current theme.
 * This function is designed for use from within themes & engines
 * to determine theme settings made in the admin interface.
 *
 * Caches values for speed (use $refresh = TRUE to refresh cache)
 *
 * @param $setting_name
 *  The name of the setting to be retrieved.
 *
 * @param $refresh
 *  Whether to reload the cache of settings.
 *
 * @return
 *   The value of the requested setting, NULL if the setting does not exist.
 */
858
function theme_get_setting($setting_name, $refresh = FALSE) {
859
  global $theme_key;
860 861 862
  static $settings;

  if (empty($settings) || $refresh) {
863
    $settings = theme_get_settings($theme_key);
864 865 866 867 868 869 870 871 872 873 874 875 876 877

    $themes = list_themes();
    $theme_object = $themes[$theme_key];

    if ($settings['mission'] == '') {
      $settings['mission'] = variable_get('site_mission', '');
    }

    if (!$settings['toggle_mission']) {
      $settings['mission'] = '';
    }

    if ($settings['toggle_logo']) {
      if ($settings['default_logo']) {
878
        $settings['logo'] = base_path() . dirname($theme_object->filename) .'/logo.png';
879 880
      }
      elseif ($settings['logo_path']) {
881
        $settings['logo'] = base_path() . $settings['logo_path'];
882 883 884
      }
    }

885 886 887
    if ($settings['toggle_favicon']) {
      if ($settings['default_favicon']) {
        if (file_exists($favicon = dirname($theme_object->filename) .'/favicon.ico')) {
888
          $settings['favicon'] = base_path() . $favicon;
889 890
        }
        else {
891
          $settings['favicon'] = base_path() .'misc/favicon.ico';
892 893 894
        }
      }
      elseif ($settings['favicon_path']) {
895
        $settings['favicon'] = base_path() . $settings['favicon_path'];
896
      }
897 898 899
      else {
        $settings['toggle_favicon'] = FALSE;
      }
900
    }
901 902 903 904 905
  }

  return isset($settings[$setting_name]) ? $settings[$setting_name] : NULL;
}

906
/**
907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927
 * Render a system default template, which is essentially a PHP template.
 *
 * @param $file
 *   The filename of the template to render.
 * @param $variables
 *   A keyed array of variables that will appear in the output.
 *
 * @return
 *   The output generated by the template.
 */
function theme_render_template($file, $variables) {
  extract($variables, EXTR_SKIP);  // Extract the variables to a local namespace
  ob_start();                      // Start output buffering
  include "./$file";               // Include the file
  $contents = ob_get_contents();   // Get the contents of the buffer
  ob_end_clean();                  // End buffering and discard
  return $contents;                // Return the contents
}

/**
 * @defgroup themeable Default theme implementations
928
 * @{
929 930 931 932 933 934
 * Functions and templates that present output to the user, and can be
 * implemented by themes.
 *
 * Drupal's presentation layer is a pluggable system known as the theme
 * layer. Each theme can take control over most of Drupal's output, and
 * has complete control over the CSS.
935
 *
936 937 938
 * Inside Drupal, the theme layer is utilized by the use of the theme()
 * function, which is passed the name of a component (the theme hook)
 * and several arguments. For example, theme('table', $header, $rows);
939 940 941 942 943 944 945 946
 * Additionally, the theme() function can take an array of theme
 * hooks, which can be used to provide 'fallback' implementations to
 * allow for more specific control of output. For example, the function:
 * theme(array('table__foo', 'table), $header, $rows) would look to see if
 * 'table__foo' is registered anywhere; if it is not, it would 'fall back'
 * to the generic 'table' implementation. This can be used to attach specific
 * theme functions to named objects, allowing the themer more control over
 * specific types of output.
947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984
 *
 * As of Drupal 6, every theme hook is required to be registered by the
 * module that owns it, so that Drupal can tell what to do with it and
 * to make it simple for themes to identify and override the behavior
 * for these calls.
 *
 * The theme hooks are registered via hook_theme(), which returns an
 * array of arrays with information about the hook. It describes the
 * arguments the function or template will need, and provides
 * defaults for the template in case they are not filled in. If the default
 * implementation is a function, by convention it is named theme_HOOK().
 *
 * Each module should provide a default implementation for themes that
 * it registers. This implementation may be either a function or a template;
 * if it is a function it must be specified via hook_theme(). By convention,
 * default implementations of theme hooks are named theme_HOOK. Default
 * template implementations are stored in the module directory.
 *
 * Drupal's default template renderer is a simple PHP parsing engine that
 * includes the template and stores the output. Drupal's theme engines
 * can provide alternate template engines, such as XTemplate, Smarty and
 * PHPTal. The most common template engine is PHPTemplate (included with
 * Drupal and implemented in phptemplate.engine, which uses Drupal's default
 * template renderer.
 *
 * In order to create theme-specific implementations of these hooks,
 * themes can implement their own version of theme hooks, either as functions
 * or templates. These implementations will be used instead of the default
 * implementation. If using a pure .theme without an engine, the .theme is
 * required to implement its own version of hook_theme() to tell Drupal what
 * it is implementing; themes utilizing an engine will have their well-named
 * theming functions automatically registered for them. While this can vary
 * based upon the theme engine, the standard set by phptemplate is that theme
 * functions should be named either phptemplate_HOOK or THEMENAME_HOOK. For
 * example, for Drupal's default theme (Garland) to implement the 'table' hook,
 * the phptemplate.engine would find phptemplate_table() or garland_table().
 * The ENGINE_HOOK() syntax is preferred, as this can be used by sub-themes
 * (which are themes that share code but use different stylesheets).
985
 *
986
 * The theme system is described and defined in theme.inc.
987 988 989
 *
 * @see theme()
 * @see hook_theme()
990
 */
991

992
/**
993 994
 * Formats text for emphasized display in a placeholder inside a sentence.
 * Used automatically by t().
995 996 997 998 999 1000 1001 1002 1003
 *
 * @param $text
 *   The text to format (plain-text).
 * @return
 *   The formatted text (html).
 */
function theme_placeholder($text) {
  return '<em>'. check_plain($text) .'</em>';
}
1004

1005 1006 1007
/**
 * Generate a themed maintenance page.
 *
1008
 * Note: this function is not themeable.
1009 1010 1011
 *
 * @param $content
 *   The page content to show.
1012
 * @param $show_messages
1013
 *   Whether to output status and error messages.
1014
 *   FALSE can be useful to postpone the messages to a subsequent page.
1015
 */
1016
function theme_maintenance_page($content, $show_messages = TRUE) {
1017
  // Set required headers.
1018
  drupal_set_header('Content-Type: text/html; charset=utf-8');
1019 1020 1021
  drupal_set_html_head('<link type="text/css" rel="stylesheet" media="all" href="'. base_path() .'misc/maintenance.css" />');
  drupal_set_html_head('<link type="text/css" rel="stylesheet" media="all" href="'. base_path() . drupal_get_path('module', 'system') .'/defaults.css" />');
  drupal_set_html_head('<link type="text/css" rel="stylesheet" media="all" href="'. base_path() . drupal_get_path('module', 'system') .'/system.css" />');
1022
  drupal_set_html_head('<link rel="shortcut icon" href="'. base_path() .'misc/favicon.ico" type="image/x-icon" />');
1023

1024 1025 1026 1027 1028 1029
  // Prepare variables.
  $variables = array(
    'head_title' => strip_tags(drupal_get_title()),
    'head' => drupal_get_html_head(),
    'styles' => '',
    'scripts' => drupal_get_js(),
1030 1031
    'left' => drupal_get_content('left'),
    'right' => drupal_get_content('right'),
1032 1033 1034 1035 1036
    'base_path' => base_path(),
    'path_to_theme' => base_path() .'themes/garland/minnelli',
    'logo' => base_path() .'themes/garland/minnelli/logo.png',
    'site_title' => t('Drupal'),
    'title' => drupal_get_title(),
1037
    'messages' => $show_messages ? theme('status_messages') : '',
1038 1039
    'content' => $content,
  );
1040

1041
  $output = theme_render_template('misc/maintenance.tpl.php', $variables);
1042 1043 1044 1045

  return $output;
}

1046 1047 1048
/**
 * Generate a themed installation page.
 *
1049
 * Note: this function is not themeable.
1050 1051 1052 1053
 *
 * @param $content
 *   The page content to show.
 */
1054 1055
function theme_install_page($content) {
  drupal_set_header('Content-Type: text/html; charset=utf-8');
1056
  drupal_add_css('misc/maintenance.css', 'module', 'all', FALSE);
1057 1058
  drupal_set_html_head('<link rel="shortcut icon" href="'. base_path() .'misc/favicon.ico" type="image/x-icon" />');

1059 1060 1061 1062 1063
  $variables = array(
    'head_title' => strip_tags(drupal_get_title()),
    'head' => drupal_get_html_head(),
    'styles' => drupal_get_css(),
    'scripts' => drupal_get_js(),
1064 1065
    'left' => drupal_get_content('left'),
    'right' => drupal_get_content('right'),
1066 1067 1068 1069 1070 1071 1072 1073 1074 1075
    'base_path' => base_path(),
    'path_to_theme' => base_path() .'themes/garland/minnelli',
    'logo' => base_path() .'themes/garland/minnelli/logo.png',
    'site_title' => st('Drupal Installation'),
    'title' => drupal_get_title(),
    'messages' => '',
    'content' => $content,
  );

  // Special handling of error messages
1076 1077
  $messages = drupal_set_message();
  if (isset($messages['error'])) {
1078
    $title = count($messages['error']) > 1 ? st('The following errors must be resolved before you can continue the installation process') : st('The following error must be resolved before you can continue the installation process');
1079 1080
    $variables['messages'] .= '<h3>'. $title .':</h3>';
    $variables['messages'] .= theme('status_messages', 'error');
1081 1082
  }

1083
  // Special handling of status messages
1084
  if (isset($messages['status'])) {
1085
    $warnings = count($messages['status']) > 1 ? st('The following installation warnings should be carefully reviewed, but in most cases may be safely ignored') : st('The following installation warning should be carefully reviewed, but in most cases may be safely ignored');
1086 1087
    $variables['messages'] .= '<h4>'. $title .':</h4>';
    $variables['messages'] .= theme('status_messages', 'status');
1088 1089
  }

1090
  return theme_render_template('misc/maintenance.tpl.php', $variables);
1091
}
1092

1093 1094 1095
/**
 * Return a themed list of maintenance tasks to perform.
 *
1096
 * Note: this function is not themeable.
1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111
 */
function theme_task_list($items, $active = NULL) {
  $done = isset($items[$active]) || $active == NULL;
  $output = '<ol class="task-list">';
  foreach ($items as $k => $item) {
    if ($active == $k) {
      $class = 'active';
      $done = false;
    }
    else {
      $class = $done ? 'done' : '';
    }
    $output .= '<li class="'. $class .'">'. $item .'</li>';
  }
  $output .= '</ol>';
1112 1113 1114
  return $output;
}

1115
/**
1116
 * Return a themed set of status and/or error messages. The messages are grouped
1117 1118
 * by type.
 *
1119 1120 1121
 * @param $display
 *   (optional) Set to 'status' or 'error' to display only messages of that type.
 *
1122 1123
 * @return
 *   A string containing the messages.
1124
 */
1125 1126 1127 1128 1129 1130
function theme_status_messages($display = NULL) {
  $output = '';
  foreach (drupal_get_messages($display) as $type => $messages) {
    $output .= "<div class=\"messages $type\">\n";
    if (count($messages) > 1) {
      $output .= " <ul>\n";
1131
      foreach ($messages as $message) {
1132
        $output .= '  <li>'. $message ."</li>\n";
1133
      }
1134
      $output .= " </ul>\n";
1135
    }
1136 1137 1138 1139
    else {
      $output .= $messages[0];
    }
    $output .= "</div>\n";
1140
  }
1141
  return $output;
1142 1143
}

1144
/**
1145 1146 1147
 * Return a themed set of links.
 *
 * @param $links
1148
 *   A keyed array of links to be themed.
1149 1150
 * @param $attributes
 *   A keyed array of attributes
1151
 * @return
1152
 *   A string containing an unordered list of links.
1153
 */
1154 1155
function theme_links($links, $attributes = array('class' => 'links')) {
  $output = '';
1156

1157
  if (count($links) > 0) {
1158 1159 1160 1161 1162
    $output = '<ul'. drupal_attributes($attributes) .'>';

    $num_links = count($links);
    $i = 1;

1163
    foreach ($links as $key => $link) {
1164
      $class = $key;
1165

1166
      // Add first, last and active classes to the list of links to help out themers.
1167
      if ($i == 1) {
1168
        $class .= ' first';
1169 1170
      }
      if ($i == $num_links) {
1171 1172
        $class .= ' last';
      }
1173
      if (isset($link['href']) && $link['href'] == $_GET['q']) {
1174
        $class .= ' active';
1175
      }
1176
      $output .= '<li class="'. $class .'">';
1177

1178
      if (isset($link['href'])) {
1179 1180
        // Pass in $link as $options, they share the same keys.
        $output .= l($link['title'], $link['href'], $link);
1181
      }
1182
      else if (!empty($link['title'])) {
1183 1184
        // Some links are actually not links, but we wrap these in <span> for adding title and class attributes
        if (empty($link['html'])) {
1185 1186
          $link['title'] = check_plain($link['title']);
        }
1187 1188 1189 1190 1191
        $span_attributes = '';
        if (isset($link['attributes'])) {
          $span_attributes = drupal_attributes($link['attributes']);
        }
        $output .= '<span'. $span_attributes .'>'. $link['title'] .'</span>';
1192
      }
1193 1194

      $i++;
1195
      $output .= "</li>\n";
1196
    }
1197 1198

    $output .= '</ul>';
1199
  }
1200

1201
  return $output;
1202
}
Dries's avatar
Dries committed
1203

1204
/**
1205
 * Return a themed image.
1206
 *
1207
 * @param $path
1208
 *   Either the path of the image file (relative to base_path()) or a full URL.
1209 1210 1211 1212
 * @param $alt
 *   The alternative text for text-based browsers.
 * @param $title
 *   The title text is displayed when the image is hovered in some popular browsers.
1213 1214
 * @param $attributes
 *   Associative array of attributes to be placed in the img tag.
Steven Wittens's avatar
Steven Wittens committed
1215
 * @param $getsize
1216
 *   If set to TRUE, the image's dimension are fetched and added as width/height attributes.
1217
 * @return
1218
 *   A string containing the image tag.
1219
 */
1220
function theme_image($path, $alt = '', $title = '', $attributes = NULL, $getsize = TRUE) {
1221
  if (!$getsize || (is_file($path) && (list($width, $height, $type, $image_attributes) = @getimagesize($path)))) {
1222
    $attributes = drupal_attributes($attributes);
1223
    $url = (url($path) == $path) ? $path : (base_path() . $path);
1224
    return '<img src="'. check_url($url) .'" alt="'. check_plain($alt) .'" title="'. check_plain($title) .'" '. (isset($image_attributes) ? $image_attributes : '') . $attributes .' />';
1225
  }
1226
}
Dries's avatar
Dries committed
1227

1228
/**
1229
 * Return a themed breadcrumb trail.
1230
 *
1231 1232 1233
 * @param $breadcrumb
 *   An array containing the breadcrumb links.
 * @return a string containing the breadcrumb output.
1234
 */
1235
function theme_breadcrumb($breadcrumb) {
1236 1237 1238
  if (!empty($breadcrumb)) {
    return '<div class="breadcrumb">'. implode(' » ', $breadcrumb) .'</div>';
  }
1239
}
Dries's avatar
Dries committed
1240

1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251
/**
 * Return a themed help message.
 *
 * @return a string containing the helptext for the current page.
 */
function theme_help() {
  if ($help = menu_get_active_help()) {
    return '<div class="help">'. $help .'</div>';
  }
}

Dries's avatar
Dries committed
1252
/**
1253
 * Return a themed submenu, typically displayed under the tabs.
Dries's avatar
Dries committed
1254
 *
1255 1256
 * @param $links
 *   An array of links.
Dries's avatar
Dries committed
1257 1258
 */
function theme_submenu($links) {
1259
  return '<div class="submenu">'. implode(' | ', $links) .'</div>';
Dries's avatar
Dries committed
1260 1261
}

1262
/**
1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273
 * Return a themed table.
 *
 * @param $header
 *   An array containing the table headers. Each element of the array can be
 *   either a localized string or an associative array with the following keys:
 *   - "data"