Completed
Push — master ( 474902...a16483 )
by Andreas
17:23
created

midcom_services_i18n::convert_to_utf8()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 1
dl 0
loc 6
ccs 0
cts 4
cp 0
crap 6
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
use Symfony\Component\Translation\Translator;
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
        foreach ($request->getLanguages() as $lang) {
122
            // we can't use strings like en_US, so we only use the first two characters
123 2
            $lang = substr($lang, 0, 2);
124 2
            if ($this->set_language($lang)) {
125 2
                return true;
126
            }
127
        }
128
        return false;
129
    }
130
131 1
    public function get_translator(array $prefixes) : Translator
132
    {
133 1
        $locale = $this->get_current_language();
134 1
        $translator = new Translator($locale);
135 1
        foreach ($prefixes as $prefix) {
136 1
            $translator->addResource('xlf', $prefix . $locale . '.xlf', $locale);
137
        }
138 1
        return $translator;
139
    }
140
141
    /**
142
     * Set output character set.
143
     *
144
     * @param string $charset    Charset name.
145
     */
146
    public function set_charset(string $charset)
147
    {
148
        $this->_current_charset = strtolower($charset);
149
    }
150
151
    /**
152
     * Set output language.
153
     *
154
     * This will set the character encoding to the language's default
155
     * encoding and will also set the system locale to the one
156
     * specified in the language database.
157
     *
158
     * If you want another character encoding as the default one, you
159
     * have to override it manually using midcom_services_i18n::set_charset()
160
     * after calling this method.
161
     *
162
     * @param string $lang    Language ISO 639-1 code
163
     */
164 3
    public function set_language(string $lang) : bool
165
    {
166 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...
167
            debug_add("Language {$lang} not found.", MIDCOM_LOG_ERROR);
168
            return false;
169
        }
170
171 3
        $this->_current_language = $lang;
172
173 3
        setlocale(LC_ALL, $lang);
174 3
        if (Intl::isExtensionLoaded()) {
175 3
            Locale::setDefault($lang);
176
        }
177
178 3
        foreach ($this->_obj_l10n as $object) {
179
            $object->set_language($lang);
180
        }
181 3
        return true;
182
    }
183
184
    /**
185
     * Set the fallback language.
186
     *
187
     * @param string $lang    Language name.
188
     */
189
    public function set_fallback_language(string $lang)
190
    {
191
        $this->_fallback_language = $lang;
192
        foreach ($this->_obj_l10n as $object) {
193
            $object->set_fallback_language($lang);
194
        }
195
    }
196
197
    /**
198
     * Returns the current language code
199
     *
200
     * @return string
201
     */
202 378
    public function get_current_language()
203
    {
204 378
        return $this->_current_language;
205
    }
206
207
    /**
208
     * Returns the current fallback language code
209
     *
210
     * @return string
211
     */
212 52
    public function get_fallback_language()
213
    {
214 52
        return $this->_fallback_language;
215
    }
216
217
    /**
218
     * Returns the current character set
219
     *
220
     * @return string
221
     */
222 103
    public function get_current_charset()
223
    {
224 103
        return $this->_current_charset;
225
    }
226
227
    /**
228
     * Returns a l10n class instance which can be used to
229
     * access the localization data of the current component.
230
     *
231
     * If loading failed, midcom_error is thrown, otherwise the l10n
232
     * db cache is populated accordingly.
233
     *
234
     * @see midcom_services_i18n_l10n
235
     * @param string $component    The component for which to retrieve a string database.
236
     * @param string $database    The string table to retrieve from the component's locale directory.
237
     */
238 399
    public function get_l10n(string $component = 'midcom', string $database = 'default') : midcom_services_i18n_l10n
239
    {
240 399
        $cacheid = "{$component}/{$database}";
241
242 399
        if (!array_key_exists($cacheid, $this->_obj_l10n)) {
243 6
            $obj = new midcom_services_i18n_l10n($component, $database);
244 6
            $obj->set_language($this->_current_language);
245 6
            $obj->set_fallback_language($this->_fallback_language);
246 6
            $this->_obj_l10n[$cacheid] = $obj;
247
        }
248
249 399
        return $this->_obj_l10n[$cacheid];
250
    }
251
252
    /**
253
     * Returns a translated string using the l10n database specified in the function
254
     * arguments.
255
     *
256
     * @param string $stringid The string to translate.
257
     * @param string $component    The component for which to retrieve a string database. If omitted, this defaults to the
258
     *     current component (out of the component context).
259
     * @param string $database    The string table to retrieve from the component's locale directory. If omitted, the 'default'
260
     *     database is used.
261
     * @see midcom_services_i18n_l10n::get()
262
     */
263 359
    public function get_string(string $stringid, $component = null, string $database = 'default') : string
264
    {
265 359
        if ($component === null) {
266 5
            $component = midcom_core_context::get()->get_key(MIDCOM_CONTEXT_COMPONENT) ?? 'midcom';
267
        }
268
269 359
        return $this->get_l10n($component, $database)->get($stringid);
270
    }
271
272
    /**
273
     * This is a shortcut for echo $this->get_string(...);.
274
     *
275
     * To keep the naming stable with the actual l10n class, this is not called
276
     * echo_string (Zend won't allow $l10n->echo().)
277
     *
278
     * @param string $stringid The string to translate.
279
     * @param string $component    The component for which to retrieve a string database. If omitted, this defaults to the
280
     *     current component (out of the component context).
281
     * @param string $database    The string table to retrieve from the component's locale directory. If omitted, the 'default'
282
     *     database is used.
283
     * @see midcom_services_i18n_l10n::get()
284
     * @see get_string()
285
     */
286
    public function show_string(string $stringid, $component = null, string $database = 'default')
287
    {
288
        echo $this->get_string($stringid, $component, $database);
289
    }
290
291
    /**
292
     * This is a calling wrapper to the iconv library.
293
     *
294
     * See the PHP iconv() function for the exact parameter definitions.
295
     *
296
     * @param string $source_charset The charset to convert from.
297
     * @param string $destination_charset The charset to convert to.
298
     * @param string $string The string to convert.
299
     * @return mixed The converted string or false on any error.
300
     */
301
    private function iconv(string $source_charset, string $destination_charset, string $string)
302
    {
303
        $result = @iconv($source_charset, $destination_charset, $string);
304
        if ($result === false && !empty($string)) {
305
            debug_add("Iconv returned failed to convert a string, returning an empty string.", MIDCOM_LOG_WARN);
306
            debug_print_r("Tried to convert this string from {$source_charset} to {$destination_charset}:", $string);
307
            midcom::get()->debug->log_php_error(MIDCOM_LOG_WARN);
308
            return false;
309
        }
310
        return $result;
311
    }
312
313
    /**
314
     * Convert a string assumed to be in the currently active charset to UTF8.
315
     *
316
     * @param string $string The string to convert
317
     * @return string The string converted to UTF-8
318
     */
319
    public function convert_to_utf8(string $string)
320
    {
321
        if ($this->_current_charset == 'utf-8') {
322
            return $string;
323
        }
324
        return $this->iconv($this->_current_charset, 'utf-8', $string);
325
    }
326
327
    /**
328
     * Convert a string assumed to be in UTF-8 to the currently active charset.
329
     *
330
     * @param string $string The string to convert
331
     * @return string The string converted to the current charset
332
     */
333 372
    public function convert_from_utf8(string $string)
334
    {
335 372
        if ($this->_current_charset == 'utf-8') {
336 372
            return $string;
337
        }
338
        return $this->iconv('utf-8', $this->_current_charset, $string);
339
    }
340
341
    /**
342
     * Converts the given string to the current site charset.
343
     *
344
     * @param string $string The string to convert.
345
     * @return string The converted string.
346
     */
347
    public function convert_to_current_charset(string $string)
348
    {
349
        $charset = mb_detect_encoding($string, "UTF-8, UTF-7, ASCII, ISO-8859-15");
350
        debug_add("mb_detect_encoding got {$charset}");
351
        return $this->iconv($charset, $this->_current_charset, $string);
352
    }
353
354
    /**
355
     * Wrapped html_entity_decode call
356
     *
357
     * @param string $text The text with HTML entities, which should be replaced by their native equivalents.
358
     */
359 14
    public function html_entity_decode(string $text) : string
360
    {
361 14
        return html_entity_decode($text, ENT_COMPAT, $this->_current_charset);
362
    }
363
}
364