Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
 
3
/**
4
 +-----------------------------------------------------------------------+
5
 | This file is part of the Roundcube Webmail client                     |
6
 |                                                                       |
7
 | Copyright (C) The Roundcube Dev Team                                  |
8
 | Copyright (C) Kolab Systems AG                                        |
9
 |                                                                       |
10
 | Licensed under the GNU General Public License version 3 or            |
11
 | any later version with exceptions for skins & plugins.                |
12
 | See the README file for a full license statement.                     |
13
 |                                                                       |
14
 | PURPOSE:                                                              |
15
 |   Utility class providing common functions                            |
16
 +-----------------------------------------------------------------------+
17
 | Author: Thomas Bruederli <roundcube@gmail.com>                        |
18
 | Author: Aleksander Machniak <alec@alec.pl>                            |
19
 +-----------------------------------------------------------------------+
20
*/
21
 
22
/**
23
 * Utility class providing common functions
24
 *
25
 * @package    Framework
26
 * @subpackage Utils
27
 */
28
class rcube_utils
29
{
30
    // define constants for input reading
31
    const INPUT_GET    = 1;
32
    const INPUT_POST   = 2;
33
    const INPUT_COOKIE = 4;
34
    const INPUT_GP     = 3; // GET + POST
35
    const INPUT_GPC    = 7; // GET + POST + COOKIE
36
 
37
 
38
    /**
39
     * A wrapper for PHP's explode() that does not throw a warning
40
     * when the separator does not exist in the string
41
     *
42
     * @param string $separator Separator string
43
     * @param string $string    The string to explode
44
     *
45
     * @return array Exploded string. Still an array if there's no separator in the string
46
     */
47
    public static function explode($separator, $string)
48
    {
49
        if (strpos($string, $separator) !== false) {
50
            return explode($separator, $string);
51
        }
52
 
53
        return [$string, null];
54
    }
55
 
56
    /**
57
     * Helper method to set a cookie with the current path and host settings
58
     *
59
     * @param string $name      Cookie name
60
     * @param string $value     Cookie value
61
     * @param int    $exp       Expiration time
62
     * @param bool   $http_only HTTP Only
63
     */
64
    public static function setcookie($name, $value, $exp = 0, $http_only = true)
65
    {
66
        if (headers_sent()) {
67
            return;
68
        }
69
 
70
        $attrib             = session_get_cookie_params();
71
        $attrib['expires']  = $exp;
72
        $attrib['secure']   = $attrib['secure'] || self::https_check();
73
        $attrib['httponly'] = $http_only;
74
 
75
        // session_get_cookie_params() return includes 'lifetime' but setcookie() does not use it, instead it uses 'expires'
76
        unset($attrib['lifetime']);
77
 
78
        setcookie($name, $value, $attrib);
79
    }
80
 
81
    /**
82
     * E-mail address validation.
83
     *
84
     * @param string $email     Email address
85
     * @param bool   $dns_check True to check dns
86
     *
87
     * @return bool True on success, False if address is invalid
88
     */
89
    public static function check_email($email, $dns_check = true)
90
    {
91
        // Check for invalid (control) characters
92
        if (preg_match('/\p{Cc}/u', $email)) {
93
            return false;
94
        }
95
 
96
        // Check for length limit specified by RFC 5321 (#1486453)
97
        if (strlen($email) > 254) {
98
            return false;
99
        }
100
 
101
        $pos = strrpos($email, '@');
102
        if (!$pos) {
103
            return false;
104
        }
105
 
106
        $domain_part = substr($email, $pos + 1);
107
        $local_part  = substr($email, 0, $pos);
108
 
109
        // quoted-string, make sure all backslashes and quotes are
110
        // escaped
111
        if (substr($local_part, 0, 1) == '"') {
112
            $local_quoted = preg_replace('/\\\\(\\\\|\")/','', substr($local_part, 1, -1));
113
            if (preg_match('/\\\\|"/', $local_quoted)) {
114
                return false;
115
            }
116
        }
117
        // dot-atom portion, make sure there's no prohibited characters
118
        else if (preg_match('/(^\.|\.\.|\.$)/', $local_part)
119
            || preg_match('/[\\ ",:;<>@]/', $local_part)
120
        ) {
121
            return false;
122
        }
123
 
124
        // Validate domain part
125
        if (preg_match('/^\[((IPv6:[0-9a-f:.]+)|([0-9.]+))\]$/i', $domain_part, $matches)) {
126
            return self::check_ip(preg_replace('/^IPv6:/i', '', $matches[1])); // valid IPv4 or IPv6 address
127
        }
128
        else {
129
            // If not an IP address
130
            $domain_array = explode('.', $domain_part);
131
            // Not enough parts to be a valid domain
132
            if (count($domain_array) < 2) {
133
                return false;
134
            }
135
 
136
            foreach ($domain_array as $part) {
137
                if (!preg_match('/^((xn--)?([A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9])|([A-Za-z0-9]))$/', $part)) {
138
                    return false;
139
                }
140
            }
141
 
142
            // last domain part (allow extended TLD)
143
            $last_part = array_pop($domain_array);
144
            if (strpos($last_part, 'xn--') !== 0
145
                && (preg_match('/[^a-zA-Z0-9]/', $last_part) || preg_match('/^[0-9]+$/', $last_part))
146
            ) {
147
                return false;
148
            }
149
 
150
            $rcube = rcube::get_instance();
151
 
152
            if (!$dns_check || !function_exists('checkdnsrr') || !$rcube->config->get('email_dns_check')) {
153
                return true;
154
            }
155
 
156
            // Check DNS record(s)
157
            // Note: We can't use ANY (#6581)
158
            foreach (['A', 'MX', 'CNAME', 'AAAA'] as $type) {
159
                if (checkdnsrr($domain_part, $type)) {
160
                    return true;
161
                }
162
            }
163
        }
164
 
165
        return false;
166
    }
167
 
168
    /**
169
     * Validates IPv4 or IPv6 address
170
     *
171
     * @param string $ip IP address in v4 or v6 format
172
     *
173
     * @return bool True if the address is valid
174
     */
175
    public static function check_ip($ip)
176
    {
177
        return filter_var($ip, FILTER_VALIDATE_IP) !== false;
178
    }
179
 
180
    /**
181
     * Replacing specials characters to a specific encoding type
182
     *
183
     * @param string $str      Input string
184
     * @param string $enctype  Encoding type: text|html|xml|js|url
185
     * @param string $mode     Replace mode for tags: show|remove|strict
186
     * @param bool   $newlines Convert newlines
187
     *
188
     * @return string The quoted string
189
     */
190
    public static function rep_specialchars_output($str, $enctype = '', $mode = '', $newlines = true)
191
    {
192
        static $html_encode_arr = false;
193
        static $js_rep_table    = false;
194
        static $xml_rep_table   = false;
195
 
196
        if (!is_string($str)) {
197
            $str = strval($str);
198
        }
199
 
200
        // encode for HTML output
201
        if ($enctype == 'html') {
202
            if (!$html_encode_arr) {
203
                $html_encode_arr = get_html_translation_table(HTML_SPECIALCHARS);
204
                unset($html_encode_arr['?']);
205
            }
206
 
207
            $encode_arr = $html_encode_arr;
208
 
209
            if ($mode == 'remove') {
210
                $str = strip_tags($str);
211
            }
212
            else if ($mode != 'strict') {
213
                // don't replace quotes and html tags
214
                $ltpos = strpos($str, '<');
215
                if ($ltpos !== false && strpos($str, '>', $ltpos) !== false) {
216
                    unset($encode_arr['"']);
217
                    unset($encode_arr['<']);
218
                    unset($encode_arr['>']);
219
                    unset($encode_arr['&']);
220
                }
221
            }
222
 
223
            $out = strtr($str, $encode_arr);
224
 
225
            return $newlines ? nl2br($out) : $out;
226
        }
227
 
228
        // if the replace tables for XML and JS are not yet defined
229
        if ($js_rep_table === false) {
230
            $js_rep_table = $xml_rep_table = [];
231
            $xml_rep_table['&'] = '&amp;';
232
 
233
            // can be increased to support more charsets
234
            for ($c=160; $c<256; $c++) {
235
                $xml_rep_table[chr($c)] = "&#$c;";
236
            }
237
 
238
            $xml_rep_table['"'] = '&quot;';
239
            $js_rep_table['"']  = '\\"';
240
            $js_rep_table["'"]  = "\\'";
241
            $js_rep_table["\\"] = "\\\\";
242
            // Unicode line and paragraph separators (#1486310)
243
            $js_rep_table[chr(hexdec('E2')).chr(hexdec('80')).chr(hexdec('A8'))] = '&#8232;';
244
            $js_rep_table[chr(hexdec('E2')).chr(hexdec('80')).chr(hexdec('A9'))] = '&#8233;';
245
        }
246
 
247
        // encode for javascript use
248
        if ($enctype == 'js') {
249
            return preg_replace(["/\r?\n/", "/\r/", '/<\\//'], ['\n', '\n', '<\\/'], strtr($str, $js_rep_table));
250
        }
251
 
252
        // encode for plaintext
253
        if ($enctype == 'text') {
254
            return str_replace("\r\n", "\n", $mode == 'remove' ? strip_tags($str) : $str);
255
        }
256
 
257
        if ($enctype == 'url') {
258
            return rawurlencode($str);
259
        }
260
 
261
        // encode for XML
262
        if ($enctype == 'xml') {
263
            return strtr($str, $xml_rep_table);
264
        }
265
 
266
        // no encoding given -> return original string
267
        return $str;
268
    }
269
 
270
    /**
271
     * Read input value and make sure it is a string.
272
     *
273
     * @param string $fname      Field name to read
274
     * @param int    $source     Source to get value from (see self::INPUT_*)
275
     * @param bool   $allow_html Allow HTML tags in field value
276
     * @param string $charset    Charset to convert into
277
     *
278
     * @return string Request parameter value
279
     * @see self::get_input_value()
280
     */
281
    public static function get_input_string($fname, $source, $allow_html = false, $charset = null)
282
    {
283
        $value = self::get_input_value($fname, $source, $allow_html, $charset);
284
 
285
        return is_string($value) ? $value : '';
286
    }
287
 
288
    /**
289
     * Read request parameter value and convert it for internal use
290
     * Performs stripslashes() and charset conversion if necessary
291
     *
292
     * @param string $fname      Field name to read
293
     * @param int    $source     Source to get value from (see self::INPUT_*)
294
     * @param bool   $allow_html Allow HTML tags in field value
295
     * @param string $charset    Charset to convert into
296
     *
297
     * @return string|array|null Request parameter value or NULL if not set
298
     */
299
    public static function get_input_value($fname, $source, $allow_html = false, $charset = null)
300
    {
301
        $value = null;
302
 
303
        if (($source & self::INPUT_GET) && isset($_GET[$fname])) {
304
            $value = $_GET[$fname];
305
        }
306
 
307
        if (($source & self::INPUT_POST) && isset($_POST[$fname])) {
308
            $value = $_POST[$fname];
309
        }
310
 
311
        if (($source & self::INPUT_COOKIE) && isset($_COOKIE[$fname])) {
312
            $value = $_COOKIE[$fname];
313
        }
314
 
315
        return self::parse_input_value($value, $allow_html, $charset);
316
    }
317
 
318
    /**
319
     * Parse/validate input value. See self::get_input_value()
320
     * Performs stripslashes() and charset conversion if necessary
321
     *
322
     * @param string $value      Input value
323
     * @param bool   $allow_html Allow HTML tags in field value
324
     * @param string $charset    Charset to convert into
325
     *
326
     * @return string Parsed value
327
     */
328
    public static function parse_input_value($value, $allow_html = false, $charset = null)
329
    {
330
        if (empty($value)) {
331
            return $value;
332
        }
333
 
334
        if (is_array($value)) {
335
            foreach ($value as $idx => $val) {
336
                $value[$idx] = self::parse_input_value($val, $allow_html, $charset);
337
            }
338
 
339
            return $value;
340
        }
341
 
342
        // remove HTML tags if not allowed
343
        if (!$allow_html) {
344
            $value = strip_tags($value);
345
        }
346
 
347
        $rcube          = rcube::get_instance();
348
        $output_charset = is_object($rcube->output) ? $rcube->output->get_charset() : null;
349
 
350
        // remove invalid characters (#1488124)
351
        if ($output_charset == 'UTF-8') {
352
            $value = rcube_charset::clean($value);
353
        }
354
 
355
        // convert to internal charset
356
        if ($charset && $output_charset) {
357
            $value = rcube_charset::convert($value, $output_charset, $charset);
358
        }
359
 
360
        return $value;
361
    }
362
 
363
    /**
364
     * Convert array of request parameters (prefixed with _)
365
     * to a regular array with non-prefixed keys.
366
     *
367
     * @param int    $mode       Source to get value from (GPC)
368
     * @param string $ignore     PCRE expression to skip parameters by name
369
     * @param bool   $allow_html Allow HTML tags in field value
370
     *
371
     * @return array Hash array with all request parameters
372
     */
373
    public static function request2param($mode = null, $ignore = 'task|action', $allow_html = false)
374
    {
375
        $out = [];
376
        $src = $mode == self::INPUT_GET ? $_GET : ($mode == self::INPUT_POST ? $_POST : $_REQUEST);
377
 
378
        foreach (array_keys($src) as $key) {
379
            $fname = $key[0] == '_' ? substr($key, 1) : $key;
380
            if ($ignore && !preg_match('/^(' . $ignore . ')$/', $fname)) {
381
                $out[$fname] = self::get_input_value($key, $mode, $allow_html);
382
            }
383
        }
384
 
385
        return $out;
386
    }
387
 
388
    /**
389
     * Convert the given string into a valid HTML identifier
390
     * Same functionality as done in app.js with rcube_webmail.html_identifier()
391
     *
392
     * @param string $str    String input
393
     * @param bool   $encode Use base64 encoding
394
     *
395
     * @return string Valid HTML identifier
396
     */
397
    public static function html_identifier($str, $encode = false)
398
    {
399
        if ($encode) {
400
            return rtrim(strtr(base64_encode($str), '+/', '-_'), '=');
401
        }
402
 
403
        return asciiwords($str, true, '_');
404
    }
405
 
406
    /**
407
     * Replace all css definitions with #container [def]
408
     * and remove css-inlined scripting, make position style safe
409
     *
410
     * @param string $source       CSS source code
411
     * @param string $container_id Container ID to use as prefix
412
     * @param bool   $allow_remote Allow remote content
413
     * @param string $prefix       Prefix to be added to id/class identifier
414
     *
415
     * @return string Modified CSS source
416
     */
417
    public static function mod_css_styles($source, $container_id, $allow_remote = false, $prefix = '')
418
    {
419
        $source   = self::xss_entity_decode($source);
420
 
1441 ariadna 421
        // No @import allowed
422
        // TODO: We should just remove it, not invalidate the whole content
423
        if (stripos($source, '@import') !== false) {
1 efrain 424
            return '/* evil! */';
425
        }
426
 
1441 ariadna 427
        // Incomplete style expression
428
        if (strpos($source, '{') === false) {
429
            return '/* invalid! */';
430
        }
1 efrain 431
 
1441 ariadna 432
        // To prevent from a double-escaping tricks we consider a script with
433
        // any escape sequences (after de-escaping them above) an evil script.
434
        // This probably catches many valid scripts, but we\'re on the safe side.
435
        if (preg_match('/\\\[0-9a-fA-F]{2}/', $source)) {
436
            return '/* evil! */';
437
        }
438
 
1 efrain 439
        // remove html comments
440
        $source = preg_replace('/(^\s*<\!--)|(-->\s*$)/m', '', $source);
441
 
1441 ariadna 442
        $url_callback = static function ($url) use ($allow_remote) {
443
            if (strpos($url, 'data:image') === 0) {
444
                return $url;
445
            }
446
            if ($allow_remote && preg_match('|^https?://[a-z0-9/._+-]+$|i', $url)) {
447
                return $url;
448
            }
449
        };
450
 
451
        $last_pos = 0;
452
        $replacements = new rcube_string_replacer();
453
 
1 efrain 454
        // cut out all contents between { and }
455
        while (($pos = strpos($source, '{', $last_pos)) && ($pos2 = strpos($source, '}', $pos))) {
456
            $nested = strpos($source, '{', $pos+1);
457
            if ($nested && $nested < $pos2) { // when dealing with nested blocks (e.g. @media), take the inner one
458
                $pos = $nested;
459
            }
460
            $length = $pos2 - $pos - 1;
461
            $styles = substr($source, $pos+1, $length);
1441 ariadna 462
            $styles = self::sanitize_css_block($styles, $url_callback);
1 efrain 463
 
1441 ariadna 464
            $key      = $replacements->add(strlen($styles) ? " {$styles} " : '');
1 efrain 465
            $repl     = $replacements->get_replacement($key);
466
            $source   = substr_replace($source, $repl, $pos+1, $length);
467
            $last_pos = $pos2 - ($length - strlen($repl));
468
        }
469
 
470
        // add #container to each tag selector and prefix to id/class identifiers
471
        if ($container_id || $prefix) {
472
            // Exclude rcube_string_replacer pattern matches, this is needed
473
            // for cases like @media { body { position: fixed; } } (#5811)
474
            $excl     = '(?!' . substr($replacements->pattern, 1, -1) . ')';
475
            $regexp   = '/(^\s*|,\s*|\}\s*|\{\s*)(' . $excl . ':?[a-z0-9\._#\*\[][a-z0-9\._:\(\)#=~ \[\]"\|\>\+\$\^-]*)/im';
476
            $callback = function($matches) use ($container_id, $prefix) {
477
                $replace = $matches[2];
478
 
479
                if (stripos($replace, ':root') === 0) {
480
                    $replace = substr($replace, 5);
481
                }
482
 
483
                if ($prefix) {
484
                    $replace = str_replace(['.', '#'], [".$prefix", "#$prefix"], $replace);
485
                }
486
 
487
                if ($container_id) {
488
                    $replace = "#$container_id " . $replace;
489
                }
490
 
491
                // Remove redundant spaces (for simpler testing)
492
                $replace = preg_replace('/\s+/', ' ', $replace);
493
 
494
                return str_replace($matches[2], $replace, $matches[0]);
495
            };
496
 
497
            $source = preg_replace_callback($regexp, $callback, $source);
498
        }
499
 
500
        // replace body definition because we also stripped off the <body> tag
501
        if ($container_id) {
502
            $regexp = '/#' . preg_quote($container_id, '/') . '\s+body/i';
503
            $source = preg_replace($regexp, "#$container_id", $source);
504
        }
505
 
506
        // put block contents back in
507
        $source = $replacements->resolve($source);
508
 
509
        return $source;
510
    }
511
 
512
    /**
1441 ariadna 513
     * Parse and sanitize single CSS block
514
     *
515
     * @param string    $styles       CSS styles block
516
     * @param ?callable $url_callback URL validator callback
517
     *
518
     * @return string
519
     */
520
    public static function sanitize_css_block($styles, $url_callback = null)
521
    {
522
        $output = [];
523
 
524
        // check every css rule in the style block...
525
        foreach (self::parse_css_block($styles) as $rule) {
526
            $property = $rule[0];
527
            $value = $rule[1];
528
 
529
            if ($property == 'page') {
530
                // Remove 'page' attributes (#7604)
531
                continue;
532
            } elseif ($property == 'position' && strcasecmp($value, 'fixed') === 0) {
533
                // Convert position:fixed to position:absolute (#5264)
534
                $value = 'absolute';
535
            } elseif (preg_match('/expression|image-set/i', $value)) {
536
                continue;
537
            } else {
538
                $value = '';
539
                foreach (self::explode_css_property_block($rule[1]) as $val) {
540
                    if ($url_callback && preg_match('/^url\s*\(/i', $val)) {
541
                        if (preg_match('/^url\s*\(\s*[\'"]?([^\'"\)]*)[\'"]?\s*\)/iu', $val, $match)) {
542
                            if ($url = $url_callback($match[1])) {
543
                                $value .= ' url(' . $url . ')';
544
                            }
545
                        }
546
                    } else {
547
                        // whitelist ?
548
                        $value .= ' ' . $val;
549
 
550
                        // #1488535: Fix size units, so width:800 would be changed to width:800px
551
                        if ($val
552
                            && preg_match('/^(left|right|top|bottom|width|height)/i', $property)
553
                            && preg_match('/^[0-9]+$/', $val)
554
                        ) {
555
                            $value .= 'px';
556
                        }
557
                    }
558
                }
559
            }
560
 
561
            if (strlen($value)) {
562
                $output[] = $property . ': ' . trim($value);
563
            }
564
        }
565
 
566
        return count($output) > 0 ? implode('; ', $output) . ';' : '';
567
    }
568
 
569
    /**
1 efrain 570
     * Explode css style. Property names will be lower-cased and trimmed.
571
     * Values will be trimmed. Invalid entries will be skipped.
572
     *
573
     * @param string $style CSS style
574
     *
575
     * @return array List of CSS rule pairs, e.g. [['color', 'red'], ['top', '0']]
576
     */
577
    public static function parse_css_block($style)
578
    {
579
        $pos = 0;
580
 
581
        // first remove comments
582
        while (($pos = strpos($style, '/*', $pos)) !== false) {
583
            $end = strpos($style, '*/', $pos+2);
584
 
585
            if ($end === false) {
586
                $style = substr($style, 0, $pos);
587
            }
588
            else {
589
                $style = substr_replace($style, '', $pos, $end - $pos + 2);
590
            }
591
        }
592
 
593
        // Replace new lines with spaces
594
        $style = preg_replace('/[\r\n]+/', ' ', $style);
595
 
596
        $style  = trim($style);
597
        $length = strlen($style);
598
        $result = [];
599
        $pos    = 0;
600
 
601
        while ($pos < $length && ($colon_pos = strpos($style, ':', $pos))) {
602
            // Property name
603
            $name = strtolower(trim(substr($style, $pos, $colon_pos - $pos)));
604
 
605
            // get the property value
606
            $q = $s = false;
607
            for ($i = $colon_pos + 1; $i < $length; $i++) {
608
                if (($style[$i] == "\"" || $style[$i] == "'") && ($i == 0 || $style[$i-1] != "\\")) {
609
                    if ($q == $style[$i]) {
610
                        $q = false;
611
                    }
612
                    else if ($q === false) {
613
                        $q = $style[$i];
614
                    }
615
                }
616
                else if ($style[$i] == "(" && !$q && ($i == 0 || $style[$i-1] != "\\")) {
617
                    $q = "(";
618
                }
619
                else if ($style[$i] == ")" && $q == "(" && $style[$i-1] != "\\") {
620
                    $q = false;
621
                }
622
 
623
                if ($q === false && (($s = $style[$i] == ';') || $i == $length - 1)) {
624
                    break;
625
                }
626
            }
627
 
628
            $value_length = $i - $colon_pos - ($s ? 1 : 0);
629
            $value        = trim(substr($style, $colon_pos + 1, $value_length));
630
 
631
            if (strlen($name) && !preg_match('/[^a-z-]/', $name) && strlen($value) && $value !== ';') {
632
                $result[] = [$name, $value];
633
            }
634
 
635
            $pos = $i + 1;
636
        }
637
 
638
        return $result;
639
    }
640
 
641
    /**
1441 ariadna 642
     * Explode css style value
643
     *
644
     * @param string $style CSS style
645
     *
646
     * @return array List of CSS values
647
     */
648
    public static function explode_css_property_block($style)
649
    {
650
        $style = preg_replace('/\s+/', ' ', $style);
651
        $result = [];
652
        $strlen = strlen($style);
653
        $q = false;
654
 
655
        // explode value
656
        for ($p = $i = 0; $i < $strlen; $i++) {
657
            if (($style[$i] == '"' || $style[$i] == "'") && ($i == 0 || $style[$i - 1] != '\\')) {
658
                if ($q == $style[$i]) {
659
                    $q = false;
660
                } elseif (!$q) {
661
                    $q = $style[$i];
662
                }
663
            }
664
 
665
            if (!$q && $style[$i] == ' ' && ($i == 0 || !preg_match('/[,\(]/', $style[$i - 1]))) {
666
                $result[] = substr($style, $p, $i - $p);
667
                $p = $i + 1;
668
            }
669
        }
670
 
671
        $result[] = (string) substr($style, $p);
672
 
673
        return $result;
674
    }
675
 
676
    /**
1 efrain 677
     * Generate CSS classes from mimetype and filename extension
678
     *
679
     * @param string $mimetype Mimetype
680
     * @param string $filename Filename
681
     *
682
     * @return string CSS classes separated by space
683
     */
684
    public static function file2class($mimetype, $filename)
685
    {
686
        $mimetype = strtolower($mimetype);
687
        $filename = strtolower($filename);
688
 
689
        list($primary, $secondary) = rcube_utils::explode('/', $mimetype);
690
 
691
        $classes = [$primary ?: 'unknown'];
692
 
693
        if (!empty($secondary)) {
694
            $classes[] = $secondary;
695
        }
696
 
697
        if (preg_match('/\.([a-z0-9]+)$/', $filename, $m)) {
698
            if (!in_array($m[1], $classes)) {
699
                $classes[] = $m[1];
700
            }
701
        }
702
 
703
        return implode(' ', $classes);
704
    }
705
 
706
    /**
707
     * Decode escaped entities used by known XSS exploits.
708
     * See http://downloads.securityfocus.com/vulnerabilities/exploits/26800.eml for examples
709
     *
710
     * @param string $content CSS content to decode
711
     *
712
     * @return string Decoded string
713
     */
714
    public static function xss_entity_decode($content)
715
    {
716
        $callback = function($matches) { return chr(hexdec($matches[1])); };
717
 
718
        $out = html_entity_decode(html_entity_decode($content));
719
        $out = trim(preg_replace('/(^<!--|-->$)/', '', trim($out)));
720
        $out = preg_replace_callback('/\\\([0-9a-f]{2,6})\s*/i', $callback, $out);
721
        $out = preg_replace('/\\\([^0-9a-f])/i', '\\1', $out);
722
        $out = preg_replace('#/\*.*\*/#Ums', '', $out);
723
        $out = strip_tags($out);
724
 
725
        return $out;
726
    }
727
 
728
    /**
729
     * Check if we can process not exceeding memory_limit
730
     *
731
     * @param int $need Required amount of memory
732
     *
733
     * @return bool True if memory won't be exceeded, False otherwise
734
     */
735
    public static function mem_check($need)
736
    {
737
        $mem_limit = parse_bytes(ini_get('memory_limit'));
738
        $memory    = function_exists('memory_get_usage') ? memory_get_usage() : 16*1024*1024; // safe value: 16MB
739
 
740
        return $mem_limit > 0 && $memory + $need > $mem_limit ? false : true;
741
    }
742
 
743
    /**
744
     * Check if working in SSL mode
745
     *
746
     * @param int  $port      HTTPS port number
747
     * @param bool $use_https Enables 'use_https' option checking
748
     *
749
     * @return bool True in SSL mode, False otherwise
750
     */
751
    public static function https_check($port = null, $use_https = true)
752
    {
753
        if (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) != 'off') {
754
            return true;
755
        }
756
 
757
        if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO'])
758
            && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https'
759
            && self::check_proxy_whitelist_ip()
760
        ) {
761
            return true;
762
        }
763
 
764
        if ($port && isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == $port) {
765
            return true;
766
        }
767
 
768
        if ($use_https && rcube::get_instance()->config->get('use_https')) {
769
            return true;
770
        }
771
 
772
        return false;
773
    }
774
 
775
    /**
776
     * Check if the reported REMOTE_ADDR is in the 'proxy_whitelist' config option
777
     */
778
    public static function check_proxy_whitelist_ip() {
779
        return in_array($_SERVER['REMOTE_ADDR'], (array) rcube::get_instance()->config->get('proxy_whitelist', []));
780
    }
781
 
782
    /**
783
     * Replaces hostname variables.
784
     *
785
     * @param string $name Hostname
786
     * @param string $host Optional IMAP hostname
787
     *
788
     * @return string Hostname
789
     */
790
    public static function parse_host($name, $host = '')
791
    {
792
        if (!is_string($name)) {
793
            return $name;
794
        }
795
 
796
        // %n - host
797
        $n = self::server_name();
798
        // %t - host name without first part, e.g. %n=mail.domain.tld, %t=domain.tld
799
        // If %n=domain.tld then %t=domain.tld as well (remains valid)
800
        $t = preg_replace('/^[^.]+\.(?![^.]+$)/', '', $n);
801
        // %d - domain name without first part (up to domain.tld)
802
        $d = preg_replace('/^[^.]+\.(?![^.]+$)/', '', self::server_name('HTTP_HOST'));
803
        // %h - IMAP host
804
        $h = !empty($_SESSION['storage_host']) ? $_SESSION['storage_host'] : $host;
805
        // %z - IMAP domain without first part, e.g. %h=imap.domain.tld, %z=domain.tld
806
        // If %h=domain.tld then %z=domain.tld as well (remains valid)
807
        $z = preg_replace('/^[^.]+\.(?![^.]+$)/', '', $h);
808
        // %s - domain name after the '@' from e-mail address provided at login screen.
809
        //      Returns FALSE if an invalid email is provided
810
        $s = '';
811
        if (strpos($name, '%s') !== false) {
812
            $user_email = self::idn_to_ascii(self::get_input_value('_user', self::INPUT_POST));
813
            $matches    = preg_match('/(.*)@([a-z0-9\.\-\[\]\:]+)/i', $user_email, $s);
814
            if ($matches < 1 || filter_var($s[1]."@".$s[2], FILTER_VALIDATE_EMAIL) === false) {
815
                return false;
816
            }
817
            $s = $s[2];
818
        }
819
 
820
        return str_replace(['%n', '%t', '%d', '%h', '%z', '%s'], [$n, $t, $d, $h, $z, $s], $name);
821
    }
822
 
823
    /**
824
     * Parse host specification URI.
825
     *
826
     * @param string $host       Host URI
827
     * @param int    $plain_port Plain port number
828
     * @param int    $ssl_port   SSL port number
829
     *
830
     * @return array An array with three elements (hostname, scheme, port)
831
     */
832
    public static function parse_host_uri($host, $plain_port = null, $ssl_port = null)
833
    {
834
        if (preg_match('#^(unix|ldapi)://#i', $host, $matches)) {
835
            return [$host, $matches[1], -1];
836
        }
837
 
838
        $url    = parse_url($host);
839
        $port   = $plain_port;
840
        $scheme = null;
841
 
842
        if (!empty($url['host'])) {
843
            $host   = $url['host'];
844
            $scheme = $url['scheme'] ?? null;
845
 
846
            if (!empty($url['port'])) {
847
                $port = $url['port'];
848
            }
849
            else if (
850
                $scheme
851
                && $ssl_port
852
                && ($scheme === 'ssl' || ($scheme != 'tls' && $scheme[strlen($scheme) - 1] === 's'))
853
            ) {
854
                // assign SSL port to ssl://, imaps://, ldaps://, but not tls://
855
                $port = $ssl_port;
856
            }
857
        }
858
 
859
        return [$host, $scheme, $port];
860
    }
861
 
862
    /**
863
     * Returns the server name after checking it against trusted hostname patterns.
864
     *
865
     * Returns 'localhost' and logs a warning when the hostname is not trusted.
866
     *
867
     * @param string $type       The $_SERVER key, e.g. 'HTTP_HOST', Default: 'SERVER_NAME'.
868
     * @param bool   $strip_port Strip port from the host name
869
     *
870
     * @return string Server name
871
     */
872
    public static function server_name($type = null, $strip_port = true)
873
    {
874
        if (!$type) {
875
            $type = 'SERVER_NAME';
876
        }
877
 
878
        $name     = $_SERVER[$type] ?? '';
879
        $rcube    = rcube::get_instance();
880
        $patterns = (array) $rcube->config->get('trusted_host_patterns');
881
 
882
        if (!empty($name)) {
883
            if ($strip_port) {
884
                $name = preg_replace('/:\d+$/', '', $name);
885
            }
886
 
887
            if (empty($patterns)) {
888
                return $name;
889
            }
890
 
891
            foreach ($patterns as $pattern) {
892
                // the pattern might be a regular expression or just a host/domain name
893
                if (preg_match('/[^a-zA-Z0-9.:-]/', $pattern)) {
894
                    if (preg_match("/$pattern/", $name)) {
895
                        return $name;
896
                    }
897
                }
898
                else if (strtolower($name) === strtolower($pattern)) {
899
                    return $name;
900
                }
901
            }
902
 
903
            $rcube->raise_error([
904
                    'file' => __FILE__, 'line' => __LINE__,
905
                    'message' => "Specified host is not trusted. Using 'localhost'."
906
                ]
907
                , true, false
908
            );
909
        }
910
 
911
        return 'localhost';
912
    }
913
 
914
    /**
915
     * Returns remote IP address and forwarded addresses if found
916
     *
917
     * @return string Remote IP address(es)
918
     */
919
    public static function remote_ip()
920
    {
921
        $address = $_SERVER['REMOTE_ADDR'] ?? '';
922
 
923
        // append the NGINX X-Real-IP header, if set
924
        if (!empty($_SERVER['HTTP_X_REAL_IP']) && $_SERVER['HTTP_X_REAL_IP'] != $address) {
925
            $remote_ip[] = 'X-Real-IP: ' . $_SERVER['HTTP_X_REAL_IP'];
926
        }
927
 
928
        // append the X-Forwarded-For header, if set
929
        if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
930
            $remote_ip[] = 'X-Forwarded-For: ' . $_SERVER['HTTP_X_FORWARDED_FOR'];
931
        }
932
 
933
        if (!empty($remote_ip)) {
934
            $address .= ' (' . implode(',', $remote_ip) . ')';
935
        }
936
 
937
        return $address;
938
    }
939
 
940
    /**
941
     * Returns the real remote IP address
942
     *
943
     * @return string Remote IP address
944
     */
945
    public static function remote_addr()
946
    {
947
        // Check if any of the headers are set first to improve performance
948
        if (!empty($_SERVER['HTTP_X_FORWARDED_FOR']) || !empty($_SERVER['HTTP_X_REAL_IP'])) {
949
            $proxy_whitelist = (array) rcube::get_instance()->config->get('proxy_whitelist', []);
950
            if (in_array($_SERVER['REMOTE_ADDR'], $proxy_whitelist)) {
951
                if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
952
                    foreach (array_reverse(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])) as $forwarded_ip) {
953
                        $forwarded_ip = trim($forwarded_ip);
954
                        if (!in_array($forwarded_ip, $proxy_whitelist)) {
955
                            return $forwarded_ip;
956
                        }
957
                    }
958
                }
959
 
960
                if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
961
                    return $_SERVER['HTTP_X_REAL_IP'];
962
                }
963
            }
964
        }
965
 
966
        if (!empty($_SERVER['REMOTE_ADDR'])) {
967
            return $_SERVER['REMOTE_ADDR'];
968
        }
969
 
970
        return '';
971
    }
972
 
973
    /**
974
     * Read a specific HTTP request header.
975
     *
976
     * @param string $name Header name
977
     *
978
     * @return string|null Header value or null if not available
979
     */
980
    public static function request_header($name)
981
    {
982
        if (function_exists('apache_request_headers')) {
983
            $headers = apache_request_headers();
984
            $key     = strtoupper($name);
985
        }
986
        else {
987
            $headers = $_SERVER;
988
            $key     = 'HTTP_' . strtoupper(strtr($name, '-', '_'));
989
        }
990
 
991
        if (!empty($headers)) {
992
            $headers = array_change_key_case($headers, CASE_UPPER);
993
 
994
            return $headers[$key] ?? null;
995
        }
996
    }
997
 
998
    /**
999
     * Explode quoted string
1000
     *
1001
     * @param string $delimiter Delimiter expression string for preg_match()
1002
     * @param string $string    Input string
1003
     *
1004
     * @return array String items
1005
     */
1006
    public static function explode_quoted_string($delimiter, $string)
1007
    {
1008
        $result = [];
1009
        $strlen = strlen($string);
1010
 
1011
        for ($q=$p=$i=0; $i < $strlen; $i++) {
1012
            if ($string[$i] == "\"" && (!isset($string[$i-1]) || $string[$i-1] != "\\")) {
1013
                $q = $q ? false : true;
1014
            }
1015
            else if (!$q && preg_match("/$delimiter/", $string[$i])) {
1016
                $result[] = substr($string, $p, $i - $p);
1017
                $p = $i + 1;
1018
            }
1019
        }
1020
 
1021
        $result[] = (string) substr($string, $p);
1022
 
1023
        return $result;
1024
    }
1025
 
1026
    /**
1027
     * Improved equivalent to strtotime()
1028
     *
1029
     * @param string       $date     Date string
1030
     * @param DateTimeZone $timezone Timezone to use for DateTime object
1031
     *
1032
     * @return int Unix timestamp
1033
     */
1034
    public static function strtotime($date, $timezone = null)
1035
    {
1036
        $date   = self::clean_datestr($date);
1037
        $tzname = $timezone ? ' ' . $timezone->getName() : '';
1038
 
1039
        // unix timestamp
1040
        if (is_numeric($date)) {
1041
            return (int) $date;
1042
        }
1043
 
1044
        // It can be very slow when provided string is not a date and very long
1045
        if (strlen($date) > 128) {
1046
            $date = substr($date, 0, 128);
1047
        }
1048
 
1049
        // if date parsing fails, we have a date in non-rfc format.
1050
        // remove token from the end and try again
1051
        while (($ts = @strtotime($date . $tzname)) === false || $ts < 0) {
1052
            if (($pos = strrpos($date, ' ')) === false) {
1053
                break;
1054
            }
1055
 
1056
            $date = rtrim(substr($date, 0, $pos));
1057
        }
1058
 
1059
        return (int) $ts;
1060
    }
1061
 
1062
    /**
1063
     * Date parsing function that turns the given value into a DateTime object
1064
     *
1065
     * @param string       $date     Date string
1066
     * @param DateTimeZone $timezone Timezone to use for DateTime object
1067
     *
1068
     * @return DateTime|false DateTime object or False on failure
1069
     */
1070
    public static function anytodatetime($date, $timezone = null)
1071
    {
1072
        if ($date instanceof DateTime) {
1073
            return $date;
1074
        }
1075
 
1076
        $dt   = false;
1077
        $date = self::clean_datestr($date);
1078
 
1079
        // try to parse string with DateTime first
1080
        if (!empty($date)) {
1081
            try {
1082
                $_date = preg_match('/^[0-9]+$/', $date) ? "@$date" : $date;
1083
                $dt    = $timezone ? new DateTime($_date, $timezone) : new DateTime($_date);
1084
            }
1085
            catch (Exception $e) {
1086
                // ignore
1087
            }
1088
        }
1089
 
1090
        // try our advanced strtotime() method
1091
        if (!$dt && ($timestamp = self::strtotime($date, $timezone))) {
1092
            try {
1093
                $dt = new DateTime("@".$timestamp);
1094
                if ($timezone) {
1095
                    $dt->setTimezone($timezone);
1096
                }
1097
            }
1098
            catch (Exception $e) {
1099
                // ignore
1100
            }
1101
        }
1102
 
1103
        return $dt;
1104
    }
1105
 
1106
    /**
1107
     * Clean up date string for strtotime() input
1108
     *
1109
     * @param string $date Date string
1110
     *
1111
     * @return string Date string
1112
     */
1113
    public static function clean_datestr($date)
1114
    {
1115
        $date = trim((string) $date);
1116
 
1117
        // check for MS Outlook vCard date format YYYYMMDD
1118
        if (preg_match('/^([12][90]\d\d)([01]\d)([0123]\d)$/', $date, $m)) {
1119
            return sprintf('%04d-%02d-%02d 00:00:00', intval($m[1]), intval($m[2]), intval($m[3]));
1120
        }
1121
 
1122
        // Clean malformed data
1123
        $date = preg_replace(
1124
            [
1125
                '/\(.*\)/',                                 // remove RFC comments
1126
                '/GMT\s*([+-][0-9]+)/',                     // support non-standard "GMTXXXX" literal
1127
                '/[^a-z0-9\x20\x09:\/\.+-]/i',              // remove any invalid characters
1128
                '/\s*(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s*/i',   // remove weekday names
1129
            ],
1130
            [
1131
                '',
1132
                '\\1',
1133
                '',
1134
                '',
1135
            ],
1136
            $date
1137
        );
1138
 
1139
        $date = trim($date);
1140
 
1141
        // try to fix dd/mm vs. mm/dd discrepancy, we can't do more here
1142
        if (preg_match('/^(\d{1,2})[.\/-](\d{1,2})[.\/-](\d{4})(\s.*)?$/', $date, $m)) {
1143
            $mdy   = $m[2] > 12 && $m[1] <= 12;
1144
            $day   = $mdy ? $m[2] : $m[1];
1145
            $month = $mdy ? $m[1] : $m[2];
1146
            $date  = sprintf('%04d-%02d-%02d%s', $m[3], $month, $day, $m[4] ?? ' 00:00:00');
1147
        }
1148
        // I've found that YYYY.MM.DD is recognized wrong, so here's a fix
1149
        else if (preg_match('/^(\d{4})\.(\d{1,2})\.(\d{1,2})(\s.*)?$/', $date, $m)) {
1150
            $date  = sprintf('%04d-%02d-%02d%s', $m[1], $m[2], $m[3], $m[4] ?? ' 00:00:00');
1151
        }
1152
 
1153
        return $date;
1154
    }
1155
 
1156
    /**
1157
     * Turns the given date-only string in defined format into YYYY-MM-DD format.
1158
     *
1159
     * Supported formats: 'Y/m/d', 'Y.m.d', 'd-m-Y', 'd/m/Y', 'd.m.Y', 'j.n.Y'
1160
     *
1161
     * @param string $date   Date string
1162
     * @param string $format Input date format
1163
     *
1164
     * @return string Date string in YYYY-MM-DD format, or the original string
1165
     *                if format is not supported
1166
     */
1167
    public static function format_datestr($date, $format)
1168
    {
1169
        $format_items = preg_split('/[.-\/\\\\]/', $format);
1170
        $date_items   = preg_split('/[.-\/\\\\]/', $date);
1171
        $iso_format   = '%04d-%02d-%02d';
1172
 
1173
        if (count($format_items) == 3 && count($date_items) == 3) {
1174
            if ($format_items[0] == 'Y') {
1175
                $date = sprintf($iso_format, $date_items[0], $date_items[1], $date_items[2]);
1176
            }
1177
            else if (strpos('dj', $format_items[0]) !== false) {
1178
                $date = sprintf($iso_format, $date_items[2], $date_items[1], $date_items[0]);
1179
            }
1180
            else if (strpos('mn', $format_items[0]) !== false) {
1181
                $date = sprintf($iso_format, $date_items[2], $date_items[0], $date_items[1]);
1182
            }
1183
        }
1184
 
1185
        return $date;
1186
    }
1187
 
1188
    /**
1189
     * Wrapper for idn_to_ascii with support for e-mail address.
1190
     *
1191
     * Warning: Domain names may be lowercase'd.
1192
     * Warning: An empty string may be returned on invalid domain.
1193
     *
1194
     * @param string $str Decoded e-mail address
1195
     *
1196
     * @return string Encoded e-mail address
1197
     */
1198
    public static function idn_to_ascii($str)
1199
    {
1200
        return self::idn_convert($str, true);
1201
    }
1202
 
1203
    /**
1204
     * Wrapper for idn_to_utf8 with support for e-mail address
1205
     *
1206
     * @param string $str Decoded e-mail address
1207
     *
1208
     * @return string Encoded e-mail address
1209
     */
1210
    public static function idn_to_utf8($str)
1211
    {
1212
        return self::idn_convert($str, false);
1213
    }
1214
 
1215
    /**
1216
     * Convert a string to ascii or utf8 (using IDNA standard)
1217
     *
1218
     * @param string $input  Decoded e-mail address
1219
     * @param bool   $is_utf Convert by idn_to_ascii if true and idn_to_utf8 if false
1220
     *
1221
     * @return string Encoded e-mail address
1222
     */
1223
    public static function idn_convert($input, $is_utf = false)
1224
    {
1225
        if ($at = strpos($input, '@')) {
1226
            $user   = substr($input, 0, $at);
1227
            $domain = substr($input, $at + 1);
1228
        }
1229
        else {
1230
            $user   = '';
1231
            $domain = $input;
1232
        }
1233
 
1234
        // Note that in PHP 7.2/7.3 calling idn_to_* functions with default arguments
1235
        // throws a warning, so we have to set the variant explicitly (#6075)
1236
        $variant = INTL_IDNA_VARIANT_UTS46;
1237
        $options = 0;
1238
 
1239
        // Because php-intl extension lowercases domains and return false
1240
        // on invalid input (#6224), we skip conversion when not needed
1241
 
1242
        if ($is_utf) {
1243
            if (preg_match('/[^\x20-\x7E]/', $domain)) {
1244
                $options = IDNA_NONTRANSITIONAL_TO_ASCII;
1245
                $domain  = idn_to_ascii($domain, $options, $variant);
1246
            }
1247
        }
1248
        else if (preg_match('/(^|\.)xn--/i', $domain)) {
1249
            $options = IDNA_NONTRANSITIONAL_TO_UNICODE;
1250
            $domain  = idn_to_utf8($domain, $options, $variant);
1251
        }
1252
 
1253
        if ($domain === false) {
1254
            return '';
1255
        }
1256
 
1257
        return $at ? $user . '@' . $domain : $domain;
1258
    }
1259
 
1260
    /**
1261
     * Split the given string into word tokens
1262
     *
1263
     * @param string $str     Input to tokenize
1264
     * @param int    $minlen  Minimum length of a single token
1265
     *
1266
     * @return array List of tokens
1267
     */
1268
    public static function tokenize_string($str, $minlen = 2)
1269
    {
1270
        if (!is_string($str)) {
1271
            return [];
1272
        }
1273
 
1274
        $expr = ['/[\s;,"\'\/+-]+/ui', '/(\d)[-.\s]+(\d)/u'];
1275
        $repl = [' ', '\\1\\2'];
1276
 
1277
        if ($minlen > 1) {
1278
            $minlen--;
1279
            $expr[] = "/(^|\s+)\w{1,$minlen}(\s+|$)/u";
1280
            $repl[] = ' ';
1281
        }
1282
 
1283
        $str = preg_replace($expr, $repl, $str);
1284
 
1285
        return is_string($str) ? array_filter(explode(" ", $str)) : [];
1286
    }
1287
 
1288
    /**
1289
     * Normalize the given string for fulltext search.
1290
     * Currently only optimized for ISO-8859-1 and ISO-8859-2 characters; to be extended
1291
     *
1292
     * @param string $str      Input string (UTF-8)
1293
     * @param bool   $as_array True to return list of words as array
1294
     * @param int    $minlen   Minimum length of tokens
1295
     *
1296
     * @return string|array Normalized string or a list of normalized tokens
1297
     */
1298
    public static function normalize_string($str, $as_array = false, $minlen = 2)
1299
    {
1300
        // replace 4-byte unicode characters with '?' character,
1301
        // these are not supported in default utf-8 charset on mysql,
1302
        // the chance we'd need them in searching is very low
1303
        $str = preg_replace('/('
1304
            . '\xF0[\x90-\xBF][\x80-\xBF]{2}'
1305
            . '|[\xF1-\xF3][\x80-\xBF]{3}'
1306
            . '|\xF4[\x80-\x8F][\x80-\xBF]{2}'
1307
            . ')/', '?', $str);
1308
 
1309
        // split by words
1310
        $arr = self::tokenize_string($str, $minlen);
1311
 
1312
        // detect character set
1313
        if (rcube_charset::convert(rcube_charset::convert($str, 'UTF-8', 'ISO-8859-1'), 'ISO-8859-1', 'UTF-8') == $str)  {
1314
            // ISO-8859-1 (or ASCII)
1315
            preg_match_all('/./u', 'äâàåáãæçéêëèïîìíñöôòøõóüûùúýÿ', $keys);
1316
            preg_match_all('/./',  'aaaaaaaceeeeiiiinoooooouuuuyy', $values);
1317
 
1318
            $mapping = array_combine($keys[0], $values[0]);
1319
            $mapping = array_merge($mapping, ['ß' => 'ss', 'ae' => 'a', 'oe' => 'o', 'ue' => 'u']);
1320
        }
1321
        else if (rcube_charset::convert(rcube_charset::convert($str, 'UTF-8', 'ISO-8859-2'), 'ISO-8859-2', 'UTF-8') == $str) {
1322
            // ISO-8859-2
1323
            preg_match_all('/./u', 'ąáâäćçčéęëěíîłľĺńňóôöŕřśšşťţůúűüźžżý', $keys);
1324
            preg_match_all('/./',  'aaaaccceeeeiilllnnooorrsssttuuuuzzzy', $values);
1325
 
1326
            $mapping = array_combine($keys[0], $values[0]);
1327
            $mapping = array_merge($mapping, ['ß' => 'ss', 'ae' => 'a', 'oe' => 'o', 'ue' => 'u']);
1328
        }
1329
 
1330
        foreach ($arr as $i => $part) {
1331
            $part = mb_strtolower($part);
1332
 
1333
            if (!empty($mapping)) {
1334
                $part = strtr($part, $mapping);
1335
            }
1336
 
1337
            $arr[$i] = $part;
1338
        }
1339
 
1340
        return $as_array ? $arr : implode(' ', $arr);
1341
    }
1342
 
1343
    /**
1344
     * Compare two strings for matching words (order not relevant)
1345
     *
1346
     * @param string $haystack Haystack
1347
     * @param string $needle   Needle
1348
     *
1349
     * @return bool True if match, False otherwise
1350
     */
1351
    public static function words_match($haystack, $needle)
1352
    {
1353
        $a_needle  = self::tokenize_string($needle, 1);
1354
        $_haystack = implode(' ', self::tokenize_string($haystack, 1));
1355
        $valid     = strlen($_haystack) > 0;
1356
        $hits      = 0;
1357
 
1358
        foreach ($a_needle as $w) {
1359
            if ($valid) {
1360
                if (stripos($_haystack, $w) !== false) {
1361
                    $hits++;
1362
                }
1363
            }
1364
            else if (stripos($haystack, $w) !== false) {
1365
                $hits++;
1366
            }
1367
        }
1368
 
1369
        return $hits >= count($a_needle);
1370
    }
1371
 
1372
    /**
1373
     * Parse commandline arguments into a hash array
1374
     *
1375
     * @param array $aliases Argument alias names
1376
     *
1377
     * @return array Argument values hash
1378
     */
1379
    public static function get_opt($aliases = [])
1380
    {
1381
        $args = [];
1382
        $bool = [];
1383
 
1384
        // find boolean (no value) options
1385
        foreach ($aliases as $key => $alias) {
1386
            if ($pos = strpos($alias, ':')) {
1387
                $aliases[$key] = substr($alias, 0, $pos);
1388
                $bool[] = $key;
1389
                $bool[] = $aliases[$key];
1390
            }
1391
        }
1392
 
1393
        for ($i=1; $i < count($_SERVER['argv']); $i++) {
1394
            $arg   = $_SERVER['argv'][$i];
1395
            $value = true;
1396
            $key   = null;
1397
 
1398
            if (strlen($arg) && $arg[0] == '-') {
1399
                $key = preg_replace('/^-+/', '', $arg);
1400
                $sp  = strpos($arg, '=');
1401
 
1402
                if ($sp > 0) {
1403
                    $key   = substr($key, 0, $sp - 2);
1404
                    $value = substr($arg, $sp+1);
1405
                }
1406
                else if (in_array($key, $bool)) {
1407
                    $value = true;
1408
                }
1409
                else if (
1410
                    isset($_SERVER['argv'][$i + 1])
1411
                    && strlen($_SERVER['argv'][$i + 1])
1412
                    && $_SERVER['argv'][$i + 1][0] != '-'
1413
                ) {
1414
                    $value = $_SERVER['argv'][++$i];
1415
                }
1416
 
1417
                $args[$key] = is_string($value) ? preg_replace(['/^["\']/', '/["\']$/'], '', $value) : $value;
1418
            }
1419
            else {
1420
                $args[] = $arg;
1421
            }
1422
 
1423
            if (!empty($aliases[$key])) {
1424
                $alias = $aliases[$key];
1425
                $args[$alias] = $args[$key];
1426
            }
1427
        }
1428
 
1429
        return $args;
1430
    }
1431
 
1432
    /**
1433
     * Safe password prompt for command line
1434
     * from http://blogs.sitepoint.com/2009/05/01/interactive-cli-password-prompt-in-php/
1435
     *
1436
     * @param string $prompt Prompt text
1437
     *
1438
     * @return string Password
1439
     */
1440
    public static function prompt_silent($prompt = "Password:")
1441
    {
1442
        if (preg_match('/^win/i', PHP_OS)) {
1443
            $vbscript  = sys_get_temp_dir() . 'prompt_password.vbs';
1444
            $vbcontent = 'wscript.echo(InputBox("' . addslashes($prompt) . '", "", "password here"))';
1445
            file_put_contents($vbscript, $vbcontent);
1446
 
1447
            $command  = "cscript //nologo " . escapeshellarg($vbscript);
1448
            $password = rtrim(shell_exec($command));
1449
            unlink($vbscript);
1450
 
1451
            return $password;
1452
        }
1453
 
1454
        $command = "/usr/bin/env bash -c 'echo OK'";
1455
 
1456
        if (rtrim(shell_exec($command)) !== 'OK') {
1457
            echo $prompt;
1458
            $pass = trim(fgets(STDIN));
1459
            echo chr(8)."\r" . $prompt . str_repeat("*", strlen($pass))."\n";
1460
 
1461
            return $pass;
1462
        }
1463
 
1464
        $command  = "/usr/bin/env bash -c 'read -s -p \"" . addslashes($prompt) . "\" mypassword && echo \$mypassword'";
1465
        $password = rtrim(shell_exec($command));
1466
        echo "\n";
1467
 
1468
        return $password;
1469
    }
1470
 
1471
    /**
1472
     * Find out if the string content means true or false
1473
     *
1474
     * @param string $str Input value
1475
     *
1476
     * @return bool Boolean value
1477
     */
1478
    public static function get_boolean($str)
1479
    {
1480
        $str = strtolower((string) $str);
1481
 
1482
        return !in_array($str, ['false', '0', 'no', 'off', 'nein', ''], true);
1483
    }
1484
 
1485
    /**
1486
     * OS-dependent absolute path detection
1487
     *
1488
     * @param string $path File path
1489
     *
1490
     * @return bool True if the path is absolute, False otherwise
1491
     */
1492
    public static function is_absolute_path($path)
1493
    {
1494
        if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
1495
            return (bool) preg_match('!^[a-z]:[\\\\/]!i', $path);
1496
        }
1497
 
1498
        return isset($path[0]) && $path[0] == '/';
1499
    }
1500
 
1501
    /**
1502
     * Resolve relative URL
1503
     *
1504
     * @param string $url Relative URL
1505
     *
1506
     * @return string Absolute URL
1507
     */
1508
    public static function resolve_url($url)
1509
    {
1510
        // prepend protocol://hostname:port
1511
        if (!preg_match('|^https?://|', $url)) {
1512
            $schema       = 'http';
1513
            $default_port = 80;
1514
 
1515
            if (self::https_check()) {
1516
                $schema       = 'https';
1517
                $default_port = 443;
1518
            }
1519
 
1520
            $host = $_SERVER['HTTP_HOST'] ?? '';
1521
            $port = $_SERVER['SERVER_PORT'] ?? 0;
1522
 
1523
            $prefix = $schema . '://' . preg_replace('/:\d+$/', '', $host);
1524
            if ($port && $port != $default_port && $port != 80) {
1525
                $prefix .= ':' . $port;
1526
            }
1527
 
1528
            $url = $prefix . ($url[0] == '/' ? '' : '/') . $url;
1529
        }
1530
 
1531
        return $url;
1532
    }
1533
 
1534
    /**
1535
     * Generate a random string
1536
     *
1537
     * @param int  $length String length
1538
     * @param bool $raw    Return RAW data instead of ascii
1539
     *
1540
     * @return string The generated random string
1541
     */
1542
    public static function random_bytes($length, $raw = false)
1543
    {
1544
        // Use PHP7 true random generator
1545
        if ($raw) {
1546
            return random_bytes($length);
1547
        }
1548
 
1549
        $hextab  = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
1550
        $tabsize = strlen($hextab);
1551
 
1552
        $result = '';
1553
        while ($length-- > 0) {
1554
            $result .= $hextab[random_int(0, $tabsize - 1)];
1555
        }
1556
 
1557
        return $result;
1558
    }
1559
 
1560
    /**
1561
     * Convert binary data into readable form (containing a-zA-Z0-9 characters)
1562
     *
1563
     * @param string $input Binary input
1564
     *
1565
     * @return string Readable output (Base62)
1566
     * @deprecated since 1.3.1
1567
     */
1568
    public static function bin2ascii($input)
1569
    {
1570
        $hextab = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
1571
        $result = '';
1572
 
1573
        for ($x = 0; $x < strlen($input); $x++) {
1574
            $result .= $hextab[ord($input[$x]) % 62];
1575
        }
1576
 
1577
        return $result;
1578
    }
1579
 
1580
    /**
1581
     * Format current date according to specified format.
1582
     * This method supports microseconds (u).
1583
     *
1584
     * @param string $format Date format (default: 'd-M-Y H:i:s O')
1585
     *
1586
     * @return string Formatted date
1587
     */
1588
    public static function date_format($format = null)
1589
    {
1590
        if (empty($format)) {
1591
            $format = 'd-M-Y H:i:s O';
1592
        }
1593
 
1594
        if (strpos($format, 'u') !== false) {
1595
            $dt = number_format(microtime(true), 6, '.', '');
1596
 
1597
            try {
1598
                $date = date_create_from_format('U.u', $dt);
1599
                $date->setTimeZone(new DateTimeZone(date_default_timezone_get()));
1600
 
1601
                return $date->format($format);
1602
            }
1603
            catch (Exception $e) {
1604
                // ignore, fallback to date()
1605
            }
1606
        }
1607
 
1608
        return date($format);
1609
    }
1610
 
1611
    /**
1612
     * Parses socket options and returns options for specified hostname.
1613
     *
1614
     * @param array  &$options Configured socket options
1615
     * @param string $host     Hostname
1616
     */
1617
    public static function parse_socket_options(&$options, $host = null)
1618
    {
1619
        if (empty($host) || empty($options)) {
1620
            return;
1621
        }
1622
 
1623
        // get rid of schema and port from the hostname
1624
        $host_url = parse_url($host);
1625
        if (isset($host_url['host'])) {
1626
            $host = $host_url['host'];
1627
        }
1628
 
1629
        // find per-host options
1630
        if ($host && array_key_exists($host, $options)) {
1631
            $options = $options[$host];
1632
        }
1633
    }
1634
 
1635
    /**
1636
     * Get maximum upload size
1637
     *
1638
     * @return int Maximum size in bytes
1639
     */
1640
    public static function max_upload_size()
1641
    {
1642
        // find max filesize value
1643
        $max_filesize = parse_bytes(ini_get('upload_max_filesize'));
1644
        $max_postsize = parse_bytes(ini_get('post_max_size'));
1645
 
1646
        if ($max_postsize && $max_postsize < $max_filesize) {
1647
            $max_filesize = $max_postsize;
1648
        }
1649
 
1650
        return $max_filesize;
1651
    }
1652
 
1653
    /**
1654
     * Detect and log last PREG operation error
1655
     *
1656
     * @param array $error     Error data (line, file, code, message)
1657
     * @param bool  $terminate Stop script execution
1658
     *
1659
     * @return bool True on error, False otherwise
1660
     */
1661
    public static function preg_error($error = [], $terminate = false)
1662
    {
1663
        if (($preg_error = preg_last_error()) != PREG_NO_ERROR) {
1664
            $errstr = "PCRE Error: $preg_error.";
1665
 
1666
            if (function_exists('preg_last_error_msg')) {
1667
                $errstr .= ' ' . preg_last_error_msg();
1668
            }
1669
 
1670
            if ($preg_error == PREG_BACKTRACK_LIMIT_ERROR) {
1671
                $errstr .= " Consider raising pcre.backtrack_limit!";
1672
            }
1673
            if ($preg_error == PREG_RECURSION_LIMIT_ERROR) {
1674
                $errstr .= " Consider raising pcre.recursion_limit!";
1675
            }
1676
 
1677
            $error = array_merge(['code' => 620, 'line' => __LINE__, 'file' => __FILE__], $error);
1678
 
1679
            if (!empty($error['message'])) {
1680
                $error['message'] .= ' ' . $errstr;
1681
            }
1682
            else {
1683
                $error['message'] = $errstr;
1684
            }
1685
 
1686
            rcube::raise_error($error, true, $terminate);
1687
 
1688
            return true;
1689
        }
1690
 
1691
        return false;
1692
    }
1693
 
1694
    /**
1695
     * Generate a temporary file path in the Roundcube temp directory
1696
     *
1697
     * @param string $file_name String identifier for the type of temp file
1698
     * @param bool   $unique    Generate unique file names based on $file_name
1699
     * @param bool   $create    Create the temp file or not
1700
     *
1701
     * @return string temporary file path
1702
     */
1703
    public static function temp_filename($file_name, $unique = true, $create = true)
1704
    {
1705
        $temp_dir = rcube::get_instance()->config->get('temp_dir');
1706
 
1707
        // Fall back to system temp dir if configured dir is not writable
1708
        if (!is_writable($temp_dir)) {
1709
            $temp_dir = sys_get_temp_dir();
1710
        }
1711
 
1712
        // On Windows tempnam() uses only the first three characters of prefix so use uniqid() and manually add the prefix
1713
        // Full prefix is required for garbage collection to recognise the file
1714
        $temp_file = $unique ? str_replace('.', '', uniqid($file_name, true)) : $file_name;
1715
        $temp_path = unslashify($temp_dir) . '/' . RCUBE_TEMP_FILE_PREFIX . $temp_file;
1716
 
1717
        // Sanity check for unique file name
1718
        if ($unique && file_exists($temp_path)) {
1719
            return self::temp_filename($file_name, $unique, $create);
1720
        }
1721
 
1722
        // Create the file to prevent possible race condition like tempnam() does
1723
        if ($create) {
1724
            touch($temp_path);
1725
        }
1726
 
1727
        return $temp_path;
1728
    }
1729
 
1730
    /**
1731
     * Clean the subject from reply and forward prefix
1732
     *
1733
     * @param string $subject Subject to clean
1734
     * @param string $mode Mode of cleaning : reply, forward or both
1735
     *
1736
     * @return string Cleaned subject
1737
     */
1738
    public static function remove_subject_prefix($subject, $mode = 'both')
1739
    {
1740
        $config = rcmail::get_instance()->config;
1741
 
1742
        // Clean subject prefix for reply, forward or both
1743
        if ($mode == 'both') {
1744
            $reply_prefixes = $config->get('subject_reply_prefixes', ['Re:']);
1745
            $forward_prefixes = $config->get('subject_forward_prefixes', ['Fwd:', 'Fw:']);
1746
            $prefixes = array_merge($reply_prefixes, $forward_prefixes);
1747
        }
1748
        else if ($mode == 'reply') {
1749
            $prefixes = $config->get('subject_reply_prefixes', ['Re:']);
1750
            // replace (was: ...) (#1489375)
1751
            $subject = preg_replace('/\s*\([wW]as:[^\)]+\)\s*$/', '', $subject);
1752
        }
1753
        else if ($mode == 'forward') {
1754
            $prefixes = $config->get('subject_forward_prefixes', ['Fwd:', 'Fw:']);
1755
        }
1756
 
1757
        // replace Re:, Re[x]:, Re-x (#1490497)
1758
        $pieces = array_map(function($prefix) {
1759
            $prefix = strtolower(str_replace(':', '', $prefix));
1760
            return "$prefix:|$prefix\[\d\]:|$prefix-\d:";
1761
        }, $prefixes);
1762
        $pattern = '/^('.implode('|', $pieces).')\s*/i';
1763
        do {
1764
            $subject = preg_replace($pattern, '', $subject, -1, $count);
1765
        }
1766
        while ($count);
1767
 
1768
        return trim($subject);
1769
    }
1770
}