Happy New Year
[squirrelmail.git] / functions / strings.php
index c497ff9033c0565593ebac14ffc8419f4c40e79e..ddb13b2a0713ab7ac9c6123ab9670df7796d69fb 100644 (file)
@@ -6,7 +6,7 @@
  * This code provides various string manipulation functions that are
  * used by the rest of the SquirrelMail code.
  *
- * @copyright © 1999-2007 The SquirrelMail Project Team
+ * @copyright 1999-2020 The SquirrelMail Project Team
  * @license http://opensource.org/licenses/gpl-license.php GNU Public License
  * @version $Id$
  * @package squirrelmail
@@ -349,12 +349,12 @@ function sqWordWrap(&$line, $wrap, $charset='') {
         }
     }
 
-    ereg("^([\t >]*)([^\t >].*)?$", $line, $regs);
+    preg_match('/^([\t >]*)([^\t >].*)?$/', $line, $regs);
     $beginning_spaces = $regs[1];
     if (isset($regs[2])) {
         $words = explode(' ', $regs[2]);
     } else {
-        $words = '';
+        $words = array();
     }
 
     $i = 0;
@@ -470,7 +470,8 @@ function readShortMailboxName($haystack, $needle) {
  */
 function get_location () {
 
-    global $imap_server_type, $config_location_base;
+    global $imap_server_type, $config_location_base,
+           $is_secure_connection, $sq_ignore_http_x_forwarded_headers;
 
     /* Get the path, handle virtual directories */
     if(strpos(php_self(), '?')) {
@@ -492,25 +493,13 @@ function get_location () {
 
     /* Check if this is a HTTPS or regular HTTP request. */
     $proto = 'http://';
-
-    /*
-     * If you have 'SSLOptions +StdEnvVars' in your apache config
-     *     OR if you have HTTPS=on in your HTTP_SERVER_VARS
-     *     OR if you have HTTP_X_FORWARDED_PROTO=https in your HTTP_SERVER_VARS
-     *     OR if you are on port 443
-     */
-    $getEnvVar = getenv('HTTPS');
-    if (!sqgetGlobalVar('HTTP_X_FORWARDED_PROTO', $forwarded_proto, SQ_SERVER))
-        $forwarded_proto = '';
-    if ((isset($getEnvVar) && strcasecmp($getEnvVar, 'on') === 0) ||
-        (sqgetGlobalVar('HTTPS', $https_on, SQ_SERVER) && strcasecmp($https_on, 'on') === 0) ||
-        (strcasecmp($forwarded_proto, 'https') === 0) ||
-        (sqgetGlobalVar('SERVER_PORT', $server_port, SQ_SERVER) &&  $server_port == 443)) {
+    if ($is_secure_connection)
         $proto = 'https://';
-    }
 
     /* Get the hostname from the Host header or server config. */
-    if ( !sqgetGlobalVar('HTTP_X_FORWARDED_HOST', $host, SQ_SERVER) || empty($host) ) {
+    if ($sq_ignore_http_x_forwarded_headers
+     || !sqgetGlobalVar('HTTP_X_FORWARDED_HOST', $host, SQ_SERVER)
+     || empty($host)) {
         if ( !sqgetGlobalVar('HTTP_HOST', $host, SQ_SERVER) || empty($host) ) {
             if ( !sqgetGlobalVar('SERVER_NAME', $host, SQ_SERVER) || empty($host) ) {
                 $host = '';
@@ -520,6 +509,16 @@ function get_location () {
 
     $port = '';
     if (! strstr($host, ':')) {
+        // Note: HTTP_X_FORWARDED_PROTO could be sent from the client and
+        //       therefore possibly spoofed/hackable.  Thus, SquirrelMail
+        //       ignores such headers by default.  The administrator
+        //       can tell SM to use such header values by setting
+        //       $sq_ignore_http_x_forwarded_headers to boolean FALSE
+        //       in config/config.php or by using config/conf.pl.
+        global $sq_ignore_http_x_forwarded_headers;
+        if ($sq_ignore_http_x_forwarded_headers
+         || !sqgetGlobalVar('HTTP_X_FORWARDED_PROTO', $forwarded_proto, SQ_SERVER))
+            $forwarded_proto = '';
         if (sqgetGlobalVar('SERVER_PORT', $server_port, SQ_SERVER)) {
             if (($server_port != 80 && $proto == 'http://') ||
                 ($server_port != 443 && $proto == 'https://' &&
@@ -635,83 +634,6 @@ function OneTimePadDecrypt ($string, $epad) {
     return $decrypted;
 }
 
-
-/**
- * Randomizes the mt_rand() function.
- *
- * Toss this in strings or integers and it will seed the generator
- * appropriately. With strings, it is better to get them long.
- * Use md5() to lengthen smaller strings.
- *
- * @param mixed $val a value to seed the random number generator. mixed = integer or string.
- * @return void
- * @since 1.0
- */
-function sq_mt_seed($Val) {
-    /* if mt_getrandmax() does not return a 2^n - 1 number,
-       this might not work well.  This uses $Max as a bitmask. */
-    $Max = mt_getrandmax();
-
-    if (! is_int($Val)) {
-            $Val = crc32($Val);
-    }
-
-    if ($Val < 0) {
-        $Val *= -1;
-    }
-
-    if ($Val == 0) {
-        return;
-    }
-
-    mt_srand(($Val ^ mt_rand(0, $Max)) & $Max);
-}
-
-
-/**
- * Init random number generator
- *
- * This function initializes the random number generator fairly well.
- * It also only initializes it once, so you don't accidentally get
- * the same 'random' numbers twice in one session.
- *
- * @return void
- * @since 1.0
- */
-function sq_mt_randomize() {
-    static $randomized;
-
-    if ($randomized) {
-        return;
-    }
-
-    /* Global. */
-    sqgetGlobalVar('REMOTE_PORT', $remote_port, SQ_SERVER);
-    sqgetGlobalVar('REMOTE_ADDR', $remote_addr, SQ_SERVER);
-    sq_mt_seed((int)((double) microtime() * 1000000));
-    sq_mt_seed(md5($remote_port . $remote_addr . getmypid()));
-
-    /* getrusage */
-    if (function_exists('getrusage')) {
-        /* Avoid warnings with Win32 */
-        $dat = @getrusage();
-        if (isset($dat) && is_array($dat)) {
-            $Str = '';
-            foreach ($dat as $k => $v)
-                {
-                    $Str .= $k . $v;
-                }
-            sq_mt_seed(md5($Str));
-        }
-    }
-
-    if(sqgetGlobalVar('UNIQUE_ID', $unique_id, SQ_SERVER)) {
-        sq_mt_seed(md5($unique_id));
-    }
-
-    $randomized = 1;
-}
-
 /**
  * Creates encryption key
  *
@@ -724,8 +646,6 @@ function sq_mt_randomize() {
  * @since 1.0
  */
 function OneTimePadCreate ($length=100) {
-    sq_mt_randomize();
-
     $pad = '';
     for ($i = 0; $i < $length; $i++) {
         $pad .= chr(mt_rand(0,255));
@@ -735,19 +655,23 @@ function OneTimePadCreate ($length=100) {
 }
 
 /**
- * Returns a string showing the size of the message/attachment.
- *
- * @param int $bytes the filesize in bytes
- * @return string the filesize in human readable format
- * @since 1.0
- */
+  * Returns a string showing a byte size figure in
+  * a more easily digested (readable) format
+  *
+  * @param int $bytes the size in bytes
+  *
+  * @return string The size in human readable format
+  *
+  * @since 1.0
+  *
+  */
 function show_readable_size($bytes) {
     $bytes /= 1024;
-    $type = 'KiB';
+    $type = _("KiB");
 
     if ($bytes / 1024 > 1) {
         $bytes /= 1024;
-        $type = 'MiB';
+        $type = _("MiB");
     }
 
     if ($bytes < 10) {
@@ -758,7 +682,8 @@ function show_readable_size($bytes) {
         settype($bytes, 'integer');
     }
 
-    return $bytes . '&nbsp;' . $type;
+    global $nbsp;
+    return $bytes . $nbsp . $type;
 }
 
 /**
@@ -789,8 +714,6 @@ function GenerateRandomString($size, $chars, $flags = 0) {
         return '';
     }
 
-    sq_mt_randomize(); /* Initialize the random number generator */
-
     $String = '';
     $j = strlen( $chars ) - 1;
     while (strlen($String) < $size) {
@@ -808,7 +731,7 @@ function GenerateRandomString($size, $chars, $flags = 0) {
  * @since 1.0.3
  */
 function quoteimap($str) {
-    return preg_replace("/([\"\\\\])/", "\\\\$1", $str);
+    return str_replace(array('\\', '"'), array('\\\\', '\\"'), $str);
 }
 
 /**
@@ -816,13 +739,17 @@ function quoteimap($str) {
  *
  * Returns a link to the compose-page, taking in consideration
  * the compose_in_new and javascript settings.
- * @param string $url the URL to the compose page
- * @param string $text the link text, default "Compose"
- * @param string $target (since 1.4.3) url target
+ *
+ * @param string $url       The URL to the compose page
+ * @param string $text      The link text, default "Compose"
+ * @param string $target    URL target, if any (since 1.4.3)
+ * @param string $accesskey The access key to be used, if any
+ *
  * @return string a link to the compose page
+ *
  * @since 1.4.2
  */
-function makeComposeLink($url, $text = null, $target='') {
+function makeComposeLink($url, $text = null, $target='', $accesskey='NONE') {
     global $compose_new_win, $compose_width, 
            $compose_height, $oTemplate;
 
@@ -833,7 +760,7 @@ function makeComposeLink($url, $text = null, $target='') {
     // if not using "compose in new window", make
     // regular link and be done with it
     if($compose_new_win != '1') {
-        return makeInternalLink($url, $text, $target);
+        return makeInternalLink($url, $text, $target, $accesskey);
     }
 
     // build the compose in new window link...
@@ -844,11 +771,16 @@ function makeComposeLink($url, $text = null, $target='') {
         sqgetGlobalVar('base_uri', $base_uri, SQ_SESSION);
         $compuri = SM_BASE_URI.$url;
 
-        return create_hyperlink('javascript:void(0)', $text, '', "comp_in_new('$compuri','$compose_width','$compose_height')");
+        return create_hyperlink('javascript:void(0)', $text, '',
+                                "comp_in_new('$compuri','$compose_width','$compose_height')",
+                                '', '', '',
+                                ($accesskey == 'NONE'
+                                ? array()
+                                : array('accesskey' => $accesskey)));
     }
 
     // otherwise, just open new window using regular HTML
-    return makeInternalLink($url, $text, '_blank');
+    return makeInternalLink($url, $text, '_blank', $accesskey);
 }
 
 /**
@@ -1167,7 +1099,16 @@ function sq_str_pad($string, $width, $pad, $padtype, $charset='') {
  * @link http://www.php.net/substr
  * @link http://www.php.net/mb_substr
  */
-function sq_substr($string,$start,$length,$charset='auto') {
+function sq_substr($string,$start,$length=NULL,$charset='auto') {
+
+    // if $length is NULL, use the full string length...
+    // we have to do this to mimick the use of substr()
+    // where $length is not given
+    //
+    if (is_null($length))
+        $length = sq_strlen($length);
+
+
     // use automatic charset detection, if function call asks for it
     static $charset_auto, $bUse_mb;
 
@@ -1203,6 +1144,125 @@ function sq_substr($string,$start,$length,$charset='auto') {
     return substr($string,$start,$length);
 }
 
+/**
+  * This is a replacement for PHP's substr_replace() that is
+  * multibyte-aware.
+  *
+  * @param string $string      The string to operate upon
+  * @param string $replacement The string to be inserted
+  * @param int    $start       The offset at which to begin substring replacement
+  * @param int    $length      The number of characters after $start to remove
+  *                            NOTE that if you need to specify a charset but
+  *                            want to achieve normal substr_replace() behavior
+  *                            where $length is not specified, use NULL (OPTIONAL;
+  *                            default from $start to end of string)
+  * @param string $charset     The charset of the given string.  A value of NULL
+  *                            here will force the use of PHP's standard substr().
+  *                            (OPTIONAL; default is "auto", which indicates that
+  *                            the user's current charset should be used).
+  *
+  * @return string The manipulated string
+  *
+  * Of course, you can use more advanced (e.g., negative) values
+  * for $start and $length as needed - see the PHP manual for more
+  * information:  http://www.php.net/manual/function.substr-replace.php
+  *
+  */
+function sq_substr_replace($string, $replacement, $start, $length=NULL,
+                           $charset='auto')
+{
+
+   // NULL charset?  Just use substr_replace()
+   //
+   if (is_null($charset))
+      return is_null($length) ? substr_replace($string, $replacement, $start)
+                              : substr_replace($string, $replacement, $start, $length);
+
+
+   // use current character set?
+   //
+   if ($charset == 'auto')
+   {
+//FIXME: is there any reason why this cannot be a global flag used by all string wrapper functions?
+      static $auto_charset;
+      if (!isset($auto_charset))
+      {
+         global $default_charset;
+//FIXME - do we need this?
+global $squirrelmail_language;
+         set_my_charset();
+         $auto_charset = $default_charset;
+//FIXME - do we need this?
+if ($squirrelmail_language == 'ja_JP') $auto_charset = 'euc-jp';
+      }
+      $charset = $auto_charset;
+   }
+
+
+   // standardize character set name
+   //
+   $charset = strtolower($charset);
+
+
+/* ===== 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
+   // only use mbstring with the following character sets
+   //
+   $sq_substr_replace_mb_charsets = array(
+      'utf-8',
+      'big5',
+      'gb2312',
+      'gb18030',
+      'euc-jp',
+      'euc-cn',
+      'euc-tw',
+      'euc-kr'
+   );
+
+
+   // now we can use our own implementation using
+   // mb_substr() and mb_strlen() if needed
+   //
+   if (in_array($charset, $sq_substr_replace_mb_charsets)
+    && in_array($charset, sq_mb_list_encodings()))
+===== */
+//FIXME: is there any reason why this cannot be a global array used by all string wrapper functions?
+   if (in_array($charset, sq_mb_list_encodings()))
+   {
+
+      $string_length = mb_strlen($string, $charset);
+
+      if ($start < 0)
+         $start = max(0, $string_length + $start);
+
+      else if ($start > $string_length)
+         $start = $string_length;
+
+      if ($length < 0)
+         $length = max(0, $string_length - $start + $length);
+
+      else if (is_null($length) || $length > $string_length)
+         $length = $string_length;
+
+      if ($start + $length > $string_length)
+         $length = $string_length - $start;
+
+      return mb_substr($string, 0, $start, $charset)
+           . $replacement
+           . mb_substr($string,
+                       $start + $length,
+                       $string_length, // FIXME: I can't see why this is needed:  - $start - $length,
+                       $charset);
+
+   }
+
+
+   // else use normal substr_replace()
+   //
+   return is_null($length) ? substr_replace($string, $replacement, $start)
+                           : substr_replace($string, $replacement, $start, $length);
+
+}
+
 /**
  * Wrapper that is used to switch between vanilla and multibyte strpos
  * functions.
@@ -1318,3 +1378,393 @@ function sq_count8bit($string) {
 function sq_trim_value ( &$value ) {
     $value = trim($value);
 }
+
+/**
+  * Truncates the given string so that it has at
+  * most $max_chars characters.  NOTE that a "character"
+  * may be a multibyte character, or (optionally), an
+  * HTML entity , so this function is different than
+  * using substr() or mb_substr().
+  *
+  * NOTE that if $elipses is given and used, the returned
+  *      number of characters will be $max_chars PLUS the
+  *      length of $elipses
+  *
+  * @param string  $string    The string to truncate
+  * @param int     $max_chars The maximum allowable characters
+  * @param string  $elipses   A string that will be added to
+  *                           the end of the truncated string
+  *                           (ONLY if it is truncated) (OPTIONAL;
+  *                           default not used)
+  * @param boolean $html_entities_as_chars Whether or not to keep
+  *                                        HTML entities together
+  *                                        (OPTIONAL; default ignore
+  *                                        HTML entities)
+  *
+  * @return string The truncated string
+  *
+  * @since 1.4.20 and 1.5.2 (replaced truncateWithEntities())
+  *
+  */
+function sm_truncate_string($string, $max_chars, $elipses='',
+                            $html_entities_as_chars=FALSE)
+{
+
+   // if the length of the string is less than
+   // the allowable number of characters, just
+   // return it as is (even if it contains any
+   // HTML entities, that would just make the
+   // actual length even smaller)
+   //
+   $actual_strlen = sq_strlen($string, 'auto');
+   if ($max_chars <= 0 || $actual_strlen <= $max_chars)
+      return $string;
+
+
+   // if needed, count the number of HTML entities in
+   // the string up to the maximum character limit,
+   // pushing that limit up for each entity found
+   //
+   $adjusted_max_chars = $max_chars;
+   if ($html_entities_as_chars)
+   {
+
+      // $loop_count is needed to prevent an endless loop
+      // which is caused by buggy mbstring versions that
+      // return 0 (zero) instead of FALSE in some rare
+      // cases.  Thanks, PHP.
+      // see: http://bugs.php.net/bug.php?id=52731
+      // also: tracker $3053349
+      //
+      $loop_count = 0;
+      $entity_pos = $entity_end_pos = -1;
+      while ($entity_end_pos + 1 < $actual_strlen
+          && ($entity_pos = sq_strpos($string, '&', $entity_end_pos + 1)) !== FALSE
+          && ($entity_end_pos = sq_strpos($string, ';', $entity_pos)) !== FALSE
+          && $entity_pos <= $adjusted_max_chars
+          && $loop_count++ < $max_chars)
+      {
+         $adjusted_max_chars += $entity_end_pos - $entity_pos;
+      }
+
+
+      // this isn't necessary because sq_substr() would figure this
+      // out anyway, but we can avoid a sq_substr() call and we
+      // know that we don't have to add an elipses (this is now
+      // an accurate comparison, since $adjusted_max_chars, like
+      // $actual_strlen, does not take into account HTML entities)
+      //
+      if ($actual_strlen <= $adjusted_max_chars)
+         return $string;
+
+   }
+
+
+   // get the truncated string
+   //
+   $truncated_string = sq_substr($string, 0, $adjusted_max_chars);
+
+
+   // return with added elipses
+   //
+   return $truncated_string . $elipses;
+
+}
+
+/**
+  * Gathers the list of secuirty tokens currently
+  * stored in the user's preferences and optionally
+  * purges old ones from the list.
+  *
+  * @param boolean $purge_old Indicates if old tokens
+  *                           should be purged from the
+  *                           list ("old" is 2 days or
+  *                           older unless the administrator
+  *                           overrides that value using
+  *                           $max_token_age_days in
+  *                           config/config_local.php)
+  *                           (OPTIONAL; default is to always
+  *                           purge old tokens)
+  *
+  * @return array The list of tokens
+  *
+  * @since 1.4.19 and 1.5.2
+  *
+  */
+function sm_get_user_security_tokens($purge_old=TRUE)
+{
+
+   global $data_dir, $username, $max_token_age_days,
+          $use_expiring_security_tokens;
+
+   $tokens = getPref($data_dir, $username, 'security_tokens', '');
+   if (($tokens = unserialize($tokens)) === FALSE || !is_array($tokens))
+      $tokens = array();
+
+   // purge old tokens if necessary
+   //
+   if ($purge_old)
+   {
+      if (empty($max_token_age_days)) $max_token_age_days = 2;
+      $now = time();
+      $discard_token_date = $now - ($max_token_age_days * 86400);
+      $cleaned_tokens = array();
+      foreach ($tokens as $token => $timestamp)
+         if ($timestamp >= $discard_token_date)
+            $cleaned_tokens[$token] = $timestamp;
+      $tokens = $cleaned_tokens;
+   }
+
+   return $tokens;
+
+}
+
+/**
+  * Generates a security token that is then stored in
+  * the user's preferences with a timestamp for later
+  * verification/use (although session-based tokens
+  * are not stored in user preferences).
+  *
+  * NOTE: By default SquirrelMail will use a single session-based
+  *       token, but if desired, user tokens can have expiration
+  *       dates associated with them and become invalid even during
+  *       the same login session.  When in that mode, the note
+  *       immediately below applies, otherwise it is irrelevant.
+  *       To enable that mode, the administrator must add the
+  *       following to config/config_local.php:
+  *       $use_expiring_security_tokens = TRUE;
+  *
+  * NOTE: The administrator can force SquirrelMail to generate
+  * a new token every time one is requested (which may increase
+  * obscurity through token randomness at the cost of some
+  * performance) by adding the following to
+  * config/config_local.php:   $do_not_use_single_token = TRUE;
+  * Otherwise, only one token will be generated per user which
+  * will change only after it expires or is used outside of the
+  * validity period specified when calling sm_validate_security_token()
+  *
+  * WARNING: If the administrator has turned the token system
+  *          off by setting $disable_security_tokens to TRUE in
+  *          config/config.php or the configuration tool, this
+  *          function will not store tokens in the user
+  *          preferences (but it will still generate and return
+  *          a random string).
+  *
+  * @param boolean $force_generate_new When TRUE, a new token will
+  *                                    always be created even if current
+  *                                    configuration dictates otherwise
+  *                                    (OPTIONAL; default FALSE)
+  *
+  * @return string A security token
+  *
+  * @since 1.4.19 and 1.5.2
+  *
+  */
+function sm_generate_security_token($force_generate_new=FALSE)
+{
+
+   global $data_dir, $username, $disable_security_tokens, $do_not_use_single_token,
+          $use_expiring_security_tokens;
+   $max_generation_tries = 1000;
+
+   // if we're using session-based tokens, just return
+   // the same one every time (generate it if it's not there)
+   //
+   if (!$use_expiring_security_tokens)
+   {
+      if (sqgetGlobalVar('sm_security_token', $token, SQ_SESSION))
+         return $token;
+
+      // create new one since there was none in session
+      $token = GenerateRandomString(12, '', 7);
+      sqsession_register($token, 'sm_security_token');
+      return $token;
+   }
+
+   $tokens = sm_get_user_security_tokens();
+
+   if (!$force_generate_new && !$do_not_use_single_token && !empty($tokens))
+      return key($tokens);
+
+   $new_token = GenerateRandomString(12, '', 7);
+   $count = 0;
+   while (isset($tokens[$new_token]))
+   {
+      $new_token = GenerateRandomString(12, '', 7);
+      if (++$count > $max_generation_tries)
+      {
+         logout_error(_("Fatal token generation error; please contact your system administrator or the SquirrelMail Team"));
+         exit;
+      }
+   }
+
+   // is the token system enabled?  CAREFUL!
+   //
+   if (!$disable_security_tokens)
+   {
+      $tokens[$new_token] = time();
+      setPref($data_dir, $username, 'security_tokens', serialize($tokens));
+   }
+
+   return $new_token;
+
+}
+
+/**
+  * Validates a given security token and optionally remove it
+  * from the user's preferences if it was valid.  If the token
+  * is too old but otherwise valid, it will still be rejected.
+  *
+  * "Too old" is 2 days or older unless the administrator
+  * overrides that value using $max_token_age_days in
+  * config/config_local.php
+  *
+  * Session-based tokens of course are always reused and are
+  * valid for the lifetime of the login session.
+  *
+  * WARNING: If the administrator has turned the token system
+  *          off by setting $disable_security_tokens to TRUE in
+  *          config/config.php or the configuration tool, this
+  *          function will always return TRUE.
+  *
+  * @param string  $token           The token to validate
+  * @param int     $validity_period The number of seconds tokens are valid
+  *                                 for (set to zero to remove valid tokens
+  *                                 after only one use; set to -1 to allow
+  *                                 indefinite re-use (but still subject to
+  *                                 $max_token_age_days - see elsewhere);
+  *                                 use 3600 to allow tokens to be reused for
+  *                                 an hour) (OPTIONAL; default is to only
+  *                                 allow tokens to be used once)
+  *                                 NOTE this is unrelated to $max_token_age_days
+  *                                 or rather is an additional time constraint on
+  *                                 tokens that allows them to be re-used (or not)
+  *                                 within a more narrow timeframe
+  * @param boolean $show_error      Indicates that if the token is not
+  *                                 valid, this function should display
+  *                                 a generic error, log the user out
+  *                                 and exit - this function will never
+  *                                 return in that case.
+  *                                 (OPTIONAL; default FALSE)
+  *
+  * @return boolean TRUE if the token validated; FALSE otherwise
+  *
+  * @since 1.4.19 and 1.5.2
+  *
+  */
+function sm_validate_security_token($token, $validity_period=0, $show_error=FALSE)
+{
+
+   global $data_dir, $username, $max_token_age_days,
+          $use_expiring_security_tokens,
+          $disable_security_tokens;
+
+   // bypass token validation?  CAREFUL!
+   //
+   if ($disable_security_tokens) return TRUE;
+
+   // if we're using session-based tokens, just compare
+   // the same one every time
+   //
+   if (!$use_expiring_security_tokens)
+   {
+      if (!sqgetGlobalVar('sm_security_token', $session_token, SQ_SESSION))
+      {
+         if (!$show_error) return FALSE;
+         logout_error(_("Fatal security token error; please log in again"));
+         exit;
+      }
+      if ($token !== $session_token)
+      {
+         if (!$show_error) return FALSE;
+         logout_error(_("The current page request appears to have originated from an untrusted source."));
+         exit;
+      }
+      return TRUE;
+   }
+
+   // don't purge old tokens here because we already
+   // do it when generating tokens
+   //
+   $tokens = sm_get_user_security_tokens(FALSE);
+
+   // token not found?
+   //
+   if (empty($tokens[$token]))
+   {
+      if (!$show_error) return FALSE;
+      logout_error(_("This page request could not be verified and appears to have expired."));
+      exit;
+   }
+
+   $now = time();
+   $timestamp = $tokens[$token];
+
+   // whether valid or not, we want to remove it from
+   // user prefs if it's old enough (unless requested to
+   // bypass this (in which case $validity_period is -1))
+   //
+   if ($validity_period >= 0
+    && $timestamp < $now - $validity_period)
+   {
+      unset($tokens[$token]);
+      setPref($data_dir, $username, 'security_tokens', serialize($tokens));
+   }
+
+   // reject tokens that are too old
+   //
+   if (empty($max_token_age_days)) $max_token_age_days = 2;
+   $old_token_date = $now - ($max_token_age_days * 86400);
+   if ($timestamp < $old_token_date)
+   {
+      if (!$show_error) return FALSE;
+      logout_error(_("The current page request appears to have originated from an untrusted source."));
+      exit;
+   }
+
+   // token OK!
+   //
+   return TRUE;
+
+}
+
+/**
+  * Wrapper for PHP's htmlspecialchars() that
+  * attempts to add the correct character encoding
+  *
+  * @param string $string The string to be converted
+  * @param int $flags A bitmask that controls the behavior of htmlspecialchars()
+  *                   (See http://php.net/manual/function.htmlspecialchars.php )
+  *                   (OPTIONAL; default ENT_COMPAT, ENT_COMPAT | ENT_SUBSTITUTE for PHP >=5.4)
+  * @param string $encoding The character encoding to use in the conversion
+  *                         (OPTIONAL; default automatic detection)
+  * @param boolean $double_encode Whether or not to convert entities that are
+  *                               already in the string (only supported in
+  *                               PHP 5.2.3+) (OPTIONAL; default TRUE)
+  *
+  * @return string The converted text
+  *
+  */
+function sm_encode_html_special_chars($string, $flags=ENT_COMPAT,
+                                      $encoding=NULL, $double_encode=TRUE)
+{
+   if (!$encoding)
+   {
+      global $default_charset;
+      if ($default_charset == 'iso-2022-jp')
+         $default_charset = 'EUC-JP';
+      $encoding = $default_charset;
+   }
+
+   if (check_php_version(5, 2, 3)) {
+      // Replace invalid characters with a symbol instead of returning
+      // empty string for the entire to be encoded string.
+      if (check_php_version(5, 4, 0) && $flags == ENT_COMPAT) {
+         $flags = $flags | ENT_SUBSTITUTE;
+      }
+      return htmlspecialchars($string, $flags, $encoding, $double_encode);
+   }
+
+   return htmlspecialchars($string, $flags, $encoding);
+}
+