Passed
Push — master ( b36f9b...dbd7d4 )
by Andreas
17:55
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 language code corresponding to current content language
237
     *
238
     * @return string
239
     */
240
    public function get_content_language()
241
    {
242
        return $this->get_current_language();
243
    }
244
245
    /**
246
     * Returns the current fallback language code
247
     *
248
     * @return string
249
     */
250 52
    public function get_fallback_language()
251
    {
252 52
        return $this->_fallback_language;
253
    }
254
255
    /**
256
     * Returns the current character set
257
     *
258
     * @return string
259
     */
260 103
    public function get_current_charset()
261
    {
262 103
        return $this->_current_charset;
263
    }
264
265
    /**
266
     * Returns a l10n class instance which can be used to
267
     * access the localization data of the current component.
268
     *
269
     * If loading failed, midcom_error is thrown, otherwise the l10n
270
     * db cache is populated accordingly.
271
     *
272
     * @see midcom_services_i18n_l10n
273
     * @param string $component    The component for which to retrieve a string database.
274
     * @param string $database    The string table to retrieve from the component's locale directory.
275
     */
276 398
    public function get_l10n(string $component = 'midcom', string $database = 'default') : midcom_services_i18n_l10n
277
    {
278 398
        $cacheid = "{$component}/{$database}";
279
280 398
        if (!array_key_exists($cacheid, $this->_obj_l10n)) {
281 6
            $obj = new midcom_services_i18n_l10n($component, $database);
282 6
            $obj->set_language($this->_current_language);
283 6
            $obj->set_fallback_language($this->_fallback_language);
284 6
            $this->_obj_l10n[$cacheid] = $obj;
285
        }
286
287 398
        return $this->_obj_l10n[$cacheid];
288
    }
289
290
    /**
291
     * Returns a translated string using the l10n database specified in the function
292
     * arguments.
293
     *
294
     * @param string $stringid The string to translate.
295
     * @param string $component    The component for which to retrieve a string database. If omitted, this defaults to the
296
     *     current component (out of the component context).
297
     * @param string $database    The string table to retrieve from the component's locale directory. If omitted, the 'default'
298
     *     database is used.
299
     * @see midcom_services_i18n_l10n::get()
300
     */
301 359
    public function get_string(string $stringid, $component = null, string $database = 'default') : string
302
    {
303 359
        if ($component === null) {
304 5
            $component = midcom_core_context::get()->get_key(MIDCOM_CONTEXT_COMPONENT) ?? 'midcom';
305
        }
306
307 359
        return $this->get_l10n($component, $database)->get($stringid);
308
    }
309
310
    /**
311
     * This is a shortcut for echo $this->get_string(...);.
312
     *
313
     * To keep the naming stable with the actual l10n class, this is not called
314
     * echo_string (Zend won't allow $l10n->echo().)
315
     *
316
     * @param string $stringid The string to translate.
317
     * @param string $component    The component for which to retrieve a string database. If omitted, this defaults to the
318
     *     current component (out of the component context).
319
     * @param string $database    The string table to retrieve from the component's locale directory. If omitted, the 'default'
320
     *     database is used.
321
     * @see midcom_services_i18n_l10n::get()
322
     * @see get_string()
323
     */
324
    public function show_string(string $stringid, $component = null, string $database = 'default')
325
    {
326
        echo $this->get_string($stringid, $component, $database);
327
    }
328
329
    /**
330
     * Lists languages as identifier -> name pairs
331
     */
332 2
    public function list_languages() : array
333
    {
334 2
        $languages = Languages::getNames('en');
335 2
        foreach ($languages as $identifier => &$language) {
336 2
            $localname = Languages::getName($identifier, $identifier);
337 2
            if ($localname != $language) {
338 2
                $language .= ' (' . $localname . ')';
339
            }
340
        }
341 2
        return $languages;
342
    }
343
344
    /**
345
     * This is a calling wrapper to the iconv library.
346
     *
347
     * See the PHP iconv() function for the exact parameter definitions.
348
     *
349
     * @param string $source_charset The charset to convert from.
350
     * @param string $destination_charset The charset to convert to.
351
     * @param string $string The string to convert.
352
     * @return mixed The converted string or false on any error.
353
     */
354
    private function iconv(string $source_charset, string $destination_charset, string $string)
355
    {
356
        $result = @iconv($source_charset, $destination_charset, $string);
357
        if ($result === false && !empty($string)) {
358
            debug_add("Iconv returned failed to convert a string, returning an empty string.", MIDCOM_LOG_WARN);
359
            debug_print_r("Tried to convert this string from {$source_charset} to {$destination_charset}:", $string);
360
            midcom::get()->debug->log_php_error(MIDCOM_LOG_WARN);
361
            return false;
362
        }
363
        return $result;
364
    }
365
366
    /**
367
     * Convert a string assumed to be in the currently active charset to UTF8.
368
     *
369
     * @param string $string The string to convert
370
     * @return string The string converted to UTF-8
371
     */
372
    public function convert_to_utf8(string $string)
373
    {
374
        if ($this->_current_charset == 'utf-8') {
375
            return $string;
376
        }
377
        return $this->iconv($this->_current_charset, 'utf-8', $string);
378
    }
379
380
    /**
381
     * Convert a string assumed to be in UTF-8 to the currently active charset.
382
     *
383
     * @param string $string The string to convert
384
     * @return string The string converted to the current charset
385
     */
386 371
    public function convert_from_utf8(string $string)
387
    {
388 371
        if ($this->_current_charset == 'utf-8') {
389 371
            return $string;
390
        }
391
        return $this->iconv('utf-8', $this->_current_charset, $string);
392
    }
393
394
    /**
395
     * Converts the given string to the current site charset.
396
     *
397
     * @param string $string The string to convert.
398
     * @return string The converted string.
399
     */
400
    public function convert_to_current_charset(string $string)
401
    {
402
        $charset = mb_detect_encoding($string, "UTF-8, UTF-7, ASCII, ISO-8859-15");
403
        debug_add("mb_detect_encoding got {$charset}");
404
        return $this->iconv($charset, $this->_current_charset, $string);
405
    }
406
407
    /**
408
     * Wrapped html_entity_decode call
409
     *
410
     * @param string $text The text with HTML entities, which should be replaced by their native equivalents.
411
     */
412 14
    public function html_entity_decode(string $text) : string
413
    {
414 14
        return html_entity_decode($text, ENT_COMPAT, $this->_current_charset);
415
    }
416
}
417