Passed
Push — master ( 73f0a8...2b6bea )
by Andreas
22:42
created

midcom_services_i18n::set_charset()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

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