| Drupal | PHP Cross Reference | Content Management Systems |
1 <?php 2 3 /** 4 * @file 5 * Common functions that many Drupal modules will need to reference. 6 * 7 * The functions that are critical and need to be available even when serving 8 * a cached page are instead located in bootstrap.inc. 9 */ 10 11 /** 12 * @defgroup php_wrappers PHP wrapper functions 13 * @{ 14 * Functions that are wrappers or custom implementations of PHP functions. 15 * 16 * Certain PHP functions should not be used in Drupal. Instead, Drupal's 17 * replacement functions should be used. 18 * 19 * For example, for improved or more secure UTF8-handling, or RFC-compliant 20 * handling of URLs in Drupal. 21 * 22 * For ease of use and memorizing, all these wrapper functions use the same name 23 * as the original PHP function, but prefixed with "drupal_". Beware, however, 24 * that not all wrapper functions support the same arguments as the original 25 * functions. 26 * 27 * You should always use these wrapper functions in your code. 28 * 29 * Wrong: 30 * @code 31 * $my_substring = substr($original_string, 0, 5); 32 * @endcode 33 * 34 * Correct: 35 * @code 36 * $my_substring = drupal_substr($original_string, 0, 5); 37 * @endcode 38 * 39 * @} 40 */ 41 42 /** 43 * Return status for saving which involved creating a new item. 44 */ 45 define('SAVED_NEW', 1); 46 47 /** 48 * Return status for saving which involved an update to an existing item. 49 */ 50 define('SAVED_UPDATED', 2); 51 52 /** 53 * Return status for saving which deleted an existing item. 54 */ 55 define('SAVED_DELETED', 3); 56 57 /** 58 * The default group for system CSS files added to the page. 59 */ 60 define('CSS_SYSTEM', -100); 61 62 /** 63 * The default group for module CSS files added to the page. 64 */ 65 define('CSS_DEFAULT', 0); 66 67 /** 68 * The default group for theme CSS files added to the page. 69 */ 70 define('CSS_THEME', 100); 71 72 /** 73 * The default group for JavaScript and jQuery libraries added to the page. 74 */ 75 define('JS_LIBRARY', -100); 76 77 /** 78 * The default group for module JavaScript code added to the page. 79 */ 80 define('JS_DEFAULT', 0); 81 82 /** 83 * The default group for theme JavaScript code added to the page. 84 */ 85 define('JS_THEME', 100); 86 87 /** 88 * Error code indicating that the request exceeded the specified timeout. 89 * 90 * @see drupal_http_request() 91 */ 92 define('HTTP_REQUEST_TIMEOUT', -1); 93 94 /** 95 * @defgroup block_caching Block Caching 96 * @{ 97 * Constants that define each block's caching state. 98 * 99 * Modules specify how their blocks can be cached in their hook_block_info() 100 * implementations. Caching can be turned off (DRUPAL_NO_CACHE), managed by the 101 * module declaring the block (DRUPAL_CACHE_CUSTOM), or managed by the core 102 * Block module. If the Block module is managing the cache, you can specify that 103 * the block is the same for every page and user (DRUPAL_CACHE_GLOBAL), or that 104 * it can change depending on the page (DRUPAL_CACHE_PER_PAGE) or by user 105 * (DRUPAL_CACHE_PER_ROLE or DRUPAL_CACHE_PER_USER). Page and user settings can 106 * be combined with a bitwise-binary or operator; for example, 107 * DRUPAL_CACHE_PER_ROLE | DRUPAL_CACHE_PER_PAGE means that the block can change 108 * depending on the user role or page it is on. 109 * 110 * The block cache is cleared in cache_clear_all(), and uses the same clearing 111 * policy than page cache (node, comment, user, taxonomy added or updated...). 112 * Blocks requiring more fine-grained clearing might consider disabling the 113 * built-in block cache (DRUPAL_NO_CACHE) and roll their own. 114 * 115 * Note that user 1 is excluded from block caching. 116 */ 117 118 /** 119 * The block should not get cached. 120 * 121 * This setting should be used: 122 * - For simple blocks (notably those that do not perform any db query), where 123 * querying the db cache would be more expensive than directly generating the 124 * content. 125 * - For blocks that change too frequently. 126 */ 127 define('DRUPAL_NO_CACHE', -1); 128 129 /** 130 * The block is handling its own caching in its hook_block_view(). 131 * 132 * This setting is useful when time based expiration is needed or a site uses a 133 * node access which invalidates standard block cache. 134 */ 135 define('DRUPAL_CACHE_CUSTOM', -2); 136 137 /** 138 * The block or element can change depending on the user's roles. 139 * 140 * This is the default setting for blocks, used when the block does not specify 141 * anything. 142 */ 143 define('DRUPAL_CACHE_PER_ROLE', 0x0001); 144 145 /** 146 * The block or element can change depending on the user. 147 * 148 * This setting can be resource-consuming for sites with large number of users, 149 * and thus should only be used when DRUPAL_CACHE_PER_ROLE is not sufficient. 150 */ 151 define('DRUPAL_CACHE_PER_USER', 0x0002); 152 153 /** 154 * The block or element can change depending on the page being viewed. 155 */ 156 define('DRUPAL_CACHE_PER_PAGE', 0x0004); 157 158 /** 159 * The block or element is the same for every user and page that it is visible. 160 */ 161 define('DRUPAL_CACHE_GLOBAL', 0x0008); 162 163 /** 164 * @} End of "defgroup block_caching". 165 */ 166 167 /** 168 * Adds content to a specified region. 169 * 170 * @param $region 171 * Page region the content is added to. 172 * @param $data 173 * Content to be added. 174 */ 175 function drupal_add_region_content($region = NULL, $data = NULL) { 176 static $content = array(); 177 178 if (isset($region) && isset($data)) { 179 $content[$region][] = $data; 180 } 181 return $content; 182 } 183 184 /** 185 * Gets assigned content for a given region. 186 * 187 * @param $region 188 * A specified region to fetch content for. If NULL, all regions will be 189 * returned. 190 * @param $delimiter 191 * Content to be inserted between imploded array elements. 192 */ 193 function drupal_get_region_content($region = NULL, $delimiter = ' ') { 194 $content = drupal_add_region_content(); 195 if (isset($region)) { 196 if (isset($content[$region]) && is_array($content[$region])) { 197 return implode($delimiter, $content[$region]); 198 } 199 } 200 else { 201 foreach (array_keys($content) as $region) { 202 if (is_array($content[$region])) { 203 $content[$region] = implode($delimiter, $content[$region]); 204 } 205 } 206 return $content; 207 } 208 } 209 210 /** 211 * Gets the name of the currently active installation profile. 212 * 213 * When this function is called during Drupal's initial installation process, 214 * the name of the profile that's about to be installed is stored in the global 215 * installation state. At all other times, the standard Drupal systems variable 216 * table contains the name of the current profile, and we can call 217 * variable_get() to determine what one is active. 218 * 219 * @return $profile 220 * The name of the installation profile. 221 */ 222 function drupal_get_profile() { 223 global $install_state; 224 225 if (isset($install_state['parameters']['profile'])) { 226 $profile = $install_state['parameters']['profile']; 227 } 228 else { 229 $profile = variable_get('install_profile', 'standard'); 230 } 231 232 return $profile; 233 } 234 235 236 /** 237 * Sets the breadcrumb trail for the current page. 238 * 239 * @param $breadcrumb 240 * Array of links, starting with "home" and proceeding up to but not including 241 * the current page. 242 */ 243 function drupal_set_breadcrumb($breadcrumb = NULL) { 244 $stored_breadcrumb = &drupal_static(__FUNCTION__); 245 246 if (isset($breadcrumb)) { 247 $stored_breadcrumb = $breadcrumb; 248 } 249 return $stored_breadcrumb; 250 } 251 252 /** 253 * Gets the breadcrumb trail for the current page. 254 */ 255 function drupal_get_breadcrumb() { 256 $breadcrumb = drupal_set_breadcrumb(); 257 258 if (!isset($breadcrumb)) { 259 $breadcrumb = menu_get_active_breadcrumb(); 260 } 261 262 return $breadcrumb; 263 } 264 265 /** 266 * Returns a string containing RDF namespace declarations for use in XML and 267 * XHTML output. 268 */ 269 function drupal_get_rdf_namespaces() { 270 $xml_rdf_namespaces = array(); 271 272 // Serializes the RDF namespaces in XML namespace syntax. 273 if (function_exists('rdf_get_namespaces')) { 274 foreach (rdf_get_namespaces() as $prefix => $uri) { 275 $xml_rdf_namespaces[] = 'xmlns:' . $prefix . '="' . $uri . '"'; 276 } 277 } 278 return count($xml_rdf_namespaces) ? "\n " . implode("\n ", $xml_rdf_namespaces) : ''; 279 } 280 281 /** 282 * Adds output to the HEAD tag of the HTML page. 283 * 284 * This function can be called as long the headers aren't sent. Pass no 285 * arguments (or NULL for both) to retrieve the currently stored elements. 286 * 287 * @param $data 288 * A renderable array. If the '#type' key is not set then 'html_tag' will be 289 * added as the default '#type'. 290 * @param $key 291 * A unique string key to allow implementations of hook_html_head_alter() to 292 * identify the element in $data. Required if $data is not NULL. 293 * 294 * @return 295 * An array of all stored HEAD elements. 296 * 297 * @see theme_html_tag() 298 */ 299 function drupal_add_html_head($data = NULL, $key = NULL) { 300 $stored_head = &drupal_static(__FUNCTION__); 301 302 if (!isset($stored_head)) { 303 // Make sure the defaults, including Content-Type, come first. 304 $stored_head = _drupal_default_html_head(); 305 } 306 307 if (isset($data) && isset($key)) { 308 if (!isset($data['#type'])) { 309 $data['#type'] = 'html_tag'; 310 } 311 $stored_head[$key] = $data; 312 } 313 return $stored_head; 314 } 315 316 /** 317 * Returns elements that are always displayed in the HEAD tag of the HTML page. 318 */ 319 function _drupal_default_html_head() { 320 // Add default elements. Make sure the Content-Type comes first because the 321 // IE browser may be vulnerable to XSS via encoding attacks from any content 322 // that comes before this META tag, such as a TITLE tag. 323 $elements['system_meta_content_type'] = array( 324 '#type' => 'html_tag', 325 '#tag' => 'meta', 326 '#attributes' => array( 327 'http-equiv' => 'Content-Type', 328 'content' => 'text/html; charset=utf-8', 329 ), 330 // Security: This always has to be output first. 331 '#weight' => -1000, 332 ); 333 // Show Drupal and the major version number in the META GENERATOR tag. 334 // Get the major version. 335 list($version, ) = explode('.', VERSION); 336 $elements['system_meta_generator'] = array( 337 '#type' => 'html_tag', 338 '#tag' => 'meta', 339 '#attributes' => array( 340 'name' => 'Generator', 341 'content' => 'Drupal ' . $version . ' (http://drupal.org)', 342 ), 343 ); 344 // Also send the generator in the HTTP header. 345 $elements['system_meta_generator']['#attached']['drupal_add_http_header'][] = array('X-Generator', $elements['system_meta_generator']['#attributes']['content']); 346 return $elements; 347 } 348 349 /** 350 * Retrieves output to be displayed in the HEAD tag of the HTML page. 351 */ 352 function drupal_get_html_head() { 353 $elements = drupal_add_html_head(); 354 drupal_alter('html_head', $elements); 355 return drupal_render($elements); 356 } 357 358 /** 359 * Adds a feed URL for the current page. 360 * 361 * This function can be called as long the HTML header hasn't been sent. 362 * 363 * @param $url 364 * An internal system path or a fully qualified external URL of the feed. 365 * @param $title 366 * The title of the feed. 367 */ 368 function drupal_add_feed($url = NULL, $title = '') { 369 $stored_feed_links = &drupal_static(__FUNCTION__, array()); 370 371 if (isset($url)) { 372 $stored_feed_links[$url] = theme('feed_icon', array('url' => $url, 'title' => $title)); 373 374 drupal_add_html_head_link(array( 375 'rel' => 'alternate', 376 'type' => 'application/rss+xml', 377 'title' => $title, 378 // Force the URL to be absolute, for consistency with other <link> tags 379 // output by Drupal. 380 'href' => url($url, array('absolute' => TRUE)), 381 )); 382 } 383 return $stored_feed_links; 384 } 385 386 /** 387 * Gets the feed URLs for the current page. 388 * 389 * @param $delimiter 390 * A delimiter to split feeds by. 391 */ 392 function drupal_get_feeds($delimiter = "\n") { 393 $feeds = drupal_add_feed(); 394 return implode($feeds, $delimiter); 395 } 396 397 /** 398 * @defgroup http_handling HTTP handling 399 * @{ 400 * Functions to properly handle HTTP responses. 401 */ 402 403 /** 404 * Processes a URL query parameter array to remove unwanted elements. 405 * 406 * @param $query 407 * (optional) An array to be processed. Defaults to $_GET. 408 * @param $exclude 409 * (optional) A list of $query array keys to remove. Use "parent[child]" to 410 * exclude nested items. Defaults to array('q'). 411 * @param $parent 412 * Internal use only. Used to build the $query array key for nested items. 413 * 414 * @return 415 * An array containing query parameters, which can be used for url(). 416 */ 417 function drupal_get_query_parameters(array $query = NULL, array $exclude = array('q'), $parent = '') { 418 // Set defaults, if none given. 419 if (!isset($query)) { 420 $query = $_GET; 421 } 422 // If $exclude is empty, there is nothing to filter. 423 if (empty($exclude)) { 424 return $query; 425 } 426 elseif (!$parent) { 427 $exclude = array_flip($exclude); 428 } 429 430 $params = array(); 431 foreach ($query as $key => $value) { 432 $string_key = ($parent ? $parent . '[' . $key . ']' : $key); 433 if (isset($exclude[$string_key])) { 434 continue; 435 } 436 437 if (is_array($value)) { 438 $params[$key] = drupal_get_query_parameters($value, $exclude, $string_key); 439 } 440 else { 441 $params[$key] = $value; 442 } 443 } 444 445 return $params; 446 } 447 448 /** 449 * Splits a URL-encoded query string into an array. 450 * 451 * @param $query 452 * The query string to split. 453 * 454 * @return 455 * An array of URL decoded couples $param_name => $value. 456 */ 457 function drupal_get_query_array($query) { 458 $result = array(); 459 if (!empty($query)) { 460 foreach (explode('&', $query) as $param) { 461 $param = explode('=', $param); 462 $result[$param[0]] = isset($param[1]) ? rawurldecode($param[1]) : ''; 463 } 464 } 465 return $result; 466 } 467 468 /** 469 * Parses an array into a valid, rawurlencoded query string. 470 * 471 * This differs from http_build_query() as we need to rawurlencode() (instead of 472 * urlencode()) all query parameters. 473 * 474 * @param $query 475 * The query parameter array to be processed, e.g. $_GET. 476 * @param $parent 477 * Internal use only. Used to build the $query array key for nested items. 478 * 479 * @return 480 * A rawurlencoded string which can be used as or appended to the URL query 481 * string. 482 * 483 * @see drupal_get_query_parameters() 484 * @ingroup php_wrappers 485 */ 486 function drupal_http_build_query(array $query, $parent = '') { 487 $params = array(); 488 489 foreach ($query as $key => $value) { 490 $key = ($parent ? $parent . '[' . rawurlencode($key) . ']' : rawurlencode($key)); 491 492 // Recurse into children. 493 if (is_array($value)) { 494 $params[] = drupal_http_build_query($value, $key); 495 } 496 // If a query parameter value is NULL, only append its key. 497 elseif (!isset($value)) { 498 $params[] = $key; 499 } 500 else { 501 // For better readability of paths in query strings, we decode slashes. 502 $params[] = $key . '=' . str_replace('%2F', '/', rawurlencode($value)); 503 } 504 } 505 506 return implode('&', $params); 507 } 508 509 /** 510 * Prepares a 'destination' URL query parameter for use with drupal_goto(). 511 * 512 * Used to direct the user back to the referring page after completing a form. 513 * By default the current URL is returned. If a destination exists in the 514 * previous request, that destination is returned. As such, a destination can 515 * persist across multiple pages. 516 * 517 * @return 518 * An associative array containing the key: 519 * - destination: The path provided via the destination query string or, if 520 * not available, the current path. 521 * 522 * @see current_path() 523 * @see drupal_goto() 524 */ 525 function drupal_get_destination() { 526 $destination = &drupal_static(__FUNCTION__); 527 528 if (isset($destination)) { 529 return $destination; 530 } 531 532 if (isset($_GET['destination'])) { 533 $destination = array('destination' => $_GET['destination']); 534 } 535 else { 536 $path = $_GET['q']; 537 $query = drupal_http_build_query(drupal_get_query_parameters()); 538 if ($query != '') { 539 $path .= '?' . $query; 540 } 541 $destination = array('destination' => $path); 542 } 543 return $destination; 544 } 545 546 /** 547 * Parses a system URL string into an associative array suitable for url(). 548 * 549 * This function should only be used for URLs that have been generated by the 550 * system, such as via url(). It should not be used for URLs that come from 551 * external sources, or URLs that link to external resources. 552 * 553 * The returned array contains a 'path' that may be passed separately to url(). 554 * For example: 555 * @code 556 * $options = drupal_parse_url($_GET['destination']); 557 * $my_url = url($options['path'], $options); 558 * $my_link = l('Example link', $options['path'], $options); 559 * @endcode 560 * 561 * This is required, because url() does not support relative URLs containing a 562 * query string or fragment in its $path argument. Instead, any query string 563 * needs to be parsed into an associative query parameter array in 564 * $options['query'] and the fragment into $options['fragment']. 565 * 566 * @param $url 567 * The URL string to parse, f.e. $_GET['destination']. 568 * 569 * @return 570 * An associative array containing the keys: 571 * - 'path': The path of the URL. If the given $url is external, this includes 572 * the scheme and host. 573 * - 'query': An array of query parameters of $url, if existent. 574 * - 'fragment': The fragment of $url, if existent. 575 * 576 * @see url() 577 * @see drupal_goto() 578 * @ingroup php_wrappers 579 */ 580 function drupal_parse_url($url) { 581 $options = array( 582 'path' => NULL, 583 'query' => array(), 584 'fragment' => '', 585 ); 586 587 // External URLs: not using parse_url() here, so we do not have to rebuild 588 // the scheme, host, and path without having any use for it. 589 if (strpos($url, '://') !== FALSE) { 590 // Split off everything before the query string into 'path'. 591 $parts = explode('?', $url); 592 $options['path'] = $parts[0]; 593 // If there is a query string, transform it into keyed query parameters. 594 if (isset($parts[1])) { 595 $query_parts = explode('#', $parts[1]); 596 parse_str($query_parts[0], $options['query']); 597 // Take over the fragment, if there is any. 598 if (isset($query_parts[1])) { 599 $options['fragment'] = $query_parts[1]; 600 } 601 } 602 } 603 // Internal URLs. 604 else { 605 // parse_url() does not support relative URLs, so make it absolute. E.g. the 606 // relative URL "foo/bar:1" isn't properly parsed. 607 $parts = parse_url('http://example.com/' . $url); 608 // Strip the leading slash that was just added. 609 $options['path'] = substr($parts['path'], 1); 610 if (isset($parts['query'])) { 611 parse_str($parts['query'], $options['query']); 612 } 613 if (isset($parts['fragment'])) { 614 $options['fragment'] = $parts['fragment']; 615 } 616 } 617 // The 'q' parameter contains the path of the current page if clean URLs are 618 // disabled. It overrides the 'path' of the URL when present, even if clean 619 // URLs are enabled, due to how Apache rewriting rules work. 620 if (isset($options['query']['q'])) { 621 $options['path'] = $options['query']['q']; 622 unset($options['query']['q']); 623 } 624 625 return $options; 626 } 627 628 /** 629 * Encodes a Drupal path for use in a URL. 630 * 631 * For aesthetic reasons slashes are not escaped. 632 * 633 * Note that url() takes care of calling this function, so a path passed to that 634 * function should not be encoded in advance. 635 * 636 * @param $path 637 * The Drupal path to encode. 638 */ 639 function drupal_encode_path($path) { 640 return str_replace('%2F', '/', rawurlencode($path)); 641 } 642 643 /** 644 * Sends the user to a different Drupal page. 645 * 646 * This issues an on-site HTTP redirect. The function makes sure the redirected 647 * URL is formatted correctly. 648 * 649 * Usually the redirected URL is constructed from this function's input 650 * parameters. However you may override that behavior by setting a 651 * destination in either the $_REQUEST-array (i.e. by using 652 * the query string of an URI) This is used to direct the user back to 653 * the proper page after completing a form. For example, after editing 654 * a post on the 'admin/content'-page or after having logged on using the 655 * 'user login'-block in a sidebar. The function drupal_get_destination() 656 * can be used to help set the destination URL. 657 * 658 * Drupal will ensure that messages set by drupal_set_message() and other 659 * session data are written to the database before the user is redirected. 660 * 661 * This function ends the request; use it instead of a return in your menu 662 * callback. 663 * 664 * @param $path 665 * (optional) A Drupal path or a full URL, which will be passed to url() to 666 * compute the redirect for the URL. 667 * @param $options 668 * (optional) An associative array of additional URL options to pass to url(). 669 * @param $http_response_code 670 * (optional) The HTTP status code to use for the redirection, defaults to 671 * 302. The valid values for 3xx redirection status codes are defined in 672 * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3 RFC 2616 @endlink 673 * and the 674 * @link http://tools.ietf.org/html/draft-reschke-http-status-308-07 draft for the new HTTP status codes: @endlink 675 * - 301: Moved Permanently (the recommended value for most redirects). 676 * - 302: Found (default in Drupal and PHP, sometimes used for spamming search 677 * engines). 678 * - 303: See Other. 679 * - 304: Not Modified. 680 * - 305: Use Proxy. 681 * - 307: Temporary Redirect. 682 * 683 * @see drupal_get_destination() 684 * @see url() 685 */ 686 function drupal_goto($path = '', array $options = array(), $http_response_code = 302) { 687 // A destination in $_GET always overrides the function arguments. 688 // We do not allow absolute URLs to be passed via $_GET, as this can be an attack vector. 689 if (isset($_GET['destination']) && !url_is_external($_GET['destination'])) { 690 $destination = drupal_parse_url($_GET['destination']); 691 $path = $destination['path']; 692 $options['query'] = $destination['query']; 693 $options['fragment'] = $destination['fragment']; 694 } 695 696 drupal_alter('drupal_goto', $path, $options, $http_response_code); 697 698 // The 'Location' HTTP header must be absolute. 699 $options['absolute'] = TRUE; 700 701 $url = url($path, $options); 702 703 header('Location: ' . $url, TRUE, $http_response_code); 704 705 // The "Location" header sends a redirect status code to the HTTP daemon. In 706 // some cases this can be wrong, so we make sure none of the code below the 707 // drupal_goto() call gets executed upon redirection. 708 drupal_exit($url); 709 } 710 711 /** 712 * Delivers a "site is under maintenance" message to the browser. 713 * 714 * Page callback functions wanting to report a "site offline" message should 715 * return MENU_SITE_OFFLINE instead of calling drupal_site_offline(). However, 716 * functions that are invoked in contexts where that return value might not 717 * bubble up to menu_execute_active_handler() should call drupal_site_offline(). 718 */ 719 function drupal_site_offline() { 720 drupal_deliver_page(MENU_SITE_OFFLINE); 721 } 722 723 /** 724 * Delivers a "page not found" error to the browser. 725 * 726 * Page callback functions wanting to report a "page not found" message should 727 * return MENU_NOT_FOUND instead of calling drupal_not_found(). However, 728 * functions that are invoked in contexts where that return value might not 729 * bubble up to menu_execute_active_handler() should call drupal_not_found(). 730 */ 731 function drupal_not_found() { 732 drupal_deliver_page(MENU_NOT_FOUND); 733 } 734 735 /** 736 * Delivers an "access denied" error to the browser. 737 * 738 * Page callback functions wanting to report an "access denied" message should 739 * return MENU_ACCESS_DENIED instead of calling drupal_access_denied(). However, 740 * functions that are invoked in contexts where that return value might not 741 * bubble up to menu_execute_active_handler() should call 742 * drupal_access_denied(). 743 */ 744 function drupal_access_denied() { 745 drupal_deliver_page(MENU_ACCESS_DENIED); 746 } 747 748 /** 749 * Performs an HTTP request. 750 * 751 * This is a flexible and powerful HTTP client implementation. Correctly 752 * handles GET, POST, PUT or any other HTTP requests. Handles redirects. 753 * 754 * @param $url 755 * A string containing a fully qualified URI. 756 * @param array $options 757 * (optional) An array that can have one or more of the following elements: 758 * - headers: An array containing request headers to send as name/value pairs. 759 * - method: A string containing the request method. Defaults to 'GET'. 760 * - data: A string containing the request body, formatted as 761 * 'param=value¶m=value&...'. Defaults to NULL. 762 * - max_redirects: An integer representing how many times a redirect 763 * may be followed. Defaults to 3. 764 * - timeout: A float representing the maximum number of seconds the function 765 * call may take. The default is 30 seconds. If a timeout occurs, the error 766 * code is set to the HTTP_REQUEST_TIMEOUT constant. 767 * - context: A context resource created with stream_context_create(). 768 * 769 * @return object 770 * An object that can have one or more of the following components: 771 * - request: A string containing the request body that was sent. 772 * - code: An integer containing the response status code, or the error code 773 * if an error occurred. 774 * - protocol: The response protocol (e.g. HTTP/1.1 or HTTP/1.0). 775 * - status_message: The status message from the response, if a response was 776 * received. 777 * - redirect_code: If redirected, an integer containing the initial response 778 * status code. 779 * - redirect_url: If redirected, a string containing the URL of the redirect 780 * target. 781 * - error: If an error occurred, the error message. Otherwise not set. 782 * - headers: An array containing the response headers as name/value pairs. 783 * HTTP header names are case-insensitive (RFC 2616, section 4.2), so for 784 * easy access the array keys are returned in lower case. 785 * - data: A string containing the response body that was received. 786 */ 787 function drupal_http_request($url, array $options = array()) { 788 $result = new stdClass(); 789 790 // Parse the URL and make sure we can handle the schema. 791 $uri = @parse_url($url); 792 793 if ($uri == FALSE) { 794 $result->error = 'unable to parse URL'; 795 $result->code = -1001; 796 return $result; 797 } 798 799 if (!isset($uri['scheme'])) { 800 $result->error = 'missing schema'; 801 $result->code = -1002; 802 return $result; 803 } 804 805 timer_start(__FUNCTION__); 806 807 // Merge the default options. 808 $options += array( 809 'headers' => array(), 810 'method' => 'GET', 811 'data' => NULL, 812 'max_redirects' => 3, 813 'timeout' => 30.0, 814 'context' => NULL, 815 ); 816 817 // Merge the default headers. 818 $options['headers'] += array( 819 'User-Agent' => 'Drupal (+http://drupal.org/)', 820 ); 821 822 // stream_socket_client() requires timeout to be a float. 823 $options['timeout'] = (float) $options['timeout']; 824 825 // Use a proxy if one is defined and the host is not on the excluded list. 826 $proxy_server = variable_get('proxy_server', ''); 827 if ($proxy_server && _drupal_http_use_proxy($uri['host'])) { 828 // Set the scheme so we open a socket to the proxy server. 829 $uri['scheme'] = 'proxy'; 830 // Set the path to be the full URL. 831 $uri['path'] = $url; 832 // Since the URL is passed as the path, we won't use the parsed query. 833 unset($uri['query']); 834 835 // Add in username and password to Proxy-Authorization header if needed. 836 if ($proxy_username = variable_get('proxy_username', '')) { 837 $proxy_password = variable_get('proxy_password', ''); 838 $options['headers']['Proxy-Authorization'] = 'Basic ' . base64_encode($proxy_username . (!empty($proxy_password) ? ":" . $proxy_password : '')); 839 } 840 // Some proxies reject requests with any User-Agent headers, while others 841 // require a specific one. 842 $proxy_user_agent = variable_get('proxy_user_agent', ''); 843 // The default value matches neither condition. 844 if ($proxy_user_agent === NULL) { 845 unset($options['headers']['User-Agent']); 846 } 847 elseif ($proxy_user_agent) { 848 $options['headers']['User-Agent'] = $proxy_user_agent; 849 } 850 } 851 852 switch ($uri['scheme']) { 853 case 'proxy': 854 // Make the socket connection to a proxy server. 855 $socket = 'tcp://' . $proxy_server . ':' . variable_get('proxy_port', 8080); 856 // The Host header still needs to match the real request. 857 $options['headers']['Host'] = $uri['host']; 858 $options['headers']['Host'] .= isset($uri['port']) && $uri['port'] != 80 ? ':' . $uri['port'] : ''; 859 break; 860 861 case 'http': 862 case 'feed': 863 $port = isset($uri['port']) ? $uri['port'] : 80; 864 $socket = 'tcp://' . $uri['host'] . ':' . $port; 865 // RFC 2616: "non-standard ports MUST, default ports MAY be included". 866 // We don't add the standard port to prevent from breaking rewrite rules 867 // checking the host that do not take into account the port number. 868 $options['headers']['Host'] = $uri['host'] . ($port != 80 ? ':' . $port : ''); 869 break; 870 871 case 'https': 872 // Note: Only works when PHP is compiled with OpenSSL support. 873 $port = isset($uri['port']) ? $uri['port'] : 443; 874 $socket = 'ssl://' . $uri['host'] . ':' . $port; 875 $options['headers']['Host'] = $uri['host'] . ($port != 443 ? ':' . $port : ''); 876 break; 877 878 default: 879 $result->error = 'invalid schema ' . $uri['scheme']; 880 $result->code = -1003; 881 return $result; 882 } 883 884 if (empty($options['context'])) { 885 $fp = @stream_socket_client($socket, $errno, $errstr, $options['timeout']); 886 } 887 else { 888 // Create a stream with context. Allows verification of a SSL certificate. 889 $fp = @stream_socket_client($socket, $errno, $errstr, $options['timeout'], STREAM_CLIENT_CONNECT, $options['context']); 890 } 891 892 // Make sure the socket opened properly. 893 if (!$fp) { 894 // When a network error occurs, we use a negative number so it does not 895 // clash with the HTTP status codes. 896 $result->code = -$errno; 897 $result->error = trim($errstr) ? trim($errstr) : t('Error opening socket @socket', array('@socket' => $socket)); 898 899 // Mark that this request failed. This will trigger a check of the web 900 // server's ability to make outgoing HTTP requests the next time that 901 // requirements checking is performed. 902 // See system_requirements(). 903 variable_set('drupal_http_request_fails', TRUE); 904 905 return $result; 906 } 907 908 // Construct the path to act on. 909 $path = isset($uri['path']) ? $uri['path'] : '/'; 910 if (isset($uri['query'])) { 911 $path .= '?' . $uri['query']; 912 } 913 914 // Only add Content-Length if we actually have any content or if it is a POST 915 // or PUT request. Some non-standard servers get confused by Content-Length in 916 // at least HEAD/GET requests, and Squid always requires Content-Length in 917 // POST/PUT requests. 918 $content_length = strlen($options['data']); 919 if ($content_length > 0 || $options['method'] == 'POST' || $options['method'] == 'PUT') { 920 $options['headers']['Content-Length'] = $content_length; 921 } 922 923 // If the server URL has a user then attempt to use basic authentication. 924 if (isset($uri['user'])) { 925 $options['headers']['Authorization'] = 'Basic ' . base64_encode($uri['user'] . (isset($uri['pass']) ? ':' . $uri['pass'] : '')); 926 } 927 928 // If the database prefix is being used by SimpleTest to run the tests in a copied 929 // database then set the user-agent header to the database prefix so that any 930 // calls to other Drupal pages will run the SimpleTest prefixed database. The 931 // user-agent is used to ensure that multiple testing sessions running at the 932 // same time won't interfere with each other as they would if the database 933 // prefix were stored statically in a file or database variable. 934 $test_info = &$GLOBALS['drupal_test_info']; 935 if (!empty($test_info['test_run_id'])) { 936 $options['headers']['User-Agent'] = drupal_generate_test_ua($test_info['test_run_id']); 937 } 938 939 $request = $options['method'] . ' ' . $path . " HTTP/1.0\r\n"; 940 foreach ($options['headers'] as $name => $value) { 941 $request .= $name . ': ' . trim($value) . "\r\n"; 942 } 943 $request .= "\r\n" . $options['data']; 944 $result->request = $request; 945 // Calculate how much time is left of the original timeout value. 946 $timeout = $options['timeout'] - timer_read(__FUNCTION__) / 1000; 947 if ($timeout > 0) { 948 stream_set_timeout($fp, floor($timeout), floor(1000000 * fmod($timeout, 1))); 949 fwrite($fp, $request); 950 } 951 952 // Fetch response. Due to PHP bugs like http://bugs.php.net/bug.php?id=43782 953 // and http://bugs.php.net/bug.php?id=46049 we can't rely on feof(), but 954 // instead must invoke stream_get_meta_data() each iteration. 955 $info = stream_get_meta_data($fp); 956 $alive = !$info['eof'] && !$info['timed_out']; 957 $response = ''; 958 959 while ($alive) { 960 // Calculate how much time is left of the original timeout value. 961 $timeout = $options['timeout'] - timer_read(__FUNCTION__) / 1000; 962 if ($timeout <= 0) { 963 $info['timed_out'] = TRUE; 964 break; 965 } 966 stream_set_timeout($fp, floor($timeout), floor(1000000 * fmod($timeout, 1))); 967 $chunk = fread($fp, 1024); 968 $response .= $chunk; 969 $info = stream_get_meta_data($fp); 970 $alive = !$info['eof'] && !$info['timed_out'] && $chunk; 971 } 972 fclose($fp); 973 974 if ($info['timed_out']) { 975 $result->code = HTTP_REQUEST_TIMEOUT; 976 $result->error = 'request timed out'; 977 return $result; 978 } 979 // Parse response headers from the response body. 980 // Be tolerant of malformed HTTP responses that separate header and body with 981 // \n\n or \r\r instead of \r\n\r\n. 982 list($response, $result->data) = preg_split("/\r\n\r\n|\n\n|\r\r/", $response, 2); 983 $response = preg_split("/\r\n|\n|\r/", $response); 984 985 // Parse the response status line. 986 list($protocol, $code, $status_message) = explode(' ', trim(array_shift($response)), 3); 987 $result->protocol = $protocol; 988 $result->status_message = $status_message; 989 990 $result->headers = array(); 991 992 // Parse the response headers. 993 while ($line = trim(array_shift($response))) { 994 list($name, $value) = explode(':', $line, 2); 995 $name = strtolower($name); 996 if (isset($result->headers[$name]) && $name == 'set-cookie') { 997 // RFC 2109: the Set-Cookie response header comprises the token Set- 998 // Cookie:, followed by a comma-separated list of one or more cookies. 999 $result->headers[$name] .= ',' . trim($value); 1000 } 1001 else { 1002 $result->headers[$name] = trim($value); 1003 } 1004 } 1005 1006 $responses = array( 1007 100 => 'Continue', 1008 101 => 'Switching Protocols', 1009 200 => 'OK', 1010 201 => 'Created', 1011 202 => 'Accepted', 1012 203 => 'Non-Authoritative Information', 1013 204 => 'No Content', 1014 205 => 'Reset Content', 1015 206 => 'Partial Content', 1016 300 => 'Multiple Choices', 1017 301 => 'Moved Permanently', 1018 302 => 'Found', 1019 303 => 'See Other', 1020 304 => 'Not Modified', 1021 305 => 'Use Proxy', 1022 307 => 'Temporary Redirect', 1023 400 => 'Bad Request', 1024 401 => 'Unauthorized', 1025 402 => 'Payment Required', 1026 403 => 'Forbidden', 1027 404 => 'Not Found', 1028 405 => 'Method Not Allowed', 1029 406 => 'Not Acceptable', 1030 407 => 'Proxy Authentication Required', 1031 408 => 'Request Time-out', 1032 409 => 'Conflict', 1033 410 => 'Gone', 1034 411 => 'Length Required', 1035 412 => 'Precondition Failed', 1036 413 => 'Request Entity Too Large', 1037 414 => 'Request-URI Too Large', 1038 415 => 'Unsupported Media Type', 1039 416 => 'Requested range not satisfiable', 1040 417 => 'Expectation Failed', 1041 500 => 'Internal Server Error', 1042 501 => 'Not Implemented', 1043 502 => 'Bad Gateway', 1044 503 => 'Service Unavailable', 1045 504 => 'Gateway Time-out', 1046 505 => 'HTTP Version not supported', 1047 ); 1048 // RFC 2616 states that all unknown HTTP codes must be treated the same as the 1049 // base code in their class. 1050 if (!isset($responses[$code])) { 1051 $code = floor($code / 100) * 100; 1052 } 1053 $result->code = $code; 1054 1055 switch ($code) { 1056 case 200: // OK 1057 case 304: // Not modified 1058 break; 1059 case 301: // Moved permanently 1060 case 302: // Moved temporarily 1061 case 307: // Moved temporarily 1062 $location = $result->headers['location']; 1063 $options['timeout'] -= timer_read(__FUNCTION__) / 1000; 1064 if ($options['timeout'] <= 0) { 1065 $result->code = HTTP_REQUEST_TIMEOUT; 1066 $result->error = 'request timed out'; 1067 } 1068 elseif ($options['max_redirects']) { 1069 // Redirect to the new location. 1070 $options['max_redirects']--; 1071 $result = drupal_http_request($location, $options); 1072 $result->redirect_code = $code; 1073 } 1074 if (!isset($result->redirect_url)) { 1075 $result->redirect_url = $location; 1076 } 1077 break; 1078 default: 1079 $result->error = $status_message; 1080 } 1081 1082 return $result; 1083 } 1084 1085 /** 1086 * Helper function for determining hosts excluded from needing a proxy. 1087 * 1088 * @return 1089 * TRUE if a proxy should be used for this host. 1090 */ 1091 function _drupal_http_use_proxy($host) { 1092 $proxy_exceptions = variable_get('proxy_exceptions', array('localhost', '127.0.0.1')); 1093 return !in_array(strtolower($host), $proxy_exceptions, TRUE); 1094 } 1095 1096 /** 1097 * @} End of "HTTP handling". 1098 */ 1099 1100 /** 1101 * Strips slashes from a string or array of strings. 1102 * 1103 * Callback for array_walk() within fix_gpx_magic(). 1104 * 1105 * @param $item 1106 * An individual string or array of strings from superglobals. 1107 */ 1108 function _fix_gpc_magic(&$item) { 1109 if (is_array($item)) { 1110 array_walk($item, '_fix_gpc_magic'); 1111 } 1112 else { 1113 $item = stripslashes($item); 1114 } 1115 } 1116 1117 /** 1118 * Strips slashes from $_FILES items. 1119 * 1120 * Callback for array_walk() within fix_gpc_magic(). 1121 * 1122 * The tmp_name key is skipped keys since PHP generates single backslashes for 1123 * file paths on Windows systems. 1124 * 1125 * @param $item 1126 * An item from $_FILES. 1127 * @param $key 1128 * The key for the item within $_FILES. 1129 * 1130 * @see http://php.net/manual/en/features.file-upload.php#42280 1131 */ 1132 function _fix_gpc_magic_files(&$item, $key) { 1133 if ($key != 'tmp_name') { 1134 if (is_array($item)) { 1135 array_walk($item, '_fix_gpc_magic_files'); 1136 } 1137 else { 1138 $item = stripslashes($item); 1139 } 1140 } 1141 } 1142 1143 /** 1144 * Fixes double-escaping caused by "magic quotes" in some PHP installations. 1145 * 1146 * @see _fix_gpc_magic() 1147 * @see _fix_gpc_magic_files() 1148 */ 1149 function fix_gpc_magic() { 1150 static $fixed = FALSE; 1151 if (!$fixed && ini_get('magic_quotes_gpc')) { 1152 array_walk($_GET, '_fix_gpc_magic'); 1153 array_walk($_POST, '_fix_gpc_magic'); 1154 array_walk($_COOKIE, '_fix_gpc_magic'); 1155 array_walk($_REQUEST, '_fix_gpc_magic'); 1156 array_walk($_FILES, '_fix_gpc_magic_files'); 1157 } 1158 $fixed = TRUE; 1159 } 1160 1161 /** 1162 * @defgroup validation Input validation 1163 * @{ 1164 * Functions to validate user input. 1165 */ 1166 1167 /** 1168 * Verifies the syntax of the given e-mail address. 1169 * 1170 * See @link http://tools.ietf.org/html/rfc5321 RFC 5321 @endlink for details. 1171 * 1172 * @param $mail 1173 * A string containing an e-mail address. 1174 * 1175 * @return 1176 * TRUE if the address is in a valid format. 1177 */ 1178 function valid_email_address($mail) { 1179 return (bool)filter_var($mail, FILTER_VALIDATE_EMAIL); 1180 } 1181 1182 /** 1183 * Verifies the syntax of the given URL. 1184 * 1185 * This function should only be used on actual URLs. It should not be used for 1186 * Drupal menu paths, which can contain arbitrary characters. 1187 * Valid values per RFC 3986. 1188 * @param $url 1189 * The URL to verify. 1190 * @param $absolute 1191 * Whether the URL is absolute (beginning with a scheme such as "http:"). 1192 * 1193 * @return 1194 * TRUE if the URL is in a valid format. 1195 */ 1196 function valid_url($url, $absolute = FALSE) { 1197 if ($absolute) { 1198 return (bool)preg_match(" 1199 /^ # Start at the beginning of the text 1200 (?:ftp|https?|feed):\/\/ # Look for ftp, http, https or feed schemes 1201 (?: # Userinfo (optional) which is typically 1202 (?:(?:[\w\.\-\+!$&'\(\)*\+,;=]|%[0-9a-f]{2})+:)* # a username or a username and password 1203 (?:[\w\.\-\+%!$&'\(\)*\+,;=]|%[0-9a-f]{2})+@ # combination 1204 )? 1205 (?: 1206 (?:[a-z0-9\-\.]|%[0-9a-f]{2})+ # A domain name or a IPv4 address 1207 |(?:\[(?:[0-9a-f]{0,4}:)*(?:[0-9a-f]{0,4})\]) # or a well formed IPv6 address 1208 ) 1209 (?::[0-9]+)? # Server port number (optional) 1210 (?:[\/|\?] 1211 (?:[\w#!:\.\?\+=&@$'~*,;\/\(\)\[\]\-]|%[0-9a-f]{2}) # The path and query (optional) 1212 *)? 1213 $/xi", $url); 1214 } 1215 else { 1216 return (bool)preg_match("/^(?:[\w#!:\.\?\+=&@$'~*,;\/\(\)\[\]\-]|%[0-9a-f]{2})+$/i", $url); 1217 } 1218 } 1219 1220 /** 1221 * @} End of "defgroup validation". 1222 */ 1223 1224 /** 1225 * Registers an event for the current visitor to the flood control mechanism. 1226 * 1227 * @param $name 1228 * The name of an event. 1229 * @param $window 1230 * Optional number of seconds before this event expires. Defaults to 3600 (1 1231 * hour). Typically uses the same value as the flood_is_allowed() $window 1232 * parameter. Expired events are purged on cron run to prevent the flood table 1233 * from growing indefinitely. 1234 * @param $identifier 1235 * Optional identifier (defaults to the current user's IP address). 1236 */ 1237 function flood_register_event($name, $window = 3600, $identifier = NULL) { 1238 if (!isset($identifier)) { 1239 $identifier = ip_address(); 1240 } 1241 db_insert('flood') 1242 ->fields(array( 1243 'event' => $name, 1244 'identifier' => $identifier, 1245 'timestamp' => REQUEST_TIME, 1246 'expiration' => REQUEST_TIME + $window, 1247 )) 1248 ->execute(); 1249 } 1250 1251 /** 1252 * Makes the flood control mechanism forget an event for the current visitor. 1253 * 1254 * @param $name 1255 * The name of an event. 1256 * @param $identifier 1257 * Optional identifier (defaults to the current user's IP address). 1258 */ 1259 function flood_clear_event($name, $identifier = NULL) { 1260 if (!isset($identifier)) { 1261 $identifier = ip_address(); 1262 } 1263 db_delete('flood') 1264 ->condition('event', $name) 1265 ->condition('identifier', $identifier) 1266 ->execute(); 1267 } 1268 1269 /** 1270 * Checks whether a user is allowed to proceed with the specified event. 1271 * 1272 * Events can have thresholds saying that each user can only do that event 1273 * a certain number of times in a time window. This function verifies that the 1274 * current user has not exceeded this threshold. 1275 * 1276 * @param $name 1277 * The unique name of the event. 1278 * @param $threshold 1279 * The maximum number of times each user can do this event per time window. 1280 * @param $window 1281 * Number of seconds in the time window for this event (default is 3600 1282 * seconds, or 1 hour). 1283 * @param $identifier 1284 * Unique identifier of the current user. Defaults to their IP address. 1285 * 1286 * @return 1287 * TRUE if the user is allowed to proceed. FALSE if they have exceeded the 1288 * threshold and should not be allowed to proceed. 1289 */ 1290 function flood_is_allowed($name, $threshold, $window = 3600, $identifier = NULL) { 1291 if (!isset($identifier)) { 1292 $identifier = ip_address(); 1293 } 1294 $number = db_query("SELECT COUNT(*) FROM {flood} WHERE event = :event AND identifier = :identifier AND timestamp > :timestamp", array( 1295 ':event' => $name, 1296 ':identifier' => $identifier, 1297 ':timestamp' => REQUEST_TIME - $window)) 1298 ->fetchField(); 1299 return ($number < $threshold); 1300 } 1301 1302 /** 1303 * @defgroup sanitization Sanitization functions 1304 * @{ 1305 * Functions to sanitize values. 1306 * 1307 * See http://drupal.org/writing-secure-code for information 1308 * on writing secure code. 1309 */ 1310 1311 /** 1312 * Strips dangerous protocols (e.g. 'javascript:') from a URI. 1313 * 1314 * This function must be called for all URIs within user-entered input prior 1315 * to being output to an HTML attribute value. It is often called as part of 1316 * check_url() or filter_xss(), but those functions return an HTML-encoded 1317 * string, so this function can be called independently when the output needs to 1318 * be a plain-text string for passing to t(), l(), drupal_attributes(), or 1319 * another function that will call check_plain() separately. 1320 * 1321 * @param $uri 1322 * A plain-text URI that might contain dangerous protocols. 1323 * 1324 * @return 1325 * A plain-text URI stripped of dangerous protocols. As with all plain-text 1326 * strings, this return value must not be output to an HTML page without 1327 * check_plain() being called on it. However, it can be passed to functions 1328 * expecting plain-text strings. 1329 * 1330 * @see check_url() 1331 */ 1332 function drupal_strip_dangerous_protocols($uri) { 1333 static $allowed_protocols; 1334 1335 if (!isset($allowed_protocols)) { 1336 $allowed_protocols = array_flip(variable_get('filter_allowed_protocols', array('ftp', 'http', 'https', 'irc', 'mailto', 'news', 'nntp', 'rtsp', 'sftp', 'ssh', 'tel', 'telnet', 'webcal'))); 1337 } 1338 1339 // Iteratively remove any invalid protocol found. 1340 do { 1341 $before = $uri; 1342 $colonpos = strpos($uri, ':'); 1343 if ($colonpos > 0) { 1344 // We found a colon, possibly a protocol. Verify. 1345 $protocol = substr($uri, 0, $colonpos); 1346 // If a colon is preceded by a slash, question mark or hash, it cannot 1347 // possibly be part of the URL scheme. This must be a relative URL, which 1348 // inherits the (safe) protocol of the base document. 1349 if (preg_match('![/?#]!', $protocol)) { 1350 break; 1351 } 1352 // Check if this is a disallowed protocol. Per RFC2616, section 3.2.3 1353 // (URI Comparison) scheme comparison must be case-insensitive. 1354 if (!isset($allowed_protocols[strtolower($protocol)])) { 1355 $uri = substr($uri, $colonpos + 1); 1356 } 1357 } 1358 } while ($before != $uri); 1359 1360 return $uri; 1361 } 1362 1363 /** 1364 * Strips dangerous protocols from a URI and encodes it for output to HTML. 1365 * 1366 * @param $uri 1367 * A plain-text URI that might contain dangerous protocols. 1368 * 1369 * @return 1370 * A URI stripped of dangerous protocols and encoded for output to an HTML 1371 * attribute value. Because it is already encoded, it should not be set as a 1372 * value within a $attributes array passed to drupal_attributes(), because 1373 * drupal_attributes() expects those values to be plain-text strings. To pass 1374 * a filtered URI to drupal_attributes(), call 1375 * drupal_strip_dangerous_protocols() instead. 1376 * 1377 * @see drupal_strip_dangerous_protocols() 1378 */ 1379 function check_url($uri) { 1380 return check_plain(drupal_strip_dangerous_protocols($uri)); 1381 } 1382 1383 /** 1384 * Applies a very permissive XSS/HTML filter for admin-only use. 1385 * 1386 * Use only for fields where it is impractical to use the 1387 * whole filter system, but where some (mainly inline) mark-up 1388 * is desired (so check_plain() is not acceptable). 1389 * 1390 * Allows all tags that can be used inside an HTML body, save 1391 * for scripts and styles. 1392 */ 1393 function filter_xss_admin($string) { 1394 return filter_xss($string, array('a', 'abbr', 'acronym', 'address', 'article', 'aside', 'b', 'bdi', 'bdo', 'big', 'blockquote', 'br', 'caption', 'cite', 'code', 'col', 'colgroup', 'command', 'dd', 'del', 'details', 'dfn', 'div', 'dl', 'dt', 'em', 'figcaption', 'figure', 'footer', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'i', 'img', 'ins', 'kbd', 'li', 'mark', 'menu', 'meter', 'nav', 'ol', 'output', 'p', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'section', 'small', 'span', 'strong', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time', 'tr', 'tt', 'u', 'ul', 'var', 'wbr')); 1395 } 1396 1397 /** 1398 * Filters HTML to prevent cross-site-scripting (XSS) vulnerabilities. 1399 * 1400 * Based on kses by Ulf Harnhammar, see http://sourceforge.net/projects/kses. 1401 * For examples of various XSS attacks, see: http://ha.ckers.org/xss.html. 1402 * 1403 * This code does four things: 1404 * - Removes characters and constructs that can trick browsers. 1405 * - Makes sure all HTML entities are well-formed. 1406 * - Makes sure all HTML tags and attributes are well-formed. 1407 * - Makes sure no HTML tags contain URLs with a disallowed protocol (e.g. 1408 * javascript:). 1409 * 1410 * @param $string 1411 * The string with raw HTML in it. It will be stripped of everything that can 1412 * cause an XSS attack. 1413 * @param $allowed_tags 1414 * An array of allowed tags. 1415 * 1416 * @return 1417 * An XSS safe version of $string, or an empty string if $string is not 1418 * valid UTF-8. 1419 * 1420 * @see drupal_validate_utf8() 1421 * @ingroup sanitization 1422 */ 1423 function filter_xss($string, $allowed_tags = array('a', 'em', 'strong', 'cite', 'blockquote', 'code', 'ul', 'ol', 'li', 'dl', 'dt', 'dd')) { 1424 // Only operate on valid UTF-8 strings. This is necessary to prevent cross 1425 // site scripting issues on Internet Explorer 6. 1426 if (!drupal_validate_utf8($string)) { 1427 return ''; 1428 } 1429 // Store the text format. 1430 _filter_xss_split($allowed_tags, TRUE); 1431 // Remove NULL characters (ignored by some browsers). 1432 $string = str_replace(chr(0), '', $string); 1433 // Remove Netscape 4 JS entities. 1434 $string = preg_replace('%&\s*\{[^}]*(\}\s*;?|$)%', '', $string); 1435 1436 // Defuse all HTML entities. 1437 $string = str_replace('&', '&', $string); 1438 // Change back only well-formed entities in our whitelist: 1439 // Decimal numeric entities. 1440 $string = preg_replace('/&#([0-9]+;)/', '&#\1', $string); 1441 // Hexadecimal numeric entities. 1442 $string = preg_replace('/&#[Xx]0*((?:[0-9A-Fa-f]{2})+;)/', '&#x\1', $string); 1443 // Named entities. 1444 $string = preg_replace('/&([A-Za-z][A-Za-z0-9]*;)/', '&\1', $string); 1445 1446 return preg_replace_callback('% 1447 ( 1448 <(?=[^a-zA-Z!/]) # a lone < 1449 | # or 1450 <!--.*?--> # a comment 1451 | # or 1452 <[^>]*(>|$) # a string that starts with a <, up until the > or the end of the string 1453 | # or 1454 > # just a > 1455 )%x', '_filter_xss_split', $string); 1456 } 1457 1458 /** 1459 * Processes an HTML tag. 1460 * 1461 * @param $m 1462 * An array with various meaning depending on the value of $store. 1463 * If $store is TRUE then the array contains the allowed tags. 1464 * If $store is FALSE then the array has one element, the HTML tag to process. 1465 * @param $store 1466 * Whether to store $m. 1467 * 1468 * @return 1469 * If the element isn't allowed, an empty string. Otherwise, the cleaned up 1470 * version of the HTML element. 1471 */ 1472 function _filter_xss_split($m, $store = FALSE) { 1473 static $allowed_html; 1474 1475 if ($store) { 1476 $allowed_html = array_flip($m); 1477 return; 1478 } 1479 1480 $string = $m[1]; 1481 1482 if (substr($string, 0, 1) != '<') { 1483 // We matched a lone ">" character. 1484 return '>'; 1485 } 1486 elseif (strlen($string) == 1) { 1487 // We matched a lone "<" character. 1488 return '<'; 1489 } 1490 1491 if (!preg_match('%^<\s*(/\s*)?([a-zA-Z0-9]+)([^>]*)>?|(<!--.*?-->)$%', $string, $matches)) { 1492 // Seriously malformed. 1493 return ''; 1494 } 1495 1496 $slash = trim($matches[1]); 1497 $elem = &$matches[2]; 1498 $attrlist = &$matches[3]; 1499 $comment = &$matches[4]; 1500 1501 if ($comment) { 1502 $elem = '!--'; 1503 } 1504 1505 if (!isset($allowed_html[strtolower($elem)])) { 1506 // Disallowed HTML element. 1507 return ''; 1508 } 1509 1510 if ($comment) { 1511 return $comment; 1512 } 1513 1514 if ($slash != '') { 1515 return "</$elem>"; 1516 } 1517 1518 // Is there a closing XHTML slash at the end of the attributes? 1519 $attrlist = preg_replace('%(\s?)/\s*$%', '\1', $attrlist, -1, $count); 1520 $xhtml_slash = $count ? ' /' : ''; 1521 1522 // Clean up attributes. 1523 $attr2 = implode(' ', _filter_xss_attributes($attrlist)); 1524 $attr2 = preg_replace('/[<>]/', '', $attr2); 1525 $attr2 = strlen($attr2) ? ' ' . $attr2 : ''; 1526 1527 return "<$elem$attr2$xhtml_slash>"; 1528 } 1529 1530 /** 1531 * Processes a string of HTML attributes. 1532 * 1533 * @return 1534 * Cleaned up version of the HTML attributes. 1535 */ 1536 function _filter_xss_attributes($attr) { 1537 $attrarr = array(); 1538 $mode = 0; 1539 $attrname = ''; 1540 1541 while (strlen($attr) != 0) { 1542 // Was the last operation successful? 1543 $working = 0; 1544 1545 switch ($mode) { 1546 case 0: 1547 // Attribute name, href for instance. 1548 if (preg_match('/^([-a-zA-Z]+)/', $attr, $match)) { 1549 $attrname = strtolower($match[1]); 1550 $skip = ($attrname == 'style' || substr($attrname, 0, 2) == 'on'); 1551 $working = $mode = 1; 1552 $attr = preg_replace('/^[-a-zA-Z]+/', '', $attr); 1553 } 1554 break; 1555 1556 case 1: 1557 // Equals sign or valueless ("selected"). 1558 if (preg_match('/^\s*=\s*/', $attr)) { 1559 $working = 1; $mode = 2; 1560 $attr = preg_replace('/^\s*=\s*/', '', $attr); 1561 break; 1562 } 1563 1564 if (preg_match('/^\s+/', $attr)) { 1565 $working = 1; $mode = 0; 1566 if (!$skip) { 1567 $attrarr[] = $attrname; 1568 } 1569 $attr = preg_replace('/^\s+/', '', $attr); 1570 } 1571 break; 1572 1573 case 2: 1574 // Attribute value, a URL after href= for instance. 1575 if (preg_match('/^"([^"]*)"(\s+|$)/', $attr, $match)) { 1576 $thisval = filter_xss_bad_protocol($match[1]); 1577 1578 if (!$skip) { 1579 $attrarr[] = "$attrname=\"$thisval\""; 1580 } 1581 $working = 1; 1582 $mode = 0; 1583 $attr = preg_replace('/^"[^"]*"(\s+|$)/', '', $attr); 1584 break; 1585 } 1586 1587 if (preg_match("/^'([^']*)'(\s+|$)/", $attr, $match)) { 1588 $thisval = filter_xss_bad_protocol($match[1]); 1589 1590 if (!$skip) { 1591 $attrarr[] = "$attrname='$thisval'"; 1592 } 1593 $working = 1; $mode = 0; 1594 $attr = preg_replace("/^'[^']*'(\s+|$)/", '', $attr); 1595 break; 1596 } 1597 1598 if (preg_match("%^([^\s\"']+)(\s+|$)%", $attr, $match)) { 1599 $thisval = filter_xss_bad_protocol($match[1]); 1600 1601 if (!$skip) { 1602 $attrarr[] = "$attrname=\"$thisval\""; 1603 } 1604 $working = 1; $mode = 0; 1605 $attr = preg_replace("%^[^\s\"']+(\s+|$)%", '', $attr); 1606 } 1607 break; 1608 } 1609 1610 if ($working == 0) { 1611 // Not well formed; remove and try again. 1612 $attr = preg_replace('/ 1613 ^ 1614 ( 1615 "[^"]*("|$) # - a string that starts with a double quote, up until the next double quote or the end of the string 1616 | # or 1617 \'[^\']*(\'|$)| # - a string that starts with a quote, up until the next quote or the end of the string 1618 | # or 1619 \S # - a non-whitespace character 1620 )* # any number of the above three 1621 \s* # any number of whitespaces 1622 /x', '', $attr); 1623 $mode = 0; 1624 } 1625 } 1626 1627 // The attribute list ends with a valueless attribute like "selected". 1628 if ($mode == 1 && !$skip) { 1629 $attrarr[] = $attrname; 1630 } 1631 return $attrarr; 1632 } 1633 1634 /** 1635 * Processes an HTML attribute value and strips dangerous protocols from URLs. 1636 * 1637 * @param $string 1638 * The string with the attribute value. 1639 * @param $decode 1640 * (deprecated) Whether to decode entities in the $string. Set to FALSE if the 1641 * $string is in plain text, TRUE otherwise. Defaults to TRUE. This parameter 1642 * is deprecated and will be removed in Drupal 8. To process a plain-text URI, 1643 * call drupal_strip_dangerous_protocols() or check_url() instead. 1644 * 1645 * @return 1646 * Cleaned up and HTML-escaped version of $string. 1647 */ 1648 function filter_xss_bad_protocol($string, $decode = TRUE) { 1649 // Get the plain text representation of the attribute value (i.e. its meaning). 1650 // @todo Remove the $decode parameter in Drupal 8, and always assume an HTML 1651 // string that needs decoding. 1652 if ($decode) { 1653 if (!function_exists('decode_entities')) { 1654 require_once DRUPAL_ROOT . '/includes/unicode.inc'; 1655 } 1656 1657 $string = decode_entities($string); 1658 } 1659 return check_plain(drupal_strip_dangerous_protocols($string)); 1660 } 1661 1662 /** 1663 * @} End of "defgroup sanitization". 1664 */ 1665 1666 /** 1667 * @defgroup format Formatting 1668 * @{ 1669 * Functions to format numbers, strings, dates, etc. 1670 */ 1671 1672 /** 1673 * Formats an RSS channel. 1674 * 1675 * Arbitrary elements may be added using the $args associative array. 1676 */ 1677 function format_rss_channel($title, $link, $description, $items, $langcode = NULL, $args = array()) { 1678 global $language_content; 1679 $langcode = $langcode ? $langcode : $language_content->language; 1680 1681 $output = "<channel>\n"; 1682 $output .= ' <title>' . check_plain($title) . "</title>\n"; 1683 $output .= ' <link>' . check_url($link) . "</link>\n"; 1684 1685 // The RSS 2.0 "spec" doesn't indicate HTML can be used in the description. 1686 // We strip all HTML tags, but need to prevent double encoding from properly 1687 // escaped source data (such as & becoming &amp;). 1688 $output .= ' <description>' . check_plain(decode_entities(strip_tags($description))) . "</description>\n"; 1689 $output .= ' <language>' . check_plain($langcode) . "</language>\n"; 1690 $output .= format_xml_elements($args); 1691 $output .= $items; 1692 $output .= "</channel>\n"; 1693 1694 return $output; 1695 } 1696 1697 /** 1698 * Formats a single RSS item. 1699 * 1700 * Arbitrary elements may be added using the $args associative array. 1701 */ 1702 function format_rss_item($title, $link, $description, $args = array()) { 1703 $output = "<item>\n"; 1704 $output .= ' <title>' . check_plain($title) . "</title>\n"; 1705 $output .= ' <link>' . check_url($link) . "</link>\n"; 1706 $output .= ' <description>' . check_plain($description) . "</description>\n"; 1707 $output .= format_xml_elements($args); 1708 $output .= "</item>\n"; 1709 1710 return $output; 1711 } 1712 1713 /** 1714 * Formats XML elements. 1715 * 1716 * @param $array 1717 * An array where each item represents an element and is either a: 1718 * - (key => value) pair (<key>value</key>) 1719 * - Associative array with fields: 1720 * - 'key': element name 1721 * - 'value': element contents 1722 * - 'attributes': associative array of element attributes 1723 * 1724 * In both cases, 'value' can be a simple string, or it can be another array 1725 * with the same format as $array itself for nesting. 1726 */ 1727 function format_xml_elements($array) { 1728 $output = ''; 1729 foreach ($array as $key => $value) { 1730 if (is_numeric($key)) { 1731 if ($value['key']) { 1732 $output .= ' <' . $value['key']; 1733 if (isset($value['attributes']) && is_array($value['attributes'])) { 1734 $output .= drupal_attributes($value['attributes']); 1735 } 1736 1737 if (isset($value['value']) && $value['value'] != '') { 1738 $output .= '>' . (is_array($value['value']) ? format_xml_elements($value['value']) : check_plain($value['value'])) . '</' . $value['key'] . ">\n"; 1739 } 1740 else { 1741 $output .= " />\n"; 1742 } 1743 } 1744 } 1745 else { 1746 $output .= ' <' . $key . '>' . (is_array($value) ? format_xml_elements($value) : check_plain($value)) . "</$key>\n"; 1747 } 1748 } 1749 return $output; 1750 } 1751 1752 /** 1753 * Formats a string containing a count of items. 1754 * 1755 * This function ensures that the string is pluralized correctly. Since t() is 1756 * called by this function, make sure not to pass already-localized strings to 1757 * it. 1758 * 1759 * For example: 1760 * @code 1761 * $output = format_plural($node->comment_count, '1 comment', '@count comments'); 1762 * @endcode 1763 * 1764 * Example with additional replacements: 1765 * @code 1766 * $output = format_plural($update_count, 1767 * 'Changed the content type of 1 post from %old-type to %new-type.', 1768 * 'Changed the content type of @count posts from %old-type to %new-type.', 1769 * array('%old-type' => $info->old_type, '%new-type' => $info->new_type)); 1770 * @endcode 1771 * 1772 * @param $count 1773 * The item count to display. 1774 * @param $singular 1775 * The string for the singular case. Make sure it is clear this is singular, 1776 * to ease translation (e.g. use "1 new comment" instead of "1 new"). Do not 1777 * use @count in the singular string. 1778 * @param $plural 1779 * The string for the plural case. Make sure it is clear this is plural, to 1780 * ease translation. Use @count in place of the item count, as in 1781 * "@count new comments". 1782 * @param $args 1783 * An associative array of replacements to make after translation. Instances 1784 * of any key in this array are replaced with the corresponding value. 1785 * Based on the first character of the key, the value is escaped and/or 1786 * themed. See format_string(). Note that you do not need to include @count 1787 * in this array; this replacement is done automatically for the plural case. 1788 * @param $options 1789 * An associative array of additional options. See t() for allowed keys. 1790 * 1791 * @return 1792 * A translated string. 1793 * 1794 * @see t() 1795 * @see format_string() 1796 */ 1797 function format_plural($count, $singular, $plural, array $args = array(), array $options = array()) { 1798 $args['@count'] = $count; 1799 if ($count == 1) { 1800 return t($singular, $args, $options); 1801 } 1802 1803 // Get the plural index through the gettext formula. 1804 $index = (function_exists('locale_get_plural')) ? locale_get_plural($count, isset($options['langcode']) ? $options['langcode'] : NULL) : -1; 1805 // If the index cannot be computed, use the plural as a fallback (which 1806 // allows for most flexiblity with the replaceable @count value). 1807 if ($index < 0) { 1808 return t($plural, $args, $options); 1809 } 1810 else { 1811 switch ($index) { 1812 case "0": 1813 return t($singular, $args, $options); 1814 case "1": 1815 return t($plural, $args, $options); 1816 default: 1817 unset($args['@count']); 1818 $args['@count[' . $index . ']'] = $count; 1819 return t(strtr($plural, array('@count' => '@count[' . $index . ']')), $args, $options); 1820 } 1821 } 1822 } 1823 1824 /** 1825 * Parses a given byte count. 1826 * 1827 * @param $size 1828 * A size expressed as a number of bytes with optional SI or IEC binary unit 1829 * prefix (e.g. 2, 3K, 5MB, 10G, 6GiB, 8 bytes, 9mbytes). 1830 * 1831 * @return 1832 * An integer representation of the size in bytes. 1833 */ 1834 function parse_size($size) { 1835 $unit = preg_replace('/[^bkmgtpezy]/i', '', $size); // Remove the non-unit characters from the size. 1836 $size = preg_replace('/[^0-9\.]/', '', $size); // Remove the non-numeric characters from the size. 1837 if ($unit) { 1838 // Find the position of the unit in the ordered string which is the power of magnitude to multiply a kilobyte by. 1839 return round($size * pow(DRUPAL_KILOBYTE, stripos('bkmgtpezy', $unit[0]))); 1840 } 1841 else { 1842 return round($size); 1843 } 1844 } 1845 1846 /** 1847 * Generates a string representation for the given byte count. 1848 * 1849 * @param $size 1850 * A size in bytes. 1851 * @param $langcode 1852 * Optional language code to translate to a language other than what is used 1853 * to display the page. 1854 * 1855 * @return 1856 * A translated string representation of the size. 1857 */ 1858 function format_size($size, $langcode = NULL) { 1859 if ($size < DRUPAL_KILOBYTE) { 1860 return format_plural($size, '1 byte', '@count bytes', array(), array('langcode' => $langcode)); 1861 } 1862 else { 1863 $size = $size / DRUPAL_KILOBYTE; // Convert bytes to kilobytes. 1864 $units = array( 1865 t('@size KB', array(), array('langcode' => $langcode)), 1866 t('@size MB', array(), array('langcode' => $langcode)), 1867 t('@size GB', array(), array('langcode' => $langcode)), 1868 t('@size TB', array(), array('langcode' => $langcode)), 1869 t('@size PB', array(), array('langcode' => $langcode)), 1870 t('@size EB', array(), array('langcode' => $langcode)), 1871 t('@size ZB', array(), array('langcode' => $langcode)), 1872 t('@size YB', array(), array('langcode' => $langcode)), 1873 ); 1874 foreach ($units as $unit) { 1875 if (round($size, 2) >= DRUPAL_KILOBYTE) { 1876 $size = $size / DRUPAL_KILOBYTE; 1877 } 1878 else { 1879 break; 1880 } 1881 } 1882 return str_replace('@size', round($size, 2), $unit); 1883 } 1884 } 1885 1886 /** 1887 * Formats a time interval with the requested granularity. 1888 * 1889 * @param $interval 1890 * The length of the interval in seconds. 1891 * @param $granularity 1892 * How many different units to display in the string. 1893 * @param $langcode 1894 * Optional language code to translate to a language other than 1895 * what is used to display the page. 1896 * 1897 * @return 1898 * A translated string representation of the interval. 1899 */ 1900 function format_interval($interval, $granularity = 2, $langcode = NULL) { 1901 $units = array( 1902 '1 year|@count years' => 31536000, 1903 '1 month|@count months' => 2592000, 1904 '1 week|@count weeks' => 604800, 1905 '1 day|@count days' => 86400, 1906 '1 hour|@count hours' => 3600, 1907 '1 min|@count min' => 60, 1908 '1 sec|@count sec' => 1 1909 ); 1910 $output = ''; 1911 foreach ($units as $key => $value) { 1912 $key = explode('|', $key); 1913 if ($interval >= $value) { 1914 $output .= ($output ? ' ' : '') . format_plural(floor($interval / $value), $key[0], $key[1], array(), array('langcode' => $langcode)); 1915 $interval %= $value; 1916 $granularity--; 1917 } 1918 1919 if ($granularity == 0) { 1920 break; 1921 } 1922 } 1923 return $output ? $output : t('0 sec', array(), array('langcode' => $langcode)); 1924 } 1925 1926 /** 1927 * Formats a date, using a date type or a custom date format string. 1928 * 1929 * @param $timestamp 1930 * A UNIX timestamp to format. 1931 * @param $type 1932 * (optional) The format to use, one of: 1933 * - 'short', 'medium', or 'long' (the corresponding built-in date formats). 1934 * - The name of a date type defined by a module in hook_date_format_types(), 1935 * if it's been assigned a format. 1936 * - The machine name of an administrator-defined date format. 1937 * - 'custom', to use $format. 1938 * Defaults to 'medium'. 1939 * @param $format 1940 * (optional) If $type is 'custom', a PHP date format string suitable for 1941 * input to date(). Use a backslash to escape ordinary text, so it does not 1942 * get interpreted as date format characters. 1943 * @param $timezone 1944 * (optional) Time zone identifier, as described at 1945 * http://php.net/manual/en/timezones.php Defaults to the time zone used to 1946 * display the page. 1947 * @param $langcode 1948 * (optional) Language code to translate to. Defaults to the language used to 1949 * display the page. 1950 * 1951 * @return 1952 * A translated date string in the requested format. 1953 */ 1954 function format_date($timestamp, $type = 'medium', $format = '', $timezone = NULL, $langcode = NULL) { 1955 // Use the advanced drupal_static() pattern, since this is called very often. 1956 static $drupal_static_fast; 1957 if (!isset($drupal_static_fast)) { 1958 $drupal_static_fast['timezones'] = &drupal_static(__FUNCTION__); 1959 } 1960 $timezones = &$drupal_static_fast['timezones']; 1961 1962 if (!isset($timezone)) { 1963 $timezone = date_default_timezone_get(); 1964 } 1965 // Store DateTimeZone objects in an array rather than repeatedly 1966 // constructing identical objects over the life of a request. 1967 if (!isset($timezones[$timezone])) { 1968 $timezones[$timezone] = timezone_open($timezone); 1969 } 1970 1971 // Use the default langcode if none is set. 1972 global $language; 1973 if (empty($langcode)) { 1974 $langcode = isset($language->language) ? $language->language : 'en'; 1975 } 1976 1977 switch ($type) { 1978 case 'short': 1979 $format = variable_get('date_format_short', 'm/d/Y - H:i'); 1980 break; 1981 1982 case 'long': 1983 $format = variable_get('date_format_long', 'l, F j, Y - H:i'); 1984 break; 1985 1986 case 'custom': 1987 // No change to format. 1988 break; 1989 1990 case 'medium': 1991 default: 1992 // Retrieve the format of the custom $type passed. 1993 if ($type != 'medium') { 1994 $format = variable_get('date_format_' . $type, ''); 1995 } 1996 // Fall back to 'medium'. 1997 if ($format === '') { 1998 $format = variable_get('date_format_medium', 'D, m/d/Y - H:i'); 1999 } 2000 break; 2001 } 2002 2003 // Create a DateTime object from the timestamp. 2004 $date_time = date_create('@' . $timestamp); 2005 // Set the time zone for the DateTime object. 2006 date_timezone_set($date_time, $timezones[$timezone]); 2007 2008 // Encode markers that should be translated. 'A' becomes '\xEF\AA\xFF'. 2009 // xEF and xFF are invalid UTF-8 sequences, and we assume they are not in the 2010 // input string. 2011 // Paired backslashes are isolated to prevent errors in read-ahead evaluation. 2012 // The read-ahead expression ensures that A matches, but not \A. 2013 $format = preg_replace(array('/\\\\\\\\/', '/(?<!\\\\)([AaeDlMTF])/'), array("\xEF\\\\\\\\\xFF", "\xEF\\\\\$1\$1\xFF"), $format); 2014 2015 // Call date_format(). 2016 $format = date_format($date_time, $format); 2017 2018 // Pass the langcode to _format_date_callback(). 2019 _format_date_callback(NULL, $langcode); 2020 2021 // Translate the marked sequences. 2022 return preg_replace_callback('/\xEF([AaeDlMTF]?)(.*?)\xFF/', '_format_date_callback', $format); 2023 } 2024 2025 /** 2026 * Returns an ISO8601 formatted date based on the given date. 2027 * 2028 * Callback for use within hook_rdf_mapping() implementations. 2029 * 2030 * @param $date 2031 * A UNIX timestamp. 2032 * 2033 * @return string 2034 * An ISO8601 formatted date. 2035 */ 2036 function date_iso8601($date) { 2037 // The DATE_ISO8601 constant cannot be used here because it does not match 2038 // date('c') and produces invalid RDF markup. 2039 return date('c', $date); 2040 } 2041 2042 /** 2043 * Translates a formatted date string. 2044 * 2045 * Callback for preg_replace_callback() within format_date(). 2046 */ 2047 function _format_date_callback(array $matches = NULL, $new_langcode = NULL) { 2048 // We cache translations to avoid redundant and rather costly calls to t(). 2049 static $cache, $langcode; 2050 2051 if (!isset($matches)) { 2052 $langcode = $new_langcode; 2053 return; 2054 } 2055 2056 $code = $matches[1]; 2057 $string = $matches[2]; 2058 2059 if (!isset($cache[$langcode][$code][$string])) { 2060 $options = array( 2061 'langcode' => $langcode, 2062 ); 2063 2064 if ($code == 'F') { 2065 $options['context'] = 'Long month name'; 2066 } 2067 2068 if ($code == '') { 2069 $cache[$langcode][$code][$string] = $string; 2070 } 2071 else { 2072 $cache[$langcode][$code][$string] = t($string, array(), $options); 2073 } 2074 } 2075 return $cache[$langcode][$code][$string]; 2076 } 2077 2078 /** 2079 * Format a username. 2080 * 2081 * By default, the passed-in object's 'name' property is used if it exists, or 2082 * else, the site-defined value for the 'anonymous' variable. However, a module 2083 * may override this by implementing hook_username_alter(&$name, $account). 2084 * 2085 * @see hook_username_alter() 2086 * 2087 * @param $account 2088 * The account object for the user whose name is to be formatted. 2089 * 2090 * @return 2091 * An unsanitized string with the username to display. The code receiving 2092 * this result must ensure that check_plain() is called on it before it is 2093 * printed to the page. 2094 */ 2095 function format_username($account) { 2096 $name = !empty($account->name) ? $account->name : variable_get('anonymous', t('Anonymous')); 2097 drupal_alter('username', $name, $account); 2098 return $name; 2099 } 2100 2101 /** 2102 * @} End of "defgroup format". 2103 */ 2104 2105 /** 2106 * Generates an internal or external URL. 2107 * 2108 * When creating links in modules, consider whether l() could be a better 2109 * alternative than url(). 2110 * 2111 * @param $path 2112 * (optional) The internal path or external URL being linked to, such as 2113 * "node/34" or "http://example.com/foo". The default value is equivalent to 2114 * passing in '<front>'. A few notes: 2115 * - If you provide a full URL, it will be considered an external URL. 2116 * - If you provide only the path (e.g. "node/34"), it will be 2117 * considered an internal link. In this case, it should be a system URL, 2118 * and it will be replaced with the alias, if one exists. Additional query 2119 * arguments for internal paths must be supplied in $options['query'], not 2120 * included in $path. 2121 * - If you provide an internal path and $options['alias'] is set to TRUE, the 2122 * path is assumed already to be the correct path alias, and the alias is 2123 * not looked up. 2124 * - The special string '<front>' generates a link to the site's base URL. 2125 * - If your external URL contains a query (e.g. http://example.com/foo?a=b), 2126 * then you can either URL encode the query keys and values yourself and 2127 * include them in $path, or use $options['query'] to let this function 2128 * URL encode them. 2129 * @param $options 2130 * (optional) An associative array of additional options, with the following 2131 * elements: 2132 * - 'query': An array of query key/value-pairs (without any URL-encoding) to 2133 * append to the URL. 2134 * - 'fragment': A fragment identifier (named anchor) to append to the URL. 2135 * Do not include the leading '#' character. 2136 * - 'absolute': Defaults to FALSE. Whether to force the output to be an 2137 * absolute link (beginning with http:). Useful for links that will be 2138 * displayed outside the site, such as in an RSS feed. 2139 * - 'alias': Defaults to FALSE. Whether the given path is a URL alias 2140 * already. 2141 * - 'external': Whether the given path is an external URL. 2142 * - 'language': An optional language object. If the path being linked to is 2143 * internal to the site, $options['language'] is used to look up the alias 2144 * for the URL. If $options['language'] is omitted, the global $language_url 2145 * will be used. 2146 * - 'https': Whether this URL should point to a secure location. If not 2147 * defined, the current scheme is used, so the user stays on HTTP or HTTPS 2148 * respectively. TRUE enforces HTTPS and FALSE enforces HTTP, but HTTPS can 2149 * only be enforced when the variable 'https' is set to TRUE. 2150 * - 'base_url': Only used internally, to modify the base URL when a language 2151 * dependent URL requires so. 2152 * - 'prefix': Only used internally, to modify the path when a language 2153 * dependent URL requires so. 2154 * - 'script': The script filename in Drupal's root directory to use when 2155 * clean URLs are disabled, such as 'index.php'. Defaults to an empty 2156 * string, as most modern web servers automatically find 'index.php'. If 2157 * clean URLs are disabled, the value of $path is appended as query 2158 * parameter 'q' to $options['script'] in the returned URL. When deploying 2159 * Drupal on a web server that cannot be configured to automatically find 2160 * index.php, then hook_url_outbound_alter() can be implemented to force 2161 * this value to 'index.php'. 2162 * - 'entity_type': The entity type of the object that called url(). Only 2163 * set if url() is invoked by entity_uri(). 2164 * - 'entity': The entity object (such as a node) for which the URL is being 2165 * generated. Only set if url() is invoked by entity_uri(). 2166 * 2167 * @return 2168 * A string containing a URL to the given path. 2169 */ 2170 function url($path = NULL, array $options = array()) { 2171 // Merge in defaults. 2172 $options += array( 2173 'fragment' => '', 2174 'query' => array(), 2175 'absolute' => FALSE, 2176 'alias' => FALSE, 2177 'prefix' => '' 2178 ); 2179 2180 if (!isset($options['external'])) { 2181 // Return an external link if $path contains an allowed absolute URL. Only 2182 // call the slow drupal_strip_dangerous_protocols() if $path contains a ':' 2183 // before any / ? or #. Note: we could use url_is_external($path) here, but 2184 // that would require another function call, and performance inside url() is 2185 // critical. 2186 $colonpos = strpos($path, ':'); 2187 $options['external'] = ($colonpos !== FALSE && !preg_match('![/?#]!', substr($path, 0, $colonpos)) && drupal_strip_dangerous_protocols($path) == $path); 2188 } 2189 2190 // Preserve the original path before altering or aliasing. 2191 $original_path = $path; 2192 2193 // Allow other modules to alter the outbound URL and options. 2194 drupal_alter('url_outbound', $path, $options, $original_path); 2195 2196 if (isset($options['fragment']) && $options['fragment'] !== '') { 2197 $options['fragment'] = '#' . $options['fragment']; 2198 } 2199 2200 if ($options['external']) { 2201 // Split off the fragment. 2202 if (strpos($path, '#') !== FALSE) { 2203 list($path, $old_fragment) = explode('#', $path, 2); 2204 // If $options contains no fragment, take it over from the path. 2205 if (isset($old_fragment) && !$options['fragment']) { 2206 $options['fragment'] = '#' . $old_fragment; 2207 } 2208 } 2209 // Append the query. 2210 if ($options['query']) { 2211 $path .= (strpos($path, '?') !== FALSE ? '&' : '?') . drupal_http_build_query($options['query']); 2212 } 2213 if (isset($options['https']) && variable_get('https', FALSE)) { 2214 if ($options['https'] === TRUE) { 2215 $path = str_replace('http://', 'https://', $path); 2216 } 2217 elseif ($options['https'] === FALSE) { 2218 $path = str_replace('https://', 'http://', $path); 2219 } 2220 } 2221 // Reassemble. 2222 return $path . $options['fragment']; 2223 } 2224 2225 global $base_url, $base_secure_url, $base_insecure_url; 2226 2227 // The base_url might be rewritten from the language rewrite in domain mode. 2228 if (!isset($options['base_url'])) { 2229 if (isset($options['https']) && variable_get('https', FALSE)) { 2230 if ($options['https'] === TRUE) { 2231 $options['base_url'] = $base_secure_url; 2232 $options['absolute'] = TRUE; 2233 } 2234 elseif ($options['https'] === FALSE) { 2235 $options['base_url'] = $base_insecure_url; 2236 $options['absolute'] = TRUE; 2237 } 2238 } 2239 else { 2240 $options['base_url'] = $base_url; 2241 } 2242 } 2243 2244 // The special path '<front>' links to the default front page. 2245 if ($path == '<front>') { 2246 $path = ''; 2247 } 2248 elseif (!empty($path) && !$options['alias']) { 2249 $language = isset($options['language']) && isset($options['language']->language) ? $options['language']->language : ''; 2250 $alias = drupal_get_path_alias($original_path, $language); 2251 if ($alias != $original_path) { 2252 $path = $alias; 2253 } 2254 } 2255 2256 $base = $options['absolute'] ? $options['base_url'] . '/' : base_path(); 2257 $prefix = empty($path) ? rtrim($options['prefix'], '/') : $options['prefix']; 2258 2259 // With Clean URLs. 2260 if (!empty($GLOBALS['conf']['clean_url'])) { 2261 $path = drupal_encode_path($prefix . $path); 2262 if ($options['query']) { 2263 return $base . $path . '?' . drupal_http_build_query($options['query']) . $options['fragment']; 2264 } 2265 else { 2266 return $base . $path . $options['fragment']; 2267 } 2268 } 2269 // Without Clean URLs. 2270 else { 2271 $path = $prefix . $path; 2272 $query = array(); 2273 if (!empty($path)) { 2274 $query['q'] = $path; 2275 } 2276 if ($options['query']) { 2277 // We do not use array_merge() here to prevent overriding $path via query 2278 // parameters. 2279 $query += $options['query']; 2280 } 2281 $query = $query ? ('?' . drupal_http_build_query($query)) : ''; 2282 $script = isset($options['script']) ? $options['script'] : ''; 2283 return $base . $script . $query . $options['fragment']; 2284 } 2285 } 2286 2287 /** 2288 * Returns TRUE if a path is external to Drupal (e.g. http://example.com). 2289 * 2290 * If a path cannot be assessed by Drupal's menu handler, then we must 2291 * treat it as potentially insecure. 2292 * 2293 * @param $path 2294 * The internal path or external URL being linked to, such as "node/34" or 2295 * "http://example.com/foo". 2296 * 2297 * @return 2298 * Boolean TRUE or FALSE, where TRUE indicates an external path. 2299 */ 2300 function url_is_external($path) { 2301 $colonpos = strpos($path, ':'); 2302 // Avoid calling drupal_strip_dangerous_protocols() if there is any 2303 // slash (/), hash (#) or question_mark (?) before the colon (:) 2304 // occurrence - if any - as this would clearly mean it is not a URL. 2305 return $colonpos !== FALSE && !preg_match('![/?#]!', substr($path, 0, $colonpos)) && drupal_strip_dangerous_protocols($path) == $path; 2306 } 2307 2308 /** 2309 * Formats an attribute string for an HTTP header. 2310 * 2311 * @param $attributes 2312 * An associative array of attributes such as 'rel'. 2313 * 2314 * @return 2315 * A ; separated string ready for insertion in a HTTP header. No escaping is 2316 * performed for HTML entities, so this string is not safe to be printed. 2317 * 2318 * @see drupal_add_http_header() 2319 */ 2320 function drupal_http_header_attributes(array $attributes = array()) { 2321 foreach ($attributes as $attribute => &$data) { 2322 if (is_array($data)) { 2323 $data = implode(' ', $data); 2324 } 2325 $data = $attribute . '="' . $data . '"'; 2326 } 2327 return $attributes ? ' ' . implode('; ', $attributes) : ''; 2328 } 2329 2330 /** 2331 * Converts an associative array to an XML/HTML tag attribute string. 2332 * 2333 * Each array key and its value will be formatted into an attribute string. 2334 * If a value is itself an array, then its elements are concatenated to a single 2335 * space-delimited string (for example, a class attribute with multiple values). 2336 * 2337 * Attribute values are sanitized by running them through check_plain(). 2338 * Attribute names are not automatically sanitized. When using user-supplied 2339 * attribute names, it is strongly recommended to allow only white-listed names, 2340 * since certain attributes carry security risks and can be abused. 2341 * 2342 * Examples of security aspects when using drupal_attributes: 2343 * @code 2344 * // By running the value in the following statement through check_plain, 2345 * // the malicious script is neutralized. 2346 * drupal_attributes(array('title' => t('<script>steal_cookie();</script>'))); 2347 * 2348 * // The statement below demonstrates dangerous use of drupal_attributes, and 2349 * // will return an onmouseout attribute with JavaScript code that, when used 2350 * // as attribute in a tag, will cause users to be redirected to another site. 2351 * // 2352 * // In this case, the 'onmouseout' attribute should not be whitelisted -- 2353 * // you don't want users to have the ability to add this attribute or others 2354 * // that take JavaScript commands. 2355 * drupal_attributes(array('onmouseout' => 'window.location="http://malicious.com/";'))); 2356 * @endcode 2357 * 2358 * @param $attributes 2359 * An associative array of key-value pairs to be converted to attributes. 2360 * 2361 * @return 2362 * A string ready for insertion in a tag (starts with a space). 2363 * 2364 * @ingroup sanitization 2365 */ 2366 function drupal_attributes(array $attributes = array()) { 2367 foreach ($attributes as $attribute => &$data) { 2368 $data = implode(' ', (array) $data); 2369 $data = $attribute . '="' . check_plain($data) . '"'; 2370 } 2371 return $attributes ? ' ' . implode(' ', $attributes) : ''; 2372 } 2373 2374 /** 2375 * Formats an internal or external URL link as an HTML anchor tag. 2376 * 2377 * This function correctly handles aliased paths and adds an 'active' class 2378 * attribute to links that point to the current page (for theming), so all 2379 * internal links output by modules should be generated by this function if 2380 * possible. 2381 * 2382 * @param string $text 2383 * The translated link text for the anchor tag. 2384 * @param string $path 2385 * The internal path or external URL being linked to, such as "node/34" or 2386 * "http://example.com/foo". After the url() function is called to construct 2387 * the URL from $path and $options, the resulting URL is passed through 2388 * check_plain() before it is inserted into the HTML anchor tag, to ensure 2389 * well-formed HTML. See url() for more information and notes. 2390 * @param array $options 2391 * An associative array of additional options. Defaults to an empty array. It 2392 * may contain the following elements. 2393 * - 'attributes': An associative array of HTML attributes to apply to the 2394 * anchor tag. If element 'class' is included, it must be an array; 'title' 2395 * must be a string; other elements are more flexible, as they just need 2396 * to work in a call to drupal_attributes($options['attributes']). 2397 * - 'html' (default FALSE): Whether $text is HTML or just plain-text. For 2398 * example, to make an image tag into a link, this must be set to TRUE, or 2399 * you will see the escaped HTML image tag. $text is not sanitized if 2400 * 'html' is TRUE. The calling function must ensure that $text is already 2401 * safe. 2402 * - 'language': An optional language object. If the path being linked to is 2403 * internal to the site, $options['language'] is used to determine whether 2404 * the link is "active", or pointing to the current page (the language as 2405 * well as the path must match). This element is also used by url(). 2406 * - Additional $options elements used by the url() function. 2407 * 2408 * @return string 2409 * An HTML string containing a link to the given path. 2410 * 2411 * @see url() 2412 */ 2413 function l($text, $path, array $options = array()) { 2414 global $language_url; 2415 static $use_theme = NULL; 2416 2417 // Merge in defaults. 2418 $options += array( 2419 'attributes' => array(), 2420 'html' => FALSE, 2421 ); 2422 2423 // Append active class. 2424 if (($path == $_GET['q'] || ($path == '<front>' && drupal_is_front_page())) && 2425 (empty($options['language']) || $options['language']->language == $language_url->language)) { 2426 $options['attributes']['class'][] = 'active'; 2427 } 2428 2429 // Remove all HTML and PHP tags from a tooltip. For best performance, we act only 2430 // if a quick strpos() pre-check gave a suspicion (because strip_tags() is expensive). 2431 if (isset($options['attributes']['title']) && strpos($options['attributes']['title'], '<') !== FALSE) { 2432 $options['attributes']['title'] = strip_tags($options['attributes']['title']); 2433 } 2434 2435 // Determine if rendering of the link is to be done with a theme function 2436 // or the inline default. Inline is faster, but if the theme system has been 2437 // loaded and a module or theme implements a preprocess or process function 2438 // or overrides the theme_link() function, then invoke theme(). Preliminary 2439 // benchmarks indicate that invoking theme() can slow down the l() function 2440 // by 20% or more, and that some of the link-heavy Drupal pages spend more 2441 // than 10% of the total page request time in the l() function. 2442 if (!isset($use_theme) && function_exists('theme')) { 2443 // Allow edge cases to prevent theme initialization and force inline link 2444 // rendering. 2445 if (variable_get('theme_link', TRUE)) { 2446 drupal_theme_initialize(); 2447 $registry = theme_get_registry(FALSE); 2448 // We don't want to duplicate functionality that's in theme(), so any 2449 // hint of a module or theme doing anything at all special with the 'link' 2450 // theme hook should simply result in theme() being called. This includes 2451 // the overriding of theme_link() with an alternate function or template, 2452 // the presence of preprocess or process functions, or the presence of 2453 // include files. 2454 $use_theme = !isset($registry['link']['function']) || ($registry['link']['function'] != 'theme_link'); 2455 $use_theme = $use_theme || !empty($registry['link']['preprocess functions']) || !empty($registry['link']['process functions']) || !empty($registry['link']['includes']); 2456 } 2457 else { 2458 $use_theme = FALSE; 2459 } 2460 } 2461 if ($use_theme) { 2462 return theme('link', array('text' => $text, 'path' => $path, 'options' => $options)); 2463 } 2464 // The result of url() is a plain-text URL. Because we are using it here 2465 // in an HTML argument context, we need to encode it properly. 2466 return '<a href="' . check_plain(url($path, $options)) . '"' . drupal_attributes($options['attributes']) . '>' . ($options['html'] ? $text : check_plain($text)) . '</a>'; 2467 } 2468 2469 /** 2470 * Delivers a page callback result to the browser in the appropriate format. 2471 * 2472 * This function is most commonly called by menu_execute_active_handler(), but 2473 * can also be called by error conditions such as drupal_not_found(), 2474 * drupal_access_denied(), and drupal_site_offline(). 2475 * 2476 * When a user requests a page, index.php calls menu_execute_active_handler(), 2477 * which calls the 'page callback' function registered in hook_menu(). The page 2478 * callback function can return one of: 2479 * - NULL: to indicate no content. 2480 * - An integer menu status constant: to indicate an error condition. 2481 * - A string of HTML content. 2482 * - A renderable array of content. 2483 * Returning a renderable array rather than a string of HTML is preferred, 2484 * because that provides modules with more flexibility in customizing the final 2485 * result. 2486 * 2487 * When the page callback returns its constructed content to 2488 * menu_execute_active_handler(), this function gets called. The purpose of 2489 * this function is to determine the most appropriate 'delivery callback' 2490 * function to route the content to. The delivery callback function then 2491 * sends the content to the browser in the needed format. The default delivery 2492 * callback is drupal_deliver_html_page(), which delivers the content as an HTML 2493 * page, complete with blocks in addition to the content. This default can be 2494 * overridden on a per menu router item basis by setting 'delivery callback' in 2495 * hook_menu() or hook_menu_alter(), and can also be overridden on a per request 2496 * basis in hook_page_delivery_callback_alter(). 2497 * 2498 * For example, the same page callback function can be used for an HTML 2499 * version of the page and an Ajax version of the page. The page callback 2500 * function just needs to decide what content is to be returned and the 2501 * delivery callback function will send it as an HTML page or an Ajax 2502 * response, as appropriate. 2503 * 2504 * In order for page callbacks to be reusable in different delivery formats, 2505 * they should not issue any "print" or "echo" statements, but instead just 2506 * return content. 2507 * 2508 * Also note that this function does not perform access checks. The delivery 2509 * callback function specified in hook_menu(), hook_menu_alter(), or 2510 * hook_page_delivery_callback_alter() will be called even if the router item 2511 * access checks fail. This is intentional (it is needed for JSON and other 2512 * purposes), but it has security implications. Do not call this function 2513 * directly unless you understand the security implications, and be careful in 2514 * writing delivery callbacks, so that they do not violate security. See 2515 * drupal_deliver_html_page() for an example of a delivery callback that 2516 * respects security. 2517 * 2518 * @param $page_callback_result 2519 * The result of a page callback. Can be one of: 2520 * - NULL: to indicate no content. 2521 * - An integer menu status constant: to indicate an error condition. 2522 * - A string of HTML content. 2523 * - A renderable array of content. 2524 * @param $default_delivery_callback 2525 * (Optional) If given, it is the name of a delivery function most likely 2526 * to be appropriate for the page request as determined by the calling 2527 * function (e.g., menu_execute_active_handler()). If not given, it is 2528 * determined from the menu router information of the current page. 2529 * 2530 * @see menu_execute_active_handler() 2531 * @see hook_menu() 2532 * @see hook_menu_alter() 2533 * @see hook_page_delivery_callback_alter() 2534 */ 2535 function drupal_deliver_page($page_callback_result, $default_delivery_callback = NULL) { 2536 if (!isset($default_delivery_callback) && ($router_item = menu_get_item())) { 2537 $default_delivery_callback = $router_item['delivery_callback']; 2538 } 2539 $delivery_callback = !empty($default_delivery_callback) ? $default_delivery_callback : 'drupal_deliver_html_page'; 2540 // Give modules a chance to alter the delivery callback used, based on 2541 // request-time context (e.g., HTTP request headers). 2542 drupal_alter('page_delivery_callback', $delivery_callback); 2543 if (function_exists($delivery_callback)) { 2544 $delivery_callback($page_callback_result); 2545 } 2546 else { 2547 // If a delivery callback is specified, but doesn't exist as a function, 2548 // something is wrong, but don't print anything, since it's not known 2549 // what format the response needs to be in. 2550 watchdog('delivery callback not found', 'callback %callback not found: %q.', array('%callback' => $delivery_callback, '%q' => $_GET['q']), WATCHDOG_ERROR); 2551 } 2552 } 2553 2554 /** 2555 * Packages and sends the result of a page callback to the browser as HTML. 2556 * 2557 * @param $page_callback_result 2558 * The result of a page callback. Can be one of: 2559 * - NULL: to indicate no content. 2560 * - An integer menu status constant: to indicate an error condition. 2561 * - A string of HTML content. 2562 * - A renderable array of content. 2563 * 2564 * @see drupal_deliver_page() 2565 */ 2566 function drupal_deliver_html_page($page_callback_result) { 2567 // Emit the correct charset HTTP header, but not if the page callback 2568 // result is NULL, since that likely indicates that it printed something 2569 // in which case, no further headers may be sent, and not if code running 2570 // for this page request has already set the content type header. 2571 if (isset($page_callback_result) && is_null(drupal_get_http_header('Content-Type'))) { 2572 drupal_add_http_header('Content-Type', 'text/html; charset=utf-8'); 2573 } 2574 2575 // Send appropriate HTTP-Header for browsers and search engines. 2576 global $language; 2577 drupal_add_http_header('Content-Language', $language->language); 2578 2579 // Menu status constants are integers; page content is a string or array. 2580 if (is_int($page_callback_result)) { 2581 // @todo: Break these up into separate functions? 2582 switch ($page_callback_result) { 2583 case MENU_NOT_FOUND: 2584 // Print a 404 page. 2585 drupal_add_http_header('Status', '404 Not Found'); 2586 2587 watchdog('page not found', check_plain($_GET['q']), NULL, WATCHDOG_WARNING); 2588 2589 // Check for and return a fast 404 page if configured. 2590 drupal_fast_404(); 2591 2592 // Keep old path for reference, and to allow forms to redirect to it. 2593 if (!isset($_GET['destination'])) { 2594 $_GET['destination'] = $_GET['q']; 2595 } 2596 2597 $path = drupal_get_normal_path(variable_get('site_404', '')); 2598 if ($path && $path != $_GET['q']) { 2599 // Custom 404 handler. Set the active item in case there are tabs to 2600 // display, or other dependencies on the path. 2601 menu_set_active_item($path); 2602 $return = menu_execute_active_handler($path, FALSE); 2603 } 2604 2605 if (empty($return) || $return == MENU_NOT_FOUND || $return == MENU_ACCESS_DENIED) { 2606 // Standard 404 handler. 2607 drupal_set_title(t('Page not found')); 2608 $return = t('The requested page "@path" could not be found.', array('@path' => request_uri())); 2609 } 2610 2611 drupal_set_page_content($return); 2612 $page = element_info('page'); 2613 print drupal_render_page($page); 2614 break; 2615 2616 case MENU_ACCESS_DENIED: 2617 // Print a 403 page. 2618 drupal_add_http_header('Status', '403 Forbidden'); 2619 watchdog('access denied', check_plain($_GET['q']), NULL, WATCHDOG_WARNING); 2620 2621 // Keep old path for reference, and to allow forms to redirect to it. 2622 if (!isset($_GET['destination'])) { 2623 $_GET['destination'] = $_GET['q']; 2624 } 2625 2626 $path = drupal_get_normal_path(variable_get('site_403', '')); 2627 if ($path && $path != $_GET['q']) { 2628 // Custom 403 handler. Set the active item in case there are tabs to 2629 // display or other dependencies on the path. 2630 menu_set_active_item($path); 2631 $return = menu_execute_active_handler($path, FALSE); 2632 } 2633 2634 if (empty($return) || $return == MENU_NOT_FOUND || $return == MENU_ACCESS_DENIED) { 2635 // Standard 403 handler. 2636 drupal_set_title(t('Access denied')); 2637 $return = t('You are not authorized to access this page.'); 2638 } 2639 2640 print drupal_render_page($return); 2641 break; 2642 2643 case MENU_SITE_OFFLINE: 2644 // Print a 503 page. 2645 drupal_maintenance_theme(); 2646 drupal_add_http_header('Status', '503 Service unavailable'); 2647 drupal_set_title(t('Site under maintenance')); 2648 print theme('maintenance_page', array('content' => filter_xss_admin(variable_get('maintenance_mode_message', 2649 t('@site is currently under maintenance. We should be back shortly. Thank you for your patience.', array('@site' => variable_get('site_name', 'Drupal'))))))); 2650 break; 2651 } 2652 } 2653 elseif (isset($page_callback_result)) { 2654 // Print anything besides a menu constant, assuming it's not NULL or 2655 // undefined. 2656 print drupal_render_page($page_callback_result); 2657 } 2658 2659 // Perform end-of-request tasks. 2660 drupal_page_footer(); 2661 } 2662 2663 /** 2664 * Performs end-of-request tasks. 2665 * 2666 * This function sets the page cache if appropriate, and allows modules to 2667 * react to the closing of the page by calling hook_exit(). 2668 */ 2669 function drupal_page_footer() { 2670 global $user; 2671 2672 module_invoke_all('exit'); 2673 2674 // Commit the user session, if needed. 2675 drupal_session_commit(); 2676 2677 if (variable_get('cache', 0) && ($cache = drupal_page_set_cache())) { 2678 drupal_serve_page_from_cache($cache); 2679 } 2680 else { 2681 ob_flush(); 2682 } 2683 2684 _registry_check_code(REGISTRY_WRITE_LOOKUP_CACHE); 2685 drupal_cache_system_paths(); 2686 module_implements_write_cache(); 2687 system_run_automated_cron(); 2688 } 2689 2690 /** 2691 * Performs end-of-request tasks. 2692 * 2693 * In some cases page requests need to end without calling drupal_page_footer(). 2694 * In these cases, call drupal_exit() instead. There should rarely be a reason 2695 * to call exit instead of drupal_exit(); 2696 * 2697 * @param $destination 2698 * If this function is called from drupal_goto(), then this argument 2699 * will be a fully-qualified URL that is the destination of the redirect. 2700 * This should be passed along to hook_exit() implementations. 2701 */ 2702 function drupal_exit($destination = NULL) { 2703 if (drupal_get_bootstrap_phase() == DRUPAL_BOOTSTRAP_FULL) { 2704 if (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update') { 2705 module_invoke_all('exit', $destination); 2706 } 2707 drupal_session_commit(); 2708 } 2709 exit; 2710 } 2711 2712 /** 2713 * Forms an associative array from a linear array. 2714 * 2715 * This function walks through the provided array and constructs an associative 2716 * array out of it. The keys of the resulting array will be the values of the 2717 * input array. The values will be the same as the keys unless a function is 2718 * specified, in which case the output of the function is used for the values 2719 * instead. 2720 * 2721 * @param $array 2722 * A linear array. 2723 * @param $function 2724 * A name of a function to apply to all values before output. 2725 * 2726 * @return 2727 * An associative array. 2728 */ 2729 function drupal_map_assoc($array, $function = NULL) { 2730 // array_combine() fails with empty arrays: 2731 // http://bugs.php.net/bug.php?id=34857. 2732 $array = !empty($array) ? array_combine($array, $array) : array(); 2733 if (is_callable($function)) { 2734 $array = array_map($function, $array); 2735 } 2736 return $array; 2737 } 2738 2739 /** 2740 * Attempts to set the PHP maximum execution time. 2741 * 2742 * This function is a wrapper around the PHP function set_time_limit(). 2743 * When called, set_time_limit() restarts the timeout counter from zero. 2744 * In other words, if the timeout is the default 30 seconds, and 25 seconds 2745 * into script execution a call such as set_time_limit(20) is made, the 2746 * script will run for a total of 45 seconds before timing out. 2747 * 2748 * It also means that it is possible to decrease the total time limit if 2749 * the sum of the new time limit and the current time spent running the 2750 * script is inferior to the original time limit. It is inherent to the way 2751 * set_time_limit() works, it should rather be called with an appropriate 2752 * value every time you need to allocate a certain amount of time 2753 * to execute a task than only once at the beginning of the script. 2754 * 2755 * Before calling set_time_limit(), we check if this function is available 2756 * because it could be disabled by the server administrator. We also hide all 2757 * the errors that could occur when calling set_time_limit(), because it is 2758 * not possible to reliably ensure that PHP or a security extension will 2759 * not issue a warning/error if they prevent the use of this function. 2760 * 2761 * @param $time_limit 2762 * An integer specifying the new time limit, in seconds. A value of 0 2763 * indicates unlimited execution time. 2764 * 2765 * @ingroup php_wrappers 2766 */ 2767 function drupal_set_time_limit($time_limit) { 2768 if (function_exists('set_time_limit')) { 2769 @set_time_limit($time_limit); 2770 } 2771 } 2772 2773 /** 2774 * Returns the path to a system item (module, theme, etc.). 2775 * 2776 * @param $type 2777 * The type of the item (i.e. theme, theme_engine, module, profile). 2778 * @param $name 2779 * The name of the item for which the path is requested. 2780 * 2781 * @return 2782 * The path to the requested item. 2783 */ 2784 function drupal_get_path($type, $name) { 2785 return dirname(drupal_get_filename($type, $name)); 2786 } 2787 2788 /** 2789 * Returns the base URL path (i.e., directory) of the Drupal installation. 2790 * 2791 * base_path() adds a "/" to the beginning and end of the returned path if the 2792 * path is not empty. At the very least, this will return "/". 2793 * 2794 * Examples: 2795 * - http://example.com returns "/" because the path is empty. 2796 * - http://example.com/drupal/folder returns "/drupal/folder/". 2797 */ 2798 function base_path() { 2799 return $GLOBALS['base_path']; 2800 } 2801 2802 /** 2803 * Adds a LINK tag with a distinct 'rel' attribute to the page's HEAD. 2804 * 2805 * This function can be called as long the HTML header hasn't been sent, which 2806 * on normal pages is up through the preprocess step of theme('html'). Adding 2807 * a link will overwrite a prior link with the exact same 'rel' and 'href' 2808 * attributes. 2809 * 2810 * @param $attributes 2811 * Associative array of element attributes including 'href' and 'rel'. 2812 * @param $header 2813 * Optional flag to determine if a HTTP 'Link:' header should be sent. 2814 */ 2815 function drupal_add_html_head_link($attributes, $header = FALSE) { 2816 $element = array( 2817 '#tag' => 'link', 2818 '#attributes' => $attributes, 2819 ); 2820 $href = $attributes['href']; 2821 2822 if ($header) { 2823 // Also add a HTTP header "Link:". 2824 $href = '<' . check_plain($attributes['href']) . '>;'; 2825 unset($attributes['href']); 2826 $element['#attached']['drupal_add_http_header'][] = array('Link', $href . drupal_http_header_attributes($attributes), TRUE); 2827 } 2828 2829 drupal_add_html_head($element, 'drupal_add_html_head_link:' . $attributes['rel'] . ':' . $href); 2830 } 2831 2832 /** 2833 * Adds a cascading stylesheet to the stylesheet queue. 2834 * 2835 * Calling drupal_static_reset('drupal_add_css') will clear all cascading 2836 * stylesheets added so far. 2837 * 2838 * If CSS aggregation/compression is enabled, all cascading style sheets added 2839 * with $options['preprocess'] set to TRUE will be merged into one aggregate 2840 * file and compressed by removing all extraneous white space. 2841 * Preprocessed inline stylesheets will not be aggregated into this single file; 2842 * instead, they are just compressed upon output on the page. Externally hosted 2843 * stylesheets are never aggregated or compressed. 2844 * 2845 * The reason for aggregating the files is outlined quite thoroughly here: 2846 * http://www.die.net/musings/page_load_time/ "Load fewer external objects. Due 2847 * to request overhead, one bigger file just loads faster than two smaller ones 2848 * half its size." 2849 * 2850 * $options['preprocess'] should be only set to TRUE when a file is required for 2851 * all typical visitors and most pages of a site. It is critical that all 2852 * preprocessed files are added unconditionally on every page, even if the 2853 * files do not happen to be needed on a page. This is normally done by calling 2854 * drupal_add_css() in a hook_init() implementation. 2855 * 2856 * Non-preprocessed files should only be added to the page when they are 2857 * actually needed. 2858 * 2859 * @param $data 2860 * (optional) The stylesheet data to be added, depending on what is passed 2861 * through to the $options['type'] parameter: 2862 * - 'file': The path to the CSS file relative to the base_path(), or a 2863 * stream wrapper URI. For example: "modules/devel/devel.css" or 2864 * "public://generated_css/stylesheet_1.css". Note that Modules should 2865 * always prefix the names of their CSS files with the module name; for 2866 * example, system-menus.css rather than simply menus.css. Themes can 2867 * override module-supplied CSS files based on their filenames, and this 2868 * prefixing helps prevent confusing name collisions for theme developers. 2869 * See drupal_get_css() where the overrides are performed. Also, if the 2870 * direction of the current language is right-to-left (Hebrew, Arabic, 2871 * etc.), the function will also look for an RTL CSS file and append it to 2872 * the list. The name of this file should have an '-rtl.css' suffix. For 2873 * example, a CSS file called 'mymodule-name.css' will have a 2874 * 'mymodule-name-rtl.css' file added to the list, if exists in the same 2875 * directory. This CSS file should contain overrides for properties which 2876 * should be reversed or otherwise different in a right-to-left display. 2877 * - 'inline': A string of CSS that should be placed in the given scope. Note 2878 * that it is better practice to use 'file' stylesheets, rather than 2879 * 'inline', as the CSS would then be aggregated and cached. 2880 * - 'external': The absolute path to an external CSS file that is not hosted 2881 * on the local server. These files will not be aggregated if CSS 2882 * aggregation is enabled. 2883 * @param $options 2884 * (optional) A string defining the 'type' of CSS that is being added in the 2885 * $data parameter ('file', 'inline', or 'external'), or an array which can 2886 * have any or all of the following keys: 2887 * - 'type': The type of stylesheet being added. Available options are 'file', 2888 * 'inline' or 'external'. Defaults to 'file'. 2889 * - 'basename': Force a basename for the file being added. Modules are 2890 * expected to use stylesheets with unique filenames, but integration of 2891 * external libraries may make this impossible. The basename of 2892 * 'modules/node/node.css' is 'node.css'. If the external library "node.js" 2893 * ships with a 'node.css', then a different, unique basename would be 2894 * 'node.js.css'. 2895 * - 'group': A number identifying the group in which to add the stylesheet. 2896 * Available constants are: 2897 * - CSS_SYSTEM: Any system-layer CSS. 2898 * - CSS_DEFAULT: (default) Any module-layer CSS. 2899 * - CSS_THEME: Any theme-layer CSS. 2900 * The group number serves as a weight: the markup for loading a stylesheet 2901 * within a lower weight group is output to the page before the markup for 2902 * loading a stylesheet within a higher weight group, so CSS within higher 2903 * weight groups take precendence over CSS within lower weight groups. 2904 * - 'every_page': For optimal front-end performance when aggregation is 2905 * enabled, this should be set to TRUE if the stylesheet is present on every 2906 * page of the website for users for whom it is present at all. This 2907 * defaults to FALSE. It is set to TRUE for stylesheets added via module and 2908 * theme .info files. Modules that add stylesheets within hook_init() 2909 * implementations, or from other code that ensures that the stylesheet is 2910 * added to all website pages, should also set this flag to TRUE. All 2911 * stylesheets within the same group that have the 'every_page' flag set to 2912 * TRUE and do not have 'preprocess' set to FALSE are aggregated together 2913 * into a single aggregate file, and that aggregate file can be reused 2914 * across a user's entire site visit, leading to faster navigation between 2915 * pages. However, stylesheets that are only needed on pages less frequently 2916 * visited, can be added by code that only runs for those particular pages, 2917 * and that code should not set the 'every_page' flag. This minimizes the 2918 * size of the aggregate file that the user needs to download when first 2919 * visiting the website. Stylesheets without the 'every_page' flag are 2920 * aggregated into a separate aggregate file. This other aggregate file is 2921 * likely to change from page to page, and each new aggregate file needs to 2922 * be downloaded when first encountered, so it should be kept relatively 2923 * small by ensuring that most commonly needed stylesheets are added to 2924 * every page. 2925 * - 'weight': The weight of the stylesheet specifies the order in which the 2926 * CSS will appear relative to other stylesheets with the same group and 2927 * 'every_page' flag. The exact ordering of stylesheets is as follows: 2928 * - First by group. 2929 * - Then by the 'every_page' flag, with TRUE coming before FALSE. 2930 * - Then by weight. 2931 * - Then by the order in which the CSS was added. For example, all else 2932 * being the same, a stylesheet added by a call to drupal_add_css() that 2933 * happened later in the page request gets added to the page after one for 2934 * which drupal_add_css() happened earlier in the page request. 2935 * - 'media': The media type for the stylesheet, e.g., all, print, screen. 2936 * Defaults to 'all'. 2937 * - 'preprocess': If TRUE and CSS aggregation/compression is enabled, the 2938 * styles will be aggregated and compressed. Defaults to TRUE. 2939 * - 'browsers': An array containing information specifying which browsers 2940 * should load the CSS item. See drupal_pre_render_conditional_comments() 2941 * for details. 2942 * 2943 * @return 2944 * An array of queued cascading stylesheets. 2945 * 2946 * @see drupal_get_css() 2947 */ 2948 function drupal_add_css($data = NULL, $options = NULL) { 2949 $css = &drupal_static(__FUNCTION__, array()); 2950 2951 // Construct the options, taking the defaults into consideration. 2952 if (isset($options)) { 2953 if (!is_array($options)) { 2954 $options = array('type' => $options); 2955 } 2956 } 2957 else { 2958 $options = array(); 2959 } 2960 2961 // Create an array of CSS files for each media type first, since each type needs to be served 2962 // to the browser differently. 2963 if (isset($data)) { 2964 $options += array( 2965 'type' => 'file', 2966 'group' => CSS_DEFAULT, 2967 'weight' => 0, 2968 'every_page' => FALSE, 2969 'media' => 'all', 2970 'preprocess' => TRUE, 2971 'data' => $data, 2972 'browsers' => array(), 2973 ); 2974 $options['browsers'] += array( 2975 'IE' => TRUE, 2976 '!IE' => TRUE, 2977 ); 2978 2979 // Files with a query string cannot be preprocessed. 2980 if ($options['type'] === 'file' && $options['preprocess'] && strpos($options['data'], '?') !== FALSE) { 2981 $options['preprocess'] = FALSE; 2982 } 2983 2984 // Always add a tiny value to the weight, to conserve the insertion order. 2985 $options['weight'] += count($css) / 1000; 2986 2987 // Add the data to the CSS array depending on the type. 2988 switch ($options['type']) { 2989 case 'inline': 2990 // For inline stylesheets, we don't want to use the $data as the array 2991 // key as $data could be a very long string of CSS. 2992 $css[] = $options; 2993 break; 2994 default: 2995 // Local and external files must keep their name as the associative key 2996 // so the same CSS file is not be added twice. 2997 $css[$data] = $options; 2998 } 2999 } 3000 3001 return $css; 3002 } 3003 3004 /** 3005 * Returns a themed representation of all stylesheets to attach to the page. 3006 * 3007 * It loads the CSS in order, with 'module' first, then 'theme' afterwards. 3008 * This ensures proper cascading of styles so themes can easily override 3009 * module styles through CSS selectors. 3010 * 3011 * Themes may replace module-defined CSS files by adding a stylesheet with the 3012 * same filename. For example, themes/bartik/system-menus.css would replace 3013 * modules/system/system-menus.css. This allows themes to override complete 3014 * CSS files, rather than specific selectors, when necessary. 3015 * 3016 * If the original CSS file is being overridden by a theme, the theme is 3017 * responsible for supplying an accompanying RTL CSS file to replace the 3018 * module's. 3019 * 3020 * @param $css 3021 * (optional) An array of CSS files. If no array is provided, the default 3022 * stylesheets array is used instead. 3023 * @param $skip_alter 3024 * (optional) If set to TRUE, this function skips calling drupal_alter() on 3025 * $css, useful when the calling function passes a $css array that has already 3026 * been altered. 3027 * 3028 * @return 3029 * A string of XHTML CSS tags. 3030 * 3031 * @see drupal_add_css() 3032 */ 3033 function drupal_get_css($css = NULL, $skip_alter = FALSE) { 3034 if (!isset($css)) { 3035 $css = drupal_add_css(); 3036 } 3037 3038 // Allow modules and themes to alter the CSS items. 3039 if (!$skip_alter) { 3040 drupal_alter('css', $css); 3041 } 3042 3043 // Sort CSS items, so that they appear in the correct order. 3044 uasort($css, 'drupal_sort_css_js'); 3045 3046 // Provide the page with information about the individual CSS files used, 3047 // information not otherwise available when CSS aggregation is enabled. The 3048 // setting is attached later in this function, but is set here, so that CSS 3049 // files removed below are still considered "used" and prevented from being 3050 // added in a later AJAX request. 3051 // Skip if no files were added to the page or jQuery.extend() will overwrite 3052 // the Drupal.settings.ajaxPageState.css object with an empty array. 3053 if (!empty($css)) { 3054 // Cast the array to an object to be on the safe side even if not empty. 3055 $setting['ajaxPageState']['css'] = (object) array_fill_keys(array_keys($css), 1); 3056 } 3057 3058 // Remove the overridden CSS files. Later CSS files override former ones. 3059 $previous_item = array(); 3060 foreach ($css as $key => $item) { 3061 if ($item['type'] == 'file') { 3062 // If defined, force a unique basename for this file. 3063 $basename = isset($item['basename']) ? $item['basename'] : drupal_basename($item['data']); 3064 if (isset($previous_item[$basename])) { 3065 // Remove the previous item that shared the same base name. 3066 unset($css[$previous_item[$basename]]); 3067 } 3068 $previous_item[$basename] = $key; 3069 } 3070 } 3071 3072 // Render the HTML needed to load the CSS. 3073 $styles = array( 3074 '#type' => 'styles', 3075 '#items' => $css, 3076 ); 3077 3078 if (!empty($setting)) { 3079 $styles['#attached']['js'][] = array('type' => 'setting', 'data' => $setting); 3080 } 3081 3082 return drupal_render($styles); 3083 } 3084 3085 /** 3086 * Sorts CSS and JavaScript resources. 3087 * 3088 * Callback for uasort() within: 3089 * - drupal_get_css() 3090 * - drupal_get_js() 3091 * 3092 * This sort order helps optimize front-end performance while providing modules 3093 * and themes with the necessary control for ordering the CSS and JavaScript 3094 * appearing on a page. 3095 * 3096 * @param $a 3097 * First item for comparison. The compared items should be associative arrays 3098 * of member items from drupal_add_css() or drupal_add_js(). 3099 * @param $b 3100 * Second item for comparison. 3101 * 3102 * @see drupal_add_css() 3103 * @see drupal_add_js() 3104 */ 3105 function drupal_sort_css_js($a, $b) { 3106 // First order by group, so that, for example, all items in the CSS_SYSTEM 3107 // group appear before items in the CSS_DEFAULT group, which appear before 3108 // all items in the CSS_THEME group. Modules may create additional groups by 3109 // defining their own constants. 3110 if ($a['group'] < $b['group']) { 3111 return -1; 3112 } 3113 elseif ($a['group'] > $b['group']) { 3114 return 1; 3115 } 3116 // Within a group, order all infrequently needed, page-specific files after 3117 // common files needed throughout the website. Separating this way allows for 3118 // the aggregate file generated for all of the common files to be reused 3119 // across a site visit without being cut by a page using a less common file. 3120 elseif ($a['every_page'] && !$b['every_page']) { 3121 return -1; 3122 } 3123 elseif (!$a['every_page'] && $b['every_page']) { 3124 return 1; 3125 } 3126 // Finally, order by weight. 3127 elseif ($a['weight'] < $b['weight']) { 3128 return -1; 3129 } 3130 elseif ($a['weight'] > $b['weight']) { 3131 return 1; 3132 } 3133 else { 3134 return 0; 3135 } 3136 } 3137 3138 /** 3139 * Default callback to group CSS items. 3140 * 3141 * This function arranges the CSS items that are in the #items property of the 3142 * styles element into groups. Arranging the CSS items into groups serves two 3143 * purposes. When aggregation is enabled, files within a group are aggregated 3144 * into a single file, significantly improving page loading performance by 3145 * minimizing network traffic overhead. When aggregation is disabled, grouping 3146 * allows multiple files to be loaded from a single STYLE tag, enabling sites 3147 * with many modules enabled or a complex theme being used to stay within IE's 3148 * 31 CSS inclusion tag limit: http://drupal.org/node/228818. 3149 * 3150 * This function puts multiple items into the same group if they are groupable 3151 * and if they are for the same 'media' and 'browsers'. Items of the 'file' type 3152 * are groupable if their 'preprocess' flag is TRUE, items of the 'inline' type 3153 * are always groupable, and items of the 'external' type are never groupable. 3154 * This function also ensures that the process of grouping items does not change 3155 * their relative order. This requirement may result in multiple groups for the 3156 * same type, media, and browsers, if needed to accommodate other items in 3157 * between. 3158 * 3159 * @param $css 3160 * An array of CSS items, as returned by drupal_add_css(), but after 3161 * alteration performed by drupal_get_css(). 3162 * 3163 * @return 3164 * An array of CSS groups. Each group contains the same keys (e.g., 'media', 3165 * 'data', etc.) as a CSS item from the $css parameter, with the value of 3166 * each key applying to the group as a whole. Each group also contains an 3167 * 'items' key, which is the subset of items from $css that are in the group. 3168 * 3169 * @see drupal_pre_render_styles() 3170 * @see system_element_info() 3171 */ 3172 function drupal_group_css($css) { 3173 $groups = array(); 3174 // If a group can contain multiple items, we track the information that must 3175 // be the same for each item in the group, so that when we iterate the next 3176 // item, we can determine if it can be put into the current group, or if a 3177 // new group needs to be made for it. 3178 $current_group_keys = NULL; 3179 // When creating a new group, we pre-increment $i, so by initializing it to 3180 // -1, the first group will have index 0. 3181 $i = -1; 3182 foreach ($css as $item) { 3183 // The browsers for which the CSS item needs to be loaded is part of the 3184 // information that determines when a new group is needed, but the order of 3185 // keys in the array doesn't matter, and we don't want a new group if all 3186 // that's different is that order. 3187 ksort($item['browsers']); 3188 3189 // If the item can be grouped with other items, set $group_keys to an array 3190 // of information that must be the same for all items in its group. If the 3191 // item can't be grouped with other items, set $group_keys to FALSE. We 3192 // put items into a group that can be aggregated together: whether they will 3193 // be aggregated is up to the _drupal_css_aggregate() function or an 3194 // override of that function specified in hook_css_alter(), but regardless 3195 // of the details of that function, a group represents items that can be 3196 // aggregated. Since a group may be rendered with a single HTML tag, all 3197 // items in the group must share the same information that would need to be 3198 // part of that HTML tag. 3199 switch ($item['type']) { 3200 case 'file': 3201 // Group file items if their 'preprocess' flag is TRUE. 3202 // Help ensure maximum reuse of aggregate files by only grouping 3203 // together items that share the same 'group' value and 'every_page' 3204 // flag. See drupal_add_css() for details about that. 3205 $group_keys = $item['preprocess'] ? array($item['type'], $item['group'], $item['every_page'], $item['media'], $item['browsers']) : FALSE; 3206 break; 3207 case 'inline': 3208 // Always group inline items. 3209 $group_keys = array($item['type'], $item['media'], $item['browsers']); 3210 break; 3211 case 'external': 3212 // Do not group external items. 3213 $group_keys = FALSE; 3214 break; 3215 } 3216 3217 // If the group keys don't match the most recent group we're working with, 3218 // then a new group must be made. 3219 if ($group_keys !== $current_group_keys) { 3220 $i++; 3221 // Initialize the new group with the same properties as the first item 3222 // being placed into it. The item's 'data' and 'weight' properties are 3223 // unique to the item and should not be carried over to the group. 3224 $groups[$i] = $item; 3225 unset($groups[$i]['data'], $groups[$i]['weight']); 3226 $groups[$i]['items'] = array(); 3227 $current_group_keys = $group_keys ? $group_keys : NULL; 3228 } 3229 3230 // Add the item to the current group. 3231 $groups[$i]['items'][] = $item; 3232 } 3233 return $groups; 3234 } 3235 3236 /** 3237 * Default callback to aggregate CSS files and inline content. 3238 * 3239 * Having the browser load fewer CSS files results in much faster page loads 3240 * than when it loads many small files. This function aggregates files within 3241 * the same group into a single file unless the site-wide setting to do so is 3242 * disabled (commonly the case during site development). To optimize download, 3243 * it also compresses the aggregate files by removing comments, whitespace, and 3244 * other unnecessary content. Additionally, this functions aggregates inline 3245 * content together, regardless of the site-wide aggregation setting. 3246 * 3247 * @param $css_groups 3248 * An array of CSS groups as returned by drupal_group_css(). This function 3249 * modifies the group's 'data' property for each group that is aggregated. 3250 * 3251 * @see drupal_group_css() 3252 * @see drupal_pre_render_styles() 3253 * @see system_element_info() 3254 */ 3255 function drupal_aggregate_css(&$css_groups) { 3256 $preprocess_css = (variable_get('preprocess_css', FALSE) && (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update')); 3257 3258 // For each group that needs aggregation, aggregate its items. 3259 foreach ($css_groups as $key => $group) { 3260 switch ($group['type']) { 3261 // If a file group can be aggregated into a single file, do so, and set 3262 // the group's data property to the file path of the aggregate file. 3263 case 'file': 3264 if ($group['preprocess'] && $preprocess_css) { 3265 $css_groups[$key]['data'] = drupal_build_css_cache($group['items']); 3266 } 3267 break; 3268 // Aggregate all inline CSS content into the group's data property. 3269 case 'inline': 3270 $css_groups[$key]['data'] = ''; 3271 foreach ($group['items'] as $item) { 3272 $css_groups[$key]['data'] .= drupal_load_stylesheet_content($item['data'], $item['preprocess']); 3273 } 3274 break; 3275 } 3276 } 3277 } 3278 3279 /** 3280 * #pre_render callback to add the elements needed for CSS tags to be rendered. 3281 * 3282 * For production websites, LINK tags are preferable to STYLE tags with @import 3283 * statements, because: 3284 * - They are the standard tag intended for linking to a resource. 3285 * - On Firefox 2 and perhaps other browsers, CSS files included with @import 3286 * statements don't get saved when saving the complete web page for offline 3287 * use: http://drupal.org/node/145218. 3288 * - On IE, if only LINK tags and no @import statements are used, all the CSS 3289 * files are downloaded in parallel, resulting in faster page load, but if 3290 * @import statements are used and span across multiple STYLE tags, all the 3291 * ones from one STYLE tag must be downloaded before downloading begins for 3292 * the next STYLE tag. Furthermore, IE7 does not support media declaration on 3293 * the @import statement, so multiple STYLE tags must be used when different 3294 * files are for different media types. Non-IE browsers always download in 3295 * parallel, so this is an IE-specific performance quirk: 3296 * http://www.stevesouders.com/blog/2009/04/09/dont-use-import/. 3297 * 3298 * However, IE has an annoying limit of 31 total CSS inclusion tags 3299 * (http://drupal.org/node/228818) and LINK tags are limited to one file per 3300 * tag, whereas STYLE tags can contain multiple @import statements allowing 3301 * multiple files to be loaded per tag. When CSS aggregation is disabled, a 3302 * Drupal site can easily have more than 31 CSS files that need to be loaded, so 3303 * using LINK tags exclusively would result in a site that would display 3304 * incorrectly in IE. Depending on different needs, different strategies can be 3305 * employed to decide when to use LINK tags and when to use STYLE tags. 3306 * 3307 * The strategy employed by this function is to use LINK tags for all aggregate 3308 * files and for all files that cannot be aggregated (e.g., if 'preprocess' is 3309 * set to FALSE or the type is 'external'), and to use STYLE tags for groups 3310 * of files that could be aggregated together but aren't (e.g., if the site-wide 3311 * aggregation setting is disabled). This results in all LINK tags when 3312 * aggregation is enabled, a guarantee that as many or only slightly more tags 3313 * are used with aggregation disabled than enabled (so that if the limit were to 3314 * be crossed with aggregation enabled, the site developer would also notice the 3315 * problem while aggregation is disabled), and an easy way for a developer to 3316 * view HTML source while aggregation is disabled and know what files will be 3317 * aggregated together when aggregation becomes enabled. 3318 * 3319 * This function evaluates the aggregation enabled/disabled condition on a group 3320 * by group basis by testing whether an aggregate file has been made for the 3321 * group rather than by testing the site-wide aggregation setting. This allows 3322 * this function to work correctly even if modules have implemented custom 3323 * logic for grouping and aggregating files. 3324 * 3325 * @param $element 3326 * A render array containing: 3327 * - '#items': The CSS items as returned by drupal_add_css() and altered by 3328 * drupal_get_css(). 3329 * - '#group_callback': A function to call to group #items to enable the use 3330 * of fewer tags by aggregating files and/or using multiple @import 3331 * statements within a single tag. 3332 * - '#aggregate_callback': A function to call to aggregate the items within 3333 * the groups arranged by the #group_callback function. 3334 * 3335 * @return 3336 * A render array that will render to a string of XHTML CSS tags. 3337 * 3338 * @see drupal_get_css() 3339 */ 3340 function drupal_pre_render_styles($elements) { 3341 // Group and aggregate the items. 3342 if (isset($elements['#group_callback'])) { 3343 $elements['#groups'] = $elements['#group_callback']($elements['#items']); 3344 } 3345 if (isset($elements['#aggregate_callback'])) { 3346 $elements['#aggregate_callback']($elements['#groups']); 3347 } 3348 3349 // A dummy query-string is added to filenames, to gain control over 3350 // browser-caching. The string changes on every update or full cache 3351 // flush, forcing browsers to load a new copy of the files, as the 3352 // URL changed. 3353 $query_string = variable_get('css_js_query_string', '0'); 3354 3355 // For inline CSS to validate as XHTML, all CSS containing XHTML needs to be 3356 // wrapped in CDATA. To make that backwards compatible with HTML 4, we need to 3357 // comment out the CDATA-tag. 3358 $embed_prefix = "\n<!--/*--><![CDATA[/*><!--*/\n"; 3359 $embed_suffix = "\n/*]]>*/-->\n"; 3360 3361 // Defaults for LINK and STYLE elements. 3362 $link_element_defaults = array( 3363 '#type' => 'html_tag', 3364 '#tag' => 'link', 3365 '#attributes' => array( 3366 'type' => 'text/css', 3367 'rel' => 'stylesheet', 3368 ), 3369 ); 3370 $style_element_defaults = array( 3371 '#type' => 'html_tag', 3372 '#tag' => 'style', 3373 '#attributes' => array( 3374 'type' => 'text/css', 3375 ), 3376 ); 3377 3378 // Loop through each group. 3379 foreach ($elements['#groups'] as $group) { 3380 switch ($group['type']) { 3381 // For file items, there are three possibilites. 3382 // - The group has been aggregated: in this case, output a LINK tag for 3383 // the aggregate file. 3384 // - The group can be aggregated but has not been (most likely because 3385 // the site administrator disabled the site-wide setting): in this case, 3386 // output as few STYLE tags for the group as possible, using @import 3387 // statement for each file in the group. This enables us to stay within 3388 // IE's limit of 31 total CSS inclusion tags. 3389 // - The group contains items not eligible for aggregation (their 3390 // 'preprocess' flag has been set to FALSE): in this case, output a LINK 3391 // tag for each file. 3392 case 'file': 3393 // The group has been aggregated into a single file: output a LINK tag 3394 // for the aggregate file. 3395 if (isset($group['data'])) { 3396 $element = $link_element_defaults; 3397 $element['#attributes']['href'] = file_create_url($group['data']); 3398 $element['#attributes']['media'] = $group['media']; 3399 $element['#browsers'] = $group['browsers']; 3400 $elements[] = $element; 3401 } 3402 // The group can be aggregated, but hasn't been: combine multiple items 3403 // into as few STYLE tags as possible. 3404 elseif ($group['preprocess']) { 3405 $import = array(); 3406 foreach ($group['items'] as $item) { 3407 // A theme's .info file may have an entry for a file that doesn't 3408 // exist as a way of overriding a module or base theme CSS file from 3409 // being added to the page. Normally, file_exists() calls that need 3410 // to run for every page request should be minimized, but this one 3411 // is okay, because it only runs when CSS aggregation is disabled. 3412 // On a server under heavy enough load that file_exists() calls need 3413 // to be minimized, CSS aggregation should be enabled, in which case 3414 // this code is not run. When aggregation is enabled, 3415 // drupal_load_stylesheet() checks file_exists(), but only when 3416 // building the aggregate file, which is then reused for many page 3417 // requests. 3418 if (file_exists($item['data'])) { 3419 // The dummy query string needs to be added to the URL to control 3420 // browser-caching. IE7 does not support a media type on the 3421 // @import statement, so we instead specify the media for the 3422 // group on the STYLE tag. 3423 $import[] = '@import url("' . check_plain(file_create_url($item['data']) . '?' . $query_string) . '");'; 3424 } 3425 } 3426 // In addition to IE's limit of 31 total CSS inclusion tags, it also 3427 // has a limit of 31 @import statements per STYLE tag. 3428 while (!empty($import)) { 3429 $import_batch = array_slice($import, 0, 31); 3430 $import = array_slice($import, 31); 3431 $element = $style_element_defaults; 3432 $element['#value'] = implode("\n", $import_batch); 3433 $element['#attributes']['media'] = $group['media']; 3434 $element['#browsers'] = $group['browsers']; 3435 $elements[] = $element; 3436 } 3437 } 3438 // The group contains items ineligible for aggregation: output a LINK 3439 // tag for each file. 3440 else { 3441 foreach ($group['items'] as $item) { 3442 $element = $link_element_defaults; 3443 // We do not check file_exists() here, because this code runs for 3444 // files whose 'preprocess' is set to FALSE, and therefore, even 3445 // when aggregation is enabled, and we want to avoid needlessly 3446 // taxing a server that may be under heavy load. The file_exists() 3447 // performed above for files whose 'preprocess' is TRUE is done for 3448 // the benefit of theme .info files, but code that deals with files 3449 // whose 'preprocess' is FALSE is responsible for ensuring the file 3450 // exists. 3451 // The dummy query string needs to be added to the URL to control 3452 // browser-caching. 3453 $query_string_separator = (strpos($item['data'], '?') !== FALSE) ? '&' : '?'; 3454 $element['#attributes']['href'] = file_create_url($item['data']) . $query_string_separator . $query_string; 3455 $element['#attributes']['media'] = $item['media']; 3456 $element['#browsers'] = $group['browsers']; 3457 $elements[] = $element; 3458 } 3459 } 3460 break; 3461 // For inline content, the 'data' property contains the CSS content. If 3462 // the group's 'data' property is set, then output it in a single STYLE 3463 // tag. Otherwise, output a separate STYLE tag for each item. 3464 case 'inline': 3465 if (isset($group['data'])) { 3466 $element = $style_element_defaults; 3467 $element['#value'] = $group['data']; 3468 $element['#value_prefix'] = $embed_prefix; 3469 $element['#value_suffix'] = $embed_suffix; 3470 $element['#attributes']['media'] = $group['media']; 3471 $element['#browsers'] = $group['browsers']; 3472 $elements[] = $element; 3473 } 3474 else { 3475 foreach ($group['items'] as $item) { 3476 $element = $style_element_defaults; 3477 $element['#value'] = $item['data']; 3478 $element['#value_prefix'] = $embed_prefix; 3479 $element['#value_suffix'] = $embed_suffix; 3480 $element['#attributes']['media'] = $item['media']; 3481 $element['#browsers'] = $group['browsers']; 3482 $elements[] = $element; 3483 } 3484 } 3485 break; 3486 // Output a LINK tag for each external item. The item's 'data' property 3487 // contains the full URL. 3488 case 'external': 3489 foreach ($group['items'] as $item) { 3490 $element = $link_element_defaults; 3491 $element['#attributes']['href'] = $item['data']; 3492 $element['#attributes']['media'] = $item['media']; 3493 $element['#browsers'] = $group['browsers']; 3494 $elements[] = $element; 3495 } 3496 break; 3497 } 3498 } 3499 3500 return $elements; 3501 } 3502 3503 /** 3504 * Aggregates and optimizes CSS files into a cache file in the files directory. 3505 * 3506 * The file name for the CSS cache file is generated from the hash of the 3507 * aggregated contents of the files in $css. This forces proxies and browsers 3508 * to download new CSS when the CSS changes. 3509 * 3510 * The cache file name is retrieved on a page load via a lookup variable that 3511 * contains an associative array. The array key is the hash of the file names 3512 * in $css while the value is the cache file name. The cache file is generated 3513 * in two cases. First, if there is no file name value for the key, which will 3514 * happen if a new file name has been added to $css or after the lookup 3515 * variable is emptied to force a rebuild of the cache. Second, the cache file 3516 * is generated if it is missing on disk. Old cache files are not deleted 3517 * immediately when the lookup variable is emptied, but are deleted after a set 3518 * period by drupal_delete_file_if_stale(). This ensures that files referenced 3519 * by a cached page will still be available. 3520 * 3521 * @param $css 3522 * An array of CSS files to aggregate and compress into one file. 3523 * 3524 * @return 3525 * The URI of the CSS cache file, or FALSE if the file could not be saved. 3526 */ 3527 function drupal_build_css_cache($css) { 3528 $data = ''; 3529 $uri = ''; 3530 $map = variable_get('drupal_css_cache_files', array()); 3531 // Create a new array so that only the file names are used to create the hash. 3532 // This prevents new aggregates from being created unnecessarily. 3533 $css_data = array(); 3534 foreach ($css as $css_file) { 3535 $css_data[] = $css_file['data']; 3536 } 3537 $key = hash('sha256', serialize($css_data)); 3538 if (isset($map[$key])) { 3539 $uri = $map[$key]; 3540 } 3541 3542 if (empty($uri) || !file_exists($uri)) { 3543 // Build aggregate CSS file. 3544 foreach ($css as $stylesheet) { 3545 // Only 'file' stylesheets can be aggregated. 3546 if ($stylesheet['type'] == 'file') { 3547 $contents = drupal_load_stylesheet($stylesheet['data'], TRUE); 3548 3549 // Build the base URL of this CSS file: start with the full URL. 3550 $css_base_url = file_create_url($stylesheet['data']); 3551 // Move to the parent. 3552 $css_base_url = substr($css_base_url, 0, strrpos($css_base_url, '/')); 3553 // Simplify to a relative URL if the stylesheet URL starts with the 3554 // base URL of the website. 3555 if (substr($css_base_url, 0, strlen($GLOBALS['base_root'])) == $GLOBALS['base_root']) { 3556 $css_base_url = substr($css_base_url, strlen($GLOBALS['base_root'])); 3557 } 3558 3559 _drupal_build_css_path(NULL, $css_base_url . '/'); 3560 // Anchor all paths in the CSS with its base URL, ignoring external and absolute paths. 3561 $data .= preg_replace_callback('/url\(\s*[\'"]?(?![a-z]+:|\/+)([^\'")]+)[\'"]?\s*\)/i', '_drupal_build_css_path', $contents); 3562 } 3563 } 3564 3565 // Per the W3C specification at http://www.w3.org/TR/REC-CSS2/cascade.html#at-import, 3566 // @import rules must proceed any other style, so we move those to the top. 3567 $regexp = '/@import[^;]+;/i'; 3568 preg_match_all($regexp, $data, $matches); 3569 $data = preg_replace($regexp, '', $data); 3570 $data = implode('', $matches[0]) . $data; 3571 3572 // Prefix filename to prevent blocking by firewalls which reject files 3573 // starting with "ad*". 3574 $filename = 'css_' . drupal_hash_base64($data) . '.css'; 3575 // Create the css/ within the files folder. 3576 $csspath = 'public://css'; 3577 $uri = $csspath . '/' . $filename; 3578 // Create the CSS file. 3579 file_prepare_directory($csspath, FILE_CREATE_DIRECTORY); 3580 if (!file_exists($uri) && !file_unmanaged_save_data($data, $uri, FILE_EXISTS_REPLACE)) { 3581 return FALSE; 3582 } 3583 // If CSS gzip compression is enabled, clean URLs are enabled (which means 3584 // that rewrite rules are working) and the zlib extension is available then 3585 // create a gzipped version of this file. This file is served conditionally 3586 // to browsers that accept gzip using .htaccess rules. 3587 if (variable_get('css_gzip_compression', TRUE) && variable_get('clean_url', 0) && extension_loaded('zlib')) { 3588 if (!file_exists($uri . '.gz') && !file_unmanaged_save_data(gzencode($data, 9, FORCE_GZIP), $uri . '.gz', FILE_EXISTS_REPLACE)) { 3589 return FALSE; 3590 } 3591 } 3592 // Save the updated map. 3593 $map[$key] = $uri; 3594 variable_set('drupal_css_cache_files', $map); 3595 } 3596 return $uri; 3597 } 3598 3599 /** 3600 * Prefixes all paths within a CSS file for drupal_build_css_cache(). 3601 */ 3602 function _drupal_build_css_path($matches, $base = NULL) { 3603 $_base = &drupal_static(__FUNCTION__); 3604 // Store base path for preg_replace_callback. 3605 if (isset($base)) { 3606 $_base = $base; 3607 } 3608 3609 // Prefix with base and remove '../' segments where possible. 3610 $path = $_base . $matches[1]; 3611 $last = ''; 3612 while ($path != $last) { 3613 $last = $path; 3614 $path = preg_replace('`(^|/)(?!\.\./)([^/]+)/\.\./`', '$1', $path); 3615 } 3616 return 'url(' . $path . ')'; 3617 } 3618 3619 /** 3620 * Loads the stylesheet and resolves all @import commands. 3621 * 3622 * Loads a stylesheet and replaces @import commands with the contents of the 3623 * imported file. Use this instead of file_get_contents when processing 3624 * stylesheets. 3625 * 3626 * The returned contents are compressed removing white space and comments only 3627 * when CSS aggregation is enabled. This optimization will not apply for 3628 * color.module enabled themes with CSS aggregation turned off. 3629 * 3630 * @param $file 3631 * Name of the stylesheet to be processed. 3632 * @param $optimize 3633 * Defines if CSS contents should be compressed or not. 3634 * @param $reset_basepath 3635 * Used internally to facilitate recursive resolution of @import commands. 3636 * 3637 * @return 3638 * Contents of the stylesheet, including any resolved @import commands. 3639 */ 3640 function drupal_load_stylesheet($file, $optimize = NULL, $reset_basepath = TRUE) { 3641 // These statics are not cache variables, so we don't use drupal_static(). 3642 static $_optimize, $basepath; 3643 if ($reset_basepath) { 3644 $basepath = ''; 3645 } 3646 // Store the value of $optimize for preg_replace_callback with nested 3647 // @import loops. 3648 if (isset($optimize)) { 3649 $_optimize = $optimize; 3650 } 3651 3652 // Stylesheets are relative one to each other. Start by adding a base path 3653 // prefix provided by the parent stylesheet (if necessary). 3654 if ($basepath && !file_uri_scheme($file)) { 3655 $file = $basepath . '/' . $file; 3656 } 3657 $basepath = dirname($file); 3658 3659 // Load the CSS stylesheet. We suppress errors because themes may specify 3660 // stylesheets in their .info file that don't exist in the theme's path, 3661 // but are merely there to disable certain module CSS files. 3662 if ($contents = @file_get_contents($file)) { 3663 // Return the processed stylesheet. 3664 return drupal_load_stylesheet_content($contents, $_optimize); 3665 } 3666 3667 return ''; 3668 } 3669 3670 /** 3671 * Processes the contents of a stylesheet for aggregation. 3672 * 3673 * @param $contents 3674 * The contents of the stylesheet. 3675 * @param $optimize 3676 * (optional) Boolean whether CSS contents should be minified. Defaults to 3677 * FALSE. 3678 * 3679 * @return 3680 * Contents of the stylesheet including the imported stylesheets. 3681 */ 3682 function drupal_load_stylesheet_content($contents, $optimize = FALSE) { 3683 // Remove multiple charset declarations for standards compliance (and fixing Safari problems). 3684 $contents = preg_replace('/^@charset\s+[\'"](\S*)\b[\'"];/i', '', $contents); 3685 3686 if ($optimize) { 3687 // Perform some safe CSS optimizations. 3688 // Regexp to match comment blocks. 3689 $comment = '/\*[^*]*\*+(?:[^/*][^*]*\*+)*/'; 3690 // Regexp to match double quoted strings. 3691 $double_quot = '"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"'; 3692 // Regexp to match single quoted strings. 3693 $single_quot = "'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'"; 3694 // Strip all comment blocks, but keep double/single quoted strings. 3695 $contents = preg_replace( 3696 "<($double_quot|$single_quot)|$comment>Ss", 3697 "$1", 3698 $contents 3699 ); 3700 // Remove certain whitespace. 3701 // There are different conditions for removing leading and trailing 3702 // whitespace. 3703 // @see http://php.net/manual/en/regexp.reference.subpatterns.php 3704 $contents = preg_replace('< 3705 # Strip leading and trailing whitespace. 3706 \s*([@{};,])\s* 3707 # Strip only leading whitespace from: 3708 # - Closing parenthesis: Retain "@media (bar) and foo". 3709 | \s+([\)]) 3710 # Strip only trailing whitespace from: 3711 # - Opening parenthesis: Retain "@media (bar) and foo". 3712 # - Colon: Retain :pseudo-selectors. 3713 | ([\(:])\s+ 3714 >xS', 3715 // Only one of the three capturing groups will match, so its reference 3716 // will contain the wanted value and the references for the 3717 // two non-matching groups will be replaced with empty strings. 3718 '$1$2$3', 3719 $contents 3720 ); 3721 // End the file with a new line. 3722 $contents = trim($contents); 3723 $contents .= "\n"; 3724 } 3725 3726 // Replaces @import commands with the actual stylesheet content. 3727 // This happens recursively but omits external files. 3728 $contents = preg_replace_callback('/@import\s*(?:url\(\s*)?[\'"]?(?![a-z]+:)([^\'"\()]+)[\'"]?\s*\)?\s*;/', '_drupal_load_stylesheet', $contents); 3729 return $contents; 3730 } 3731 3732 /** 3733 * Loads stylesheets recursively and returns contents with corrected paths. 3734 * 3735 * This function is used for recursive loading of stylesheets and 3736 * returns the stylesheet content with all url() paths corrected. 3737 */ 3738 function _drupal_load_stylesheet($matches) { 3739 $filename = $matches[1]; 3740 // Load the imported stylesheet and replace @import commands in there as well. 3741 $file = drupal_load_stylesheet($filename, NULL, FALSE); 3742 3743 // Determine the file's directory. 3744 $directory = dirname($filename); 3745 // If the file is in the current directory, make sure '.' doesn't appear in 3746 // the url() path. 3747 $directory = $directory == '.' ? '' : $directory .'/'; 3748 3749 // Alter all internal url() paths. Leave external paths alone. We don't need 3750 // to normalize absolute paths here (i.e. remove folder/... segments) because 3751 // that will be done later. 3752 return preg_replace('/url\(\s*([\'"]?)(?![a-z]+:|\/+)/i', 'url(\1'. $directory, $file); 3753 } 3754 3755 /** 3756 * Deletes old cached CSS files. 3757 */ 3758 function drupal_clear_css_cache() { 3759 variable_del('drupal_css_cache_files'); 3760 file_scan_directory('public://css', '/.*/', array('callback' => 'drupal_delete_file_if_stale')); 3761 } 3762 3763 /** 3764 * Callback to delete files modified more than a set time ago. 3765 */ 3766 function drupal_delete_file_if_stale($uri) { 3767 // Default stale file threshold is 30 days. 3768 if (REQUEST_TIME - filemtime($uri) > variable_get('drupal_stale_file_threshold', 2592000)) { 3769 file_unmanaged_delete($uri); 3770 } 3771 } 3772 3773 /** 3774 * Prepares a string for use as a CSS identifier (element, class, or ID name). 3775 * 3776 * http://www.w3.org/TR/CSS21/syndata.html#characters shows the syntax for valid 3777 * CSS identifiers (including element names, classes, and IDs in selectors.) 3778 * 3779 * @param $identifier 3780 * The identifier to clean. 3781 * @param $filter 3782 * An array of string replacements to use on the identifier. 3783 * 3784 * @return 3785 * The cleaned identifier. 3786 */ 3787 function drupal_clean_css_identifier($identifier, $filter = array(' ' => '-', '_' => '-', '/' => '-', '[' => '-', ']' => '')) { 3788 // By default, we filter using Drupal's coding standards. 3789 $identifier = strtr($identifier, $filter); 3790 3791 // Valid characters in a CSS identifier are: 3792 // - the hyphen (U+002D) 3793 // - a-z (U+0030 - U+0039) 3794 // - A-Z (U+0041 - U+005A) 3795 // - the underscore (U+005F) 3796 // - 0-9 (U+0061 - U+007A) 3797 // - ISO 10646 characters U+00A1 and higher 3798 // We strip out any character not in the above list. 3799 $identifier = preg_replace('/[^\x{002D}\x{0030}-\x{0039}\x{0041}-\x{005A}\x{005F}\x{0061}-\x{007A}\x{00A1}-\x{FFFF}]/u', '', $identifier); 3800 3801 return $identifier; 3802 } 3803 3804 /** 3805 * Prepares a string for use as a valid class name. 3806 * 3807 * Do not pass one string containing multiple classes as they will be 3808 * incorrectly concatenated with dashes, i.e. "one two" will become "one-two". 3809 * 3810 * @param $class 3811 * The class name to clean. 3812 * 3813 * @return 3814 * The cleaned class name. 3815 */ 3816 function drupal_html_class($class) { 3817 return drupal_clean_css_identifier(drupal_strtolower($class)); 3818 } 3819 3820 /** 3821 * Prepares a string for use as a valid HTML ID and guarantees uniqueness. 3822 * 3823 * This function ensures that each passed HTML ID value only exists once on the 3824 * page. By tracking the already returned ids, this function enables forms, 3825 * blocks, and other content to be output multiple times on the same page, 3826 * without breaking (X)HTML validation. 3827 * 3828 * For already existing IDs, a counter is appended to the ID string. Therefore, 3829 * JavaScript and CSS code should not rely on any value that was generated by 3830 * this function and instead should rely on manually added CSS classes or 3831 * similarly reliable constructs. 3832 * 3833 * Two consecutive hyphens separate the counter from the original ID. To manage 3834 * uniqueness across multiple Ajax requests on the same page, Ajax requests 3835 * POST an array of all IDs currently present on the page, which are used to 3836 * prime this function's cache upon first invocation. 3837 * 3838 * To allow reverse-parsing of IDs submitted via Ajax, any multiple consecutive 3839 * hyphens in the originally passed $id are replaced with a single hyphen. 3840 * 3841 * @param $id 3842 * The ID to clean. 3843 * 3844 * @return 3845 * The cleaned ID. 3846 */ 3847 function drupal_html_id($id) { 3848 // If this is an Ajax request, then content returned by this page request will 3849 // be merged with content already on the base page. The HTML IDs must be 3850 // unique for the fully merged content. Therefore, initialize $seen_ids to 3851 // take into account IDs that are already in use on the base page. 3852 $seen_ids_init = &drupal_static(__FUNCTION__ . ':init'); 3853 if (!isset($seen_ids_init)) { 3854 // Ideally, Drupal would provide an API to persist state information about 3855 // prior page requests in the database, and we'd be able to add this 3856 // function's $seen_ids static variable to that state information in order 3857 // to have it properly initialized for this page request. However, no such 3858 // page state API exists, so instead, ajax.js adds all of the in-use HTML 3859 // IDs to the POST data of Ajax submissions. Direct use of $_POST is 3860 // normally not recommended as it could open up security risks, but because 3861 // the raw POST data is cast to a number before being returned by this 3862 // function, this usage is safe. 3863 if (empty($_POST['ajax_html_ids'])) { 3864 $seen_ids_init = array(); 3865 } 3866 else { 3867 // This function ensures uniqueness by appending a counter to the base id 3868 // requested by the calling function after the first occurrence of that 3869 // requested id. $_POST['ajax_html_ids'] contains the ids as they were 3870 // returned by this function, potentially with the appended counter, so 3871 // we parse that to reconstruct the $seen_ids array. 3872 foreach ($_POST['ajax_html_ids'] as $seen_id) { 3873 // We rely on '--' being used solely for separating a base id from the 3874 // counter, which this function ensures when returning an id. 3875 $parts = explode('--', $seen_id, 2); 3876 if (!empty($parts[1]) && is_numeric($parts[1])) { 3877 list($seen_id, $i) = $parts; 3878 } 3879 else { 3880 $i = 1; 3881 } 3882 if (!isset($seen_ids_init[$seen_id]) || ($i > $seen_ids_init[$seen_id])) { 3883 $seen_ids_init[$seen_id] = $i; 3884 } 3885 } 3886 } 3887 } 3888 $seen_ids = &drupal_static(__FUNCTION__, $seen_ids_init); 3889 3890 $id = strtr(drupal_strtolower($id), array(' ' => '-', '_' => '-', '[' => '-', ']' => '')); 3891 3892 // As defined in http://www.w3.org/TR/html4/types.html#type-name, HTML IDs can 3893 // only contain letters, digits ([0-9]), hyphens ("-"), underscores ("_"), 3894 // colons (":"), and periods ("."). We strip out any character not in that 3895 // list. Note that the CSS spec doesn't allow colons or periods in identifiers 3896 // (http://www.w3.org/TR/CSS21/syndata.html#characters), so we strip those two 3897 // characters as well. 3898 $id = preg_replace('/[^A-Za-z0-9\-_]/', '', $id); 3899 3900 // Removing multiple consecutive hyphens. 3901 $id = preg_replace('/\-+/', '-', $id); 3902 // Ensure IDs are unique by appending a counter after the first occurrence. 3903 // The counter needs to be appended with a delimiter that does not exist in 3904 // the base ID. Requiring a unique delimiter helps ensure that we really do 3905 // return unique IDs and also helps us re-create the $seen_ids array during 3906 // Ajax requests. 3907 if (isset($seen_ids[$id])) { 3908 $id = $id . '--' . ++$seen_ids[$id]; 3909 } 3910 else { 3911 $seen_ids[$id] = 1; 3912 } 3913 3914 return $id; 3915 } 3916 3917 /** 3918 * Provides a standard HTML class name that identifies a page region. 3919 * 3920 * It is recommended that template preprocess functions apply this class to any 3921 * page region that is output by the theme (Drupal core already handles this in 3922 * the standard template preprocess implementation). Standardizing the class 3923 * names in this way allows modules to implement certain features, such as 3924 * drag-and-drop or dynamic Ajax loading, in a theme-independent way. 3925 * 3926 * @param $region 3927 * The name of the page region (for example, 'page_top' or 'content'). 3928 * 3929 * @return 3930 * An HTML class that identifies the region (for example, 'region-page-top' 3931 * or 'region-content'). 3932 * 3933 * @see template_preprocess_region() 3934 */ 3935 function drupal_region_class($region) { 3936 return drupal_html_class("region-$region"); 3937 } 3938 3939 /** 3940 * Adds a JavaScript file, setting, or inline code to the page. 3941 * 3942 * The behavior of this function depends on the parameters it is called with. 3943 * Generally, it handles the addition of JavaScript to the page, either as 3944 * reference to an existing file or as inline code. The following actions can be 3945 * performed using this function: 3946 * - Add a file ('file'): Adds a reference to a JavaScript file to the page. 3947 * - Add inline JavaScript code ('inline'): Executes a piece of JavaScript code 3948 * on the current page by placing the code directly in the page (for example, 3949 * to tell the user that a new message arrived, by opening a pop up, alert 3950 * box, etc.). This should only be used for JavaScript that cannot be executed 3951 * from a file. When adding inline code, make sure that you are not relying on 3952 * $() being the jQuery function. Wrap your code in 3953 * @code (function ($) {... })(jQuery); @endcode 3954 * or use jQuery() instead of $(). 3955 * - Add external JavaScript ('external'): Allows the inclusion of external 3956 * JavaScript files that are not hosted on the local server. Note that these 3957 * external JavaScript references do not get aggregated when preprocessing is 3958 * on. 3959 * - Add settings ('setting'): Adds settings to Drupal's global storage of 3960 * JavaScript settings. Per-page settings are required by some modules to 3961 * function properly. All settings will be accessible at Drupal.settings. 3962 * 3963 * Examples: 3964 * @code 3965 * drupal_add_js('misc/collapse.js'); 3966 * drupal_add_js('misc/collapse.js', 'file'); 3967 * drupal_add_js('jQuery(document).ready(function () { alert("Hello!"); });', 'inline'); 3968 * drupal_add_js('jQuery(document).ready(function () { alert("Hello!"); });', 3969 * array('type' => 'inline', 'scope' => 'footer', 'weight' => 5) 3970 * ); 3971 * drupal_add_js('http://example.com/example.js', 'external'); 3972 * drupal_add_js(array('myModule' => array('key' => 'value')), 'setting'); 3973 * @endcode 3974 * 3975 * Calling drupal_static_reset('drupal_add_js') will clear all JavaScript added 3976 * so far. 3977 * 3978 * If JavaScript aggregation is enabled, all JavaScript files added with 3979 * $options['preprocess'] set to TRUE will be merged into one aggregate file. 3980 * Preprocessed inline JavaScript will not be aggregated into this single file. 3981 * Externally hosted JavaScripts are never aggregated. 3982 * 3983 * The reason for aggregating the files is outlined quite thoroughly here: 3984 * http://www.die.net/musings/page_load_time/ "Load fewer external objects. Due 3985 * to request overhead, one bigger file just loads faster than two smaller ones 3986 * half its size." 3987 * 3988 * $options['preprocess'] should be only set to TRUE when a file is required for 3989 * all typical visitors and most pages of a site. It is critical that all 3990 * preprocessed files are added unconditionally on every page, even if the 3991 * files are not needed on a page. This is normally done by calling 3992 * drupal_add_js() in a hook_init() implementation. 3993 * 3994 * Non-preprocessed files should only be added to the page when they are 3995 * actually needed. 3996 * 3997 * @param $data 3998 * (optional) If given, the value depends on the $options parameter, or 3999 * $options['type'] if $options is passed as an associative array: 4000 * - 'file': Path to the file relative to base_path(). 4001 * - 'inline': The JavaScript code that should be placed in the given scope. 4002 * - 'external': The absolute path to an external JavaScript file that is not 4003 * hosted on the local server. These files will not be aggregated if 4004 * JavaScript aggregation is enabled. 4005 * - 'setting': An associative array with configuration options. The array is 4006 * merged directly into Drupal.settings. All modules should wrap their 4007 * actual configuration settings in another variable to prevent conflicts in 4008 * the Drupal.settings namespace. Items added with a string key will replace 4009 * existing settings with that key; items with numeric array keys will be 4010 * added to the existing settings array. 4011 * @param $options 4012 * (optional) A string defining the type of JavaScript that is being added in 4013 * the $data parameter ('file'/'setting'/'inline'/'external'), or an 4014 * associative array. JavaScript settings should always pass the string 4015 * 'setting' only. Other types can have the following elements in the array: 4016 * - type: The type of JavaScript that is to be added to the page. Allowed 4017 * values are 'file', 'inline', 'external' or 'setting'. Defaults 4018 * to 'file'. 4019 * - scope: The location in which you want to place the script. Possible 4020 * values are 'header' or 'footer'. If your theme implements different 4021 * regions, you can also use these. Defaults to 'header'. 4022 * - group: A number identifying the group in which to add the JavaScript. 4023 * Available constants are: 4024 * - JS_LIBRARY: Any libraries, settings, or jQuery plugins. 4025 * - JS_DEFAULT: Any module-layer JavaScript. 4026 * - JS_THEME: Any theme-layer JavaScript. 4027 * The group number serves as a weight: JavaScript within a lower weight 4028 * group is presented on the page before JavaScript within a higher weight 4029 * group. 4030 * - every_page: For optimal front-end performance when aggregation is 4031 * enabled, this should be set to TRUE if the JavaScript is present on every 4032 * page of the website for users for whom it is present at all. This 4033 * defaults to FALSE. It is set to TRUE for JavaScript files that are added 4034 * via module and theme .info files. Modules that add JavaScript within 4035 * hook_init() implementations, or from other code that ensures that the 4036 * JavaScript is added to all website pages, should also set this flag to 4037 * TRUE. All JavaScript files within the same group and that have the 4038 * 'every_page' flag set to TRUE and do not have 'preprocess' set to FALSE 4039 * are aggregated together into a single aggregate file, and that aggregate 4040 * file can be reused across a user's entire site visit, leading to faster 4041 * navigation between pages. However, JavaScript that is only needed on 4042 * pages less frequently visited, can be added by code that only runs for 4043 * those particular pages, and that code should not set the 'every_page' 4044 * flag. This minimizes the size of the aggregate file that the user needs 4045 * to download when first visiting the website. JavaScript without the 4046 * 'every_page' flag is aggregated into a separate aggregate file. This 4047 * other aggregate file is likely to change from page to page, and each new 4048 * aggregate file needs to be downloaded when first encountered, so it 4049 * should be kept relatively small by ensuring that most commonly needed 4050 * JavaScript is added to every page. 4051 * - weight: A number defining the order in which the JavaScript is added to 4052 * the page relative to other JavaScript with the same 'scope', 'group', 4053 * and 'every_page' value. In some cases, the order in which the JavaScript 4054 * is presented on the page is very important. jQuery, for example, must be 4055 * added to the page before any jQuery code is run, so jquery.js uses the 4056 * JS_LIBRARY group and a weight of -20, jquery.once.js (a library drupal.js 4057 * depends on) uses the JS_LIBRARY group and a weight of -19, drupal.js uses 4058 * the JS_LIBRARY group and a weight of -1, other libraries use the 4059 * JS_LIBRARY group and a weight of 0 or higher, and all other scripts use 4060 * one of the other group constants. The exact ordering of JavaScript is as 4061 * follows: 4062 * - First by scope, with 'header' first, 'footer' last, and any other 4063 * scopes provided by a custom theme coming in between, as determined by 4064 * the theme. 4065 * - Then by group. 4066 * - Then by the 'every_page' flag, with TRUE coming before FALSE. 4067 * - Then by weight. 4068 * - Then by the order in which the JavaScript was added. For example, all 4069 * else being the same, JavaScript added by a call to drupal_add_js() that 4070 * happened later in the page request gets added to the page after one for 4071 * which drupal_add_js() happened earlier in the page request. 4072 * - defer: If set to TRUE, the defer attribute is set on the <script> 4073 * tag. Defaults to FALSE. 4074 * - cache: If set to FALSE, the JavaScript file is loaded anew on every page 4075 * call; in other words, it is not cached. Used only when 'type' references 4076 * a JavaScript file. Defaults to TRUE. 4077 * - preprocess: If TRUE and JavaScript aggregation is enabled, the script 4078 * file will be aggregated. Defaults to TRUE. 4079 * 4080 * @return 4081 * The current array of JavaScript files, settings, and in-line code, 4082 * including Drupal defaults, anything previously added with calls to 4083 * drupal_add_js(), and this function call's additions. 4084 * 4085 * @see drupal_get_js() 4086 */ 4087 function drupal_add_js($data = NULL, $options = NULL) { 4088 $javascript = &drupal_static(__FUNCTION__, array()); 4089 4090 // Construct the options, taking the defaults into consideration. 4091 if (isset($options)) { 4092 if (!is_array($options)) { 4093 $options = array('type' => $options); 4094 } 4095 } 4096 else { 4097 $options = array(); 4098 } 4099 $options += drupal_js_defaults($data); 4100 4101 // Preprocess can only be set if caching is enabled. 4102 $options['preprocess'] = $options['cache'] ? $options['preprocess'] : FALSE; 4103 4104 // Tweak the weight so that files of the same weight are included in the 4105 // order of the calls to drupal_add_js(). 4106 $options['weight'] += count($javascript) / 1000; 4107 4108 if (isset($data)) { 4109 // Add jquery.js and drupal.js, as well as the basePath setting, the 4110 // first time a JavaScript file is added. 4111 if (empty($javascript)) { 4112 // url() generates the prefix using hook_url_outbound_alter(). Instead of 4113 // running the hook_url_outbound_alter() again here, extract the prefix 4114 // from url(). 4115 url('', array('prefix' => &$prefix)); 4116 $javascript = array( 4117 'settings' => array( 4118 'data' => array( 4119 array('basePath' => base_path()), 4120 array('pathPrefix' => empty($prefix) ? '' : $prefix), 4121 ), 4122 'type' => 'setting', 4123 'scope' => 'header', 4124 'group' => JS_LIBRARY, 4125 'every_page' => TRUE, 4126 'weight' => 0, 4127 ), 4128 'misc/drupal.js' => array( 4129 'data' => 'misc/drupal.js', 4130 'type' => 'file', 4131 'scope' => 'header', 4132 'group' => JS_LIBRARY, 4133 'every_page' => TRUE, 4134 'weight' => -1, 4135 'preprocess' => TRUE, 4136 'cache' => TRUE, 4137 'defer' => FALSE, 4138 ), 4139 ); 4140 // Register all required libraries. 4141 drupal_add_library('system', 'jquery', TRUE); 4142 drupal_add_library('system', 'jquery.once', TRUE); 4143 } 4144 4145 switch ($options['type']) { 4146 case 'setting': 4147 // All JavaScript settings are placed in the header of the page with 4148 // the library weight so that inline scripts appear afterwards. 4149 $javascript['settings']['data'][] = $data; 4150 break; 4151 4152 case 'inline': 4153 $javascript[] = $options; 4154 break; 4155 4156 default: // 'file' and 'external' 4157 // Local and external files must keep their name as the associative key 4158 // so the same JavaScript file is not added twice. 4159 $javascript[$options['data']] = $options; 4160 } 4161 } 4162 return $javascript; 4163 } 4164 4165 /** 4166 * Constructs an array of the defaults that are used for JavaScript items. 4167 * 4168 * @param $data 4169 * (optional) The default data parameter for the JavaScript item array. 4170 * 4171 * @see drupal_get_js() 4172 * @see drupal_add_js() 4173 */ 4174 function drupal_js_defaults($data = NULL) { 4175 return array( 4176 'type' => 'file', 4177 'group' => JS_DEFAULT, 4178 'every_page' => FALSE, 4179 'weight' => 0, 4180 'scope' => 'header', 4181 'cache' => TRUE, 4182 'defer' => FALSE, 4183 'preprocess' => TRUE, 4184 'version' => NULL, 4185 'data' => $data, 4186 ); 4187 } 4188 4189 /** 4190 * Returns a themed presentation of all JavaScript code for the current page. 4191 * 4192 * References to JavaScript files are placed in a certain order: first, all 4193 * 'core' files, then all 'module' and finally all 'theme' JavaScript files 4194 * are added to the page. Then, all settings are output, followed by 'inline' 4195 * JavaScript code. If running update.php, all preprocessing is disabled. 4196 * 4197 * Note that hook_js_alter(&$javascript) is called during this function call 4198 * to allow alterations of the JavaScript during its presentation. Calls to 4199 * drupal_add_js() from hook_js_alter() will not be added to the output 4200 * presentation. The correct way to add JavaScript during hook_js_alter() 4201 * is to add another element to the $javascript array, deriving from 4202 * drupal_js_defaults(). See locale_js_alter() for an example of this. 4203 * 4204 * @param $scope 4205 * (optional) The scope for which the JavaScript rules should be returned. 4206 * Defaults to 'header'. 4207 * @param $javascript 4208 * (optional) An array with all JavaScript code. Defaults to the default 4209 * JavaScript array for the given scope. 4210 * @param $skip_alter 4211 * (optional) If set to TRUE, this function skips calling drupal_alter() on 4212 * $javascript, useful when the calling function passes a $javascript array 4213 * that has already been altered. 4214 * 4215 * @return 4216 * All JavaScript code segments and includes for the scope as HTML tags. 4217 * 4218 * @see drupal_add_js() 4219 * @see locale_js_alter() 4220 * @see drupal_js_defaults() 4221 */ 4222 function drupal_get_js($scope = 'header', $javascript = NULL, $skip_alter = FALSE) { 4223 if (!isset($javascript)) { 4224 $javascript = drupal_add_js(); 4225 } 4226 if (empty($javascript)) { 4227 return ''; 4228 } 4229 4230 // Allow modules to alter the JavaScript. 4231 if (!$skip_alter) { 4232 drupal_alter('js', $javascript); 4233 } 4234 4235 // Filter out elements of the given scope. 4236 $items = array(); 4237 foreach ($javascript as $key => $item) { 4238 if ($item['scope'] == $scope) { 4239 $items[$key] = $item; 4240 } 4241 } 4242 4243 $output = ''; 4244 // The index counter is used to keep aggregated and non-aggregated files in 4245 // order by weight. 4246 $index = 1; 4247 $processed = array(); 4248 $files = array(); 4249 $preprocess_js = (variable_get('preprocess_js', FALSE) && (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update')); 4250 4251 // A dummy query-string is added to filenames, to gain control over 4252 // browser-caching. The string changes on every update or full cache 4253 // flush, forcing browsers to load a new copy of the files, as the 4254 // URL changed. Files that should not be cached (see drupal_add_js()) 4255 // get REQUEST_TIME as query-string instead, to enforce reload on every 4256 // page request. 4257 $default_query_string = variable_get('css_js_query_string', '0'); 4258 4259 // For inline JavaScript to validate as XHTML, all JavaScript containing 4260 // XHTML needs to be wrapped in CDATA. To make that backwards compatible 4261 // with HTML 4, we need to comment out the CDATA-tag. 4262 $embed_prefix = "\n<!--//--><![CDATA[//><!--\n"; 4263 $embed_suffix = "\n//--><!]]>\n"; 4264 4265 // Since JavaScript may look for arguments in the URL and act on them, some 4266 // third-party code might require the use of a different query string. 4267 $js_version_string = variable_get('drupal_js_version_query_string', 'v='); 4268 4269 // Sort the JavaScript so that it appears in the correct order. 4270 uasort($items, 'drupal_sort_css_js'); 4271 4272 // Provide the page with information about the individual JavaScript files 4273 // used, information not otherwise available when aggregation is enabled. 4274 $setting['ajaxPageState']['js'] = array_fill_keys(array_keys($items), 1); 4275 unset($setting['ajaxPageState']['js']['settings']); 4276 drupal_add_js($setting, 'setting'); 4277 4278 // If we're outputting the header scope, then this might be the final time 4279 // that drupal_get_js() is running, so add the setting to this output as well 4280 // as to the drupal_add_js() cache. If $items['settings'] doesn't exist, it's 4281 // because drupal_get_js() was intentionally passed a $javascript argument 4282 // stripped off settings, potentially in order to override how settings get 4283 // output, so in this case, do not add the setting to this output. 4284 if ($scope == 'header' && isset($items['settings'])) { 4285 $items['settings']['data'][] = $setting; 4286 } 4287 4288 // Loop through the JavaScript to construct the rendered output. 4289 $element = array( 4290 '#tag' => 'script', 4291 '#value' => '', 4292 '#attributes' => array( 4293 'type' => 'text/javascript', 4294 ), 4295 ); 4296 foreach ($items as $item) { 4297 $query_string = empty($item['version']) ? $default_query_string : $js_version_string . $item['version']; 4298 4299 switch ($item['type']) { 4300 case 'setting': 4301 $js_element = $element; 4302 $js_element['#value_prefix'] = $embed_prefix; 4303 $js_element['#value'] = 'jQuery.extend(Drupal.settings, ' . drupal_json_encode(drupal_array_merge_deep_array($item['data'])) . ");"; 4304 $js_element['#value_suffix'] = $embed_suffix; 4305 $output .= theme('html_tag', array('element' => $js_element)); 4306 break; 4307 4308 case 'inline': 4309 $js_element = $element; 4310 if ($item['defer']) { 4311 $js_element['#attributes']['defer'] = 'defer'; 4312 } 4313 $js_element['#value_prefix'] = $embed_prefix; 4314 $js_element['#value'] = $item['data']; 4315 $js_element['#value_suffix'] = $embed_suffix; 4316 $processed[$index++] = theme('html_tag', array('element' => $js_element)); 4317 break; 4318 4319 case 'file': 4320 $js_element = $element; 4321 if (!$item['preprocess'] || !$preprocess_js) { 4322 if ($item['defer']) { 4323 $js_element['#attributes']['defer'] = 'defer'; 4324 } 4325 $query_string_separator = (strpos($item['data'], '?') !== FALSE) ? '&' : '?'; 4326 $js_element['#attributes']['src'] = file_create_url($item['data']) . $query_string_separator . ($item['cache'] ? $query_string : REQUEST_TIME); 4327 $processed[$index++] = theme('html_tag', array('element' => $js_element)); 4328 } 4329 else { 4330 // By increasing the index for each aggregated file, we maintain 4331 // the relative ordering of JS by weight. We also set the key such 4332 // that groups are split by items sharing the same 'group' value and 4333 // 'every_page' flag. While this potentially results in more aggregate 4334 // files, it helps make each one more reusable across a site visit, 4335 // leading to better front-end performance of a website as a whole. 4336 // See drupal_add_js() for details. 4337 $key = 'aggregate_' . $item['group'] . '_' . $item['every_page'] . '_' . $index; 4338 $processed[$key] = ''; 4339 $files[$key][$item['data']] = $item; 4340 } 4341 break; 4342 4343 case 'external': 4344 $js_element = $element; 4345 // Preprocessing for external JavaScript files is ignored. 4346 if ($item['defer']) { 4347 $js_element['#attributes']['defer'] = 'defer'; 4348 } 4349 $js_element['#attributes']['src'] = $item['data']; 4350 $processed[$index++] = theme('html_tag', array('element' => $js_element)); 4351 break; 4352 } 4353 } 4354 4355 // Aggregate any remaining JS files that haven't already been output. 4356 if ($preprocess_js && count($files) > 0) { 4357 foreach ($files as $key => $file_set) { 4358 $uri = drupal_build_js_cache($file_set); 4359 // Only include the file if was written successfully. Errors are logged 4360 // using watchdog. 4361 if ($uri) { 4362 $preprocess_file = file_create_url($uri); 4363 $js_element = $element; 4364 $js_element['#attributes']['src'] = $preprocess_file; 4365 $processed[$key] = theme('html_tag', array('element' => $js_element)); 4366 } 4367 } 4368 } 4369 4370 // Keep the order of JS files consistent as some are preprocessed and others are not. 4371 // Make sure any inline or JS setting variables appear last after libraries have loaded. 4372 return implode('', $processed) . $output; 4373 } 4374 4375 /** 4376 * Adds attachments to a render() structure. 4377 * 4378 * Libraries, JavaScript, CSS and other types of custom structures are attached 4379 * to elements using the #attached property. The #attached property is an 4380 * associative array, where the keys are the the attachment types and the values 4381 * are the attached data. For example: 4382 * @code 4383 * $build['#attached'] = array( 4384 * 'js' => array(drupal_get_path('module', 'taxonomy') . '/taxonomy.js'), 4385 * 'css' => array(drupal_get_path('module', 'taxonomy') . '/taxonomy.css'), 4386 * ); 4387 * @endcode 4388 * 4389 * 'js', 'css', and 'library' are types that get special handling. For any 4390 * other kind of attached data, the array key must be the full name of the 4391 * callback function and each value an array of arguments. For example: 4392 * @code 4393 * $build['#attached']['drupal_add_http_header'] = array( 4394 * array('Content-Type', 'application/rss+xml; charset=utf-8'), 4395 * ); 4396 * @endcode 4397 * 4398 * External 'js' and 'css' files can also be loaded. For example: 4399 * @code 4400 * $build['#attached']['js'] = array( 4401 * 'http://code.jquery.com/jquery-1.4.2.min.js' => array( 4402 * 'type' => 'external', 4403 * ), 4404 * ); 4405 * @endcode 4406 * 4407 * @param $elements 4408 * The structured array describing the data being rendered. 4409 * @param $group 4410 * The default group of JavaScript and CSS being added. This is only applied 4411 * to the stylesheets and JavaScript items that don't have an explicit group 4412 * assigned to them. 4413 * @param $dependency_check 4414 * When TRUE, will exit if a given library's dependencies are missing. When 4415 * set to FALSE, will continue to add the libraries, even though one or more 4416 * dependencies are missing. Defaults to FALSE. 4417 * @param $every_page 4418 * Set to TRUE to indicate that the attachments are added to every page on the 4419 * site. Only attachments with the every_page flag set to TRUE can participate 4420 * in JavaScript/CSS aggregation. 4421 * 4422 * @return 4423 * FALSE if there were any missing library dependencies; TRUE if all library 4424 * dependencies were met. 4425 * 4426 * @see drupal_add_library() 4427 * @see drupal_add_js() 4428 * @see drupal_add_css() 4429 * @see drupal_render() 4430 */ 4431 function drupal_process_attached($elements, $group = JS_DEFAULT, $dependency_check = FALSE, $every_page = NULL) { 4432 // Add defaults to the special attached structures that should be processed differently. 4433 $elements['#attached'] += array( 4434 'library' => array(), 4435 'js' => array(), 4436 'css' => array(), 4437 ); 4438 4439 // Add the libraries first. 4440 $success = TRUE; 4441 foreach ($elements['#attached']['library'] as $library) { 4442 if (drupal_add_library($library[0], $library[1], $every_page) === FALSE) { 4443 $success = FALSE; 4444 // Exit if the dependency is missing. 4445 if ($dependency_check) { 4446 return $success; 4447 } 4448 } 4449 } 4450 unset($elements['#attached']['library']); 4451 4452 // Add both the JavaScript and the CSS. 4453 // The parameters for drupal_add_js() and drupal_add_css() require special 4454 // handling. 4455 foreach (array('js', 'css') as $type) { 4456 foreach ($elements['#attached'][$type] as $data => $options) { 4457 // If the value is not an array, it's a filename and passed as first 4458 // (and only) argument. 4459 if (!is_array($options)) { 4460 $data = $options; 4461 $options = NULL; 4462 } 4463 // In some cases, the first parameter ($data) is an array. Arrays can't be 4464 // passed as keys in PHP, so we have to get $data from the value array. 4465 if (is_numeric($data)) { 4466 $data = $options['data']; 4467 unset($options['data']); 4468 } 4469 // Apply the default group if it isn't explicitly given. 4470 if (!isset($options['group'])) { 4471 $options['group'] = $group; 4472 } 4473 // Set the every_page flag if one was passed. 4474 if (isset($every_page)) { 4475 $options['every_page'] = $every_page; 4476 } 4477 call_user_func('drupal_add_' . $type, $data, $options); 4478 } 4479 unset($elements['#attached'][$type]); 4480 } 4481 4482 // Add additional types of attachments specified in the render() structure. 4483 // Libraries, JavaScript and CSS have been added already, as they require 4484 // special handling. 4485 foreach ($elements['#attached'] as $callback => $options) { 4486 if (function_exists($callback)) { 4487 foreach ($elements['#attached'][$callback] as $args) { 4488 call_user_func_array($callback, $args); 4489 } 4490 } 4491 } 4492 4493 return $success; 4494 } 4495 4496 /** 4497 * Adds JavaScript to change the state of an element based on another element. 4498 * 4499 * A "state" means a certain property on a DOM element, such as "visible" or 4500 * "checked". A state can be applied to an element, depending on the state of 4501 * another element on the page. In general, states depend on HTML attributes and 4502 * DOM element properties, which change due to user interaction. 4503 * 4504 * Since states are driven by JavaScript only, it is important to understand 4505 * that all states are applied on presentation only, none of the states force 4506 * any server-side logic, and that they will not be applied for site visitors 4507 * without JavaScript support. All modules implementing states have to make 4508 * sure that the intended logic also works without JavaScript being enabled. 4509 * 4510 * #states is an associative array in the form of: 4511 * @code 4512 * array( 4513 * STATE1 => CONDITIONS_ARRAY1, 4514 * STATE2 => CONDITIONS_ARRAY2, 4515 * ... 4516 * ) 4517 * @endcode 4518 * Each key is the name of a state to apply to the element, such as 'visible'. 4519 * Each value is a list of conditions that denote when the state should be 4520 * applied. 4521 * 4522 * Multiple different states may be specified to act on complex conditions: 4523 * @code 4524 * array( 4525 * 'visible' => CONDITIONS, 4526 * 'checked' => OTHER_CONDITIONS, 4527 * ) 4528 * @endcode 4529 * 4530 * Every condition is a key/value pair, whose key is a jQuery selector that 4531 * denotes another element on the page, and whose value is an array of 4532 * conditions, which must bet met on that element: 4533 * @code 4534 * array( 4535 * 'visible' => array( 4536 * JQUERY_SELECTOR => REMOTE_CONDITIONS, 4537 * JQUERY_SELECTOR => REMOTE_CONDITIONS, 4538 * ... 4539 * ), 4540 * ) 4541 * @endcode 4542 * All conditions must be met for the state to be applied. 4543 * 4544 * Each remote condition is a key/value pair specifying conditions on the other 4545 * element that need to be met to apply the state to the element: 4546 * @code 4547 * array( 4548 * 'visible' => array( 4549 * ':input[name="remote_checkbox"]' => array('checked' => TRUE), 4550 * ), 4551 * ) 4552 * @endcode 4553 * 4554 * For example, to show a textfield only when a checkbox is checked: 4555 * @code 4556 * $form['toggle_me'] = array( 4557 * '#type' => 'checkbox', 4558 * '#title' => t('Tick this box to type'), 4559 * ); 4560 * $form['settings'] = array( 4561 * '#type' => 'textfield', 4562 * '#states' => array( 4563 * // Only show this field when the 'toggle_me' checkbox is enabled. 4564 * 'visible' => array( 4565 * ':input[name="toggle_me"]' => array('checked' => TRUE), 4566 * ), 4567 * ), 4568 * ); 4569 * @endcode 4570 * 4571 * The following states may be applied to an element: 4572 * - enabled 4573 * - disabled 4574 * - required 4575 * - optional 4576 * - visible 4577 * - invisible 4578 * - checked 4579 * - unchecked 4580 * - expanded 4581 * - collapsed 4582 * 4583 * The following states may be used in remote conditions: 4584 * - empty 4585 * - filled 4586 * - checked 4587 * - unchecked 4588 * - expanded 4589 * - collapsed 4590 * - value 4591 * 4592 * The following states exist for both elements and remote conditions, but are 4593 * not fully implemented and may not change anything on the element: 4594 * - relevant 4595 * - irrelevant 4596 * - valid 4597 * - invalid 4598 * - touched 4599 * - untouched 4600 * - readwrite 4601 * - readonly 4602 * 4603 * When referencing select lists and radio buttons in remote conditions, a 4604 * 'value' condition must be used: 4605 * @code 4606 * '#states' => array( 4607 * // Show the settings if 'bar' has been selected for 'foo'. 4608 * 'visible' => array( 4609 * ':input[name="foo"]' => array('value' => 'bar'), 4610 * ), 4611 * ), 4612 * @endcode 4613 * 4614 * @param $elements 4615 * A renderable array element having a #states property as described above. 4616 * 4617 * @see form_example_states_form() 4618 */ 4619 function drupal_process_states(&$elements) { 4620 $elements['#attached']['library'][] = array('system', 'drupal.states'); 4621 $elements['#attached']['js'][] = array( 4622 'type' => 'setting', 4623 'data' => array('states' => array('#' . $elements['#id'] => $elements['#states'])), 4624 ); 4625 } 4626 4627 /** 4628 * Adds multiple JavaScript or CSS files at the same time. 4629 * 4630 * A library defines a set of JavaScript and/or CSS files, optionally using 4631 * settings, and optionally requiring another library. For example, a library 4632 * can be a jQuery plugin, a JavaScript framework, or a CSS framework. This 4633 * function allows modules to load a library defined/shipped by itself or a 4634 * depending module, without having to add all files of the library separately. 4635 * Each library is only loaded once. 4636 * 4637 * @param $module 4638 * The name of the module that registered the library. 4639 * @param $name 4640 * The name of the library to add. 4641 * @param $every_page 4642 * Set to TRUE if this library is added to every page on the site. Only items 4643 * with the every_page flag set to TRUE can participate in aggregation. 4644 * 4645 * @return 4646 * TRUE if the library was successfully added; FALSE if the library or one of 4647 * its dependencies could not be added. 4648 * 4649 * @see drupal_get_library() 4650 * @see hook_library() 4651 * @see hook_library_alter() 4652 */ 4653 function drupal_add_library($module, $name, $every_page = NULL) { 4654 $added = &drupal_static(__FUNCTION__, array()); 4655 4656 // Only process the library if it exists and it was not added already. 4657 if (!isset($added[$module][$name])) { 4658 if ($library = drupal_get_library($module, $name)) { 4659 // Add all components within the library. 4660 $elements['#attached'] = array( 4661 'library' => $library['dependencies'], 4662 'js' => $library['js'], 4663 'css' => $library['css'], 4664 ); 4665 $added[$module][$name] = drupal_process_attached($elements, JS_LIBRARY, TRUE, $every_page); 4666 } 4667 else { 4668 // Requested library does not exist. 4669 $added[$module][$name] = FALSE; 4670 } 4671 } 4672 4673 return $added[$module][$name]; 4674 } 4675 4676 /** 4677 * Retrieves information for a JavaScript/CSS library. 4678 * 4679 * Library information is statically cached. Libraries are keyed by module for 4680 * several reasons: 4681 * - Libraries are not unique. Multiple modules might ship with the same library 4682 * in a different version or variant. This registry cannot (and does not 4683 * attempt to) prevent library conflicts. 4684 * - Modules implementing and thereby depending on a library that is registered 4685 * by another module can only rely on that module's library. 4686 * - Two (or more) modules can still register the same library and use it 4687 * without conflicts in case the libraries are loaded on certain pages only. 4688 * 4689 * @param $module 4690 * The name of a module that registered a library. 4691 * @param $name 4692 * (optional) The name of a registered library to retrieve. By default, all 4693 * libraries registered by $module are returned. 4694 * 4695 * @return 4696 * The definition of the requested library, if $name was passed and it exists, 4697 * or FALSE if it does not exist. If no $name was passed, an associative array 4698 * of libraries registered by $module is returned (which may be empty). 4699 * 4700 * @see drupal_add_library() 4701 * @see hook_library() 4702 * @see hook_library_alter() 4703 * 4704 * @todo The purpose of drupal_get_*() is completely different to other page 4705 * requisite API functions; find and use a different name. 4706 */ 4707 function drupal_get_library($module, $name = NULL) { 4708 $libraries = &drupal_static(__FUNCTION__, array()); 4709 4710 if (!isset($libraries[$module])) { 4711 // Retrieve all libraries associated with the module. 4712 $module_libraries = module_invoke($module, 'library'); 4713 if (empty($module_libraries)) { 4714 $module_libraries = array(); 4715 } 4716 // Allow modules to alter the module's registered libraries. 4717 drupal_alter('library', $module_libraries, $module); 4718 4719 foreach ($module_libraries as $key => $data) { 4720 if (is_array($data)) { 4721 // Add default elements to allow for easier processing. 4722 $module_libraries[$key] += array('dependencies' => array(), 'js' => array(), 'css' => array()); 4723 foreach ($module_libraries[$key]['js'] as $file => $options) { 4724 $module_libraries[$key]['js'][$file]['version'] = $module_libraries[$key]['version']; 4725 } 4726 } 4727 } 4728 $libraries[$module] = $module_libraries; 4729 } 4730 if (isset($name)) { 4731 if (!isset($libraries[$module][$name])) { 4732 $libraries[$module][$name] = FALSE; 4733 } 4734 return $libraries[$module][$name]; 4735 } 4736 return $libraries[$module]; 4737 } 4738 4739 /** 4740 * Assists in adding the tableDrag JavaScript behavior to a themed table. 4741 * 4742 * Draggable tables should be used wherever an outline or list of sortable items 4743 * needs to be arranged by an end-user. Draggable tables are very flexible and 4744 * can manipulate the value of form elements placed within individual columns. 4745 * 4746 * To set up a table to use drag and drop in place of weight select-lists or in 4747 * place of a form that contains parent relationships, the form must be themed 4748 * into a table. The table must have an ID attribute set. If using 4749 * theme_table(), the ID may be set as follows: 4750 * @code 4751 * $output = theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'my-module-table'))); 4752 * return $output; 4753 * @endcode 4754 * 4755 * In the theme function for the form, a special class must be added to each 4756 * form element within the same column, "grouping" them together. 4757 * 4758 * In a situation where a single weight column is being sorted in the table, the 4759 * classes could be added like this (in the theme function): 4760 * @code 4761 * $form['my_elements'][$delta]['weight']['#attributes']['class'] = array('my-elements-weight'); 4762 * @endcode 4763 * 4764 * Each row of the table must also have a class of "draggable" in order to 4765 * enable the drag handles: 4766 * @code 4767 * $row = array(...); 4768 * $rows[] = array( 4769 * 'data' => $row, 4770 * 'class' => array('draggable'), 4771 * ); 4772 * @endcode 4773 * 4774 * When tree relationships are present, the two additional classes 4775 * 'tabledrag-leaf' and 'tabledrag-root' can be used to refine the behavior: 4776 * - Rows with the 'tabledrag-leaf' class cannot have child rows. 4777 * - Rows with the 'tabledrag-root' class cannot be nested under a parent row. 4778 * 4779 * Calling drupal_add_tabledrag() would then be written as such: 4780 * @code 4781 * drupal_add_tabledrag('my-module-table', 'order', 'sibling', 'my-elements-weight'); 4782 * @endcode 4783 * 4784 * In a more complex case where there are several groups in one column (such as 4785 * the block regions on the admin/structure/block page), a separate subgroup 4786 * class must also be added to differentiate the groups. 4787 * @code 4788 * $form['my_elements'][$region][$delta]['weight']['#attributes']['class'] = array('my-elements-weight', 'my-elements-weight-' . $region); 4789 * @endcode 4790 * 4791 * $group is still 'my-element-weight', and the additional $subgroup variable 4792 * will be passed in as 'my-elements-weight-' . $region. This also means that 4793 * you'll need to call drupal_add_tabledrag() once for every region added. 4794 * 4795 * @code 4796 * foreach ($regions as $region) { 4797 * drupal_add_tabledrag('my-module-table', 'order', 'sibling', 'my-elements-weight', 'my-elements-weight-' . $region); 4798 * } 4799 * @endcode 4800 * 4801 * In a situation where tree relationships are present, adding multiple 4802 * subgroups is not necessary, because the table will contain indentations that 4803 * provide enough information about the sibling and parent relationships. See 4804 * theme_menu_overview_form() for an example creating a table containing parent 4805 * relationships. 4806 * 4807 * Note that this function should be called from the theme layer, such as in a 4808 * .tpl.php file, theme_ function, or in a template_preprocess function, not in 4809 * a form declaration. Though the same JavaScript could be added to the page 4810 * using drupal_add_js() directly, this function helps keep template files 4811 * clean and readable. It also prevents tabledrag.js from being added twice 4812 * accidentally. 4813 * 4814 * @param $table_id 4815 * String containing the target table's id attribute. If the table does not 4816 * have an id, one will need to be set, such as <table id="my-module-table">. 4817 * @param $action 4818 * String describing the action to be done on the form item. Either 'match' 4819 * 'depth', or 'order'. Match is typically used for parent relationships. 4820 * Order is typically used to set weights on other form elements with the same 4821 * group. Depth updates the target element with the current indentation. 4822 * @param $relationship 4823 * String describing where the $action variable should be performed. Either 4824 * 'parent', 'sibling', 'group', or 'self'. Parent will only look for fields 4825 * up the tree. Sibling will look for fields in the same group in rows above 4826 * and below it. Self affects the dragged row itself. Group affects the 4827 * dragged row, plus any children below it (the entire dragged group). 4828 * @param $group 4829 * A class name applied on all related form elements for this action. 4830 * @param $subgroup 4831 * (optional) If the group has several subgroups within it, this string should 4832 * contain the class name identifying fields in the same subgroup. 4833 * @param $source 4834 * (optional) If the $action is 'match', this string should contain the class 4835 * name identifying what field will be used as the source value when matching 4836 * the value in $subgroup. 4837 * @param $hidden 4838 * (optional) The column containing the field elements may be entirely hidden 4839 * from view dynamically when the JavaScript is loaded. Set to FALSE if the 4840 * column should not be hidden. 4841 * @param $limit 4842 * (optional) Limit the maximum amount of parenting in this table. 4843 * @see block-admin-display-form.tpl.php 4844 * @see theme_menu_overview_form() 4845 */ 4846 function drupal_add_tabledrag($table_id, $action, $relationship, $group, $subgroup = NULL, $source = NULL, $hidden = TRUE, $limit = 0) { 4847 $js_added = &drupal_static(__FUNCTION__, FALSE); 4848 if (!$js_added) { 4849 // Add the table drag JavaScript to the page before the module JavaScript 4850 // to ensure that table drag behaviors are registered before any module 4851 // uses it. 4852 drupal_add_library('system', 'jquery.cookie'); 4853 drupal_add_js('misc/tabledrag.js', array('weight' => -1)); 4854 $js_added = TRUE; 4855 } 4856 4857 // If a subgroup or source isn't set, assume it is the same as the group. 4858 $target = isset($subgroup) ? $subgroup : $group; 4859 $source = isset($source) ? $source : $target; 4860 $settings['tableDrag'][$table_id][$group][] = array( 4861 'target' => $target, 4862 'source' => $source, 4863 'relationship' => $relationship, 4864 'action' => $action, 4865 'hidden' => $hidden, 4866 'limit' => $limit, 4867 ); 4868 drupal_add_js($settings, 'setting'); 4869 } 4870 4871 /** 4872 * Aggregates JavaScript files into a cache file in the files directory. 4873 * 4874 * The file name for the JavaScript cache file is generated from the hash of 4875 * the aggregated contents of the files in $files. This forces proxies and 4876 * browsers to download new JavaScript when the JavaScript changes. 4877 * 4878 * The cache file name is retrieved on a page load via a lookup variable that 4879 * contains an associative array. The array key is the hash of the names in 4880 * $files while the value is the cache file name. The cache file is generated 4881 * in two cases. First, if there is no file name value for the key, which will 4882 * happen if a new file name has been added to $files or after the lookup 4883 * variable is emptied to force a rebuild of the cache. Second, the cache file 4884 * is generated if it is missing on disk. Old cache files are not deleted 4885 * immediately when the lookup variable is emptied, but are deleted after a set 4886 * period by drupal_delete_file_if_stale(). This ensures that files referenced 4887 * by a cached page will still be available. 4888 * 4889 * @param $files 4890 * An array of JavaScript files to aggregate and compress into one file. 4891 * 4892 * @return 4893 * The URI of the cache file, or FALSE if the file could not be saved. 4894 */ 4895 function drupal_build_js_cache($files) { 4896 $contents = ''; 4897 $uri = ''; 4898 $map = variable_get('drupal_js_cache_files', array()); 4899 // Create a new array so that only the file names are used to create the hash. 4900 // This prevents new aggregates from being created unnecessarily. 4901 $js_data = array(); 4902 foreach ($files as $file) { 4903 $js_data[] = $file['data']; 4904 } 4905 $key = hash('sha256', serialize($js_data)); 4906 if (isset($map[$key])) { 4907 $uri = $map[$key]; 4908 } 4909 4910 if (empty($uri) || !file_exists($uri)) { 4911 // Build aggregate JS file. 4912 foreach ($files as $path => $info) { 4913 if ($info['preprocess']) { 4914 // Append a ';' and a newline after each JS file to prevent them from running together. 4915 $contents .= file_get_contents($path) . ";\n"; 4916 } 4917 } 4918 // Prefix filename to prevent blocking by firewalls which reject files 4919 // starting with "ad*". 4920 $filename = 'js_' . drupal_hash_base64($contents) . '.js'; 4921 // Create the js/ within the files folder. 4922 $jspath = 'public://js'; 4923 $uri = $jspath . '/' . $filename; 4924 // Create the JS file. 4925 file_prepare_directory($jspath, FILE_CREATE_DIRECTORY); 4926 if (!file_exists($uri) && !file_unmanaged_save_data($contents, $uri, FILE_EXISTS_REPLACE)) { 4927 return FALSE; 4928 } 4929 // If JS gzip compression is enabled, clean URLs are enabled (which means 4930 // that rewrite rules are working) and the zlib extension is available then 4931 // create a gzipped version of this file. This file is served conditionally 4932 // to browsers that accept gzip using .htaccess rules. 4933 if (variable_get('js_gzip_compression', TRUE) && variable_get('clean_url', 0) && extension_loaded('zlib')) { 4934 if (!file_exists($uri . '.gz') && !file_unmanaged_save_data(gzencode($contents, 9, FORCE_GZIP), $uri . '.gz', FILE_EXISTS_REPLACE)) { 4935 return FALSE; 4936 } 4937 } 4938 $map[$key] = $uri; 4939 variable_set('drupal_js_cache_files', $map); 4940 } 4941 return $uri; 4942 } 4943 4944 /** 4945 * Deletes old cached JavaScript files and variables. 4946 */ 4947 function drupal_clear_js_cache() { 4948 variable_del('javascript_parsed'); 4949 variable_del('drupal_js_cache_files'); 4950 file_scan_directory('public://js', '/.*/', array('callback' => 'drupal_delete_file_if_stale')); 4951 } 4952 4953 /** 4954 * Converts a PHP variable into its JavaScript equivalent. 4955 * 4956 * We use HTML-safe strings, with several characters escaped. 4957 * 4958 * @see drupal_json_decode() 4959 * @see drupal_json_encode_helper() 4960 * @ingroup php_wrappers 4961 */ 4962 function drupal_json_encode($var) { 4963 // The PHP version cannot change within a request. 4964 static $php530; 4965 4966 if (!isset($php530)) { 4967 $php530 = version_compare(PHP_VERSION, '5.3.0', '>='); 4968 } 4969 4970 if ($php530) { 4971 // Encode <, >, ', &, and " using the json_encode() options parameter. 4972 return json_encode($var, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT); 4973 } 4974 4975 // json_encode() escapes <, >, ', &, and " using its options parameter, but 4976 // does not support this parameter prior to PHP 5.3.0. Use a helper instead. 4977 include_once DRUPAL_ROOT . '/includes/json-encode.inc'; 4978 return drupal_json_encode_helper($var); 4979 } 4980 4981 /** 4982 * Converts an HTML-safe JSON string into its PHP equivalent. 4983 * 4984 * @see drupal_json_encode() 4985 * @ingroup php_wrappers 4986 */ 4987 function drupal_json_decode($var) { 4988 return json_decode($var, TRUE); 4989 } 4990 4991 /** 4992 * Returns data in JSON format. 4993 * 4994 * This function should be used for JavaScript callback functions returning 4995 * data in JSON format. It sets the header for JavaScript output. 4996 * 4997 * @param $var 4998 * (optional) If set, the variable will be converted to JSON and output. 4999 */ 5000 function drupal_json_output($var = NULL) { 5001 // We are returning JSON, so tell the browser. 5002 drupal_add_http_header('Content-Type', 'application/json'); 5003 5004 if (isset($var)) { 5005 echo drupal_json_encode($var); 5006 } 5007 } 5008 5009 /** 5010 * Gets a salt useful for hardening against SQL injection. 5011 * 5012 * @return 5013 * A salt based on information in settings.php, not in the database. 5014 */ 5015 function drupal_get_hash_salt() { 5016 global $drupal_hash_salt, $databases; 5017 // If the $drupal_hash_salt variable is empty, a hash of the serialized 5018 // database credentials is used as a fallback salt. 5019 return empty($drupal_hash_salt) ? hash('sha256', serialize($databases)) : $drupal_hash_salt; 5020 } 5021 5022 /** 5023 * Ensures the private key variable used to generate tokens is set. 5024 * 5025 * @return 5026 * The private key. 5027 */ 5028 function drupal_get_private_key() { 5029 if (!($key = variable_get('drupal_private_key', 0))) { 5030 $key = drupal_hash_base64(drupal_random_bytes(55)); 5031 variable_set('drupal_private_key', $key); 5032 } 5033 return $key; 5034 } 5035 5036 /** 5037 * Generates a token based on $value, the user session, and the private key. 5038 * 5039 * @param $value 5040 * An additional value to base the token on. 5041 */ 5042 function drupal_get_token($value = '') { 5043 return drupal_hmac_base64($value, session_id() . drupal_get_private_key() . drupal_get_hash_salt()); 5044 } 5045 5046 /** 5047 * Validates a token based on $value, the user session, and the private key. 5048 * 5049 * @param $token 5050 * The token to be validated. 5051 * @param $value 5052 * An additional value to base the token on. 5053 * @param $skip_anonymous 5054 * Set to true to skip token validation for anonymous users. 5055 * 5056 * @return 5057 * True for a valid token, false for an invalid token. When $skip_anonymous 5058 * is true, the return value will always be true for anonymous users. 5059 */ 5060 function drupal_valid_token($token, $value = '', $skip_anonymous = FALSE) { 5061 global $user; 5062 return (($skip_anonymous && $user->uid == 0) || ($token == drupal_get_token($value))); 5063 } 5064 5065 function _drupal_bootstrap_full() { 5066 static $called = FALSE; 5067 5068 if ($called) { 5069 return; 5070 } 5071 $called = TRUE; 5072 require_once DRUPAL_ROOT . '/' . variable_get('path_inc', 'includes/path.inc'); 5073 require_once DRUPAL_ROOT . '/includes/theme.inc'; 5074 require_once DRUPAL_ROOT . '/includes/pager.inc'; 5075 require_once DRUPAL_ROOT . '/' . variable_get('menu_inc', 'includes/menu.inc'); 5076 require_once DRUPAL_ROOT . '/includes/tablesort.inc'; 5077 require_once DRUPAL_ROOT . '/includes/file.inc'; 5078 require_once DRUPAL_ROOT . '/includes/unicode.inc'; 5079 require_once DRUPAL_ROOT . '/includes/image.inc'; 5080 require_once DRUPAL_ROOT . '/includes/form.inc'; 5081 require_once DRUPAL_ROOT . '/includes/mail.inc'; 5082 require_once DRUPAL_ROOT . '/includes/actions.inc'; 5083 require_once DRUPAL_ROOT . '/includes/ajax.inc'; 5084 require_once DRUPAL_ROOT . '/includes/token.inc'; 5085 require_once DRUPAL_ROOT . '/includes/errors.inc'; 5086 5087 // Detect string handling method 5088 unicode_check(); 5089 // Undo magic quotes 5090 fix_gpc_magic(); 5091 // Load all enabled modules 5092 module_load_all(); 5093 // Make sure all stream wrappers are registered. 5094 file_get_stream_wrappers(); 5095 5096 $test_info = &$GLOBALS['drupal_test_info']; 5097 if (!empty($test_info['in_child_site'])) { 5098 // Running inside the simpletest child site, log fatal errors to test 5099 // specific file directory. 5100 ini_set('log_errors', 1); 5101 ini_set('error_log', 'public://error.log'); 5102 } 5103 5104 // Initialize $_GET['q'] prior to invoking hook_init(). 5105 drupal_path_initialize(); 5106 5107 // Let all modules take action before the menu system handles the request. 5108 // We do not want this while running update.php. 5109 if (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update') { 5110 // Prior to invoking hook_init(), initialize the theme (potentially a custom 5111 // one for this page), so that: 5112 // - Modules with hook_init() implementations that call theme() or 5113 // theme_get_registry() don't initialize the incorrect theme. 5114 // - The theme can have hook_*_alter() implementations affect page building 5115 // (e.g., hook_form_alter(), hook_node_view_alter(), hook_page_alter()), 5116 // ahead of when rendering starts. 5117 menu_set_custom_theme(); 5118 drupal_theme_initialize(); 5119 module_invoke_all('init'); 5120 } 5121 } 5122 5123 /** 5124 * Stores the current page in the cache. 5125 * 5126 * If page_compression is enabled, a gzipped version of the page is stored in 5127 * the cache to avoid compressing the output on each request. The cache entry 5128 * is unzipped in the relatively rare event that the page is requested by a 5129 * client without gzip support. 5130 * 5131 * Page compression requires the PHP zlib extension 5132 * (http://php.net/manual/en/ref.zlib.php). 5133 * 5134 * @see drupal_page_header() 5135 */ 5136 function drupal_page_set_cache() { 5137 global $base_root; 5138 5139 if (drupal_page_is_cacheable()) { 5140 $cache = (object) array( 5141 'cid' => $base_root . request_uri(), 5142 'data' => array( 5143 'path' => $_GET['q'], 5144 'body' => ob_get_clean(), 5145 'title' => drupal_get_title(), 5146 'headers' => array(), 5147 ), 5148 'expire' => CACHE_TEMPORARY, 5149 'created' => REQUEST_TIME, 5150 ); 5151 5152 // Restore preferred header names based on the lower-case names returned 5153 // by drupal_get_http_header(). 5154 $header_names = _drupal_set_preferred_header_name(); 5155 foreach (drupal_get_http_header() as $name_lower => $value) { 5156 $cache->data['headers'][$header_names[$name_lower]] = $value; 5157 if ($name_lower == 'expires') { 5158 // Use the actual timestamp from an Expires header if available. 5159 $cache->expire = strtotime($value); 5160 } 5161 } 5162 5163 if ($cache->data['body']) { 5164 if (variable_get('page_compression', TRUE) && extension_loaded('zlib')) { 5165 $cache->data['body'] = gzencode($cache->data['body'], 9, FORCE_GZIP); 5166 } 5167 cache_set($cache->cid, $cache->data, 'cache_page', $cache->expire); 5168 } 5169 return $cache; 5170 } 5171 } 5172 5173 /** 5174 * Executes a cron run when called. 5175 * 5176 * Do not call this function from a test. Use $this->cronRun() instead. 5177 * 5178 * @return 5179 * TRUE if cron ran successfully. 5180 */ 5181 function drupal_cron_run() { 5182 // Allow execution to continue even if the request gets canceled. 5183 @ignore_user_abort(TRUE); 5184 5185 // Prevent session information from being saved while cron is running. 5186 $original_session_saving = drupal_save_session(); 5187 drupal_save_session(FALSE); 5188 5189 // Force the current user to anonymous to ensure consistent permissions on 5190 // cron runs. 5191 $original_user = $GLOBALS['user']; 5192 $GLOBALS['user'] = drupal_anonymous_user(); 5193 5194 // Try to allocate enough time to run all the hook_cron implementations. 5195 drupal_set_time_limit(240); 5196 5197 $return = FALSE; 5198 // Grab the defined cron queues. 5199 $queues = module_invoke_all('cron_queue_info'); 5200 drupal_alter('cron_queue_info', $queues); 5201 5202 // Try to acquire cron lock. 5203 if (!lock_acquire('cron', 240.0)) { 5204 // Cron is still running normally. 5205 watchdog('cron', 'Attempting to re-run cron while it is already running.', array(), WATCHDOG_WARNING); 5206 } 5207 else { 5208 // Make sure every queue exists. There is no harm in trying to recreate an 5209 // existing queue. 5210 foreach ($queues as $queue_name => $info) { 5211 DrupalQueue::get($queue_name)->createQueue(); 5212 } 5213 // Register shutdown callback. 5214 drupal_register_shutdown_function('drupal_cron_cleanup'); 5215 5216 // Iterate through the modules calling their cron handlers (if any): 5217 foreach (module_implements('cron') as $module) { 5218 // Do not let an exception thrown by one module disturb another. 5219 try { 5220 module_invoke($module, 'cron'); 5221 } 5222 catch (Exception $e) { 5223 watchdog_exception('cron', $e); 5224 } 5225 } 5226 5227 // Record cron time. 5228 variable_set('cron_last', REQUEST_TIME); 5229 watchdog('cron', 'Cron run completed.', array(), WATCHDOG_NOTICE); 5230 5231 // Release cron lock. 5232 lock_release('cron'); 5233 5234 // Return TRUE so other functions can check if it did run successfully 5235 $return = TRUE; 5236 } 5237 5238 foreach ($queues as $queue_name => $info) { 5239 $function = $info['worker callback']; 5240 $end = time() + (isset($info['time']) ? $info['time'] : 15); 5241 $queue = DrupalQueue::get($queue_name); 5242 while (time() < $end && ($item = $queue->claimItem())) { 5243 $function($item->data); 5244 $queue->deleteItem($item); 5245 } 5246 } 5247 // Restore the user. 5248 $GLOBALS['user'] = $original_user; 5249 drupal_save_session($original_session_saving); 5250 5251 return $return; 5252 } 5253 5254 /** 5255 * Shutdown function: Performs cron cleanup. 5256 * 5257 * @see drupal_cron_run() 5258 * @see drupal_register_shutdown_function() 5259 */ 5260 function drupal_cron_cleanup() { 5261 // See if the semaphore is still locked. 5262 if (variable_get('cron_semaphore', FALSE)) { 5263 watchdog('cron', 'Cron run exceeded the time limit and was aborted.', array(), WATCHDOG_WARNING); 5264 5265 // Release cron semaphore. 5266 variable_del('cron_semaphore'); 5267 } 5268 } 5269 5270 /** 5271 * Returns information about system object files (modules, themes, etc.). 5272 * 5273 * This function is used to find all or some system object files (module files, 5274 * theme files, etc.) that exist on the site. It searches in several locations, 5275 * depending on what type of object you are looking for. For instance, if you 5276 * are looking for modules and call: 5277 * @code 5278 * drupal_system_listing("/\.module$/", "modules", 'name', 0); 5279 * @endcode 5280 * this function will search the site-wide modules directory (i.e., /modules/), 5281 * your installation profile's directory (i.e., 5282 * /profiles/your_site_profile/modules/), the all-sites directory (i.e., 5283 * /sites/all/modules/), and your site-specific directory (i.e., 5284 * /sites/your_site_dir/modules/), in that order, and return information about 5285 * all of the files ending in .module in those directories. 5286 * 5287 * The information is returned in an associative array, which can be keyed on 5288 * the file name ($key = 'filename'), the file name without the extension ($key 5289 * = 'name'), or the full file stream URI ($key = 'uri'). If you use a key of 5290 * 'filename' or 'name', files found later in the search will take precedence 5291 * over files found earlier (unless they belong to a module or theme not 5292 * compatible with Drupal core); if you choose a key of 'uri', you will get all 5293 * files found. 5294 * 5295 * @param string $mask 5296 * The preg_match() regular expression for the files to find. 5297 * @param string $directory 5298 * The subdirectory name in which the files are found. For example, 5299 * 'modules' will search in sub-directories of the top-level /modules 5300 * directory, sub-directories of /sites/all/modules/, etc. 5301 * @param string $key 5302 * The key to be used for the associative array returned. Possible values are 5303 * 'uri', for the file's URI; 'filename', for the basename of the file; and 5304 * 'name' for the name of the file without the extension. If you choose 'name' 5305 * or 'filename', only the highest-precedence file will be returned. 5306 * @param int $min_depth 5307 * Minimum depth of directories to return files from, relative to each 5308 * directory searched. For instance, a minimum depth of 2 would find modules 5309 * inside /modules/node/tests, but not modules directly in /modules/node. 5310 * 5311 * @return array 5312 * An associative array of file objects, keyed on the chosen key. Each element 5313 * in the array is an object containing file information, with properties: 5314 * - 'uri': Full URI of the file. 5315 * - 'filename': File name. 5316 * - 'name': Name of file without the extension. 5317 */ 5318 function drupal_system_listing($mask, $directory, $key = 'name', $min_depth = 1) { 5319 $config = conf_path(); 5320 5321 $searchdir = array($directory); 5322 $files = array(); 5323 5324 // The 'profiles' directory contains pristine collections of modules and 5325 // themes as organized by a distribution. It is pristine in the same way 5326 // that /modules is pristine for core; users should avoid changing anything 5327 // there in favor of sites/all or sites/<domain> directories. 5328 $profiles = array(); 5329 $profile = drupal_get_profile(); 5330 // For SimpleTest to be able to test modules packaged together with a 5331 // distribution we need to include the profile of the parent site (in which 5332 // test runs are triggered). 5333 if (drupal_valid_test_ua()) { 5334 $testing_profile = variable_get('simpletest_parent_profile', FALSE); 5335 if ($testing_profile && $testing_profile != $profile) { 5336 $profiles[] = $testing_profile; 5337 } 5338 } 5339 // In case both profile directories contain the same extension, the actual 5340 // profile always has precedence. 5341 $profiles[] = $profile; 5342 foreach ($profiles as $profile) { 5343 if (file_exists("profiles/$profile/$directory")) { 5344 $searchdir[] = "profiles/$profile/$directory"; 5345 } 5346 } 5347 5348 // Always search sites/all/* as well as the global directories. 5349 $searchdir[] = 'sites/all/' . $directory; 5350 5351 if (file_exists("$config/$directory")) { 5352 $searchdir[] = "$config/$directory"; 5353 } 5354 5355 // Get current list of items. 5356 if (!function_exists('file_scan_directory')) { 5357 require_once DRUPAL_ROOT . '/includes/file.inc'; 5358 } 5359 foreach ($searchdir as $dir) { 5360 $files_to_add = file_scan_directory($dir, $mask, array('key' => $key, 'min_depth' => $min_depth)); 5361 5362 // Duplicate files found in later search directories take precedence over 5363 // earlier ones, so we want them to overwrite keys in our resulting 5364 // $files array. 5365 // The exception to this is if the later file is from a module or theme not 5366 // compatible with Drupal core. This may occur during upgrades of Drupal 5367 // core when new modules exist in core while older contrib modules with the 5368 // same name exist in a directory such as sites/all/modules/. 5369 foreach (array_intersect_key($files_to_add, $files) as $file_key => $file) { 5370 // If it has no info file, then we just behave liberally and accept the 5371 // new resource on the list for merging. 5372 if (file_exists($info_file = dirname($file->uri) . '/' . $file->name . '.info')) { 5373 // Get the .info file for the module or theme this file belongs to. 5374 $info = drupal_parse_info_file($info_file); 5375 5376 // If the module or theme is incompatible with Drupal core, remove it 5377 // from the array for the current search directory, so it is not 5378 // overwritten when merged with the $files array. 5379 if (isset($info['core']) && $info['core'] != DRUPAL_CORE_COMPATIBILITY) { 5380 unset($files_to_add[$file_key]); 5381 } 5382 } 5383 } 5384 $files = array_merge($files, $files_to_add); 5385 } 5386 5387 return $files; 5388 } 5389 5390 /** 5391 * Sets the main page content value for later use. 5392 * 5393 * Given the nature of the Drupal page handling, this will be called once with 5394 * a string or array. We store that and return it later as the block is being 5395 * displayed. 5396 * 5397 * @param $content 5398 * A string or renderable array representing the body of the page. 5399 * 5400 * @return 5401 * If called without $content, a renderable array representing the body of 5402 * the page. 5403 */ 5404 function drupal_set_page_content($content = NULL) { 5405 $content_block = &drupal_static(__FUNCTION__, NULL); 5406 $main_content_display = &drupal_static('system_main_content_added', FALSE); 5407 5408 if (!empty($content)) { 5409 $content_block = (is_array($content) ? $content : array('main' => array('#markup' => $content))); 5410 } 5411 else { 5412 // Indicate that the main content has been requested. We assume that 5413 // the module requesting the content will be adding it to the page. 5414 // A module can indicate that it does not handle the content by setting 5415 // the static variable back to FALSE after calling this function. 5416 $main_content_display = TRUE; 5417 return $content_block; 5418 } 5419 } 5420 5421 /** 5422 * #pre_render callback to render #browsers into #prefix and #suffix. 5423 * 5424 * @param $elements 5425 * A render array with a '#browsers' property. The '#browsers' property can 5426 * contain any or all of the following keys: 5427 * - 'IE': If FALSE, the element is not rendered by Internet Explorer. If 5428 * TRUE, the element is rendered by Internet Explorer. Can also be a string 5429 * containing an expression for Internet Explorer to evaluate as part of a 5430 * conditional comment. For example, this can be set to 'lt IE 7' for the 5431 * element to be rendered in Internet Explorer 6, but not in Internet 5432 * Explorer 7 or higher. Defaults to TRUE. 5433 * - '!IE': If FALSE, the element is not rendered by browsers other than 5434 * Internet Explorer. If TRUE, the element is rendered by those browsers. 5435 * Defaults to TRUE. 5436 * Examples: 5437 * - To render an element in all browsers, '#browsers' can be left out or set 5438 * to array('IE' => TRUE, '!IE' => TRUE). 5439 * - To render an element in Internet Explorer only, '#browsers' can be set 5440 * to array('!IE' => FALSE). 5441 * - To render an element in Internet Explorer 6 only, '#browsers' can be set 5442 * to array('IE' => 'lt IE 7', '!IE' => FALSE). 5443 * - To render an element in Internet Explorer 8 and higher and in all other 5444 * browsers, '#browsers' can be set to array('IE' => 'gte IE 8'). 5445 * 5446 * @return 5447 * The passed-in element with markup for conditional comments potentially 5448 * added to '#prefix' and '#suffix'. 5449 */ 5450 function drupal_pre_render_conditional_comments($elements) { 5451 $browsers = isset($elements['#browsers']) ? $elements['#browsers'] : array(); 5452 $browsers += array( 5453 'IE' => TRUE, 5454 '!IE' => TRUE, 5455 ); 5456 5457 // If rendering in all browsers, no need for conditional comments. 5458 if ($browsers['IE'] === TRUE && $browsers['!IE']) { 5459 return $elements; 5460 } 5461 5462 // Determine the conditional comment expression for Internet Explorer to 5463 // evaluate. 5464 if ($browsers['IE'] === TRUE) { 5465 $expression = 'IE'; 5466 } 5467 elseif ($browsers['IE'] === FALSE) { 5468 $expression = '!IE'; 5469 } 5470 else { 5471 $expression = $browsers['IE']; 5472 } 5473 5474 // Wrap the element's potentially existing #prefix and #suffix properties with 5475 // conditional comment markup. The conditional comment expression is evaluated 5476 // by Internet Explorer only. To control the rendering by other browsers, 5477 // either the "downlevel-hidden" or "downlevel-revealed" technique must be 5478 // used. See http://en.wikipedia.org/wiki/Conditional_comment for details. 5479 $elements += array( 5480 '#prefix' => '', 5481 '#suffix' => '', 5482 ); 5483 if (!$browsers['!IE']) { 5484 // "downlevel-hidden". 5485 $elements['#prefix'] = "\n<!--[if $expression]>\n" . $elements['#prefix']; 5486 $elements['#suffix'] .= "<![endif]-->\n"; 5487 } 5488 else { 5489 // "downlevel-revealed". 5490 $elements['#prefix'] = "\n<!--[if $expression]><!-->\n" . $elements['#prefix']; 5491 $elements['#suffix'] .= "<!--<![endif]-->\n"; 5492 } 5493 5494 return $elements; 5495 } 5496 5497 /** 5498 * #pre_render callback to render a link into #markup. 5499 * 5500 * Doing so during pre_render gives modules a chance to alter the link parts. 5501 * 5502 * @param $elements 5503 * A structured array whose keys form the arguments to l(): 5504 * - #title: The link text to pass as argument to l(). 5505 * - #href: The URL path component to pass as argument to l(). 5506 * - #options: (optional) An array of options to pass to l(). 5507 * 5508 * @return 5509 * The passed-in elements containing a rendered link in '#markup'. 5510 */ 5511 function drupal_pre_render_link($element) { 5512 // By default, link options to pass to l() are normally set in #options. 5513 $element += array('#options' => array()); 5514 // However, within the scope of renderable elements, #attributes is a valid 5515 // way to specify attributes, too. Take them into account, but do not override 5516 // attributes from #options. 5517 if (isset($element['#attributes'])) { 5518 $element['#options'] += array('attributes' => array()); 5519 $element['#options']['attributes'] += $element['#attributes']; 5520 } 5521 5522 // This #pre_render callback can be invoked from inside or outside of a Form 5523 // API context, and depending on that, a HTML ID may be already set in 5524 // different locations. #options should have precedence over Form API's #id. 5525 // #attributes have been taken over into #options above already. 5526 if (isset($element['#options']['attributes']['id'])) { 5527 $element['#id'] = $element['#options']['attributes']['id']; 5528 } 5529 elseif (isset($element['#id'])) { 5530 $element['#options']['attributes']['id'] = $element['#id']; 5531 } 5532 5533 // Conditionally invoke ajax_pre_render_element(), if #ajax is set. 5534 if (isset($element['#ajax']) && !isset($element['#ajax_processed'])) { 5535 // If no HTML ID was found above, automatically create one. 5536 if (!isset($element['#id'])) { 5537 $element['#id'] = $element['#options']['attributes']['id'] = drupal_html_id('ajax-link'); 5538 } 5539 // If #ajax['path] was not specified, use the href as Ajax request URL. 5540 if (!isset($element['#ajax']['path'])) { 5541 $element['#ajax']['path'] = $element['#href']; 5542 $element['#ajax']['options'] = $element['#options']; 5543 } 5544 $element = ajax_pre_render_element($element); 5545 } 5546 5547 $element['#markup'] = l($element['#title'], $element['#href'], $element['#options']); 5548 return $element; 5549 } 5550 5551 /** 5552 * #pre_render callback that collects child links into a single array. 5553 * 5554 * This function can be added as a pre_render callback for a renderable array, 5555 * usually one which will be themed by theme_links(). It iterates through all 5556 * unrendered children of the element, collects any #links properties it finds, 5557 * merges them into the parent element's #links array, and prevents those 5558 * children from being rendered separately. 5559 * 5560 * The purpose of this is to allow links to be logically grouped into related 5561 * categories, so that each child group can be rendered as its own list of 5562 * links if drupal_render() is called on it, but calling drupal_render() on the 5563 * parent element will still produce a single list containing all the remaining 5564 * links, regardless of what group they were in. 5565 * 5566 * A typical example comes from node links, which are stored in a renderable 5567 * array similar to this: 5568 * @code 5569 * $node->content['links'] = array( 5570 * '#theme' => 'links__node', 5571 * '#pre_render' = array('drupal_pre_render_links'), 5572 * 'comment' => array( 5573 * '#theme' => 'links__node__comment', 5574 * '#links' => array( 5575 * // An array of links associated with node comments, suitable for 5576 * // passing in to theme_links(). 5577 * ), 5578 * ), 5579 * 'statistics' => array( 5580 * '#theme' => 'links__node__statistics', 5581 * '#links' => array( 5582 * // An array of links associated with node statistics, suitable for 5583 * // passing in to theme_links(). 5584 * ), 5585 * ), 5586 * 'translation' => array( 5587 * '#theme' => 'links__node__translation', 5588 * '#links' => array( 5589 * // An array of links associated with node translation, suitable for 5590 * // passing in to theme_links(). 5591 * ), 5592 * ),