| Line # | Frequency | Source Line | | 1 | | <?php |
| 2 | | // $Id: theme.inc,v 1.419 2008/04/02 20:23:14 dries Exp $ |
| 3 | | |
| 4 | | /** |
| 5 | | * @file |
| 6 | | * The theme system, which controls the output of Drupal. |
| 7 | | * |
| 8 | | * The theme system allows for nearly all output of the Drupal system to be |
| 9 | | * customized by user themes. |
| 10 | | * |
| 11 | | * @see <a href="http://drupal.org/node/253">Theme system</a> |
| 12 | | * @see themeable |
| 13 | | */ |
| 14 | | |
| 15 | | /** |
| 16 | | * @name Content markers |
| 17 | | * @{ |
| 18 | | * Markers used by theme_mark() and node_mark() to designate content. |
| 19 | | * @see theme_mark(), node_mark() |
| 20 | | */ |
| 21 | | define('MARK_READ', 0); |
| 22 | | define('MARK_NEW', 1); |
| 23 | | define('MARK_UPDATED', 2); |
| 24 | | /** |
| 25 | | * @} End of "Content markers". |
| 26 | | */ |
| 27 | | |
| 28 | | /** |
| 29 | | * Initialize the theme system by loading the theme. |
| 30 | | */ |
| 31 | | function init_theme() { |
| 32 | 1 | global $theme, $user, $custom_theme, $theme_key; |
| 33 | | |
| 34 | | // If $theme is already set, assume the others are set, too, and do nothing |
| 35 | 1 | if (isset($theme)) { |
| 36 | | return; |
| 37 | | } |
| 38 | | |
| 39 | 1 | drupal_bootstrap(DRUPAL_BOOTSTRAP_DATABASE); |
| 40 | 1 | $themes = list_themes(); |
| 41 | | |
| 42 | | // Only select the user selected theme if it is available in the |
| 43 | | // list of enabled themes. |
| 44 | 1 | $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 | 1 | $theme = $custom_theme && $themes[$custom_theme] ? $custom_theme : $theme; |
| 49 | | |
| 50 | | // Store the identifier for retrieving theme settings with. |
| 51 | 1 | $theme_key = $theme; |
| 52 | | |
| 53 | | // Find all our ancestor themes and put them in an array. |
| 54 | | $base_theme = array(); |
| 55 | 1 | $ancestor = $theme; |
| 56 | 1 | while ($ancestor && isset($themes[$ancestor]->base_theme)) { |
| 57 | | $base_theme[] = $new_base_theme = $themes[$themes[$ancestor]->base_theme]; |
| 58 | | $ancestor = $themes[$ancestor]->base_theme; |
| 59 | | } |
| 60 | 1 | _init_theme($themes[$theme], array_reverse($base_theme)); |
| 61 | | } |
| 62 | | |
| 63 | | /** |
| 64 | | * Initialize the theme system given already loaded information. This |
| 65 | | * function is useful to initialize a theme when no database is present. |
| 66 | | * |
| 67 | | * @param $theme |
| 68 | | * An object with the following information: |
| 69 | | * filename |
| 70 | | * The .info file for this theme. The 'path' to |
| 71 | | * the theme will be in this file's directory. (Required) |
| 72 | | * owner |
| 73 | | * The path to the .theme file or the .engine file to load for |
| 74 | | * the theme. (Required) |
| 75 | | * stylesheet |
| 76 | | * The primary stylesheet for the theme. (Optional) |
| 77 | | * engine |
| 78 | | * The name of theme engine to use. (Optional) |
| 79 | | * @param $base_theme |
| 80 | | * An optional array of objects that represent the 'base theme' if the |
| 81 | | * theme is meant to be derivative of another theme. It requires |
| 82 | | * the same information as the $theme object. It should be in |
| 83 | | * 'oldest first' order, meaning the top level of the chain will |
| 84 | | * be first. |
| 85 | | * @param $registry_callback |
| 86 | | * The callback to invoke to set the theme registry. |
| 87 | | */ |
| 88 | | function _init_theme($theme, $base_theme = array(), $registry_callback = '_theme_load_registry') { |
| 89 | 1 | global $theme_info, $base_theme_info, $theme_engine, $theme_path; |
| 90 | 1 | $theme_info = $theme; |
| 91 | 1 | $base_theme_info = $base_theme; |
| 92 | | |
| 93 | 1 | $theme_path = dirname($theme->filename); |
| 94 | | |
| 95 | | // Prepare stylesheets from this theme as well as all ancestor themes. |
| 96 | | // We work it this way so that we can have child themes override parent |
| 97 | | // theme stylesheets easily. |
| 98 | | $final_stylesheets = array(); |
| 99 | | |
| 100 | | // Grab stylesheets from base theme |
| 101 | 1 | foreach ($base_theme as $base) { |
| 102 | | if (!empty($base->stylesheets)) { |
| 103 | | foreach ($base->stylesheets as $media => $stylesheets) { |
| 104 | | foreach ($stylesheets as $name => $stylesheet) { |
| 105 | | $final_stylesheets[$media][$name] = $stylesheet; |
| 106 | | } |
| 107 | | } |
| 108 | | } |
| 109 | | } |
| 110 | | |
| 111 | | // Add stylesheets used by this theme. |
| 112 | 1 | if (!empty($theme->stylesheets)) { |
| 113 | 1 | foreach ($theme->stylesheets as $media => $stylesheets) { |
| 114 | 1 | foreach ($stylesheets as $name => $stylesheet) { |
| 115 | 1 | $final_stylesheets[$media][$name] = $stylesheet; |
| 116 | | } |
| 117 | | } |
| 118 | | } |
| 119 | | |
| 120 | | // And now add the stylesheets properly |
| 121 | 1 | foreach ($final_stylesheets as $media => $stylesheets) { |
| 122 | 1 | foreach ($stylesheets as $stylesheet) { |
| 123 | 1 | drupal_add_css($stylesheet, 'theme', $media); |
| 124 | | } |
| 125 | | } |
| 126 | | |
| 127 | | // Do basically the same as the above for scripts |
| 128 | | $final_scripts = array(); |
| 129 | | |
| 130 | | // Grab scripts from base theme |
| 131 | 1 | foreach ($base_theme as $base) { |
| 132 | | if (!empty($base->scripts)) { |
| 133 | | foreach ($base->scripts as $name => $script) { |
| 134 | | $final_scripts[$name] = $script; |
| 135 | | } |
| 136 | | } |
| 137 | | } |
| 138 | | |
| 139 | | // Add scripts used by this theme. |
| 140 | 1 | if (!empty($theme->scripts)) { |
| 141 | | foreach ($theme->scripts as $name => $script) { |
| 142 | | $final_scripts[$name] = $script; |
| 143 | | } |
| 144 | | } |
| 145 | | |
| 146 | | // Add scripts used by this theme. |
| 147 | 1 | foreach ($final_scripts as $script) { |
| 148 | | drupal_add_js($script, 'theme'); |
| 149 | | } |
| 150 | | |
| 151 | 1 | $theme_engine = NULL; |
| 152 | | |
| 153 | | // Initialize the theme. |
| 154 | 1 | if (isset($theme->engine)) { |
| 155 | | // Include the engine. |
| 156 | | include_once './'. $theme->owner; |
| 157 | | |
| 158 | 1 | $theme_engine = $theme->engine; |
| 159 | 1 | if (function_exists($theme_engine .'_init')) { |
| 160 | 1 | foreach ($base_theme as $base) { |
| 161 | | call_user_func($theme_engine .'_init', $base); |
| 162 | | } |
| 163 | 1 | call_user_func($theme_engine .'_init', $theme); |
| 164 | | } |
| 165 | | } |
| 166 | | else { |
| 167 | | // include non-engine theme files |
| 168 | | foreach ($base_theme as $base) { |
| 169 | | // Include the theme file or the engine. |
| 170 | | if (!empty($base->owner)) { |
| 171 | | include_once './'. $base->owner; |
| 172 | | } |
| 173 | | } |
| 174 | | // and our theme gets one too. |
| 175 | | if (!empty($theme->owner)) { |
| 176 | | include_once './'. $theme->owner; |
| 177 | | } |
| 178 | | } |
| 179 | | |
| 180 | 1 | $registry_callback($theme, $base_theme, $theme_engine); |
| 181 | | } |
| 182 | | |
| 183 | | /** |
| 184 | | * Retrieve the stored theme registry. If the theme registry is already |
| 185 | | * in memory it will be returned; otherwise it will attempt to load the |
| 186 | | * registry from cache. If this fails, it will construct the registry and |
| 187 | | * cache it. |
| 188 | | */ |
| 189 | | function theme_get_registry($registry = NULL) { |
| 190 | 1 | static $theme_registry = NULL; |
| 191 | 1 | if (isset($registry)) { |
| 192 | 1 | $theme_registry = $registry; |
| 193 | | } |
| 194 | | |
| 195 | 1 | return $theme_registry; |
| 196 | | } |
| 197 | | |
| 198 | | /** |
| 199 | | * Store the theme registry in memory. |
| 200 | | */ |
| 201 | | function _theme_set_registry($registry) { |
| 202 | | // Pass through for setting of static variable. |
| 203 | 1 | return theme_get_registry($registry); |
| 204 | | } |
| 205 | | |
| 206 | | /** |
| 207 | | * Get the theme_registry cache from the database; if it doesn't exist, build |
| 208 | | * it. |
| 209 | | * |
| 210 | | * @param $theme |
| 211 | | * The loaded $theme object. |
| 212 | | * @param $base_theme |
| 213 | | * An array of loaded $theme objects representing the ancestor themes in |
| 214 | | * oldest first order. |
| 215 | | * @param theme_engine |
| 216 | | * The name of the theme engine. |
| 217 | | */ |
| 218 | | function _theme_load_registry($theme, $base_theme = NULL, $theme_engine = NULL) { |
| 219 | | // Check the theme registry cache; if it exists, use it. |
| 220 | 1 | $cache = cache_get("theme_registry:$theme->name", 'cache'); |
| 221 | 1 | if (isset($cache->data)) { |
| 222 | 1 | $registry = $cache->data; |
| 223 | | } |
| 224 | | else { |
| 225 | | // If not, build one and cache it. |
| 226 | | $registry = _theme_build_registry($theme, $base_theme, $theme_engine); |
| 227 | | _theme_save_registry($theme, $registry); |
| 228 | | } |
| 229 | 1 | _theme_set_registry($registry); |
| 230 | | } |
| 231 | | |
| 232 | | /** |
| 233 | | * Write the theme_registry cache into the database. |
| 234 | | */ |
| 235 | | function _theme_save_registry($theme, $registry) { |
| 236 | | cache_set("theme_registry:$theme->name", $registry); |
| 237 | | } |
| 238 | | |
| 239 | | /** |
| 240 | | * Force the system to rebuild the theme registry; this should be called |
| 241 | | * when modules are added to the system, or when a dynamic system needs |
| 242 | | * to add more theme hooks. |
| 243 | | */ |
| 244 | | function drupal_rebuild_theme_registry() { |
| 245 | | cache_clear_all('theme_registry', 'cache', TRUE); |
| 246 | | } |
| 247 | | |
| 248 | | /** |
| 249 | | * Process a single invocation of the theme hook. $type will be one |
| 250 | | * of 'module', 'theme_engine' or 'theme' and it tells us some |
| 251 | | * important information. |
| 252 | | * |
| 253 | | * Because $cache is a reference, the cache will be continually |
| 254 | | * expanded upon; new entries will replace old entries in the |
| 255 | | * array_merge, but we are careful to ensure some data is carried |
| 256 | | * forward, such as the arguments a theme hook needs. |
| 257 | | * |
| 258 | | * An override flag can be set for preprocess functions. When detected the |
| 259 | | * cached preprocessors for the hook will not be merged with the newly set. |
| 260 | | * This can be useful to themes and theme engines by giving them more control |
| 261 | | * over how and when the preprocess functions are run. |
| 262 | | */ |
| 263 | | function _theme_process_registry(&$cache, $name, $type, $theme, $path) { |
| 264 | | $function = $name .'_theme'; |
| 265 | | if (function_exists($function)) { |
| 266 | | $result = $function($cache, $type, $theme, $path); |
| 267 | | |
| 268 | | foreach ($result as $hook => $info) { |
| 269 | | $result[$hook]['type'] = $type; |
| 270 | | $result[$hook]['theme path'] = $path; |
| 271 | | // if function and file are left out, default to standard naming |
| 272 | | // conventions. |
| 273 | | if (!isset($info['template']) && !isset($info['function'])) { |
| 274 | | $result[$hook]['function'] = ($type == 'module' ? 'theme_' : $name .'_') . $hook; |
| 275 | | } |
| 276 | | // If a path is set in the info, use what was set. Otherwise use the |
| 277 | | // default path. This is mostly so system.module can declare theme |
| 278 | | // functions on behalf of core .include files. |
| 279 | | // All files are included to be safe. Conditionally included |
| 280 | | // files can prevent them from getting registered. |
| 281 | | if (isset($info['file']) && !isset($info['path'])) { |
| 282 | | $result[$hook]['file'] = $path .'/'. $info['file']; |
| 283 | | include_once($result[$hook]['file']); |
| 284 | | } |
| 285 | | elseif (isset($info['file']) && isset($info['path'])) { |
| 286 | | include_once($info['path'] .'/'. $info['file']); |
| 287 | | } |
| 288 | | |
| 289 | | if (isset($info['template']) && !isset($info['path'])) { |
| 290 | | $result[$hook]['template'] = $path .'/'. $info['template']; |
| 291 | | } |
| 292 | | // If 'arguments' have been defined previously, carry them forward. |
| 293 | | // This should happen if a theme overrides a Drupal defined theme |
| 294 | | // function, for example. |
| 295 | | if (!isset($info['arguments']) && isset($cache[$hook])) { |
| 296 | | $result[$hook]['arguments'] = $cache[$hook]['arguments']; |
| 297 | | } |
| 298 | | // Likewise with theme paths. These are used for template naming suggestions. |
| 299 | | // Theme implementations can occur in multiple paths. Suggestions should follow. |
| 300 | | if (!isset($info['theme paths']) && isset($cache[$hook])) { |
| 301 | | $result[$hook]['theme paths'] = $cache[$hook]['theme paths']; |
| 302 | | } |
| 303 | | // Check for sub-directories. |
| 304 | | $result[$hook]['theme paths'][] = isset($info['path']) ? $info['path'] : $path; |
| 305 | | |
| 306 | | // Check for default _preprocess_ functions. Ensure arrayness. |
| 307 | | if (!isset($info['preprocess functions']) || !is_array($info['preprocess functions'])) { |
| 308 | | $info['preprocess functions'] = array(); |
| 309 | | $prefixes = array(); |
| 310 | | if ($type == 'module') { |
| 311 | | // Default preprocessor prefix. |
| 312 | | $prefixes[] = 'template'; |
| 313 | | // Add all modules so they can intervene with their own preprocessors. This allows them |
| 314 | | // to provide preprocess functions even if they are not the owner of the current hook. |
| 315 | | $prefixes += module_list(); |
| 316 | | } |
| 317 | | elseif ($type == 'theme_engine') { |
| 318 | | // Theme engines get an extra set that come before the normally named preprocessors. |
| 319 | | $prefixes[] = $name .'_engine'; |
| 320 | | // The theme engine also registers on behalf of the theme. The theme or engine name can be used. |
| 321 | | $prefixes[] = $name; |
| 322 | | $prefixes[] = $theme; |
| 323 | | } |
| 324 | | else { |
| 325 | | // This applies when the theme manually registers their own preprocessors. |
| 326 | | $prefixes[] = $name; |
| 327 | | } |
| 328 | | |
| 329 | | foreach ($prefixes as $prefix) { |
| 330 | | if (function_exists($prefix .'_preprocess')) { |
| 331 | | $info['preprocess functions'][] = $prefix .'_preprocess'; |
| 332 | | } |
| 333 | | if (function_exists($prefix .'_preprocess_'. $hook)) { |
| 334 | | $info['preprocess functions'][] = $prefix .'_preprocess_'. $hook; |
| 335 | | } |
| 336 | | } |
| 337 | | } |
| 338 | | // Check for the override flag and prevent the cached preprocess functions from being used. |
| 339 | | // This allows themes or theme engines to remove preprocessors set earlier in the registry build. |
| 340 | | if (!empty($info['override preprocess functions'])) { |
| 341 | | // Flag not needed inside the registry. |
| 342 | | unset($result[$hook]['override preprocess functions']); |
| 343 | | } |
| 344 | | elseif (isset($cache[$hook]['preprocess functions']) && is_array($cache[$hook]['preprocess functions'])) { |
| 345 | | $info['preprocess functions'] = array_merge($cache[$hook]['preprocess functions'], $info['preprocess functions']); |
| 346 | | } |
| 347 | | $result[$hook]['preprocess functions'] = $info['preprocess functions']; |
| 348 | | } |
| 349 | | |
| 350 | | // Merge the newly created theme hooks into the existing cache. |
| 351 | | $cache = array_merge($cache, $result); |
| 352 | | } |
| 353 | | } |
| 354 | | |
| 355 | | /** |
| 356 | | * Rebuild the hook theme_registry cache. |
| 357 | | * |
| 358 | | * @param $theme |
| 359 | | * The loaded $theme object. |
| 360 | | * @param $base_theme |
| 361 | | * An array of loaded $theme objects representing the ancestor themes in |
| 362 | | * oldest first order. |
| 363 | | * @param theme_engine |
| 364 | | * The name of the theme engine. |
| 365 | | */ |
| 366 | | function _theme_build_registry($theme, $base_theme, $theme_engine) { |
| 367 | | $cache = array(); |
| 368 | | // First, process the theme hooks advertised by modules. This will |
| 369 | | // serve as the basic registry. |
| 370 | | foreach (module_implements('theme') as $module) { |
| 371 | | _theme_process_registry($cache, $module, 'module', $module, drupal_get_path('module', $module)); |
| 372 | | } |
| 373 | | |
| 374 | | // Process each base theme. |
| 375 | | foreach ($base_theme as $base) { |
| 376 | | // If the theme uses a theme engine, process its hooks. |
| 377 | | $base_path = dirname($base->filename); |
| 378 | | if ($theme_engine) { |
| 379 | | _theme_process_registry($cache, $theme_engine, 'base_theme_engine', $base->name, $base_path); |
| 380 | | } |
| 381 | | _theme_process_registry($cache, $base->name, 'base_theme', $base->name, $base_path); |
| 382 | | } |
| 383 | | |
| 384 | | // And then the same thing, but for the theme. |
| 385 | | if ($theme_engine) { |
| 386 | | _theme_process_registry($cache, $theme_engine, 'theme_engine', $theme->name, dirname($theme->filename)); |
| 387 | | } |
| 388 | | |
| 389 | | // Finally, hooks provided by the theme itself. |
| 390 | | _theme_process_registry($cache, $theme->name, 'theme', $theme->name, dirname($theme->filename)); |
| 391 | | |
| 392 | | // Let modules alter the registry |
| 393 | | drupal_alter('theme_registry', $cache); |
| 394 | | return $cache; |
| 395 | | } |
| 396 | | |
| 397 | | /** |
| 398 | | * Provides a list of currently available themes. |
| 399 | | * |
| 400 | | * If the database is active then it will be retrieved from the database. |
| 401 | | * Otherwise it will retrieve a new list. |
| 402 | | * |
| 403 | | * @param $refresh |
| 404 | | * Whether to reload the list of themes from the database. |
| 405 | | * @return |
| 406 | | * An array of the currently available themes. |
| 407 | | */ |
| 408 | | function list_themes($refresh = FALSE) { |
| 409 | | static $list = array(); |
| 410 | | |
| 411 | 1 | if ($refresh) { |
| 412 | | $list = array(); |
| 413 | | } |
| 414 | | |
| 415 | 1 | if (empty($list)) { |
| 416 | | $list = array(); |
| 417 | | $themes = array(); |
| 418 | | // Extract from the database only when it is available. |
| 419 | | // Also check that the site is not in the middle of an install or update. |
| 420 | 1 | if (db_is_active() && !defined('MAINTENANCE_MODE')) { |
| 421 | 1 | $result = db_query("SELECT * FROM {system} WHERE type = '%s'", 'theme'); |
| 422 | 1 | while ($theme = db_fetch_object($result)) { |
| 423 | 1 | if (file_exists($theme->filename)) { |
| 424 | 1 | $theme->info = unserialize($theme->info); |
| 425 | 1 | $themes[] = $theme; |
| 426 | | } |
| 427 | | } |
| 428 | | } |
| 429 | | else { |
| 430 | | // Scan the installation when the database should not be read. |
| 431 | | $themes = _system_theme_data(); |
| 432 | | } |
| 433 | | |
| 434 | 1 | foreach ($themes as $theme) { |
| 435 | 1 | foreach ($theme->info['stylesheets'] as $media => $stylesheets) { |
| 436 | 1 | foreach ($stylesheets as $stylesheet => $path) { |
| 437 | 1 | if (file_exists($path)) { |
| 438 | 1 | $theme->stylesheets[$media][$stylesheet] = $path; |
| 439 | | } |
| 440 | | } |
| 441 | | } |
| 442 | 1 | foreach ($theme->info['scripts'] as $script => $path) { |
| 443 | 1 | if (file_exists($path)) { |
| 444 | | $theme->scripts[$script] = $path; |
| 445 | | } |
| 446 | | } |
| 447 | 1 | if (isset($theme->info['engine'])) { |
| 448 | 1 | $theme->engine = $theme->info['engine']; |
| 449 | | } |
| 450 | 1 | if (isset($theme->info['base theme'])) { |
| 451 | 1 | $theme->base_theme = $theme->info['base theme']; |
| 452 | | } |
| 453 | | // Status is normally retrieved from the database. Add zero values when |
| 454 | | // read from the installation directory to prevent notices. |
| 455 | 1 | if (!isset($theme->status)) { |
| 456 | | $theme->status = 0; |
| 457 | | } |
| 458 | 1 | $list[$theme->name] = $theme; |
| 459 | | } |
| 460 | | } |
| 461 | | |
| 462 | 1 | return $list; |
| 463 | | } |
| 464 | | |
| 465 | | /** |
| 466 | | * Generate the themed output. |
| 467 | | * |
| 468 | | * All requests for theme hooks must go through this function. It examines |
| 469 | | * the request and routes it to the appropriate theme function. The theme |
| 470 | | * registry is checked to determine which implementation to use, which may |
| 471 | | * be a function or a template. |
| 472 | | * |
| 473 | | * If the implementation is a function, it is executed and its return value |
| 474 | | * passed along. |
| 475 | | * |
| 476 | | * If the implementation is a template, the arguments are converted to a |
| 477 | | * $variables array. This array is then modified by the module implementing |
| 478 | | * the hook, theme engine (if applicable) and the theme. The following |
| 479 | | * functions may be used to modify the $variables array. They are processed in |
| 480 | | * this order when available: |
| 481 | | * |
| 482 | | * - template_preprocess(&$variables) |
| 483 | | * This sets a default set of variables for all template implementations. |
| 484 | | * |
| 485 | | * - template_preprocess_HOOK(&$variables) |
| 486 | | * This is the first preprocessor called specific to the hook; it should be |
| 487 | | * implemented by the module that registers it. |
| 488 | | * |
| 489 | | * - MODULE_preprocess(&$variables) |
| 490 | | * This will be called for all templates; it should only be used if there |
| 491 | | * is a real need. It's purpose is similar to template_preprocess(). |
| 492 | | * |
| 493 | | * - MODULE_preprocess_HOOK(&$variables) |
| 494 | | * This is for modules that want to alter or provide extra variables for |
| 495 | | * theming hooks not registered to itself. For example, if a module named |
| 496 | | * "foo" wanted to alter the $submitted variable for the hook "node" a |
| 497 | | * preprocess function of foo_preprocess_node() can be created to intercept |
| 498 | | * and alter the variable. |
| 499 | | * |
| 500 | | * - ENGINE_engine_preprocess(&$variables) |
| 501 | | * This function should only be implemented by theme engines and exists |
| 502 | | * so that it can set necessary variables for all hooks. |
| 503 | | * |
| 504 | | |