1<?php 2 3declare(strict_types=1); 4 5namespace PhpMyAdmin; 6 7use function array_intersect; 8use function array_map; 9use function explode; 10use function fclose; 11use function feof; 12use function fgets; 13use function fopen; 14use function function_exists; 15use function fwrite; 16use function recode_string; 17use function mb_convert_encoding; 18use function mb_convert_kana; 19use function mb_detect_encoding; 20use function mb_list_encodings; 21use function tempnam; 22use function unlink; 23use function iconv; 24 25/** 26 * Encoding conversion helper class 27 */ 28class Encoding 29{ 30 /** 31 * None encoding conversion engine 32 */ 33 public const ENGINE_NONE = 0; 34 35 /** 36 * iconv encoding conversion engine 37 */ 38 public const ENGINE_ICONV = 1; 39 40 /** 41 * recode encoding conversion engine 42 */ 43 public const ENGINE_RECODE = 2; 44 45 /** 46 * mbstring encoding conversion engine 47 */ 48 public const ENGINE_MB = 3; 49 50 /** 51 * Chosen encoding engine 52 * 53 * @var int 54 */ 55 private static $engine = null; 56 57 /** 58 * Map of conversion engine configurations 59 * 60 * Each entry contains: 61 * 62 * - function to detect 63 * - engine contant 64 * - extension name to warn when missing 65 * 66 * @var array 67 */ 68 private static $enginemap = [ 69 'iconv' => [ 70 'iconv', 71 self::ENGINE_ICONV, 72 'iconv', 73 ], 74 'recode' => [ 75 'recode_string', 76 self::ENGINE_RECODE, 77 'recode', 78 ], 79 'mb' => [ 80 'mb_convert_encoding', 81 self::ENGINE_MB, 82 'mbstring', 83 ], 84 'none' => [ 85 'isset', 86 self::ENGINE_NONE, 87 '', 88 ], 89 ]; 90 91 /** 92 * Order of automatic detection of engines 93 * 94 * @var array 95 */ 96 private static $engineorder = [ 97 'iconv', 98 'mb', 99 'recode', 100 ]; 101 102 /** 103 * Kanji encodings list 104 * 105 * @var string 106 */ 107 private static $kanjiEncodings = 'ASCII,SJIS,EUC-JP,JIS'; 108 109 /** 110 * Initializes encoding engine detecting available backends. 111 */ 112 public static function initEngine(): void 113 { 114 $engine = 'auto'; 115 if (isset($GLOBALS['cfg']['RecodingEngine'])) { 116 $engine = $GLOBALS['cfg']['RecodingEngine']; 117 } 118 119 /* Use user configuration */ 120 if (isset(self::$enginemap[$engine])) { 121 if (function_exists(self::$enginemap[$engine][0])) { 122 self::$engine = self::$enginemap[$engine][1]; 123 124 return; 125 } 126 127 Core::warnMissingExtension(self::$enginemap[$engine][2]); 128 } 129 130 /* Autodetection */ 131 foreach (self::$engineorder as $engine) { 132 if (function_exists(self::$enginemap[$engine][0])) { 133 self::$engine = self::$enginemap[$engine][1]; 134 135 return; 136 } 137 } 138 139 /* Fallback to none conversion */ 140 self::$engine = self::ENGINE_NONE; 141 } 142 143 /** 144 * Setter for engine. Use with caution, mostly useful for testing. 145 * 146 * @param int $engine Engine encoding 147 */ 148 public static function setEngine(int $engine): void 149 { 150 self::$engine = $engine; 151 } 152 153 /** 154 * Checks whether there is any charset conversion supported 155 */ 156 public static function isSupported(): bool 157 { 158 if (self::$engine === null) { 159 self::initEngine(); 160 } 161 162 return self::$engine != self::ENGINE_NONE; 163 } 164 165 /** 166 * Converts encoding of text according to parameters with detected 167 * conversion function. 168 * 169 * @param string $src_charset source charset 170 * @param string $dest_charset target charset 171 * @param string $what what to convert 172 * 173 * @return string converted text 174 * 175 * @access public 176 */ 177 public static function convertString( 178 string $src_charset, 179 string $dest_charset, 180 string $what 181 ): string { 182 if ($src_charset == $dest_charset) { 183 return $what; 184 } 185 if (self::$engine === null) { 186 self::initEngine(); 187 } 188 switch (self::$engine) { 189 case self::ENGINE_RECODE: 190 return recode_string( 191 $src_charset . '..' . $dest_charset, 192 $what 193 ); 194 case self::ENGINE_ICONV: 195 return iconv( 196 $src_charset, 197 $dest_charset . 198 ($GLOBALS['cfg']['IconvExtraParams'] ?? ''), 199 $what 200 ); 201 case self::ENGINE_MB: 202 return mb_convert_encoding( 203 $what, 204 $dest_charset, 205 $src_charset 206 ); 207 default: 208 return $what; 209 } 210 } 211 212 /** 213 * Detects whether Kanji encoding is available 214 */ 215 public static function canConvertKanji(): bool 216 { 217 return $GLOBALS['lang'] === 'ja'; 218 } 219 220 /** 221 * Setter for Kanji encodings. Use with caution, mostly useful for testing. 222 */ 223 public static function getKanjiEncodings(): string 224 { 225 return self::$kanjiEncodings; 226 } 227 228 /** 229 * Setter for Kanji encodings. Use with caution, mostly useful for testing. 230 * 231 * @param string $value Kanji encodings list 232 */ 233 public static function setKanjiEncodings(string $value): void 234 { 235 self::$kanjiEncodings = $value; 236 } 237 238 /** 239 * Reverses SJIS & EUC-JP position in the encoding codes list 240 */ 241 public static function kanjiChangeOrder(): void 242 { 243 $parts = explode(',', self::$kanjiEncodings); 244 if ($parts[1] === 'EUC-JP') { 245 self::$kanjiEncodings = 'ASCII,SJIS,EUC-JP,JIS'; 246 } else { 247 self::$kanjiEncodings = 'ASCII,EUC-JP,SJIS,JIS'; 248 } 249 } 250 251 /** 252 * Kanji string encoding convert 253 * 254 * @param string $str the string to convert 255 * @param string $enc the destination encoding code 256 * @param string $kana set 'kana' convert to JIS-X208-kana 257 * 258 * @return string the converted string 259 */ 260 public static function kanjiStrConv(string $str, string $enc, string $kana): string 261 { 262 if ($enc == '' && $kana == '') { 263 return $str; 264 } 265 266 $string_encoding = mb_detect_encoding($str, self::$kanjiEncodings); 267 if ($string_encoding === false) { 268 $string_encoding = 'utf-8'; 269 } 270 271 if ($kana === 'kana') { 272 $dist = mb_convert_kana($str, 'KV', $string_encoding); 273 $str = $dist; 274 } 275 if ($string_encoding != $enc && $enc != '') { 276 $dist = mb_convert_encoding($str, $enc, $string_encoding); 277 } else { 278 $dist = $str; 279 } 280 281 return $dist; 282 } 283 284 /** 285 * Kanji file encoding convert 286 * 287 * @param string $file the name of the file to convert 288 * @param string $enc the destination encoding code 289 * @param string $kana set 'kana' convert to JIS-X208-kana 290 * 291 * @return string the name of the converted file 292 */ 293 public static function kanjiFileConv(string $file, string $enc, string $kana): string 294 { 295 if ($enc == '' && $kana == '') { 296 return $file; 297 } 298 $tmpfname = (string) tempnam($GLOBALS['PMA_Config']->getUploadTempDir(), $enc); 299 $fpd = fopen($tmpfname, 'wb'); 300 $fps = fopen($file, 'r'); 301 self::kanjiChangeOrder(); 302 while (! feof($fps)) { 303 $line = fgets($fps, 4096); 304 $dist = self::kanjiStrConv($line, $enc, $kana); 305 fwrite($fpd, $dist); 306 } 307 self::kanjiChangeOrder(); 308 fclose($fps); 309 fclose($fpd); 310 unlink($file); 311 312 return $tmpfname; 313 } 314 315 /** 316 * Defines radio form fields to switch between encoding modes 317 * 318 * @return string HTML code for the radio controls 319 */ 320 public static function kanjiEncodingForm(): string 321 { 322 $template = new Template(); 323 324 return $template->render('encoding/kanji_encoding_form'); 325 } 326 327 /** 328 * Lists available encodings. 329 * 330 * @return array 331 */ 332 public static function listEncodings(): array 333 { 334 if (self::$engine === null) { 335 self::initEngine(); 336 } 337 /* Most engines do not support listing */ 338 if (self::$engine != self::ENGINE_MB) { 339 return $GLOBALS['cfg']['AvailableCharsets']; 340 } 341 342 return array_intersect( 343 array_map('strtolower', mb_list_encodings()), 344 $GLOBALS['cfg']['AvailableCharsets'] 345 ); 346 } 347} 348