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