fsf changes, meant to be rebased on upstream
[squirrelmail.git] / functions / strings.php
index 5a1c5b8f2c5856d33c206c7936565adfb7decd0a..030f3c8a7a252ca28fa0e4a402772188333ca0ae 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-2012 The SquirrelMail Project Team
+ * @copyright 1999-2024 The SquirrelMail Project Team
  * @license http://opensource.org/licenses/gpl-license.php GNU Public License
  * @version $Id$
  * @package squirrelmail
@@ -136,7 +136,7 @@ function &sqBodyWrap (&$body, $wrap) {
        // (i.e. try to preserve original paragraph breaks)
        // unless they occur at the very beginning of the text
        if ((sq_substr($body,$pos,1) == "\n" ) && (sq_strlen($outString) != 0)) {
-           $outStringLast = $outString{sq_strlen($outString) - 1};
+           $outStringLast = $outString[sq_strlen($outString) - 1];
            if ($outStringLast != "\n") {
                $outString .= "\n";
            }
@@ -227,7 +227,7 @@ function &sqBodyWrap (&$body, $wrap) {
 /*
                      $ldnspacecnt = 0;
                      if ($mypos == $nextNewline+1) {
-                        while (($mypos < $length) && ($body{$mypos} == ' ')) {
+                        while (($mypos < $length) && ($body[$mypos] == ' ')) {
                          $ldnspacecnt++;
                         }
                      }
@@ -236,9 +236,9 @@ function &sqBodyWrap (&$body, $wrap) {
                    $firstword = sq_substr($body,$mypos,sq_strpos($body,' ',$mypos) - $mypos);
                    //if ($dowrap || $ldnspacecnt > 1 || ($firstword && (
                    if (!$smartwrap || $firstword && (
-                                        $firstword{0} == '-' ||
-                                        $firstword{0} == '+' ||
-                                        $firstword{0} == '*' ||
+                                        $firstword[0] == '-' ||
+                                        $firstword[0] == '+' ||
+                                        $firstword[0] == '*' ||
                                         sq_substr($firstword,0,1) == sq_strtoupper(sq_substr($firstword,0,1)) ||
                                         strpos($firstword,':'))) {
                         $outString .= sq_substr($body,$pos,($lastRealChar - $pos+1));
@@ -474,12 +474,7 @@ function get_location () {
            $is_secure_connection, $sq_ignore_http_x_forwarded_headers;
 
     /* Get the path, handle virtual directories */
-    if(strpos(php_self(), '?')) {
-        $path = substr(php_self(), 0, strpos(php_self(), '?'));
-    } else {
-        $path = php_self();
-    }
-    $path = substr($path, 0, strrpos($path, '/'));
+    $path = substr(php_self(FALSE), 0, strrpos(php_self(FALSE), '/'));
 
     // proto+host+port are already set in config:
     if ( !empty($config_location_base) ) {
@@ -659,18 +654,19 @@ function OneTimePadCreate ($length=100) {
   * a more easily digested (readable) format
   *
   * @param int $bytes the size in bytes
+  * @param int $filesize_divisor the divisor we'll use (OPTIONAL; default 1024)
   *
   * @return string The size in human readable format
   *
   * @since 1.0
   *
   */
-function show_readable_size($bytes) {
-    $bytes /= 1024;
+function show_readable_size($bytes, $filesize_divisor=1024) {
+    $bytes /= $filesize_divisor;
     $type = _("KiB");
 
-    if ($bytes / 1024 > 1) {
-        $bytes /= 1024;
+    if ($bytes / $filesize_divisor > 1) {
+        $bytes /= $filesize_divisor;
         $type = _("MiB");
     }
 
@@ -717,7 +713,7 @@ function GenerateRandomString($size, $chars, $flags = 0) {
     $String = '';
     $j = strlen( $chars ) - 1;
     while (strlen($String) < $size) {
-        $String .= $chars{mt_rand(0, $j)};
+        $String .= $chars[mt_rand(0, $j)];
     }
 
     return $String;
@@ -1494,7 +1490,8 @@ function sm_truncate_string($string, $max_chars, $elipses='',
 function sm_get_user_security_tokens($purge_old=TRUE)
 {
 
-   global $data_dir, $username, $max_token_age_days;
+   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))
@@ -1521,7 +1518,17 @@ function sm_get_user_security_tokens($purge_old=TRUE)
 /**
   * Generates a security token that is then stored in
   * the user's preferences with a timestamp for later
-  * verification/use.
+  * 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
@@ -1552,9 +1559,24 @@ function sm_get_user_security_tokens($purge_old=TRUE)
 function sm_generate_security_token($force_generate_new=FALSE)
 {
 
-   global $data_dir, $username, $disable_security_tokens, $do_not_use_single_token;
+   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))
@@ -1593,6 +1615,9 @@ function sm_generate_security_token($force_generate_new=FALSE)
   * 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
@@ -1601,10 +1626,12 @@ function sm_generate_security_token($force_generate_new=FALSE)
   * @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; use 3600 to allow
-  *                                 tokens to be reused for an hour)
-  *                                 (OPTIONAL; default is to only allow tokens
-  *                                 to be used once)
+  *                                 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)
@@ -1625,12 +1652,33 @@ function sm_validate_security_token($token, $validity_period=0, $show_error=FALS
 {
 
    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
    //
@@ -1649,9 +1697,11 @@ function sm_validate_security_token($token, $validity_period=0, $show_error=FALS
    $timestamp = $tokens[$token];
 
    // whether valid or not, we want to remove it from
-   // user prefs if it's old enough
+   // user prefs if it's old enough (unless requested to
+   // bypass this (in which case $validity_period is -1))
    //
-   if ($timestamp < $now - $validity_period)
+   if ($validity_period >= 0
+    && $timestamp < $now - $validity_period)
    {
       unset($tokens[$token]);
       setPref($data_dir, $username, 'security_tokens', serialize($tokens));
@@ -1679,21 +1729,75 @@ function sm_validate_security_token($token, $validity_period=0, $show_error=FALS
   * 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()
+  * @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
-  *                         (OPTIONAL; default automatic detection)
+  *                         (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)
+                                      $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;
@@ -1702,10 +1806,58 @@ function sm_encode_html_special_chars($string, $flags=ENT_COMPAT,
       $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))
-      return htmlspecialchars($string, $flags, $encoding, $double_encode);
+      $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 htmlspecialchars($string, $flags, $encoding);
+   return $ret;
 }