1<?php
2
3/**
4 * strings.php
5 *
6 * This code provides various string manipulation functions that are
7 * used by the rest of the SquirrelMail code.
8 *
9 * @copyright 1999-2021 The SquirrelMail Project Team
10 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
11 * @version $Id: strings.php 14926 2021-08-25 03:33:09Z pdontthink $
12 * @package squirrelmail
13 */
14
15/**
16 * SquirrelMail version number -- DO NOT CHANGE
17 */
18global $version;
19$version = '1.4.23 [SVN]';
20
21/**
22 * SquirrelMail internal version number -- DO NOT CHANGE
23 * $sm_internal_version = array (release, major, minor)
24 */
25global $SQM_INTERNAL_VERSION;
26$SQM_INTERNAL_VERSION = array(1, 4, 23);
27
28/**
29 * There can be a circular issue with includes, where the $version string is
30 * referenced by the include of global.php, etc. before it's defined.
31 * For that reason, bring in global.php AFTER we define the version strings.
32 */
33require_once(SM_PATH . 'functions/global.php');
34
35if (file_exists(SM_PATH . 'plugins/compatibility/functions.php')) {
36    include_once(SM_PATH . 'plugins/compatibility/functions.php');
37}
38
39/**
40 * Wraps text at $wrap characters
41 *
42 * Has a problem with special HTML characters, so call this before
43 * you do character translation.
44 *
45 * Specifically, &#039 comes up as 5 characters instead of 1.
46 * This should not add newlines to the end of lines.
47 */
48function sqWordWrap(&$line, $wrap, $charset=null) {
49    global $languages, $squirrelmail_language;
50
51    if (isset($languages[$squirrelmail_language]['XTRA_CODE']) &&
52        function_exists($languages[$squirrelmail_language]['XTRA_CODE'])) {
53        if (mb_detect_encoding($line) != 'ASCII') {
54            $line = $languages[$squirrelmail_language]['XTRA_CODE']('wordwrap', $line, $wrap);
55            return;
56        }
57    }
58
59    preg_match('/^([\t >]*)([^\t >].*)?$/', $line, $regs);
60    $beginning_spaces = $regs[1];
61    if (isset($regs[2])) {
62        $words = explode(' ', $regs[2]);
63    } else {
64        $words = array();
65    }
66
67    $i = 0;
68    $line = $beginning_spaces;
69
70    while ($i < count($words)) {
71        /* Force one word to be on a line (minimum) */
72        $line .= $words[$i];
73        $line_len = strlen($beginning_spaces) + sq_strlen($words[$i],$charset) + 2;
74        if (isset($words[$i + 1]))
75            $line_len += sq_strlen($words[$i + 1],$charset);
76        $i ++;
77
78        /* Add more words (as long as they fit) */
79        while ($line_len < $wrap && $i < count($words)) {
80            $line .= ' ' . $words[$i];
81            $i++;
82            if (isset($words[$i]))
83                $line_len += sq_strlen($words[$i],$charset) + 1;
84            else
85                $line_len += 1;
86        }
87
88        /* Skip spaces if they are the first thing on a continued line */
89        while (!isset($words[$i]) && $i < count($words)) {
90            $i ++;
91        }
92
93        /* Go to the next line if we have more to process */
94        if ($i < count($words)) {
95            $line .= "\n";
96        }
97    }
98}
99
100/**
101 * Does the opposite of sqWordWrap()
102 * @param string body the text to un-wordwrap
103 * @return void
104 */
105function sqUnWordWrap(&$body) {
106    global $squirrelmail_language;
107
108    if ($squirrelmail_language == 'ja_JP') {
109        return;
110    }
111
112    $lines = explode("\n", $body);
113    $body = '';
114    $PreviousSpaces = '';
115    $cnt = count($lines);
116    for ($i = 0; $i < $cnt; $i ++) {
117        preg_match("/^([\t >]*)([^\t >].*)?$/", $lines[$i], $regs);
118        $CurrentSpaces = $regs[1];
119        if (isset($regs[2])) {
120            $CurrentRest = $regs[2];
121        } else {
122            $CurrentRest = '';
123        }
124
125        if ($i == 0) {
126            $PreviousSpaces = $CurrentSpaces;
127            $body = $lines[$i];
128        } else if (($PreviousSpaces == $CurrentSpaces) /* Do the beginnings match */
129                   && (strlen($lines[$i - 1]) > 65)    /* Over 65 characters long */
130                   && strlen($CurrentRest)) {          /* and there's a line to continue with */
131            $body .= ' ' . $CurrentRest;
132        } else {
133            $body .= "\n" . $lines[$i];
134            $PreviousSpaces = $CurrentSpaces;
135        }
136    }
137    $body .= "\n";
138}
139
140/**
141  * Truncates the given string so that it has at
142  * most $max_chars characters.  NOTE that a "character"
143  * may be a multibyte character, or (optionally), an
144  * HTML entity, so this function is different than
145  * using substr() or mb_substr().
146  *
147  * NOTE that if $elipses is given and used, the returned
148  *      number of characters will be $max_chars PLUS the
149  *      length of $elipses
150  *
151  * @param string  $string    The string to truncate
152  * @param int     $max_chars The maximum allowable characters
153  * @param string  $elipses   A string that will be added to
154  *                           the end of the truncated string
155  *                           (ONLY if it is truncated) (OPTIONAL;
156  *                           default not used)
157  * @param boolean $html_entities_as_chars Whether or not to keep
158  *                                        HTML entities together
159  *                                        (OPTIONAL; default ignore
160  *                                        HTML entities)
161  *
162  * @return string The truncated string
163  *
164  * @since 1.4.20 and 1.5.2 (replaced truncateWithEntities())
165  *
166  */
167function sm_truncate_string($string, $max_chars, $elipses='',
168                            $html_entities_as_chars=FALSE)
169{
170
171   // if the length of the string is less than
172   // the allowable number of characters, just
173   // return it as is (even if it contains any
174   // HTML entities, that would just make the
175   // actual length even smaller)
176   //
177   $actual_strlen = sq_strlen($string, 'auto');
178   if ($max_chars <= 0 || $actual_strlen <= $max_chars)
179      return $string;
180
181
182   // if needed, count the number of HTML entities in
183   // the string up to the maximum character limit,
184   // pushing that limit up for each entity found
185   //
186   $adjusted_max_chars = $max_chars;
187   if ($html_entities_as_chars)
188   {
189
190      // $loop_count is needed to prevent an endless loop
191      // which is caused by buggy mbstring versions that
192      // return 0 (zero) instead of FALSE in some rare
193      // cases.  Thanks, PHP.
194      // see: http://bugs.php.net/bug.php?id=52731
195      // also: tracker $3053349
196      //
197      $loop_count = 0;
198      $entity_pos = $entity_end_pos = -1;
199      while ($entity_end_pos + 1 < $actual_strlen
200          && ($entity_pos = sq_strpos($string, '&', $entity_end_pos + 1)) !== FALSE
201          && ($entity_end_pos = sq_strpos($string, ';', $entity_pos)) !== FALSE
202          && $entity_pos <= $adjusted_max_chars
203          && $loop_count++ < $max_chars)
204      {
205         $adjusted_max_chars += $entity_end_pos - $entity_pos;
206      }
207
208
209      // this isn't necessary because sq_substr() would figure this
210      // out anyway, but we can avoid a sq_substr() call and we
211      // know that we don't have to add an elipses (this is now
212      // an accurate comparison, since $adjusted_max_chars, like
213      // $actual_strlen, does not take into account HTML entities)
214      //
215      if ($actual_strlen <= $adjusted_max_chars)
216         return $string;
217
218   }
219
220
221   // get the truncated string
222   //
223   $truncated_string = sq_substr($string, 0, $adjusted_max_chars);
224
225
226   // return with added elipses
227   //
228   return $truncated_string . $elipses;
229
230}
231
232/**
233 * If $haystack is a full mailbox name and $needle is the mailbox
234 * separator character, returns the last part of the mailbox name.
235 *
236 * @param string haystack full mailbox name to search
237 * @param string needle the mailbox separator character
238 * @return string the last part of the mailbox name
239 */
240function readShortMailboxName($haystack, $needle) {
241
242    if ($needle == '') {
243        $elem = $haystack;
244    } else {
245        $parts = explode($needle, $haystack);
246        $elem = array_pop($parts);
247        while ($elem == '' && count($parts)) {
248            $elem = array_pop($parts);
249        }
250    }
251    return( $elem );
252}
253
254/**
255 * php_self
256 *
257 * Attempts to determine the path and filename and any arguments
258 * for the currently executing script.  This is usually found in
259 * $_SERVER['REQUEST_URI'], but some environments may differ, so
260 * this function tries to standardize this value.
261 *
262 * @since 1.2.3
263 * @return string The path, filename and any arguments for the
264 *                current script
265 */
266function php_self($with_query_string=TRUE) {
267
268    static $request_uri = '';
269    if (!empty($request_uri))
270        return ($with_query_string ? $request_uri : (strpos($request_uri, '?') !== FALSE ? substr($request_uri, 0, strpos($request_uri, '?')) : $request_uri));
271
272    // first try $_SERVER['PHP_SELF'], which seems most reliable
273    // (albeit it usually won't include the query string)
274    //
275    $request_uri = '';
276    if (!sqgetGlobalVar('PHP_SELF', $request_uri, SQ_SERVER)
277     || empty($request_uri)) {
278
279        // well, then let's try $_SERVER['REQUEST_URI']
280        //
281        $request_uri = '';
282        if (!sqgetGlobalVar('REQUEST_URI', $request_uri, SQ_SERVER)
283         || empty($request_uri)) {
284
285            // TODO: anyone have any other ideas?  maybe $_SERVER['SCRIPT_NAME']???
286            //
287            return '';
288        }
289
290    }
291
292    // we may or may not have any query arguments, depending on
293    // which environment variable was used above, and the PHP
294    // version, etc., so let's check for it now
295    //
296    $query_string = '';
297    if (strpos($request_uri, '?') === FALSE
298     && sqgetGlobalVar('QUERY_STRING', $query_string, SQ_SERVER)
299     && !empty($query_string)) {
300
301        $request_uri .= '?' . $query_string;
302    }
303
304    global $php_self_pattern, $php_self_replacement;
305    if (!empty($php_self_pattern))
306    $request_uri = preg_replace($php_self_pattern, $php_self_replacement, $request_uri);
307    return ($with_query_string ? $request_uri : (strpos($request_uri, '?') !== FALSE ? substr($request_uri, 0, strpos($request_uri, '?')) : $request_uri));
308
309}
310
311
312/**
313 * Find out where squirrelmail lives and try to be smart about it.
314 * The only problem would be when squirrelmail lives in directories
315 * called "src", "functions", or "plugins", but people who do that need
316 * to be beaten with a steel pipe anyway.
317 *
318 * @return string the base uri of squirrelmail installation.
319 */
320function sqm_baseuri(){
321    global $base_uri;
322    /**
323     * If it is in the session, just return it.
324     */
325    if (sqgetGlobalVar('base_uri',$base_uri,SQ_SESSION)){
326        return $base_uri;
327    }
328    $dirs = array('|src/.*|', '|plugins/.*|', '|functions/.*|');
329    $repl = array('', '', '');
330    $base_uri = preg_replace($dirs, $repl, php_self(FALSE));
331    return $base_uri;
332}
333
334/**
335 * get_location
336 *
337 * Determines the location to forward to, relative to your server.
338 * This is used in HTTP Location: redirects.
339 * If set, it uses $config_location_base as the first part of the URL,
340 * specifically, the protocol, hostname and port parts. The path is
341 * always autodetected.
342 *
343 * @return string the base url for this SquirrelMail installation
344 */
345function get_location () {
346
347    global $imap_server_type, $config_location_base,
348           $is_secure_connection, $sq_ignore_http_x_forwarded_headers;
349
350    /* Get the path, handle virtual directories */
351    $path = substr(php_self(FALSE), 0, strrpos(php_self(FALSE), '/'));
352
353    // proto+host+port are already set in config:
354    if ( !empty($config_location_base) ) {
355        // register it in the session just in case some plugin depends on this
356        sqsession_register($config_location_base . $path, 'sq_base_url');
357        return $config_location_base . $path ;
358    }
359    // we computed it before, get it from the session:
360    if ( sqgetGlobalVar('sq_base_url', $full_url, SQ_SESSION) ) {
361        return $full_url . $path;
362    }
363    // else: autodetect
364
365    /* Check if this is a HTTPS or regular HTTP request. */
366    $proto = 'http://';
367    if ($is_secure_connection)
368        $proto = 'https://';
369
370    /* Get the hostname from the Host header or server config. */
371    if ($sq_ignore_http_x_forwarded_headers
372     || !sqgetGlobalVar('HTTP_X_FORWARDED_HOST', $host, SQ_SERVER)
373     || empty($host)) {
374        if ( !sqgetGlobalVar('HTTP_HOST', $host, SQ_SERVER) || empty($host) ) {
375            if ( !sqgetGlobalVar('SERVER_NAME', $host, SQ_SERVER) || empty($host) ) {
376                $host = '';
377            }
378        }
379    }
380
381    $port = '';
382    if (strpos($host, ':') === FALSE) {
383        // Note: HTTP_X_FORWARDED_PROTO could be sent from the client and
384        //       therefore possibly spoofed/hackable - for now, the
385        //       administrator can tell SM to ignore this value by setting
386        //       $sq_ignore_http_x_forwarded_headers to boolean TRUE in
387        //       config/config_local.php, but in the future we may
388        //       want to default this to TRUE and make administrators
389        //       who use proxy systems turn it off (see 1.5.2+).
390        global $sq_ignore_http_x_forwarded_headers;
391        if ($sq_ignore_http_x_forwarded_headers
392         || !sqgetGlobalVar('HTTP_X_FORWARDED_PROTO', $forwarded_proto, SQ_SERVER))
393            $forwarded_proto = '';
394        if (sqgetGlobalVar('SERVER_PORT', $server_port, SQ_SERVER)) {
395            if (($server_port != 80 && $proto == 'http://') ||
396                ($server_port != 443 && $proto == 'https://' &&
397                 strcasecmp($forwarded_proto, 'https') !== 0)) {
398                $port = sprintf(':%d', $server_port);
399            }
400        }
401    }
402
403   /* this is a workaround for the weird macosx caching that
404      causes Apache to return 16080 as the port number, which causes
405      SM to bail */
406
407   if ($imap_server_type == 'macosx' && $port == ':16080') {
408        $port = '';
409   }
410
411   /* Fallback is to omit the server name and use a relative */
412   /* URI, although this is not RFC 2616 compliant.          */
413   $full_url = ($host ? $proto . $host . $port : '');
414   sqsession_register($full_url, 'sq_base_url');
415   return $full_url . $path;
416}
417
418
419/**
420 * Encrypts password
421 *
422 * These functions are used to encrypt the password before it is
423 * stored in a cookie. The encryption key is generated by
424 * OneTimePadCreate();
425 *
426 * @param string string the (password)string to encrypt
427 * @param string epad the encryption key
428 * @return string the base64-encoded encrypted password
429 */
430function OneTimePadEncrypt ($string, $epad) {
431    $pad = base64_decode($epad);
432
433    if (strlen($pad)>0) {
434        // make sure that pad is longer than string
435        while (strlen($string)>strlen($pad)) {
436            $pad.=$pad;
437        }
438    } else {
439        // FIXME: what should we do when $epad is not base64 encoded or empty.
440    }
441
442    $encrypted = '';
443    for ($i = 0; $i < strlen ($string); $i++) {
444        $encrypted .= chr (ord($string[$i]) ^ ord($pad[$i]));
445    }
446
447    return base64_encode($encrypted);
448}
449
450/**
451 * Decrypts a password from the cookie
452 *
453 * Decrypts a password from the cookie, encrypted by OneTimePadEncrypt.
454 * This uses the encryption key that is stored in the session.
455 *
456 * @param string string the string to decrypt
457 * @param string epad the encryption key from the session
458 * @return string the decrypted password
459 */
460function OneTimePadDecrypt ($string, $epad) {
461    $pad = base64_decode($epad);
462
463    if (strlen($pad)>0) {
464        // make sure that pad is longer than string
465        while (strlen($string)>strlen($pad)) {
466            $pad.=$pad;
467        }
468    } else {
469        // FIXME: what should we do when $epad is not base64 encoded or empty.
470    }
471
472    $encrypted = base64_decode ($string);
473    $decrypted = '';
474    for ($i = 0; $i < strlen ($encrypted); $i++) {
475        $decrypted .= chr (ord($encrypted[$i]) ^ ord($pad[$i]));
476    }
477
478    return $decrypted;
479}
480
481
482/**
483 * Randomizes the mt_rand() function.
484 *
485 * Toss this in strings or integers and it will seed the generator
486 * appropriately. With strings, it is better to get them long.
487 * Use md5() to lengthen smaller strings.
488 *
489 * @param mixed val a value to seed the random number generator
490 * @return void
491 */
492function sq_mt_seed($Val) {
493    /* if mt_getrandmax() does not return a 2^n - 1 number,
494       this might not work well.  This uses $Max as a bitmask. */
495    $Max = mt_getrandmax();
496
497    if (! is_int($Val)) {
498            $Val = crc32($Val);
499    }
500
501    if ($Val < 0) {
502        $Val *= -1;
503    }
504
505    if ($Val == 0) {
506        return;
507    }
508
509    mt_srand(($Val ^ mt_rand(0, $Max)) & $Max);
510}
511
512
513/**
514 * Init random number generator
515 *
516 * This function initializes the random number generator fairly well.
517 * It also only initializes it once, so you don't accidentally get
518 * the same 'random' numbers twice in one session.
519 *
520 * @return void
521 */
522function sq_mt_randomize() {
523    static $randomized;
524
525    if ($randomized) {
526        return;
527    }
528
529    /* Global. */
530    sqgetGlobalVar('REMOTE_PORT', $remote_port, SQ_SERVER);
531    sqgetGlobalVar('REMOTE_ADDR', $remote_addr, SQ_SERVER);
532    sq_mt_seed((int)((double) microtime() * 1000000));
533    sq_mt_seed(md5($remote_port . $remote_addr . getmypid()));
534
535    /* getrusage */
536    if (function_exists('getrusage')) {
537        /* Avoid warnings with Win32 */
538        $dat = @getrusage();
539        if (isset($dat) && is_array($dat)) {
540            $Str = '';
541            foreach ($dat as $k => $v)
542                {
543                    $Str .= $k . $v;
544                }
545            sq_mt_seed(md5($Str));
546        }
547    }
548
549    if(sqgetGlobalVar('UNIQUE_ID', $unique_id, SQ_SERVER)) {
550        sq_mt_seed(md5($unique_id));
551    }
552
553    $randomized = 1;
554}
555
556/**
557 * Creates encryption key
558 *
559 * Creates an encryption key for encrypting the password stored in the cookie.
560 * The encryption key itself is stored in the session.
561 *
562 * @param int length optional, length of the string to generate
563 * @return string the encryption key
564 */
565function OneTimePadCreate ($length=100) {
566    sq_mt_randomize();
567
568    $pad = '';
569    for ($i = 0; $i < $length; $i++) {
570        $pad .= chr(mt_rand(0,255));
571    }
572
573    return base64_encode($pad);
574}
575
576/**
577 * Returns a string showing the size of the message/attachment.
578 *
579 * @param int bytes the filesize in bytes
580 * @param int filesize_divisor the divisor we'll use (OPTIONAL; default 1024)
581 * @return string the filesize in human readable format
582 */
583function show_readable_size($bytes, $filesize_divisor=1024) {
584
585    $bytes /= $filesize_divisor;
586    $type = 'k';
587
588    if ($bytes / $filesize_divisor > 1) {
589        $bytes /= $filesize_divisor;
590        $type = 'M';
591    }
592
593    if ($bytes < 10) {
594        $bytes *= 10;
595        settype($bytes, 'integer');
596        $bytes /= 10;
597    } else {
598        settype($bytes, 'integer');
599    }
600
601    return $bytes . '<small>&nbsp;' . $type . '</small>';
602}
603
604/**
605 * Generates a random string from the caracter set you pass in
606 *
607 * @param int size the size of the string to generate
608 * @param string chars a string containing the characters to use
609 * @param int flags a flag to add a specific set to the characters to use:
610 *     Flags:
611 *       1 = add lowercase a-z to $chars
612 *       2 = add uppercase A-Z to $chars
613 *       4 = add numbers 0-9 to $chars
614 * @return string the random string
615 */
616function GenerateRandomString($size, $chars, $flags = 0) {
617    if ($flags & 0x1) {
618        $chars .= 'abcdefghijklmnopqrstuvwxyz';
619    }
620    if ($flags & 0x2) {
621        $chars .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
622    }
623    if ($flags & 0x4) {
624        $chars .= '0123456789';
625    }
626
627    if (($size < 1) || (strlen($chars) < 1)) {
628        return '';
629    }
630
631    sq_mt_randomize(); /* Initialize the random number generator */
632
633    $String = '';
634    $j = strlen( $chars ) - 1;
635    while (strlen($String) < $size) {
636        $String .= $chars[mt_rand(0, $j)];
637    }
638
639    return $String;
640}
641
642/**
643 * Escapes special characters for use in IMAP commands.
644 *
645 * @param string the string to escape
646 * @return string the escaped string
647 */
648function quoteimap($str) {
649    // FIXME use this performance improvement (not changing because this is STABLE branch): return str_replace(array('\\', '"'), array('\\\\', '\\"'), $str);
650    return preg_replace("/([\"\\\\])/", "\\\\$1", $str);
651}
652
653/**
654 * Trims array
655 *
656 * Trims every element in the array, ie. remove the first char of each element
657 * Obsolete: will probably removed soon
658 * @param array array the array to trim
659 * @obsolete
660 */
661function TrimArray(&$array) {
662    foreach ($array as $k => $v) {
663        global $$k;
664        if (is_array($$k)) {
665            foreach ($$k as $k2 => $v2) {
666                $$k[$k2] = substr($v2, 1);
667            }
668        } else {
669            $$k = substr($v, 1);
670        }
671
672        /* Re-assign back to array. */
673        $array[$k] = $$k;
674    }
675}
676
677/**
678 * Removes slashes from every element in the array
679 */
680function RemoveSlashes(&$array) {
681    foreach ($array as $k => $v) {
682        global $$k;
683        if (is_array($$k)) {
684            foreach ($$k as $k2 => $v2) {
685                $newArray[stripslashes($k2)] = stripslashes($v2);
686            }
687            $$k = $newArray;
688        } else {
689            $$k = stripslashes($v);
690        }
691
692        /* Re-assign back to the array. */
693        $array[$k] = $$k;
694    }
695}
696
697/**
698 * Create compose link
699 *
700 * Returns a link to the compose-page, taking in consideration
701 * the compose_in_new and javascript settings.
702 * @param string url the URL to the compose page
703 * @param string text the link text, default "Compose"
704 * @return string a link to the compose page
705 */
706function makeComposeLink($url, $text = null, $target='')
707{
708    global $compose_new_win,$javascript_on;
709
710    if(!$text) {
711        $text = _("Compose");
712    }
713
714
715    // if not using "compose in new window", make
716    // regular link and be done with it
717    if($compose_new_win != '1') {
718        return makeInternalLink($url, $text, $target);
719    }
720
721
722    // build the compose in new window link...
723
724
725    // if javascript is on, use onClick event to handle it
726    if($javascript_on) {
727        sqgetGlobalVar('base_uri', $base_uri, SQ_SESSION);
728        return '<a href="javascript:void(0)" onclick="comp_in_new(\''.$base_uri.$url.'\')">'. $text.'</a>';
729    }
730
731
732    // otherwise, just open new window using regular HTML
733    return makeInternalLink($url, $text, '_blank');
734
735}
736
737/**
738 * Print variable
739 *
740 * sm_print_r($some_variable, [$some_other_variable [, ...]]);
741 *
742 * Debugging function - does the same as print_r, but makes sure special
743 * characters are converted to htmlentities first.  This will allow
744 * values like <some@email.address> to be displayed.
745 * The output is wrapped in <<pre>> and <</pre>> tags.
746 *
747 * @return void
748 */
749function sm_print_r() {
750    ob_start();  // Buffer output
751    foreach(func_get_args() as $var) {
752        print_r($var);
753        echo "\n";
754    }
755    $buffer = ob_get_contents(); // Grab the print_r output
756    ob_end_clean();  // Silently discard the output & stop buffering
757    print '<pre>';
758    print htmlentities($buffer);
759    print '</pre>';
760}
761
762/**
763 * version of fwrite which checks for failure
764 */
765function sq_fwrite($fp, $string) {
766        // write to file
767        $count = @fwrite($fp,$string);
768        // the number of bytes written should be the length of the string
769        if($count != strlen($string)) {
770                return FALSE;
771        }
772
773        return $count;
774}
775/**
776 * Tests if string contains 8bit symbols.
777 *
778 * If charset is not set, function defaults to default_charset.
779 * $default_charset global must be set correctly if $charset is
780 * not used.
781 * @param string $string tested string
782 * @param string $charset charset used in a string
783 * @return bool true if 8bit symbols are detected
784 * @since 1.5.1 and 1.4.4
785 */
786function sq_is8bit($string,$charset='') {
787    global $default_charset;
788
789    if ($charset=='') $charset=$default_charset;
790
791    /**
792     * Don't use \240 in ranges. Sometimes RH 7.2 doesn't like it.
793     * Don't use \200-\237 for iso-8859-x charsets. This ranges
794     * stores control symbols in those charsets.
795     * Use preg_match instead of ereg in order to avoid problems
796     * with mbstring overloading
797     */
798    if (preg_match("/^iso-8859/i",$charset)) {
799        $needle='/\240|[\241-\377]/';
800    } else {
801        $needle='/[\200-\237]|\240|[\241-\377]/';
802    }
803    return preg_match("$needle",$string);
804}
805
806/**
807 * Function returns number of characters in string.
808 *
809 * Returned number might be different from number of bytes in string,
810 * if $charset is multibyte charset. Detection depends on mbstring
811 * functions. If mbstring does not support tested multibyte charset,
812 * vanilla string length function is used.
813 * @param string $str string
814 * @param string $charset charset
815 * @since 1.5.1 and 1.4.6
816 * @return integer number of characters in string
817 */
818function sq_strlen($string, $charset=NULL){
819
820   // NULL charset?  Just use strlen()
821   //
822   if (is_null($charset))
823      return strlen($string);
824
825
826   // use current character set?
827   //
828   if ($charset == 'auto')
829   {
830//FIXME: this may or may not be better as a session value instead of a global one
831      global $sq_string_func_auto_charset;
832      if (!isset($sq_string_func_auto_charset))
833      {
834         global $default_charset, $squirrelmail_language;
835         set_my_charset();
836         $sq_string_func_auto_charset = $default_charset;
837         if ($squirrelmail_language == 'ja_JP') $sq_string_func_auto_charset = 'euc-jp';
838      }
839      $charset = $sq_string_func_auto_charset;
840   }
841
842
843   // standardize character set name
844   //
845   $charset = strtolower($charset);
846
847
848/* ===== FIXME: this list is not used in 1.5.x, but if we need it, unless this differs between all our string function wrappers, we should store this info in the session
849   // only use mbstring with the following character sets
850   //
851   $sq_strlen_mb_charsets = array(
852      'utf-8',
853      'big5',
854      'gb2312',
855      'gb18030',
856      'euc-jp',
857      'euc-cn',
858      'euc-tw',
859      'euc-kr'
860   );
861
862
863   // now we can use mb_strlen() if needed
864   //
865   if (in_array($charset, $sq_strlen_mb_charsets)
866    && in_array($charset, sq_mb_list_encodings()))
867===== */
868//FIXME: is there any reason why this cannot be a static global array used by all string wrapper functions?
869   if (in_array($charset, sq_mb_list_encodings()))
870      return mb_strlen($string, $charset);
871
872
873   // else use normal strlen()
874   //
875   return strlen($string);
876
877}
878
879/**
880  * This is a replacement for PHP's strpos() that is
881  * multibyte-aware.
882  *
883  * @param string $haystack The string to search within
884  * @param string $needle   The substring to search for
885  * @param int    $offset   The offset from the beginning of $haystack
886  *                         from which to start searching
887  *                         (OPTIONAL; default none)
888  * @param string $charset  The charset of the given string.  A value of NULL
889  *                         here will force the use of PHP's standard strpos().
890  *                         (OPTIONAL; default is "auto", which indicates that
891  *                         the user's current charset should be used).
892  *
893  * @return mixed The integer offset of the next $needle in $haystack,
894  *               if found, or FALSE if not found
895  *
896  */
897function sq_strpos($haystack, $needle, $offset=0, $charset='auto')
898{
899
900   // NULL charset?  Just use strpos()
901   //
902   if (is_null($charset))
903      return strpos($haystack, $needle, $offset);
904
905
906   // use current character set?
907   //
908   if ($charset == 'auto')
909   {
910//FIXME: this may or may not be better as a session value instead of a global one
911      global $sq_string_func_auto_charset;
912      if (!isset($sq_string_func_auto_charset))
913      {
914         global $default_charset, $squirrelmail_language;
915         set_my_charset();
916         $sq_string_func_auto_charset = $default_charset;
917         if ($squirrelmail_language == 'ja_JP') $sq_string_func_auto_charset = 'euc-jp';
918      }
919      $charset = $sq_string_func_auto_charset;
920   }
921
922
923   // standardize character set name
924   //
925   $charset = strtolower($charset);
926
927
928/* ===== FIXME: this list is not used in 1.5.x, but if we need it, unless this differs between all our string function wrappers, we should store this info in the session
929   // only use mbstring with the following character sets
930   //
931   $sq_strpos_mb_charsets = array(
932      'utf-8',
933      'big5',
934      'gb2312',
935      'gb18030',
936      'euc-jp',
937      'euc-cn',
938      'euc-tw',
939      'euc-kr'
940   );
941
942
943   // now we can use mb_strpos() if needed
944   //
945   if (in_array($charset, $sq_strpos_mb_charsets)
946    && in_array($charset, sq_mb_list_encodings()))
947===== */
948//FIXME: is there any reason why this cannot be a static global array used by all string wrapper functions?
949   if (in_array($charset, sq_mb_list_encodings()))
950       return mb_strpos($haystack, $needle, $offset, $charset);
951
952
953   // else use normal strpos()
954   //
955   return strpos($haystack, $needle, $offset);
956
957}
958
959/**
960  * This is a replacement for PHP's substr() that is
961  * multibyte-aware.
962  *
963  * @param string $string  The string to operate upon
964  * @param int    $start   The offset at which to begin substring extraction
965  * @param int    $length  The number of characters after $start to return
966  *                        NOTE that if you need to specify a charset but
967  *                        want to achieve normal substr() behavior where
968  *                        $length is not specified, use NULL (OPTIONAL;
969  *                        default from $start to end of string)
970  * @param string $charset The charset of the given string.  A value of NULL
971  *                        here will force the use of PHP's standard substr().
972  *                        (OPTIONAL; default is "auto", which indicates that
973  *                        the user's current charset should be used).
974  *
975  * @return string The desired substring
976  *
977  * Of course, you can use more advanced (e.g., negative) values
978  * for $start and $length as needed - see the PHP manual for more
979  * information:  http://www.php.net/manual/function.substr.php
980  *
981  */
982function sq_substr($string, $start, $length=NULL, $charset='auto')
983{
984
985   // if $length is NULL, use the full string length...
986   // we have to do this to mimick the use of substr()
987   // where $length is not given
988   //
989   if (is_null($length))
990      $length = sq_strlen($length, $charset);
991
992
993   // NULL charset?  Just use substr()
994   //
995   if (is_null($charset))
996      return substr($string, $start, $length);
997
998
999   // use current character set?
1000   //
1001   if ($charset == 'auto')
1002   {
1003//FIXME: this may or may not be better as a session value instead of a global one
1004      global $sq_string_func_auto_charset;
1005      if (!isset($sq_string_func_auto_charset))
1006      {
1007         global $default_charset, $squirrelmail_language;
1008         set_my_charset();
1009         $sq_string_func_auto_charset = $default_charset;
1010         if ($squirrelmail_language == 'ja_JP') $sq_string_func_auto_charset = 'euc-jp';
1011      }
1012      $charset = $sq_string_func_auto_charset;
1013   }
1014
1015
1016   // standardize character set name
1017   //
1018   $charset = strtolower($charset);
1019
1020
1021/* ===== FIXME: this list is not used in 1.5.x, but if we need it, unless this differs between all our string function wrappers, we should store this info in the session
1022   // only use mbstring with the following character sets
1023   //
1024   $sq_substr_mb_charsets = array(
1025      'utf-8',
1026      'big5',
1027      'gb2312',
1028      'gb18030',
1029      'euc-jp',
1030      'euc-cn',
1031      'euc-tw',
1032      'euc-kr'
1033   );
1034
1035
1036   // now we can use mb_substr() if needed
1037   //
1038   if (in_array($charset, $sq_substr_mb_charsets)
1039    && in_array($charset, sq_mb_list_encodings()))
1040===== */
1041//FIXME: is there any reason why this cannot be a global array used by all string wrapper functions?
1042   if (in_array($charset, sq_mb_list_encodings()))
1043      return mb_substr($string, $start, $length, $charset);
1044
1045
1046   // else use normal substr()
1047   //
1048   return substr($string, $start, $length);
1049
1050}
1051
1052/**
1053  * This is a replacement for PHP's substr_replace() that is
1054  * multibyte-aware.
1055  *
1056  * @param string $string      The string to operate upon
1057  * @param string $replacement The string to be inserted
1058  * @param int    $start       The offset at which to begin substring replacement
1059  * @param int    $length      The number of characters after $start to remove
1060  *                            NOTE that if you need to specify a charset but
1061  *                            want to achieve normal substr_replace() behavior
1062  *                            where $length is not specified, use NULL (OPTIONAL;
1063  *                            default from $start to end of string)
1064  * @param string $charset     The charset of the given string.  A value of NULL
1065  *                            here will force the use of PHP's standard substr().
1066  *                            (OPTIONAL; default is "auto", which indicates that
1067  *                            the user's current charset should be used).
1068  *
1069  * @return string The manipulated string
1070  *
1071  * Of course, you can use more advanced (e.g., negative) values
1072  * for $start and $length as needed - see the PHP manual for more
1073  * information:  http://www.php.net/manual/function.substr-replace.php
1074  *
1075  */
1076function sq_substr_replace($string, $replacement, $start, $length=NULL,
1077                           $charset='auto')
1078{
1079
1080   // NULL charset?  Just use substr_replace()
1081   //
1082   if (is_null($charset))
1083      return is_null($length) ? substr_replace($string, $replacement, $start)
1084                              : substr_replace($string, $replacement, $start, $length);
1085
1086
1087   // use current character set?
1088   //
1089   if ($charset == 'auto')
1090   {
1091//FIXME: this may or may not be better as a session value instead of a global one
1092      $charset = $auto_charset;
1093      global $sq_string_func_auto_charset;
1094      if (!isset($sq_string_func_auto_charset))
1095      {
1096         global $default_charset, $squirrelmail_language;
1097         set_my_charset();
1098         $sq_string_func_auto_charset = $default_charset;
1099         if ($squirrelmail_language == 'ja_JP') $sq_string_func_auto_charset = 'euc-jp';
1100      }
1101      $charset = $sq_string_func_auto_charset;
1102   }
1103
1104
1105   // standardize character set name
1106   //
1107   $charset = strtolower($charset);
1108
1109
1110/* ===== FIXME: this list is not used in 1.5.x, but if we need it, unless this differs between all our string function wrappers, we should store this info in the session
1111   // only use mbstring with the following character sets
1112   //
1113   $sq_substr_replace_mb_charsets = array(
1114      'utf-8',
1115      'big5',
1116      'gb2312',
1117      'gb18030',
1118      'euc-jp',
1119      'euc-cn',
1120      'euc-tw',
1121      'euc-kr'
1122   );
1123
1124
1125   // now we can use our own implementation using
1126   // mb_substr() and mb_strlen() if needed
1127   //
1128   if (in_array($charset, $sq_substr_replace_mb_charsets)
1129    && in_array($charset, sq_mb_list_encodings()))
1130===== */
1131//FIXME: is there any reason why this cannot be a global array used by all string wrapper functions?
1132   if (in_array($charset, sq_mb_list_encodings()))
1133   {
1134
1135      $string_length = mb_strlen($string, $charset);
1136
1137      if ($start < 0)
1138         $start = max(0, $string_length + $start);
1139
1140      else if ($start > $string_length)
1141         $start = $string_length;
1142
1143      if ($length < 0)
1144         $length = max(0, $string_length - $start + $length);
1145
1146      else if (is_null($length) || $length > $string_length)
1147         $length = $string_length;
1148
1149      if ($start + $length > $string_length)
1150         $length = $string_length - $start;
1151
1152      return mb_substr($string, 0, $start, $charset)
1153           . $replacement
1154           . mb_substr($string,
1155                       $start + $length,
1156                       $string_length, // FIXME: I can't see why this is needed:  - $start - $length,
1157                       $charset);
1158
1159   }
1160
1161
1162   // else use normal substr_replace()
1163   //
1164   return is_null($length) ? substr_replace($string, $replacement, $start)
1165                           : substr_replace($string, $replacement, $start, $length);
1166
1167}
1168
1169/**
1170 * Replacement of mb_list_encodings function
1171 *
1172 * This function provides replacement for function that is available only
1173 * in php 5.x. Function does not test all mbstring encodings. Only the ones
1174 * that might be used in SM translations.
1175 *
1176 * Supported strings are stored in session in order to reduce number of
1177 * mb_internal_encoding function calls.
1178 *
1179 * If mb_list_encodings() function is present, code uses it. Main difference
1180 * from original function behaviour - array values are lowercased in order to
1181 * simplify use of returned array in in_array() checks.
1182 *
1183 * If you want to test all mbstring encodings - fill $list_of_encodings
1184 * array.
1185 * @return array list of encodings supported by php mbstring extension
1186 * @since 1.5.1 and 1.4.6
1187 */
1188function sq_mb_list_encodings() {
1189
1190    // if it's already in the session, don't need to regenerate it
1191    if (sqgetGlobalVar('mb_supported_encodings',$mb_supported_encodings,SQ_SESSION)
1192     && is_array($mb_supported_encodings))
1193        return $mb_supported_encodings;
1194
1195    // check if mbstring extension is present
1196    if (! function_exists('mb_internal_encoding')) {
1197        $supported_encodings = array();
1198        sqsession_register($supported_encodings, 'mb_supported_encodings');
1199        return $supported_encodings;
1200    }
1201
1202    // php 5+ function
1203    if (function_exists('mb_list_encodings')) {
1204        $supported_encodings = mb_list_encodings();
1205        array_walk($supported_encodings, 'sq_lowercase_array_vals');
1206        sqsession_register($supported_encodings, 'mb_supported_encodings');
1207        return $supported_encodings;
1208    }
1209
1210    // save original encoding
1211    $orig_encoding=mb_internal_encoding();
1212
1213    $list_of_encoding=array(
1214        'pass',
1215        'auto',
1216        'ascii',
1217        'jis',
1218        'utf-8',
1219        'sjis',
1220        'euc-jp',
1221        'iso-8859-1',
1222        'iso-8859-2',
1223        'iso-8859-7',
1224        'iso-8859-9',
1225        'iso-8859-15',
1226        'koi8-r',
1227        'koi8-u',
1228        'big5',
1229        'gb2312',
1230        'gb18030',
1231        'windows-1251',
1232        'windows-1255',
1233        'windows-1256',
1234        'tis-620',
1235        'iso-2022-jp',
1236        'euc-cn',
1237        'euc-kr',
1238        'euc-tw',
1239        'uhc',
1240        'utf7-imap');
1241
1242    $supported_encodings=array();
1243
1244    foreach ($list_of_encoding as $encoding) {
1245        // try setting encodings. suppress warning messages
1246        if (@mb_internal_encoding($encoding))
1247            $supported_encodings[]=$encoding;
1248    }
1249
1250    // restore original encoding
1251    mb_internal_encoding($orig_encoding);
1252
1253    // register list in session
1254    sqsession_register($supported_encodings, 'mb_supported_encodings');
1255
1256    return $supported_encodings;
1257}
1258
1259/**
1260 * Callback function used to lowercase array values.
1261 * @param string $val array value
1262 * @param mixed $key array key
1263 * @since 1.5.1 and 1.4.6
1264 */
1265function sq_lowercase_array_vals(&$val,$key) {
1266    $val = strtolower($val);
1267}
1268
1269/**
1270 * Callback function to trim whitespace from a value, to be used in array_walk
1271 * @param string $value value to trim
1272 * @since 1.5.2 and 1.4.7
1273 */
1274function sq_trim_value ( &$value ) {
1275    $value = trim($value);
1276}
1277
1278/**
1279  * Gathers the list of secuirty tokens currently
1280  * stored in the user's preferences and optionally
1281  * purges old ones from the list.
1282  *
1283  * @param boolean $purge_old Indicates if old tokens
1284  *                           should be purged from the
1285  *                           list ("old" is 2 days or
1286  *                           older unless the administrator
1287  *                           overrides that value using
1288  *                           $max_token_age_days in
1289  *                           config/config_local.php)
1290  *                           (OPTIONAL; default is to always
1291  *                           purge old tokens)
1292  *
1293  * @return array The list of tokens
1294  *
1295  * @since 1.4.19 and 1.5.2
1296  *
1297  */
1298function sm_get_user_security_tokens($purge_old=TRUE)
1299{
1300
1301   global $data_dir, $username, $max_token_age_days;
1302
1303   $tokens = getPref($data_dir, $username, 'security_tokens', '');
1304   if (($tokens = unserialize($tokens)) === FALSE || !is_array($tokens))
1305      $tokens = array();
1306
1307   // purge old tokens if necessary
1308   //
1309   if ($purge_old)
1310   {
1311      if (empty($max_token_age_days)) $max_token_age_days = 2;
1312      $now = time();
1313      $discard_token_date = $now - ($max_token_age_days * 86400);
1314      $cleaned_tokens = array();
1315      foreach ($tokens as $token => $timestamp)
1316         if ($timestamp >= $discard_token_date)
1317            $cleaned_tokens[$token] = $timestamp;
1318      $tokens = $cleaned_tokens;
1319   }
1320
1321   return $tokens;
1322
1323}
1324
1325/**
1326  * Generates a security token that is then stored in
1327  * the user's preferences with a timestamp for later
1328  * verification/use (although session-based tokens
1329  * are not stored in user preferences).
1330  *
1331  * NOTE: By default SquirrelMail will use a single session-based
1332  *       token, but if desired, user tokens can have expiration
1333  *       dates associated with them and become invalid even during
1334  *       the same login session.  When in that mode, the note
1335  *       immediately below applies, otherwise it is irrelevant.
1336  *       To enable that mode, the administrator must add the
1337  *       following to config/config_local.php:
1338  *       $use_expiring_security_tokens = TRUE;
1339  *
1340  * NOTE: The administrator can force SquirrelMail to generate
1341  * a new token every time one is requested (which may increase
1342  * obscurity through token randomness at the cost of some
1343  * performance) by adding the following to
1344  * config/config_local.php:   $do_not_use_single_token = TRUE;
1345  * Otherwise, only one token will be generated per user which
1346  * will change only after it expires or is used outside of the
1347  * validity period specified when calling sm_validate_security_token()
1348  *
1349  * WARNING: If the administrator has turned the token system
1350  *          off by setting $disable_security_tokens to TRUE in
1351  *          config/config.php or the configuration tool, this
1352  *          function will not store tokens in the user
1353  *          preferences (but it will still generate and return
1354  *          a random string).
1355  *
1356  * @param boolean $force_generate_new When TRUE, a new token will
1357  *                                    always be created even if current
1358  *                                    configuration dictates otherwise
1359  *                                    (OPTIONAL; default FALSE)
1360  *
1361  * @return string A security token
1362  *
1363  * @since 1.4.19 and 1.5.2
1364  *
1365  */
1366function sm_generate_security_token($force_generate_new=FALSE)
1367{
1368
1369   global $data_dir, $username, $disable_security_tokens, $do_not_use_single_token,
1370          $use_expiring_security_tokens;
1371   $max_generation_tries = 1000;
1372
1373   // if we're using session-based tokens, just return
1374   // the same one every time (generate it if it's not there)
1375   //
1376   if (!$use_expiring_security_tokens)
1377   {
1378      if (sqgetGlobalVar('sm_security_token', $token, SQ_SESSION))
1379         return $token;
1380
1381      // create new one since there was none in session
1382      $token = GenerateRandomString(12, '', 7);
1383      sqsession_register($token, 'sm_security_token');
1384      return $token;
1385   }
1386
1387   $tokens = sm_get_user_security_tokens();
1388
1389   if (!$force_generate_new && !$do_not_use_single_token && !empty($tokens))
1390      return key($tokens);
1391
1392   $new_token = GenerateRandomString(12, '', 7);
1393   $count = 0;
1394   while (isset($tokens[$new_token]))
1395   {
1396      $new_token = GenerateRandomString(12, '', 7);
1397      if (++$count > $max_generation_tries)
1398      {
1399         logout_error(_("Fatal token generation error; please contact your system administrator or the SquirrelMail Team"));
1400         exit;
1401      }
1402   }
1403
1404   // is the token system enabled?  CAREFUL!
1405   //
1406   if (!$disable_security_tokens)
1407   {
1408      $tokens[$new_token] = time();
1409      setPref($data_dir, $username, 'security_tokens', serialize($tokens));
1410   }
1411
1412   return $new_token;
1413
1414}
1415
1416/**
1417  * Validates a given security token and optionally remove it
1418  * from the user's preferences if it was valid.  If the token
1419  * is too old but otherwise valid, it will still be rejected.
1420  *
1421  * "Too old" is 2 days or older unless the administrator
1422  * overrides that value using $max_token_age_days in
1423  * config/config_local.php
1424  *
1425  * Session-based tokens of course are always reused and are
1426  * valid for the lifetime of the login session.
1427  *
1428  * WARNING: If the administrator has turned the token system
1429  *          off by setting $disable_security_tokens to TRUE in
1430  *          config/config.php or the configuration tool, this
1431  *          function will always return TRUE.
1432  *
1433  * @param string  $token           The token to validate
1434  * @param int     $validity_period The number of seconds tokens are valid
1435  *                                 for (set to zero to remove valid tokens
1436  *                                 after only one use; set to -1 to allow
1437  *                                 indefinite re-use (but still subject to
1438  *                                 $max_token_age_days - see elsewhere);
1439  *                                 use 3600 to allow tokens to be reused for
1440  *                                 an hour) (OPTIONAL; default is to only
1441  *                                 allow tokens to be used once)
1442  *                                 NOTE this is unrelated to $max_token_age_days
1443  *                                 or rather is an additional time constraint on
1444  *                                 tokens that allows them to be re-used (or not)
1445  *                                 within a more narrow timeframe
1446  * @param boolean $show_error      Indicates that if the token is not
1447  *                                 valid, this function should display
1448  *                                 a generic error, log the user out
1449  *                                 and exit - this function will never
1450  *                                 return in that case.
1451  *                                 (OPTIONAL; default FALSE)
1452  *
1453  * @return boolean TRUE if the token validated; FALSE otherwise
1454  *
1455  * @since 1.4.19 and 1.5.2
1456  *
1457  */
1458function sm_validate_security_token($token, $validity_period=0, $show_error=FALSE)
1459{
1460
1461   global $data_dir, $username, $max_token_age_days,
1462          $use_expiring_security_tokens,
1463          $disable_security_tokens;
1464
1465   // bypass token validation?  CAREFUL!
1466   //
1467   if ($disable_security_tokens) return TRUE;
1468
1469   // if we're using session-based tokens, just compare
1470   // the same one every time
1471   //
1472   if (!$use_expiring_security_tokens)
1473   {
1474      if (!sqgetGlobalVar('sm_security_token', $session_token, SQ_SESSION))
1475      {
1476         if (!$show_error) return FALSE;
1477         logout_error(_("Fatal security token error; please log in again"));
1478         exit;
1479      }
1480      if ($token !== $session_token)
1481      {
1482         if (!$show_error) return FALSE;
1483         logout_error(_("The current page request appears to have originated from an untrusted source."));
1484         exit;
1485      }
1486      return TRUE;
1487   }
1488
1489   // don't purge old tokens here because we already
1490   // do it when generating tokens
1491   //
1492   $tokens = sm_get_user_security_tokens(FALSE);
1493
1494   // token not found?
1495   //
1496   if (empty($tokens[$token]))
1497   {
1498      if (!$show_error) return FALSE;
1499      logout_error(_("This page request could not be verified and appears to have expired."));
1500      exit;
1501   }
1502
1503   $now = time();
1504   $timestamp = $tokens[$token];
1505
1506   // whether valid or not, we want to remove it from
1507   // user prefs if it's old enough (unless requested to
1508   // bypass this (in which case $validity_period is -1))
1509   //
1510   if ($validity_period >= 0
1511    && $timestamp < $now - $validity_period)
1512   {
1513      unset($tokens[$token]);
1514      setPref($data_dir, $username, 'security_tokens', serialize($tokens));
1515   }
1516
1517   // reject tokens that are too old
1518   //
1519   if (empty($max_token_age_days)) $max_token_age_days = 2;
1520   $old_token_date = $now - ($max_token_age_days * 86400);
1521   if ($timestamp < $old_token_date)
1522   {
1523      if (!$show_error) return FALSE;
1524      logout_error(_("The current page request appears to have originated from an untrusted source."));
1525      exit;
1526   }
1527
1528   // token OK!
1529   //
1530   return TRUE;
1531
1532}
1533
1534/**
1535  * Wrapper for PHP's htmlspecialchars() that
1536  * attempts to add the correct character encoding
1537  *
1538  * @param string $string The string to be converted
1539  * @param int $flags A bitmask that controls the behavior of
1540  *                   htmlspecialchars() -- NOTE that this parameter
1541  *                   should only be used to dictate handling of
1542  *                   quotes; handling invalid code sequences is done
1543  *                   using the $invalid_sequence_flag parameter below
1544  *                   (See http://php.net/manual/function.htmlspecialchars.php )
1545  *                   (OPTIONAL; default ENT_COMPAT)
1546  * @param string $encoding The character encoding to use in the conversion
1547  *                         (if not one of the character sets supported
1548  *                         by PHP's htmlspecialchars(), then $encoding
1549  *                         will be ignored and iso-8859-1 will be used,
1550  *                         unless a default has been specified in
1551  *                         $default_htmlspecialchars_encoding in
1552  *                         config_local.php) (OPTIONAL; default automatic
1553  *                         detection)
1554  * @param boolean $double_encode Whether or not to convert entities that are
1555  *                               already in the string (only supported in
1556  *                               PHP 5.2.3+) (OPTIONAL; default TRUE)
1557  * @param mixed $invalid_sequence_flag A bitmask that controls how invalid
1558  *                                     code sequences should be handled;
1559  *                                     When calling htmlspecialchars(),
1560  *                                     this value will be combined with
1561  *                                     the $flags parameter above
1562  *                                     (See http://php.net/manual/function.htmlspecialchars.php )
1563  *                                     (OPTIONAL; defaults to the string
1564  *                                     "ent_substitute" that, for PHP 5.4+,
1565  *                                     is converted to the ENT_SUBSTITUTE
1566  *                                     constant, otherwise empty)
1567  *
1568  * @return string The converted text
1569  *
1570  */
1571function sm_encode_html_special_chars($string, $flags=ENT_COMPAT,
1572                                      $encoding=NULL, $double_encode=TRUE,
1573                                      $invalid_sequence_flag='ent_substitute')
1574{
1575   if ($invalid_sequence_flag === 'ent_substitute')
1576   {
1577      if (check_php_version(5, 4, 0))
1578         $invalid_sequence_flag = ENT_SUBSTITUTE;
1579      else
1580         $invalid_sequence_flag = 0;
1581   }
1582
1583
1584   // charsets supported by PHP's htmlspecialchars
1585   // (move this elsewhere if needed)
1586   //
1587   static $htmlspecialchars_charsets = array(
1588      'iso-8859-1', 'iso8859-1',
1589      'iso-8859-5', 'iso8859-5',
1590      'iso-8859-15', 'iso8859-15',
1591      'utf-8',
1592      'cp866', 'ibm866', '866',
1593      'cp1251', 'windows-1251', 'win-1251', '1251',
1594      'cp1252', 'windows-1252', '1252',
1595      'koi8-R', 'koi8-ru', 'koi8r',
1596      'big5', '950',
1597      'gb2312', '936',
1598      'big5-hkscs',
1599      'shift_jis', 'sjis', 'sjis-win', 'cp932', '932',
1600      'euc-jp', 'eucjp', 'eucjp-win',
1601      'macroman',
1602   );
1603
1604
1605   // if not given, set encoding to the charset being
1606   // used by the current user interface language
1607   //
1608   if (!$encoding)
1609   {
1610      global $default_charset;
1611      if ($default_charset == 'iso-2022-jp')
1612         $default_charset = 'EUC-JP';
1613      $encoding = $default_charset;
1614   }
1615
1616
1617   // two ways to handle encodings not supported by htmlspecialchars() -
1618   // one takes less CPU cycles but can munge characters in certain
1619   // translations, the other is more exact but requires more resources
1620   //
1621   global $html_special_chars_extended_fix;
1622//FIXME: need to document that the config switch above can be enabled in config_local... but first, we need to decide if we will implement the second option here -- currently there hasn't been a need for it (munged characters seem quite rare).... see tracker #2806 for some tips https://sourceforge.net/p/squirrelmail/bugs/2806
1623   if (!in_array(strtolower($encoding), $htmlspecialchars_charsets))
1624   {
1625      if ($html_special_chars_extended_fix)
1626      {
1627         // convert to utf-8 first, run htmlspecialchars() and convert
1628         // back to original encoding below
1629         //
1630//FIXME: try conversion functions in this order: recode_string(), iconv(), mbstring (with various charset checks: sq_mb_list_encodings(), mb_check_encoding) -- oh, first check for internal charset_decode_CHARSET() function?? or just use (does this put everything into HTML entities already? shouldn't, but if it does, return right here):
1631         $string = charset_decode($encoding, $string, TRUE, TRUE);
1632         $string = charset_encode($string, $encoding, TRUE);
1633      }
1634      else
1635      {
1636         // simply force use of an encoding that is supported (some
1637         // characters may be munged)
1638         //
1639         // use default from configuration if provided or hard-coded fallback
1640         //
1641         global $default_htmlspecialchars_encoding;
1642         if (!empty($default_htmlspecialchars_encoding))
1643            $encoding = $default_htmlspecialchars_encoding;
1644         else
1645            $encoding = 'iso-8859-1';
1646      }
1647   }
1648
1649
1650// TODO: Is adding this check an unnecessary performance hit?
1651   if (check_php_version(5, 2, 3))
1652      $ret = htmlspecialchars($string, $flags | $invalid_sequence_flag,
1653                              $encoding, $double_encode);
1654   else
1655      $ret = htmlspecialchars($string, $flags | $invalid_sequence_flag,
1656                              $encoding);
1657
1658
1659   // convert back to original encoding if needed (see above)
1660   //
1661   if ($html_special_chars_extended_fix
1662    && !in_array(strtolower($encoding), $htmlspecialchars_charsets))
1663   {
1664//FIXME: NOT FINISHED - here, we'd convert from utf-8 back to original charset (if we obey $lossy_encoding and end up returning in utf-8 instead of original charset, does that screw up the caller?)
1665   }
1666
1667
1668   return $ret;
1669}
1670
1671