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