3 * FusionForge miscellaneous utils
5 * Copyright 1999-2001, VA Linux Systems, Inc.
6 * Copyright 2009-2011, Roland Mas
7 * Copyright 2009-2011, Franck Villaume - Capgemini
8 * Copyright 2010-2012, Thorsten Glaser - Tarent
9 * Copyright 2010-2012, Alain Peyrat - Alcatel-Lucent
10 * Copyright 2013,2016-2018, Franck Villaume - TrivialDev
11 * Copyright 2016, Stéphane-Eymeric Bredthauer - TrivalDev
13 * This file is part of FusionForge. FusionForge is free software;
14 * you can redistribute it and/or modify it under the terms of the
15 * GNU General Public License as published by the Free Software
16 * Foundation; either version 2 of the Licence, or (at your option)
19 * FusionForge is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
24 * You should have received a copy of the GNU General Public License along
25 * with FusionForge; if not, write to the Free Software Foundation, Inc.,
26 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
30 * is_utf8 - utf-8 detection
32 * @param string $str the string to analyze
35 * From http://www.php.net/manual/en/function.mb-detect-encoding.php#85294
37 function is_utf8($str) {
41 for($i=0; $i<$len; $i++){
44 if(($c >= 254)) return false;
45 elseif($c >= 252) $bits=6;
46 elseif($c >= 248) $bits=5;
47 elseif($c >= 240) $bits=4;
48 elseif($c >= 224) $bits=3;
49 elseif($c >= 192) $bits=2;
51 if(($i+$bits) > $len) return false;
55 if($b < 128 || $b > 191) return false;
64 * util_strip_unprintable - ???
69 function util_strip_unprintable(&$data) {
70 if (is_array($data)) {
71 foreach ($data as $key => &$value) {
72 util_strip_unprintable($value);
75 $data = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $data);
81 * removeCRLF - remove any Carriage Return-Line Feed from a string.
82 * That function is useful to remove the possibility of a CRLF Injection when sending mail
83 * All the data that we will send should be passed through that function
85 * @param string $str The string that we want to empty from any CRLF
88 function util_remove_CRLF($str) {
89 return strtr($str, "\015\012", ' ');
93 * util_check_fileupload - determines if a filename is appropriate for upload
95 * @param array $filename The uploaded file as returned by getUploadedFile()
98 function util_check_fileupload($filename) {
100 /* Empty file is a valid file.
101 This is because this function should be called
102 unconditionally at the top of submit action processing
103 and many forms have optional file upload. */
104 if ($filename == 'none' || $filename == '') {
108 /* This should be enough... */
109 if (!is_uploaded_file($filename)) {
112 /* ... but we'd rather be paranoid */
113 if (strstr($filename, '..')) {
116 if (!is_file($filename)) {
119 if (!file_exists($filename)) {
122 if ((dirname($filename) != '/tmp') &&
123 (dirname($filename) != "/var/tmp")) {
130 * util_check_url - determines if given URL is valid.
132 * Currently, test is very basic, only the protocol is
133 * checked, allowed values are: http, https, ftp.
135 * @param string $url The URL
136 * @return bool true if valid, false if not valid.
138 function util_check_url($url) {
139 return (preg_match('/^(http|https|ftp):\/\//', $url) > 0);
143 * util_send_message - Send email
144 * This function should be used in place of the PHP mail() function
146 * @param string $to The email recipients address
147 * @param string $subject The email subject
148 * @param string $body The body of the email message
149 * @param string $from The optional email sender address. Defaults to 'noreply@'
150 * @param string $BCC The addresses to blind-carbon-copy this message (comma-separated)
151 * @param string $sendername The optional email sender name. Defaults to ''
152 * @param bool|string $extra_headers
153 * @param bool $send_html_email Whether to send plain text or html email
154 * @param string $CC The addresses to carbon-copy this message (comma-separated)
156 function util_send_message($to, $subject, $body, $from = '', $BCC = '', $sendername = '', $extra_headers = '',
157 $send_html_email = false, $CC = '') {
159 $to = 'noreply@'.forge_get_config('web_host');
162 $from = 'noreply@'.forge_get_config('web_host');
165 $charset = _('UTF-8');
170 $body2 = "Auto-Submitted: auto-generated\n";
171 if ($extra_headers) {
172 $body2 .= $extra_headers."\n";
175 "\nFrom: ".util_encode_mailaddr($from, $sendername, $charset);
176 if (forge_get_config('bcc_all_emails') != '') {
177 $BCC .= ",".forge_get_config('bcc_all_emails');
180 $body2 .= "\nBCC: $BCC";
183 $body2 .= "\nCC: $CC";
185 $send_html_email? $type = "html" : $type = "plain";
186 $body2 .= "\n".util_encode_mimeheader("Subject", $subject, $charset).
187 "\nContent-type: text/$type; charset=$charset".
188 "\nContent-Transfer-Encoding: 8bit".
190 util_convert_body($body, $charset);
192 $handle = popen(forge_get_config('sendmail_path')." -f'$from' -t -i", 'w');
193 fwrite($handle, $body2);
198 * util_encode_mailaddr - Encode email address to MIME format
200 * @param string $email The email address
201 * @param string $name The email's owner name
202 * @param string $charset The converting charset
205 function util_encode_mailaddr($email, $name, $charset) {
206 if (function_exists('mb_convert_encoding') && trim($name) != "") {
207 $name = "=?".$charset."?B?".
208 base64_encode(mb_convert_encoding(
209 $name, $charset, "UTF-8")).
213 return $name." <".$email.">";
217 * util_encode_mimeheader - Encode mimeheader
219 * @param string $headername The name of the header (e.g. "Subject")
220 * @param string $str The email subject
221 * @param string $charset The converting charset (like ISO-2022-JP)
222 * @return string The MIME encoded subject
225 function util_encode_mimeheader($headername, $str, $charset) {
226 if (function_exists('mb_internal_encoding') &&
227 function_exists('mb_encode_mimeheader')) {
228 $x = mb_internal_encoding();
229 mb_internal_encoding("UTF-8");
230 $y = mb_encode_mimeheader($headername.": ".$str,
232 mb_internal_encoding($x);
236 if (!function_exists('mb_convert_encoding')) {
237 return $headername.": ".$str;
240 return $headername.": "."=?".$charset."?B?".
241 base64_encode(mb_convert_encoding(
242 $str, $charset, "UTF-8")).
247 * util_convert_body - Convert body of the email message
249 * @param string $str The body of the email message
250 * @param string $charset The charset of the email message
251 * @return string The converted body of the email message
254 function util_convert_body($str, $charset) {
255 if (!function_exists('mb_convert_encoding') || $charset == 'UTF-8') {
259 return mb_convert_encoding($str, $charset, "UTF-8");
263 * util_handle_message - a convenience wrapper which sends messages
264 * to an email account
266 * @param array $id_arr array of user_id's from the user table
267 * @param string $subject subject of the message
268 * @param string $body the message body
269 * @param string $extra_emails a comma-separated list of email address
270 * @param string $dummy1 ignored (no longer used)
271 * @param string $from From header
273 function util_handle_message($id_arr, $subject, $body, $extra_emails = '', $dummy1 = '', $from = '') {
276 if (count($id_arr) < 1) {
279 $res = db_query_params('SELECT user_id,email FROM users WHERE user_id = ANY ($1)',
280 array(db_int_array_to_any_clause($id_arr)));
281 $rows = db_numrows($res);
283 for ($i = 0; $i < $rows; $i++) {
284 if (db_result($res, $i, 'user_id') == 100) {
285 // Do not send messages to "Nobody"
288 $address['email'][] = db_result($res,$i,'email');
290 if (isset ($address['email']) && count($address['email']) > 0) {
291 $extra_emails = implode($address['email'], ',').','.$extra_emails;
295 util_send_message('', $subject, $body, $from, $extra_emails);
300 * util_unconvert_htmlspecialchars - Unconverts a string converted with htmlspecialchars()
302 * @param string $string The string to unconvert
303 * @return string The unconverted string
306 function util_unconvert_htmlspecialchars($string) {
307 return html_entity_decode($string, ENT_QUOTES, "UTF-8");
311 * util_result_columns_to_assoc - Takes a result set and turns the column pair into an associative array
313 * @param string $result The result set ID
314 * @param int $col_key The column key
315 * @param int $col_val The optional column value
316 * @return array An associative array
319 function util_result_columns_to_assoc($result, $col_key = 0, $col_val = 1) {
321 $rows = db_numrows($result);
324 for ($i = 0; $i < $rows; $i++) {
325 $arr[db_result($result, $i, $col_key)] = db_result($result, $i, $col_val);
332 * util_result_column_to_array - Takes a result set and turns the optional column into an array
334 * @param resource $result The result set
335 * @param int $col The column
339 function &util_result_column_to_array($result, $col = 0) {
341 $rows = db_numrows($result);
344 for ($i = 0; $i < $rows; $i++) {
345 $arr[$i] = db_result($result, $i, $col);
352 * util_line_wrap - Automatically linewrap text
354 * @param string $text The text to wrap
355 * @param int $wrap The number of characters to wrap - Default is 80
356 * @param string $break The line break to use - Default is '\n'
357 * @return string The wrapped text
360 function util_line_wrap($text, $wrap = 80, $break = "\n") {
361 return wordwrap($text, $wrap, $break, false);
365 * util_make_links - Turn URL's into HREF's.
367 * @param string $data The URL
368 * @return mixed|string The HREF'ed URL
371 function util_make_links($data = '') {
376 for ($i = 0; $i < 5; $i++) {
377 $randPattern = rand(10000, 30000);
378 if (!preg_match("/$randPattern/", $data)) {
385 while(preg_match('/<a [^>]*>[^<]*<\/a>/i', $data, $part)) {
387 $data = preg_replace('/<a [^>]*>[^<]*<\/a>/i', $randPattern, $data, 1);
391 while (preg_match('/<a [^>]*>.*<\/a>/siU', $data, $part)) {
393 $data = preg_replace('/<a [^>]*>.*<\/a>/siU', $randPattern, $data, 1);
395 while (preg_match('/<img [^>]*\/>/siU', $data, $part)) {
397 $data = preg_replace('/<img [^>]*\/>/siU', $randPattern, $data, 1);
399 $data = str_replace('>', "\1", $data);
400 $data = preg_replace("#([ \t]|^)www\.#i", " http://www.", $data);
401 $data = preg_replace("#([[:alnum:]]+)://([^[:space:]<\1]*)([[:alnum:]\#?/&=])#i", "<a href=\"\\1://\\2\\3\" target=\"_blank\">\\1://\\2\\3</a>", $data);
402 $data = preg_replace("#([[:space:]]|^)(([a-z0-9_]|\\-|\\.)+@([^[:space:]<\1]*)([[:alnum:]-]))#i", "\\1<a href=\"mailto:\\2\" target=\"_blank\">\\2</a>", $data);
403 $data = str_replace("\1", '>', $data);
404 for ($i = 0; $i < count($mem); $i++) {
405 $data = preg_replace("/$randPattern/", $mem[$i], $data, 1);
410 $lines = split("\n", $data);
412 foreach ($lines as $key => $line) {
413 // Do not scan lines if they already have hyperlinks.
414 // Avoid problem with text written with an WYSIWYG HTML editor.
415 if (eregi('<a ([^>]*)>.*</a>', $line, $linePart)) {
416 if (eregi('href="[^"]*"', $linePart[1])) {
422 // Skip </img> tag also
423 if (eregi('<img ([^>]*)/>', $line, $linePart)) {
424 if (eregi('href="[^"]*"', $linePart[1])) {
430 // When we come here, we usually have form input
431 // encoded in entities. Our aim is to NOT include
432 // angle brackets in the URL
433 // (RFC2396; http://www.w3.org/Addressing/URL/5.1_Wrappers.html)
434 $line = str_replace('>', "\1", $line);
435 $line = preg_replace("/([ \t]|^)www\./i", " http://www.", $line);
436 $line = preg_replace("/([[:alnum:]]+):\/\/([^[:space:]<\1]*)([[:alnum:]#?\/&=])/i",
437 "<a href=\"\\1://\\2\\3\" target=\"_blank\">\\1://\\2\\3</a>", $line);
438 $line = preg_replace(
439 "/([[:space:]]|^)(([a-z0-9_]|\\-|\\.)+@([^[:space:]]*)([[:alnum:]-]))/i",
440 "\\1<a href=\"mailto:\\2\" target=\"_blank\">\\2</a>",
443 $line = str_replace("\1", '>', $line);
450 * utils_requiredField - Adds the required field marker
452 * @return string A string holding the HTML to mark a required field
454 function utils_requiredField() {
455 return html_e('span', array('class' => 'requiredfield'), '*');
459 * ShowResultSet - Show a generic result set
460 * Very simple, plain way to show a generic result set
462 * @param resource $result The result set ID
463 * @param string $title The title of the result set
464 * @param bool $linkify The option to turn URL's into links
465 * @param bool $displayHeaders The option to display headers
466 * @param array $headerMapping The db field name -> label mapping
467 * @param array $excludedCols Don't display these cols
469 function ShowResultSet($result, $title = '', $linkify = false, $displayHeaders = true, $headerMapping = array(), $excludedCols = array()) {
470 global $group_id, $HTML;
473 $rows = db_numrows($result);
474 $cols = db_numfields($result);
476 echo $HTML->listTableTop();
478 /* Create the headers */
479 $headersCellData = array();
480 $colsToKeep = array();
481 for ($i = 0; $i < $cols; $i++) {
482 $fieldName = db_fieldname($result, $i);
483 if (in_array($fieldName, $excludedCols)) {
487 if (isset($headerMapping[$fieldName])) {
488 if (is_array($headerMapping[$fieldName])) {
489 $headersCellData[] = $headerMapping[$fieldName];
491 $headersCellData[] = array($headerMapping[$fieldName]);
494 $headersCellData[] = array($fieldName);
498 /* Create the title */
499 if (strlen($title) > 0) {
500 $titleCellData = array();
501 $titleCellData[] = array($title, 'colspan' => count($headersCellData));
502 echo $HTML->multiTableRow(array(), $titleCellData, TRUE);
505 /* Display the headers */
506 if ($displayHeaders) {
507 echo $HTML->multiTableRow(array(), $headersCellData, TRUE);
510 /* Create the rows */
511 for ($j = 0; $j < $rows; $j++) {
513 for ($i = 0; $i < $cols; $i++) {
514 if (in_array($i, $colsToKeep)) {
515 if ($linkify && $i == 0) {
516 if ($linkify == "bug_cat") {
517 $linkUrl = util_make_link(getStringFromServer('PHP_SELF').'?group_id='.$group_id.'&bug_cat_mod=y&bug_cat_id='.db_result($result, $j, 'bug_category_id'), db_result($result, $j, $i));
518 } elseif ($linkify == "bug_group") {
519 $linkUrl = util_make_link(getStringFromServer('PHP_SELF').'?group_id='.$group_id.'&bug_group_mod=y&bug_group_id='.db_result($result, $j, 'bug_group_id'), db_result($result, $j, $i));
520 } elseif ($linkify == "patch_cat") {
521 $linkUrl = util_make_link(getStringFromServer('PHP_SELF').'?group_id='.$group_id.'&patch_cat_mod=y&patch_cat_id='.db_result($result, $j, 'patch_category_id'), db_result($result, $j, $i));
522 } elseif ($linkify == "support_cat") {
523 $linkUrl = util_make_link(getStringFromServer('PHP_SELF').'?group_id='.$group_id.'&support_cat_mod=y&support_cat_id='.db_result($result, $j, 'support_category_id'), db_result($result, $j, $i));
524 } elseif ($linkify == "pm_project") {
525 $linkUrl = util_make_link(getStringFromServer('PHP_SELF').'?group_id='.$group_id.'&project_cat_mod=y&project_cat_id='.db_result($result, $j, 'group_project_id'), db_result($result, $j, $i));
527 $linkUrl = db_result($result, $j, $i);
530 $linkUrl = db_result($result, $j, $i);
532 echo '<td>'.$linkUrl.'</td>';
537 echo $HTML->listTableBottom();
544 * validate_email - Validate an email address
546 * @param string $address The address string to validate
547 * @return bool true on success/false on error
550 function validate_email($address) {
551 if (filter_var($address, FILTER_VALIDATE_EMAIL)) {
558 * validate_emails - Validate a list of e-mail addresses
560 * @param string $addresses E-mail list
561 * @param string $separator Separator
562 * @return array Array of invalid e-mail addresses (if empty, all addresses are OK)
564 function validate_emails($addresses, $separator = ',') {
565 if (strlen($addresses) == 0) {
568 $emails = explode($separator, $addresses);
571 if (is_array($emails)) {
572 foreach ($emails as $email) {
573 $email = trim($email); // This is done so we can validate lists like "a@b.com, c@d.com"
574 if (!validate_email($email)) {
583 * util_is_valid_filename - Verifies whether a file has a valid filename
585 * @param string $file The file to verify
586 * @return bool true on success/false on error
589 function util_is_valid_filename($file) {
591 $invalidchars = preg_replace("/[-A-Z0-9+_\. ~]/i", "", $file);
593 if (!empty($invalidchars)) {
596 if (strstr($file, '..')) {
605 * util_is_valid_repository_name - Verifies whether a repository name is valid
607 * @param string $file name to verify
608 * @return bool true on success/false on error
611 function util_is_valid_repository_name ($file) {
613 $invalidchars = preg_replace("/[-A-Z0-9+_\.]/i","",$file);
615 if (!empty($invalidchars)) {
618 if (strstr($file,'..')) {
625 * valid_hostname - Validates a hostname string to make sure it doesn't contain invalid characters
627 * @param string $hostname The optional hostname string
628 * @return bool true on success/false on failure
631 function valid_hostname($hostname = "xyz") {
634 $invalidchars = preg_replace("/[-A-Z0-9\.]/i", "", $hostname);
636 if (!empty($invalidchars)) {
640 //double dot, starts with a . or -
641 if (preg_match("/\.\./", $hostname) || preg_match("/^\./", $hostname) || preg_match("/^\-/", $hostname)) {
645 $multipoint = explode(".", $hostname);
647 if (!(is_array($multipoint)) || ((count($multipoint) - 1) < 1)) {
657 * human_readable_bytes - Translates an integer representing bytes to a human-readable format.
659 * Format file size in a human-readable way
660 * such as "xx Megabytes" or "xx Mo"
662 * @author Andrea Paleni <andreaSPAMLESS_AT_SPAMLESScriticalbit.com>
665 * @param int $bytes is the size
666 * @param bool $base10 enable base 10 representation, otherwise default base 2 is used
667 * @param int $round number of fractional digits
668 * @param array $labels strings associated to each 2^10 or 10^3(base10==true) multiple of base units
671 function human_readable_bytes($bytes, $base10 = false, $round = 0, $labels = array()) {
676 return "-".human_readable_bytes(-$bytes, $base10, $round);
679 $labels = array(_('bytes'), _('kB'), _('MB'), _('GB'), _('TB'));
683 $labels = array(_('bytes'), _('KiB'), _('MiB'), _('GiB'), _('TiB'));
687 $log = (int)(log10($bytes)/log10($base));
689 foreach ($labels as $p => $lab) {
694 if ($lab != _("bytes") and $lab != _("kB") and $lab != _("KiB")) {
697 $text = round($bytes/pow($base, $pow), $round)." ".$lab;
704 * ls - lists a specified directory and returns an array of files
705 * @param string $dir the path of the directory to list
706 * @param bool $filter whether to filter out directories and illegal filenames
707 * @param string|bool $regex filter filename based on this regex
708 * @return array array of file names.
710 function &ls($dir, $filter = false, $regex = false) {
713 if (is_dir($dir) && ($h = opendir($dir))) {
714 while (($f = readdir($h)) !== false) {
718 if (!util_is_valid_filename($f) ||
719 !is_file($dir."/".$f)
723 if ($regex !== false) {
724 if (!preg_match($regex, $f)) {
736 * readfile_chunked - replacement for readfile
738 * @param string $filename The file path
739 * @param bool $returnBytes Whether to return bytes served or just a bool
742 function readfile_chunked($filename, $returnBytes = true) {
743 $chunksize = 1*(1024*1024); // 1MB chunks
747 $handle = fopen($filename, 'rb');
748 if ($handle === false) {
753 while (!feof($handle)) {
754 $buffer = fread($handle, $chunksize);
759 $byteCounter += strlen($buffer);
763 $status = fclose($handle);
764 if ($returnBytes && $status) {
765 return $byteCounter; // return num. bytes delivered like readfile() does.
771 * util_is_root_dir - Checks if a directory points to the root dir
773 * @param string $dir Directory
776 function util_is_root_dir($dir) {
777 return !preg_match('/[^\\/]/', $dir);
781 * util_is_dot_or_dotdot - Checks if a directory points to . or ..
783 * @param string $dir Directory
786 function util_is_dot_or_dotdot($dir) {
787 return preg_match('/^\.\.?$/', trim($dir, '/'));
791 * util_containts_dot_or_dotdot - Checks if a directory containts . or ..
793 * @param string $dir Directory
796 function util_containts_dot_or_dotdot($dir) {
797 foreach (explode('/', $dir) as $sub_dir) {
798 if (util_is_dot_or_dotdot($sub_dir)) {
806 * util_secure_filename - Returns a secured file name
808 * @param string $file Filename
809 * @return string Filename
811 function util_secure_filename($file) {
812 $f = preg_replace("/[^-A-Z0-9_\.]/i", '', $file);
813 if (util_containts_dot_or_dotdot($f)) {
814 $f = preg_replace("/\./", '_', $f);
823 * util_strip_accents - Remove accents from given text.
825 * @param string $text Text
828 function util_strip_accents($text) {
829 $find = utf8_decode($text);
831 utf8_decode('àáâãäçèéêëìíîïñòóôõöùúûüýÿÀÁÂÃÄÇÈÉÊËÌÍÎÏÑÒÓÔÕÖÙÚÛÜÝ'),
832 'aaaaaceeeeiiiinooooouuuuyyAAAAACEEEEIIIINOOOOOUUUUY');
833 return utf8_encode($find);
837 * normalized_urlprefix - Constructs the forge's URL prefix out of forge_get_config('url_prefix')
841 function normalized_urlprefix() {
842 $prefix = forge_get_config('url_prefix');
843 $prefix = preg_replace("/^\//", "", $prefix);
844 $prefix = preg_replace("/\/$/", "", $prefix);
845 $prefix = "/$prefix/";
846 if ($prefix == '//') {
853 * util_url_prefix - Return URL prefix (http:// or https://)
855 * @param string $prefix (optional) : 'http' or 'https' to force it
856 * @return string URL prefix
858 function util_url_prefix($prefix = '') {
859 if ($prefix == 'http' || $prefix == 'https' ) {
860 return $prefix . '://';
862 if (forge_get_config('use_ssl')) {
871 * util_make_base_url - Construct the base URL http[s]://forge_name[:port]
873 * @param string $prefix (optional) : 'http' or 'https' to force it
874 * @return string base URL
876 function util_make_base_url($prefix = '') {
877 $url = util_url_prefix($prefix);
878 $url .= forge_get_config('web_host');
879 if (forge_get_config('use_ssl')) {
880 if (forge_get_config('https_port') && (forge_get_config('https_port') != 443)) {
881 $url .= ":".forge_get_config('https_port');
884 if (forge_get_config('http_port') && (forge_get_config('http_port') != 80)) {
885 $url .= ":".forge_get_config('http_port');
892 * util_make_url - Construct full URL from a relative path
894 * @param string $path (optional)
895 * @param string $prefix (optional) : 'http' or 'https' to force it
898 function util_make_url($path = '', $prefix = '') {
899 return util_make_base_url($prefix).util_make_uri($path);
903 * util_find_relative_referer - Find the relative URL from full URL, removing http[s]://forge_name[:port]
905 * @param string $url URL
908 function util_find_relative_referer($url) {
909 $relative_url = str_replace(util_make_base_url().normalized_urlprefix(), '', $url);
910 return $relative_url;
914 * util_make_uri - Construct proper (relative) URI (prepending prefix)
916 * @param string $path
919 function util_make_uri($path) {
920 $path = preg_replace('/^\//', '', $path);
921 $uri = normalized_urlprefix();
927 * util_make_link - Construct proper URL/URI from path & text
929 * @param string $path
930 * @param string $text
931 * @param array|bool $extra_params
932 * @param bool $absolute
935 function util_make_link($path, $text, $extra_params = false, $absolute = false) {
936 global $use_tooltips;
938 if (is_array($extra_params)) {
939 foreach ($extra_params as $key => $value) {
940 if ($key != 'title') {
941 $attrs[$key] = $value;
943 if ($key == 'title' && $use_tooltips) {
944 $attrs[$key] = $value;
949 $attrs['href'] = $path;
951 $attrs['href'] = util_make_uri($path);
953 return html_e('a', $attrs, $text, true, false);
957 * util_make_link_u - Create an HTML link to a user's profile page
959 * @param string $username
960 * @param int $user_id
961 * @param string $text
964 function util_make_link_u($username, $user_id, $text) {
965 return util_make_link(util_make_url_u($username, $user_id), $text, false, true);
969 * util_display_user - Display username with link to a user's profile page
970 * and icon face if possible.
972 * @param string $username
973 * @param int $user_id
974 * @param string $text
975 * @param string $size
978 function util_display_user($username, $user_id = 0, $text = '', $size = 'xs') {
979 $user = user_get_object_by_name($username);
980 if (!$user || !is_object($user) || $user->isError() || !$user->isActive()) {
983 if (forge_get_config('restrict_users_visibility')) {
984 if (!session_loggedin()) {
988 $u2gl = $user->getGroupIds();
990 foreach ($u2gl as $u2g) {
991 if (forge_check_perm('project_read', $u2g)) {
996 if ($seen == false) {
1001 // Invoke user_link_with_tooltip plugin
1002 $hook_params = array('resource_type' => 'user', 'username' => $username, 'user_id' => $user_id, 'size' => $size, 'link_text' => $text, 'user_link' => '');
1003 plugin_hook_by_reference('user_link_with_tooltip', $hook_params);
1004 if ($hook_params['user_link'] != '') {
1005 return html_e('div', array('class' => 'box'), $hook_params['user_link']);
1008 // If no plugin replaced it, then back to default standard link
1010 // Invoke user_logo plugin (see gravatar plugin for instance)
1011 $params = array('user_id' => $user_id, 'size' => $size, 'content' => '');
1012 plugin_hook_by_reference('user_logo', $params);
1014 $url = util_make_link_u($username, $user_id, $text);
1015 if ($params['content']) {
1016 return html_e('div', array('class' => 'box'), $params['content'].' '.$url);
1022 * util_make_url_u - Create URL for user's profile page
1024 * @param string $username
1025 * @param int $user_id
1026 * @return string URL
1028 function util_make_url_u($username, $user_id) {
1029 return util_make_uri('/users/'.$username.'/');
1033 * util_make_link_g - Create a HTML link to a project's page
1035 * @param string $group_name
1036 * @param int $group_id
1037 * @param string $text
1040 function util_make_link_g($group_name, $group_id, $text) {
1041 $hook_params = array();
1042 $hook_params['resource_type'] = 'group';
1043 $hook_params['group_name'] = $group_name;
1044 $hook_params['group_id'] = $group_id;
1045 $hook_params['link_text'] = $text;
1046 $hook_params['group_link'] = '';
1047 plugin_hook_by_reference('project_link_with_tooltip', $hook_params);
1048 if ($hook_params['group_link'] != '') {
1049 return $hook_params['group_link'];
1052 return html_e('a', array('href' => util_make_url_g($group_name, $group_id)), $text, true);
1056 * util_make_url_g - Create URL for a project's page
1058 * @param string $group_name
1059 * @param int $group_id
1062 function util_make_url_g($group_name, $group_id) {
1063 return util_make_uri('/projects/'.$group_name.'/');
1066 function util_ensure_value_in_set($value, $set) {
1067 if (in_array($value, $set)) {
1075 * check_email_available - ???
1077 * @param Group $group
1078 * @param string $email
1079 * @param string $response
1082 function check_email_available($group, $email, &$response) {
1083 // Check if a mailing list with same name already exists
1084 if ($group->usesMail()) {
1085 $mlFactory = new MailingListFactory($group);
1086 if (!$mlFactory || !is_object($mlFactory) || $mlFactory->isError()) {
1087 $response .= $mlFactory->getErrorMessage();
1090 $mlArray = $mlFactory->getMailingLists();
1091 if ($mlFactory->isError()) {
1092 $response .= $mlFactory->getErrorMessage();
1095 for ($j = 0; $j < count($mlArray); $j++) {
1096 $currentList =& $mlArray[$j];
1097 if ($email == $currentList->getName()) {
1098 $response .= _('Error: a mailing list with the same email address already exists.');
1104 // Check if a forum with same name already exists
1105 if ($group->usesForum()) {
1106 $ff = new ForumFactory($group);
1107 if (!$ff || !is_object($ff) || $ff->isError()) {
1108 $response .= $ff->getErrorMessage();
1111 $farr = $ff->getForums();
1112 $prefix = $group->getUnixName().'-';
1113 for ($j = 0; $j < count($farr); $j++) {
1114 if (is_object($farr[$j])) {
1115 if ($email == $prefix.$farr[$j]->getName()) {
1116 $response .= _('Error: a forum with the same email address already exists.');
1123 // Email is available
1128 * Adds the Javascript file to the list to be used
1131 function use_javascript($js) {
1132 return $GLOBALS['HTML']->addJavascript($js);
1135 function use_stylesheet($css, $media = '') {
1136 return $GLOBALS['HTML']->addStylesheet($css, $media);
1139 /* returns an integer from http://forge/foo/bar.php/123 or false */
1140 function util_path_info_last_numeric_component() {
1141 if (!isset($_SERVER['PATH_INFO'])) {
1145 foreach (str_split($_SERVER['PATH_INFO']) as $x) {
1149 } elseif ($ok == false) {
1150 ; /* need reset using slash */
1151 } elseif ((ord($x) >= 48) && (ord($x) <= 57)) {
1152 $rv = $rv * 10 + ord($x) - 48;
1163 function get_cvs_binary_version() {
1164 $string = `cvs --version 2>/dev/null | grep ^Concurrent.Versions.System.*client/server`;
1165 if (preg_match('/^Concurrent Versions System .CVS. 1.11.[0-9]*/', $string)) {
1167 } elseif (preg_match('/^Concurrent Versions System .CVS. 1.12.[0-9]*/', $string)) {
1174 /* get a backtrace as string */
1175 function debug_string_backtrace() {
1177 debug_print_backtrace();
1178 $trace = ob_get_contents();
1181 // Remove first item from backtrace as it's this function
1182 // which is redundant.
1183 $trace = preg_replace('/^#0\s+'.__FUNCTION__."[^\n]*\n/", '', $trace, 1);
1185 // Renumber backtrace items.
1186 $trace = preg_replace_callback('/^#(\d+)/m', function($m) { return '#' . (ltrim($m[0], '#') - 1); }, $trace);
1191 function util_ini_get_bytes($id) {
1192 $val = substr(trim(ini_get($id)), 0, -1);
1193 $last = strtolower($val[strlen($val)-1]);
1205 function util_get_maxuploadfilesize() {
1206 $postmax = util_ini_get_bytes('post_max_size');
1207 $maxfile = util_ini_get_bytes('upload_max_filesize');
1209 return min($postmax, $maxfile);
1212 function util_get_compressed_file_extension() {
1213 $m = forge_get_config('compression_method');
1214 if (preg_match('/^gzip\b/', $m)) {
1216 } elseif (preg_match('/^bzip2\b/', $m)) {
1218 } elseif (preg_match('/^lzma\b/', $m)) {
1220 } elseif (preg_match('/^xz\b/', $m)) {
1222 } elseif (preg_match('/^cat\b/', $m)) {
1225 return '.compressed';
1230 * return $1 if $1 is set, ${2:-false} otherwise
1232 * Shortcomings: may create $$val = NULL in the
1233 * current namespace; see the (rejected – but
1234 * then, with PHP, you know where you stand…)
1235 * https://wiki.php.net/rfc/ifsetor#userland_2
1236 * proposal for details and a (rejected) fix.
1238 * Do not use this function if $val is “magic”,
1239 * for example, an overloaded \ArrayAccess.
1242 * @param bool $default
1245 function util_ifsetor(&$val, $default = false) {
1246 return (isset($val) ? $val : $default);
1249 function util_randbytes($num = 6) {
1252 // Let's try /dev/urandom first
1253 $f = @fopen("/dev/urandom", "rb");
1255 $b .= @fread($f, $num);
1259 // Hm. No /dev/urandom? Try /dev/random.
1260 if (strlen($b) < $num) {
1261 $f = @fopen("/dev/random", "rb");
1263 $b .= @fread($f, $num);
1268 // Still no luck? Fall back to PHP's built-in PRNG
1269 while (strlen($b) < $num) {
1270 $b .= uniqid(mt_rand(), true);
1273 $b = substr($b, 0, $num);
1277 /* maximum: 2^31 - 1 due to PHP weakness */
1278 function util_randnum($min = 0, $max = 32767) {
1279 $ta = unpack("L", util_randbytes(4));
1280 $n = $ta[1] & 0x7FFFFFFF;
1281 $v = $n % (1 + $max - $min);
1285 /* convert '\n' to <br /> or </p><p> */
1286 function util_pwrap($encoded_string) {
1287 return str_replace("<p></p>", "",
1288 str_replace("<br /></p>", "</p>",
1289 str_replace("<p><br />", "<p>",
1290 "<p>".str_replace("<br /><br />", "</p><p>",
1291 implode("<br />", explode("\n",
1292 $encoded_string)))."</p>")));
1295 /* takes a string and returns it HTML encoded, URIs made to hrefs */
1296 function util_uri_grabber($unencoded_string, $tryaidtid = false) {
1297 /* escape all ^A and ^B as ^BX^B and ^BY^B, respectively */
1298 $s = str_replace("\x01", "\x02X\x02", str_replace("\x02", "\x02Y\x02",
1299 $unencoded_string));
1300 /* replace all URIs with ^AURI^A */
1302 '|([a-zA-Z][a-zA-Z0-9+.-]*:[#0-9a-zA-Z;/?:@&=+$,_.!~*\'()%-]+)|',
1305 return htmlentities($unencoded_string, ENT_QUOTES, "UTF-8");
1307 /* encode the string */
1308 $s = htmlentities($s, ENT_QUOTES, "UTF-8");
1309 /* convert 「^Afoo^A」 to 「<a href="foo">foo</a>」 */
1310 $s = preg_replace('|\x01([^\x01]+)\x01|',
1311 '<a href="$1">$1</a>', $s);
1313 return htmlentities($unencoded_string, ENT_QUOTES, "UTF-8");
1315 // /* convert [#123] to links if found */
1317 // $s = util_tasktracker_links($s);
1318 /* convert ^BX^B and ^BY^B back to ^A and ^B, respectively */
1319 $s = str_replace("\x02Y\x02", "\x02", str_replace("\x02X\x02", "\x01",
1321 /* return the final result */
1325 function util_html_encode($s) {
1326 return htmlspecialchars($s, ENT_QUOTES, "UTF-8");
1329 /* secure a (possibly already HTML encoded) string */
1330 function util_html_secure($s) {
1331 return util_html_encode(util_unconvert_htmlspecialchars($s));
1334 /* return integral value (ℕ₀) of passed string if it matches, or false */
1335 function util_nat0(&$s) {
1337 /* unset variable */
1341 if (count($s) == 1) {
1342 /* one-element array */
1343 return util_nat0($s[0]);
1345 /* not one element, or element not at [0] */
1348 if (!is_numeric($s)) {
1354 /* number element of ℕ₀ */
1355 $text = (string)$num;
1357 /* number matches its textual representation */
1360 /* doesn't match, like 0123 or 1.2 or " 1" */
1367 * util_negociate_alternate_content_types() - Manage content-type negociation based on 'script_accepted_types' hooks
1368 * @param string $script
1369 * @param string $default_content_type
1370 * @param string|bool $forced_content_type
1373 function util_negociate_alternate_content_types($script, $default_content_type, $forced_content_type=false) {
1375 $content_type = $default_content_type;
1377 // we can force the content-type to be returned automatically if necessary
1378 if ($forced_content_type) {
1379 // TODO ideally, in this case we could try and apply the negociation to see if it matches
1380 // one provided by the hooks, but negotiateMimeType() doesn't allow this so for the moment,
1381 // we just force it whatever the hooks support
1382 $content_type = $forced_content_type;
1384 // Invoke plugins' hooks 'script_accepted_types' to discover which alternate content types they would accept for /users/...
1385 $hook_params = array();
1386 $hook_params['script'] = $script;
1387 $hook_params['accepted_types'] = array();
1389 plugin_hook_by_reference('script_accepted_types', $hook_params);
1391 if (count($hook_params['accepted_types'])) {
1392 // By default, text/html is accepted
1393 $accepted_types = array($default_content_type);
1394 $new_accepted_types = $hook_params['accepted_types'];
1395 $accepted_types = array_merge($accepted_types, $new_accepted_types);
1397 // PEAR::HTTP (for negotiateMimeType())
1398 require_once 'HTTP.php';
1400 // negociate accepted content-type depending on the preferred ones declared by client
1402 $content_type = $http->negotiateMimeType($accepted_types, false);
1405 return $content_type;
1409 * util_gethref() - Construct a hypertext reference
1411 * @param string $baseurl
1412 * (optional) base URL (absolute or relative);
1413 * urlencoded, but not htmlencoded
1414 * (default (falsy): PHP_SELF)
1415 * @param array $args
1416 * (optional) associative array of unencoded query parameters;
1417 * false values are ignored
1418 * @param bool $ashtml
1419 * (optional) htmlencode the result?
1421 * @param string $sep
1422 * (optional) argument separator ('&' or ';')
1424 * @return string URL, possibly htmlencoded
1426 function util_gethref($baseurl = '', $args = array(), $ashtml = true, $sep = '&') {
1427 $rv = $baseurl? $baseurl : getStringFromServer('PHP_SELF');
1429 foreach ($args as $k => $v) {
1433 $rv .= $pfx.urlencode($k).'='.urlencode($v);
1436 return ($ashtml? util_html_encode($rv) : $rv);
1440 * util_sanitise_multiline_submission() – Convert text to ASCII CR-LF
1442 * @param string $text
1443 * input string to sanitise
1445 * sanitised string: CR, LF or CR-LF converted to CR-LF
1447 function util_sanitise_multiline_submission($text) {
1448 /* convert all CR-LF into LF */
1449 $text = preg_replace("/\015+\012+/m", "\012", $text);
1450 /* convert all CR or LF into CR-LF */
1451 $text = preg_replace("/[\012\015]/m", "\015\012", $text);
1456 function util_is_html($string) {
1457 return (strip_tags(util_unconvert_htmlspecialchars($string)) != $string);
1460 function util_init_messages() {
1461 global $feedback, $warning_msg, $error_msg;
1463 if (PHP_SAPI == 'cli') {
1464 $feedback = $warning_msg = $error_msg = '';
1466 $feedback = getStringFromCookie('feedback', '');
1468 setcookie('feedback', '', time()-3600, '/');
1471 $warning_msg = getStringFromCookie('warning_msg', '');
1473 setcookie('warning_msg', '', time()-3600, '/');
1476 $error_msg = getStringFromCookie('error_msg', '');
1478 setcookie('error_msg', '', time()-3600, '/');
1483 function util_save_messages() {
1484 global $feedback, $warning_msg, $error_msg;
1486 setcookie('feedback', $feedback, time() + 10, '/');
1487 setcookie('warning_msg', $warning_msg, time() + 10, '/');
1488 setcookie('error_msg', $error_msg, time() + 10, '/');
1492 * util_create_file_with_contents() — Securely create (or replace) a file with given contents
1494 * @param string $path Path of the file to be created
1495 * @param string $contents Contents of the file
1497 * @return bool false on error
1499 function util_create_file_with_contents($path, $contents) {
1500 if (file_exists($path) && !unlink($path)) {
1503 $handle = fopen($path, "x+");
1504 if ($handle == false) {
1507 fwrite($handle, $contents);
1513 * Create a directory in the system temp directory with a hard-to-predict name.
1514 * Does not have the guarantees of the actual BSD libc function or Python tempfile function.
1515 * @param string $suffix Append to the new directory's name
1516 * @param string $prefix Prepend to the new directory's name
1517 * @return string The path of the new directory.
1519 * Mostly taken from https://gist.github.com/1407245 as a "temporary"
1520 * workaround to https://bugs.php.net/bug.php?id=49211
1522 function util_mkdtemp($suffix = '', $prefix = 'tmp') {
1523 $tempdir = sys_get_temp_dir();
1524 for ($i=0; $i<5; $i++) {
1525 $id = strtr(base64_encode(util_randbytes(6)), '+/', '-_');
1526 $path = "{$tempdir}/{$prefix}{$id}{$suffix}";
1527 if (mkdir($path, 0700)) {
1535 * Run a function with only the permissions of a given Unix user
1536 * Function can be an anonymous
1537 * Used to rely on posix_seteuid, but standard Bash reverts euid=uid,
1538 * cf. Debian patch "privmode.diff", so using fork&exec
1539 * Optional arguments in an array
1540 * @param string $username Unix user name
1541 * @param function $function function to run (possibly anonymous)
1542 * @param array $params parameters
1543 * @return bool true on success, false on error
1545 function util_sudo_effective_user($username, $function, $params=array()) {
1546 $userinfo = posix_getpwnam($username);
1547 if ($userinfo === false) {
1551 $pid = pcntl_fork();
1556 pcntl_waitpid($pid, $status);
1558 if (posix_setgid($userinfo['gid']) &&
1559 posix_initgroups($username, $userinfo['gid']) &&
1560 posix_setuid($userinfo['uid'])) {
1561 putenv('HOME='.$userinfo['dir']);
1562 call_user_func($function, $params);
1564 //exit(1); // too nice, PHP gracefully quits and closes DB connection
1565 posix_kill(posix_getpid(), 9);
1570 function getselfhref($p = array(), $return_encoded = true) {
1571 global $group_id, $atid, $aid, $is_add;
1572 $p['group_id'] = $group_id;
1577 $p['artifact_id'] = $aid;
1579 return util_gethref(false, $p, $return_encoded);
1583 * getThemeIdFromName()
1585 * @param string $dirname the dirname of the theme
1586 * @return int the theme id
1588 function getThemeIdFromName($dirname) {
1589 $res = db_query_params ('SELECT theme_id FROM themes WHERE dirname=$1',
1591 return db_result($res,0,'theme_id');
1595 * utils_headers_download() - Generate attachment download headers, with security checks around the MIME type
1597 * @param string $filename
1598 * @param string $mimetype
1601 function utils_headers_download($filename, $mimetype, $size) {
1602 /* SECURITY: do not serve content with JavaScript execution (and e.g. cookie theft) */
1603 /* Namely do NOT include: text/html, image/svg+xml, application/pdf... */
1604 /* https://grepular.com/Scalable_Vector_Graphics_and_XSS */
1605 /* https://lists.wikimedia.org/pipermail/mediawiki-announce/2015-March/000175.html */
1606 /* https://www.owasp.org/images/a/ac/PDF_XSS_vulnerability.pdf */
1607 /* https://groups.google.com/forum/#!topic/mozilla.dev.pdf-js/Fyl5RnaUWVc */
1608 /* (PDF theoretically supports JS, not sure how pdf.js deals with that) */
1609 $authorized_inline = ',^(text/plain|image/png|image/jpe?g|image/gif)$,';
1610 /* Disarm XSS-able text/html, and inline common text files (*.c, *.pl...) */
1611 $force_text_plain = ',^(text/html|text/.*|application/x-perl|application/x-ruby)$,';
1613 if (preg_match($force_text_plain, $mimetype)) {
1614 $mimetype = 'text/plain';
1616 if (preg_match($authorized_inline, $mimetype)) {
1617 header('Content-Disposition: inline; filename="' . str_replace('"', '', $filename) . '"');
1618 header('Content-Type: '. $mimetype);
1620 header('Content-Disposition: attachment; filename="' . str_replace('"', '', $filename) . '"');
1621 header('Content-Type: '. $mimetype);
1623 header('Content-Length: ' . $size);
1625 /* Also, make sure browsers such as IE8 don't interpret a non text/html attachment as HTML... */
1626 /* https://blogs.msdn.com/b/ie/archive/2008/09/02/ie8-security-part-vi-beta-2-update.aspx?Redirected=true */
1627 /* IE6 ignores this, but IE6 users have higher security concerns than this.. */
1628 header('X-Content-Type-Options: nosniff');
1631 function compareObjectName ($a, $b) {
1632 return strcoll($a->getName(),$b->getName()) ;
1636 * compute the differences between two arrays //TODO: looks like array_udiff
1637 * @param array $tab1
1638 * @param array $tab2
1641 function utils_array_diff_names($tab1, $tab2) {
1643 foreach($tab1 as $e1) {
1646 foreach($tab2 as $e2) {
1647 $found = !count(array_diff($e1, $e2));
1658 // c-file-style: "bsd"