initial import from onelab svn codebase
[plewww.git] / includes / common.inc
1 <?php
2 // $Id: common.inc 144 2007-03-28 07:52:20Z thierry $
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  * Return status for saving which involved creating a new item.
14  */
15 define('SAVED_NEW', 1);
16
17 /**
18  * Return status for saving which involved an update to an existing item.
19  */
20 define('SAVED_UPDATED', 2);
21
22 /**
23  * Return status for saving which deleted an existing item.
24  */
25 define('SAVED_DELETED', 3);
26
27 /**
28  * Set content for a specified region.
29  *
30  * @param $region
31  *   Page region the content is assigned to.
32  *
33  * @param $data
34  *   Content to be set.
35  */
36 function drupal_set_content($region = null, $data = null) {
37   static $content = array();
38
39   if (!is_null($region) && !is_null($data)) {
40     $content[$region][] = $data;
41   }
42   return $content;
43 }
44
45 /**
46  * Get assigned content.
47  *
48  * @param $region
49  *   A specified region to fetch content for.  If null, all regions will be returned.
50  *
51  * @param $delimiter
52  *   Content to be inserted between exploded array elements.
53  */
54 function drupal_get_content($region = NULL, $delimiter = ' ') {
55   $content = drupal_set_content();
56   if (isset($region)) {
57     if (isset($content[$region]) && is_array($content[$region])) {
58       return implode($delimiter, $content[$region]);
59     }
60   }
61   else {
62     foreach (array_keys($content) as $region) {
63       if (is_array($content[$region])) {
64         $content[$region] = implode($delimiter, $content[$region]);
65       }
66     }
67     return $content;
68   }
69 }
70
71 /**
72  * Set the breadcrumb trail for the current page.
73  *
74  * @param $breadcrumb
75  *   Array of links, starting with "home" and proceeding up to but not including
76  *   the current page.
77  */
78 function drupal_set_breadcrumb($breadcrumb = NULL) {
79   static $stored_breadcrumb;
80
81   if (!is_null($breadcrumb)) {
82     $stored_breadcrumb = $breadcrumb;
83   }
84   return $stored_breadcrumb;
85 }
86
87 /**
88  * Get the breadcrumb trail for the current page.
89  */
90 function drupal_get_breadcrumb() {
91   $breadcrumb = drupal_set_breadcrumb();
92
93   if (is_null($breadcrumb)) {
94     $breadcrumb = menu_get_active_breadcrumb();
95   }
96
97   return $breadcrumb;
98 }
99
100 /**
101  * Add output to the head tag of the HTML page.
102  * This function can be called as long the headers aren't sent.
103  */
104 function drupal_set_html_head($data = NULL) {
105   static $stored_head = '';
106
107   if (!is_null($data)) {
108     $stored_head .= $data ."\n";
109   }
110   return $stored_head;
111 }
112
113 /**
114  * Retrieve output to be displayed in the head tag of the HTML page.
115  */
116 function drupal_get_html_head() {
117   $output = "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n";
118   $output .= theme('stylesheet_import', base_path() .'misc/drupal.css');
119   return $output . drupal_set_html_head();
120 }
121
122 /**
123  * Reset the static variable which holds the aliases mapped for this request.
124  */
125 function drupal_clear_path_cache() {
126   drupal_lookup_path('wipe');
127 }
128
129 /**
130  * Set an HTTP response header for the current page.
131  */
132 function drupal_set_header($header = NULL) {
133   // We use an array to guarantee there are no leading or trailing delimiters.
134   // Otherwise, header('') could get called when serving the page later, which
135   // ends HTTP headers prematurely on some PHP versions.
136   static $stored_headers = array();
137
138   if (strlen($header)) {
139     header($header);
140     $stored_headers[] = $header;
141   }
142   return implode("\n", $stored_headers);
143 }
144
145 /**
146  * Get the HTTP response headers for the current page.
147  */
148 function drupal_get_headers() {
149   return drupal_set_header();
150 }
151
152 /**
153  * @name HTTP handling
154  * @{
155  * Functions to properly handle HTTP responses.
156  */
157
158 /**
159  * Parse an array into a valid urlencoded query string.
160  *
161  * @param $query
162  *   The array to be processed e.g. $_GET
163  * @param $exclude
164  *   The array filled with keys to be excluded. Use parent[child] to exclude nested items.
165  * @param $urlencode
166  *   If TRUE, the keys and values are both urlencoded.
167  * @param $parent
168  *   Should not be passed, only used in recursive calls
169  * @return
170  *   urlencoded string which can be appended to/as the URL query string
171  */
172 function drupal_query_string_encode($query, $exclude = array(), $parent = '') {
173   $params = array();
174
175   foreach ($query as $key => $value) {
176     $key = drupal_urlencode($key);
177     if ($parent) {
178       $key = $parent .'['. $key .']';
179     }
180
181     if (in_array($key, $exclude)) {
182       continue;
183     }
184
185     if (is_array($value)) {
186       $params[] = drupal_query_string_encode($value, $exclude, $key);
187     }
188     else {
189       $params[] = $key .'='. drupal_urlencode($value);
190     }
191   }
192
193   return implode('&', $params);
194 }
195
196 /**
197  * Prepare a destination query string for use in combination with
198  * drupal_goto(). Used to direct the user back to the referring page
199  * after completing a form. By default the current URL is returned.
200  * If a destination exists in the previous request, that destination
201  * is returned.  As such, a destination can persist across multiple
202  * pages.
203  *
204  * @see drupal_goto()
205  */
206 function drupal_get_destination() {
207   if (isset($_REQUEST['destination'])) {
208     return 'destination='. urlencode($_REQUEST['destination']);
209   }
210   else {
211     // Use $_REQUEST here to retrieve the original path.
212     $path = isset($_REQUEST['q']) ? $_REQUEST['q'] : '';
213     $query = drupal_query_string_encode($_GET, array('q'));
214     if ($query != '') {
215       $path .= '?'. $query;
216     }
217     return 'destination='. urlencode($path);
218   }
219 }
220
221 /**
222  * Send the user to a different Drupal page.
223  *
224  * This issues an on-site HTTP redirect. The function makes sure the redirected
225  * URL is formatted correctly.
226  *
227  * Usually the redirected URL is constructed from this function's input
228  * parameters.  However you may override that behavior by setting a
229  * <em>destination</em> in either the $_REQUEST-array (i.e. by using
230  * the query string of an URI) or the $_REQUEST['edit']-array (i.e. by
231  * using a hidden form field).  This is used to direct the user back to
232  * the proper page after completing a form.  For example, after editing
233  * a post on the 'admin/node'-page or after having logged on using the
234  * 'user login'-block in a sidebar.  The function drupal_get_destination()
235  * can be used to help set the destination URL.
236  *
237  * It is advised to use drupal_goto() instead of PHP's header(), because
238  * drupal_goto() will append the user's session ID to the URI when PHP is
239  * compiled with "--enable-trans-sid".
240  *
241  * This function ends the request; use it rather than a print theme('page')
242  * statement in your menu callback.
243  *
244  * @param $path
245  *   A Drupal path.
246  * @param $query
247  *   The query string component, if any.
248  * @param $fragment
249  *   The destination fragment identifier (named anchor).
250  *
251  * @see drupal_get_destination()
252  */
253 function drupal_goto($path = '', $query = NULL, $fragment = NULL) {
254   if (isset($_REQUEST['destination'])) {
255     extract(parse_url($_REQUEST['destination']));
256   }
257   else if (isset($_REQUEST['edit']['destination'])) {
258     extract(parse_url($_REQUEST['edit']['destination']));
259   }
260
261   $url = url($path, $query, $fragment, TRUE);
262
263   // Before the redirect, allow modules to react to the end of the page request.
264   module_invoke_all('exit', $url);
265
266   header('Location: '. $url);
267
268   // The "Location" header sends a REDIRECT status code to the http
269   // daemon. In some cases this can go wrong, so we make sure none
270   // of the code below the drupal_goto() call gets executed when we redirect.
271   exit();
272 }
273
274 /**
275  * Generates a site off-line message
276  */
277 function drupal_site_offline() {
278   drupal_set_header('HTTP/1.0 503 Service unavailable');
279   drupal_set_title(t('Site off-line'));
280   print theme('maintenance_page', filter_xss_admin(variable_get('site_offline_message',
281     t('%site is currently under maintenance. We should be back shortly. Thank you for your patience.', array('%site' => variable_get('site_name', t('This Drupal site')))))));
282 }
283
284 /**
285  * Generates a 404 error if the request can not be handled.
286  */
287 function drupal_not_found() {
288   drupal_set_header('HTTP/1.0 404 Not Found');
289   watchdog('page not found', t('%page not found.', array('%page' => theme('placeholder', $_GET['q']))), WATCHDOG_WARNING);
290
291   // Keep old path for reference
292   if (!isset($_REQUEST['destination'])) {
293     $_REQUEST['destination'] = $_GET['q'];
294   }
295
296   $path = drupal_get_normal_path(variable_get('site_404', ''));
297   if ($path && $path != $_GET['q']) {
298     menu_set_active_item($path);
299     $return = menu_execute_active_handler();
300   }
301   else {
302     // Redirect to a non-existent menu item to make possible tabs disappear.
303     menu_set_active_item('');
304   }
305
306   if (empty($return)) {
307     drupal_set_title(t('Page not found'));
308   }
309   print theme('page', $return);
310 }
311
312 /**
313  * Generates a 403 error if the request is not allowed.
314  */
315 function drupal_access_denied() {
316   drupal_set_header('HTTP/1.0 403 Forbidden');
317   watchdog('access denied', t('%page denied access.', array('%page' => theme('placeholder', $_GET['q']))), WATCHDOG_WARNING, l(t('view'), $_GET['q']));
318
319   // Keep old path for reference
320   if (!isset($_REQUEST['destination'])) {
321     $_REQUEST['destination'] = $_GET['q'];
322   }
323
324   $path = drupal_get_normal_path(variable_get('site_403', ''));
325   if ($path && $path != $_GET['q']) {
326     menu_set_active_item($path);
327     $return = menu_execute_active_handler();
328   }
329   else {
330     // Redirect to a non-existent menu item to make possible tabs disappear.
331     menu_set_active_item('');
332   }
333
334   if (empty($return)) {
335     drupal_set_title(t('Access denied'));
336     $return = t('You are not authorized to access this page.');
337   }
338   print theme('page', $return);
339 }
340
341 /**
342  * Perform an HTTP request.
343  *
344  * This is a flexible and powerful HTTP client implementation. Correctly handles
345  * GET, POST, PUT or any other HTTP requests. Handles redirects.
346  *
347  * @param $url
348  *   A string containing a fully qualified URI.
349  * @param $headers
350  *   An array containing an HTTP header => value pair.
351  * @param $method
352  *   A string defining the HTTP request to use.
353  * @param $data
354  *   A string containing data to include in the request.
355  * @param $retry
356  *   An integer representing how many times to retry the request in case of a
357  *   redirect.
358  * @return
359  *   An object containing the HTTP request headers, response code, headers,
360  *   data, and redirect status.
361  */
362 function drupal_http_request($url, $headers = array(), $method = 'GET', $data = NULL, $retry = 3) {
363   $result = new StdClass();
364
365   // Parse the URL, and make sure we can handle the schema.
366   $uri = parse_url($url);
367   switch ($uri['scheme']) {
368     case 'http':
369       $port = isset($uri['port']) ? $uri['port'] : 80;
370       $host = $uri['host'] . ($port != 80 ? ':'. $port : '');
371       $fp = @fsockopen($uri['host'], $port, $errno, $errstr, 15);
372       break;
373     case 'https':
374       // Note: Only works for PHP 4.3 compiled with OpenSSL.
375       $port = isset($uri['port']) ? $uri['port'] : 443;
376       $host = $uri['host'] . ($port != 443 ? ':'. $port : '');
377       $fp = @fsockopen('ssl://'. $uri['host'], $port, $errno, $errstr, 20);
378       break;
379     default:
380       $result->error = 'invalid schema '. $uri['scheme'];
381       return $result;
382   }
383
384   // Make sure the socket opened properly.
385   if (!$fp) {
386     $result->error = trim($errno .' '. $errstr);
387     return $result;
388   }
389
390   // Construct the path to act on.
391   $path = isset($uri['path']) ? $uri['path'] : '/';
392   if (isset($uri['query'])) {
393     $path .= '?'. $uri['query'];
394   }
395
396   // Create HTTP request.
397   $defaults = array(
398     // RFC 2616: "non-standard ports MUST, default ports MAY be included".
399     // We don't add the port to prevent from breaking rewrite rules checking
400     // the host that do not take into account the port number.
401     'Host' => "Host: $host",
402     'User-Agent' => 'User-Agent: Drupal (+http://drupal.org/)',
403     'Content-Length' => 'Content-Length: '. strlen($data)
404   );
405
406   foreach ($headers as $header => $value) {
407     $defaults[$header] = $header .': '. $value;
408   }
409
410   $request = $method .' '. $path ." HTTP/1.0\r\n";
411   $request .= implode("\r\n", $defaults);
412   $request .= "\r\n\r\n";
413   if ($data) {
414     $request .= $data ."\r\n";
415   }
416   $result->request = $request;
417
418   fwrite($fp, $request);
419
420   // Fetch response.
421   $response = '';
422   while (!feof($fp) && $chunk = fread($fp, 1024)) {
423     $response .= $chunk;
424   }
425   fclose($fp);
426
427   // Parse response.
428   list($split, $result->data) = explode("\r\n\r\n", $response, 2);
429   $split = preg_split("/\r\n|\n|\r/", $split);
430
431   list($protocol, $code, $text) = explode(' ', trim(array_shift($split)), 3);
432   $result->headers = array();
433
434   // Parse headers.
435   while ($line = trim(array_shift($split))) {
436     list($header, $value) = explode(':', $line, 2);
437     if (isset($result->headers[$header]) && $header == 'Set-Cookie') {
438       // RFC 2109: the Set-Cookie response header comprises the token Set-
439       // Cookie:, followed by a comma-separated list of one or more cookies.
440       $result->headers[$header] .= ','. trim($value);
441     }
442     else {
443       $result->headers[$header] = trim($value);
444     }
445   }
446
447   $responses = array(
448     100 => 'Continue', 101 => 'Switching Protocols',
449     200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content',
450     300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 307 => 'Temporary Redirect',
451     400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Time-out', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Large', 415 => 'Unsupported Media Type', 416 => 'Requested range not satisfiable', 417 => 'Expectation Failed',
452     500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Time-out', 505 => 'HTTP Version not supported'
453   );
454   // RFC 2616 states that all unknown HTTP codes must be treated the same as
455   // the base code in their class.
456   if (!isset($responses[$code])) {
457     $code = floor($code / 100) * 100;
458   }
459
460   switch ($code) {
461     case 200: // OK
462     case 304: // Not modified
463       break;
464     case 301: // Moved permanently
465     case 302: // Moved temporarily
466     case 307: // Moved temporarily
467       $location = $result->headers['Location'];
468
469       if ($retry) {
470         $result = drupal_http_request($result->headers['Location'], $headers, $method, $data, --$retry);
471         $result->redirect_code = $result->code;
472       }
473       $result->redirect_url = $location;
474
475       break;
476     default:
477       $result->error = $text;
478   }
479
480   $result->code = $code;
481   return $result;
482 }
483 /**
484  * @} End of "HTTP handling".
485  */
486
487 /**
488  * Log errors as defined by administrator
489  * Error levels:
490  *  0 = Log errors to database.
491  *  1 = Log errors to database and to screen.
492  */
493 function error_handler($errno, $message, $filename, $line) {
494   if ($errno & (E_ALL ^ E_NOTICE)) {
495     $types = array(1 => 'error', 2 => 'warning', 4 => 'parse error', 8 => 'notice', 16 => 'core error', 32 => 'core warning', 64 => 'compile error', 128 => 'compile warning', 256 => 'user error', 512 => 'user warning', 1024 => 'user notice', 2048 => 'strict warning');
496     $entry = $types[$errno] .': '. $message .' in '. $filename .' on line '. $line .'.';
497
498     // Note: force display of error messages in update.php
499     if (variable_get('error_level', 1) == 1 || strstr($_SERVER['PHP_SELF'], 'update.php')) {
500       drupal_set_message($entry, 'error');
501     }
502
503     watchdog('php', t('%message in %file on line %line.', array('%error' => $types[$errno], '%message' => $message, '%file' => $filename, '%line' => $line)), WATCHDOG_ERROR);
504   }
505 }
506
507 function _fix_gpc_magic(&$item) {
508   if (is_array($item)) {
509     array_walk($item, '_fix_gpc_magic');
510   }
511   else {
512     $item = stripslashes($item);
513   }
514 }
515
516 /**
517  * Correct double-escaping problems caused by "magic quotes" in some PHP
518  * installations.
519  */
520 function fix_gpc_magic() {
521   static $fixed = false;
522   if (!$fixed && ini_get('magic_quotes_gpc')) {
523     array_walk($_GET, '_fix_gpc_magic');
524     array_walk($_POST, '_fix_gpc_magic');
525     array_walk($_COOKIE, '_fix_gpc_magic');
526     array_walk($_REQUEST, '_fix_gpc_magic');
527     $fixed = true;
528   }
529 }
530
531 /**
532  * @name Messages
533  * @{
534  * Frequently used messages.
535  */
536
537 /**
538  * Return a string with a "not applicable" message.
539  */
540 function message_na() {
541   return t('n/a');
542 }
543
544 /**
545  * @} End of "Messages".
546  */
547
548 /**
549  * Initialize the localization system.
550  */
551 function locale_initialize() {
552   global $user;
553
554   if (function_exists('i18n_get_lang')) {
555     return i18n_get_lang();
556   }
557
558   if (function_exists('locale')) {
559     $languages = locale_supported_languages();
560     $languages = $languages['name'];
561   }
562   else {
563     // Ensure the locale/language is correctly returned, even without locale.module.
564     // Useful for e.g. XML/HTML 'lang' attributes.
565     $languages = array('en' => 'English');
566   }
567   if ($user->uid && isset($languages[$user->language])) {
568     return $user->language;
569   }
570   else {
571     return key($languages);
572   }
573 }
574
575 /**
576  * Translate strings to the current locale.
577  *
578  * When using t(), try to put entire sentences and strings in one t() call.
579  * This makes it easier for translators. HTML markup within translation strings
580  * is acceptable, if necessary. The suggested syntax for a link embedded
581  * within a translation string is:
582  * @code
583  *   $msg = t('You must log in below or <a href="%url">create a new
584  *             account</a> before viewing the next page.', array('%url'
585  *             => url('user/register')));
586  * @endcode
587  * We suggest the same syntax for links to other sites. This makes it easy to
588  * change link URLs if needed (which happens often) without requiring updates
589  * to translations.
590  *
591  * @param $string
592  *   A string containing the English string to translate.
593  * @param $args
594  *   An associative array of replacements to make after translation. Incidences
595  *   of any key in this array are replaced with the corresponding value.
596  * @return
597  *   The translated string.
598  */
599 function t($string, $args = 0) {
600   global $locale;
601   if (function_exists('locale') && $locale != 'en') {
602     $string = locale($string);
603   }
604
605   if (!$args) {
606     return $string;
607   }
608   else {
609     return strtr($string, $args);
610   }
611 }
612
613 /**
614  * @defgroup validation Input validation
615  * @{
616  * Functions to validate user input.
617  */
618
619 /**
620  * Verify the syntax of the given e-mail address.
621  *
622  * Empty e-mail addresses are allowed. See RFC 2822 for details.
623  *
624  * @param $mail
625  *   A string containing an e-mail address.
626  * @return
627  *   TRUE if the address is in a valid format.
628  */
629 function valid_email_address($mail) {
630   $user = '[a-zA-Z0-9_\-\.\+\^!#\$%&*+\/\=\?\`\|\{\}~\']+';
631   $domain = '(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.?)+';
632   $ipv4 = '[0-9]{1,3}(\.[0-9]{1,3}){3}';
633   $ipv6 = '[0-9a-fA-F]{1,4}(\:[0-9a-fA-F]{1,4}){7}';
634
635   return preg_match("/^$user@($domain|(\[($ipv4|$ipv6)\]))$/", $mail);
636 }
637
638 /**
639  * Verify the syntax of the given URL.
640  *
641  * @param $url
642  *   The URL to verify.
643  * @param $absolute
644  *   Whether the URL is absolute (beginning with a scheme such as "http:").
645  * @return
646  *   TRUE if the URL is in a valid format.
647  */
648 function valid_url($url, $absolute = FALSE) {
649   $allowed_characters = '[a-z0-9\/:_\-_\.\?\$,~=#&%\+]';
650   if ($absolute) {
651     return preg_match("/^(http|https|ftp):\/\/". $allowed_characters ."+$/i", $url);
652   }
653   else {
654     return preg_match("/^". $allowed_characters ."+$/i", $url);
655   }
656 }
657
658 /**
659  * Register an event for the current visitor (hostname/IP) to the flood control mechanism.
660  *
661  * @param $name
662  *   The name of the event.
663  */
664 function flood_register_event($name) {
665   db_query("INSERT INTO {flood} (event, hostname, timestamp) VALUES ('%s', '%s', %d)", $name, $_SERVER['REMOTE_ADDR'], time());
666 }
667
668 /**
669  * Check if the current visitor (hostname/IP) is allowed to proceed with the specified event.
670  * The user is allowed to proceed if he did not trigger the specified event more than
671  * $threshold times per hour.
672  *
673  * @param $name
674  *   The name of the event.
675  * @param $number
676  *   The maximum number of the specified event per hour (per visitor).
677  * @return
678  *   True if the user did not exceed the hourly threshold.  False otherwise.
679  */
680 function flood_is_allowed($name, $threshold) {
681   $number = db_num_rows(db_query("SELECT event FROM {flood} WHERE event = '%s' AND hostname = '%s' AND timestamp > %d", $name, $_SERVER['REMOTE_ADDR'], time() - 3600));
682   return ($number < $threshold ? TRUE : FALSE);
683 }
684
685 function check_file($filename) {
686   return is_uploaded_file($filename);
687 }
688
689 /**
690  * Prepare a URL for use in an HTML attribute. Strips harmful protocols.
691  *
692  */
693 function check_url($uri) {
694   return filter_xss_bad_protocol($uri, FALSE);
695 }
696
697 /**
698  * @defgroup format Formatting
699  * @{
700  * Functions to format numbers, strings, dates, etc.
701  */
702
703 /**
704  * Formats an RSS channel.
705  *
706  * Arbitrary elements may be added using the $args associative array.
707  */
708 function format_rss_channel($title, $link, $description, $items, $language = 'en', $args = array()) {
709   // arbitrary elements may be added using the $args associative array
710
711   $output = "<channel>\n";
712   $output .= ' <title>'. check_plain($title) ."</title>\n";
713   $output .= ' <link>'. check_url($link) ."</link>\n";
714
715   // The RSS 2.0 "spec" doesn't indicate HTML can be used in the description.
716   // We strip all HTML tags, but need to prevent double encoding from properly
717   // escaped source data (such as &amp becoming &amp;amp;).
718   $output .= ' <description>'. check_plain(decode_entities(strip_tags($description))) ."</description>\n";
719   $output .= ' <language>'. check_plain($language) ."</language>\n";
720   $output .= format_xml_elements($args);
721   $output .= $items;
722   $output .= "</channel>\n";
723
724   return $output;
725 }
726
727 /**
728  * Format a single RSS item.
729  *
730  * Arbitrary elements may be added using the $args associative array.
731  */
732 function format_rss_item($title, $link, $description, $args = array()) {
733   $output = "<item>\n";
734   $output .= ' <title>'. check_plain($title) ."</title>\n";
735   $output .= ' <link>'. check_url($link) ."</link>\n";
736   $output .= ' <description>'. check_plain($description) ."</description>\n";
737   $output .= format_xml_elements($args);
738   $output .= "</item>\n";
739
740   return $output;
741 }
742
743 /**
744  * Format XML elements.
745  *
746  * @param $array
747  *   An array where each item represent an element and is either a:
748  *   - (key => value) pair (<key>value</key>)
749  *   - Associative array with fields:
750  *     - 'key': element name
751  *     - 'value': element contents
752  *     - 'attributes': associative array of element attributes
753  *
754  * In both cases, 'value' can be a simple string, or it can be another array
755  * with the same format as $array itself for nesting.
756  */
757 function format_xml_elements($array) {
758   foreach ($array as $key => $value) {
759     if (is_numeric($key)) {
760       if ($value['key']) {
761         $output .= ' <'. $value['key'];
762         if (isset($value['attributes']) && is_array($value['attributes'])) {
763           $output .= drupal_attributes($value['attributes']);
764         }
765
766         if ($value['value'] != '') {
767           $output .= '>'. (is_array($value['value']) ? format_xml_tags($value['value']) : check_plain($value['value'])) .'</'. $value['key'] .">\n";
768         }
769         else {
770           $output .= " />\n";
771         }
772       }
773     }
774     else {
775       $output .= ' <'. $key .'>'. (is_array($value) ? format_xml_tags($value) : check_plain($value)) ."</$key>\n";
776     }
777   }
778   return $output;
779 }
780
781 /**
782  * Format a string containing a count of items.
783  *
784  * This function ensures that the string is pluralized correctly. Since t() is
785  * called by this function, make sure not to pass already-localized strings to it.
786  *
787  * @param $count
788  *   The item count to display.
789  * @param $singular
790  *   The string for the singular case. Please make sure it is clear this is
791  *   singular, to ease translation (e.g. use "1 new comment" instead of "1 new").
792  * @param $plural
793  *   The string for the plural case. Please make sure it is clear this is plural,
794  *   to ease translation. Use %count in place of the item count, as in "%count
795  *   new comments".
796  * @return
797  *   A translated string.
798  */
799 function format_plural($count, $singular, $plural) {
800   if ($count == 1) return t($singular, array("%count" => $count));
801
802   // get the plural index through the gettext formula
803   $index = (function_exists('locale_get_plural')) ? locale_get_plural($count) : -1;
804   if ($index < 0) { // backward compatibility
805     return t($plural, array("%count" => $count));
806   }
807   else {
808     switch ($index) {
809       case "0":
810         return t($singular, array("%count" => $count));
811       case "1":
812         return t($plural, array("%count" => $count));
813       default:
814         return t(strtr($plural, array("%count" => '%count['. $index .']')), array('%count['. $index .']' => $count));
815     }
816   }
817 }
818
819 /**
820  * Generate a string representation for the given byte count.
821  *
822  * @param $size
823  *   The size in bytes.
824  * @return
825  *   A translated string representation of the size.
826  */
827 function format_size($size) {
828   $suffix = t('bytes');
829   if ($size >= 1024) {
830     $size = round($size / 1024, 2);
831     $suffix = t('KB');
832   }
833   if ($size >= 1024) {
834     $size = round($size / 1024, 2);
835     $suffix = t('MB');
836   }
837   return t('%size %suffix', array('%size' => $size, '%suffix' => $suffix));
838 }
839
840 /**
841  * Format a time interval with the requested granularity.
842  *
843  * @param $timestamp
844  *   The length of the interval in seconds.
845  * @param $granularity
846  *   How many different units to display in the string.
847  * @return
848  *   A translated string representation of the interval.
849  */
850 function format_interval($timestamp, $granularity = 2) {
851   $units = array('1 year|%count years' => 31536000, '1 week|%count weeks' => 604800, '1 day|%count days' => 86400, '1 hour|%count hours' => 3600, '1 min|%count min' => 60, '1 sec|%count sec' => 1);
852   $output = '';
853   foreach ($units as $key => $value) {
854     $key = explode('|', $key);
855     if ($timestamp >= $value) {
856       $output .= ($output ? ' ' : '') . format_plural(floor($timestamp / $value), $key[0], $key[1]);
857       $timestamp %= $value;
858       $granularity--;
859     }
860
861     if ($granularity == 0) {
862       break;
863     }
864   }
865   return $output ? $output : t('0 sec');
866 }
867
868 /**
869  * Format a date with the given configured format or a custom format string.
870  *
871  * Drupal allows administrators to select formatting strings for 'small',
872  * 'medium' and 'large' date formats. This function can handle these formats,
873  * as well as any custom format.
874  *
875  * @param $timestamp
876  *   The exact date to format, as a UNIX timestamp.
877  * @param $type
878  *   The format to use. Can be "small", "medium" or "large" for the preconfigured
879  *   date formats. If "custom" is specified, then $format is required as well.
880  * @param $format
881  *   A PHP date format string as required by date(). A backslash should be used
882  *   before a character to avoid interpreting the character as part of a date
883  *   format.
884  * @param $timezone
885  *   Time zone offset in seconds; if omitted, the user's time zone is used.
886  * @return
887  *   A translated date string in the requested format.
888  */
889 function format_date($timestamp, $type = 'medium', $format = '', $timezone = NULL) {
890   if (!isset($timezone)) {
891     global $user;
892     if (variable_get('configurable_timezones', 1) && $user->uid && strlen($user->timezone)) {
893       $timezone = $user->timezone;
894     }
895     else {
896       $timezone = variable_get('date_default_timezone', 0);
897     }
898   }
899
900   $timestamp += $timezone;
901
902   switch ($type) {
903     case 'small':
904       $format = variable_get('date_format_short', 'm/d/Y - H:i');
905       break;
906     case 'large':
907       $format = variable_get('date_format_long', 'l, F j, Y - H:i');
908       break;
909     case 'custom':
910       // No change to format
911       break;
912     case 'medium':
913     default:
914       $format = variable_get('date_format_medium', 'D, m/d/Y - H:i');
915   }
916
917   $max = strlen($format);
918   $date = '';
919   for ($i = 0; $i < $max; $i++) {
920     $c = $format[$i];
921     if (strpos('AaDFlM', $c) !== false) {
922       $date .= t(gmdate($c, $timestamp));
923     }
924     else if (strpos('BdgGhHiIjLmnsStTUwWYyz', $c) !== false) {
925       $date .= gmdate($c, $timestamp);
926     }
927     else if ($c == 'r') {
928       $date .= format_date($timestamp - $timezone, 'custom', 'D, d M Y H:i:s O', $timezone);
929     }
930     else if ($c == 'O') {
931       $date .= sprintf('%s%02d%02d', ($timezone < 0 ? '-' : '+'), abs($timezone / 3600), abs($timezone % 3600) / 60);
932     }
933     else if ($c == 'Z') {
934       $date .= $timezone;
935     }
936     else if ($c == '\\') {
937       $date .= $format[++$i];
938     }
939     else {
940       $date .= $c;
941     }
942   }
943
944   return $date;
945 }
946
947 /**
948  * @} End of "defgroup format".
949  */
950
951 /**
952  * Generate a URL from a Drupal menu path. Will also pass-through existing URLs.
953  *
954  * @param $path
955  *   The Drupal path being linked to, such as "admin/node", or an existing URL
956  *   like "http://drupal.org/".
957  * @param $query
958  *   A query string to append to the link or URL.
959  * @param $fragment
960  *   A fragment identifier (named anchor) to append to the link. If an existing
961  *   URL with a fragment identifier is used, it will be replaced. Note, do not
962  *   include the '#'.
963  * @param $absolute
964  *   Whether to force the output to be an absolute link (beginning with http:).
965  *   Useful for links that will be displayed outside the site, such as in an
966  *   RSS feed.
967  * @return
968  *   a string containing a URL to the given path.
969  *
970  * When creating links in modules, consider whether l() could be a better
971  * alternative than url().
972  */
973 function url($path = NULL, $query = NULL, $fragment = NULL, $absolute = FALSE) {
974   if (isset($fragment)) {
975     $fragment = '#'. $fragment;
976   }
977
978   // Return an external link if $path contains an allowed absolute URL.
979   // Only call the slow filter_xss_bad_protocol if $path contains a ':'.
980   if (strpos($path, ':') !== FALSE && filter_xss_bad_protocol($path, FALSE) == check_plain($path)) {
981     // Split off the fragment
982     if (strpos($path, '#')) {
983       list($path, $old_fragment) = explode('#', $path, 2);
984       if (isset($old_fragment) && !isset($fragment)) {
985         $fragment = '#'. $old_fragment;
986       }
987     }
988     // Append the query
989     if (isset($query)) {
990       $path .= (strpos($path, '?') ? '&' : '?') . $query;
991     }
992     // Reassemble
993     return $path . $fragment;
994   }
995
996   global $base_url;
997   static $script;
998   static $clean_url;
999
1000   if (empty($script)) {
1001     // On some web servers, such as IIS, we can't omit "index.php".  So, we
1002     // generate "index.php?q=foo" instead of "?q=foo" on anything that is not
1003     // Apache.
1004     $script = (strpos($_SERVER['SERVER_SOFTWARE'], 'Apache') === false) ? 'index.php' : '';
1005   }
1006
1007   // Cache the clean_url variable to improve performance.
1008   if (!isset($clean_url)) {
1009     $clean_url = (bool)variable_get('clean_url', '0');
1010   }
1011
1012   $base = ($absolute ? $base_url . '/' : base_path());
1013
1014   // The special path '<front>' links to the default front page.
1015   if (!empty($path) && $path != '<front>') {
1016     $path = drupal_get_path_alias($path);
1017     $path = drupal_urlencode($path);
1018     if (!$clean_url) {
1019       if (isset($query)) {
1020         return $base . $script .'?q='. $path .'&'. $query . $fragment;
1021       }
1022       else {
1023         return $base . $script .'?q='. $path . $fragment;
1024       }
1025     }
1026     else {
1027       if (isset($query)) {
1028         return $base . $path .'?'. $query . $fragment;
1029       }
1030       else {
1031         return $base . $path . $fragment;
1032       }
1033     }
1034   }
1035   else {
1036     if (isset($query)) {
1037       return $base . $script .'?'. $query . $fragment;
1038     }
1039     else {
1040       return $base . $fragment;
1041     }
1042   }
1043 }
1044
1045 /**
1046  * Format an attribute string to insert in a tag.
1047  *
1048  * @param $attributes
1049  *   An associative array of HTML attributes.
1050  * @return
1051  *   An HTML string ready for insertion in a tag.
1052  */
1053 function drupal_attributes($attributes = array()) {
1054   if (is_array($attributes)) {
1055     $t = '';
1056     foreach ($attributes as $key => $value) {
1057       $t .= " $key=".'"'. check_plain($value) .'"';
1058     }
1059     return $t;
1060   }
1061 }
1062
1063 /**
1064  * Format an internal Drupal link.
1065  *
1066  * This function correctly handles aliased paths, and allows themes to highlight
1067  * links to the current page correctly, so all internal links output by modules
1068  * should be generated by this function if possible.
1069  *
1070  * @param $text
1071  *   The text to be enclosed with the anchor tag.
1072  * @param $path
1073  *   The Drupal path being linked to, such as "admin/node". Can be an external
1074  *   or internal URL.
1075  *     - If you provide the full URL, it will be considered an
1076  *   external URL.
1077  *     - If you provide only the path (e.g. "admin/node"), it is considered an
1078  *   internal link. In this case, it must be a system URL as the url() function
1079  *   will generate the alias.
1080  * @param $attributes
1081  *   An associative array of HTML attributes to apply to the anchor tag.
1082  * @param $query
1083  *   A query string to append to the link.
1084  * @param $fragment
1085  *   A fragment identifier (named anchor) to append to the link.
1086  * @param $absolute
1087  *   Whether to force the output to be an absolute link (beginning with http:).
1088  *   Useful for links that will be displayed outside the site, such as in an RSS
1089  *   feed.
1090  * @param $html
1091  *   Whether the title is HTML, or just plain-text. For example for making an
1092  *   image a link, this must be set to TRUE, or else you will see the encoded
1093  *   HTML.
1094  * @return
1095  *   an HTML string containing a link to the given path.
1096  */
1097 function l($text, $path, $attributes = array(), $query = NULL, $fragment = NULL, $absolute = FALSE, $html = FALSE) {
1098   if ($path == $_GET['q']) {
1099     if (isset($attributes['class'])) {
1100       $attributes['class'] .= ' active';
1101     }
1102     else {
1103       $attributes['class'] = 'active';
1104     }
1105   }
1106   return '<a href="'. check_url(url($path, $query, $fragment, $absolute)) .'"'. drupal_attributes($attributes) .'>'. ($html ? $text : check_plain($text)) .'</a>';
1107 }
1108
1109 /**
1110  * Perform end-of-request tasks.
1111  *
1112  * This function sets the page cache if appropriate, and allows modules to
1113  * react to the closing of the page by calling hook_exit().
1114  */
1115 function drupal_page_footer() {
1116   if (variable_get('cache', 0)) {
1117     page_set_cache();
1118   }
1119
1120   module_invoke_all('exit');
1121 }
1122
1123 /**
1124  * Form an associative array from a linear array.
1125  *
1126  * This function walks through the provided array and constructs an associative
1127  * array out of it. The keys of the resulting array will be the values of the
1128  * input array. The values will be the same as the keys unless a function is
1129  * specified, in which case the output of the function is used for the values
1130  * instead.
1131  *
1132  * @param $array
1133  *   A linear array.
1134  * @param $function
1135  *   The name of a function to apply to all values before output.
1136  * @result
1137  *   An associative array.
1138  */
1139 function drupal_map_assoc($array, $function = NULL) {
1140   if (!isset($function)) {
1141     $result = array();
1142     foreach ($array as $value) {
1143       $result[$value] = $value;
1144     }
1145     return $result;
1146   }
1147   elseif (function_exists($function)) {
1148     $result = array();
1149     foreach($array as $value) {
1150       $result[$value] = $function($value);
1151     }
1152     return $result;
1153   }
1154 }
1155
1156 /**
1157  * Evaluate a string of PHP code.
1158  *
1159  * This is a wrapper around PHP's eval(). It uses output buffering to capture both
1160  * returned and printed text. Unlike eval(), we require code to be surrounded by
1161  * <?php ?> tags; in other words, we evaluate the code as if it were a stand-alone
1162  * PHP file.
1163  *
1164  * Using this wrapper also ensures that the PHP code which is evaluated can not
1165  * overwrite any variables in the calling code, unlike a regular eval() call.
1166  *
1167  * @param $code
1168  *   The code to evaluate.
1169  * @return
1170  *   A string containing the printed output of the code, followed by the returned
1171  *   output of the code.
1172  */
1173 function drupal_eval($code) {
1174   ob_start();
1175   print eval('?>'. $code);
1176   $output = ob_get_contents();
1177   ob_end_clean();
1178   return $output;
1179 }
1180
1181 /**
1182  * Returns the path to a system item (module, theme, etc.).
1183  *
1184  * @param $type
1185  *   The type of the item (i.e. theme, theme_engine, module).
1186  * @param $name
1187  *   The name of the item for which the path is requested.
1188  *
1189  * @return
1190  *   The path to the requested item.
1191  */
1192 function drupal_get_path($type, $name) {
1193   return dirname(drupal_get_filename($type, $name));
1194 }
1195
1196 /**
1197  * Returns the base URL path of the Drupal installation.
1198  * At the very least, this will always default to /.
1199  */
1200 function base_path() {
1201   return $GLOBALS['base_path'];
1202 }
1203
1204 /**
1205  * Provide a substitute clone() function for PHP4.
1206  */
1207 function drupal_clone($object) {
1208   return version_compare(phpversion(), '5.0') < 0 ? $object : clone($object);
1209 }
1210
1211 /**
1212  * Add a <link> tag to the page's HEAD.
1213  */
1214 function drupal_add_link($attributes) {
1215   drupal_set_html_head('<link'. drupal_attributes($attributes) ." />\n");
1216 }
1217
1218 /**
1219  * Add a JavaScript file to the output.
1220  *
1221  * The first time this function is invoked per page request,
1222  * it adds "misc/drupal.js" to the output. Other scripts
1223  * depends on the 'killswitch' inside it.
1224  */
1225 function drupal_add_js($file, $nocache = FALSE) {
1226   static $sent = array();
1227
1228   $postfix = $nocache ? '?'. time() : '';
1229   if (!isset($sent['misc/drupal.js'])) {
1230     drupal_set_html_head('<script type="text/javascript" src="'. base_path() .'misc/drupal.js'. $postfix .'"></script>');
1231     $sent['misc/drupal.js'] = true;
1232   }
1233   if (!isset($sent[$file])) {
1234     drupal_set_html_head('<script type="text/javascript" src="'. check_url(base_path() . $file) . $postfix .'"></script>');
1235     $sent[$file] = true;
1236   }
1237 }
1238
1239 /**
1240  * Generates a Javascript call, while importing the arguments as is.
1241  * PHP arrays are turned into JS objects to preserve keys. This means the array
1242  * keys must conform to JS's member naming rules.
1243  *
1244  * @param $function
1245  *   The name of the function to call.
1246  * @param $arguments
1247  *   An array of arguments.
1248  */
1249 function drupal_call_js($function) {
1250   $arguments = func_get_args();
1251   array_shift($arguments);
1252   $args = array();
1253   foreach ($arguments as $arg) {
1254     $args[] = drupal_to_js($arg);
1255   }
1256   $output = '<script type="text/javascript">'. $function .'('. implode(', ', $args) .');</script>';
1257   return $output;
1258 }
1259
1260 /**
1261  * Converts a PHP variable into its Javascript equivalent.
1262  *
1263  * We use HTML-safe strings, i.e. with <, > and & escaped.
1264  */
1265 function drupal_to_js($var) {
1266   switch (gettype($var)) {
1267     case 'boolean':
1268       return $var ? 'true' : 'false'; // Lowercase necessary!
1269     case 'integer':
1270     case 'double':
1271       return $var;
1272     case 'resource':
1273     case 'string':
1274       return '"'. str_replace(array("\r", "\n", "<", ">", "&"),
1275                               array('\r', '\n', '\x3c', '\x3e', '\x26'),
1276                               addslashes($var)) .'"';
1277     case 'array':
1278       if (array_keys($var) === range(0, sizeof($var) - 1)) {
1279         $output = array();
1280         foreach($var as $v) {
1281           $output[] = drupal_to_js($v);
1282         }
1283         return '[ '. implode(', ', $output) .' ]';
1284       }
1285       // Fall through
1286     case 'object':
1287       $output = array();
1288       foreach ($var as $k => $v) {
1289         $output[] = drupal_to_js(strval($k)) .': '. drupal_to_js($v);
1290       }
1291       return '{ '. implode(', ', $output) .' }';
1292     default:
1293       return 'null';
1294   }
1295 }
1296
1297 /**
1298  * Wrapper around urlencode() which avoids Apache quirks.
1299  *
1300  * Should be used when placing arbitrary data in an URL. Note that Drupal paths
1301  * are urlencoded() when passed through url() and do not require urlencoding()
1302  * of individual components.
1303  *
1304  * Notes:
1305  * - For esthetic reasons, we do not escape slashes. This also avoids a 'feature'
1306  *   in Apache where it 404s on any path containing '%2F'.
1307  * - mod_rewrite's unescapes %-encoded ampersands and hashes when clean URLs
1308  *   are used, which are interpreted as delimiters by PHP. These characters are
1309  *   double escaped so PHP will still see the encoded version.
1310  *
1311  * @param $text
1312  *   String to encode
1313  */
1314 function drupal_urlencode($text) {
1315   if (variable_get('clean_url', '0')) {
1316     return str_replace(array('%2F', '%26', '%23'),
1317                        array('/', '%2526', '%2523'),
1318                        urlencode($text));
1319   }
1320   else {
1321     return str_replace('%2F', '/', urlencode($text));    
1322   }
1323 }
1324
1325 /**
1326  * Performs one or more XML-RPC request(s).
1327  *
1328  * @param $url
1329  *   An absolute URL of the XML-RPC endpoint.
1330  *     Example:
1331  *     http://www.domain.com/xmlrpc.php
1332  * @param ...
1333  *   For one request:
1334  *     The method name followed by a variable number of arguments to the method.
1335  *   For multiple requests (system.multicall):
1336  *     An array of call arrays. Each call array follows the pattern of the single
1337  *     request: method name followed by the arguments to the method.
1338  * @return
1339  *   For one request:
1340  *     Either the return value of the method on success, or FALSE.
1341  *     If FALSE is returned, see xmlrpc_errno() and xmlrpc_error_msg().
1342  *   For multiple requests:
1343  *     An array of results. Each result will either be the result
1344  *     returned by the method called, or an xmlrpc_error object if the call
1345  *     failed. See xmlrpc_error().
1346  */
1347 function xmlrpc($url) {
1348   require_once './includes/xmlrpc.inc';
1349   $args = func_get_args();
1350   return call_user_func_array('_xmlrpc', $args);
1351 }
1352
1353 function _drupal_bootstrap_full() {
1354   static $called;
1355   global $locale;
1356
1357   if ($called) {
1358     return;
1359   }
1360   $called = 1;
1361   require_once './includes/theme.inc';
1362   require_once './includes/pager.inc';
1363   require_once './includes/menu.inc';
1364   require_once './includes/tablesort.inc';
1365   require_once './includes/file.inc';
1366   require_once './includes/unicode.inc';
1367   require_once './includes/image.inc';
1368   require_once './includes/form.inc';
1369   // Set the Drupal custom error handler.
1370   set_error_handler('error_handler');
1371   // Emit the correct charset HTTP header.
1372   drupal_set_header('Content-Type: text/html; charset=utf-8');
1373   // Detect string handling method
1374   unicode_check();
1375   // Undo magic quotes
1376   fix_gpc_magic();
1377   // Load all enabled modules
1378   module_load_all();
1379   // Initialize the localization system. Depends on i18n.module being loaded already.
1380   $locale = locale_initialize();
1381   // Let all modules take action before menu system handles the reqest
1382   module_invoke_all('init');
1383
1384 }
1385
1386 /**
1387  * Store the current page in the cache.
1388  *
1389  * We try to store a gzipped version of the cache. This requires the
1390  * PHP zlib extension (http://php.net/manual/en/ref.zlib.php).
1391  * Presence of the extension is checked by testing for the function
1392  * gzencode. There are two compression algorithms: gzip and deflate.
1393  * The majority of all modern browsers support gzip or both of them.
1394  * We thus only deal with the gzip variant and unzip the cache in case
1395  * the browser does not accept gzip encoding.
1396  *
1397  * @see drupal_page_header
1398  */
1399 function page_set_cache() {
1400   global $user, $base_root;
1401
1402   if (!$user->uid && $_SERVER['REQUEST_METHOD'] == 'GET') {
1403     // This will fail in some cases, see page_get_cache() for the explanation.
1404     if ($data = ob_get_contents()) {
1405       $cache = TRUE;
1406       if (function_exists('gzencode')) {
1407         // We do not store the data in case the zlib mode is deflate.
1408         // This should be rarely happening.
1409         if (zlib_get_coding_type() == 'deflate') {
1410           $cache = FALSE;
1411         }
1412         else if (zlib_get_coding_type() == FALSE) {
1413           $data = gzencode($data, 9, FORCE_GZIP);
1414         }
1415         // The remaining case is 'gzip' which means the data is
1416         // already compressed and nothing left to do but to store it.
1417       }
1418       ob_end_flush();
1419       if ($cache && $data) {
1420         cache_set($base_root . request_uri(), $data, CACHE_TEMPORARY, drupal_get_headers());
1421       }
1422     }
1423   }
1424 }