<?php
//============================================================+
// File name   : tce_functions_general.php
// Begin       : 2001-09-08
// Last Update : 2013-07-02
//
// Description : General functions.
//
// Author: Nicola Asuni
//
// (c) Copyright:
//               Nicola Asuni
//               Tecnick.com LTD
//               www.tecnick.com
//               info@tecnick.com
//
// License:
//    Copyright (C) 2004-2013 Nicola Asuni - Tecnick.com LTD
//
//    This program is free software: you can redistribute it and/or modify
//    it under the terms of the GNU Affero General Public License as
//    published by the Free Software Foundation, either version 3 of the
//    License, or (at your option) any later version.
//
//    This program is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//    GNU Affero General Public License for more details.
//
//    You should have received a copy of the GNU Affero General Public License
//    along with this program.  If not, see <http://www.gnu.org/licenses/>.
//
//    Additionally, you can't remove, move or hide the original TCExam logo,
//    copyrights statements and links to Tecnick.com and TCExam websites.
//
//    See LICENSE.TXT file for more information.
//============================================================+

/**
 * @file
 * General functions.
 * @package com.tecnick.tcexam.shared
 * @author Nicola Asuni
 * @since 2001-09-08
 */

/**
 * Count rows of the given table.
 * @param $dbtable (string) database table name
 * @param $where (string) optional where SQL clause (including the WHERE keyword).
 * @return number of rows
 */
function F_count_rows($dbtable, $where='') {
	global $db;
	require_once('../config/tce_config.php');
	$numofrows = 0;
	$sql = 'SELECT COUNT(*) AS numrows FROM '.$dbtable.' '.$where.'';
	if ($r = F_db_query($sql, $db)) {
		if ($m = F_db_fetch_array($r)) {
			$numofrows = $m['numrows'];
		}
	} else {
		F_display_db_error();
	}
	return($numofrows);
}

/**
 * Prepare field value for SQL query.<br>
 * Returns the quoted string if not empty, NULL otherwise.
 * @param $str (string) string to check.
 * @return string $str quoted if not empty, NULL otherwise
 */
function F_empty_to_null($str) {
	require_once('../../shared/code/tce_db_dal.php');
	if (strlen($str) > 0) {
		return '\''.F_escape_sql($str).'\'';
	}
	return 'NULL';
}

/**
 * Prepare field value for SQL query.<br>
 * Returns the num if different from zero, NULL otherwise.
 * @param $num (string) string to check.
 * @return string $num if != 0, NULL otherwise
 */
function F_zero_to_null($num) {
	require_once('../../shared/code/tce_db_dal.php');
	if ($num == 0) {
		return 'NULL';
	}
	return F_escape_sql($num);
}

/**
 * Returns boolean value from string or integer.<br>
 * This function is needed to get the right boolean value from boolean field returned by PostgreSQL query.
 * @param $str (string) string to check.
 * @return boolean value.
 */
function F_getBoolean($str) {
	if (is_bool($str)) {
		return $str;
	}
	if (is_string($str) AND ((strncasecmp($str, 't', 1) == 0) OR (strncasecmp($str, '1', 1) == 0))) {
		return true;
	}
	if (is_int($str) AND ($str == 1)) {
		return true;
	}
	return false;
}

/**
 * Check if specified fields are unique on table.
 * @param $table (string) table name
 * @param $where (string) SQL where clause
 * @param $fieldname (mixed) name of table column to check
 * @param $fieldid (mixed) ID of table row to check
 * @return bool true if unique, false otherwise
 */
function F_check_unique($table, $where, $fieldname=FALSE, $fieldid=FALSE) {
	require_once('../config/tce_config.php');
	global $l, $db;
	$sqlc = 'SELECT * FROM '.$table.' WHERE '.$where.' LIMIT 1';
	if ($rc = F_db_query($sqlc, $db)) {
		if (($fieldname === FALSE) AND ($fieldid === FALSE) AND (F_count_rows($table, 'WHERE '.$where) > 0)) {
			return FALSE;
		}
		if ($mc = F_db_fetch_array($rc)) {
			if ($mc[$fieldname] == $fieldid) {
				return TRUE; // the values are unchanged
			}
		} else {
			// the new values are not yet present on table
			return TRUE;
		}
	} else {
		F_display_db_error();
	}
	// another table row contains the same values
	return FALSE;
}

/**
 * Reverse function for htmlentities.
 * @param $text_to_convert (string) input string to convert
 * @param $preserve_tagsign (boolean) if true preserve <> symbols, default=FALSE
 * @return converted string
 */
function unhtmlentities($text_to_convert, $preserve_tagsign=FALSE) {
	$trans_tbl = get_html_translation_table(HTML_ENTITIES);
	$trans_tbl = array_flip($trans_tbl);
	if ($preserve_tagsign) {
		$trans_tbl['&lt;'] = '&lt;'; //do not convert '<' equivalent
		$trans_tbl['&gt;'] = '&gt;'; //do not convert '>' equivalent
	}
	$return_text = strtr($text_to_convert, $trans_tbl);
	$return_text = preg_replace('/\&\#([0-9]+)\;/me', "chr('\\1')", $return_text);
	return $return_text;
}

/**
 * Remove the following characters:
 * <ul>
 * <li>"\t" (ASCII 9 (0x09)), a tab.</li>
 * <li>"\n" (ASCII 10 (0x0A)), a new line (line feed)</li>
 * <li>"\r" (ASCII 13 (0x0D)), a carriage return</li>
 * <li>"\0" (ASCII 0 (0x00)), the NUL-byte</li>
 * <li>"\x0B" (ASCII 11 (0x0B)), a vertical tab</li>
 * </ul>
 * @param $string (string) input string to convert
 * @param $dquotes (boolean) If true add slash in fron of double quotes;
 * @return converted string
 */
function F_compact_string($string, $dquotes=false) {
	$repTable = array("\t" => ' ', "\n" => ' ', "\r" => ' ', "\0" => ' ', "\x0B" => ' ');
	if ($dquotes) {
		$repTable['"'] = '&quot;';
	}
	return strtr($string, $repTable);
}

/**
 * Replace angular parenthesis with html equivalents (html entities).
 * @param $str (string) input string to convert
 * @return converted string
 */
function F_replace_angulars($str) {
	$replaceTable = array('<' => '&lt;', '>' => '&gt;');
	return strtr($str, $replaceTable);
}

/**
 * Performs a multi-byte safe substr() operation based on number of characters.
 * @param $str (string) input string
 * @param $start (int) substring start index
 * @param $length (int) substring max lenght
 * @return substring
 */
function F_substr_utf8($str, $start=0, $length) {
	$str .= ''; // force $str to be a string
	$bytelen = strlen($str);
	$i = 0;
	$j = 0;
	$str_start = 0;
	$str_end = $bytelen;
	while ($i < $bytelen) {
		if ($j == $start) {
			$str_start = $i;
		} elseif ($j == $length) {
			$str_end = $i;
			break;
		}
		$char = ord($str{$i}); // get one string character at time
		if ($char <= 0x7F) {
			$i += 1;
		} elseif (($char >> 0x05) == 0x06) { // 2 bytes character (0x06 = 110 BIN)
			$i += 2;
		} elseif (($char >> 0x04) == 0x0E) { // 3 bytes character (0x0E = 1110 BIN)
			$i += 3;
		} elseif (($char >> 0x03) == 0x1E) { // 4 bytes character (0x1E = 11110 BIN)
			$i += 4;
		} else {
			$i += 1;
		}
		++$j;
	}
	$str = substr($str, $str_start, $str_end);
	return $str;
}

/**
 * Escape some special characters (&lt; &gt; &amp;).
 * @param $str (string) input string to convert
 * @return converted string
 */
function F_text_to_xml($str) {
	$replaceTable = array("\0" => '', '&' => '&amp;', '<' => '&lt;', '>' => '&gt;');
	return strtr($str, $replaceTable);
}

/**
 * Unescape some special characters (&lt; &gt; &amp;).
 * @param $str (string) input string to convert
 * @return converted string
 */
function F_xml_to_text($str) {
	$replaceTable = array('&amp;' => '&', '&lt;' => '<', '&gt;' => '>');
	return strtr($str, $replaceTable);
}

/**
 * Escape some special characters for TSV output.
 * @param $str (string) input string to convert
 * @return converted string
 */
function F_text_to_tsv($str) {
	$replaceTable = array("\0" => '', "\t" => '\t', "\n" => '\n', "\r" => '\r');
	return strtr($str, $replaceTable);
}

/**
 * Unescape some special characters from TSV format.
 * @param $str (string) input string to convert
 * @return converted string
 */
function F_tsv_to_text($str) {
	$replaceTable = array('\t' => "\t", '\n' => "\n", '\r' => "\r");
	return strtr($str, $replaceTable);
}

/**
 * Return a string containing an HTML acronym for required/not required fields.
 * @param $mode (int) field mode: 1=not required; 2=required.
 * @return html string
 */
function showRequiredField($mode=1) {
	global $l;
	$str = '';
	if ($mode == 2) {
		$str = ' <acronym class="requiredonbox" title="'.$l['w_required'].'">+</acronym>';
	} else {
		$str = ' <acronym class="requiredoffbox" title="'.$l['w_not_required'].'">-</acronym>';
	}
	return $str;
}

/**
 * Strip whitespace (or other characters) from the beginning and end of an UTF-8 string and replace the "\xA0" with normal space.
 * @param $txt (string) The string that will be trimmed.
 * @return string The trimmed string.
 */
function utrim($txt) {
	$txt = preg_replace('/\xA0/u', ' ', $txt);
	$txt = preg_replace('/^([\s]+)/u', '', $txt);
	$txt = preg_replace('/([\s]+)$/u', '', $txt);
	return $txt;
}

/**
 * Convert all IP addresses to IPv6 expanded notation.
 * @param $ip (string) IP address to normalize.
 * @return string IPv6 address in expanded notation or false in case of invalid input.
 * @since 7.1.000 (2009-02-13)
 */
function getNormalizedIP($ip) {
	if (($ip == '0000:0000:0000:0000:0000:0000:0000:0001') OR ($ip == '::1')) {
		// fix localhost problem
		$ip = '127.0.0.1';
	}
	$ip = strtolower($ip);
	// remove unsupported parts
	if (($pos = strrpos($ip, '%')) !== false) {
		$ip = substr($ip, 0, $pos);
	}
	if (($pos = strrpos($ip, '/')) !== false) {
		$ip = substr($ip, 0, $pos);
	}
	$ip = preg_replace("/[^0-9a-f:\.]+/si", '', $ip);
	// check address type
	$is_ipv6 = (strpos($ip, ':') !== false);
	$is_ipv4 = (strpos($ip, '.') !== false);
	if ((!$is_ipv4) AND (!$is_ipv6)) {
		return false;
	}
	if ($is_ipv6 AND $is_ipv4) {
		// strip IPv4 compatibility notation from IPv6 address
		$ip = substr($ip, strrpos($ip, ':') + 1);
		$is_ipv6 = false;
	}
	if ($is_ipv4) {
		// convert IPv4 to IPv6
		$ip_parts = array_pad(explode('.', $ip), 4, 0);
		if (count($ip_parts) > 4) {
			return false;
		}
		for ($i = 0; $i < 4; ++$i) {
			if ($ip_parts[$i] > 255) {
				return false;
			}
		}
		$part7 = base_convert(($ip_parts[0] * 256) + $ip_parts[1], 10, 16);
		$part8 = base_convert(($ip_parts[2] * 256) + $ip_parts[3], 10, 16);
		$ip = '::ffff:'.$part7.':'.$part8;
	}
	// expand IPv6 notation
	if (strpos($ip, '::') !== false) {
		$ip = str_replace('::', str_repeat(':0000', (8 - substr_count($ip, ':'))).':', $ip);
	}
	if (strpos($ip, ':') === 0) {
		$ip = '0000'.$ip;
	}
	// normalize parts to 4 bytes
	$ip_parts = explode(':', $ip);
	foreach ($ip_parts as $key => $num) {
		$ip_parts[$key] = sprintf('%04s', $num);
	}
	$ip = implode(':', $ip_parts);
	return $ip;
}

/**
 * Converts a string containing an IP address into its integer value.
 * @param $ip (string) IP address to convert.
 * @return int IP address as integer number.
 * @since 7.1.000 (2009-02-13)
 */
function getIpAsInt($ip) {
	$ip = getNormalizedIP($ip);
	$ip = str_replace(':', '', $ip);
	return hexdec($ip);
}

/**
 * Converts a string containing an IP address into its integer value and return string representation.
 * @param $ip (string) IP address to convert.
 * @return int IP address as string.
 * @since 9.0.033 (2009-11-03)
 */
function getIpAsString($ip) {
	$ip = getIpAsInt($ip);
	return sprintf('%.0f', $ip);
}

/**
 * Format a percentage number.
 * @param $num (float) number to be formatted
 * @return formatted string
 */
function F_formatFloat($num) {
	return sprintf('%.03f', round($num, 3));
}

/**
 * Format a percentage number.
 * @param $num (float) Number to be formatted.
 * @param $ratio (boolean) Set to true if the number is a ratio between 0 and 1, false if is a percentage number between 0 an 100.
 * @return formatted string
 */
function F_formatPercentage($num, $ratio=true) {
	if ($ratio) {
		$num = (100 * $num);
	}
	return '('.str_replace(' ', '&nbsp;', sprintf('% 3d', round($num))).'%)';
}

/**
 * format a percentage number
 * @param $num (float) number to be formatted
 * @param $ratio (boolean) Set to true if the number is a ratio between 0 and 1, false if is a percentage number between 0 an 100.
 * @return string
 */
function F_formatPdfPercentage($num, $ratio=true) {
	if ($ratio) {
		$num = (100 * $num);
	}
	return '('.sprintf('% 3d', round($num)).'%)';
}


/**
 * format a percentage number for XML
 * @param $num (float) number to be formatted
 * @param $ratio (boolean) Set to true if the number is a ratio between 0 and 1, false if is a percentage number between 0 an 100.
 * @return string
 */
function F_formatXMLPercentage($num, $ratio=true) {
	if ($ratio) {
		$num = (100 * $num);
	}
	return sprintf('%3d', round($num));
}

/**
 * Returns the UTC time offset in seconds
 * @param $timezone (string) current user timezone
 * @return int UTC time offset in seconds
 */
function F_getUTCoffset($timezone) {
	$user_timezone = new DateTimeZone($timezone);
	$user_datetime = new DateTime('now', $user_timezone);
	return $user_timezone->getOffset($user_datetime);
}

/**
 * Returns the UTC time offset yo be used with CONVERT_TZ function
 * @param $timezone (string) current user timezone
 * @return string UTC time offset (+HH:mm)
 */
function F_db_getUTCoffset($timezone) {
	$time_offset = F_getUTCoffset($timezone);
	$sign = ($time_offset >= 0)?'+':'-';
	return $sign.gmdate('H:i', abs($time_offset));
}

/**
 * Get data array in XML format.
 * @param $data (array) Array of data (key => value).
 * @param $level (int) Indentation level.
 * @return string XML data
 */
function getDataXML($data, $level=1) {
	$xml = '';
	$tb = str_repeat("\t", $level);
	foreach ($data as $key => $value) {
		$key = strtolower($key);
		$key = preg_replace('/[^a-z0-9]+/', '_', $key);
		if (is_numeric($key[0]) OR ($key[0] == '_')) {
			$key = 'item'.$key;
		}
		$xml .= $tb.'<'.$key.'>';
		if (is_array($value)) {
			$xml .= "\n".getDataXML($value, ($level + 1));
		} else {
			$xml .= F_text_to_xml($value);
		}
		$xml .= '</'.$key.'>'."\n";
	}
	return $xml;
}

/**
 * Get data headers (keys) in TSV header (tab separated text values).
 * @param $data (array) Array of data (key => value).
 * @param $prefix (string) Prefix to add to keys.
 * @return string data
 */
function getDataTSVHeader($data, $prefix='') {
	$tsv = '';
	foreach ($data as $key => $value) {
		if (is_array($value)) {
			$tsv .= getDataTSVHeader($value, $prefix.$key.'_');
		} else {
			$tsv .= "\t".$prefix.$key;
		}
	}
	return $tsv;
}

/**
 * Get data in TSV format (tab separated text values).
 * @param $data (array) Array of data.
 * @return string XML data
 */
function getDataTSV($data) {
	$tsv = '';
	foreach ($data as $value) {
		if (is_array($value)) {
			$tsv .= getDataTSV($value);
		} else {
			$tsv .= "\t".F_text_to_tsv($value);
		}
	}
	return $tsv;
}

/**
 * Convert HTML code to TSV string.
 * @param $str (string) HTML string to convert.
 * @return string TSV
 */
function F_html_to_TSV($str) {
	$dollar_replacement = ":.dlr.:"; //string replacement for dollar symbol
	//tags conversion table
	$tags2textTable = array (
		"'<br[^>]*?>'i" => ' ',
		"'<table[^>]*?>'i" => "\n",
		"'</table>'i" => "\n",
		"'<tr[^>]*?>'i" => "\n",
		"'<th[^>]*?>'i" => "\t",
		"'<td[^>]*?>'i" => "\t",
		"'<h[0-9][^>]*?>'i" => "\n\n",
		"'</h[0-9]>'i" => "\n"
	);
	$str = str_replace('&nbsp;', ' ',  $str);
	$str = str_replace('&rarr;', '-',  $str);
	$str = str_replace('&darr;', '',  $str);
	$str = str_replace("\t", ' ',  $str);
	$str = preg_replace_callback('/colspan="([0-9]*)"/x', create_function('$matches', 'if ($matches[1] > 1) {return str_repeat("></td><td", ($matches[1] - 1));} return "";'), $str);
	$str = str_replace("\r\n", "\n",  $str);
	$str = str_replace("\$", $dollar_replacement,  $str); //replace special character
	//remove newlines
	$str = str_replace("\n", '',  $str);
	$str = preg_replace(array_keys($tags2textTable), array_values($tags2textTable), $str);
	$str = preg_replace("'<[^>]*?>'si", '', $str); //strip out remaining tags
	//remove some newlines in excess
	$str = preg_replace("'[ \t\f]+[\r\n]'si", "\n",  $str);
	$str = preg_replace("'[\r\n][\r\n]+'si", "\n\n",  $str);
	$str = unhtmlentities($str, FALSE);
	$str = str_replace($dollar_replacement, "\$",  $str); //restore special character
	$str = rtrim($str);
	$str = ltrim($str, " \r\n\0\x0B");
	$str = stripslashes($str);
	return $str;
}

/**
 * Display table header element with order link.
 * @param $order_field (string) name of table field
 * @param $orderdir (string) order direction
 * @param $title title (string) field of anchor link
 * @param $name column (string) name
 * @param $current_order_field (string) current order field name
 * @param $filter (string) additional parameters to pass on URL
 * @return table header element string
 */
function F_select_table_header_element($order_field, $orderdir, $title, $name, $current_order_field='', $filter='') {
	global $l;
	require_once('../config/tce_config.php');
	$ord = '';
	if ($order_field == $current_order_field) {
		if ($orderdir == 1) {
			$ord = ' <acronym title="'.$l['w_ascent'].'">&gt;</acronym>';
		} else {
			$ord = ' <acronym title="'.$l['w_descent'].'">&lt;</acronym>';
		}
	}
	$str = '<th><a href="'.$_SERVER['SCRIPT_NAME'].'?'.$filter.'&amp;firstrow=0&amp;order_field='.$order_field.'&amp;orderdir='.$orderdir.'" title="'.$title.'">'.$name.'</a>'.$ord.'</th>'."\n";
	return $str;
}

/**
 * Get a black or white color that maximize contrast.
 * @param $color (string) color in HEX format.
 * @return (string) Color.
 */
function getContrastColor($color) {
	$r = hexdec(substr($color, 0, 2));
	$g = hexdec(substr($color, 2, 2));
	$b = hexdec(substr($color, 4, 2));
	// brightness of the selected color
	$br = (((299 * $r) + (587 * $g) + (114 * $b)) / 1000);
	if ($br < 128) {
		// white
		return 'ffffff';
	}
	// black
	return '000000';
}

/**
 * Returns true if the string is an URL.
 * @param $str (string) String to check.
 * @return boolean true or false.
 */
function F_isURL($str) {
	if ((preg_match('/^(ftp|http|https|mail|sftp|ssh|telnet|vnc)[:][\/][\/]/', $str) > 0) AND (parse_url($str) !== false)) {
		return true;
	}
	return false;
}

/**
 * Normalize the UTF-8 input string.
 * Modes greater than 0 requires php5-intl module.
 * Please edit this function to implement your custom normalization method.
 * @param $str (string) UTF-8 string to normalize.
 * @param $mode (int) Normalization type: NONE=None; C=Normalization Form C (NFC) - Canonical Decomposition followed by Canonical Composition; D=Normalization Form D (NFD) - Canonical Decomposition; KC=Normalization Form KC (NFKC) - Compatibility Decomposition, followed by Canonical Composition; KD=Normalization Form KD (NFKD) - Compatibility Decomposition; CUSTOM=Custom normalization using user defined function 'user_utf8_custom_normalizer'.
 * @return normalized string using the specified algorithm.
 */
function F_utf8_normalizer($str, $mode='NONE') {
	switch($mode) {
		case 'CUSTOM': {
			if (function_exists('user_utf8_custom_normalizer')) {
				return call_user_func('user_utf8_custom_normalizer', $str);
			} else {
				return $str;
			}
			break;
		}
		case 'C': {
			// Normalization Form C (NFC) - Canonical Decomposition followed by Canonical Composition
			return normalizer_normalize($str, Normalizer::FORM_C);
			break;
		}
		case 'D': {
			// Normalization Form D (NFD) - Canonical Decomposition
			return normalizer_normalize($str, Normalizer::FORM_D);
			break;
		}
		case 'KC': {
			// Normalization Form KC (NFKC) - Compatibility Decomposition, followed by Canonical Composition
			return normalizer_normalize($str, Normalizer::FORM_KC);
			break;
		}
		case 'KD': {
			// Normalization Form KD (NFKD) - Compatibility Decomposition
			return normalizer_normalize($str, Normalizer::FORM_KD);
			break;
		}
		case 'NONE':
		default: {
			return $str;
			break;
		}
	}
}

/**
 * Convert an long integer number to a Hexadecimal representation
 * @param string|int $dec Decimal number to convert.
 * @return string containing the HEX representation in uppercase.
 * @author Nicola Asuni
 * @since 2013-07-02
 */
function bcdechex($dec) {
	$last = bcmod($dec, 16);
	$remain = bcdiv(bcsub($dec, $last), 16);
	if ($remain == 0) {
		return strtoupper(dechex($last));
	} else {
		return bcdechex($remain).strtoupper(dechex($last));
	}
}

//============================================================+
// END OF FILE
//============================================================+
