<?php

/**
 * <pre>
 * Invision Power Services
 * IP.Board v3.4.6
 * Character set conversion library
 * Last Updated: $Date: 2012-12-07 14:02:27 -0500 (Fri, 07 Dec 2012) $
 * </pre>
 *
 * @author 		$Author: bfarber $
 * @author		Mikolaj Jedrzejak <mikolajj@op.pl>
 * @copyright	(c) 2001 - 2009 Invision Power Services, Inc.
 * @copyright	Copyright Mikolaj Jedrzejak (c) 2003-2004
 *
 * @package		IP.Board
 * @subpackage	Kernel
 *
 * @since		Friday 2nd December 2005 10:18
 * @version		$Revision: 11687 $
 * @version		1.0 2004-07-27 00:37
 * @link		http://www.unicode.org	Unicode Homepage
 * @link		http://www.mikkom.pl	My Homepage
 * @link		https://www.---.com/index.php?appcomponent=downloads	Downloads
 *
 * This class is an adaptation of the ConvertCharset.class.php provided by Mikolaj Jedrzejak.
 * In order to use character set conversions, you must first download the conversion libraries from our website.  See the 'Downloads' link.
 * Upon downloading the tables, extract the zip and place the top-level i18n folder under ips_kernel/.
 * His original conversion class and License are provided in the download with the conversion libraries for reference.
 *
 * Convert from iso-8859-7 to utf-8
 * <code>
 * $convert = new classConvertCharset();
 * $converted_text = $convert->convertEncoding( $string, 'iso-8859-7', 'utf-8' );
 * print $converted_text;
 * </code>
 *
 */


if ( ! defined( 'IPS_CLASSES_PATH' ) )
{
	/**
	* Define classes path
	*/
	define( 'IPS_CLASSES_PATH', dirname(__FILE__) );
}

class classConvertCharset
{
	/**
	 * Array of error messages associated with the conversion
	 *
	 * @var		array
	 */
	public $errors			= array();
	
	/**
	 * Should characters be turned into numeric entities
	 *
	 * @var		boolean
	 */
	protected $entities		= false;
	
	/**
	 * Conversion method to use.
	 * Valid values include: mb, iconv, recode, internal
	 *
	 * @var		string
	 */
	public $method			= 'internal';
	
	/**
	 * Path for character sets
	 *
	 * @var		string
	 */
	public $charsetPath		= '';
	
	/**
	 * Converts a text string from its current charset to a destination charset
	 *
	 * @param	string		Text string
	 * @param	string		Text string char set (original)
	 * @param	string		Desired character set (destination)
	 * @return	@e string
	 */
	public function convertEncoding( $string, $string_char_set, $destination_char_set='UTF-8' )
	{
		$string_char_set = strtolower($string_char_set);
		$t               = $string;
		
		/* Return bools, null, ints, etc. */
		if ( !is_string( $t ) )
		{
			return $t;
		}
		
		//-----------------------------------------
		// Did we pass a destination?
		//-----------------------------------------
		
		$destination_char_set = strtolower($destination_char_set);
		
		//-----------------------------------------
		// Not the same?
		//-----------------------------------------
		
		if ( $destination_char_set == $string_char_set )
		{
			return $string;
		}
		
		if( !$string_char_set )
		{
			return $string;
		}
		
		if( !$t OR $t == '' )
		{
			return $string;
		}		
		
		//-----------------------------------------
		// Do the convert - internally..
		//-----------------------------------------
		
		$method	= "convertUsing_" . $this->method;
		$text	= $this->$method( $string, $string_char_set, $destination_char_set );

		return $text ? $text : $t;
	}
	
	/**
	 * Converts a text string from its current charset to a destination charset using mb_convert_encoding.  If mb* functions are not available, logs an error to $this->errors.
	 *
	 * @param	string		Text string
	 * @param	string		Text string char set (original)
	 * @param	string		Desired character set (destination)
	 * @return	@e string
	 */
	protected function convertUsing_mb( $string, $string_char_set, $destination_char_set='UTF-8' )
	{
		if ( function_exists( 'mb_convert_encoding' ) )
		{
			$encodings	= array_map( 'strtolower', mb_list_encodings() );
			
			if( in_array( strtolower( $destination_char_set ), $encodings ) AND in_array( strtolower( $string_char_set ), $encodings ) )
			{
				$text = mb_convert_encoding( $string, $destination_char_set, $string_char_set );
			}
			else
			{
				$this->errors[]	= "NO_MB_FUNCTION";
			}
		}
		else
		{
			$this->errors[]	= "NO_MB_FUNCTION";
		}
		
		return $text ? $text : $string;
	}
	
	/**
	 * Converts a text string from its current charset to a destination charset using iconv.  If iconv is not available, logs an error to $this->errors.
	 *
	 * @param	string		Text string
	 * @param	string		Text string char set (original)
	 * @param	string		Desired character set (destination)
	 * @return	@e string
	 */
	protected function convertUsing_iconv( $string, $string_char_set, $destination_char_set='UTF-8' )
	{
		if ( function_exists( 'iconv' ) )
		{
			$text = iconv( $string_char_set, $destination_char_set.'//TRANSLIT', $string );
		}
		else
		{
			$this->errors[]	= "NO_ICONV_FUNCTION";
		}
		
		return $text ? $text : $string;
	}
	
	/**
	 * Converts a text string from its current charset to a destination charset using recode_string.  If recode is not available, logs an error to $this->errors.
	 *
	 * @param	string		Text string
	 * @param	string		Text string char set (original)
	 * @param	string		Desired character set (destination)
	 * @return	@e string
	 */
	protected function convertUsing_recode( $string, $string_char_set, $destination_char_set='UTF-8' )
	{
		if ( function_exists( 'recode_string' ) )
		{
			$text = recode_string( $string_char_set.'..'.$destination_char_set, $string );
		}
		else
		{
			$this->errors[]	= "NO_RECODE_FUNCTION";
		}
		
		return $text ? $text : $string;
	}
	
	/**
	 * Converts a text string from its current charset to a destination charset using internal conversion class. 
	 * The bulk of this function was written by Mikolaj Jedrzejak and used with permission via the License
	 *
	 * @param	string		Text string
	 * @param	string		Text string char set (original)
	 * @param	string		Desired character set (destination)
	 * @return	@e string
	 */
	protected function convertUsing_internal( $string, $string_char_set, $destination_char_set='UTF-8' )
	{
		$text    	= '';
		$original	= $string;
		
		if( !$this->charsetPath )
		{
			$this->charsetPath	= IPS_KERNEL_PATH . '/i18n/ConvertTables/';
		}
		
		/**
		 * This divison was made to prevent errors during convertion to/from utf-8 with
		 * "entities" enabled, because we need to use proper destination(to)/source(from)
		 * encoding table to write proper entities.
		 * 
		 * This is the first case. We are converting from 1byte chars...
		 */
		if ($string_char_set != "utf-8") 
		{ 
			/* Cache the conversion table to prevent rebuilding on every request */
			static $charsetTables = array();

			/**
			 * Now build table with both charsets for encoding change. 
			 */
			if( !$charsetTables[ $string_char_set ] )
			{
				if ($destination_char_set != "utf-8") 
				{ 
					$charsetTables[ $string_char_set ] = $this->_makeConversionTable( $string_char_set, $destination_char_set );
				}
				else
				{
					$charsetTables[ $string_char_set ] = $this->_makeConversionTable( $string_char_set );
				}
			}

			$charsetTable = $charsetTables[ $string_char_set ];
			
			if( !count($charsetTable) )
			{
				$this->errors[]	= "Character set table not found";
				return $original;
			}

			/**
			 * For each char in a string... 
			 */
			for ($i = 0; $i < strlen($string); $i++)
			{
				$hexChar		= "";
				$unicodeHexChar	= "";
				$hexChar		= strtoupper(dechex(ord($string[$i])));

				// This is fix from Mario Klingemann, it prevents
				// droping chars below 16 because of missing leading 0 [zeros]
				if ( strlen($hexChar)==1 )
				{
					$hexChar = "0" . $hexChar;
				}

				// This is quick fix of 10 chars in gsm0338
				// Thanks goes to Andrea Carpani who pointed on this problem and solve it ;)
				if ( ( $string_char_set == "gsm0338" ) && ( $hexChar == '1B' ) )
				{
					$i++;
					$hexChar .= strtoupper(dechex(ord($string[$i])));
				}

				if ( $destination_char_set != "utf-8" ) 
				{
					if ( in_array( $hexChar, $charsetTable[$string_char_set] ) )
					{
						$unicodeHexChar		= array_search( $hexChar, $charsetTable[$string_char_set] );
						$unicodeHexChars	= explode( "+",$unicodeHexChar );

						for( $unicodeHexCharElement = 0; $unicodeHexCharElement < count($unicodeHexChars); $unicodeHexCharElement++ )
						{
							if ( array_key_exists( $unicodeHexChars[$unicodeHexCharElement], $charsetTable[$destination_char_set] ) ) 
							{
								if ( $this->entities == true ) 
								{
									$text .= $this->_unicodeEntity( $this->_hexToUtf( $unicodeHexChars[$unicodeHexCharElement] ) );
								}
								else
								{
									$text .= chr(hexdec($charsetTable[$destination_char_set][$unicodeHexChars[$unicodeHexCharElement]]));
								}
							}
						 	else
							{
								$this->errors[]	= "NO_CHAR_IN_DESTINATION: {$string[$i]}";
								return $original;
							}
						}
					}
					else
					{
						$this->errors[]	= "NO_CHAR_IN_SOURCE: {$string[$i]}";
						return $original;
					}
				}
				else
				{
					if ( in_array( $hexChar, $charsetTable[$string_char_set] ) ) 
					{
						$unicodeHexChar		= array_search( $hexChar, $charsetTable[$string_char_set] );
						$unicodeHexChars	= explode( "+", $unicodeHexChar );

						/**
				     	 * Sometimes there are two or more utf-8 chars per one regular char.
						 * Extream, example is polish old Mazovia encoding, where one char contains
						 * two lettes 007a (z) and 0142 (l slash), we need to figure out how to
						 * solve this problem.
						 * The letters are merge with "plus" sign, there can be more than two chars.
						 * In Mazowia we have 007A+0142, but sometimes it can look like this
						 * 0x007A+0x0142+0x2034 (that string means nothing, it just shows the possibility...)
				     	 */
						for( $unicodeHexCharElement = 0; $unicodeHexCharElement < count($unicodeHexChars); $unicodeHexCharElement++)
						{
							if ( $this->entities == true ) 
							{
								$text .= $this->_unicodeEntity( $this->_hexToUtf( $unicodeHexChars[$unicodeHexCharElement] ) );
							}
							else
							{
								$text .= $this->_hexToUtf( $unicodeHexChars[$unicodeHexCharElement] );
							}
						}							
					}
					else
					{
						$this->errors[]	= "NO_CHAR_IN_SOURCE: {$string[$i]}";
						return $original;
					}
				}					
			}
		}
		
		/**
		 * This is second case. We are encoding from multibyte char string. 
		 */

		else if( $string_char_set == "utf-8" )
		{
			$hexChar		= "";
			$unicodeHexChar	= "";
			$charsetTable 	= $this->_makeConversionTable( $destination_char_set );

			if( is_array($charsetTable[$destination_char_set]) AND count($charsetTable[$destination_char_set]) )
			{
				foreach ( $charsetTable[$destination_char_set] as $unicodeHexChar => $hexChar )
				{
					if ($this->entities == true)
					{
						$entityOrChar	= $this->_unicodeEntity($this->_hexToUtf($unicodeHexChar));
					}
					else
					{
						$entityOrChar	= chr(hexdec($hexChar));
					}
	
					$string = str_replace( $this->_hexToUtf( $unicodeHexChar), $entityOrChar, $string );
				}
			}

			$text = $string;
		}
	
		return $text ? $text : $original;
	}

	/**
	 * Convert unicode characters to unicode HTML entities
	 * 
	 * @param	string		$unicodeString		Input Unicode string (1 char can take more than 1 byte)
	 * @return	@e string
	 * @see		_hexToUtf()
	 */
	protected function _unicodeEntity( $unicodeString ) 
	{
		$outString		= "";
		$stringLength	= strlen( $unicodeString );

		for( $charPosition = 0; $charPosition < $stringLength; $charPosition++ ) 
		{
			$char		= $unicodeString [$charPosition];
			$asciiChar	= ord ($char);

			if ($asciiChar < 128) //1 7 0bbbbbbb (127)
			{
			   $outString .= $char; 
			}
			else if ($asciiChar >> 5 == 6) //2 11 110bbbbb 10bbbbbb (2047)
			{
			   $firstByte	= ($asciiChar & 31);
			   $charPosition++;
			   $char		= $unicodeString [$charPosition];
			   $asciiChar	= ord ($char);
			   $secondByte	= ($asciiChar & 63);
			   $asciiChar	= ($firstByte * 64) + $secondByte;

			   $entity		= sprintf ( "&#%d;", $asciiChar );
			   $outString	.= $entity;
			}
			else if ($asciiChar >> 4  == 14)  //3 16 1110bbbb 10bbbbbb 10bbbbbb
			{
				$firstByte	= ($asciiChar & 31);
				$charPosition++;
				$char		= $unicodeString [$charPosition];
				$asciiChar	= ord ($char);
				$secondByte	= ($asciiChar & 63);
				$charPosition++;
				$char		= $unicodeString [$charPosition];
				$asciiChar	= ord ($char);
				$thirdByte	= ($asciiChar & 63);
				$asciiChar	= ((($firstByte * 64) + $secondByte) * 64) + $thirdByte;
				
				$entity		= sprintf ("&#%d;", $asciiChar);
				$outString	.= $entity;
			}
			else if ($asciiChar >> 3 == 30) //4 21 11110bbb 10bbbbbb 10bbbbbb 10bbbbbb
			{
				$firstByte	= ($asciiChar & 31);
				$charPosition++;
				$char		= $unicodeString [$charPosition];
				$asciiChar	= ord ($char);
				$secondByte	= ($asciiChar & 63);
				$charPosition++;
				$char		= $unicodeString [$charPosition];
				$asciiChar	= ord ($char);
				$thirdByte	= ($asciiChar & 63);
				$charPosition++;
				$char		= $unicodeString [$charPosition];
				$asciiChar	= ord ($char);
				$fourthByte	= ($asciiChar & 63);
				$asciiChar	= ((((($firstByte * 64) + $secondByte) * 64) + $thirdByte) * 64) + $fourthByte;
			
				$entity		= sprintf ("&#%d;", $asciiChar);
				$outString	.= $entity;
			}
	  	}

		return $outString;
	} 
	
	/**
	 * Convert unicode characters to unicode HTML entities. 
	 * It is very similar to _unicodeEntity function (link below). There is one difference 
	 * in returned format. This time it's a regular char(s), in most cases it will be one or two chars. 
	 * 
	 * @param	string		$utfCharInHex	Hexadecimal value of a unicode char.
	 * @return	@e string
	 * @see		_unicodeEntity()
	 */
	protected function _hexToUtf( $utfCharInHex )
	{
		$outputChar		= "";
		$utfCharInDec	= hexdec($utfCharInHex);

		if( $utfCharInDec < 128 )
		{
			$outputChar .= chr($utfCharInDec);
		}
    	else if( $utfCharInDec < 2048 )
    	{
    		$outputChar .= chr( ( $utfCharInDec>>6 ) + 192 ) . chr( ( $utfCharInDec&63 ) + 128 );
		}
    	else if( $utfCharInDec < 65536 )
    	{
    		$outputChar .= chr( ( $utfCharInDec>>12 ) + 224 ) . chr( ( ( $utfCharInDec>>6 ) &63 ) + 128 ) . chr( ( $utfCharInDec&63 ) + 128 );
		}
    	else if( $utfCharInDec < 2097152 )
    	{
    		$outputChar .= chr( $utfCharInDec>>18+240 ) . chr( ( ( $utfCharInDec>>12 ) &63 ) + 128 ) . chr( ( $utfCharInDec>>6 ) &63 + 128 ) . chr( $utfCharInDec&63 + 128 );
		}

		return $outputChar;
	}


	/**
	 * Create the conversion tables to use.
	 * 
	 * This function creates table with two SBCS (Single Byte Character Set). Every conversion is through this table.
	 *  
	 * - The file with encoding tables have to be save in "Format A" of unicode.org charset table format.
	 * - BOTH charsets MUST be SBCS
	 * - The files with encoding tables have to be complete (None of chars can be missing, unles you are sure you are not going to use it)
	 * 
	 * "Format A" encoding file, if you have to build it by yourself should aplly these rules:
	 * - you can comment everything with #
	 * - first column contains 1 byte chars in hex starting from 0x..
	 * - second column contains unicode equivalent in hex starting from 0x....
	 * - then every next column is optional, but in "Format A" it should contain unicode char name or/and your own comment
	 * - the columns can be splited by "spaces", "tabs", "," or any combination of these
	 * - below is an example
	 * 
	 * <code>
	 * #
	 * #	The entries are in ANSI X3.4 order.
	 * #
	 * 0x00	0x0000	#	NULL end extra comment, if needed
	 * 0x01	0x0001	#	START OF HEADING
	 * # Oh, one more thing, you can make comments inside of a rows if you like.
	 * 0x02	0x0002	#	START OF TEXT
	 * 0x03	0x0003	#	END OF TEXT
	 * next line, and so on...
	 * </code>
	 * 
	 * You can get full tables with encodings from http://www.unicode.org
	 * 
	 * @param	string		$firstEncoding		Name of first encoding and first encoding filename (thay have to be the same)
	 * @param	string		$secondEncoding		Name of second encoding and second encoding filename (thay have to be the same). Optional for building a joined table.
	 * @return	@e array
	 */
	protected function _makeConversionTable( $firstEncoding, $secondEncoding = "" ) 
	{
		$convertTable	= array();

		for( $i = 0; $i < func_num_args(); $i++ )
		{
			/**
			 * Because func_*** can't be used inside of another function call
			 * we have to save it as a separate value.
			 */
			$fileName = func_get_arg($i);

			if ( !is_file( $this->charsetPath . $fileName ) ) 
			{
				$this->errors[]	= "FILE_NOT_FOUND: {$fileName}";
				return;
			}

			$fileWithEncTabe	= fopen( $this->charsetPath . $fileName, "r" ) or die(); //This die(); is just to make sure...

			while(!feof($fileWithEncTabe))
			{
				/**
				 * We asume that line is not longer
				 * than 1024 which is the default value for fgets function 
				 */
				if( $oneLine = trim( fgets( $fileWithEncTabe, 1024 ) ) )
				{
					/**
					 * We don't need all comment lines. I check only for "#" sign, because
					 * this is a way of making comments by unicode.org in thair encoding files
					 * and that's where the files are from :-)
					 */
					if ( substr( $oneLine, 0, 1 ) != "#" ) 
					{
						/**
						 * Sometimes inside the charset file the hex walues are separated by
						 * "space" and sometimes by "tab", the below preg_split can also be used
						 * to split files where separator is a ",", "\r", "\n" and "\f"
						 */
						$hexValue = preg_split ( "/[\s,]+/", $oneLine, 3 );  //We need only first 2 values

						/**
						 * Sometimes char is UNDEFINED, or missing so we can't use it for convertion
						 */
						if (substr($hexValue[1], 0, 1) != "#") 
						{
							$arrayKey	= strtoupper(str_replace(strtolower("0x"), "", $hexValue[1]));
							$arrayValue	= strtoupper(str_replace(strtolower("0x"), "", $hexValue[0]));

							$convertTable[ func_get_arg($i) ][ $arrayKey ] = $arrayValue;
						}
					}
				}
			}
		}

		return $convertTable;
	}
}