6 * This code provides various string manipulation functions that are
7 * used by the rest of the SquirrelMail code.
9 * @copyright 1999-2012 The SquirrelMail Project Team
10 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
11 * @version $Id: strings.php 14274 2012-02-07 23:04:45Z pdontthink $
12 * @package squirrelmail
16 * SquirrelMail version number -- DO NOT CHANGE
19 $version = '1.4.23 [SVN]';
22 * SquirrelMail internal version number -- DO NOT CHANGE
23 * $sm_internal_version = array (release, major, minor)
25 global $SQM_INTERNAL_VERSION;
26 $SQM_INTERNAL_VERSION = array(1, 4, 23);
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.
33 require_once(SM_PATH . 'functions/global.php');
35 if (file_exists(SM_PATH . 'plugins/compatibility/functions.php')) {
36 include_once(SM_PATH . 'plugins/compatibility/functions.php');
40 * Wraps text at $wrap characters
42 * Has a problem with special HTML characters, so call this before
43 * you do character translation.
45 * Specifically, ' comes up as 5 characters instead of 1.
46 * This should not add newlines to the end of lines.
48 function sqWordWrap(&$line, $wrap, $charset=null) {
49 global $languages, $squirrelmail_language;
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);
59 preg_match('/^([\t >]*)([^\t >].*)?$/', $line, $regs);
60 $beginning_spaces = $regs[1];
61 if (isset($regs[2])) {
62 $words = explode(' ', $regs[2]);
68 $line = $beginning_spaces;
70 while ($i < count($words)) {
71 /* Force one word to be on a line (minimum) */
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);
78 /* Add more words (as long as they fit) */
79 while ($line_len < $wrap && $i < count($words)) {
80 $line .= ' ' . $words[$i];
82 if (isset($words[$i]))
83 $line_len += sq_strlen($words[$i],$charset) + 1;
88 /* Skip spaces if they are the first thing on a continued line */
89 while (!isset($words[$i]) && $i < count($words)) {
93 /* Go to the next line if we have more to process */
94 if ($i < count($words)) {
101 * Does the opposite of sqWordWrap()
102 * @param string body the text to un-wordwrap
105 function sqUnWordWrap(&$body) {
106 global $squirrelmail_language;
108 if ($squirrelmail_language == 'ja_JP') {
112 $lines = explode("\n", $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];
126 $PreviousSpaces = $CurrentSpaces;
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;
133 $body .= "\n" . $lines[$i];
134 $PreviousSpaces = $CurrentSpaces;
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().
147 * NOTE that if $elipses is given and used, the returned
148 * number of characters will be $max_chars PLUS the
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;
157 * @param boolean $html_entities_as_chars Whether or not to keep
158 * HTML entities together
159 * (OPTIONAL; default ignore
162 * @return string The truncated string
164 * @since 1.4.20 and 1.5.2 (replaced truncateWithEntities())
167 function sm_truncate_string($string, $max_chars, $elipses='',
168 $html_entities_as_chars=FALSE)
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)
177 $actual_strlen = sq_strlen($string, 'auto');
178 if ($max_chars <= 0 || $actual_strlen <= $max_chars)
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
186 $adjusted_max_chars = $max_chars;
187 if ($html_entities_as_chars)
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
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)
205 $adjusted_max_chars += $entity_end_pos - $entity_pos;
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)
215 if ($actual_strlen <= $adjusted_max_chars)
221 // get the truncated string
223 $truncated_string = sq_substr($string, 0, $adjusted_max_chars);
226 // return with added elipses
228 return $truncated_string . $elipses;
233 * If $haystack is a full mailbox name and $needle is the mailbox
234 * separator character, returns the last part of the mailbox name.
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
240 function readShortMailboxName($haystack, $needle) {
245 $parts = explode($needle, $haystack);
246 $elem = array_pop($parts);
247 while ($elem == '' && count($parts)) {
248 $elem = array_pop($parts);
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.
263 * @return string The path, filename and any arguments for the
266 function php_self() {
270 // first try $_SERVER['PHP_SELF'], which seems most reliable
271 // (albeit it usually won't include the query string)
274 if (!sqgetGlobalVar('PHP_SELF', $request_uri, SQ_SERVER)
275 || empty($request_uri)) {
277 // well, then let's try $_SERVER['REQUEST_URI']
280 if (!sqgetGlobalVar('REQUEST_URI', $request_uri, SQ_SERVER)
281 || empty($request_uri)) {
283 // TODO: anyone have any other ideas? maybe $_SERVER['SCRIPT_NAME']???
290 // we may or may not have any query arguments, depending on
291 // which environment variable was used above, and the PHP
292 // version, etc., so let's check for it now
295 if (strpos($request_uri, '?') === FALSE
296 && sqgetGlobalVar('QUERY_STRING', $query_string, SQ_SERVER)
297 && !empty($query_string)) {
299 $request_uri .= '?' . $query_string;
308 * Find out where squirrelmail lives and try to be smart about it.
309 * The only problem would be when squirrelmail lives in directories
310 * called "src", "functions", or "plugins", but people who do that need
311 * to be beaten with a steel pipe anyway.
313 * @return string the base uri of squirrelmail installation.
315 function sqm_baseuri(){
316 global $base_uri, $PHP_SELF;
318 * If it is in the session, just return it.
320 if (sqgetGlobalVar('base_uri',$base_uri,SQ_SESSION)){
323 $dirs = array('|src/.*|', '|plugins/.*|', '|functions/.*|');
324 $repl = array('', '', '');
325 $base_uri = preg_replace($dirs, $repl, $PHP_SELF);
332 * Determines the location to forward to, relative to your server.
333 * This is used in HTTP Location: redirects.
334 * If set, it uses $config_location_base as the first part of the URL,
335 * specifically, the protocol, hostname and port parts. The path is
336 * always autodetected.
338 * @return string the base url for this SquirrelMail installation
340 function get_location () {
342 global $imap_server_type, $config_location_base,
343 $is_secure_connection, $sq_ignore_http_x_forwarded_headers;
345 /* Get the path, handle virtual directories */
346 if(strpos(php_self(), '?')) {
347 $path = substr(php_self(), 0, strpos(php_self(), '?'));
351 $path = substr($path, 0, strrpos($path, '/'));
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 ;
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;
365 /* Check if this is a HTTPS or regular HTTP request. */
367 if ($is_secure_connection)
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)
374 if ( !sqgetGlobalVar('HTTP_HOST', $host, SQ_SERVER) || empty($host) ) {
375 if ( !sqgetGlobalVar('SERVER_NAME', $host, SQ_SERVER) || empty($host) ) {
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);
403 /* this is a workaround for the weird macosx caching that
404 causes Apache to return 16080 as the port number, which causes
407 if ($imap_server_type == 'macosx' && $port == ':16080') {
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;
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();
426 * @param string string the (password)string to encrypt
427 * @param string epad the encryption key
428 * @return string the base64-encoded encrypted password
430 function OneTimePadEncrypt ($string, $epad) {
431 $pad = base64_decode($epad);
433 if (strlen($pad)>0) {
434 // make sure that pad is longer than string
435 while (strlen($string)>strlen($pad)) {
439 // FIXME: what should we do when $epad is not base64 encoded or empty.
443 for ($i = 0; $i < strlen ($string); $i++) {
444 $encrypted .= chr (ord($string[$i]) ^ ord($pad[$i]));
447 return base64_encode($encrypted);
451 * Decrypts a password from the cookie
453 * Decrypts a password from the cookie, encrypted by OneTimePadEncrypt.
454 * This uses the encryption key that is stored in the session.
456 * @param string string the string to decrypt
457 * @param string epad the encryption key from the session
458 * @return string the decrypted password
460 function OneTimePadDecrypt ($string, $epad) {
461 $pad = base64_decode($epad);
463 if (strlen($pad)>0) {
464 // make sure that pad is longer than string
465 while (strlen($string)>strlen($pad)) {
469 // FIXME: what should we do when $epad is not base64 encoded or empty.
472 $encrypted = base64_decode ($string);
474 for ($i = 0; $i < strlen ($encrypted); $i++) {
475 $decrypted .= chr (ord($encrypted[$i]) ^ ord($pad[$i]));
483 * Randomizes the mt_rand() function.
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.
489 * @param mixed val a value to seed the random number generator
492 function 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();
497 if (! is_int($Val)) {
509 mt_srand(($Val ^ mt_rand(0, $Max)) & $Max);
514 * Init random number generator
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.
522 function sq_mt_randomize() {
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()));
536 if (function_exists('getrusage')) {
537 /* Avoid warnings with Win32 */
539 if (isset($dat) && is_array($dat)) {
541 foreach ($dat as $k => $v)
545 sq_mt_seed(md5($Str));
549 if(sqgetGlobalVar('UNIQUE_ID', $unique_id, SQ_SERVER)) {
550 sq_mt_seed(md5($unique_id));
557 * Creates encryption key
559 * Creates an encryption key for encrypting the password stored in the cookie.
560 * The encryption key itself is stored in the session.
562 * @param int length optional, length of the string to generate
563 * @return string the encryption key
565 function OneTimePadCreate ($length=100) {
569 for ($i = 0; $i < $length; $i++) {
570 $pad .= chr(mt_rand(0,255));
573 return base64_encode($pad);
577 * Returns a string showing the size of the message/attachment.
579 * @param int bytes the filesize in bytes
580 * @return string the filesize in human readable format
582 function show_readable_size($bytes) {
586 if ($bytes / 1024 > 1) {
593 settype($bytes, 'integer');
596 settype($bytes, 'integer');
599 return $bytes . '<small> ' . $type . '</small>';
603 * Generates a random string from the caracter set you pass in
605 * @param int size the size of the string to generate
606 * @param string chars a string containing the characters to use
607 * @param int flags a flag to add a specific set to the characters to use:
609 * 1 = add lowercase a-z to $chars
610 * 2 = add uppercase A-Z to $chars
611 * 4 = add numbers 0-9 to $chars
612 * @return string the random string
614 function GenerateRandomString($size, $chars, $flags = 0) {
616 $chars .= 'abcdefghijklmnopqrstuvwxyz';
619 $chars .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
622 $chars .= '0123456789';
625 if (($size < 1) || (strlen($chars) < 1)) {
629 sq_mt_randomize(); /* Initialize the random number generator */
632 $j = strlen( $chars ) - 1;
633 while (strlen($String) < $size) {
634 $String .= $chars{mt_rand(0, $j)};
641 * Escapes special characters for use in IMAP commands.
643 * @param string the string to escape
644 * @return string the escaped string
646 function quoteimap($str) {
647 // FIXME use this performance improvement (not changing because this is STABLE branch): return str_replace(array('\\', '"'), array('\\\\', '\\"'), $str);
648 return preg_replace("/([\"\\\\])/", "\\\\$1", $str);
654 * Trims every element in the array, ie. remove the first char of each element
655 * Obsolete: will probably removed soon
656 * @param array array the array to trim
659 function TrimArray(&$array) {
660 foreach ($array as $k => $v) {
663 foreach ($$k as $k2 => $v2) {
664 $$k[$k2] = substr($v2, 1);
670 /* Re-assign back to array. */
676 * Removes slashes from every element in the array
678 function RemoveSlashes(&$array) {
679 foreach ($array as $k => $v) {
682 foreach ($$k as $k2 => $v2) {
683 $newArray[stripslashes($k2)] = stripslashes($v2);
687 $$k = stripslashes($v);
690 /* Re-assign back to the array. */
696 * Create compose link
698 * Returns a link to the compose-page, taking in consideration
699 * the compose_in_new and javascript settings.
700 * @param string url the URL to the compose page
701 * @param string text the link text, default "Compose"
702 * @return string a link to the compose page
704 function makeComposeLink($url, $text = null, $target='')
706 global $compose_new_win,$javascript_on;
709 $text = _("Compose");
713 // if not using "compose in new window", make
714 // regular link and be done with it
715 if($compose_new_win != '1') {
716 return makeInternalLink($url, $text, $target);
720 // build the compose in new window link...
723 // if javascript is on, use onClick event to handle it
725 sqgetGlobalVar('base_uri', $base_uri, SQ_SESSION);
726 return '<a href="javascript:void(0)" onclick="comp_in_new(\''.$base_uri.$url.'\')">'. $text.'</a>';
730 // otherwise, just open new window using regular HTML
731 return makeInternalLink($url, $text, '_blank');
738 * sm_print_r($some_variable, [$some_other_variable [, ...]]);
740 * Debugging function - does the same as print_r, but makes sure special
741 * characters are converted to htmlentities first. This will allow
742 * values like <some@email.address> to be displayed.
743 * The output is wrapped in <<pre>> and <</pre>> tags.
747 function sm_print_r() {
748 ob_start(); // Buffer output
749 foreach(func_get_args() as $var) {
753 $buffer = ob_get_contents(); // Grab the print_r output
754 ob_end_clean(); // Silently discard the output & stop buffering
756 print htmlentities($buffer);
761 * version of fwrite which checks for failure
763 function sq_fwrite($fp, $string) {
765 $count = @fwrite($fp,$string);
766 // the number of bytes written should be the length of the string
767 if($count != strlen($string)) {
774 * Tests if string contains 8bit symbols.
776 * If charset is not set, function defaults to default_charset.
777 * $default_charset global must be set correctly if $charset is
779 * @param string $string tested string
780 * @param string $charset charset used in a string
781 * @return bool true if 8bit symbols are detected
782 * @since 1.5.1 and 1.4.4
784 function sq_is8bit($string,$charset='') {
785 global $default_charset;
787 if ($charset=='') $charset=$default_charset;
790 * Don't use \240 in ranges. Sometimes RH 7.2 doesn't like it.
791 * Don't use \200-\237 for iso-8859-x charsets. This ranges
792 * stores control symbols in those charsets.
793 * Use preg_match instead of ereg in order to avoid problems
794 * with mbstring overloading
796 if (preg_match("/^iso-8859/i",$charset)) {
797 $needle='/\240|[\241-\377]/';
799 $needle='/[\200-\237]|\240|[\241-\377]/';
801 return preg_match("$needle",$string);
805 * Function returns number of characters in string.
807 * Returned number might be different from number of bytes in string,
808 * if $charset is multibyte charset. Detection depends on mbstring
809 * functions. If mbstring does not support tested multibyte charset,
810 * vanilla string length function is used.
811 * @param string $str string
812 * @param string $charset charset
813 * @since 1.5.1 and 1.4.6
814 * @return integer number of characters in string
816 function sq_strlen($string, $charset=NULL){
818 // NULL charset? Just use strlen()
820 if (is_null($charset))
821 return strlen($string);
824 // use current character set?
826 if ($charset == 'auto')
828 //FIXME: this may or may not be better as a session value instead of a global one
829 global $sq_string_func_auto_charset;
830 if (!isset($sq_string_func_auto_charset))
832 global $default_charset, $squirrelmail_language;
834 $sq_string_func_auto_charset = $default_charset;
835 if ($squirrelmail_language == 'ja_JP') $sq_string_func_auto_charset = 'euc-jp';
837 $charset = $sq_string_func_auto_charset;
841 // standardize character set name
843 $charset = strtolower($charset);
846 /* ===== 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
847 // only use mbstring with the following character sets
849 $sq_strlen_mb_charsets = array(
861 // now we can use mb_strlen() if needed
863 if (in_array($charset, $sq_strlen_mb_charsets)
864 && in_array($charset, sq_mb_list_encodings()))
866 //FIXME: is there any reason why this cannot be a static global array used by all string wrapper functions?
867 if (in_array($charset, sq_mb_list_encodings()))
868 return mb_strlen($string, $charset);
871 // else use normal strlen()
873 return strlen($string);
878 * This is a replacement for PHP's strpos() that is
881 * @param string $haystack The string to search within
882 * @param string $needle The substring to search for
883 * @param int $offset The offset from the beginning of $haystack
884 * from which to start searching
885 * (OPTIONAL; default none)
886 * @param string $charset The charset of the given string. A value of NULL
887 * here will force the use of PHP's standard strpos().
888 * (OPTIONAL; default is "auto", which indicates that
889 * the user's current charset should be used).
891 * @return mixed The integer offset of the next $needle in $haystack,
892 * if found, or FALSE if not found
895 function sq_strpos($haystack, $needle, $offset=0, $charset='auto')
898 // NULL charset? Just use strpos()
900 if (is_null($charset))
901 return strpos($haystack, $needle, $offset);
904 // use current character set?
906 if ($charset == 'auto')
908 //FIXME: this may or may not be better as a session value instead of a global one
909 global $sq_string_func_auto_charset;
910 if (!isset($sq_string_func_auto_charset))
912 global $default_charset, $squirrelmail_language;
914 $sq_string_func_auto_charset = $default_charset;
915 if ($squirrelmail_language == 'ja_JP') $sq_string_func_auto_charset = 'euc-jp';
917 $charset = $sq_string_func_auto_charset;
921 // standardize character set name
923 $charset = strtolower($charset);
926 /* ===== 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
927 // only use mbstring with the following character sets
929 $sq_strpos_mb_charsets = array(
941 // now we can use mb_strpos() if needed
943 if (in_array($charset, $sq_strpos_mb_charsets)
944 && in_array($charset, sq_mb_list_encodings()))
946 //FIXME: is there any reason why this cannot be a static global array used by all string wrapper functions?
947 if (in_array($charset, sq_mb_list_encodings()))
948 return mb_strpos($haystack, $needle, $offset, $charset);
951 // else use normal strpos()
953 return strpos($haystack, $needle, $offset);
958 * This is a replacement for PHP's substr() that is
961 * @param string $string The string to operate upon
962 * @param int $start The offset at which to begin substring extraction
963 * @param int $length The number of characters after $start to return
964 * NOTE that if you need to specify a charset but
965 * want to achieve normal substr() behavior where
966 * $length is not specified, use NULL (OPTIONAL;
967 * default from $start to end of string)
968 * @param string $charset The charset of the given string. A value of NULL
969 * here will force the use of PHP's standard substr().
970 * (OPTIONAL; default is "auto", which indicates that
971 * the user's current charset should be used).
973 * @return string The desired substring
975 * Of course, you can use more advanced (e.g., negative) values
976 * for $start and $length as needed - see the PHP manual for more
977 * information: http://www.php.net/manual/function.substr.php
980 function sq_substr($string, $start, $length=NULL, $charset='auto')
983 // if $length is NULL, use the full string length...
984 // we have to do this to mimick the use of substr()
985 // where $length is not given
987 if (is_null($length))
988 $length = sq_strlen($length, $charset);
991 // NULL charset? Just use substr()
993 if (is_null($charset))
994 return substr($string, $start, $length);
997 // use current character set?
999 if ($charset == 'auto')
1001 //FIXME: this may or may not be better as a session value instead of a global one
1002 global $sq_string_func_auto_charset;
1003 if (!isset($sq_string_func_auto_charset))
1005 global $default_charset, $squirrelmail_language;
1007 $sq_string_func_auto_charset = $default_charset;
1008 if ($squirrelmail_language == 'ja_JP') $sq_string_func_auto_charset = 'euc-jp';
1010 $charset = $sq_string_func_auto_charset;
1014 // standardize character set name
1016 $charset = strtolower($charset);
1019 /* ===== 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
1020 // only use mbstring with the following character sets
1022 $sq_substr_mb_charsets = array(
1034 // now we can use mb_substr() if needed
1036 if (in_array($charset, $sq_substr_mb_charsets)
1037 && in_array($charset, sq_mb_list_encodings()))
1039 //FIXME: is there any reason why this cannot be a global array used by all string wrapper functions?
1040 if (in_array($charset, sq_mb_list_encodings()))
1041 return mb_substr($string, $start, $length, $charset);
1044 // else use normal substr()
1046 return substr($string, $start, $length);
1051 * This is a replacement for PHP's substr_replace() that is
1054 * @param string $string The string to operate upon
1055 * @param string $replacement The string to be inserted
1056 * @param int $start The offset at which to begin substring replacement
1057 * @param int $length The number of characters after $start to remove
1058 * NOTE that if you need to specify a charset but
1059 * want to achieve normal substr_replace() behavior
1060 * where $length is not specified, use NULL (OPTIONAL;
1061 * default from $start to end of string)
1062 * @param string $charset The charset of the given string. A value of NULL
1063 * here will force the use of PHP's standard substr().
1064 * (OPTIONAL; default is "auto", which indicates that
1065 * the user's current charset should be used).
1067 * @return string The manipulated string
1069 * Of course, you can use more advanced (e.g., negative) values
1070 * for $start and $length as needed - see the PHP manual for more
1071 * information: http://www.php.net/manual/function.substr-replace.php
1074 function sq_substr_replace($string, $replacement, $start, $length=NULL,
1078 // NULL charset? Just use substr_replace()
1080 if (is_null($charset))
1081 return is_null($length) ? substr_replace($string, $replacement, $start)
1082 : substr_replace($string, $replacement, $start, $length);
1085 // use current character set?
1087 if ($charset == 'auto')
1089 //FIXME: this may or may not be better as a session value instead of a global one
1090 $charset = $auto_charset;
1091 global $sq_string_func_auto_charset;
1092 if (!isset($sq_string_func_auto_charset))
1094 global $default_charset, $squirrelmail_language;
1096 $sq_string_func_auto_charset = $default_charset;
1097 if ($squirrelmail_language == 'ja_JP') $sq_string_func_auto_charset = 'euc-jp';
1099 $charset = $sq_string_func_auto_charset;
1103 // standardize character set name
1105 $charset = strtolower($charset);
1108 /* ===== 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
1109 // only use mbstring with the following character sets
1111 $sq_substr_replace_mb_charsets = array(
1123 // now we can use our own implementation using
1124 // mb_substr() and mb_strlen() if needed
1126 if (in_array($charset, $sq_substr_replace_mb_charsets)
1127 && in_array($charset, sq_mb_list_encodings()))
1129 //FIXME: is there any reason why this cannot be a global array used by all string wrapper functions?
1130 if (in_array($charset, sq_mb_list_encodings()))
1133 $string_length = mb_strlen($string, $charset);
1136 $start = max(0, $string_length + $start);
1138 else if ($start > $string_length)
1139 $start = $string_length;
1142 $length = max(0, $string_length - $start + $length);
1144 else if (is_null($length) || $length > $string_length)
1145 $length = $string_length;
1147 if ($start + $length > $string_length)
1148 $length = $string_length - $start;
1150 return mb_substr($string, 0, $start, $charset)
1152 . mb_substr($string,
1154 $string_length, // FIXME: I can't see why this is needed: - $start - $length,
1160 // else use normal substr_replace()
1162 return is_null($length) ? substr_replace($string, $replacement, $start)
1163 : substr_replace($string, $replacement, $start, $length);
1168 * Replacement of mb_list_encodings function
1170 * This function provides replacement for function that is available only
1171 * in php 5.x. Function does not test all mbstring encodings. Only the ones
1172 * that might be used in SM translations.
1174 * Supported strings are stored in session in order to reduce number of
1175 * mb_internal_encoding function calls.
1177 * If mb_list_encodings() function is present, code uses it. Main difference
1178 * from original function behaviour - array values are lowercased in order to
1179 * simplify use of returned array in in_array() checks.
1181 * If you want to test all mbstring encodings - fill $list_of_encodings
1183 * @return array list of encodings supported by php mbstring extension
1184 * @since 1.5.1 and 1.4.6
1186 function sq_mb_list_encodings() {
1188 // if it's already in the session, don't need to regenerate it
1189 if (sqgetGlobalVar('mb_supported_encodings',$mb_supported_encodings,SQ_SESSION)
1190 && is_array($mb_supported_encodings))
1191 return $mb_supported_encodings;
1193 // check if mbstring extension is present
1194 if (! function_exists('mb_internal_encoding')) {
1195 $supported_encodings = array();
1196 sqsession_register($supported_encodings, 'mb_supported_encodings');
1197 return $supported_encodings;
1201 if (function_exists('mb_list_encodings')) {
1202 $supported_encodings = mb_list_encodings();
1203 array_walk($supported_encodings, 'sq_lowercase_array_vals');
1204 sqsession_register($supported_encodings, 'mb_supported_encodings');
1205 return $supported_encodings;
1208 // save original encoding
1209 $orig_encoding=mb_internal_encoding();
1211 $list_of_encoding=array(
1240 $supported_encodings=array();
1242 foreach ($list_of_encoding as $encoding) {
1243 // try setting encodings. suppress warning messages
1244 if (@mb_internal_encoding($encoding))
1245 $supported_encodings[]=$encoding;
1248 // restore original encoding
1249 mb_internal_encoding($orig_encoding);
1251 // register list in session
1252 sqsession_register($supported_encodings, 'mb_supported_encodings');
1254 return $supported_encodings;
1258 * Callback function used to lowercase array values.
1259 * @param string $val array value
1260 * @param mixed $key array key
1261 * @since 1.5.1 and 1.4.6
1263 function sq_lowercase_array_vals(&$val,$key) {
1264 $val = strtolower($val);
1268 * Callback function to trim whitespace from a value, to be used in array_walk
1269 * @param string $value value to trim
1270 * @since 1.5.2 and 1.4.7
1272 function sq_trim_value ( &$value ) {
1273 $value = trim($value);
1277 * Gathers the list of secuirty tokens currently
1278 * stored in the user's preferences and optionally
1279 * purges old ones from the list.
1281 * @param boolean $purge_old Indicates if old tokens
1282 * should be purged from the
1283 * list ("old" is 2 days or
1284 * older unless the administrator
1285 * overrides that value using
1286 * $max_token_age_days in
1287 * config/config_local.php)
1288 * (OPTIONAL; default is to always
1291 * @return array The list of tokens
1293 * @since 1.4.19 and 1.5.2
1296 function sm_get_user_security_tokens($purge_old=TRUE)
1299 global $data_dir, $username, $max_token_age_days;
1301 $tokens = getPref($data_dir, $username, 'security_tokens', '');
1302 if (($tokens = unserialize($tokens)) === FALSE || !is_array($tokens))
1305 // purge old tokens if necessary
1309 if (empty($max_token_age_days)) $max_token_age_days = 2;
1311 $discard_token_date = $now - ($max_token_age_days * 86400);
1312 $cleaned_tokens = array();
1313 foreach ($tokens as $token => $timestamp)
1314 if ($timestamp >= $discard_token_date)
1315 $cleaned_tokens[$token] = $timestamp;
1316 $tokens = $cleaned_tokens;
1324 * Generates a security token that is then stored in
1325 * the user's preferences with a timestamp for later
1328 * NOTE: The administrator can force SquirrelMail to generate
1329 * a new token every time one is requested (which may increase
1330 * obscurity through token randomness at the cost of some
1331 * performance) by adding the following to
1332 * config/config_local.php: $do_not_use_single_token = TRUE;
1333 * Otherwise, only one token will be generated per user which
1334 * will change only after it expires or is used outside of the
1335 * validity period specified when calling sm_validate_security_token()
1337 * WARNING: If the administrator has turned the token system
1338 * off by setting $disable_security_tokens to TRUE in
1339 * config/config.php or the configuration tool, this
1340 * function will not store tokens in the user
1341 * preferences (but it will still generate and return
1344 * @param boolean $force_generate_new When TRUE, a new token will
1345 * always be created even if current
1346 * configuration dictates otherwise
1347 * (OPTIONAL; default FALSE)
1349 * @return string A security token
1351 * @since 1.4.19 and 1.5.2
1354 function sm_generate_security_token($force_generate_new=FALSE)
1357 global $data_dir, $username, $disable_security_tokens, $do_not_use_single_token;
1358 $max_generation_tries = 1000;
1360 $tokens = sm_get_user_security_tokens();
1362 if (!$force_generate_new && !$do_not_use_single_token && !empty($tokens))
1363 return key($tokens);
1365 $new_token = GenerateRandomString(12, '', 7);
1367 while (isset($tokens[$new_token]))
1369 $new_token = GenerateRandomString(12, '', 7);
1370 if (++$count > $max_generation_tries)
1372 logout_error(_("Fatal token generation error; please contact your system administrator or the SquirrelMail Team"));
1377 // is the token system enabled? CAREFUL!
1379 if (!$disable_security_tokens)
1381 $tokens[$new_token] = time();
1382 setPref($data_dir, $username, 'security_tokens', serialize($tokens));
1390 * Validates a given security token and optionally remove it
1391 * from the user's preferences if it was valid. If the token
1392 * is too old but otherwise valid, it will still be rejected.
1394 * "Too old" is 2 days or older unless the administrator
1395 * overrides that value using $max_token_age_days in
1396 * config/config_local.php
1398 * WARNING: If the administrator has turned the token system
1399 * off by setting $disable_security_tokens to TRUE in
1400 * config/config.php or the configuration tool, this
1401 * function will always return TRUE.
1403 * @param string $token The token to validate
1404 * @param int $validity_period The number of seconds tokens are valid
1405 * for (set to zero to remove valid tokens
1406 * after only one use; use 3600 to allow
1407 * tokens to be reused for an hour)
1408 * (OPTIONAL; default is to only allow tokens
1410 * NOTE this is unrelated to $max_token_age_days
1411 * or rather is an additional time constraint on
1412 * tokens that allows them to be re-used (or not)
1413 * within a more narrow timeframe
1414 * @param boolean $show_error Indicates that if the token is not
1415 * valid, this function should display
1416 * a generic error, log the user out
1417 * and exit - this function will never
1418 * return in that case.
1419 * (OPTIONAL; default FALSE)
1421 * @return boolean TRUE if the token validated; FALSE otherwise
1423 * @since 1.4.19 and 1.5.2
1426 function sm_validate_security_token($token, $validity_period=0, $show_error=FALSE)
1429 global $data_dir, $username, $max_token_age_days,
1430 $disable_security_tokens;
1432 // bypass token validation? CAREFUL!
1434 if ($disable_security_tokens) return TRUE;
1436 // don't purge old tokens here because we already
1437 // do it when generating tokens
1439 $tokens = sm_get_user_security_tokens(FALSE);
1443 if (empty($tokens[$token]))
1445 if (!$show_error) return FALSE;
1446 logout_error(_("This page request could not be verified and appears to have expired."));
1451 $timestamp = $tokens[$token];
1453 // whether valid or not, we want to remove it from
1454 // user prefs if it's old enough
1456 if ($timestamp < $now - $validity_period)
1458 unset($tokens[$token]);
1459 setPref($data_dir, $username, 'security_tokens', serialize($tokens));
1462 // reject tokens that are too old
1464 if (empty($max_token_age_days)) $max_token_age_days = 2;
1465 $old_token_date = $now - ($max_token_age_days * 86400);
1466 if ($timestamp < $old_token_date)
1468 if (!$show_error) return FALSE;
1469 logout_error(_("The current page request appears to have originated from an untrusted source."));
1480 * Wrapper for PHP's htmlspecialchars() that
1481 * attempts to add the correct character encoding
1483 * @param string $string The string to be converted
1484 * @param int $flags A bitmask that controls the behavior of htmlspecialchars()
1485 * (See http://php.net/manual/function.htmlspecialchars.php )
1486 * (OPTIONAL; default ENT_COMPAT)
1487 * @param string $encoding The character encoding to use in the conversion
1488 * (OPTIONAL; default automatic detection)
1489 * @param boolean $double_encode Whether or not to convert entities that are
1490 * already in the string (only supported in
1491 * PHP 5.2.3+) (OPTIONAL; default TRUE)
1493 * @return string The converted text
1496 function sm_encode_html_special_chars($string, $flags=ENT_COMPAT,
1497 $encoding=NULL, $double_encode=TRUE)
1501 global $default_charset;
1502 if ($default_charset == 'iso-2022-jp')
1503 $default_charset = 'EUC-JP';
1504 $encoding = $default_charset;
1507 $string = iconv($encoding, 'UTF-8', $string);
1508 if (check_php_version(5, 2, 3))
1509 $ret = htmlspecialchars($string, $flags, 'UTF-8', $double_encode);
1511 $ret = htmlspecialchars($string, $flags, 'UTF-8');
1513 return iconv('UTF-8', $encoding, $ret);
1516 $PHP_SELF = php_self();