+
+/**
+ * Counts 8bit bytes in string
+ * @param string $string tested string
+ * @return integer number of 8bit bytes
+ */
+function sq_count8bit($string) {
+ $count=0;
+ for ($i=0; $i<strlen($string); $i++) {
+ if (ord($string[$i]) > 127) $count++;
+ }
+ return $count;
+}
+
+/**
+ * Callback function to trim whitespace from a value, to be used in array_walk
+ * @param string $value value to trim
+ * @since 1.5.2 and 1.4.7
+ */
+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() -- NOTE that this parameter
+ * should only be used to dictate handling of
+ * quotes; handling invalid code sequences is done
+ * using the $invalid_sequence_flag parameter below
+ * (See http://php.net/manual/function.htmlspecialchars.php )
+ * (OPTIONAL; default ENT_COMPAT)
+ * @param string $encoding The character encoding to use in the conversion
+ * (if not one of the character sets supported
+ * by PHP's htmlspecialchars(), then $encoding
+ * will be ignored and iso-8859-1 will be used,
+ * unless a default has been specified in
+ * $default_htmlspecialchars_encoding in
+ * config_local.php) (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)
+ * @param mixed $invalid_sequence_flag A bitmask that controls how invalid
+ * code sequences should be handled;
+ * When calling htmlspecialchars(),
+ * this value will be combined with
+ * the $flags parameter above
+ * (See http://php.net/manual/function.htmlspecialchars.php )
+ * (OPTIONAL; defaults to the string
+ * "ent_substitute" that, for PHP 5.4+,
+ * is converted to the ENT_SUBSTITUTE
+ * constant, otherwise empty)
+ *
+ * @return string The converted text
+ *
+ */
+function sm_encode_html_special_chars($string, $flags=ENT_COMPAT,
+ $encoding=NULL, $double_encode=TRUE,
+ $invalid_sequence_flag='ent_substitute')
+{
+ if ($invalid_sequence_flag === 'ent_substitute')
+ {
+ if (check_php_version(5, 4, 0))
+ $invalid_sequence_flag = ENT_SUBSTITUTE;
+ else
+ $invalid_sequence_flag = 0;
+ }
+
+
+ // charsets supported by PHP's htmlspecialchars
+ // (move this elsewhere if needed)
+ //
+ static $htmlspecialchars_charsets = array(
+ 'iso-8859-1', 'iso8859-1',
+ 'iso-8859-5', 'iso8859-5',
+ 'iso-8859-15', 'iso8859-15',
+ 'utf-8',
+ 'cp866', 'ibm866', '866',
+ 'cp1251', 'windows-1251', 'win-1251', '1251',
+ 'cp1252', 'windows-1252', '1252',
+ 'koi8-R', 'koi8-ru', 'koi8r',
+ 'big5', '950',
+ 'gb2312', '936',
+ 'big5-hkscs',
+ 'shift_jis', 'sjis', 'sjis-win', 'cp932', '932',
+ 'euc-jp', 'eucjp', 'eucjp-win',
+ 'macroman',
+ );
+
+
+ // if not given, set encoding to the charset being
+ // used by the current user interface language
+ //
+ if (!$encoding)
+ {
+ global $default_charset;
+ if ($default_charset == 'iso-2022-jp')
+ $default_charset = 'EUC-JP';
+ $encoding = $default_charset;
+ }
+
+
+ // two ways to handle encodings not supported by htmlspecialchars() -
+ // one takes less CPU cycles but can munge characters in certain
+ // translations, the other is more exact but requires more resources
+ //
+ global $html_special_chars_extended_fix;
+//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
+ if (!in_array(strtolower($encoding), $htmlspecialchars_charsets))
+ {
+ if ($html_special_chars_extended_fix)
+ {
+ // convert to utf-8 first, run htmlspecialchars() and convert
+ // back to original encoding below
+ //
+//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):
+ $string = charset_decode($encoding, $string, TRUE, TRUE);
+ $string = charset_encode($string, $encoding, TRUE);
+ }
+ else
+ {
+ // simply force use of an encoding that is supported (some
+ // characters may be munged)
+ //
+ // use default from configuration if provided or hard-coded fallback
+ //
+ global $default_htmlspecialchars_encoding;
+ if (!empty($default_htmlspecialchars_encoding))
+ $encoding = $default_htmlspecialchars_encoding;
+ else
+ $encoding = 'iso-8859-1';
+ }
+ }
+
+
+// TODO: Is adding this check an unnecessary performance hit?
+ if (check_php_version(5, 2, 3))
+ $ret = htmlspecialchars($string, $flags | $invalid_sequence_flag,
+ $encoding, $double_encode);
+ else
+ $ret = htmlspecialchars($string, $flags | $invalid_sequence_flag,
+ $encoding);
+
+
+ // convert back to original encoding if needed (see above)
+ //
+ if ($html_special_chars_extended_fix
+ && !in_array(strtolower($encoding), $htmlspecialchars_charsets))
+ {
+//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?)
+ }
+
+
+ return $ret;
+}
+