Simpletest Coverage - includes/common.inc

1 <?php
2 // $Id: common.inc,v 1.961 2009/08/15 06:20:20 webchick Exp $
3
4 /**
5 * @file
6 * Common functions that many Drupal modules will need to reference.
7 *
8 * The functions that are critical and need to be available even when serving
9 * a cached page are instead located in bootstrap.inc.
10 */
11
12 /**
13 * Error reporting level: display no errors.
14 */
15 define('ERROR_REPORTING_HIDE', 0);
16
17 /**
18 * Error reporting level: display errors and warnings.
19 */
20 define('ERROR_REPORTING_DISPLAY_SOME', 1);
21
22 /**
23 * Error reporting level: display all messages.
24 */
25 define('ERROR_REPORTING_DISPLAY_ALL', 2);
26
27 /**
28 * Return status for saving which involved creating a new item.
29 */
30 define('SAVED_NEW', 1);
31
32 /**
33 * Return status for saving which involved an update to an existing item.
34 */
35 define('SAVED_UPDATED', 2);
36
37 /**
38 * Return status for saving which deleted an existing item.
39 */
40 define('SAVED_DELETED', 3);
41
42 /**
43 * The default weight of system CSS files added to the page.
44 */
45 define('CSS_SYSTEM', -100);
46
47 /**
48 * The default weight of CSS files added to the page.
49 */
50 define('CSS_DEFAULT', 0);
51
52 /**
53 * The default weight of theme CSS files added to the page.
54 */
55 define('CSS_THEME', 100);
56
57 /**
58 * The weight of JavaScript libraries, settings or jQuery plugins being
59 * added to the page.
60 */
61 define('JS_LIBRARY', -100);
62
63 /**
64 * The default weight of JavaScript being added to the page.
65 */
66 define('JS_DEFAULT', 0);
67
68 /**
69 * The weight of theme JavaScript code being added to the page.
70 */
71 define('JS_THEME', 100);
72
73 /**
74 * Error code indicating that the request made by drupal_http_request() exceeded
75 * the specified timeout.
76 */
77 define('HTTP_REQUEST_TIMEOUT', 1);
78
79 /**
80 * Add content to a specified region.
81 *
82 * @param $region
83 * Page region the content is added to.
84 * @param $data
85 * Content to be added.
86 */
87 function drupal_add_region_content($region = NULL, $data = NULL) {
88 static $content = array();
89
90 if (!is_null($region) && !is_null($data)) {
91 $content[$region][] = $data;
92 }
93 return $content;
94 }
95
96 /**
97 * Get assigned content for a given region.
98 *
99 * @param $region
100 * A specified region to fetch content for. If NULL, all regions will be
101 * returned.
102 * @param $delimiter
103 * Content to be inserted between imploded array elements.
104 */
105 function drupal_get_region_content($region = NULL, $delimiter = ' ') {
106 $content = drupal_add_region_content();
107 if (isset($region)) {
108 if (isset($content[$region]) && is_array($content[$region])) {
109 return implode($delimiter, $content[$region]);
110 }
111 }
112 else {
113 foreach (array_keys($content) as $region) {
114 if (is_array($content[$region])) {
115 $content[$region] = implode($delimiter, $content[$region]);
116 }
117 }
118 return $content;
119 }
120 }
121
122 /**
123 * Set the breadcrumb trail for the current page.
124 *
125 * @param $breadcrumb
126 * Array of links, starting with "home" and proceeding up to but not including
127 * the current page.
128 */
129 function drupal_set_breadcrumb($breadcrumb = NULL) {
130 $stored_breadcrumb = &drupal_static(__FUNCTION__);
131
132 if (!is_null($breadcrumb)) {
133 $stored_breadcrumb = $breadcrumb;
134 }
135 return $stored_breadcrumb;
136 }
137
138 /**
139 * Get the breadcrumb trail for the current page.
140 */
141 function drupal_get_breadcrumb() {
142 $breadcrumb = drupal_set_breadcrumb();
143
144 if (is_null($breadcrumb)) {
145 $breadcrumb = menu_get_active_breadcrumb();
146 }
147
148 return $breadcrumb;
149 }
150
151 /**
152 * Return a string containing RDF namespaces for the <html> tag of an XHTML
153 * page.
154 */
155 function drupal_get_rdf_namespaces() {
156 // Serialize the RDF namespaces used in RDFa annotation.
157 $xml_rdf_namespaces = array();
158 foreach (module_invoke_all('rdf_namespaces') as $prefix => $uri) {
159 $xml_rdf_namespaces[] = 'xmlns:' . $prefix . '="' . $uri . '"';
160 }
161 return implode("\n ", $xml_rdf_namespaces);
162 }
163
164 /**
165 * Add output to the head tag of the HTML page.
166 *
167 * This function can be called as long the headers aren't sent.
168 */
169 function drupal_add_html_head($data = NULL) {
170 $stored_head = &drupal_static(__FUNCTION__, '');
171
172 if (!is_null($data)) {
173 $stored_head .= $data . "\n";
174 }
175 return $stored_head;
176 }
177
178 /**
179 * Retrieve output to be displayed in the head tag of the HTML page.
180 */
181 function drupal_get_html_head() {
182 $output = "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n";
183 return $output . drupal_add_html_head();
184 }
185
186 /**
187 * Reset the static variable which holds the aliases mapped for this request.
188 */
189 function drupal_clear_path_cache() {
190 drupal_lookup_path('wipe');
191 }
192
193 /**
194 * Add a feed URL for the current page.
195 *
196 * This function can be called as long the HTML header hasn't been sent.
197 *
198 * @param $url
199 * A url for the feed.
200 * @param $title
201 * The title of the feed.
202 */
203 function drupal_add_feed($url = NULL, $title = '') {
204 $stored_feed_links = &drupal_static(__FUNCTION__, array());
205
206 if (!is_null($url) && !isset($stored_feed_links[$url])) {
207 $stored_feed_links[$url] = theme('feed_icon', $url, $title);
208
209 drupal_add_link(array('rel' => 'alternate',
210 'type' => 'application/rss+xml',
211 'title' => $title,
212 'href' => $url));
213 }
214 return $stored_feed_links;
215 }
216
217 /**
218 * Get the feed URLs for the current page.
219 *
220 * @param $delimiter
221 * A delimiter to split feeds by.
222 */
223 function drupal_get_feeds($delimiter = "\n") {
224 $feeds = drupal_add_feed();
225 return implode($feeds, $delimiter);
226 }
227
228 /**
229 * @name HTTP handling
230 * @{
231 * Functions to properly handle HTTP responses.
232 */
233
234 /**
235 * Parse an array into a valid urlencoded query string.
236 *
237 * @param $query
238 * The array to be processed e.g. $_GET.
239 * @param $exclude
240 * The array filled with keys to be excluded. Use parent[child] to exclude
241 * nested items.
242 * @param $parent
243 * Should not be passed, only used in recursive calls.
244 * @return
245 * An urlencoded string which can be appended to/as the URL query string.
246 */
247 function drupal_query_string_encode($query, $exclude = array(), $parent = '') {
248 $params = array();
249
250 foreach ($query as $key => $value) {
251 $key = rawurlencode($key);
252 if ($parent) {
253 $key = $parent . '[' . $key . ']';
254 }
255
256 if (in_array($key, $exclude)) {
257 continue;
258 }
259
260 if (is_array($value)) {
261 $params[] = drupal_query_string_encode($value, $exclude, $key);
262 }
263 else {
264 $params[] = $key . '=' . rawurlencode($value);
265 }
266 }
267
268 return implode('&', $params);
269 }
270
271 /**
272 * Prepare a destination query string for use in combination with drupal_goto().
273 *
274 * Used to direct the user back to the referring page after completing a form.
275 * By default the current URL is returned. If a destination exists in the
276 * previous request, that destination is returned. As such, a destination can
277 * persist across multiple pages.
278 *
279 * @see drupal_goto()
280 */
281 function drupal_get_destination() {
282 if (isset($_REQUEST['destination'])) {
283 return 'destination=' . urlencode($_REQUEST['destination']);
284 }
285 else {
286 // Use $_GET here to retrieve the original path in source form.
287 $path = isset($_GET['q']) ? $_GET['q'] : '';
288 $query = drupal_query_string_encode($_GET, array('q'));
289 if ($query != '') {
290 $path .= '?' . $query;
291 }
292 return 'destination=' . urlencode($path);
293 }
294 }
295
296 /**
297 * Send the user to a different Drupal page.
298 *
299 * This issues an on-site HTTP redirect. The function makes sure the redirected
300 * URL is formatted correctly.
301 *
302 * Usually the redirected URL is constructed from this function's input
303 * parameters. However you may override that behavior by setting a
304 * destination in either the $_REQUEST-array (i.e. by using
305 * the query string of an URI) This is used to direct the user back to
306 * the proper page after completing a form. For example, after editing
307 * a post on the 'admin/content'-page or after having logged on using the
308 * 'user login'-block in a sidebar. The function drupal_get_destination()
309 * can be used to help set the destination URL.
310 *
311 * Drupal will ensure that messages set by drupal_set_message() and other
312 * session data are written to the database before the user is redirected.
313 *
314 * This function ends the request; use it instead of a return in your menu callback.
315 *
316 * @param $path
317 * A Drupal path or a full URL.
318 * @param $query
319 * A query string component, if any.
320 * @param $fragment
321 * A destination fragment identifier (named anchor).
322 * @param $http_response_code
323 * Valid values for an actual "goto" as per RFC 2616 section 10.3 are:
324 * - 301 Moved Permanently (the recommended value for most redirects)
325 * - 302 Found (default in Drupal and PHP, sometimes used for spamming search
326 * engines)
327 * - 303 See Other
328 * - 304 Not Modified
329 * - 305 Use Proxy
330 * - 307 Temporary Redirect (alternative to "503 Site Down for Maintenance")
331 * Note: Other values are defined by RFC 2616, but are rarely used and poorly
332 * supported.
333 * @see drupal_get_destination()
334 */
335 function drupal_goto($path = '', $query = NULL, $fragment = NULL, $http_response_code = 302) {
336
337 if (isset($_REQUEST['destination'])) {
338 extract(parse_url(urldecode($_REQUEST['destination'])));
339 }
340
341 $url = url($path, array('query' => $query, 'fragment' => $fragment, 'absolute' => TRUE));
342
343 // Allow modules to react to the end of the page request before redirecting.
344 // We do not want this while running update.php.
345 if (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update') {
346 module_invoke_all('exit', $url);
347 }
348
349 // Commit the session, if necessary. We need all session data written to the
350 // database before redirecting.
351 drupal_session_commit();
352
353 header('Location: ' . $url, TRUE, $http_response_code);
354
355 // The "Location" header sends a redirect status code to the HTTP daemon. In
356 // some cases this can be wrong, so we make sure none of the code below the
357 // drupal_goto() call gets executed upon redirection.
358 exit();
359 }
360
361 /**
362 * Generates a site offline message.
363 */
364 function drupal_site_offline() {
365 drupal_maintenance_theme();
366 drupal_set_header('503 Service unavailable');
367 drupal_set_title(t('Site offline'));
368 print theme('maintenance_page', filter_xss_admin(variable_get('site_offline_message',
369 t('@site is currently under maintenance. We should be back shortly. Thank you for your patience.', array('@site' => variable_get('site_name', 'Drupal'))))));
370 }
371
372 /**
373 * Generates a 404 error if the request can not be handled.
374 */
375 function drupal_not_found() {
376 drupal_set_header('404 Not Found');
377
378 watchdog('page not found', check_plain($_GET['q']), NULL, WATCHDOG_WARNING);
379
380 // Keep old path for reference, and to allow forms to redirect to it.
381 if (!isset($_REQUEST['destination'])) {
382 $_REQUEST['destination'] = $_GET['q'];
383 }
384
385 $path = drupal_get_normal_path(variable_get('site_404', ''));
386 if ($path && $path != $_GET['q']) {
387 // Custom 404 handler. Set the active item in case there are tabs to
388 // display, or other dependencies on the path.
389 menu_set_active_item($path);
390 $return = menu_execute_active_handler($path);
391 }
392
393 if (empty($return) || $return == MENU_NOT_FOUND || $return == MENU_ACCESS_DENIED) {
394 // Standard 404 handler.
395 drupal_set_title(t('Page not found'));
396 $return = t('The requested page could not be found.');
397 }
398
399 drupal_set_page_content($return);
400 $page = element_info('page');
401 // Optionally omit the blocks to conserve CPU and bandwidth.
402 $page['#show_blocks'] = variable_get('site_404_blocks', FALSE);
403
404 print drupal_render_page($page);
405 }
406
407 /**
408 * Generates a 403 error if the request is not allowed.
409 */
410 function drupal_access_denied() {
411 drupal_set_header('403 Forbidden');
412 watchdog('access denied', check_plain($_GET['q']), NULL, WATCHDOG_WARNING);
413
414 // Keep old path for reference, and to allow forms to redirect to it.
415 if (!isset($_REQUEST['destination'])) {
416 $_REQUEST['destination'] = $_GET['q'];
417 }
418
419 $path = drupal_get_normal_path(variable_get('site_403', ''));
420 if ($path && $path != $_GET['q']) {
421 // Custom 403 handler. Set the active item in case there are tabs to
422 // display or other dependencies on the path.
423 menu_set_active_item($path);
424 $return = menu_execute_active_handler($path);
425 }
426
427 if (empty($return) || $return == MENU_NOT_FOUND || $return == MENU_ACCESS_DENIED) {
428 // Standard 403 handler.
429 drupal_set_title(t('Access denied'));
430 $return = t('You are not authorized to access this page.');
431 }
432
433 print drupal_render_page($return);
434 }
435
436 /**
437 * Perform an HTTP request.
438 *
439 * This is a flexible and powerful HTTP client implementation. Correctly
440 * handles GET, POST, PUT or any other HTTP requests. Handles redirects.
441 *
442 * @param $url
443 * A string containing a fully qualified URI.
444 * @param $options
445 * (optional) An array which can have one or more of following keys:
446 * - headers
447 * An array containing request headers to send as name/value pairs.
448 * - method
449 * A string containing the request method. Defaults to 'GET'.
450 * - data
451 * A string containing the request body. Defaults to NULL.
452 * - max_redirects
453 * An integer representing how many times a redirect may be followed.
454 * Defaults to 3.
455 * - timeout
456 * A float representing the maximum number of seconds the function call
457 * may take. The default is 30 seconds. If a timeout occurs, the error
458 * code is set to the HTTP_REQUEST_TIMEOUT constant.
459 * @return
460 * An object which can have one or more of the following parameters:
461 * - request
462 * A string containing the request body that was sent.
463 * - code
464 * An integer containing the response status code, or the error code if
465 * an error occurred.
466 * - protocol
467 * The response protocol (e.g. HTTP/1.1 or HTTP/1.0).
468 * - status_message
469 * The status message from the response, if a response was received.
470 * - redirect_code
471 * If redirected, an integer containing the initial response status code.
472 * - redirect_url
473 * If redirected, a string containing the redirection location.
474 * - error
475 * If an error occurred, the error message. Otherwise not set.
476 * - headers
477 * An array containing the response headers as name/value pairs.
478 * - data
479 * A string containing the response body that was received.
480 */
481 function drupal_http_request($url, array $options = array()) {
482 global $db_prefix;
483
484 $result = new stdClass();
485
486 // Parse the URL and make sure we can handle the schema.
487 $uri = @parse_url($url);
488
489 if ($uri == FALSE) {
490 $result->error = 'unable to parse URL';
491 return $result;
492 }
493
494 if (!isset($uri['scheme'])) {
495 $result->error = 'missing schema';
496 return $result;
497 }
498
499 timer_start(__FUNCTION__);
500
501 // Merge the default options.
502 $options += array(
503 'headers' => array(),
504 'method' => 'GET',
505 'data' => NULL,
506 'max_redirects' => 3,
507 'timeout' => 30,
508 );
509
510 switch ($uri['scheme']) {
511 case 'http':
512 $port = isset($uri['port']) ? $uri['port'] : 80;
513 $host = $uri['host'] . ($port != 80 ? ':' . $port : '');
514 $fp = @fsockopen($uri['host'], $port, $errno, $errstr, $options['timeout']);
515 break;
516 case 'https':
517 // Note: Only works when PHP is compiled with OpenSSL support.
518 $port = isset($uri['port']) ? $uri['port'] : 443;
519 $host = $uri['host'] . ($port != 443 ? ':' . $port : '');
520 $fp = @fsockopen('ssl://' . $uri['host'], $port, $errno, $errstr, $options['timeout']);
521 break;
522 default:
523 $result->error = 'invalid schema ' . $uri['scheme'];
524 return $result;
525 }
526
527 // Make sure the socket opened properly.
528 if (!$fp) {
529 // When a network error occurs, we use a negative number so it does not
530 // clash with the HTTP status codes.
531 $result->code = -$errno;
532 $result->error = trim($errstr);
533
534 // Mark that this request failed. This will trigger a check of the web
535 // server's ability to make outgoing HTTP requests the next time that
536 // requirements checking is performed.
537 // @see system_requirements()
538 variable_set('drupal_http_request_fails', TRUE);
539
540 return $result;
541 }
542
543 // Construct the path to act on.
544 $path = isset($uri['path']) ? $uri['path'] : '/';
545 if (isset($uri['query'])) {
546 $path .= '?' . $uri['query'];
547 }
548
549 // Merge the default headers.
550 $options['headers'] += array(
551 'User-Agent' => 'Drupal (+http://drupal.org/)',
552 );
553
554 // RFC 2616: "non-standard ports MUST, default ports MAY be included".
555 // We don't add the standard port to prevent from breaking rewrite rules
556 // checking the host that do not take into account the port number.
557 $options['headers']['Host'] = $host;
558
559 // Only add Content-Length if we actually have any content or if it is a POST
560 // or PUT request. Some non-standard servers get confused by Content-Length in
561 // at least HEAD/GET requests, and Squid always requires Content-Length in
562 // POST/PUT requests.
563 $content_length = strlen($options['data']);
564 if ($content_length > 0 || $options['method'] == 'POST' || $options['method'] == 'PUT') {
565 $options['headers']['Content-Length'] = $content_length;
566 }
567
568 // If the server URL has a user then attempt to use basic authentication.
569 if (isset($uri['user'])) {
570 $options['headers']['Authorization'] = 'Basic ' . base64_encode($uri['user'] . (!empty($uri['pass']) ? ":" . $uri['pass'] : ''));
571 }
572
573 // If the database prefix is being used by SimpleTest to run the tests in a copied
574 // database then set the user-agent header to the database prefix so that any
575 // calls to other Drupal pages will run the SimpleTest prefixed database. The
576 // user-agent is used to ensure that multiple testing sessions running at the
577 // same time won't interfere with each other as they would if the database
578 // prefix were stored statically in a file or database variable.
579 if (is_string($db_prefix) && preg_match("/simpletest\d+/", $db_prefix, $matches)) {
580 $options['headers']['User-Agent'] = drupal_generate_test_ua($matches[0]);
581 }
582
583 $request = $options['method'] . ' ' . $path . " HTTP/1.0\r\n";
584 foreach ($options['headers'] as $name => $value) {
585 $request .= $name . ': ' . trim($value) . "\r\n";
586 }
587 $request .= "\r\n" . $options['data'];
588 $result->request = $request;
589
590 fwrite($fp, $request);
591
592 // Fetch response.
593 $response = '';
594 while (!feof($fp)) {
595 // Calculate how much time is left of the original timeout value.
596 $timeout = $options['timeout'] - timer_read(__FUNCTION__) / 1000;
597 if ($timeout <= 0) {
598 $result->code = HTTP_REQUEST_TIMEOUT;
599 $result->error = 'request timed out';
600 return $result;
601 }
602 stream_set_timeout($fp, floor($timeout), floor(1000000 * fmod($timeout, 1)));
603 $response .= fread($fp, 1024);
604 }
605 fclose($fp);
606
607 // Parse response headers from the response body.
608 list($response, $result->data) = explode("\r\n\r\n", $response, 2);
609 $response = preg_split("/\r\n|\n|\r/", $response);
610
611 // Parse the response status line.
612 list($protocol, $code, $status_message) = explode(' ', trim(array_shift($response)), 3);
613 $result->protocol = $protocol;
614 $result->status_message = $status_message;
615
616 $result->headers = array();
617
618 // Parse the response headers.
619 while ($line = trim(array_shift($response))) {
620 list($header, $value) = explode(':', $line, 2);
621 if (isset($result->headers[$header]) && $header == 'Set-Cookie') {
622 // RFC 2109: the Set-Cookie response header comprises the token Set-
623 // Cookie:, followed by a comma-separated list of one or more cookies.
624 $result->headers[$header] .= ',' . trim($value);
625 }
626 else {
627 $result->headers[$header] = trim($value);
628 }
629 }
630
631 $responses = array(
632 100 => 'Continue',
633 101 => 'Switching Protocols',
634 200 => 'OK',
635 201 => 'Created',
636 202 => 'Accepted',
637 203 => 'Non-Authoritative Information',
638 204 => 'No Content',
639 205 => 'Reset Content',
640 206 => 'Partial Content',
641 300 => 'Multiple Choices',
642 301 => 'Moved Permanently',
643 302 => 'Found',
644 303 => 'See Other',
645 304 => 'Not Modified',
646 305 => 'Use Proxy',
647 307 => 'Temporary Redirect',
648 400 => 'Bad Request',
649 401 => 'Unauthorized',
650 402 => 'Payment Required',
651 403 => 'Forbidden',
652 404 => 'Not Found',
653 405 => 'Method Not Allowed',
654 406 => 'Not Acceptable',
655 407 => 'Proxy Authentication Required',
656 408 => 'Request Time-out',
657 409 => 'Conflict',
658 410 => 'Gone',
659 411 => 'Length Required',
660 412 => 'Precondition Failed',
661 413 => 'Request Entity Too Large',
662 414 => 'Request-URI Too Large',
663 415 => 'Unsupported Media Type',
664 416 => 'Requested range not satisfiable',
665 417 => 'Expectation Failed',
666 500 => 'Internal Server Error',
667 501 => 'Not Implemented',
668 502 => 'Bad Gateway',
669 503 => 'Service Unavailable',
670 504 => 'Gateway Time-out',
671 505 => 'HTTP Version not supported',
672 );
673 // RFC 2616 states that all unknown HTTP codes must be treated the same as the
674 // base code in their class.
675 if (!isset($responses[$code])) {
676 $code = floor($code / 100) * 100;
677 }
678 $result->code = $code;
679
680 switch ($code) {
681 case 200: // OK
682 case 304: // Not modified
683 break;
684 case 301: // Moved permanently
685 case 302: // Moved temporarily
686 case 307: // Moved temporarily
687 $location = $result->headers['Location'];
688 $options['timeout'] -= timer_read(__FUNCTION__) / 1000;
689 if ($options['timeout'] <= 0) {
690 $result->code = HTTP_REQUEST_TIMEOUT;
691 $result->error = 'request timed out';
692 }
693 elseif ($options['max_redirects']) {
694 // Redirect to the new location.
695 $options['max_redirects']--;
696 $result = drupal_http_request($location, $options);
697 $result->redirect_code = $code;
698 }
699 $result->redirect_url = $location;
700 break;
701 default:
702 $result->error = $status_message;
703 }
704
705 return $result;
706 }
707 /**
708 * @} End of "HTTP handling".
709 */
710
711 /**
712 * Custom PHP error handler.
713 *
714 * @param $error_level
715 * The level of the error raised.
716 * @param $message
717 * The error message.
718 * @param $filename
719 * The filename that the error was raised in.
720 * @param $line
721 * The line number the error was raised at.
722 * @param $context
723 * An array that points to the active symbol table at the point the error occurred.
724 */
725 function _drupal_error_handler($error_level, $message, $filename, $line, $context) {
726 if ($error_level & error_reporting()) {
727 // All these constants are documented at http://php.net/manual/en/errorfunc.constants.php
728 $types = array(
729 E_ERROR => 'Error',
730 E_WARNING => 'Warning',
731 E_PARSE => 'Parse error',
732 E_NOTICE => 'Notice',
733 E_CORE_ERROR => 'Core error',
734 E_CORE_WARNING => 'Core warning',
735 E_COMPILE_ERROR => 'Compile error',
736 E_COMPILE_WARNING => 'Compile warning',
737 E_USER_ERROR => 'User error',
738 E_USER_WARNING => 'User warning',
739 E_USER_NOTICE => 'User notice',
740 E_STRICT => 'Strict warning',
741 E_RECOVERABLE_ERROR => 'Recoverable fatal error'
742 );
743 $caller = _drupal_get_last_caller(debug_backtrace());
744
745 // We treat recoverable errors as fatal.
746 _drupal_log_error(array(
747 '%type' => isset($types[$error_level]) ? $types[$error_level] : 'Unknown error',
748 '%message' => $message,
749 '%function' => $caller['function'],
750 '%file' => $caller['file'],
751 '%line' => $caller['line'],
752 ), $error_level == E_RECOVERABLE_ERROR);
753 }
754 }
755
756 /**
757 * Custom PHP exception handler.
758 *
759 * Uncaught exceptions are those not enclosed in a try/catch block. They are
760 * always fatal: the execution of the script will stop as soon as the exception
761 * handler exits.
762 *
763 * @param $exception
764 * The exception object that was thrown.
765 */
766 function _drupal_exception_handler($exception) {
767 // Log the message to the watchdog and return an error page to the user.
768 _drupal_log_error(_drupal_decode_exception($exception), TRUE);
769 }
770
771 /**
772 * Decode an exception, especially to retrive the correct caller.
773 *
774 * @param $exception
775 * The exception object that was thrown.
776 * @return An error in the format expected by _drupal_log_error().
777 */
778 function _drupal_decode_exception($exception) {
779 $message = $exception->getMessage();
780
781 $backtrace = $exception->getTrace();
782 // Add the line throwing the exception to the backtrace.
783 array_unshift($backtrace, array('line' => $exception->getLine(), 'file' => $exception->getFile()));
784
785 // For PDOException errors, we try to return the initial caller,
786 // skipping internal functions of the database layer.
787 if ($exception instanceof PDOException) {
788 // The first element in the stack is the call, the second element gives us the caller.
789 // We skip calls that occurred in one of the classes of the database layer
790 // or in one of its global functions.
791 $db_functions = array('db_query', 'pager_query', 'db_query_range', 'db_query_temporary', 'update_sql');
792 while (!empty($backtrace[1]) && ($caller = $backtrace[1]) &&
793 ((isset($caller['class']) && (strpos($caller['class'], 'Query') !== FALSE || strpos($caller['class'], 'Database') !== FALSE || strpos($caller['class'], 'PDO') !== FALSE)) ||
794 in_array($caller['function'], $db_functions))) {
795 // We remove that call.
796 array_shift($backtrace);
797 }
798 if (isset($exception->query_string, $exception->args)) {
799 $message .= ": " . $exception->query_string . "; " . print_r($exception->args, TRUE);
800 }
801 }
802 $caller = _drupal_get_last_caller($backtrace);
803
804 return array(
805 '%type' => get_class($exception),
806 '%message' => $message,
807 '%function' => $caller['function'],
808 '%file' => $caller['file'],
809 '%line' => $caller['line'],
810 );
811 }
812
813 /**
814 * Log a PHP error or exception, display an error page in fatal cases.
815 *
816 * @param $error
817 * An array with the following keys: %type, %message, %function, %file, %line.
818 * @param $fatal
819 * TRUE if the error is fatal.
820 */
821 function _drupal_log_error($error, $fatal = FALSE) {
822 // Initialize a maintenance theme if the boostrap was not complete.
823 // Do it early because drupal_set_message() triggers a drupal_theme_initialize().
824 if ($fatal && (drupal_get_bootstrap_phase() != DRUPAL_BOOTSTRAP_FULL)) {
825 unset($GLOBALS['theme']);
826 if (!defined('MAINTENANCE_MODE')) {
827 define('MAINTENANCE_MODE', 'error');
828 }
829 drupal_maintenance_theme();
830 }
831
832 // When running inside the testing framework, we relay the errors
833 // to the tested site by the way of HTTP headers.
834 if (isset($_SERVER['HTTP_USER_AGENT']) && preg_match("/^simpletest\d+;/", $_SERVER['HTTP_USER_AGENT']) && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) {
835 // $number does not use drupal_static as it should not be reset
836 // as it uniquely identifies each PHP error.
837 static $number = 0;
838 $assertion = array(
839 $error['%message'],
840 $error['%type'],
841 array(
842 'function' => $error['%function'],
843 'file' => $error['%file'],
844 'line' => $error['%line'],
845 ),
846 );
847 header('X-Drupal-Assertion-' . $number . ': ' . rawurlencode(serialize($assertion)));
848 $number++;
849 }
850
851 try {
852 watchdog('php', '%type: %message in %function (line %line of %file).', $error, WATCHDOG_ERROR);
853 }
854 catch (Exception $e) {
855 // Ignore any additional watchdog exception, as that probably means
856 // that the database was not initialized correctly.
857 }
858
859 if ($fatal) {
860 drupal_set_header('500 Service unavailable (with message)');
861 }
862
863 if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') {
864 if ($fatal) {
865 // When called from JavaScript, simply output the error message.
866 print t('%type: %message in %function (line %line of %file).', $error);
867 exit;
868 }
869 }
870 else {
871 // Display the message if the current error reporting level allows this type
872 // of message to be displayed, and unconditionnaly in update.php.
873 $error_level = variable_get('error_level', ERROR_REPORTING_DISPLAY_ALL);
874 $display_error = $error_level == ERROR_REPORTING_DISPLAY_ALL || ($error_level == ERROR_REPORTING_DISPLAY_SOME && $error['%type'] != 'Notice');
875 if ($display_error || (defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'update')) {
876 $class = 'error';
877
878 // If error type is 'User notice' then treat it as debug information
879 // instead of an error message, see dd().
880 if ($error['%type'] == 'User notice') {
881 $error['%type'] = 'Debug';
882 $class = 'status';
883 }
884
885 drupal_set_message(t('%type: %message in %function (line %line of %file).', $error), $class);
886 }
887
888 if ($fatal) {
889 drupal_set_title(t('Error'));
890 // We fallback to a maintenance page at this point, because the page generation
891 // itself can generate errors.
892 print theme('maintenance_page', t('The website encountered an unexpected error. Please try again later.'), FALSE);
893 exit;
894 }
895 }
896 }
897
898 /**
899 * Gets the last caller from a backtrace.
900 *
901 * @param $backtrace
902 * A standard PHP backtrace.
903 * @return
904 * An associative array with keys 'file', 'line' and 'function'.
905 */
906 function _drupal_get_last_caller($backtrace) {
907 // Errors that occur inside PHP internal functions do not generate
908 // information about file and line. Ignore black listed functions.
909 $blacklist = array('debug');
910 while (($backtrace && !isset($backtrace[0]['line'])) ||
911 (isset($backtrace[1]['function']) && in_array($backtrace[1]['function'], $blacklist))) {
912 array_shift($backtrace);
913 }
914
915 // The first trace is the call itself.
916 // It gives us the line and the file of the last call.
917 $call = $backtrace[0];
918
919 // The second call give us the function where the call originated.
920 if (isset($backtrace[1])) {
921 if (isset($backtrace[1]['class'])) {
922 $call['function'] = $backtrace[1]['class'] . $backtrace[1]['type'] . $backtrace[1]['function'] . '()';
923 }
924 else {
925 $call['function'] = $backtrace[1]['function'] . '()';
926 }
927 }
928 else {
929 $call['function'] = 'main()';
930 }
931 return $call;
932 }
933
934 function _fix_gpc_magic(&$item) {
935 if (is_array($item)) {
936 array_walk($item, '_fix_gpc_magic');
937 }
938 else {
939 $item = stripslashes($item);
940 }
941 }
942
943 /**
944 * Helper function to strip slashes from $_FILES skipping over the tmp_name keys
945 * since PHP generates single backslashes for file paths on Windows systems.
946 *
947 * tmp_name does not have backslashes added see
948 * http://php.net/manual/en/features.file-upload.php#42280
949 */
950 function _fix_gpc_magic_files(&$item, $key) {
951 if ($key != 'tmp_name') {
952 if (is_array($item)) {
953 array_walk($item, '_fix_gpc_magic_files');
954 }
955 else {
956 $item = stripslashes($item);
957 }
958 }
959 }
960
961 /**
962 * Fix double-escaping problems caused by "magic quotes" in some PHP installations.
963 */
964 function fix_gpc_magic() {
965 $fixed = &drupal_static(__FUNCTION__, FALSE);
966 if (!$fixed && ini_get('magic_quotes_gpc')) {
967 array_walk($_GET, '_fix_gpc_magic');
968 array_walk($_POST, '_fix_gpc_magic');
969 array_walk($_COOKIE, '_fix_gpc_magic');
970 array_walk($_REQUEST, '_fix_gpc_magic');
971 array_walk($_FILES, '_fix_gpc_magic_files');
972 $fixed = TRUE;
973 }
974 }
975
976 /**
977 * Translate strings to the page language or a given language.
978 *
979 * Human-readable text that will be displayed somewhere within a page should
980 * be run through the t() function.
981 *
982 * Examples:
983 * @code
984 * if (!$info || !$info['extension']) {
985 * form_set_error('picture_upload', t('The uploaded file was not an image.'));
986 * }
987 *
988 * $form['submit'] = array(
989 * '#type' => 'submit',
990 * '#value' => t('Log in'),
991 * );
992 * @endcode
993 *
994 * Any text within t() can be extracted by translators and changed into
995 * the equivalent text in their native language.
996 *
997 * Special variables called "placeholders" are used to signal dynamic
998 * information in a string which should not be translated. Placeholders
999 * can also be used for text that may change from time to time (such as
1000 * link paths) to be changed without requiring updates to translations.
1001 *
1002 * For example:
1003 * @code
1004 * $output = t('There are currently %members and %visitors online.', array(
1005 * '%members' => format_plural($total_users, '1 user', '@count users'),
1006 * '%visitors' => format_plural($guests->count, '1 guest', '@count guests')));
1007 * @endcode
1008 *
1009 * There are three styles of placeholders:
1010 * - !variable, which indicates that the text should be inserted as-is. This is
1011 * useful for inserting variables into things like e-mail.
1012 * @code
1013 * $message[] = t("If you don't want to receive such e-mails, you can change your settings at !url.", array('!url' => url("user/$account->uid", array('absolute' => TRUE))));
1014 * @endcode
1015 *
1016 * - @variable, which indicates that the text should be run through
1017 * check_plain, to escape HTML characters. Use this for any output that's
1018 * displayed within a Drupal page.
1019 * @code
1020 * drupal_set_title($title = t("@name's blog", array('@name' => $account->name)), PASS_THROUGH);
1021 * @endcode
1022 *
1023 * - %variable, which indicates that the string should be HTML escaped and
1024 * highlighted with theme_placeholder() which shows up by default as
1025 * <em>emphasized</em>.
1026 * @code
1027 * $message = t('%name-from sent %name-to an e-mail.', array('%name-from' => $user->name, '%name-to' => $account->name));
1028 * @endcode
1029 *
1030 * When using t(), try to put entire sentences and strings in one t() call.
1031 * This makes it easier for translators, as it provides context as to what
1032 * each word refers to. HTML markup within translation strings is allowed, but
1033 * should be avoided if possible. The exception are embedded links; link
1034 * titles add a context for translators, so should be kept in the main string.
1035 *
1036 * Here is an example of incorrect usage of t():
1037 * @code
1038 * $output .= t('<p>Go to the @contact-page.</p>', array('@contact-page' => l(t('contact page'), 'contact')));
1039 * @endcode
1040 *
1041 * Here is an example of t() used correctly:
1042 * @code
1043 * $output .= '<p>' . t('Go to the <a href="@contact-page">contact page</a>.', array('@contact-page' => url('contact'))) . '</p>';
1044 * @endcode
1045 *
1046 * Avoid escaping quotation marks wherever possible.
1047 *
1048 * Incorrect:
1049 * @code
1050 * $output .= t('Don\'t click me.');
1051 * @endcode
1052 *
1053 * Correct:
1054 * @code
1055 * $output .= t("Don't click me.");
1056 * @endcode
1057 *
1058 * Because t() is designed for handling code-based strings, in almost all
1059 * cases, the actual string and not a variable must be passed through t().
1060 *
1061 * Extraction of translations is done based on the strings contained in t()
1062 * calls. If a variable is passed through t(), the content of the variable
1063 * cannot be extracted from the file for translation.
1064 *
1065 * Incorrect:
1066 * @code
1067 * $message = 'An error occurred.';
1068 * drupal_set_message(t($message), 'error');
1069 * $output .= t($message);
1070 * @endcode
1071 *
1072 * Correct:
1073 * @code
1074 * $message = t('An error occurred.');
1075 * drupal_set_message($message, 'error');
1076 * $output .= $message;
1077 * @endcode
1078 *
1079 * The only case in which variables can be passed safely through t() is when
1080 * code-based versions of the same strings will be passed through t() (or
1081 * otherwise extracted) elsewhere.
1082 *
1083 * In some cases, modules may include strings in code that can't use t()
1084 * calls. For example, a module may use an external PHP application that
1085 * produces strings that are loaded into variables in Drupal for output.
1086 * In these cases, module authors may include a dummy file that passes the
1087 * relevant strings through t(). This approach will allow the strings to be
1088 * extracted.
1089 *
1090 * Sample external (non-Drupal) code:
1091 * @code
1092 * class Time {
1093 * public $yesterday = 'Yesterday';
1094 * public $today = 'Today';
1095 * public $tomorrow = 'Tomorrow';
1096 * }
1097 * @endcode
1098 *
1099 * Sample dummy file.
1100 * @code
1101 * // Dummy function included in example.potx.inc.
1102 * function example_potx() {
1103 * $strings = array(
1104 * t('Yesterday'),
1105 * t('Today'),
1106 * t('Tomorrow'),
1107 * );
1108 * // No return value needed, since this is a dummy function.
1109 * }
1110 * @endcode
1111 *
1112 * Having passed strings through t() in a dummy function, it is then
1113 * okay to pass variables through t().
1114 *
1115 * Correct (if a dummy file was used):
1116 * @code
1117 * $time = new Time();
1118 * $output .= t($time->today);
1119 * @endcode
1120 *
1121 * However tempting it is, custom data from user input or other non-code
1122 * sources should not be passed through t(). Doing so leads to the following
1123 * problems and errors:
1124 * - The t() system doesn't support updates to existing strings. When user
1125 * data is updated, the next time it's passed through t() a new record is
1126 * created instead of an update. The database bloats over time and any
1127 * existing translations are orphaned with each update.
1128 * - The t() system assumes any data it receives is in English. User data may
1129 * be in another language, producing translation errors.
1130 * - The "Built-in interface" text group in the locale system is used to
1131 * produce translations for storage in .po files. When non-code strings are
1132 * passed through t(), they are added to this text group, which is rendered
1133 * inaccurate since it is a mix of actual interface strings and various user
1134 * input strings of uncertain origin.
1135 *
1136 * Incorrect:
1137 * @code
1138 * $item = item_load();
1139 * $output .= check_plain(t($item['title']));
1140 * @endcode
1141 *
1142 * Instead, translation of these data can be done through the locale system,
1143 * either directly or through helper functions provided by contributed
1144 * modules.
1145 * @see hook_locale()
1146 *
1147 * During installation, st() is used in place of t(). Code that may be called
1148 * during installation or during normal operation should use the get_t()
1149 * helper function.
1150 * @see st()
1151 * @see get_t()
1152 *
1153 * @param $string
1154 * A string containing the English string to translate.
1155 * @param $args
1156 * An associative array of replacements to make after translation. Incidences
1157 * of any key in this array are replaced with the corresponding value. Based
1158 * on the first character of the key, the value is escaped and/or themed:
1159 * - !variable: inserted as is
1160 * - @variable: escape plain text to HTML (check_plain)
1161 * - %variable: escape text and theme as a placeholder for user-submitted
1162 * content (check_plain + theme_placeholder)
1163 * @param $options
1164 * An associative array of additional options, with the following keys:
1165 * - 'langcode' (default to the current language) The language code to
1166 * translate to a language other than what is used to display the page.
1167 * - 'context' (default to the empty context) The context the source string
1168 * belongs to.
1169 * @return
1170 * The translated string.
1171 */
1172 function t($string, array $args = array(), array $options = array()) {
1173 global $language;
1174 static $custom_strings;
1175
1176 // Merge in default.
1177 if (empty($options['langcode'])) {
1178 $options['langcode'] = isset($language->language) ? $language->language : 'en';
1179 }
1180 if (empty($options['context'])) {
1181 $options['context'] = '';
1182 }
1183
1184 // First, check for an array of customized strings. If present, use the array
1185 // *instead of* database lookups. This is a high performance way to provide a
1186 // handful of string replacements. See settings.php for examples.
1187 // Cache the $custom_strings variable to improve performance.
1188 if (!isset($custom_strings[$options['langcode']])) {
1189 $custom_strings[$options['langcode']] = variable_get('locale_custom_strings_' . $options['langcode'], array());
1190 }
1191 // Custom strings work for English too, even if locale module is disabled.
1192 if (isset($custom_strings[$options['langcode']][$options['context']][$string])) {
1193 $string = $custom_strings[$options['langcode']][$options['context']][$string];
1194 }
1195 // Translate with locale module if enabled.
1196 // We don't use drupal_function_exists() here, because it breaks the testing
1197 // framework if the locale module is enabled in the parent site (we cannot
1198 // unload functions in PHP).
1199 elseif (function_exists('locale') && $options['langcode'] != 'en') {
1200 $string = locale($string, $options['context'], $options['langcode']);
1201 }
1202 if (empty($args)) {
1203 return $string;
1204 }
1205 else {
1206 // Transform arguments before inserting them.
1207 foreach ($args as $key => $value) {
1208 switch ($key[0]) {
1209 case '@':
1210 // Escaped only.
1211 $args[$key] = check_plain($value);
1212 break;
1213
1214 case '%':
1215 default:
1216 // Escaped and placeholder.
1217 $args[$key] = theme('placeholder', $value);
1218 break;
1219
1220 case '!':
1221 // Pass-through.
1222 }
1223 }
1224 return strtr($string, $args);
1225 }
1226 }
1227
1228 /**
1229 * @defgroup validation Input validation
1230 * @{
1231 * Functions to validate user input.
1232 */
1233
1234 /**
1235 * Verify the syntax of the given e-mail address.
1236 *
1237 * Empty e-mail addresses are allowed. See RFC 2822 for details.
1238 *
1239 * @param $mail
1240 * A string containing an e-mail address.
1241 * @return
1242 * TRUE if the address is in a valid format.
1243 */
1244 function valid_email_address($mail) {
1245 return (bool)filter_var($mail, FILTER_VALIDATE_EMAIL);
1246 }
1247
1248 /**
1249 * Verify the syntax of the given URL.
1250 *
1251 * This function should only be used on actual URLs. It should not be used for
1252 * Drupal menu paths, which can contain arbitrary characters.
1253 * Valid values per RFC 3986.
1254 * @param $url
1255 * The URL to verify.
1256 * @param $absolute
1257 * Whether the URL is absolute (beginning with a scheme such as "http:").
1258 * @return
1259 * TRUE if the URL is in a valid format.
1260 */
1261 function valid_url($url, $absolute = FALSE) {
1262 if ($absolute) {
1263 return (bool)preg_match("
1264 /^ # Start at the beginning of the text
1265 (?:ftp|https?):\/\/ # Look for ftp, http, or https schemes
1266 (?: # Userinfo (optional) which is typically
1267 (?:(?:[\w\.\-\+!$&'\(\)*\+,;=]|%[0-9a-f]{2})+:)* # a username or a username and password
1268 (?:[\w\.\-\+%!$&'\(\)*\+,;=]|%[0-9a-f]{2})+@ # combination
1269 )?
1270 (?:
1271 (?:[a-z0-9\-\.]|%[0-9a-f]{2})+ # A domain name or a IPv4 address
1272 |(?:\[(?:[0-9a-f]{0,4}:)*(?:[0-9a-f]{0,4})\]) # or a well formed IPv6 address
1273 )
1274 (?::[0-9]+)? # Server port number (optional)
1275 (?:[\/|\?]
1276 (?:[\w#!:\.\?\+=&@$'~*,;\/\(\)\[\]\-]|%[0-9a-f]{2}) # The path and query (optional)
1277 *)?
1278 $/xi", $url);
1279 }
1280 else {
1281 return (bool)preg_match("/^(?:[\w#!:\.\?\+=&@$'~*,;\/\(\)\[\]\-]|%[0-9a-f]{2})+$/i", $url);
1282 }
1283 }
1284
1285 /**
1286 * @} End of "defgroup validation".
1287 */
1288
1289 /**
1290 * Register an event for the current visitor to the flood control mechanism.
1291 *
1292 * @param $name
1293 * The name of an event.
1294 * @param $identifier
1295 * Optional identifier (defaults to the current user's IP address).
1296 */
1297 function flood_register_event($name, $identifier = NULL) {
1298 if (!isset($identifier)) {
1299 $identifier = ip_address();
1300 }
1301 db_insert('flood')
1302 ->fields(array(
1303 'event' => $name,
1304 'identifier' => $identifier,
1305 'timestamp' => REQUEST_TIME,
1306 ))
1307 ->execute();
1308 }
1309
1310 /**
1311 * Make the flood control mechanism forget about an event for the current visitor.
1312 *
1313 * @param $name
1314 * The name of an event.
1315 * @param $identifier
1316 * Optional identifier (defaults to the current user's IP address).
1317 */
1318 function flood_clear_event($name, $identifier = NULL) {
1319 if (!isset($identifier)) {
1320 $identifier = ip_address();
1321 }
1322 db_delete('flood')
1323 ->condition('event', $name)
1324 ->condition('identifier', $identifier)
1325 ->execute();
1326 }
1327
1328 /**
1329 * Check if the current visitor is allowed to proceed with the specified event.
1330 *
1331 * The user is allowed to proceed if he did not trigger the specified event more
1332 * than $threshold times in the specified time window.
1333 *
1334 * @param $name
1335 * The name of the event.
1336 * @param $threshold
1337 * The maximum number of the specified event allowed per time window.
1338 * @param $window
1339 * Optional number of seconds over which to look for events. Defaults to
1340 * 3600 (1 hour).
1341 * @param $identifier
1342 * Optional identifier (defaults to the current user's IP address).
1343 * @return
1344 * True if the user did not exceed the hourly threshold. False otherwise.
1345 */
1346 function flood_is_allowed($name, $threshold, $window = 3600, $identifier = NULL) {
1347 if (!isset($identifier)) {
1348 $identifier = ip_address();
1349 }
1350 $number = db_query("SELECT COUNT(*) FROM {flood} WHERE event = :event AND identifier = :identifier AND timestamp > :timestamp", array(
1351 ':event' => $name,
1352 ':identifier' => $identifier,
1353 ':timestamp' => REQUEST_TIME - $window))
1354 ->fetchField();
1355 return ($number < $threshold);
1356 }
1357
1358 function check_file($filename) {
1359 return is_uploaded_file($filename);
1360 }
1361
1362 /**
1363 * @defgroup sanitization Sanitization functions
1364 * @{
1365 * Functions to sanitize values.
1366 */
1367
1368 /**
1369 * Prepare a URL for use in an HTML attribute. Strips harmful protocols.
1370 */
1371 function check_url($uri) {
1372 return filter_xss_bad_protocol($uri, FALSE);
1373 }
1374
1375 /**
1376 * Very permissive XSS/HTML filter for admin-only use.
1377 *
1378 * Use only for fields where it is impractical to use the
1379 * whole filter system, but where some (mainly inline) mark-up
1380 * is desired (so check_plain() is not acceptable).
1381 *
1382 * Allows all tags that can be used inside an HTML body, save
1383 * for scripts and styles.
1384 */
1385 function filter_xss_admin($string) {
1386 return filter_xss($string, array('a', 'abbr', 'acronym', 'address', 'b', 'bdo', 'big', 'blockquote', 'br', 'caption', 'cite', 'code', 'col', 'colgroup', 'dd', 'del', 'dfn', 'div', 'dl', 'dt', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'ins', 'kbd', 'li', 'ol', 'p', 'pre', 'q', 'samp', 'small', 'span', 'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'tt', 'ul', 'var'));
1387 }
1388
1389 /**
1390 * Filter XSS.
1391 *
1392 * Based on kses by Ulf Harnhammar, see
1393 * http://sourceforge.net/projects/kses
1394 *
1395 * For examples of various XSS attacks, see:
1396 * http://ha.ckers.org/xss.html
1397 *
1398 * This code does four things:
1399 * - Removes characters and constructs that can trick browsers
1400 * - Makes sure all HTML entities are well-formed
1401 * - Makes sure all HTML tags and attributes are well-formed
1402 * - Makes sure no HTML tags contain URLs with a disallowed protocol (e.g. javascript:)
1403 *
1404 * @param $string
1405 * The string with raw HTML in it. It will be stripped of everything that can cause
1406 * an XSS attack.
1407 * @param $allowed_tags
1408 * An array of allowed tags.
1409 */
1410 function filter_xss($string, $allowed_tags = array('a', 'em', 'strong', 'cite', 'blockquote', 'code', 'ul', 'ol', 'li', 'dl', 'dt', 'dd')) {
1411 // Only operate on valid UTF-8 strings. This is necessary to prevent cross
1412 // site scripting issues on Internet Explorer 6.
1413 if (!drupal_validate_utf8($string)) {
1414 return '';
1415 }
1416 // Store the text format
1417 _filter_xss_split($allowed_tags, TRUE);
1418 // Remove NULL characters (ignored by some browsers)
1419 $string = str_replace(chr(0), '', $string);
1420 // Remove Netscape 4 JS entities
1421 $string = preg_replace('%&\s*\{[^}]*(\}\s*;?|$)%', '', $string);
1422
1423 // Defuse all HTML entities
1424 $string = str_replace('&', '&amp;', $string);
1425 // Change back only well-formed entities in our whitelist
1426 // Decimal numeric entities
1427 $string = preg_replace('/&amp;#([0-9]+;)/', '&#\1', $string);
1428 // Hexadecimal numeric entities
1429 $string = preg_replace('/&amp;#[Xx]0*((?:[0-9A-Fa-f]{2})+;)/', '&#x\1', $string);
1430 // Named entities
1431 $string = preg_replace('/&amp;([A-Za-z][A-Za-z0-9]*;)/', '&\1', $string);
1432
1433 return preg_replace_callback('%
1434 (
1435 <(?=[^a-zA-Z!/]) # a lone <
1436 | # or
1437 <[^>]*(>|$) # a string that starts with a <, up until the > or the end of the string
1438 | # or
1439 > # just a >
1440 )%x', '_filter_xss_split', $string);
1441 }
1442
1443 /**
1444 * Processes an HTML tag.
1445 *
1446 * @param $m
1447 * An array with various meaning depending on the value of $store.
1448 * If $store is TRUE then the array contains the allowed tags.
1449 * If $store is FALSE then the array has one element, the HTML tag to process.
1450 * @param $store
1451 * Whether to store $m.
1452 * @return
1453 * If the element isn't allowed, an empty string. Otherwise, the cleaned up
1454 * version of the HTML element.
1455 */
1456 function _filter_xss_split($m, $store = FALSE) {
1457 static $allowed_html;
1458
1459 if ($store) {
1460 $allowed_html = array_flip($m);
1461 return;
1462 }
1463
1464 $string = $m[1];
1465
1466 if (substr($string, 0, 1) != '<') {
1467 // We matched a lone ">" character
1468 return '&gt;';
1469 }
1470 elseif (strlen($string) == 1) {
1471 // We matched a lone "<" character
1472 return '&lt;';
1473 }
1474
1475 if (!preg_match('%^<\s*(/\s*)?([a-zA-Z0-9]+)([^>]*)>?$%', $string, $matches)) {
1476 // Seriously malformed
1477 return '';
1478 }
1479
1480 $slash = trim($matches[1]);
1481 $elem = &$matches[2];
1482 $attrlist = &$matches[3];
1483
1484 if (!isset($allowed_html[strtolower($elem)])) {
1485 // Disallowed HTML element
1486 return '';
1487 }
1488
1489 if ($slash != '') {
1490 return "</$elem>";
1491 }
1492
1493 // Is there a closing XHTML slash at the end of the attributes?
1494 $attrlist = preg_replace('%(\s?)/\s*$%', '\1', $attrlist, -1, $count);
1495 $xhtml_slash = $count ? ' /' : '';
1496
1497 // Clean up attributes
1498 $attr2 = implode(' ', _filter_xss_attributes($attrlist));
1499 $attr2 = preg_replace('/[<>]/', '', $attr2);
1500 $attr2 = strlen($attr2) ? ' ' . $attr2 : '';
1501
1502 return "<$elem$attr2$xhtml_slash>";
1503 }
1504
1505 /**
1506 * Processes a string of HTML attributes.
1507 *
1508 * @return
1509 * Cleaned up version of the HTML attributes.
1510 */
1511 function _filter_xss_attributes($attr) {
1512 $attrarr = array();
1513 $mode = 0;
1514 $attrname = '';
1515
1516 while (strlen($attr) != 0) {
1517 // Was the last operation successful?
1518 $working = 0;
1519
1520 switch ($mode) {
1521 case 0:
1522 // Attribute name, href for instance
1523 if (preg_match('/^([-a-zA-Z]+)/', $attr, $match)) {
1524 $attrname = strtolower($match[1]);
1525 $skip = ($attrname == 'style' || substr($attrname, 0, 2) == 'on');
1526 $working = $mode = 1;
1527 $attr = preg_replace('/^[-a-zA-Z]+/', '', $attr);
1528 }
1529 break;
1530
1531 case 1:
1532 // Equals sign or valueless ("selected")
1533 if (preg_match('/^\s*=\s*/', $attr)) {
1534 $working = 1; $mode = 2;
1535 $attr = preg_replace('/^\s*=\s*/', '', $attr);
1536 break;
1537 }
1538
1539 if (preg_match('/^\s+/', $attr)) {
1540 $working = 1; $mode = 0;
1541 if (!$skip) {
1542 $attrarr[] = $attrname;
1543 }
1544 $attr = preg_replace('/^\s+/', '', $attr);
1545 }
1546 break;
1547
1548 case 2:
1549 // Attribute value, a URL after href= for instance
1550 if (preg_match('/^"([^"]*)"(\s+|$)/', $attr, $match)) {
1551 $thisval = filter_xss_bad_protocol($match[1]);
1552
1553 if (!$skip) {
1554 $attrarr[] = "$attrname=\"$thisval\"";
1555 }
1556 $working = 1;
1557 $mode = 0;
1558 $attr = preg_replace('/^"[^"]*"(\s+|$)/', '', $attr);
1559 break;
1560 }
1561
1562 if (preg_match("/^'([^']*)'(\s+|$)/", $attr, $match)) {
1563 $thisval = filter_xss_bad_protocol($match[1]);
1564
1565 if (!$skip) {
1566 $attrarr[] = "$attrname='$thisval'";
1567 }
1568 $working = 1; $mode = 0;
1569 $attr = preg_replace("/^'[^']*'(\s+|$)/", '', $attr);
1570 break;
1571 }
1572
1573 if (preg_match("%^([^\s\"']+)(\s+|$)%", $attr, $match)) {
1574 $thisval = filter_xss_bad_protocol($match[1]);
1575
1576 if (!$skip) {
1577 $attrarr[] = "$attrname=\"$thisval\"";
1578 }
1579 $working = 1; $mode = 0;
1580 $attr = preg_replace("%^[^\s\"']+(\s+|$)%", '', $attr);
1581 }
1582 break;
1583 }
1584
1585 if ($working == 0) {
1586 // not well formed, remove and try again
1587 $attr = preg_replace('/
1588 ^
1589 (
1590 "[^"]*("|$) # - a string that starts with a double quote, up until the next double quote or the end of the string
1591 | # or
1592 \'[^\']*(\'|$)| # - a string that starts with a quote, up until the next quote or the end of the string
1593 | # or
1594 \S # - a non-whitespace character
1595 )* # any number of the above three
1596 \s* # any number of whitespaces
1597 /x', '', $attr);
1598 $mode = 0;
1599 }
1600 }
1601
1602 // The attribute list ends with a valueless attribute like "selected".
1603 if ($mode == 1) {
1604 $attrarr[] = $attrname;
1605 }
1606 return $attrarr;
1607 }
1608
1609 /**
1610 * Processes an HTML attribute value and ensures it does not contain an URL with a disallowed protocol (e.g. javascript:).
1611 *
1612 * @param $string
1613 * The string with the attribute value.
1614 * @param $decode
1615 * Whether to decode entities in the $string. Set to FALSE if the $string
1616 * is in plain text, TRUE otherwise. Defaults to TRUE.
1617 * @return
1618 * Cleaned up and HTML-escaped version of $string.
1619 */
1620 function filter_xss_bad_protocol($string, $decode = TRUE) {
1621 static $allowed_protocols;
1622
1623 if (!isset($allowed_protocols)) {
1624 $allowed_protocols = array_flip(variable_get('filter_allowed_protocols', array('ftp', 'http', 'https', 'irc', 'mailto', 'news', 'nntp', 'rtsp', 'sftp', 'ssh', 'telnet', 'webcal')));
1625 }
1626
1627 // Get the plain text representation of the attribute value (i.e. its meaning).
1628 if ($decode) {
1629 $string = decode_entities($string);
1630 }
1631
1632 // Iteratively remove any invalid protocol found.
1633 do {
1634 $before = $string;
1635 $colonpos = strpos($string, ':');
1636 if ($colonpos > 0) {
1637 // We found a colon, possibly a protocol. Verify.
1638 $protocol = substr($string, 0, $colonpos);
1639 // If a colon is preceded by a slash, question mark or hash, it cannot
1640 // possibly be part of the URL scheme. This must be a relative URL,
1641 // which inherits the (safe) protocol of the base document.
1642 if (preg_match('![/?#]!', $protocol)) {
1643 break;
1644 }
1645 // Per RFC2616, section 3.2.3 (URI Comparison) scheme comparison must be case-insensitive
1646 // Check if this is a disallowed protocol.
1647 if (!isset($allowed_protocols[strtolower($protocol)])) {
1648 $string = substr($string, $colonpos + 1);
1649 }
1650 }
1651 } while ($before != $string);
1652
1653 return check_plain($string);
1654 }
1655
1656 /**
1657 * @} End of "defgroup sanitization".
1658 */
1659
1660 /**
1661 * @defgroup format Formatting
1662 * @{
1663 * Functions to format numbers, strings, dates, etc.
1664 */
1665
1666 /**
1667 * Formats an RSS channel.
1668 *
1669 * Arbitrary elements may be added using the $args associative array.
1670 */
1671 function format_rss_channel($title, $link, $description, $items, $langcode = NULL, $args = array()) {
1672 global $language;
1673 $langcode = $langcode ? $langcode : $language->language;
1674
1675 $output = "<channel>\n";
1676 $output .= ' <title>' . check_plain($title) . "</title>\n";
1677 $output .= ' <link>' . check_url($link) . "</link>\n";
1678
1679 // The RSS 2.0 "spec" doesn't indicate HTML can be used in the description.
1680 // We strip all HTML tags, but need to prevent double encoding from properly
1681 // escaped source data (such as &amp becoming &amp;amp;).
1682 $output .= ' <description>' . check_plain(decode_entities(strip_tags($description))) . "</description>\n";
1683 $output .= ' <language>' . check_plain($langcode) . "</language>\n";
1684 $output .= format_xml_elements($args);
1685 $output .= $items;
1686 $output .= "</channel>\n";
1687
1688 return $output;
1689 }
1690
1691 /**
1692 * Format a single RSS item.
1693 *
1694 * Arbitrary elements may be added using the $args associative array.
1695 */
1696 function format_rss_item($title, $link, $description, $args = array()) {
1697 $output = "<item>\n";
1698 $output .= ' <title>' . check_plain($title) . "</title>\n";
1699 $output .= ' <link>' . check_url($link) . "</link>\n";
1700 $output .= ' <description>' . check_plain($description) . "</description>\n";
1701 $output .= format_xml_elements($args);
1702 $output .= "</item>\n";
1703
1704 return $output;
1705 }
1706
1707 /**
1708 * Format XML elements.
1709 *
1710 * @param $array
1711 * An array where each item represent an element and is either a:
1712 * - (key => value) pair (<key>value</key>)
1713 * - Associative array with fields:
1714 * - 'key': element name
1715 * - 'value': element contents
1716 * - 'attributes': associative array of element attributes
1717 *
1718 * In both cases, 'value' can be a simple string, or it can be another array
1719 * with the same format as $array itself for nesting.
1720 */
1721 function format_xml_elements($array) {
1722 $output = '';
1723 foreach ($array as $key => $value) {
1724 if (is_numeric($key)) {
1725 if ($value['key']) {
1726 $output .= ' <' . $value['key'];
1727 if (isset($value['attributes']) && is_array($value['attributes'])) {
1728 $output .= drupal_attributes($value['attributes']);
1729 }
1730
1731 if (isset($value['value']) && $value['value'] != '') {
1732 $output .= '>' . (is_array($value['value']) ? format_xml_elements($value['value']) : check_plain($value['value'])) . '</' . $value['key'] . ">\n";
1733 }
1734 else {
1735 $output .= " />\n";
1736 }
1737 }
1738 }
1739 else {
1740 $output .= ' <' . $key . '>' . (is_array($value) ? format_xml_elements($value) : check_plain($value)) . "</$key>\n";
1741 }
1742 }
1743 return $output;
1744 }
1745
1746 /**
1747 * Format a string containing a count of items.
1748 *
1749 * This function ensures that the string is pluralized correctly. Since t() is
1750 * called by this function, make sure not to pass already-localized strings to
1751 * it.
1752 *
1753 * For example:
1754 * @code
1755 * $output = format_plural($node->comment_count, '1 comment', '@count comments');
1756 * @endcode
1757 *
1758 * Example with additional replacements:
1759 * @code
1760 * $output = format_plural($update_count,
1761 * 'Changed the content type of 1 post from %old-type to %new-type.',
1762 * 'Changed the content type of @count posts from %old-type to %new-type.',
1763 * array('%old-type' => $info->old_type, '%new-type' => $info->new_type)));
1764 * @endcode
1765 *
1766 * @param $count
1767 * The item count to display.
1768 * @param $singular
1769 * The string for the singular case. Please make sure it is clear this is
1770 * singular, to ease translation (e.g. use "1 new comment" instead of "1 new").
1771 * Do not use @count in the singular string.
1772 * @param $plural
1773 * The string for the plural case. Please make sure it is clear this is plural,
1774 * to ease translation. Use @count in place of the item count, as in "@count
1775 * new comments".
1776 * @param $args
1777 * An associative array of replacements to make after translation. Incidences
1778 * of any key in this array are replaced with the corresponding value.
1779 * Based on the first character of the key, the value is escaped and/or themed:
1780 * - !variable: inserted as is
1781 * - @variable: escape plain text to HTML (check_plain)
1782 * - %variable: escape text and theme as a placeholder for user-submitted
1783 * content (check_plain + theme_placeholder)
1784 * Note that you do not need to include @count in this array.
1785 * This replacement is done automatically for the plural case.
1786 * @param $options
1787 * An associative array of additional options, with the following keys:
1788 * - 'langcode' (default to the current language) The language code to
1789 * translate to a language other than what is used to display the page.
1790 * - 'context' (default to the empty context) The context the source string
1791 * belongs to.
1792 * @return
1793 * A translated string.
1794 */
1795 function format_plural($count, $singular, $plural, array $args = array(), array $options = array()) {
1796 $args['@count'] = $count;
1797 if ($count == 1) {
1798 return t($singular, $args, $options);
1799 }
1800
1801 // Get the plural index through the gettext formula.
1802 $index = (function_exists('locale_get_plural')) ? locale_get_plural($count, isset($options['langcode']) ? $options['langcode'] : NULL) : -1;
1803 // Backwards compatibility.
1804 if ($index < 0) {
1805 return t($plural, $args, $options);
1806 }
1807 else {
1808 switch ($index) {
1809 case "0":
1810 return t($singular, $args, $options);
1811 case "1":
1812 return t($plural, $args, $options);
1813 default:
1814 unset($args['@count']);
1815 $args['@count[' . $index . ']'] = $count;
1816 return t(strtr($plural, array('@count' => '@count[' . $index . ']')), $args, $options);
1817 }
1818 }
1819 }
1820
1821 /**
1822 * Parse a given byte count.
1823 *
1824 * @param $size
1825 * A size expressed as a number of bytes with optional SI or IEC binary unit
1826 * prefix (e.g. 2, 3K, 5MB, 10G, 6GiB, 8 bytes, 9mbytes).
1827 * @return
1828 * An integer representation of the size in bytes.
1829 */
1830 function parse_size($size) {
1831 $unit = preg_replace('/[^bkmgtpezy]/i', '', $size); // Remove the non-unit characters from the size.
1832 $size = preg_replace('/[^0-9\.]/', '', $size); // Remove the non-numeric characters from the size.
1833 if ($unit) {
1834 // Find the position of the unit in the ordered string which is the power of magnitude to multiply a kilobyte by.
1835 return round($size * pow(DRUPAL_KILOBYTE, stripos('bkmgtpezy', $unit[0])));
1836 }
1837 else {
1838 return round($size);
1839 }
1840 }
1841
1842 /**
1843 * Generate a string representation for the given byte count.
1844 *
1845 * @param $size
1846 * A size in bytes.
1847 * @param $langcode
1848 * Optional language code to translate to a language other than what is used
1849 * to display the page.
1850 * @return
1851 * A translated string representation of the size.
1852 */
1853 function format_size($size, $langcode = NULL) {
1854 if ($size < DRUPAL_KILOBYTE) {
1855 return format_plural($size, '1 byte', '@count bytes', array(), array('langcode' => $langcode));
1856 }
1857 else {
1858 $size = $size / DRUPAL_KILOBYTE; // Convert bytes to kilobytes.
1859 $units = array(
1860 t('@size KB', array(), array('langcode' => $langcode)),
1861 t('@size MB', array(), array('langcode' => $langcode)),
1862 t('@size GB', array(), array('langcode' => $langcode)),
1863 t('@size TB', array(), array('langcode' => $langcode)),
1864 t('@size PB', array(), array('langcode' => $langcode)),
1865 t('@size EB', array(), array('langcode' => $langcode)),
1866 t('@size ZB', array(), array('langcode' => $langcode)),
1867 t('@size YB', array(), array('langcode' => $langcode)),
1868 );
1869 foreach ($units as $unit) {
1870 if (round($size, 2) >= DRUPAL_KILOBYTE) {
1871 $size = $size / DRUPAL_KILOBYTE;
1872 }
1873 else {
1874 break;
1875 }
1876 }
1877 return str_replace('@size', round($size, 2), $unit);
1878 }
1879 }
1880
1881 /**
1882 * Format a time interval with the requested granularity.
1883 *
1884 * @param $timestamp
1885 * The length of the interval in seconds.
1886 * @param $granularity
1887 * How many different units to display in the string.
1888 * @param $langcode
1889 * Optional language code to translate to a language other than
1890 * what is used to display the page.
1891 * @return
1892 * A translated string representation of the interval.
1893 */
1894 function format_interval($timestamp, $granularity = 2, $langcode = NULL) {
1895 $units = array(
1896 '1 year|@count years' => 31536000,
1897 '1 month|@count months' => 2592000,
1898 '1 week|@count weeks' => 604800,
1899 '1 day|@count days' => 86400,
1900 '1 hour|@count hours' => 3600,
1901 '1 min|@count min' => 60,
1902 '1 sec|@count sec' => 1
1903 );
1904 $output = '';
1905 foreach ($units as $key => $value) {
1906 $key = explode('|', $key);
1907 if ($timestamp >= $value) {
1908 $output .= ($output ? ' ' : '') . format_plural(floor($timestamp / $value), $key[0], $key[1], array(), array('langcode' => $langcode));
1909 $timestamp %= $value;
1910 $granularity--;
1911 }
1912
1913 if ($granularity == 0) {
1914 break;
1915 }
1916 }
1917 return $output ? $output : t('0 sec', array(), array('langcode' => $langcode));
1918 }
1919
1920 /**
1921 * Format a date with the given configured format or a custom format string.
1922 *
1923 * Drupal allows administrators to select formatting strings for 'small',
1924 * 'medium' and 'large' date formats. This function can handle these formats,
1925 * as well as any custom format.
1926 *
1927 * @param $timestamp
1928 * The exact date to format, as a UNIX timestamp.
1929 * @param $type
1930 * The format to use. Can be "small", "medium" or "large" for the preconfigured
1931 * date formats. If "custom" is specified, then $format is required as well.
1932 * @param $format
1933 * A PHP date format string as required by date(). A backslash should be used
1934 * before a character to avoid interpreting the character as part of a date
1935 * format.
1936 * @param $timezone
1937 * Time zone identifier; if omitted, the user's time zone is used.
1938 * @param $langcode
1939 * Optional language code to translate to a language other than what is used
1940 * to display the page.
1941 * @return
1942 * A translated date string in the requested format.
1943 */
1944 function format_date($timestamp, $type = 'medium', $format = '', $timezone = NULL, $langcode = NULL) {
1945 $timezones = &drupal_static(__FUNCTION__, array());
1946 if (!isset($timezone)) {
1947 global $user;
1948 if (variable_get('configurable_timezones', 1) && $user->uid && $user->timezone) {
1949 $timezone = $user->timezone;
1950 }
1951 else {
1952 $timezone = variable_get('date_default_timezone', 'UTC');
1953 }
1954 }
1955 // Store DateTimeZone objects in an array rather than repeatedly
1956 // contructing identical objects over the life of a request.
1957 if (!isset($timezones[$timezone])) {
1958 $timezones[$timezone] = timezone_open($timezone);
1959 }
1960
1961 // Use the default langcode if none is set.
1962 global $language;
1963 if (empty($langcode)) {
1964 $langcode = isset($language->language) ? $language->language : 'en';
1965 }
1966
1967 switch ($type) {
1968 case 'small':
1969 $format = variable_get('date_format_short', 'm/d/Y - H:i');
1970 break;
1971 case 'large':
1972 $format = variable_get('date_format_long', 'l, F j, Y - H:i');
1973 break;
1974 case 'custom':
1975 // No change to format.
1976 break;
1977 case 'medium':
1978 default:
1979 $format = variable_get('date_format_medium', 'D, m/d/Y - H:i');
1980 }
1981
1982 // Create a DateTime object from the timestamp.
1983 $date_time = date_create('@' . $timestamp);
1984 // Set the time zone for the DateTime object.
1985 date_timezone_set($date_time, $timezones[$timezone]);
1986
1987 // Encode markers that should be translated. 'A' becomes '\xEF\AA\xFF'.
1988 // xEF and xFF are invalid UTF-8 sequences, and we assume they are not in the
1989 // input string.
1990 // Paired backslashes are isolated to prevent errors in read-ahead evaluation.
1991 // The read-ahead expression ensures that A matches, but not \A.
1992 $format = preg_replace(array('/\\\\\\\\/', '/(?<!\\\\)([AaeDlMTF])/'), array("\xEF\\\\\\\\\xFF", "\xEF\\\\\$1\$1\xFF"), $format);
1993
1994 // Call date_format().
1995 $format = date_format($date_time, $format);
1996
1997 // Pass the langcode to _format_date_callback().
1998 _format_date_callback(NULL, $langcode);
1999
2000 // Translate the marked sequences.
2001 return preg_replace_callback('/\xEF([AaeDlMTF]?)(.*?)\xFF/', '_format_date_callback', $format);
2002 }
2003
2004 /**
2005 * Callback function for preg_replace_callback().
2006 */
2007 function _format_date_callback(array $matches = NULL, $new_langcode = NULL) {
2008 // We cache translations to avoid redundant and rather costly calls to t().
2009 static $cache, $langcode;
2010
2011 if (!isset($matches)) {
2012 $langcode = $new_langcode;
2013 return;
2014 }
2015
2016 $code = $matches[1];
2017 $string = $matches[2];
2018
2019 if (!isset($cache[$langcode][$code][$string])) {
2020 $options = array(
2021 'langcode' => $langcode,
2022 );
2023
2024 if ($code == 'F') {
2025 $options['context'] = 'Long month name';
2026 }
2027
2028 if ($code == '') {
2029 $cache[$langcode][$code][$string] = $string;
2030 }
2031 else {
2032 $cache[$langcode][$code][$string] = t($string, array(), $options);
2033 }
2034 }
2035 return $cache[$langcode][$code][$string];
2036 }
2037
2038 /**
2039 * @} End of "defgroup format".
2040 */
2041
2042 /**
2043 * Generate a URL from a Drupal menu path. Will also pass-through existing URLs.
2044 *
2045 * @param $path
2046 * The Drupal path being linked to, such as "admin/content", or an
2047 * existing URL like "http://drupal.org/". The special path
2048 * '<front>' may also be given and will generate the site's base URL.
2049 * @param $options
2050 * An associative array of additional options, with the following keys:
2051 * - 'query'
2052 * A URL-encoded query string to append to the link, or an array of query
2053 * key/value-pairs without any URL-encoding.
2054 * - 'fragment'
2055 * A fragment identifier (or named anchor) to append to the link.
2056 * Do not include the '#' character.
2057 * - 'absolute' (default FALSE)
2058 * Whether to force the output to be an absolute link (beginning with
2059 * http:). Useful for links that will be displayed outside the site, such
2060 * as in an RSS feed.
2061 * - 'alias' (default FALSE)
2062 * Whether the given path is an alias already.
2063 * - 'external'
2064 * Whether the given path is an external URL.
2065 * - 'language'
2066 * An optional language object. Used to build the URL to link to and
2067 * look up the proper alias for the link.
2068 * - 'base_url'
2069 * Only used internally, to modify the base URL when a language dependent
2070 * URL requires so.
2071 * - 'prefix'
2072 * Only used internally, to modify the path when a language dependent URL
2073 * requires so.
2074 * @return
2075 * A string containing a URL to the given path.
2076 *
2077 * When creating links in modules, consider whether l() could be a better
2078 * alternative than url().
2079 */
2080 function url($path = NULL, array $options = array()) {
2081 // Merge in defaults.
2082 $options += array(
2083 'fragment' => '',
2084 'query' => '',
2085 'absolute' => FALSE,
2086 'alias' => FALSE,
2087 'prefix' => ''
2088 );
2089 if (!isset($options['external'])) {
2090 // Return an external link if $path contains an allowed absolute URL.
2091 // Only call the slow filter_xss_bad_protocol if $path contains a ':' before
2092 // any / ? or #.
2093 $colonpos = strpos($path, ':');
2094 $options['external'] = ($colonpos !== FALSE && !preg_match('![/?#]!', substr($path, 0, $colonpos)) && filter_xss_bad_protocol($path, FALSE) == check_plain($path));
2095 }
2096
2097 // May need language dependent rewriting if language.inc is present.
2098 if (function_exists('language_url_rewrite')) {
2099 language_url_rewrite($path, $options);
2100 }
2101 if ($options['fragment']) {
2102 $options['fragment'] = '#' . $options['fragment'];
2103 }
2104 if (is_array($options['query'])) {
2105 $options['query'] = drupal_query_string_encode($options['query']);
2106 }
2107
2108 if ($options['external']) {
2109 // Split off the fragment.
2110 if (strpos($path, '#') !== FALSE) {
2111 list($path, $old_fragment) = explode('#', $path, 2);
2112 if (isset($old_fragment) && !$options['fragment']) {
2113 $options['fragment'] = '#' . $old_fragment;
2114 }
2115 }
2116 // Append the query.
2117 if ($options['query']) {
2118 $path .= (strpos($path, '?') !== FALSE ? '&' : '?') . $options['query'];
2119 }
2120 // Reassemble.
2121 return $path . $options['fragment'];
2122 }
2123
2124 global $base_url;
2125 $script = &drupal_static(__FUNCTION__);
2126
2127 if (!isset($script)) {
2128 // On some web servers, such as IIS, we can't omit "index.php". So, we
2129 // generate "index.php?q=foo" instead of "?q=foo" on anything that is not
2130 // Apache.
2131 $script = (strpos($_SERVER['SERVER_SOFTWARE'], 'Apache') === FALSE) ? 'index.php' : '';
2132 }
2133
2134 if (!isset($options['base_url'])) {
2135 // The base_url might be rewritten from the language rewrite in domain mode.
2136 $options['base_url'] = $base_url;
2137 }
2138
2139 // Preserve the original path before aliasing.
2140 $original_path = $path;
2141
2142 // The special path '<front>' links to the default front page.
2143 if ($path == '<front>') {
2144 $path = '';
2145 }
2146 elseif (!empty($path) && !$options['alias']) {
2147 $path = drupal_get_path_alias($path, isset($options['language']) ? $options['language']->language : '');
2148 }
2149
2150 if (function_exists('custom_url_rewrite_outbound')) {
2151 // Modules may alter outbound links by reference.
2152 custom_url_rewrite_outbound($path, $options, $original_path);
2153 }
2154
2155 $base = $options['absolute'] ? $options['base_url'] . '/' : base_path();
2156 $prefix = empty($path) ? rtrim($options['prefix'], '/') : $options['prefix'];
2157 $path = drupal_encode_path($prefix . $path);
2158
2159 if (variable_get('clean_url', '0')) {
2160 // With Clean URLs.
2161 if ($options['query']) {
2162 return $base . $path . '?' . $options['query'] . $options['fragment'];
2163 }
2164 else {
2165 return $base . $path . $options['fragment'];
2166 }
2167 }
2168 else {
2169 // Without Clean URLs.
2170 $variables = array();
2171 if (!empty($path)) {
2172 $variables[] = 'q=' . $path;
2173 }
2174 if (!empty($options['query'])) {
2175 $variables[] = $options['query'];
2176 }
2177 if ($query = join('&', $variables)) {
2178 return $base . $script . '?' . $query . $options['fragment'];
2179 }
2180 else {
2181 return $base . $options['fragment'];
2182 }
2183 }
2184 }
2185
2186 /**
2187 * Format an attribute string to insert in a tag.
2188 *
2189 * Each array key and its value will be formatted into an HTML attribute string.
2190 * If a value is itself an array, then each array element is concatenated with a
2191 * space between each value (e.g. a multi-value class attribute).
2192 *
2193 * @param $attributes
2194 * An associative array of HTML attributes.
2195 * @return
2196 * An HTML string ready for insertion in a tag.
2197 */
2198 function drupal_attributes(array $attributes = array()) {
2199 foreach ($attributes as $attribute => &$data) {
2200 if (is_array($data)) {
2201 $data = implode(' ', $data);
2202 }
2203 $data = $attribute . '="' . check_plain($data) . '"';
2204 }
2205 return $attributes ? ' ' . implode(' ', $attributes) : '';
2206 }
2207
2208 /**
2209 * Format an internal Drupal link.
2210 *
2211 * This function correctly handles aliased paths, and allows themes to highlight
2212 * links to the current page correctly, so all internal links output by modules
2213 * should be generated by this function if possible.
2214 *
2215 * @param $text
2216 * The text to be enclosed with the anchor tag.
2217 * @param $path
2218 * The Drupal path being linked to, such as "admin/content". Can be an
2219 * external or internal URL.
2220 * - If you provide the full URL, it will be considered an external URL.
2221 * - If you provide only the path (e.g. "admin/content"), it is
2222 * considered an internal link. In this case, it must be a system URL
2223 * as the url() function will generate the alias.
2224 * - If you provide '<front>', it generates a link to the site's
2225 * base URL (again via the url() function).
2226 * - If you provide a path, and 'alias' is set to TRUE (see below), it is
2227 * used as is.
2228 * @param $options
2229 * An associative array of additional options, with the following keys:
2230 * - 'attributes'
2231 * An associative array of HTML attributes to apply to the anchor tag.
2232 * - 'query'
2233 * A query string to append to the link, or an array of query key/value
2234 * properties.
2235 * - 'fragment'
2236 * A fragment identifier (named anchor) to append to the link.
2237 * Do not include the '#' character.
2238 * - 'absolute' (default FALSE)
2239 * Whether to force the output to be an absolute link (beginning with
2240 * http:). Useful for links that will be displayed outside the site, such
2241 * as in an RSS feed.
2242 * - 'html' (default FALSE)
2243 * Whether $text is HTML, or just plain-text. For example for making
2244 * an image a link, this must be set to TRUE, or else you will see the
2245 * escaped HTML.
2246 * - 'alias' (default FALSE)
2247 * Whether the given path is an alias already.
2248 * @return
2249 * an HTML string containing a link to the given path.
2250 */
2251 function l($text, $path, array $options = array()) {
2252 global $language;
2253
2254 // Merge in defaults.
2255 $options += array(
2256 'attributes' => array(),
2257 'html' => FALSE,
2258 );
2259
2260 // Append active class.
2261 if (($path == $_GET['q'] || ($path == '<front>' && drupal_is_front_page())) &&
2262 (empty($options['language']) || $options['language']->language == $language->language)) {
2263 if (isset($options['attributes']['class'])) {
2264 $options['attributes']['class'] .= ' active';
2265 }
2266 else {
2267 $options['attributes']['class'] = 'active';
2268 }
2269 }
2270
2271 // Remove all HTML and PHP tags from a tooltip. For best performance, we act only
2272 // if a quick strpos() pre-check gave a suspicion (because strip_tags() is expensive).
2273 if (isset($options['attributes']['title']) && strpos($options['attributes']['title'], '<') !== FALSE) {
2274 $options['attributes']['title'] = strip_tags($options['attributes']['title']);
2275 }
2276
2277 return '<a href="' . check_plain(url($path, $options)) . '"' . drupal_attributes($options['attributes']) . '>' . ($options['html'] ? $text : check_plain($text)) . '</a>';
2278 }
2279
2280 /**
2281 * Perform end-of-request tasks.
2282 *
2283 * This function sets the page cache if appropriate, and allows modules to
2284 * react to the closing of the page by calling hook_exit().
2285 */
2286 function drupal_page_footer() {
2287 global $user;
2288
2289 module_invoke_all('exit');
2290
2291 // Commit the user session, if needed.
2292 drupal_session_commit();
2293
2294 if (variable_get('cache', CACHE_DISABLED) != CACHE_DISABLED && ($cache = drupal_page_set_cache())) {
2295 drupal_serve_page_from_cache($cache);
2296 }
2297 else {
2298 ob_flush();
2299 }
2300
2301 module_implements(MODULE_IMPLEMENTS_WRITE_CACHE);
2302 _registry_check_code(REGISTRY_WRITE_LOOKUP_CACHE);
2303 drupal_cache_system_paths();
2304 }
2305
2306 /**
2307 * Form an associative array from a linear array.
2308 *
2309 * This function walks through the provided array and constructs an associative
2310 * array out of it. The keys of the resulting array will be the values of the
2311 * input array. The values will be the same as the keys unless a function is
2312 * specified, in which case the output of the function is used for the values
2313 * instead.
2314 *
2315 * @param $array
2316 * A linear array.
2317 * @param $function
2318 * A name of a function to apply to all values before output.
2319 * @result
2320 * An associative array.
2321 */
2322 function drupal_map_assoc($array, $function = NULL) {
2323 if (!isset($function)) {
2324 $result = array();
2325 foreach ($array as $value) {
2326 $result[$value] = $value;
2327 }
2328 return $result;
2329 }
2330 elseif (function_exists($function)) {
2331 $result = array();
2332 foreach ($array as $value) {
2333 $result[$value] = $function($value);
2334 }
2335 return $result;
2336 }
2337 }
2338
2339 /**
2340 * Attempts to set the PHP maximum execution time.
2341 *
2342 * This function is a wrapper around the PHP function set_time_limit().
2343 * When called, set_time_limit() restarts the timeout counter from zero.
2344 * In other words, if the timeout is the default 30 seconds, and 25 seconds
2345 * into script execution a call such as set_time_limit(20) is made, the
2346 * script will run for a total of 45 seconds before timing out.
2347 *
2348 * It also means that it is possible to decrease the total time limit if
2349 * the sum of the new time limit and the current time spent running the
2350 * script is inferior to the original time limit. It is inherent to the way
2351 * set_time_limit() works, it should rather be called with an appropriate
2352 * value every time you need to allocate a certain amount of time
2353 * to execute a task than only once at the beginning of the script.
2354 *
2355 * Before calling set_time_limit(), we check if this function is available
2356 * because it could be disabled by the server administrator. We also hide all
2357 * the errors that could occur when calling set_time_limit(), because it is
2358 * not possible to reliably ensure that PHP or a security extension will
2359 * not issue a warning/error if they prevent the use of this function.
2360 *
2361 * @param $time_limit
2362 * An integer specifying the new time limit, in seconds. A value of 0
2363 * indicates unlimited execution time.
2364 */
2365 function drupal_set_time_limit($time_limit) {
2366 if (function_exists('set_time_limit')) {
2367 @set_time_limit($time_limit);
2368 }
2369 }
2370
2371 /**
2372 * Returns the path to a system item (module, theme, etc.).
2373 *
2374 * @param $type
2375 * The type of the item (i.e. theme, theme_engine, module).
2376 * @param $name
2377 * The name of the item for which the path is requested.
2378 *
2379 * @return
2380 * The path to the requested item.
2381 */
2382 function drupal_get_path($type, $name) {
2383 return dirname(drupal_get_filename($type, $name));
2384 }
2385
2386 /**
2387 * Return the base URL path (i.e., directory) of the Drupal installation.
2388 *
2389 * base_path() prefixes and suffixes a "/" onto the returned path if the path is
2390 * not empty. At the very least, this will return "/".
2391 *
2392 * Examples:
2393 * - http://example.com returns "/" because the path is empty.
2394 * - http://example.com/drupal/folder returns "/drupal/folder/".
2395 */
2396 function base_path() {
2397 return $GLOBALS['base_path'];
2398 }
2399
2400 /**
2401 * Add a <link> tag to the page's HEAD.
2402 *
2403 * This function can be called as long the HTML header hasn't been sent.
2404 */
2405 function drupal_add_link($attributes) {
2406 drupal_add_html_head('<link' . drupal_attributes($attributes) . " />\n");
2407 }
2408
2409 /**
2410 * Adds a cascading stylesheet to the stylesheet queue.
2411 *
2412 * Calling drupal_static_reset('drupal_add_css') will clear all cascading
2413 * stylesheets added so far.
2414 *
2415 * @param $data
2416 * (optional) The stylesheet data to be added, depending on what is passed
2417 * through to the $options['type'] parameter:
2418 * - 'file': The path to the CSS file relative to the base_path(),
2419 * e.g., "modules/devel/devel.css".
2420 *
2421 * Modules should always prefix the names of their CSS files with the
2422 * module name, for example: system-menus.css rather than simply menus.css.
2423 * Themes can override module-supplied CSS files based on their filenames,
2424 * and this prefixing helps prevent confusing name collisions for theme
2425 * developers. See drupal_get_css where the overrides are performed.
2426 *
2427 * If the direction of the current language is right-to-left (Hebrew,
2428 * Arabic, etc.), the function will also look for an RTL CSS file and append
2429 * it to the list. The name of this file should have an '-rtl.css' suffix.
2430 * For example a CSS file called 'mymodule-name.css' will have a
2431 * 'mymodule-name-rtl.css' file added to the list, if exists in the same
2432 * directory. This CSS file should contain overrides for properties which
2433 * should be reversed or otherwise different in a right-to-left display.
2434 * - 'inline': A string of CSS that should be placed in the given scope. Note
2435 * that it is better practice to use 'file' stylesheets, rather than 'inline'
2436 * as the CSS would then be aggregated and cached.
2437 * - 'external': The absolute path to an external CSS file that is not hosted
2438 * on the local server. These files will not be aggregated if CSS aggregation
2439 * is enabled.
2440 *
2441 * @param $options
2442 * (optional) A string defining the 'type' of CSS that is being added in the
2443 * $data parameter ('file'/'inline'), or an array which can have any or all of
2444 * the following keys:
2445 * - 'type': The type of stylesheet being added. Available options are 'file',
2446 * 'inline' or 'external'. Defaults to 'file'.
2447 * - 'weight': The weight of the stylesheet specifies the order in which the
2448 * CSS will appear when presented on the page.
2449 *
2450 * Available constants are:
2451 * - CSS_SYSTEM: Any system-layer CSS.
2452 * - CSS_DEFAULT: Any module-layer CSS.
2453 * - CSS_THEME: Any theme-layer CSS.
2454 *
2455 * If you need to embed a CSS file before any other module's stylesheets,
2456 * for example, you would use CSS_DEFAULT - 1. Note that inline CSS is
2457 * simply appended to the end of the specified scope (region), so they
2458 * always come last.
2459 *
2460 * - 'media': The media type for the stylesheet, e.g., all, print, screen.
2461 * Defaults to 'all'.
2462 * - 'preprocess': Allows the CSS to be aggregated and compressed if the
2463 * Optimize CSS feature has been turned on under the performance section.
2464 * Defaults to TRUE.
2465 *
2466 * What does this actually mean?
2467 * CSS preprocessing is the process of aggregating a bunch of separate CSS
2468 * files into one file that is then compressed by removing all extraneous
2469 * white space. Note that preprocessed inline stylesheets will not be
2470 * aggregated into this single file, instead it will just be compressed
2471 * when being output on the page. External stylesheets will not be
2472 * aggregated.
2473 *
2474 * The reason for merging the CSS files is outlined quite thoroughly here:
2475 * http://www.die.net/musings/page_load_time/
2476 * "Load fewer external objects. Due to request overhead, one bigger file
2477 * just loads faster than two smaller ones half its size."
2478 *
2479 * However, you should *not* preprocess every file as this can lead to
2480 * redundant caches. You should set $preprocess = FALSE when your styles
2481 * are only used rarely on the site. This could be a special admin page,
2482 * the homepage, or a handful of pages that does not represent the
2483 * majority of the pages on your site.
2484 *
2485 * Typical candidates for caching are for example styles for nodes across
2486 * the site, or used in the theme.
2487 * @return
2488 * An array of queued cascading stylesheets.
2489 */
2490 function drupal_add_css($data = NULL, $options = NULL) {
2491 $css = &drupal_static(__FUNCTION__, array());
2492
2493 // Construct the options, taking the defaults into consideration.
2494 if (isset($options)) {
2495 if (!is_array($options)) {
2496 $options = array('type' => $options);
2497 }
2498 }
2499 else {
2500 $options = array();
2501 }
2502
2503 // Create an array of CSS files for each media type first, since each type needs to be served
2504 // to the browser differently.
2505 if (isset($data)) {
2506 $options += array(
2507 'type' => 'file',
2508 'weight' => CSS_DEFAULT,
2509 'media' => 'all',
2510 'preprocess' => TRUE,
2511 'data' => $data,
2512 );
2513
2514 // Always add a tiny value to the weight, to conserve the insertion order.
2515 $options['weight'] += count($css) / 1000;
2516
2517 // Add the data to the CSS array depending on the type.
2518 switch ($options['type']) {
2519 case 'inline':
2520 // For inline stylesheets, we don't want to use the $data as the array
2521 // key as $data could be a very long string of CSS.
2522 $css[] = $options;
2523 break;
2524 default:
2525 // Local and external files must keep their name as the associative key
2526 // so the same CSS file is not be added twice.
2527 $css[$data] = $options;
2528 }
2529 }
2530
2531 return $css;
2532 }
2533
2534 /**
2535 * Returns a themed representation of all stylesheets that should be attached to the page.
2536 *
2537 * It loads the CSS in order, with 'module' first, then 'theme' afterwards.
2538 * This ensures proper cascading of styles so themes can easily override
2539 * module styles through CSS selectors.
2540 *
2541 * Themes may replace module-defined CSS files by adding a stylesheet with the
2542 * same filename. For example, themes/garland/system-menus.css would replace
2543 * modules/system/system-menus.css. This allows themes to override complete
2544 * CSS files, rather than specific selectors, when necessary.
2545 *
2546 * If the original CSS file is being overridden by a theme, the theme is
2547 * responsible for supplying an accompanying RTL CSS file to replace the
2548 * module's.
2549 *
2550 * @param $css
2551 * (optional) An array of CSS files. If no array is provided, the default
2552 * stylesheets array is used instead.
2553 * @return
2554 * A string of XHTML CSS tags.
2555 */
2556 function drupal_get_css($css = NULL) {
2557 $output = '';
2558 if (!isset($css)) {
2559 $css = drupal_add_css();
2560 }
2561
2562 $preprocess_css = (variable_get('preprocess_css', FALSE) && (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update'));
2563 $directory = file_directory_path();
2564 $is_writable = is_dir($directory) && is_writable($directory) && (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC) == FILE_DOWNLOADS_PUBLIC);
2565
2566 // A dummy query-string is added to filenames, to gain control over
2567 // browser-caching. The string changes on every update or full cache
2568 // flush, forcing browsers to load a new copy of the files, as the
2569 // URL changed.
2570 $query_string = '?' . substr(variable_get('css_js_query_string', '0'), 0, 1);
2571
2572 // Allow modules to alter the css items.
2573 drupal_alter('css', $css);
2574
2575 // Sort css items according to their weights.
2576 uasort($css, 'drupal_sort_weight');
2577
2578 // Remove the overriden CSS files. Later CSS files override former ones.
2579 $previous_item = array();
2580 foreach ($css as $key => $item) {
2581 if ($item['type'] == 'file') {
2582 $basename = basename($item['data']);
2583 if (isset($previous_item[$basename])) {
2584 // Remove the previous item that shared the same base name.
2585 unset($css[$previous_item[$basename]]);
2586 }
2587 $previous_item[$basename] = $key;
2588 }
2589 }
2590
2591 // If CSS preprocessing is off, we still need to output the styles.
2592 // Additionally, go through any remaining styles if CSS preprocessing is on and output the non-cached ones.
2593 $rendered_css = array();
2594 $inline_css = '';
2595 $external_css = '';
2596 $preprocess_items = array();
2597 foreach ($css as $data => $item) {
2598 // Loop through each of the stylesheets, including them appropriately based
2599 // on their type.
2600 switch ($item['type']) {
2601 case 'file':
2602 // Depending on whether aggregation is desired, include the file.
2603 if (!$item['preprocess'] || !($is_writable && $preprocess_css)) {
2604 $rendered_css[] = '<link type="text/css" rel="stylesheet" media="' . $item['media'] . '" href="' . base_path() . $item['data'] . $query_string . '" />';
2605 }
2606 else {
2607 $preprocess_items[$item['media']][] = $item;
2608 // Mark the position of the preprocess element,
2609 // it should be at the position of the first preprocessed file.
2610 $rendered_css['preprocess'] = '';
2611 }
2612 break;
2613 case 'inline':
2614 // Include inline stylesheets.
2615 $inline_css .= drupal_load_stylesheet_content($item['data'], $item['preprocess']);
2616 break;
2617 case 'external':
2618 // Preprocessing for external CSS files is ignored.
2619 $external_css .= '<link type="text/css" rel="stylesheet" media="' . $item['media'] . '" href="' . $item['data'] . '" />' . "\n";
2620 break;
2621 }
2622 }
2623
2624 if (!empty($preprocess_items)) {
2625 foreach ($preprocess_items as $media => $items) {
2626 // Prefix filename to prevent blocking by firewalls which reject files
2627 // starting with "ad*".
2628 $filename = 'css_' . md5(serialize($items) . $query_string) . '.css';
2629 $preprocess_file = drupal_build_css_cache($items, $filename);
2630 $rendered_css['preprocess'] .= '<link type="text/css" rel="stylesheet" media="' . $media . '" href="' . base_path() . $preprocess_file . '" />' . "\n";
2631 }
2632 }
2633 // Enclose the inline CSS with the style tag if required.
2634 if (!empty($inline_css)) {
2635 $inline_css = "\n" . '<style type="text/css">' . $inline_css .'</style>';
2636 }
2637
2638 // Output all the CSS files with the inline stylesheets showing up last.
2639 return implode("\n", $rendered_css) . $external_css . $inline_css;
2640 }
2641
2642 /**
2643 * Aggregate and optimize CSS files, putting them in the files directory.
2644 *
2645 * @param $css
2646 * An array of CSS files to aggregate and compress into one file.
2647 * @param $filename
2648 * The name of the aggregate CSS file.
2649 * @return
2650 * The name of the CSS file.
2651 */
2652 function drupal_build_css_cache($css, $filename) {
2653 $data = '';
2654
2655 // Create the css/ within the files folder.
2656 $csspath = file_create_path('css');
2657 file_check_directory($csspath, FILE_CREATE_DIRECTORY);
2658
2659 if (!file_exists($csspath . '/' . $filename)) {
2660 // Build aggregate CSS file.
2661 foreach ($css as $stylesheet) {
2662 // Only 'file' stylesheets can be aggregated.
2663 if ($stylesheet['type'] == 'file') {
2664 $contents = drupal_load_stylesheet($stylesheet['data'], TRUE);
2665 // Return the path to where this CSS file originated from.
2666 $base = base_path() . dirname($stylesheet['data']) . '/';
2667 _drupal_build_css_path(NULL, $base);
2668 // Prefix all paths within this CSS file, ignoring external and absolute paths.
2669 $data .= preg_replace_callback('/url\([\'"]?(?![a-z]+:|\/+)([^\'")]+)[\'"]?\)/i', '_drupal_build_css_path', $contents);
2670 }
2671 }
2672
2673 // Per the W3C specification at http://www.w3.org/TR/REC-CSS2/cascade.html#at-import,
2674 // @import rules must proceed any other style, so we move those to the top.
2675 $regexp = '/@import[^;]+;/i';
2676 preg_match_all($regexp, $data, $matches);
2677 $data = preg_replace($regexp, '', $data);
2678 $data = implode('', $matches[0]) . $data;
2679
2680 // Create the CSS file.
2681 file_unmanaged_save_data($data, $csspath . '/' . $filename, FILE_EXISTS_REPLACE);
2682 }
2683 return $csspath . '/' . $filename;
2684 }
2685
2686 /**
2687 * Helper function for drupal_build_css_cache().
2688 *
2689 * This function will prefix all paths within a CSS file.
2690 */
2691 function _drupal_build_css_path($matches, $base = NULL) {
2692 $_base = &drupal_static(__FUNCTION__);
2693 // Store base path for preg_replace_callback.
2694 if (isset($base)) {
2695 $_base = $base;
2696 }
2697
2698 // Prefix with base and remove '../' segments where possible.
2699 $path = $_base . $matches[1];
2700 $last = '';
2701 while ($path != $last) {
2702 $last = $path;
2703 $path = preg_replace('`(^|/)(?!\.\./)([^/]+)/\.\./`', '$1', $path);
2704 }
2705 return 'url(' . $path . ')';
2706 }
2707
2708 /**
2709 * Loads the stylesheet and resolves all @import commands.
2710 *
2711 * Loads a stylesheet and replaces @import commands with the contents of the
2712 * imported file. Use this instead of file_get_contents when processing
2713 * stylesheets.
2714 *
2715 * The returned contents are compressed removing white space and comments only
2716 * when CSS aggregation is enabled. This optimization will not apply for
2717 * color.module enabled themes with CSS aggregation turned off.
2718 *
2719 * @param $file
2720 * Name of the stylesheet to be processed.
2721 * @param $optimize
2722 * Defines if CSS contents should be compressed or not.
2723 * @return
2724 * Contents of the stylesheet, including any resolved @import commands.
2725 */
2726 function drupal_load_stylesheet($file, $optimize = NULL) {
2727 // $_optimize does not use drupal_static as it is set by $optimize.
2728 static $_optimize;
2729 // Store optimization parameter for preg_replace_callback with nested @import loops.
2730 if (isset($optimize)) {
2731 $_optimize = $optimize;
2732 }
2733
2734 $contents = '';
2735 if (file_exists($file)) {
2736 // Load the local CSS stylesheet.
2737 $contents = file_get_contents($file);
2738
2739 // Change to the current stylesheet's directory.
2740 $cwd = getcwd();
2741 chdir(dirname($file));
2742
2743 // Process the stylesheet.
2744 $contents = drupal_load_stylesheet_content($contents, $_optimize);
2745
2746 // Change back directory.
2747 chdir($cwd);
2748 }
2749
2750 return $contents;
2751 }
2752
2753 /**
2754 * Process the contents of a stylesheet for aggregation.
2755 *
2756 * @param $contents
2757 * The contents of the stylesheet.
2758 * @param $optimize
2759 * (optional) Boolean whether CSS contents should be minified. Defaults to
2760 * FALSE.
2761 * @return
2762 * Contents of the stylesheet including the imported stylesheets.
2763 */
2764 function drupal_load_stylesheet_content($contents, $optimize = FALSE) {
2765 // Replaces @import commands with the actual stylesheet content.
2766 // This happens recursively but omits external files.
2767 $contents = preg_replace_callback('/@import\s*(?:url\()?[\'"]?(?![a-z]+:)([^\'"\()]+)[\'"]?\)?;/', '_drupal_load_stylesheet', $contents);
2768 // Remove multiple charset declarations for standards compliance (and fixing Safari problems).
2769 $contents = preg_replace('/^@charset\s+[\'"](\S*)\b[\'"];/i', '', $contents);
2770
2771 if ($optimize) {
2772 // Perform some safe CSS optimizations.
2773 $contents = preg_replace('<
2774 \s*([@{}:;,]|\)\s|\s\()\s* | # Remove whitespace around separators, but keep space around parentheses.
2775 /\*([^*\\\\]|\*(?!/))+\*/ | # Remove comments that are not CSS hacks.
2776 [\n\r] # Remove line breaks.
2777 >x', '\1', $contents);
2778 }
2779 return $contents;
2780 }
2781
2782 /**
2783 * Loads stylesheets recursively and returns contents with corrected paths.
2784 *
2785 * This function is used for recursive loading of stylesheets and
2786 * returns the stylesheet content with all url() paths corrected.
2787 */
2788 function _drupal_load_stylesheet($matches) {
2789 $filename = $matches[1];
2790 // Load the imported stylesheet and replace @import commands in there as well.
2791 $file = drupal_load_stylesheet($filename);
2792 // Alter all url() paths, but not external.
2793 return preg_replace('/url\(([\'"]?)(?![a-z]+:)([^\'")]+)[\'"]?\)?;/i', 'url(\1' . dirname($filename) . '/', $file);
2794 }
2795
2796 /**
2797 * Delete all cached CSS files.
2798 */
2799 function drupal_clear_css_cache() {
2800 file_scan_directory(file_create_path('css'), '/.*/', array('callback' => 'file_unmanaged_delete'));
2801 }
2802
2803 /**
2804 * Add a JavaScript file, setting or inline code to the page.
2805 *
2806 * The behavior of this function depends on the parameters it is called with.
2807 * Generally, it handles the addition of JavaScript to the page, either as
2808 * reference to an existing file or as inline code. The following actions can be
2809 * performed using this function:
2810 *
2811 * - Add a file ('file'):
2812 * Adds a reference to a JavaScript file to the page.
2813 *
2814 * - Add inline JavaScript code ('inline'):
2815 * Executes a piece of JavaScript code on the current page by placing the code
2816 * directly in the page. This can, for example, be useful to tell the user that
2817 * a new message arrived, by opening a pop up, alert box etc. This should only
2818 * be used for JavaScript which cannot be placed and executed from a file.
2819 * When adding inline code, make sure that you are not relying on $ being jQuery.
2820 * Wrap your code in (function ($) { ... })(jQuery); or use jQuery instead of $.
2821 *
2822 * - Add external JavaScript ('external'):
2823 * Allows the inclusion of external JavaScript files that are not hosted on the
2824 * local server. Note that these external JavaScript references do not get
2825 * aggregated when preprocessing is on.
2826 *
2827 * - Add settings ('setting'):
2828 * Adds a setting to Drupal's global storage of JavaScript settings. Per-page
2829 * settings are required by some modules to function properly. All settings
2830 * will be accessible at Drupal.settings.
2831 *
2832 * Examples:
2833 * @code
2834 * drupal_add_js('misc/collapse.js');
2835 * drupal_add_js('misc/collapse.js', 'file');
2836 * drupal_add_js('jQuery(document).ready(function () { alert("Hello!"); });', 'inline');
2837 * drupal_add_js('jQuery(document).ready(function () { alert("Hello!"); });',
2838 * array('type' => 'inline', 'scope' => 'footer', 'weight' => 5)
2839 * );
2840 * drupal_add_js('http://example.com/example.js', 'external');
2841 * @endcode
2842 *
2843 * Calling drupal_static_reset('drupal_add_js') will clear all JavaScript added
2844 * so far.
2845 *
2846 * @param $data
2847 * (optional) If given, the value depends on the $options parameter:
2848 * - 'file': Path to the file relative to base_path().
2849 * - 'inline': The JavaScript code that should be placed in the given scope.
2850 * - 'external': The absolute path to an external JavaScript file that is not
2851 * hosted on the local server. These files will not be aggregated if
2852 * JavaScript aggregation is enabled.
2853 * - 'setting': An array with configuration options as associative array. The
2854 * array is directly placed in Drupal.settings. All modules should wrap
2855 * their actual configuration settings in another variable to prevent
2856 * the pollution of the Drupal.settings namespace.
2857 * @param $options
2858 * (optional) A string defining the type of JavaScript that is being added
2859 * in the $data parameter ('file'/'setting'/'inline'), or an array which
2860 * can have any or all of the following keys. JavaScript settings should
2861 * always pass the string 'setting' only.
2862 * - type
2863 * The type of JavaScript that is to be added to the page. Allowed
2864 * values are 'file', 'inline', 'external' or 'setting'. Defaults
2865 * to 'file'.
2866 * - scope
2867 * The location in which you want to place the script. Possible values
2868 * are 'header' or 'footer'. If your theme implements different regions,
2869 * however, you can also use these. Defaults to 'header'.
2870 * - weight
2871 * A number defining the order in which the JavaScript is added to the
2872 * page. In some cases, the order in which the JavaScript is presented
2873 * on the page is very important. jQuery, for example, must be added to
2874 * to the page before any jQuery code is run, so jquery.js uses a weight
2875 * of JS_LIBRARY - 2, drupal.js uses a weight of JS_LIBRARY - 1, and all
2876 * following scripts depending on jQuery and Drupal behaviors are simply
2877 * added using the default weight of JS_DEFAULT.
2878 *
2879 * Available constants are:
2880 * - JS_LIBRARY: Any libraries, settings, or jQuery plugins.
2881 * - JS_DEFAULT: Any module-layer JavaScript.
2882 * - JS_THEME: Any theme-layer JavaScript.
2883 *
2884 * If you need to invoke a JavaScript file before any other module's
2885 * JavaScript, for example, you would use JS_DEFAULT - 1.
2886 * Note that inline JavaScripts are simply appended to the end of the
2887 * specified scope (region), so they always come last.
2888 * - defer
2889 * If set to TRUE, the defer attribute is set on the &lt;script&gt; tag.
2890 * Defaults to FALSE.
2891 * - cache
2892 * If set to FALSE, the JavaScript file is loaded anew on every page
2893 * call, that means, it is not cached. Used only when 'type' references
2894 * a JavaScript file. Defaults to TRUE.
2895 * - preprocess
2896 * Aggregate the JavaScript if the JavaScript optimization setting has
2897 * been toggled in admin/config/development/performance. Note that
2898 * JavaScript of type 'external' is not aggregated. Defaults to TRUE.
2899 * @return
2900 * The contructed array of JavaScript files.
2901 * @see drupal_get_js()
2902 */
2903 function drupal_add_js($data = NULL, $options = NULL) {
2904 $javascript = &drupal_static(__FUNCTION__, array());
2905
2906 // Construct the options, taking the defaults into consideration.
2907 if (isset($options)) {
2908 if (!is_array($options)) {
2909 $options = array('type' => $options);
2910 }
2911 }
2912 else {
2913 $options = array();
2914 }
2915 $options += drupal_js_defaults($data);
2916
2917 // Preprocess can only be set if caching is enabled.
2918 $options['preprocess'] = $options['cache'] ? $options['preprocess'] : FALSE;
2919
2920 // Tweak the weight so that files of the same weight are included in the
2921 // order of the calls to drupal_add_js().
2922 $options['weight'] += count($javascript) / 1000;
2923
2924 if (isset($data)) {
2925 // Add jquery.js and drupal.js, as well as the basePath setting, the
2926 // first time a Javascript file is added.
2927 if (empty($javascript)) {
2928 $javascript = array(
2929 'settings' => array(
2930 'data' => array(
2931 array('basePath' => base_path()),
2932 ),
2933 'type' => 'setting',
2934 'scope' => 'header',
2935 'weight' => JS_LIBRARY,
2936 ),
2937 'misc/drupal.js' => array(
2938 'data' => 'misc/drupal.js',
2939 'type' => 'file',
2940 'scope' => 'header',
2941 'weight' => JS_LIBRARY - 1,
2942 'cache' => TRUE,
2943 'defer' => FALSE,
2944 'preprocess' => TRUE,
2945 ),
2946 );
2947 // jQuery itself is registered as a library.
2948 drupal_add_library('system', 'jquery');
2949 }
2950
2951 switch ($options['type']) {
2952 case 'setting':
2953 // All JavaScript settings are placed in the header of the page with
2954 // the library weight so that inline scripts appear afterwards.
2955 $javascript['settings']['data'][] = $data;
2956 break;
2957
2958 case 'inline':
2959 $javascript[] = $options;
2960 break;
2961
2962 default: // 'file' and 'external'
2963 // Local and external files must keep their name as the associative key
2964 // so the same JavaScript file is not be added twice.
2965 $javascript[$options['data']] = $options;
2966 }
2967 }
2968 return $javascript;
2969 }
2970
2971 /**
2972 * Constructs an array of the defaults that are used for JavaScript items.
2973 *
2974 * @param $data
2975 * (optional) The default data parameter for the JavaScript item array.
2976 * @see drupal_get_js()
2977 * @see drupal_add_js()
2978 */
2979 function drupal_js_defaults($data = NULL) {
2980 return array(
2981 'type' => 'file',
2982 'weight' => JS_DEFAULT,
2983 'scope' => 'header',
2984 'cache' => TRUE,
2985 'defer' => FALSE,
2986 'preprocess' => TRUE,
2987 'data' => $data,
2988 );
2989 }
2990
2991 /**
2992 * Returns a themed presentation of all JavaScript code for the current page.
2993 *
2994 * References to JavaScript files are placed in a certain order: first, all
2995 * 'core' files, then all 'module' and finally all 'theme' JavaScript files
2996 * are added to the page. Then, all settings are output, followed by 'inline'
2997 * JavaScript code. If running update.php, all preprocessing is disabled.
2998 *
2999 * Note that hook_js_alter(&$javascript) is called during this function call
3000 * to allow alterations of the JavaScript during its presentation. Calls to
3001 * drupal_add_js() from hook_js_alter() will not be added to the output
3002 * presentation. The correct way to add JavaScript during hook_js_alter()
3003 * is to add another element to the $javascript array, deriving from
3004 * drupal_js_defaults(). See locale_js_alter() for an example of this.
3005 *
3006 * @param $scope
3007 * (optional) The scope for which the JavaScript rules should be returned.
3008 * Defaults to 'header'.
3009 * @param $javascript
3010 * (optional) An array with all JavaScript code. Defaults to the default
3011 * JavaScript array for the given scope.
3012 * @return
3013 * All JavaScript code segments and includes for the scope as HTML tags.
3014 * @see drupal_add_js()
3015 * @see locale_js_alter()
3016 * @see drupal_js_defaults()
3017 */
3018 function drupal_get_js($scope = 'header', $javascript = NULL) {
3019 if (!isset($javascript)) {
3020 $javascript = drupal_add_js();
3021 }
3022 if (empty($javascript)) {
3023 return '';
3024 }
3025
3026 // Allow modules to alter the JavaScript.
3027 drupal_alter('js', $javascript);
3028
3029 // Filter out elements of the given scope.
3030 $items = array();
3031 foreach ($javascript as $item) {
3032 if ($item['scope'] == $scope) {
3033 $items[] = $item;
3034 }
3035 }
3036
3037 $output = '';
3038 $preprocessed = '';
3039 $no_preprocess = '';
3040 $files = array();
3041 $preprocess_js = (variable_get('preprocess_js', FALSE) && (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update'));
3042 $directory = file_directory_path();
3043 $is_writable = is_dir($directory) && is_writable($directory) && (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC) == FILE_DOWNLOADS_PUBLIC);
3044
3045 // A dummy query-string is added to filenames, to gain control over
3046 // browser-caching. The string changes on every update or full cache
3047 // flush, forcing browsers to load a new copy of the files, as the
3048 // URL changed. Files that should not be cached (see drupal_add_js())
3049 // get REQUEST_TIME as query-string instead, to enforce reload on every
3050 // page request.
3051 $query_string = '?' . substr(variable_get('css_js_query_string', '0'), 0, 1);
3052
3053 // For inline Javascript to validate as XHTML, all Javascript containing
3054 // XHTML needs to be wrapped in CDATA. To make that backwards compatible
3055 // with HTML 4, we need to comment out the CDATA-tag.
3056 $embed_prefix = "\n<!--//--><![CDATA[//><!--\n";
3057 $embed_suffix = "\n//--><!]]>\n";
3058
3059 // Sort the JavaScript by weight so that it appears in the correct order.
3060 uasort($items, 'drupal_sort_weight');
3061
3062 // Loop through the JavaScript to construct the rendered output.
3063 foreach ($items as $item) {
3064 switch ($item['type']) {
3065 case 'setting':
3066 $output .= '<script type="text/javascript">' . $embed_prefix . 'jQuery.extend(Drupal.settings, ' . drupal_to_js(call_user_func_array('array_merge_recursive', $item['data'])) . ");" . $embed_suffix . "</script>\n";
3067 break;
3068
3069 case 'inline':
3070 $output .= '<script type="text/javascript"' . ($item['defer'] ? ' defer="defer"' : '') . '>' . $embed_prefix . $item['data'] . $embed_suffix . "</script>\n";
3071 break;
3072
3073 case 'file':
3074 if (!$item['preprocess'] || !$is_writable || !$preprocess_js) {
3075 $no_preprocess .= '<script type="text/javascript"' . ($item['defer'] ? ' defer="defer"' : '') . ' src="' . base_path() . $item['data'] . ($item['cache'] ? $query_string : '?' . REQUEST_TIME) . "\"></script>\n";
3076 }
3077 else {
3078 $files[$item['data']] = $item;
3079 }
3080 break;
3081
3082 case 'external':
3083 // Preprocessing for external JavaScript files is ignored.
3084 $output .= '<script type="text/javascript"' . ($item['defer'] ? ' defer="defer"' : '') . ' src="' . $item['data'] . "\"></script>\n";
3085 break;
3086 }
3087 }
3088
3089 // Aggregate any remaining JS files that haven't already been output.
3090 if ($is_writable && $preprocess_js && count($files) > 0) {
3091 // Prefix filename to prevent blocking by firewalls which reject files
3092 // starting with "ad*".
3093 $filename = 'js_' . md5(serialize($files) . $query_string) . '.js';
3094 $preprocess_file = drupal_build_js_cache($files, $filename);
3095 $preprocessed .= '<script type="text/javascript" src="' . base_path() . $preprocess_file . '"></script>' . "\n";
3096 }
3097
3098 // Keep the order of JS files consistent as some are preprocessed and others are not.
3099 // Make sure any inline or JS setting variables appear last after libraries have loaded.
3100 return $preprocessed . $no_preprocess . $output;
3101 }
3102
3103 /**
3104 * Adds multiple JavaScript or CSS files at the same time.
3105 *
3106 * A library defines a set of JavaScript and/or CSS files, optionally using
3107 * settings, and optionally requiring another library. For example, a library
3108 * can be a jQuery plugin, a JavaScript framework, or a CSS framework. This
3109 * function allows modules to load a library defined/shipped by itself or a
3110 * depending module; without having to add all files of the library separately.
3111 * Each library is only loaded once.
3112 *
3113 * @param $module
3114 * The name of the module that registered the library.
3115 * @param $name
3116 * The name of the library to add.
3117 * @return
3118 * TRUE when the library was successfully added or FALSE if the library or one
3119 * of its dependencies could not be added.
3120 *
3121 * @see drupal_get_library()
3122 * @see hook_library()
3123 * @see hook_library_alter()
3124 */
3125 function drupal_add_library($module, $name) {
3126 $added = &drupal_static(__FUNCTION__, array());
3127
3128 // Only process the library if it exists and it was not added already.
3129 if (!isset($added[$module][$name]) && $library = drupal_get_library($module, $name)) {
3130 // Prevent repeated/recursive processing.
3131 $added[$module][$name] = TRUE;
3132
3133 // Ensure dependencies first.
3134 foreach ($library['dependencies'] as $dependency) {
3135 if (drupal_add_library($dependency[0], $dependency[1]) === FALSE) {
3136 // If any dependent library could not be added, this library will break;
3137 // stop here.
3138 $added[$module][$name] = FALSE;
3139 return FALSE;
3140 }
3141 }
3142
3143 // Add defined JavaScript.
3144 foreach ($library['js'] as $data => $options) {
3145 // For JS settings we need to transform $options['data'] into $data.
3146 if (isset($options['type'], $options['data']) && $options['type'] == 'setting') {
3147 $data = $options['data'];
3148 unset($options['data']);
3149 }
3150 // If not specified, assign a default weight of JS_LIBRARY.
3151 elseif (!isset($options['weight'])) {
3152 $options['weight'] = JS_LIBRARY;
3153 }
3154 drupal_add_js($data, $options);
3155 }
3156
3157 // Add defined stylesheets.
3158 foreach ($library['css'] as $data => $options) {
3159 drupal_add_css($data, $options);
3160 }
3161 }
3162 // Requested library does not exist.
3163 else {
3164 $added[$module][$name] = FALSE;
3165 }
3166
3167 return $added[$module][$name];
3168 }
3169
3170 /**
3171 * Retrieves information for a JavaScript/CSS library.
3172 *
3173 * Library information is statically cached. Libraries are keyed by module for
3174 * several reasons:
3175 * - Libraries are not unique. Multiple modules might ship with the same library
3176 * in a different version or variant. This registry cannot (and does not
3177 * attempt to) prevent library conflicts.
3178 * - Modules implementing and thereby depending on a library that is registered
3179 * by another module can only rely on that module's library.
3180 * - Two (or more) modules can still register the same library and use it
3181 * without conflicts in case the libraries are loaded on certain pages only.
3182 *
3183 * @param $module
3184 * The name of a module that registered a library.
3185 * @param $library
3186 * The name of a registered library.
3187 * @return
3188 * The definition of the requested library, if existent, or FALSE.
3189 *
3190 * @see drupal_add_library()
3191 * @see hook_library()
3192 * @see hook_library_alter()
3193 *
3194 * @todo The purpose of drupal_get_*() is completely different to other page
3195 * requisite API functions; find and use a different name.
3196 */
3197 function drupal_get_library($module, $name) {
3198 $libraries = &drupal_static(__FUNCTION__, array());
3199
3200 if (!array_key_exists($module, $libraries)) {
3201 // Retrieve all libraries associated with the module.
3202 $module_libraries = module_invoke($module, 'library');
3203
3204 // Allow modules to alter the module's registered libraries.
3205 if (!empty($module_libraries)) {
3206 drupal_alter('library', $module_libraries, $module);
3207 }
3208 $libraries[$module] = $module_libraries;
3209 }
3210 if (!empty($libraries[$module][$name]) && is_array($libraries[$module][$name])) {
3211 // Add default elements to allow for easier processing.
3212 $libraries[$module][$name] += array('dependencies' => array(), 'js' => array(), 'css' => array());
3213 }
3214 else {
3215 $libraries[$module][$name] = FALSE;
3216 }
3217
3218 return $libraries[$module][$name];
3219 }
3220
3221 /**
3222 * Assist in adding the tableDrag JavaScript behavior to a themed table.
3223 *
3224 * Draggable tables should be used wherever an outline or list of sortable items
3225 * needs to be arranged by an end-user. Draggable tables are very flexible and
3226 * can manipulate the value of form elements placed within individual columns.
3227 *
3228 * To set up a table to use drag and drop in place of weight select-lists or
3229 * in place of a form that contains parent relationships, the form must be
3230 * themed into a table. The table must have an id attribute set. If using
3231 * theme_table(), the id may be set as such:
3232 * @code
3233 * $output = theme('table', $header, $rows, array('id' => 'my-module-table'));
3234 * return $output;
3235 * @endcode
3236 *
3237 * In the theme function for the form, a special class must be added to each
3238 * form element within the same column, "grouping" them together.
3239 *
3240 * In a situation where a single weight column is being sorted in the table, the
3241 * classes could be added like this (in the theme function):
3242 * @code
3243 * $form['my_elements'][$delta]['weight']['#attributes']['class'] = "my-elements-weight";
3244 * @endcode
3245 *
3246 * Each row of the table must also have a class of "draggable" in order to enable the
3247 * drag handles:
3248 * @code
3249 * $row = array(...);
3250 * $rows[] = array(
3251 * 'data' => $row,
3252 * 'class' => 'draggable',
3253 * );
3254 * @endcode
3255 *
3256 * When tree relationships are present, the two additional classes
3257 * 'tabledrag-leaf' and 'tabledrag-root' can be used to refine the behavior:
3258 * - Rows with the 'tabledrag-leaf' class cannot have child rows.
3259 * - Rows with the 'tabledrag-root' class cannot be nested under a parent row.
3260 *
3261 * Calling drupal_add_tabledrag() would then be written as such:
3262 * @code
3263 * drupal_add_tabledrag('my-module-table', 'order', 'sibling', 'my-elements-weight');
3264 * @endcode
3265 *
3266 * In a more complex case where there are several groups in one column (such as
3267 * the block regions on the admin/structure/block page), a separate subgroup class
3268 * must also be added to differentiate the groups.
3269 * @code
3270 * $form['my_elements'][$region][$delta]['weight']['#attributes']['class'] = "my-elements-weight my-elements-weight-" . $region;
3271 * @endcode
3272 *
3273 * $group is still 'my-element-weight', and the additional $subgroup variable
3274 * will be passed in as 'my-elements-weight-' . $region. This also means that
3275 * you'll need to call drupal_add_tabledrag() once for every region added.
3276 *
3277 * @code
3278 * foreach ($regions as $region) {
3279 * drupal_add_tabledrag('my-module-table', 'order', 'sibling', 'my-elements-weight', 'my-elements-weight-' . $region);
3280 * }
3281 * @endcode
3282 *
3283 * In a situation where tree relationships are present, adding multiple
3284 * subgroups is not necessary, because the table will contain indentations that
3285 * provide enough information about the sibling and parent relationships.
3286 * See theme_menu_overview_form() for an example creating a table containing
3287 * parent relationships.
3288 *
3289 * Please note that this function should be called from the theme layer, such as
3290 * in a .tpl.php file, theme_ function, or in a template_preprocess function,
3291 * not in a form declaration. Though the same JavaScript could be added to the
3292 * page using drupal_add_js() directly, this function helps keep template files
3293 * clean and readable. It also prevents tabledrag.js from being added twice
3294 * accidentally.
3295 *
3296 * @param $table_id
3297 * String containing the target table's id attribute. If the table does not
3298 * have an id, one will need to be set, such as <table id="my-module-table">.
3299 * @param $action
3300 * String describing the action to be done on the form item. Either 'match'
3301 * 'depth', or 'order'. Match is typically used for parent relationships.
3302 * Order is typically used to set weights on other form elements with the same
3303 * group. Depth updates the target element with the current indentation.
3304 * @param $relationship
3305 * String describing where the $action variable should be performed. Either
3306 * 'parent', 'sibling', 'group', or 'self'. Parent will only look for fields
3307 * up the tree. Sibling will look for fields in the same group in rows above
3308 * and below it. Self affects the dragged row itself. Group affects the
3309 * dragged row, plus any children below it (the entire dragged group).
3310 * @param $group
3311 * A class name applied on all related form elements for this action.
3312 * @param $subgroup
3313 * (optional) If the group has several subgroups within it, this string should
3314 * contain the class name identifying fields in the same subgroup.
3315 * @param $source
3316 * (optional) If the $action is 'match', this string should contain the class
3317 * name identifying what field will be used as the source value when matching
3318 * the value in $subgroup.
3319 * @param $hidden
3320 * (optional) The column containing the field elements may be entirely hidden
3321 * from view dynamically when the JavaScript is loaded. Set to FALSE if the
3322 * column should not be hidden.
3323 * @param $limit
3324 * (optional) Limit the maximum amount of parenting in this table.
3325 * @see block-admin-display-form.tpl.php
3326 * @see theme_menu_overview_form()
3327 */
3328 function drupal_add_tabledrag($table_id, $action, $relationship, $group, $subgroup = NULL, $source = NULL, $hidden = TRUE, $limit = 0) {
3329 $js_added = &drupal_static(__FUNCTION__, FALSE);
3330 if (!$js_added) {
3331 // Add the table drag JavaScript to the page before the module JavaScript
3332 // to ensure that table drag behaviors are registered before any module
3333 // uses it.
3334 drupal_add_js('misc/tabledrag.js', array('weight' => JS_DEFAULT - 1));
3335 $js_added = TRUE;
3336 }
3337
3338 // If a subgroup or source isn't set, assume it is the same as the group.
3339 $target = isset($subgroup) ? $subgroup : $group;
3340 $source = isset($source) ? $source : $target;
3341 $settings['tableDrag'][$table_id][$group][] = array(
3342 'target' => $target,
3343 'source' => $source,
3344 'relationship' => $relationship,
3345 'action' => $action,
3346 'hidden' => $hidden,
3347 'limit' => $limit,
3348 );
3349 drupal_add_js($settings, 'setting');
3350 }
3351
3352 /**
3353 * Aggregate JS files, putting them in the files directory.
3354 *
3355 * @param $files
3356 * An array of JS files to aggregate and compress into one file.
3357 * @param $filename
3358 * The name of the aggregate JS file.
3359 * @return
3360 * The name of the JS file.
3361 */
3362 function drupal_build_js_cache($files, $filename) {
3363 $contents = '';
3364
3365 // Create the js/ within the files folder.
3366 $jspath = file_create_path('js');
3367 file_check_directory($jspath, FILE_CREATE_DIRECTORY);
3368
3369 if (!file_exists($jspath . '/' . $filename)) {
3370 // Build aggregate JS file.
3371 foreach ($files as $path => $info) {
3372 if ($info['preprocess']) {
3373 // Append a ';' after each JS file to prevent them from running together.
3374 $contents .= file_get_contents($path) . ';';
3375 }
3376 }
3377
3378 // Create the JS file.
3379 file_unmanaged_save_data($contents, $jspath . '/' . $filename, FILE_EXISTS_REPLACE);
3380 }