Passed
Push — master ( dbd7d4...73f0a8 )
by Andreas
27:07 queued 10:33
created

midcom_services_i18n::get_current_charset()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @package midcom.services
4
 * @author The Midgard Project, http://www.midgard-project.org
5
 * @copyright The Midgard Project, http://www.midgard-project.org
6
 * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
7
 */
8
9
use Symfony\Component\Intl\Intl;
10
use Symfony\Component\Intl\Languages;
11
use Symfony\Component\Intl\Locales;
12
use Symfony\Component\HttpFoundation\RequestStack;
13
use Symfony\Component\HttpFoundation\Request;
14
15
/**
16
 * This is a basic MidCOM Service which provides an interfaces to the
17
 * various I18n facilities of MidCOM.
18
 *
19
 * The I18n service serves as a central access point for all aspects
20
 * around internationalization and localization. It provides auto-detection
21
 * of language data using HTTP Content-Negotiation along with a cookie-based
22
 * fallback.
23
 *
24
 * This class is able to run independently from midcom_application
25
 * due to the fact that it is used in the cache_hit code.
26
 *
27
 * Use this class to set the language preferences (charset and locale) and to gain
28
 * access to the l10n string databases. A few helpers which can be used to ease
29
 * translation work (like charset conversion) are in here as well.
30
 *
31
 * All language codes used here are ISO 639-1 two-letter codes.
32
 *
33
 * @package midcom.services
34
 */
35
class midcom_services_i18n
36
{
37
    /**
38
     * Fallback language, in case the selected language is not available.
39
     *
40
     * @var string
41
     */
42
    private $_fallback_language;
43
44
    /**
45
     * Cache of all instantiated localization classes.
46
     *
47
     * @var midcom_services_i18n_l10n[]
48
     */
49
    private $_obj_l10n = [];
50
51
    /**
52
     * Current language.
53
     *
54
     * @var string
55
     */
56
    private $_current_language;
57
58
    /**
59
     * Current character set
60
     *
61
     * @var string
62
     */
63
    private $_current_charset = 'utf-8';
64
65
    /**
66
     * Initialize the available i18n framework by determining the desired language
67
     * from these different sources: HTTP Content Negotiation, Client side language cookie.
68
     *
69
     * Its two parameters set the default language in case that none is supplied
70
     * via HTTP Content Negotiation or through Cookies.
71
     *
72
     * The default language set on startup is currently hardcoded to 'en',
73
     * you should override it after initialization, if you want something
74
     * else using the setter methods below.
75
     *
76
     * The fallback language is read from the MidCOM configuration directive
77
     * <i>i18n_fallback_language</i>.
78
     */
79 3
    public function __construct(RequestStack $request_stack)
80
    {
81 3
        $this->_fallback_language = midcom::get()->config->get('i18n_fallback_language');
82
83 3
        $found = false;
84 3
        if ($request = $request_stack->getCurrentRequest()) {
85 2
            $found = $this->_read_cookie($request) || $this->_read_http_negotiation($request);
86
        }
87 3
        if (!$found) {
88 1
            $this->set_language($this->_fallback_language);
0 ignored issues
show
Bug introduced by
It seems like $this->_fallback_language can also be of type null; however, parameter $lang of midcom_services_i18n::set_language() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

88
            $this->set_language(/** @scrutinizer ignore-type */ $this->_fallback_language);
Loading history...
89
        }
90 3
    }
91
92
    /**
93
     * Try to pull the user's preferred language and
94
     * character set out of a cookie named "midcom_services_i18n".
95
     */
96 2
    private function _read_cookie(Request $request) : bool
97
    {
98 2
        if (!$request->cookies->has('midcom_services_i18n')) {
99 2
            return false;
100
        }
101
102
        $rawdata = base64_decode($request->cookies->get('midcom_services_i18n'));
103
        $array = unserialize($rawdata);
104
105
        if (   !array_key_exists('language', $array)
106
            || !array_key_exists('charset', $array)) {
107
            debug_add("Rejecting cookie, it seems invalid.");
108
            return false;
109
        }
110
        $this->set_charset($array['charset']);
111
        return $this->set_language($array['language']);
112
    }
113
114
    /**
115
     * Pull available language out of the HTTP Headers
116
     *
117
     * q-parameters for prioritization are supported.
118
     */
119 2
    private function _read_http_negotiation(Request $request) : bool
120
    {
121 2
        if (!$request->server->has('HTTP_ACCEPT_LANGUAGE')) {
122
            return false;
123
        }
124 2
        $rawdata = explode(",", $request->server->get('HTTP_ACCEPT_LANGUAGE'));
125 2
        $http_langs = [];
126 2
        foreach ($rawdata as $data) {
127 2
            $params = explode(";", $data);
128 2
            $lang = array_shift($params);
129
130
            // we can't use strings like en-US, so we only use the first two characters
131 2
            $lang = substr($lang, 0, 2);
132 2
            $q = $this->_get_q($params);
133
134 2
            if (   !isset($http_langs[$lang])
135 2
                || $http_langs[$lang] < $q) {
136 2
                $http_langs[$lang] = $q;
137
            }
138
        }
139 2
        arsort($http_langs, SORT_NUMERIC);
140 2
        foreach (array_keys($http_langs) as $name) {
141 2
            if ($this->set_language($name)) {
142 2
                return true;
143
            }
144
        }
145
146
        return false;
147
    }
148
149 2
    private function _get_q(array $params) : float
150
    {
151 2
        $q = 1.0;
152 2
        $option = array_shift($params);
153 2
        while ($option !== null) {
154 1
            $option_params = explode("=", $option);
155 1
            if (count($option_params) != 2) {
156
                $option = array_shift($params);
157
                continue;
158
            }
159 1
            if (   $option_params[0] == "q"
160 1
                && is_numeric($option_params[1])) {
161
                // make sure that 0.0 <= $q <= 1.0
162 1
                $q = max(0.0, min(1.0, $option_params[1]));
163
            }
164 1
            $option = array_shift($params);
165
        }
166 2
        return $q;
167
    }
168
169
    /**
170
     * Set output character set.
171
     *
172
     * @param string $charset    Charset name.
173
     */
174
    public function set_charset(string $charset)
175
    {
176
        $this->_current_charset = strtolower($charset);
177
    }
178
179
    /**
180
     * Set output language.
181
     *
182
     * This will set the character encoding to the language's default
183
     * encoding and will also set the system locale to the one
184
     * specified in the language database.
185
     *
186
     * If you want another character encoding as the default one, you
187
     * have to override it manually using midcom_services_i18n::set_charset()
188
     * after calling this method.
189
     *
190
     * @param string $lang    Language ISO 639-1 code
191
     */
192 3
    public function set_language(string $lang) : bool
193
    {
194 3
        if (Locales::getName($lang) === null) {
0 ignored issues
show
introduced by
The condition Symfony\Component\Intl\L...getName($lang) === null is always false.
Loading history...
195
            debug_add("Language {$lang} not found.", MIDCOM_LOG_ERROR);
196
            return false;
197
        }
198
199 3
        $this->_current_language = $lang;
200
201 3
        setlocale(LC_ALL, $lang);
202 3
        if (Intl::isExtensionLoaded()) {
203 3
            Locale::setDefault($lang);
204
        }
205
206 3
        foreach ($this->_obj_l10n as $object) {
207
            $object->set_language($lang);
208
        }
209 3
        return true;
210
    }
211
212
    /**
213
     * Set the fallback language.
214
     *
215
     * @param string $lang    Language name.
216
     */
217
    public function set_fallback_language(string $lang)
218
    {
219
        $this->_fallback_language = $lang;
220
        foreach ($this->_obj_l10n as $object) {
221
            $object->set_fallback_language($lang);
222
        }
223
    }
224
225
    /**
226
     * Returns the current language code
227
     *
228
     * @return string
229
     */
230 376
    public function get_current_language()
231
    {
232 376
        return $this->_current_language;
233
    }
234
235
    /**
236
     * Returns the current fallback language code
237
     *
238
     * @return string
239
     */
240 52
    public function get_fallback_language()
241
    {
242 52
        return $this->_fallback_language;
243
    }
244
245
    /**
246
     * Returns the current character set
247
     *
248
     * @return string
249
     */
250 103
    public function get_current_charset()
251
    {
252 103
        return $this->_current_charset;
253
    }
254
255
    /**
256
     * Returns a l10n class instance which can be used to
257
     * access the localization data of the current component.
258
     *
259
     * If loading failed, midcom_error is thrown, otherwise the l10n
260
     * db cache is populated accordingly.
261
     *
262
     * @see midcom_services_i18n_l10n
263
     * @param string $component    The component for which to retrieve a string database.
264
     * @param string $database    The string table to retrieve from the component's locale directory.
265
     */
266 398
    public function get_l10n(string $component = 'midcom', string $database = 'default') : midcom_services_i18n_l10n
267
    {
268 398
        $cacheid = "{$component}/{$database}";
269
270 398
        if (!array_key_exists($cacheid, $this->_obj_l10n)) {
271 6
            $obj = new midcom_services_i18n_l10n($component, $database);
272 6
            $obj->set_language($this->_current_language);
273 6
            $obj->set_fallback_language($this->_fallback_language);
274 6
            $this->_obj_l10n[$cacheid] = $obj;
275
        }
276
277 398
        return $this->_obj_l10n[$cacheid];
278
    }
279
280
    /**
281
     * Returns a translated string using the l10n database specified in the function
282
     * arguments.
283
     *
284
     * @param string $stringid The string to translate.
285
     * @param string $component    The component for which to retrieve a string database. If omitted, this defaults to the
286
     *     current component (out of the component context).
287
     * @param string $database    The string table to retrieve from the component's locale directory. If omitted, the 'default'
288
     *     database is used.
289
     * @see midcom_services_i18n_l10n::get()
290
     */
291 359
    public function get_string(string $stringid, $component = null, string $database = 'default') : string
292
    {
293 359
        if ($component === null) {
294 5
            $component = midcom_core_context::get()->get_key(MIDCOM_CONTEXT_COMPONENT) ?? 'midcom';
295
        }
296
297 359
        return $this->get_l10n($component, $database)->get($stringid);
298
    }
299
300
    /**
301
     * This is a shortcut for echo $this->get_string(...);.
302
     *
303
     * To keep the naming stable with the actual l10n class, this is not called
304
     * echo_string (Zend won't allow $l10n->echo().)
305
     *
306
     * @param string $stringid The string to translate.
307
     * @param string $component    The component for which to retrieve a string database. If omitted, this defaults to the
308
     *     current component (out of the component context).
309
     * @param string $database    The string table to retrieve from the component's locale directory. If omitted, the 'default'
310
     *     database is used.
311
     * @see midcom_services_i18n_l10n::get()
312
     * @see get_string()
313
     */
314
    public function show_string(string $stringid, $component = null, string $database = 'default')
315
    {
316
        echo $this->get_string($stringid, $component, $database);
317
    }
318
319
    /**
320
     * Lists languages as identifier -> name pairs
321
     */
322 2
    public function list_languages() : array
323
    {
324 2
        $languages = Languages::getNames('en');
325 2
        foreach ($languages as $identifier => &$language) {
326 2
            $localname = Languages::getName($identifier, $identifier);
327 2
            if ($localname != $language) {
328 2
                $language .= ' (' . $localname . ')';
329
            }
330
        }
331 2
        return $languages;
332
    }
333
334
    /**
335
     * This is a calling wrapper to the iconv library.
336
     *
337
     * See the PHP iconv() function for the exact parameter definitions.
338
     *
339
     * @param string $source_charset The charset to convert from.
340
     * @param string $destination_charset The charset to convert to.
341
     * @param string $string The string to convert.
342
     * @return mixed The converted string or false on any error.
343
     */
344
    private function iconv(string $source_charset, string $destination_charset, string $string)
345
    {
346
        $result = @iconv($source_charset, $destination_charset, $string);
347
        if ($result === false && !empty($string)) {
348
            debug_add("Iconv returned failed to convert a string, returning an empty string.", MIDCOM_LOG_WARN);
349
            debug_print_r("Tried to convert this string from {$source_charset} to {$destination_charset}:", $string);
350
            midcom::get()->debug->log_php_error(MIDCOM_LOG_WARN);
351
            return false;
352
        }
353
        return $result;
354
    }
355
356
    /**
357
     * Convert a string assumed to be in the currently active charset to UTF8.
358
     *
359
     * @param string $string The string to convert
360
     * @return string The string converted to UTF-8
361
     */
362
    public function convert_to_utf8(string $string)
363
    {
364
        if ($this->_current_charset == 'utf-8') {
365
            return $string;
366
        }
367
        return $this->iconv($this->_current_charset, 'utf-8', $string);
368
    }
369
370
    /**
371
     * Convert a string assumed to be in UTF-8 to the currently active charset.
372
     *
373
     * @param string $string The string to convert
374
     * @return string The string converted to the current charset
375
     */
376 371
    public function convert_from_utf8(string $string)
377
    {
378 371
        if ($this->_current_charset == 'utf-8') {
379 371
            return $string;
380
        }
381
        return $this->iconv('utf-8', $this->_current_charset, $string);
382
    }
383
384
    /**
385
     * Converts the given string to the current site charset.
386
     *
387
     * @param string $string The string to convert.
388
     * @return string The converted string.
389
     */
390
    public function convert_to_current_charset(string $string)
391
    {
392
        $charset = mb_detect_encoding($string, "UTF-8, UTF-7, ASCII, ISO-8859-15");
393
        debug_add("mb_detect_encoding got {$charset}");
394
        return $this->iconv($charset, $this->_current_charset, $string);
395
    }
396
397
    /**
398
     * Wrapped html_entity_decode call
399
     *
400
     * @param string $text The text with HTML entities, which should be replaced by their native equivalents.
401
     */
402 14
    public function html_entity_decode(string $text) : string
403
    {
404 14
        return html_entity_decode($text, ENT_COMPAT, $this->_current_charset);
405
    }
406
}
407